dsh-qqchat 0.2.3 → 0.2.5
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 +1 -1
- package/README.md +1 -1
- package/client-src/plugin.cts +2 -1
- package/client-src/settings.cts +18 -1
- package/client-src/shared.cts +43 -2
- package/docs/MEMORY.md +32 -4
- package/docs/MEMORY_CONTEXT.md +16 -0
- package/docs/README.md +2 -0
- 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 +64 -2
- package/lib/config.js +3 -2
- package/lib/index.js +3 -1
- package/lib/session/memory-snapshot.d.ts +4 -0
- package/lib/session/memory-snapshot.js +6 -2
- package/lib/session/runtime.js +14 -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 +2 -1
- 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 +3 -1
- package/src/session/agent-bridge.ts +3 -1
- package/src/session/memory-snapshot.ts +6 -3
- package/src/session/runtime.ts +13 -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
package/README.en.md
CHANGED
|
@@ -220,7 +220,7 @@ QQ Sessions use DSH's native Agent Preset mechanism. The preset configured for t
|
|
|
220
220
|
## Notes
|
|
221
221
|
|
|
222
222
|
- An official QQ Bot is required; personal QQ account login is not supported.
|
|
223
|
-
- `0.2.
|
|
223
|
+
- `0.2.2` is still being refined; test with a dedicated Bot and group first.
|
|
224
224
|
- Rich media, full QQ emoji handling and some complex message formats may require compatibility mode.
|
|
225
225
|
- Uninstalling the plugin does not automatically delete saved chat or memory data.
|
|
226
226
|
|
package/README.md
CHANGED
|
@@ -202,7 +202,7 @@ QQ Session 使用 DSH 原生的 Agent Preset 机制。插件配置中的 preset
|
|
|
202
202
|
## 注意事项
|
|
203
203
|
|
|
204
204
|
- 需要 QQ 官方 Bot,不支持个人 QQ 账号登录。
|
|
205
|
-
- `0.2.
|
|
205
|
+
- `0.2.2` 仍在持续完善,建议先在测试 Bot 和测试群中使用。
|
|
206
206
|
- 富媒体、完整 QQ 表情和部分复杂消息格式可能需要使用兼容模式。
|
|
207
207
|
- 卸载插件不会自动删除已经保存的聊天和记忆数据。
|
|
208
208
|
|
package/client-src/plugin.cts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { installStyles, setQQRpc } from './shared.cjs'
|
|
1
|
+
import { installQQSettingsIcon, installStyles, setQQRpc } from './shared.cjs'
|
|
2
2
|
import type { Ctx } from './shared.cjs'
|
|
3
3
|
import { QQSettings } from './settings.cjs'
|
|
4
4
|
import { QQSessionUtility, QQTranscriptNode, createQQMessageDefinition } from './workspace.cjs'
|
|
@@ -7,6 +7,7 @@ exports.inject = ['slots', 'connection', 'conversationEvents'] as const
|
|
|
7
7
|
exports.apply = function apply(ctx: Ctx) {
|
|
8
8
|
setQQRpc(ctx.connection.rpc)
|
|
9
9
|
ctx.effect(installStyles, 'dsh-qqchat: client styles')
|
|
10
|
+
ctx.effect(installQQSettingsIcon, 'dsh-qqchat: QQ settings icon')
|
|
10
11
|
ctx.conversationEvents.register(createQQMessageDefinition())
|
|
11
12
|
|
|
12
13
|
ctx.slots.inject('settings.section', () => ctx.slots.register({ name: 'settings.section', id: 'qqchat', order: 35, label: () => 'QQ Chat', inject: () => ({ rpc: ctx.connection.rpc }) }, QQSettings))
|
package/client-src/settings.cts
CHANGED
|
@@ -25,6 +25,7 @@ export function QQSettings({ rpc }: { rpc: Rpc }) {
|
|
|
25
25
|
const [groupReplyMenuOpen, setGroupReplyMenuOpen] = useState(false)
|
|
26
26
|
const [directReplyMenuOpen, setDirectReplyMenuOpen] = useState(false)
|
|
27
27
|
const [ownerMenuOpen, setOwnerMenuOpen] = useState(false)
|
|
28
|
+
const [receiveHelpOpen, setReceiveHelpOpen] = useState(false)
|
|
28
29
|
|
|
29
30
|
const refresh = useCallback(async () => {
|
|
30
31
|
try {
|
|
@@ -168,11 +169,27 @@ export function QQSettings({ rpc }: { rpc: Rpc }) {
|
|
|
168
169
|
variant: settings.groupReceiveMode === mode ? 'primary' : 'outline',
|
|
169
170
|
onClick: () => void patch({ groupReceiveMode: mode }),
|
|
170
171
|
}, mode === 'auto' ? '自动回应' : mode === 'mention' ? '@回复' : '静默记录')))
|
|
172
|
+
const receiveHelp = h(Menu, {
|
|
173
|
+
open: receiveHelpOpen,
|
|
174
|
+
onClose: () => setReceiveHelpOpen(false),
|
|
175
|
+
onSelect: () => undefined,
|
|
176
|
+
portal: true,
|
|
177
|
+
align: 'end',
|
|
178
|
+
items: [
|
|
179
|
+
{ type: 'label', id: 'receive-help-title', text: '开启全量消息接收' },
|
|
180
|
+
{ type: 'label', id: 'receive-help-step-1', text: '1. 手机端 QQ 打开机器人所在的群' },
|
|
181
|
+
{ type: 'label', id: 'receive-help-step-2', text: '2. 点击机器人的头像进入资料页' },
|
|
182
|
+
{ type: 'label', id: 'receive-help-step-3', text: '3. 点击右上角「设置」' },
|
|
183
|
+
{ type: 'label', id: 'receive-help-step-4', text: '4. 打开「全量消息接收」' },
|
|
184
|
+
],
|
|
185
|
+
footer: [{ type: 'label', id: 'receive-help-note', text: '未开启时只能收到 @ 消息,非 @ 消息不会进入会话记录' }],
|
|
186
|
+
anchor: h(Button, { size: 'sm', variant: 'outline', onClick: () => setReceiveHelpOpen(!receiveHelpOpen) }, '设置方法'),
|
|
187
|
+
})
|
|
171
188
|
|
|
172
189
|
const receiveGroup = h('div', { className: 'qqSettingsGroup' },
|
|
173
190
|
h('h3', null, '消息接收'),
|
|
174
191
|
h('div', { className: 'qqSetting' },
|
|
175
|
-
h('div', null, h('div', { className: 'qqSettingTitle' }, '群聊接收方式'), h('div', { className: 'qqSettingHelp' }, '自动回应:每条群消息都会触发 Agent;@回复:所有消息都记录,但只有 @Bot 才唤醒 Agent;静默记录:只记录群聊与记忆,不主动回应。')),
|
|
192
|
+
h('div', null, h('div', { className: 'qqSettingTitle' }, '群聊接收方式'), h('div', { className: 'qqSettingHelp qqHelpRow' }, '自动回应:每条群消息都会触发 Agent;@回复:所有消息都记录,但只有 @Bot 才唤醒 Agent;静默记录:只记录群聊与记忆,不主动回应。', receiveHelp)),
|
|
176
193
|
h('div', { className: 'qqControl' }, receiveButtons)),
|
|
177
194
|
h('div', { className: 'qqSetting' },
|
|
178
195
|
h('div', null, h('div', { className: 'qqSettingTitle' }, '群聊消息兼容格式'), h('div', { className: 'qqSettingHelp' }, '仅控制发送到 QQ 群聊时使用的消息格式。')),
|
package/client-src/shared.cts
CHANGED
|
@@ -38,7 +38,7 @@ export interface Ctx {
|
|
|
38
38
|
slots: SlotRegistry
|
|
39
39
|
}
|
|
40
40
|
export interface Account { id: number; appId: string; enabled: boolean; gatewayStatus: string; gatewayLastError?: string | null }
|
|
41
|
-
export interface Settings { memoryEnabled: boolean; groupReceiveMode: GroupReceiveMode; groupReplyFormat: ReplyFormat; directReplyFormat: ReplyFormat; directStreamingEnabled: boolean; groupMembersCanUseTools: boolean; groupMembersCanReceiveMedia: boolean; groupMembersCanReadMedia: boolean; ownerUserId: string }
|
|
41
|
+
export interface Settings { memoryEnabled: boolean; memoryMemberBatchEnabled: boolean; groupReceiveMode: GroupReceiveMode; groupReplyFormat: ReplyFormat; directReplyFormat: ReplyFormat; directStreamingEnabled: boolean; groupMembersCanUseTools: boolean; groupMembersCanReceiveMedia: boolean; groupMembersCanReadMedia: boolean; ownerUserId: string }
|
|
42
42
|
export interface KnownMember { id: number; platformUserId: string; displayName: string; lastSeenAt: number }
|
|
43
43
|
export interface ChatItem { chatType: ChatType; rowId: number; platformId: string; displayName: string; dshSessionId: string | null; lastMessageAt: number | null; messageCount: number }
|
|
44
44
|
export interface Memory { profile: string; summary: string; daily: string; memory: string; pattern: string }
|
|
@@ -55,6 +55,47 @@ export interface SidebarActionProps { wide: boolean; rpc: Rpc; sessions: Session
|
|
|
55
55
|
export const CHANNEL = '/qqchat'
|
|
56
56
|
const STYLE_ID = 'dsh-qqchat/client-ui-v2'
|
|
57
57
|
|
|
58
|
+
const QQ_ICON_PATH = 'M17.5359 12.5144L16.8402 10.7175C16.8408 10.6968 16.8494 10.3429 16.8494 10.1604C16.8494 7.08792 15.448 4.0003 12.0012 4C8.55459 4.0003 7.15292 7.08792 7.15292 10.1604C7.15292 10.3429 7.16151 10.6968 7.16209 10.7175L6.4667 12.5144C6.27608 13.0285 6.08776 13.564 5.94988 14.0232C5.29262 16.2126 5.50559 17.1186 5.66783 17.139C6.01581 17.1823 7.02221 15.4908 7.02221 15.4908C7.02221 16.4704 7.5095 17.7487 8.56405 18.6719C8.16963 18.7976 7.68635 18.9911 7.37564 19.2284C7.09645 19.442 7.13142 19.6594 7.18158 19.7473C7.40258 20.1329 10.9713 19.9935 12.0017 19.8733C13.0319 19.9935 16.6009 20.1329 16.8216 19.7473C16.872 19.6594 16.9067 19.442 16.6275 19.2284C16.3168 18.9911 15.8333 18.7976 15.4386 18.6716C16.4928 17.7487 16.9801 16.4704 16.9801 15.4908C16.9801 15.4908 17.9868 17.1823 18.3348 17.139C18.4967 17.1186 18.7131 16.2108 18.0524 14.0232C17.9125 13.56 17.7265 13.0285 17.5359 12.5144ZM18.5574 20.7407C18.1843 21.3926 17.7237 21.6334 17.1187 21.7981C16.8792 21.8633 16.621 21.9056 16.325 21.936C15.8844 21.9814 15.3392 22.001 14.712 22C13.786 21.9985 12.693 21.9491 12.0017 21.884C11.3103 21.9491 10.2173 21.9985 9.29129 22C8.66414 22.001 8.11889 21.9814 7.67832 21.936C7.38236 21.9056 7.12409 21.8633 6.88467 21.7981C6.27994 21.6335 5.81954 21.393 5.44496 20.7393C5.15165 20.2258 5.07747 19.6406 5.20612 19.0866C4.61376 18.9546 4.20483 18.6045 3.92733 18.1757C3.77911 17.9466 3.68408 17.7127 3.61845 17.4663C3.53001 17.1344 3.49486 16.7666 3.50184 16.3601C3.51532 15.5749 3.68902 14.5984 4.03435 13.4481C4.17427 12.9821 4.3614 12.4396 4.6015 11.7926L5.15467 10.3632C5.1536 10.287 5.15292 10.2154 5.15292 10.1604C5.15292 5.6047 7.58875 2.00038 12.0013 2C16.4138 2.00038 18.8494 5.60454 18.8494 10.1604C18.8494 10.2154 18.8487 10.2869 18.8477 10.3631L19.401 11.7923L19.4112 11.8191C19.636 12.4254 19.8242 12.9722 19.967 13.445C20.3145 14.5956 20.4889 15.5735 20.5018 16.361C20.5085 16.768 20.4728 17.1365 20.3837 17.4689C20.3178 17.7148 20.2228 17.9483 20.0746 18.1768C19.7976 18.6041 19.3905 18.9532 18.7974 19.0862C18.9266 19.6411 18.8523 20.2274 18.5574 20.7407Z'
|
|
59
|
+
|
|
60
|
+
/** Temporarily replaces DSH's fallback settings glyph for the QQ Chat row. */
|
|
61
|
+
export function installQQSettingsIcon(): () => void {
|
|
62
|
+
const originals = new Map<HTMLButtonElement, SVGElement>()
|
|
63
|
+
const apply = () => {
|
|
64
|
+
for (const button of Array.from(document.querySelectorAll('button'))) {
|
|
65
|
+
const label = button.querySelector('span[class$="_navLabel"]')
|
|
66
|
+
if (label?.textContent?.trim() !== 'QQ Chat') continue
|
|
67
|
+
if (button.querySelector('[data-dsh-qqchat-icon]')) continue
|
|
68
|
+
const icon = button.querySelector('svg[class$="_navIcon"]')
|
|
69
|
+
if (!icon) continue
|
|
70
|
+
originals.set(button, icon.cloneNode(true) as SVGElement)
|
|
71
|
+
icon.remove()
|
|
72
|
+
const replacement = document.createElementNS('http://www.w3.org/2000/svg', 'svg')
|
|
73
|
+
replacement.setAttribute('width', '16')
|
|
74
|
+
replacement.setAttribute('height', '16')
|
|
75
|
+
replacement.setAttribute('viewBox', '0 0 24 24')
|
|
76
|
+
replacement.setAttribute('fill', 'currentColor')
|
|
77
|
+
replacement.setAttribute('aria-hidden', 'true')
|
|
78
|
+
replacement.setAttribute('data-dsh-qqchat-icon', 'true')
|
|
79
|
+
replacement.classList.add('dsh-qqchat-nav-icon')
|
|
80
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path')
|
|
81
|
+
path.setAttribute('d', QQ_ICON_PATH)
|
|
82
|
+
replacement.append(path)
|
|
83
|
+
button.insertBefore(replacement, label)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
const observer = new MutationObserver(apply)
|
|
87
|
+
observer.observe(document.body, { childList: true, subtree: true })
|
|
88
|
+
apply()
|
|
89
|
+
return () => {
|
|
90
|
+
observer.disconnect()
|
|
91
|
+
for (const [button, original] of originals) {
|
|
92
|
+
const replacement = button.querySelector('[data-dsh-qqchat-icon]')
|
|
93
|
+
if (replacement) replacement.replaceWith(original)
|
|
94
|
+
}
|
|
95
|
+
originals.clear()
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
58
99
|
const css = `
|
|
59
100
|
.qqs{display:flex;flex-direction:column;gap:18px;color:var(--dsw-alias-label-primary);font:inherit}.qqs *{box-sizing:border-box}
|
|
60
101
|
.qqsHead{display:flex;align-items:center;justify-content:space-between;gap:12px}.qqsBrand{display:flex;align-items:center;gap:10px}.qqsLogo{width:34px;height:34px;border-radius:10px;display:grid;place-items:center;background:var(--dsw-alias-bg-layer-2);border:1px solid var(--dsw-alias-border-l1);font-weight:700}.qqsTitle{font-size:15px;font-weight:620}.qqMuted{font-size:12px;color:var(--dsw-alias-label-secondary);line-height:1.55}.qqStatus{display:flex;align-items:center;gap:7px;font-size:12px;color:var(--dsw-alias-label-secondary)}.qqDot{width:7px;height:7px;border-radius:50%;background:var(--dsw-alias-state-warn-primary)}.qqDot.online{background:var(--dsw-alias-state-success-primary)}
|
|
@@ -77,7 +118,7 @@ const css = `
|
|
|
77
118
|
.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}
|
|
78
119
|
`.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)}')
|
|
79
120
|
|
|
80
|
-
const qqThemeOverrides = '.qqTranscript.out .qqBubble{background:var(--dsw-
|
|
121
|
+
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)}'
|
|
81
122
|
|
|
82
123
|
export function installStyles(): () => void {
|
|
83
124
|
if (typeof document === 'undefined' || document.querySelector(`style[data-plugin-css="${STYLE_ID}"]`)) return () => {}
|
package/docs/MEMORY.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
## 易读概述
|
|
4
4
|
|
|
5
|
-
QQChat 的 SQLite 是 QQ 世界事实来源,DSH Session 只保存 Agent 实际经历过的 turn 和展示事件。记忆反思从 SQLite 读取消息,再写回 group/member scope。
|
|
5
|
+
QQChat 的 SQLite 是 QQ 世界事实来源,DSH Session 只保存 Agent 实际经历过的 turn 和展示事件。记忆反思从 SQLite 读取消息,再写回 group/member scope。群记忆和群友批量记忆的后续调整见 [PRD-MEM-1](prds/PRD-MEM-1-群记忆与群友记忆策略调整.md)。
|
|
6
6
|
|
|
7
7
|
身份主键始终是 stable sender ID;昵称、群内称呼和历史昵称是展示或画像信息,不能替代身份主键。
|
|
8
8
|
|
|
@@ -45,12 +45,28 @@ QQChat 的 SQLite 是 QQ 世界事实来源,DSH Session 只保存 Agent 实际
|
|
|
45
45
|
|
|
46
46
|
默认配置:
|
|
47
47
|
|
|
48
|
-
-
|
|
49
|
-
-
|
|
48
|
+
- 群聊连续空闲 15 分钟后触发一次群反思
|
|
49
|
+
- 群反思不因固定消息条数立即触发;成员批量反思达到 50 条群消息时仍可提前触发
|
|
50
50
|
- 单次最多取 80 条未反思消息
|
|
51
51
|
|
|
52
|
+
当前已拆分群记忆与成员记忆:群反思只写 group scope;群友批量反思使用独立的成员游标,累计 50 条已落库群消息或空闲时触发,一次处理本批真实出现的多个成员。私聊反思使用独立的 `private-owner` 任务,并保留用户/Bot 消息方向。任务、游标和 snapshot 集成仍在持续完善。
|
|
53
|
+
|
|
54
|
+
成员批量反思可以通过运行时设置 `memoryMemberBatchEnabled` 灰度关闭;关闭只停止
|
|
55
|
+
新的群友批量调度,不删除已有成员记忆,也不影响群反思和私聊记忆。
|
|
56
|
+
|
|
52
57
|
群聊和私聊分别维护反思游标和模型路由。私聊首次没有成功 Agent 路由时不能凭空创建反思请求;需要先完成一次正常 Agent 回复。
|
|
53
58
|
|
|
59
|
+
反思任务统一使用以下 scope/task 契约:
|
|
60
|
+
|
|
61
|
+
```text
|
|
62
|
+
scope: group | member | private
|
|
63
|
+
task: group | member-batch | private-owner
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
群反思和群友批量反思共享群消息,但使用两条独立游标;私聊任务使用 member scope
|
|
67
|
+
和私聊自己的消息范围。旧数据库如果缺少成员批量游标或任务表,启动迁移会补齐
|
|
68
|
+
默认值,不会删除已有记忆或跳过未处理消息。
|
|
69
|
+
|
|
54
70
|
## 压缩策略
|
|
55
71
|
|
|
56
72
|
记忆如何进入 DSH Session 的生命周期、TTL 和 Session compact 处理,见[记忆系统上下文注入方案](./MEMORY_CONTEXT.md)。
|
|
@@ -60,7 +76,7 @@ QQChat 的 SQLite 是 QQ 世界事实来源,DSH Session 只保存 Agent 实际
|
|
|
60
76
|
| Scope | 触发条数 | 保留最近 | 压缩到 |
|
|
61
77
|
| --- | ---: | ---: | --- |
|
|
62
78
|
| member | 100 | 50 | `memory` |
|
|
63
|
-
| group |
|
|
79
|
+
| group | 200 | 100 | `memory` |
|
|
64
80
|
|
|
65
81
|
压缩模型最多使用 `memoryCompressionMaxTokens=15000`。压缩输入包含已有 memory、相关 profile/pattern 和待压缩 daily。
|
|
66
82
|
|
|
@@ -77,9 +93,21 @@ QQChat 的 SQLite 是 QQ 世界事实来源,DSH Session 只保存 Agent 实际
|
|
|
77
93
|
`@deepseek-ai/dsh-system-prompt` 来源标记。注入策略遵循“新 Session、TTL
|
|
78
94
|
过期、Session compact 后刷新”的生命周期规则,连续对话只刷新对应群或当前
|
|
79
95
|
成员的 TTL,不重复注入完整记忆。群持续活跃不会替潜水成员刷新成员 TTL。
|
|
96
|
+
每个记忆 scope 的直注入预算为 2000 个字符,daily 优先保留最新内容;超出
|
|
97
|
+
部分仍保存在 SQLite,不会因上下文裁剪而删除。
|
|
80
98
|
具体状态、hash、版本和失败处理见[记忆系统上下文注入方案](./MEMORY_CONTEXT.md)。
|
|
81
99
|
当前 QQ 消息仍作为正常 user prompt 进入 turn。
|
|
82
100
|
|
|
101
|
+
### 反思提示词策略
|
|
102
|
+
|
|
103
|
+
三类反思提示词分别对应不同的记忆主体,不共享一套模糊的“总结聊天”指令:
|
|
104
|
+
|
|
105
|
+
- **群组反思**只维护当前群的公开事实、规则、协作约定、群项目和群状态;不写成员个人资料,也不把昵称、引用、转述或推测当成群事实。
|
|
106
|
+
- **群友批量反思**先判断语义主体,再按真实 `senderId` 分配成员记忆;只允许本批实际发言且有明确个人事实的成员进入输出,群规、群项目和第三方属性留在群 scope。
|
|
107
|
+
- **私聊反思**使用消息 `direction` 作为事实边界:只有用户入站消息可以产生用户画像,BOT 出站消息只能作为上下文,不能反向证明用户的身份、偏好或经历。
|
|
108
|
+
|
|
109
|
+
三类提示词都要求增量输出、保留已有事实、使用绝对日期,并在主体不明确时宁可漏记;`profile`、`pattern`、`summary`、`daily` 和 `memory` 的字段职责分别受证据门槛约束。具体提示词实现位于 `src/storage/memory.ts`,压缩提示词则单独负责 `daily → memory` 沉淀,不参与成员主体判断。
|
|
110
|
+
|
|
83
111
|
群聊 Agent turn 注入:
|
|
84
112
|
|
|
85
113
|
```text
|
package/docs/MEMORY_CONTEXT.md
CHANGED
|
@@ -16,6 +16,22 @@ QQChat 的记忆属于 DSH Session 上下文的一部分,不脱离 Session 单
|
|
|
16
16
|
|
|
17
17
|
## 2. 记忆快照的范围
|
|
18
18
|
|
|
19
|
+
### 2.1 直注入预算
|
|
20
|
+
|
|
21
|
+
记忆快照遵循与咕咕一致的按 scope 限制策略,避免 profile、daily 或长期
|
|
22
|
+
memory 无限增长后挤占 DSH 对话上下文:
|
|
23
|
+
|
|
24
|
+
- 群级记忆字段合计最多直注入 2000 个字符;
|
|
25
|
+
- 当前发言成员的记忆字段合计最多直注入 2000 个字符;
|
|
26
|
+
- 私聊成员记忆字段合计最多直注入 2000 个字符;
|
|
27
|
+
- 近期沉淀优先保留末尾的最新内容;其他字段从开头保留;
|
|
28
|
+
- 被裁剪的内容仍保存在 SQLite 中,不会因为注入预算被删除;
|
|
29
|
+
- 预算只限制本轮 snapshot 文本,不改变 DSH Session 的历史或 compact 规则。
|
|
30
|
+
|
|
31
|
+
这里的 2000 是稳定、可预测的字符预算,不是模型输出 `maxTokens`。反思和
|
|
32
|
+
`daily → memory` 压缩仍分别使用 `memoryMaxTokens` 与
|
|
33
|
+
`memoryCompressionMaxTokens` 控制输出长度。
|
|
34
|
+
|
|
19
35
|
快照按照 Session 的聊天范围生成:
|
|
20
36
|
|
|
21
37
|
### 群聊 Session
|
package/docs/README.md
CHANGED
|
@@ -12,6 +12,8 @@ QQChat 是 DSH 的 out-of-tree QQ 官方 Bot 插件。文档按“如何开发
|
|
|
12
12
|
| [DEVELOPMENT.md](DEVELOPMENT.md) | 安装、构建、测试、启动和 UI 验证 |
|
|
13
13
|
| [MEMORY.md](MEMORY.md) | group/member scope、daily、memory、反思和压缩 |
|
|
14
14
|
| [MEMORY_CONTEXT.md](MEMORY_CONTEXT.md) | DSH Session 内的记忆快照、TTL、compact 刷新和注入生命周期 |
|
|
15
|
+
| [prds/README.md](prds/README.md) | PRD 撰写规范和产品需求文档导航 |
|
|
16
|
+
| [prds/PRD-MEM-1-群记忆与群友记忆策略调整.md](prds/PRD-MEM-1-群记忆与群友记忆策略调整.md) | 基于 Gugu 策略的群记忆、群友批量记忆和私聊记忆调整计划 |
|
|
15
17
|
| [MEDIA_AND_QUOTES.md](MEDIA_AND_QUOTES.md) | 媒体输入、引用消息、附件存储、安全和分阶段实现方案 |
|
|
16
18
|
| [QUOTES.md](QUOTES.md) | QQ 引用索引、引用回填、附件复用和 Web 展示实施方案 |
|
|
17
19
|
| [STREAMING.md](STREAMING.md) | QQ 私聊官方流式传输、队列、降级和故障复盘 |
|
|
@@ -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)
|