mirror of
https://github.com/Ed1s0nZ/CyberStrikeAI.git
synced 2026-07-16 00:47:29 +02:00
Add files via upload
This commit is contained in:
@@ -729,7 +729,7 @@ func (h *AgentHandler) runRobotMultiAgentWithRetry(
|
||||
}
|
||||
|
||||
// ProcessMessageForRobot 供机器人(企业微信/钉钉/飞书)调用:Eino 单/多代理执行路径(含 progressCallback、过程详情),仅不发送 SSE,最后返回完整回复
|
||||
func (h *AgentHandler) ProcessMessageForRobot(ctx context.Context, platform string, principal authctx.Principal, conversationID, message, role string) (response string, convID string, err error) {
|
||||
func (h *AgentHandler) ProcessMessageForRobot(ctx context.Context, platform string, principal authctx.Principal, conversationID, message, role, agentMode string) (response string, convID string, err error) {
|
||||
ownerUserID := strings.TrimSpace(principal.UserID)
|
||||
if ownerUserID == "" {
|
||||
return "", "", fmt.Errorf("authenticated robot principal is required")
|
||||
@@ -814,18 +814,14 @@ func (h *AgentHandler) ProcessMessageForRobot(ctx context.Context, platform stri
|
||||
}
|
||||
progressCallback := h.createProgressCallback(taskCtx, cancelWithCause, conversationID, assistantMessageID, nil)
|
||||
|
||||
robotMode := "eino_single"
|
||||
if h.config != nil {
|
||||
robotMode = config.NormalizeRobotAgentMode(h.config.MultiAgent)
|
||||
}
|
||||
robotMode := config.NormalizeAgentMode(agentMode)
|
||||
switch robotMode {
|
||||
case "eino_single":
|
||||
return h.runRobotEinoSingleWithRetry(taskCtx, conversationID, finalMessage, agentHistoryMessages, roleTools, progressCallback, assistantMessageID, &taskStatus)
|
||||
case "deep", "plan_execute", "supervisor":
|
||||
if h.config == nil || !h.config.MultiAgent.Enabled {
|
||||
h.logger.Warn("机器人配置为多代理模式但未启用 multi_agent,回退 Eino 单代理",
|
||||
zap.String("robot_mode", robotMode))
|
||||
return h.runRobotEinoSingleWithRetry(taskCtx, conversationID, finalMessage, agentHistoryMessages, roleTools, progressCallback, assistantMessageID, &taskStatus)
|
||||
taskStatus = "failed"
|
||||
return "", conversationID, fmt.Errorf("机器人对话模式 %s 需要启用 Eino 多代理", robotMode)
|
||||
}
|
||||
return h.runRobotMultiAgentWithRetry(taskCtx, conversationID, finalMessage, robotMode, agentHistoryMessages, roleTools, progressCallback, assistantMessageID, &taskStatus)
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func (h *MonitorHandler) SetAgentHandler(ah *AgentHandler) {
|
||||
h.agentHandler = ah
|
||||
}
|
||||
|
||||
const monitorPageTopTools = 6
|
||||
const monitorPageTopTools = 3
|
||||
|
||||
// MonitorStatsSummary 工具调用汇总
|
||||
type MonitorStatsSummary struct {
|
||||
|
||||
+402
-63
@@ -41,11 +41,14 @@ const (
|
||||
robotCmdContinue = "继续"
|
||||
robotCmdNew = "新对话"
|
||||
robotCmdClear = "清空"
|
||||
robotCmdCurrent = "当前"
|
||||
robotCmdStatus = "状态"
|
||||
robotCmdStop = "停止"
|
||||
robotCmdRoles = "角色"
|
||||
robotCmdRolesList = "角色列表"
|
||||
robotCmdSwitchRole = "切换角色"
|
||||
robotCmdModes = "模式"
|
||||
robotCmdModesList = "模式列表"
|
||||
robotCmdSwitchMode = "切换模式"
|
||||
robotCmdDelete = "删除"
|
||||
robotCmdVersion = "版本"
|
||||
robotCmdProjects = "项目"
|
||||
@@ -56,35 +59,51 @@ const (
|
||||
robotCmdBindUser = "绑定"
|
||||
robotCmdUnbindUser = "解绑"
|
||||
robotCmdIdentity = "身份"
|
||||
robotCmdTask = "任务"
|
||||
robotCmdRename = "重命名"
|
||||
robotCmdPermissions = "权限"
|
||||
robotCmdDoctor = "诊断"
|
||||
robotCmdConfirm = "确认"
|
||||
robotCmdCancel = "取消"
|
||||
robotBindingCodeTTL = 5 * time.Minute
|
||||
)
|
||||
|
||||
type robotPendingConfirmation struct {
|
||||
Action string
|
||||
Target string
|
||||
ExpiresAt time.Time
|
||||
}
|
||||
|
||||
// RobotHandler 企业微信/钉钉/飞书等机器人回调处理
|
||||
type RobotHandler struct {
|
||||
config *config.Config
|
||||
db *database.DB
|
||||
agentHandler *AgentHandler
|
||||
logger *zap.Logger
|
||||
mu sync.RWMutex
|
||||
sessions map[string]string // key: "platform_userID", value: conversationID
|
||||
sessionRoles map[string]string // key: "platform_userID", value: roleName(默认"默认")
|
||||
cancelMu sync.Mutex // 保护 runningCancels
|
||||
runningCancels map[string]context.CancelFunc // key: "platform_userID", 用于停止命令中断任务
|
||||
wecomReplay map[string]time.Time
|
||||
audit *audit.Service
|
||||
config *config.Config
|
||||
db *database.DB
|
||||
agentHandler *AgentHandler
|
||||
logger *zap.Logger
|
||||
mu sync.RWMutex
|
||||
sessions map[string]string // key: "platform_userID", value: conversationID
|
||||
sessionRoles map[string]string // key: "platform_userID", value: roleName(默认"默认")
|
||||
sessionModes map[string]string // key: "platform_userID", value: agent mode
|
||||
cancelMu sync.Mutex // 保护 runningCancels
|
||||
runningCancels map[string]context.CancelFunc // key: "platform_userID", 用于停止命令中断任务
|
||||
wecomReplay map[string]time.Time
|
||||
pendingConfirmations map[string]robotPendingConfirmation
|
||||
audit *audit.Service
|
||||
}
|
||||
|
||||
// NewRobotHandler 创建机器人处理器
|
||||
func NewRobotHandler(cfg *config.Config, db *database.DB, agentHandler *AgentHandler, logger *zap.Logger) *RobotHandler {
|
||||
return &RobotHandler{
|
||||
config: cfg,
|
||||
db: db,
|
||||
agentHandler: agentHandler,
|
||||
logger: logger,
|
||||
sessions: make(map[string]string),
|
||||
sessionRoles: make(map[string]string),
|
||||
runningCancels: make(map[string]context.CancelFunc),
|
||||
wecomReplay: make(map[string]time.Time),
|
||||
config: cfg,
|
||||
db: db,
|
||||
agentHandler: agentHandler,
|
||||
logger: logger,
|
||||
sessions: make(map[string]string),
|
||||
sessionRoles: make(map[string]string),
|
||||
sessionModes: make(map[string]string),
|
||||
runningCancels: make(map[string]context.CancelFunc),
|
||||
wecomReplay: make(map[string]time.Time),
|
||||
pendingConfirmations: make(map[string]robotPendingConfirmation),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,26 +194,26 @@ func (h *RobotHandler) robotAccessDeniedMessage(platform string) string {
|
||||
return "当前平台账号尚未绑定 CyberStrikeAI 用户。请先在网页端生成绑定码,然后发送:绑定 XXXX-XXXX"
|
||||
}
|
||||
|
||||
func (h *RobotHandler) loadSessionBinding(sk string) (convID, role string) {
|
||||
func (h *RobotHandler) loadSessionBinding(sk string) (convID, role, agentMode string) {
|
||||
if h.db == nil || strings.TrimSpace(sk) == "" {
|
||||
return "", ""
|
||||
return "", "", ""
|
||||
}
|
||||
binding, err := h.db.GetRobotSessionBinding(sk)
|
||||
if err != nil {
|
||||
h.logger.Warn("读取机器人会话绑定失败", zap.String("session_key", sk), zap.Error(err))
|
||||
return "", ""
|
||||
return "", "", ""
|
||||
}
|
||||
if binding == nil {
|
||||
return "", ""
|
||||
return "", "", ""
|
||||
}
|
||||
return binding.ConversationID, binding.RoleName
|
||||
return binding.ConversationID, binding.RoleName, binding.AgentMode
|
||||
}
|
||||
|
||||
func (h *RobotHandler) persistSessionBinding(sk, convID, role string) {
|
||||
func (h *RobotHandler) persistSessionBinding(sk, convID, role, agentMode string) {
|
||||
if h.db == nil || strings.TrimSpace(sk) == "" || strings.TrimSpace(convID) == "" {
|
||||
return
|
||||
}
|
||||
if err := h.db.UpsertRobotSessionBinding(sk, convID, role); err != nil {
|
||||
if err := h.db.UpsertRobotSessionBinding(sk, convID, role, agentMode); err != nil {
|
||||
h.logger.Warn("写入机器人会话绑定失败", zap.String("session_key", sk), zap.Error(err))
|
||||
}
|
||||
}
|
||||
@@ -219,7 +238,7 @@ func (h *RobotHandler) getOrCreateConversation(platform, userID, title string, a
|
||||
if convID != "" && access.Permissions["chat:read"] && h.db.UserCanAccessResource(ownerID, readScope, "conversation", convID) {
|
||||
return convID, false
|
||||
}
|
||||
if persistedConvID, persistedRole := h.loadSessionBinding(sk); strings.TrimSpace(persistedConvID) != "" {
|
||||
if persistedConvID, persistedRole, persistedMode := h.loadSessionBinding(sk); strings.TrimSpace(persistedConvID) != "" {
|
||||
if !access.Permissions["chat:read"] || !h.db.UserCanAccessResource(ownerID, readScope, "conversation", persistedConvID) {
|
||||
h.deleteSessionBinding(sk)
|
||||
} else {
|
||||
@@ -229,6 +248,9 @@ func (h *RobotHandler) getOrCreateConversation(platform, userID, title string, a
|
||||
if strings.TrimSpace(persistedRole) != "" {
|
||||
h.sessionRoles[sk] = persistedRole
|
||||
}
|
||||
if strings.TrimSpace(persistedMode) != "" {
|
||||
h.sessionModes[sk] = config.NormalizeAgentMode(persistedMode)
|
||||
}
|
||||
h.mu.Unlock()
|
||||
return persistedConvID, false
|
||||
}
|
||||
@@ -256,9 +278,13 @@ func (h *RobotHandler) getOrCreateConversation(platform, userID, title string, a
|
||||
_ = h.db.SetResourceOwner("conversation", convID, ownerID)
|
||||
h.mu.Lock()
|
||||
role := h.sessionRoles[sk]
|
||||
agentMode := h.sessionModes[sk]
|
||||
h.sessions[sk] = convID
|
||||
h.mu.Unlock()
|
||||
h.persistSessionBinding(sk, convID, role)
|
||||
if agentMode == "" {
|
||||
agentMode = config.NormalizeRobotAgentMode(h.config.MultiAgent)
|
||||
}
|
||||
h.persistSessionBinding(sk, convID, role, agentMode)
|
||||
return convID, true
|
||||
}
|
||||
|
||||
@@ -267,9 +293,10 @@ func (h *RobotHandler) setConversation(platform, userID, convID string) {
|
||||
sk := h.sessionKey(platform, userID)
|
||||
h.mu.Lock()
|
||||
role := h.sessionRoles[sk]
|
||||
agentMode := h.sessionModes[sk]
|
||||
h.sessions[sk] = convID
|
||||
h.mu.Unlock()
|
||||
h.persistSessionBinding(sk, convID, role)
|
||||
h.persistSessionBinding(sk, convID, role, agentMode)
|
||||
}
|
||||
|
||||
// getRole 获取当前用户使用的角色,未设置时返回"默认"
|
||||
@@ -281,7 +308,7 @@ func (h *RobotHandler) getRole(platform, userID string) string {
|
||||
if strings.TrimSpace(role) != "" {
|
||||
return role
|
||||
}
|
||||
if _, persistedRole := h.loadSessionBinding(sk); strings.TrimSpace(persistedRole) != "" {
|
||||
if _, persistedRole, _ := h.loadSessionBinding(sk); strings.TrimSpace(persistedRole) != "" {
|
||||
h.mu.Lock()
|
||||
h.sessionRoles[sk] = persistedRole
|
||||
h.mu.Unlock()
|
||||
@@ -296,8 +323,38 @@ func (h *RobotHandler) setRole(platform, userID, roleName string) {
|
||||
h.mu.Lock()
|
||||
h.sessionRoles[sk] = roleName
|
||||
convID := h.sessions[sk]
|
||||
agentMode := h.sessionModes[sk]
|
||||
h.mu.Unlock()
|
||||
h.persistSessionBinding(sk, convID, roleName)
|
||||
h.persistSessionBinding(sk, convID, roleName, agentMode)
|
||||
}
|
||||
|
||||
func (h *RobotHandler) getAgentMode(platform, userID string) string {
|
||||
sk := h.sessionKey(platform, userID)
|
||||
h.mu.RLock()
|
||||
mode := h.sessionModes[sk]
|
||||
h.mu.RUnlock()
|
||||
if mode != "" {
|
||||
return config.NormalizeAgentMode(mode)
|
||||
}
|
||||
if _, _, persistedMode := h.loadSessionBinding(sk); persistedMode != "" {
|
||||
mode = config.NormalizeAgentMode(persistedMode)
|
||||
h.mu.Lock()
|
||||
h.sessionModes[sk] = mode
|
||||
h.mu.Unlock()
|
||||
return mode
|
||||
}
|
||||
return config.NormalizeRobotAgentMode(h.config.MultiAgent)
|
||||
}
|
||||
|
||||
func (h *RobotHandler) setAgentMode(platform, userID, mode string) {
|
||||
sk := h.sessionKey(platform, userID)
|
||||
mode = config.NormalizeAgentMode(mode)
|
||||
h.mu.Lock()
|
||||
h.sessionModes[sk] = mode
|
||||
convID := h.sessions[sk]
|
||||
role := h.sessionRoles[sk]
|
||||
h.mu.Unlock()
|
||||
h.persistSessionBinding(sk, convID, role, mode)
|
||||
}
|
||||
|
||||
// clearConversation 清空当前会话(切换到新对话)
|
||||
@@ -379,7 +436,8 @@ func (h *RobotHandler) HandleMessage(platform, userID, text string) (reply strin
|
||||
h.cancelMu.Unlock()
|
||||
}()
|
||||
role := h.getRole(platform, userID)
|
||||
resp, newConvID, err := h.agentHandler.ProcessMessageForRobot(ctx, platform, robotPrincipal(access), convID, text, role)
|
||||
agentMode := h.getAgentMode(platform, userID)
|
||||
resp, newConvID, err := h.agentHandler.ProcessMessageForRobot(ctx, platform, robotPrincipal(access), convID, text, role, agentMode)
|
||||
if err != nil {
|
||||
h.logger.Warn("机器人 Agent 执行失败", zap.String("platform", platform), zap.String("userID", userID), zap.Error(err))
|
||||
if errors.Is(err, context.Canceled) {
|
||||
@@ -401,32 +459,51 @@ func (h *RobotHandler) robotMessageTimeout() time.Duration {
|
||||
return 10 * time.Hour
|
||||
}
|
||||
|
||||
func (h *RobotHandler) cmdHelp() string {
|
||||
func (h *RobotHandler) cmdHelp(platform, userID string) string {
|
||||
access, _ := h.resolveRobotAccess(platform, userID)
|
||||
can := func(permission string) bool {
|
||||
return access != nil && access.Permissions[permission]
|
||||
}
|
||||
var b strings.Builder
|
||||
b.WriteString("【CyberStrikeAI 机器人命令】\n\n")
|
||||
b.WriteString("【通用 General】\n")
|
||||
b.WriteString("· 帮助 / help — 显示本帮助\n")
|
||||
b.WriteString("· 版本 / version — 显示当前版本号\n")
|
||||
b.WriteString("· 绑定 <绑定码> / bind <code> — 绑定网页端 RBAC 用户\n")
|
||||
b.WriteString("· 解绑 / unbind — 解除当前平台账号绑定\n")
|
||||
b.WriteString("· 解绑 / unbind — 请求解除账号绑定(需确认)\n")
|
||||
b.WriteString("· 身份 / whoami — 显示平台发送者、鉴权模式及当前实际 RBAC 身份\n")
|
||||
b.WriteString("\n【对话 Conversation】\n")
|
||||
b.WriteString("· 列表 / list — 列出所有对话标题与 ID\n")
|
||||
b.WriteString("· 切换 <ID> / switch <ID> — 指定对话继续\n")
|
||||
b.WriteString("· 新对话 / new — 开启新对话\n")
|
||||
b.WriteString("· 清空 / clear — 清空当前上下文\n")
|
||||
b.WriteString("· 当前 / current — 显示当前对话、角色与项目\n")
|
||||
b.WriteString("· 停止 / stop — 中断当前任务\n")
|
||||
b.WriteString("· 删除 <ID> / delete <ID> — 删除指定对话\n")
|
||||
b.WriteString("\n【角色 Role】\n")
|
||||
b.WriteString("· 角色 / roles — 列出所有可用角色\n")
|
||||
b.WriteString("· 角色 <名> / role <name> — 切换当前角色\n")
|
||||
if h.projectsEnabled() {
|
||||
if can("chat:read") || can("chat:write") || can("chat:delete") {
|
||||
b.WriteString("\n【对话 Conversation】\n")
|
||||
if can("chat:read") {
|
||||
b.WriteString("· 列表 / list — 列出所有对话标题与 ID\n· 切换 <ID> / switch <ID> — 指定对话继续\n· 状态 / status — 汇总当前选择\n· 任务 / task — 查看当前任务状态\n")
|
||||
}
|
||||
if can("chat:write") {
|
||||
b.WriteString("· 新对话 / new;清空 / clear — 开启新对话\n· 重命名 <名称> / rename <name> — 修改当前对话标题\n")
|
||||
}
|
||||
if can("chat:delete") {
|
||||
b.WriteString("· 删除 <ID> / delete <ID> — 删除指定对话(需确认)\n")
|
||||
}
|
||||
}
|
||||
if can("roles:read") {
|
||||
b.WriteString("\n【角色 Role】\n· 角色 / roles — 列出所有可用角色\n· 角色 <名> / role <name> — 切换当前角色\n")
|
||||
}
|
||||
if can("agent:execute") {
|
||||
b.WriteString("\n【模式 Mode】\n· 模式 / modes — 列出对话模式与当前选择\n· 模式 <名称> / mode <name> — 切换对话模式\n· 停止 / stop — 中断当前任务\n")
|
||||
}
|
||||
b.WriteString("\n【诊断 Diagnostics】\n")
|
||||
b.WriteString("· 权限 / permissions — 查看当前业务权限\n")
|
||||
if can("config:read") {
|
||||
b.WriteString("· 诊断 / doctor — 检查机器人关键配置状态\n")
|
||||
}
|
||||
b.WriteString("· 确认 / confirm;取消 / cancel — 处理高风险操作确认\n")
|
||||
if h.projectsEnabled() && (can("project:read") || can("project:write")) {
|
||||
b.WriteString("\n【项目 Project】\n")
|
||||
b.WriteString("· 项目 / projects — 列出所有项目\n")
|
||||
b.WriteString("· 新建项目 <名称> / new project <name> — 创建并绑定当前对话\n")
|
||||
b.WriteString("· 绑定项目 <ID或名称> / bind project <ID|name> — 绑定到已有项目\n")
|
||||
b.WriteString("· 解除项目 / unbind project — 解除项目绑定\n")
|
||||
if can("project:read") {
|
||||
b.WriteString("· 项目 / projects — 列出所有项目\n")
|
||||
}
|
||||
if can("project:write") {
|
||||
b.WriteString("· 新建项目 <名称> / new project <name> — 创建并绑定当前对话\n· 绑定项目 <ID或名称> / bind project <ID|name> — 绑定已有项目\n· 解除项目 / unbind project — 解除项目绑定\n")
|
||||
}
|
||||
}
|
||||
b.WriteString("\n──────────────\n")
|
||||
b.WriteString("除以上命令外,直接输入内容将发送给 AI 进行渗透测试/安全分析。")
|
||||
@@ -575,7 +652,7 @@ func (h *RobotHandler) cmdUnbindProject(platform, userID string) string {
|
||||
convID := h.sessions[sk]
|
||||
h.mu.RUnlock()
|
||||
if convID == "" {
|
||||
if persistedConvID, _ := h.loadSessionBinding(sk); persistedConvID != "" {
|
||||
if persistedConvID, _, _ := h.loadSessionBinding(sk); persistedConvID != "" {
|
||||
convID = persistedConvID
|
||||
}
|
||||
}
|
||||
@@ -673,12 +750,10 @@ func (h *RobotHandler) cmdStop(platform, userID string) string {
|
||||
return "已停止当前任务。"
|
||||
}
|
||||
|
||||
func (h *RobotHandler) cmdCurrent(platform, userID string) string {
|
||||
h.mu.RLock()
|
||||
convID := h.sessions[h.sessionKey(platform, userID)]
|
||||
h.mu.RUnlock()
|
||||
func (h *RobotHandler) cmdStatus(platform, userID string) string {
|
||||
convID := h.currentConversationID(platform, userID)
|
||||
if convID == "" {
|
||||
return "当前没有进行中的对话。发送任意内容将创建新对话。"
|
||||
return fmt.Sprintf("【当前状态】\n当前对话: 无\n当前角色: %s\n当前模式: %s\n当前项目: 无\n\n发送任意内容将创建新对话。", h.getRole(platform, userID), robotAgentModeLabel(h.getAgentMode(platform, userID)))
|
||||
}
|
||||
access, err := h.resolveRobotAccess(platform, userID)
|
||||
if err != nil {
|
||||
@@ -692,14 +767,122 @@ func (h *RobotHandler) cmdCurrent(platform, userID string) string {
|
||||
return "当前对话 ID: " + convID + "(获取标题失败)"
|
||||
}
|
||||
role := h.getRole(platform, userID)
|
||||
reply := fmt.Sprintf("当前对话:「%s」\nID: %s\n当前角色: %s", conv.Title, conv.ID, role)
|
||||
reply := fmt.Sprintf("【当前状态】\n当前对话: %s\n对话 ID: %s\n当前模式: %s\n当前角色: %s", conv.Title, conv.ID, robotAgentModeLabel(h.getAgentMode(platform, userID)), role)
|
||||
if h.projectsEnabled() {
|
||||
projectID, _ := h.db.GetConversationProjectID(conv.ID)
|
||||
reply += "\n当前项目: " + h.formatProjectLabel(projectID)
|
||||
} else {
|
||||
reply += "\n当前项目: 未启用"
|
||||
}
|
||||
return reply
|
||||
}
|
||||
|
||||
func (h *RobotHandler) currentConversationID(platform, userID string) string {
|
||||
sk := h.sessionKey(platform, userID)
|
||||
h.mu.RLock()
|
||||
convID := h.sessions[sk]
|
||||
h.mu.RUnlock()
|
||||
if convID != "" {
|
||||
return convID
|
||||
}
|
||||
persistedConvID, persistedRole, persistedMode := h.loadSessionBinding(sk)
|
||||
if persistedConvID == "" {
|
||||
return ""
|
||||
}
|
||||
h.mu.Lock()
|
||||
h.sessions[sk] = persistedConvID
|
||||
h.sessionRoles[sk] = persistedRole
|
||||
h.sessionModes[sk] = config.NormalizeAgentMode(persistedMode)
|
||||
h.mu.Unlock()
|
||||
return persistedConvID
|
||||
}
|
||||
|
||||
func (h *RobotHandler) cmdTask(platform, userID string) string {
|
||||
convID := h.currentConversationID(platform, userID)
|
||||
if convID == "" {
|
||||
return "【任务状态】\n当前没有对话,也没有正在执行的任务。"
|
||||
}
|
||||
if h.agentHandler == nil || h.agentHandler.tasks == nil {
|
||||
return "任务状态服务不可用。"
|
||||
}
|
||||
task := h.agentHandler.tasks.GetTaskSnapshot(convID)
|
||||
if task == nil {
|
||||
return "【任务状态】\n状态: 空闲\n当前没有正在执行的任务。"
|
||||
}
|
||||
elapsed := time.Since(task.StartedAt).Round(time.Second)
|
||||
return fmt.Sprintf("【任务状态】\n状态: %s\n已运行: %s\n对话 ID: %s\n模式: %s\n可用操作: 停止 / stop", task.Status, elapsed, convID, robotAgentModeLabel(h.getAgentMode(platform, userID)))
|
||||
}
|
||||
|
||||
func (h *RobotHandler) cmdRename(platform, userID, title string) string {
|
||||
title = strings.TrimSpace(title)
|
||||
if title == "" {
|
||||
return "请指定新标题,例如:重命名 外网资产排查"
|
||||
}
|
||||
title = safeTruncateString(title, 100)
|
||||
convID := h.currentConversationID(platform, userID)
|
||||
if convID == "" {
|
||||
return "当前没有对话,无法重命名。"
|
||||
}
|
||||
access, err := h.resolveRobotAccess(platform, userID)
|
||||
if err != nil || !h.db.UserCanAccessResource(access.User.ID, robotPrincipal(access).ScopeFor("chat:write"), "conversation", convID) {
|
||||
return "当前对话不存在或无权修改。"
|
||||
}
|
||||
if err := h.db.UpdateConversationTitle(convID, title); err != nil {
|
||||
return "重命名失败: " + err.Error()
|
||||
}
|
||||
h.recordRobotCommandAudit(access, platform, "conversation_rename", "conversation", convID, "机器人重命名当前对话")
|
||||
return fmt.Sprintf("已将当前对话重命名为:「%s」", title)
|
||||
}
|
||||
|
||||
func (h *RobotHandler) cmdPermissions(platform, userID string) string {
|
||||
access, err := h.resolveRobotAccess(platform, userID)
|
||||
if err != nil {
|
||||
return h.robotAccessDeniedMessage(platform)
|
||||
}
|
||||
allowed := func(permission string) string {
|
||||
if access.Permissions[permission] {
|
||||
return "允许"
|
||||
}
|
||||
return "不允许"
|
||||
}
|
||||
return fmt.Sprintf("【当前权限】\n执行 Agent: %s\n读取对话: %s\n编辑对话: %s\n删除对话: %s\n读取角色: %s\n读取项目: %s\n编辑项目: %s\n资源范围: %s", allowed("agent:execute"), allowed("chat:read"), allowed("chat:write"), allowed("chat:delete"), allowed("roles:read"), allowed("project:read"), allowed("project:write"), access.Scope)
|
||||
}
|
||||
|
||||
func (h *RobotHandler) cmdDoctor() string {
|
||||
configured := func(ok bool) string {
|
||||
if ok {
|
||||
return "正常"
|
||||
}
|
||||
return "未配置"
|
||||
}
|
||||
enabled := func(ok bool) string {
|
||||
if ok {
|
||||
return "已启用"
|
||||
}
|
||||
return "已关闭"
|
||||
}
|
||||
enabledInternalTools := 0
|
||||
for _, tool := range h.config.Security.Tools {
|
||||
if tool.Enabled {
|
||||
enabledInternalTools++
|
||||
}
|
||||
}
|
||||
enabledExternal := 0
|
||||
for _, server := range h.config.ExternalMCP.Servers {
|
||||
if server.ExternalMCPEnable && !server.Disabled {
|
||||
enabledExternal++
|
||||
}
|
||||
}
|
||||
return fmt.Sprintf("【配置诊断】\n主模型: %s\nEino 多代理: %s\n内置 MCP 工具: %d/%d 个已启用\nHTTP MCP 服务: %s\n外部 MCP: %d 个已启用\n知识库: %s\n项目功能: %s\n说明: 内置工具不依赖 HTTP MCP 服务;此命令只检查配置,不主动探测外部服务。", configured(strings.TrimSpace(h.config.OpenAI.Model) != "" && strings.TrimSpace(h.config.OpenAI.BaseURL) != ""), enabled(h.config.MultiAgent.Enabled), enabledInternalTools, len(h.config.Security.Tools), enabled(h.config.MCP.Enabled), enabledExternal, enabled(h.config.Knowledge.Enabled), enabled(h.config.Project.Enabled))
|
||||
}
|
||||
|
||||
func (h *RobotHandler) recordRobotCommandAudit(access *database.RBACAccess, platform, action, resourceType, resourceID, message string) {
|
||||
if h.audit == nil || access == nil {
|
||||
return
|
||||
}
|
||||
h.audit.RecordSystem(audit.Entry{Category: "robot", Action: action, Result: "success", Actor: access.User.Username, ResourceType: resourceType, ResourceID: resourceID, Message: message + "(" + platform + ")"})
|
||||
}
|
||||
|
||||
func (h *RobotHandler) cmdRoles() string {
|
||||
if h.config.Roles == nil || len(h.config.Roles) == 0 {
|
||||
return "暂无可用角色。"
|
||||
@@ -753,6 +936,55 @@ func (h *RobotHandler) cmdSwitchRole(platform, userID, roleName string) string {
|
||||
return fmt.Sprintf("已切换到角色:「%s」\n%s", roleName, role.Description)
|
||||
}
|
||||
|
||||
func robotAgentModeLabel(mode string) string {
|
||||
switch config.NormalizeAgentMode(mode) {
|
||||
case "deep":
|
||||
return "Deep"
|
||||
case "plan_execute":
|
||||
return "Plan-Execute"
|
||||
case "supervisor":
|
||||
return "Supervisor"
|
||||
default:
|
||||
return "Eino 单代理"
|
||||
}
|
||||
}
|
||||
|
||||
func parseRobotAgentMode(input string) (string, bool) {
|
||||
switch strings.ToLower(strings.TrimSpace(input)) {
|
||||
case "eino_single", "eino-single", "single", "单代理", "eino单代理", "eino 单代理":
|
||||
return "eino_single", true
|
||||
case "deep":
|
||||
return "deep", true
|
||||
case "plan_execute", "plan-execute", "planexecute", "pe":
|
||||
return "plan_execute", true
|
||||
case "supervisor", "super", "sv":
|
||||
return "supervisor", true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RobotHandler) cmdModes(platform, userID string) string {
|
||||
current := h.getAgentMode(platform, userID)
|
||||
multiStatus := "可用"
|
||||
if h.config == nil || !h.config.MultiAgent.Enabled {
|
||||
multiStatus = "不可用(需在系统设置中启用 Eino 多代理)"
|
||||
}
|
||||
return fmt.Sprintf("【对话模式】\n· Eino 单代理 — 可用\n· Deep — %s\n· Plan-Execute — %s\n· Supervisor — %s\n\n当前模式: %s\n切换示例:模式 deep", multiStatus, multiStatus, multiStatus, robotAgentModeLabel(current))
|
||||
}
|
||||
|
||||
func (h *RobotHandler) cmdSwitchMode(platform, userID, input string) string {
|
||||
mode, ok := parseRobotAgentMode(input)
|
||||
if !ok {
|
||||
return fmt.Sprintf("不支持的对话模式「%s」。发送「模式」查看可用模式。", strings.TrimSpace(input))
|
||||
}
|
||||
if mode != "eino_single" && (h.config == nil || !h.config.MultiAgent.Enabled) {
|
||||
return fmt.Sprintf("无法切换到 %s:请先在系统设置中启用 Eino 多代理。", robotAgentModeLabel(mode))
|
||||
}
|
||||
h.setAgentMode(platform, userID, mode)
|
||||
return fmt.Sprintf("已切换对话模式:%s\n后续消息和新对话将使用该模式。", robotAgentModeLabel(mode))
|
||||
}
|
||||
|
||||
func (h *RobotHandler) cmdDelete(platform, userID, convID string) string {
|
||||
if convID == "" {
|
||||
return "请指定对话 ID,例如:删除 xxx-xxx-xxx"
|
||||
@@ -764,6 +996,15 @@ func (h *RobotHandler) cmdDelete(platform, userID, convID string) string {
|
||||
if !h.db.UserCanAccessResource(access.User.ID, robotPrincipal(access).ScopeFor("chat:delete"), "conversation", convID) {
|
||||
return "对话不存在或无权访问。"
|
||||
}
|
||||
h.setPendingConfirmation(platform, userID, "delete_conversation", convID)
|
||||
return fmt.Sprintf("⚠️ 即将删除对话 ID: %s\n此操作不可撤销。请在 2 分钟内发送「确认」继续,或发送「取消」。", convID)
|
||||
}
|
||||
|
||||
func (h *RobotHandler) executeDelete(platform, userID, convID string) string {
|
||||
access, err := h.resolveRobotAccess(platform, userID)
|
||||
if err != nil || !h.db.UserCanAccessResource(access.User.ID, robotPrincipal(access).ScopeFor("chat:delete"), "conversation", convID) {
|
||||
return "对话不存在或无权删除。"
|
||||
}
|
||||
sk := h.sessionKey(platform, userID)
|
||||
h.mu.RLock()
|
||||
currentConvID := h.sessions[sk]
|
||||
@@ -773,6 +1014,7 @@ func (h *RobotHandler) cmdDelete(platform, userID, convID string) string {
|
||||
h.mu.Lock()
|
||||
delete(h.sessions, sk)
|
||||
delete(h.sessionRoles, sk)
|
||||
delete(h.sessionModes, sk)
|
||||
h.mu.Unlock()
|
||||
h.deleteSessionBinding(sk)
|
||||
}
|
||||
@@ -782,6 +1024,7 @@ func (h *RobotHandler) cmdDelete(platform, userID, convID string) string {
|
||||
if err := h.db.DeleteConversation(convID); err != nil {
|
||||
return "删除失败: " + err.Error()
|
||||
}
|
||||
h.recordRobotCommandAudit(access, platform, "conversation_delete", "conversation", convID, "机器人删除对话")
|
||||
return fmt.Sprintf("已删除对话 ID: %s", convID)
|
||||
}
|
||||
|
||||
@@ -844,9 +1087,10 @@ func robotCommandPermission(text string) (string, bool) {
|
||||
case text == robotCmdList || text == robotCmdListAlt || text == "list",
|
||||
strings.HasPrefix(text, robotCmdSwitch+" "), strings.HasPrefix(text, robotCmdContinue+" "),
|
||||
strings.HasPrefix(text, "switch "), strings.HasPrefix(text, "continue "),
|
||||
text == robotCmdCurrent || text == "current":
|
||||
text == robotCmdStatus || text == "status", text == robotCmdTask || text == "task":
|
||||
return "chat:read", true
|
||||
case text == robotCmdNew || text == "new", text == robotCmdClear || text == "clear":
|
||||
case text == robotCmdNew || text == "new", text == robotCmdClear || text == "clear",
|
||||
strings.HasPrefix(text, robotCmdRename+" "), strings.HasPrefix(text, "rename "):
|
||||
return "chat:write", true
|
||||
case strings.HasPrefix(text, robotCmdDelete+" "), strings.HasPrefix(text, "delete "):
|
||||
return "chat:delete", true
|
||||
@@ -855,6 +1099,15 @@ func robotCommandPermission(text string) (string, bool) {
|
||||
case text == robotCmdRoles || text == robotCmdRolesList || text == "roles",
|
||||
strings.HasPrefix(text, robotCmdRoles+" "), strings.HasPrefix(text, robotCmdSwitchRole+" "), strings.HasPrefix(text, "role "):
|
||||
return "roles:read", true
|
||||
case text == robotCmdModes || text == robotCmdModesList || text == "modes",
|
||||
strings.HasPrefix(text, robotCmdModes+" "), strings.HasPrefix(text, robotCmdSwitchMode+" "), strings.HasPrefix(text, "mode "):
|
||||
return "agent:execute", true
|
||||
case text == robotCmdPermissions || text == "permissions":
|
||||
return "", true
|
||||
case text == robotCmdConfirm || text == "confirm", text == robotCmdCancel || text == "cancel":
|
||||
return "", true
|
||||
case text == robotCmdDoctor || text == "doctor":
|
||||
return "config:read", true
|
||||
case text == robotCmdProjects || text == robotCmdProjectsList || text == "projects":
|
||||
return "project:read", true
|
||||
case text == robotCmdUnbindProject || text == "unbind project",
|
||||
@@ -883,6 +1136,7 @@ func (h *RobotHandler) cmdBindUser(platform, userID, code string) string {
|
||||
h.mu.Lock()
|
||||
delete(h.sessions, sk)
|
||||
delete(h.sessionRoles, sk)
|
||||
delete(h.sessionModes, sk)
|
||||
h.mu.Unlock()
|
||||
h.deleteSessionBinding(sk)
|
||||
name := strings.TrimSpace(user.DisplayName)
|
||||
@@ -903,6 +1157,15 @@ func (h *RobotHandler) cmdUnbindUser(platform, userID string) string {
|
||||
if h.config.Robots.AuthorizationFor(platform).EffectiveMode() != config.RobotAuthModeUserBinding {
|
||||
return "该机器人使用受控服务账号模式,无需用户解绑。"
|
||||
}
|
||||
_, accessErr := h.resolveRobotAccess(platform, userID)
|
||||
if accessErr != nil {
|
||||
return "当前平台账号尚未绑定。"
|
||||
}
|
||||
h.setPendingConfirmation(platform, userID, "unbind_user", "")
|
||||
return "⚠️ 即将解除当前平台账号绑定。请在 2 分钟内发送「确认」继续,或发送「取消」。"
|
||||
}
|
||||
|
||||
func (h *RobotHandler) executeUnbindUser(platform, userID string) string {
|
||||
access, accessErr := h.resolveRobotAccess(platform, userID)
|
||||
if accessErr != nil {
|
||||
return "当前平台账号尚未绑定。"
|
||||
@@ -914,6 +1177,7 @@ func (h *RobotHandler) cmdUnbindUser(platform, userID string) string {
|
||||
h.mu.Lock()
|
||||
delete(h.sessions, sk)
|
||||
delete(h.sessionRoles, sk)
|
||||
delete(h.sessionModes, sk)
|
||||
h.mu.Unlock()
|
||||
h.deleteSessionBinding(sk)
|
||||
if h.audit != nil {
|
||||
@@ -926,6 +1190,50 @@ func (h *RobotHandler) cmdUnbindUser(platform, userID string) string {
|
||||
return "已解除当前平台账号与 CyberStrikeAI 用户的绑定。"
|
||||
}
|
||||
|
||||
func (h *RobotHandler) setPendingConfirmation(platform, userID, action, target string) {
|
||||
sk := h.sessionKey(platform, userID)
|
||||
now := time.Now()
|
||||
h.mu.Lock()
|
||||
for key, pending := range h.pendingConfirmations {
|
||||
if now.After(pending.ExpiresAt) {
|
||||
delete(h.pendingConfirmations, key)
|
||||
}
|
||||
}
|
||||
h.pendingConfirmations[sk] = robotPendingConfirmation{Action: action, Target: target, ExpiresAt: now.Add(2 * time.Minute)}
|
||||
h.mu.Unlock()
|
||||
}
|
||||
|
||||
func (h *RobotHandler) cmdConfirm(platform, userID string) string {
|
||||
sk := h.sessionKey(platform, userID)
|
||||
h.mu.Lock()
|
||||
pending, ok := h.pendingConfirmations[sk]
|
||||
delete(h.pendingConfirmations, sk)
|
||||
h.mu.Unlock()
|
||||
if !ok || time.Now().After(pending.ExpiresAt) {
|
||||
return "当前没有待确认操作,或确认已超时。"
|
||||
}
|
||||
switch pending.Action {
|
||||
case "delete_conversation":
|
||||
return h.executeDelete(platform, userID, pending.Target)
|
||||
case "unbind_user":
|
||||
return h.executeUnbindUser(platform, userID)
|
||||
default:
|
||||
return "待确认操作无效,已取消。"
|
||||
}
|
||||
}
|
||||
|
||||
func (h *RobotHandler) cmdCancelConfirmation(platform, userID string) string {
|
||||
sk := h.sessionKey(platform, userID)
|
||||
h.mu.Lock()
|
||||
_, ok := h.pendingConfirmations[sk]
|
||||
delete(h.pendingConfirmations, sk)
|
||||
h.mu.Unlock()
|
||||
if !ok {
|
||||
return "当前没有待确认操作。"
|
||||
}
|
||||
return "已取消待确认操作。"
|
||||
}
|
||||
|
||||
// handleRobotCommand 处理机器人内置命令;若匹配到命令返回 (回复内容, true),否则返回 ("", false)
|
||||
func (h *RobotHandler) handleRobotCommand(platform, userID, text string) (string, bool) {
|
||||
if (strings.HasPrefix(text, robotCmdBindUser+" ") || strings.HasPrefix(text, "bind ")) && !strings.HasPrefix(text, "bind project ") {
|
||||
@@ -946,9 +1254,13 @@ func (h *RobotHandler) handleRobotCommand(platform, userID, text string) (string
|
||||
}
|
||||
switch {
|
||||
case text == robotCmdHelp || text == "help" || text == "?" || text == "?":
|
||||
return h.cmdHelp(), true
|
||||
return h.cmdHelp(platform, userID), true
|
||||
case text == robotCmdIdentity || text == "whoami":
|
||||
return h.cmdIdentity(platform, userID), true
|
||||
case text == robotCmdConfirm || text == "confirm":
|
||||
return h.cmdConfirm(platform, userID), true
|
||||
case text == robotCmdCancel || text == "cancel":
|
||||
return h.cmdCancelConfirmation(platform, userID), true
|
||||
case text == robotCmdList || text == robotCmdListAlt || text == "list":
|
||||
return h.cmdList(platform, userID), true
|
||||
case strings.HasPrefix(text, robotCmdSwitch+" ") || strings.HasPrefix(text, robotCmdContinue+" ") || strings.HasPrefix(text, "switch ") || strings.HasPrefix(text, "continue "):
|
||||
@@ -968,8 +1280,18 @@ func (h *RobotHandler) handleRobotCommand(platform, userID, text string) (string
|
||||
return h.cmdNew(platform, userID), true
|
||||
case text == robotCmdClear || text == "clear":
|
||||
return h.cmdClear(platform, userID), true
|
||||
case text == robotCmdCurrent || text == "current":
|
||||
return h.cmdCurrent(platform, userID), true
|
||||
case text == robotCmdStatus || text == "status":
|
||||
return h.cmdStatus(platform, userID), true
|
||||
case text == robotCmdTask || text == "task":
|
||||
return h.cmdTask(platform, userID), true
|
||||
case strings.HasPrefix(text, robotCmdRename+" ") || strings.HasPrefix(text, "rename "):
|
||||
var title string
|
||||
if strings.HasPrefix(text, robotCmdRename+" ") {
|
||||
title = strings.TrimSpace(text[len(robotCmdRename)+1:])
|
||||
} else {
|
||||
title = strings.TrimSpace(text[len("rename "):])
|
||||
}
|
||||
return h.cmdRename(platform, userID, title), true
|
||||
case text == robotCmdStop || text == "stop":
|
||||
return h.cmdStop(platform, userID), true
|
||||
case text == robotCmdRoles || text == robotCmdRolesList || text == "roles":
|
||||
@@ -985,6 +1307,23 @@ func (h *RobotHandler) handleRobotCommand(platform, userID, text string) (string
|
||||
roleName = strings.TrimSpace(text[5:])
|
||||
}
|
||||
return h.cmdSwitchRole(platform, userID, roleName), true
|
||||
case text == robotCmdModes || text == robotCmdModesList || text == "modes":
|
||||
return h.cmdModes(platform, userID), true
|
||||
case strings.HasPrefix(text, robotCmdModes+" ") || strings.HasPrefix(text, robotCmdSwitchMode+" ") || strings.HasPrefix(text, "mode "):
|
||||
var mode string
|
||||
switch {
|
||||
case strings.HasPrefix(text, robotCmdModes+" "):
|
||||
mode = strings.TrimSpace(text[len(robotCmdModes)+1:])
|
||||
case strings.HasPrefix(text, robotCmdSwitchMode+" "):
|
||||
mode = strings.TrimSpace(text[len(robotCmdSwitchMode)+1:])
|
||||
default:
|
||||
mode = strings.TrimSpace(text[5:])
|
||||
}
|
||||
return h.cmdSwitchMode(platform, userID, mode), true
|
||||
case text == robotCmdPermissions || text == "permissions":
|
||||
return h.cmdPermissions(platform, userID), true
|
||||
case text == robotCmdDoctor || text == "doctor":
|
||||
return h.cmdDoctor(), true
|
||||
case strings.HasPrefix(text, robotCmdDelete+" ") || strings.HasPrefix(text, "delete "):
|
||||
var convID string
|
||||
if strings.HasPrefix(text, robotCmdDelete+" ") {
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"cyberstrike-ai/internal/config"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func TestRobotModeSwitch(t *testing.T) {
|
||||
h := NewRobotHandler(&config.Config{MultiAgent: config.MultiAgentConfig{Enabled: true}}, nil, nil, zap.NewNop())
|
||||
|
||||
if got := h.cmdSwitchMode("lark", "user-1", "plan-execute"); !strings.Contains(got, "Plan-Execute") {
|
||||
t.Fatalf("unexpected switch response: %s", got)
|
||||
}
|
||||
if got := h.getAgentMode("lark", "user-1"); got != "plan_execute" {
|
||||
t.Fatalf("mode = %q, want plan_execute", got)
|
||||
}
|
||||
if got := h.cmdModes("lark", "user-1"); !strings.Contains(got, "当前模式: Plan-Execute") {
|
||||
t.Fatalf("unexpected modes response: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRobotModeRejectsUnavailableMultiAgent(t *testing.T) {
|
||||
h := NewRobotHandler(&config.Config{}, nil, nil, zap.NewNop())
|
||||
|
||||
if got := h.cmdSwitchMode("lark", "user-1", "deep"); !strings.Contains(got, "启用 Eino 多代理") {
|
||||
t.Fatalf("unexpected rejection: %s", got)
|
||||
}
|
||||
if got := h.getAgentMode("lark", "user-1"); got != "eino_single" {
|
||||
t.Fatalf("mode changed after rejection: %q", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseRobotAgentModeRejectsUnknownMode(t *testing.T) {
|
||||
if mode, ok := parseRobotAgentMode("unknown"); ok || mode != "" {
|
||||
t.Fatalf("parseRobotAgentMode returned (%q, %v), want empty,false", mode, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRobotStatusCommandPermission(t *testing.T) {
|
||||
for _, command := range []string{"状态", "status"} {
|
||||
permission, recognized := robotCommandPermission(command)
|
||||
if !recognized || permission != "chat:read" {
|
||||
t.Fatalf("command %q returned permission=%q recognized=%v", command, permission, recognized)
|
||||
}
|
||||
}
|
||||
for _, removed := range []string{"当前", "current"} {
|
||||
if _, recognized := robotCommandPermission(removed); recognized {
|
||||
t.Fatalf("removed command %q is still recognized", removed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRobotBestPracticeCommandPermissions(t *testing.T) {
|
||||
cases := map[string]string{
|
||||
"任务": "chat:read",
|
||||
"task": "chat:read",
|
||||
"重命名 新标题": "chat:write",
|
||||
"rename x": "chat:write",
|
||||
"诊断": "config:read",
|
||||
"doctor": "config:read",
|
||||
}
|
||||
for command, want := range cases {
|
||||
permission, recognized := robotCommandPermission(command)
|
||||
if !recognized || permission != want {
|
||||
t.Fatalf("command %q returned permission=%q recognized=%v, want %q,true", command, permission, recognized, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRobotConfirmationCanBeCancelled(t *testing.T) {
|
||||
h := NewRobotHandler(&config.Config{}, nil, nil, zap.NewNop())
|
||||
h.setPendingConfirmation("lark", "user-1", "delete_conversation", "conv-1")
|
||||
if got := h.cmdCancelConfirmation("lark", "user-1"); got != "已取消待确认操作。" {
|
||||
t.Fatalf("unexpected cancel response: %s", got)
|
||||
}
|
||||
if got := h.cmdConfirm("lark", "user-1"); !strings.Contains(got, "没有待确认操作") {
|
||||
t.Fatalf("confirmation survived cancellation: %s", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRobotDoctorSeparatesInternalToolsFromHTTPMCP(t *testing.T) {
|
||||
h := NewRobotHandler(&config.Config{
|
||||
Security: config.SecurityConfig{Tools: []config.ToolConfig{
|
||||
{Name: "enabled-tool", Enabled: true},
|
||||
{Name: "disabled-tool", Enabled: false},
|
||||
}},
|
||||
MCP: config.MCPConfig{Enabled: false},
|
||||
}, nil, nil, zap.NewNop())
|
||||
|
||||
got := h.cmdDoctor()
|
||||
if !strings.Contains(got, "内置 MCP 工具: 1/2 个已启用") {
|
||||
t.Fatalf("internal tool status missing: %s", got)
|
||||
}
|
||||
if !strings.Contains(got, "HTTP MCP 服务: 已关闭") {
|
||||
t.Fatalf("HTTP MCP status missing: %s", got)
|
||||
}
|
||||
}
|
||||
@@ -298,6 +298,18 @@ func (m *AgentTaskManager) GetTask(conversationID string) *AgentTask {
|
||||
return m.tasks[conversationID]
|
||||
}
|
||||
|
||||
// GetTaskSnapshot 返回运行任务的只读副本,供状态展示使用,避免锁外读取可变任务字段。
|
||||
func (m *AgentTaskManager) GetTaskSnapshot(conversationID string) *AgentTask {
|
||||
m.mu.RLock()
|
||||
defer m.mu.RUnlock()
|
||||
task := m.tasks[conversationID]
|
||||
if task == nil {
|
||||
return nil
|
||||
}
|
||||
snapshot := *task
|
||||
return &snapshot
|
||||
}
|
||||
|
||||
// runStuckCancellingCleanup 定期将长时间处于「取消中」的任务强制结束,避免卡住无法发新消息
|
||||
func (m *AgentTaskManager) runStuckCancellingCleanup() {
|
||||
ticker := time.NewTicker(cleanupInterval)
|
||||
|
||||
Reference in New Issue
Block a user