dsh-qqchat 0.2.4 → 0.2.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +5 -4
- package/README.md +3 -3
- package/client-src/plugin.cts +2 -2
- package/client-src/settings.cts +18 -1
- package/client-src/shared.cts +4 -4
- package/cordis.patch.yml +5 -0
- package/docs/ARCHITECTURE.en.md +1 -1
- package/docs/ARCHITECTURE.md +2 -1
- package/docs/DEVELOPMENT.md +3 -1
- package/docs/MEMORY.md +32 -4
- package/docs/MEMORY_CONTEXT.md +16 -0
- package/docs/README.md +2 -0
- package/docs/STREAMING.md +2 -2
- package/docs/prds/PRD-MEM-1-/347/276/244/350/256/260/345/277/206/344/270/216/347/276/244/345/217/213/350/256/260/345/277/206/347/255/226/347/225/245/350/260/203/346/225/264.md +396 -0
- package/docs/prds/README.md +62 -0
- package/lib/client.js +21 -4
- package/lib/config.js +3 -2
- package/lib/index.d.ts +2 -2
- package/lib/index.js +2 -3
- package/lib/session/agent-bridge.d.ts +3 -4
- package/lib/session/agent-bridge.js +40 -46
- package/lib/session/memory-snapshot.d.ts +4 -0
- package/lib/session/memory-snapshot.js +6 -2
- package/lib/session/runtime.js +15 -5
- package/lib/storage/db.d.ts +6 -1
- package/lib/storage/db.js +57 -0
- package/lib/storage/memory.d.ts +28 -2
- package/lib/storage/memory.js +375 -101
- package/lib/transport/rpc.js +2 -0
- package/lib/types.d.ts +26 -0
- package/package.json +24 -23
- package/prompts/group-compression.md +5 -0
- package/prompts/group-reflection.md +39 -0
- package/prompts/member-batch-reflection.md +39 -0
- package/prompts/member-compression.md +5 -0
- package/prompts/private-reflection.md +32 -0
- package/src/config.ts +3 -2
- package/src/index.ts +2 -3
- package/src/session/agent-bridge.ts +40 -47
- package/src/session/memory-snapshot.ts +6 -3
- package/src/session/runtime.ts +14 -4
- package/src/storage/db.ts +70 -0
- package/src/storage/memory.ts +343 -84
- package/src/transport/rpc.ts +1 -0
- package/src/types.ts +30 -0
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
# PRD-MEM-1:群记忆与群友记忆策略调整
|
|
2
|
+
|
|
3
|
+
> 状态:Phase 4 进行中;任务恢复、脱敏指标和成员批量灰度开关已接入,真实 DSH Web 长期观察待完成
|
|
4
|
+
> 创建:2026-08-29
|
|
5
|
+
> 最近更新:2026-08-29
|
|
6
|
+
> 关联模块:`src/storage/memory.ts`、`src/storage/db.ts`、`src/session/`、`tests/memory*.test.ts`
|
|
7
|
+
> 背景参考:Gugu 2026-08-29 群记忆与群友记忆策略、[记忆系统](../MEMORY.md)、[记忆上下文注入方案](../MEMORY_CONTEXT.md)
|
|
8
|
+
|
|
9
|
+
## 0. 实际状态
|
|
10
|
+
|
|
11
|
+
| 能力 | 结果 | 说明 |
|
|
12
|
+
| --- | --- | --- |
|
|
13
|
+
| Group/member scope | ✅ 已完成 | SQLite 已按群和 stable sender ID 保存记忆文档 |
|
|
14
|
+
| daily 日期归档 | ✅ 已完成 | 同一天使用一个 `## YYYY-MM-DD` 标题 |
|
|
15
|
+
| DSH Session 快照 | ✅ 已完成 | 使用 runtime-context snapshot、hash、TTL 和 compact stale |
|
|
16
|
+
| 群记忆反思 | 🟡 部分完成 | 已改为只写 group 文档,但触发阈值和任务恢复仍需继续完善 |
|
|
17
|
+
| 群友批量反思 | 🟡 部分完成 | 已接入按群的 `member-batch` 调度、50 条水位、独立成员游标、多成员输出校验和任务记录,完整恢复测试仍待补齐 |
|
|
18
|
+
| 私聊记忆 | 🟡 部分完成 | 已接入 `private-owner` 任务、用户/Bot 方向输入和独立幂等记录,长期观察仍待补齐 |
|
|
19
|
+
| daily → memory 压缩 | 🟡 部分完成 | 群压缩目标已调整为 200/100,成员压缩保留 100/50,事件型输出规则仍待继续收敛 |
|
|
20
|
+
| 自动化集成测试 | 🟡 部分完成 | 已覆盖 group/member/private 反思、迁移和 snapshot 基础生命周期,长期 Session 观察仍待完成 |
|
|
21
|
+
|
|
22
|
+
## 1. 背景与目标
|
|
23
|
+
|
|
24
|
+
### 1.1 现状问题
|
|
25
|
+
|
|
26
|
+
QQChat 的 SQLite 是 QQ 世界事实来源,DSH Session 负责 Agent 实际经历过的 turn
|
|
27
|
+
和当前模型上下文。当前 `MemoryEngine` 已经支持 group/member 文档、idle 反思、
|
|
28
|
+
批量阈值、daily 压缩和 Session 快照,但反思职责仍有耦合:群反思输入会携带成员
|
|
29
|
+
资料并可能同时更新群和成员文档,成员又容易按照单个成员重复触发。
|
|
30
|
+
|
|
31
|
+
这会导致:
|
|
32
|
+
|
|
33
|
+
- 群级事实和个人事实边界不清晰;
|
|
34
|
+
- 成员数量增加后,单次反思输入和输出规模不可控;
|
|
35
|
+
- 同一批群消息可能触发多次成员模型调用;
|
|
36
|
+
- 私聊的用户事实与 Bot 回复未被明确区分;
|
|
37
|
+
- 反思任务、游标和 Session 快照的失效关系难以审计。
|
|
38
|
+
|
|
39
|
+
Gugu 的新策略将群反思、群友批量反思和私聊反思拆成不同任务类型,并使用独立
|
|
40
|
+
游标。QQChat 采用同样的产品语义,但保留 TypeScript、SQLite 和 DSH 官方集成
|
|
41
|
+
边界,不复制 Gugu 的 Python 实现。
|
|
42
|
+
|
|
43
|
+
### 1.2 目标
|
|
44
|
+
|
|
45
|
+
1. 群记忆只保存群体事实,不直接写入成员个人画像。
|
|
46
|
+
2. 群友记忆改为按消息范围批量处理,一次调用可以更新多个成员。
|
|
47
|
+
3. 私聊记忆独立处理用户与 Bot 的对话边界,并拥有完整 daily 生命周期。
|
|
48
|
+
4. 不同任务使用独立游标、幂等键、状态和并发锁,失败可安全重试。
|
|
49
|
+
5. 记忆继续作为 DSH Session 上下文的一部分,不在每轮重复追加完整记忆。
|
|
50
|
+
6. 保持 account、group、member 和 stable sender ID 的数据隔离。
|
|
51
|
+
|
|
52
|
+
### 1.3 非目标
|
|
53
|
+
|
|
54
|
+
- 不修改、patch 或重新构建 DSH;只使用官方可用的 runtime-context、Session、RPC、
|
|
55
|
+
Hook 和类型边界。
|
|
56
|
+
- 不复制 DSH AgentLoop、Session persistence 或工具运行时。
|
|
57
|
+
- 不把记忆移出 SQLite/DSH Session 的既有职责边界。
|
|
58
|
+
- 本阶段不支持每个群独立的回复模式覆盖;全局 `groupReceiveMode` 仍是唯一回复策略。
|
|
59
|
+
- 不把每次记忆变化编码成模型可见的长 diff。
|
|
60
|
+
- 不因群持续活跃而刷新没有发言成员的成员级快照。
|
|
61
|
+
|
|
62
|
+
## 2. 功能需求
|
|
63
|
+
|
|
64
|
+
### FR-MEM-001:群记忆只写入 Group scope
|
|
65
|
+
|
|
66
|
+
群反思可以读取当前群的成员名单、发送者 ID 和显示名来理解消息,但输出只能
|
|
67
|
+
更新当前群的 `profile`、`summary`、`daily` 和 `memory`。成员个人偏好、背景、
|
|
68
|
+
行为模式和个人事件不得由群反思直接写入 member scope。
|
|
69
|
+
|
|
70
|
+
群反思的长期记忆应保留群规、项目、决定、关系、公共约定和群体阶段变化;不应
|
|
71
|
+
把某个成员的个人资料原样复制到群记忆。
|
|
72
|
+
|
|
73
|
+
### FR-MEM-002:群友记忆使用 Member batch
|
|
74
|
+
|
|
75
|
+
群消息达到成员批量水位后,创建一个 `member-batch` 任务。任务可以包含多个真实
|
|
76
|
+
发送者,并按 stable sender ID 分组返回结果。相同群、相同消息范围和相同任务类型
|
|
77
|
+
只能产生一个有效任务。
|
|
78
|
+
|
|
79
|
+
本项目默认统计所有已经落库的群消息,包括触发 Agent 的消息、被 @ 的消息和静默
|
|
80
|
+
记录消息。是否送入 Agent 与是否参与记忆是两个独立判断,避免回复模式切换导致
|
|
81
|
+
owner 或活跃成员的画像事实丢失。
|
|
82
|
+
|
|
83
|
+
### FR-MEM-003:严格的个人事实归属
|
|
84
|
+
|
|
85
|
+
成员批量反思只能更新本批消息中真实出现的 sender ID。以下内容不能归属到个人:
|
|
86
|
+
|
|
87
|
+
- 第三方转述、引用或代为描述的事实;
|
|
88
|
+
- 没有明确主语的“他/她/他们”等代词;
|
|
89
|
+
- 模型根据语气、昵称或上下文自行推测的身份;
|
|
90
|
+
- 其他成员说出的个人信息;
|
|
91
|
+
- 仅由 Bot 回复或建议产生的结论。
|
|
92
|
+
|
|
93
|
+
昵称、历史昵称和群内称呼只用于展示或作为经过确认的画像字段,不能作为身份主键。
|
|
94
|
+
|
|
95
|
+
### FR-MEM-004:私聊反思区分用户和 Bot
|
|
96
|
+
|
|
97
|
+
私聊只有在一次正常 Agent turn 成功完成后才允许创建 `private-owner` 反思任务。
|
|
98
|
+
输入同时包含用户消息和 Bot 回复,但只有用户明确表达的内容才可成为用户事实。
|
|
99
|
+
Bot 的建议、猜测和普通回答只作为对话上下文,不能直接写入 profile 或 memory。
|
|
100
|
+
|
|
101
|
+
私聊使用对应用户的 member scope,支持 `profile`、`pattern`、`summary`、`daily`
|
|
102
|
+
和 `memory`;其游标不能与群反思或群友批量反思共用。
|
|
103
|
+
|
|
104
|
+
### FR-MEM-005:按日期维护 daily
|
|
105
|
+
|
|
106
|
+
同一 scope、同一天的记录必须追加到同一个日期标题下:
|
|
107
|
+
|
|
108
|
+
```markdown
|
|
109
|
+
## 2026-08-29
|
|
110
|
+
- 重要记录一
|
|
111
|
+
- 重要记录二
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
不得为每条消息重复创建日期标题。群、跨群成员和私聊的 daily 仍然属于各自 scope,
|
|
115
|
+
不因格式相同而合并数据。
|
|
116
|
+
|
|
117
|
+
### FR-MEM-006:daily 压缩和长期 memory
|
|
118
|
+
|
|
119
|
+
daily 是近期事实缓冲,达到水位后才压缩到长期 `memory`。目标策略参考 Gugu:
|
|
120
|
+
|
|
121
|
+
| Scope | 压缩水位 | 保留最近 | 单次硬上限 |
|
|
122
|
+
| --- | ---: | ---: | ---: |
|
|
123
|
+
| group | 200 条 | 100 条 | 300 条输入 |
|
|
124
|
+
| member/private | 100 条 | 50 条 | 由现有压缩预算限制 |
|
|
125
|
+
|
|
126
|
+
压缩输出应保留日期、事件背景、关键事实、结果和后续约定。压缩失败、空输出、
|
|
127
|
+
无效 JSON 或日期校验失败时,保留旧 memory 和原 daily,不推进游标、不删除记录。
|
|
128
|
+
|
|
129
|
+
### FR-MEM-007:记忆更新与 Session snapshot 协作
|
|
130
|
+
|
|
131
|
+
记忆文档更新后只标记对应 scope 的 version/hash 变化,不在每轮 Agent turn 追加一
|
|
132
|
+
份完整记忆。新 Session、默认 30 分钟 TTL 到期、Session compact 完成或显式 stale
|
|
133
|
+
时,重新组装当前有效的群记忆与当前发言成员记忆。
|
|
134
|
+
|
|
135
|
+
群持续活跃不能刷新潜水成员的成员快照。成员再次发言时,刷新该成员并将最新群级
|
|
136
|
+
记忆与该成员记忆组合为当前 Session snapshot。快照仍使用 DSH 官方 runtime-context
|
|
137
|
+
来源,事件数据必须可 JSON 序列化。
|
|
138
|
+
|
|
139
|
+
## 3. 技术方案
|
|
140
|
+
|
|
141
|
+
### 3.1 任务模型
|
|
142
|
+
|
|
143
|
+
反思任务至少区分:
|
|
144
|
+
|
|
145
|
+
```text
|
|
146
|
+
group 群体反思和群 daily 压缩
|
|
147
|
+
member-batch 群消息中的成员批量反思
|
|
148
|
+
private-owner 私聊用户记忆反思
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
任务需要表达 scope、scope ID、task type、消息范围、状态、重试次数、创建时间和
|
|
152
|
+
幂等键。幂等键必须包含 task type,避免 group 与 member-batch 消息范围相同而互相
|
|
153
|
+
覆盖。
|
|
154
|
+
|
|
155
|
+
### 3.2 游标和事务
|
|
156
|
+
|
|
157
|
+
群至少维护两条独立游标:
|
|
158
|
+
|
|
159
|
+
```text
|
|
160
|
+
last_reflected_message_id
|
|
161
|
+
last_member_reflected_message_id
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
group 成功只推进第一条;member-batch 成功只推进第二条;private-owner 使用私聊
|
|
165
|
+
自己的范围和游标。文档写入、输出校验和游标推进必须形成一个可重试的事务边界。
|
|
166
|
+
|
|
167
|
+
本项目采用整批成功后推进游标的语义:批量输出中任意成员 ID 无效、字段非法或写入
|
|
168
|
+
失败时,整批不推进,下一次按幂等键重试,避免出现已跳过但没有落库的消息。
|
|
169
|
+
|
|
170
|
+
### 3.3 Group reflection 输入输出
|
|
171
|
+
|
|
172
|
+
Group 输入包括当前群身份、已有 group 文档和游标之后的消息。成员名单只作为理解
|
|
173
|
+
上下文的只读参考。输出只能包含 group 文档字段:
|
|
174
|
+
|
|
175
|
+
```json
|
|
176
|
+
{
|
|
177
|
+
"profile": [],
|
|
178
|
+
"summary": "...",
|
|
179
|
+
"daily": [],
|
|
180
|
+
"memory": "..."
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Host 必须拒绝或忽略 `members`、`profile_add` 等个人文档输出,不能因为模型返回了
|
|
185
|
+
额外字段就顺带写入成员记忆。
|
|
186
|
+
|
|
187
|
+
### 3.4 Member batch 输入输出
|
|
188
|
+
|
|
189
|
+
输入按当前群和消息范围提供成员已有文档:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"group": { "id": "group-id", "name": "群名" },
|
|
194
|
+
"members": [
|
|
195
|
+
{
|
|
196
|
+
"senderId": "stable-sender-id",
|
|
197
|
+
"displayName": "展示名",
|
|
198
|
+
"existing": {
|
|
199
|
+
"profile": "...",
|
|
200
|
+
"pattern": "...",
|
|
201
|
+
"summary": "...",
|
|
202
|
+
"daily": "...",
|
|
203
|
+
"memory": "..."
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
],
|
|
207
|
+
"messages": []
|
|
208
|
+
}
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
输出按 sender ID 分组:
|
|
212
|
+
|
|
213
|
+
```json
|
|
214
|
+
{
|
|
215
|
+
"members": [
|
|
216
|
+
{
|
|
217
|
+
"senderId": "stable-sender-id",
|
|
218
|
+
"profile_add": [],
|
|
219
|
+
"profile_remove": [],
|
|
220
|
+
"pattern_add": [],
|
|
221
|
+
"pattern_remove": [],
|
|
222
|
+
"summary": "...",
|
|
223
|
+
"daily": [],
|
|
224
|
+
"memory": "..."
|
|
225
|
+
}
|
|
226
|
+
]
|
|
227
|
+
}
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
Host 先验证 sender ID 属于当前 account、group 和本批消息,再合并文档。profile
|
|
231
|
+
使用类型化字段,pattern 区分观察和推断;所有字段限制长度并检查 JSON 可序列化。
|
|
232
|
+
|
|
233
|
+
### 3.5 Private-owner 输入输出
|
|
234
|
+
|
|
235
|
+
私聊输入必须保留消息方向:
|
|
236
|
+
|
|
237
|
+
```text
|
|
238
|
+
用户消息 -> 用户事实候选
|
|
239
|
+
Bot 回复 -> 对话上下文
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
私聊反思可以使用与 member batch 相同的文档合并器,但不能复用群消息游标和
|
|
243
|
+
group prompt。私聊 daily 使用相同的日期标题规范,长期压缩使用 member 侧预算。
|
|
244
|
+
|
|
245
|
+
### 3.6 并发、恢复和重试
|
|
246
|
+
|
|
247
|
+
同一个 scope + task type 同时只能运行一个任务;不同群和不同私聊可以并发。内存
|
|
248
|
+
Promise 锁用于同进程复用,数据库任务状态和幂等键用于进程重启后的恢复。
|
|
249
|
+
|
|
250
|
+
LLM 超时、无效 JSON、空结果、schema 校验失败或 SQLite 写入失败时:保留原文档,
|
|
251
|
+
不推进对应游标,并记录不包含正文的错误类别。重复任务必须在执行前按幂等键去重。
|
|
252
|
+
|
|
253
|
+
### 3.7 DSH 兼容边界
|
|
254
|
+
|
|
255
|
+
所有 Agent 调用继续通过 DSH 官方 Agent、Session 和模型路由能力完成;插件只负责
|
|
256
|
+
整理输入、保存结果和注入 snapshot。插件不能要求用户修改 DSH 源码、锁文件或私有
|
|
257
|
+
模块,也不能把自定义记忆事件写入不可恢复的非 JSON 数据。
|
|
258
|
+
|
|
259
|
+
### 3.8 Phase 0 基线盘点
|
|
260
|
+
|
|
261
|
+
当前实现的事实来源和职责映射如下:
|
|
262
|
+
|
|
263
|
+
| 能力 | 当前事实来源/入口 | Phase 0 约束 |
|
|
264
|
+
| --- | --- | --- |
|
|
265
|
+
| QQ 消息 | `messages`,按 `account_id`、`group_id`、`member_id` 隔离 | SQLite 是 QQ 世界事实来源,Session 不重放完整历史 |
|
|
266
|
+
| 群记忆 | `memory_documents(scope_type='group')` | 只允许 `group` task 写入 |
|
|
267
|
+
| 成员记忆 | `memory_documents(scope_type='member')` | 只接受 stable sender ID,成员可跨群复用 |
|
|
268
|
+
| 群反思游标 | `reflection_state.last_message_id` | 只由 `group` task 推进 |
|
|
269
|
+
| 群友批量游标 | `reflection_state.last_member_reflected_message_id` | 只由 `member-batch` task 推进,旧表默认值为 `0` |
|
|
270
|
+
| 私聊游标 | `plugin_settings.memberReflection:<memberId>` | 只由 `private-owner` 任务使用,需成功 Agent turn 后调度 |
|
|
271
|
+
| 任务幂等 | `reflection_tasks.idempotency_key` | key 必须包含 scope、消息范围和 task type |
|
|
272
|
+
| DSH 上下文 | runtime-context snapshot | 只在新 Session、TTL、hash 变化或 compact 后刷新 |
|
|
273
|
+
|
|
274
|
+
旧数据库兼容规则:缺少 `reflection_state.last_member_reflected_message_id` 时添加
|
|
275
|
+
`INTEGER NOT NULL DEFAULT 0`;缺少 `reflection_tasks` 时创建空任务表;既有
|
|
276
|
+
`last_message_id`、成员设置、memory_documents 和 Session 文件保持原样。历史 group
|
|
277
|
+
反思不会自动推进成员批量游标,升级后会按未处理消息范围重新生成 `member-batch`。
|
|
278
|
+
|
|
279
|
+
## 4. 验证与上线
|
|
280
|
+
|
|
281
|
+
### 4.1 自动化验证
|
|
282
|
+
|
|
283
|
+
需要覆盖以下行为:
|
|
284
|
+
|
|
285
|
+
- 群反思只改变 group 文档,成员文档保持不变;
|
|
286
|
+
- 50 条群消息生成一个 `member-batch`,且一次任务可以更新多个真实成员;
|
|
287
|
+
- 重复触发、并发触发和进程重启不会重复处理同一消息范围;
|
|
288
|
+
- 未知 sender ID、第三方转述和非法 JSON 不会写入个人记忆;
|
|
289
|
+
- group 与 member-batch 各自推进正确游标;
|
|
290
|
+
- 私聊反思包含用户和 Bot 消息,未完成成功 Agent turn 时不创建任务;
|
|
291
|
+
- group daily 200/100/300 和 member daily 100/50 的压缩失败保护;
|
|
292
|
+
- TTL 未过期不重复注入,TTL 到期和 compact 后只建立一次新 snapshot;
|
|
293
|
+
- snapshot 和 `qqchat/message` 事件始终可 JSON 序列化。
|
|
294
|
+
|
|
295
|
+
标准命令:
|
|
296
|
+
|
|
297
|
+
```bash
|
|
298
|
+
npm run typecheck
|
|
299
|
+
npm test
|
|
300
|
+
npm run build
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### 4.2 真实环境验证
|
|
304
|
+
|
|
305
|
+
在未修改的官方 DSH 工作区中加载构建产物,验证 DSH Web 中的 Session 恢复、群静默
|
|
306
|
+
记录、群回复、私聊、记忆查看、Session compact 和长时间连续对话。验证日志只检查
|
|
307
|
+
scope、task type、数量、耗时和错误类别,不读取或展示聊天正文。
|
|
308
|
+
|
|
309
|
+
### 4.3 观测指标
|
|
310
|
+
|
|
311
|
+
Host 只记录脱敏指标:
|
|
312
|
+
|
|
313
|
+
```text
|
|
314
|
+
task_type
|
|
315
|
+
scope_type
|
|
316
|
+
message_count
|
|
317
|
+
member_count
|
|
318
|
+
duration_ms
|
|
319
|
+
retry_count
|
|
320
|
+
result_status
|
|
321
|
+
memory_version
|
|
322
|
+
```
|
|
323
|
+
|
|
324
|
+
不记录用户正文、附件内容、完整 QQ payload、Token、AppSecret 或模型原始 prompt。
|
|
325
|
+
|
|
326
|
+
### 4.4 灰度和回滚
|
|
327
|
+
|
|
328
|
+
新任务调度应先以设置或内部配置开关灰度,旧反思路径保留到新路径完成稳定验证。
|
|
329
|
+
灰度期间新旧任务不能同时推进同一条游标。出现异常时关闭新调度开关,继续读取
|
|
330
|
+
已有文档和 daily;失败任务不删除,待修复后按幂等键重试。
|
|
331
|
+
|
|
332
|
+
数据库迁移必须向前兼容。迁移失败时保留旧表和旧文档,不以空表或默认值覆盖已有
|
|
333
|
+
记忆。回滚代码时,旧实现应忽略新任务类型但不能误推进新游标。
|
|
334
|
+
|
|
335
|
+
## 5. 风险与待确认问题
|
|
336
|
+
|
|
337
|
+
### 5.1 风险
|
|
338
|
+
|
|
339
|
+
| 风险 | 影响 | 对策 |
|
|
340
|
+
| --- | --- | --- |
|
|
341
|
+
| 批量输出错误归因 | 错误修改成员画像 | stable sender ID 校验、主语归属规则、整批拒绝 |
|
|
342
|
+
| 群反思继续污染成员记忆 | 群级事实泄漏到个人 scope | group 输出 schema 白名单和成员文档不变回归测试 |
|
|
343
|
+
| 游标先推进后写入 | 消息永久丢失 | 文档写入成功后再推进,失败整批重试 |
|
|
344
|
+
| 多任务重复调用模型 | 成本上升、文档互相覆盖 | task type 幂等键、scope 锁和数据库状态 |
|
|
345
|
+
| 私聊把 Bot 观点当用户事实 | 个人画像失真 | 保留消息 direction,private prompt 明确事实来源 |
|
|
346
|
+
| Session 快照重复膨胀 | 上下文缓存率下降 | snapshot projection、hash、TTL 和 compact stale |
|
|
347
|
+
| 旧数据库迁移不完整 | 服务启动失败或记忆不可读 | 可回滚迁移、旧字段兼容和恢复测试 |
|
|
348
|
+
|
|
349
|
+
### 5.2 待确认问题
|
|
350
|
+
|
|
351
|
+
1. 成员批量反思的 50 条水位是否按“所有已落库群消息”计算,还是仅统计静默群消息。
|
|
352
|
+
当前 PRD 选择前者,原因是记忆链路不应依赖回复模式。
|
|
353
|
+
2. `member-batch` 的消息范围是否按群切分,还是允许同一 account 下多个群合并处理。
|
|
354
|
+
当前选择按群切分,优先保证群范围和失败重试的可解释性。
|
|
355
|
+
3. 成员批量输出中单个成员写入失败时,是否未来支持按成员部分提交。当前选择整批
|
|
356
|
+
回滚,避免游标语义和审计复杂化。
|
|
357
|
+
|
|
358
|
+
## 6. 唯一实施 TODO
|
|
359
|
+
|
|
360
|
+
### Phase 0:契约与基线
|
|
361
|
+
|
|
362
|
+
- [x] `MEM1-001` 盘点当前 `MemoryEngine`、SQLite schema、反思游标和 Session snapshot;验收:已形成字段映射与旧数据库兼容说明,且未修改 DSH。
|
|
363
|
+
- [x] `MEM1-002` 固定 `group`、`member-batch`、`private-owner` 任务类型及 group/member/private scope 边界;验收:统一类型定义、Prompt 契约和文档术语。
|
|
364
|
+
- [x] `MEM1-003` 增加 group 输出白名单、stable sender ID 和个人事实归属校验;验收:group 反思忽略成员输出,成员批量输出只接受当前批真实成员,未知 ID 和重复 ID 会被拒绝。
|
|
365
|
+
|
|
366
|
+
### Phase 1:群记忆与成员批量任务
|
|
367
|
+
|
|
368
|
+
- [x] `MEM1-004` 增加反思任务 `task_type`、消息范围和幂等键;验收:相同 scope、范围和任务类型不会产生重复运行任务,失败范围可重试。
|
|
369
|
+
- [x] `MEM1-005` 增加成员批量独立游标并实现 50 条群消息水位和 idle 补偿;验收:group 与 member-batch 各自推进游标,失败不推进成员游标。
|
|
370
|
+
- [x] `MEM1-006` 将群友反思改为一次处理多个真实成员;验收:成员批量链路一次读取一个群的消息范围,并按 stable sender ID 合并多个成员文档。
|
|
371
|
+
- [ ] `MEM1-007` 增加同群并发、重复任务、进程重启、未知成员和整批回滚测试;验收:测试覆盖锁、幂等、恢复和失败重试。
|
|
372
|
+
|
|
373
|
+
### Phase 2:私聊记忆
|
|
374
|
+
|
|
375
|
+
- [x] `MEM1-008` 增加 `private-owner` Prompt、独立游标和用户/Bot 消息方向输入;验收:Bot 回复不会被写成用户事实,私聊任务使用独立幂等范围。
|
|
376
|
+
- [x] `MEM1-009` 将私聊 daily 接入 member 压缩和日期规范;验收:daily 标题按日期合并,压缩失败保留原始记录。
|
|
377
|
+
|
|
378
|
+
### Phase 3:压缩与上下文快照
|
|
379
|
+
|
|
380
|
+
- [x] `MEM1-010` 调整 group daily 压缩水位为 200/100/300,并校验事件日期与失败保护;验收:压缩成功才裁剪旧 daily,失败不覆盖 memory。
|
|
381
|
+
- [x] `MEM1-011` 将 batch 反思结果接入 memory version/hash 和 Session stale;验收:文档变化由 snapshot hash 感知,连续对话不重复追加完整快照,TTL 到期和 compact 后只刷新一次。
|
|
382
|
+
- [x] `MEM1-012` 增加可控时间的 group/member/private TTL 与 compact 集成测试;验收:已有 snapshot 测试覆盖内容变化、TTL 到期和 compact stale,group/private 反思测试覆盖新任务上下文。
|
|
383
|
+
|
|
384
|
+
### Phase 4:迁移、灰度与收尾
|
|
385
|
+
|
|
386
|
+
- [x] `MEM1-013` 实现旧数据库迁移、任务恢复和回滚保护;验收:旧数据库可启动,新增批量游标和任务表自动迁移,超时运行任务可重试,已有文档不丢失。
|
|
387
|
+
- [x] `MEM1-014` 增加脱敏任务指标和灰度开关;验收:日志只包含任务类型、数量和耗时,不含正文/凭据;关闭成员批量开关后旧 Session 与记忆仍可读取。
|
|
388
|
+
- [ ] `MEM1-015` 完成官方未修改 DSH 工作区的 typecheck、test、build、Session 恢复和 DSH Web 长期观察;验收:记录测试结果和真实 smoke test 结论,并同步相关文档。
|
|
389
|
+
|
|
390
|
+
## 7. 相关文档
|
|
391
|
+
|
|
392
|
+
- [记忆系统](../MEMORY.md)
|
|
393
|
+
- [记忆系统上下文注入方案](../MEMORY_CONTEXT.md)
|
|
394
|
+
- [架构说明](../ARCHITECTURE.md)
|
|
395
|
+
- [安全与数据边界](../SECURITY.md)
|
|
396
|
+
- [开发与验证](../DEVELOPMENT.md)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# PRD 撰写规范
|
|
2
|
+
|
|
3
|
+
本目录存放 dsh-qqchat 的产品需求文档。PRD 用于描述产品目标、功能契约、技术
|
|
4
|
+
边界、验收方式和唯一实施清单;实现状态以文档头部和“实际状态”为准。
|
|
5
|
+
|
|
6
|
+
## 文件命名
|
|
7
|
+
|
|
8
|
+
使用:
|
|
9
|
+
|
|
10
|
+
```text
|
|
11
|
+
PRD-<领域>-<编号>-<中文主题>.md
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
例如:`PRD-MEM-1-群记忆与群友记忆策略调整.md`。
|
|
15
|
+
|
|
16
|
+
- `MEM` 表示记忆领域;其他领域可以使用 `IM`、`UI`、`SEC` 等短标识。
|
|
17
|
+
- 编号按领域递增,不使用全局流水号。
|
|
18
|
+
- 主题使用简短中文,不堆叠英文缩写。
|
|
19
|
+
- 同一领域的延续性工作优先更新原 PRD;全新独立特性再新建文档。
|
|
20
|
+
|
|
21
|
+
## 文档头部
|
|
22
|
+
|
|
23
|
+
```markdown
|
|
24
|
+
# PRD 标题
|
|
25
|
+
|
|
26
|
+
> 状态:当前实际能力和缺口
|
|
27
|
+
> 创建:YYYY-MM-DD
|
|
28
|
+
> 最近更新:YYYY-MM-DD
|
|
29
|
+
> 关联模块:`path/to/file.ts`、`path/to/other.ts`
|
|
30
|
+
> 背景参考:参考实现、调查报告或相关文档
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
“状态”描述真实结果,不写 Phase 作为状态。代码、测试或验证结果发生变化时,
|
|
34
|
+
必须同步更新状态和最近更新日期。
|
|
35
|
+
|
|
36
|
+
## 推荐章节顺序
|
|
37
|
+
|
|
38
|
+
1. **实际状态**:只读摘要表,说明当前能力和结果,不列任务。
|
|
39
|
+
2. **背景与目标**:现状、问题、目标和非目标。
|
|
40
|
+
3. **功能需求**:使用 `FR-<领域>-<编号>` 编号,描述目标行为和边界。
|
|
41
|
+
4. **技术方案**:职责、数据、依赖、兼容性、隐私和错误处理。
|
|
42
|
+
5. **验证与上线**:验收方法、测试命令、观测、灰度和回滚。
|
|
43
|
+
6. **风险与待确认问题**:先列风险,再列尚未定案的问题。
|
|
44
|
+
7. **唯一实施 TODO**:全文唯一的可执行复选清单。
|
|
45
|
+
|
|
46
|
+
只有“唯一实施 TODO”可以使用 `[ ]` 和 `[x]`。其他章节不得用复选框表达进度,
|
|
47
|
+
也不得创建“剩余工作”“Phase TODO”或“验收清单”等第二个任务来源。
|
|
48
|
+
|
|
49
|
+
## 状态标记
|
|
50
|
+
|
|
51
|
+
- ✅ 已完成:代码已合并并验证。
|
|
52
|
+
- 🟡 部分完成:已有可运行实现,但仍存在明确缺口。
|
|
53
|
+
- 🔲 待评估:尚未实现或尚未完成真实环境验证。
|
|
54
|
+
- 🚧 进行中:正在实施。
|
|
55
|
+
|
|
56
|
+
TODO 按 Phase 分组时,每条任务必须有稳定编号、独立复选状态、责任边界和验收
|
|
57
|
+
标准。需求、方案和验收章节只引用任务编号,不复制任务内容。
|
|
58
|
+
|
|
59
|
+
## 语言与链接
|
|
60
|
+
|
|
61
|
+
正文使用简体中文;代码标识、字段名、环境变量和命令用反引号包裹。文档之间使用
|
|
62
|
+
相对路径,必要时同时链接 `docs/README.md` 中的导航入口。
|
package/lib/client.js
CHANGED
|
@@ -6,12 +6,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
const shared_cjs_1 = require("./shared.cjs");
|
|
7
7
|
const settings_cjs_1 = require("./settings.cjs");
|
|
8
8
|
const workspace_cjs_1 = require("./workspace.cjs");
|
|
9
|
-
exports.inject = ['slots', 'connection', '
|
|
9
|
+
exports.inject = ['slots', 'connection', 'uiConversation'];
|
|
10
10
|
exports.apply = function apply(ctx) {
|
|
11
11
|
(0, shared_cjs_1.setQQRpc)(ctx.connection.rpc);
|
|
12
12
|
ctx.effect(shared_cjs_1.installStyles, 'dsh-qqchat: client styles');
|
|
13
13
|
ctx.effect(shared_cjs_1.installQQSettingsIcon, 'dsh-qqchat: QQ settings icon');
|
|
14
|
-
ctx.
|
|
14
|
+
ctx.uiConversation.events.register((0, workspace_cjs_1.createQQMessageDefinition)());
|
|
15
15
|
ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'qqchat', order: 35, label: () => 'QQ Chat', inject: () => ({ rpc: ctx.connection.rpc }) }, settings_cjs_1.QQSettings));
|
|
16
16
|
ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({ name: 'conversation.chat.node', key: 'qqchat-message' }, workspace_cjs_1.QQTranscriptNode));
|
|
17
17
|
ctx.slots.inject('conversation.session.header.utilities', () => ctx.slots.register({ name: 'conversation.session.header.utilities', id: 'qqchat-memory', order: 30, inject: () => ({ rpc: ctx.connection.rpc }) }, workspace_cjs_1.QQSessionUtility));
|
|
@@ -40,6 +40,7 @@ function QQSettings({ rpc }) {
|
|
|
40
40
|
const [groupReplyMenuOpen, setGroupReplyMenuOpen] = (0, shared_cjs_1.useState)(false);
|
|
41
41
|
const [directReplyMenuOpen, setDirectReplyMenuOpen] = (0, shared_cjs_1.useState)(false);
|
|
42
42
|
const [ownerMenuOpen, setOwnerMenuOpen] = (0, shared_cjs_1.useState)(false);
|
|
43
|
+
const [receiveHelpOpen, setReceiveHelpOpen] = (0, shared_cjs_1.useState)(false);
|
|
43
44
|
const refresh = (0, shared_cjs_1.useCallback)(async () => {
|
|
44
45
|
try {
|
|
45
46
|
const [status, prefs] = await Promise.all([
|
|
@@ -218,7 +219,23 @@ function QQSettings({ rpc }) {
|
|
|
218
219
|
variant: settings.groupReceiveMode === mode ? 'primary' : 'outline',
|
|
219
220
|
onClick: () => void patch({ groupReceiveMode: mode }),
|
|
220
221
|
}, mode === 'auto' ? '自动回应' : mode === 'mention' ? '@回复' : '静默记录')));
|
|
221
|
-
const
|
|
222
|
+
const receiveHelp = (0, shared_cjs_1.h)(shared_cjs_1.Menu, {
|
|
223
|
+
open: receiveHelpOpen,
|
|
224
|
+
onClose: () => setReceiveHelpOpen(false),
|
|
225
|
+
onSelect: () => undefined,
|
|
226
|
+
portal: true,
|
|
227
|
+
align: 'end',
|
|
228
|
+
items: [
|
|
229
|
+
{ type: 'label', id: 'receive-help-title', text: '开启全量消息接收' },
|
|
230
|
+
{ type: 'label', id: 'receive-help-step-1', text: '1. 手机端 QQ 打开机器人所在的群' },
|
|
231
|
+
{ type: 'label', id: 'receive-help-step-2', text: '2. 点击机器人的头像进入资料页' },
|
|
232
|
+
{ type: 'label', id: 'receive-help-step-3', text: '3. 点击右上角「设置」' },
|
|
233
|
+
{ type: 'label', id: 'receive-help-step-4', text: '4. 打开「全量消息接收」' },
|
|
234
|
+
],
|
|
235
|
+
footer: [{ type: 'label', id: 'receive-help-note', text: '未开启时只能收到 @ 消息,非 @ 消息不会进入会话记录' }],
|
|
236
|
+
anchor: (0, shared_cjs_1.h)(shared_cjs_1.Button, { size: 'sm', variant: 'outline', onClick: () => setReceiveHelpOpen(!receiveHelpOpen) }, '设置方法'),
|
|
237
|
+
});
|
|
238
|
+
const receiveGroup = (0, shared_cjs_1.h)('div', { className: 'qqSettingsGroup' }, (0, shared_cjs_1.h)('h3', null, '消息接收'), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('div', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, '群聊接收方式'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp qqHelpRow' }, '自动回应:每条群消息都会触发 Agent;@回复:所有消息都记录,但只有 @Bot 才唤醒 Agent;静默记录:只记录群聊与记忆,不主动回应。', receiveHelp)), (0, shared_cjs_1.h)('div', { className: 'qqControl' }, receiveButtons)), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('div', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, '群聊消息兼容格式'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp' }, '仅控制发送到 QQ 群聊时使用的消息格式。')), (0, shared_cjs_1.h)('div', { className: 'qqControl' }, groupReplyPicker)), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('div', null, (0, shared_cjs_1.h)('div', { className: 'qqSettingTitle' }, '私聊消息兼容格式'), (0, shared_cjs_1.h)('div', { className: 'qqSettingHelp' }, '仅控制发送到 QQ 私聊时使用的消息格式。')), (0, shared_cjs_1.h)('div', { className: 'qqControl' }, directReplyPicker)), (0, shared_cjs_1.h)('div', { className: 'qqSetting' }, (0, shared_cjs_1.h)('label', { className: 'qqCheckRow' }, (0, shared_cjs_1.h)('input', {
|
|
222
239
|
type: 'checkbox',
|
|
223
240
|
checked: settings.directStreamingEnabled && settings.directReplyFormat !== 'compat',
|
|
224
241
|
disabled: settings.directReplyFormat === 'compat',
|
|
@@ -331,7 +348,7 @@ const css = `
|
|
|
331
348
|
.qqs{display:block;width:100%;min-width:0}.qqsHead{width:100%}.qqSettingsGroup{display:block;width:100%;min-width:0;margin-top:22px}.qqSettingsGroup h3{margin:0 0 8px;font-size:13px;font-weight:600;color:var(--dsw-alias-label-primary)}.qqSettingsGroup .qqSetting{display:flex;width:100%;min-width:0;flex:0 0 auto;padding:12px 0;border-bottom:1px solid var(--dsw-alias-border-l1)}.qqSettingsGroup .qqSetting:last-child{border-bottom:0}.qqSetting>div:first-child{flex:1 1 auto;min-width:0}.qqControl{flex:0 0 auto}.qqChoiceButtons{display:flex;flex-wrap:wrap;justify-content:flex-end;gap:6px}.qqSettingsGroup select{font:inherit;color:var(--dsw-alias-label-primary);background:var(--dsw-alias-bg-layer-1);border:1px solid var(--dsw-alias-border-l2);border-radius:8px;padding:5px 8px}.qqCheckRow{display:flex;align-items:flex-start;gap:10px;cursor:pointer}.qqCheckRow input{flex:none;width:16px;height:16px;margin:2px 0 0;accent-color:var(--dsw-alias-button-primary-fill);cursor:pointer}.qqConnect{display:flex;flex-direction:column;align-items:center;gap:14px;padding:24px 0;text-align:center}.qqTranscript{margin:16px 0}.qqTranscriptBody{max-width:min(78%,680px)}.qqBubble{border:0;border-radius:18px;padding:9px 13px;background:var(--dsw-alias-bg-layer-2);color:var(--dsw-alias-label-primary)}.qqTranscript.out .qqBubble{border-radius:18px;background:var(--dsw-alias-button-primary-fill);color:var(--dsw-alias-label-primary-foreground)}.qqTranscriptMeta{margin:0 4px 4px;font-size:12px;color:var(--dsw-alias-label-secondary)}.qqQuote{border-color:var(--dsw-alias-border-l2);background:var(--dsw-alias-bg-layer-1)}
|
|
332
349
|
.qqWideModal{width:min(760px,calc(100vw - 48px));max-height:calc(100vh - 48px);min-height:0;overflow:hidden}.qqModalContent{min-height:0;overflow:auto}.qqModalContent .qqMemoryGrid{min-width:0}.qqModalContent .qqLog{overflow-wrap:anywhere;word-break:break-word}
|
|
333
350
|
`.concat('.qqQuoteSender{margin-top:4px;font-size:10px;color:var(--dsw-alias-label-secondary)}.qqMediaList{display:flex;flex-wrap:wrap;gap:6px;margin-top:7px}.qqMediaCard{display:flex;align-items:center;gap:6px;max-width:100%;padding:6px 8px;border:1px solid var(--dsw-alias-border-l1);border-radius:8px;background:var(--dsw-alias-bg-layer-1);font-size:11px}.qqMediaKind{color:var(--dsw-alias-brand-primary);font-weight:650}.qqMediaPreview{width:42px;height:42px;object-fit:cover;border-radius:6px}.qqMediaName{max-width:220px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.qqMediaSize,.qqMediaQuoted{color:var(--dsw-alias-label-secondary)}');
|
|
334
|
-
const qqThemeOverrides = '.qqTranscript.out .qqBubble{background:var(--dsw-
|
|
351
|
+
const qqThemeOverrides = '.qqBubble{background:var(--dsw-specific-bubble);color:var(--dsw-alias-label-primary)}.qqTranscript.out .qqBubble{background:var(--dsw-specific-bubble);color:var(--dsw-alias-label-primary)}';
|
|
335
352
|
function installStyles() {
|
|
336
353
|
if (typeof document === 'undefined' || document.querySelector(`style[data-plugin-css="${STYLE_ID}"]`))
|
|
337
354
|
return () => { };
|
package/lib/config.js
CHANGED
|
@@ -8,10 +8,10 @@ const DEFAULT_CONFIG = Object.freeze({
|
|
|
8
8
|
groupReadEnabled: true,
|
|
9
9
|
replyFormat: 'smart',
|
|
10
10
|
recentGroupMessages: 40,
|
|
11
|
-
reflectionIdleMs:
|
|
11
|
+
reflectionIdleMs: 900_000,
|
|
12
12
|
reflectionBatchSize: 20,
|
|
13
13
|
reflectionMaxMessages: 80,
|
|
14
|
-
memoryMaxTokens:
|
|
14
|
+
memoryMaxTokens: 4096,
|
|
15
15
|
memoryCompressionMaxTokens: 15000,
|
|
16
16
|
});
|
|
17
17
|
export function resolveConfig(input = {}) {
|
|
@@ -45,6 +45,7 @@ export function defaultRuntimeSettings(config) {
|
|
|
45
45
|
: config.groupRequiresAt ? 'mention' : 'auto';
|
|
46
46
|
return {
|
|
47
47
|
memoryEnabled: true,
|
|
48
|
+
memoryMemberBatchEnabled: true,
|
|
48
49
|
groupReceiveMode,
|
|
49
50
|
groupReplyFormat: 'compat',
|
|
50
51
|
directReplyFormat: config.replyFormat,
|
package/lib/index.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
2
|
import type { QQChatConfigInput } from './types.js';
|
|
3
3
|
export declare const name = "dsh-qqchat";
|
|
4
|
-
export declare const inject: readonly ["connection", "agents", "agentDefaultModel", "commands", "llm", "tools", "workspaceRegistry"];
|
|
4
|
+
export declare const inject: readonly ["connection", "agents", "agentDefaultModel", "commands", "llm", "tools", "webServer", "workspaceRegistry"];
|
|
5
5
|
export declare function apply(ctx: Context, inputConfig?: QQChatConfigInput): void;
|
|
6
6
|
export type { QQChatConfig, QQChatConfigInput } from './types.js';
|
|
7
7
|
export { QQChatDatabase } from './storage/db.js';
|
|
8
8
|
export { normalizeQQDispatch } from './gateway/normalize.js';
|
|
9
9
|
declare const _default: {
|
|
10
10
|
name: string;
|
|
11
|
-
inject: readonly ["connection", "agents", "agentDefaultModel", "commands", "llm", "tools", "workspaceRegistry"];
|
|
11
|
+
inject: readonly ["connection", "agents", "agentDefaultModel", "commands", "llm", "tools", "webServer", "workspaceRegistry"];
|
|
12
12
|
apply: typeof apply;
|
|
13
13
|
};
|
|
14
14
|
export default _default;
|
package/lib/index.js
CHANGED
|
@@ -13,7 +13,7 @@ import { QQChatRuntime } from './session/runtime.js';
|
|
|
13
13
|
import { registerQQChatSessionEventType } from './session/registration.js';
|
|
14
14
|
const qqChatSessionEventRegistrations = await registerQQChatSessionEventType();
|
|
15
15
|
export const name = 'dsh-qqchat';
|
|
16
|
-
export const inject = ['connection', 'agents', 'agentDefaultModel', 'commands', 'llm', 'tools', 'workspaceRegistry'];
|
|
16
|
+
export const inject = ['connection', 'agents', 'agentDefaultModel', 'commands', 'llm', 'tools', 'webServer', 'workspaceRegistry'];
|
|
17
17
|
export function apply(ctx, inputConfig = {}) {
|
|
18
18
|
const config = resolveConfig(inputConfig);
|
|
19
19
|
mkdirSync(config.dataDir, { recursive: true, mode: 0o700 });
|
|
@@ -29,10 +29,9 @@ export function apply(ctx, inputConfig = {}) {
|
|
|
29
29
|
const media = new QQMediaStore(db, ctx, logger);
|
|
30
30
|
const bridge = new DshQQBridge(ctx, db, memory, config, logger);
|
|
31
31
|
const runtime = new QQChatRuntime(ctx, db, api, auth, memory, bridge, config, logger, media);
|
|
32
|
-
ctx.connection.rpc.handle('/qqchat', createQQChatRpc(runtime)
|
|
32
|
+
ctx.connection.rpc.handle('/qqchat', createQQChatRpc(runtime));
|
|
33
33
|
ctx.effect(() => {
|
|
34
34
|
void (async () => {
|
|
35
|
-
await bridge.ensureWorkspaceMembership();
|
|
36
35
|
await runtime.start();
|
|
37
36
|
})().catch(error => logger.error?.(error));
|
|
38
37
|
return () => runtime.stop();
|
|
@@ -22,25 +22,24 @@ export declare class DshQQBridge {
|
|
|
22
22
|
private readonly activeAttachments;
|
|
23
23
|
private readonly activeMediaReadable;
|
|
24
24
|
private readonly memorySnapshots;
|
|
25
|
-
private qqWorkspace?;
|
|
26
25
|
private readonly disposeEvent;
|
|
26
|
+
private readonly disposeAssistantStream;
|
|
27
27
|
private readonly disposeToolGate;
|
|
28
28
|
private readonly disposeCommands;
|
|
29
29
|
private readonly disposeImageTool;
|
|
30
30
|
private readonly disposeMediaTools;
|
|
31
31
|
constructor(ctx: Context, db: QQChatDatabase, memory: MemoryEngine, config: QQChatConfig, logger?: LoggerLike);
|
|
32
|
+
attachMappedSessionsToWorkspace(): Promise<void>;
|
|
32
33
|
dispose(): Promise<void>;
|
|
33
34
|
ensureChatSession(chatType: ChatType, row: GroupRow | MemberRow): Promise<string>;
|
|
34
|
-
ensureWorkspaceMembership(): Promise<void>;
|
|
35
35
|
recordTranscript(event: QQChatDisplayEvent, row: GroupRow | MemberRow, createSession?: boolean): Promise<string | undefined>;
|
|
36
36
|
reply(message: QQNormalizedMessage, group: GroupRow | undefined, member: MemberRow, attachments?: StoredAttachmentSummary[], onTextDelta?: (delta: string) => void, isOwner?: boolean): Promise<string>;
|
|
37
37
|
private onSessionEvent;
|
|
38
38
|
private injectMemorySnapshot;
|
|
39
39
|
private restoreMemorySnapshot;
|
|
40
40
|
private ensureAgent;
|
|
41
|
-
/** Keep all QQ sessions together in one DSH Workspace using the host API. */
|
|
42
|
-
private attachToQQWorkspace;
|
|
43
41
|
private isArchived;
|
|
42
|
+
private attachToWorkspace;
|
|
44
43
|
private rememberRoute;
|
|
45
44
|
private appendDisplayIfMissing;
|
|
46
45
|
private appendOwnerMessageIfMissing;
|