返回 DeepSeek-Reasonix
session.go
根目录 / internal / bot / session.go
1 package bot
2
3 import (
4 "crypto/sha256"
5 "encoding/hex"
6 "fmt"
7 "strings"
8 "sync"
9 "time"
10 )
11
12 const (
13 QueueModeSteer = "steer"
14 QueueModeFollowup = "followup"
15 QueueModeCollect = "collect"
16 QueueModeInterrupt = "interrupt"
17
18 QueueDropSummarize = "summarize"
19 QueueDropOld = "old"
20 QueueDropNew = "new"
21
22 DefaultQueueCap = 20
23 )
24
25 type QueueOptions struct {
26 Mode string
27 Cap int
28 Drop string
29 }
30
31 type QueueResult struct {
32 Acquired bool
33 Queued bool
34 Rejected bool
35 Dropped bool
36 Pending int
37 Mode string
38 }
39
40 type QueueSnapshot struct {
41 Active int
42 Pending int
43 Dropped int
44 Sessions int
45 }
46
47 func NormalizeQueueMode(mode string) string {
48 switch strings.ToLower(strings.TrimSpace(mode)) {
49 case QueueModeSteer:
50 return QueueModeSteer
51 case QueueModeFollowup:
52 return QueueModeFollowup
53 case QueueModeCollect:
54 return QueueModeCollect
55 case QueueModeInterrupt:
56 return QueueModeInterrupt
57 default:
58 return QueueModeSteer
59 }
60 }
61
62 func NormalizeOptionalQueueMode(mode string) string {
63 switch strings.ToLower(strings.TrimSpace(mode)) {
64 case QueueModeSteer:
65 return QueueModeSteer
66 case QueueModeFollowup:
67 return QueueModeFollowup
68 case QueueModeCollect:
69 return QueueModeCollect
70 case QueueModeInterrupt:
71 return QueueModeInterrupt
72 default:
73 return ""
74 }
75 }
76
77 func NormalizeQueueDrop(drop string) string {
78 switch strings.ToLower(strings.TrimSpace(drop)) {
79 case QueueDropOld:
80 return QueueDropOld
81 case QueueDropNew:
82 return QueueDropNew
83 default:
84 return QueueDropSummarize
85 }
86 }
87
88 // BuildSessionKey 根据 Hermes 模式生成稳定的 session key:
89 // - DM:按 chat 隔离(同一 DM 会话共享历史)
90 // - 群聊:按 user 隔离(每人独立会话)
91 // - thread:共享(thread 内所有人共享上下文)
92 func BuildSessionKey(src SessionSource) string {
93 var scope string
94 source := sessionSourceID(src)
95 switch src.ChatType {
96 case ChatDM:
97 scope = fmt.Sprintf("%s:dm:%s", source, src.ChatID)
98 case ChatGroup:
99 scope = fmt.Sprintf("%s:group:%s:%s", source, src.ChatID, src.UserID)
100 case ChatGuild:
101 scope = fmt.Sprintf("%s:guild:%s:%s", source, src.ChatID, src.UserID)
102 case ChatDirect:
103 scope = fmt.Sprintf("%s:direct:%s", source, src.ChatID)
104 case ChatThread:
105 threadID := src.ThreadID
106 if threadID == "" {
107 threadID = src.ChatID
108 }
109 scope = fmt.Sprintf("%s:thread:%s", source, threadID)
110 default:
111 scope = fmt.Sprintf("%s:%s:%s:%s", source, src.ChatType, src.ChatID, src.UserID)
112 }
113 h := sha256.Sum256([]byte(scope))
114 return hex.EncodeToString(h[:])[:16]
115 }
116
117 func sessionSourceID(src SessionSource) string {
118 if src.ConnectionID != "" {
119 return src.ConnectionID
120 }
121 if src.Domain != "" {
122 return fmt.Sprintf("%s:%s", src.Platform, src.Domain)
123 }
124 return string(src.Platform)
125 }
126
127 // slashCommands 是绕过忙碌队列的命令集合。
128 var slashCommands = map[string]bool{
129 "/stop": true,
130 "/new": true,
131 "/reset": true,
132 "/approve": true,
133 "/deny": true,
134 "/answer": true,
135 "/yolo": true,
136 "/mode": true,
137 "/queue": true,
138 "/projects": true,
139 "/use": true,
140 "/sessions": true,
141 "/attach": true,
142 "/search": true,
143 "/desktop": true,
144 "/status": true,
145 "/help": true,
146 }
147
148 // IsSlashBypass 判断消息是否为绕过队列的斜杠命令。
149 func IsSlashBypass(text string) bool {
150 if len(text) == 0 {
151 return false
152 }
153 cmd := text
154 for i, r := range text {
155 if r == ' ' {
156 cmd = text[:i]
157 break
158 }
159 }
160 return slashCommands[cmd]
161 }
162
163 // pendingTurn 是等待执行的一轮对话。
164 type pendingTurn struct {
165 msg InboundMessage
166 timestamp time.Time
167 mode string
168 }
169
170 // SessionManager 管理 session 级别的并发控制:同一 session 同时只跑一个任务。
171 type SessionManager struct {
172 mu sync.Mutex
173 active map[string]bool // session key -> 是否正在运行
174 pending map[string][]pendingTurn // session key -> 等待队列
175 debounce time.Duration
176 modeOverrides map[string]string
177 dropped map[string][]string
178 }
179
180 // NewSessionManager 创建一个新的 session 管理器。debounce 是消息合并窗口。
181 func NewSessionManager(debounce time.Duration) *SessionManager {
182 if debounce <= 0 {
183 debounce = 1500 * time.Millisecond
184 }
185 return &SessionManager{
186 active: make(map[string]bool),
187 pending: make(map[string][]pendingTurn),
188 debounce: debounce,
189 modeOverrides: make(map[string]string),
190 dropped: make(map[string][]string),
191 }
192 }
193
194 // TryAcquire 尝试获取 session 锁。如果 session 正忙且消息非绕过命令,返回 false。
195 // 返回 (acquired, merged) — merged 为 true 表示消息已合并到等待队列。
196 func (sm *SessionManager) TryAcquire(key string, msg InboundMessage) (acquired bool, merged bool) {
197 result := sm.TryAcquireWithQueue(key, msg, QueueOptions{Mode: QueueModeCollect, Cap: DefaultQueueCap, Drop: QueueDropSummarize})
198 return result.Acquired, result.Queued
199 }
200
201 func (sm *SessionManager) TryAcquireWithQueue(key string, msg InboundMessage, opts QueueOptions) QueueResult {
202 sm.mu.Lock()
203 defer sm.mu.Unlock()
204
205 mode := NormalizeQueueMode(opts.Mode)
206 if mode == QueueModeSteer || mode == QueueModeInterrupt {
207 mode = QueueModeFollowup
208 }
209 cap := opts.Cap
210 if cap <= 0 {
211 cap = DefaultQueueCap
212 }
213 drop := NormalizeQueueDrop(opts.Drop)
214
215 if sm.active[key] {
216 // 绕过命令立即返回 true(让调用方直接处理)
217 if IsSlashBypass(msg.Text) {
218 return QueueResult{Acquired: true, Mode: mode}
219 }
220 queue := sm.pending[key]
221 if len(queue) >= cap {
222 switch drop {
223 case QueueDropNew:
224 return QueueResult{Rejected: true, Pending: len(queue), Mode: mode}
225 case QueueDropOld, QueueDropSummarize:
226 removed := queue[0]
227 queue = queue[1:]
228 if drop == QueueDropSummarize {
229 sm.dropped[key] = append(sm.dropped[key], queueSummary(removed.msg.Text))
230 }
231 }
232 }
233 if mode == QueueModeCollect && len(queue) > 0 {
234 last := &queue[len(queue)-1]
235 if msg.Text != "" && time.Since(last.timestamp) < sm.debounce {
236 if last.msg.Text != "" {
237 last.msg.Text = last.msg.Text + "\n" + msg.Text
238 } else {
239 last.msg.Text = msg.Text
240 }
241 last.timestamp = time.Now()
242 last.mode = mode
243 sm.pending[key] = queue
244 return QueueResult{Queued: true, Dropped: len(sm.dropped[key]) > 0, Pending: len(queue), Mode: mode}
245 }
246 }
247 queue = append(queue, pendingTurn{msg: msg, timestamp: time.Now(), mode: mode})
248 sm.pending[key] = queue
249 return QueueResult{Queued: true, Dropped: len(sm.dropped[key]) > 0, Pending: len(queue), Mode: mode}
250 }
251
252 sm.active[key] = true
253 return QueueResult{Acquired: true, Mode: mode}
254 }
255
256 func (sm *SessionManager) ReplacePending(key string, msg InboundMessage) QueueResult {
257 sm.mu.Lock()
258 defer sm.mu.Unlock()
259 if !sm.active[key] {
260 sm.active[key] = true
261 return QueueResult{Acquired: true, Mode: QueueModeInterrupt}
262 }
263 sm.pending[key] = []pendingTurn{{msg: msg, timestamp: time.Now(), mode: QueueModeFollowup}}
264 delete(sm.dropped, key)
265 return QueueResult{Queued: true, Pending: 1, Mode: QueueModeInterrupt}
266 }
267
268 // Release 释放 session 锁,返回等待队列中的下一条消息(合并后)。
269 func (sm *SessionManager) Release(key string) *InboundMessage {
270 sm.mu.Lock()
271 defer sm.mu.Unlock()
272
273 queue := sm.pending[key]
274 if len(queue) == 0 {
275 delete(sm.active, key)
276 delete(sm.pending, key)
277 delete(sm.dropped, key)
278 return nil
279 }
280
281 mode := NormalizeQueueMode(queue[0].mode)
282 var merged *InboundMessage
283 if mode == QueueModeFollowup {
284 m := queue[0].msg
285 merged = &m
286 if len(queue) == 1 {
287 delete(sm.pending, key)
288 } else {
289 sm.pending[key] = queue[1:]
290 }
291 merged.Text = sm.consumeDroppedPrefixLocked(key, merged.Text)
292 return merged
293 }
294
295 // collect 模式取出等待队列,并合并其中所有消息。
296 for i := range queue {
297 if merged == nil {
298 m := queue[i].msg
299 merged = &m
300 } else {
301 if queue[i].msg.Text != "" {
302 merged.Text = merged.Text + "\n" + queue[i].msg.Text
303 }
304 }
305 }
306 delete(sm.pending, key)
307 merged.Text = sm.consumeDroppedPrefixLocked(key, merged.Text)
308 // active 保持 true,因为调用方会立即用 merged 消息开始新 turn
309 return merged
310 }
311
312 func (sm *SessionManager) consumeDroppedPrefixLocked(key, text string) string {
313 dropped := sm.dropped[key]
314 if len(dropped) == 0 {
315 return text
316 }
317 delete(sm.dropped, key)
318 var b strings.Builder
319 fmt.Fprintf(&b, "[Queue note: %d older pending message(s) were dropped because this bot session reached its queue cap.", len(dropped))
320 if len(dropped) > 0 {
321 b.WriteString(" Dropped summaries:")
322 limit := len(dropped)
323 if limit > 3 {
324 limit = 3
325 }
326 for i := 0; i < limit; i++ {
327 fmt.Fprintf(&b, "\n- %s", dropped[i])
328 }
329 if len(dropped) > limit {
330 fmt.Fprintf(&b, "\n- ... and %d more", len(dropped)-limit)
331 }
332 }
333 b.WriteString("]\n\n")
334 b.WriteString(text)
335 return b.String()
336 }
337
338 func queueSummary(text string) string {
339 text = strings.TrimSpace(text)
340 if text == "" {
341 return "(empty message)"
342 }
343 runes := []rune(text)
344 if len(runes) <= 180 {
345 return text
346 }
347 return string(runes[:180]) + "..."
348 }
349
350 // IsActive 返回 session 是否有正在运行的任务。
351 func (sm *SessionManager) IsActive(key string) bool {
352 sm.mu.Lock()
353 defer sm.mu.Unlock()
354 return sm.active[key]
355 }
356
357 // runIfIdle holds the per-gateway admission lock while fn switches runtime
358 // ownership for key. A normal message cannot become active between the idle
359 // check and the controller unlink/close sequence.
360 func (sm *SessionManager) runIfIdle(key string, fn func() bool) bool {
361 sm.mu.Lock()
362 defer sm.mu.Unlock()
363 if sm.active[key] || fn == nil {
364 return false
365 }
366 if !fn() {
367 return false
368 }
369 delete(sm.pending, key)
370 delete(sm.dropped, key)
371 return true
372 }
373
374 // ActiveCount 返回当前活跃 session 数。
375 func (sm *SessionManager) ActiveCount() int {
376 sm.mu.Lock()
377 defer sm.mu.Unlock()
378 return len(sm.active)
379 }
380
381 func (sm *SessionManager) PendingCount(key string) int {
382 sm.mu.Lock()
383 defer sm.mu.Unlock()
384 return len(sm.pending[key])
385 }
386
387 func (sm *SessionManager) Snapshot() QueueSnapshot {
388 sm.mu.Lock()
389 defer sm.mu.Unlock()
390 var pending int
391 var dropped int
392 for _, queue := range sm.pending {
393 pending += len(queue)
394 }
395 for _, summaries := range sm.dropped {
396 dropped += len(summaries)
397 }
398 return QueueSnapshot{
399 Active: len(sm.active),
400 Pending: pending,
401 Dropped: dropped,
402 Sessions: len(sm.active) + len(sm.pending),
403 }
404 }
405
406 func (sm *SessionManager) QueueMode(key, fallback string) string {
407 sm.mu.Lock()
408 defer sm.mu.Unlock()
409 if mode := sm.modeOverrides[key]; mode != "" {
410 return mode
411 }
412 return NormalizeQueueMode(fallback)
413 }
414
415 func (sm *SessionManager) SetQueueMode(key, mode string) {
416 sm.mu.Lock()
417 defer sm.mu.Unlock()
418 if normalized := NormalizeOptionalQueueMode(mode); normalized != "" {
419 sm.modeOverrides[key] = normalized
420 }
421 }
422
423 func (sm *SessionManager) ClearQueueMode(key string) {
424 sm.mu.Lock()
425 defer sm.mu.Unlock()
426 delete(sm.modeOverrides, key)
427 }
428
429 // ForceRelease 强制释放 session(用于 session 关闭或错误恢复)。
430 func (sm *SessionManager) ForceRelease(key string) {
431 sm.mu.Lock()
432 defer sm.mu.Unlock()
433 delete(sm.active, key)
434 delete(sm.pending, key)
435 delete(sm.dropped, key)
436 }
437
437 lines GO