mirror of
https://github.com/1Panel-dev/1Panel.git
synced 2026-09-22 16:00:51 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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...)
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
};
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '釘釘',
|
||||
|
||||
@@ -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',
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {}
|
||||
};
|
||||
|
||||
@@ -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') +
|
||||
': ' +
|
||||
|
||||
Reference in New Issue
Block a user