diff --git a/agent/app/dto/agents.go b/agent/app/dto/agents.go index f693b76cc..fcf358a48 100644 --- a/agent/app/dto/agents.go +++ b/agent/app/dto/agents.go @@ -105,7 +105,7 @@ type AgentAccountCreateReq struct { RememberAPIKey bool `json:"rememberApiKey"` BaseURL string `json:"baseURL"` Models []AgentAccountModel `json:"models"` - APIType string `json:"apiType"` + APIType string `json:"apiType" validate:"required"` Remark string `json:"remark"` } @@ -115,7 +115,7 @@ type AgentAccountUpdateReq struct { APIKey string `json:"apiKey" validate:"required"` RememberAPIKey bool `json:"rememberApiKey"` BaseURL string `json:"baseURL"` - APIType string `json:"apiType"` + APIType string `json:"apiType" validate:"required"` Remark string `json:"remark"` SyncAgents bool `json:"syncAgents"` } diff --git a/agent/app/provider/catalog.go b/agent/app/provider/catalog.go index 7f13f19c5..a13a15ce4 100644 --- a/agent/app/provider/catalog.go +++ b/agent/app/provider/catalog.go @@ -13,12 +13,20 @@ type Model struct { Input []string } +type RuntimeDefault struct { + APIType string + ContextWindow int + MaxTokens int + Input []string +} + type Meta struct { Key string DisplayName string Sort uint DefaultBaseURL string EnvKey string + Default RuntimeDefault Models []Model } @@ -29,12 +37,24 @@ var catalog = map[string]Meta{ Sort: 10, DefaultBaseURL: "", EnvKey: "CUSTOM_API_KEY", - Models: []Model{}, + Default: RuntimeDefault{ + APIType: "openai-completions", + ContextWindow: 128000, + MaxTokens: 8192, + Input: []string{"text"}, + }, + Models: []Model{}, }, "ollama": { Key: "ollama", DisplayName: "Ollama", Sort: 15, + Default: RuntimeDefault{ + APIType: "openai-responses", + ContextWindow: 160000, + MaxTokens: 8192, + Input: []string{"text"}, + }, }, "vllm": { Key: "vllm", @@ -42,7 +62,13 @@ var catalog = map[string]Meta{ Sort: 20, DefaultBaseURL: "", EnvKey: "VLLM_API_KEY", - Models: []Model{}, + Default: RuntimeDefault{ + APIType: "openai-completions", + ContextWindow: 128000, + MaxTokens: 8192, + Input: []string{"text"}, + }, + Models: []Model{}, }, "deepseek": { Key: "deepseek", @@ -50,10 +76,16 @@ var catalog = map[string]Meta{ Sort: 25, DefaultBaseURL: "https://api.deepseek.com/v1", EnvKey: "DEEPSEEK_API_KEY", + Default: RuntimeDefault{ + APIType: "openai-completions", + ContextWindow: 128000, + MaxTokens: 8192, + Input: []string{"text"}, + }, Models: []Model{ {ID: "deepseek/deepseek-chat", Name: "DeepSeek Chat"}, - {ID: "deepseek/deepseek-reasoner", Name: "DeepSeek Reasoner"}, - {ID: "deepseek/deepseek-r1:1.5b", Name: "DeepSeek R1 1.5B"}, + {ID: "deepseek/deepseek-reasoner", Name: "DeepSeek Reasoner", Reasoning: true}, + {ID: "deepseek/deepseek-r1:1.5b", Name: "DeepSeek R1 1.5B", Reasoning: true}, }, }, "bailian-coding-plan": { @@ -62,15 +94,21 @@ var catalog = map[string]Meta{ Sort: 30, DefaultBaseURL: "https://coding.dashscope.aliyuncs.com/v1", EnvKey: "QWEN_API_KEY", + Default: RuntimeDefault{ + APIType: "openai-completions", + ContextWindow: 256000, + MaxTokens: 8192, + Input: []string{"text"}, + }, Models: []Model{ - {ID: "bailian-coding-plan/qwen3.5-plus", Name: "Qwen3.5-Plus"}, - {ID: "bailian-coding-plan/qwen3-max", Name: "Qwen3-Max"}, - {ID: "bailian-coding-plan/qwen3-coder-next", Name: "Qwen3-Coder-Next"}, - {ID: "bailian-coding-plan/qwen3-coder-plus", Name: "Qwen3-Coder-Plus"}, - {ID: "bailian-coding-plan/minimax-m2.5", Name: "MiniMax M2.5"}, - {ID: "bailian-coding-plan/glm-5", Name: "GLM-5"}, - {ID: "bailian-coding-plan/kimi-k2.5", Name: "Kimi-k2.5"}, - {ID: "bailian-coding-plan/glm-4.7", Name: "GLM-4.7"}, + {ID: "bailian-coding-plan/qwen3.5-plus", Name: "Qwen3.5-Plus", Reasoning: true}, + {ID: "bailian-coding-plan/qwen3-max", Name: "Qwen3-Max", Reasoning: true}, + {ID: "bailian-coding-plan/qwen3-coder-next", Name: "Qwen3-Coder-Next", Reasoning: true}, + {ID: "bailian-coding-plan/qwen3-coder-plus", Name: "Qwen3-Coder-Plus", Reasoning: true}, + {ID: "bailian-coding-plan/minimax-m2.5", Name: "MiniMax M2.5", Reasoning: true}, + {ID: "bailian-coding-plan/glm-5", Name: "GLM-5", Reasoning: true}, + {ID: "bailian-coding-plan/kimi-k2.5", Name: "Kimi-k2.5", Reasoning: true}, + {ID: "bailian-coding-plan/glm-4.7", Name: "GLM-4.7", Reasoning: true}, }, }, "ark-coding-plan": { @@ -79,13 +117,19 @@ var catalog = map[string]Meta{ Sort: 35, DefaultBaseURL: "https://ark.cn-beijing.volces.com/api/coding/v3", EnvKey: "ARK_API_KEY", + Default: RuntimeDefault{ + APIType: "openai-completions", + ContextWindow: 256000, + MaxTokens: 8192, + Input: []string{"text"}, + }, Models: []Model{ {ID: "ark-coding-plan/doubao-seed-2.0-code", Name: "Doubao-Seed-2.0-Code"}, {ID: "ark-coding-plan/doubao-seed-code", Name: "Doubao-Seed-Code"}, - {ID: "ark-coding-plan/kimi-k2.5", Name: "Kimi-K2.5"}, - {ID: "ark-coding-plan/glm-4.7", Name: "GLM-4.7"}, - {ID: "ark-coding-plan/deepseek-v3.2", Name: "DeepSeek-V3.2"}, - {ID: "ark-coding-plan/kimi-k2-thinking", Name: "Kimi-K2-thinking"}, + {ID: "ark-coding-plan/kimi-k2.5", Name: "Kimi-K2.5", Reasoning: true}, + {ID: "ark-coding-plan/glm-4.7", Name: "GLM-4.7", Reasoning: true}, + {ID: "ark-coding-plan/deepseek-v3.2", Name: "DeepSeek-V3.2", Reasoning: true}, + {ID: "ark-coding-plan/kimi-k2-thinking", Name: "Kimi-K2-thinking", Reasoning: true}, }, }, "zai": { @@ -94,11 +138,17 @@ var catalog = map[string]Meta{ Sort: 40, DefaultBaseURL: "https://open.bigmodel.cn/api/paas/v4", EnvKey: "ZAI_API_KEY", + Default: RuntimeDefault{ + APIType: "openai-completions", + ContextWindow: 204800, + MaxTokens: 131072, + Input: []string{"text"}, + }, Models: []Model{ - {ID: "zai/glm-5", Name: "GLM-5"}, - {ID: "zai/glm-4.7", Name: "GLM-4.7"}, - {ID: "zai/glm-4.7-flash", Name: "GLM-4.7-Flash"}, - {ID: "zai/glm-4.7-flashx", Name: "GLM-4.7-FlashX"}, + {ID: "zai/glm-5", Name: "GLM-5", Reasoning: true}, + {ID: "zai/glm-4.7", Name: "GLM-4.7", Reasoning: true}, + {ID: "zai/glm-4.7-flash", Name: "GLM-4.7-Flash", Reasoning: true}, + {ID: "zai/glm-4.7-flashx", Name: "GLM-4.7-FlashX", Reasoning: true}, }, }, "minimax": { @@ -107,10 +157,16 @@ var catalog = map[string]Meta{ Sort: 45, DefaultBaseURL: "https://api.minimaxi.com/anthropic", EnvKey: "MINIMAX_API_KEY", + Default: RuntimeDefault{ + APIType: "anthropic-messages", + ContextWindow: 200000, + MaxTokens: 8192, + Input: []string{"text"}, + }, Models: []Model{ {ID: "minimax/MiniMax-M2.7", Name: "MiniMax M2.7"}, {ID: "minimax/MiniMax-M2.7-highspeed", Name: "MiniMax M2.7 highspeed"}, - {ID: "minimax/MiniMax-M2.5", Name: "MiniMax M2.5"}, + {ID: "minimax/MiniMax-M2.5", Name: "MiniMax M2.5", Reasoning: true}, {ID: "minimax/MiniMax-M2.5-highspeed", Name: "MiniMax M2.5 highspeed"}, }, }, @@ -120,11 +176,17 @@ var catalog = map[string]Meta{ Sort: 46, DefaultBaseURL: "https://api.xiaomimimo.com/anthropic", EnvKey: "XIAOMI_API_KEY", + Default: RuntimeDefault{ + APIType: "anthropic-messages", + ContextWindow: 262144, + MaxTokens: 8192, + Input: []string{"text"}, + }, Models: []Model{ - {ID: "xiaomi/mimo-v2-pro", Name: "Xiaomi MiMo V2 Pro", ContextWindow: 262144, MaxTokens: 8192, Reasoning: false, Input: []string{"text"}}, - {ID: "xiaomi/mimo-v2-omni", Name: "Xiaomi MiMo V2 Omni", ContextWindow: 262144, MaxTokens: 8192, Reasoning: false, Input: []string{"text"}}, - {ID: "xiaomi/mimo-v2-tts", Name: "Xiaomi MiMo V2 TTS", ContextWindow: 262144, MaxTokens: 8192, Reasoning: false, Input: []string{"text"}}, - {ID: "xiaomi/mimo-v2-flash", Name: "Xiaomi MiMo V2 Flash", ContextWindow: 262144, MaxTokens: 8192, Reasoning: false, Input: []string{"text"}}, + {ID: "xiaomi/mimo-v2-pro", Name: "Xiaomi MiMo V2 Pro"}, + {ID: "xiaomi/mimo-v2-omni", Name: "Xiaomi MiMo V2 Omni"}, + {ID: "xiaomi/mimo-v2-tts", Name: "Xiaomi MiMo V2 TTS"}, + {ID: "xiaomi/mimo-v2-flash", Name: "Xiaomi MiMo V2 Flash"}, }, }, "kimi": { @@ -133,10 +195,16 @@ var catalog = map[string]Meta{ Sort: 50, DefaultBaseURL: "https://api.moonshot.cn/v1", EnvKey: "KIMI_API_KEY", + Default: RuntimeDefault{ + APIType: "openai-completions", + ContextWindow: 256000, + MaxTokens: 8192, + Input: []string{"text"}, + }, Models: []Model{ - {ID: "kimi/kimi-k2.5", Name: "Kimi K2.5"}, + {ID: "kimi/kimi-k2.5", Name: "Kimi K2.5", Reasoning: true}, {ID: "kimi/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"}, - {ID: "kimi/kimi-k2-thinking", Name: "Kimi K2 Thinking"}, + {ID: "kimi/kimi-k2-thinking", Name: "Kimi K2 Thinking", Reasoning: true}, }, }, "kimi-coding": { @@ -145,8 +213,14 @@ var catalog = map[string]Meta{ Sort: 51, DefaultBaseURL: "https://api.kimi.com/coding/", EnvKey: "KIMI_API_KEY", + Default: RuntimeDefault{ + APIType: "openai-completions", + ContextWindow: 262144, + MaxTokens: 32768, + Input: []string{"text", "image"}, + }, Models: []Model{ - {ID: "kimi-coding/k2p5", Name: "Kimi K2.5"}, + {ID: "kimi-coding/k2p5", Name: "Kimi K2.5", Reasoning: true}, }, }, "openai": { @@ -155,13 +229,18 @@ var catalog = map[string]Meta{ Sort: 55, DefaultBaseURL: "https://api.openai.com/v1", EnvKey: "OPENAI_API_KEY", + Default: RuntimeDefault{ + APIType: "openai-completions", + ContextWindow: 256000, + MaxTokens: 8192, + Input: []string{"text"}, + }, Models: []Model{ - {ID: "openai/codex-mini-latest", Name: "Codex Mini"}, - {ID: "openai/gpt-4.1", Name: "GPT-4.1"}, - {ID: "openai/gpt-4o", Name: "GPT-4o"}, - {ID: "openai/gpt-4o-mini", Name: "GPT-4o Mini"}, - {ID: "openai/gpt-5", Name: "GPT-5"}, - {ID: "openai/gpt-5-mini", Name: "GPT-5 Mini"}, + {ID: "openai/codex-mini-latest", Name: "Codex Mini", Reasoning: true}, + {ID: "openai/gpt-5", Name: "GPT-5", Reasoning: true}, + {ID: "openai/gpt-5-mini", Name: "GPT-5 Mini", Reasoning: true}, + {ID: "openai/gpt-5.4", Name: "GPT-5.4", Reasoning: true}, + {ID: "openai/gpt-5.3-codex", Name: "GPT-5.3-Codex", Reasoning: true}, }, }, "openrouter": { @@ -170,6 +249,12 @@ var catalog = map[string]Meta{ Sort: 56, DefaultBaseURL: "https://openrouter.ai/api/v1", EnvKey: "OPENROUTER_API_KEY", + Default: RuntimeDefault{ + APIType: "openai-completions", + ContextWindow: 128000, + MaxTokens: 8192, + Input: []string{"text"}, + }, Models: []Model{ {ID: "openrouter/free", Name: "openrouter/free"}, {ID: "openrouter/auto", Name: "openrouter/auto"}, @@ -181,12 +266,18 @@ var catalog = map[string]Meta{ Sort: 60, DefaultBaseURL: "https://api.anthropic.com", EnvKey: "ANTHROPIC_API_KEY", + Default: RuntimeDefault{ + APIType: "openai-completions", + ContextWindow: 256000, + MaxTokens: 8192, + Input: []string{"text"}, + }, Models: []Model{ {ID: "anthropic/claude-3-haiku-20240307", Name: "Claude 3 Haiku"}, {ID: "anthropic/claude-3-5-haiku-latest", Name: "Claude 3.5 Haiku"}, {ID: "anthropic/claude-3-5-sonnet-20241022", Name: "Claude 3.5 Sonnet"}, - {ID: "anthropic/claude-3-7-sonnet-20250219", Name: "Claude 3.7 Sonnet"}, - {ID: "anthropic/claude-opus-4-1", Name: "Claude Opus 4.1"}, + {ID: "anthropic/claude-3-7-sonnet-20250219", Name: "Claude 3.7 Sonnet", Reasoning: true}, + {ID: "anthropic/claude-opus-4-1", Name: "Claude Opus 4.1", Reasoning: true}, }, }, "gemini": { @@ -195,10 +286,16 @@ var catalog = map[string]Meta{ Sort: 65, DefaultBaseURL: "https://generativelanguage.googleapis.com", EnvKey: "GEMINI_API_KEY", + Default: RuntimeDefault{ + APIType: "openai-completions", + ContextWindow: 256000, + MaxTokens: 8192, + Input: []string{"text"}, + }, Models: []Model{ - {ID: "google/gemini-3-flash-preview", Name: "Gemini 3 Flash Preview"}, + {ID: "google/gemini-3-flash-preview", Name: "Gemini 3 Flash Preview", Reasoning: true}, {ID: "google/gemini-flash-latest", Name: "Gemini Flash Latest"}, - {ID: "google/gemini-3-pro-preview", Name: "Gemini 3 Pro Preview"}, + {ID: "google/gemini-3-pro-preview", Name: "Gemini 3 Pro Preview", Reasoning: true}, }, }, "moonshot": { @@ -207,10 +304,16 @@ var catalog = map[string]Meta{ Sort: 70, DefaultBaseURL: "https://api.moonshot.ai/v1", EnvKey: "MOONSHOT_API_KEY", + Default: RuntimeDefault{ + APIType: "openai-completions", + ContextWindow: 256000, + MaxTokens: 8192, + Input: []string{"text"}, + }, Models: []Model{ - {ID: "moonshot/kimi-k2.5", Name: "Kimi K2.5"}, + {ID: "moonshot/kimi-k2.5", Name: "Kimi K2.5", Reasoning: true}, {ID: "moonshot/kimi-k2-0905-preview", Name: "Kimi K2 0905 Preview"}, - {ID: "moonshot/kimi-k2-thinking", Name: "Kimi K2 Thinking"}, + {ID: "moonshot/kimi-k2-thinking", Name: "Kimi K2 Thinking", Reasoning: true}, }, }, } @@ -258,64 +361,80 @@ func DisplayName(key string) string { return meta.DisplayName } +func FindModel(key, modelID string) (Model, bool) { + meta, ok := Get(key) + if !ok { + return Model{}, false + } + for _, item := range meta.Models { + if item.ID == modelID { + return item, true + } + } + return Model{}, false +} + func cloneMeta(meta Meta) Meta { clone := meta + if len(meta.Default.Input) > 0 { + clone.Default.Input = make([]string, len(meta.Default.Input)) + copy(clone.Default.Input, meta.Default.Input) + } if len(meta.Models) > 0 { clone.Models = make([]Model, len(meta.Models)) for i, item := range meta.Models { - clone.Models[i] = normalizeModel(meta.Key, item) + clone.Models[i] = normalizeModel(meta, item) } } return clone } -func normalizeModel(provider string, model Model) Model { +func normalizeModel(meta Meta, model Model) Model { clone := model clone.ID = strings.TrimSpace(clone.ID) clone.Name = strings.TrimSpace(clone.Name) if clone.Name == "" { clone.Name = clone.ID } - if clone.MaxTokens <= 0 || clone.ContextWindow <= 0 { - resolvedMaxTokens, resolvedContextWindow := catalogRuntimeDefaults(strings.ToLower(strings.TrimSpace(provider))) - if clone.MaxTokens <= 0 { - clone.MaxTokens = resolvedMaxTokens - } - if clone.ContextWindow <= 0 { - clone.ContextWindow = resolvedContextWindow - } + if clone.MaxTokens <= 0 { + clone.MaxTokens = meta.Default.MaxTokens } - if len(clone.Input) == 0 { - clone.Input = defaultModelInputs(provider) + if clone.ContextWindow <= 0 { + clone.ContextWindow = meta.Default.ContextWindow } - if !clone.Reasoning { - clone.Reasoning = isReasoningModel(clone.ID) + if len(clone.Input) == 0 && len(meta.Default.Input) > 0 { + clone.Input = make([]string, len(meta.Default.Input)) + copy(clone.Input, meta.Default.Input) } return clone } -func defaultModelInputs(provider string) []string { - switch strings.ToLower(strings.TrimSpace(provider)) { - case "kimi-coding": - return []string{"text", "image"} - default: - return []string{"text"} +func ResolveRuntimeParams(provider, apiType string, maxTokens, contextWindow int) (string, int, int) { + defaultAPIType := "openai-completions" + defaultMaxTokens := 8192 + defaultContextWindow := 256000 + if meta, ok := Get(provider); ok { + if meta.Default.APIType != "" { + defaultAPIType = meta.Default.APIType + } + if meta.Default.MaxTokens > 0 { + defaultMaxTokens = meta.Default.MaxTokens + } + if meta.Default.ContextWindow > 0 { + defaultContextWindow = meta.Default.ContextWindow + } } -} - -func catalogRuntimeDefaults(provider string) (int, int) { - switch provider { - case "deepseek": - return 8192, 128000 - case "zai": - return 131072, 204800 - case "openrouter": - return 8192, 128000 - case "minimax", "kimi-coding": - return 8192, 200000 - case "custom", "vllm": - return 8192, 128000 - default: - return 8192, 256000 + resolvedAPI := apiType + if strings.TrimSpace(apiType) == "" { + resolvedAPI = defaultAPIType } + resolvedMaxTokens := defaultMaxTokens + resolvedContextWindow := defaultContextWindow + if maxTokens > 0 { + resolvedMaxTokens = maxTokens + } + if contextWindow > 0 { + resolvedContextWindow = contextWindow + } + return resolvedAPI, resolvedMaxTokens, resolvedContextWindow } diff --git a/agent/app/provider/openclaw.go b/agent/app/provider/openclaw.go index 3e1242361..230a08232 100644 --- a/agent/app/provider/openclaw.go +++ b/agent/app/provider/openclaw.go @@ -10,288 +10,322 @@ type OpenClawPatch struct { Models map[string]interface{} } -func BuildOpenClawPatch(provider, modelName, apiType string, maxTokens, contextWindow int, baseURL, apiKey string) (*OpenClawPatch, error) { +type openClawModelSpec struct { + ID string + Name string + Reasoning bool + Input []string + ContextWindow int + MaxTokens int +} + +type openClawPatchSpec struct { + PrimaryModel string + Provider string + APIKey string + BaseURL string + APIType string + AuthHeader bool + Model openClawModelSpec +} + +func BuildOpenClawPatch(provider, modelName, apiType string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) (*OpenClawPatch, error) { provider = strings.ToLower(strings.TrimSpace(provider)) modelName = strings.TrimSpace(modelName) if modelName == "" { return nil, fmt.Errorf("model is required") } + apiType, maxTokens, contextWindow = ResolveRuntimeParams(provider, apiType, maxTokens, contextWindow) modelID := modelName if parts := strings.SplitN(modelName, "/", 2); len(parts) == 2 { modelID = parts[1] } + var spec openClawPatchSpec switch provider { case "deepseek": - return buildDeepseekPatch(modelName, baseURL, apiKey), nil + spec = buildDeepseekPatchSpec(modelName, reasoning, maxTokens, contextWindow, baseURL, apiKey) case "gemini": - return buildGenericPatch(provider, modelName, modelID, apiType, maxTokens, contextWindow, baseURL, apiKey), nil + spec = buildGenericPatchSpec(provider, modelName, modelID, apiType, reasoning, maxTokens, contextWindow, baseURL, apiKey) case "moonshot", "kimi": - return buildMoonshotPatch(provider, modelName, modelID, baseURL, apiKey), nil + spec = buildMoonshotPatchSpec(provider, modelName, modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey) case "bailian-coding-plan": - return buildBailianPatch(modelID, maxTokens, contextWindow, baseURL, apiKey), nil + spec = buildBailianPatchSpec(modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey) case "ark-coding-plan": - return buildArkPatch(modelID, maxTokens, contextWindow, baseURL, apiKey), nil + spec = buildArkPatchSpec(modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey) case "minimax": - return buildMiniMaxPatch(modelID, baseURL, apiKey), nil + spec = buildMiniMaxPatchSpec(modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey) case "xiaomi": - return buildXiaomiPatch(modelID, baseURL, apiKey), nil + spec = buildXiaomiPatchSpec(modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey) case "custom", "vllm": - return buildCustomPatch(provider, modelName, apiType, maxTokens, contextWindow, baseURL, apiKey), nil + spec = buildCustomPatchSpec(provider, modelName, apiType, reasoning, maxTokens, contextWindow, baseURL, apiKey) case "ollama": - return buildOllamaPatch(modelName, modelID, apiType, baseURL), nil + spec = buildOllamaPatchSpec(modelName, modelID, apiType, reasoning, maxTokens, contextWindow, baseURL) case "kimi-coding": - return buildKimiCodingPatch(modelName, modelID, baseURL, apiKey), nil + spec = buildKimiCodingPatchSpec(modelName, modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey) case "zai": - return buildZaiPatch(modelID, maxTokens, contextWindow, baseURL, apiKey), nil + spec = buildZaiPatchSpec(modelID, reasoning, maxTokens, contextWindow, baseURL, apiKey) default: - return buildGenericPatch(provider, modelName, modelID, apiType, maxTokens, contextWindow, baseURL, apiKey), nil + spec = buildGenericPatchSpec(provider, modelName, modelID, apiType, reasoning, maxTokens, contextWindow, baseURL, apiKey) } + return buildOpenClawPatch(spec), nil } -func buildDeepseekPatch(modelName, baseURL, apiKey string) *OpenClawPatch { +func buildOpenClawPatch(spec openClawPatchSpec) *OpenClawPatch { return &OpenClawPatch{ - PrimaryModel: modelName, - Models: providerModels("deepseek", strings.TrimSpace(apiKey), firstNonEmpty(strings.TrimSpace(baseURL), "https://api.deepseek.com/v1"), "openai-completions", map[string]interface{}{ - "id": "deepseek-chat", - "name": "DeepSeek Chat", - "reasoning": false, - "input": []string{"text"}, - "contextWindow": 128000, - "maxTokens": 8192, - "cost": map[string]interface{}{}, - }), + PrimaryModel: spec.PrimaryModel, + Models: providerModels( + spec.Provider, + spec.APIKey, + spec.BaseURL, + spec.APIType, + spec.AuthHeader, + buildOpenClawModel(spec.Model), + ), } } -func buildMoonshotPatch(provider, modelName, modelID, baseURL, apiKey string) *OpenClawPatch { +func buildOpenClawModel(spec openClawModelSpec) map[string]interface{} { + return map[string]interface{}{ + "id": spec.ID, + "name": spec.Name, + "reasoning": spec.Reasoning, + "input": spec.Input, + "contextWindow": spec.ContextWindow, + "maxTokens": spec.MaxTokens, + "cost": map[string]interface{}{}, + } +} + +func buildDeepseekPatchSpec(modelName string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec { + return openClawPatchSpec{ + PrimaryModel: modelName, + Provider: "deepseek", + APIKey: strings.TrimSpace(apiKey), + BaseURL: baseURL, + APIType: "openai-completions", + Model: openClawModelSpec{ + ID: "deepseek-chat", + Name: "DeepSeek Chat", + Reasoning: reasoning, + Input: []string{"text"}, + ContextWindow: contextWindow, + MaxTokens: maxTokens, + }, + } +} + +func buildMoonshotPatchSpec(provider, modelName, modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec { configProvider := provider primaryModel := modelName if provider == "kimi" { configProvider = "moonshot" primaryModel = "moonshot/" + modelID } - return &OpenClawPatch{ + return openClawPatchSpec{ PrimaryModel: primaryModel, - Models: providerModels(configProvider, strings.TrimSpace(apiKey), withCatalogDefault(provider, baseURL), "openai-completions", map[string]interface{}{ - "id": modelID, - "name": modelID, - "reasoning": strings.Contains(strings.ToLower(modelID), "thinking"), - "input": []string{"text"}, - "contextWindow": 256000, - "maxTokens": 8192, - "cost": map[string]interface{}{}, - }), - } -} - -func buildBailianPatch(modelID string, maxTokens, contextWindow int, baseURL, apiKey string) *OpenClawPatch { - normalizedID := normalizeBailianCodingPlanModelID(modelID) - return &OpenClawPatch{ - PrimaryModel: "bailian-coding-plan/" + bailianPrimaryModelID(normalizedID), - Models: providerModels("bailian-coding-plan", strings.TrimSpace(apiKey), withCatalogDefault("bailian-coding-plan", baseURL), "openai-completions", map[string]interface{}{ - "id": normalizedID, - "name": normalizedID, - "reasoning": isReasoningModel(normalizedID), - "input": []string{"text"}, - "contextWindow": fallbackInt(contextWindow, 256000), - "maxTokens": fallbackInt(maxTokens, 8192), - "cost": map[string]interface{}{}, - }), - } -} - -func buildArkPatch(modelID string, maxTokens, contextWindow int, baseURL, apiKey string) *OpenClawPatch { - normalizedID := normalizeArkCodingPlanModelID(modelID) - return &OpenClawPatch{ - PrimaryModel: "ark-coding-plan/" + normalizedID, - Models: providerModels("ark-coding-plan", strings.TrimSpace(apiKey), withCatalogDefault("ark-coding-plan", baseURL), "openai-completions", map[string]interface{}{ - "id": normalizedID, - "name": normalizedID, - "reasoning": isReasoningModel(normalizedID), - "input": []string{"text"}, - "contextWindow": fallbackInt(contextWindow, 256000), - "maxTokens": fallbackInt(maxTokens, 8192), - "cost": map[string]interface{}{}, - }), - } -} - -func buildMiniMaxPatch(modelID, baseURL, apiKey string) *OpenClawPatch { - normalizedID := normalizeMiniMaxModelID(modelID) - return &OpenClawPatch{ - PrimaryModel: "minimax/" + normalizedID, - Models: map[string]interface{}{ - "mode": "merge", - "providers": map[string]interface{}{ - "minimax": map[string]interface{}{ - "apiKey": strings.TrimSpace(apiKey), - "baseUrl": firstNonEmpty(strings.TrimSpace(baseURL), "https://api.minimaxi.com/anthropic"), - "api": "anthropic-messages", - "authHeader": true, - "models": []map[string]interface{}{{ - "id": normalizedID, - "name": strings.ReplaceAll(normalizedID, "-", " "), - "reasoning": false, - "input": []string{"text"}, - "contextWindow": 200000, - "maxTokens": 8192, - "cost": map[string]interface{}{}, - }}, - }, - }, + Provider: configProvider, + APIKey: strings.TrimSpace(apiKey), + BaseURL: baseURL, + APIType: "openai-completions", + Model: openClawModelSpec{ + ID: modelID, + Name: modelID, + Reasoning: reasoning, + Input: []string{"text"}, + ContextWindow: contextWindow, + MaxTokens: maxTokens, }, } } -func buildXiaomiPatch(modelID, baseURL, apiKey string) *OpenClawPatch { +func buildBailianPatchSpec(modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec { + return openClawPatchSpec{ + PrimaryModel: "bailian-coding-plan/" + modelID, + Provider: "bailian-coding-plan", + APIKey: strings.TrimSpace(apiKey), + BaseURL: baseURL, + APIType: "openai-completions", + Model: openClawModelSpec{ + ID: modelID, + Name: modelID, + Reasoning: reasoning, + Input: []string{"text"}, + ContextWindow: contextWindow, + MaxTokens: maxTokens, + }, + } +} + +func buildArkPatchSpec(modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec { + return openClawPatchSpec{ + PrimaryModel: "ark-coding-plan/" + modelID, + Provider: "ark-coding-plan", + APIKey: strings.TrimSpace(apiKey), + BaseURL: baseURL, + APIType: "openai-completions", + Model: openClawModelSpec{ + ID: modelID, + Name: modelID, + Reasoning: reasoning, + Input: []string{"text"}, + ContextWindow: contextWindow, + MaxTokens: maxTokens, + }, + } +} + +func buildMiniMaxPatchSpec(modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec { + return openClawPatchSpec{ + PrimaryModel: "minimax/" + modelID, + Provider: "minimax", + APIKey: strings.TrimSpace(apiKey), + BaseURL: baseURL, + APIType: "anthropic-messages", + AuthHeader: true, + Model: openClawModelSpec{ + ID: modelID, + Name: modelID, + Reasoning: reasoning, + Input: []string{"text"}, + ContextWindow: contextWindow, + MaxTokens: maxTokens, + }, + } +} + +func buildXiaomiPatchSpec(modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec { normalizedID := strings.TrimSpace(modelID) - return &OpenClawPatch{ + return openClawPatchSpec{ PrimaryModel: "xiaomi/" + normalizedID, - Models: providerModels("xiaomi", strings.TrimSpace(apiKey), firstNonEmpty(strings.TrimSpace(baseURL), "https://api.xiaomimimo.com/anthropic"), "anthropic-messages", map[string]interface{}{ - "id": normalizedID, - "name": normalizedID, - "reasoning": false, - "input": []string{"text"}, - "contextWindow": 262144, - "maxTokens": 8192, - "cost": map[string]interface{}{}, - }), + Provider: "xiaomi", + APIKey: strings.TrimSpace(apiKey), + BaseURL: baseURL, + APIType: "anthropic-messages", + Model: openClawModelSpec{ + ID: normalizedID, + Name: normalizedID, + Reasoning: reasoning, + Input: []string{"text"}, + ContextWindow: contextWindow, + MaxTokens: maxTokens, + }, } } -func buildCustomPatch(provider, modelName, apiType string, maxTokens, contextWindow int, baseURL, apiKey string) *OpenClawPatch { +func buildCustomPatchSpec(provider, modelName, apiType string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec { customModelID := normalizeCustomModel(modelName) - return &OpenClawPatch{ + return openClawPatchSpec{ PrimaryModel: provider + "/" + customModelID, - Models: providerModels(provider, strings.TrimSpace(apiKey), strings.TrimSpace(baseURL), apiType, map[string]interface{}{ - "id": customModelID, - "name": customModelID, - "reasoning": isReasoningModel(customModelID), - "input": []string{"text"}, - "contextWindow": fallbackInt(contextWindow, 128000), - "maxTokens": fallbackInt(maxTokens, 8192), - "cost": map[string]interface{}{}, - }), + Provider: provider, + APIKey: strings.TrimSpace(apiKey), + BaseURL: strings.TrimSpace(baseURL), + APIType: apiType, + Model: openClawModelSpec{ + ID: customModelID, + Name: customModelID, + Reasoning: reasoning, + Input: []string{"text"}, + ContextWindow: contextWindow, + MaxTokens: maxTokens, + }, } } -func buildOllamaPatch(modelName, modelID, apiType, baseURL string) *OpenClawPatch { - api := normalizeAPIType(apiType) - if api != "openai-completions" && api != "openai-responses" { - api = "openai-responses" - } - return &OpenClawPatch{ +func buildOllamaPatchSpec(modelName, modelID, apiType string, reasoning bool, maxTokens, contextWindow int, baseURL string) openClawPatchSpec { + return openClawPatchSpec{ PrimaryModel: modelName, - Models: providerModels("ollama", "ollama", strings.TrimSpace(baseURL), api, map[string]interface{}{ - "id": modelID, - "name": modelID, - "reasoning": api != "openai-completions", - "input": []string{"text"}, - "contextWindow": 160000, - "maxTokens": 8192, - "cost": map[string]interface{}{}, - }), + Provider: "ollama", + APIKey: "ollama", + BaseURL: strings.TrimSpace(baseURL), + APIType: apiType, + Model: openClawModelSpec{ + ID: modelID, + Name: modelID, + Reasoning: reasoning, + Input: []string{"text"}, + ContextWindow: contextWindow, + MaxTokens: maxTokens, + }, } } -func buildKimiCodingPatch(modelName, modelID, baseURL, apiKey string) *OpenClawPatch { - return &OpenClawPatch{ +func buildKimiCodingPatchSpec(modelName, modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec { + return openClawPatchSpec{ PrimaryModel: modelName, - Models: providerModels("kimi-coding", strings.TrimSpace(apiKey), withCatalogDefault("kimi-coding", baseURL), "anthropic-messages", map[string]interface{}{ - "id": modelID, - "name": "Kimi for Coding", - "reasoning": true, - "input": []string{"text", "image"}, - "contextWindow": 262144, - "maxTokens": 32768, - "cost": map[string]interface{}{}, - }), + Provider: "kimi-coding", + APIKey: strings.TrimSpace(apiKey), + BaseURL: baseURL, + APIType: "anthropic-messages", + Model: openClawModelSpec{ + ID: modelID, + Name: "Kimi for Coding", + Reasoning: reasoning, + Input: []string{"text", "image"}, + ContextWindow: contextWindow, + MaxTokens: maxTokens, + }, } } -func buildZaiPatch(modelID string, maxTokens, contextWindow int, baseURL, apiKey string) *OpenClawPatch { - return &OpenClawPatch{ +func buildZaiPatchSpec(modelID string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec { + return openClawPatchSpec{ PrimaryModel: "zai/" + modelID, - Models: providerModels("zai", strings.TrimSpace(apiKey), withCatalogDefault("zai", baseURL), "openai-completions", map[string]interface{}{ - "id": modelID, - "name": zaiModelDisplayName(modelID), - "reasoning": modelID == "glm-5", - "input": []string{"text"}, - "contextWindow": fallbackInt(contextWindow, 204800), - "maxTokens": fallbackInt(maxTokens, 131072), - "cost": map[string]interface{}{}, - }), + Provider: "zai", + APIKey: strings.TrimSpace(apiKey), + BaseURL: baseURL, + APIType: "openai-completions", + Model: openClawModelSpec{ + ID: modelID, + Name: modelID, + Reasoning: reasoning, + Input: []string{"text"}, + ContextWindow: contextWindow, + MaxTokens: maxTokens, + }, } } -func buildGenericPatch(provider, modelName, modelID, apiType string, maxTokens, contextWindow int, baseURL, apiKey string) *OpenClawPatch { +func buildGenericPatchSpec(provider, modelName, modelID, apiType string, reasoning bool, maxTokens, contextWindow int, baseURL, apiKey string) openClawPatchSpec { providerName := provider primaryModel := modelName if provider == "gemini" { providerName = "google" primaryModel = "google/" + modelID } - return &OpenClawPatch{ + return openClawPatchSpec{ PrimaryModel: primaryModel, - Models: providerModels(providerName, strings.TrimSpace(apiKey), withCatalogDefault(provider, baseURL), normalizeAPIType(apiType), map[string]interface{}{ - "id": modelID, - "name": modelID, - "reasoning": isReasoningModel(modelID), - "input": []string{"text"}, - "contextWindow": fallbackInt(contextWindow, 256000), - "maxTokens": fallbackInt(maxTokens, 8192), - "cost": map[string]interface{}{}, - }), - } -} - -func providerModels(provider, apiKey, baseURL, api string, model map[string]interface{}) map[string]interface{} { - return map[string]interface{}{ - "mode": "merge", - "providers": map[string]interface{}{ - provider: map[string]interface{}{ - "apiKey": apiKey, - "baseUrl": baseURL, - "api": api, - "models": []map[string]interface{}{model}, - }, + Provider: providerName, + APIKey: strings.TrimSpace(apiKey), + BaseURL: baseURL, + APIType: apiType, + Model: openClawModelSpec{ + ID: modelID, + Name: modelID, + Reasoning: reasoning, + Input: []string{"text"}, + ContextWindow: contextWindow, + MaxTokens: maxTokens, }, } } -func withCatalogDefault(provider, baseURL string) string { - if strings.TrimSpace(baseURL) != "" { - return strings.TrimSpace(baseURL) +func providerModels(provider, apiKey, baseURL, api string, authHeader bool, model map[string]interface{}) map[string]interface{} { + providerConfig := map[string]interface{}{ + "apiKey": apiKey, + "baseUrl": baseURL, + "api": api, + "models": []map[string]interface{}{model}, } - if defaultURL, ok := DefaultBaseURL(provider); ok { - return defaultURL + if authHeader { + providerConfig["authHeader"] = true } - return "" -} - -func firstNonEmpty(values ...string) string { - for _, value := range values { - if strings.TrimSpace(value) != "" { - return strings.TrimSpace(value) - } + return map[string]interface{}{ + "mode": "merge", + "providers": map[string]interface{}{ + provider: providerConfig, + }, } - return "" -} - -func fallbackInt(value, fallback int) int { - if value > 0 { - return value - } - return fallback -} - -func normalizeAPIType(apiType string) string { - trim := strings.ToLower(strings.TrimSpace(apiType)) - if trim == "" { - return "openai-completions" - } - return trim } func normalizeCustomModel(modelName string) string { @@ -302,63 +336,3 @@ func normalizeCustomModel(modelName string) string { } return trim } - -func normalizeBailianCodingPlanModelID(modelID string) string { - trim := strings.TrimSpace(modelID) - switch strings.ToLower(trim) { - case "minimax-m2.5", "minimax m2.5", "minimax/minimax-m2.5", "minimax/minimax m2.5": - return "MiniMax/MiniMax-M2.5" - default: - return trim - } -} - -func normalizeArkCodingPlanModelID(modelID string) string { - return strings.ToLower(strings.TrimSpace(modelID)) -} - -func normalizeMiniMaxModelID(modelID string) string { - switch strings.ToLower(strings.TrimSpace(modelID)) { - case "minimax-m2.1", "minimax m2.1", "minimax-m2.1-preview", "minimax-m2.1-latest": - return "MiniMax-M2.1" - case "minimax-m2.1-lightning", "minimax m2.1 lightning": - return "MiniMax-M2.1-lightning" - default: - return modelID - } -} - -func zaiModelDisplayName(modelID string) string { - switch strings.ToLower(strings.TrimSpace(modelID)) { - case "glm-5": - return "GLM-5" - case "glm-4.7": - return "GLM-4.7" - case "glm-4.7-flash": - return "GLM-4.7-Flash" - case "glm-4.7-flashx": - return "GLM-4.7-FlashX" - default: - return strings.TrimSpace(modelID) - } -} - -func bailianPrimaryModelID(modelID string) string { - trim := strings.TrimSpace(modelID) - if trim == "" { - return "" - } - parts := strings.Split(trim, "/") - for i := len(parts) - 1; i >= 0; i-- { - part := strings.TrimSpace(parts[i]) - if part != "" { - return part - } - } - return trim -} - -func isReasoningModel(modelID string) bool { - trim := strings.ToLower(strings.TrimSpace(modelID)) - return strings.Contains(trim, "reason") || strings.Contains(trim, "thinking") -} diff --git a/agent/app/provider/verify.go b/agent/app/provider/verify.go index bb70e540e..6317951a4 100644 --- a/agent/app/provider/verify.go +++ b/agent/app/provider/verify.go @@ -163,6 +163,13 @@ func BuildVerifyRequest(provider, baseURL, apiKey string) VerifyRequest { }}, }}, }) + case "openrouter": + headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey) + if strings.Contains(base, "/v1") { + request.URL = base + "/key" + } else { + request.URL = base + "/v1/key" + } default: headers["Authorization"] = fmt.Sprintf("Bearer %s", apiKey) if strings.Contains(base, "/v1") { diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go index b913fbed7..49db55063 100644 --- a/agent/app/service/agents.go +++ b/agent/app/service/agents.go @@ -7,17 +7,15 @@ import ( "path" "sort" "strings" - "time" "github.com/1Panel-dev/1Panel/agent/app/dto" "github.com/1Panel-dev/1Panel/agent/app/dto/request" "github.com/1Panel-dev/1Panel/agent/app/model" + providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider" "github.com/1Panel-dev/1Panel/agent/app/repo" - "github.com/1Panel-dev/1Panel/agent/app/task" "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" - "github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/xpack" "gorm.io/gorm" ) @@ -29,6 +27,11 @@ type IAgentService interface { ResetToken(req dto.AgentTokenResetReq) error UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error GetProviders() ([]dto.ProviderInfo, error) + GetSecurityConfig(req dto.AgentSecurityConfigReq) (*dto.AgentSecurityConfig, error) + UpdateSecurityConfig(req dto.AgentSecurityConfigUpdateReq) error + GetOtherConfig(req dto.AgentOtherConfigReq) (*dto.AgentOtherConfig, error) + UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error + CreateAccount(req dto.AgentAccountCreateReq) error UpdateAccount(req dto.AgentAccountUpdateReq) error SyncAgentsByAccount(account *model.AgentAccount) error @@ -39,6 +42,7 @@ type IAgentService interface { DeleteAccountModel(req dto.AgentAccountModelDeleteReq) error VerifyAccount(req dto.AgentAccountVerifyReq) error DeleteAccount(req dto.AgentAccountDeleteReq) error + GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentFeishuConfig, error) UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) error GetTelegramConfig(req dto.AgentTelegramConfigReq) (*dto.AgentTelegramConfig, error) @@ -53,10 +57,6 @@ type IAgentService interface { UpdateQQBotConfig(req dto.AgentQQBotConfigUpdateReq) error InstallPlugin(req dto.AgentPluginInstallReq) error CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error) - GetSecurityConfig(req dto.AgentSecurityConfigReq) (*dto.AgentSecurityConfig, error) - UpdateSecurityConfig(req dto.AgentSecurityConfigUpdateReq) error - GetOtherConfig(req dto.AgentOtherConfigReq) (*dto.AgentOtherConfig, error) - UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error } @@ -340,15 +340,26 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error } func (a AgentService) GetProviders() ([]dto.ProviderInfo, error) { - definitions := providerDefinitions() + definitions := providercatalog.All() providers := make([]dto.ProviderInfo, 0, len(definitions)) for key, def := range definitions { + models := make([]dto.ProviderModelInfo, 0, len(def.Models)) + for _, item := range def.Models { + models = append(models, dto.ProviderModelInfo{ + ID: item.ID, + Name: item.Name, + ContextWindow: item.ContextWindow, + MaxTokens: item.MaxTokens, + Reasoning: item.Reasoning, + Input: append([]string(nil), item.Input...), + }) + } providers = append(providers, dto.ProviderInfo{ Sort: def.Sort, Provider: key, DisplayName: def.DisplayName, - BaseURL: def.BaseURL, - Models: def.Models, + BaseURL: def.DefaultBaseURL, + Models: models, }) } sort.Slice(providers, func(i, j int) bool { @@ -362,7 +373,7 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error { if exist, _ := agentAccountRepo.GetFirst(repo.WithByProvider(provider), repo.WithByName(req.Name)); exist != nil && exist.ID > 0 { return buserr.New("ErrRecordExist") } - resolvedInput, err := resolveAgentAccountInput(provider, req.APIKey, req.BaseURL, req.APIType, "") + resolvedInput, err := resolveAgentAccountInput(provider, req.APIKey, req.BaseURL) if err != nil { return err } @@ -372,7 +383,7 @@ func (a AgentService) CreateAccount(req dto.AgentAccountCreateReq) error { APIKey: resolvedInput.APIKey, RememberAPIKey: req.RememberAPIKey, BaseURL: resolvedInput.BaseURL, - APIType: resolvedInput.APIType, + APIType: req.APIType, Verified: true, Remark: req.Remark, } @@ -401,7 +412,7 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error { return err } provider := account.Provider - resolvedInput, err := resolveAgentAccountInput(provider, req.APIKey, req.BaseURL, req.APIType, account.APIType) + resolvedInput, err := resolveAgentAccountInput(provider, req.APIKey, req.BaseURL) if err != nil { return err } @@ -409,7 +420,7 @@ func (a AgentService) UpdateAccount(req dto.AgentAccountUpdateReq) error { account.APIKey = resolvedInput.APIKey account.RememberAPIKey = req.RememberAPIKey account.BaseURL = resolvedInput.BaseURL - account.APIType = resolvedInput.APIType + account.APIType = req.APIType account.Remark = req.Remark account.Verified = true @@ -445,7 +456,7 @@ func (a AgentService) PageAccounts(req dto.AgentAccountSearch) (int64, []dto.Age items = append(items, dto.AgentAccountInfo{ ID: item.ID, Provider: item.Provider, - ProviderName: providerDisplayName(item.Provider), + ProviderName: providercatalog.DisplayName(item.Provider), Name: item.Name, APIKey: apiKey, RememberAPIKey: item.RememberAPIKey, @@ -484,25 +495,22 @@ func (a AgentService) CreateAccountModel(req dto.AgentAccountModelCreateReq) err if err != nil { return err } - normalized, err := normalizeAgentAccountModel(account, req.Model) - if err != nil { - return err - } - if _, ok := findAgentAccountModelForProvider(account.Provider, models, normalized.ID); ok { + nextModel := cloneAgentAccountModel(req.Model) + if _, ok := findAgentAccountModelForProvider(account.Provider, models, nextModel.ID); ok { return buserr.New("ErrRecordExist") } - inputPayload, err := json.Marshal(sanitizeAgentAccountModelInputs(normalized.Input)) + inputPayload, err := json.Marshal(nextModel.Input) if err != nil { return err } sortOrder := len(models) + 1 record := &model.AgentAccountModel{ AccountID: account.ID, - Model: normalized.ID, - Name: normalized.Name, - ContextWindow: normalized.ContextWindow, - MaxTokens: normalized.MaxTokens, - Reasoning: normalized.Reasoning, + Model: nextModel.ID, + Name: nextModel.Name, + ContextWindow: nextModel.ContextWindow, + MaxTokens: nextModel.MaxTokens, + Reasoning: nextModel.Reasoning, Input: string(inputPayload), SortOrder: sortOrder, } @@ -525,22 +533,19 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err if err != nil { return err } - normalized, err := normalizeAgentAccountModel(account, req.Model) - if err != nil { - return err - } + nextModel := cloneAgentAccountModel(req.Model) for _, item := range models { if item.RecordID == req.Model.RecordID { continue } - if sameProviderModelID(account.Provider, item.ID, normalized.ID) { + if sameProviderModelID(account.Provider, item.ID, nextModel.ID) { return buserr.New("ErrRecordExist") } } nextModels := make([]dto.AgentAccountModel, 0, len(models)) for _, item := range models { if item.RecordID == req.Model.RecordID { - nextModels = append(nextModels, normalized) + nextModels = append(nextModels, nextModel) continue } nextModels = append(nextModels, item) @@ -548,15 +553,15 @@ func (a AgentService) UpdateAccountModel(req dto.AgentAccountModelUpdateReq) err if err := ensureAccountModelsNotBound(account, nextModels); err != nil { return err } - inputPayload, err := json.Marshal(sanitizeAgentAccountModelInputs(normalized.Input)) + inputPayload, err := json.Marshal(nextModel.Input) if err != nil { return err } - record.Model = normalized.ID - record.Name = normalized.Name - record.ContextWindow = normalized.ContextWindow - record.MaxTokens = normalized.MaxTokens - record.Reasoning = normalized.Reasoning + record.Model = nextModel.ID + record.Name = nextModel.Name + record.ContextWindow = nextModel.ContextWindow + record.MaxTokens = nextModel.MaxTokens + record.Reasoning = nextModel.Reasoning record.Input = string(inputPayload) if err := agentAccountModelRepo.Save(record); err != nil { return err @@ -603,11 +608,8 @@ func (a AgentService) SyncAgentsByAccount(account *model.AgentAccount) error { } func (a AgentService) VerifyAccount(req dto.AgentAccountVerifyReq) error { - resolvedVerification, err := resolveAgentAccountVerification(req.Provider, req.APIKey, req.BaseURL) - if err != nil { - return err - } - return verifyResolvedAgentAccount(resolvedVerification) + _, err := resolveAgentAccountInput(req.Provider, req.APIKey, req.BaseURL) + return err } func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error { @@ -620,180 +622,6 @@ func (a AgentService) DeleteAccount(req dto.AgentAccountDeleteReq) error { return agentAccountRepo.DeleteByID(req.ID) } -func (a AgentService) GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentFeishuConfig, error) { - _, _, conf, err := a.loadAgentConfig(req.AgentID) - if err != nil { - return nil, err - } - result := extractFeishuConfig(conf) - return &result, nil -} - -func (a AgentService) UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) error { - return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error { - setFeishuConfig(conf, dto.AgentFeishuConfig{ - Enabled: req.Enabled, - DmPolicy: req.DmPolicy, - BotName: req.BotName, - AppID: req.AppID, - AppSecret: req.AppSecret, - }) - setFeishuPluginEnabled(conf, req.Enabled) - return nil - }) -} - -func (a AgentService) GetTelegramConfig(req dto.AgentTelegramConfigReq) (*dto.AgentTelegramConfig, error) { - _, _, conf, err := a.loadAgentConfig(req.AgentID) - if err != nil { - return nil, err - } - result := extractTelegramConfig(conf) - return &result, nil -} - -func (a AgentService) UpdateTelegramConfig(req dto.AgentTelegramConfigUpdateReq) error { - return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error { - setTelegramConfig(conf, dto.AgentTelegramConfig{ - Enabled: req.Enabled, - DmPolicy: req.DmPolicy, - BotToken: req.BotToken, - Proxy: req.Proxy, - }) - return nil - }) -} - -func (a AgentService) GetDiscordConfig(req dto.AgentDiscordConfigReq) (*dto.AgentDiscordConfig, error) { - _, _, conf, err := a.loadAgentConfig(req.AgentID) - if err != nil { - return nil, err - } - result := extractDiscordConfig(conf) - return &result, nil -} - -func (a AgentService) UpdateDiscordConfig(req dto.AgentDiscordConfigUpdateReq) error { - return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error { - setDiscordConfig(conf, dto.AgentDiscordConfig{ - Enabled: req.Enabled, - DmPolicy: req.DmPolicy, - GroupPolicy: req.GroupPolicy, - Token: req.Token, - Proxy: req.Proxy, - }) - return nil - }) -} - -func (a AgentService) GetQQBotConfig(req dto.AgentQQBotConfigReq) (*dto.AgentQQBotConfig, error) { - _, install, conf, err := a.loadAgentConfig(req.AgentID) - if err != nil { - return nil, err - } - result := extractQQBotConfig(conf) - installed, _ := checkPluginInstalled(install.ContainerName, "qqbot") - result.Installed = installed - return &result, nil -} - -func (a AgentService) UpdateQQBotConfig(req dto.AgentQQBotConfigUpdateReq) error { - return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error { - setQQBotConfig(conf, dto.AgentQQBotConfig{ - Enabled: req.Enabled, - AppID: req.AppID, - ClientSecret: req.ClientSecret, - }) - return nil - }) -} - -func (a AgentService) GetWecomConfig(req dto.AgentWecomConfigReq) (*dto.AgentWecomConfig, error) { - _, install, conf, err := a.loadAgentConfig(req.AgentID) - if err != nil { - return nil, err - } - result := extractWecomConfig(conf) - installed, _ := checkPluginInstalled(install.ContainerName, "wecom") - result.Installed = installed - return &result, nil -} - -func (a AgentService) UpdateWecomConfig(req dto.AgentWecomConfigUpdateReq) error { - return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error { - setWecomConfig(conf, dto.AgentWecomConfig{ - Enabled: req.Enabled, - DmPolicy: req.DmPolicy, - BotID: req.BotID, - Secret: req.Secret, - }) - return nil - }) -} - -func (a AgentService) GetDingTalkConfig(req dto.AgentDingTalkConfigReq) (*dto.AgentDingTalkConfig, error) { - _, install, conf, err := a.loadAgentConfig(req.AgentID) - if err != nil { - return nil, err - } - result := extractDingTalkConfig(conf) - installed, _ := checkPluginInstalled(install.ContainerName, "dingtalk") - result.Installed = installed - return &result, nil -} - -func (a AgentService) UpdateDingTalkConfig(req dto.AgentDingTalkConfigUpdateReq) error { - return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error { - setDingTalkConfig(conf, dto.AgentDingTalkConfig{ - Enabled: req.Enabled, - ClientID: req.ClientID, - ClientSecret: req.ClientSecret, - DmPolicy: req.DmPolicy, - AllowFrom: req.AllowFrom, - GroupPolicy: req.GroupPolicy, - GroupAllowFrom: req.GroupAllowFrom, - }) - return nil - }) -} - -func (a AgentService) InstallPlugin(req dto.AgentPluginInstallReq) error { - _, install, err := a.loadAgentAndInstall(req.AgentID) - if err != nil { - return err - } - spec, _, err := resolvePluginMeta(req.Type) - if err != nil { - return err - } - installTask, err := task.NewTaskWithOps(req.Type, task.TaskInstall, task.TaskScopeAI, req.TaskID, req.AgentID) - if err != nil { - return err - } - installTask.AddSubTask("Install OpenClaw plugin", func(t *task.Task) error { - mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute)) - return mgr.RunBashCf("docker exec %s openclaw plugins install %s", install.ContainerName, spec) - }, nil) - go func() { - if err := installTask.Execute(); err != nil { - global.LOG.Errorf("install openclaw plugin failed: %v", err) - } - }() - return nil -} - -func (a AgentService) CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error) { - _, install, err := a.loadAgentAndInstall(req.AgentID) - if err != nil { - return nil, err - } - installed, err := checkPluginInstalled(install.ContainerName, req.Type) - if err != nil { - return nil, err - } - return &dto.AgentPluginStatus{Installed: installed}, nil -} - func (a AgentService) GetSecurityConfig(req dto.AgentSecurityConfigReq) (*dto.AgentSecurityConfig, error) { agent, _, err := a.loadAgentAndInstall(req.AgentID) if err != nil { @@ -871,23 +699,6 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error return nil } -func (a AgentService) ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error { - _, install, err := a.loadAgentAndInstall(req.AgentID) - if err != nil { - return err - } - channelType := req.Type - if err := cmd.RunDefaultBashCf( - "docker exec %s openclaw pairing approve %s %q", - install.ContainerName, - channelType, - strings.TrimSpace(req.PairingCode), - ); err != nil { - return err - } - return nil -} - func (a AgentService) loadAgentAndInstall(agentID uint) (*model.Agent, *model.AppInstall, error) { agent, err := agentRepo.GetFirst(repo.WithByID(agentID)) if err != nil { diff --git a/agent/app/service/agents_channels.go b/agent/app/service/agents_channels.go new file mode 100644 index 000000000..e11fa8086 --- /dev/null +++ b/agent/app/service/agents_channels.go @@ -0,0 +1,544 @@ +package service + +import ( + "fmt" + "path" + "strings" + "time" + + "github.com/1Panel-dev/1Panel/agent/app/dto" + "github.com/1Panel-dev/1Panel/agent/app/model" + "github.com/1Panel-dev/1Panel/agent/app/task" + "github.com/1Panel-dev/1Panel/agent/buserr" + "github.com/1Panel-dev/1Panel/agent/global" + "github.com/1Panel-dev/1Panel/agent/utils/cmd" +) + +func (a AgentService) GetFeishuConfig(req dto.AgentFeishuConfigReq) (*dto.AgentFeishuConfig, error) { + _, _, conf, err := a.loadAgentConfig(req.AgentID) + if err != nil { + return nil, err + } + result := extractFeishuConfig(conf) + return &result, nil +} + +func (a AgentService) UpdateFeishuConfig(req dto.AgentFeishuConfigUpdateReq) error { + return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error { + setFeishuConfig(conf, dto.AgentFeishuConfig{ + Enabled: req.Enabled, + DmPolicy: req.DmPolicy, + BotName: req.BotName, + AppID: req.AppID, + AppSecret: req.AppSecret, + }) + setFeishuPluginEnabled(conf, req.Enabled) + return nil + }) +} + +func (a AgentService) GetTelegramConfig(req dto.AgentTelegramConfigReq) (*dto.AgentTelegramConfig, error) { + _, _, conf, err := a.loadAgentConfig(req.AgentID) + if err != nil { + return nil, err + } + result := extractTelegramConfig(conf) + return &result, nil +} + +func (a AgentService) UpdateTelegramConfig(req dto.AgentTelegramConfigUpdateReq) error { + return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error { + setTelegramConfig(conf, dto.AgentTelegramConfig{ + Enabled: req.Enabled, + DmPolicy: req.DmPolicy, + BotToken: req.BotToken, + Proxy: req.Proxy, + }) + return nil + }) +} + +func (a AgentService) GetDiscordConfig(req dto.AgentDiscordConfigReq) (*dto.AgentDiscordConfig, error) { + _, _, conf, err := a.loadAgentConfig(req.AgentID) + if err != nil { + return nil, err + } + result := extractDiscordConfig(conf) + return &result, nil +} + +func (a AgentService) UpdateDiscordConfig(req dto.AgentDiscordConfigUpdateReq) error { + return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error { + setDiscordConfig(conf, dto.AgentDiscordConfig{ + Enabled: req.Enabled, + DmPolicy: req.DmPolicy, + GroupPolicy: req.GroupPolicy, + Token: req.Token, + Proxy: req.Proxy, + }) + return nil + }) +} + +func (a AgentService) GetQQBotConfig(req dto.AgentQQBotConfigReq) (*dto.AgentQQBotConfig, error) { + _, install, conf, err := a.loadAgentConfig(req.AgentID) + if err != nil { + return nil, err + } + result := extractQQBotConfig(conf) + installed, _ := checkPluginInstalled(install.ContainerName, "qqbot") + result.Installed = installed + return &result, nil +} + +func (a AgentService) UpdateQQBotConfig(req dto.AgentQQBotConfigUpdateReq) error { + return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error { + setQQBotConfig(conf, dto.AgentQQBotConfig{ + Enabled: req.Enabled, + AppID: req.AppID, + ClientSecret: req.ClientSecret, + }) + return nil + }) +} + +func (a AgentService) GetWecomConfig(req dto.AgentWecomConfigReq) (*dto.AgentWecomConfig, error) { + _, install, conf, err := a.loadAgentConfig(req.AgentID) + if err != nil { + return nil, err + } + result := extractWecomConfig(conf) + installed, _ := checkPluginInstalled(install.ContainerName, "wecom") + result.Installed = installed + return &result, nil +} + +func (a AgentService) UpdateWecomConfig(req dto.AgentWecomConfigUpdateReq) error { + return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error { + setWecomConfig(conf, dto.AgentWecomConfig{ + Enabled: req.Enabled, + DmPolicy: req.DmPolicy, + BotID: req.BotID, + Secret: req.Secret, + }) + return nil + }) +} + +func (a AgentService) GetDingTalkConfig(req dto.AgentDingTalkConfigReq) (*dto.AgentDingTalkConfig, error) { + _, install, conf, err := a.loadAgentConfig(req.AgentID) + if err != nil { + return nil, err + } + result := extractDingTalkConfig(conf) + installed, _ := checkPluginInstalled(install.ContainerName, "dingtalk") + result.Installed = installed + return &result, nil +} + +func (a AgentService) UpdateDingTalkConfig(req dto.AgentDingTalkConfigUpdateReq) error { + return a.mutateAgentConfig(req.AgentID, func(_ *model.Agent, _ *model.AppInstall, conf map[string]interface{}) error { + setDingTalkConfig(conf, dto.AgentDingTalkConfig{ + Enabled: req.Enabled, + ClientID: req.ClientID, + ClientSecret: req.ClientSecret, + DmPolicy: req.DmPolicy, + AllowFrom: req.AllowFrom, + GroupPolicy: req.GroupPolicy, + GroupAllowFrom: req.GroupAllowFrom, + }) + return nil + }) +} + +func (a AgentService) InstallPlugin(req dto.AgentPluginInstallReq) error { + _, install, err := a.loadAgentAndInstall(req.AgentID) + if err != nil { + return err + } + spec, _, err := resolvePluginMeta(req.Type) + if err != nil { + return err + } + installTask, err := task.NewTaskWithOps(req.Type, task.TaskInstall, task.TaskScopeAI, req.TaskID, req.AgentID) + if err != nil { + return err + } + installTask.AddSubTask("Install OpenClaw plugin", func(t *task.Task) error { + mgr := cmd.NewCommandMgr(cmd.WithTask(*t), cmd.WithContext(t.TaskCtx), cmd.WithTimeout(10*time.Minute)) + return mgr.RunBashCf("docker exec %s openclaw plugins install %s", install.ContainerName, spec) + }, nil) + go func() { + if err := installTask.Execute(); err != nil { + global.LOG.Errorf("install openclaw plugin failed: %v", err) + } + }() + return nil +} + +func (a AgentService) CheckPlugin(req dto.AgentPluginCheckReq) (*dto.AgentPluginStatus, error) { + _, install, err := a.loadAgentAndInstall(req.AgentID) + if err != nil { + return nil, err + } + installed, err := checkPluginInstalled(install.ContainerName, req.Type) + if err != nil { + return nil, err + } + return &dto.AgentPluginStatus{Installed: installed}, nil +} + +func (a AgentService) ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error { + _, install, err := a.loadAgentAndInstall(req.AgentID) + if err != nil { + return err + } + if err := cmd.RunDefaultBashCf( + "docker exec %s openclaw pairing approve %s %q", + install.ContainerName, + req.Type, + strings.TrimSpace(req.PairingCode), + ); err != nil { + return err + } + return nil +} + +func extractFeishuConfig(conf map[string]interface{}) dto.AgentFeishuConfig { + result := dto.AgentFeishuConfig{Enabled: true, DmPolicy: "pairing"} + channels, ok := conf["channels"].(map[string]interface{}) + if !ok { + return result + } + feishu, ok := channels["feishu"].(map[string]interface{}) + if !ok { + return result + } + if enabled, ok := feishu["enabled"].(bool); ok { + result.Enabled = enabled + } + if dmPolicy, ok := feishu["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" { + result.DmPolicy = dmPolicy + } + accounts, ok := feishu["accounts"].(map[string]interface{}) + if !ok { + return result + } + main, ok := accounts["main"].(map[string]interface{}) + if !ok { + return result + } + if appID, ok := main["appId"].(string); ok { + result.AppID = appID + } + if appSecret, ok := main["appSecret"].(string); ok { + result.AppSecret = appSecret + } + if botName, ok := main["botName"].(string); ok { + result.BotName = botName + } + return result +} + +func setFeishuConfig(conf map[string]interface{}, config dto.AgentFeishuConfig) { + channels := ensureChildMap(conf, "channels") + feishu := ensureChildMap(channels, "feishu") + feishu["enabled"] = config.Enabled + feishu["dmPolicy"] = config.DmPolicy + + accounts := ensureChildMap(feishu, "accounts") + main := ensureChildMap(accounts, "main") + main["appId"] = config.AppID + main["appSecret"] = config.AppSecret + main["botName"] = config.BotName + + if strings.EqualFold(config.DmPolicy, "open") { + feishu["allowFrom"] = []string{"*"} + } +} + +func setFeishuPluginEnabled(conf map[string]interface{}, enabled bool) { + plugins := ensureChildMap(conf, "plugins") + entries := ensureChildMap(plugins, "entries") + feishu := ensureChildMap(entries, "feishu") + feishu["enabled"] = enabled +} + +func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig { + result := dto.AgentTelegramConfig{Enabled: true, DmPolicy: "pairing"} + channels, ok := conf["channels"].(map[string]interface{}) + if !ok { + return result + } + telegram, ok := channels["telegram"].(map[string]interface{}) + if !ok { + return result + } + if enabled, ok := telegram["enabled"].(bool); ok { + result.Enabled = enabled + } + if dmPolicy, ok := telegram["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" { + result.DmPolicy = dmPolicy + } + if botToken, ok := telegram["botToken"].(string); ok { + result.BotToken = botToken + } + if proxy, ok := telegram["proxy"].(string); ok { + result.Proxy = proxy + } + return result +} + +func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConfig) { + channels := ensureChildMap(conf, "channels") + telegram := map[string]interface{}{ + "enabled": config.Enabled, + "dmPolicy": config.DmPolicy, + "botToken": config.BotToken, + } + if strings.EqualFold(config.DmPolicy, "open") { + telegram["allowFrom"] = []string{"*"} + } + if strings.TrimSpace(config.Proxy) != "" { + telegram["proxy"] = strings.TrimSpace(config.Proxy) + } + channels["telegram"] = telegram +} + +func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig { + result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", GroupPolicy: "open"} + channels, ok := conf["channels"].(map[string]interface{}) + if !ok { + return result + } + discord, ok := channels["discord"].(map[string]interface{}) + if !ok { + return result + } + if enabled, ok := discord["enabled"].(bool); ok { + result.Enabled = enabled + } + if token, ok := discord["token"].(string); ok { + result.Token = token + } + if groupPolicy, ok := discord["groupPolicy"].(string); ok && strings.TrimSpace(groupPolicy) != "" { + result.GroupPolicy = groupPolicy + } + if proxy, ok := discord["proxy"].(string); ok { + result.Proxy = proxy + } + if policy, ok := discord["dmPolicy"].(string); ok && strings.TrimSpace(policy) != "" { + result.DmPolicy = policy + return result + } + dm, ok := discord["dm"].(map[string]interface{}) + if ok { + if policy, ok := dm["policy"].(string); ok && strings.TrimSpace(policy) != "" { + result.DmPolicy = policy + } + } + return result +} + +func setDiscordConfig(conf map[string]interface{}, config dto.AgentDiscordConfig) { + channels := ensureChildMap(conf, "channels") + discord := ensureChildMap(channels, "discord") + discord["enabled"] = config.Enabled + discord["token"] = config.Token + discord["dmPolicy"] = config.DmPolicy + discord["groupPolicy"] = config.GroupPolicy + if strings.EqualFold(config.DmPolicy, "open") { + discord["allowFrom"] = []string{"*"} + } else { + delete(discord, "allowFrom") + } + if strings.TrimSpace(config.Proxy) != "" { + discord["proxy"] = strings.TrimSpace(config.Proxy) + } else { + delete(discord, "proxy") + } + delete(discord, "dm") +} + +func extractQQBotConfig(conf map[string]interface{}) dto.AgentQQBotConfig { + result := dto.AgentQQBotConfig{Enabled: true} + channels, ok := conf["channels"].(map[string]interface{}) + if !ok { + return result + } + qqbot, ok := channels["qqbot"].(map[string]interface{}) + if !ok { + return result + } + if enabled, ok := qqbot["enabled"].(bool); ok { + result.Enabled = enabled + } + if appID, ok := qqbot["appId"].(string); ok { + result.AppID = appID + } + if clientSecret, ok := qqbot["clientSecret"].(string); ok { + result.ClientSecret = clientSecret + } + return result +} + +func extractWecomConfig(conf map[string]interface{}) dto.AgentWecomConfig { + result := dto.AgentWecomConfig{Enabled: true, DmPolicy: "pairing"} + channels, ok := conf["channels"].(map[string]interface{}) + if !ok { + return result + } + wecom, ok := channels["wecom"].(map[string]interface{}) + if !ok { + return result + } + if enabled, ok := wecom["enabled"].(bool); ok { + result.Enabled = enabled + } + if dmPolicy, ok := wecom["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" { + result.DmPolicy = strings.TrimSpace(dmPolicy) + } + if botID, ok := wecom["botId"].(string); ok { + result.BotID = botID + } + if secret, ok := wecom["secret"].(string); ok { + result.Secret = secret + } + return result +} + +func extractDingTalkConfig(conf map[string]interface{}) dto.AgentDingTalkConfig { + result := dto.AgentDingTalkConfig{ + Enabled: true, + DmPolicy: "pairing", + GroupPolicy: "disabled", + AllowFrom: []string{}, + GroupAllowFrom: []string{}, + } + channels, ok := conf["channels"].(map[string]interface{}) + if !ok { + return result + } + dingtalk, ok := channels["dingtalk-connector"].(map[string]interface{}) + if !ok { + return result + } + if enabled, ok := dingtalk["enabled"].(bool); ok { + result.Enabled = enabled + } + if clientID, ok := dingtalk["clientId"].(string); ok { + result.ClientID = clientID + } + if clientSecret, ok := dingtalk["clientSecret"].(string); ok { + result.ClientSecret = clientSecret + } + if dmPolicy, ok := dingtalk["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" { + result.DmPolicy = dmPolicy + } + if groupPolicy, ok := dingtalk["groupPolicy"].(string); ok && strings.TrimSpace(groupPolicy) != "" { + result.GroupPolicy = groupPolicy + } + result.AllowFrom = extractStringList(dingtalk["allowFrom"]) + result.GroupAllowFrom = extractStringList(dingtalk["groupAllowFrom"]) + return result +} + +func setWecomConfig(conf map[string]interface{}, config dto.AgentWecomConfig) { + channels := ensureChildMap(conf, "channels") + wecom := ensureChildMap(channels, "wecom") + wecom["enabled"] = config.Enabled + wecom["botId"] = strings.TrimSpace(config.BotID) + wecom["secret"] = strings.TrimSpace(config.Secret) + wecom["dmPolicy"] = strings.TrimSpace(config.DmPolicy) + if strings.EqualFold(config.DmPolicy, "open") { + wecom["allowFrom"] = []string{"*"} + } else { + wecom["allowFrom"] = []string{} + } + + plugins := ensureChildMap(conf, "plugins") + entries := ensureChildMap(plugins, "entries") + wecomEntry := ensureChildMap(entries, "wecom-openclaw-plugin") + wecomEntry["enabled"] = config.Enabled +} + +func setDingTalkConfig(conf map[string]interface{}, config dto.AgentDingTalkConfig) { + channels := ensureChildMap(conf, "channels") + dingtalk := ensureChildMap(channels, "dingtalk-connector") + dingtalk["enabled"] = config.Enabled + dingtalk["clientId"] = strings.TrimSpace(config.ClientID) + dingtalk["clientSecret"] = strings.TrimSpace(config.ClientSecret) + dingtalk["dmPolicy"] = config.DmPolicy + dingtalk["groupPolicy"] = config.GroupPolicy + dingtalk["gatewayToken"] = extractGatewayToken(conf) + switch config.DmPolicy { + case "open": + dingtalk["allowFrom"] = []string{"*"} + case "allowlist": + dingtalk["allowFrom"] = append([]string(nil), config.AllowFrom...) + default: + delete(dingtalk, "allowFrom") + } + switch config.GroupPolicy { + case "open": + dingtalk["groupAllowFrom"] = []string{"*"} + case "allowlist": + dingtalk["groupAllowFrom"] = append([]string(nil), config.GroupAllowFrom...) + default: + delete(dingtalk, "groupAllowFrom") + } + + plugins := ensureChildMap(conf, "plugins") + entries := ensureChildMap(plugins, "entries") + dingtalkEntry := ensureChildMap(entries, "dingtalk-connector") + dingtalkEntry["enabled"] = config.Enabled + + gateway := ensureChildMap(conf, "gateway") + httpMap := ensureChildMap(gateway, "http") + endpoints := ensureChildMap(httpMap, "endpoints") + chatCompletions := ensureChildMap(endpoints, "chatCompletions") + chatCompletions["enabled"] = true +} + +func setQQBotConfig(conf map[string]interface{}, config dto.AgentQQBotConfig) { + channels := ensureChildMap(conf, "channels") + qqbot := ensureChildMap(channels, "qqbot") + qqbot["enabled"] = config.Enabled + qqbot["allowFrom"] = []string{"*"} + qqbot["appId"] = strings.TrimSpace(config.AppID) + qqbot["clientSecret"] = strings.TrimSpace(config.ClientSecret) + + plugins := ensureChildMap(conf, "plugins") + entries := ensureChildMap(plugins, "entries") + qqbotEntry := ensureChildMap(entries, "qqbot") + qqbotEntry["enabled"] = config.Enabled +} + +func resolvePluginMeta(pluginType string) (string, string, error) { + switch pluginType { + case "qqbot": + return "@sliverp/qqbot@latest", "qqbot", nil + case "wecom": + return "@wecom/wecom-openclaw-plugin", "wecom-openclaw-plugin", nil + case "dingtalk": + return "@dingtalk-real-ai/dingtalk-connector", "dingtalk-connector", nil + default: + return "", "", fmt.Errorf("unsupported plugin type") + } +} + +func checkPluginInstalled(containerName, pluginType string) (bool, error) { + _, pluginDir, err := resolvePluginMeta(pluginType) + if err != nil { + return false, err + } + if strings.TrimSpace(containerName) == "" { + return false, buserr.New("ErrRecordNotFound") + } + pluginPath := path.Join(openclawPluginBaseDir, pluginDir) + mgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second)) + if err := mgr.RunBashCf("docker exec %s test -d %s", containerName, pluginPath); err != nil { + return false, nil + } + return true, nil +} diff --git a/agent/app/service/agents_utils.go b/agent/app/service/agents_utils.go index f85edc9a5..080ad90f2 100644 --- a/agent/app/service/agents_utils.go +++ b/agent/app/service/agents_utils.go @@ -19,7 +19,6 @@ import ( "github.com/1Panel-dev/1Panel/agent/buserr" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" - "github.com/1Panel-dev/1Panel/agent/utils/cmd" "github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/files" openclawutil "github.com/1Panel-dev/1Panel/agent/utils/openclaw" @@ -33,100 +32,32 @@ func NewIAgentService() IAgentService { return &AgentService{} } -type resolvedAgentAccountVerification struct { - Provider string - APIKey string - BaseURL string -} - type resolvedAgentAccountInput struct { Provider string APIKey string BaseURL string - APIType string } -func resolveAgentAccountVerification(provider, apiKey, baseURL string) (resolvedAgentAccountVerification, error) { +func resolveAgentAccountInput(provider, apiKey, baseURL string) (resolvedAgentAccountInput, error) { resolvedAPIKey := strings.TrimSpace(apiKey) - if resolvedAPIKey == "" { - return resolvedAgentAccountVerification{}, buserr.New("ErrAgentApiKeyRequired") - } resolvedBaseURL := strings.TrimSpace(baseURL) - if fixedURL, ok := fixedProviderBaseURL(provider); ok { - resolvedBaseURL = fixedURL - } - if (provider == "custom" || provider == "vllm") && resolvedBaseURL == "" { - return resolvedAgentAccountVerification{}, buserr.New("ErrAgentBaseURLRequired") - } - if provider != "custom" && provider != "vllm" && resolvedBaseURL == "" { - if defaultURL, ok := providerDefaultBaseURL(provider); ok { + if resolvedBaseURL == "" { + if requiresInitialAgentAccountModels(provider) { + return resolvedAgentAccountInput{}, buserr.New("ErrAgentBaseURLRequired") + } + if defaultURL, ok := providercatalog.DefaultBaseURL(provider); ok { resolvedBaseURL = defaultURL } } - if provider == "ollama" && resolvedBaseURL == "" { - return resolvedAgentAccountVerification{}, buserr.New("ErrAgentBaseURLRequired") - } - return resolvedAgentAccountVerification{ - Provider: provider, - APIKey: resolvedAPIKey, - BaseURL: resolvedBaseURL, - }, nil -} - -func verifyResolvedAgentAccount(input resolvedAgentAccountVerification) error { - if providercatalog.SkipVerification(input.Provider) { - return nil - } - return providercatalog.VerifyAccount(input.Provider, input.BaseURL, input.APIKey) -} - -func resolveAgentAccountAPIType(provider, apiType, fallbackAPIType string) (string, error) { - if provider == "minimax" || provider == "xiaomi" { - return "anthropic-messages", nil - } - resolvedAPIType := normalizeAPIType(apiType) - if provider == "custom" || provider == "vllm" { - if !isSupportedAPIType(resolvedAPIType) { - return "", fmt.Errorf("apiType is invalid") + if !providercatalog.SkipVerification(provider) { + if err := providercatalog.VerifyAccount(provider, resolvedBaseURL, resolvedAPIKey); err != nil { + return resolvedAgentAccountInput{}, err } - return resolvedAPIType, nil - } - if provider == "ollama" { - if apiType == "" && fallbackAPIType != "" { - resolvedAPIType = normalizeAPIType(fallbackAPIType) - if !isSupportedOllamaAPIType(resolvedAPIType) { - resolvedAPIType = "openai-responses" - } - return resolvedAPIType, nil - } - if !isSupportedOllamaAPIType(resolvedAPIType) { - return "", fmt.Errorf("apiType is invalid") - } - return resolvedAPIType, nil - } - if fallbackAPIType != "" { - return normalizeAPIType(fallbackAPIType), nil - } - return resolvedAPIType, nil -} - -func resolveAgentAccountInput(provider, apiKey, baseURL, apiType, fallbackAPIType string) (resolvedAgentAccountInput, error) { - resolvedVerification, err := resolveAgentAccountVerification(provider, apiKey, baseURL) - if err != nil { - return resolvedAgentAccountInput{}, err - } - if err := verifyResolvedAgentAccount(resolvedVerification); err != nil { - return resolvedAgentAccountInput{}, err - } - resolvedAPIType, err := resolveAgentAccountAPIType(provider, apiType, fallbackAPIType) - if err != nil { - return resolvedAgentAccountInput{}, err } return resolvedAgentAccountInput{ Provider: provider, - APIKey: resolvedVerification.APIKey, - BaseURL: resolvedVerification.BaseURL, - APIType: resolvedAPIType, + APIKey: resolvedAPIKey, + BaseURL: resolvedBaseURL, }, nil } @@ -300,163 +231,6 @@ func setTrustedProxies(gateway map[string]interface{}) { gateway["trustedProxies"] = proxies } -func extractFeishuConfig(conf map[string]interface{}) dto.AgentFeishuConfig { - result := dto.AgentFeishuConfig{Enabled: true, DmPolicy: "pairing"} - channels, ok := conf["channels"].(map[string]interface{}) - if !ok { - return result - } - feishu, ok := channels["feishu"].(map[string]interface{}) - if !ok { - return result - } - if enabled, ok := feishu["enabled"].(bool); ok { - result.Enabled = enabled - } - if dmPolicy, ok := feishu["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" { - result.DmPolicy = dmPolicy - } - accounts, ok := feishu["accounts"].(map[string]interface{}) - if !ok { - return result - } - main, ok := accounts["main"].(map[string]interface{}) - if !ok { - return result - } - if appID, ok := main["appId"].(string); ok { - result.AppID = appID - } - if appSecret, ok := main["appSecret"].(string); ok { - result.AppSecret = appSecret - } - if botName, ok := main["botName"].(string); ok { - result.BotName = botName - } - return result -} - -func setFeishuConfig(conf map[string]interface{}, config dto.AgentFeishuConfig) { - channels := ensureChildMap(conf, "channels") - feishu := ensureChildMap(channels, "feishu") - feishu["enabled"] = config.Enabled - feishu["dmPolicy"] = config.DmPolicy - - accounts := ensureChildMap(feishu, "accounts") - main := ensureChildMap(accounts, "main") - main["appId"] = config.AppID - main["appSecret"] = config.AppSecret - main["botName"] = config.BotName - - if strings.EqualFold(config.DmPolicy, "open") { - feishu["allowFrom"] = []string{"*"} - } -} - -func setFeishuPluginEnabled(conf map[string]interface{}, enabled bool) { - plugins := ensureChildMap(conf, "plugins") - entries := ensureChildMap(plugins, "entries") - feishu := ensureChildMap(entries, "feishu") - feishu["enabled"] = enabled -} - -func extractTelegramConfig(conf map[string]interface{}) dto.AgentTelegramConfig { - result := dto.AgentTelegramConfig{Enabled: true, DmPolicy: "pairing"} - channels, ok := conf["channels"].(map[string]interface{}) - if !ok { - return result - } - telegram, ok := channels["telegram"].(map[string]interface{}) - if !ok { - return result - } - if enabled, ok := telegram["enabled"].(bool); ok { - result.Enabled = enabled - } - if dmPolicy, ok := telegram["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" { - result.DmPolicy = dmPolicy - } - if botToken, ok := telegram["botToken"].(string); ok { - result.BotToken = botToken - } - if proxy, ok := telegram["proxy"].(string); ok { - result.Proxy = proxy - } - return result -} - -func setTelegramConfig(conf map[string]interface{}, config dto.AgentTelegramConfig) { - channels := ensureChildMap(conf, "channels") - telegram := map[string]interface{}{ - "enabled": config.Enabled, - "dmPolicy": config.DmPolicy, - "botToken": config.BotToken, - } - if strings.EqualFold(config.DmPolicy, "open") { - telegram["allowFrom"] = []string{"*"} - } - if strings.TrimSpace(config.Proxy) != "" { - telegram["proxy"] = strings.TrimSpace(config.Proxy) - } - channels["telegram"] = telegram -} - -func extractDiscordConfig(conf map[string]interface{}) dto.AgentDiscordConfig { - result := dto.AgentDiscordConfig{Enabled: true, DmPolicy: "pairing", GroupPolicy: "open"} - channels, ok := conf["channels"].(map[string]interface{}) - if !ok { - return result - } - discord, ok := channels["discord"].(map[string]interface{}) - if !ok { - return result - } - if enabled, ok := discord["enabled"].(bool); ok { - result.Enabled = enabled - } - if token, ok := discord["token"].(string); ok { - result.Token = token - } - if groupPolicy, ok := discord["groupPolicy"].(string); ok && strings.TrimSpace(groupPolicy) != "" { - result.GroupPolicy = groupPolicy - } - if proxy, ok := discord["proxy"].(string); ok { - result.Proxy = proxy - } - if policy, ok := discord["dmPolicy"].(string); ok && strings.TrimSpace(policy) != "" { - result.DmPolicy = policy - return result - } - // backward compatibility: old nested style - dm, ok := discord["dm"].(map[string]interface{}) - if ok { - if policy, ok := dm["policy"].(string); ok && strings.TrimSpace(policy) != "" { - result.DmPolicy = policy - } - } - return result -} - -func setDiscordConfig(conf map[string]interface{}, config dto.AgentDiscordConfig) { - channels := ensureChildMap(conf, "channels") - discord := ensureChildMap(channels, "discord") - discord["enabled"] = config.Enabled - discord["token"] = config.Token - discord["dmPolicy"] = config.DmPolicy - discord["groupPolicy"] = config.GroupPolicy - if strings.EqualFold(config.DmPolicy, "open") { - discord["allowFrom"] = []string{"*"} - } else { - delete(discord, "allowFrom") - } - if strings.TrimSpace(config.Proxy) != "" { - discord["proxy"] = strings.TrimSpace(config.Proxy) - } else { - delete(discord, "proxy") - } - delete(discord, "dm") -} - func extractBrowserConfig(conf map[string]interface{}) browserConfig { result := browserConfig{ Enabled: true, @@ -500,189 +274,6 @@ func setBrowserConfig(conf map[string]interface{}, config browserConfig) { } } -func extractQQBotConfig(conf map[string]interface{}) dto.AgentQQBotConfig { - result := dto.AgentQQBotConfig{Enabled: true} - channels, ok := conf["channels"].(map[string]interface{}) - if !ok { - return result - } - qqbot, ok := channels["qqbot"].(map[string]interface{}) - if !ok { - return result - } - if enabled, ok := qqbot["enabled"].(bool); ok { - result.Enabled = enabled - } - if appID, ok := qqbot["appId"].(string); ok { - result.AppID = appID - } - if clientSecret, ok := qqbot["clientSecret"].(string); ok { - result.ClientSecret = clientSecret - } - return result -} - -func extractWecomConfig(conf map[string]interface{}) dto.AgentWecomConfig { - result := dto.AgentWecomConfig{Enabled: true, DmPolicy: "pairing"} - channels, ok := conf["channels"].(map[string]interface{}) - if !ok { - return result - } - wecom, ok := channels["wecom"].(map[string]interface{}) - if !ok { - return result - } - if enabled, ok := wecom["enabled"].(bool); ok { - result.Enabled = enabled - } - if dmPolicy, ok := wecom["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" { - result.DmPolicy = strings.TrimSpace(dmPolicy) - } - if botID, ok := wecom["botId"].(string); ok { - result.BotID = botID - } - if secret, ok := wecom["secret"].(string); ok { - result.Secret = secret - } - return result -} - -func extractDingTalkConfig(conf map[string]interface{}) dto.AgentDingTalkConfig { - result := dto.AgentDingTalkConfig{ - Enabled: true, - DmPolicy: "pairing", - GroupPolicy: "disabled", - AllowFrom: []string{}, - GroupAllowFrom: []string{}, - } - channels, ok := conf["channels"].(map[string]interface{}) - if !ok { - return result - } - dingtalk, ok := channels["dingtalk-connector"].(map[string]interface{}) - if !ok { - return result - } - if enabled, ok := dingtalk["enabled"].(bool); ok { - result.Enabled = enabled - } - if clientID, ok := dingtalk["clientId"].(string); ok { - result.ClientID = clientID - } - if clientSecret, ok := dingtalk["clientSecret"].(string); ok { - result.ClientSecret = clientSecret - } - if dmPolicy, ok := dingtalk["dmPolicy"].(string); ok && strings.TrimSpace(dmPolicy) != "" { - result.DmPolicy = dmPolicy - } - if groupPolicy, ok := dingtalk["groupPolicy"].(string); ok && strings.TrimSpace(groupPolicy) != "" { - result.GroupPolicy = groupPolicy - } - result.AllowFrom = extractStringList(dingtalk["allowFrom"]) - result.GroupAllowFrom = extractStringList(dingtalk["groupAllowFrom"]) - return result -} - -func setWecomConfig(conf map[string]interface{}, config dto.AgentWecomConfig) { - channels := ensureChildMap(conf, "channels") - wecom := ensureChildMap(channels, "wecom") - wecom["enabled"] = config.Enabled - wecom["botId"] = strings.TrimSpace(config.BotID) - wecom["secret"] = strings.TrimSpace(config.Secret) - wecom["dmPolicy"] = strings.TrimSpace(config.DmPolicy) - if strings.EqualFold(config.DmPolicy, "open") { - wecom["allowFrom"] = []string{"*"} - } else { - wecom["allowFrom"] = []string{} - } - - plugins := ensureChildMap(conf, "plugins") - entries := ensureChildMap(plugins, "entries") - wecomEntry := ensureChildMap(entries, "wecom-openclaw-plugin") - wecomEntry["enabled"] = config.Enabled -} - -func setDingTalkConfig(conf map[string]interface{}, config dto.AgentDingTalkConfig) { - channels := ensureChildMap(conf, "channels") - dingtalk := ensureChildMap(channels, "dingtalk-connector") - dingtalk["enabled"] = config.Enabled - dingtalk["clientId"] = strings.TrimSpace(config.ClientID) - dingtalk["clientSecret"] = strings.TrimSpace(config.ClientSecret) - dingtalk["dmPolicy"] = config.DmPolicy - dingtalk["groupPolicy"] = config.GroupPolicy - dingtalk["gatewayToken"] = extractGatewayToken(conf) - switch config.DmPolicy { - case "open": - dingtalk["allowFrom"] = []string{"*"} - case "allowlist": - dingtalk["allowFrom"] = append([]string(nil), config.AllowFrom...) - default: - delete(dingtalk, "allowFrom") - } - switch config.GroupPolicy { - case "open": - dingtalk["groupAllowFrom"] = []string{"*"} - case "allowlist": - dingtalk["groupAllowFrom"] = append([]string(nil), config.GroupAllowFrom...) - default: - delete(dingtalk, "groupAllowFrom") - } - - plugins := ensureChildMap(conf, "plugins") - entries := ensureChildMap(plugins, "entries") - dingtalkEntry := ensureChildMap(entries, "dingtalk-connector") - dingtalkEntry["enabled"] = config.Enabled - - gateway := ensureChildMap(conf, "gateway") - httpMap := ensureChildMap(gateway, "http") - endpoints := ensureChildMap(httpMap, "endpoints") - chatCompletions := ensureChildMap(endpoints, "chatCompletions") - chatCompletions["enabled"] = true -} - -func setQQBotConfig(conf map[string]interface{}, config dto.AgentQQBotConfig) { - channels := ensureChildMap(conf, "channels") - qqbot := ensureChildMap(channels, "qqbot") - qqbot["enabled"] = config.Enabled - qqbot["allowFrom"] = []string{"*"} - qqbot["appId"] = strings.TrimSpace(config.AppID) - qqbot["clientSecret"] = strings.TrimSpace(config.ClientSecret) - - plugins := ensureChildMap(conf, "plugins") - entries := ensureChildMap(plugins, "entries") - qqbotEntry := ensureChildMap(entries, "qqbot") - qqbotEntry["enabled"] = config.Enabled -} - -func resolvePluginMeta(pluginType string) (string, string, error) { - switch pluginType { - case "qqbot": - return "@sliverp/qqbot@latest", "qqbot", nil - case "wecom": - return "@wecom/wecom-openclaw-plugin", "wecom-openclaw-plugin", nil - case "dingtalk": - return "@dingtalk-real-ai/dingtalk-connector", "dingtalk-connector", nil - default: - return "", "", fmt.Errorf("unsupported plugin type") - } -} - -func checkPluginInstalled(containerName, pluginType string) (bool, error) { - _, pluginDir, err := resolvePluginMeta(pluginType) - if err != nil { - return false, err - } - if strings.TrimSpace(containerName) == "" { - return false, buserr.New("ErrRecordNotFound") - } - pluginPath := path.Join(openclawPluginBaseDir, pluginDir) - mgr := cmd.NewCommandMgr(cmd.WithTimeout(20 * time.Second)) - if err := mgr.RunBashCf("docker exec %s test -d %s", containerName, pluginPath); err != nil { - return false, nil - } - return true, nil -} - func extractOtherConfig(conf map[string]interface{}) dto.AgentOtherConfig { result := dto.AgentOtherConfig{UserTimezone: resolveServerTimezone(), BrowserEnabled: true} agents, ok := conf["agents"].(map[string]interface{}) @@ -732,7 +323,7 @@ func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map Name: agent.Name, AgentType: agentType, Provider: agent.Provider, - ProviderName: providerDisplayName(agent.Provider), + ProviderName: providercatalog.DisplayName(agent.Provider), Model: agent.Model, APIType: agent.APIType, MaxTokens: agent.MaxTokens, @@ -1174,7 +765,7 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, } envPath := path.Join(confDir, ".env") lines := []string{fmt.Sprintf("OPENCLAW_GATEWAY_TOKEN=%s", token)} - if envKey := providerEnvKey(account.Provider); envKey != "" && strings.TrimSpace(account.APIKey) != "" { + if envKey := providercatalog.EnvKey(account.Provider); envKey != "" && strings.TrimSpace(account.APIKey) != "" { lines = append(lines, fmt.Sprintf("%s=%s", envKey, account.APIKey)) } content := strings.Join(lines, "\n") + "\n" @@ -1250,7 +841,7 @@ func buildOpenclawModelsFromAccount(account *model.AgentAccount, selectedModel s } func buildOpenclawCatalogModel(account *model.AgentAccount, model dto.AgentAccountModel) (string, modelEntry, string, modelProvider, error) { - primaryModel, inferredEntry, providerKey, providerCfg, err := inferOpenclawCatalogModel(account, model.ID, model.MaxTokens, model.ContextWindow) + primaryModel, inferredEntry, providerKey, providerCfg, err := inferOpenclawCatalogModel(account, model.ID, model.Reasoning, model.MaxTokens, model.ContextWindow) if err != nil { return "", modelEntry{}, "", modelProvider{}, err } @@ -1279,7 +870,7 @@ type openclawAccountModelRuntime struct { } func buildOpenclawAccountModelRuntime(account *model.AgentAccount, model dto.AgentAccountModel) (openclawAccountModelRuntime, error) { - apiType, maxTokens, contextWindow := resolveRuntimeParams( + apiType, maxTokens, contextWindow := providercatalog.ResolveRuntimeParams( account.Provider, account.APIType, model.MaxTokens, @@ -1310,10 +901,10 @@ func resolveOpenclawAccountModelRuntimeByID(account *model.AgentAccount, modelID return buildOpenclawAccountModelRuntime(account, selectedAccountModel) } -func inferOpenclawCatalogModel(account *model.AgentAccount, modelID string, maxTokens, contextWindow int) (string, modelEntry, string, modelProvider, error) { +func inferOpenclawCatalogModel(account *model.AgentAccount, modelID string, reasoning bool, maxTokens, contextWindow int) (string, modelEntry, string, modelProvider, error) { baseURL := resolveAccountBaseURL(account) - resolvedAPIType, resolvedMaxTokens, resolvedContextWindow := resolveRuntimeParams(account.Provider, account.APIType, maxTokens, contextWindow) - patch, err := providercatalog.BuildOpenClawPatch(account.Provider, modelID, resolvedAPIType, resolvedMaxTokens, resolvedContextWindow, baseURL, account.APIKey) + resolvedAPIType, resolvedMaxTokens, resolvedContextWindow := providercatalog.ResolveRuntimeParams(account.Provider, account.APIType, maxTokens, contextWindow) + patch, err := providercatalog.BuildOpenClawPatch(account.Provider, modelID, resolvedAPIType, reasoning, resolvedMaxTokens, resolvedContextWindow, baseURL, account.APIKey) if err != nil { return "", modelEntry{}, "", modelProvider{}, err } @@ -1341,7 +932,7 @@ func inferOpenclawCatalogModel(account *model.AgentAccount, modelID string, maxT func resolveAccountBaseURL(account *model.AgentAccount) string { baseURL := strings.TrimSpace(account.BaseURL) if baseURL == "" { - if defaultURL, ok := providerDefaultBaseURL(account.Provider); ok { + if defaultURL, ok := providercatalog.DefaultBaseURL(account.Provider); ok { baseURL = defaultURL } } @@ -1356,9 +947,9 @@ func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto return nil, buserr.New("ErrAgentAccountSingleInitialModel") } if len(requested) > 0 { - models, _, err := normalizeAgentAccountModels(account, requested, "", true) - if err != nil { - return nil, err + models := make([]dto.AgentAccountModel, 0, len(requested)) + for _, item := range requested { + models = append(models, cloneAgentAccountModel(item)) } return models, nil } @@ -1380,11 +971,7 @@ func buildInitialAgentAccountModels(account *model.AgentAccount, requested []dto Input: append([]string(nil), item.Input...), }) } - models, _, err := normalizeAgentAccountModels(account, requested, "", true) - if err != nil { - return nil, err - } - return models, nil + return requested, nil } func compactPersistedAgentAccountModelSortOrder(accountID uint) error { @@ -1412,6 +999,18 @@ func loadAgentAccountModels(account *model.AgentAccount) ([]dto.AgentAccountMode return listPersistedAgentAccountModels(account.ID) } +func cloneAgentAccountModel(model dto.AgentAccountModel) dto.AgentAccountModel { + return dto.AgentAccountModel{ + RecordID: model.RecordID, + ID: model.ID, + Name: model.Name, + ContextWindow: model.ContextWindow, + MaxTokens: model.MaxTokens, + Reasoning: model.Reasoning, + Input: append([]string(nil), model.Input...), + } +} + func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, existing []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) { if account == nil { return nil, fmt.Errorf("account is required") @@ -1423,13 +1022,15 @@ func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, exi requested := append([]dto.AgentAccountModel(nil), existing...) seen := make(map[string]struct{}, len(existing)) for _, item := range existing { - if strings.TrimSpace(item.ID) == "" { + target := strings.TrimSpace(item.ID) + if target == "" { continue } - seen[strings.TrimSpace(item.ID)] = struct{}{} + seen[target] = struct{}{} } for _, item := range meta.Models { - if _, ok := seen[strings.TrimSpace(item.ID)]; ok { + target := strings.TrimSpace(item.ID) + if _, ok := seen[target]; ok { continue } requested = append(requested, dto.AgentAccountModel{ @@ -1444,11 +1045,7 @@ func MergeCatalogAgentAccountModelsForMigration(account *model.AgentAccount, exi if len(requested) == len(existing) { return append([]dto.AgentAccountModel(nil), existing...), nil } - normalized, _, err := normalizeAgentAccountModels(account, requested, "", true) - if err != nil { - return nil, err - } - return normalized, nil + return normalizeAgentAccountModels(account, requested) } func listPersistedAgentAccountModels(accountID uint) ([]dto.AgentAccountModel, error) { @@ -1510,21 +1107,17 @@ func replacePersistedAgentAccountModels(accountID uint, models []dto.AgentAccoun }) } -func normalizeAgentAccountModels(account *model.AgentAccount, models []dto.AgentAccountModel, defaultModel string, allowFallbackDefault bool) ([]dto.AgentAccountModel, string, error) { +func normalizeAgentAccountModels(account *model.AgentAccount, models []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) { requested := append([]dto.AgentAccountModel(nil), models...) if len(requested) == 0 { - if strings.TrimSpace(defaultModel) != "" { - requested = []dto.AgentAccountModel{{ID: defaultModel}} - } else { - requested = buildLegacyAgentAccountModels(account) - } + return nil, fmt.Errorf("model is required") } normalized := make([]dto.AgentAccountModel, 0, len(requested)) seen := make(map[string]struct{}, len(requested)) for _, item := range requested { normalizedItem, err := normalizeAgentAccountModel(account, item) if err != nil { - return nil, "", err + return nil, err } if strings.TrimSpace(normalizedItem.ID) == "" { continue @@ -1536,27 +1129,9 @@ func normalizeAgentAccountModels(account *model.AgentAccount, models []dto.Agent normalized = append(normalized, normalizedItem) } if len(normalized) == 0 { - return nil, "", fmt.Errorf("model is required") + return nil, fmt.Errorf("model is required") } - - resolvedDefault := strings.TrimSpace(defaultModel) - if resolvedDefault != "" { - defaultItem, err := normalizeAgentAccountModel(account, dto.AgentAccountModel{ID: resolvedDefault}) - if err == nil { - resolvedDefault = defaultItem.ID - } - } - if resolvedDefault == "" && allowFallbackDefault { - resolvedDefault = normalized[0].ID - } - if _, ok := findAgentAccountModelForProvider(account.Provider, normalized, resolvedDefault); !ok { - if allowFallbackDefault { - resolvedDefault = normalized[0].ID - } else { - return nil, "", buserr.New("ErrAgentModelNotInAccount") - } - } - return normalized, resolvedDefault, nil + return normalized, nil } func normalizeAgentAccountModel(account *model.AgentAccount, model dto.AgentAccountModel) (dto.AgentAccountModel, error) { @@ -1564,7 +1139,13 @@ func normalizeAgentAccountModel(account *model.AgentAccount, model dto.AgentAcco if modelID == "" { return dto.AgentAccountModel{}, fmt.Errorf("model is required") } - primaryModel, inferredEntry, _, _, err := inferOpenclawCatalogModel(account, modelID, model.MaxTokens, model.ContextWindow) + inferredReasoning := model.Reasoning + if !model.Reasoning && model.Name == "" && model.MaxTokens == 0 && model.ContextWindow == 0 && len(model.Input) == 0 { + if catalogModel, ok := providercatalog.FindModel(account.Provider, modelID); ok { + inferredReasoning = catalogModel.Reasoning + } + } + primaryModel, inferredEntry, _, _, err := inferOpenclawCatalogModel(account, modelID, inferredReasoning, model.MaxTokens, model.ContextWindow) if err != nil { return dto.AgentAccountModel{}, err } @@ -1657,39 +1238,6 @@ func normalizeAgentAccountModelID(provider, primaryModel, requestedID string) st } } -func buildLegacyAgentAccountModels(account *model.AgentAccount) []dto.AgentAccountModel { - modelIDs := make([]string, 0, 4) - seen := make(map[string]struct{}, 4) - appendModel := func(value string) { - target := strings.TrimSpace(value) - if target == "" { - return - } - if _, ok := seen[target]; ok { - return - } - seen[target] = struct{}{} - modelIDs = append(modelIDs, target) - } - if account.ID > 0 { - if agents, err := agentRepo.List(repo.WithByAccountID(account.ID)); err == nil { - for _, agent := range agents { - appendModel(agent.Model) - } - } - } - if definitions, ok := providerDefinitions()[account.Provider]; ok && len(definitions.Models) > 0 { - for _, item := range definitions.Models { - appendModel(item.ID) - } - } - models := make([]dto.AgentAccountModel, 0, len(modelIDs)) - for _, modelID := range modelIDs { - models = append(models, dto.AgentAccountModel{ID: modelID}) - } - return models -} - func sanitizeAgentAccountModelInputs(values []string) []string { result := make([]string, 0, len(values)) seen := make(map[string]struct{}, len(values)) @@ -1872,64 +1420,6 @@ func mapToModelsConfig(value map[string]interface{}) (*modelsConfig, error) { return result, nil } -func providerEnvKey(provider string) string { - return providercatalog.EnvKey(provider) -} - -type providerDefinition struct { - Sort uint - DisplayName string - BaseURL string - Models []dto.ProviderModelInfo -} - -func providerDefinitions() map[string]providerDefinition { - definitions := map[string]providerDefinition{} - for key, meta := range providercatalog.All() { - models := make([]dto.ProviderModelInfo, 0, len(meta.Models)) - for _, m := range meta.Models { - models = append(models, dto.ProviderModelInfo{ - ID: m.ID, - Name: m.Name, - ContextWindow: m.ContextWindow, - MaxTokens: m.MaxTokens, - Reasoning: m.Reasoning, - Input: append([]string(nil), m.Input...), - }) - } - definitions[key] = providerDefinition{ - Sort: meta.Sort, - DisplayName: meta.DisplayName, - BaseURL: meta.DefaultBaseURL, - Models: models, - } - } - return definitions -} - -func providerDefaultBaseURL(provider string) (string, bool) { - return providercatalog.DefaultBaseURL(provider) -} - -func fixedProviderBaseURL(provider string) (string, bool) { - switch provider { - case "bailian-coding-plan": - return providerDefaultBaseURL(provider) - case "ark-coding-plan": - return providerDefaultBaseURL(provider) - case "minimax": - return providerDefaultBaseURL(provider) - case "xiaomi": - return providerDefaultBaseURL(provider) - default: - return "", false - } -} - -func providerDisplayName(provider string) string { - return providercatalog.DisplayName(provider) -} - func readInstallEnv(envStr string) map[string]interface{} { if strings.TrimSpace(envStr) == "" { return nil @@ -2009,14 +1499,14 @@ func runtimeProviderModelPrefix(provider string) string { } func poolModelPrefix(provider string) string { - target := provider - if definitions, ok := providerDefinitions()[target]; ok && len(definitions.Models) > 0 { - parts := strings.SplitN(strings.TrimSpace(definitions.Models[0].ID), "/", 2) + meta, ok := providercatalog.Get(provider) + if ok && len(meta.Models) > 0 { + parts := strings.SplitN(strings.TrimSpace(meta.Models[0].ID), "/", 2) if len(parts) == 2 && strings.TrimSpace(parts[0]) != "" { return parts[0] } } - return target + return provider } func supportedProviderModelPrefixes(provider string) []string { @@ -2037,72 +1527,6 @@ func supportedProviderModelPrefixes(provider string) []string { return result } -func normalizeAPIType(apiType string) string { - trim := apiType - if trim == "" { - return "openai-completions" - } - return trim -} - -func isSupportedAPIType(apiType string) bool { - switch normalizeAPIType(apiType) { - case "openai-completions", "openai-responses", "anthropic-messages": - return true - default: - return false - } -} - -func isSupportedOllamaAPIType(apiType string) bool { - switch normalizeAPIType(apiType) { - case "openai-completions", "openai-responses": - return true - default: - return false - } -} - -func resolveRuntimeParams(provider, apiType string, maxTokens, contextWindow int) (string, int, int) { - resolvedAPI := normalizeAPIType(apiType) - if provider == "ollama" && !isSupportedOllamaAPIType(resolvedAPI) { - resolvedAPI = "openai-responses" - } - resolvedMaxTokens := maxTokens - resolvedContextWindow := contextWindow - if resolvedMaxTokens <= 0 { - switch provider { - case "deepseek": - resolvedMaxTokens = 8192 - case "zai": - resolvedMaxTokens = 131072 - case "openrouter": - resolvedMaxTokens = 8192 - case "minimax", "kimi-coding", "custom": - resolvedMaxTokens = 8192 - default: - resolvedMaxTokens = 8192 - } - } - if resolvedContextWindow <= 0 { - switch provider { - case "deepseek": - resolvedContextWindow = 128000 - case "zai": - resolvedContextWindow = 204800 - case "openrouter": - resolvedContextWindow = 128000 - case "minimax", "kimi-coding": - resolvedContextWindow = 200000 - case "custom", "vllm": - resolvedContextWindow = 128000 - default: - resolvedContextWindow = 256000 - } - } - return resolvedAPI, resolvedMaxTokens, resolvedContextWindow -} - func generateToken() string { bytes := make([]byte, 24) if _, err := rand.Read(bytes); err != nil { diff --git a/agent/init/migration/migrate.go b/agent/init/migration/migrate.go index de56374e0..8616c6330 100644 --- a/agent/init/migration/migrate.go +++ b/agent/init/migration/migrate.go @@ -65,7 +65,6 @@ func InitAgentDB() { migrations.AddCronjobArgs, migrations.AddWebsiteAcmeAccountColumn, migrations.AddAgentTables, - migrations.MigrateOpenclawAgents, migrations.AddAgentCustomModelFields, migrations.AddAppInstallSortOrder, migrations.AddAgentAccountRememberAPIKey, diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index 325106c72..3fbddca43 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -897,114 +897,6 @@ var AddAgentTables = &gormigrate.Migration{ }, } -var MigrateOpenclawAgents = &gormigrate.Migration{ - ID: "20260207-migrate-openclaw-agents", - Migrate: func(tx *gorm.DB) error { - var installs []model.AppInstall - if err := tx.Preload("App").Find(&installs).Error; err != nil { - return err - } - for _, install := range installs { - appKey := install.App.Key - if appKey == "" || install.App.Resource == "" { - var app model.App - if err := tx.First(&app, install.AppId).Error; err == nil { - install.App = app - appKey = app.Key - } - } - if appKey != constant.AppOpenclaw { - continue - } - var count int64 - if err := tx.Model(&model.Agent{}).Where("app_install_id = ?", install.ID).Count(&count).Error; err != nil { - return err - } - if count > 0 { - continue - } - envMap := map[string]interface{}{} - if strings.TrimSpace(install.Env) != "" { - _ = json.Unmarshal([]byte(install.Env), &envMap) - } - configPath := path.Join(install.GetPath(), "data", "conf", "openclaw.json") - cfgMeta := migrationutils.OpenclawMeta{} - if fileData, err := os.ReadFile(configPath); err == nil { - cfgMeta = migrationutils.ParseOpenclawMeta(fileData) - } - provider := strings.ToLower(migrationutils.GetEnvStr(envMap, "PROVIDER")) - if provider == "" { - provider = strings.ToLower(cfgMeta.Provider) - } - if provider == "" { - continue - } - provider = migrationutils.NormalizeOpenclawProvider(provider, cfgMeta.BaseURL) - modelName := migrationutils.GetEnvStr(envMap, "MODEL") - if modelName == "" { - modelName = cfgMeta.Model - } - baseURL := migrationutils.GetEnvStr(envMap, "BASE_URL") - if baseURL == "" { - baseURL = cfgMeta.BaseURL - } - apiKey := migrationutils.GetEnvStr(envMap, "API_KEY") - if apiKey == "" { - apiKey = cfgMeta.APIKey - } - token := migrationutils.GetEnvStr(envMap, "OPENCLAW_GATEWAY_TOKEN") - if token == "" { - token = cfgMeta.Token - } - if provider != "ollama" { - if baseURL == "" { - if defaultURL, ok := migrationutils.DefaultBaseURL(provider); ok { - baseURL = defaultURL - } - } - } - if provider == "ollama" && baseURL == "" { - continue - } - var account model.AgentAccount - err := tx.Where("provider = ? AND api_key = ? AND base_url = ?", provider, apiKey, baseURL).First(&account).Error - if err != nil { - if errors.Is(err, gorm.ErrRecordNotFound) { - account = model.AgentAccount{ - Provider: provider, - Name: install.Name, - APIKey: apiKey, - BaseURL: baseURL, - Verified: apiKey != "" || provider == "ollama", - } - if err := tx.Create(&account).Error; err != nil { - return err - } - } else { - return err - } - } - agent := model.Agent{ - Name: install.Name, - Provider: provider, - Model: modelName, - BaseURL: baseURL, - APIKey: apiKey, - Token: token, - Status: install.Status, - Message: install.Message, - AppInstallID: install.ID, - AccountID: account.ID, - ConfigPath: configPath, - } - if err := tx.Create(&agent).Error; err != nil { - return err - } - } - return nil - }, -} - var AddAgentCustomModelFields = &gormigrate.Migration{ ID: "20260224-add-agent-custom-model-fields", Migrate: func(tx *gorm.DB) error { diff --git a/agent/init/migration/migrations/utils/openclaw_agent.go b/agent/init/migration/migrations/utils/openclaw_agent.go deleted file mode 100644 index 279d03ff2..000000000 --- a/agent/init/migration/migrations/utils/openclaw_agent.go +++ /dev/null @@ -1,162 +0,0 @@ -package utils - -import ( - "encoding/json" - "fmt" - "strings" - - providercatalog "github.com/1Panel-dev/1Panel/agent/app/provider" -) - -type OpenclawMeta struct { - Provider string - Model string - BaseURL string - APIKey string - Token string -} - -func GetEnvStr(envMap map[string]interface{}, key string) string { - if envMap == nil { - return "" - } - if value, ok := envMap[key]; ok { - switch v := value.(type) { - case string: - return strings.TrimSpace(v) - default: - return strings.TrimSpace(fmt.Sprintf("%v", v)) - } - } - return "" -} - -func ParseOpenclawMeta(fileData []byte) OpenclawMeta { - meta := OpenclawMeta{} - content := map[string]interface{}{} - if err := json.Unmarshal(fileData, &content); err != nil { - return meta - } - - meta.Token = getNestedString(content, "gateway", "auth", "token") - meta.Model = getNestedString(content, "agents", "defaults", "model", "primary") - - providerKey := "" - if parts := strings.SplitN(meta.Model, "/", 2); len(parts) == 2 { - providerKey = strings.TrimSpace(parts[0]) - } - - providers := getNestedMap(content, "models", "providers") - providerConfig := map[string]interface{}{} - if providerKey != "" { - if cfg, ok := providers[providerKey].(map[string]interface{}); ok { - providerConfig = cfg - } - } - if len(providerConfig) == 0 && len(providers) == 1 { - for key, value := range providers { - if cfg, ok := value.(map[string]interface{}); ok { - providerKey = key - providerConfig = cfg - break - } - } - } - - meta.Provider = providerKey - meta.BaseURL = getString(providerConfig, "baseUrl") - meta.APIKey = getString(providerConfig, "apiKey") - - if meta.Model == "" && providerKey != "" { - if modelID := getProviderFirstModelID(providerConfig); modelID != "" { - meta.Model = providerKey + "/" + modelID - } - } - - return meta -} - -func NormalizeOpenclawProvider(provider, baseURL string) string { - p := strings.ToLower(strings.TrimSpace(provider)) - base := strings.ToLower(strings.TrimSpace(baseURL)) - switch p { - case "moonshot": - if strings.Contains(base, "moonshot.cn") { - return "kimi" - } - return "moonshot" - default: - return p - } -} - -func DefaultBaseURL(provider string) (string, bool) { - return providercatalog.DefaultBaseURL(provider) -} - -func getNestedMap(data map[string]interface{}, keys ...string) map[string]interface{} { - current := data - for _, key := range keys { - next, ok := current[key].(map[string]interface{}) - if !ok { - return map[string]interface{}{} - } - current = next - } - return current -} - -func getNestedString(data map[string]interface{}, keys ...string) string { - current := data - for i, key := range keys { - value, ok := current[key] - if !ok { - return "" - } - if i == len(keys)-1 { - switch v := value.(type) { - case string: - return strings.TrimSpace(v) - default: - return strings.TrimSpace(fmt.Sprintf("%v", v)) - } - } - next, ok := value.(map[string]interface{}) - if !ok { - return "" - } - current = next - } - return "" -} - -func getString(data map[string]interface{}, key string) string { - if data == nil { - return "" - } - value, ok := data[key] - if !ok { - return "" - } - switch v := value.(type) { - case string: - return strings.TrimSpace(v) - default: - return strings.TrimSpace(fmt.Sprintf("%v", v)) - } -} - -func getProviderFirstModelID(data map[string]interface{}) string { - if data == nil { - return "" - } - rawModels, ok := data["models"].([]interface{}) - if !ok || len(rawModels) == 0 { - return "" - } - first, ok := rawModels[0].(map[string]interface{}) - if !ok { - return "" - } - return getString(first, "id") -} diff --git a/frontend/src/views/ai/agents/model/add/index.vue b/frontend/src/views/ai/agents/model/add/index.vue index 3af46bfea..326d0d53e 100644 --- a/frontend/src/views/ai/agents/model/add/index.vue +++ b/frontend/src/views/ai/agents/model/add/index.vue @@ -33,19 +33,23 @@