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,10 +1,15 @@
1
1
  import { BlockAssembler, createUserMessage } from '@deepseek-ai/dsh-llm';
2
2
  import { SessionId } from '@deepseek-ai/dsh-session';
3
+ import { readFileSync } from 'node:fs';
3
4
  const DOCS = ['profile', 'summary', 'daily', 'memory', 'pattern'];
4
5
  const MEMBER_DAILY_COMPACT_AT = 100;
5
6
  const MEMBER_DAILY_KEEP_RECENT = 50;
6
- const GROUP_DAILY_COMPACT_AT = 1000;
7
- const GROUP_DAILY_KEEP_RECENT = 500;
7
+ const GROUP_DAILY_COMPACT_AT = 200;
8
+ const GROUP_DAILY_KEEP_RECENT = 100;
9
+ const MEMBER_BATCH_SIZE = 50;
10
+ const REFLECTION_RETRY_COOLDOWN_MS = 60_000;
11
+ /** 与咕咕一致:每个记忆 scope 的本轮直注入预算(按字符计)。 */
12
+ export const MEMORY_INJECT_CHARS = 2_000;
8
13
  export class MemoryEngine {
9
14
  ctx;
10
15
  db;
@@ -12,10 +17,15 @@ export class MemoryEngine {
12
17
  logger;
13
18
  timers = new Map();
14
19
  memberTimers = new Map();
20
+ memberBatchTimers = new Map();
15
21
  routes = new Map();
16
22
  memberRoutes = new Map();
17
23
  reflectingGroups = new Map();
18
24
  reflectingMembers = new Map();
25
+ reflectingMemberBatches = new Map();
26
+ groupRetryAt = new Map();
27
+ memberRetryAt = new Map();
28
+ memberBatchRetryAt = new Map();
19
29
  constructor(ctx, db, config, logger = console) {
20
30
  this.ctx = ctx;
21
31
  this.db = db;
@@ -27,10 +37,17 @@ export class MemoryEngine {
27
37
  clearTimeout(timer);
28
38
  for (const timer of this.memberTimers.values())
29
39
  clearTimeout(timer);
40
+ for (const timer of this.memberBatchTimers.values())
41
+ clearTimeout(timer);
30
42
  this.timers.clear();
31
43
  this.memberTimers.clear();
44
+ this.memberBatchTimers.clear();
32
45
  this.reflectingGroups.clear();
33
46
  this.reflectingMembers.clear();
47
+ this.reflectingMemberBatches.clear();
48
+ this.groupRetryAt.clear();
49
+ this.memberRetryAt.clear();
50
+ this.memberBatchRetryAt.clear();
34
51
  }
35
52
  setRoute(groupId, provider, model, sessionId) {
36
53
  if (!provider || !model)
@@ -49,14 +66,13 @@ export class MemoryEngine {
49
66
  const existing = this.timers.get(groupId);
50
67
  if (existing)
51
68
  clearTimeout(existing);
52
- if (this.db.unreflectedCount(groupId) >= this.config.reflectionBatchSize) {
53
- void this.reflectNow(groupId).catch(error => this.logger.warn?.(`[dsh-qqchat] memory reflection: ${errorMessage(error)}`));
54
- return;
55
- }
56
69
  const timer = setTimeout(() => {
57
70
  this.timers.delete(groupId);
58
- void this.reflectNow(groupId).catch(error => this.logger.warn?.(`[dsh-qqchat] memory reflection: ${errorMessage(error)}`));
59
- }, this.config.reflectionIdleMs);
71
+ if (this.inRetryCooldown(this.groupRetryAt, groupId))
72
+ this.schedule(groupId);
73
+ else
74
+ void this.runScheduledGroupReflection(groupId);
75
+ }, this.reflectionDelay(this.groupRetryAt, groupId));
60
76
  timer.unref?.();
61
77
  this.timers.set(groupId, timer);
62
78
  }
@@ -67,17 +83,82 @@ export class MemoryEngine {
67
83
  const existing = this.memberTimers.get(memberId);
68
84
  if (existing)
69
85
  clearTimeout(existing);
70
- if (this.unreflectedDirectMessages(memberId, this.config.reflectionBatchSize).length >= this.config.reflectionBatchSize) {
71
- void this.reflectMemberNow(memberId).catch(error => this.logger.warn?.(`[dsh-qqchat] member memory reflection: ${errorMessage(error)}`));
86
+ if (this.unreflectedDirectMessages(memberId, this.config.reflectionBatchSize).length >= this.config.reflectionBatchSize && !this.inRetryCooldown(this.memberRetryAt, memberId)) {
87
+ void this.runScheduledMemberReflection(memberId);
72
88
  return;
73
89
  }
74
90
  const timer = setTimeout(() => {
75
91
  this.memberTimers.delete(memberId);
76
- void this.reflectMemberNow(memberId).catch(error => this.logger.warn?.(`[dsh-qqchat] member memory reflection: ${errorMessage(error)}`));
77
- }, this.config.reflectionIdleMs);
92
+ if (this.inRetryCooldown(this.memberRetryAt, memberId))
93
+ this.scheduleMember(memberId);
94
+ else
95
+ void this.runScheduledMemberReflection(memberId);
96
+ }, this.reflectionDelay(this.memberRetryAt, memberId));
78
97
  timer.unref?.();
79
98
  this.memberTimers.set(memberId, timer);
80
99
  }
100
+ scheduleGroupMembers(groupId) {
101
+ groupId = Number(groupId);
102
+ if (!this.routes.has(groupId))
103
+ return;
104
+ const existing = this.memberBatchTimers.get(groupId);
105
+ if (existing)
106
+ clearTimeout(existing);
107
+ if (this.db.unreflectedMemberCount(groupId) >= MEMBER_BATCH_SIZE && !this.inRetryCooldown(this.memberBatchRetryAt, groupId)) {
108
+ void this.runScheduledMemberBatchReflection(groupId);
109
+ return;
110
+ }
111
+ const timer = setTimeout(() => {
112
+ this.memberBatchTimers.delete(groupId);
113
+ if (this.inRetryCooldown(this.memberBatchRetryAt, groupId))
114
+ this.scheduleGroupMembers(groupId);
115
+ else
116
+ void this.runScheduledMemberBatchReflection(groupId);
117
+ }, this.reflectionDelay(this.memberBatchRetryAt, groupId));
118
+ timer.unref?.();
119
+ this.memberBatchTimers.set(groupId, timer);
120
+ }
121
+ inRetryCooldown(retryAt, id) {
122
+ const until = retryAt.get(id) || 0;
123
+ if (until <= Date.now()) {
124
+ retryAt.delete(id);
125
+ return false;
126
+ }
127
+ return true;
128
+ }
129
+ reflectionDelay(retryAt, id) {
130
+ return Math.max(this.config.reflectionIdleMs, (retryAt.get(id) || 0) - Date.now());
131
+ }
132
+ async runScheduledGroupReflection(groupId) {
133
+ try {
134
+ await this.reflectNow(groupId);
135
+ this.groupRetryAt.delete(groupId);
136
+ }
137
+ catch (error) {
138
+ this.groupRetryAt.set(groupId, Date.now() + REFLECTION_RETRY_COOLDOWN_MS);
139
+ this.logger.warn?.(`[dsh-qqchat] memory reflection: ${errorMessage(error)}`);
140
+ }
141
+ }
142
+ async runScheduledMemberReflection(memberId) {
143
+ try {
144
+ await this.reflectMemberNow(memberId);
145
+ this.memberRetryAt.delete(memberId);
146
+ }
147
+ catch (error) {
148
+ this.memberRetryAt.set(memberId, Date.now() + REFLECTION_RETRY_COOLDOWN_MS);
149
+ this.logger.warn?.(`[dsh-qqchat] member memory reflection: ${errorMessage(error)}`);
150
+ }
151
+ }
152
+ async runScheduledMemberBatchReflection(groupId) {
153
+ try {
154
+ await this.reflectMembersNow(groupId);
155
+ this.memberBatchRetryAt.delete(groupId);
156
+ }
157
+ catch (error) {
158
+ this.memberBatchRetryAt.set(groupId, Date.now() + REFLECTION_RETRY_COOLDOWN_MS);
159
+ this.logger.warn?.(`[dsh-qqchat] member batch reflection: ${errorMessage(error)}`);
160
+ }
161
+ }
81
162
  async reflectNow(groupId) {
82
163
  groupId = Number(groupId);
83
164
  const existing = this.reflectingGroups.get(groupId);
@@ -105,42 +186,134 @@ export class MemoryEngine {
105
186
  throw new Error('群不存在');
106
187
  return view;
107
188
  }
108
- const members = this.db.listGroupMembers(groupId);
109
- const currentGroup = this.db.memoryDocs('group', groupId);
110
- const memberMemory = Object.fromEntries(members.map(member => [member.platform_user_id, {
111
- profile: this.db.memoryDocs('member', Number(member.id)).profile || '',
112
- pattern: this.db.memoryDocs('member', Number(member.id)).pattern || '',
113
- summary: this.db.memoryDocs('member', Number(member.id)).summary || '',
114
- memory: this.db.memoryDocs('member', Number(member.id)).memory || '',
115
- name: member.display_name || '',
116
- aliases: parseStringList(member.aliases_json),
117
- nicknames: parseStringList(member.nicknames_json),
118
- }]));
119
- const transcript = messages.map(message => ({
120
- id: Number(message.id),
121
- at: message.created_at,
122
- direction: message.direction,
123
- senderId: message.direction === 'outbound' ? 'BOT' : message.platform_user_id,
124
- senderName: message.direction === 'outbound' ? 'DSH Agent' : (message.display_name || ''),
125
- text: message.content,
126
- quotedText: message.quoted_text || '',
127
- ...(parseReflectionQuote(message.quote_json) ? { quote: parseReflectionQuote(message.quote_json) } : {}),
128
- }));
129
- const input = {
130
- group: { id: group.platform_group_id, name: group.name || '' },
131
- existing: { group: pickDocs(currentGroup), members: memberMemory },
132
- messages: transcript,
133
- };
134
- const reflected = await this.generateReflection(route, memorySystemPrompt(), input, `qqchat-memory-${groupId}`);
135
- this.applyReflection(groupId, members, reflected);
136
- await this.compactDaily('group', groupId, route, GROUP_DAILY_COMPACT_AT, GROUP_DAILY_KEEP_RECENT, `qqchat-group-compress-${groupId}`);
137
- const lastMessage = messages.at(-1);
138
- if (lastMessage)
139
- this.db.markReflected(groupId, Number(lastMessage.id));
140
- const view = this.memoryView(groupId);
141
- if (!view)
189
+ const taskKey = `group:${groupId}:${Number(messages[0]?.id)}:${Number(messages.at(-1)?.id)}`;
190
+ if (!this.db.startReflectionTask({
191
+ scopeType: 'group', scopeKey: groupId, taskType: 'group',
192
+ startMessageId: Number(messages[0]?.id), endMessageId: Number(messages.at(-1)?.id), idempotencyKey: taskKey,
193
+ })) {
194
+ const view = this.memoryView(groupId);
195
+ if (!view)
196
+ throw new Error('群不存在');
197
+ return view;
198
+ }
199
+ const startedAt = Date.now();
200
+ try {
201
+ const currentGroup = this.db.memoryDocs('group', groupId);
202
+ const transcript = messages.map(message => ({
203
+ id: Number(message.id),
204
+ at: message.created_at,
205
+ direction: message.direction,
206
+ senderId: message.direction === 'outbound' ? 'BOT' : message.platform_user_id,
207
+ senderName: message.direction === 'outbound' ? 'DSH Agent' : (message.display_name || ''),
208
+ text: message.content,
209
+ quotedText: message.quoted_text || '',
210
+ ...(parseReflectionQuote(message.quote_json) ? { quote: parseReflectionQuote(message.quote_json) } : {}),
211
+ }));
212
+ const input = {
213
+ group: { id: group.platform_group_id, name: group.name || '' },
214
+ existing: { group: pickDocs(currentGroup) },
215
+ messages: transcript,
216
+ };
217
+ const reflected = await this.generateReflection(route, memorySystemPrompt(), input, `qqchat-memory-${groupId}`);
218
+ this.applyGroupReflection(groupId, reflected);
219
+ await this.compactDaily('group', groupId, route, GROUP_DAILY_COMPACT_AT, GROUP_DAILY_KEEP_RECENT, `qqchat-group-compress-${groupId}`);
220
+ const lastMessage = messages.at(-1);
221
+ if (lastMessage)
222
+ this.db.markReflected(groupId, Number(lastMessage.id));
223
+ this.db.finishReflectionTask(taskKey, 'completed');
224
+ this.logger.info?.(`[dsh-qqchat] memory reflection completed task=group messages=${messages.length} durationMs=${Date.now() - startedAt}`);
225
+ const view = this.memoryView(groupId);
226
+ if (!view)
227
+ throw new Error('群不存在');
228
+ return view;
229
+ }
230
+ catch (error) {
231
+ this.db.finishReflectionTask(taskKey, 'failed');
232
+ this.logger.warn?.(`[dsh-qqchat] memory reflection failed task=group messages=${messages.length} durationMs=${Date.now() - startedAt}`);
233
+ throw error;
234
+ }
235
+ }
236
+ async reflectMembersNow(groupId) {
237
+ groupId = Number(groupId);
238
+ const existing = this.reflectingMemberBatches.get(groupId);
239
+ if (existing)
240
+ return existing;
241
+ const current = this.reflectMembersNowInternal(groupId);
242
+ this.reflectingMemberBatches.set(groupId, current);
243
+ return current.finally(() => {
244
+ if (this.reflectingMemberBatches.get(groupId) === current)
245
+ this.reflectingMemberBatches.delete(groupId);
246
+ });
247
+ }
248
+ async reflectMembersNowInternal(groupId) {
249
+ groupId = Number(groupId);
250
+ const route = this.routes.get(groupId);
251
+ if (!route)
252
+ throw new Error('这个群还没有可复用的 DSH 模型路由;先让 Agent 在群里完成一次回复');
253
+ const group = this.db.groupById(groupId);
254
+ if (!group)
142
255
  throw new Error('群不存在');
143
- return view;
256
+ const messages = this.db.unreflectedMemberMessages(groupId, this.config.reflectionMaxMessages);
257
+ if (messages.length === 0) {
258
+ const view = this.memoryView(groupId);
259
+ if (!view)
260
+ throw new Error('群不存在');
261
+ return view;
262
+ }
263
+ const taskKey = `member-batch:${groupId}:${Number(messages[0]?.id)}:${Number(messages.at(-1)?.id)}`;
264
+ if (!this.db.startReflectionTask({
265
+ scopeType: 'group', scopeKey: groupId, taskType: 'member-batch',
266
+ startMessageId: Number(messages[0]?.id), endMessageId: Number(messages.at(-1)?.id), idempotencyKey: taskKey,
267
+ })) {
268
+ const view = this.memoryView(groupId);
269
+ if (!view)
270
+ throw new Error('群不存在');
271
+ return view;
272
+ }
273
+ const startedAt = Date.now();
274
+ try {
275
+ const senderIds = new Set(messages
276
+ .filter(message => message.direction !== 'outbound' && message.platform_user_id)
277
+ .map(message => message.platform_user_id));
278
+ const members = this.db.listGroupMembers(groupId).filter(member => senderIds.has(member.platform_user_id));
279
+ const input = {
280
+ group: { id: group.platform_group_id, name: group.name || '' },
281
+ members: members.map(member => ({
282
+ senderId: member.platform_user_id,
283
+ displayName: member.display_name || '',
284
+ existing: pickMemberDocs(this.db.memoryDocs('member', Number(member.id))),
285
+ })),
286
+ messages: messages.map(message => ({
287
+ id: Number(message.id),
288
+ at: message.created_at,
289
+ direction: message.direction,
290
+ senderId: message.direction === 'outbound' ? 'BOT' : message.platform_user_id,
291
+ senderName: message.direction === 'outbound' ? 'DSH Agent' : (message.display_name || ''),
292
+ text: message.content,
293
+ quotedText: message.quoted_text || '',
294
+ ...(parseReflectionQuote(message.quote_json) ? { quote: parseReflectionQuote(message.quote_json) } : {}),
295
+ })),
296
+ };
297
+ const reflected = await this.generateReflection(route, memberBatchMemorySystemPrompt(), input, `qqchat-member-batch-${groupId}`);
298
+ if (!validateMemberBatchPayload(reflected, new Set(members.map(member => member.platform_user_id)))) {
299
+ throw new Error('成员批量反思输出包含无效成员或格式');
300
+ }
301
+ this.applyMemberBatchReflection(groupId, members, reflected, messages.at(-1)?.created_at);
302
+ const lastMessage = messages.at(-1);
303
+ if (lastMessage)
304
+ this.db.markMembersReflected(groupId, Number(lastMessage.id));
305
+ this.db.finishReflectionTask(taskKey, 'completed');
306
+ this.logger.info?.(`[dsh-qqchat] memory reflection completed task=member-batch messages=${messages.length} members=${members.length} durationMs=${Date.now() - startedAt}`);
307
+ const view = this.memoryView(groupId);
308
+ if (!view)
309
+ throw new Error('群不存在');
310
+ return view;
311
+ }
312
+ catch (error) {
313
+ this.db.finishReflectionTask(taskKey, 'failed');
314
+ this.logger.warn?.(`[dsh-qqchat] memory reflection failed task=member-batch messages=${messages.length} durationMs=${Date.now() - startedAt}`);
315
+ throw error;
316
+ }
144
317
  }
145
318
  async reflectMemberNow(memberId) {
146
319
  memberId = Number(memberId);
@@ -182,24 +355,46 @@ export class MemoryEngine {
182
355
  ...(parseReflectionQuote(message.quote_json) ? { quote: parseReflectionQuote(message.quote_json) } : {}),
183
356
  })),
