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.
- package/README.en.md +5 -4
- package/README.md +3 -3
- package/client-src/plugin.cts +2 -2
- package/client-src/settings.cts +18 -1
- package/client-src/shared.cts +4 -4
- package/cordis.patch.yml +5 -0
- package/docs/ARCHITECTURE.en.md +1 -1
- package/docs/ARCHITECTURE.md +2 -1
- package/docs/DEVELOPMENT.md +3 -1
- package/docs/MEMORY.md +32 -4
- package/docs/MEMORY_CONTEXT.md +16 -0
- package/docs/README.md +2 -0
- package/docs/STREAMING.md +2 -2
- 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 +21 -4
- package/lib/config.js +3 -2
- package/lib/index.d.ts +2 -2
- package/lib/index.js +2 -3
- package/lib/session/agent-bridge.d.ts +3 -4
- package/lib/session/agent-bridge.js +40 -46
- package/lib/session/memory-snapshot.d.ts +4 -0
- package/lib/session/memory-snapshot.js +6 -2
- package/lib/session/runtime.js +15 -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 +24 -23
- 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 +2 -3
- package/src/session/agent-bridge.ts +40 -47
- package/src/session/memory-snapshot.ts +6 -3
- package/src/session/runtime.ts +14 -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/storage/memory.js
CHANGED
|
@@ -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 =
|
|
7
|
-
const GROUP_DAILY_KEEP_RECENT =
|
|
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
|
-
|
|
59
|
-
|
|
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.
|
|
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
|
-
|
|
77
|
-
|
|
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
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
|
|
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
|
|
186
|
-
if (
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
this.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
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
|
-
|
|
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.
|
|
280
|
-
this.db.setMemoryDoc('member', memberId, 'profile',
|
|
281
|
-
|
|
282
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
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
|
|
614
|
+
export function groupCompressionSystemPrompt() {
|
|
615
|
+
return loadReflectionPrompt('group-compression.md');
|
|
406
616
|
}
|
|
407
|
-
function memberCompressionSystemPrompt() {
|
|
408
|
-
return
|
|
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
|
|
435
|
-
|
|
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
|
|
562
|
-
|
|
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);
|
package/lib/transport/rpc.js
CHANGED
|
@@ -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
|
}
|