feat: support session delete for hermes-agent (#12533)

This commit is contained in:
CityFun
2026-04-20 06:08:56 +00:00
committed by GitHub
parent 646826769a
commit cd30ec7632
22 changed files with 164 additions and 40 deletions
+20
View File
@@ -275,6 +275,26 @@ func (b *BaseApi) RenameHermesChatSession(c *gin.Context) {
helper.Success(c)
}
// @Tags AI
// @Summary Delete Hermes chat session
// @Accept json
// @Param request body dto.AgentHermesChatSessionDeleteReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/hermes/chat/sessions/delete [post]
func (b *BaseApi) DeleteHermesChatSession(c *gin.Context) {
var req dto.AgentHermesChatSessionDeleteReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.DeleteHermesChatSession(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Get Providers
// @Success 200 {array} dto.ProviderInfo
+5
View File
@@ -115,6 +115,11 @@ type AgentHermesChatSessionRenameReq struct {
Title string `json:"title" validate:"required"`
}
type AgentHermesChatSessionDeleteReq struct {
AgentID uint `json:"agentId" validate:"required"`
ID string `json:"id" validate:"required"`
}
type AgentOverview struct {
Snapshot AgentOverviewSnapshot `json:"snapshot"`
}
+13 -3
View File
@@ -41,6 +41,7 @@ type IAgentService interface {
UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
GetHermesChatSessions(req dto.AgentIDReq) ([]dto.AgentHermesChatSessionItem, error)
RenameHermesChatSession(req dto.AgentHermesChatSessionRenameReq) error
DeleteHermesChatSession(req dto.AgentHermesChatSessionDeleteReq) error
GetOverview(req dto.AgentOverviewReq) (*dto.AgentOverview, error)
GetProviders() ([]dto.ProviderInfo, error)
GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error)
@@ -456,7 +457,7 @@ func (a AgentService) GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig,
if err != nil {
return nil, err
}
model, err := resolveHermesConfiguredModelIDStrict(account, accountModels, cfg.Model.Default)
model, err := resolveHermesConfiguredModelID(account, accountModels, cfg.Model.Default)
if err != nil {
return nil, err
}
@@ -905,7 +906,13 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
if err != nil {
return err
}
return writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone))
if err := writeHermesConfig(path.Dir(agent.ConfigPath), account, agent.Model, strings.TrimSpace(req.UserTimezone)); err != nil {
return err
}
return NewIAppInstalledService().Operate(request.AppInstalledOperate{
InstallId: install.ID,
Operate: constant.Restart,
})
}
if err := ensureContainerRunning(install.ContainerName); err != nil {
return err
@@ -921,7 +928,10 @@ func (a AgentService) UpdateOtherConfig(req dto.AgentOtherConfigUpdateReq) error
if err := writeOpenclawConfigRaw(agent.ConfigPath, conf); err != nil {
return err
}
return setOpenclawNPMRegistry(install.ContainerName, req.NPMRegistry)
if err := setOpenclawNPMRegistry(install.ContainerName, req.NPMRegistry); err != nil {
return err
}
return nil
}
func (a AgentService) GetConfigFile(req dto.AgentConfigFileReq) (*dto.AgentConfigFile, error) {
+5 -24
View File
@@ -54,17 +54,6 @@ func buildHermesDockerExecArgs(containerName string, hermesArgs ...string) []str
return buildHermesDockerExecCommandArgs(containerName, "hermes", hermesArgs...)
}
func buildHermesSkillUninstallArgs(containerName, skillName string) []string {
return buildHermesDockerExecCommandArgs(
containerName,
"sh",
"-lc",
fmt.Sprintf(`printf 'y\n' | %s skills uninstall "$1"`, hermesExecutablePath),
"sh",
skillName,
)
}
func writeHermesConfig(confDir string, account *model.AgentAccount, modelName string, timezone string) error {
if strings.TrimSpace(confDir) == "" {
return fmt.Errorf("config dir is required")
@@ -382,29 +371,21 @@ func resolveHermesModel(sourceProvider, targetProvider, modelName string) string
return target
}
func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels []dto.AgentAccountModel, configuredModel string) string {
func resolveHermesConfiguredModelID(account *model.AgentAccount, accountModels []dto.AgentAccountModel, configuredModel string) (string, error) {
if account == nil {
return ""
return "", buserr.New("ErrAgentModelNotInAccount")
}
configuredModel = strings.TrimSpace(configuredModel)
if configuredModel == "" {
return ""
return "", buserr.New("ErrAgentModelNotInAccount")
}
provider := resolveHermesProvider(account.Provider)
for _, item := range accountModels {
if resolveHermesModel(account.Provider, provider, item.ID) == configuredModel {
return item.ID
return item.ID, nil
}
}
return ""
}
func resolveHermesConfiguredModelIDStrict(account *model.AgentAccount, accountModels []dto.AgentAccountModel, configuredModel string) (string, error) {
modelID := resolveHermesConfiguredModelID(account, accountModels, configuredModel)
if modelID == "" {
return "", buserr.New("ErrAgentModelNotInAccount")
}
return modelID, nil
return "", buserr.New("ErrAgentModelNotInAccount")
}
func resolveHermesEnvEntries(account *model.AgentAccount) []hermesEnvEntry {
+16
View File
@@ -43,6 +43,22 @@ func (a AgentService) RenameHermesChatSession(req dto.AgentHermesChatSessionRena
return err
}
func (a AgentService) DeleteHermesChatSession(req dto.AgentHermesChatSessionDeleteReq) error {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return err
}
if agent.AgentType != constant.AppHermesAgent {
return fmt.Errorf("%s does not support", agent.AgentType)
}
_, err = cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout(
"docker",
buildHermesDockerExecArgs(install.ContainerName, "sessions", "delete", req.ID, "--yes")...,
)
return err
}
func listHermesChatSessionsFromStateDB(stateDBPath string) ([]dto.AgentHermesChatSessionItem, error) {
if !files.NewFileOp().Stat(stateDBPath) {
return []dto.AgentHermesChatSessionItem{}, nil
+8 -1
View File
@@ -166,7 +166,14 @@ func (a AgentService) UninstallSkill(req dto.AgentSkillUninstallReq) error {
}
return cmd.NewCommandMgr(cmd.WithTimeout(5*time.Minute)).Run(
"docker",
buildHermesSkillUninstallArgs(install.ContainerName, req.Name)...,
buildHermesDockerExecCommandArgs(
install.ContainerName,
"sh",
"-lc",
fmt.Sprintf(`printf 'y\n' | %s skills uninstall "$1"`, hermesExecutablePath),
"sh",
req.Name,
)...,
)
}
+1
View File
@@ -52,6 +52,7 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.POST("/agents/model/update", baseApi.UpdateAgentModelConfig)
aiToolsRouter.POST("/agents/hermes/chat/sessions", baseApi.GetHermesChatSessions)
aiToolsRouter.POST("/agents/hermes/chat/sessions/rename", baseApi.RenameHermesChatSession)
aiToolsRouter.POST("/agents/hermes/chat/sessions/delete", baseApi.DeleteHermesChatSession)
aiToolsRouter.POST("/agents/overview", baseApi.GetAgentOverview)
aiToolsRouter.GET("/agents/providers", baseApi.GetAgentProviders)
aiToolsRouter.POST("/agents/accounts", baseApi.CreateAgentAccount)
+5
View File
@@ -343,6 +343,11 @@ export namespace AI {
title: string;
}
export interface AgentHermesChatSessionDeleteReq {
agentId: number;
id: string;
}
export interface AgentOverviewReq {
agentId: number;
}
+4
View File
@@ -142,6 +142,10 @@ export const renameAgentHermesChatSession = (req: AI.AgentHermesChatSessionRenam
return http.post(`/ai/agents/hermes/chat/sessions/rename`, req);
};
export const deleteAgentHermesChatSession = (req: AI.AgentHermesChatSessionDeleteReq) => {
return http.post(`/ai/agents/hermes/chat/sessions/delete`, req);
};
export const getAgentOverview = (req: AI.AgentOverviewReq) => {
return http.post<AI.AgentOverview>(`/ai/agents/overview`, req, TimeoutEnum.T_5M);
};
+2
View File
@@ -783,6 +783,8 @@ const message = {
hermesChatEmptyHint: 'Select a session or click New Chat',
hermesChatTitlePlaceholder: 'Enter session title',
hermesChatRenameSuccess: 'Session title updated',
hermesChatDeleteConfirm: 'Delete session {0}?',
hermesChatDeleteSuccess: 'Session deleted',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+2
View File
@@ -793,6 +793,8 @@ const message = {
hermesChatEmptyHint: 'Seleccione una sesión a la izquierda o haga clic en Nueva conversación',
hermesChatTitlePlaceholder: 'Introduzca el título de la sesión',
hermesChatRenameSuccess: 'El título de la sesión se ha actualizado',
hermesChatDeleteConfirm: '¿Eliminar la sesión {0}?',
hermesChatDeleteSuccess: 'Sesión eliminada',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+2
View File
@@ -786,6 +786,8 @@ const message = {
hermesChatEmptyHint: '左側のセッションを選択するか新しい会話をクリックしてください',
hermesChatTitlePlaceholder: 'セッションタイトルを入力してください',
hermesChatRenameSuccess: 'セッションタイトルを更新しました',
hermesChatDeleteConfirm: 'セッション {0} を削除しますか',
hermesChatDeleteSuccess: 'セッションを削除しました',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+2
View File
@@ -772,6 +772,8 @@ const message = {
hermesChatEmptyHint: '왼쪽 세션을 선택하거나 대화를 클릭하세요',
hermesChatTitlePlaceholder: '세션 제목을 입력하세요',
hermesChatRenameSuccess: '세션 제목이 업데이트되었습니다',
hermesChatDeleteConfirm: '세션 {0}() 삭제하시겠습니까?',
hermesChatDeleteSuccess: '세션이 삭제되었습니다',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+2
View File
@@ -791,6 +791,8 @@ const message = {
hermesChatEmptyHint: 'Pilih sesi di sebelah kiri atau klik Perbualan baharu',
hermesChatTitlePlaceholder: 'Masukkan tajuk sesi',
hermesChatRenameSuccess: 'Tajuk sesi telah dikemas kini',
hermesChatDeleteConfirm: 'Padam sesi {0}?',
hermesChatDeleteSuccess: 'Sesi berjaya dipadam',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+2
View File
@@ -788,6 +788,8 @@ const message = {
hermesChatEmptyHint: 'Selecione uma sessão à esquerda ou clique em Nova conversa',
hermesChatTitlePlaceholder: 'Digite o título da sessão',
hermesChatRenameSuccess: 'O título da sessão foi atualizado',
hermesChatDeleteConfirm: 'Excluir a sessão {0}?',
hermesChatDeleteSuccess: 'Sessão excluída',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+2
View File
@@ -783,6 +783,8 @@ const message = {
hermesChatEmptyHint: 'Выберите сессию слева или нажмите «Новый диалог»',
hermesChatTitlePlaceholder: 'Введите название сессии',
hermesChatRenameSuccess: 'Название сессии обновлено',
hermesChatDeleteConfirm: 'Удалить сессию {0}?',
hermesChatDeleteSuccess: 'Сессия удалена',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+2
View File
@@ -790,6 +790,8 @@ const message = {
hermesChatEmptyHint: "Soldan bir oturum seçin veya Yeni sohbet'e tıklayın",
hermesChatTitlePlaceholder: 'Oturum başlığını girin',
hermesChatRenameSuccess: 'Oturum başlığı güncellendi',
hermesChatDeleteConfirm: '{0} oturumu silinsin mi?',
hermesChatDeleteSuccess: 'Oturum silindi',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+2
View File
@@ -737,6 +737,8 @@ const message = {
hermesChatEmptyHint: '選擇左側會話或點擊新對話',
hermesChatTitlePlaceholder: '請輸入會話標題',
hermesChatRenameSuccess: '會話標題已更新',
hermesChatDeleteConfirm: '確認刪除會話 {0}',
hermesChatDeleteSuccess: '會話已刪除',
weixin: '微信',
wecom: '企業微信',
dingtalk: '釘釘',
+2
View File
@@ -732,6 +732,8 @@ const message = {
hermesChatEmptyHint: '选择左侧会话或点击新对话',
hermesChatTitlePlaceholder: '请输入会话标题',
hermesChatRenameSuccess: '会话标题已更新',
hermesChatDeleteConfirm: '确认删除会话 {0}',
hermesChatDeleteSuccess: '会话已删除',
weixin: '微信',
wecom: '企业微信',
dingtalk: '钉钉',
@@ -43,7 +43,12 @@
<div v-else class="hermes-chat-dialog__item-title">{{ item.title || item.id }}</div>
<div class="hermes-chat-dialog__item-actions">
<template v-if="editingSessionId === item.id">
<el-button link type="primary" @click.stop="saveSessionTitle(item)">
<el-button
link
type="primary"
:disabled="!canSaveSessionTitle"
@click.stop="saveSessionTitle(item)"
>
{{ $t('commons.button.save') }}
</el-button>
<el-button link @click.stop="cancelEditSessionTitle">
@@ -58,6 +63,16 @@
@mousedown.stop
@click.stop="startEditSessionTitle(item)"
/>
<el-button
v-if="editingSessionId !== item.id"
link
type="danger"
icon="Delete"
:disabled="isDeleteDisabled(item)"
:loading="deletingSessionId === item.id"
@mousedown.stop
@click.stop="deleteSession(item)"
/>
</div>
</div>
<div class="hermes-chat-dialog__item-model">{{ item.model || '-' }}</div>
@@ -80,11 +95,16 @@
</template>
<script setup lang="ts">
import { nextTick, ref } from 'vue';
import { ElMessageBox } from 'element-plus';
import { computed, nextTick, ref } from 'vue';
import { useI18n } from 'vue-i18n';
import DialogPro from '@/components/dialog-pro/index.vue';
import Terminal from '@/components/terminal/index.vue';
import { getAgentHermesChatSessions, renameAgentHermesChatSession } from '@/api/modules/ai';
import {
deleteAgentHermesChatSession,
getAgentHermesChatSessions,
renameAgentHermesChatSession,
} from '@/api/modules/ai';
import { AI } from '@/api/interface/ai';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { dateFormat } from '@/utils/date';
@@ -113,6 +133,8 @@ const sessions = ref<AI.AgentHermesChatSessionItem[]>([]);
const activeSessionId = ref('');
const editingSessionId = ref('');
const editingTitle = ref('');
const deletingSessionId = ref('');
const canSaveSessionTitle = computed(() => editingTitle.value.length > 0);
const formatTime = (value: string) => {
if (!value) {
@@ -192,6 +214,9 @@ const cancelEditSessionTitle = () => {
};
const saveSessionTitle = async (item: AI.AgentHermesChatSessionItem) => {
if (!canSaveSessionTitle.value) {
return;
}
await renameAgentHermesChatSession({
agentId: agentId.value,
id: item.id,
@@ -202,6 +227,44 @@ const saveSessionTitle = async (item: AI.AgentHermesChatSessionItem) => {
await loadSessions();
};
const isDeleteDisabled = (item: AI.AgentHermesChatSessionItem) => {
return terminalOpen.value && activeSessionId.value === item.id;
};
const deleteSession = async (item: AI.AgentHermesChatSessionItem) => {
if (isDeleteDisabled(item)) {
return;
}
try {
await ElMessageBox.confirm(
t('aiTools.agents.hermesChatDeleteConfirm', [item.title || item.id]),
t('commons.button.delete'),
{
confirmButtonText: t('commons.button.delete'),
cancelButtonText: t('commons.button.cancel'),
type: 'warning',
},
);
} catch {
return;
}
deletingSessionId.value = item.id;
try {
await deleteAgentHermesChatSession({
agentId: agentId.value,
id: item.id,
});
MsgSuccess(t('aiTools.agents.hermesChatDeleteSuccess'));
cancelEditSessionTitle();
if (activeSessionId.value === item.id) {
activeSessionId.value = '';
}
await loadSessions();
} finally {
deletingSessionId.value = '';
}
};
const disconnectTerminal = () => {
terminalRef.value?.onClose();
terminalOpen.value = false;
@@ -54,11 +54,6 @@
/>
<span class="input-help">{{ t('aiTools.agents.groupAllowFromHelper') }}</span>
</el-form-item>
<el-form-item>
<el-link type="primary" icon="Position" @click="toFeishuDoc">
{{ t('container.mirrorsHelper2') }}
</el-link>
</el-form-item>
</div>
<ChannelBots
:bots="form.bots"
@@ -250,10 +245,6 @@ const updateBots = (bots: FeishuBotForm[]) => {
const isApproveDisabled = (bot: FeishuBotForm) => bot.dmPolicy !== 'pairing';
const toFeishuDoc = () => {
window.open('https://openclaw.club/guides/feishu-platform', '_blank');
};
const load = async (id: number) => {
await loadPlugin(id);
const res = await getAgentFeishuConfig({ agentId: id });
@@ -17,6 +17,7 @@
{{ t('commons.button.save') }}
</el-button>
</el-form-item>
<span class="input-help">{{ t('aiTools.agents.channelAutoRestartHelper') }}</span>
</el-form>
</template>