184
357
  };
185
- const reflected = await this.generateReflection(route, memberMemorySystemPrompt(), input, `qqchat-member-memory-${memberId}`);
186
- if (reflected.profile !== undefined)
187
- this.db.setMemoryDoc('member', memberId, 'profile', stringifyProfile(reflected.profile));
188
- if (reflected.pattern !== undefined)
189
- this.db.setMemoryDoc('member', memberId, 'pattern', stringifyDoc(reflected.pattern));
190
- if (reflected.summary !== undefined)
191
- this.db.setMemoryDoc('member', memberId, 'summary', stringifyDoc(reflected.summary));
192
- if (reflected.memory !== undefined)
193
- this.db.setMemoryDoc('member', memberId, 'memory', stringifyMemory(reflected.memory));
194
- if (reflected.daily !== undefined) {
195
- const last = messages.at(-1);
196
- this.db.appendDailyDoc('member', memberId, dateForTimestamp(last?.created_at), stringifyDoc(reflected.daily));
358
+ const taskKey = `private-owner:${memberId}:${Number(messages[0]?.id)}:${Number(messages.at(-1)?.id)}`;
359
+ if (!this.db.startReflectionTask({
360
+ scopeType: 'member', scopeKey: memberId, taskType: 'private-owner',
361
+ startMessageId: Number(messages[0]?.id), endMessageId: Number(messages.at(-1)?.id), idempotencyKey: taskKey,
362
+ }))
363
+ return this.db.memoryDocs('member', memberId);
364
+ const startedAt = Date.now();
365
+ try {
366
+ const reflected = await this.generateReflection(route, privateMemorySystemPrompt(), input, `qqchat-private-memory-${memberId}`);
367
+ if (reflected.profile_add !== undefined || reflected.profile_remove !== undefined) {
368
+ this.db.setMemoryDoc('member', memberId, 'profile', mergeProfile(this.db.memoryDocs('member', memberId).profile, reflected.profile_add, reflected.profile_remove));
369
+ }
370
+ else if (reflected.profile !== undefined)
371
+ this.db.setMemoryDoc('member', memberId, 'profile', stringifyProfile(reflected.profile));
372
+ if (reflected.pattern_add !== undefined || reflected.pattern_remove !== undefined) {
373
+ this.db.setMemoryDoc('member', memberId, 'pattern', mergeTextDoc(this.db.memoryDocs('member', memberId).pattern, reflected.pattern_add, reflected.pattern_remove));
374
+ }
375
+ else if (reflected.pattern !== undefined)
376
+ this.db.setMemoryDoc('member', memberId, 'pattern', stringifyDoc(reflected.pattern));
377
+ if (reflected.summary !== undefined)
378
+ this.db.setMemoryDoc('member', memberId, 'summary', stringifyDoc(reflected.summary));
379
+ if (reflected.memory !== undefined)
380
+ this.db.setMemoryDoc('member', memberId, 'memory', stringifyMemory(reflected.memory));
381
+ if (reflected.daily !== undefined) {
382
+ const last = messages.at(-1);
383
+ this.db.appendDailyDoc('member', memberId, dateForTimestamp(last?.created_at), stringifyDoc(reflected.daily));
384
+ }
385
+ await this.compactDaily('member', memberId, route, MEMBER_DAILY_COMPACT_AT, MEMBER_DAILY_KEEP_RECENT, `qqchat-member-compress-${memberId}`);
386
+ const lastMessage = messages.at(-1);
387
+ if (lastMessage)
388
+ this.db.setSetting(memberCursorKey(memberId), Number(lastMessage.id));
389
+ this.db.finishReflectionTask(taskKey, 'completed');
390
+ this.logger.info?.(`[dsh-qqchat] memory reflection completed task=private-owner messages=${messages.length} durationMs=${Date.now() - startedAt}`);
391
+ return this.db.memoryDocs('member', memberId);
392
+ }
393
+ catch (error) {
394
+ this.db.finishReflectionTask(taskKey, 'failed');
395
+ this.logger.warn?.(`[dsh-qqchat] memory reflection failed task=private-owner messages=${messages.length} durationMs=${Date.now() - startedAt}`);
396
+ throw error;
197
397
  }
198
- await this.compactDaily('member', memberId, route, MEMBER_DAILY_COMPACT_AT, MEMBER_DAILY_KEEP_RECENT, `qqchat-member-compress-${memberId}`);
199
- const lastMessage = messages.at(-1);
200
- if (lastMessage)
201
- this.db.setSetting(memberCursorKey(memberId), Number(lastMessage.id));
202
- return this.db.memoryDocs('member', memberId);
203
398
  }
