From f0f2087f5437f8bb09c9df9c2453fa0d19cbfbd1 Mon Sep 17 00:00:00 2001 From: CityFun <31820853+zhengkunwang223@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:59:48 +0800 Subject: [PATCH] feat: add browser config for Agent (#11958) --- agent/app/api/v2/agents.go | 41 +++++++ agent/app/dto/agents.go | 20 ++++ agent/app/service/agents.go | 108 ++++++++++++++++++ agent/router/ro_ai.go | 2 + frontend/src/api/interface/ai.ts | 20 ++++ frontend/src/api/modules/ai.ts | 8 ++ frontend/src/lang/modules/en.ts | 7 ++ frontend/src/lang/modules/es-es.ts | 7 ++ frontend/src/lang/modules/ja.ts | 7 ++ frontend/src/lang/modules/ko.ts | 7 ++ frontend/src/lang/modules/ms.ts | 7 ++ frontend/src/lang/modules/pt-br.ts | 7 ++ frontend/src/lang/modules/ru.ts | 7 ++ frontend/src/lang/modules/tr.ts | 7 ++ frontend/src/lang/modules/zh-Hant.ts | 7 ++ frontend/src/lang/modules/zh.ts | 7 ++ .../src/views/ai/agents/agent/add/index.vue | 7 +- .../views/ai/agents/agent/config/index.vue | 24 +++- .../ai/agents/agent/config/tabs/settings.vue | 42 +++++++ .../agent/config/tabs/settings/browser.vue | 87 ++++++++++++++ 20 files changed, 422 insertions(+), 7 deletions(-) create mode 100644 frontend/src/views/ai/agents/agent/config/tabs/settings.vue create mode 100644 frontend/src/views/ai/agents/agent/config/tabs/settings/browser.vue diff --git a/agent/app/api/v2/agents.go b/agent/app/api/v2/agents.go index 941982ac0..96b814154 100644 --- a/agent/app/api/v2/agents.go +++ b/agent/app/api/v2/agents.go @@ -353,6 +353,47 @@ func (b *BaseApi) UpdateAgentDiscordConfig(c *gin.Context) { helper.Success(c) } +// @Tags AI +// @Summary Get Agent Browser config +// @Accept json +// @Param request body dto.AgentBrowserConfigReq true "request" +// @Success 200 {object} dto.AgentBrowserConfig +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/browser/get [post] +func (b *BaseApi) GetAgentBrowserConfig(c *gin.Context) { + var req dto.AgentBrowserConfigReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + data, err := agentService.GetBrowserConfig(req) + if err != nil { + helper.BadRequest(c, err) + return + } + helper.SuccessWithData(c, data) +} + +// @Tags AI +// @Summary Update Agent Browser config +// @Accept json +// @Param request body dto.AgentBrowserConfigUpdateReq true "request" +// @Success 200 +// @Security ApiKeyAuth +// @Security Timestamp +// @Router /ai/agents/browser/update [post] +func (b *BaseApi) UpdateAgentBrowserConfig(c *gin.Context) { + var req dto.AgentBrowserConfigUpdateReq + if err := helper.CheckBindAndValidate(&req, c); err != nil { + return + } + if err := agentService.UpdateBrowserConfig(req); err != nil { + helper.BadRequest(c, err) + return + } + helper.Success(c) +} + // @Tags AI // @Summary Approve Agent Feishu pairing code // @Accept json diff --git a/agent/app/dto/agents.go b/agent/app/dto/agents.go index 0dad9f993..bafbaf14f 100644 --- a/agent/app/dto/agents.go +++ b/agent/app/dto/agents.go @@ -216,3 +216,23 @@ type AgentDiscordConfig struct { Token string `json:"token"` Proxy string `json:"proxy"` } + +type AgentBrowserConfigReq struct { + AgentID uint `json:"agentId" validate:"required"` +} + +type AgentBrowserConfigUpdateReq struct { + AgentID uint `json:"agentId" validate:"required"` + Enabled bool `json:"enabled"` + Headless bool `json:"headless"` + NoSandbox bool `json:"noSandbox"` + DefaultProfile string `json:"defaultProfile" validate:"required"` +} + +type AgentBrowserConfig struct { + Enabled bool `json:"enabled"` + ExecutablePath string `json:"executablePath"` + Headless bool `json:"headless"` + NoSandbox bool `json:"noSandbox"` + DefaultProfile string `json:"defaultProfile"` +} diff --git a/agent/app/service/agents.go b/agent/app/service/agents.go index 7ad9670e1..c5ed472c1 100644 --- a/agent/app/service/agents.go +++ b/agent/app/service/agents.go @@ -47,6 +47,8 @@ type IAgentService interface { UpdateTelegramConfig(req dto.AgentTelegramConfigUpdateReq) error GetDiscordConfig(req dto.AgentDiscordConfigReq) (*dto.AgentDiscordConfig, error) UpdateDiscordConfig(req dto.AgentDiscordConfigUpdateReq) error + GetBrowserConfig(req dto.AgentBrowserConfigReq) (*dto.AgentBrowserConfig, error) + UpdateBrowserConfig(req dto.AgentBrowserConfigUpdateReq) error ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error ApproveFeishuPairing(req dto.AgentFeishuPairingApproveReq) error } @@ -55,6 +57,11 @@ func NewIAgentService() IAgentService { return &AgentService{} } +const ( + defaultBrowserExecutablePath = "/home/node/.cache/ms-playwright/chromium-1208/chrome-linux64/chrome" + defaultBrowserProfile = "openclaw" +) + func (a AgentService) Create(req dto.AgentCreateReq) (*dto.AgentItem, error) { provider := strings.ToLower(strings.TrimSpace(req.Provider)) if !isSupportedAgentProvider(provider) { @@ -653,6 +660,41 @@ func (a AgentService) UpdateDiscordConfig(req dto.AgentDiscordConfigUpdateReq) e return nil } +func (a AgentService) GetBrowserConfig(req dto.AgentBrowserConfigReq) (*dto.AgentBrowserConfig, error) { + agent, _, err := a.loadAgentAndInstall(req.AgentID) + if err != nil { + return nil, err + } + conf, err := readOpenclawConfig(agent.ConfigPath) + if err != nil { + return nil, err + } + result := extractBrowserConfig(conf) + return &result, nil +} + +func (a AgentService) UpdateBrowserConfig(req dto.AgentBrowserConfigUpdateReq) error { + agent, _, err := a.loadAgentAndInstall(req.AgentID) + if err != nil { + return err + } + conf, err := readOpenclawConfig(agent.ConfigPath) + if err != nil { + return err + } + setBrowserConfig(conf, dto.AgentBrowserConfig{ + Enabled: req.Enabled, + ExecutablePath: defaultBrowserExecutablePath, + Headless: req.Headless, + NoSandbox: req.NoSandbox, + DefaultProfile: strings.TrimSpace(req.DefaultProfile), + }) + if err := writeOpenclawConfigRaw(agent.ConfigPath, conf); err != nil { + return err + } + return nil +} + func (a AgentService) ApproveChannelPairing(req dto.AgentChannelPairingApproveReq) error { _, install, err := a.loadAgentAndInstall(req.AgentID) if err != nil { @@ -887,6 +929,49 @@ func setDiscordConfig(conf map[string]interface{}, config dto.AgentDiscordConfig delete(discord, "dm") } +func extractBrowserConfig(conf map[string]interface{}) dto.AgentBrowserConfig { + result := dto.AgentBrowserConfig{ + Enabled: true, + ExecutablePath: defaultBrowserExecutablePath, + Headless: true, + NoSandbox: true, + DefaultProfile: defaultBrowserProfile, + } + browser, ok := conf["browser"].(map[string]interface{}) + if !ok { + return result + } + if enabled, ok := browser["enabled"].(bool); ok { + result.Enabled = enabled + } + if executablePath, ok := browser["executablePath"].(string); ok && strings.TrimSpace(executablePath) != "" { + result.ExecutablePath = executablePath + } + if headless, ok := browser["headless"].(bool); ok { + result.Headless = headless + } + if noSandbox, ok := browser["noSandbox"].(bool); ok { + result.NoSandbox = noSandbox + } + if defaultProfile, ok := browser["defaultProfile"].(string); ok && strings.TrimSpace(defaultProfile) != "" { + result.DefaultProfile = defaultProfile + } + return result +} + +func setBrowserConfig(conf map[string]interface{}, config dto.AgentBrowserConfig) { + browser := ensureChildMap(conf, "browser") + browser["enabled"] = config.Enabled + browser["executablePath"] = defaultBrowserExecutablePath + browser["headless"] = config.Headless + browser["noSandbox"] = config.NoSandbox + if strings.TrimSpace(config.DefaultProfile) == "" { + browser["defaultProfile"] = defaultBrowserProfile + } else { + browser["defaultProfile"] = strings.TrimSpace(config.DefaultProfile) + } +} + func (a AgentService) syncAgentsByAccount(account *model.AgentAccount) error { agents, err := agentRepo.List(repo.WithByAccountID(account.ID)) if err != nil { @@ -1112,6 +1197,7 @@ func (a AgentService) writeConfigWithRetry(appInstall *model.AppInstall, provide type openclawConfig struct { Gateway gatewayConfig `json:"gateway"` Agents agentsConfig `json:"agents"` + Browser browserConfig `json:"browser"` Models *modelsConfig `json:"models,omitempty"` } @@ -1174,6 +1260,14 @@ type modelCost struct { CacheWrite float64 `json:"cacheWrite"` } +type browserConfig struct { + Enabled bool `json:"enabled"` + ExecutablePath string `json:"executablePath"` + Headless bool `json:"headless"` + NoSandbox bool `json:"noSandbox"` + DefaultProfile string `json:"defaultProfile"` +} + func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens, contextWindow int, baseURL, apiKey, token string) error { if strings.TrimSpace(confDir) == "" { return fmt.Errorf("config dir is required") @@ -1210,6 +1304,13 @@ func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens Model: modelRef{Primary: modelName}, }, }, + Browser: browserConfig{ + Enabled: true, + ExecutablePath: defaultBrowserExecutablePath, + Headless: true, + NoSandbox: true, + DefaultProfile: defaultBrowserProfile, + }, } provider = strings.ToLower(strings.TrimSpace(provider)) @@ -1431,6 +1532,13 @@ func writeOpenclawConfig(confDir, provider, modelName, apiType string, maxTokens } conf["models"] = modelsMap } + if _, ok := conf["browser"]; !ok { + browserMap, err := structToMap(cfg.Browser) + if err != nil { + return err + } + conf["browser"] = browserMap + } agentsMap := ensureChildMap(conf, "agents") defaultsMap := ensureChildMap(agentsMap, "defaults") modelMap := ensureChildMap(defaultsMap, "model") diff --git a/agent/router/ro_ai.go b/agent/router/ro_ai.go index 5e30ee5f3..e29c8def8 100644 --- a/agent/router/ro_ai.go +++ b/agent/router/ro_ai.go @@ -58,6 +58,8 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) { aiToolsRouter.POST("/agents/channel/telegram/update", baseApi.UpdateAgentTelegramConfig) aiToolsRouter.POST("/agents/channel/discord/get", baseApi.GetAgentDiscordConfig) aiToolsRouter.POST("/agents/channel/discord/update", baseApi.UpdateAgentDiscordConfig) + aiToolsRouter.POST("/agents/browser/get", baseApi.GetAgentBrowserConfig) + aiToolsRouter.POST("/agents/browser/update", baseApi.UpdateAgentBrowserConfig) aiToolsRouter.POST("/agents/channel/pairing/approve", baseApi.ApproveAgentChannelPairing) } } diff --git a/frontend/src/api/interface/ai.ts b/frontend/src/api/interface/ai.ts index d3df72c64..ca048068e 100644 --- a/frontend/src/api/interface/ai.ts +++ b/frontend/src/api/interface/ai.ts @@ -450,4 +450,24 @@ export namespace AI { token: string; proxy: string; } + + export interface AgentBrowserConfigReq { + agentId: number; + } + + export interface AgentBrowserConfig { + enabled: boolean; + executablePath: string; + headless: boolean; + noSandbox: boolean; + defaultProfile: string; + } + + export interface AgentBrowserConfigUpdateReq { + agentId: number; + enabled: boolean; + headless: boolean; + noSandbox: boolean; + defaultProfile: string; + } } diff --git a/frontend/src/api/modules/ai.ts b/frontend/src/api/modules/ai.ts index 66955ca4b..eab64a743 100644 --- a/frontend/src/api/modules/ai.ts +++ b/frontend/src/api/modules/ai.ts @@ -164,6 +164,14 @@ export const updateAgentDiscordConfig = (req: AI.AgentDiscordConfigUpdateReq) => return http.post(`/ai/agents/channel/discord/update`, req); }; +export const getAgentBrowserConfig = (req: AI.AgentBrowserConfigReq) => { + return http.post(`/ai/agents/browser/get`, req); +}; + +export const updateAgentBrowserConfig = (req: AI.AgentBrowserConfigUpdateReq) => { + return http.post(`/ai/agents/browser/update`, req); +}; + export const approveAgentChannelPairing = (req: AI.AgentChannelPairingApproveReq) => { return http.post(`/ai/agents/channel/pairing/approve`, req); }; diff --git a/frontend/src/lang/modules/en.ts b/frontend/src/lang/modules/en.ts index ef0031d42..2d2a3694c 100644 --- a/frontend/src/lang/modules/en.ts +++ b/frontend/src/lang/modules/en.ts @@ -698,6 +698,13 @@ const message = { manualModel: 'Manual input', verified: 'Verified', configTitle: 'Configuration', + settingsTab: 'Settings', + browserTab: 'Browser', + browserEnabled: 'Browser Enabled', + headless: 'Headless', + noSandbox: 'No Sandbox', + defaultProfile: 'Default Profile', + executablePath: 'Executable Path', switchModelSuccess: 'Model switched successfully', channelsTab: 'Channels', feishu: 'Feishu', diff --git a/frontend/src/lang/modules/es-es.ts b/frontend/src/lang/modules/es-es.ts index 8f9e440c4..83fbf8190 100644 --- a/frontend/src/lang/modules/es-es.ts +++ b/frontend/src/lang/modules/es-es.ts @@ -694,6 +694,13 @@ const message = { manualModel: 'Entrada manual de modelo', verified: 'Verificado', configTitle: 'Configuration', + settingsTab: 'Settings', + browserTab: 'Browser', + browserEnabled: 'Browser Enabled', + headless: 'Headless', + noSandbox: 'No Sandbox', + defaultProfile: 'Default Profile', + executablePath: 'Executable Path', switchModelSuccess: 'Model switched successfully', channelsTab: 'Channels', feishu: 'Feishu', diff --git a/frontend/src/lang/modules/ja.ts b/frontend/src/lang/modules/ja.ts index e6674a88b..84eb4ad14 100644 --- a/frontend/src/lang/modules/ja.ts +++ b/frontend/src/lang/modules/ja.ts @@ -683,6 +683,13 @@ const message = { manualModel: '手動入力', verified: '検証済み', configTitle: 'Configuration', + settingsTab: 'Settings', + browserTab: 'Browser', + browserEnabled: 'Browser Enabled', + headless: 'Headless', + noSandbox: 'No Sandbox', + defaultProfile: 'Default Profile', + executablePath: 'Executable Path', switchModelSuccess: 'Model switched successfully', channelsTab: 'Channels', feishu: 'Feishu', diff --git a/frontend/src/lang/modules/ko.ts b/frontend/src/lang/modules/ko.ts index bc9bab9fe..31ad8e51a 100644 --- a/frontend/src/lang/modules/ko.ts +++ b/frontend/src/lang/modules/ko.ts @@ -680,6 +680,13 @@ const message = { manualModel: '수동 입력', verified: '검증됨', configTitle: 'Configuration', + settingsTab: 'Settings', + browserTab: 'Browser', + browserEnabled: 'Browser Enabled', + headless: 'Headless', + noSandbox: 'No Sandbox', + defaultProfile: 'Default Profile', + executablePath: 'Executable Path', switchModelSuccess: 'Model switched successfully', channelsTab: 'Channels', feishu: 'Feishu', diff --git a/frontend/src/lang/modules/ms.ts b/frontend/src/lang/modules/ms.ts index efe76cecc..64cdf28d4 100644 --- a/frontend/src/lang/modules/ms.ts +++ b/frontend/src/lang/modules/ms.ts @@ -695,6 +695,13 @@ const message = { manualModel: 'Input manual', verified: 'Disahkan', configTitle: 'Configuration', + settingsTab: 'Settings', + browserTab: 'Browser', + browserEnabled: 'Browser Enabled', + headless: 'Headless', + noSandbox: 'No Sandbox', + defaultProfile: 'Default Profile', + executablePath: 'Executable Path', switchModelSuccess: 'Model switched successfully', channelsTab: 'Channels', feishu: 'Feishu', diff --git a/frontend/src/lang/modules/pt-br.ts b/frontend/src/lang/modules/pt-br.ts index a2d3354cf..016cbae17 100644 --- a/frontend/src/lang/modules/pt-br.ts +++ b/frontend/src/lang/modules/pt-br.ts @@ -692,6 +692,13 @@ const message = { manualModel: 'Entrada manual', verified: 'Verificado', configTitle: 'Configuration', + settingsTab: 'Settings', + browserTab: 'Browser', + browserEnabled: 'Browser Enabled', + headless: 'Headless', + noSandbox: 'No Sandbox', + defaultProfile: 'Default Profile', + executablePath: 'Executable Path', switchModelSuccess: 'Model switched successfully', channelsTab: 'Channels', feishu: 'Feishu', diff --git a/frontend/src/lang/modules/ru.ts b/frontend/src/lang/modules/ru.ts index 121bb64ea..9764fa310 100644 --- a/frontend/src/lang/modules/ru.ts +++ b/frontend/src/lang/modules/ru.ts @@ -688,6 +688,13 @@ const message = { manualModel: 'Ручной ввод', verified: 'Проверено', configTitle: 'Configuration', + settingsTab: 'Settings', + browserTab: 'Browser', + browserEnabled: 'Browser Enabled', + headless: 'Headless', + noSandbox: 'No Sandbox', + defaultProfile: 'Default Profile', + executablePath: 'Executable Path', switchModelSuccess: 'Model switched successfully', channelsTab: 'Channels', feishu: 'Feishu', diff --git a/frontend/src/lang/modules/tr.ts b/frontend/src/lang/modules/tr.ts index 5c8c3c0c3..9cde04251 100644 --- a/frontend/src/lang/modules/tr.ts +++ b/frontend/src/lang/modules/tr.ts @@ -702,6 +702,13 @@ const message = { manualModel: 'Manuel giriş', verified: 'Doğrulandı', configTitle: 'Configuration', + settingsTab: 'Settings', + browserTab: 'Browser', + browserEnabled: 'Browser Enabled', + headless: 'Headless', + noSandbox: 'No Sandbox', + defaultProfile: 'Default Profile', + executablePath: 'Executable Path', switchModelSuccess: 'Model switched successfully', channelsTab: 'Channels', feishu: 'Feishu', diff --git a/frontend/src/lang/modules/zh-Hant.ts b/frontend/src/lang/modules/zh-Hant.ts index c23e3c492..1b4e3d9b6 100644 --- a/frontend/src/lang/modules/zh-Hant.ts +++ b/frontend/src/lang/modules/zh-Hant.ts @@ -670,6 +670,13 @@ const message = { manualModel: '手動輸入模型', verified: '驗證狀態', configTitle: '配置', + settingsTab: '設定', + browserTab: '瀏覽器', + browserEnabled: '瀏覽器開關', + headless: '無頭模式', + noSandbox: '禁用沙箱', + defaultProfile: '預設配置檔', + executablePath: '瀏覽器可執行路徑', switchModelSuccess: '模型切換成功', channelsTab: '聊天渠道', feishu: '飛書', diff --git a/frontend/src/lang/modules/zh.ts b/frontend/src/lang/modules/zh.ts index 73f79c1ca..c2fb95fa5 100644 --- a/frontend/src/lang/modules/zh.ts +++ b/frontend/src/lang/modules/zh.ts @@ -672,6 +672,13 @@ const message = { manualModel: '手动输入模型', verified: '验证状态', configTitle: '配置', + settingsTab: '设置', + browserTab: '浏览器', + browserEnabled: '浏览器开关', + headless: '无头模式', + noSandbox: '禁用沙箱', + defaultProfile: '默认配置文件', + executablePath: '浏览器可执行路径', switchModelSuccess: '模型切换成功', channelsTab: '聊天渠道', feishu: '飞书', diff --git a/frontend/src/views/ai/agents/agent/add/index.vue b/frontend/src/views/ai/agents/agent/add/index.vue index 0eae8b8ab..7361648f6 100644 --- a/frontend/src/views/ai/agents/agent/add/index.vue +++ b/frontend/src/views/ai/agents/agent/add/index.vue @@ -39,9 +39,7 @@ - - {{ $t('aiTools.agents.manualModel') }} - + @@ -53,6 +51,9 @@ + + {{ $t('aiTools.agents.manualModel') }} + diff --git a/frontend/src/views/ai/agents/agent/config/index.vue b/frontend/src/views/ai/agents/agent/config/index.vue index 60264945d..c54abc92a 100644 --- a/frontend/src/views/ai/agents/agent/config/index.vue +++ b/frontend/src/views/ai/agents/agent/config/index.vue @@ -2,6 +2,9 @@