dsh-qqchat 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.en.md +1 -1
- package/README.md +1 -1
- package/client-src/plugin.cts +2 -1
- package/client-src/settings.cts +18 -1
- package/client-src/shared.cts +43 -2
- package/docs/MEMORY.md +32 -4
- package/docs/MEMORY_CONTEXT.md +16 -0
- package/docs/README.md +2 -0
- package/docs/prds/PRD-MEM-1-/347/276/244/350/256/260/345/277/206/344/270/216/347/276/244/345/217/213/350/256/260/345/277/206/347/255/226/347/225/245/350/260/203/346/225/264.md +396 -0
- package/docs/prds/README.md +62 -0
- package/lib/client.js +64 -2
- package/lib/config.js +3 -2
- package/lib/index.js +3 -1
- package/lib/session/memory-snapshot.d.ts +4 -0
- package/lib/session/memory-snapshot.js +6 -2
- package/lib/session/runtime.js +14 -5
- package/lib/storage/db.d.ts +6 -1
- package/lib/storage/db.js +57 -0
- package/lib/storage/memory.d.ts +28 -2
- package/lib/storage/memory.js +375 -101
- package/lib/transport/rpc.js +2 -0
- package/lib/types.d.ts +26 -0
- package/package.json +2 -1
- package/prompts/group-compression.md +5 -0
- package/prompts/group-reflection.md +39 -0
- package/prompts/member-batch-reflection.md +39 -0
- package/prompts/member-compression.md +5 -0
- package/prompts/private-reflection.md +32 -0
- package/src/config.ts +3 -2
- package/src/index.ts +3 -1
- package/src/session/agent-bridge.ts +3 -1
- package/src/session/memory-snapshot.ts +6 -3
- package/src/session/runtime.ts +13 -4
- package/src/storage/db.ts +70 -0
- package/src/storage/memory.ts +343 -84
- package/src/transport/rpc.ts +1 -0
- package/src/types.ts +30 -0
package/src/storage/memory.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { Context } from '@deepseek-ai/cordis'
|
|
2
2
|
import { BlockAssembler, createUserMessage } from '@deepseek-ai/dsh-llm'
|
|
3
3
|
import { SessionId } from '@deepseek-ai/dsh-session'
|
|
4
|
+
import { readFileSync } from 'node:fs'
|
|
4
5
|
import type { QQChatDatabase } from './db.js'
|
|
5
6
|
import type {
|
|
6
7
|
GroupMemberRow,
|
|
@@ -15,6 +16,7 @@ import type {
|
|
|
15
16
|
QQChatConfig,
|
|
16
17
|
DailyEntry,
|
|
17
18
|
ReflectionPayload,
|
|
19
|
+
MemberBatchReflectionPayload,
|
|
18
20
|
ProfileEntry,
|
|
19
21
|
ProfileEntryType,
|
|
20
22
|
QQNormalizedMessage,
|
|
@@ -23,6 +25,10 @@ import type {
|
|
|
23
25
|
const DOCS = ['profile', 'summary', 'daily', 'memory', 'pattern'] as const satisfies readonly MemoryDocType[]
|
|
24
26
|
|
|
25
27
|
interface MemberReflectionPayload {
|
|
28
|
+
profile_add?: unknown
|
|
29
|
+
profile_remove?: unknown
|
|
30
|
+
pattern_add?: unknown
|
|
31
|
+
pattern_remove?: unknown
|
|
26
32
|
profile?: unknown
|
|
27
33
|
pattern?: unknown
|
|
28
34
|
summary?: unknown
|
|
@@ -32,16 +38,25 @@ interface MemberReflectionPayload {
|
|
|
32
38
|
|
|
33
39
|
const MEMBER_DAILY_COMPACT_AT = 100
|
|
34
40
|
const MEMBER_DAILY_KEEP_RECENT = 50
|
|
35
|
-
const GROUP_DAILY_COMPACT_AT =
|
|
36
|
-
const GROUP_DAILY_KEEP_RECENT =
|
|
41
|
+
const GROUP_DAILY_COMPACT_AT = 200
|
|
42
|
+
const GROUP_DAILY_KEEP_RECENT = 100
|
|
43
|
+
const MEMBER_BATCH_SIZE = 50
|
|
44
|
+
const REFLECTION_RETRY_COOLDOWN_MS = 60_000
|
|
45
|
+
/** 与咕咕一致:每个记忆 scope 的本轮直注入预算(按字符计)。 */
|
|
46
|
+
export const MEMORY_INJECT_CHARS = 2_000
|
|
37
47
|
|
|
38
48
|
export class MemoryEngine {
|
|
39
49
|
private readonly timers = new Map<number, ReturnType<typeof setTimeout>>()
|
|
40
50
|
private readonly memberTimers = new Map<number, ReturnType<typeof setTimeout>>()
|
|
51
|
+
private readonly memberBatchTimers = new Map<number, ReturnType<typeof setTimeout>>()
|
|
41
52
|
private readonly routes = new Map<number, ModelRoute>()
|
|
42
53
|
private readonly memberRoutes = new Map<number, ModelRoute>()
|
|
43
54
|
private readonly reflectingGroups = new Map<number, Promise<MemoryView>>()
|
|
44
55
|
private readonly reflectingMembers = new Map<number, Promise<MemoryDocuments>>()
|
|
56
|
+
private readonly reflectingMemberBatches = new Map<number, Promise<MemoryView>>()
|
|
57
|
+
private readonly groupRetryAt = new Map<number, number>()
|
|
58
|
+
private readonly memberRetryAt = new Map<number, number>()
|
|
59
|
+
private readonly memberBatchRetryAt = new Map<number, number>()
|
|
45
60
|
|
|
46
61
|
constructor(
|
|
47
62
|
private readonly ctx: Context,
|
|
@@ -53,10 +68,16 @@ export class MemoryEngine {
|
|
|
53
68
|
dispose(): void {
|
|
54
69
|
for (const timer of this.timers.values()) clearTimeout(timer)
|
|
55
70
|
for (const timer of this.memberTimers.values()) clearTimeout(timer)
|
|
71
|
+
for (const timer of this.memberBatchTimers.values()) clearTimeout(timer)
|
|
56
72
|
this.timers.clear()
|
|
57
73
|
this.memberTimers.clear()
|
|
74
|
+
this.memberBatchTimers.clear()
|
|
58
75
|
this.reflectingGroups.clear()
|
|
59
76
|
this.reflectingMembers.clear()
|
|
77
|
+
this.reflectingMemberBatches.clear()
|
|
78
|
+
this.groupRetryAt.clear()
|
|
79
|
+
this.memberRetryAt.clear()
|
|
80
|
+
this.memberBatchRetryAt.clear()
|
|
60
81
|
}
|
|
61
82
|
|
|
62
83
|
setRoute(groupId: number, provider: string, model: string, sessionId: string): void {
|
|
@@ -74,14 +95,11 @@ export class MemoryEngine {
|
|
|
74
95
|
if (!this.routes.has(groupId)) return
|
|
75
96
|
const existing = this.timers.get(groupId)
|
|
76
97
|
if (existing) clearTimeout(existing)
|
|
77
|
-
if (this.db.unreflectedCount(groupId) >= this.config.reflectionBatchSize) {
|
|
78
|
-
void this.reflectNow(groupId).catch(error => this.logger.warn?.(`[dsh-qqchat] memory reflection: ${errorMessage(error)}`))
|
|
79
|
-
return
|
|
80
|
-
}
|
|
81
98
|
const timer = setTimeout(() => {
|
|
82
99
|
this.timers.delete(groupId)
|
|
83
|
-
|
|
84
|
-
|
|
100
|
+
if (this.inRetryCooldown(this.groupRetryAt, groupId)) this.schedule(groupId)
|
|
101
|
+
else void this.runScheduledGroupReflection(groupId)
|
|
102
|
+
}, this.reflectionDelay(this.groupRetryAt, groupId))
|
|
85
103
|
timer.unref?.()
|
|
86
104
|
this.timers.set(groupId, timer)
|
|
87
105
|
}
|
|
@@ -91,18 +109,80 @@ export class MemoryEngine {
|
|
|
91
109
|
if (!this.memberRoutes.has(memberId)) return
|
|
92
110
|
const existing = this.memberTimers.get(memberId)
|
|
93
111
|
if (existing) clearTimeout(existing)
|
|
94
|
-
if (this.unreflectedDirectMessages(memberId, this.config.reflectionBatchSize).length >= this.config.reflectionBatchSize) {
|
|
95
|
-
void this.
|
|
112
|
+
if (this.unreflectedDirectMessages(memberId, this.config.reflectionBatchSize).length >= this.config.reflectionBatchSize && !this.inRetryCooldown(this.memberRetryAt, memberId)) {
|
|
113
|
+
void this.runScheduledMemberReflection(memberId)
|
|
96
114
|
return
|
|
97
115
|
}
|
|
98
116
|
const timer = setTimeout(() => {
|
|
99
117
|
this.memberTimers.delete(memberId)
|
|
100
|
-
|
|
101
|
-
|
|
118
|
+
if (this.inRetryCooldown(this.memberRetryAt, memberId)) this.scheduleMember(memberId)
|
|
119
|
+
else void this.runScheduledMemberReflection(memberId)
|
|
120
|
+
}, this.reflectionDelay(this.memberRetryAt, memberId))
|
|
102
121
|
timer.unref?.()
|
|
103
122
|
this.memberTimers.set(memberId, timer)
|
|
104
123
|
}
|
|
105
124
|
|
|
125
|
+
scheduleGroupMembers(groupId: number): void {
|
|
126
|
+
groupId = Number(groupId)
|
|
127
|
+
if (!this.routes.has(groupId)) return
|
|
128
|
+
const existing = this.memberBatchTimers.get(groupId)
|
|
129
|
+
if (existing) clearTimeout(existing)
|
|
130
|
+
if (this.db.unreflectedMemberCount(groupId) >= MEMBER_BATCH_SIZE && !this.inRetryCooldown(this.memberBatchRetryAt, groupId)) {
|
|
131
|
+
void this.runScheduledMemberBatchReflection(groupId)
|
|
132
|
+
return
|
|
133
|
+
}
|
|
134
|
+
const timer = setTimeout(() => {
|
|
135
|
+
this.memberBatchTimers.delete(groupId)
|
|
136
|
+
if (this.inRetryCooldown(this.memberBatchRetryAt, groupId)) this.scheduleGroupMembers(groupId)
|
|
137
|
+
else void this.runScheduledMemberBatchReflection(groupId)
|
|
138
|
+
}, this.reflectionDelay(this.memberBatchRetryAt, groupId))
|
|
139
|
+
timer.unref?.()
|
|
140
|
+
this.memberBatchTimers.set(groupId, timer)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
private inRetryCooldown(retryAt: Map<number, number>, id: number): boolean {
|
|
144
|
+
const until = retryAt.get(id) || 0
|
|
145
|
+
if (until <= Date.now()) {
|
|
146
|
+
retryAt.delete(id)
|
|
147
|
+
return false
|
|
148
|
+
}
|
|
149
|
+
return true
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
private reflectionDelay(retryAt: Map<number, number>, id: number): number {
|
|
153
|
+
return Math.max(this.config.reflectionIdleMs, (retryAt.get(id) || 0) - Date.now())
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
private async runScheduledGroupReflection(groupId: number): Promise<void> {
|
|
157
|
+
try {
|
|
158
|
+
await this.reflectNow(groupId)
|
|
159
|
+
this.groupRetryAt.delete(groupId)
|
|
160
|
+
} catch (error) {
|
|
161
|
+
this.groupRetryAt.set(groupId, Date.now() + REFLECTION_RETRY_COOLDOWN_MS)
|
|
162
|
+
this.logger.warn?.(`[dsh-qqchat] memory reflection: ${errorMessage(error)}`)
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private async runScheduledMemberReflection(memberId: number): Promise<void> {
|
|
167
|
+
try {
|
|
168
|
+
await this.reflectMemberNow(memberId)
|
|
169
|
+
this.memberRetryAt.delete(memberId)
|
|
170
|
+
} catch (error) {
|
|
171
|
+
this.memberRetryAt.set(memberId, Date.now() + REFLECTION_RETRY_COOLDOWN_MS)
|
|
172
|
+
this.logger.warn?.(`[dsh-qqchat] member memory reflection: ${errorMessage(error)}`)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
private async runScheduledMemberBatchReflection(groupId: number): Promise<void> {
|
|
177
|
+
try {
|
|
178
|
+
await this.reflectMembersNow(groupId)
|
|
179
|
+
this.memberBatchRetryAt.delete(groupId)
|
|
180
|
+
} catch (error) {
|
|
181
|
+
this.memberBatchRetryAt.set(groupId, Date.now() + REFLECTION_RETRY_COOLDOWN_MS)
|
|
182
|
+
this.logger.warn?.(`[dsh-qqchat] member batch reflection: ${errorMessage(error)}`)
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
106
186
|
async reflectNow(groupId: number): Promise<MemoryView> {
|
|
107
187
|
groupId = Number(groupId)
|
|
108
188
|
const existing = this.reflectingGroups.get(groupId)
|
|
@@ -126,21 +206,20 @@ export class MemoryEngine {
|
|
|
126
206
|
if (!view) throw new Error('群不存在')
|
|
127
207
|
return view
|
|
128
208
|
}
|
|
209
|
+
const taskKey = `group:${groupId}:${Number(messages[0]?.id)}:${Number(messages.at(-1)?.id)}`
|
|
210
|
+
if (!this.db.startReflectionTask({
|
|
211
|
+
scopeType: 'group', scopeKey: groupId, taskType: 'group',
|
|
212
|
+
startMessageId: Number(messages[0]?.id), endMessageId: Number(messages.at(-1)?.id), idempotencyKey: taskKey,
|
|
213
|
+
})) {
|
|
214
|
+
const view = this.memoryView(groupId)
|
|
215
|
+
if (!view) throw new Error('群不存在')
|
|
216
|
+
return view
|
|
217
|
+
}
|
|
218
|
+
const startedAt = Date.now()
|
|
219
|
+
try {
|
|
129
220
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
const memberMemory = Object.fromEntries(
|
|
133
|
-
members.map(member => [member.platform_user_id, {
|
|
134
|
-
profile: this.db.memoryDocs('member', Number(member.id)).profile || '',
|
|
135
|
-
pattern: this.db.memoryDocs('member', Number(member.id)).pattern || '',
|
|
136
|
-
summary: this.db.memoryDocs('member', Number(member.id)).summary || '',
|
|
137
|
-
memory: this.db.memoryDocs('member', Number(member.id)).memory || '',
|
|
138
|
-
name: member.display_name || '',
|
|
139
|
-
aliases: parseStringList(member.aliases_json),
|
|
140
|
-
nicknames: parseStringList(member.nicknames_json),
|
|
141
|
-
}]),
|
|
142
|
-
)
|
|
143
|
-
const transcript = messages.map(message => ({
|
|
221
|
+
const currentGroup = this.db.memoryDocs('group', groupId)
|
|
222
|
+
const transcript = messages.map(message => ({
|
|
144
223
|
id: Number(message.id),
|
|
145
224
|
at: message.created_at,
|
|
146
225
|
direction: message.direction,
|
|
@@ -149,21 +228,103 @@ export class MemoryEngine {
|
|
|
149
228
|
text: message.content,
|
|
150
229
|
quotedText: message.quoted_text || '',
|
|
151
230
|
...(parseReflectionQuote(message.quote_json) ? { quote: parseReflectionQuote(message.quote_json) } : {}),
|
|
152
|
-
|
|
231
|
+
}))
|
|
153
232
|
|
|
233
|
+
const input = {
|
|
234
|
+
group: { id: group.platform_group_id, name: group.name || '' },
|
|
235
|
+
existing: { group: pickDocs(currentGroup) },
|
|
236
|
+
messages: transcript,
|
|
237
|
+
}
|
|
238
|
+
const reflected = await this.generateReflection(route, memorySystemPrompt(), input, `qqchat-memory-${groupId}`) as ReflectionPayload
|
|
239
|
+
this.applyGroupReflection(groupId, reflected)
|
|
240
|
+
await this.compactDaily('group', groupId, route, GROUP_DAILY_COMPACT_AT, GROUP_DAILY_KEEP_RECENT, `qqchat-group-compress-${groupId}`)
|
|
241
|
+
const lastMessage = messages.at(-1)
|
|
242
|
+
if (lastMessage) this.db.markReflected(groupId, Number(lastMessage.id))
|
|
243
|
+
this.db.finishReflectionTask(taskKey, 'completed')
|
|
244
|
+
this.logger.info?.(`[dsh-qqchat] memory reflection completed task=group messages=${messages.length} durationMs=${Date.now() - startedAt}`)
|
|
245
|
+
const view = this.memoryView(groupId)
|
|
246
|
+
if (!view) throw new Error('群不存在')
|
|
247
|
+
return view
|
|
248
|
+
} catch (error) {
|
|
249
|
+
this.db.finishReflectionTask(taskKey, 'failed')
|
|
250
|
+
this.logger.warn?.(`[dsh-qqchat] memory reflection failed task=group messages=${messages.length} durationMs=${Date.now() - startedAt}`)
|
|
251
|
+
throw error
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async reflectMembersNow(groupId: number): Promise<MemoryView> {
|
|
256
|
+
groupId = Number(groupId)
|
|
257
|
+
const existing = this.reflectingMemberBatches.get(groupId)
|
|
258
|
+
if (existing) return existing
|
|
259
|
+
const current = this.reflectMembersNowInternal(groupId)
|
|
260
|
+
this.reflectingMemberBatches.set(groupId, current)
|
|
261
|
+
return current.finally(() => {
|
|
262
|
+
if (this.reflectingMemberBatches.get(groupId) === current) this.reflectingMemberBatches.delete(groupId)
|
|
263
|
+
})
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
private async reflectMembersNowInternal(groupId: number): Promise<MemoryView> {
|
|
267
|
+
groupId = Number(groupId)
|
|
268
|
+
const route = this.routes.get(groupId)
|
|
269
|
+
if (!route) throw new Error('这个群还没有可复用的 DSH 模型路由;先让 Agent 在群里完成一次回复')
|
|
270
|
+
const group = this.db.groupById(groupId)
|
|
271
|
+
if (!group) throw new Error('群不存在')
|
|
272
|
+
const messages = this.db.unreflectedMemberMessages(groupId, this.config.reflectionMaxMessages)
|
|
273
|
+
if (messages.length === 0) {
|
|
274
|
+
const view = this.memoryView(groupId)
|
|
275
|
+
if (!view) throw new Error('群不存在')
|
|
276
|
+
return view
|
|
277
|
+
}
|
|
278
|
+
const taskKey = `member-batch:${groupId}:${Number(messages[0]?.id)}:${Number(messages.at(-1)?.id)}`
|
|
279
|
+
if (!this.db.startReflectionTask({
|
|
280
|
+
scopeType: 'group', scopeKey: groupId, taskType: 'member-batch',
|
|
281
|
+
startMessageId: Number(messages[0]?.id), endMessageId: Number(messages.at(-1)?.id), idempotencyKey: taskKey,
|
|
282
|
+
})) {
|
|
283
|
+
const view = this.memoryView(groupId)
|
|
284
|
+
if (!view) throw new Error('群不存在')
|
|
285
|
+
return view
|
|
286
|
+
}
|
|
287
|
+
const startedAt = Date.now()
|
|
288
|
+
try {
|
|
289
|
+
const senderIds = new Set(messages
|
|
290
|
+
.filter(message => message.direction !== 'outbound' && message.platform_user_id)
|
|
291
|
+
.map(message => message.platform_user_id as string))
|
|
292
|
+
const members = this.db.listGroupMembers(groupId).filter(member => senderIds.has(member.platform_user_id))
|
|
154
293
|
const input = {
|
|
155
294
|
group: { id: group.platform_group_id, name: group.name || '' },
|
|
156
|
-
|
|
157
|
-
|
|
295
|
+
members: members.map(member => ({
|
|
296
|
+
senderId: member.platform_user_id,
|
|
297
|
+
displayName: member.display_name || '',
|
|
298
|
+
existing: pickMemberDocs(this.db.memoryDocs('member', Number(member.id))),
|
|
299
|
+
})),
|
|
300
|
+
messages: messages.map(message => ({
|
|
301
|
+
id: Number(message.id),
|
|
302
|
+
at: message.created_at,
|
|
303
|
+
direction: message.direction,
|
|
304
|
+
senderId: message.direction === 'outbound' ? 'BOT' : message.platform_user_id,
|
|
305
|
+
senderName: message.direction === 'outbound' ? 'DSH Agent' : (message.display_name || ''),
|
|
306
|
+
text: message.content,
|
|
307
|
+
quotedText: message.quoted_text || '',
|
|
308
|
+
...(parseReflectionQuote(message.quote_json) ? { quote: parseReflectionQuote(message.quote_json) } : {}),
|
|
309
|
+
})),
|
|
310
|
+
}
|
|
311
|
+
const reflected = await this.generateReflection(route, memberBatchMemorySystemPrompt(), input, `qqchat-member-batch-${groupId}`)
|
|
312
|
+
if (!validateMemberBatchPayload(reflected, new Set(members.map(member => member.platform_user_id)))) {
|
|
313
|
+
throw new Error('成员批量反思输出包含无效成员或格式')
|
|
314
|
+
}
|
|
315
|
+
this.applyMemberBatchReflection(groupId, members, reflected, messages.at(-1)?.created_at)
|
|
316
|
+
const lastMessage = messages.at(-1)
|
|
317
|
+
if (lastMessage) this.db.markMembersReflected(groupId, Number(lastMessage.id))
|
|
318
|
+
this.db.finishReflectionTask(taskKey, 'completed')
|
|
319
|
+
this.logger.info?.(`[dsh-qqchat] memory reflection completed task=member-batch messages=${messages.length} members=${members.length} durationMs=${Date.now() - startedAt}`)
|
|
320
|
+
const view = this.memoryView(groupId)
|
|
321
|
+
if (!view) throw new Error('群不存在')
|
|
322
|
+
return view
|
|
323
|
+
} catch (error) {
|
|
324
|
+
this.db.finishReflectionTask(taskKey, 'failed')
|
|
325
|
+
this.logger.warn?.(`[dsh-qqchat] memory reflection failed task=member-batch messages=${messages.length} durationMs=${Date.now() - startedAt}`)
|
|
326
|
+
throw error
|
|
158
327
|
}
|
|
159
|
-
const reflected = await this.generateReflection(route, memorySystemPrompt(), input, `qqchat-memory-${groupId}`) as ReflectionPayload
|
|
160
|
-
this.applyReflection(groupId, members, reflected)
|
|
161
|
-
await this.compactDaily('group', groupId, route, GROUP_DAILY_COMPACT_AT, GROUP_DAILY_KEEP_RECENT, `qqchat-group-compress-${groupId}`)
|
|
162
|
-
const lastMessage = messages.at(-1)
|
|
163
|
-
if (lastMessage) this.db.markReflected(groupId, Number(lastMessage.id))
|
|
164
|
-
const view = this.memoryView(groupId)
|
|
165
|
-
if (!view) throw new Error('群不存在')
|
|
166
|
-
return view
|
|
167
328
|
}
|
|
168
329
|
|
|
169
330
|
async reflectMemberNow(memberId: number): Promise<MemoryDocuments> {
|
|
@@ -203,19 +364,37 @@ export class MemoryEngine {
|
|
|
203
364
|
...(parseReflectionQuote(message.quote_json) ? { quote: parseReflectionQuote(message.quote_json) } : {}),
|
|
204
365
|
})),
|
|
205
366
|
}
|
|
206
|
-
const
|
|
207
|
-
if (
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
this.
|
|
367
|
+
const taskKey = `private-owner:${memberId}:${Number(messages[0]?.id)}:${Number(messages.at(-1)?.id)}`
|
|
368
|
+
if (!this.db.startReflectionTask({
|
|
369
|
+
scopeType: 'member', scopeKey: memberId, taskType: 'private-owner',
|
|
370
|
+
startMessageId: Number(messages[0]?.id), endMessageId: Number(messages.at(-1)?.id), idempotencyKey: taskKey,
|
|
371
|
+
})) return this.db.memoryDocs('member', memberId)
|
|
372
|
+
const startedAt = Date.now()
|
|
373
|
+
try {
|
|
374
|
+
const reflected = await this.generateReflection(route, privateMemorySystemPrompt(), input, `qqchat-private-memory-${memberId}`) as MemberReflectionPayload
|
|
375
|
+
if (reflected.profile_add !== undefined || reflected.profile_remove !== undefined) {
|
|
376
|
+
this.db.setMemoryDoc('member', memberId, 'profile', mergeProfile(this.db.memoryDocs('member', memberId).profile, reflected.profile_add, reflected.profile_remove))
|
|
377
|
+
} else if (reflected.profile !== undefined) this.db.setMemoryDoc('member', memberId, 'profile', stringifyProfile(reflected.profile))
|
|
378
|
+
if (reflected.pattern_add !== undefined || reflected.pattern_remove !== undefined) {
|
|
379
|
+
this.db.setMemoryDoc('member', memberId, 'pattern', mergeTextDoc(this.db.memoryDocs('member', memberId).pattern, reflected.pattern_add, reflected.pattern_remove))
|
|
380
|
+
} else if (reflected.pattern !== undefined) this.db.setMemoryDoc('member', memberId, 'pattern', stringifyDoc(reflected.pattern))
|
|
381
|
+
if (reflected.summary !== undefined) this.db.setMemoryDoc('member', memberId, 'summary', stringifyDoc(reflected.summary))
|
|
382
|
+
if (reflected.memory !== undefined) this.db.setMemoryDoc('member', memberId, 'memory', stringifyMemory(reflected.memory))
|
|
383
|
+
if (reflected.daily !== undefined) {
|
|
384
|
+
const last = messages.at(-1)
|
|
385
|
+
this.db.appendDailyDoc('member', memberId, dateForTimestamp(last?.created_at), stringifyDoc(reflected.daily))
|
|
386
|
+
}
|
|
387
|
+
await this.compactDaily('member', memberId, route, MEMBER_DAILY_COMPACT_AT, MEMBER_DAILY_KEEP_RECENT, `qqchat-member-compress-${memberId}`)
|
|
388
|
+
const lastMessage = messages.at(-1)
|
|
389
|
+
if (lastMessage) this.db.setSetting(memberCursorKey(memberId), Number(lastMessage.id))
|
|
390
|
+
this.db.finishReflectionTask(taskKey, 'completed')
|
|
391
|
+
this.logger.info?.(`[dsh-qqchat] memory reflection completed task=private-owner messages=${messages.length} durationMs=${Date.now() - startedAt}`)
|
|
392
|
+
return this.db.memoryDocs('member', memberId)
|
|
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
|
|
214
397
|
}
|
|
215
|
-
await this.compactDaily('member', memberId, route, MEMBER_DAILY_COMPACT_AT, MEMBER_DAILY_KEEP_RECENT, `qqchat-member-compress-${memberId}`)
|
|
216
|
-
const lastMessage = messages.at(-1)
|
|
217
|
-
if (lastMessage) this.db.setSetting(memberCursorKey(memberId), Number(lastMessage.id))
|
|
218
|
-
return this.db.memoryDocs('member', memberId)
|
|
219
398
|
}
|
|
220
399
|
|
|
221
400
|
private async compactDaily(
|
|
@@ -280,7 +459,7 @@ export class MemoryEngine {
|
|
|
280
459
|
.slice(-limit)
|
|
281
460
|
}
|
|
282
461
|
|
|
283
|
-
private
|
|
462
|
+
private applyGroupReflection(groupId: number, reflected: ReflectionPayload): void {
|
|
284
463
|
const group = reflected.group || {}
|
|
285
464
|
if (group.profile !== undefined) this.db.setMemoryDoc('group', groupId, 'profile', stringifyDoc(group.profile))
|
|
286
465
|
if (group.summary !== undefined) this.db.setMemoryDoc('group', groupId, 'summary', stringifyDoc(group.summary))
|
|
@@ -289,15 +468,25 @@ export class MemoryEngine {
|
|
|
289
468
|
const stamp = new Date().toISOString().slice(0, 10)
|
|
290
469
|
this.db.appendDailyDoc('group', groupId, stamp, stringifyDoc(group.daily))
|
|
291
470
|
}
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
private applyMemberBatchReflection(groupId: number, members: GroupMemberRow[], reflected: MemberBatchReflectionPayload, createdAt?: number): void {
|
|
292
474
|
const byOpenid = new Map(members.map(member => [member.platform_user_id, Number(member.id)]))
|
|
293
475
|
const updates = Array.isArray(reflected.members) ? reflected.members : []
|
|
294
476
|
for (const update of updates) {
|
|
295
477
|
const memberId = byOpenid.get(String(update.senderId || ''))
|
|
296
478
|
if (!memberId) continue
|
|
297
|
-
if (update.
|
|
298
|
-
|
|
479
|
+
if (update.profile_add !== undefined || update.profile_remove !== undefined) {
|
|
480
|
+
this.db.setMemoryDoc('member', memberId, 'profile', mergeProfile(this.db.memoryDocs('member', memberId).profile, update.profile_add, update.profile_remove))
|
|
481
|
+
}
|
|
482
|
+
if (update.pattern_add !== undefined || update.pattern_remove !== undefined) {
|
|
483
|
+
this.db.setMemoryDoc('member', memberId, 'pattern', mergeTextDoc(this.db.memoryDocs('member', memberId).pattern, update.pattern_add, update.pattern_remove))
|
|
484
|
+
}
|
|
299
485
|
if (update.summary !== undefined) this.db.setMemoryDoc('member', memberId, 'summary', stringifyDoc(update.summary))
|
|
300
486
|
if (update.memory !== undefined) this.db.setMemoryDoc('member', memberId, 'memory', stringifyMemory(update.memory))
|
|
487
|
+
if (update.daily !== undefined && stringifyDoc(update.daily).trim()) {
|
|
488
|
+
this.db.appendDailyDoc('member', memberId, dateForTimestamp(createdAt), stringifyDoc(update.daily))
|
|
489
|
+
}
|
|
301
490
|
if (Array.isArray(update.nicknames)) {
|
|
302
491
|
for (const nickname of update.nicknames) {
|
|
303
492
|
if (typeof nickname === 'string') this.db.addGroupMemberNickname(groupId, memberId, nickname)
|
|
@@ -312,20 +501,26 @@ export class MemoryEngine {
|
|
|
312
501
|
): string {
|
|
313
502
|
const groupDocs = this.db.memoryDocs('group', Number(group.id))
|
|
314
503
|
const memberDocs = currentMember ? this.db.memoryDocs('member', Number(currentMember.id)) : {}
|
|
504
|
+
const groupMemory = boundedMemorySections([
|
|
505
|
+
['群画像', groupDocs.profile],
|
|
506
|
+
['群摘要', groupDocs.summary],
|
|
507
|
+
['群长期记忆', groupDocs.memory],
|
|
508
|
+
['群最近沉淀', groupDocs.daily],
|
|
509
|
+
])
|
|
510
|
+
const memberMemory = currentMember ? boundedMemorySections([
|
|
511
|
+
['当前成员画像', memberDocs.profile],
|
|
512
|
+
['当前成员模式', memberDocs.pattern],
|
|
513
|
+
['当前成员摘要', memberDocs.summary],
|
|
514
|
+
['当前成员长期记忆', memberDocs.memory],
|
|
515
|
+
]) : []
|
|
315
516
|
return [
|
|
316
517
|
'[QQ 群聊上下文快照]',
|
|
317
518
|
'以下 sender ID / group ID 是可靠平台元数据。不要根据昵称猜身份,不要把其他群成员的兴趣、关系或记忆归到当前发言人。',
|
|
318
519
|
`群ID=${group.platform_group_id}`,
|
|
319
520
|
group.name ? `群名=${group.name}` : '',
|
|
320
|
-
|
|
321
|
-
section('群摘要', groupDocs.summary),
|
|
322
|
-
section('群长期记忆', groupDocs.memory),
|
|
323
|
-
section('群最近沉淀', groupDocs.daily),
|
|
521
|
+
...groupMemory,
|
|
324
522
|
currentMember ? `当前成员ID=${currentMember.platform_user_id}\n当前成员显示名=${currentMember.display_name || ''}` : '',
|
|
325
|
-
|
|
326
|
-
section('当前成员模式', memberDocs.pattern),
|
|
327
|
-
section('当前成员摘要', memberDocs.summary),
|
|
328
|
-
section('当前成员长期记忆', memberDocs.memory),
|
|
523
|
+
...memberMemory,
|
|
329
524
|
].filter(Boolean).join('\n')
|
|
330
525
|
}
|
|
331
526
|
|
|
@@ -336,11 +531,13 @@ export class MemoryEngine {
|
|
|
336
531
|
`成员ID=${member.platform_user_id}`,
|
|
337
532
|
`显示名=${member.display_name || ''}`,
|
|
338
533
|
'成员ID 是可靠平台身份;昵称仅供展示。',
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
534
|
+
...boundedMemorySections([
|
|
535
|
+
['成员画像', docs.profile],
|
|
536
|
+
['行为模式', docs.pattern],
|
|
537
|
+
['成员摘要', docs.summary],
|
|
538
|
+
['成员近期沉淀', docs.daily],
|
|
539
|
+
['成员长期记忆', docs.memory],
|
|
540
|
+
]),
|
|
344
541
|
].filter(Boolean).join('\n')
|
|
345
542
|
}
|
|
346
543
|
|
|
@@ -378,17 +575,12 @@ export class MemoryEngine {
|
|
|
378
575
|
}
|
|
379
576
|
}
|
|
380
577
|
|
|
381
|
-
function memorySystemPrompt(): string {
|
|
382
|
-
return
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
5. daily 只写本批消息值得留档的新进展;memory 保留真正长期有用的项目、关系、约定和背景。
|
|
388
|
-
6. 输出严格 JSON,不要 Markdown 代码块、解释或额外文字。
|
|
389
|
-
输出结构:
|
|
390
|
-
{"group":{"profile":{},"summary":"","daily":"","memory":["..."]},"members":[{"senderId":"可靠ID","profile":[{"type":"name|address|pronoun|background|preference|note","text":"...","ts":0}],"pattern":{},"summary":"","memory":"","nicknames":["群内称呼"]}]}
|
|
391
|
-
没有变化的成员可省略;不要为 BOT 创建成员记忆。`
|
|
578
|
+
export function memorySystemPrompt(): string {
|
|
579
|
+
return loadReflectionPrompt('group-reflection.md')
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
export function memberBatchMemorySystemPrompt(): string {
|
|
583
|
+
return loadReflectionPrompt('member-batch-reflection.md')
|
|
392
584
|
}
|
|
393
585
|
|
|
394
586
|
function memberMemorySystemPrompt(): string {
|
|
@@ -401,6 +593,14 @@ function memberMemorySystemPrompt(): string {
|
|
|
401
593
|
输出结构:{"profile":[{"type":"name|address|pronoun|background|preference|note","text":"...","ts":0}],"pattern":{},"summary":"","memory":"","daily":""}`
|
|
402
594
|
}
|
|
403
595
|
|
|
596
|
+
export function privateMemorySystemPrompt(): string {
|
|
597
|
+
return loadReflectionPrompt('private-reflection.md')
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
function loadReflectionPrompt(filename: string): string {
|
|
601
|
+
return readFileSync(new URL(`../../prompts/${filename}`, import.meta.url), 'utf8').trim()
|
|
602
|
+
}
|
|
603
|
+
|
|
404
604
|
function parseReflectionQuote(value: string | null): { senderId?: string; senderName?: string; messageId?: string; text: string; attachments: Array<Record<string, unknown>> } | undefined {
|
|
405
605
|
if (!value) return undefined
|
|
406
606
|
try {
|
|
@@ -421,12 +621,12 @@ function parseReflectionQuote(value: string | null): { senderId?: string; sender
|
|
|
421
621
|
}
|
|
422
622
|
}
|
|
423
623
|
|
|
424
|
-
function groupCompressionSystemPrompt(): string {
|
|
425
|
-
return
|
|
624
|
+
export function groupCompressionSystemPrompt(): string {
|
|
625
|
+
return loadReflectionPrompt('group-compression.md')
|
|
426
626
|
}
|
|
427
627
|
|
|
428
|
-
function memberCompressionSystemPrompt(): string {
|
|
429
|
-
return
|
|
628
|
+
export function memberCompressionSystemPrompt(): string {
|
|
629
|
+
return loadReflectionPrompt('member-compression.md')
|
|
430
630
|
}
|
|
431
631
|
|
|
432
632
|
function renderDailyEntries(entries: DailyEntry[]): string {
|
|
@@ -454,8 +654,12 @@ function dateForTimestamp(value: number | null | undefined): string {
|
|
|
454
654
|
|
|
455
655
|
export function parseJsonObject(text: string): ReflectionPayload | MemberReflectionPayload {
|
|
456
656
|
const trimmed = String(text || '').trim()
|
|
457
|
-
const
|
|
458
|
-
|
|
657
|
+
const withoutThinking = trimmed
|
|
658
|
+
.replace(/<think>[\s\S]*?<\/think>/gi, '')
|
|
659
|
+
.replace(/^<thinking>[\s\S]*?<\/thinking>/i, '')
|
|
660
|
+
.trim()
|
|
661
|
+
const unfenced = withoutThinking.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '').trim()
|
|
662
|
+
for (const candidate of [trimmed, withoutThinking, unfenced]) {
|
|
459
663
|
try {
|
|
460
664
|
const parsed = JSON.parse(candidate) as unknown
|
|
461
665
|
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) return parsed as ReflectionPayload | MemberReflectionPayload
|
|
@@ -496,6 +700,20 @@ export function parseJsonObject(text: string): ReflectionPayload | MemberReflect
|
|
|
496
700
|
throw new Error('记忆反思模型没有返回有效 JSON')
|
|
497
701
|
}
|
|
498
702
|
|
|
703
|
+
export function validateMemberBatchPayload(value: unknown, allowedSenderIds: ReadonlySet<string>): value is MemberBatchReflectionPayload {
|
|
704
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) return false
|
|
705
|
+
const members = (value as { members?: unknown }).members
|
|
706
|
+
if (!Array.isArray(members)) return false
|
|
707
|
+
const seen = new Set<string>()
|
|
708
|
+
return members.every(item => {
|
|
709
|
+
if (!item || typeof item !== 'object' || Array.isArray(item)) return false
|
|
710
|
+
const senderId = (item as { senderId?: unknown }).senderId
|
|
711
|
+
if (typeof senderId !== 'string' || !allowedSenderIds.has(senderId) || seen.has(senderId)) return false
|
|
712
|
+
seen.add(senderId)
|
|
713
|
+
return true
|
|
714
|
+
})
|
|
715
|
+
}
|
|
716
|
+
|
|
499
717
|
function stringifyDoc(value: unknown): string {
|
|
500
718
|
if (typeof value === 'string') return value
|
|
501
719
|
return JSON.stringify(value, null, 2) ?? ''
|
|
@@ -506,6 +724,35 @@ function stringifyProfile(value: unknown): string {
|
|
|
506
724
|
return JSON.stringify(entries, null, 2)
|
|
507
725
|
}
|
|
508
726
|
|
|
727
|
+
function mergeProfile(existing: string | undefined, additions: unknown, removals: unknown): string {
|
|
728
|
+
const current = normalizeProfileEntries(existing || '')
|
|
729
|
+
const removeTexts = new Set(normalizeProfileEntries(removals).map(entry => entry.text))
|
|
730
|
+
const additionsList = normalizeProfileEntries(additions).filter(entry => !removeTexts.has(entry.text))
|
|
731
|
+
const merged = [...current.filter(entry => !removeTexts.has(entry.text)), ...additionsList]
|
|
732
|
+
const unique = merged.filter((entry, index, all) => all.findIndex(item => item.type === entry.type && item.text === entry.text) === index)
|
|
733
|
+
return JSON.stringify(unique, null, 2)
|
|
734
|
+
}
|
|
735
|
+
|
|
736
|
+
function mergeTextDoc(existing: string | undefined, additions: unknown, removals: unknown): string {
|
|
737
|
+
const remove = new Set(normalizeTextList(removals))
|
|
738
|
+
const lines = String(existing || '').split(/\r?\n/).map(line => line.trim()).filter(Boolean).filter(line => !remove.has(line))
|
|
739
|
+
const added = normalizeTextList(additions).filter(line => !lines.includes(line) && !remove.has(line))
|
|
740
|
+
return [...lines, ...added].join('\n')
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
function normalizeTextList(value: unknown): string[] {
|
|
744
|
+
if (typeof value === 'string') return value.trim() ? [value.trim()] : []
|
|
745
|
+
if (!Array.isArray(value)) return []
|
|
746
|
+
return value.flatMap(item => {
|
|
747
|
+
if (typeof item === 'string') return item.trim() ? [item.trim()] : []
|
|
748
|
+
if (item && typeof item === 'object') {
|
|
749
|
+
const text = (item as Record<string, unknown>).text
|
|
750
|
+
return typeof text === 'string' && text.trim() ? [text.trim()] : []
|
|
751
|
+
}
|
|
752
|
+
return []
|
|
753
|
+
})
|
|
754
|
+
}
|
|
755
|
+
|
|
509
756
|
function normalizeProfileEntries(value: unknown): ProfileEntry[] {
|
|
510
757
|
let input = value
|
|
511
758
|
if (typeof input === 'string') {
|
|
@@ -572,8 +819,20 @@ function memberCursorKey(memberId: number): string {
|
|
|
572
819
|
return `memberReflection:${memberId}`
|
|
573
820
|
}
|
|
574
821
|
|
|
575
|
-
function
|
|
576
|
-
|
|
822
|
+
function boundedMemorySections(parts: Array<[title: string, content: string | undefined]>, budget = MEMORY_INJECT_CHARS): string[] {
|
|
823
|
+
let used = 0
|
|
824
|
+
const output: string[] = []
|
|
825
|
+
for (const [title, content] of parts) {
|
|
826
|
+
const value = content?.trim() || ''
|
|
827
|
+
if (!value || used >= budget) 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) continue
|
|
832
|
+
output.push(`[${title}]\n${bounded}`)
|
|
833
|
+
used += bounded.length
|
|
834
|
+
}
|
|
835
|
+
return output
|
|
577
836
|
}
|
|
578
837
|
|
|
579
838
|
function errorMessage(error: unknown): string {
|
package/src/transport/rpc.ts
CHANGED
|
@@ -307,6 +307,7 @@ function asSettingsPatch(value: unknown): QQChatRuntimeSettingsPatch {
|
|
|
307
307
|
if (!record) return {}
|
|
308
308
|
const patch: QQChatRuntimeSettingsPatch = {}
|
|
309
309
|
if (typeof record.memoryEnabled === 'boolean') patch.memoryEnabled = record.memoryEnabled
|
|
310
|
+
if (typeof record.memoryMemberBatchEnabled === 'boolean') patch.memoryMemberBatchEnabled = record.memoryMemberBatchEnabled
|
|
310
311
|
if (record.groupReceiveMode === 'auto' || record.groupReceiveMode === 'mention' || record.groupReceiveMode === 'silent') {
|
|
311
312
|
patch.groupReceiveMode = record.groupReceiveMode
|
|
312
313
|
}
|