feat: optimize the validation logic for model accounts (#12251)

This commit is contained in:
CityFun
2026-03-20 09:55:54 +00:00
committed by GitHub
parent 8dfa7945d7
commit df90286253
12 changed files with 1114 additions and 1496 deletions
+2 -2
View File
@@ -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"`
}
+195 -76
View File
@@ -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
}
+239 -265
View File
@@ -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")
}
+7
View File
@@ -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") {
+45 -234
View File
@@ -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 {
+544
View File
@@ -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
}
+57 -633
View File
@@ -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 {
-1
View File
@@ -65,7 +65,6 @@ func InitAgentDB() {
migrations.AddCronjobArgs,
migrations.AddWebsiteAcmeAccountColumn,
migrations.AddAgentTables,
migrations.MigrateOpenclawAgents,
migrations.AddAgentCustomModelFields,
migrations.AddAppInstallSortOrder,
migrations.AddAgentAccountRememberAPIKey,
-108
View File
@@ -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 {
@@ -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")
}
@@ -33,19 +33,23 @@
</el-form-item>
<template v-if="showInitialModel">
<el-divider content-position="left">{{ $t('aiTools.agents.accountModels') }}</el-divider>
<el-form-item :label="$t('aiTools.model.model')" prop="initialModel.id" :rules="[Rules.requiredInput]">
<el-form-item :label="$t('aiTools.model.model')" prop="initialModel.id" :rules="[Rules.noSpace]">
<el-input v-model="form.initialModel.id" />
</el-form-item>
<el-form-item :label="$t('commons.table.name')" prop="initialModel.name">
<el-form-item :label="$t('commons.table.name')" prop="initialModel.name" :rules="[Rules.requiredInput]">
<el-input v-model="form.initialModel.name" />
</el-form-item>
<el-form-item label="Context Window" prop="initialModel.contextWindow">
<el-form-item label="Context Window" prop="initialModel.contextWindow" :rules="[Rules.integerNumber]">
<el-input-number v-model="form.initialModel.contextWindow" :min="1" :max="2000000" />
</el-form-item>
<el-form-item label="Max Tokens" prop="initialModel.maxTokens">
<el-form-item label="Max Tokens" prop="initialModel.maxTokens" :rules="[Rules.integerNumber]">
<el-input-number v-model="form.initialModel.maxTokens" :min="1" :max="2000000" />
</el-form-item>
<el-form-item :label="$t('aiTools.agents.modelInputTypes')" prop="initialModel.input">
<el-form-item
:label="$t('aiTools.agents.modelInputTypes')"
prop="initialModel.input"
:rules="[Rules.requiredSelect]"
>
<el-checkbox-group v-model="form.initialModel.input">
<el-checkbox label="text">Text</el-checkbox>
<el-checkbox label="image">Image</el-checkbox>
@@ -155,15 +159,21 @@ const normalizeInitialModel = () => {
const item = {
recordId: 0,
id: String(form.initialModel.id || '').trim(),
name: String(form.initialModel.name || form.initialModel.id || '').trim(),
contextWindow: Number(form.initialModel.contextWindow || 0) || defaultContextWindowForProvider(form.provider),
maxTokens: Number(form.initialModel.maxTokens || 0) || 8192,
name: String(form.initialModel.name || '').trim(),
contextWindow: Number(form.initialModel.contextWindow || 0),
maxTokens: Number(form.initialModel.maxTokens || 0),
reasoning: Boolean(form.initialModel.reasoning),
input: Array.from(
new Set((form.initialModel.input || []).filter((value) => value === 'text' || value === 'image')),
),
};
if (item.id === '') {
if (
item.id === '' ||
item.name === '' ||
item.contextWindow <= 0 ||
item.maxTokens <= 0 ||
item.input.length === 0
) {
return null;
}
return item;
@@ -121,8 +121,11 @@ const editorTitle = computed(() =>
);
const rules = reactive({
id: [Rules.requiredInput],
id: [Rules.noSpace],
name: [Rules.requiredInput],
contextWindow: [Rules.integerNumber],
maxTokens: [Rules.integerNumber],
input: [Rules.requiredSelect],
});
const buttons = [
@@ -140,8 +143,8 @@ const buildModelItem = (model?: Partial<AI.AgentAccountModel>): AI.AgentAccountM
recordId: Number(model?.recordId || 0),
id: String(model?.id || '').trim(),
name: String(model?.name || '').trim(),
contextWindow: Number(model?.contextWindow || 0) || 128000,
maxTokens: Number(model?.maxTokens || 0) || 8192,
contextWindow: Number(model?.contextWindow || 0),
maxTokens: Number(model?.maxTokens || 0),
reasoning: Boolean(model?.reasoning),
input: Array.from(new Set((model?.input || []).filter((value) => value === 'text' || value === 'image'))),
});
@@ -190,9 +193,6 @@ const submit = async () => {
MsgError(i18n.global.t('aiTools.agents.accountModelsDuplicate'));
return;
}
if (!form.input?.length) {
form.input = ['text'];
}
saving.value = true;
try {
if (form.recordId) {