feat: add browser config for Agent (#11958)

This commit is contained in:
CityFun
2026-02-26 09:59:48 +00:00
committed by GitHub
parent f3de09e5d7
commit f0f2087f54
20 changed files with 422 additions and 7 deletions
+41
View File
@@ -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
+20
View File
@@ -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"`
}
+108
View File
@@ -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")
+2
View File
@@ -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)
}
}
+20
View File
@@ -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;
}
}
+8
View File
@@ -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.AgentBrowserConfig>(`/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);
};
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -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',
+7
View File
@@ -670,6 +670,13 @@ const message = {
manualModel: '手動輸入模型',
verified: '驗證狀態',
configTitle: '配置',
settingsTab: '設定',
browserTab: '瀏覽器',
browserEnabled: '瀏覽器開關',
headless: '無頭模式',
noSandbox: '禁用沙箱',
defaultProfile: '預設配置檔',
executablePath: '瀏覽器可執行路徑',
switchModelSuccess: '模型切換成功',
channelsTab: '聊天渠道',
feishu: '飛書',
+7
View File
@@ -672,6 +672,13 @@ const message = {
manualModel: '手动输入模型',
verified: '验证状态',
configTitle: '配置',
settingsTab: '设置',
browserTab: '浏览器',
browserEnabled: '浏览器开关',
headless: '无头模式',
noSandbox: '禁用沙箱',
defaultProfile: '默认配置文件',
executablePath: '浏览器可执行路径',
switchModelSuccess: '模型切换成功',
channelsTab: '聊天渠道',
feishu: '飞书',
@@ -39,9 +39,7 @@
</el-option>
</el-select>
</el-form-item>
<el-form-item>
<el-checkbox v-model="manualModel">{{ $t('aiTools.agents.manualModel') }}</el-checkbox>
</el-form-item>
<el-form-item :label="$t('aiTools.agents.account')" prop="accountId">
<el-select v-model="form.accountId" @change="handleAccountChange">
<el-option v-for="item in accountOptions" :key="item.id" :label="item.name" :value="item.id" />
@@ -53,6 +51,9 @@
</el-button>
</span>
</el-form-item>
<el-form-item>
<el-checkbox v-model="manualModel">{{ $t('aiTools.agents.manualModel') }}</el-checkbox>
</el-form-item>
<el-form-item :label="$t('aiTools.model.model')" prop="model">
<el-input v-if="manualModel" v-model="form.model" />
@@ -2,6 +2,9 @@
<DrawerPro v-model="open" :header="header" size="large" @close="handleClose">
<template #content>
<el-tabs v-model="activeTab" tab-position="left" class="config-tabs" @tab-click="handleTabClick">
<el-tab-pane :label="t('aiTools.agents.settingsTab')" name="settings">
<SettingsTab ref="settingsRef" />
</el-tab-pane>
<el-tab-pane :label="t('aiTools.model.model')" name="model">
<ModelTab ref="modelRef" @updated="handleModelUpdated" />
</el-tab-pane>
@@ -20,16 +23,26 @@ import { useI18n } from 'vue-i18n';
import { AI } from '@/api/interface/ai';
import ChannelsTab from './tabs/channels.vue';
import ModelTab from './tabs/model.vue';
import SettingsTab from './tabs/settings.vue';
const { t } = useI18n();
const emit = defineEmits(['updated']);
const open = ref(false);
const activeTab = ref('model');
const activeTab = ref('settings');
const header = ref('');
const agentId = ref(0);
const currentAgent = ref<AI.AgentItem>();
const channelsRef = ref();
const modelRef = ref();
const settingsRef = ref();
const loadSettings = async () => {
if (agentId.value <= 0) {
return;
}
await nextTick();
await settingsRef.value?.load(agentId.value);
};
const loadModel = async () => {
if (!currentAgent.value) {
@@ -48,10 +61,13 @@ const loadChannels = async () => {
};
const handleClose = () => {
activeTab.value = 'model';
activeTab.value = 'settings';
};
const handleTabClick = async (pane: TabsPaneContext) => {
if (pane.paneName === 'settings' && agentId.value > 0) {
await loadSettings();
}
if (pane.paneName === 'model' && currentAgent.value) {
await loadModel();
}
@@ -68,9 +84,9 @@ const openDrawer = async (agent: AI.AgentItem) => {
agentId.value = agent.id;
currentAgent.value = agent;
header.value = `${agent.name} - ${t('menu.config')}`;
activeTab.value = 'model';
activeTab.value = 'settings';
open.value = true;
await loadModel();
await loadSettings();
};
defineExpose({
@@ -0,0 +1,42 @@
<template>
<el-tabs v-model="activeTab" @tab-click="handleTabClick">
<el-tab-pane :label="t('aiTools.agents.browserTab')" name="browser">
<BrowserTab ref="browserRef" />
</el-tab-pane>
</el-tabs>
</template>
<script setup lang="ts">
import { nextTick, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import BrowserTab from './settings/browser.vue';
const { t } = useI18n();
const activeTab = ref('browser');
const agentId = ref(0);
const browserRef = ref();
const loadCurrentTab = async () => {
if (agentId.value <= 0) {
return;
}
await nextTick();
if (activeTab.value === 'browser') {
await browserRef.value?.load(agentId.value);
}
};
const handleTabClick = async () => {
await loadCurrentTab();
};
const load = async (id: number) => {
agentId.value = id;
activeTab.value = 'browser';
await loadCurrentTab();
};
defineExpose({
load,
});
</script>
@@ -0,0 +1,87 @@
<template>
<el-form ref="formRef" :model="form" :rules="rules" label-position="top" v-loading="loading">
<el-form-item :label="t('aiTools.agents.browserEnabled')">
<el-switch v-model="form.enabled" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.headless')">
<el-switch v-model="form.headless" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.noSandbox')">
<el-switch v-model="form.noSandbox" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.defaultProfile')" prop="defaultProfile">
<el-input v-model="form.defaultProfile" />
</el-form-item>
<el-form-item :label="t('aiTools.agents.executablePath')">
<el-input v-model="form.executablePath" disabled />
</el-form-item>
<el-form-item>
<el-button type="primary" :loading="saving" @click="saveConfig">
{{ t('commons.button.save') }}
</el-button>
</el-form-item>
</el-form>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import type { FormInstance } from 'element-plus';
import { useI18n } from 'vue-i18n';
import { Rules } from '@/global/form-rules';
import { AI } from '@/api/interface/ai';
import { getAgentBrowserConfig, updateAgentBrowserConfig } from '@/api/modules/ai';
import { MsgSuccess } from '@/utils/message';
const { t } = useI18n();
const loading = ref(false);
const saving = ref(false);
const agentId = ref(0);
const formRef = ref<FormInstance>();
const form = reactive<AI.AgentBrowserConfig>({
enabled: true,
executablePath: '',
headless: true,
noSandbox: true,
defaultProfile: 'openclaw',
});
const rules = reactive({
defaultProfile: [Rules.requiredInput],
});
const load = async (id: number) => {
agentId.value = id;
loading.value = true;
try {
const res = await getAgentBrowserConfig({ agentId: id });
Object.assign(form, res.data || {});
} finally {
loading.value = false;
}
};
const saveConfig = async () => {
if (!agentId.value || !formRef.value) {
return;
}
await formRef.value.validate();
saving.value = true;
try {
await updateAgentBrowserConfig({
agentId: agentId.value,
enabled: form.enabled,
headless: form.headless,
noSandbox: form.noSandbox,
defaultProfile: form.defaultProfile,
});
MsgSuccess(t('aiTools.agents.saveSuccess'));
} finally {
saving.value = false;
}
};
defineExpose({
load,
});
</script>