feat: support edit config file (#12313)

This commit is contained in:
CityFun
2026-03-24 09:08:07 +00:00
committed by GitHub
parent 8533dd1d33
commit 2cb2058cfa
19 changed files with 233 additions and 0 deletions
+41
View File
@@ -680,6 +680,47 @@ func (b *BaseApi) UpdateAgentOtherConfig(c *gin.Context) {
helper.Success(c)
}
// @Tags AI
// @Summary Get Agent config file
// @Accept json
// @Param request body dto.AgentConfigFileReq true "request"
// @Success 200 {object} dto.AgentConfigFile
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/config-file/get [post]
func (b *BaseApi) GetAgentConfigFile(c *gin.Context) {
var req dto.AgentConfigFileReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
data, err := agentService.GetConfigFile(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, data)
}
// @Tags AI
// @Summary Update Agent config file
// @Accept json
// @Param request body dto.AgentConfigFileUpdateReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/config-file/update [post]
func (b *BaseApi) UpdateAgentConfigFile(c *gin.Context) {
var req dto.AgentConfigFileUpdateReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.UpdateConfigFile(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary List Agent skills
// @Accept json
+13
View File
@@ -354,6 +354,19 @@ type AgentOtherConfig struct {
NPMRegistry string `json:"npmRegistry"`
}
type AgentConfigFileReq struct {
AgentID uint `json:"agentId" validate:"required"`
}
type AgentConfigFileUpdateReq struct {
AgentID uint `json:"agentId" validate:"required"`
Content string `json:"content" validate:"required"`
}
type AgentConfigFile struct {
Content string `json:"content"`
}
type AgentSkillsReq struct {
AgentID uint `json:"agentId" validate:"required"`
}
+43
View File
@@ -4,6 +4,7 @@ import (
"context"
"encoding/json"
"fmt"
"os"
"path"
"sort"
"strings"
@@ -33,6 +34,8 @@ type IAgentService interface {
UpdateSecurityConfig(req dto.AgentSecurityConfigUpdateReq) error
GetOtherConfig(req dto.AgentOtherConfigReq) (*dto.AgentOtherConfig, error)
UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
GetConfigFile(req dto.AgentConfigFileReq) (*dto.AgentConfigFile, error)
UpdateConfigFile(req dto.AgentConfigFileUpdateReq) error
ListSkills(req dto.AgentSkillsReq) ([]dto.AgentSkillItem, error)
UpdateSkill(req dto.AgentSkillUpdateReq) error
@@ -717,6 +720,46 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
return setOpenclawNPMRegistry(install.ContainerName, req.NPMRegistry)
}
func (a AgentService) GetConfigFile(req dto.AgentConfigFileReq) (*dto.AgentConfigFile, error) {
agent, _, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
if agent.AgentType == constant.AppCopaw {
return nil, fmt.Errorf("copaw does not support config file")
}
content, err := os.ReadFile(agent.ConfigPath)
if err != nil {
return nil, err
}
return &dto.AgentConfigFile{Content: string(content)}, nil
}
func (a AgentService) UpdateConfigFile(req dto.AgentConfigFileUpdateReq) error {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if agent.AgentType == constant.AppCopaw {
return fmt.Errorf("copaw does not support config file")
}
var payload interface{}
if err := json.Unmarshal([]byte(req.Content), &payload); err != nil {
return err
}
info, err := os.Stat(agent.ConfigPath)
if err != nil {
return err
}
if err := os.WriteFile(agent.ConfigPath, []byte(req.Content), info.Mode()); err != nil {
return err
}
return NewIAppInstalledService().Operate(request.AppInstalledOperate{
InstallId: install.ID,
Operate: constant.Restart,
})
}
func getOpenclawNPMRegistry(containerName string) (string, error) {
registry, err := cmd.RunDefaultWithStdoutBashCfAndTimeOut("docker exec %s npm get registry", 20*time.Second, containerName)
if err != nil {
+2
View File
@@ -540,6 +540,7 @@ func setDingTalkConfig(conf map[string]interface{}, config dto.AgentDingTalkConf
func setQQBotConfig(conf map[string]interface{}, config dto.AgentQQBotConfig) {
channels := ensureChildMap(conf, "channels")
qqbot := ensureChildMap(channels, "qqbot")
delete(qqbot, "dmPolicy")
qqbot["enabled"] = config.Enabled
qqbot["allowFrom"] = []string{"*"}
qqbot["appId"] = strings.TrimSpace(config.AppID)
@@ -547,6 +548,7 @@ func setQQBotConfig(conf map[string]interface{}, config dto.AgentQQBotConfig) {
plugins := ensureChildMap(conf, "plugins")
entries := ensureChildMap(plugins, "entries")
delete(entries, "qqbot")
qqbotEntry := ensureChildMap(entries, "openclaw-qqbot")
qqbotEntry["enabled"] = config.Enabled
}
+2
View File
@@ -74,6 +74,8 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.POST("/agents/security/update", baseApi.UpdateAgentSecurityConfig)
aiToolsRouter.POST("/agents/other/get", baseApi.GetAgentOtherConfig)
aiToolsRouter.POST("/agents/other/update", baseApi.UpdateAgentOtherConfig)
aiToolsRouter.POST("/agents/config-file/get", baseApi.GetAgentConfigFile)
aiToolsRouter.POST("/agents/config-file/update", baseApi.UpdateAgentConfigFile)
aiToolsRouter.POST("/agents/skills/list", baseApi.ListAgentSkills)
aiToolsRouter.POST("/agents/skills/update", baseApi.UpdateAgentSkill)
aiToolsRouter.POST("/agents/channel/pairing/approve", baseApi.ApproveAgentChannelPairing)
+13
View File
@@ -588,6 +588,19 @@ export namespace AI {
npmRegistry: string;
}
export interface AgentConfigFileReq {
agentId: number;
}
export interface AgentConfigFile {
content: string;
}
export interface AgentConfigFileUpdateReq {
agentId: number;
content: string;
}
export interface AgentSkillsReq {
agentId: number;
}
+8
View File
@@ -229,6 +229,14 @@ export const updateAgentOtherConfig = (req: AI.AgentOtherConfigUpdateReq) => {
return http.post(`/ai/agents/other/update`, req);
};
export const getAgentConfigFile = (req: AI.AgentConfigFileReq) => {
return http.post<AI.AgentConfigFile>(`/ai/agents/config-file/get`, req);
};
export const updateAgentConfigFile = (req: AI.AgentConfigFileUpdateReq) => {
return http.post(`/ai/agents/config-file/update`, req);
};
export const listAgentSkills = (req: AI.AgentSkillsReq) => {
return http.post<AI.AgentSkillItem[]>(`/ai/agents/skills/list`, req);
};
+2
View File
@@ -715,6 +715,8 @@ const message = {
skillsGroupWorkspace: 'Workspace',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
configFileRestartHelper:
'Saving the config file requires immediately restarting the container to take effect.',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+2
View File
@@ -723,6 +723,8 @@ const message = {
skillsGroupWorkspace: 'Workspace',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
configFileRestartHelper:
'Saving the config file requires immediately restarting the container to take effect.',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+2
View File
@@ -716,6 +716,8 @@ const message = {
skillsGroupWorkspace: 'Workspace',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
configFileRestartHelper:
'Saving the config file requires immediately restarting the container to take effect.',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+2
View File
@@ -708,6 +708,8 @@ const message = {
skillsGroupWorkspace: 'Workspace',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
configFileRestartHelper:
'Saving the config file requires immediately restarting the container to take effect.',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+2
View File
@@ -723,6 +723,8 @@ const message = {
skillsGroupWorkspace: 'Workspace',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
configFileRestartHelper:
'Saving the config file requires immediately restarting the container to take effect.',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+2
View File
@@ -718,6 +718,8 @@ const message = {
skillsGroupWorkspace: 'Workspace',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
configFileRestartHelper:
'Saving the config file requires immediately restarting the container to take effect.',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+2
View File
@@ -715,6 +715,8 @@ const message = {
skillsGroupWorkspace: 'Workspace',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
configFileRestartHelper:
'Saving the config file requires immediately restarting the container to take effect.',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+2
View File
@@ -719,6 +719,8 @@ const message = {
skillsGroupWorkspace: 'Workspace',
switchModelSuccess: 'Model switched successfully',
channelsTab: 'Channels',
configFileRestartHelper:
'Saving the config file requires immediately restarting the container to take effect.',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+1
View File
@@ -681,6 +681,7 @@ const message = {
skillsGroupWorkspace: '工作區',
switchModelSuccess: '模型切換成功',
channelsTab: '頻道',
configFileRestartHelper: '保存配置檔後需要立即重新啟動容器才能生效。',
weixin: '微信',
wecom: '企業微信',
dingtalk: '釘釘',
+1
View File
@@ -680,6 +680,7 @@ const message = {
skillsGroupWorkspace: '工作区',
switchModelSuccess: '模型切换成功',
channelsTab: '频道',
configFileRestartHelper: '保存配置文件后需要重启容器才能生效。',
weixin: '微信',
wecom: '企业微信',
dingtalk: '钉钉',
@@ -6,6 +6,9 @@
<el-tab-pane :label="t('aiTools.agents.otherTab')" name="other">
<OtherTab ref="otherRef" />
</el-tab-pane>
<el-tab-pane :label="t('website.source')" name="configFile">
<ConfigFileTab ref="configFileRef" />
</el-tab-pane>
</el-tabs>
</template>
@@ -14,12 +17,14 @@ import { nextTick, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import SecurityTab from './settings/security.vue';
import OtherTab from './settings/other.vue';
import ConfigFileTab from './settings/config-file.vue';
const { t } = useI18n();
const activeTab = ref('security');
const agentId = ref(0);
const securityRef = ref();
const otherRef = ref();
const configFileRef = ref();
const loadCurrentTab = async () => {
if (agentId.value <= 0) {
@@ -32,6 +37,10 @@ const loadCurrentTab = async () => {
}
if (activeTab.value === 'other') {
await otherRef.value?.load(agentId.value);
return;
}
if (activeTab.value === 'configFile') {
await configFileRef.value?.load(agentId.value);
}
};
@@ -0,0 +1,84 @@
<template>
<div v-loading="loading">
<CodemirrorPro
v-model="form.content"
mode="json"
:lineWrapping="true"
:heightDiff="360"
:placeholder="t('commons.msg.noneData')"
/>
<div class="mt-4">
<el-button type="primary" :loading="saving" @click="confirmSave">
{{ t('commons.button.save') }}
</el-button>
</div>
<ConfirmDialog ref="confirmDialogRef" @confirm="saveConfig" />
</div>
</template>
<script setup lang="ts">
import { reactive, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import CodemirrorPro from '@/components/codemirror-pro/index.vue';
import ConfirmDialog from '@/components/confirm-dialog/index.vue';
import { getAgentConfigFile, updateAgentConfigFile } from '@/api/modules/ai';
import { MsgError, MsgSuccess } from '@/utils/message';
const { t } = useI18n();
const loading = ref(false);
const saving = ref(false);
const agentId = ref(0);
const confirmDialogRef = ref();
const form = reactive({
content: '',
});
const load = async (id: number) => {
agentId.value = id;
loading.value = true;
try {
const res = await getAgentConfigFile({ agentId: id });
form.content = res.data?.content || '';
} finally {
loading.value = false;
}
};
const confirmSave = async () => {
if (!agentId.value) {
return;
}
try {
JSON.parse(form.content);
} catch {
MsgError(t('commons.rule.formatErr'));
return;
}
confirmDialogRef.value?.acceptParams({
header: t('database.confChange'),
operationInfo: t('aiTools.agents.configFileRestartHelper'),
submitInputInfo: t('database.restartNow'),
});
};
const saveConfig = async () => {
if (!agentId.value) {
return;
}
saving.value = true;
try {
await updateAgentConfigFile({
agentId: agentId.value,
content: form.content,
});
MsgSuccess(t('aiTools.agents.saveSuccess'));
} finally {
saving.value = false;
}
};
defineExpose({
load,
});
</script>