|
| 1 | +--- |
| 2 | +title: feat: 添加会话指令系统 (/new, /clear) |
| 3 | +type: feat |
| 4 | +status: active |
| 5 | +date: 2026-04-02 |
| 6 | +--- |
| 7 | + |
| 8 | +# 添加会话指令系统 (/new, /clear) |
| 9 | + |
| 10 | +## 概述 |
| 11 | + |
| 12 | +在消息处理流程中检测特殊指令(如 `/new`),执行对应操作(如重置会话)后再进入正常 LLM 处理流程。指令在内容中居首位的才有效。 |
| 13 | + |
| 14 | +## 动机 |
| 15 | + |
| 16 | +- 用户需要一个快捷方式来重置会话、开始新的对话上下文 |
| 17 | +- 当前没有机制来区分普通用户输入和系统指令 |
| 18 | +- 未来可能扩展更多指令(/help, /reset 等) |
| 19 | + |
| 20 | +## 解决方案 |
| 21 | + |
| 22 | +### 1. 设计指令检测与执行机制 |
| 23 | + |
| 24 | +在 `pkg/web/api/handle_platform.go` 的 `MessageHandler` 中,在构建 `cs` (Conversation) **之前** 检测消息内容是否以指令开头。 |
| 25 | + |
| 26 | +#### 指令注册表设计 |
| 27 | + |
| 28 | +```go |
| 29 | +// pkg/web/api/commands.go |
| 30 | + |
| 31 | +type Command struct { |
| 32 | + Name string // 指令名,如 "new", "clear" |
| 33 | + Aliases []string // 别名列表,如 []string{"/new", "/clear"} |
| 34 | + Desc string // 指令描述 |
| 35 | + Action func(ctx context.Context, msg *channel.Message) (bool, error) |
| 36 | + // 返回 (handled, error): handled=true 表示指令已处理,不再继续 LLM 调用 |
| 37 | +} |
| 38 | + |
| 39 | +var commandRegistry = []Command{ |
| 40 | + { |
| 41 | + Name: "reset", |
| 42 | + Aliases: []string{"/reset", "/new", "/clear"}, |
| 43 | + Desc: "重置会话,创建新的 csid", |
| 44 | + Action: handleResetCommand, |
| 45 | + }, |
| 46 | +} |
| 47 | + |
| 48 | +func handleResetCommand(ctx context.Context, msg *channel.Message) (bool, error) { |
| 49 | + // 调用 stores 包的导出函数删除 sessionKey -> csid 映射 |
| 50 | + if err := stores.ResetSessionBySessionKey(ctx, msg.SessionKey); err != nil { |
| 51 | + return false, err |
| 52 | + } |
| 53 | + logger().Infow("command: session reset", "sessionKey", msg.SessionKey) |
| 54 | + return true, nil // 指令已处理,不继续 LLM 调用 |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +#### 修改 MessageHandler 流程 |
| 59 | + |
| 60 | +``` |
| 61 | +MessageHandler |
| 62 | + └─> 检测指令 ──> 执行指令 Action |
| 63 | + └─> handled=true? ─┬─> true: 发送确认回复,结束 |
| 64 | + └─> false: 继续正常 LLM 流程 |
| 65 | +``` |
| 66 | + |
| 67 | +在 `handle_platform.go:MessageHandler` 的第 112 行 `cs := stores.GetOrCreateConversationBySessionKey(...)` 之前插入指令检测: |
| 68 | + |
| 69 | +```go |
| 70 | +// Check for commands at the beginning of content |
| 71 | +if cmd := DetectCommand(msg.Content); cmd != nil { |
| 72 | + handled, err := cmd.Action(ctx, msg) |
| 73 | + if err != nil { |
| 74 | + logger().Warnw("command execution failed", "cmd", cmd.Name, "err", err) |
| 75 | + } |
| 76 | + if handled { |
| 77 | + // Send acknowledgment to user |
| 78 | + if err := p.Reply(ctx, msg.ReplyCtx, "会话已重置,开始新对话"); err != nil { |
| 79 | + logger().Warnw("reply after command failed", "err", err) |
| 80 | + } |
| 81 | + return |
| 82 | + } |
| 83 | + // Fall through to normal processing with trimmed content |
| 84 | +} |
| 85 | +``` |
| 86 | + |
| 87 | +### 2. 实现 DetectCommand 函数 |
| 88 | + |
| 89 | +```go |
| 90 | +// pkg/web/api/commands.go |
| 91 | + |
| 92 | +func DetectCommand(content string) *Command { |
| 93 | + trimmed := strings.TrimSpace(content) |
| 94 | + for _, cmd := range commandRegistry { |
| 95 | + for _, alias := range cmd.Aliases { |
| 96 | + if strings.HasPrefix(trimmed, alias) { |
| 97 | + return &cmd |
| 98 | + } |
| 99 | + } |
| 100 | + } |
| 101 | + return nil |
| 102 | +} |
| 103 | +``` |
| 104 | + |
| 105 | +### 3. 扩展指令注册表(未来) |
| 106 | + |
| 107 | +在 `commands.go` 的 `commandRegistry` 中添加新条目即可: |
| 108 | + |
| 109 | +```go |
| 110 | +{ |
| 111 | + Name: "help", |
| 112 | + Aliases: []string{"/help", "/h", "?"}, |
| 113 | + Desc: "显示可用指令列表", |
| 114 | + Action: handleHelpCommand, |
| 115 | +}, |
| 116 | +``` |
| 117 | + |
| 118 | +## 系统影响 |
| 119 | + |
| 120 | +### 交互图 |
| 121 | + |
| 122 | +``` |
| 123 | +MessageHandler |
| 124 | + └─> DetectCommand() ──> Command.Action() |
| 125 | + ├─> new/clear: Del Redis key |
| 126 | + └─> (future) help: Reply with command list |
| 127 | +``` |
| 128 | + |
| 129 | +### 错误传播 |
| 130 | + |
| 131 | +- 指令执行失败:记录日志,继续正常 LLM 处理流程(不阻塞用户) |
| 132 | +- Redis 删除失败:返回 false 让流程继续(容错设计) |
| 133 | + |
| 134 | +### 状态生命周期 |
| 135 | + |
| 136 | +- `/new` 删除 Redis 中的 `platform:csid:{sessionKey}` 映射 |
| 137 | +- 后续 `GetOrCreateConversationBySessionKey` 会创建新的 csid |
| 138 | +- 旧的 Redis history key (`convs-{old_csid}`) 保留直到 TTL 过期 |
| 139 | + |
| 140 | +## 验收标准 |
| 141 | + |
| 142 | +- [ ] 发送 `/new` 或 `/clear` 时,系统生成新的 csid,后续对话与之前隔离 |
| 143 | +- [ ] 发送普通消息时,不触发指令检测,正常进入 LLM 处理 |
| 144 | +- [ ] 指令处理后向用户发送确认消息 |
| 145 | +- [ ] 指令可扩展,添加新指令只需在 registry 中添加条目 |
| 146 | + |
| 147 | +## 技术细节 |
| 148 | + |
| 149 | +### 关键文件 |
| 150 | + |
| 151 | +- `pkg/services/stores/conversation.go` — 添加 `ResetSessionBySessionKey()` 导出函数 |
| 152 | +- `pkg/web/api/handle_platform.go:112` — 插入指令检测逻辑 |
| 153 | +- `pkg/web/api/commands.go` (新建) — 指令注册与执行逻辑 |
| 154 | + |
| 155 | +### 别名实现 |
| 156 | + |
| 157 | +别名在 `Command.Aliases` 中定义,检测时遍历所有别名匹配。指令匹配以空格或消息结束为边界,防止误匹配(如 `/newuser` 不会匹配 `/new`)。 |
| 158 | + |
| 159 | +## 依赖与风险 |
| 160 | + |
| 161 | +- 无新依赖 |
| 162 | +- 风险:指令检测应在 LLM 处理之前,但不影响现有消息流 |
| 163 | + |
| 164 | +## 实现步骤 |
| 165 | + |
| 166 | +1. 在 `stores/conversation.go` 添加 `ResetSessionBySessionKey(ctx, sessionKey)` 导出函数 |
| 167 | +2. 创建 `pkg/web/api/commands.go`,定义 `Command` 结构体和 `commandRegistry` |
| 168 | +3. 实现 `DetectCommand()` 函数 |
| 169 | +4. 在 `MessageHandler` 第 112 行前插入指令检测 |
| 170 | +5. 实现 `handleNewCommand`,调用 `stores.ResetSessionBySessionKey` 后发送确认 |
| 171 | +6. 添加单元测试(检测指令解析、别名匹配) |
0 commit comments