dsh-qqchat 0.2.4 → 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/client-src/settings.cts +18 -1
- package/client-src/shared.cts +2 -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 +19 -2
- package/lib/config.js +3 -2
- package/lib/index.js +0 -1
- package/lib/session/agent-bridge.d.ts +0 -4
- package/lib/session/agent-bridge.js +0 -35
- 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 +0 -1
- package/src/session/agent-bridge.ts +0 -35
- 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/lib/client.js
CHANGED
|
@@ -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.js
CHANGED
|
@@ -32,7 +32,6 @@ export function apply(ctx, inputConfig = {}) {
|
|
|
32
32
|
ctx.connection.rpc.handle('/qqchat', createQQChatRpc(runtime), { authority: 'loopback' });
|
|
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,7 +22,6 @@ 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;
|
|
27
26
|
private readonly disposeToolGate;
|
|
28
27
|
private readonly disposeCommands;
|
|
@@ -31,15 +30,12 @@ export declare class DshQQBridge {
|
|
|
31
30
|
constructor(ctx: Context, db: QQChatDatabase, memory: MemoryEngine, config: QQChatConfig, logger?: LoggerLike);
|
|
32
31
|
dispose(): Promise<void>;
|
|
33
32
|
ensureChatSession(chatType: ChatType, row: GroupRow | MemberRow): Promise<string>;
|
|
34
|
-
ensureWorkspaceMembership(): Promise<void>;
|
|
35
33
|
recordTranscript(event: QQChatDisplayEvent, row: GroupRow | MemberRow, createSession?: boolean): Promise<string | undefined>;
|
|
36
34
|
reply(message: QQNormalizedMessage, group: GroupRow | undefined, member: MemberRow, attachments?: StoredAttachmentSummary[], onTextDelta?: (delta: string) => void, isOwner?: boolean): Promise<string>;
|
|
37
35
|
private onSessionEvent;
|
|
38
36
|
private injectMemorySnapshot;
|
|
39
37
|
private restoreMemorySnapshot;
|
|
40
38
|
private ensureAgent;
|
|
41
|
-
/** Keep all QQ sessions together in one DSH Workspace using the host API. */
|
|
42
|
-
private attachToQQWorkspace;
|
|
43
39
|
private isArchived;
|
|
44
40
|
private rememberRoute;
|
|
45
41
|
private appendDisplayIfMissing;
|
|
@@ -33,7 +33,6 @@ export class DshQQBridge {
|
|
|
33
33
|
activeAttachments = new Map();
|
|
34
34
|
activeMediaReadable = new Map();
|
|
35
35
|
memorySnapshots = new Map();
|
|
36
|
-
qqWorkspace;
|
|
37
36
|
disposeEvent;
|
|
38
37
|
disposeToolGate;
|
|
39
38
|
disposeCommands;
|
|
@@ -98,16 +97,6 @@ export class DshQQBridge {
|
|
|
98
97
|
const { sessionId } = await this.ensureAgent(chatType, row);
|
|
99
98
|
return sessionId;
|
|
100
99
|
}
|
|
101
|
-
async ensureWorkspaceMembership() {
|
|
102
|
-
const sessionIds = [
|
|
103
|
-
...this.db.listGroups().map(row => row.dsh_session_id),
|
|
104
|
-
...this.db.listDirectChats().map(row => row.dsh_session_id),
|
|
105
|
-
];
|
|
106
|
-
for (const sessionId of sessionIds) {
|
|
107
|
-
if (sessionId)
|
|
108
|
-
await this.attachToQQWorkspace(sessionId);
|
|
109
|
-
}
|
|
110
|
-
}
|
|
111
100
|
async recordTranscript(event, row, createSession = false) {
|
|
112
101
|
const existing = this.db.getChatSession(event.chatType, Number(row.id));
|
|
113
102
|
if (!existing && !createSession)
|
|
@@ -285,7 +274,6 @@ export class DshQQBridge {
|
|
|
285
274
|
this.ensureSelection(live);
|
|
286
275
|
this.ensureTitle(live.session, chatType, row);
|
|
287
276
|
this.rememberRoute(chatType, row, live, sessionId);
|
|
288
|
-
await this.attachToQQWorkspace(sessionId);
|
|
289
277
|
return { agent: live, sessionId };
|
|
290
278
|
}
|
|
291
279
|
const resumed = await this.tryResume(sessionId);
|
|
@@ -293,7 +281,6 @@ export class DshQQBridge {
|
|
|
293
281
|
this.ensureSelection(resumed.agent);
|
|
294
282
|
this.ensureTitle(resumed.agent.session, chatType, row);
|
|
295
283
|
this.rememberRoute(chatType, row, resumed.agent, sessionId);
|
|
296
|
-
await this.attachToQQWorkspace(sessionId);
|
|
297
284
|
return { agent: resumed.agent, sessionId };
|
|
298
285
|
}
|
|
299
286
|
}
|
|
@@ -312,30 +299,8 @@ export class DshQQBridge {
|
|
|
312
299
|
this.db.setChatSession(chatType, Number(row.id), sessionId);
|
|
313
300
|
this.ensureTitle(handle.agent.session, chatType, row);
|
|
314
301
|
this.rememberRoute(chatType, row, handle.agent, sessionId);
|
|
315
|
-
await this.attachToQQWorkspace(sessionId);
|
|
316
302
|
return { agent: handle.agent, sessionId };
|
|
317
303
|
}
|
|
318
|
-
/** Keep all QQ sessions together in one DSH Workspace using the host API. */
|
|
319
|
-
async attachToQQWorkspace(sessionId) {
|
|
320
|
-
const registry = this.ctx.workspaceRegistry;
|
|
321
|
-
if (!registry)
|
|
322
|
-
return;
|
|
323
|
-
if (!this.qqWorkspace) {
|
|
324
|
-
this.qqWorkspace = registry.create(process.cwd(), 'QQ Chat').catch(error => {
|
|
325
|
-
this.logger.warn?.(`[dsh-qqchat] QQ Workspace 创建失败,将保留在未分组: ${error instanceof Error ? error.message : String(error)}`);
|
|
326
|
-
return undefined;
|
|
327
|
-
});
|
|
328
|
-
}
|
|
329
|
-
const workspace = await this.qqWorkspace;
|
|
330
|
-
if (!workspace)
|
|
331
|
-
return;
|
|
332
|
-
try {
|
|
333
|
-
await workspace.attachSession(SessionId(sessionId));
|
|
334
|
-
}
|
|
335
|
-
catch (error) {
|
|
336
|
-
this.logger.warn?.(`[dsh-qqchat] QQ session ${sessionId} 加入 Workspace 失败,将保留在未分组: ${error instanceof Error ? error.message : String(error)}`);
|
|
337
|
-
}
|
|
338
|
-
}
|
|
339
304
|
isArchived(sessionId) {
|
|
340
305
|
const registry = this.ctx.workspaceRegistry;
|
|
341
306
|
return registry?.archivedSessionIds.includes(sessionId) ?? false;
|
|
@@ -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 {};
|