1 line
102 KiB
JSON
1 line
102 KiB
JSON
{"repo": "browserwing/browserwing", "files": {"backend/cli/admin.go": {"3cafc852aab1e28f86e8a293dc27c2254e1a2ee6": {"t": 1785746054.0693338, "n": 18580, "h": "668bbc913ef40c8b", "c": "package cli\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n)\n\n// --- config (LLM configuration) ---\n\nfunc handleConfig(args []string) bool {\n\tif len(args) == 0 {\n\t\tprintConfigHelp()\n\t\treturn true\n\t}\n\taction := args[0]\n\trest := args[1:]\n\tformat := extractFormat(&rest, \"table\")\n\n\tswitch action {\n\tcase \"list\", \"ls\":\n\t\treturn execGet(\"/api/v1/llm-configs\", format)\n\tcase \"add\":\n\t\treturn configAdd(rest, format)\n\tcase \"test\":\n\t\treturn configTest(rest, format)\n\tcase \"delete\", \"rm\":\n\t\tif len(rest) == 0 {\n\t\t\texitWithError(ExitBadArgs, \"config ID is required\", \"Usage: browserwing config delete <id>\")\n\t\t}\n\t\tbody, err := apiDelete(\"/api/v1/llm-configs/\" + rest[0])\n\t\tif err != nil {\n\t\t\texitWithError(ExitConnectError, err.Error(), \"\")\n\t\t}\n\t\tprintAPIResponse(body, format)\n\t\treturn true\n\tcase \"help\":\n\t\tprintConfigHelp()\n\t\treturn true\n\tdefault:\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"unknown config action: %s\", action),\n\t\t\t\"Available: list, add, test, delete\")\n\t}\n\treturn true\n}\n\nfunc configAdd(args []string, format string) bool {\n\tpayload := map[string]interface{}{}\n\tfor _, a := range args {\n\t\tif strings.HasPrefix(a, \"--\") {\n\t\t\tkv := strings.TrimPrefix(a, \"--\")\n\t\t\tparts := strings.SplitN(kv, \"=\", 2)\n\t\t\tif len(parts) == 2 {\n\t\t\t\tkey := strings.ReplaceAll(parts[0], \"-\", \"_\")\n\t\t\t\tswitch key {\n\t\t\t\tcase \"default\":\n\t\t\t\t\tpayload[\"is_default\"] = parts[1] == \"true\" || parts[1] == \"1\"\n\t\t\t\tcase \"active\":\n\t\t\t\t\tpayload[\"is_active\"] = parts[1] == \"true\" || parts[1] == \"1\"\n\t\t\t\tdefault:\n\t\t\t\t\tpayload[key] = parts[1]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\trequired := []string{\"name\", \"provider\", \"api_key\", \"model\"}\n\tfor _, r := range required {\n\t\tif _, ok := payload[r]; !ok {\n\t\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"--%s is required\", strings.ReplaceAll(r, \"_\", \"-\")),\n\t\t\t\t\"Usage: browserwing config add --name=my-llm --provider=openai --api-key=sk-xxx --model=gpt-4o\")\n\t\t}\n\t}\n\tif _, ok := payload[\"is_active\"]; !ok {\n\t\tpayload[\"is_active\"] = true\n\t}\n\treturn execPost(\"/api/v1/llm-configs\", payload, format)\n}\n\nfunc configTest(args []string, format string) bool {\n\tif len(args) == 0 {\n\t\texitWithError(ExitBadArgs, \"config name is required\", \"Usage: browserwing config test <name>\")\n\t}\n\treturn execPost(\"/api/v1/llm-configs/test\", map[string]interface{}{\n\t\t\"name\": args[0],\n\t}, format)\n}\n\nfunc printConfigHelp() {\n\tfmt.Print(`LLM CONFIGURATION\n\n browserwing config <action> [options]\n\nACTIONS:\n list List all LLM configurations\n add [options] Add a new LLM configuration\n test <name> Test LLM connection\n delete <id> Delete a configuration\n\nADD OPTIONS:\n --name=<name> Config name (required)\n --provider=<provider> Provider: openai, anthropic, deepseek (required)\n --api-key=<key> API key (required)\n --model=<model> Model name, e.g. gpt-4o (required)\n --base-url=<url> Custom API base URL\n --default=true Set as default config\n\nEXAMPLES:\n browserwing config list\n browserwing config add --name=my-gpt --provider=openai --api-key=sk-xxx --model=gpt-4o --default=true\n browserwing config add --name=deepseek --provider=deepseek --api-key=sk-xxx --model=deepseek-chat --base-url=https://api.deepseek.com/v1\n browserwing config test my-gpt\n browserwing config delete <config-id>\n`)\n}\n\n// --- browser (instance management) ---\n\nfunc handleBrowser(args []string) bool {\n\tif len(args) == 0 {\n\t\treturn execGet(\"/api/v1/browser/instances\", \"table\")\n\t}\n\taction := args[0]\n\trest := args[1:]\n\tformat := extractFormat(&rest, \"json\")\n\n\tswitch action {\n\tcase \"list\", \"ls\":\n\t\treturn execGet(\"/api/v1/browser/instances\", format)\n\tcase \"start\":\n\t\tid := \"default\"\n\t\tif len(rest) > 0 {\n\t\t\tid = rest[0]\n\t\t}\n\t\treturn execPost(\"/api/v1/browser/instances/\"+id+\"/start\", nil, format)\n\tcase \"stop\":\n\t\tid := \"default\"\n\t\tif len(rest) > 0 {\n\t\t\tid = rest[0]\n\t\t}\n\t\treturn execPost(\"/api/v1/browser/instances/\"+id+\"/stop\", nil, format)\n\tcase \"help\":\n\t\tfmt.Print(`BROWSER INSTANCE MANAGEMENT\n\n browserwing browser <action> [instance-id]\n\nACTIONS:\n list List all browser instances\n start [id] Start a browser instance (default: \"default\")\n stop [id] Stop a browser instance\n\nEXAMPLES:\n browserwing browser list\n browserwing browser start\n browserwing browser stop\n`)\n\t\treturn true\n\tdefault:\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"unknown browser action: %s\", action),\n\t\t\t\"Available: list, start, stop\")\n\t}\n\treturn true\n}\n\n// --- cookie management ---\n\nfunc handleCookie(args []string) bool {\n\tif len(args) == 0 {\n\t\treturn execGet(\"/api/v1/cookies/browser\", \"json\")\n\t}\n\taction := args[0]\n\trest := args[1:]\n\tformat := extractFormat(&rest, \"json\")\n\n\tswitch action {\n\tcase \"list\", \"ls\":\n\t\tstoreID := \"browser\"\n\t\tif len(rest) > 0 {\n\t\t\tstoreID = rest[0]\n\t\t}\n\t\treturn execGet(\"/api/v1/cookies/\"+storeID, format)\n\tcase \"save\":\n\t\treturn execPost(\"/api/v1/browser/cookies/save\", nil, format)\n\tcase \"import\":\n\t\treturn cookieImport(rest, format)\n\tcase \"delete\", \"rm\":\n\t\treturn cookieDelete(rest, format)\n\tcase \"help\":\n\t\tfmt.Print(`COOKIE MANAGEMENT\n\n browserwing cookie <action> [options]\n\nACTIONS:\n list [store-id] List cookies (default store: \"browser\")\n save Save current browser cookies to database\n import <file.json> Import cookies from JSON file\n delete [options] Delete a cookie\n\nIMPORT FILE FORMAT:\n {\n \"url\": \"https://example.com\",\n \"cookies\": [\n {\"name\": \"session\", \"value\": \"abc\", \"domain\": \".example.com\"}\n ]\n }\n\nDELETE OPTIONS:\n --name=<name> Cookie name (required)\n --domain=<domain> Cookie domain (required)\n --path=<path> Cookie path (default: \"/\")\n --store=<id> Cookie store ID (default: \"browser\")\n\nEXAMPLES:\n browserwing cookie list\n browserwing cookie save\n browserwing cookie import cookies.json\n browserwing cookie delete --name=session --domain=.example.com\n`)\n\t\treturn true\n\tdefault:\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"unknown cookie action: %s\", action),\n\t\t\t\"Available: list, save, import, delete\")\n\t}\n\treturn true\n}\n\nfunc cookieImport(args []string, format string) bool {\n\tif len(args) == 0 {\n\t\texitWithError(ExitBadArgs, \"JSON file path is required\",\n\t\t\t\"Usage: browserwing cookie import <cookies.json>\")\n\t}\n\tdata, err := os.ReadFile(args[0])\n\tif err != nil {\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"failed to read file: %v\", err), \"\")\n\t}\n\tvar payload interface{}\n\tif err := json.Unmarshal(data, &payload); err != nil {\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"invalid JSON: %v\", err), \"\")\n\t}\n\treturn execPost(\"/api/v1/browser/cookies/import\", payload, format)\n}\n\nfunc cookieDelete(args []string, format string) bool {\n\tpayload := map[string]interface{}{\"id\": \"browser\", \"path\": \"/\"}\n\tfor _, a := range args {\n\t\tif strings.HasPrefix(a, \"--name=\") {\n\t\t\tpayload[\"name\"] = strings.TrimPrefix(a, \"--name=\")\n\t\t} else if strings.HasPrefix(a, \"--domain=\") {\n\t\t\tpayload[\"domain\"] = strings.TrimPrefix(a, \"--domain=\")\n\t\t} else if strings.HasPrefix(a, \"--path=\") {\n\t\t\tpayload[\"path\"] = strings.TrimPrefix(a, \"--path=\")\n\t\t} else if strings.HasPrefix(a, \"--store=\") {\n\t\t\tpayload[\"id\"] = strings.TrimPrefix(a, \"--store=\")\n\t\t}\n\t}\n\tif _, ok := payload[\"name\"]; !ok {\n\t\texitWithError(ExitBadArgs, \"--name is required\",\n\t\t\t\"Usage: browserwing cookie delete --name=session --domain=.example.com\")\n\t}\n\treturn execPost(\"/api/v1/browser/cookies/delete\", payload, format)\n}\n\n// --- script management (create, get, delete, export) ---\n\nfunc handleScript(args []string) bool {\n\tif len(args) == 0 {\n\t\tprintScriptHelp()\n\t\treturn true\n\t}\n\taction := args[0]\n\trest := args[1:]\n\tformat := extractFormat(&rest, \"json\")\n\n\tswitch action {\n\tcase \"get\", \"show\":\n\t\tif len(rest) == 0 {\n\t\t\texitWithError(ExitBadArgs, \"script ID is required\", \"Usage: browserwing script get <id>\")\n\t\t}\n\t\treturn execGet(\"/api/v1/scripts/\"+rest[0], format)\n\tcase \"create\":\n\t\treturn scriptCreate(rest, format)\n\tcase \"delete\", \"rm\":\n\t\tif len(rest) == 0 {\n\t\t\texitWithError(ExitBadArgs, \"script ID is required\", \"Usage: browserwing script delete <id>\")\n\t\t}\n\t\tbody, err := apiDelete(\"/api/v1/scripts/\" + rest[0])\n\t\tif err != nil {\n\t\t\texitWithError(ExitConnectError, err.Error(), \"\")\n\t\t}\n\t\tprintAPIResponse(body, format)\n\t\treturn true\n\tcase \"export\":\n\t\treturn scriptExport(rest)\n\tcase \"export-executor\":\n\t\treturn scriptExportExecutor(rest)\n\tcase \"export-admin\":\n\t\treturn scriptExportAdmin(rest)\n\tcase \"summary\":\n\t\treturn execGet(\"/api/v1/scripts/summary\", format)\n\tcase \"history\":\n\t\treturn execGet(\"/api/v1/script-executions?page=1&page_size=20\", format)\n\tcase \"result\":\n\t\treturn execGet(\"/api/v1/scripts/play/result\", format)\n\tcase \"help\":\n\t\tprintScriptHelp()\n\t\treturn true\n\tdefault:\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"unknown script action: %s\", action),\n\t\t\t\"Available: get, create, delete, export, export-executor, export-admin, summary, history, result\")\n\t}\n\treturn true\n}\n\nfunc scriptCreate(args []string, format string) bool {\n\tif len(args) == 0 {\n\t\texitWithError(ExitBadArgs, \"JSON file is required\",\n\t\t\t\"Usage: browserwing script create <script.json>\")\n\t}\n\tdata, err := os.ReadFile(args[0])\n\tif err != nil {\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"failed to read file: %v\", err), \"\")\n\t}\n\tvar payload interface{}\n\tif err := json.Unmarshal(data, &payload); err != nil {\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"invalid JSON: %v\", err), \"\")\n\t}\n\treturn execPost(\"/api/v1/scripts\", payload, format)\n}\n\nfunc scriptExport(args []string) bool {\n\toutput := \"\"\n\tvar ids []string\n\tfor _, a := range args {\n\t\tif strings.HasPrefix(a, \"--output=\") {\n\t\t\toutput = strings.TrimPrefix(a, \"--output=\")\n\t\t} else if strings.HasPrefix(a, \"--ids=\") {\n\t\t\tids = strings.Split(strings.TrimPrefix(a, \"--ids=\"), \",\")\n\t\t} else if !strings.HasPrefix(a, \"--\") {\n\t\t\tids = append(ids, a)\n\t\t}\n\t}\n\tbody, err := apiPost(\"/api/v1/scripts/export/skill\", map[string]interface{}{\n\t\t\"script_ids\": ids,\n\t})\n\tif err != nil {\n\t\texitWithError(ExitConnectError, err.Error(), \"\")\n\t}\n\treturn writeSkillOutput(body, output, \"SKILL.md\")\n}\n\nfunc scriptExportExecutor(args []string) bool {\n\toutput := \"\"\n\tfor _, a := range args {\n\t\tif strings.HasPrefix(a, \"--output=\") {\n\t\t\toutput = strings.TrimPrefix(a, \"--output=\")\n\t\t}\n\t}\n\tbody, err := apiGet(\"/api/v1/executor/export/skill\")\n\tif err != nil {\n\t\texitWithError(ExitConnectError, err.Error(), \"\")\n\t}\n\treturn writeSkillOutput(body, output, \"SKILL_EXECUTOR.md\")\n}\n\nfunc scriptExportAdmin(args []string) bool {\n\toutput := \"\"\n\tfor _, a := range args {\n\t\tif strings.HasPrefix(a, \"--output=\") {\n\t\t\toutput = strings.TrimPrefix(a, \"--output=\")\n\t\t}\n\t}\n\tbody, err := apiGet(\"/api/v1/admin/export/skill\")\n\tif err != nil {\n\t\texitWithError(ExitConnectError, err.Error(), \"\")\n\t}\n\treturn writeSkillOutput(body, output, \"SKILL_ADMIN.md\")\n}\n\nfunc writeSkillOutput(body []byte, output string, defaultName string) bool {\n\tcontent := string(body)\n\tvar resp map[string]interface{}\n\tif json.Unmarshal(body, &resp) == nil {\n\t\tif md, ok := resp[\"content\"].(string); ok {\n\t\t\tcontent = md\n\t\t} else if md, ok := resp[\"skill\"].(string); ok {\n\t\t\tcontent = md\n\t\t}\n\t}\n\tif output != \"\" {\n\t\tif err := os.WriteFile(output, []byte(content), 0644); err != nil {\n\t\t\texitWithError(ExitGeneralError, fmt.Sprintf(\"failed to write file: %v\", err), \"\")\n\t\t}\n\t\tfmt.Fprintf(os.Stderr, \"Exported to %s (%d bytes)\\n\", output, len(content))\n\t} else {\n\t\tfmt.Print(content)\n\t}\n\treturn true\n}\n\nfunc printScriptHelp() {\n\tfmt.Print(`SCRIPT MANAGEMENT\n\n browserwing script <action> [options]\n\nACTIONS:\n get <id> Get script details\n create <file.json> Create script from JSON file\n delete <id> Delete a script\n export [ids...] [--output=SKILL.md] Export scripts as SKILL.md\n export-executor [--output=SKILL_EXECUTOR.md] Export executor API as SKILL\n export-admin [--output=SKILL_ADMIN.md] Export admin API as SKILL\n summary Get all scripts summary\n history List recent execution history\n result Get last execution result\n\nEXAMPLES:\n browserwing script get bilibili-hot\n browserwing script create my-script.json\n browserwing script delete my-script-id\n browserwing script export --output=SKILL.md\n browserwing script export script-1 script-2 --output=SKILL.md\n browserwing script export-executor --output=SKILL_EXECUTOR.md\n browserwing script summary --format=json\n`)\n}\n\n// --- explore (AI exploration) ---\n\nfunc handleExplore(args []string) bool {\n\tif len(args) == 0 {\n\t\tprintExploreHelp()\n\t\treturn true\n\t}\n\taction := args[0]\n\trest := args[1:]\n\tformat := extractFormat(&rest, \"json\")\n\n\tswitch action {\n\tcase \"start\":\n\t\treturn exploreStart(rest, format)\n\tcase \"stop\":\n\t\tif len(rest) == 0 {\n\t\t\texitWithError(ExitBadArgs, \"session ID is required\", \"Usage: browserwing explore stop <session-id>\")\n\t\t}\n\t\treturn execPost(\"/api/v1/ai-explore/\"+rest[0]+\"/stop\", nil, format)\n\tcase \"script\":\n\t\tif len(rest) == 0 {\n\t\t\texitWithError(ExitBadArgs, \"session ID is required\", \"Usage: browserwing explore script <session-id>\")\n\t\t}\n\t\treturn execGet(\"/api/v1/ai-explore/\"+rest[0]+\"/script\", format)\n\tcase \"save\":\n\t\tif len(rest) == 0 {\n\t\t\texitWithError(ExitBadArgs, \"session ID is required\", \"Usage: browserwing explore save <session-id>\")\n\t\t}\n\t\treturn execPost(\"/api/v1/ai-explore/\"+rest[0]+\"/save\", nil, format)\n\tcase \"help\":\n\t\tprintExploreHelp()\n\t\treturn true\n\tdefault:\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"unknown explore action: %s\", action),\n\t\t\t\"Available: start, stop, script, save\")\n\t}\n\treturn true\n}\n\nfunc exploreStart(args []string, format string) bool {\n\tpayload := map[string]interface{}{}\n\tfor _, a := range args {\n\t\tif strings.HasPrefix(a, \"--url=\") {\n\t\t\tpayload[\"start_url\"] = strings.TrimPrefix(a, \"--url=\")\n\t\t} else if strings.HasPrefix(a, \"--task=\") {\n\t\t\tpayload[\"task_desc\"] = strings.TrimPrefix(a, \"--task=\")\n\t\t} else if strings.HasPrefix(a, \"--llm=\") {\n\t\t\tpayload[\"llm_config_id\"] = strings.TrimPrefix(a, \"--llm=\")\n\t\t}\n\t}\n\tif _, ok := payload[\"start_url\"]; !ok {\n\t\texitWithError(ExitBadArgs, \"--url is required\",\n\t\t\t\"Usage: browserwing explore start --url=https://example.com --task=\\\"search for AI\\\"\")\n\t}\n\tif _, ok := payload[\"task_desc\"]; !ok {\n\t\texitWithError(ExitBadArgs, \"--task is required\",\n\t\t\t\"Usage: browserwing explore start --url=https://example.com --task=\\\"search for AI\\\"\")\n\t}\n\treturn execPost(\"/api/v1/ai-explore/start\", payload, format)\n}\n\nfunc printExploreHelp() {\n\tfmt.Print(`AI EXPLORATION\n\n Use AI to autonomously browse websites and generate scripts.\n\n browserwing explore <action> [options]\n\nACTIONS:\n start [options] Start AI exploration\n stop <session-id> Stop an exploration session\n script <session-id> Get the generated script\n save <session-id> Save generated script to library\n\nSTART OPTIONS:\n --url=<url> Starting URL (required)\n --task=<description> Task description (required)\n --llm=<config-name> LLM config to use\n\nEXAMPLES:\n browserwing explore start --url=https://bilibili.com --task=\"search for AI and get video results\"\n browserwing explore stop abc123\n browserwing explore script abc123\n browserwing explore save abc123\n`)\n}\n\n// --- health check ---\n\nfunc handleHealth(args []string) bool {\n\tformat := \"json\"\n\tif len(args) > 0 {\n\t\tfor _, a := range args {\n\t\t\tif strings.HasPrefix(a, \"--format=\") {\n\t\t\t\tformat = strings.TrimPrefix(a, \"--format=\")\n\t\t\t}\n\t\t}\n\t}\n\treturn execGet(\"/health\", format)\n}\n\n// --- prompt management ---\n\nfunc handlePrompt(args []string) bool {\n\tif len(args) == 0 {\n\t\treturn execGet(\"/api/v1/prompts\", \"json\")\n\t}\n\taction := args[0]\n\trest := args[1:]\n\tformat := extractFormat(&rest, \"json\")\n\n\tswitch action {\n\tcase \"list\", \"ls\":\n\t\treturn execGet(\"/api/v1/prompts\", format)\n\tcase \"get\":\n\t\tif len(rest) == 0 {\n\t\t\texitWithError(ExitBadArgs, \"prompt ID is required\",\n\t\t\t\t\"System prompts: system-extractor, system-formfiller, system-aiagent, system-ai-explorer\")\n\t\t}\n\t\treturn execGet(\"/api/v1/prompts/\"+rest[0], format)\n\tcase \"update\", \"set\":\n\t\treturn promptUpdate(rest, format)\n\tcase \"help\":\n\t\tfmt.Print(`PROMPT MANAGEMENT\n\n browserwing prompt <action> [options]\n\nACTIONS:\n list List all system prompts\n get <id> Get a specific prompt\n update <id> --content=\"...\" Update a prompt\n\nSYSTEM PROMPT IDS:\n system-extractor Data extraction prompt\n system-formfiller Form filling prompt\n system-aiagent AI agent prompt\n system-ai-explorer AI exploration prompt\n\nEXAMPLES:\n browserwing prompt list\n browserwing prompt get system-extractor\n browserwing prompt update system-extractor --content=\"Custom prompt...\"\n`)\n\t\treturn true\n\tdefault:\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"unknown prompt action: %s\", action),\n\t\t\t\"Available: list, get, update\")\n\t}\n\treturn true\n}\n\nfunc promptUpdate(args []string, format string) bool {\n\tif len(args) == 0 {\n\t\texitWithError(ExitBadArgs, \"prompt ID is required\", \"Usage: browserwing prompt update <id> --content=\\\"...\\\"\")\n\t}\n\tid := args[0]\n\tcontent := \"\"\n\tfor _, a := range args[1:] {\n\t\tif strings.HasPrefix(a, \"--content=\") {\n\t\t\tcontent = strings.TrimPrefix(a, \"--content=\")\n\t\t}\n\t}\n\tif content == \"\" {\n\t\texitWithError(ExitBadArgs, \"--content is required\", \"\")\n\t}\n\tbody, err := apiPut(\"/api/v1/prompts/\"+id, map[string]interface{}{\n\t\t\"content\": content,\n\t})\n\tif err != nil {\n\t\texitWithError(ExitConnectError, err.Error(), \"\")\n\t}\n\tprintAPIResponse(body, format)\n\treturn true\n}\n\n// --- MCP status ---\n\nfunc handleMCP(args []string) bool {\n\tif len(args) == 0 {\n\t\treturn execGet(\"/api/v1/mcp/status\", \"json\")\n\t}\n\taction := args[0]\n\trest := args[1:]\n\tformat := extractFormat(&rest, \"json\")\n\n\tswitch action {\n\tcase \"status\":\n\t\treturn execGet(\"/api/v1/mcp/status\", format)\n\tcase \"commands\", \"tools\":\n\t\treturn execGet(\"/api/v1/mcp/commands\", format)\n\tcase \"help\":\n\t\tfmt.Print(`MCP (MODEL CONTEXT PROTOCOL)\n\n browserwing mcp <action>\n\nACTIONS:\n status Show MCP server status\n commands List all registered MCP tools\n\nEXAMPLES:\n browserwing mcp status\n browserwing mcp commands\n`)\n\t\treturn true\n\tdefault:\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"unknown mcp action: %s\", action),\n\t\t\t\"Available: status, commands\")\n\t}\n\treturn true\n}\n\n// --- shared helpers ---\n\nfunc extractFormat(args *[]string, defaultFmt string) string {\n\tformat := defaultFmt\n\tvar filtered []string\n\tfor _, a := range *args {\n\t\tif strings.HasPrefix(a, \"--format=\") {\n\t\t\tformat = strings.TrimPrefix(a, \"--format=\")\n\t\t} else {\n\t\t\tfiltered = append(filtered, a)\n\t\t}\n\t}\n\t*args = filtered\n\treturn format\n}\n"}, "d1963881b61b9b51493eb1ec08327d401a766973": {"t": 1785759247.8693159, "n": 18580, "h": "668bbc913ef40c8b", "c": "package cli\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n)\n\n// --- config (LLM configuration) ---\n\nfunc handleConfig(args []string) bool {\n\tif len(args) == 0 {\n\t\tprintConfigHelp()\n\t\treturn true\n\t}\n\taction := args[0]\n\trest := args[1:]\n\tformat := extractFormat(&rest, \"table\")\n\n\tswitch action {\n\tcase \"list\", \"ls\":\n\t\treturn execGet(\"/api/v1/llm-configs\", format)\n\tcase \"add\":\n\t\treturn configAdd(rest, format)\n\tcase \"test\":\n\t\treturn configTest(rest, format)\n\tcase \"delete\", \"rm\":\n\t\tif len(rest) == 0 {\n\t\t\texitWithError(ExitBadArgs, \"config ID is required\", \"Usage: browserwing config delete <id>\")\n\t\t}\n\t\tbody, err := apiDelete(\"/api/v1/llm-configs/\" + rest[0])\n\t\tif err != nil {\n\t\t\texitWithError(ExitConnectError, err.Error(), \"\")\n\t\t}\n\t\tprintAPIResponse(body, format)\n\t\treturn true\n\tcase \"help\":\n\t\tprintConfigHelp()\n\t\treturn true\n\tdefault:\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"unknown config action: %s\", action),\n\t\t\t\"Available: list, add, test, delete\")\n\t}\n\treturn true\n}\n\nfunc configAdd(args []string, format string) bool {\n\tpayload := map[string]interface{}{}\n\tfor _, a := range args {\n\t\tif strings.HasPrefix(a, \"--\") {\n\t\t\tkv := strings.TrimPrefix(a, \"--\")\n\t\t\tparts := strings.SplitN(kv, \"=\", 2)\n\t\t\tif len(parts) == 2 {\n\t\t\t\tkey := strings.ReplaceAll(parts[0], \"-\", \"_\")\n\t\t\t\tswitch key {\n\t\t\t\tcase \"default\":\n\t\t\t\t\tpayload[\"is_default\"] = parts[1] == \"true\" || parts[1] == \"1\"\n\t\t\t\tcase \"active\":\n\t\t\t\t\tpayload[\"is_active\"] = parts[1] == \"true\" || parts[1] == \"1\"\n\t\t\t\tdefault:\n\t\t\t\t\tpayload[key] = parts[1]\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\trequired := []string{\"name\", \"provider\", \"api_key\", \"model\"}\n\tfor _, r := range required {\n\t\tif _, ok := payload[r]; !ok {\n\t\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"--%s is required\", strings.ReplaceAll(r, \"_\", \"-\")),\n\t\t\t\t\"Usage: browserwing config add --name=my-llm --provider=openai --api-key=sk-xxx --model=gpt-4o\")\n\t\t}\n\t}\n\tif _, ok := payload[\"is_active\"]; !ok {\n\t\tpayload[\"is_active\"] = true\n\t}\n\treturn execPost(\"/api/v1/llm-configs\", payload, format)\n}\n\nfunc configTest(args []string, format string) bool {\n\tif len(args) == 0 {\n\t\texitWithError(ExitBadArgs, \"config name is required\", \"Usage: browserwing config test <name>\")\n\t}\n\treturn execPost(\"/api/v1/llm-configs/test\", map[string]interface{}{\n\t\t\"name\": args[0],\n\t}, format)\n}\n\nfunc printConfigHelp() {\n\tfmt.Print(`LLM CONFIGURATION\n\n browserwing config <action> [options]\n\nACTIONS:\n list List all LLM configurations\n add [options] Add a new LLM configuration\n test <name> Test LLM connection\n delete <id> Delete a configuration\n\nADD OPTIONS:\n --name=<name> Config name (required)\n --provider=<provider> Provider: openai, anthropic, deepseek (required)\n --api-key=<key> API key (required)\n --model=<model> Model name, e.g. gpt-4o (required)\n --base-url=<url> Custom API base URL\n --default=true Set as default config\n\nEXAMPLES:\n browserwing config list\n browserwing config add --name=my-gpt --provider=openai --api-key=sk-xxx --model=gpt-4o --default=true\n browserwing config add --name=deepseek --provider=deepseek --api-key=sk-xxx --model=deepseek-chat --base-url=https://api.deepseek.com/v1\n browserwing config test my-gpt\n browserwing config delete <config-id>\n`)\n}\n\n// --- browser (instance management) ---\n\nfunc handleBrowser(args []string) bool {\n\tif len(args) == 0 {\n\t\treturn execGet(\"/api/v1/browser/instances\", \"table\")\n\t}\n\taction := args[0]\n\trest := args[1:]\n\tformat := extractFormat(&rest, \"json\")\n\n\tswitch action {\n\tcase \"list\", \"ls\":\n\t\treturn execGet(\"/api/v1/browser/instances\", format)\n\tcase \"start\":\n\t\tid := \"default\"\n\t\tif len(rest) > 0 {\n\t\t\tid = rest[0]\n\t\t}\n\t\treturn execPost(\"/api/v1/browser/instances/\"+id+\"/start\", nil, format)\n\tcase \"stop\":\n\t\tid := \"default\"\n\t\tif len(rest) > 0 {\n\t\t\tid = rest[0]\n\t\t}\n\t\treturn execPost(\"/api/v1/browser/instances/\"+id+\"/stop\", nil, format)\n\tcase \"help\":\n\t\tfmt.Print(`BROWSER INSTANCE MANAGEMENT\n\n browserwing browser <action> [instance-id]\n\nACTIONS:\n list List all browser instances\n start [id] Start a browser instance (default: \"default\")\n stop [id] Stop a browser instance\n\nEXAMPLES:\n browserwing browser list\n browserwing browser start\n browserwing browser stop\n`)\n\t\treturn true\n\tdefault:\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"unknown browser action: %s\", action),\n\t\t\t\"Available: list, start, stop\")\n\t}\n\treturn true\n}\n\n// --- cookie management ---\n\nfunc handleCookie(args []string) bool {\n\tif len(args) == 0 {\n\t\treturn execGet(\"/api/v1/cookies/browser\", \"json\")\n\t}\n\taction := args[0]\n\trest := args[1:]\n\tformat := extractFormat(&rest, \"json\")\n\n\tswitch action {\n\tcase \"list\", \"ls\":\n\t\tstoreID := \"browser\"\n\t\tif len(rest) > 0 {\n\t\t\tstoreID = rest[0]\n\t\t}\n\t\treturn execGet(\"/api/v1/cookies/\"+storeID, format)\n\tcase \"save\":\n\t\treturn execPost(\"/api/v1/browser/cookies/save\", nil, format)\n\tcase \"import\":\n\t\treturn cookieImport(rest, format)\n\tcase \"delete\", \"rm\":\n\t\treturn cookieDelete(rest, format)\n\tcase \"help\":\n\t\tfmt.Print(`COOKIE MANAGEMENT\n\n browserwing cookie <action> [options]\n\nACTIONS:\n list [store-id] List cookies (default store: \"browser\")\n save Save current browser cookies to database\n import <file.json> Import cookies from JSON file\n delete [options] Delete a cookie\n\nIMPORT FILE FORMAT:\n {\n \"url\": \"https://example.com\",\n \"cookies\": [\n {\"name\": \"session\", \"value\": \"abc\", \"domain\": \".example.com\"}\n ]\n }\n\nDELETE OPTIONS:\n --name=<name> Cookie name (required)\n --domain=<domain> Cookie domain (required)\n --path=<path> Cookie path (default: \"/\")\n --store=<id> Cookie store ID (default: \"browser\")\n\nEXAMPLES:\n browserwing cookie list\n browserwing cookie save\n browserwing cookie import cookies.json\n browserwing cookie delete --name=session --domain=.example.com\n`)\n\t\treturn true\n\tdefault:\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"unknown cookie action: %s\", action),\n\t\t\t\"Available: list, save, import, delete\")\n\t}\n\treturn true\n}\n\nfunc cookieImport(args []string, format string) bool {\n\tif len(args) == 0 {\n\t\texitWithError(ExitBadArgs, \"JSON file path is required\",\n\t\t\t\"Usage: browserwing cookie import <cookies.json>\")\n\t}\n\tdata, err := os.ReadFile(args[0])\n\tif err != nil {\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"failed to read file: %v\", err), \"\")\n\t}\n\tvar payload interface{}\n\tif err := json.Unmarshal(data, &payload); err != nil {\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"invalid JSON: %v\", err), \"\")\n\t}\n\treturn execPost(\"/api/v1/browser/cookies/import\", payload, format)\n}\n\nfunc cookieDelete(args []string, format string) bool {\n\tpayload := map[string]interface{}{\"id\": \"browser\", \"path\": \"/\"}\n\tfor _, a := range args {\n\t\tif strings.HasPrefix(a, \"--name=\") {\n\t\t\tpayload[\"name\"] = strings.TrimPrefix(a, \"--name=\")\n\t\t} else if strings.HasPrefix(a, \"--domain=\") {\n\t\t\tpayload[\"domain\"] = strings.TrimPrefix(a, \"--domain=\")\n\t\t} else if strings.HasPrefix(a, \"--path=\") {\n\t\t\tpayload[\"path\"] = strings.TrimPrefix(a, \"--path=\")\n\t\t} else if strings.HasPrefix(a, \"--store=\") {\n\t\t\tpayload[\"id\"] = strings.TrimPrefix(a, \"--store=\")\n\t\t}\n\t}\n\tif _, ok := payload[\"name\"]; !ok {\n\t\texitWithError(ExitBadArgs, \"--name is required\",\n\t\t\t\"Usage: browserwing cookie delete --name=session --domain=.example.com\")\n\t}\n\treturn execPost(\"/api/v1/browser/cookies/delete\", payload, format)\n}\n\n// --- script management (create, get, delete, export) ---\n\nfunc handleScript(args []string) bool {\n\tif len(args) == 0 {\n\t\tprintScriptHelp()\n\t\treturn true\n\t}\n\taction := args[0]\n\trest := args[1:]\n\tformat := extractFormat(&rest, \"json\")\n\n\tswitch action {\n\tcase \"get\", \"show\":\n\t\tif len(rest) == 0 {\n\t\t\texitWithError(ExitBadArgs, \"script ID is required\", \"Usage: browserwing script get <id>\")\n\t\t}\n\t\treturn execGet(\"/api/v1/scripts/\"+rest[0], format)\n\tcase \"create\":\n\t\treturn scriptCreate(rest, format)\n\tcase \"delete\", \"rm\":\n\t\tif len(rest) == 0 {\n\t\t\texitWithError(ExitBadArgs, \"script ID is required\", \"Usage: browserwing script delete <id>\")\n\t\t}\n\t\tbody, err := apiDelete(\"/api/v1/scripts/\" + rest[0])\n\t\tif err != nil {\n\t\t\texitWithError(ExitConnectError, err.Error(), \"\")\n\t\t}\n\t\tprintAPIResponse(body, format)\n\t\treturn true\n\tcase \"export\":\n\t\treturn scriptExport(rest)\n\tcase \"export-executor\":\n\t\treturn scriptExportExecutor(rest)\n\tcase \"export-admin\":\n\t\treturn scriptExportAdmin(rest)\n\tcase \"summary\":\n\t\treturn execGet(\"/api/v1/scripts/summary\", format)\n\tcase \"history\":\n\t\treturn execGet(\"/api/v1/script-executions?page=1&page_size=20\", format)\n\tcase \"result\":\n\t\treturn execGet(\"/api/v1/scripts/play/result\", format)\n\tcase \"help\":\n\t\tprintScriptHelp()\n\t\treturn true\n\tdefault:\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"unknown script action: %s\", action),\n\t\t\t\"Available: get, create, delete, export, export-executor, export-admin, summary, history, result\")\n\t}\n\treturn true\n}\n\nfunc scriptCreate(args []string, format string) bool {\n\tif len(args) == 0 {\n\t\texitWithError(ExitBadArgs, \"JSON file is required\",\n\t\t\t\"Usage: browserwing script create <script.json>\")\n\t}\n\tdata, err := os.ReadFile(args[0])\n\tif err != nil {\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"failed to read file: %v\", err), \"\")\n\t}\n\tvar payload interface{}\n\tif err := json.Unmarshal(data, &payload); err != nil {\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"invalid JSON: %v\", err), \"\")\n\t}\n\treturn execPost(\"/api/v1/scripts\", payload, format)\n}\n\nfunc scriptExport(args []string) bool {\n\toutput := \"\"\n\tvar ids []string\n\tfor _, a := range args {\n\t\tif strings.HasPrefix(a, \"--output=\") {\n\t\t\toutput = strings.TrimPrefix(a, \"--output=\")\n\t\t} else if strings.HasPrefix(a, \"--ids=\") {\n\t\t\tids = strings.Split(strings.TrimPrefix(a, \"--ids=\"), \",\")\n\t\t} else if !strings.HasPrefix(a, \"--\") {\n\t\t\tids = append(ids, a)\n\t\t}\n\t}\n\tbody, err := apiPost(\"/api/v1/scripts/export/skill\", map[string]interface{}{\n\t\t\"script_ids\": ids,\n\t})\n\tif err != nil {\n\t\texitWithError(ExitConnectError, err.Error(), \"\")\n\t}\n\treturn writeSkillOutput(body, output, \"SKILL.md\")\n}\n\nfunc scriptExportExecutor(args []string) bool {\n\toutput := \"\"\n\tfor _, a := range args {\n\t\tif strings.HasPrefix(a, \"--output=\") {\n\t\t\toutput = strings.TrimPrefix(a, \"--output=\")\n\t\t}\n\t}\n\tbody, err := apiGet(\"/api/v1/executor/export/skill\")\n\tif err != nil {\n\t\texitWithError(ExitConnectError, err.Error(), \"\")\n\t}\n\treturn writeSkillOutput(body, output, \"SKILL_EXECUTOR.md\")\n}\n\nfunc scriptExportAdmin(args []string) bool {\n\toutput := \"\"\n\tfor _, a := range args {\n\t\tif strings.HasPrefix(a, \"--output=\") {\n\t\t\toutput = strings.TrimPrefix(a, \"--output=\")\n\t\t}\n\t}\n\tbody, err := apiGet(\"/api/v1/admin/export/skill\")\n\tif err != nil {\n\t\texitWithError(ExitConnectError, err.Error(), \"\")\n\t}\n\treturn writeSkillOutput(body, output, \"SKILL_ADMIN.md\")\n}\n\nfunc writeSkillOutput(body []byte, output string, defaultName string) bool {\n\tcontent := string(body)\n\tvar resp map[string]interface{}\n\tif json.Unmarshal(body, &resp) == nil {\n\t\tif md, ok := resp[\"content\"].(string); ok {\n\t\t\tcontent = md\n\t\t} else if md, ok := resp[\"skill\"].(string); ok {\n\t\t\tcontent = md\n\t\t}\n\t}\n\tif output != \"\" {\n\t\tif err := os.WriteFile(output, []byte(content), 0644); err != nil {\n\t\t\texitWithError(ExitGeneralError, fmt.Sprintf(\"failed to write file: %v\", err), \"\")\n\t\t}\n\t\tfmt.Fprintf(os.Stderr, \"Exported to %s (%d bytes)\\n\", output, len(content))\n\t} else {\n\t\tfmt.Print(content)\n\t}\n\treturn true\n}\n\nfunc printScriptHelp() {\n\tfmt.Print(`SCRIPT MANAGEMENT\n\n browserwing script <action> [options]\n\nACTIONS:\n get <id> Get script details\n create <file.json> Create script from JSON file\n delete <id> Delete a script\n export [ids...] [--output=SKILL.md] Export scripts as SKILL.md\n export-executor [--output=SKILL_EXECUTOR.md] Export executor API as SKILL\n export-admin [--output=SKILL_ADMIN.md] Export admin API as SKILL\n summary Get all scripts summary\n history List recent execution history\n result Get last execution result\n\nEXAMPLES:\n browserwing script get bilibili-hot\n browserwing script create my-script.json\n browserwing script delete my-script-id\n browserwing script export --output=SKILL.md\n browserwing script export script-1 script-2 --output=SKILL.md\n browserwing script export-executor --output=SKILL_EXECUTOR.md\n browserwing script summary --format=json\n`)\n}\n\n// --- explore (AI exploration) ---\n\nfunc handleExplore(args []string) bool {\n\tif len(args) == 0 {\n\t\tprintExploreHelp()\n\t\treturn true\n\t}\n\taction := args[0]\n\trest := args[1:]\n\tformat := extractFormat(&rest, \"json\")\n\n\tswitch action {\n\tcase \"start\":\n\t\treturn exploreStart(rest, format)\n\tcase \"stop\":\n\t\tif len(rest) == 0 {\n\t\t\texitWithError(ExitBadArgs, \"session ID is required\", \"Usage: browserwing explore stop <session-id>\")\n\t\t}\n\t\treturn execPost(\"/api/v1/ai-explore/\"+rest[0]+\"/stop\", nil, format)\n\tcase \"script\":\n\t\tif len(rest) == 0 {\n\t\t\texitWithError(ExitBadArgs, \"session ID is required\", \"Usage: browserwing explore script <session-id>\")\n\t\t}\n\t\treturn execGet(\"/api/v1/ai-explore/\"+rest[0]+\"/script\", format)\n\tcase \"save\":\n\t\tif len(rest) == 0 {\n\t\t\texitWithError(ExitBadArgs, \"session ID is required\", \"Usage: browserwing explore save <session-id>\")\n\t\t}\n\t\treturn execPost(\"/api/v1/ai-explore/\"+rest[0]+\"/save\", nil, format)\n\tcase \"help\":\n\t\tprintExploreHelp()\n\t\treturn true\n\tdefault:\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"unknown explore action: %s\", action),\n\t\t\t\"Available: start, stop, script, save\")\n\t}\n\treturn true\n}\n\nfunc exploreStart(args []string, format string) bool {\n\tpayload := map[string]interface{}{}\n\tfor _, a := range args {\n\t\tif strings.HasPrefix(a, \"--url=\") {\n\t\t\tpayload[\"start_url\"] = strings.TrimPrefix(a, \"--url=\")\n\t\t} else if strings.HasPrefix(a, \"--task=\") {\n\t\t\tpayload[\"task_desc\"] = strings.TrimPrefix(a, \"--task=\")\n\t\t} else if strings.HasPrefix(a, \"--llm=\") {\n\t\t\tpayload[\"llm_config_id\"] = strings.TrimPrefix(a, \"--llm=\")\n\t\t}\n\t}\n\tif _, ok := payload[\"start_url\"]; !ok {\n\t\texitWithError(ExitBadArgs, \"--url is required\",\n\t\t\t\"Usage: browserwing explore start --url=https://example.com --task=\\\"search for AI\\\"\")\n\t}\n\tif _, ok := payload[\"task_desc\"]; !ok {\n\t\texitWithError(ExitBadArgs, \"--task is required\",\n\t\t\t\"Usage: browserwing explore start --url=https://example.com --task=\\\"search for AI\\\"\")\n\t}\n\treturn execPost(\"/api/v1/ai-explore/start\", payload, format)\n}\n\nfunc printExploreHelp() {\n\tfmt.Print(`AI EXPLORATION\n\n Use AI to autonomously browse websites and generate scripts.\n\n browserwing explore <action> [options]\n\nACTIONS:\n start [options] Start AI exploration\n stop <session-id> Stop an exploration session\n script <session-id> Get the generated script\n save <session-id> Save generated script to library\n\nSTART OPTIONS:\n --url=<url> Starting URL (required)\n --task=<description> Task description (required)\n --llm=<config-name> LLM config to use\n\nEXAMPLES:\n browserwing explore start --url=https://bilibili.com --task=\"search for AI and get video results\"\n browserwing explore stop abc123\n browserwing explore script abc123\n browserwing explore save abc123\n`)\n}\n\n// --- health check ---\n\nfunc handleHealth(args []string) bool {\n\tformat := \"json\"\n\tif len(args) > 0 {\n\t\tfor _, a := range args {\n\t\t\tif strings.HasPrefix(a, \"--format=\") {\n\t\t\t\tformat = strings.TrimPrefix(a, \"--format=\")\n\t\t\t}\n\t\t}\n\t}\n\treturn execGet(\"/health\", format)\n}\n\n// --- prompt management ---\n\nfunc handlePrompt(args []string) bool {\n\tif len(args) == 0 {\n\t\treturn execGet(\"/api/v1/prompts\", \"json\")\n\t}\n\taction := args[0]\n\trest := args[1:]\n\tformat := extractFormat(&rest, \"json\")\n\n\tswitch action {\n\tcase \"list\", \"ls\":\n\t\treturn execGet(\"/api/v1/prompts\", format)\n\tcase \"get\":\n\t\tif len(rest) == 0 {\n\t\t\texitWithError(ExitBadArgs, \"prompt ID is required\",\n\t\t\t\t\"System prompts: system-extractor, system-formfiller, system-aiagent, system-ai-explorer\")\n\t\t}\n\t\treturn execGet(\"/api/v1/prompts/\"+rest[0], format)\n\tcase \"update\", \"set\":\n\t\treturn promptUpdate(rest, format)\n\tcase \"help\":\n\t\tfmt.Print(`PROMPT MANAGEMENT\n\n browserwing prompt <action> [options]\n\nACTIONS:\n list List all system prompts\n get <id> Get a specific prompt\n update <id> --content=\"...\" Update a prompt\n\nSYSTEM PROMPT IDS:\n system-extractor Data extraction prompt\n system-formfiller Form filling prompt\n system-aiagent AI agent prompt\n system-ai-explorer AI exploration prompt\n\nEXAMPLES:\n browserwing prompt list\n browserwing prompt get system-extractor\n browserwing prompt update system-extractor --content=\"Custom prompt...\"\n`)\n\t\treturn true\n\tdefault:\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"unknown prompt action: %s\", action),\n\t\t\t\"Available: list, get, update\")\n\t}\n\treturn true\n}\n\nfunc promptUpdate(args []string, format string) bool {\n\tif len(args) == 0 {\n\t\texitWithError(ExitBadArgs, \"prompt ID is required\", \"Usage: browserwing prompt update <id> --content=\\\"...\\\"\")\n\t}\n\tid := args[0]\n\tcontent := \"\"\n\tfor _, a := range args[1:] {\n\t\tif strings.HasPrefix(a, \"--content=\") {\n\t\t\tcontent = strings.TrimPrefix(a, \"--content=\")\n\t\t}\n\t}\n\tif content == \"\" {\n\t\texitWithError(ExitBadArgs, \"--content is required\", \"\")\n\t}\n\tbody, err := apiPut(\"/api/v1/prompts/\"+id, map[string]interface{}{\n\t\t\"content\": content,\n\t})\n\tif err != nil {\n\t\texitWithError(ExitConnectError, err.Error(), \"\")\n\t}\n\tprintAPIResponse(body, format)\n\treturn true\n}\n\n// --- MCP status ---\n\nfunc handleMCP(args []string) bool {\n\tif len(args) == 0 {\n\t\treturn execGet(\"/api/v1/mcp/status\", \"json\")\n\t}\n\taction := args[0]\n\trest := args[1:]\n\tformat := extractFormat(&rest, \"json\")\n\n\tswitch action {\n\tcase \"status\":\n\t\treturn execGet(\"/api/v1/mcp/status\", format)\n\tcase \"commands\", \"tools\":\n\t\treturn execGet(\"/api/v1/mcp/commands\", format)\n\tcase \"help\":\n\t\tfmt.Print(`MCP (MODEL CONTEXT PROTOCOL)\n\n browserwing mcp <action>\n\nACTIONS:\n status Show MCP server status\n commands List all registered MCP tools\n\nEXAMPLES:\n browserwing mcp status\n browserwing mcp commands\n`)\n\t\treturn true\n\tdefault:\n\t\texitWithError(ExitBadArgs, fmt.Sprintf(\"unknown mcp action: %s\", action),\n\t\t\t\"Available: status, commands\")\n\t}\n\treturn true\n}\n\n// --- shared helpers ---\n\nfunc extractFormat(args *[]string, defaultFmt string) string {\n\tformat := defaultFmt\n\tvar filtered []string\n\tfor _, a := range *args {\n\t\tif strings.HasPrefix(a, \"--format=\") {\n\t\t\tformat = strings.TrimPrefix(a, \"--format=\")\n\t\t} else {\n\t\t\tfiltered = append(filtered, a)\n\t\t}\n\t}\n\t*args = filtered\n\treturn format\n}\n"}}, "backend/agent/agent_llm.go": {"3cafc852aab1e28f86e8a293dc27c2254e1a2ee6": {"t": 1785746054.2759063, "n": 14981, "h": "7dcbedcf7fc7a32d", "c": "package agent\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/Ingenimax/agent-sdk-go/pkg/interfaces\"\n\t\"github.com/Ingenimax/agent-sdk-go/pkg/llm/anthropic\"\n\t\"github.com/Ingenimax/agent-sdk-go/pkg/llm/openai\"\n\t\"github.com/browserwing/browserwing/models\"\n)\n\n// LLMAdapter LLM 适配器,用于将各种 LLM 提供商适配为 agent-sdk-go 的接口\ntype LLMAdapter struct {\n\tprovider string\n\tmodel string\n\tclient interfaces.LLM\n}\n\n// DeepSeekWrapper DeepSeek API 的包装器\n// 用于处理 DeepSeek 的特殊参数要求\ntype DeepSeekWrapper struct {\n\tclient interfaces.LLM\n}\n\n// DeepSeekToolWrapper 包装工具以确保工具名称符合 DeepSeek 的要求\ntype DeepSeekToolWrapper struct {\n\ttool interfaces.Tool\n\toriginalName string\n\tcleanedName string\n}\n\n// cleanToolName 清理工具名称,使其符合 DeepSeek 的要求 ^[a-zA-Z0-9_-]+$\nfunc cleanToolName(name string) string {\n\t// 将不允许的字符替换为下划线\n\tre := regexp.MustCompile(`[^a-zA-Z0-9_-]`)\n\tcleaned := re.ReplaceAllString(name, \"_\")\n\n\t// 确保不以数字或特殊字符开头\n\tif len(cleaned) > 0 && !regexp.MustCompile(`^[a-zA-Z]`).MatchString(cleaned) {\n\t\tcleaned = \"tool_\" + cleaned\n\t}\n\n\treturn cleaned\n}\n\n// wrapToolsForDeepSeek 包装工具数组,清理工具名称\nfunc (d *DeepSeekWrapper) wrapToolsForDeepSeek(tools []interfaces.Tool) []interfaces.Tool {\n\twrappedTools := make([]interfaces.Tool, len(tools))\n\tfor i, tool := range tools {\n\t\toriginalName := tool.Name()\n\t\tcleanedName := cleanToolName(originalName)\n\n\t\t// 如果名称需要清理,则使用包装器\n\t\tif cleanedName != originalName {\n\t\t\twrappedTools[i] = &DeepSeekToolWrapper{\n\t\t\t\ttool: tool,\n\t\t\t\toriginalName: originalName,\n\t\t\t\tcleanedName: cleanedName,\n\t\t\t}\n\t\t} else {\n\t\t\twrappedTools[i] = tool\n\t\t}\n\t}\n\treturn wrappedTools\n}\n\n// Name 实现 interfaces.Tool 接口\nfunc (w *DeepSeekToolWrapper) Name() string {\n\treturn w.cleanedName\n}\n\n// Description 实现 interfaces.Tool 接口\nfunc (w *DeepSeekToolWrapper) Description() string {\n\treturn w.tool.Description()\n}\n\n// Parameters 实现 interfaces.Tool 接口\nfunc (w *DeepSeekToolWrapper) Parameters() map[string]interfaces.ParameterSpec {\n\treturn w.tool.Parameters()\n}\n\n// Run 实现 interfaces.Tool 接口\nfunc (w *DeepSeekToolWrapper) Run(ctx context.Context, input string) (string, error) {\n\treturn w.tool.Run(ctx, input)\n}\n\n// Execute 实现 interfaces.Tool 接口\nfunc (w *DeepSeekToolWrapper) Execute(ctx context.Context, args string) (string, error) {\n\treturn w.tool.Execute(ctx, args)\n}\n\n// Generate 实现 interfaces.LLM 接口\nfunc (d *DeepSeekWrapper) Generate(ctx context.Context, prompt string, options ...interfaces.GenerateOption) (string, error) {\n\t// 修正选项中的 top_p 参数\n\tfixedOptions := d.fixTopPOptions(options)\n\treturn d.client.Generate(ctx, prompt, fixedOptions...)\n}\n\n// GenerateWithTools 实现 interfaces.LLM 接口\nfunc (d *DeepSeekWrapper) GenerateWithTools(ctx context.Context, prompt string, tools []interfaces.Tool, options ...interfaces.GenerateOption) (string, error) {\n\t// 清理工具名称\n\twrappedTools := d.wrapToolsForDeepSeek(tools)\n\t// 修正选项中的 top_p 参数\n\tfixedOptions := d.fixTopPOptions(options)\n\treturn d.client.GenerateWithTools(ctx, prompt, wrappedTools, fixedOptions...)\n}\n\n// GenerateDetailed 实现 interfaces.LLM 接口\nfunc (d *DeepSeekWrapper) GenerateDetailed(ctx context.Context, prompt string, options ...interfaces.GenerateOption) (*interfaces.LLMResponse, error) {\n\t// 修正选项中的 top_p 参数\n\tfixedOptions := d.fixTopPOptions(options)\n\treturn d.client.GenerateDetailed(ctx, prompt, fixedOptions...)\n}\n\n// GenerateWithToolsDetailed 实现 interfaces.LLM 接口\nfunc (d *DeepSeekWrapper) GenerateWithToolsDetailed(ctx context.Context, prompt string, tools []interfaces.Tool, options ...interfaces.GenerateOption) (*interfaces.LLMResponse, error) {\n\t// 清理工具名称\n\twrappedTools := d.wrapToolsForDeepSeek(tools)\n\t// 修正选项中的 top_p 参数\n\tfixedOptions := d.fixTopPOptions(options)\n\treturn d.client.GenerateWithToolsDetailed(ctx, prompt, wrappedTools, fixedOptions...)\n}\n\n// Name 实现 interfaces.LLM 接口\nfunc (d *DeepSeekWrapper) Name() string {\n\treturn d.client.Name()\n}\n\n// SupportsStreaming 实现 interfaces.LLM 接口\nfunc (d *DeepSeekWrapper) SupportsStreaming() bool {\n\treturn d.client.SupportsStreaming()\n}\n\n// GenerateStream 实现 interfaces.StreamingLLM 接口\nfunc (d *DeepSeekWrapper) GenerateStream(ctx context.Context, prompt string, options ...interfaces.GenerateOption) (<-chan interfaces.StreamEvent, error) {\n\t// 检查底层客户端是否支持流式\n\tstreamingLLM, ok := d.client.(interfaces.StreamingLLM)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"underlying client does not support streaming\")\n\t}\n\n\t// 修正选项中的 top_p 参数\n\tfixedOptions := d.fixTopPOptions(options)\n\treturn streamingLLM.GenerateStream(ctx, prompt, fixedOptions...)\n}\n\n// GenerateWithToolsStream 实现 interfaces.StreamingLLM 接口\nfunc (d *DeepSeekWrapper) GenerateWithToolsStream(ctx context.Context, prompt string, tools []interfaces.Tool, options ...interfaces.GenerateOption) (<-chan interfaces.StreamEvent, error) {\n\t// 检查底层客户端是否支持流式\n\tstreamingLLM, ok := d.client.(interfaces.StreamingLLM)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"underlying client does not support streaming\")\n\t}\n\n\t// 清理工具名称\n\twrappedTools := d.wrapToolsForDeepSeek(tools)\n\t// 修正选项中的 top_p 参数\n\tfixedOptions := d.fixTopPOptions(options)\n\treturn streamingLLM.GenerateWithToolsStream(ctx, prompt, wrappedTools, fixedOptions...)\n}\n\n// fixTopPOptions 修正 top_p 参数,确保在 (0, 1] 范围内\nfunc (d *DeepSeekWrapper) fixTopPOptions(options []interfaces.GenerateOption) []interfaces.GenerateOption {\n\t// 应用选项到配置以读取当前值\n\topts := &interfaces.GenerateOptions{\n\t\tLLMConfig: &interfaces.LLMConfig{},\n\t}\n\tfor _, opt := range options {\n\t\topt(opts)\n\t}\n\n\t// DeepSeek 特殊处理:top_p 必须在 (0, 1] 范围内\n\t// 如果 top_p 为 0 或 1,调整为 0.95\n\tif opts.LLMConfig.TopP <= 0 || opts.LLMConfig.TopP >= 1 {\n\t\topts.LLMConfig.TopP = 0.95\n\t}\n\n\t// 重新构建选项\n\tnewOptions := []interfaces.GenerateOption{\n\t\tinterfaces.WithTemperature(opts.LLMConfig.Temperature),\n\t\tinterfaces.WithTopP(opts.LLMConfig.TopP),\n\t\tinterfaces.WithFrequencyPenalty(opts.LLMConfig.FrequencyPenalty),\n\t\tinterfaces.WithPresencePenalty(opts.LLMConfig.PresencePenalty),\n\t}\n\n\tif len(opts.LLMConfig.StopSequences) > 0 {\n\t\tnewOptions = append(newOptions, interfaces.WithStopSequences(opts.LLMConfig.StopSequences))\n\t}\n\n\t// 复制其他选项\n\tif opts.SystemMessage != \"\" {\n\t\tnewOptions = append(newOptions, func(o *interfaces.GenerateOptions) {\n\t\t\to.SystemMessage = opts.SystemMessage\n\t\t})\n\t}\n\tif opts.ResponseFormat != nil {\n\t\tnewOptions = append(newOptions, func(o *interfaces.GenerateOptions) {\n\t\t\to.ResponseFormat = opts.ResponseFormat\n\t\t})\n\t}\n\tif opts.MaxIterations > 0 {\n\t\tnewOptions = append(newOptions, func(o *interfaces.GenerateOptions) {\n\t\t\to.MaxIterations = opts.MaxIterations\n\t\t})\n\t}\n\tif opts.Memory != nil {\n\t\tnewOptions = append(newOptions, func(o *interfaces.GenerateOptions) {\n\t\t\to.Memory = opts.Memory\n\t\t})\n\t}\n\tif opts.OrgID != \"\" {\n\t\tnewOptions = append(newOptions, func(o *interfaces.GenerateOptions) {\n\t\t\to.OrgID = opts.OrgID\n\t\t})\n\t}\n\n\treturn newOptions\n}\n\n// CreateLLMClient 根据配置创建 LLM 客户端\n// 支持多种 LLM 提供商,包括 OpenAI 兼容的 API\nfunc CreateLLMClient(config *models.LLMConfigModel) (interfaces.LLM, error) {\n\tprovider := strings.ToLower(config.Provider)\n\n\t// Anthropic Claude 系列 - 使用原生 SDK\n\tif provider == \"anthropic\" || provider == \"claude\" {\n\t\treturn createAnthropicClient(config)\n\t}\n\n\t// DeepSeek 需要特殊处理(top_p 不能为 0 或 1)\n\tif provider == \"deepseek\" {\n\t\treturn createDeepSeekClient(config)\n\t}\n\n\t// 其他所有提供商都使用 OpenAI 兼容模式\n\treturn createOpenAICompatibleClient(config)\n}\n\n// createAnthropicClient 创建 Anthropic 客户端\nfunc createAnthropicClient(config *models.LLMConfigModel) (interfaces.LLM, error) {\n\topts := []anthropic.Option{\n\t\tanthropic.WithModel(config.Model),\n\t}\n\n\t// Anthropic 支持自定义 BaseURL (如 Claude 代理)\n\tif config.BaseURL != \"\" {\n\t\topts = append(opts, anthropic.WithBaseURL(config.BaseURL))\n\t}\n\n\tclient := anthropic.NewClient(config.APIKey, opts...)\n\treturn client, nil\n}\n\n// createDeepSeekClient 创建 DeepSeek 客户端\n// DeepSeek API 有特殊要求:\n// 1. top_p 必须在 (0, 1.0] 区间,不能为 0 或 1(通过包装器处理)\n// 2. 工具名称只支持 ^[a-zA-Z0-9_-]+$ 模式\n// 3. BaseURL 固定为 https://api.deepseek.com/v1\nfunc createDeepSeekClient(config *models.LLMConfigModel) (interfaces.LLM, error) {\n\topts := []openai.Option{\n\t\topenai.WithModel(config.Model),\n\t}\n\n\t// DeepSeek 的 BaseURL\n\tbaseURL := \"https://api.deepseek.com/v1\"\n\tif config.BaseURL != \"\" {\n\t\tbaseURL = config.BaseURL\n\t}\n\topts = append(opts, openai.WithBaseURL(baseURL))\n\n\tclient := openai.NewClient(config.APIKey, opts...)\n\n\t// 使用包装器处理 DeepSeek 的特殊参数要求\n\treturn &DeepSeekWrapper{client: client}, nil\n}\n\n// createOpenAICompatibleClient 创建 OpenAI 兼容客户端\n// 支持所有 OpenAI API 兼容的服务\nfunc createOpenAICompatibleClient(config *models.LLMConfigModel) (interfaces.LLM, error) {\n\tprovider := strings.ToLower(config.Provider)\n\n\topts := []openai.Option{\n\t\topenai.WithModel(config.Model),\n\t}\n\n\t// 根据不同提供商设置 BaseURL\n\tbaseURL := getProviderBaseURL(provider, config.BaseURL)\n\tif baseURL != \"\" {\n\t\topts = append(opts, openai.WithBaseURL(baseURL))\n\t}\n\n\t// Ollama 本地运行时不需要真实的 API Key,提供默认值\n\tapiKey := config.APIKey\n\tif provider == \"ollama\" && apiKey == \"\" {\n\t\tapiKey = \"ollama\" // Ollama 本地不验证 API Key,提供占位符即可\n\t}\n\n\tclient := openai.NewClient(apiKey, opts...)\n\treturn client, nil\n}\n\n// getProviderBaseURL 获取各提供商的默认 BaseURL\nfunc getProviderBaseURL(provider, customBaseURL string) string {\n\t// 如果用户自定义了 BaseURL,优先使用\n\tif customBaseURL != \"\" {\n\t\treturn customBaseURL\n\t}\n\n\t// 各提供商的默认 API 端点\n\tbaseURLMap := map[string]string{\n\t\t// 国际模型\n\t\t\"openai\": \"https://api.openai.com/v1\",\n\t\t\"gemini\": \"https://generativelanguage.googleapis.com/v1beta/openai\",\n\t\t\"mistral\": \"https://api.mistral.ai/v1\",\n\t\t\"deepseek\": \"https://api.deepseek.com\",\n\t\t\"groq\": \"https://api.groq.com/openai/v1\",\n\t\t\"cohere\": \"https://api.cohere.ai/v1\",\n\t\t\"xai\": \"https://api.x.ai/v1\",\n\t\t\"together\": \"https://api.together.xyz/v1\",\n\t\t\"novita\": \"https://api.novita.ai/v3/openai\",\n\t\t\"openrouter\": \"https://openrouter.ai/api/v1\",\n\n\t\t// 国内模型\n\t\t\"qwen\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n\t\t\"siliconflow\": \"https://api.siliconflow.cn/v1\",\n\t\t\"doubao\": \"https://ark.cn-beijing.volces.com/api/v3\",\n\t\t\"ernie\": \"https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop\",\n\t\t\"spark\": \"https://spark-api-open.xf-yun.com/v1\",\n\t\t\"chatglm\": \"https://open.bigmodel.cn/api/paas/v4\",\n\t\t\"360\": \"https://api.360.cn/v1\",\n\t\t\"hunyuan\": \"https://hunyuan.tencentcloudapi.com\",\n\t\t\"moonshot\": \"https://api.moonshot.cn/v1\",\n\t\t\"baichuan\": \"https://api.baichuan-ai.com/v1\",\n\t\t\"minimax\": \"https://api.minimax.chat/v1\",\n\t\t\"yi\": \"https://api.lingyiwanwu.com/v1\",\n\t\t\"stepfun\": \"https://api.stepfun.com/v1\",\n\t\t\"coze\": \"https://api.coze.cn/open_api/v2\",\n\n\t\t// 本地模型\n\t\t\"ollama\": \"http://localhost:11434/v1\",\n\t}\n\n\tif url, ok := baseURLMap[provider]; ok {\n\t\treturn url\n\t}\n\n\t// 未知提供商,返回空字符串,使用 OpenAI SDK 默认值\n\treturn \"\"\n}\n\n// GetProviderInfo 获取提供商信息 (用于日志和调试)\nfunc GetProviderInfo(config *models.LLMConfigModel) string {\n\tprovider := strings.ToLower(config.Provider)\n\tbaseURL := getProviderBaseURL(provider, config.BaseURL)\n\n\tinfo := fmt.Sprintf(\"%s (%s)\", config.Provider, config.Model)\n\tif baseURL != \"\" {\n\t\tinfo += fmt.Sprintf(\" @ %s\", baseURL)\n\t}\n\n\treturn info\n}\n\n// ValidateLLMConfig 验证 LLM 配置\nfunc ValidateLLMConfig(config *models.LLMConfigModel) error {\n\tif config.Provider == \"\" {\n\t\treturn fmt.Errorf(\"provider cannot be empty\")\n\t}\n\n\t// Ollama 本地运行时不需要 API Key\n\tprovider := strings.ToLower(config.Provider)\n\tif provider != \"ollama\" && config.APIKey == \"\" {\n\t\treturn fmt.Errorf(\"api_key cannot be empty\")\n\t}\n\n\tif config.Model == \"\" {\n\t\treturn fmt.Errorf(\"model cannot be empty\")\n\t}\n\n\treturn nil\n}\n\n// GetRecommendedModels 获取各提供商的推荐模型列表\nfunc GetRecommendedModels(provider string) []string {\n\tprovider = strings.ToLower(provider)\n\n\tmodelsMap := map[string][]string{\n\t\t// 国际模型\n\t\t\"openai\": {\n\t\t\t\"gpt-4o\",\n\t\t\t\"gpt-4o-mini\",\n\t\t\t\"gpt-4-turbo\",\n\t\t\t\"gpt-4\",\n\t\t\t\"gpt-3.5-turbo\",\n\t\t},\n\t\t\"anthropic\": {\n\t\t\t\"claude-3-5-sonnet-20241022\",\n\t\t\t\"claude-3-5-haiku-20241022\",\n\t\t\t\"claude-3-opus-20240229\",\n\t\t\t\"claude-3-sonnet-20240229\",\n\t\t\t\"claude-3-haiku-20240307\",\n\t\t},\n\t\t\"claude\": { // anthropic 别名\n\t\t\t\"claude-3-5-sonnet-20241022\",\n\t\t\t\"claude-3-5-haiku-20241022\",\n\t\t\t\"claude-3-opus-20240229\",\n\t\t},\n\t\t\"gemini\": {\n\t\t\t\"gemini-2.0-flash-exp\",\n\t\t\t\"gemini-1.5-pro\",\n\t\t\t\"gemini-1.5-flash\",\n\t\t},\n\t\t\"mistral\": {\n\t\t\t\"mistral-large-latest\",\n\t\t\t\"mistral-medium-latest\",\n\t\t\t\"mistral-small-latest\",\n\t\t},\n\t\t\"deepseek\": {\n\t\t\t\"deepseek-chat\",\n\t\t\t\"deepseek-coder\",\n\t\t},\n\t\t\"groq\": {\n\t\t\t\"llama-3.3-70b-versatile\",\n\t\t\t\"llama-3.1-70b-versatile\",\n\t\t\t\"mixtral-8x7b-32768\",\n\t\t},\n\t\t\"xai\": {\n\t\t\t\"grok-beta\",\n\t\t\t\"grok-vision-beta\",\n\t\t},\n\n\t\t// 国内模型\n\t\t\"qwen\": {\n\t\t\t\"qwen-max\",\n\t\t\t\"qwen-plus\",\n\t\t\t\"qwen-turbo\",\n\t\t\t\"qwen-long\",\n\t\t},\n\t\t\"siliconflow\": {\n\t\t\t\"deepseek-ai/DeepSeek-V3\",\n\t\t\t\"Qwen/Qwen2.5-72B-Instruct\",\n\t\t\t\"meta-llama/Llama-3.3-70B-Instruct\",\n\t\t},\n\t\t\"doubao\": {\n\t\t\t\"doubao-pro-32k\",\n\t\t\t\"doubao-lite-32k\",\n\t\t},\n\t\t\"chatglm\": {\n\t\t\t\"glm-4-plus\",\n\t\t\t\"glm-4-air\",\n\t\t\t\"glm-4-flash\",\n\t\t},\n\t\t\"moonshot\": {\n\t\t\t\"moonshot-v1-8k\",\n\t\t\t\"moonshot-v1-32k\",\n\t\t\t\"moonshot-v1-128k\",\n\t\t},\n\t\t\"yi\": {\n\t\t\t\"yi-lightning\",\n\t\t\t\"yi-large\",\n\t\t\t\"yi-medium\",\n\t\t},\n\t\t\"stepfun\": {\n\t\t\t\"step-1-8k\",\n\t\t\t\"step-1-32k\",\n\t\t\t\"step-1-128k\",\n\t\t},\n\n\t\t// 本地模型\n\t\t\"ollama\": {\n\t\t\t\"qwen2.5:latest\",\n\t\t\t\"llama3.3:latest\",\n\t\t\t\"deepseek-r1:latest\",\n\t\t\t\"mistral:latest\",\n\t\t},\n\t}\n\n\tif models, ok := modelsMap[provider]; ok {\n\t\treturn models\n\t}\n\n\treturn []string{}\n}\n\n// SupportsToolCalling 检查模型是否支持工具调用\nfunc SupportsToolCalling(provider, model string) bool {\n\tprovider = strings.ToLower(provider)\n\tmodel = strings.ToLower(model)\n\n\t// 已知支持工具调用的模型\n\tsupportedModels := map[string][]string{\n\t\t\"openai\": {\n\t\t\t\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-4\",\n\t\t\t\"gpt-3.5-turbo\", \"gpt-3.5-turbo-0125\",\n\t\t},\n\t\t\"anthropic\": {\n\t\t\t\"claude-3-5-sonnet\", \"claude-3-5-haiku\",\n\t\t\t\"claude-3-opus\", \"claude-3-sonnet\", \"claude-3-haiku\",\n\t\t},\n\t\t\"claude\": {\n\t\t\t\"claude-3-5-sonnet\", \"claude-3-5-haiku\",\n\t\t\t\"claude-3-opus\", \"claude-3-sonnet\",\n\t\t},\n\t\t\"gemini\": {\n\t\t\t\"gemini-2.0\", \"gemini-1.5-pro\", \"gemini-1.5-flash\",\n\t\t},\n\t\t\"mistral\": {\n\t\t\t\"mistral-large\", \"mistral-medium\", \"mistral-small\",\n\t\t},\n\t\t\"deepseek\": {\n\t\t\t\"deepseek-chat\", \"deepseek-reasoner\", \"deepseek-v3\",\n\t\t},\n\t\t\"qwen\": {\n\t\t\t\"qwen-max\", \"qwen-plus\", \"qwen-turbo\", \"qwen2.5\",\n\t\t},\n\t\t\"chatglm\": {\n\t\t\t\"glm-4-plus\", \"glm-4-air\", \"glm-4\",\n\t\t},\n\t\t\"moonshot\": {\n\t\t\t\"moonshot-v1\",\n\t\t},\n\t\t\"yi\": {\n\t\t\t\"yi-lightning\", \"yi-large\",\n\t\t},\n\t\t\"siliconflow\": {\n\t\t\t\"qwen2.5\", \"qwen/qwen\", \"deepseek-v3\", \"deepseek-ai/deepseek\",\n\t\t\t\"llama-3.3\", \"llama-3.1\", \"meta-llama/llama\",\n\t\t\t\"yi-lightning\", \"01-ai/yi\",\n\t\t},\n\t\t\"ollama\": {\n\t\t\t// Ollama 支持工具调用的模型(需要较新的模型)\n\t\t\t\"qwen2.5\", \"qwen2\", \"qwen\",\n\t\t\t\"llama3.3\", \"llama3.2\", \"llama3.1\", \"llama3\",\n\t\t\t\"llama-3.3\", \"llama-3.2\", \"llama-3.1\", \"llama-3\",\n\t\t\t\"mistral\", \"mixtral\",\n\t\t\t\"deepseek-r1\", \"deepseek-v3\", \"deepseek-coder\",\n\t\t\t\"yi-coder\", \"yi-lightning\",\n\t\t\t\"phi3\", \"phi4\",\n\t\t\t\"gemma2\", \"gemma\",\n\t\t\t\"command-r\", \"command-r-plus\",\n\t\t},\n\t}\n\n\t// 检查提供商\n\tmodels, exists := supportedModels[provider]\n\tif !exists {\n\t\t// 未知提供商,默认支持(OpenAI 兼容)\n\t\treturn true\n\t}\n\n\t// 检查模型名称是否包含支持的关键词\n\tfor _, supportedModel := range models {\n\t\tif strings.Contains(model, supportedModel) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n"}, "3eb6575f035b7591359c05ffa97621f29226d88d": {"t": 1785759247.0045125, "n": 14981, "h": "7dcbedcf7fc7a32d", "c": "package agent\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/Ingenimax/agent-sdk-go/pkg/interfaces\"\n\t\"github.com/Ingenimax/agent-sdk-go/pkg/llm/anthropic\"\n\t\"github.com/Ingenimax/agent-sdk-go/pkg/llm/openai\"\n\t\"github.com/browserwing/browserwing/models\"\n)\n\n// LLMAdapter LLM 适配器,用于将各种 LLM 提供商适配为 agent-sdk-go 的接口\ntype LLMAdapter struct {\n\tprovider string\n\tmodel string\n\tclient interfaces.LLM\n}\n\n// DeepSeekWrapper DeepSeek API 的包装器\n// 用于处理 DeepSeek 的特殊参数要求\ntype DeepSeekWrapper struct {\n\tclient interfaces.LLM\n}\n\n// DeepSeekToolWrapper 包装工具以确保工具名称符合 DeepSeek 的要求\ntype DeepSeekToolWrapper struct {\n\ttool interfaces.Tool\n\toriginalName string\n\tcleanedName string\n}\n\n// cleanToolName 清理工具名称,使其符合 DeepSeek 的要求 ^[a-zA-Z0-9_-]+$\nfunc cleanToolName(name string) string {\n\t// 将不允许的字符替换为下划线\n\tre := regexp.MustCompile(`[^a-zA-Z0-9_-]`)\n\tcleaned := re.ReplaceAllString(name, \"_\")\n\n\t// 确保不以数字或特殊字符开头\n\tif len(cleaned) > 0 && !regexp.MustCompile(`^[a-zA-Z]`).MatchString(cleaned) {\n\t\tcleaned = \"tool_\" + cleaned\n\t}\n\n\treturn cleaned\n}\n\n// wrapToolsForDeepSeek 包装工具数组,清理工具名称\nfunc (d *DeepSeekWrapper) wrapToolsForDeepSeek(tools []interfaces.Tool) []interfaces.Tool {\n\twrappedTools := make([]interfaces.Tool, len(tools))\n\tfor i, tool := range tools {\n\t\toriginalName := tool.Name()\n\t\tcleanedName := cleanToolName(originalName)\n\n\t\t// 如果名称需要清理,则使用包装器\n\t\tif cleanedName != originalName {\n\t\t\twrappedTools[i] = &DeepSeekToolWrapper{\n\t\t\t\ttool: tool,\n\t\t\t\toriginalName: originalName,\n\t\t\t\tcleanedName: cleanedName,\n\t\t\t}\n\t\t} else {\n\t\t\twrappedTools[i] = tool\n\t\t}\n\t}\n\treturn wrappedTools\n}\n\n// Name 实现 interfaces.Tool 接口\nfunc (w *DeepSeekToolWrapper) Name() string {\n\treturn w.cleanedName\n}\n\n// Description 实现 interfaces.Tool 接口\nfunc (w *DeepSeekToolWrapper) Description() string {\n\treturn w.tool.Description()\n}\n\n// Parameters 实现 interfaces.Tool 接口\nfunc (w *DeepSeekToolWrapper) Parameters() map[string]interfaces.ParameterSpec {\n\treturn w.tool.Parameters()\n}\n\n// Run 实现 interfaces.Tool 接口\nfunc (w *DeepSeekToolWrapper) Run(ctx context.Context, input string) (string, error) {\n\treturn w.tool.Run(ctx, input)\n}\n\n// Execute 实现 interfaces.Tool 接口\nfunc (w *DeepSeekToolWrapper) Execute(ctx context.Context, args string) (string, error) {\n\treturn w.tool.Execute(ctx, args)\n}\n\n// Generate 实现 interfaces.LLM 接口\nfunc (d *DeepSeekWrapper) Generate(ctx context.Context, prompt string, options ...interfaces.GenerateOption) (string, error) {\n\t// 修正选项中的 top_p 参数\n\tfixedOptions := d.fixTopPOptions(options)\n\treturn d.client.Generate(ctx, prompt, fixedOptions...)\n}\n\n// GenerateWithTools 实现 interfaces.LLM 接口\nfunc (d *DeepSeekWrapper) GenerateWithTools(ctx context.Context, prompt string, tools []interfaces.Tool, options ...interfaces.GenerateOption) (string, error) {\n\t// 清理工具名称\n\twrappedTools := d.wrapToolsForDeepSeek(tools)\n\t// 修正选项中的 top_p 参数\n\tfixedOptions := d.fixTopPOptions(options)\n\treturn d.client.GenerateWithTools(ctx, prompt, wrappedTools, fixedOptions...)\n}\n\n// GenerateDetailed 实现 interfaces.LLM 接口\nfunc (d *DeepSeekWrapper) GenerateDetailed(ctx context.Context, prompt string, options ...interfaces.GenerateOption) (*interfaces.LLMResponse, error) {\n\t// 修正选项中的 top_p 参数\n\tfixedOptions := d.fixTopPOptions(options)\n\treturn d.client.GenerateDetailed(ctx, prompt, fixedOptions...)\n}\n\n// GenerateWithToolsDetailed 实现 interfaces.LLM 接口\nfunc (d *DeepSeekWrapper) GenerateWithToolsDetailed(ctx context.Context, prompt string, tools []interfaces.Tool, options ...interfaces.GenerateOption) (*interfaces.LLMResponse, error) {\n\t// 清理工具名称\n\twrappedTools := d.wrapToolsForDeepSeek(tools)\n\t// 修正选项中的 top_p 参数\n\tfixedOptions := d.fixTopPOptions(options)\n\treturn d.client.GenerateWithToolsDetailed(ctx, prompt, wrappedTools, fixedOptions...)\n}\n\n// Name 实现 interfaces.LLM 接口\nfunc (d *DeepSeekWrapper) Name() string {\n\treturn d.client.Name()\n}\n\n// SupportsStreaming 实现 interfaces.LLM 接口\nfunc (d *DeepSeekWrapper) SupportsStreaming() bool {\n\treturn d.client.SupportsStreaming()\n}\n\n// GenerateStream 实现 interfaces.StreamingLLM 接口\nfunc (d *DeepSeekWrapper) GenerateStream(ctx context.Context, prompt string, options ...interfaces.GenerateOption) (<-chan interfaces.StreamEvent, error) {\n\t// 检查底层客户端是否支持流式\n\tstreamingLLM, ok := d.client.(interfaces.StreamingLLM)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"underlying client does not support streaming\")\n\t}\n\n\t// 修正选项中的 top_p 参数\n\tfixedOptions := d.fixTopPOptions(options)\n\treturn streamingLLM.GenerateStream(ctx, prompt, fixedOptions...)\n}\n\n// GenerateWithToolsStream 实现 interfaces.StreamingLLM 接口\nfunc (d *DeepSeekWrapper) GenerateWithToolsStream(ctx context.Context, prompt string, tools []interfaces.Tool, options ...interfaces.GenerateOption) (<-chan interfaces.StreamEvent, error) {\n\t// 检查底层客户端是否支持流式\n\tstreamingLLM, ok := d.client.(interfaces.StreamingLLM)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"underlying client does not support streaming\")\n\t}\n\n\t// 清理工具名称\n\twrappedTools := d.wrapToolsForDeepSeek(tools)\n\t// 修正选项中的 top_p 参数\n\tfixedOptions := d.fixTopPOptions(options)\n\treturn streamingLLM.GenerateWithToolsStream(ctx, prompt, wrappedTools, fixedOptions...)\n}\n\n// fixTopPOptions 修正 top_p 参数,确保在 (0, 1] 范围内\nfunc (d *DeepSeekWrapper) fixTopPOptions(options []interfaces.GenerateOption) []interfaces.GenerateOption {\n\t// 应用选项到配置以读取当前值\n\topts := &interfaces.GenerateOptions{\n\t\tLLMConfig: &interfaces.LLMConfig{},\n\t}\n\tfor _, opt := range options {\n\t\topt(opts)\n\t}\n\n\t// DeepSeek 特殊处理:top_p 必须在 (0, 1] 范围内\n\t// 如果 top_p 为 0 或 1,调整为 0.95\n\tif opts.LLMConfig.TopP <= 0 || opts.LLMConfig.TopP >= 1 {\n\t\topts.LLMConfig.TopP = 0.95\n\t}\n\n\t// 重新构建选项\n\tnewOptions := []interfaces.GenerateOption{\n\t\tinterfaces.WithTemperature(opts.LLMConfig.Temperature),\n\t\tinterfaces.WithTopP(opts.LLMConfig.TopP),\n\t\tinterfaces.WithFrequencyPenalty(opts.LLMConfig.FrequencyPenalty),\n\t\tinterfaces.WithPresencePenalty(opts.LLMConfig.PresencePenalty),\n\t}\n\n\tif len(opts.LLMConfig.StopSequences) > 0 {\n\t\tnewOptions = append(newOptions, interfaces.WithStopSequences(opts.LLMConfig.StopSequences))\n\t}\n\n\t// 复制其他选项\n\tif opts.SystemMessage != \"\" {\n\t\tnewOptions = append(newOptions, func(o *interfaces.GenerateOptions) {\n\t\t\to.SystemMessage = opts.SystemMessage\n\t\t})\n\t}\n\tif opts.ResponseFormat != nil {\n\t\tnewOptions = append(newOptions, func(o *interfaces.GenerateOptions) {\n\t\t\to.ResponseFormat = opts.ResponseFormat\n\t\t})\n\t}\n\tif opts.MaxIterations > 0 {\n\t\tnewOptions = append(newOptions, func(o *interfaces.GenerateOptions) {\n\t\t\to.MaxIterations = opts.MaxIterations\n\t\t})\n\t}\n\tif opts.Memory != nil {\n\t\tnewOptions = append(newOptions, func(o *interfaces.GenerateOptions) {\n\t\t\to.Memory = opts.Memory\n\t\t})\n\t}\n\tif opts.OrgID != \"\" {\n\t\tnewOptions = append(newOptions, func(o *interfaces.GenerateOptions) {\n\t\t\to.OrgID = opts.OrgID\n\t\t})\n\t}\n\n\treturn newOptions\n}\n\n// CreateLLMClient 根据配置创建 LLM 客户端\n// 支持多种 LLM 提供商,包括 OpenAI 兼容的 API\nfunc CreateLLMClient(config *models.LLMConfigModel) (interfaces.LLM, error) {\n\tprovider := strings.ToLower(config.Provider)\n\n\t// Anthropic Claude 系列 - 使用原生 SDK\n\tif provider == \"anthropic\" || provider == \"claude\" {\n\t\treturn createAnthropicClient(config)\n\t}\n\n\t// DeepSeek 需要特殊处理(top_p 不能为 0 或 1)\n\tif provider == \"deepseek\" {\n\t\treturn createDeepSeekClient(config)\n\t}\n\n\t// 其他所有提供商都使用 OpenAI 兼容模式\n\treturn createOpenAICompatibleClient(config)\n}\n\n// createAnthropicClient 创建 Anthropic 客户端\nfunc createAnthropicClient(config *models.LLMConfigModel) (interfaces.LLM, error) {\n\topts := []anthropic.Option{\n\t\tanthropic.WithModel(config.Model),\n\t}\n\n\t// Anthropic 支持自定义 BaseURL (如 Claude 代理)\n\tif config.BaseURL != \"\" {\n\t\topts = append(opts, anthropic.WithBaseURL(config.BaseURL))\n\t}\n\n\tclient := anthropic.NewClient(config.APIKey, opts...)\n\treturn client, nil\n}\n\n// createDeepSeekClient 创建 DeepSeek 客户端\n// DeepSeek API 有特殊要求:\n// 1. top_p 必须在 (0, 1.0] 区间,不能为 0 或 1(通过包装器处理)\n// 2. 工具名称只支持 ^[a-zA-Z0-9_-]+$ 模式\n// 3. BaseURL 固定为 https://api.deepseek.com/v1\nfunc createDeepSeekClient(config *models.LLMConfigModel) (interfaces.LLM, error) {\n\topts := []openai.Option{\n\t\topenai.WithModel(config.Model),\n\t}\n\n\t// DeepSeek 的 BaseURL\n\tbaseURL := \"https://api.deepseek.com/v1\"\n\tif config.BaseURL != \"\" {\n\t\tbaseURL = config.BaseURL\n\t}\n\topts = append(opts, openai.WithBaseURL(baseURL))\n\n\tclient := openai.NewClient(config.APIKey, opts...)\n\n\t// 使用包装器处理 DeepSeek 的特殊参数要求\n\treturn &DeepSeekWrapper{client: client}, nil\n}\n\n// createOpenAICompatibleClient 创建 OpenAI 兼容客户端\n// 支持所有 OpenAI API 兼容的服务\nfunc createOpenAICompatibleClient(config *models.LLMConfigModel) (interfaces.LLM, error) {\n\tprovider := strings.ToLower(config.Provider)\n\n\topts := []openai.Option{\n\t\topenai.WithModel(config.Model),\n\t}\n\n\t// 根据不同提供商设置 BaseURL\n\tbaseURL := getProviderBaseURL(provider, config.BaseURL)\n\tif baseURL != \"\" {\n\t\topts = append(opts, openai.WithBaseURL(baseURL))\n\t}\n\n\t// Ollama 本地运行时不需要真实的 API Key,提供默认值\n\tapiKey := config.APIKey\n\tif provider == \"ollama\" && apiKey == \"\" {\n\t\tapiKey = \"ollama\" // Ollama 本地不验证 API Key,提供占位符即可\n\t}\n\n\tclient := openai.NewClient(apiKey, opts...)\n\treturn client, nil\n}\n\n// getProviderBaseURL 获取各提供商的默认 BaseURL\nfunc getProviderBaseURL(provider, customBaseURL string) string {\n\t// 如果用户自定义了 BaseURL,优先使用\n\tif customBaseURL != \"\" {\n\t\treturn customBaseURL\n\t}\n\n\t// 各提供商的默认 API 端点\n\tbaseURLMap := map[string]string{\n\t\t// 国际模型\n\t\t\"openai\": \"https://api.openai.com/v1\",\n\t\t\"gemini\": \"https://generativelanguage.googleapis.com/v1beta/openai\",\n\t\t\"mistral\": \"https://api.mistral.ai/v1\",\n\t\t\"deepseek\": \"https://api.deepseek.com\",\n\t\t\"groq\": \"https://api.groq.com/openai/v1\",\n\t\t\"cohere\": \"https://api.cohere.ai/v1\",\n\t\t\"xai\": \"https://api.x.ai/v1\",\n\t\t\"together\": \"https://api.together.xyz/v1\",\n\t\t\"novita\": \"https://api.novita.ai/v3/openai\",\n\t\t\"openrouter\": \"https://openrouter.ai/api/v1\",\n\n\t\t// 国内模型\n\t\t\"qwen\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n\t\t\"siliconflow\": \"https://api.siliconflow.cn/v1\",\n\t\t\"doubao\": \"https://ark.cn-beijing.volces.com/api/v3\",\n\t\t\"ernie\": \"https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop\",\n\t\t\"spark\": \"https://spark-api-open.xf-yun.com/v1\",\n\t\t\"chatglm\": \"https://open.bigmodel.cn/api/paas/v4\",\n\t\t\"360\": \"https://api.360.cn/v1\",\n\t\t\"hunyuan\": \"https://hunyuan.tencentcloudapi.com\",\n\t\t\"moonshot\": \"https://api.moonshot.cn/v1\",\n\t\t\"baichuan\": \"https://api.baichuan-ai.com/v1\",\n\t\t\"minimax\": \"https://api.minimax.chat/v1\",\n\t\t\"yi\": \"https://api.lingyiwanwu.com/v1\",\n\t\t\"stepfun\": \"https://api.stepfun.com/v1\",\n\t\t\"coze\": \"https://api.coze.cn/open_api/v2\",\n\n\t\t// 本地模型\n\t\t\"ollama\": \"http://localhost:11434/v1\",\n\t}\n\n\tif url, ok := baseURLMap[provider]; ok {\n\t\treturn url\n\t}\n\n\t// 未知提供商,返回空字符串,使用 OpenAI SDK 默认值\n\treturn \"\"\n}\n\n// GetProviderInfo 获取提供商信息 (用于日志和调试)\nfunc GetProviderInfo(config *models.LLMConfigModel) string {\n\tprovider := strings.ToLower(config.Provider)\n\tbaseURL := getProviderBaseURL(provider, config.BaseURL)\n\n\tinfo := fmt.Sprintf(\"%s (%s)\", config.Provider, config.Model)\n\tif baseURL != \"\" {\n\t\tinfo += fmt.Sprintf(\" @ %s\", baseURL)\n\t}\n\n\treturn info\n}\n\n// ValidateLLMConfig 验证 LLM 配置\nfunc ValidateLLMConfig(config *models.LLMConfigModel) error {\n\tif config.Provider == \"\" {\n\t\treturn fmt.Errorf(\"provider cannot be empty\")\n\t}\n\n\t// Ollama 本地运行时不需要 API Key\n\tprovider := strings.ToLower(config.Provider)\n\tif provider != \"ollama\" && config.APIKey == \"\" {\n\t\treturn fmt.Errorf(\"api_key cannot be empty\")\n\t}\n\n\tif config.Model == \"\" {\n\t\treturn fmt.Errorf(\"model cannot be empty\")\n\t}\n\n\treturn nil\n}\n\n// GetRecommendedModels 获取各提供商的推荐模型列表\nfunc GetRecommendedModels(provider string) []string {\n\tprovider = strings.ToLower(provider)\n\n\tmodelsMap := map[string][]string{\n\t\t// 国际模型\n\t\t\"openai\": {\n\t\t\t\"gpt-4o\",\n\t\t\t\"gpt-4o-mini\",\n\t\t\t\"gpt-4-turbo\",\n\t\t\t\"gpt-4\",\n\t\t\t\"gpt-3.5-turbo\",\n\t\t},\n\t\t\"anthropic\": {\n\t\t\t\"claude-3-5-sonnet-20241022\",\n\t\t\t\"claude-3-5-haiku-20241022\",\n\t\t\t\"claude-3-opus-20240229\",\n\t\t\t\"claude-3-sonnet-20240229\",\n\t\t\t\"claude-3-haiku-20240307\",\n\t\t},\n\t\t\"claude\": { // anthropic 别名\n\t\t\t\"claude-3-5-sonnet-20241022\",\n\t\t\t\"claude-3-5-haiku-20241022\",\n\t\t\t\"claude-3-opus-20240229\",\n\t\t},\n\t\t\"gemini\": {\n\t\t\t\"gemini-2.0-flash-exp\",\n\t\t\t\"gemini-1.5-pro\",\n\t\t\t\"gemini-1.5-flash\",\n\t\t},\n\t\t\"mistral\": {\n\t\t\t\"mistral-large-latest\",\n\t\t\t\"mistral-medium-latest\",\n\t\t\t\"mistral-small-latest\",\n\t\t},\n\t\t\"deepseek\": {\n\t\t\t\"deepseek-chat\",\n\t\t\t\"deepseek-coder\",\n\t\t},\n\t\t\"groq\": {\n\t\t\t\"llama-3.3-70b-versatile\",\n\t\t\t\"llama-3.1-70b-versatile\",\n\t\t\t\"mixtral-8x7b-32768\",\n\t\t},\n\t\t\"xai\": {\n\t\t\t\"grok-beta\",\n\t\t\t\"grok-vision-beta\",\n\t\t},\n\n\t\t// 国内模型\n\t\t\"qwen\": {\n\t\t\t\"qwen-max\",\n\t\t\t\"qwen-plus\",\n\t\t\t\"qwen-turbo\",\n\t\t\t\"qwen-long\",\n\t\t},\n\t\t\"siliconflow\": {\n\t\t\t\"deepseek-ai/DeepSeek-V3\",\n\t\t\t\"Qwen/Qwen2.5-72B-Instruct\",\n\t\t\t\"meta-llama/Llama-3.3-70B-Instruct\",\n\t\t},\n\t\t\"doubao\": {\n\t\t\t\"doubao-pro-32k\",\n\t\t\t\"doubao-lite-32k\",\n\t\t},\n\t\t\"chatglm\": {\n\t\t\t\"glm-4-plus\",\n\t\t\t\"glm-4-air\",\n\t\t\t\"glm-4-flash\",\n\t\t},\n\t\t\"moonshot\": {\n\t\t\t\"moonshot-v1-8k\",\n\t\t\t\"moonshot-v1-32k\",\n\t\t\t\"moonshot-v1-128k\",\n\t\t},\n\t\t\"yi\": {\n\t\t\t\"yi-lightning\",\n\t\t\t\"yi-large\",\n\t\t\t\"yi-medium\",\n\t\t},\n\t\t\"stepfun\": {\n\t\t\t\"step-1-8k\",\n\t\t\t\"step-1-32k\",\n\t\t\t\"step-1-128k\",\n\t\t},\n\n\t\t// 本地模型\n\t\t\"ollama\": {\n\t\t\t\"qwen2.5:latest\",\n\t\t\t\"llama3.3:latest\",\n\t\t\t\"deepseek-r1:latest\",\n\t\t\t\"mistral:latest\",\n\t\t},\n\t}\n\n\tif models, ok := modelsMap[provider]; ok {\n\t\treturn models\n\t}\n\n\treturn []string{}\n}\n\n// SupportsToolCalling 检查模型是否支持工具调用\nfunc SupportsToolCalling(provider, model string) bool {\n\tprovider = strings.ToLower(provider)\n\tmodel = strings.ToLower(model)\n\n\t// 已知支持工具调用的模型\n\tsupportedModels := map[string][]string{\n\t\t\"openai\": {\n\t\t\t\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-4\",\n\t\t\t\"gpt-3.5-turbo\", \"gpt-3.5-turbo-0125\",\n\t\t},\n\t\t\"anthropic\": {\n\t\t\t\"claude-3-5-sonnet\", \"claude-3-5-haiku\",\n\t\t\t\"claude-3-opus\", \"claude-3-sonnet\", \"claude-3-haiku\",\n\t\t},\n\t\t\"claude\": {\n\t\t\t\"claude-3-5-sonnet\", \"claude-3-5-haiku\",\n\t\t\t\"claude-3-opus\", \"claude-3-sonnet\",\n\t\t},\n\t\t\"gemini\": {\n\t\t\t\"gemini-2.0\", \"gemini-1.5-pro\", \"gemini-1.5-flash\",\n\t\t},\n\t\t\"mistral\": {\n\t\t\t\"mistral-large\", \"mistral-medium\", \"mistral-small\",\n\t\t},\n\t\t\"deepseek\": {\n\t\t\t\"deepseek-chat\", \"deepseek-reasoner\", \"deepseek-v3\",\n\t\t},\n\t\t\"qwen\": {\n\t\t\t\"qwen-max\", \"qwen-plus\", \"qwen-turbo\", \"qwen2.5\",\n\t\t},\n\t\t\"chatglm\": {\n\t\t\t\"glm-4-plus\", \"glm-4-air\", \"glm-4\",\n\t\t},\n\t\t\"moonshot\": {\n\t\t\t\"moonshot-v1\",\n\t\t},\n\t\t\"yi\": {\n\t\t\t\"yi-lightning\", \"yi-large\",\n\t\t},\n\t\t\"siliconflow\": {\n\t\t\t\"qwen2.5\", \"qwen/qwen\", \"deepseek-v3\", \"deepseek-ai/deepseek\",\n\t\t\t\"llama-3.3\", \"llama-3.1\", \"meta-llama/llama\",\n\t\t\t\"yi-lightning\", \"01-ai/yi\",\n\t\t},\n\t\t\"ollama\": {\n\t\t\t// Ollama 支持工具调用的模型(需要较新的模型)\n\t\t\t\"qwen2.5\", \"qwen2\", \"qwen\",\n\t\t\t\"llama3.3\", \"llama3.2\", \"llama3.1\", \"llama3\",\n\t\t\t\"llama-3.3\", \"llama-3.2\", \"llama-3.1\", \"llama-3\",\n\t\t\t\"mistral\", \"mixtral\",\n\t\t\t\"deepseek-r1\", \"deepseek-v3\", \"deepseek-coder\",\n\t\t\t\"yi-coder\", \"yi-lightning\",\n\t\t\t\"phi3\", \"phi4\",\n\t\t\t\"gemma2\", \"gemma\",\n\t\t\t\"command-r\", \"command-r-plus\",\n\t\t},\n\t}\n\n\t// 检查提供商\n\tmodels, exists := supportedModels[provider]\n\tif !exists {\n\t\t// 未知提供商,默认支持(OpenAI 兼容)\n\t\treturn true\n\t}\n\n\t// 检查模型名称是否包含支持的关键词\n\tfor _, supportedModel := range models {\n\t\tif strings.Contains(model, supportedModel) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n"}, "d7814a96f14c20e2fea486416823b97c3117aaaa": {"t": 1785759248.2775233, "n": 14368, "h": "f66614246ee03cf0", "c": "package agent\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/Ingenimax/agent-sdk-go/pkg/interfaces\"\n\t\"github.com/Ingenimax/agent-sdk-go/pkg/llm/anthropic\"\n\t\"github.com/Ingenimax/agent-sdk-go/pkg/llm/openai\"\n\t\"github.com/browserwing/browserwing/models\"\n)\n\n// LLMAdapter LLM 适配器,用于将各种 LLM 提供商适配为 agent-sdk-go 的接口\ntype LLMAdapter struct {\n\tprovider string\n\tmodel string\n\tclient interfaces.LLM\n}\n\n// DeepSeekWrapper DeepSeek API 的包装器\n// 用于处理 DeepSeek 的特殊参数要求\ntype DeepSeekWrapper struct {\n\tclient interfaces.LLM\n}\n\n// DeepSeekToolWrapper 包装工具以确保工具名称符合 DeepSeek 的要求\ntype DeepSeekToolWrapper struct {\n\ttool interfaces.Tool\n\toriginalName string\n\tcleanedName string\n}\n\n// cleanToolName 清理工具名称,使其符合 DeepSeek 的要求 ^[a-zA-Z0-9_-]+$\nfunc cleanToolName(name string) string {\n\t// 将不允许的字符替换为下划线\n\tre := regexp.MustCompile(`[^a-zA-Z0-9_-]`)\n\tcleaned := re.ReplaceAllString(name, \"_\")\n\t\n\t// 确保不以数字或特殊字符开头\n\tif len(cleaned) > 0 && !regexp.MustCompile(`^[a-zA-Z]`).MatchString(cleaned) {\n\t\tcleaned = \"tool_\" + cleaned\n\t}\n\t\n\treturn cleaned\n}\n\n// wrapToolsForDeepSeek 包装工具数组,清理工具名称\nfunc (d *DeepSeekWrapper) wrapToolsForDeepSeek(tools []interfaces.Tool) []interfaces.Tool {\n\twrappedTools := make([]interfaces.Tool, len(tools))\n\tfor i, tool := range tools {\n\t\toriginalName := tool.Name()\n\t\tcleanedName := cleanToolName(originalName)\n\t\t\n\t\t// 如果名称需要清理,则使用包装器\n\t\tif cleanedName != originalName {\n\t\t\twrappedTools[i] = &DeepSeekToolWrapper{\n\t\t\t\ttool: tool,\n\t\t\t\toriginalName: originalName,\n\t\t\t\tcleanedName: cleanedName,\n\t\t\t}\n\t\t} else {\n\t\t\twrappedTools[i] = tool\n\t\t}\n\t}\n\treturn wrappedTools\n}\n\n// Name 实现 interfaces.Tool 接口\nfunc (w *DeepSeekToolWrapper) Name() string {\n\treturn w.cleanedName\n}\n\n// Description 实现 interfaces.Tool 接口\nfunc (w *DeepSeekToolWrapper) Description() string {\n\treturn w.tool.Description()\n}\n\n// Parameters 实现 interfaces.Tool 接口\nfunc (w *DeepSeekToolWrapper) Parameters() map[string]interfaces.ParameterSpec {\n\treturn w.tool.Parameters()\n}\n\n// Run 实现 interfaces.Tool 接口\nfunc (w *DeepSeekToolWrapper) Run(ctx context.Context, input string) (string, error) {\n\treturn w.tool.Run(ctx, input)\n}\n\n// Execute 实现 interfaces.Tool 接口\nfunc (w *DeepSeekToolWrapper) Execute(ctx context.Context, args string) (string, error) {\n\treturn w.tool.Execute(ctx, args)\n}\n\n// Generate 实现 interfaces.LLM 接口\nfunc (d *DeepSeekWrapper) Generate(ctx context.Context, prompt string, options ...interfaces.GenerateOption) (string, error) {\n\t// 修正选项中的 top_p 参数\n\tfixedOptions := d.fixTopPOptions(options)\n\treturn d.client.Generate(ctx, prompt, fixedOptions...)\n}\n\n// GenerateWithTools 实现 interfaces.LLM 接口\nfunc (d *DeepSeekWrapper) GenerateWithTools(ctx context.Context, prompt string, tools []interfaces.Tool, options ...interfaces.GenerateOption) (string, error) {\n\t// 清理工具名称\n\twrappedTools := d.wrapToolsForDeepSeek(tools)\n\t// 修正选项中的 top_p 参数\n\tfixedOptions := d.fixTopPOptions(options)\n\treturn d.client.GenerateWithTools(ctx, prompt, wrappedTools, fixedOptions...)\n}\n\n// GenerateDetailed 实现 interfaces.LLM 接口\nfunc (d *DeepSeekWrapper) GenerateDetailed(ctx context.Context, prompt string, options ...interfaces.GenerateOption) (*interfaces.LLMResponse, error) {\n\t// 修正选项中的 top_p 参数\n\tfixedOptions := d.fixTopPOptions(options)\n\treturn d.client.GenerateDetailed(ctx, prompt, fixedOptions...)\n}\n\n// GenerateWithToolsDetailed 实现 interfaces.LLM 接口\nfunc (d *DeepSeekWrapper) GenerateWithToolsDetailed(ctx context.Context, prompt string, tools []interfaces.Tool, options ...interfaces.GenerateOption) (*interfaces.LLMResponse, error) {\n\t// 清理工具名称\n\twrappedTools := d.wrapToolsForDeepSeek(tools)\n\t// 修正选项中的 top_p 参数\n\tfixedOptions := d.fixTopPOptions(options)\n\treturn d.client.GenerateWithToolsDetailed(ctx, prompt, wrappedTools, fixedOptions...)\n}\n\n// Name 实现 interfaces.LLM 接口\nfunc (d *DeepSeekWrapper) Name() string {\n\treturn d.client.Name()\n}\n\n// SupportsStreaming 实现 interfaces.LLM 接口\nfunc (d *DeepSeekWrapper) SupportsStreaming() bool {\n\treturn d.client.SupportsStreaming()\n}\n\n// GenerateStream 实现 interfaces.StreamingLLM 接口\nfunc (d *DeepSeekWrapper) GenerateStream(ctx context.Context, prompt string, options ...interfaces.GenerateOption) (<-chan interfaces.StreamEvent, error) {\n\t// 检查底层客户端是否支持流式\n\tstreamingLLM, ok := d.client.(interfaces.StreamingLLM)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"underlying client does not support streaming\")\n\t}\n\n\t// 修正选项中的 top_p 参数\n\tfixedOptions := d.fixTopPOptions(options)\n\treturn streamingLLM.GenerateStream(ctx, prompt, fixedOptions...)\n}\n\n// GenerateWithToolsStream 实现 interfaces.StreamingLLM 接口\nfunc (d *DeepSeekWrapper) GenerateWithToolsStream(ctx context.Context, prompt string, tools []interfaces.Tool, options ...interfaces.GenerateOption) (<-chan interfaces.StreamEvent, error) {\n\t// 检查底层客户端是否支持流式\n\tstreamingLLM, ok := d.client.(interfaces.StreamingLLM)\n\tif !ok {\n\t\treturn nil, fmt.Errorf(\"underlying client does not support streaming\")\n\t}\n\n\t// 清理工具名称\n\twrappedTools := d.wrapToolsForDeepSeek(tools)\n\t// 修正选项中的 top_p 参数\n\tfixedOptions := d.fixTopPOptions(options)\n\treturn streamingLLM.GenerateWithToolsStream(ctx, prompt, wrappedTools, fixedOptions...)\n}\n\n// fixTopPOptions 修正 top_p 参数,确保在 (0, 1] 范围内\nfunc (d *DeepSeekWrapper) fixTopPOptions(options []interfaces.GenerateOption) []interfaces.GenerateOption {\n\t// 应用选项到配置以读取当前值\n\topts := &interfaces.GenerateOptions{\n\t\tLLMConfig: &interfaces.LLMConfig{},\n\t}\n\tfor _, opt := range options {\n\t\topt(opts)\n\t}\n\n\t// DeepSeek 特殊处理:top_p 必须在 (0, 1] 范围内\n\t// 如果 top_p 为 0 或 1,调整为 0.95\n\tif opts.LLMConfig.TopP <= 0 || opts.LLMConfig.TopP >= 1 {\n\t\topts.LLMConfig.TopP = 0.95\n\t}\n\n\t// 重新构建选项\n\tnewOptions := []interfaces.GenerateOption{\n\t\tinterfaces.WithTemperature(opts.LLMConfig.Temperature),\n\t\tinterfaces.WithTopP(opts.LLMConfig.TopP),\n\t\tinterfaces.WithFrequencyPenalty(opts.LLMConfig.FrequencyPenalty),\n\t\tinterfaces.WithPresencePenalty(opts.LLMConfig.PresencePenalty),\n\t}\n\n\tif len(opts.LLMConfig.StopSequences) > 0 {\n\t\tnewOptions = append(newOptions, interfaces.WithStopSequences(opts.LLMConfig.StopSequences))\n\t}\n\n\t// 复制其他选项\n\tif opts.SystemMessage != \"\" {\n\t\tnewOptions = append(newOptions, func(o *interfaces.GenerateOptions) {\n\t\t\to.SystemMessage = opts.SystemMessage\n\t\t})\n\t}\n\tif opts.ResponseFormat != nil {\n\t\tnewOptions = append(newOptions, func(o *interfaces.GenerateOptions) {\n\t\t\to.ResponseFormat = opts.ResponseFormat\n\t\t})\n\t}\n\tif opts.MaxIterations > 0 {\n\t\tnewOptions = append(newOptions, func(o *interfaces.GenerateOptions) {\n\t\t\to.MaxIterations = opts.MaxIterations\n\t\t})\n\t}\n\tif opts.Memory != nil {\n\t\tnewOptions = append(newOptions, func(o *interfaces.GenerateOptions) {\n\t\t\to.Memory = opts.Memory\n\t\t})\n\t}\n\tif opts.OrgID != \"\" {\n\t\tnewOptions = append(newOptions, func(o *interfaces.GenerateOptions) {\n\t\t\to.OrgID = opts.OrgID\n\t\t})\n\t}\n\n\treturn newOptions\n}\n\n// CreateLLMClient 根据配置创建 LLM 客户端\n// 支持多种 LLM 提供商,包括 OpenAI 兼容的 API\nfunc CreateLLMClient(config *models.LLMConfigModel) (interfaces.LLM, error) {\n\tprovider := strings.ToLower(config.Provider)\n\n\t// Anthropic Claude 系列 - 使用原生 SDK\n\tif provider == \"anthropic\" || provider == \"claude\" {\n\t\treturn createAnthropicClient(config)\n\t}\n\n\t// DeepSeek 需要特殊处理(top_p 不能为 0 或 1)\n\tif provider == \"deepseek\" {\n\t\treturn createDeepSeekClient(config)\n\t}\n\n\t// 其他所有提供商都使用 OpenAI 兼容模式\n\treturn createOpenAICompatibleClient(config)\n}\n\n// createAnthropicClient 创建 Anthropic 客户端\nfunc createAnthropicClient(config *models.LLMConfigModel) (interfaces.LLM, error) {\n\topts := []anthropic.Option{\n\t\tanthropic.WithModel(config.Model),\n\t}\n\n\t// Anthropic 支持自定义 BaseURL (如 Claude 代理)\n\tif config.BaseURL != \"\" {\n\t\topts = append(opts, anthropic.WithBaseURL(config.BaseURL))\n\t}\n\n\tclient := anthropic.NewClient(config.APIKey, opts...)\n\treturn client, nil\n}\n\n// createDeepSeekClient 创建 DeepSeek 客户端\n// DeepSeek API 有特殊要求:\n// 1. top_p 必须在 (0, 1.0] 区间,不能为 0 或 1(通过包装器处理)\n// 2. 工具名称只支持 ^[a-zA-Z0-9_-]+$ 模式\n// 3. BaseURL 固定为 https://api.deepseek.com/v1\nfunc createDeepSeekClient(config *models.LLMConfigModel) (interfaces.LLM, error) {\n\topts := []openai.Option{\n\t\topenai.WithModel(config.Model),\n\t}\n\n\t// DeepSeek 的 BaseURL\n\tbaseURL := \"https://api.deepseek.com/v1\"\n\tif config.BaseURL != \"\" {\n\t\tbaseURL = config.BaseURL\n\t}\n\topts = append(opts, openai.WithBaseURL(baseURL))\n\n\tclient := openai.NewClient(config.APIKey, opts...)\n\t\n\t// 使用包装器处理 DeepSeek 的特殊参数要求\n\treturn &DeepSeekWrapper{client: client}, nil\n}\n\n// createOpenAICompatibleClient 创建 OpenAI 兼容客户端\n// 支持所有 OpenAI API 兼容的服务\nfunc createOpenAICompatibleClient(config *models.LLMConfigModel) (interfaces.LLM, error) {\n\tprovider := strings.ToLower(config.Provider)\n\n\topts := []openai.Option{\n\t\topenai.WithModel(config.Model),\n\t}\n\n\t// 根据不同提供商设置 BaseURL\n\tbaseURL := getProviderBaseURL(provider, config.BaseURL)\n\tif baseURL != \"\" {\n\t\topts = append(opts, openai.WithBaseURL(baseURL))\n\t}\n\n\tclient := openai.NewClient(config.APIKey, opts...)\n\treturn client, nil\n}\n\n// getProviderBaseURL 获取各提供商的默认 BaseURL\nfunc getProviderBaseURL(provider, customBaseURL string) string {\n\t// 如果用户自定义了 BaseURL,优先使用\n\tif customBaseURL != \"\" {\n\t\treturn customBaseURL\n\t}\n\n\t// 各提供商的默认 API 端点\n\tbaseURLMap := map[string]string{\n\t\t// 国际模型\n\t\t\"openai\": \"https://api.openai.com/v1\",\n\t\t\"gemini\": \"https://generativelanguage.googleapis.com/v1beta/openai\",\n\t\t\"mistral\": \"https://api.mistral.ai/v1\",\n\t\t\"deepseek\": \"https://api.deepseek.com\",\n\t\t\"groq\": \"https://api.groq.com/openai/v1\",\n\t\t\"cohere\": \"https://api.cohere.ai/v1\",\n\t\t\"xai\": \"https://api.x.ai/v1\",\n\t\t\"together\": \"https://api.together.xyz/v1\",\n\t\t\"novita\": \"https://api.novita.ai/v3/openai\",\n\t\t\"openrouter\": \"https://openrouter.ai/api/v1\",\n\n\t\t// 国内模型\n\t\t\"qwen\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n\t\t\"siliconflow\": \"https://api.siliconflow.cn/v1\",\n\t\t\"doubao\": \"https://ark.cn-beijing.volces.com/api/v3\",\n\t\t\"ernie\": \"https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop\",\n\t\t\"spark\": \"https://spark-api-open.xf-yun.com/v1\",\n\t\t\"chatglm\": \"https://open.bigmodel.cn/api/paas/v4\",\n\t\t\"360\": \"https://api.360.cn/v1\",\n\t\t\"hunyuan\": \"https://hunyuan.tencentcloudapi.com\",\n\t\t\"moonshot\": \"https://api.moonshot.cn/v1\",\n\t\t\"baichuan\": \"https://api.baichuan-ai.com/v1\",\n\t\t\"minimax\": \"https://api.minimax.chat/v1\",\n\t\t\"yi\": \"https://api.lingyiwanwu.com/v1\",\n\t\t\"stepfun\": \"https://api.stepfun.com/v1\",\n\t\t\"coze\": \"https://api.coze.cn/open_api/v2\",\n\n\t\t// 本地模型\n\t\t\"ollama\": \"http://localhost:11434/v1\",\n\t}\n\n\tif url, ok := baseURLMap[provider]; ok {\n\t\treturn url\n\t}\n\n\t// 未知提供商,返回空字符串,使用 OpenAI SDK 默认值\n\treturn \"\"\n}\n\n// GetProviderInfo 获取提供商信息 (用于日志和调试)\nfunc GetProviderInfo(config *models.LLMConfigModel) string {\n\tprovider := strings.ToLower(config.Provider)\n\tbaseURL := getProviderBaseURL(provider, config.BaseURL)\n\n\tinfo := fmt.Sprintf(\"%s (%s)\", config.Provider, config.Model)\n\tif baseURL != \"\" {\n\t\tinfo += fmt.Sprintf(\" @ %s\", baseURL)\n\t}\n\n\treturn info\n}\n\n// ValidateLLMConfig 验证 LLM 配置\nfunc ValidateLLMConfig(config *models.LLMConfigModel) error {\n\tif config.Provider == \"\" {\n\t\treturn fmt.Errorf(\"provider cannot be empty\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn fmt.Errorf(\"api_key cannot be empty\")\n\t}\n\n\tif config.Model == \"\" {\n\t\treturn fmt.Errorf(\"model cannot be empty\")\n\t}\n\n\treturn nil\n}\n\n// GetRecommendedModels 获取各提供商的推荐模型列表\nfunc GetRecommendedModels(provider string) []string {\n\tprovider = strings.ToLower(provider)\n\n\tmodelsMap := map[string][]string{\n\t\t// 国际模型\n\t\t\"openai\": {\n\t\t\t\"gpt-4o\",\n\t\t\t\"gpt-4o-mini\",\n\t\t\t\"gpt-4-turbo\",\n\t\t\t\"gpt-4\",\n\t\t\t\"gpt-3.5-turbo\",\n\t\t},\n\t\t\"anthropic\": {\n\t\t\t\"claude-3-5-sonnet-20241022\",\n\t\t\t\"claude-3-5-haiku-20241022\",\n\t\t\t\"claude-3-opus-20240229\",\n\t\t\t\"claude-3-sonnet-20240229\",\n\t\t\t\"claude-3-haiku-20240307\",\n\t\t},\n\t\t\"claude\": { // anthropic 别名\n\t\t\t\"claude-3-5-sonnet-20241022\",\n\t\t\t\"claude-3-5-haiku-20241022\",\n\t\t\t\"claude-3-opus-20240229\",\n\t\t},\n\t\t\"gemini\": {\n\t\t\t\"gemini-2.0-flash-exp\",\n\t\t\t\"gemini-1.5-pro\",\n\t\t\t\"gemini-1.5-flash\",\n\t\t},\n\t\t\"mistral\": {\n\t\t\t\"mistral-large-latest\",\n\t\t\t\"mistral-medium-latest\",\n\t\t\t\"mistral-small-latest\",\n\t\t},\n\t\t\"deepseek\": {\n\t\t\t\"deepseek-chat\",\n\t\t\t\"deepseek-coder\",\n\t\t},\n\t\t\"groq\": {\n\t\t\t\"llama-3.3-70b-versatile\",\n\t\t\t\"llama-3.1-70b-versatile\",\n\t\t\t\"mixtral-8x7b-32768\",\n\t\t},\n\t\t\"xai\": {\n\t\t\t\"grok-beta\",\n\t\t\t\"grok-vision-beta\",\n\t\t},\n\n\t\t// 国内模型\n\t\t\"qwen\": {\n\t\t\t\"qwen-max\",\n\t\t\t\"qwen-plus\",\n\t\t\t\"qwen-turbo\",\n\t\t\t\"qwen-long\",\n\t\t},\n\t\t\"siliconflow\": {\n\t\t\t\"deepseek-ai/DeepSeek-V3\",\n\t\t\t\"Qwen/Qwen2.5-72B-Instruct\",\n\t\t\t\"meta-llama/Llama-3.3-70B-Instruct\",\n\t\t},\n\t\t\"doubao\": {\n\t\t\t\"doubao-pro-32k\",\n\t\t\t\"doubao-lite-32k\",\n\t\t},\n\t\t\"chatglm\": {\n\t\t\t\"glm-4-plus\",\n\t\t\t\"glm-4-air\",\n\t\t\t\"glm-4-flash\",\n\t\t},\n\t\t\"moonshot\": {\n\t\t\t\"moonshot-v1-8k\",\n\t\t\t\"moonshot-v1-32k\",\n\t\t\t\"moonshot-v1-128k\",\n\t\t},\n\t\t\"yi\": {\n\t\t\t\"yi-lightning\",\n\t\t\t\"yi-large\",\n\t\t\t\"yi-medium\",\n\t\t},\n\t\t\"stepfun\": {\n\t\t\t\"step-1-8k\",\n\t\t\t\"step-1-32k\",\n\t\t\t\"step-1-128k\",\n\t\t},\n\n\t\t// 本地模型\n\t\t\"ollama\": {\n\t\t\t\"qwen2.5:latest\",\n\t\t\t\"llama3.3:latest\",\n\t\t\t\"deepseek-r1:latest\",\n\t\t\t\"mistral:latest\",\n\t\t},\n\t}\n\n\tif models, ok := modelsMap[provider]; ok {\n\t\treturn models\n\t}\n\n\treturn []string{}\n}\n\n// SupportsToolCalling 检查模型是否支持工具调用\nfunc SupportsToolCalling(provider, model string) bool {\n\tprovider = strings.ToLower(provider)\n\tmodel = strings.ToLower(model)\n\n\t// 已知支持工具调用的模型\n\tsupportedModels := map[string][]string{\n\t\t\"openai\": {\n\t\t\t\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-4\",\n\t\t\t\"gpt-3.5-turbo\", \"gpt-3.5-turbo-0125\",\n\t\t},\n\t\t\"anthropic\": {\n\t\t\t\"claude-3-5-sonnet\", \"claude-3-5-haiku\",\n\t\t\t\"claude-3-opus\", \"claude-3-sonnet\", \"claude-3-haiku\",\n\t\t},\n\t\t\"claude\": {\n\t\t\t\"claude-3-5-sonnet\", \"claude-3-5-haiku\",\n\t\t\t\"claude-3-opus\", \"claude-3-sonnet\",\n\t\t},\n\t\t\"gemini\": {\n\t\t\t\"gemini-2.0\", \"gemini-1.5-pro\", \"gemini-1.5-flash\",\n\t\t},\n\t\t\"mistral\": {\n\t\t\t\"mistral-large\", \"mistral-medium\", \"mistral-small\",\n\t\t},\n\t\t\"deepseek\": {\n\t\t\t\"deepseek-chat\", \"deepseek-reasoner\", \"deepseek-v3\",\n\t\t},\n\t\t\"qwen\": {\n\t\t\t\"qwen-max\", \"qwen-plus\", \"qwen-turbo\", \"qwen2.5\",\n\t\t},\n\t\t\"chatglm\": {\n\t\t\t\"glm-4-plus\", \"glm-4-air\", \"glm-4\",\n\t\t},\n\t\t\"moonshot\": {\n\t\t\t\"moonshot-v1\",\n\t\t},\n\t\t\"yi\": {\n\t\t\t\"yi-lightning\", \"yi-large\",\n\t\t},\n\t\t\"siliconflow\": {\n\t\t\t\"qwen2.5\", \"qwen/qwen\", \"deepseek-v3\", \"deepseek-ai/deepseek\",\n\t\t\t\"llama-3.3\", \"llama-3.1\", \"meta-llama/llama\",\n\t\t\t\"yi-lightning\", \"01-ai/yi\",\n\t\t},\n\t}\n\n\t// 检查提供商\n\tmodels, exists := supportedModels[provider]\n\tif !exists {\n\t\t// 未知提供商,默认支持(OpenAI 兼容)\n\t\treturn true\n\t}\n\n\t// 检查模型名称是否包含支持的关键词\n\tfor _, supportedModel := range models {\n\t\tif strings.Contains(model, supportedModel) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n"}, "6d734ddf724fb7879f4e2a2e245f3e24503c8d7f": {"t": 1785759249.6136768, "n": 7279, "h": "cafc4ac767376d3b", "c": "package agent\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/Ingenimax/agent-sdk-go/pkg/interfaces\"\n\t\"github.com/Ingenimax/agent-sdk-go/pkg/llm/anthropic\"\n\t\"github.com/Ingenimax/agent-sdk-go/pkg/llm/openai\"\n\t\"github.com/browserwing/browserwing/models\"\n)\n\n// LLMAdapter LLM 适配器,用于将各种 LLM 提供商适配为 agent-sdk-go 的接口\ntype LLMAdapter struct {\n\tprovider string\n\tmodel string\n\tclient interfaces.LLM\n}\n\n// CreateLLMClient 根据配置创建 LLM 客户端\n// 支持多种 LLM 提供商,包括 OpenAI 兼容的 API\nfunc CreateLLMClient(config *models.LLMConfigModel) (interfaces.LLM, error) {\n\tprovider := strings.ToLower(config.Provider)\n\n\t// Anthropic Claude 系列 - 使用原生 SDK\n\tif provider == \"anthropic\" || provider == \"claude\" {\n\t\treturn createAnthropicClient(config)\n\t}\n\n\t// 其他所有提供商都使用 OpenAI 兼容模式\n\treturn createOpenAICompatibleClient(config)\n}\n\n// createAnthropicClient 创建 Anthropic 客户端\nfunc createAnthropicClient(config *models.LLMConfigModel) (interfaces.LLM, error) {\n\topts := []anthropic.Option{\n\t\tanthropic.WithModel(config.Model),\n\t}\n\n\t// Anthropic 支持自定义 BaseURL (如 Claude 代理)\n\tif config.BaseURL != \"\" {\n\t\topts = append(opts, anthropic.WithBaseURL(config.BaseURL))\n\t}\n\n\tclient := anthropic.NewClient(config.APIKey, opts...)\n\treturn client, nil\n}\n\n// createOpenAICompatibleClient 创建 OpenAI 兼容客户端\n// 支持所有 OpenAI API 兼容的服务\nfunc createOpenAICompatibleClient(config *models.LLMConfigModel) (interfaces.LLM, error) {\n\tprovider := strings.ToLower(config.Provider)\n\n\topts := []openai.Option{\n\t\topenai.WithModel(config.Model),\n\t}\n\n\t// 根据不同提供商设置 BaseURL\n\tbaseURL := getProviderBaseURL(provider, config.BaseURL)\n\tif baseURL != \"\" {\n\t\topts = append(opts, openai.WithBaseURL(baseURL))\n\t}\n\n\tclient := openai.NewClient(config.APIKey, opts...)\n\treturn client, nil\n}\n\n// getProviderBaseURL 获取各提供商的默认 BaseURL\nfunc getProviderBaseURL(provider, customBaseURL string) string {\n\t// 如果用户自定义了 BaseURL,优先使用\n\tif customBaseURL != \"\" {\n\t\treturn customBaseURL\n\t}\n\n\t// 各提供商的默认 API 端点\n\tbaseURLMap := map[string]string{\n\t\t// 国际模型\n\t\t\"openai\": \"https://api.openai.com/v1\",\n\t\t\"gemini\": \"https://generativelanguage.googleapis.com/v1beta/openai\",\n\t\t\"mistral\": \"https://api.mistral.ai/v1\",\n\t\t\"deepseek\": \"https://api.deepseek.com\",\n\t\t\"groq\": \"https://api.groq.com/openai/v1\",\n\t\t\"cohere\": \"https://api.cohere.ai/v1\",\n\t\t\"xai\": \"https://api.x.ai/v1\",\n\t\t\"together\": \"https://api.together.xyz/v1\",\n\t\t\"novita\": \"https://api.novita.ai/v3/openai\",\n\t\t\"openrouter\": \"https://openrouter.ai/api/v1\",\n\n\t\t// 国内模型\n\t\t\"qwen\": \"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n\t\t\"siliconflow\": \"https://api.siliconflow.cn/v1\",\n\t\t\"doubao\": \"https://ark.cn-beijing.volces.com/api/v3\",\n\t\t\"ernie\": \"https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop\",\n\t\t\"spark\": \"https://spark-api-open.xf-yun.com/v1\",\n\t\t\"chatglm\": \"https://open.bigmodel.cn/api/paas/v4\",\n\t\t\"360\": \"https://api.360.cn/v1\",\n\t\t\"hunyuan\": \"https://hunyuan.tencentcloudapi.com\",\n\t\t\"moonshot\": \"https://api.moonshot.cn/v1\",\n\t\t\"baichuan\": \"https://api.baichuan-ai.com/v1\",\n\t\t\"minimax\": \"https://api.minimax.chat/v1\",\n\t\t\"yi\": \"https://api.lingyiwanwu.com/v1\",\n\t\t\"stepfun\": \"https://api.stepfun.com/v1\",\n\t\t\"coze\": \"https://api.coze.cn/open_api/v2\",\n\n\t\t// 本地模型\n\t\t\"ollama\": \"http://localhost:11434/v1\",\n\t}\n\n\tif url, ok := baseURLMap[provider]; ok {\n\t\treturn url\n\t}\n\n\t// 未知提供商,返回空字符串,使用 OpenAI SDK 默认值\n\treturn \"\"\n}\n\n// GetProviderInfo 获取提供商信息 (用于日志和调试)\nfunc GetProviderInfo(config *models.LLMConfigModel) string {\n\tprovider := strings.ToLower(config.Provider)\n\tbaseURL := getProviderBaseURL(provider, config.BaseURL)\n\n\tinfo := fmt.Sprintf(\"%s (%s)\", config.Provider, config.Model)\n\tif baseURL != \"\" {\n\t\tinfo += fmt.Sprintf(\" @ %s\", baseURL)\n\t}\n\n\treturn info\n}\n\n// ValidateLLMConfig 验证 LLM 配置\nfunc ValidateLLMConfig(config *models.LLMConfigModel) error {\n\tif config.Provider == \"\" {\n\t\treturn fmt.Errorf(\"provider cannot be empty\")\n\t}\n\n\tif config.APIKey == \"\" {\n\t\treturn fmt.Errorf(\"api_key cannot be empty\")\n\t}\n\n\tif config.Model == \"\" {\n\t\treturn fmt.Errorf(\"model cannot be empty\")\n\t}\n\n\treturn nil\n}\n\n// GetRecommendedModels 获取各提供商的推荐模型列表\nfunc GetRecommendedModels(provider string) []string {\n\tprovider = strings.ToLower(provider)\n\n\tmodelsMap := map[string][]string{\n\t\t// 国际模型\n\t\t\"openai\": {\n\t\t\t\"gpt-4o\",\n\t\t\t\"gpt-4o-mini\",\n\t\t\t\"gpt-4-turbo\",\n\t\t\t\"gpt-4\",\n\t\t\t\"gpt-3.5-turbo\",\n\t\t},\n\t\t\"anthropic\": {\n\t\t\t\"claude-3-5-sonnet-20241022\",\n\t\t\t\"claude-3-5-haiku-20241022\",\n\t\t\t\"claude-3-opus-20240229\",\n\t\t\t\"claude-3-sonnet-20240229\",\n\t\t\t\"claude-3-haiku-20240307\",\n\t\t},\n\t\t\"claude\": { // anthropic 别名\n\t\t\t\"claude-3-5-sonnet-20241022\",\n\t\t\t\"claude-3-5-haiku-20241022\",\n\t\t\t\"claude-3-opus-20240229\",\n\t\t},\n\t\t\"gemini\": {\n\t\t\t\"gemini-2.0-flash-exp\",\n\t\t\t\"gemini-1.5-pro\",\n\t\t\t\"gemini-1.5-flash\",\n\t\t},\n\t\t\"mistral\": {\n\t\t\t\"mistral-large-latest\",\n\t\t\t\"mistral-medium-latest\",\n\t\t\t\"mistral-small-latest\",\n\t\t},\n\t\t\"deepseek\": {\n\t\t\t\"deepseek-chat\",\n\t\t\t\"deepseek-coder\",\n\t\t},\n\t\t\"groq\": {\n\t\t\t\"llama-3.3-70b-versatile\",\n\t\t\t\"llama-3.1-70b-versatile\",\n\t\t\t\"mixtral-8x7b-32768\",\n\t\t},\n\t\t\"xai\": {\n\t\t\t\"grok-beta\",\n\t\t\t\"grok-vision-beta\",\n\t\t},\n\n\t\t// 国内模型\n\t\t\"qwen\": {\n\t\t\t\"qwen-max\",\n\t\t\t\"qwen-plus\",\n\t\t\t\"qwen-turbo\",\n\t\t\t\"qwen-long\",\n\t\t},\n\t\t\"siliconflow\": {\n\t\t\t\"deepseek-ai/DeepSeek-V3\",\n\t\t\t\"Qwen/Qwen2.5-72B-Instruct\",\n\t\t\t\"meta-llama/Llama-3.3-70B-Instruct\",\n\t\t},\n\t\t\"doubao\": {\n\t\t\t\"doubao-pro-32k\",\n\t\t\t\"doubao-lite-32k\",\n\t\t},\n\t\t\"chatglm\": {\n\t\t\t\"glm-4-plus\",\n\t\t\t\"glm-4-air\",\n\t\t\t\"glm-4-flash\",\n\t\t},\n\t\t\"moonshot\": {\n\t\t\t\"moonshot-v1-8k\",\n\t\t\t\"moonshot-v1-32k\",\n\t\t\t\"moonshot-v1-128k\",\n\t\t},\n\t\t\"yi\": {\n\t\t\t\"yi-lightning\",\n\t\t\t\"yi-large\",\n\t\t\t\"yi-medium\",\n\t\t},\n\t\t\"stepfun\": {\n\t\t\t\"step-1-8k\",\n\t\t\t\"step-1-32k\",\n\t\t\t\"step-1-128k\",\n\t\t},\n\n\t\t// 本地模型\n\t\t\"ollama\": {\n\t\t\t\"qwen2.5:latest\",\n\t\t\t\"llama3.3:latest\",\n\t\t\t\"deepseek-r1:latest\",\n\t\t\t\"mistral:latest\",\n\t\t},\n\t}\n\n\tif models, ok := modelsMap[provider]; ok {\n\t\treturn models\n\t}\n\n\treturn []string{}\n}\n\n// SupportsToolCalling 检查模型是否支持工具调用\nfunc SupportsToolCalling(provider, model string) bool {\n\tprovider = strings.ToLower(provider)\n\tmodel = strings.ToLower(model)\n\n\t// 已知支持工具调用的模型\n\tsupportedModels := map[string][]string{\n\t\t\"openai\": {\n\t\t\t\"gpt-4o\", \"gpt-4o-mini\", \"gpt-4-turbo\", \"gpt-4\",\n\t\t\t\"gpt-3.5-turbo\", \"gpt-3.5-turbo-0125\",\n\t\t},\n\t\t\"anthropic\": {\n\t\t\t\"claude-3-5-sonnet\", \"claude-3-5-haiku\",\n\t\t\t\"claude-3-opus\", \"claude-3-sonnet\", \"claude-3-haiku\",\n\t\t},\n\t\t\"claude\": {\n\t\t\t\"claude-3-5-sonnet\", \"claude-3-5-haiku\",\n\t\t\t\"claude-3-opus\", \"claude-3-sonnet\",\n\t\t},\n\t\t\"gemini\": {\n\t\t\t\"gemini-2.0\", \"gemini-1.5-pro\", \"gemini-1.5-flash\",\n\t\t},\n\t\t\"mistral\": {\n\t\t\t\"mistral-large\", \"mistral-medium\", \"mistral-small\",\n\t\t},\n\t\t\"deepseek\": {\n\t\t\t\"deepseek-chat\", \"deepseek-reasoner\", \"deepseek-v3\",\n\t\t},\n\t\t\"qwen\": {\n\t\t\t\"qwen-max\", \"qwen-plus\", \"qwen-turbo\", \"qwen2.5\",\n\t\t},\n\t\t\"chatglm\": {\n\t\t\t\"glm-4-plus\", \"glm-4-air\", \"glm-4\",\n\t\t},\n\t\t\"moonshot\": {\n\t\t\t\"moonshot-v1\",\n\t\t},\n\t\t\"yi\": {\n\t\t\t\"yi-lightning\", \"yi-large\",\n\t\t},\n\t\t\"siliconflow\": {\n\t\t\t\"qwen2.5\", \"qwen/qwen\", \"deepseek-v3\", \"deepseek-ai/deepseek\",\n\t\t\t\"llama-3.3\", \"llama-3.1\", \"meta-llama/llama\",\n\t\t\t\"yi-lightning\", \"01-ai/yi\",\n\t\t},\n\t}\n\n\t// 检查提供商\n\tmodels, exists := supportedModels[provider]\n\tif !exists {\n\t\t// 未知提供商,默认支持(OpenAI 兼容)\n\t\treturn true\n\t}\n\n\t// 检查模型名称是否包含支持的关键词\n\tfor _, supportedModel := range models {\n\t\tif strings.Contains(model, supportedModel) {\n\t\t\treturn true\n\t\t}\n\t}\n\n\treturn false\n}\n"}}}} |