204
399
  async compactDaily(scopeType, scopeKey, route, threshold, keepRecent, fallbackSessionId) {
205
400
  const entries = this.db.dailyEntries(scopeType, scopeKey);
@@ -258,7 +453,7 @@ export class MemoryEngine {
258
453
  .filter(message => Number(message.id) > cursor)
259
454
  .slice(-limit);
260
455
  }
261
- applyReflection(groupId, members, reflected) {
456
+ applyGroupReflection(groupId, reflected) {
262
457
  const group = reflected.group || {};
263
458
  if (group.profile !== undefined)
264
459
  this.db.setMemoryDoc('group', groupId, 'profile', stringifyDoc(group.profile));
@@ -270,20 +465,27 @@ export class MemoryEngine {
270
465
  const stamp = new Date().toISOString().slice(0, 10);
271
466
  this.db.appendDailyDoc('group', groupId, stamp, stringifyDoc(group.daily));
272
467
  }
468
+ }
469
+ applyMemberBatchReflection(groupId, members, reflected, createdAt) {
273
470
  const byOpenid = new Map(members.map(member => [member.platform_user_id, Number(member.id)]));
274
471
  const updates = Array.isArray(reflected.members) ? reflected.members : [];
275
472
  for (const update of updates) {
276
473
  const memberId = byOpenid.get(String(update.senderId || ''));
277
474
  if (!memberId)
278
475
  continue;
279
- if (update.profile !== undefined)
280
- this.db.setMemoryDoc('member', memberId, 'profile', stringifyProfile(update.profile));
281
- if (update.pattern !== undefined)
282
- this.db.setMemoryDoc('member', memberId, 'pattern', stringifyDoc(update.pattern));
476
+ if (update.profile_add !== undefined || update.profile_remove !== undefined) {
477
+ this.db.setMemoryDoc('member', memberId, 'profile', mergeProfile(this.db.memoryDocs('member', memberId).profile, update.profile_add, update.profile_remove));
478
+ }
479
+ if (update.pattern_add !== undefined || update.pattern_remove !== undefined) {
480
+ this.db.setMemoryDoc('member', memberId, 'pattern', mergeTextDoc(this.db.memoryDocs('member', memberId).pattern, update.pattern_add, update.pattern_remove));
481
+ }
283
482
  if (update.summary !== undefined)
284
483
  this.db.setMemoryDoc('member', memberId, 'summary', stringifyDoc(update.summary));
285
484
  if (update.memory !== undefined)
286
485
  this.db.setMemoryDoc('member', memberId, 'memory', stringifyMemory(update.memory));
486
+ if (update.daily !== undefined && stringifyDoc(update.daily).trim()) {
487
+ this.db.appendDailyDoc('member', memberId, dateForTimestamp(createdAt), stringifyDoc(update.daily));
488
+ }
287
489
  if (Array.isArray(update.nicknames)) {
288
490
  for (const nickname of update.nicknames) {
289
491
  if (typeof nickname === 'string')
@@ -295,20 +497,26 @@ export class MemoryEngine {
295
497
  contextForGroup(group, currentMember) {
296
498
  const groupDocs = this.db.memoryDocs('group', Number(group.id));
297
499
  const memberDocs = currentMember ? this.db.memoryDocs('member', Number(currentMember.id)) : {};
500
+ const groupMemory = boundedMemorySections([
501
+ ['群画像', groupDocs.profile],
502
+ ['群摘要', groupDocs.summary],
503
+ ['群长期记忆', groupDocs.memory],
504
+ ['群最近沉淀', groupDocs.daily],
505
+ ]);
506
+ const memberMemory = currentMember ? boundedMemorySections([
507
+ ['当前成员画像', memberDocs.profile],
508
+ ['当前成员模式', memberDocs.pattern],
509
+ ['当前成员摘要', memberDocs.summary],
510
+ ['当前成员长期记忆', memberDocs.memory],
511
+ ]) : [];
298
512
  return [
299
513
  '[QQ 群聊上下文快照]',
300
514
  '以下 sender ID / group ID 是可靠平台元数据。不要根据昵称猜身份,不要把其他群成员的兴趣、关系或记忆归到当前发言人。',
301
515
  `群ID=${group.platform_group_id}`,
302
516
  group.name ? `群名=${group.name}` : '',
303
- section('群画像', groupDocs.profile),
304
- section('群摘要', groupDocs.summary),
305
- section('群长期记忆', groupDocs.memory),
306
- section('群最近沉淀', groupDocs.daily),
517
+ ...groupMemory,
307
518
  currentMember ? `当前成员ID=${currentMember.platform_user_id}\n当前成员显示名=${currentMember.display_name || ''}` : '',
308
- section('当前成员画像', memberDocs.profile),
309
- section('当前成员模式', memberDocs.pattern),
310
- section('当前成员摘要', memberDocs.summary),
311
- section('当前成员长期记忆', memberDocs.memory),
519
+ ...memberMemory,
312
520
  ].filter(Boolean).join('\n');
313
521
  }
314
522
  contextForMember(member) {
@@ -318,11 +526,13 @@ export class MemoryEngine {
318
526
  `成员ID=${member.platform_user_id}`,
319
527
  `显示名=${member.display_name || ''}`,
320
528
  '成员ID 是可靠平台身份;昵称仅供展示。',
321
- section('成员画像', docs.profile),
322
- section('行为模式', docs.pattern),
323
- section('成员摘要', docs.summary),
324
- section('成员近期沉淀', docs.daily),
325
- section('成员长期记忆', docs.memory),
529
+ ...boundedMemorySections([
530
+ ['成员画像', docs.profile],
531
+ ['行为模式', docs.pattern],
532
+ ['成员摘要', docs.summary],
533
+ ['成员近期沉淀', docs.daily],
534
+ ['成员长期记忆', docs.memory],
535
+ ]),
326
536
  ].filter(Boolean).join('\n');
327
537
  }
328
538
  currentMessageText(message) {
@@ -358,17 +568,11 @@ export class MemoryEngine {
358
568
  return { group, groupMemory: this.db.memoryDocs('group', Number(groupId)), members };
359
569
  }
360
570
  }
361
- function memorySystemPrompt() {
362
- return `你负责整理一个 QQ 群的长期记忆。目标是稳定、克制、可追溯地维护群 scope 与成员 scope,规则:
363
- 1. senderId 是唯一可靠身份,不根据昵称猜身份;不同 senderId 的个人事实绝不能混写。成员记录中的 name/aliases/nicknames 只用于称呼和检索。引用内容属于 quote.senderId;除非 quote.senderId 与当前 senderId 相同,否则不得把引用中的个人事实归给当前发言人。
364
- 2. 群 scope 只写群级角色、关系、共同决定、长期话题、群内约定;个人稳定信息写到对应 member。
365
- 3. 不把一次性寒暄、玩笑、临时情绪上升为长期事实;不确定信息宁可不记。
366
- 4. existing 是已有记忆,应在其基础上更新,而不是无条件推翻。
367
- 5. daily 只写本批消息值得留档的新进展;memory 保留真正长期有用的项目、关系、约定和背景。
368
- 6. 输出严格 JSON,不要 Markdown 代码块、解释或额外文字。
369
- 输出结构:
370
- {"group":{"profile":{},"summary":"","daily":"","memory":["..."]},"members":[{"senderId":"可靠ID","profile":[{"type":"name|address|pronoun|background|preference|note","text":"...","ts":0}],"pattern":{},"summary":"","memory":"","nicknames":["群内称呼"]}]}
371
- 没有变化的成员可省略;不要为 BOT 创建成员记忆。`;
571
+ export function memorySystemPrompt() {
572
+ return loadReflectionPrompt('group-reflection.md');
573
+ }
574
+ export function memberBatchMemorySystemPrompt() {
575
+ return loadReflectionPrompt('member-batch-reflection.md');
372
576
  }
373
577
  function memberMemorySystemPrompt() {
374
578
  return `你负责整理一个 QQ 私聊成员的长期记忆。senderId 是唯一可靠身份,displayName 只用于展示。引用内容属于 quote.senderId;除非 quote.senderId 与当前成员 senderId 相同,否则不得把引用中的个人事实归给当前成员。规则:
@@ -379,6 +583,12 @@ function memberMemorySystemPrompt() {
379
583
  5. 输出严格 JSON,不要 Markdown 代码块、解释或额外文字。
380
584
  输出结构:{"profile":[{"type":"name|address|pronoun|background|preference|note","text":"...","ts":0}],"pattern":{},"summary":"","memory":"","daily":""}`;
381
585
  }
586
+ export function privateMemorySystemPrompt() {
587
+ return loadReflectionPrompt('private-reflection.md');
588
+ }
589
+ function loadReflectionPrompt(filename) {
590
+ return readFileSync(new URL(`../../prompts/${filename}`, import.meta.url), 'utf8').trim();
591
+ }
382
592
  function parseReflectionQuote(value) {
383
593
  if (!value)
384
594
  return undefined;
@@ -401,11 +611,11 @@ function parseReflectionQuote(value) {
401
611
  return undefined;
402
612
  }
403
613
  }
404
- function groupCompressionSystemPrompt() {
405
- return `你负责维护 QQ 群长期记忆主档。把近期 daily 记录合并进已有 memory,合并重复、修正矛盾,但保留有价值的历史、日期、事件背景和变化过程;不要因为旧或细而删除历史,不评判、不推测。严格只输出 JSON:{"memory":"更新后的长期记忆全文;不能清空"}`;
614
+ export function groupCompressionSystemPrompt() {
615
+ return loadReflectionPrompt('group-compression.md');
406
616
  }
407
- function memberCompressionSystemPrompt() {
408
- return `你负责维护 QQ 成员长期记忆主档。把近期 daily 记录合并进已有 memory,结合 profile 和 pattern,保留有价值的历史、日期、项目背景和变化过程;不要重复抄写 profile/pattern,不评判、不推测,不能清空已有长期记忆。严格只输出 JSON:{"memory":"更新后的长期记忆全文;不能清空"}`;
617
+ export function memberCompressionSystemPrompt() {
618
+ return loadReflectionPrompt('member-compression.md');
409
619
  }
410
620
  function renderDailyEntries(entries) {
411
621
  const out = [];
@@ -431,8 +641,12 @@ function dateForTimestamp(value) {
431
641
  }
432
642
  export function parseJsonObject(text) {
433
643
  const trimmed = String(text || '').trim();
434
- const unfenced = trimmed.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '').trim();
435
- for (const candidate of [trimmed, unfenced]) {
644
+ const withoutThinking = trimmed
645
+ .replace(/<think>[\s\S]*?<\/think>/gi, '')
646
+ .replace(/^<thinking>[\s\S]*?<\/thinking>/i, '')
647
+ .trim();
648
+ const unfenced = withoutThinking.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '').trim();
649
+ for (const candidate of [trimmed, withoutThinking, unfenced]) {
436
650
  try {
437
651
  const parsed = JSON.parse(candidate);
438
652
  if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
@@ -479,6 +693,23 @@ export function parseJsonObject(text) {
479
693
  }
480
694
  throw new Error('记忆反思模型没有返回有效 JSON');
481
695
  }
696
+ export function validateMemberBatchPayload(value, allowedSenderIds) {
697
+ if (!value || typeof value !== 'object' || Array.isArray(value))
698
+ return false;
699
+ const members = value.members;
700
+ if (!Array.isArray(members))
701
+ return false;
702
+ const seen = new Set();
703
+ return members.every(item => {
704
+ if (!item || typeof item !== 'object' || Array.isArray(item))
705
+ return false;
706
+ const senderId = item.senderId;
707
+ if (typeof senderId !== 'string' || !allowedSenderIds.has(senderId) || seen.has(senderId))
708
+ return false;
709
+ seen.add(senderId);
710
+ return true;
711
+ });
712
+ }
482
713
  function stringifyDoc(value) {
483
714
  if (typeof value === 'string')
484
715
  return value;
@@ -488,6 +719,35 @@ function stringifyProfile(value) {
488
719
  const entries = normalizeProfileEntries(value);
489
720
  return JSON.stringify(entries, null, 2);
490
721
  }
722
+ function mergeProfile(existing, additions, removals) {
723
+ const current = normalizeProfileEntries(existing || '');
724
+ const removeTexts = new Set(normalizeProfileEntries(removals).map(entry => entry.text));
725
+ const additionsList = normalizeProfileEntries(additions).filter(entry => !removeTexts.has(entry.text));
726
+ const merged = [...current.filter(entry => !removeTexts.has(entry.text)), ...additionsList];
727
+ const unique = merged.filter((entry, index, all) => all.findIndex(item => item.type === entry.type && item.text === entry.text) === index);
728
+ return JSON.stringify(unique, null, 2);
729
+ }
730
+ function mergeTextDoc(existing, additions, removals) {
731
+ const remove = new Set(normalizeTextList(removals));
732
+ const lines = String(existing || '').split(/\r?\n/).map(line => line.trim()).filter(Boolean).filter(line => !remove.has(line));
733
+ const added = normalizeTextList(additions).filter(line => !lines.includes(line) && !remove.has(line));
734
+ return [...lines, ...added].join('\n');
735
+ }
736
+ function normalizeTextList(value) {
737
+ if (typeof value === 'string')
738
+ return value.trim() ? [value.trim()] : [];
739
+ if (!Array.isArray(value))
740
+ return [];
741
+ return value.flatMap(item => {
742
+ if (typeof item === 'string')
743
+ return item.trim() ? [item.trim()] : [];
744
+ if (item && typeof item === 'object') {
745
+ const text = item.text;
746
+ return typeof text === 'string' && text.trim() ? [text.trim()] : [];
747
+ }
748
+ return [];
749
+ });
750
+ }
491
751
  function normalizeProfileEntries(value) {
492
752
  let input = value;
493
753
  if (typeof input === 'string') {
@@ -558,8 +818,22 @@ function pickMemberDocs(docs) {
558
818
  function memberCursorKey(memberId) {
559
819
  return `memberReflection:${memberId}`;
560
820
  }
561
- function section(title, content) {
562
- return content ? `[${title}]\n${content}` : '';
821
+ function boundedMemorySections(parts, budget = MEMORY_INJECT_CHARS) {
822
+ let used = 0;
823
+ const output = [];
824
+ for (const [title, content] of parts) {
825
+ const value = content?.trim() || '';
826
+ if (!value || used >= budget)
827
+ continue;
828
+ const remaining = budget - used;
829
+ const isRecent = title.includes('近期沉淀') || title.includes('最近沉淀');
830
+ const bounded = (isRecent ? value.slice(-remaining) : value.slice(0, remaining)).trim();
831
+ if (!bounded)
832
+ continue;
833
+ output.push(`[${title}]\n${bounded}`);
834
+ used += bounded.length;
835
+ }
836
+ return output;
563
837
  }
564
838
  function errorMessage(error) {
565
839
  return error instanceof Error ? error.message : String(error);
@@ -299,6 +299,8 @@ function asSettingsPatch(value) {
299
299
  const patch = {};
300
300
  if (typeof record.memoryEnabled === 'boolean')
301
301
  patch.memoryEnabled = record.memoryEnabled;
302
+ if (typeof record.memoryMemberBatchEnabled === 'boolean')
303
+ patch.memoryMemberBatchEnabled = record.memoryMemberBatchEnabled;
302
304
  if (record.groupReceiveMode === 'auto' || record.groupReceiveMode === 'mention' || record.groupReceiveMode === 'silent') {
303
305
  patch.groupReceiveMode = record.groupReceiveMode;
304
306
  }