From 01fb9a461bdb641c61bfb0ef87c378075111504c Mon Sep 17 00:00:00 2001 From: CityFun <31820853+zhengkunwang223@users.noreply.github.com> Date: Wed, 1 Apr 2026 19:18:09 +0800 Subject: [PATCH] feat: OpenClaw supports configuring backup models. (#12386) --- agent/app/api/v2/agents.go | 21 +++ agent/app/dto/agents.go | 13 +- agent/app/service/agents.go | 34 +++- agent/app/service/agents_utils.go | 163 +++++++++++++---- agent/i18n/lang/en.yaml | 2 + agent/i18n/lang/es-ES.yaml | 2 + agent/i18n/lang/ja.yaml | 2 + agent/i18n/lang/ko.yaml | 2 + agent/i18n/lang/ms.yaml | 2 + agent/i18n/lang/pt-BR.yaml | 2 + agent/i18n/lang/ru.yaml | 2 + agent/i18n/lang/tr.yaml | 2 + agent/i18n/lang/zh-Hant.yaml | 2 + agent/i18n/lang/zh.yaml | 2 + agent/router/ro_ai.go | 1 + frontend/src/api/interface/ai.ts | 11 ++ frontend/src/api/modules/ai.ts | 4 + frontend/src/lang/modules/en.ts | 4 + frontend/src/lang/modules/es-es.ts | 4 + frontend/src/lang/modules/ja.ts | 4 + frontend/src/lang/modules/ko.ts | 4 + frontend/src/lang/modules/ms.ts | 4 + frontend/src/lang/modules/pt-br.ts | 4 + frontend/src/lang/modules/ru.ts | 4 + frontend/src/lang/modules/tr.ts | 4 + frontend/src/lang/modules/zh-Hant.ts | 4 + frontend/src/lang/modules/zh.ts | 4 + .../views/ai/agents/agent/config/index.vue | 2 - .../tabs/channels/components/channel-bots.vue | 10 +- .../agent/config/tabs/channels/dingtalk.vue | 4 +- .../agent/config/tabs/channels/discord.vue | 4 +- .../agent/config/tabs/channels/feishu.vue | 4 +- .../agents/agent/config/tabs/channels/qq.vue | 4 +- .../agent/config/tabs/channels/telegram.vue | 4 +- .../ai/agents/agent/config/tabs/model.vue | 166 ++++++++++++++++-- 35 files changed, 432 insertions(+), 73 deletions(-) diff --git a/agent/app/api/v2/agents.go b/agent/app/api/v2/agents.go index 2a68460a3..a282036da 100644 --- a/agent/app/api/v2/agents.go +++ b/agent/app/api/v2/agents.go @@ -111,6 +111,27 @@ func (b *BaseApi) UpdateAgentRemark(c *gin.Context) { helper.Success(c) } +// @Tags AI +// @Summary Get Agent model config +// @Accept json +// @Param request body dto.AgentIDReq true "request" +// @Success 200 {object} dto.AgentModelConfig +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/model/get [post] +func (b *BaseApi) GetAgentModelConfig(c *gin.Context) { + var req dto.AgentIDReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + res, err := agentService.GetModelConfig(req) + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, res) +} + // @Tags AI // @Summary Update Agent model config // @Accept json diff --git a/agent/app/dto/agents.go b/agent/app/dto/agents.go index 3e8734ea4..35a2a26db 100644 --- a/agent/app/dto/agents.go +++ b/agent/app/dto/agents.go @@ -71,9 +71,16 @@ type AgentRemarkUpdateReq struct { } type AgentModelConfigUpdateReq struct { - AgentID uint `json:"agentId" validate:"required"` - AccountID uint `json:"accountId" validate:"required"` - Model string `json:"model" validate:"required"` + AgentID uint `json:"agentId" validate:"required"` + AccountID uint `json:"accountId" validate:"required"` + Model string `json:"model" validate:"required"` + Fallbacks []string `json:"fallbacks"` +} + +type AgentModelConfig struct { + AccountID uint `json:"accountId"` + Model string `json:"model"` + Fallbacks []string `json:"fallbacks"` } type AgentOverviewReq struct { diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go index cc0412ce2..6a7d16fe6 100644 --- a/agent/app/service/agents.go +++ b/agent/app/service/agents.go @@ -30,6 +30,7 @@ type IAgentService interface { Delete(req dto.AgentDeleteReq) error ResetToken(req dto.AgentTokenResetReq) error UpdateRemark(req dto.AgentRemarkUpdateReq) error + GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig, error) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error GetOverview(req dto.AgentOverviewReq) (*dto.AgentOverview, error) GetProviders() ([]dto.ProviderInfo, error) @@ -345,6 +346,30 @@ func (a AgentService) UpdateRemark(req dto.AgentRemarkUpdateReq) error { return agentRepo.Save(agent) } +func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig, error) { + agent, _, conf, err := a.loadOpenclawAgentConfig(req.AgentID) + if err != nil { + return nil, err + } + account, err := agentAccountRepo.GetFirst(repo.WithByID(agent.AccountID)) + if err != nil { + return nil, err + } + models, err := loadAgentAccountModels(account) + if err != nil { + return nil, err + } + model := extractOpenclawPrimaryModelID(conf, account, models) + if model == "" { + model = agent.Model + } + return &dto.AgentModelConfig{ + AccountID: agent.AccountID, + Model: model, + Fallbacks: extractOpenclawFallbackModelIDs(conf, account, models, model), + }, nil +} + func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error { agent, err := loadOpenclawAgentByID(req.AgentID) if err != nil { @@ -361,7 +386,7 @@ func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error modelName := resolvedRuntime.StoredModel apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow confDir := path.Dir(agent.ConfigPath) - if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil); err != nil { + if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, req.Fallbacks); err != nil { return err } agent.Provider = account.Provider @@ -875,13 +900,18 @@ func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error { } else { selectedAccountModel = accountModels[0] } + conf, err := readOpenclawConfig(agent.ConfigPath) + if err != nil { + return err + } + fallbacks := extractOpenclawFallbackModelIDs(conf, account, accountModels, selectedAccountModel.ID) resolvedRuntime, err := buildOpenclawAccountModelRuntime(account, selectedAccountModel) if err != nil { return err } modelName = resolvedRuntime.StoredModel apiType, maxTokens, contextWindow := resolvedRuntime.APIType, resolvedRuntime.MaxTokens, resolvedRuntime.ContextWindow - if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil); err != nil { + if err := writeOpenclawConfig(confDir, account, modelName, agent.Token, nil, fallbacks); err != nil { return err } agent.BaseURL = baseURL diff --git a/agent/app/service/agents_utils.go b/agent/app/service/agents_utils.go index c9dc74a1b..b303fad42 100644 --- a/agent/app/service/agents_utils.go +++ b/agent/app/service/agents_utils.go @@ -339,10 +339,7 @@ func setOtherConfig(conf map[string]interface{}, config dto.AgentOtherConfig) { } func buildAgentItem(agent *model.Agent, appInstall *model.AppInstall, envMap map[string]interface{}) dto.AgentItem { - agentType := normalizeAgentType(agent.AgentType) - if appInstall != nil && appInstall.ID > 0 && appInstall.App.Key == constant.AppCopaw { - agentType = constant.AppCopaw - } + agentType := agent.AgentType item := dto.AgentItem{ ID: agent.ID, Name: agent.Name, @@ -660,7 +657,8 @@ type agentDefaults struct { } type modelRef struct { - Primary string `json:"primary"` + Primary string `json:"primary"` + Fallbacks []string `json:"fallbacks,omitempty"` } type modelsConfig struct { @@ -701,7 +699,7 @@ type browserConfig struct { DefaultProfile string `json:"defaultProfile"` } -func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string) error { +func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, token string, allowedOrigins []string, fallbacks []string) error { if strings.TrimSpace(confDir) == "" { return fmt.Errorf("config dir is required") } @@ -724,6 +722,10 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, if err != nil { return err } + resolvedFallbacks, err := resolveOpenclawFallbackModels(account, modelName, fallbacks) + if err != nil { + return err + } cfg := openclawConfig{ Gateway: gatewayConfig{ @@ -743,7 +745,7 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, Agents: agentsConfig{ Defaults: agentDefaults{ UserTimezone: resolveServerTimezone(), - Model: modelRef{Primary: primaryModel}, + Model: modelRef{Primary: primaryModel, Fallbacks: resolvedFallbacks}, Models: defaultsModels, }, }, @@ -811,6 +813,11 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, } modelMap := ensureChildMap(defaultsMap, "model") modelMap["primary"] = cfg.Agents.Defaults.Model.Primary + if len(cfg.Agents.Defaults.Model.Fallbacks) > 0 { + modelMap["fallbacks"] = cfg.Agents.Defaults.Model.Fallbacks + } else { + delete(modelMap, "fallbacks") + } if cfg.Agents.Defaults.Models != nil { defaultsMap["models"] = cfg.Agents.Defaults.Models } @@ -847,12 +854,128 @@ func writeOpenclawConfig(confDir string, account *model.AgentAccount, modelName, return fileOp.SaveFile(envPath, content, 0600) } +func resolveOpenclawFallbackModels(account *model.AgentAccount, primaryModel string, fallbackIDs []string) ([]string, error) { + accountModels, err := loadAgentAccountModels(account) + if err != nil { + return nil, err + } + return resolveOpenclawFallbackModelsFromPool(account, accountModels, primaryModel, fallbackIDs, true) +} + +func resolveOpenclawFallbackModelsFromPool(account *model.AgentAccount, accountModels []dto.AgentAccountModel, primaryModel string, fallbackIDs []string, strict bool) ([]string, error) { + if len(fallbackIDs) == 0 { + return nil, nil + } + result := make([]string, 0, len(fallbackIDs)) + seen := make(map[string]struct{}, len(fallbackIDs)) + for _, item := range fallbackIDs { + target := strings.TrimSpace(item) + if target == "" { + continue + } + accountModel, err := requireAgentAccountModelForProvider(account.Provider, accountModels, target) + if err != nil { + if strict { + return nil, err + } + continue + } + if sameProviderModelID(account.Provider, accountModel.ID, primaryModel) { + if strict { + return nil, buserr.New("ErrAgentFallbackModelPrimary") + } + continue + } + resolvedPrimary, _, _, _, err := buildOpenclawCatalogModel(account, accountModel) + if err != nil { + return nil, err + } + if _, ok := seen[resolvedPrimary]; ok { + if strict { + return nil, buserr.New("ErrAgentFallbackModelDuplicate") + } + continue + } + seen[resolvedPrimary] = struct{}{} + result = append(result, resolvedPrimary) + } + return result, nil +} + +func extractOpenclawPrimaryModelID(conf map[string]interface{}, account *model.AgentAccount, accountModels []dto.AgentAccountModel) string { + modelMap := openclawModelRefMap(conf) + primary := strings.TrimSpace(fmt.Sprintf("%v", modelMap["primary"])) + if primary == "" { + return "" + } + resolved, err := buildOpenclawResolvedModelIDMap(account, accountModels) + if err != nil { + return "" + } + return resolved[primary] +} + +func extractOpenclawFallbackModelIDs(conf map[string]interface{}, account *model.AgentAccount, accountModels []dto.AgentAccountModel, primaryModel string) []string { + modelMap := openclawModelRefMap(conf) + raw, ok := modelMap["fallbacks"].([]interface{}) + if !ok || len(raw) == 0 { + return nil + } + resolved, err := buildOpenclawResolvedModelIDMap(account, accountModels) + if err != nil { + return nil + } + result := make([]string, 0, len(raw)) + seen := make(map[string]struct{}, len(raw)) + for _, item := range raw { + target := strings.TrimSpace(fmt.Sprintf("%v", item)) + modelID, ok := resolved[target] + if !ok || sameProviderModelID(account.Provider, modelID, primaryModel) { + continue + } + if _, ok := seen[modelID]; ok { + continue + } + seen[modelID] = struct{}{} + result = append(result, modelID) + } + return result +} + +func openclawModelRefMap(conf map[string]interface{}) map[string]interface{} { + agentsMap, ok := conf["agents"].(map[string]interface{}) + if !ok { + return nil + } + defaultsMap, ok := agentsMap["defaults"].(map[string]interface{}) + if !ok { + return nil + } + modelMap, ok := defaultsMap["model"].(map[string]interface{}) + if !ok { + return nil + } + return modelMap +} + +func buildOpenclawResolvedModelIDMap(account *model.AgentAccount, accountModels []dto.AgentAccountModel) (map[string]string, error) { + result := make(map[string]string, len(accountModels)) + for _, item := range accountModels { + resolvedPrimary, _, _, _, err := buildOpenclawCatalogModel(account, item) + if err != nil { + return nil, err + } + result[resolvedPrimary] = item.ID + } + return result, nil +} + func prepareOpenclawInstallFiles(appInstall *model.AppInstall, account *model.AgentAccount, modelName, token string, allowedOrigins []string) error { if appInstall == nil { return fmt.Errorf("app install is required") } confDir := path.Join(appInstall.GetPath(), "data", "conf") - if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins); err != nil { + if err := writeOpenclawConfig(confDir, account, modelName, token, allowedOrigins, nil); err != nil { return err } dataDir := path.Join(appInstall.GetPath(), "data") @@ -1176,12 +1299,6 @@ func replacePersistedAgentAccountModelsWithTx(tx *gorm.DB, accountID uint, model return nil } -func replacePersistedAgentAccountModels(accountID uint, models []dto.AgentAccountModel) error { - return global.DB.Transaction(func(tx *gorm.DB) error { - return replacePersistedAgentAccountModelsWithTx(tx, accountID, models) - }) -} - func normalizeAgentAccountModels(account *model.AgentAccount, models []dto.AgentAccountModel) ([]dto.AgentAccountModel, error) { requested := append([]dto.AgentAccountModel(nil), models...) if len(requested) == 0 { @@ -1534,24 +1651,6 @@ func normalizeCustomModel(modelName string) string { return trim } -func normalizeAgentType(agentType string) string { - trim := agentType - if trim == "" { - return constant.AppOpenclaw - } - return trim -} - -func modelMatchesProvider(provider, modelName string) bool { - target := strings.TrimSpace(modelName) - for _, prefix := range supportedProviderModelPrefixes(provider) { - if prefix != "" && strings.HasPrefix(target, prefix+"/") { - return true - } - } - return false -} - func runtimeProviderModelPrefix(provider string) string { switch provider { case "gemini": diff --git a/agent/i18n/lang/en.yaml b/agent/i18n/lang/en.yaml index 9398427b2..042183f14 100644 --- a/agent/i18n/lang/en.yaml +++ b/agent/i18n/lang/en.yaml @@ -49,6 +49,8 @@ ErrAgentAccountNotVerified: 'Agent account not verified' ErrAgentAccountModelsRequired: 'Please configure at least one model' ErrAgentAccountSingleInitialModel: 'Only one initial model can be configured during account creation' ErrAgentModelNotInAccount: 'Model is not configured in the selected account' +ErrAgentFallbackModelDuplicate: 'Fallback models cannot be duplicated' +ErrAgentFallbackModelPrimary: 'Fallback model cannot match the primary model' ErrAgentModelInUse: 'A bound agent is still using this model: {{ .name }}' ErrAgentBaseURLRequired: 'Base URL is required' ErrAgentApiKeyRequired: 'API key is required' diff --git a/agent/i18n/lang/es-ES.yaml b/agent/i18n/lang/es-ES.yaml index 62269d354..5d57c1f73 100644 --- a/agent/i18n/lang/es-ES.yaml +++ b/agent/i18n/lang/es-ES.yaml @@ -44,6 +44,8 @@ ErrAgentAccountNotVerified: 'Cuenta de agente no verificada' ErrAgentAccountModelsRequired: 'Configure al menos un modelo' ErrAgentAccountSingleInitialModel: 'Solo se puede configurar un modelo inicial al crear la cuenta' ErrAgentModelNotInAccount: 'El modelo no está configurado en la cuenta seleccionada' +ErrAgentFallbackModelDuplicate: 'Los modelos de respaldo no pueden duplicarse' +ErrAgentFallbackModelPrimary: 'El modelo de respaldo no puede ser igual al modelo principal' ErrAgentModelInUse: 'Un agente vinculado todavía usa este modelo: {{ .name }}' ErrAgentBaseURLRequired: 'URL base requerida' ErrAgentApiKeyRequired: 'Clave API requerida' diff --git a/agent/i18n/lang/ja.yaml b/agent/i18n/lang/ja.yaml index b10614be8..5160b71a8 100644 --- a/agent/i18n/lang/ja.yaml +++ b/agent/i18n/lang/ja.yaml @@ -44,6 +44,8 @@ ErrAgentAccountNotVerified: 'アカウント未検証です' ErrAgentAccountModelsRequired: '少なくとも 1 つのモデルを設定してください' ErrAgentAccountSingleInitialModel: 'アカウント作成時に設定できる初期モデルは 1 つだけです' ErrAgentModelNotInAccount: '選択したモデルはこのアカウントに設定されていません' +ErrAgentFallbackModelDuplicate: 'フォールバックモデルは重複できません' +ErrAgentFallbackModelPrimary: 'フォールバックモデルは主モデルと同じにできません' ErrAgentModelInUse: 'このモデルはまだエージェントで使用中です: {{ .name }}' ErrAgentBaseURLRequired: 'ベースURLが必要です' ErrAgentApiKeyRequired: 'APIキーが必要です' diff --git a/agent/i18n/lang/ko.yaml b/agent/i18n/lang/ko.yaml index 2d5684e74..7233ca49b 100644 --- a/agent/i18n/lang/ko.yaml +++ b/agent/i18n/lang/ko.yaml @@ -44,6 +44,8 @@ ErrAgentAccountNotVerified: '에이전트 계정 미확인' ErrAgentAccountModelsRequired: '최소 한 개의 모델을 구성해 주세요' ErrAgentAccountSingleInitialModel: '계정 생성 시 초기 모델은 하나만 구성할 수 있습니다' ErrAgentModelNotInAccount: '선택한 모델이 현재 계정에 구성되어 있지 않습니다' +ErrAgentFallbackModelDuplicate: '폴백 모델은 중복될 수 없습니다' +ErrAgentFallbackModelPrimary: '폴백 모델은 기본 모델과 같을 수 없습니다' ErrAgentModelInUse: '이 모델을 아직 사용 중인 에이전트가 있습니다: {{ .name }}' ErrAgentBaseURLRequired: '기본 URL 필요' ErrAgentApiKeyRequired: 'API 키 필요' diff --git a/agent/i18n/lang/ms.yaml b/agent/i18n/lang/ms.yaml index 5a26d077f..8533811e2 100644 --- a/agent/i18n/lang/ms.yaml +++ b/agent/i18n/lang/ms.yaml @@ -44,6 +44,8 @@ ErrAgentAccountNotVerified: 'Akaun ejen belum disahkan' ErrAgentAccountModelsRequired: 'Sila konfigur sekurang-kurangnya satu model' ErrAgentAccountSingleInitialModel: 'Hanya satu model awal boleh dikonfigurasi semasa mencipta akaun' ErrAgentModelNotInAccount: 'Model tidak dikonfigurasikan dalam akaun yang dipilih' +ErrAgentFallbackModelDuplicate: 'Model fallback tidak boleh berulang' +ErrAgentFallbackModelPrimary: 'Model fallback tidak boleh sama dengan model utama' ErrAgentModelInUse: 'Masih ada ejen terikat yang menggunakan model ini: {{ .name }}' ErrAgentBaseURLRequired: 'URL asas diperlukan' ErrAgentApiKeyRequired: 'Kunci API diperlukan' diff --git a/agent/i18n/lang/pt-BR.yaml b/agent/i18n/lang/pt-BR.yaml index 1a64bf492..c66928cc0 100644 --- a/agent/i18n/lang/pt-BR.yaml +++ b/agent/i18n/lang/pt-BR.yaml @@ -44,6 +44,8 @@ ErrAgentAccountNotVerified: 'Conta de agente não verificada' ErrAgentAccountModelsRequired: 'Configure pelo menos um modelo' ErrAgentAccountSingleInitialModel: 'Apenas um modelo inicial pode ser configurado ao criar a conta' ErrAgentModelNotInAccount: 'O modelo não está configurado na conta selecionada' +ErrAgentFallbackModelDuplicate: 'Os modelos de fallback não podem ser duplicados' +ErrAgentFallbackModelPrimary: 'O modelo de fallback não pode ser igual ao modelo principal' ErrAgentModelInUse: 'Ainda há um agente vinculado usando este modelo: {{ .name }}' ErrAgentBaseURLRequired: 'URL base obrigatória' ErrAgentApiKeyRequired: 'Chave API obrigatória' diff --git a/agent/i18n/lang/ru.yaml b/agent/i18n/lang/ru.yaml index 9ccf2e3ef..b8551b339 100644 --- a/agent/i18n/lang/ru.yaml +++ b/agent/i18n/lang/ru.yaml @@ -44,6 +44,8 @@ ErrAgentAccountNotVerified: 'Аккаунт не подтверждён' ErrAgentAccountModelsRequired: 'Настройте хотя бы одну модель' ErrAgentAccountSingleInitialModel: 'При создании аккаунта можно указать только одну начальную модель' ErrAgentModelNotInAccount: 'Модель не настроена в выбранном аккаунте' +ErrAgentFallbackModelDuplicate: 'Резервные модели не могут дублироваться' +ErrAgentFallbackModelPrimary: 'Резервная модель не может совпадать с основной' ErrAgentModelInUse: 'Эта модель всё ещё используется агентом: {{ .name }}' ErrAgentBaseURLRequired: 'Нужен базовый URL' ErrAgentApiKeyRequired: 'Нужен API-ключ' diff --git a/agent/i18n/lang/tr.yaml b/agent/i18n/lang/tr.yaml index ef8a6589e..3321e413e 100644 --- a/agent/i18n/lang/tr.yaml +++ b/agent/i18n/lang/tr.yaml @@ -44,6 +44,8 @@ ErrAgentAccountNotVerified: 'Ajans hesabı doğrulanmadı' ErrAgentAccountModelsRequired: 'Lütfen en az bir model yapılandırın' ErrAgentAccountSingleInitialModel: 'Hesap oluştururken yalnızca bir başlangıç modeli yapılandırılabilir' ErrAgentModelNotInAccount: 'Model seçilen hesapta yapılandırılmadı' +ErrAgentFallbackModelDuplicate: 'Yedek modeller tekrar edemez' +ErrAgentFallbackModelPrimary: 'Yedek model birincil modelle aynı olamaz' ErrAgentModelInUse: 'Bu modeli hâlâ kullanan bağlı bir ajan var: {{ .name }}' ErrAgentBaseURLRequired: 'Temel URL gerekli' ErrAgentApiKeyRequired: 'API anahtarı gerekli' diff --git a/agent/i18n/lang/zh-Hant.yaml b/agent/i18n/lang/zh-Hant.yaml index bb1129466..b879c8c0d 100644 --- a/agent/i18n/lang/zh-Hant.yaml +++ b/agent/i18n/lang/zh-Hant.yaml @@ -44,6 +44,8 @@ ErrAgentAccountNotVerified: '帳號未驗證通過,請重試' ErrAgentAccountModelsRequired: '請至少配置一個模型' ErrAgentAccountSingleInitialModel: '建立帳號時只能配置一個初始模型' ErrAgentModelNotInAccount: '所選模型未配置在當前帳號中,請重試' +ErrAgentFallbackModelDuplicate: '備用模型不能重複' +ErrAgentFallbackModelPrimary: '備用模型不能與主模型相同' ErrAgentModelInUse: '仍有智能體正在使用該模型:{{ .name }}' ErrAgentBaseURLRequired: 'Base URL 不可為空,請重試' ErrAgentApiKeyRequired: 'API Key 不可為空,請重試' diff --git a/agent/i18n/lang/zh.yaml b/agent/i18n/lang/zh.yaml index 7ee790e38..07600acc8 100644 --- a/agent/i18n/lang/zh.yaml +++ b/agent/i18n/lang/zh.yaml @@ -49,6 +49,8 @@ ErrAgentAccountNotVerified: "账号未通过验证" ErrAgentAccountModelsRequired: "请至少配置一个模型" ErrAgentAccountSingleInitialModel: "创建账号时只能配置一个初始模型" ErrAgentModelNotInAccount: "所选模型未配置在当前账号中" +ErrAgentFallbackModelDuplicate: "备用模型不能重复" +ErrAgentFallbackModelPrimary: "备用模型不能与主模型相同" ErrAgentModelInUse: "仍有智能体正在使用该模型: {{ .name }}" ErrAgentBaseURLRequired: "Base URL 不能为空" ErrAgentApiKeyRequired: "API Key 不能为空" diff --git a/agent/router/ro_ai.go b/agent/router/ro_ai.go index e692f5c6b..c23b104ca 100644 --- a/agent/router/ro_ai.go +++ b/agent/router/ro_ai.go @@ -45,6 +45,7 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) { aiToolsRouter.POST("/agents/delete", baseApi.DeleteAgent) aiToolsRouter.POST("/agents/token/reset", baseApi.ResetAgentToken) aiToolsRouter.POST("/agents/remark", baseApi.UpdateAgentRemark) + aiToolsRouter.POST("/agents/model/get", baseApi.GetAgentModelConfig) aiToolsRouter.POST("/agents/model/update", baseApi.UpdateAgentModelConfig) aiToolsRouter.POST("/agents/overview", baseApi.GetAgentOverview) aiToolsRouter.GET("/agents/providers", baseApi.GetAgentProviders) diff --git a/frontend/src/api/interface/ai.ts b/frontend/src/api/interface/ai.ts index a3f1c498d..1e7ff251c 100644 --- a/frontend/src/api/interface/ai.ts +++ b/frontend/src/api/interface/ai.ts @@ -308,12 +308,23 @@ export namespace AI { agentId: number; accountId: number; model: string; + fallbacks: string[]; + } + + export interface AgentModelConfig { + accountId: number; + model: string; + fallbacks: string[]; } export interface AgentOverviewReq { agentId: number; } + export interface AgentIDReq { + agentId: number; + } + export interface AgentRoleCreateReq { agentId: number; name: string; diff --git a/frontend/src/api/modules/ai.ts b/frontend/src/api/modules/ai.ts index 77326a442..114421115 100644 --- a/frontend/src/api/modules/ai.ts +++ b/frontend/src/api/modules/ai.ts @@ -113,6 +113,10 @@ export const updateAgentRemark = (req: AI.AgentRemarkUpdateReq) => { return http.post(`/ai/agents/remark`, req); }; +export const getAgentModelConfig = (req: AI.AgentIDReq) => { + return http.post(`/ai/agents/model/get`, req); +}; + export const updateAgentModelConfig = (req: AI.AgentModelConfigUpdateReq) => { return http.post(`/ai/agents/model/update`, req); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index 54d7d1fd8..546e30e0e 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -688,6 +688,9 @@ const message = { accountModelsRequired: 'Configure at least one model', accountModelsDuplicate: 'Duplicate models exist in the catalog', modelPool: 'Model Pool', + fallbackModels: 'Fallback Models', + addFallbackModel: 'Add Fallback Model', + fallbackModelsEmpty: 'No fallback models configured', modelInputTypes: 'Input Types', reasoning: 'Reasoning Model', manualModel: 'Manual input', @@ -760,6 +763,7 @@ const message = { configFileRestartHelper: 'Saving the config file requires immediately restarting the container to take effect.', overviewSnapshot: 'Snapshot', + primaryModel: 'Primary Model', defaultModel: 'Default Model', channelCount: 'Configured Channels', skillCount: 'Skills', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 23c204d63..d3a1a647d 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -697,6 +697,9 @@ const message = { accountModelsRequired: 'Configure al menos un modelo', accountModelsDuplicate: 'Existen modelos duplicados en el catálogo', modelPool: 'Pool de modelos', + fallbackModels: 'Modelos de respaldo', + addFallbackModel: 'Agregar modelo de respaldo', + fallbackModelsEmpty: 'No hay modelos de respaldo configurados', modelInputTypes: 'Tipos de entrada', reasoning: 'Modelo de razonamiento', manualModel: 'Entrada manual de modelo', @@ -772,6 +775,7 @@ const message = { configFileRestartHelper: 'Guardar el archivo de configuración requiere reiniciar inmediatamente el contenedor para surtir efecto.', overviewSnapshot: 'Resumen', + primaryModel: 'Modelo principal', defaultModel: 'Modelo predeterminado', channelCount: 'Canales configurados', skillCount: 'Habilidades', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index 7e3cbcff0..7658ab015 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -689,6 +689,9 @@ const message = { accountModelsRequired: 'Configure at least one model', accountModelsDuplicate: 'カタログに重複したモデルがあります', modelPool: 'モデルプール', + fallbackModels: 'フォールバックモデル', + addFallbackModel: 'フォールバックモデルを追加', + fallbackModelsEmpty: 'フォールバックモデルは未設定です', modelInputTypes: '入力タイプ', reasoning: '推論モデル', manualModel: '手動入力', @@ -763,6 +766,7 @@ const message = { '現在の markdown ファイルをすべて保存するには、反映のためコンテナの再起動が必要です。今すぐ再起動するか後で再起動するかを選択してください。', configFileRestartHelper: '設定ファイルを保存するには、反映のため直ちにコンテナを再起動する必要があります。', overviewSnapshot: '概要', + primaryModel: 'メインモデル', defaultModel: 'デフォルトモデル', channelCount: '設定済みチャンネル数', skillCount: '技能数', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index 32901cfe9..39200911f 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -681,6 +681,9 @@ const message = { accountModelsRequired: 'Configure at least one model', accountModelsDuplicate: '카탈로그에 중복된 모델이 있습니다', modelPool: '모델 풀', + fallbackModels: '폴백 모델', + addFallbackModel: '폴백 모델 추가', + fallbackModelsEmpty: '구성된 폴백 모델이 없습니다', modelInputTypes: '입력 유형', reasoning: '추론 모델', manualModel: '수동 입력', @@ -749,6 +752,7 @@ const message = { '현재 markdown 파일을 모두 저장하려면 적용을 위해 컨테이너를 재시작해야 합니다. 지금 재시작할지 나중에 재시작할지 선택하세요.', configFileRestartHelper: '설정 파일을 저장하려면 즉시 컨테이너를 재시작해야 적용됩니다.', overviewSnapshot: '개요', + primaryModel: '주 모델', defaultModel: '기본 모델', channelCount: '설정된 채널 수', skillCount: '기술 수', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index e4a8aabfb..099ee7002 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -696,6 +696,9 @@ const message = { accountModelsRequired: 'Configure at least one model', accountModelsDuplicate: 'Duplicate models exist in the catalog', modelPool: 'Model Pool', + fallbackModels: 'Model fallback', + addFallbackModel: 'Tambah model fallback', + fallbackModelsEmpty: 'Tiada model fallback dikonfigurasikan', modelInputTypes: 'Jenis input', reasoning: 'Model penaakulan', manualModel: 'Input manual', @@ -768,6 +771,7 @@ const message = { configFileRestartHelper: 'Menyimpan fail konfigurasi memerlukan kontena dimulakan semula serta-merta untuk berkuat kuasa.', overviewSnapshot: 'Ringkasan', + primaryModel: 'Model utama', defaultModel: 'Model lalai', channelCount: 'Bilangan saluran dikonfigurasi', skillCount: 'Bilangan kemahiran', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index 1207afdff..48dc6e6c7 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -691,6 +691,9 @@ const message = { accountModelsRequired: 'Configure at least one model', accountModelsDuplicate: 'Duplicate models exist in the catalog', modelPool: 'Model Pool', + fallbackModels: 'Modelos de fallback', + addFallbackModel: 'Adicionar modelo de fallback', + fallbackModelsEmpty: 'Nenhum modelo de fallback configurado', modelInputTypes: 'Tipos de entrada', reasoning: 'Modelo de raciocínio', manualModel: 'Entrada manual', @@ -766,6 +769,7 @@ const message = { configFileRestartHelper: 'Salvar o arquivo de configuração exige reiniciar imediatamente o contêiner para entrar em vigor.', overviewSnapshot: 'Visão geral', + primaryModel: 'Modelo principal', defaultModel: 'Modelo padrão', channelCount: 'Canais configurados', skillCount: 'Habilidades', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index cd52ad233..0b748db16 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -688,6 +688,9 @@ const message = { accountModelsRequired: 'Configure at least one model', accountModelsDuplicate: 'Duplicate models exist in the catalog', modelPool: 'Model Pool', + fallbackModels: 'Резервные модели', + addFallbackModel: 'Добавить резервную модель', + fallbackModelsEmpty: 'Резервные модели не настроены', modelInputTypes: 'Input Types', reasoning: 'Reasoning Model', manualModel: 'Ручной ввод', @@ -760,6 +763,7 @@ const message = { configFileRestartHelper: 'Сохранение файла конфигурации требует немедленного перезапуска контейнера для применения.', overviewSnapshot: 'Сводка', + primaryModel: 'Основная модель', defaultModel: 'Модель по умолчанию', channelCount: 'Настроенные каналы', skillCount: 'Навыки', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 31a502c21..1fc3841ae 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -692,6 +692,9 @@ const message = { accountModelsRequired: 'Configure at least one model', accountModelsDuplicate: 'Duplicate models exist in the catalog', modelPool: 'Model Pool', + fallbackModels: 'Yedek modeller', + addFallbackModel: 'Yedek model ekle', + fallbackModelsEmpty: 'Yapılandırılmış yedek model yok', modelInputTypes: 'Input Types', reasoning: 'Reasoning Model', manualModel: 'Manuel giriş', @@ -767,6 +770,7 @@ const message = { configFileRestartHelper: 'Yapılandırma dosyasını kaydetmek, etkin olması için konteynerin hemen yeniden başlatılmasını gerektirir.', overviewSnapshot: 'Özet', + primaryModel: 'Birincil model', defaultModel: 'Varsayılan model', channelCount: 'Yapılandırılmış kanallar', skillCount: 'Yetenekler', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index afdfbd37d..fe4ad0aa4 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -656,6 +656,9 @@ const message = { accountModelsRequired: '請至少配置一個模型', accountModelsDuplicate: '模型池中存在重複模型,請檢查後重試', modelPool: '模型池', + fallbackModels: '備用模型', + addFallbackModel: '新增備用模型', + fallbackModelsEmpty: '尚未配置備用模型', modelInputTypes: '輸入類型', reasoning: '推理模型', manualModel: '手動輸入模型', @@ -714,6 +717,7 @@ const message = { roleMarkdownRestartHelper: '保存當前全部 MD 檔案後,需要重新啟動容器才能生效。請選擇立即重啟或稍後重啟。', configFileRestartHelper: '保存配置檔後需要立即重新啟動容器才能生效。', overviewSnapshot: '狀態概覽', + primaryModel: '主模型', defaultModel: '預設模型', channelCount: '已配置頻道數量', skillCount: '技能數量', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 6d91eeb48..0cdf6095e 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -655,6 +655,9 @@ const message = { accountModelsRequired: '请至少配置一个模型', accountModelsDuplicate: '模型池中存在重复模型,请检查后重试', modelPool: '模型池', + fallbackModels: '备用模型', + addFallbackModel: '新增备用模型', + fallbackModelsEmpty: '尚未配置备用模型', modelInputTypes: '输入类型', reasoning: '推理模型', manualModel: '手动输入模型', @@ -709,6 +712,7 @@ const message = { roleMarkdownRestartHelper: '保存当前全部 MD 文件后,需要重启容器才能生效。请选择立即重启或稍后重启。', configFileRestartHelper: '保存配置文件后需要重启容器才能生效。', overviewSnapshot: '状态概览', + primaryModel: '主模型', defaultModel: '默认模型', channelCount: '已配置频道数量', skillCount: '技能数量', diff --git a/frontend/src/views/ai/agents/agent/config/index.vue b/frontend/src/views/ai/agents/agent/config/index.vue index f2d3e31b3..180431b82 100644 --- a/frontend/src/views/ai/agents/agent/config/index.vue +++ b/frontend/src/views/ai/agents/agent/config/index.vue @@ -68,8 +68,6 @@ const loadModel = async () => { await nextTick(); await modelRef.value?.load({ agentId: agentId.value, - accountId: accountId.value, - model: model.value, }); }; diff --git a/frontend/src/views/ai/agents/agent/config/tabs/channels/components/channel-bots.vue b/frontend/src/views/ai/agents/agent/config/tabs/channels/components/channel-bots.vue index 07dd7acbe..cdbaf7994 100644 --- a/frontend/src/views/ai/agents/agent/config/tabs/channels/components/channel-bots.vue +++ b/frontend/src/views/ai/agents/agent/config/tabs/channels/components/channel-bots.vue @@ -214,7 +214,7 @@ const props = defineProps({ const emit = defineEmits<{ (e: 'update:bots', bots: ChannelBotItem[]): void; (e: 'approve', bot: ChannelBotItem): void; - (e: 'save'): void; + (e: 'save', action?: 'delete' | 'save'): void; }>(); const { t } = useI18n(); @@ -306,7 +306,7 @@ const saveBot = async () => { } emitBots(nextBots); dialogVisible.value = false; - emit('save'); + emit('save', 'save'); }; const removeBot = (index: number) => { @@ -316,7 +316,7 @@ const removeBot = (index: number) => { return; } emitBots(nextBots); - emit('save'); + emit('save', 'delete'); }; const setDefault = (accountId: string) => { @@ -325,14 +325,14 @@ const setDefault = (accountId: string) => { isDefault: bot.accountId === accountId, })); emitBots(nextBots); - emit('save'); + emit('save', 'save'); }; const updateEnabled = (index: number, enabled: boolean | string | number) => { const nextBots = props.bots.map((bot) => ({ ...bot })); nextBots[index].enabled = Boolean(enabled); emitBots(nextBots); - emit('save'); + emit('save', 'save'); }; diff --git a/frontend/src/views/ai/agents/agent/config/tabs/channels/dingtalk.vue b/frontend/src/views/ai/agents/agent/config/tabs/channels/dingtalk.vue index 848168dad..1af85311c 100644 --- a/frontend/src/views/ai/agents/agent/config/tabs/channels/dingtalk.vue +++ b/frontend/src/views/ai/agents/agent/config/tabs/channels/dingtalk.vue @@ -253,7 +253,7 @@ const reload = async () => { await load(agentId.value); }; -const saveChannel = async () => { +const saveChannel = async (action: 'delete' | 'save' = 'save') => { if (!supported.value || !agentId.value || !formRef.value) { return; } @@ -278,7 +278,7 @@ const saveChannel = async () => { ackText: form.ackText, bots: form.bots, }); - MsgSuccess(t('aiTools.agents.saveSuccess')); + MsgSuccess(action === 'delete' ? t('commons.msg.deleteSuccess') : t('aiTools.agents.saveSuccess')); } finally { saving.value = false; } diff --git a/frontend/src/views/ai/agents/agent/config/tabs/channels/discord.vue b/frontend/src/views/ai/agents/agent/config/tabs/channels/discord.vue index 731a670e0..a5fbe92bb 100644 --- a/frontend/src/views/ai/agents/agent/config/tabs/channels/discord.vue +++ b/frontend/src/views/ai/agents/agent/config/tabs/channels/discord.vue @@ -115,7 +115,7 @@ const load = async (id: number) => { form.bots = res.data?.bots || []; }; -const saveChannel = async () => { +const saveChannel = async (action: 'delete' | 'save' = 'save') => { if (!agentId.value || !formRef.value) { return; } @@ -135,7 +135,7 @@ const saveChannel = async () => { defaultAccount: form.defaultAccount, bots: form.bots, }); - MsgSuccess(t('aiTools.agents.saveSuccess')); + MsgSuccess(action === 'delete' ? t('commons.msg.deleteSuccess') : t('aiTools.agents.saveSuccess')); } finally { saving.value = false; } diff --git a/frontend/src/views/ai/agents/agent/config/tabs/channels/feishu.vue b/frontend/src/views/ai/agents/agent/config/tabs/channels/feishu.vue index 94d6e83ef..1a8c815d0 100644 --- a/frontend/src/views/ai/agents/agent/config/tabs/channels/feishu.vue +++ b/frontend/src/views/ai/agents/agent/config/tabs/channels/feishu.vue @@ -283,7 +283,7 @@ const validateBots = () => { return true; }; -const saveChannel = async () => { +const saveChannel = async (action: 'delete' | 'save' = 'save') => { if (!agentId.value || !formRef.value) { return; } @@ -308,7 +308,7 @@ const saveChannel = async () => { groupAllowFrom: parseTextList(form.groupAllowFromText), bots: buildPayloadBots(), }); - MsgSuccess(t('aiTools.agents.saveSuccess')); + MsgSuccess(action === 'delete' ? t('commons.msg.deleteSuccess') : t('aiTools.agents.saveSuccess')); } finally { saving.value = false; } diff --git a/frontend/src/views/ai/agents/agent/config/tabs/channels/qq.vue b/frontend/src/views/ai/agents/agent/config/tabs/channels/qq.vue index 8767829c5..ed75ed1c5 100644 --- a/frontend/src/views/ai/agents/agent/config/tabs/channels/qq.vue +++ b/frontend/src/views/ai/agents/agent/config/tabs/channels/qq.vue @@ -162,7 +162,7 @@ const reload = async () => { await load(agentId.value); }; -const saveChannel = async () => { +const saveChannel = async (action: 'delete' | 'save' = 'save') => { if (!agentId.value) { return; } @@ -186,7 +186,7 @@ const saveChannel = async () => { systemPrompt: bot.systemPrompt, })), }); - MsgSuccess(t('aiTools.agents.saveSuccess')); + MsgSuccess(action === 'delete' ? t('commons.msg.deleteSuccess') : t('aiTools.agents.saveSuccess')); } finally { saving.value = false; } diff --git a/frontend/src/views/ai/agents/agent/config/tabs/channels/telegram.vue b/frontend/src/views/ai/agents/agent/config/tabs/channels/telegram.vue index 1e8be0672..1aba1276b 100644 --- a/frontend/src/views/ai/agents/agent/config/tabs/channels/telegram.vue +++ b/frontend/src/views/ai/agents/agent/config/tabs/channels/telegram.vue @@ -244,7 +244,7 @@ const load = async (id: number) => { form.bots = res.data?.bots || []; }; -const saveChannel = async () => { +const saveChannel = async (action: 'delete' | 'save' = 'save') => { if (!agentId.value || !formRef.value) { return; } @@ -267,7 +267,7 @@ const saveChannel = async () => { defaultAccount: form.defaultAccount, bots: form.bots, }); - MsgSuccess(t('aiTools.agents.saveSuccess')); + MsgSuccess(action === 'delete' ? t('commons.msg.deleteSuccess') : t('aiTools.agents.saveSuccess')); } finally { saving.value = false; } diff --git a/frontend/src/views/ai/agents/agent/config/tabs/model.vue b/frontend/src/views/ai/agents/agent/config/tabs/model.vue index bb8a1a0f4..c2ecac4b6 100644 --- a/frontend/src/views/ai/agents/agent/config/tabs/model.vue +++ b/frontend/src/views/ai/agents/agent/config/tabs/model.vue @@ -5,12 +5,56 @@ - - + + {{ t('aiTools.agents.accountModelsHelper') }} + +
+
+ + + + + {{ t('aiTools.agents.addFallbackModel') }} + +
+ + + + + + + + + +
+
{{ t('commons.button.save') }} @@ -20,11 +64,12 @@ + +