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
|
@@ -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
|
@@ -10,6 +10,7 @@ exports.inject = ['slots', 'connection', 'conversationEvents'];
|
|
|
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
|
+
ctx.effect(shared_cjs_1.installQQSettingsIcon, 'dsh-qqchat: QQ settings icon');
|
|
13
14
|
ctx.conversationEvents.register((0, workspace_cjs_1.createQQMessageDefinition)());
|
|
14
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));
|
|
15
16
|
ctx.slots.inject('conversation.chat.node', () => ctx.slots.register({ name: 'conversation.chat.node', key: 'qqchat-message' }, workspace_cjs_1.QQTranscriptNode));
|
|
@@ -39,6 +40,7 @@ function QQSettings({ rpc }) {
|
|
|
39
40
|
const [groupReplyMenuOpen, setGroupReplyMenuOpen] = (0, shared_cjs_1.useState)(false);
|
|
40
41
|
const [directReplyMenuOpen, setDirectReplyMenuOpen] = (0, shared_cjs_1.useState)(false);
|
|
41
42
|
const [ownerMenuOpen, setOwnerMenuOpen] = (0, shared_cjs_1.useState)(false);
|
|
43
|
+
const [receiveHelpOpen, setReceiveHelpOpen] = (0, shared_cjs_1.useState)(false);
|
|
42
44
|
const refresh = (0, shared_cjs_1.useCallback)(async () => {
|
|
43
45
|
try {
|
|
44
46
|
const [status, prefs] = await Promise.all([
|
|
@@ -217,7 +219,23 @@ function QQSettings({ rpc }) {
|
|
|
217
219
|
variant: settings.groupReceiveMode === mode ? 'primary' : 'outline',
|
|
218
220
|
onClick: () => void patch({ groupReceiveMode: mode }),
|
|
219
221
|
}, mode === 'auto' ? '自动回应' : mode === 'mention' ? '@回复' : '静默记录')));
|
|
220
|
-
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', {
|
|
221
239
|
type: 'checkbox',
|
|
222
240
|
checked: settings.directStreamingEnabled && settings.directReplyFormat !== 'compat',
|
|
223
241
|
disabled: settings.directReplyFormat === 'compat',
|
|
@@ -249,6 +267,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
249
267
|
exports.dateTime = exports.time = exports.initials = exports.short = exports.CHANNEL = exports.h = exports.useSyncExternalStore = exports.useState = exports.useMemo = exports.useEffect = exports.useCallback = exports.Menu = exports.Input = exports.Button = exports.React = void 0;
|
|
250
268
|
exports.setQQRpc = setQQRpc;
|
|
251
269
|
exports.getQQRpc = getQQRpc;
|
|
270
|
+
exports.installQQSettingsIcon = installQQSettingsIcon;
|
|
252
271
|
exports.installStyles = installStyles;
|
|
253
272
|
exports.call = call;
|
|
254
273
|
exports.Modal = Modal;
|
|
@@ -265,6 +284,49 @@ function setQQRpc(rpc) { qqRpc = rpc; }
|
|
|
265
284
|
function getQQRpc() { return qqRpc; }
|
|
266
285
|
exports.CHANNEL = '/qqchat';
|
|
267
286
|
const STYLE_ID = 'dsh-qqchat/client-ui-v2';
|
|
287
|
+
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';
|
|
288
|
+
/** Temporarily replaces DSH's fallback settings glyph for the QQ Chat row. */
|
|
289
|
+
function installQQSettingsIcon() {
|
|
290
|
+
const originals = new Map();
|
|
291
|
+
const apply = () => {
|
|
292
|
+
for (const button of Array.from(document.querySelectorAll('button'))) {
|
|
293
|
+
const label = button.querySelector('span[class$="_navLabel"]');
|
|
294
|
+
if (label?.textContent?.trim() !== 'QQ Chat')
|
|
295
|
+
continue;
|
|
296
|
+
if (button.querySelector('[data-dsh-qqchat-icon]'))
|
|
297
|
+
continue;
|
|
298
|
+
const icon = button.querySelector('svg[class$="_navIcon"]');
|
|
299
|
+
if (!icon)
|
|
300
|
+
continue;
|
|
301
|
+
originals.set(button, icon.cloneNode(true));
|
|
302
|
+
icon.remove();
|
|
303
|
+
const replacement = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
|
|
304
|
+
replacement.setAttribute('width', '16');
|
|
305
|
+
replacement.setAttribute('height', '16');
|
|
306
|
+
replacement.setAttribute('viewBox', '0 0 24 24');
|
|
307
|
+
replacement.setAttribute('fill', 'currentColor');
|
|
308
|
+
replacement.setAttribute('aria-hidden', 'true');
|
|
309
|
+
replacement.setAttribute('data-dsh-qqchat-icon', 'true');
|
|
310
|
+
replacement.classList.add('dsh-qqchat-nav-icon');
|
|
311
|
+
const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
|
|
312
|
+
path.setAttribute('d', QQ_ICON_PATH);
|
|
313
|
+
replacement.append(path);
|
|
314
|
+
button.insertBefore(replacement, label);
|
|
315
|
+
}
|
|
316
|
+
};
|
|
317
|
+
const observer = new MutationObserver(apply);
|
|
318
|
+
observer.observe(document.body, { childList: true, subtree: true });
|
|
319
|
+
apply();
|
|
320
|
+
return () => {
|
|
321
|
+
observer.disconnect();
|
|
322
|
+
for (const [button, original] of originals) {
|
|
323
|
+
const replacement = button.querySelector('[data-dsh-qqchat-icon]');
|
|
324
|
+
if (replacement)
|
|
325
|
+
replacement.replaceWith(original);
|
|
326
|
+
}
|
|
327
|
+
originals.clear();
|
|
328
|
+
};
|
|
329
|
+
}
|
|
268
330
|
const css = `
|
|
269
331
|
.qqs{display:flex;flex-direction:column;gap:18px;color:var(--dsw-alias-label-primary);font:inherit}.qqs *{box-sizing:border-box}
|
|
270
332
|
.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)}
|
|
@@ -286,7 +348,7 @@ const css = `
|
|
|
286
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)}
|
|
287
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}
|
|
288
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)}');
|
|
289
|
-
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)}';
|
|
290
352
|
function installStyles() {
|
|
291
353
|
if (typeof document === 'undefined' || document.querySelector(`style[data-plugin-css="${STYLE_ID}"]`))
|
|
292
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.js
CHANGED
|
@@ -31,7 +31,9 @@ export function apply(ctx, inputConfig = {}) {
|
|
|
31
31
|
const runtime = new QQChatRuntime(ctx, db, api, auth, memory, bridge, config, logger, media);
|
|
32
32
|
ctx.connection.rpc.handle('/qqchat', createQQChatRpc(runtime), { authority: 'loopback' });
|
|
33
33
|
ctx.effect(() => {
|
|
34
|
-
void
|
|
34
|
+
void (async () => {
|
|
35
|
+
await runtime.start();
|
|
36
|
+
})().catch(error => logger.error?.(error));
|
|
35
37
|
return () => runtime.stop();
|
|
36
38
|
}, 'dsh-qqchat runtime');
|
|
37
39
|
}
|
|
@@ -16,9 +16,13 @@ export function restoreMemorySnapshotState(events, sourcePlugin) {
|
|
|
16
16
|
const event = events[index];
|
|
17
17
|
if (!event)
|
|
18
18
|
continue;
|
|
19
|
-
|
|
19
|
+
const source = event.data?.source;
|
|
20
|
+
if (event.type !== 'user/message' || source?.kind !== 'plugin' || source.plugin !== sourcePlugin)
|
|
20
21
|
continue;
|
|
21
|
-
|
|
22
|
+
if (!source.sections?.some(section => section.name === 'qq-chat-context'))
|
|
23
|
+
continue;
|
|
24
|
+
const content = event.data?.content;
|
|
25
|
+
const block = content?.length === 1 ? content[0] : undefined;
|
|
22
26
|
if (block?.type !== 'text' || typeof block.text !== 'string')
|
|
23
27
|
continue;
|
|
24
28
|
return { hash: memorySnapshotHash(block.text), lastInjectedAt: Number(event.time) || 0, stale: lastCompactionAt > (Number(event.time) || 0) };
|
package/lib/session/runtime.js
CHANGED
|
@@ -59,6 +59,8 @@ export class QQChatRuntime {
|
|
|
59
59
|
updateSettings(patch) {
|
|
60
60
|
if (patch.memoryEnabled !== undefined)
|
|
61
61
|
this.db.setSetting('memoryEnabled', Boolean(patch.memoryEnabled));
|
|
62
|
+
if (patch.memoryMemberBatchEnabled !== undefined)
|
|
63
|
+
this.db.setSetting('memoryMemberBatchEnabled', Boolean(patch.memoryMemberBatchEnabled));
|
|
62
64
|
if (patch.groupReceiveMode !== undefined) {
|
|
63
65
|
if (!isGroupReceiveMode(patch.groupReceiveMode))
|
|
64
66
|
throw new Error('无效的群聊接收模式');
|
|
@@ -131,8 +133,11 @@ export class QQChatRuntime {
|
|
|
131
133
|
content: text, quotedText: '', mentioned: false, createdAt: Date.now(),
|
|
132
134
|
}, row, true);
|
|
133
135
|
if (this.settings().memoryEnabled) {
|
|
134
|
-
if (chatType === 'group')
|
|
136
|
+
if (chatType === 'group') {
|
|
135
137
|
this.memory.schedule(Number(row.id));
|
|
138
|
+
if (settings.memoryMemberBatchEnabled)
|
|
139
|
+
this.memory.scheduleGroupMembers(Number(row.id));
|
|
140
|
+
}
|
|
136
141
|
else
|
|
137
142
|
this.memory.scheduleMember(Number(row.id));
|
|
138
143
|
}
|
|
@@ -261,10 +266,11 @@ export class QQChatRuntime {
|
|
|
261
266
|
quotedText: message.quotedText, mentioned: message.mentioned, createdAt: Date.now(), attachments: publicAttachments(ownAttachments), quote: safeQuote,
|
|
262
267
|
};
|
|
263
268
|
if (settings.memoryEnabled) {
|
|
264
|
-
if (group)
|
|
269
|
+
if (group) {
|
|
265
270
|
this.memory.schedule(Number(group.id));
|
|
266
|
-
|
|
267
|
-
|
|
271
|
+
if (settings.memoryMemberBatchEnabled)
|
|
272
|
+
this.memory.scheduleGroupMembers(Number(group.id));
|
|
273
|
+
}
|
|
268
274
|
}
|
|
269
275
|
if (!shouldReply) {
|
|
270
276
|
await this.bridge.recordTranscript(displayEvent, row, true);
|
|
@@ -303,8 +309,11 @@ export class QQChatRuntime {
|
|
|
303
309
|
memberId: message.chatType === 'c2c' ? member.id : undefined, direction: 'outbound', content: reply, mentioned: false,
|
|
304
310
|
});
|
|
305
311
|
if (settings.memoryEnabled) {
|
|
306
|
-
if (group)
|
|
312
|
+
if (group) {
|
|
307
313
|
this.memory.schedule(Number(group.id));
|
|
314
|
+
if (settings.memoryMemberBatchEnabled)
|
|
315
|
+
this.memory.scheduleGroupMembers(Number(group.id));
|
|
316
|
+
}
|
|
308
317
|
else
|
|
309
318
|
this.memory.scheduleMember(Number(member.id));
|
|
310
319
|
}
|
package/lib/storage/db.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AccountRow, AuthTaskRow, ChatType, DirectChatListRow, DailyEntry, GroupDefaults, GroupListRow, GroupMemberRow, GroupPatch, GroupRow, InsertMessageInput, MemberRow, MemoryDocuments, MemoryDocType, MemoryScopeType, MessageRow, OutboxRow, PublicAccountRow, QQChatRuntimeSettings, QQMediaKind, QQQuoteIndexInput, QQQuoteIndexRow, StoredAttachmentSummary } from '../types.js';
|
|
1
|
+
import type { AccountRow, AuthTaskRow, ChatType, DirectChatListRow, DailyEntry, GroupDefaults, GroupListRow, GroupMemberRow, GroupPatch, GroupRow, InsertMessageInput, MemberRow, MemoryDocuments, MemoryDocType, MemoryScopeType, MessageRow, OutboxRow, PublicAccountRow, QQChatRuntimeSettings, QQMediaKind, QQQuoteIndexInput, QQQuoteIndexRow, StoredAttachmentSummary, ReflectionTaskInput } from '../types.js';
|
|
2
2
|
export declare class QQChatDatabase {
|
|
3
3
|
readonly path: string;
|
|
4
4
|
private readonly db;
|
|
@@ -67,7 +67,12 @@ export declare class QQChatDatabase {
|
|
|
67
67
|
recentGroupMessages(groupId: number, limit?: number): MessageRow[];
|
|
68
68
|
unreflectedMessages(groupId: number, limit?: number): MessageRow[];
|
|
69
69
|
unreflectedCount(groupId: number): number;
|
|
70
|
+
unreflectedMemberMessages(groupId: number, limit?: number): MessageRow[];
|
|
71
|
+
unreflectedMemberCount(groupId: number): number;
|
|
70
72
|
markReflected(groupId: number, messageId: number): void;
|
|
73
|
+
markMembersReflected(groupId: number, messageId: number): void;
|
|
74
|
+
startReflectionTask(input: ReflectionTaskInput): boolean;
|
|
75
|
+
finishReflectionTask(idempotencyKey: string, status: 'completed' | 'failed'): void;
|
|
71
76
|
memoryDocs(scopeType: MemoryScopeType, scopeKey: number): MemoryDocuments;
|
|
72
77
|
setMemoryDoc(scopeType: MemoryScopeType, scopeKey: number, docType: MemoryDocType, content: string): void;
|
|
73
78
|
appendMemoryDoc(scopeType: MemoryScopeType, scopeKey: number, docType: MemoryDocType, content: string, maxChars?: number): void;
|
package/lib/storage/db.js
CHANGED
|
@@ -2,6 +2,7 @@ import { DatabaseSync } from 'node:sqlite';
|
|
|
2
2
|
import { chmodSync, mkdirSync } from 'node:fs';
|
|
3
3
|
import { dirname } from 'node:path';
|
|
4
4
|
const now = () => Date.now();
|
|
5
|
+
const REFLECTION_TASK_STALE_MS = 15 * 60 * 1000;
|
|
5
6
|
function parseStringList(value) {
|
|
6
7
|
if (!value)
|
|
7
8
|
return [];
|
|
@@ -209,8 +210,23 @@ export class QQChatDatabase {
|
|
|
209
210
|
CREATE TABLE IF NOT EXISTS reflection_state (
|
|
210
211
|
group_id INTEGER PRIMARY KEY REFERENCES groups(id) ON DELETE CASCADE,
|
|
211
212
|
last_message_id INTEGER NOT NULL DEFAULT 0,
|
|
213
|
+
last_member_reflected_message_id INTEGER NOT NULL DEFAULT 0,
|
|
212
214
|
last_reflected_at INTEGER
|
|
213
215
|
);
|
|
216
|
+
CREATE TABLE IF NOT EXISTS reflection_tasks (
|
|
217
|
+
id INTEGER PRIMARY KEY,
|
|
218
|
+
scope_type TEXT NOT NULL CHECK(scope_type IN ('group','member','private')),
|
|
219
|
+
scope_key INTEGER NOT NULL,
|
|
220
|
+
task_type TEXT NOT NULL CHECK(task_type IN ('group','member-batch','private-owner')),
|
|
221
|
+
start_message_id INTEGER NOT NULL,
|
|
222
|
+
end_message_id INTEGER NOT NULL,
|
|
223
|
+
status TEXT NOT NULL CHECK(status IN ('running','completed','failed')),
|
|
224
|
+
attempts INTEGER NOT NULL DEFAULT 0,
|
|
225
|
+
idempotency_key TEXT NOT NULL UNIQUE,
|
|
226
|
+
created_at INTEGER NOT NULL,
|
|
227
|
+
updated_at INTEGER NOT NULL
|
|
228
|
+
);
|
|
229
|
+
CREATE INDEX IF NOT EXISTS reflection_tasks_scope ON reflection_tasks(scope_type, scope_key, task_type, status);
|
|
214
230
|
CREATE TABLE IF NOT EXISTS outbox (
|
|
215
231
|
id INTEGER PRIMARY KEY,
|
|
216
232
|
account_id INTEGER NOT NULL REFERENCES accounts(id) ON DELETE CASCADE,
|
|
@@ -223,6 +239,8 @@ export class QQChatDatabase {
|
|
|
223
239
|
error TEXT
|
|
224
240
|
);
|
|
225
241
|
`);
|
|
242
|
+
this.db.prepare("UPDATE reflection_tasks SET status='failed',updated_at=? WHERE status='running' AND updated_at<?")
|
|
243
|
+
.run(now(), now() - REFLECTION_TASK_STALE_MS);
|
|
226
244
|
const columns = new Set(many(this.db.prepare('PRAGMA table_info(group_members)').all()).map(column => column.name));
|
|
227
245
|
const groupColumns = new Set(many(this.db.prepare('PRAGMA table_info(groups)').all()).map(column => column.name));
|
|
228
246
|
if (groupColumns.has('requires_at'))
|
|
@@ -234,6 +252,10 @@ export class QQChatDatabase {
|
|
|
234
252
|
if (!columns.has('message_count'))
|
|
235
253
|
this.db.exec('ALTER TABLE group_members ADD COLUMN message_count INTEGER NOT NULL DEFAULT 0');
|
|
236
254
|
const messageColumns = new Set(many(this.db.prepare('PRAGMA table_info(messages)').all()).map(column => column.name));
|
|
255
|
+
const reflectionColumns = new Set(many(this.db.prepare('PRAGMA table_info(reflection_state)').all()).map(column => column.name));
|
|
256
|
+
if (!reflectionColumns.has('last_member_reflected_message_id')) {
|
|
257
|
+
this.db.exec('ALTER TABLE reflection_state ADD COLUMN last_member_reflected_message_id INTEGER NOT NULL DEFAULT 0');
|
|
258
|
+
}
|
|
237
259
|
if (!messageColumns.has('attachments_json'))
|
|
238
260
|
this.db.exec('ALTER TABLE messages ADD COLUMN attachments_json TEXT');
|
|
239
261
|
if (!messageColumns.has('quote_json'))
|
|
@@ -342,6 +364,7 @@ export class QQChatDatabase {
|
|
|
342
364
|
runtimeSettings(defaults) {
|
|
343
365
|
return {
|
|
344
366
|
memoryEnabled: this.getSetting('memoryEnabled', defaults.memoryEnabled),
|
|
367
|
+
memoryMemberBatchEnabled: this.getSetting('memoryMemberBatchEnabled', defaults.memoryMemberBatchEnabled),
|
|
345
368
|
groupReceiveMode: this.getSetting('groupReceiveMode', defaults.groupReceiveMode),
|
|
346
369
|
groupReplyFormat: this.getSetting('groupReplyFormat', defaults.groupReplyFormat),
|
|
347
370
|
directReplyFormat: this.getSetting('directReplyFormat', this.getSetting('replyFormat', defaults.directReplyFormat)),
|
|
@@ -611,11 +634,45 @@ export class QQChatDatabase {
|
|
|
611
634
|
const row = one(this.db.prepare('SELECT COUNT(*) AS n FROM messages WHERE group_id=? AND id>?').get(groupId, Number(state?.last_message_id || 0)));
|
|
612
635
|
return Number(row?.n || 0);
|
|
613
636
|
}
|
|
637
|
+
unreflectedMemberMessages(groupId, limit = 80) {
|
|
638
|
+
const state = one(this.db.prepare('SELECT last_member_reflected_message_id FROM reflection_state WHERE group_id=?').get(groupId));
|
|
639
|
+
const after = Number(state?.last_member_reflected_message_id || 0);
|
|
640
|
+
return many(this.db.prepare(`SELECT m.*,mem.platform_user_id,mem.display_name
|
|
641
|
+
FROM messages m LEFT JOIN members mem ON mem.id=m.member_id
|
|
642
|
+
WHERE m.group_id=? AND m.id>? ORDER BY m.id ASC LIMIT ?`).all(groupId, after, limit));
|
|
643
|
+
}
|
|
644
|
+
unreflectedMemberCount(groupId) {
|
|
645
|
+
const state = one(this.db.prepare('SELECT last_member_reflected_message_id FROM reflection_state WHERE group_id=?').get(groupId));
|
|
646
|
+
const row = one(this.db.prepare('SELECT COUNT(*) AS n FROM messages WHERE group_id=? AND id>?').get(groupId, Number(state?.last_member_reflected_message_id || 0)));
|
|
647
|
+
return Number(row?.n || 0);
|
|
648
|
+
}
|
|
614
649
|
markReflected(groupId, messageId) {
|
|
615
650
|
this.db.prepare(`INSERT INTO reflection_state(group_id,last_message_id,last_reflected_at) VALUES(?,?,?)
|
|
616
651
|
ON CONFLICT(group_id) DO UPDATE SET last_message_id=excluded.last_message_id,last_reflected_at=excluded.last_reflected_at`)
|
|
617
652
|
.run(groupId, messageId, now());
|
|
618
653
|
}
|
|
654
|
+
markMembersReflected(groupId, messageId) {
|
|
655
|
+
this.db.prepare(`INSERT INTO reflection_state(group_id,last_message_id,last_member_reflected_message_id,last_reflected_at) VALUES(?,?,?,?)
|
|
656
|
+
ON CONFLICT(group_id) DO UPDATE SET last_member_reflected_message_id=excluded.last_member_reflected_message_id,last_reflected_at=excluded.last_reflected_at`)
|
|
657
|
+
.run(groupId, 0, messageId, now());
|
|
658
|
+
}
|
|
659
|
+
startReflectionTask(input) {
|
|
660
|
+
const existing = one(this.db.prepare('SELECT id,status FROM reflection_tasks WHERE idempotency_key=?').get(input.idempotencyKey));
|
|
661
|
+
if (existing?.status === 'completed' || existing?.status === 'running')
|
|
662
|
+
return false;
|
|
663
|
+
if (existing) {
|
|
664
|
+
this.db.prepare(`UPDATE reflection_tasks SET status='running',attempts=attempts+1,updated_at=? WHERE id=?`)
|
|
665
|
+
.run(now(), existing.id);
|
|
666
|
+
return true;
|
|
667
|
+
}
|
|
668
|
+
this.db.prepare(`INSERT INTO reflection_tasks(scope_type,scope_key,task_type,start_message_id,end_message_id,status,attempts,idempotency_key,created_at,updated_at)
|
|
669
|
+
VALUES(?,?,?,?,?,'running',1,?,?,?)`)
|
|
670
|
+
.run(input.scopeType, input.scopeKey, input.taskType, input.startMessageId, input.endMessageId, input.idempotencyKey, now(), now());
|
|
671
|
+
return true;
|
|
672
|
+
}
|
|
673
|
+
finishReflectionTask(idempotencyKey, status) {
|
|
674
|
+
this.db.prepare('UPDATE reflection_tasks SET status=?,updated_at=? WHERE idempotency_key=?').run(status, now(), idempotencyKey);
|
|
675
|
+
}
|
|
619
676
|
memoryDocs(scopeType, scopeKey) {
|
|
620
677
|
const rows = many(this.db.prepare('SELECT doc_type,content,updated_at FROM memory_documents WHERE scope_type=? AND scope_key=?')
|
|
621
678
|
.all(scopeType, scopeKey));
|
package/lib/storage/memory.d.ts
CHANGED
|
@@ -1,13 +1,19 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis';
|
|
2
2
|
import type { QQChatDatabase } from './db.js';
|
|
3
|
-
import type { GroupRow, LoggerLike, MemoryDocuments, MemoryView, QQChatConfig, ReflectionPayload, QQNormalizedMessage } from '../types.js';
|
|
3
|
+
import type { GroupRow, LoggerLike, MemoryDocuments, MemoryView, QQChatConfig, ReflectionPayload, MemberBatchReflectionPayload, QQNormalizedMessage } from '../types.js';
|
|
4
4
|
interface MemberReflectionPayload {
|
|
5
|
+
profile_add?: unknown;
|
|
6
|
+
profile_remove?: unknown;
|
|
7
|
+
pattern_add?: unknown;
|
|
8
|
+
pattern_remove?: unknown;
|
|
5
9
|
profile?: unknown;
|
|
6
10
|
pattern?: unknown;
|
|
7
11
|
summary?: unknown;
|
|
8
12
|
memory?: unknown;
|
|
9
13
|
daily?: unknown;
|
|
10
14
|
}
|
|
15
|
+
/** 与咕咕一致:每个记忆 scope 的本轮直注入预算(按字符计)。 */
|
|
16
|
+
export declare const MEMORY_INJECT_CHARS = 2000;
|
|
11
17
|
export declare class MemoryEngine {
|
|
12
18
|
private readonly ctx;
|
|
13
19
|
private readonly db;
|
|
@@ -15,24 +21,38 @@ export declare class MemoryEngine {
|
|
|
15
21
|
private readonly logger;
|
|
16
22
|
private readonly timers;
|
|
17
23
|
private readonly memberTimers;
|
|
24
|
+
private readonly memberBatchTimers;
|
|
18
25
|
private readonly routes;
|
|
19
26
|
private readonly memberRoutes;
|
|
20
27
|
private readonly reflectingGroups;
|
|
21
28
|
private readonly reflectingMembers;
|
|
29
|
+
private readonly reflectingMemberBatches;
|
|
30
|
+
private readonly groupRetryAt;
|
|
31
|
+
private readonly memberRetryAt;
|
|
32
|
+
private readonly memberBatchRetryAt;
|
|
22
33
|
constructor(ctx: Context, db: QQChatDatabase, config: QQChatConfig, logger?: LoggerLike);
|
|
23
34
|
dispose(): void;
|
|
24
35
|
setRoute(groupId: number, provider: string, model: string, sessionId: string): void;
|
|
25
36
|
setMemberRoute(memberId: number, provider: string, model: string, sessionId: string): void;
|
|
26
37
|
schedule(groupId: number): void;
|
|
27
38
|
scheduleMember(memberId: number): void;
|
|
39
|
+
scheduleGroupMembers(groupId: number): void;
|
|
40
|
+
private inRetryCooldown;
|
|
41
|
+
private reflectionDelay;
|
|
42
|
+
private runScheduledGroupReflection;
|
|
43
|
+
private runScheduledMemberReflection;
|
|
44
|
+
private runScheduledMemberBatchReflection;
|
|
28
45
|
reflectNow(groupId: number): Promise<MemoryView>;
|
|
29
46
|
private reflectNowInternal;
|
|
47
|
+
reflectMembersNow(groupId: number): Promise<MemoryView>;
|
|
48
|
+
private reflectMembersNowInternal;
|
|
30
49
|
reflectMemberNow(memberId: number): Promise<MemoryDocuments>;
|
|
31
50
|
private reflectMemberNowInternal;
|
|
32
51
|
private compactDaily;
|
|
33
52
|
private generateReflection;
|
|
34
53
|
private unreflectedDirectMessages;
|
|
35
|
-
private
|
|
54
|
+
private applyGroupReflection;
|
|
55
|
+
private applyMemberBatchReflection;
|
|
36
56
|
contextForGroup(group: GroupRow, currentMember?: {
|
|
37
57
|
id: number;
|
|
38
58
|
platform_user_id: string;
|
|
@@ -46,5 +66,11 @@ export declare class MemoryEngine {
|
|
|
46
66
|
currentMessageText(message: QQNormalizedMessage): string;
|
|
47
67
|
memoryView(groupId: number): MemoryView | undefined;
|
|
48
68
|
}
|
|
69
|
+
export declare function memorySystemPrompt(): string;
|
|
70
|
+
export declare function memberBatchMemorySystemPrompt(): string;
|
|
71
|
+
export declare function privateMemorySystemPrompt(): string;
|
|
72
|
+
export declare function groupCompressionSystemPrompt(): string;
|
|
73
|
+
export declare function memberCompressionSystemPrompt(): string;
|
|
49
74
|
export declare function parseJsonObject(text: string): ReflectionPayload | MemberReflectionPayload;
|
|
75
|
+
export declare function validateMemberBatchPayload(value: unknown, allowedSenderIds: ReadonlySet<string>): value is MemberBatchReflectionPayload;
|
|
50
76
|
export {};
|