feat(ai): support Hermes Agent chat sessions in 1Panel (#12497)

* feat(ai): support Hermes Agent chat sessions in 1Panel

* feat(ai): support Hermes Agent chat sessions in 1Panel

* feat(ai): change docker command
This commit is contained in:
CityFun
2026-04-15 15:44:20 +08:00
committed by GitHub
parent 7f5bb58c72
commit 4e78841bf6
26 changed files with 710 additions and 25 deletions
+41
View File
@@ -214,6 +214,47 @@ func (b *BaseApi) GetAgentOverview(c *gin.Context) {
helper.SuccessWithData(c, res)
}
// @Tags AI
// @Summary Get Hermes chat sessions
// @Accept json
// @Param request body dto.AgentIDReq true "request"
// @Success 200 {array} dto.AgentHermesChatSessionItem
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/hermes/chat/sessions [post]
func (b *BaseApi) GetHermesChatSessions(c *gin.Context) {
var req dto.AgentIDReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
res, err := agentService.GetHermesChatSessions(req)
if err != nil {
helper.BadRequest(c, err)
return
}
helper.SuccessWithData(c, res)
}
// @Tags AI
// @Summary Rename Hermes chat session
// @Accept json
// @Param request body dto.AgentHermesChatSessionRenameReq true "request"
// @Success 200
// @Security ApiKeyAuth
// @Security Timestamp
// @Router /ai/agents/hermes/chat/sessions/rename [post]
func (b *BaseApi) RenameHermesChatSession(c *gin.Context) {
var req dto.AgentHermesChatSessionRenameReq
if err := helper.CheckBindAndValidate(&req, c); err != nil {
return
}
if err := agentService.RenameHermesChatSession(req); err != nil {
helper.BadRequest(c, err)
return
}
helper.Success(c)
}
// @Tags AI
// @Summary Get Providers
// @Success 200 {array} dto.ProviderInfo
+15
View File
@@ -91,6 +91,15 @@ type AgentModelConfig struct {
Fallbacks []string `json:"fallbacks"`
}
type AgentHermesChatSessionItem struct {
ID string `json:"id"`
Title string `json:"title"`
Model string `json:"model"`
MessageCount int64 `json:"messageCount"`
StartedAt string `json:"startedAt"`
LastActive string `json:"lastActive"`
}
type AgentOverviewReq struct {
AgentID uint `json:"agentId" validate:"required"`
}
@@ -99,6 +108,12 @@ type AgentIDReq struct {
AgentID uint `json:"agentId" validate:"required"`
}
type AgentHermesChatSessionRenameReq struct {
AgentID uint `json:"agentId" validate:"required"`
ID string `json:"id" validate:"required"`
Title string `json:"title" validate:"required"`
}
type AgentOverview struct {
Snapshot AgentOverviewSnapshot `json:"snapshot"`
}
+2
View File
@@ -38,6 +38,8 @@ type IAgentService interface {
BindWebsite(req dto.AgentWebsiteBindReq) error
GetModelConfig(req dto.AgentIDReq) (*dto.AgentModelConfig, error)
UpdateModelConfig(req dto.AgentModelConfigUpdateReq) error
GetHermesChatSessions(req dto.AgentIDReq) ([]dto.AgentHermesChatSessionItem, error)
RenameHermesChatSession(req dto.AgentHermesChatSessionRenameReq) error
GetOverview(req dto.AgentOverviewReq) (*dto.AgentOverview, error)
GetProviders() ([]dto.ProviderInfo, error)
GetSecurityConfig(req dto.AgentIDReq) (*dto.AgentSecurityConfig, error)
+4 -1
View File
@@ -505,7 +505,10 @@ func (a AgentService) ApproveChannelPairing(req dto.AgentChannelPairingApproveRe
return err
}
if agent.AgentType == constant.AppHermesAgent {
output, err := cmd.RunDefaultWithStdoutBashC(buildHermesPairingApproveCommand(install.ContainerName, req.Type, req.PairingCode))
output, err := cmd.NewCommandMgr(cmd.WithTimeout(20*time.Second)).RunWithStdout(
"docker",
buildHermesDockerExecArgs(install.ContainerName, "pairing", "approve", req.Type, req.PairingCode)...,
)
if err != nil {
return err
}
-9
View File
@@ -555,15 +555,6 @@ func extractHermesEnvBool(envMap map[string]string, key string, defaultValue boo
return strings.EqualFold(value, "true")
}
func buildHermesPairingApproveCommand(containerName string, channel string, pairingCode string) string {
return fmt.Sprintf(
"docker exec -u hermes -e HOME=/opt/data/home -e HERMES_HOME=/opt/data %s /opt/hermes/.venv/bin/hermes pairing approve %s %q",
containerName,
channel,
pairingCode,
)
}
func validateHermesPairingApproveOutput(output string) error {
text := strings.TrimSpace(output)
if text == "" {
+115
View File
@@ -0,0 +1,115 @@
package service
import (
"database/sql"
"fmt"
"math"
"path/filepath"
"strings"
"time"
"github.com/1Panel-dev/1Panel/agent/app/dto"
"github.com/1Panel-dev/1Panel/agent/constant"
"github.com/1Panel-dev/1Panel/agent/utils/cmd"
"github.com/1Panel-dev/1Panel/agent/utils/files"
)
const hermesSessionListLimit = 100
func (a AgentService) GetHermesChatSessions(req dto.AgentIDReq) ([]dto.AgentHermesChatSessionItem, error) {
agent, install, err := a.loadAgentAndInstall(req.AgentID)
if err != nil {
return nil, err
}
if agent.AgentType != constant.AppHermesAgent {
return nil, fmt.Errorf("%s does not support", agent.AgentType)
}
return listHermesChatSessionsFromStateDB(filepath.Join(install.GetPath(), "data", "state.db"))
}
func (a AgentService) RenameHermesChatSession(req dto.AgentHermesChatSessionRenameReq) 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", "rename", req.ID, req.Title)...,
)
return err
}
func listHermesChatSessionsFromStateDB(stateDBPath string) ([]dto.AgentHermesChatSessionItem, error) {
if !files.NewFileOp().Stat(stateDBPath) {
return []dto.AgentHermesChatSessionItem{}, nil
}
db, err := sql.Open("sqlite", stateDBPath)
if err != nil {
return nil, err
}
defer db.Close()
rows, err := db.Query(`
SELECT
s.id,
COALESCE(NULLIF(TRIM(s.title), ''), s.id) AS title,
COALESCE(s.model, '') AS model,
COALESCE(s.message_count, 0) AS message_count,
s.started_at,
COALESCE(MAX(m.timestamp), s.started_at) AS last_active
FROM sessions s
LEFT JOIN messages m ON m.session_id = s.id
WHERE s.source = 'cli'
GROUP BY s.id, title, model, s.message_count, s.started_at
ORDER BY last_active DESC
LIMIT ?
`, hermesSessionListLimit)
if err != nil {
return nil, err
}
defer rows.Close()
items := make([]dto.AgentHermesChatSessionItem, 0, 8)
for rows.Next() {
var item dto.AgentHermesChatSessionItem
var title sql.NullString
var model sql.NullString
var startedAt sql.NullFloat64
var lastActive sql.NullFloat64
if err := rows.Scan(&item.ID, &title, &model, &item.MessageCount, &startedAt, &lastActive); err != nil {
return nil, err
}
item.Title = strings.TrimSpace(title.String)
if item.Title == "" {
item.Title = item.ID
}
item.Model = strings.TrimSpace(model.String)
item.StartedAt = formatHermesSessionTimestamp(startedAt)
item.LastActive = formatHermesSessionTimestamp(lastActive)
items = append(items, item)
}
if err := rows.Err(); err != nil {
return nil, err
}
return items, nil
}
func formatHermesSessionTimestamp(value sql.NullFloat64) string {
if !value.Valid || value.Float64 <= 0 {
return ""
}
seconds, fraction := math.Modf(value.Float64)
return time.Unix(int64(seconds), int64(fraction*float64(time.Second))).UTC().Format(time.RFC3339)
}
func buildHermesDockerExecArgs(containerName string, hermesArgs ...string) []string {
args := []string{"exec", "-u", "hermes", containerName, "hermes"}
return append(args, hermesArgs...)
}
+2 -1
View File
@@ -13,6 +13,7 @@ import (
"strconv"
"strings"
"sync"
"time"
"github.com/gin-gonic/gin"
@@ -576,7 +577,7 @@ func (a AppService) installWithHooks(req request.AppInstallCreate, executeScript
_ = appInstallRepo.Save(context.Background(), appInstall)
}
installTask.AddSubTask(task.GetTaskName(appInstall.Name, task.TaskInstall, task.TaskScopeApp), installApp, handleAppStatus)
installTask.AddSubTaskWithOps(task.GetTaskName(appInstall.Name, task.TaskInstall, task.TaskScopeApp), installApp, handleAppStatus, 0, time.Hour)
go func() {
if taskErr := installTask.Execute(); taskErr != nil {
+2
View File
@@ -49,6 +49,8 @@ func (a *AIToolsRouter) InitRouter(Router *gin.RouterGroup) {
aiToolsRouter.POST("/agents/website/bind", baseApi.BindAgentWebsite)
aiToolsRouter.POST("/agents/model/get", baseApi.GetAgentModelConfig)
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/overview", baseApi.GetAgentOverview)
aiToolsRouter.GET("/agents/providers", baseApi.GetAgentProviders)
aiToolsRouter.POST("/agents/accounts", baseApi.CreateAgentAccount)
+15
View File
@@ -327,6 +327,21 @@ export namespace AI {
fallbacks: string[];
}
export interface AgentHermesChatSessionItem {
id: string;
title: string;
model: string;
messageCount: number;
startedAt: string;
lastActive: string;
}
export interface AgentHermesChatSessionRenameReq {
agentId: number;
id: string;
title: string;
}
export interface AgentOverviewReq {
agentId: number;
}
+8
View File
@@ -130,6 +130,14 @@ export const updateAgentModelConfig = (req: AI.AgentModelConfigUpdateReq) => {
return http.post(`/ai/agents/model/update`, req);
};
export const getAgentHermesChatSessions = (req: AI.AgentIDReq) => {
return http.post<AI.AgentHermesChatSessionItem[]>(`/ai/agents/hermes/chat/sessions`, req);
};
export const renameAgentHermesChatSession = (req: AI.AgentHermesChatSessionRenameReq) => {
return http.post(`/ai/agents/hermes/chat/sessions/rename`, req);
};
export const getAgentOverview = (req: AI.AgentOverviewReq) => {
return http.post<AI.AgentOverview>(`/ai/agents/overview`, req, TimeoutEnum.T_5M);
};
+3 -1
View File
@@ -555,7 +555,9 @@ onBeforeUnmount(() => {
.ai-notice-fade-enter-active,
.ai-notice-fade-leave-active {
transition: opacity 180ms ease, transform 180ms ease;
transition:
opacity 180ms ease,
transform 180ms ease;
}
.ai-mask-fade-enter-active,
+9
View File
@@ -773,6 +773,15 @@ const message = {
skillCount: 'Skills',
jobCount: 'Scheduled Jobs',
sessionCount: 'Sessions',
hermesChatAction: 'Chat',
hermesChatTitle: 'Hermes Chat',
hermesChatDialogTitle: 'Hermes Chat - {0}',
hermesChatNewChat: 'New Chat',
hermesChatNoSessions: 'No sessions',
hermesChatMessageCount: '{0} msgs',
hermesChatEmptyHint: 'Select a session or click New Chat',
hermesChatTitlePlaceholder: 'Enter session title',
hermesChatRenameSuccess: 'Session title updated',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+9
View File
@@ -783,6 +783,15 @@ const message = {
skillCount: 'Habilidades',
jobCount: 'Tareas programadas',
sessionCount: 'Sesiones',
hermesChatAction: 'Conversación',
hermesChatTitle: 'Conversación de Hermes',
hermesChatDialogTitle: 'Conversación de Hermes - {0}',
hermesChatNewChat: 'Nueva conversación',
hermesChatNoSessions: 'No hay sesiones',
hermesChatMessageCount: '{0} mensajes',
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',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+9
View File
@@ -776,6 +776,15 @@ const message = {
skillCount: '技能数',
jobCount: '定期タスク数',
sessionCount: 'セッション数',
hermesChatAction: '会話',
hermesChatTitle: 'Hermes 会話',
hermesChatDialogTitle: 'Hermes 会話 - {0}',
hermesChatNewChat: '新しい会話',
hermesChatNoSessions: 'セッションはありません',
hermesChatMessageCount: '{0}',
hermesChatEmptyHint: '左側のセッションを選択するか新しい会話をクリックしてください',
hermesChatTitlePlaceholder: 'セッションタイトルを入力してください',
hermesChatRenameSuccess: 'セッションタイトルを更新しました',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+9
View File
@@ -762,6 +762,15 @@ const message = {
skillCount: '기술 ',
jobCount: '예약 작업 ',
sessionCount: '세션 ',
hermesChatAction: '대화',
hermesChatTitle: 'Hermes 대화',
hermesChatDialogTitle: 'Hermes 대화 - {0}',
hermesChatNewChat: ' 대화',
hermesChatNoSessions: '세션이 없습니다',
hermesChatMessageCount: '{0}',
hermesChatEmptyHint: '왼쪽 세션을 선택하거나 대화를 클릭하세요',
hermesChatTitlePlaceholder: '세션 제목을 입력하세요',
hermesChatRenameSuccess: '세션 제목이 업데이트되었습니다',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+9
View File
@@ -781,6 +781,15 @@ const message = {
skillCount: 'Bilangan kemahiran',
jobCount: 'Bilangan tugas berjadual',
sessionCount: 'Bilangan sesi',
hermesChatAction: 'Perbualan',
hermesChatTitle: 'Perbualan Hermes',
hermesChatDialogTitle: 'Perbualan Hermes - {0}',
hermesChatNewChat: 'Perbualan baharu',
hermesChatNoSessions: 'Tiada sesi',
hermesChatMessageCount: '{0} mesej',
hermesChatEmptyHint: 'Pilih sesi di sebelah kiri atau klik Perbualan baharu',
hermesChatTitlePlaceholder: 'Masukkan tajuk sesi',
hermesChatRenameSuccess: 'Tajuk sesi telah dikemas kini',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+9
View File
@@ -778,6 +778,15 @@ const message = {
skillCount: 'Habilidades',
jobCount: 'Tarefas agendadas',
sessionCount: 'Sessões',
hermesChatAction: 'Conversa',
hermesChatTitle: 'Conversa do Hermes',
hermesChatDialogTitle: 'Conversa do Hermes - {0}',
hermesChatNewChat: 'Nova conversa',
hermesChatNoSessions: 'Nenhuma sessão',
hermesChatMessageCount: '{0} mensagens',
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',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+9
View File
@@ -773,6 +773,15 @@ const message = {
skillCount: 'Навыки',
jobCount: 'Запланированные задачи',
sessionCount: 'Сессии',
hermesChatAction: 'Диалог',
hermesChatTitle: 'Диалог Hermes',
hermesChatDialogTitle: 'Диалог Hermes - {0}',
hermesChatNewChat: 'Новый диалог',
hermesChatNoSessions: 'Нет сессий',
hermesChatMessageCount: '{0} сообщений',
hermesChatEmptyHint: 'Выберите сессию слева или нажмите «Новый диалог»',
hermesChatTitlePlaceholder: 'Введите название сессии',
hermesChatRenameSuccess: 'Название сессии обновлено',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+9
View File
@@ -780,6 +780,15 @@ const message = {
skillCount: 'Yetenekler',
jobCount: 'Zamanlanmış görevler',
sessionCount: 'Oturumlar',
hermesChatAction: 'Sohbet',
hermesChatTitle: 'Hermes Sohbeti',
hermesChatDialogTitle: 'Hermes Sohbeti - {0}',
hermesChatNewChat: 'Yeni sohbet',
hermesChatNoSessions: 'Oturum yok',
hermesChatMessageCount: '{0} mesaj',
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',
weixin: 'Weixin',
wecom: 'WeCom',
dingtalk: 'DingTalk',
+9
View File
@@ -727,6 +727,15 @@ const message = {
skillCount: '技能數量',
jobCount: '定時任務數量',
sessionCount: '會話數量',
hermesChatAction: '對話',
hermesChatTitle: 'Hermes 對話',
hermesChatDialogTitle: 'Hermes 對話 - {0}',
hermesChatNewChat: '新對話',
hermesChatNoSessions: '暫無會話',
hermesChatMessageCount: '{0} ',
hermesChatEmptyHint: '選擇左側會話或點擊新對話',
hermesChatTitlePlaceholder: '請輸入會話標題',
hermesChatRenameSuccess: '會話標題已更新',
weixin: '微信',
wecom: '企業微信',
dingtalk: '釘釘',
+9
View File
@@ -722,6 +722,15 @@ const message = {
skillCount: '技能数量',
jobCount: '定时任务数量',
sessionCount: '会话数量',
hermesChatAction: '对话',
hermesChatTitle: 'Hermes 对话',
hermesChatDialogTitle: 'Hermes 对话 - {0}',
hermesChatNewChat: '新对话',
hermesChatNoSessions: '暂无会话',
hermesChatMessageCount: '{0} ',
hermesChatEmptyHint: '选择左侧会话或点击新对话',
hermesChatTitlePlaceholder: '请输入会话标题',
hermesChatRenameSuccess: '会话标题已更新',
weixin: '微信',
wecom: '企业微信',
dingtalk: '钉钉',
@@ -11,8 +11,8 @@
<el-form-item :label="`${$t('aiTools.agents.agent')}${$t('commons.table.type')}`" prop="agentType">
<el-select v-model="form.agentType" @change="handleAgentTypeChange">
<el-option :label="$t('aiTools.agents.openclawType')" value="openclaw" />
<el-option :label="$t('aiTools.agents.copawType')" value="copaw" />
<el-option :label="$t('aiTools.agents.hermesType')" value="hermes-agent" />
<el-option :label="$t('aiTools.agents.copawType')" value="copaw" />
</el-select>
</el-form-item>
<el-form-item :label="$t('aiTools.agents.appVersion')" prop="appVersion">
@@ -0,0 +1,381 @@
<template>
<DialogPro v-model="dialogVisible" :title="dialogTitle" size="w-90" @close="handleClose">
<template #content>
<div class="hermes-chat-dialog">
<div class="hermes-chat-dialog__sidebar" v-loading="loading">
<div class="hermes-chat-dialog__toolbar">
<el-button type="primary" @click="openNewChat">
{{ t('aiTools.agents.hermesChatNewChat') }}
</el-button>
<el-button plain @click="loadSessions">{{ $t('commons.button.refresh') }}</el-button>
<el-button v-if="terminalOpen" plain @click="disconnectTerminal">
{{ $t('commons.button.disConn') }}
</el-button>
</div>
<div class="hermes-chat-dialog__list">
<el-empty
v-if="sessions.length === 0"
:image-size="64"
:description="t('aiTools.agents.hermesChatNoSessions')"
/>
<div
v-for="item in sessions"
:key="item.id"
class="hermes-chat-dialog__item"
:class="{ 'is-active': activeSessionId === item.id }"
@click="openSession(item)"
>
<div
class="hermes-chat-dialog__item-header"
:class="{ 'is-editing': editingSessionId === item.id }"
>
<input
v-if="editingSessionId === item.id"
ref="editingInputRef"
v-model="editingTitle"
class="hermes-chat-dialog__item-input"
:placeholder="t('aiTools.agents.hermesChatTitlePlaceholder')"
@click.stop
@mousedown.stop
@keydown.enter.stop.prevent="saveSessionTitle(item)"
@keydown.esc.stop.prevent="cancelEditSessionTitle"
/>
<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)">
{{ $t('commons.button.save') }}
</el-button>
<el-button link @click.stop="cancelEditSessionTitle">
{{ $t('commons.button.cancel') }}
</el-button>
</template>
<el-button
v-else-if="!terminalOpen"
link
icon="Edit"
class="hermes-chat-dialog__edit-button"
@mousedown.stop
@click.stop="startEditSessionTitle(item)"
/>
</div>
</div>
<div class="hermes-chat-dialog__item-model">{{ item.model || '-' }}</div>
<div class="hermes-chat-dialog__item-meta">
<span>{{ t('aiTools.agents.hermesChatMessageCount', [item.messageCount]) }}</span>
<span>{{ formatTime(item.lastActive) }}</span>
</div>
</div>
</div>
</div>
<div class="hermes-chat-dialog__content">
<div v-if="!terminalOpen" class="hermes-chat-dialog__empty">
{{ t('aiTools.agents.hermesChatEmptyHint') }}
</div>
<Terminal v-else ref="terminalRef" class="hermes-chat-dialog__terminal" />
</div>
</div>
</template>
</DialogPro>
</template>
<script setup lang="ts">
import { 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 { AI } from '@/api/interface/ai';
import { useGlobalStore } from '@/composables/useGlobalStore';
import { dateFormat } from '@/utils/date';
import { MsgError, MsgSuccess } from '@/utils/message';
const { currentNode } = useGlobalStore();
const { t } = useI18n();
interface HermesChatDialogParams {
agentId: number;
containerID: string;
title: string;
node?: string;
}
const dialogVisible = ref(false);
const dialogTitle = ref(t('aiTools.agents.hermesChatTitle'));
const terminalOpen = ref(false);
const loading = ref(false);
const terminalRef = ref<InstanceType<typeof Terminal> | null>(null);
const editingInputRef = ref<HTMLInputElement | null>(null);
const agentId = ref(0);
const containerID = ref('');
const node = ref('');
const sessions = ref<AI.AgentHermesChatSessionItem[]>([]);
const activeSessionId = ref('');
const editingSessionId = ref('');
const editingTitle = ref('');
const formatTime = (value: string) => {
if (!value) {
return '-';
}
return dateFormat({}, {}, value);
};
const connectTerminal = async (initCmd: string) => {
terminalRef.value?.onClose();
terminalOpen.value = false;
await nextTick();
terminalOpen.value = true;
await nextTick();
let args = `source=container&containerid=${containerID.value}&user=hermes&command=/bin/bash`;
if (node.value) {
args += `&operateNode=${node.value}`;
}
terminalRef.value?.acceptParams({
endpoint: '/api/v2/containers/exec',
args,
error: '',
initCmd,
});
};
const loadSessions = async () => {
if (!agentId.value) {
sessions.value = [];
return;
}
loading.value = true;
try {
const res = await getAgentHermesChatSessions({ agentId: agentId.value });
sessions.value = res.data || [];
} catch (error) {
sessions.value = [];
MsgError(String(error?.message || t('commons.res.commonError')));
} finally {
loading.value = false;
}
};
const openNewChat = async () => {
cancelEditSessionTitle();
activeSessionId.value = '';
await connectTerminal('hermes\n');
};
const openSession = async (item: AI.AgentHermesChatSessionItem) => {
if (editingSessionId.value === item.id) {
return;
}
cancelEditSessionTitle();
activeSessionId.value = item.id;
await connectTerminal(`hermes --resume ${item.id}\n`);
};
const focusEditingInput = async () => {
await nextTick();
editingInputRef.value?.focus();
editingInputRef.value?.select();
};
const startEditSessionTitle = async (item: AI.AgentHermesChatSessionItem) => {
if (terminalOpen.value) {
return;
}
editingSessionId.value = item.id;
editingTitle.value = item.title || item.id;
await focusEditingInput();
};
const cancelEditSessionTitle = () => {
editingSessionId.value = '';
editingTitle.value = '';
};
const saveSessionTitle = async (item: AI.AgentHermesChatSessionItem) => {
await renameAgentHermesChatSession({
agentId: agentId.value,
id: item.id,
title: editingTitle.value,
});
MsgSuccess(t('aiTools.agents.hermesChatRenameSuccess'));
cancelEditSessionTitle();
await loadSessions();
};
const disconnectTerminal = () => {
terminalRef.value?.onClose();
terminalOpen.value = false;
cancelEditSessionTitle();
};
const acceptParams = async (params: HermesChatDialogParams) => {
dialogVisible.value = true;
dialogTitle.value = params.title;
agentId.value = params.agentId;
containerID.value = params.containerID;
node.value = params.node || currentNode.value;
activeSessionId.value = '';
sessions.value = [];
cancelEditSessionTitle();
terminalRef.value?.onClose();
terminalOpen.value = false;
await loadSessions();
};
const handleClose = () => {
terminalRef.value?.onClose();
terminalOpen.value = false;
dialogVisible.value = false;
activeSessionId.value = '';
cancelEditSessionTitle();
};
defineExpose({
acceptParams,
});
</script>
<style scoped lang="scss">
.hermes-chat-dialog {
display: grid;
grid-template-columns: 280px minmax(0, 1fr);
gap: 16px;
height: 68vh;
min-height: 520px;
max-height: 720px;
padding-bottom: 2px;
box-sizing: border-box;
overflow: hidden;
}
.hermes-chat-dialog__sidebar {
border: 1px solid var(--el-border-color);
border-radius: 8px;
padding: 12px;
box-sizing: border-box;
display: flex;
flex-direction: column;
min-height: 0;
height: 100%;
overflow: hidden;
}
.hermes-chat-dialog__toolbar {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.hermes-chat-dialog__list {
flex: 1;
display: flex;
flex-direction: column;
gap: 8px;
overflow: auto;
min-height: 0;
}
.hermes-chat-dialog__item {
border: 1px solid var(--el-border-color);
border-radius: 8px;
padding: 10px 12px;
cursor: pointer;
}
.hermes-chat-dialog__item-header {
display: grid;
grid-template-columns: minmax(0, 1fr) auto;
align-items: start;
gap: 8px;
}
.hermes-chat-dialog__item-header.is-editing {
align-items: center;
}
.hermes-chat-dialog__item.is-active {
border-color: var(--el-color-primary);
background: var(--el-color-primary-light-9);
}
.hermes-chat-dialog__item-title {
flex: 1;
font-weight: 500;
color: var(--el-text-color-primary);
word-break: break-all;
line-height: 20px;
}
.hermes-chat-dialog__item-actions {
display: flex;
align-items: flex-start;
gap: 4px;
flex-shrink: 0;
min-height: 20px;
}
.hermes-chat-dialog__item-header.is-editing .hermes-chat-dialog__item-actions {
align-items: center;
min-height: 28px;
}
.hermes-chat-dialog__item-input {
flex: 1;
min-width: 0;
height: 28px;
padding: 0 8px;
border: 1px solid var(--el-border-color);
border-radius: 4px;
outline: none;
font-size: 13px;
color: var(--el-text-color-primary);
background: var(--el-bg-color);
box-sizing: border-box;
}
.hermes-chat-dialog__item-input:focus {
border-color: var(--el-color-primary);
}
.hermes-chat-dialog__item-model {
margin-top: 6px;
font-size: 12px;
color: var(--el-text-color-secondary);
word-break: break-all;
}
.hermes-chat-dialog__item-meta {
margin-top: 8px;
display: flex;
justify-content: space-between;
gap: 8px;
font-size: 12px;
color: var(--el-text-color-secondary);
}
.hermes-chat-dialog__content {
border: 1px solid var(--el-border-color);
border-radius: 8px;
box-sizing: border-box;
min-height: 0;
height: 100%;
overflow: hidden;
}
.hermes-chat-dialog__empty {
height: 100%;
min-height: 0;
display: flex;
align-items: center;
justify-content: center;
color: var(--el-text-color-secondary);
}
.hermes-chat-dialog__terminal {
height: 100%;
}
.hermes-chat-dialog__list :deep(.el-empty) {
margin: auto 0;
}
</style>
@@ -164,7 +164,7 @@ const openHermesWeixinTerminal = () => {
title: `${t('aiTools.agents.agent')} ${props.agentName}`,
users: ['hermes', 'root'],
shell: '/bin/bash',
initCmd: 'source /opt/hermes/.venv/bin/activate\ncd /opt/data/workspace\nhermes gateway setup\n',
initCmd: 'hermes gateway setup\n',
});
};
+26 -7
View File
@@ -196,6 +196,7 @@
<AppUpgrade ref="upgradeRef" @close="search" />
<ComposeLogs ref="composeLogRef" />
<AgentTerminalDialog ref="dialogTerminalRef" />
<HermesChatDialog ref="hermesChatRef" />
<PortJumpDialog ref="dialogPortJumpRef" />
</div>
</template>
@@ -223,6 +224,7 @@ import AppUpgrade from '@/views/app-store/installed/upgrade/index.vue';
import TaskLog from '@/components/log/task/index.vue';
import ComposeLogs from '@/components/log/compose/index.vue';
import AgentTerminalDialog from '@/views/ai/agents/agent/components/terminal.vue';
import HermesChatDialog from '@/views/ai/agents/agent/components/hermes-chat.vue';
import i18n from '@/lang';
import PortJumpDialog from '@/components/port-jump/index.vue';
import DockerStatus from '@/views/container/docker-status/index.vue';
@@ -251,6 +253,7 @@ const bindWebsiteRef = ref();
const upgradeRef = ref();
const composeLogRef = ref();
const dialogTerminalRef = ref();
const hermesChatRef = ref();
const dialogPortJumpRef = ref();
const route = useRoute();
const router = useRouter();
@@ -276,6 +279,11 @@ const buttons = [
show: (row: AI.AgentItem) => supportsAgentModelConfig(row.agentType),
disabled: (row: AI.AgentItem) => row.status !== 'Running',
},
{
label: i18n.global.t('aiTools.agents.hermesChatAction'),
click: (row: AI.AgentItem) => openHermesChat(row),
show: (row: AI.AgentItem) => row.agentType === 'hermes-agent' && row.status === 'Running',
},
{
label: i18n.global.t('menu.terminal'),
click: (row: AI.AgentItem) => openTerminal(row),
@@ -464,18 +472,29 @@ const openTerminal = (row: AI.AgentItem) => {
title,
users: row.agentType === 'hermes-agent' ? ['hermes', 'root'] : ['node', 'root'],
shell: '/bin/bash',
initCmd:
row.agentType === 'hermes-agent' ? 'source /opt/hermes/.venv/bin/activate\ncd /opt/data/workspace\n' : '',
initCmd: '',
});
};
const openHermesChat = (row: AI.AgentItem) => {
hermesChatRef.value?.acceptParams({
agentId: row.id,
containerID: row.containerName,
title: i18n.global.t('aiTools.agents.hermesChatDialogTitle', [row.name]),
});
};
const handleConfigRestartRequired = async (installId: number) => {
try {
await ElMessageBox.confirm(t('aiTools.agents.configFileRestartHelper'), t('database.restartNow'), {
confirmButtonText: t('database.restartNow'),
cancelButtonText: t('commons.button.cancel'),
type: 'info',
});
await ElMessageBox.confirm(
i18n.global.t('aiTools.agents.configFileRestartHelper'),
i18n.global.t('database.restartNow'),
{
confirmButtonText: i18n.global.t('database.restartNow'),
cancelButtonText: i18n.global.t('commons.button.cancel'),
type: 'info',
},
);
await restartInstall(installId);
} catch {}
};
+4 -4
View File
@@ -256,8 +256,8 @@
baseInfo.prettyDistro
? baseInfo.prettyDistro
: baseInfo.platformVersion
? baseInfo.platform + '-' + baseInfo.platformVersion
: baseInfo.platform
? baseInfo.platform + '-' + baseInfo.platformVersion
: baseInfo.platform
}}
</el-descriptions-item>
<el-descriptions-item
@@ -830,8 +830,8 @@ const handleCopy = () => {
(baseInfo.value.prettyDistro
? baseInfo.value.prettyDistro
: baseInfo.value.platformVersion
? baseInfo.value.platform + '-' + baseInfo.value.platformVersion
: baseInfo.value.platform) +
? baseInfo.value.platform + '-' + baseInfo.value.platformVersion
: baseInfo.value.platform) +
'\n' +
i18n.global.t('home.kernelVersion') +
': ' +