Files
2026-07-13 13:00:08 +08:00

420 lines
10 KiB
Go
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package bot
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"sync"
"time"
)
const (
QueueModeSteer = "steer"
QueueModeFollowup = "followup"
QueueModeCollect = "collect"
QueueModeInterrupt = "interrupt"
QueueDropSummarize = "summarize"
QueueDropOld = "old"
QueueDropNew = "new"
DefaultQueueCap = 20
)
type QueueOptions struct {
Mode string
Cap int
Drop string
}
type QueueResult struct {
Acquired bool
Queued bool
Rejected bool
Dropped bool
Pending int
Mode string
}
type QueueSnapshot struct {
Active int
Pending int
Dropped int
Sessions int
}
func NormalizeQueueMode(mode string) string {
switch strings.ToLower(strings.TrimSpace(mode)) {
case QueueModeSteer:
return QueueModeSteer
case QueueModeFollowup:
return QueueModeFollowup
case QueueModeCollect:
return QueueModeCollect
case QueueModeInterrupt:
return QueueModeInterrupt
default:
return QueueModeSteer
}
}
func NormalizeOptionalQueueMode(mode string) string {
switch strings.ToLower(strings.TrimSpace(mode)) {
case QueueModeSteer:
return QueueModeSteer
case QueueModeFollowup:
return QueueModeFollowup
case QueueModeCollect:
return QueueModeCollect
case QueueModeInterrupt:
return QueueModeInterrupt
default:
return ""
}
}
func NormalizeQueueDrop(drop string) string {
switch strings.ToLower(strings.TrimSpace(drop)) {
case QueueDropOld:
return QueueDropOld
case QueueDropNew:
return QueueDropNew
default:
return QueueDropSummarize
}
}
// BuildSessionKey 根据 Hermes 模式生成稳定的 session key
// - DM:按 chat 隔离(同一 DM 会话共享历史)
// - 群聊:按 user 隔离(每人独立会话)
// - thread:共享(thread 内所有人共享上下文)
func BuildSessionKey(src SessionSource) string {
var scope string
source := sessionSourceID(src)
switch src.ChatType {
case ChatDM:
scope = fmt.Sprintf("%s:dm:%s", source, src.ChatID)
case ChatGroup:
scope = fmt.Sprintf("%s:group:%s:%s", source, src.ChatID, src.UserID)
case ChatGuild:
scope = fmt.Sprintf("%s:guild:%s:%s", source, src.ChatID, src.UserID)
case ChatDirect:
scope = fmt.Sprintf("%s:direct:%s", source, src.ChatID)
case ChatThread:
threadID := src.ThreadID
if threadID == "" {
threadID = src.ChatID
}
scope = fmt.Sprintf("%s:thread:%s", source, threadID)
default:
scope = fmt.Sprintf("%s:%s:%s:%s", source, src.ChatType, src.ChatID, src.UserID)
}
h := sha256.Sum256([]byte(scope))
return hex.EncodeToString(h[:])[:16]
}
func sessionSourceID(src SessionSource) string {
if src.ConnectionID != "" {
return src.ConnectionID
}
if src.Domain != "" {
return fmt.Sprintf("%s:%s", src.Platform, src.Domain)
}
return string(src.Platform)
}
// slashCommands 是绕过忙碌队列的命令集合。
var slashCommands = map[string]bool{
"/stop": true,
"/new": true,
"/reset": true,
"/approve": true,
"/deny": true,
"/answer": true,
"/yolo": true,
"/mode": true,
"/queue": true,
"/projects": true,
"/use": true,
"/sessions": true,
"/attach": true,
"/search": true,
"/desktop": true,
"/status": true,
"/help": true,
}
// IsSlashBypass 判断消息是否为绕过队列的斜杠命令。
func IsSlashBypass(text string) bool {
if len(text) == 0 {
return false
}
cmd := text
for i, r := range text {
if r == ' ' {
cmd = text[:i]
break
}
}
return slashCommands[cmd]
}
// pendingTurn 是等待执行的一轮对话。
type pendingTurn struct {
msg InboundMessage
timestamp time.Time
mode string
}
// SessionManager 管理 session 级别的并发控制:同一 session 同时只跑一个任务。
type SessionManager struct {
mu sync.Mutex
active map[string]bool // session key -> 是否正在运行
pending map[string][]pendingTurn // session key -> 等待队列
debounce time.Duration
modeOverrides map[string]string
dropped map[string][]string
}
// NewSessionManager 创建一个新的 session 管理器。debounce 是消息合并窗口。
func NewSessionManager(debounce time.Duration) *SessionManager {
if debounce <= 0 {
debounce = 1500 * time.Millisecond
}
return &SessionManager{
active: make(map[string]bool),
pending: make(map[string][]pendingTurn),
debounce: debounce,
modeOverrides: make(map[string]string),
dropped: make(map[string][]string),
}
}
// TryAcquire 尝试获取 session 锁。如果 session 正忙且消息非绕过命令,返回 false。
// 返回 (acquired, merged) — merged 为 true 表示消息已合并到等待队列。
func (sm *SessionManager) TryAcquire(key string, msg InboundMessage) (acquired bool, merged bool) {
result := sm.TryAcquireWithQueue(key, msg, QueueOptions{Mode: QueueModeCollect, Cap: DefaultQueueCap, Drop: QueueDropSummarize})
return result.Acquired, result.Queued
}
func (sm *SessionManager) TryAcquireWithQueue(key string, msg InboundMessage, opts QueueOptions) QueueResult {
sm.mu.Lock()
defer sm.mu.Unlock()
mode := NormalizeQueueMode(opts.Mode)
if mode == QueueModeSteer || mode == QueueModeInterrupt {
mode = QueueModeFollowup
}
cap := opts.Cap
if cap <= 0 {
cap = DefaultQueueCap
}
drop := NormalizeQueueDrop(opts.Drop)
if sm.active[key] {
// 绕过命令立即返回 true(让调用方直接处理)
if IsSlashBypass(msg.Text) {
return QueueResult{Acquired: true, Mode: mode}
}
queue := sm.pending[key]
if len(queue) >= cap {
switch drop {
case QueueDropNew:
return QueueResult{Rejected: true, Pending: len(queue), Mode: mode}
case QueueDropOld, QueueDropSummarize:
removed := queue[0]
queue = queue[1:]
if drop == QueueDropSummarize {
sm.dropped[key] = append(sm.dropped[key], queueSummary(removed.msg.Text))
}
}
}
if mode == QueueModeCollect && len(queue) > 0 {
last := &queue[len(queue)-1]
if msg.Text != "" && time.Since(last.timestamp) < sm.debounce {
if last.msg.Text != "" {
last.msg.Text = last.msg.Text + "\n" + msg.Text
} else {
last.msg.Text = msg.Text
}
last.timestamp = time.Now()
last.mode = mode
sm.pending[key] = queue
return QueueResult{Queued: true, Dropped: len(sm.dropped[key]) > 0, Pending: len(queue), Mode: mode}
}
}
queue = append(queue, pendingTurn{msg: msg, timestamp: time.Now(), mode: mode})
sm.pending[key] = queue
return QueueResult{Queued: true, Dropped: len(sm.dropped[key]) > 0, Pending: len(queue), Mode: mode}
}
sm.active[key] = true
return QueueResult{Acquired: true, Mode: mode}
}
func (sm *SessionManager) ReplacePending(key string, msg InboundMessage) QueueResult {
sm.mu.Lock()
defer sm.mu.Unlock()
if !sm.active[key] {
sm.active[key] = true
return QueueResult{Acquired: true, Mode: QueueModeInterrupt}
}
sm.pending[key] = []pendingTurn{{msg: msg, timestamp: time.Now(), mode: QueueModeFollowup}}
delete(sm.dropped, key)
return QueueResult{Queued: true, Pending: 1, Mode: QueueModeInterrupt}
}
// Release 释放 session 锁,返回等待队列中的下一条消息(合并后)。
func (sm *SessionManager) Release(key string) *InboundMessage {
sm.mu.Lock()
defer sm.mu.Unlock()
queue := sm.pending[key]
if len(queue) == 0 {
delete(sm.active, key)
delete(sm.pending, key)
delete(sm.dropped, key)
return nil
}
mode := NormalizeQueueMode(queue[0].mode)
var merged *InboundMessage
if mode == QueueModeFollowup {
m := queue[0].msg
merged = &m
if len(queue) == 1 {
delete(sm.pending, key)
} else {
sm.pending[key] = queue[1:]
}
merged.Text = sm.consumeDroppedPrefixLocked(key, merged.Text)
return merged
}
// collect 模式取出等待队列,并合并其中所有消息。
for i := range queue {
if merged == nil {
m := queue[i].msg
merged = &m
} else {
if queue[i].msg.Text != "" {
merged.Text = merged.Text + "\n" + queue[i].msg.Text
}
}
}
delete(sm.pending, key)
merged.Text = sm.consumeDroppedPrefixLocked(key, merged.Text)
// active 保持 true,因为调用方会立即用 merged 消息开始新 turn
return merged
}
func (sm *SessionManager) consumeDroppedPrefixLocked(key, text string) string {
dropped := sm.dropped[key]
if len(dropped) == 0 {
return text
}
delete(sm.dropped, key)
var b strings.Builder
fmt.Fprintf(&b, "[Queue note: %d older pending message(s) were dropped because this bot session reached its queue cap.", len(dropped))
if len(dropped) > 0 {
b.WriteString(" Dropped summaries:")
limit := len(dropped)
if limit > 3 {
limit = 3
}
for i := 0; i < limit; i++ {
fmt.Fprintf(&b, "\n- %s", dropped[i])
}
if len(dropped) > limit {
fmt.Fprintf(&b, "\n- ... and %d more", len(dropped)-limit)
}
}
b.WriteString("]\n\n")
b.WriteString(text)
return b.String()
}
func queueSummary(text string) string {
text = strings.TrimSpace(text)
if text == "" {
return "(empty message)"
}
runes := []rune(text)
if len(runes) <= 180 {
return text
}
return string(runes[:180]) + "..."
}
// IsActive 返回 session 是否有正在运行的任务。
func (sm *SessionManager) IsActive(key string) bool {
sm.mu.Lock()
defer sm.mu.Unlock()
return sm.active[key]
}
// ActiveCount 返回当前活跃 session 数。
func (sm *SessionManager) ActiveCount() int {
sm.mu.Lock()
defer sm.mu.Unlock()
return len(sm.active)
}
func (sm *SessionManager) PendingCount(key string) int {
sm.mu.Lock()
defer sm.mu.Unlock()
return len(sm.pending[key])
}
func (sm *SessionManager) Snapshot() QueueSnapshot {
sm.mu.Lock()
defer sm.mu.Unlock()
var pending int
var dropped int
for _, queue := range sm.pending {
pending += len(queue)
}
for _, summaries := range sm.dropped {
dropped += len(summaries)
}
return QueueSnapshot{
Active: len(sm.active),
Pending: pending,
Dropped: dropped,
Sessions: len(sm.active) + len(sm.pending),
}
}
func (sm *SessionManager) QueueMode(key, fallback string) string {
sm.mu.Lock()
defer sm.mu.Unlock()
if mode := sm.modeOverrides[key]; mode != "" {
return mode
}
return NormalizeQueueMode(fallback)
}
func (sm *SessionManager) SetQueueMode(key, mode string) {
sm.mu.Lock()
defer sm.mu.Unlock()
if normalized := NormalizeOptionalQueueMode(mode); normalized != "" {
sm.modeOverrides[key] = normalized
}
}
func (sm *SessionManager) ClearQueueMode(key string) {
sm.mu.Lock()
defer sm.mu.Unlock()
delete(sm.modeOverrides, key)
}
// ForceRelease 强制释放 session(用于 session 关闭或错误恢复)。
func (sm *SessionManager) ForceRelease(key string) {
sm.mu.Lock()
defer sm.mu.Unlock()
delete(sm.active, key)
delete(sm.pending, key)
delete(sm.dropped, key)
}