dsh-qqchat 0.2.4 → 0.2.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/README.en.md +5 -4
  2. package/README.md +3 -3
  3. package/client-src/plugin.cts +2 -2
  4. package/client-src/settings.cts +18 -1
  5. package/client-src/shared.cts +4 -4
  6. package/cordis.patch.yml +5 -0
  7. package/docs/ARCHITECTURE.en.md +1 -1
  8. package/docs/ARCHITECTURE.md +2 -1
  9. package/docs/DEVELOPMENT.md +3 -1
  10. package/docs/MEMORY.md +32 -4
  11. package/docs/MEMORY_CONTEXT.md +16 -0
  12. package/docs/README.md +2 -0
  13. package/docs/STREAMING.md +2 -2
  14. 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
  15. package/docs/prds/README.md +62 -0
  16. package/lib/client.js +21 -4
  17. package/lib/config.js +3 -2
  18. package/lib/index.d.ts +2 -2
  19. package/lib/index.js +2 -3
  20. package/lib/session/agent-bridge.d.ts +3 -4
  21. package/lib/session/agent-bridge.js +40 -46
  22. package/lib/session/memory-snapshot.d.ts +4 -0
  23. package/lib/session/memory-snapshot.js +6 -2
  24. package/lib/session/runtime.js +15 -5
  25. package/lib/storage/db.d.ts +6 -1
  26. package/lib/storage/db.js +57 -0
  27. package/lib/storage/memory.d.ts +28 -2
  28. package/lib/storage/memory.js +375 -101
  29. package/lib/transport/rpc.js +2 -0
  30. package/lib/types.d.ts +26 -0
  31. package/package.json +24 -23
  32. package/prompts/group-compression.md +5 -0
  33. package/prompts/group-reflection.md +39 -0
  34. package/prompts/member-batch-reflection.md +39 -0
  35. package/prompts/member-compression.md +5 -0
  36. package/prompts/private-reflection.md +32 -0
  37. package/src/config.ts +3 -2
  38. package/src/index.ts +2 -3
  39. package/src/session/agent-bridge.ts +40 -47
  40. package/src/session/memory-snapshot.ts +6 -3
  41. package/src/session/runtime.ts +14 -4
  42. package/src/storage/db.ts +70 -0
  43. package/src/storage/memory.ts +343 -84
  44. package/src/transport/rpc.ts +1 -0
  45. package/src/types.ts +30 -0
@@ -1,7 +1,7 @@
1
1
  import { randomUUID } from 'node:crypto';
2
2
  import { createRequire } from 'node:module';
3
3
  import { installModelSelection } from '@deepseek-ai/dsh-agent';
4
- import { resolveSessionPreset } from '@deepseek-ai/dsh-agent-presets';
4
+ import { agentPresetProjectionDefinition } from '@deepseek-ai/dsh-agent-presets';
5
5
  import { createUserMessage } from '@deepseek-ai/dsh-llm';
6
6
  import { SessionId } from '@deepseek-ai/dsh-session';
7
7
  import '../shared/augmentations.js';
@@ -12,7 +12,10 @@ import { dispatchQQCommand, qqCommandText, slashCommandName } from '../commands/
12
12
  import { MEMORY_SNAPSHOT_TTL_MS, memorySnapshotHash, restoreMemorySnapshotState, shouldRefreshMemorySnapshot } from './memory-snapshot.js';
13
13
  const DSH_RUNTIME_CONTEXT_SOURCE = '@deepseek-ai/dsh-system-prompt';
14
14
  export function resolveQQSessionPreset(header, events) {
15
- return resolveSessionPreset({ header, events });
15
+ let current = agentPresetProjectionDefinition.init(header);
16
+ for (const event of events)
17
+ current = agentPresetProjectionDefinition.apply(current, event);
18
+ return current || undefined;
16
19
  }
17
20
  const MEDIA_TOOL_NAMES = new Set(['qqchat_describe_image', 'qqchat_read_file', 'qqchat_media_info']);
18
21
  const packageMetadata = createRequire(import.meta.url)('../../package.json');
