From 3534a956b217750a845f30b2ea7f9f08caf368e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=85=AC=E6=98=8E?= <83812544+Ed1s0nZ@users.noreply.github.com> Date: Sat, 28 Feb 2026 23:03:09 +0800 Subject: [PATCH] Add files via upload --- docs/robot.md | 1 + docs/robot_en.md | 1 + go.mod | 4 ++ go.sum | 4 +- internal/handler/robot.go | 90 ++++++++++++++++++++++++++++----------- web/templates/index.html | 1 + 6 files changed, 75 insertions(+), 26 deletions(-) diff --git a/docs/robot.md b/docs/robot.md index 658940ab..6c529692 100644 --- a/docs/robot.md +++ b/docs/robot.md @@ -113,6 +113,7 @@ | **新对话** | 开启一个新对话,后续消息在新对话中 | | **清空** | 清空当前对话上下文(效果等同「新对话」) | | **当前** | 显示当前对话 ID 与标题 | +| **停止** | 中断当前正在执行的任务 | 除以上命令外,**直接输入任意文字**会作为用户消息发给 AI,与 Web 端对话逻辑一致(渗透测试/安全分析等)。 diff --git a/docs/robot_en.md b/docs/robot_en.md index 4096a86a..ad4d9ce9 100644 --- a/docs/robot_en.md +++ b/docs/robot_en.md @@ -112,6 +112,7 @@ Send these **text commands** to the bot in DingTalk or Lark (text only): | **新对话** (new) | Start a new conversation | | **清空** (clear) | Clear current context (same effect as new conversation) | | **当前** (current) | Show current conversation ID and title | +| **停止** (stop) | Abort the currently running task | Any other text is sent to the AI as a user message, same as in the web UI (e.g. penetration testing, security analysis). diff --git a/go.mod b/go.mod index 0040a151..9d024b62 100644 --- a/go.mod +++ b/go.mod @@ -48,3 +48,7 @@ require ( golang.org/x/text v0.13.0 // indirect google.golang.org/protobuf v1.30.0 // indirect ) + +// 修复钉钉 Stream SDK 在长连接断开(熄屏/网络中断)后 "panic: send on closed channel" 问题 +// 详见: https://github.com/open-dingtalk/dingtalk-stream-sdk-go/issues/28 +replace github.com/open-dingtalk/dingtalk-stream-sdk-go => github.com/uouuou/dingtalk-stream-sdk-go v0.0.0-20250626025113-079132acc406 diff --git a/go.sum b/go.sum index f916a284..82d1f3f8 100644 --- a/go.sum +++ b/go.sum @@ -62,8 +62,6 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1 h1:Lb/Uzkiw2Ugt2Xf03J5wmv81PdkYOiWbI8CNBi1boC8= -github.com/open-dingtalk/dingtalk-stream-sdk-go v0.9.1/go.mod h1:ln3IqPYYocZbYvl9TAOrG/cxGR9xcn4pnZRLdCTEGEU= github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pkoukk/tiktoken-go v0.1.8 h1:85ENo+3FpWgAACBaEUVp+lctuTcYUO7BtmfhlN/QTRo= @@ -85,6 +83,8 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/uouuou/dingtalk-stream-sdk-go v0.0.0-20250626025113-079132acc406 h1:b72HNsEnmTRn7vhWGOfbWHAkA5RbRCk0Pbc56V2WAuY= +github.com/uouuou/dingtalk-stream-sdk-go v0.0.0-20250626025113-079132acc406/go.mod h1:ln3IqPYYocZbYvl9TAOrG/cxGR9xcn4pnZRLdCTEGEU= github.com/yosida95/uritemplate/v3 v3.0.2 h1:Ed3Oyj9yrmi9087+NczuL5BwkIc4wvTb5zIM+UJPGz4= github.com/yosida95/uritemplate/v3 v3.0.2/go.mod h1:ILOh0sOhIJR3+L/8afwt/kE++YT040gmv5BQTMR2HP4= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= diff --git a/internal/handler/robot.go b/internal/handler/robot.go index 40541b75..27bc62bd 100644 --- a/internal/handler/robot.go +++ b/internal/handler/robot.go @@ -3,6 +3,7 @@ package handler import ( "context" "crypto/aes" + "errors" "crypto/cipher" "encoding/base64" "encoding/binary" @@ -22,34 +23,38 @@ import ( ) const ( - robotCmdHelp = "帮助" - robotCmdList = "列表" - robotCmdListAlt = "对话列表" - robotCmdSwitch = "切换" + robotCmdHelp = "帮助" + robotCmdList = "列表" + robotCmdListAlt = "对话列表" + robotCmdSwitch = "切换" robotCmdContinue = "继续" - robotCmdNew = "新对话" - robotCmdClear = "清空" - robotCmdCurrent = "当前" + robotCmdNew = "新对话" + robotCmdClear = "清空" + robotCmdCurrent = "当前" + robotCmdStop = "停止" ) // 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 + config *config.Config + db *database.DB + agentHandler *AgentHandler + logger *zap.Logger + mu sync.RWMutex + sessions map[string]string // key: "platform_userID", value: conversationID + cancelMu sync.Mutex // 保护 runningCancels + runningCancels map[string]context.CancelFunc // key: "platform_userID", 用于停止命令中断任务 } // 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), + config: cfg, + db: db, + agentHandler: agentHandler, + logger: logger, + sessions: make(map[string]string), + runningCancels: make(map[string]context.CancelFunc), } } @@ -58,15 +63,21 @@ func (h *RobotHandler) sessionKey(platform, userID string) string { return platform + "_" + userID } -// getOrCreateConversation 获取或创建当前会话 -func (h *RobotHandler) getOrCreateConversation(platform, userID string) (convID string, isNew bool) { +// getOrCreateConversation 获取或创建当前会话,title 用于新对话的标题(取用户首条消息前50字) +func (h *RobotHandler) getOrCreateConversation(platform, userID, title string) (convID string, isNew bool) { h.mu.RLock() convID = h.sessions[h.sessionKey(platform, userID)] h.mu.RUnlock() if convID != "" { return convID, false } - conv, err := h.db.CreateConversation("机器人对话") + t := strings.TrimSpace(title) + if t == "" { + t = "新对话 " + time.Now().Format("01-02 15:04") + } else { + t = safeTruncateString(t, 25) + } + conv, err := h.db.CreateConversation(t) if err != nil { h.logger.Warn("创建机器人会话失败", zap.Error(err)) return "", false @@ -87,7 +98,8 @@ func (h *RobotHandler) setConversation(platform, userID, convID string) { // clearConversation 清空当前会话(切换到新对话) func (h *RobotHandler) clearConversation(platform, userID string) (newConvID string) { - conv, err := h.db.CreateConversation("新对话") + title := "新对话 " + time.Now().Format("01-02 15:04") + conv, err := h.db.CreateConversation(title) if err != nil { h.logger.Warn("创建新对话失败", zap.Error(err)) return "" @@ -123,18 +135,32 @@ func (h *RobotHandler) HandleMessage(platform, userID, text string) (reply strin return h.cmdClear(platform, userID) case text == robotCmdCurrent: return h.cmdCurrent(platform, userID) + case text == robotCmdStop || text == "stop": + return h.cmdStop(platform, userID) } // 普通消息:走 Agent - convID, _ := h.getOrCreateConversation(platform, userID) + convID, _ := h.getOrCreateConversation(platform, userID, text) if convID == "" { return "无法创建或获取对话,请稍后再试。" } ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) - defer cancel() + sk := h.sessionKey(platform, userID) + h.cancelMu.Lock() + h.runningCancels[sk] = cancel + h.cancelMu.Unlock() + defer func() { + cancel() + h.cancelMu.Lock() + delete(h.runningCancels, sk) + h.cancelMu.Unlock() + }() resp, newConvID, err := h.agentHandler.ProcessMessageForRobot(ctx, convID, text, "默认") if err != nil { h.logger.Warn("机器人 Agent 执行失败", zap.String("platform", platform), zap.String("userID", userID), zap.Error(err)) + if errors.Is(err, context.Canceled) { + return "任务已取消。" + } return "处理失败: " + err.Error() } if newConvID != convID { @@ -151,6 +177,7 @@ func (h *RobotHandler) cmdHelp() string { · 新对话 — 开启新对话 · 清空 — 清空当前上下文(等同于新对话) · 当前 — 显示当前对话 ID 与标题 +· 停止 — 中断当前正在执行的任务 除以上命令外,直接输入内容将发送给 AI 进行渗透测试/安全分析。` } @@ -198,6 +225,21 @@ func (h *RobotHandler) cmdClear(platform, userID string) string { return h.cmdNew(platform, userID) } +func (h *RobotHandler) cmdStop(platform, userID string) string { + sk := h.sessionKey(platform, userID) + h.cancelMu.Lock() + cancel, ok := h.runningCancels[sk] + if ok { + delete(h.runningCancels, sk) + cancel() + } + h.cancelMu.Unlock() + if !ok { + return "当前没有正在执行的任务。" + } + return "已停止当前任务。" +} + func (h *RobotHandler) cmdCurrent(platform, userID string) string { h.mu.RLock() convID := h.sessions[h.sessionKey(platform, userID)] diff --git a/web/templates/index.html b/web/templates/index.html index fe3e906c..a72db3e8 100644 --- a/web/templates/index.html +++ b/web/templates/index.html @@ -1297,6 +1297,7 @@
  • 新对话 — 开启新对话
  • 清空 — 清空当前对话上下文(不删除历史)
  • 当前 — 显示当前对话 ID 与标题
  • +
  • 停止 — 中断当前正在执行的任务