Files

1 line
102 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"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"}}}}