From 77938b736d4e30450b29cb776dabcfd26c4fe51a Mon Sep 17 00:00:00 2001 From: zhengkunwang223 <1paneldev@sina.com> Date: Wed, 11 Feb 2026 14:47:52 +0800 Subject: [PATCH] feat: add token reset for agent --- agent/app/api/v2/agents.go | 20 +++ agent/app/dto/agents.go | 4 + agent/app/service/agents.go | 45 ++++- agent/init/migration/migrations/init.go | 55 +++--- .../migrations/utils/openclaw_agent.go | 164 ++++++++++++++++++ agent/router/ro_ai.go | 1 + frontend/src/api/interface/ai.ts | 4 + frontend/src/api/modules/ai.ts | 4 + .../ai/agents/agent/config/tabs/channels.vue | 9 + frontend/src/views/ai/agents/agent/index.vue | 27 ++- 10 files changed, 301 insertions(+), 32 deletions(-) create mode 100644 agent/init/migration/migrations/utils/openclaw_agent.go diff --git a/agent/app/api/v2/agents.go b/agent/app/api/v2/agents.go index e59050a9b219..f13d3b33a20a 100644 --- a/agent/app/api/v2/agents.go +++ b/agent/app/api/v2/agents.go @@ -71,6 +71,26 @@ func (b *BaseApi) DeleteAgent(c *gin.Context) { helper.Success(c) } +// @Tags AI +// @Summary Reset Agent token +// @Accept json +// @Param request body dto.AgentTokenResetReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/token/reset [post] +func (b *BaseApi) ResetAgentToken(c *gin.Context) { + var req dto.AgentTokenResetReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := agentService.ResetToken(req); err != nil { + helper.BadRequest(c, err) + return + } + helper.Success(c) +} + // @Tags AI // @Summary Update Agent model config // @Accept json diff --git a/agent/app/dto/agents.go b/agent/app/dto/agents.go index a1a9f1495711..f78741c327c7 100644 --- a/agent/app/dto/agents.go +++ b/agent/app/dto/agents.go @@ -56,6 +56,10 @@ type AgentDeleteReq struct { ForceDelete bool `json:"forceDelete"` } +type AgentTokenResetReq struct { + ID uint `json:"id" validate:"required"` +} + type AgentModelConfigUpdateReq struct { AgentID uint `json:"agentId" validate:"required"` AccountID uint `json:"accountId" validate:"required"` diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go index a36cb52bd3fa..0d0dbc24a935 100644 --- a/agent/app/service/agents.go +++ b/agent/app/service/agents.go @@ -33,6 +33,7 @@ type IAgentService interface { Create(req dto.AgentCreateReq) (*dto.AgentItem, error) Page(req dto.SearchWithPage) (int64, []dto.AgentItem, error) Delete(req dto.AgentDeleteReq) error + ResetToken(req dto.AgentTokenResetReq) error UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error GetProviders() ([]dto.ProviderInfo, error) CreateAccount(req dto.AgentAccountCreateReq) error @@ -103,7 +104,7 @@ func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { token := strings.TrimSpace(req.Token) if token == "" { - token = randomToken() + token = generateToken() } params := map[string]interface{}{ "PROVIDER": provider, @@ -211,6 +212,46 @@ func (a AgentService) Delete(req dto.AgentDeleteReq) error { return nil } +func (a AgentService) ResetToken(req dto.AgentTokenResetReq) error { + agent, err := agentRepo.GetFirst(repo.WithByID(req.ID)) + if err != nil { + return err + } + configPath := strings.TrimSpace(agent.ConfigPath) + if configPath == "" && agent.AppInstallID > 0 { + install, err := appInstallRepo.GetFirst(repo.WithByID(agent.AppInstallID)) + if err != nil { + return err + } + configPath = path.Join(install.GetPath(), "data", "conf", "openclaw.json") + } + if configPath == "" { + return buserr.New("ErrRecordNotFound") + } + conf, err := readOpenclawConfig(configPath) + if err != nil { + return err + } + newToken := generateToken() + if newToken == "" { + return fmt.Errorf("generate token failed") + } + gatewayMap := ensureChildMap(conf, "gateway") + authMap := ensureChildMap(gatewayMap, "auth") + if _, ok := authMap["mode"]; !ok { + authMap["mode"] = "token" + } + authMap["token"] = newToken + if err := writeOpenclawConfigRaw(configPath, conf); err != nil { + return err + } + agent.Token = newToken + if agent.ConfigPath == "" { + agent.ConfigPath = configPath + } + return agentRepo.Save(agent) +} + func (a AgentService) UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error { agent, err := agentRepo.GetFirst(repo.WithByID(req.AgentID)) if err != nil { @@ -1226,7 +1267,7 @@ func toInt(value interface{}) int { } } -func randomToken() string { +func generateToken() string { bytes := make([]byte, 24) if _, err := rand.Read(bytes); err != nil { return "" diff --git a/agent/init/migration/migrations/init.go b/agent/init/migration/migrations/init.go index 53fb16ec715b..975f38abe6a5 100644 --- a/agent/init/migration/migrations/init.go +++ b/agent/init/migration/migrations/init.go @@ -14,10 +14,10 @@ import ( "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/service" "github.com/1Panel-dev/1Panel/agent/constant" "github.com/1Panel-dev/1Panel/agent/global" + migrationutils "github.com/1Panel-dev/1Panel/agent/init/migration/migrations/utils" "github.com/1Panel-dev/1Panel/agent/utils/common" "github.com/1Panel-dev/1Panel/agent/utils/copier" "github.com/1Panel-dev/1Panel/agent/utils/encrypt" @@ -864,17 +864,38 @@ var MigrateOpenclawAgents = &gormigrate.Migration{ if strings.TrimSpace(install.Env) != "" { _ = json.Unmarshal([]byte(install.Env), &envMap) } - provider := strings.ToLower(getEnvStr(envMap, "PROVIDER")) + 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 } - modelName := getEnvStr(envMap, "MODEL") - baseURL := getEnvStr(envMap, "BASE_URL") - apiKey := getEnvStr(envMap, "API_KEY") - token := getEnvStr(envMap, "OPENCLAW_GATEWAY_TOKEN") + 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 := defaultBaseURL(provider); ok { + if defaultURL, ok := migrationutils.DefaultBaseURL(provider); ok { baseURL = defaultURL } } @@ -900,7 +921,6 @@ var MigrateOpenclawAgents = &gormigrate.Migration{ return err } } - configPath := path.Join(install.GetPath(), "data", "conf", "openclaw.json") agent := model.Agent{ Name: install.Name, Provider: provider, @@ -921,22 +941,3 @@ var MigrateOpenclawAgents = &gormigrate.Migration{ return nil }, } - -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 defaultBaseURL(provider string) (string, bool) { - return providercatalog.DefaultBaseURL(provider) -} diff --git a/agent/init/migration/migrations/utils/openclaw_agent.go b/agent/init/migration/migrations/utils/openclaw_agent.go new file mode 100644 index 000000000000..8d1ef013c907 --- /dev/null +++ b/agent/init/migration/migrations/utils/openclaw_agent.go @@ -0,0 +1,164 @@ +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 "minimax-portal": + return "minimax" + case "moonshot": + if strings.Contains(base, "moonshot.cn") { + return "kimi" + } + return "moonshot" + default: + return p + } +} + +func DefaultBaseURL(provider string) (string, bool) { + return providercatalog.DefaultBaseURL(provider) +} + +func getNestedMap(data map[string]interface{}, keys ...string) map[string]interface{} { + current := data + for _, key := range keys { + next, ok := current[key].(map[string]interface{}) + if !ok { + return map[string]interface{}{} + } + current = next + } + return current +} + +func getNestedString(data map[string]interface{}, keys ...string) string { + current := data + for i, key := range keys { + value, ok := current[key] + if !ok { + return "" + } + if i == len(keys)-1 { + switch v := value.(type) { + case string: + return strings.TrimSpace(v) + default: + return strings.TrimSpace(fmt.Sprintf("%v", v)) + } + } + next, ok := value.(map[string]interface{}) + if !ok { + return "" + } + current = next + } + return "" +} + +func getString(data map[string]interface{}, key string) string { + if data == nil { + return "" + } + value, ok := data[key] + if !ok { + return "" + } + switch v := value.(type) { + case string: + return strings.TrimSpace(v) + default: + return strings.TrimSpace(fmt.Sprintf("%v", v)) + } +} + +func getProviderFirstModelID(data map[string]interface{}) string { + if data == nil { + return "" + } + rawModels, ok := data["models"].([]interface{}) + if !ok || len(rawModels) == 0 { + return "" + } + first, ok := rawModels[0].(map[string]interface{}) + if !ok { + return "" + } + return getString(first, "id") +} diff --git a/agent/router/ro_ai.go b/agent/router/ro_ai.go index 1e76d4007ce1..e426a58fb09b 100644 --- a/agent/router/ro_ai.go +++ b/agent/router/ro_ai.go @@ -43,6 +43,7 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) { aiToolsRouter.POST("/agents", baseApi.CreateAgent) aiToolsRouter.POST("/agents/search", baseApi.PageAgents) aiToolsRouter.POST("/agents/delete", baseApi.DeleteAgent) + aiToolsRouter.POST("/agents/token/reset", baseApi.ResetAgentToken) aiToolsRouter.POST("/agents/model/update", baseApi.UpdateAgentModelConfig) aiToolsRouter.GET("/agents/providers", baseApi.GetAgentProviders) aiToolsRouter.POST("/agents/accounts", baseApi.CreateAgentAccount) diff --git a/frontend/src/api/interface/ai.ts b/frontend/src/api/interface/ai.ts index 7deab24befed..22d266ea58bf 100644 --- a/frontend/src/api/interface/ai.ts +++ b/frontend/src/api/interface/ai.ts @@ -290,6 +290,10 @@ export namespace AI { forceDelete: boolean; } + export interface AgentTokenResetReq { + id: number; + } + export interface AgentModelConfigUpdateReq { agentId: number; accountId: number; diff --git a/frontend/src/api/modules/ai.ts b/frontend/src/api/modules/ai.ts index d5cba785cc89..58373fb978fd 100644 --- a/frontend/src/api/modules/ai.ts +++ b/frontend/src/api/modules/ai.ts @@ -104,6 +104,10 @@ export const deleteAgent = (req: AI.AgentDeleteReq) => { return http.post(`/ai/agents/delete`, req); }; +export const resetAgentToken = (req: AI.AgentTokenResetReq) => { + return http.post(`/ai/agents/token/reset`, req); +}; + export const updateAgentModelConfig = (req: AI.AgentModelConfigUpdateReq) => { return http.post(`/ai/agents/model/update`, req); }; diff --git a/frontend/src/views/ai/agents/agent/config/tabs/channels.vue b/frontend/src/views/ai/agents/agent/config/tabs/channels.vue index e7bcd2d7db27..a958bd121726 100644 --- a/frontend/src/views/ai/agents/agent/config/tabs/channels.vue +++ b/frontend/src/views/ai/agents/agent/config/tabs/channels.vue @@ -3,6 +3,11 @@ + + + {{ t('container.mirrorsHelper2') }} + + @@ -67,6 +72,10 @@ const rules = reactive({ appSecret: [Rules.requiredInput], }); +const toFeishuDoc = () => { + window.open('https://openclaw.club/guides/feishu-platform', '_blank'); +}; + const load = async (id: number) => { agentId.value = id; pairingCode.value = ''; diff --git a/frontend/src/views/ai/agents/agent/index.vue b/frontend/src/views/ai/agents/agent/index.vue index 39ba592dc025..b7d1da5fa25a 100644 --- a/frontend/src/views/ai/agents/agent/index.vue +++ b/frontend/src/views/ai/agents/agent/index.vue @@ -58,7 +58,12 @@ import { onMounted, reactive, ref } from 'vue'; -import { pageAgents } from '@/api/modules/ai'; +import { pageAgents, resetAgentToken } from '@/api/modules/ai'; import { installedOp, searchAppInstalled } from '@/api/modules/app'; import { AI } from '@/api/interface/ai'; import { App } from '@/api/interface/app'; import { SearchWithPage } from '@/api/interface'; import { dateFormat, newUUID } from '@/utils/util'; +import { MsgSuccess } from '@/utils/message'; import RouterMenu from '@/views/ai/agents/index.vue'; import AddDialog from '@/views/ai/agents/agent/add/index.vue'; @@ -242,12 +248,27 @@ const onDelete = (row: AI.AgentItem) => { deleteRef.value?.acceptParams(row.id, row.name); }; +const onResetToken = async (row: AI.AgentItem) => { + await ElMessageBox.confirm( + i18n.global.t('aiTools.mcp.operatorHelper', ['token', i18n.global.t('commons.button.reset')]), + i18n.global.t('commons.button.reset'), + { + confirmButtonText: i18n.global.t('commons.button.confirm'), + cancelButtonText: i18n.global.t('commons.button.cancel'), + type: 'info', + }, + ); + await resetAgentToken({ id: row.id }); + MsgSuccess(i18n.global.t('commons.msg.operationSuccess')); + await search(); +}; + const openConfig = (row: AI.AgentItem) => { configRef.value?.open(row); }; const openUpgrade = async (row: AI.AgentItem) => { - const res = await searchAppInstalled({ page: 1, pageSize: 200, name: row.name }); + const res = await searchAppInstalled({ page: 1, pageSize: 200, name: row.name, update: true }); const appInstall = (res.data.items || []).find((item: App.AppInstallDto) => item.id === row.appInstallId); if (!appInstall) { return;