@@ -33,8 +36,8 @@ export class DshQQBridge {
33
36
  activeAttachments = new Map();
34
37
  activeMediaReadable = new Map();
35
38
  memorySnapshots = new Map();
36
- qqWorkspace;
37
39
  disposeEvent;
40
+ disposeAssistantStream;
38
41
  disposeToolGate;
39
42
  disposeCommands;
40
43
  disposeImageTool;
@@ -46,6 +49,11 @@ export class DshQQBridge {
46
49
  this.config = config;
47
50
  this.logger = logger;
48
51
  this.disposeEvent = ctx.on('session/event', (session, event) => this.onSessionEvent(session, event));
52
+ this.disposeAssistantStream = ctx.on('agent/assistant-stream', ({ agent, frame }) => {
53
+ if (frame.type !== 'chunk' || frame.chunk.type !== 'text-delta' || !frame.chunk.text)
54
+ return;
55
+ this.pending.get(String(agent.id))?.onTextDelta?.(frame.chunk.text);
56
+ });
49
57
  this.disposeToolGate = ctx.on('tools/pre-execute', async (exec, next) => {
50
58
  const agent = exec.agent;
51
59
  if (!agent)
@@ -72,8 +80,19 @@ export class DshQQBridge {
72
80
  this.disposeMediaTools = registerQQMediaTools(ctx, db, canReadMedia);
73
81
  this.disposeCommands = this.registerCommands();
74
82
  }
83
+ async attachMappedSessionsToWorkspace() {
84
+ const sessionIds = [
85
+ ...this.db.listGroups().map(row => row.dsh_session_id),
86
+ ...this.db.listDirectChats().map(row => row.dsh_session_id),
87
+ ];
88
+ for (const sessionId of sessionIds) {
89
+ if (sessionId)
90
+ await this.attachToWorkspace(sessionId);
91
+ }
92
+ }
75
93
  async dispose() {
76
94
  this.disposeEvent();
95
+ this.disposeAssistantStream();
77
96
  this.disposeToolGate();
78
97
  this.disposeImageTool();
79
98
  this.disposeMediaTools();
@@ -98,16 +117,6 @@ export class DshQQBridge {
98
117
  const { sessionId } = await this.ensureAgent(chatType, row);
99
118
  return sessionId;
100
119
  }
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
120
  async recordTranscript(event, row, createSession = false) {
112
121
  const existing = this.db.getChatSession(event.chatType, Number(row.id));
113
122
  if (!existing && !createSession)
@@ -223,13 +232,6 @@ export class DshQQBridge {
223
232
  pending.text = text;
224
233
  return;
225
234
  }
226
- if (event.type === 'assistant/chunk') {
227
- const pending = this.pending.get(id);
228
- const chunk = event.data.chunk;
229
- if (pending?.onTextDelta && chunk.type === 'text-delta' && chunk.text)
230
- pending.onTextDelta(chunk.text);
231
- return;
232
- }
233
235
  if (event.type === 'request/header') {
234
236
  const { config } = event.data.header;
235
237
  if (config.provider && config.model)
@@ -261,7 +263,7 @@ export class DshQQBridge {
261
263
  this.memorySnapshots.set(sessionId, previous);
262
264
  }
263
265
  restoreMemorySnapshot(session) {
264
- return restoreMemorySnapshotState(session.events, DSH_RUNTIME_CONTEXT_SOURCE);
266
+ return restoreMemorySnapshotState(session.snapshotEvents(), DSH_RUNTIME_CONTEXT_SOURCE);
265
267
  }
266
268
  async ensureAgent(chatType, row) {
267
269
  let sessionId = this.db.getChatSession(chatType, Number(row.id));
@@ -284,16 +286,16 @@ export class DshQQBridge {
284
286
  if (live) {
285
287
  this.ensureSelection(live);
286
288
  this.ensureTitle(live.session, chatType, row);
289
+ await this.attachToWorkspace(sessionId);
287
290
  this.rememberRoute(chatType, row, live, sessionId);
288
- await this.attachToQQWorkspace(sessionId);
289
291
  return { agent: live, sessionId };
290
292
  }
291
293
  const resumed = await this.tryResume(sessionId);
292
294
  if (resumed) {
293
295
  this.ensureSelection(resumed.agent);
294
296
  this.ensureTitle(resumed.agent.session, chatType, row);
297
+ await this.attachToWorkspace(sessionId);
295
298
  this.rememberRoute(chatType, row, resumed.agent, sessionId);
296
- await this.attachToQQWorkspace(sessionId);
297
299
  return { agent: resumed.agent, sessionId };
298
300
  }
299
301
  }
@@ -310,36 +312,27 @@ export class DshQQBridge {
310
312
  this.handles.set(sessionId, handle);
311
313
  this.ensureSelection(handle.agent);
312
314
  this.db.setChatSession(chatType, Number(row.id), sessionId);
315
+ await this.attachToWorkspace(sessionId);
313
316
  this.ensureTitle(handle.agent.session, chatType, row);
314
317
  this.rememberRoute(chatType, row, handle.agent, sessionId);
315
- await this.attachToQQWorkspace(sessionId);
316
318
  return { agent: handle.agent, sessionId };
317
319
  }
318
- /** Keep all QQ sessions together in one DSH Workspace using the host API. */
319
- async attachToQQWorkspace(sessionId) {
320
+ isArchived(sessionId) {
321
+ const registry = this.ctx.workspaceRegistry;
322
+ return registry?.archivedSessionIds.includes(sessionId) ?? false;
323
+ }
324
+ async attachToWorkspace(sessionId) {
320
325
  const registry = this.ctx.workspaceRegistry;
321
326
  if (!registry)
322
327
  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
328
  try {
333
- await workspace.attachSession(SessionId(sessionId));
329
+ const workspace = await registry.resolveByPath(process.cwd());
330
+ await workspace?.attachSession(SessionId(sessionId));
334
331
  }
335
332
  catch (error) {
336
- this.logger.warn?.(`[dsh-qqchat] QQ session ${sessionId} 加入 Workspace 失败,将保留在未分组: ${error instanceof Error ? error.message : String(error)}`);
333
+ this.logger.warn?.(`[dsh-qqchat] QQ session ${sessionId} 未能挂载到当前工作区: ${error instanceof Error ? error.message : String(error)}`);
337
334
  }
338
335
  }
339
- isArchived(sessionId) {
340
- const registry = this.ctx.workspaceRegistry;
341
- return registry?.archivedSessionIds.includes(sessionId) ?? false;
342
- }
343
336
  rememberRoute(chatType, row, agent, sessionId) {
344
337
  const provider = agent.options.provider || this.config.provider;
345
338
  const model = agent.options.model || this.config.model;
@@ -351,7 +344,7 @@ export class DshQQBridge {
351
344
  this.memory.setMemberRoute(Number(row.id), provider, model, sessionId);
352
345
  }
353
346
  appendDisplayIfMissing(session, event) {
354
- if (session.events.some(item => item.type === 'qqchat/message' && item.data.messageId === event.messageId))
347
+ if (session.snapshotEvents().some(item => item.type === 'qqchat/message' && item.data.messageId === event.messageId))
355
348
  return;
356
349
  // Keep this on the public Session.append signature. Official DSH releases
357
350
  // do not yet expose the optional ignorable envelope marker; if an older
@@ -360,7 +353,7 @@ export class DshQQBridge {
360
353
  session.append('qqchat/message', displayEventPayload(event));
361
354
  }
362
355
  appendOwnerMessageIfMissing(session, event) {
363
- if (session.events.some(item => {
356
+ if (session.snapshotEvents().some(item => {
364
357
  if (item.type !== 'user/message')
365
358
  return false;
366
359
  const source = item.data.source;
@@ -541,7 +534,7 @@ export class DshQQBridge {
541
534
  const available = await presets.list();
542
535
  return { kind: 'success', text: available.length ? `可用 Agent Preset:\n${available.map(preset => `/qqpreset ${preset.id}`).join('\n')}` : '当前没有可用 Agent Preset。' };
543
536
  }
544
- if (agent.session.events.some(event => event.type === 'turn/start')) {
537
+ if (agent.session.snapshotEvents().some(event => event.type === 'turn/start')) {
545
538
  return { kind: 'error', text: '当前 Session 已经开始对话,DSH 不允许更换 Agent Preset。请先使用 /qqnew 开启新 Session。' };
546
539
  }
547
540
  try {
@@ -559,8 +552,9 @@ export class DshQQBridge {
559
552
  }
560
553
  statusCommand(agent) {
561
554
  const current = this.selection(agent)?.current;
562
- const messages = agent.session.events.filter(event => event.type === 'user/message' || event.type === 'assistant/message').length;
563
- const last = agent.session.events.at(-1);
555
+ const events = agent.session.snapshotEvents();
556
+ const messages = events.filter(event => event.type === 'user/message' || event.type === 'assistant/message').length;
557
+ const last = events.at(-1);
564
558
  return {
565
559
  kind: 'success',
566
560
  text: ['📊 Session 状态', `Session:${String(agent.id)}`, `状态:${agent.status === 'running' ? '生成中' : '空闲'}`, `模型:${current ? `${current.provider}/${current.model}` : '未知'}`, `消息数:${messages}`, `最后事件:${last?.type || '无'}`].join('\n'),
@@ -11,6 +11,10 @@ export interface MemorySnapshotEvent {
11
11
  source?: {
12
12
  kind?: string;
13
13
  plugin?: string;
14
+ sections?: readonly {
15
+ name?: string;
16
+ text?: string;
17
+ }[];
14
18
  };
15
19
  content?: readonly {
16
20
  type?: string;
@@ -16,9 +16,13 @@ export function restoreMemorySnapshotState(events, sourcePlugin) {
16
16
  const event = events[index];
17
17
  if (!event)
18
18
  continue;
19
- if (event.type !== 'user/message' || event.data?.source?.kind !== 'plugin' || event.data.source.plugin !== sourcePlugin)
19
+ const source = event.data?.source;
20
+ if (event.type !== 'user/message' || source?.kind !== 'plugin' || source.plugin !== sourcePlugin)
20
21
  continue;
21
- const block = event.data.content?.length === 1 ? event.data.content[0] : undefined;
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) };
@@ -28,6 +28,7 @@ export class QQChatRuntime {
28
28
  this.media = media;
29
29
  }
30
30
  async start() {
31
+ await this.bridge.attachMappedSessionsToWorkspace();
31
32
  for (const account of this.db.enabledAccounts())
32
33
  this.startGateway(account);
33
34
  this.outboxTimer = setInterval(() => void this.flushOutbox(), 3000);
@@ -59,6 +60,8 @@ export class QQChatRuntime {
59
60
  updateSettings(patch) {
60
61
  if (patch.memoryEnabled !== undefined)
61
62
  this.db.setSetting('memoryEnabled', Boolean(patch.memoryEnabled));
63
+ if (patch.memoryMemberBatchEnabled !== undefined)
64
+ this.db.setSetting('memoryMemberBatchEnabled', Boolean(patch.memoryMemberBatchEnabled));
62
65
  if (patch.groupReceiveMode !== undefined) {
63
66
  if (!isGroupReceiveMode(patch.groupReceiveMode))
64
67
  throw new Error('无效的群聊接收模式');
@@ -131,8 +134,11 @@ export class QQChatRuntime {
131
134
  content: text, quotedText: '', mentioned: false, createdAt: Date.now(),
132
135
  }, row, true);
133
136
  if (this.settings().memoryEnabled) {
134
- if (chatType === 'group')
137
+ if (chatType === 'group') {
135
138
  this.memory.schedule(Number(row.id));
139
+ if (settings.memoryMemberBatchEnabled)
140
+ this.memory.scheduleGroupMembers(Number(row.id));
141
+ }
136
142
  else
137
143
  this.memory.scheduleMember(Number(row.id));
138
144
  }
@@ -261,10 +267,11 @@ export class QQChatRuntime {
261
267
  quotedText: message.quotedText, mentioned: message.mentioned, createdAt: Date.now(), attachments: publicAttachments(ownAttachments), quote: safeQuote,
262
268
  };
263
269
  if (settings.memoryEnabled) {
264
- if (group)
270
+ if (group) {
265
271
  this.memory.schedule(Number(group.id));
266
- else
267
- this.memory.scheduleMember(Number(member.id));
272
+ if (settings.memoryMemberBatchEnabled)
273
+ this.memory.scheduleGroupMembers(Number(group.id));
274
+ }
268
275
  }
269
276
  if (!shouldReply) {
270
277
  await this.bridge.recordTranscript(displayEvent, row, true);
@@ -303,8 +310,11 @@ export class QQChatRuntime {
303
310
  memberId: message.chatType === 'c2c' ? member.id : undefined, direction: 'outbound', content: reply, mentioned: false,
304
311
  });
305
312
  if (settings.memoryEnabled) {
306
- if (group)
313
+ if (group) {
307
314
  this.memory.schedule(Number(group.id));
315
+ if (settings.memoryMemberBatchEnabled)
316
+ this.memory.scheduleGroupMembers(Number(group.id));
317
+ }
308
318
  else
309
319
  this.memory.scheduleMember(Number(member.id));
310
320
  }
@@ -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));
@@ -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 applyReflection;
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 {};