dsh-qqchat 0.2.0
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/LICENSE +21 -0
- package/README.en.md +276 -0
- package/README.md +211 -0
- package/client-src/plugin.cts +15 -0
- package/client-src/settings.cts +216 -0
- package/client-src/shared.cts +104 -0
- package/client-src/workspace.cts +164 -0
- package/cordis.patch.yml +7 -0
- package/docs/ARCHITECTURE.en.md +346 -0
- package/docs/ARCHITECTURE.md +411 -0
- package/docs/DEVELOPMENT.md +69 -0
- package/docs/MEDIA_AND_QUOTES.md +644 -0
- package/docs/MEMORY.md +99 -0
- package/docs/MEMORY_CONTEXT.md +254 -0
- package/docs/QUOTES.md +355 -0
- package/docs/README.md +22 -0
- package/docs/SECURITY.md +62 -0
- package/docs/STREAMING.md +347 -0
- package/docs/assets//346/211/253/347/240/201/351/223/276/346/216/245.jpg +0 -0
- package/docs/assets//347/276/244/350/201/212/347/225/214/351/235/242.jpg +0 -0
- package/docs/assets//350/256/260/345/277/206/347/263/273/347/273/237.jpg +0 -0
- package/docs/assets//350/256/276/347/275/256/347/225/214/351/235/242.jpg +0 -0
- package/docs/devlog.md +24 -0
- package/lib/client.js +433 -0
- package/lib/commands/dispatch.d.ts +16 -0
- package/lib/commands/dispatch.js +48 -0
- package/lib/config.d.ts +3 -0
- package/lib/config.js +63 -0
- package/lib/gateway/api.d.ts +30 -0
- package/lib/gateway/api.js +202 -0
- package/lib/gateway/auth.d.ts +35 -0
- package/lib/gateway/auth.js +80 -0
- package/lib/gateway/crypto.d.ts +2 -0
- package/lib/gateway/crypto.js +17 -0
- package/lib/gateway/gateway.d.ts +32 -0
- package/lib/gateway/gateway.js +219 -0
- package/lib/gateway/normalize.d.ts +6 -0
- package/lib/gateway/normalize.js +248 -0
- package/lib/index.d.ts +14 -0
- package/lib/index.js +35 -0
- package/lib/media/image-tool.d.ts +4 -0
- package/lib/media/image-tool.js +62 -0
- package/lib/media/media-tools.d.ts +3 -0
- package/lib/media/media-tools.js +68 -0
- package/lib/media/store.d.ts +14 -0
- package/lib/media/store.js +176 -0
- package/lib/session/agent-bridge.d.ts +56 -0
- package/lib/session/agent-bridge.js +613 -0
- package/lib/session/memory-snapshot.d.ts +23 -0
- package/lib/session/memory-snapshot.js +27 -0
- package/lib/session/runtime.d.ts +41 -0
- package/lib/session/runtime.js +395 -0
- package/lib/shared/augmentations.d.ts +40 -0
- package/lib/shared/augmentations.js +1 -0
- package/lib/shared/logging.d.ts +14 -0
- package/lib/shared/logging.js +41 -0
- package/lib/storage/db.d.ts +82 -0
- package/lib/storage/db.js +669 -0
- package/lib/storage/memory.d.ts +50 -0
- package/lib/storage/memory.js +566 -0
- package/lib/transport/rpc.d.ts +18 -0
- package/lib/transport/rpc.js +318 -0
- package/lib/types.d.ts +416 -0
- package/lib/types.js +1 -0
- package/package.json +115 -0
- package/scripts/wrap-client.mjs +20 -0
- package/src/commands/dispatch.ts +75 -0
- package/src/config.ts +70 -0
- package/src/gateway/api.ts +260 -0
- package/src/gateway/auth.ts +99 -0
- package/src/gateway/crypto.ts +16 -0
- package/src/gateway/gateway.ts +207 -0
- package/src/gateway/normalize.ts +236 -0
- package/src/index.ts +46 -0
- package/src/media/image-tool.ts +62 -0
- package/src/media/media-tools.ts +66 -0
- package/src/media/store.ts +157 -0
- package/src/session/agent-bridge.ts +624 -0
- package/src/session/memory-snapshot.ts +41 -0
- package/src/session/runtime.ts +368 -0
- package/src/shared/augmentations.ts +45 -0
- package/src/shared/logging.ts +36 -0
- package/src/storage/db.ts +760 -0
- package/src/storage/memory.ts +581 -0
- package/src/transport/rpc.ts +316 -0
- package/src/types.ts +456 -0
- package/tsconfig.build.json +12 -0
- package/tsconfig.client.json +14 -0
- package/tsconfig.json +23 -0
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { QQChatDatabase } from './db.js';
|
|
3
|
+
import type { GroupRow, LoggerLike, MemoryDocuments, MemoryView, QQChatConfig, ReflectionPayload, QQNormalizedMessage } from '../types.js';
|
|
4
|
+
interface MemberReflectionPayload {
|
|
5
|
+
profile?: unknown;
|
|
6
|
+
pattern?: unknown;
|
|
7
|
+
summary?: unknown;
|
|
8
|
+
memory?: unknown;
|
|
9
|
+
daily?: unknown;
|
|
10
|
+
}
|
|
11
|
+
export declare class MemoryEngine {
|
|
12
|
+
private readonly ctx;
|
|
13
|
+
private readonly db;
|
|
14
|
+
private readonly config;
|
|
15
|
+
private readonly logger;
|
|
16
|
+
private readonly timers;
|
|
17
|
+
private readonly memberTimers;
|
|
18
|
+
private readonly routes;
|
|
19
|
+
private readonly memberRoutes;
|
|
20
|
+
private readonly reflectingGroups;
|
|
21
|
+
private readonly reflectingMembers;
|
|
22
|
+
constructor(ctx: Context, db: QQChatDatabase, config: QQChatConfig, logger?: LoggerLike);
|
|
23
|
+
dispose(): void;
|
|
24
|
+
setRoute(groupId: number, provider: string, model: string, sessionId: string): void;
|
|
25
|
+
setMemberRoute(memberId: number, provider: string, model: string, sessionId: string): void;
|
|
26
|
+
schedule(groupId: number): void;
|
|
27
|
+
scheduleMember(memberId: number): void;
|
|
28
|
+
reflectNow(groupId: number): Promise<MemoryView>;
|
|
29
|
+
private reflectNowInternal;
|
|
30
|
+
reflectMemberNow(memberId: number): Promise<MemoryDocuments>;
|
|
31
|
+
private reflectMemberNowInternal;
|
|
32
|
+
private compactDaily;
|
|
33
|
+
private generateReflection;
|
|
34
|
+
private unreflectedDirectMessages;
|
|
35
|
+
private applyReflection;
|
|
36
|
+
contextForGroup(group: GroupRow, currentMember?: {
|
|
37
|
+
id: number;
|
|
38
|
+
platform_user_id: string;
|
|
39
|
+
display_name: string | null;
|
|
40
|
+
}): string;
|
|
41
|
+
contextForMember(member: {
|
|
42
|
+
id: number;
|
|
43
|
+
platform_user_id: string;
|
|
44
|
+
display_name: string | null;
|
|
45
|
+
}): string;
|
|
46
|
+
currentMessageText(message: QQNormalizedMessage): string;
|
|
47
|
+
memoryView(groupId: number): MemoryView | undefined;
|
|
48
|
+
}
|
|
49
|
+
export declare function parseJsonObject(text: string): ReflectionPayload | MemberReflectionPayload;
|
|
50
|
+
export {};
|
|
@@ -0,0 +1,566 @@
|
|
|
1
|
+
import { BlockAssembler, createUserMessage } from '@deepseek-ai/dsh-llm';
|
|
2
|
+
import { SessionId } from '@deepseek-ai/dsh-session';
|
|
3
|
+
const DOCS = ['profile', 'summary', 'daily', 'memory', 'pattern'];
|
|
4
|
+
const MEMBER_DAILY_COMPACT_AT = 100;
|
|
5
|
+
const MEMBER_DAILY_KEEP_RECENT = 50;
|
|
6
|
+
const GROUP_DAILY_COMPACT_AT = 1000;
|
|
7
|
+
const GROUP_DAILY_KEEP_RECENT = 500;
|
|
8
|
+
export class MemoryEngine {
|
|
9
|
+
ctx;
|
|
10
|
+
db;
|
|
11
|
+
config;
|
|
12
|
+
logger;
|
|
13
|
+
timers = new Map();
|
|
14
|
+
memberTimers = new Map();
|
|
15
|
+
routes = new Map();
|
|
16
|
+
memberRoutes = new Map();
|
|
17
|
+
reflectingGroups = new Map();
|
|
18
|
+
reflectingMembers = new Map();
|
|
19
|
+
constructor(ctx, db, config, logger = console) {
|
|
20
|
+
this.ctx = ctx;
|
|
21
|
+
this.db = db;
|
|
22
|
+
this.config = config;
|
|
23
|
+
this.logger = logger;
|
|
24
|
+
}
|
|
25
|
+
dispose() {
|
|
26
|
+
for (const timer of this.timers.values())
|
|
27
|
+
clearTimeout(timer);
|
|
28
|
+
for (const timer of this.memberTimers.values())
|
|
29
|
+
clearTimeout(timer);
|
|
30
|
+
this.timers.clear();
|
|
31
|
+
this.memberTimers.clear();
|
|
32
|
+
this.reflectingGroups.clear();
|
|
33
|
+
this.reflectingMembers.clear();
|
|
34
|
+
}
|
|
35
|
+
setRoute(groupId, provider, model, sessionId) {
|
|
36
|
+
if (!provider || !model)
|
|
37
|
+
return;
|
|
38
|
+
this.routes.set(Number(groupId), { provider, model, sessionId });
|
|
39
|
+
}
|
|
40
|
+
setMemberRoute(memberId, provider, model, sessionId) {
|
|
41
|
+
if (!provider || !model)
|
|
42
|
+
return;
|
|
43
|
+
this.memberRoutes.set(Number(memberId), { provider, model, sessionId });
|
|
44
|
+
}
|
|
45
|
+
schedule(groupId) {
|
|
46
|
+
groupId = Number(groupId);
|
|
47
|
+
if (!this.routes.has(groupId))
|
|
48
|
+
return;
|
|
49
|
+
const existing = this.timers.get(groupId);
|
|
50
|
+
if (existing)
|
|
51
|
+
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
|
+
const timer = setTimeout(() => {
|
|
57
|
+
this.timers.delete(groupId);
|
|
58
|
+
void this.reflectNow(groupId).catch(error => this.logger.warn?.(`[dsh-qqchat] memory reflection: ${errorMessage(error)}`));
|
|
59
|
+
}, this.config.reflectionIdleMs);
|
|
60
|
+
timer.unref?.();
|
|
61
|
+
this.timers.set(groupId, timer);
|
|
62
|
+
}
|
|
63
|
+
scheduleMember(memberId) {
|
|
64
|
+
memberId = Number(memberId);
|
|
65
|
+
if (!this.memberRoutes.has(memberId))
|
|
66
|
+
return;
|
|
67
|
+
const existing = this.memberTimers.get(memberId);
|
|
68
|
+
if (existing)
|
|
69
|
+
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)}`));
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
const timer = setTimeout(() => {
|
|
75
|
+
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);
|
|
78
|
+
timer.unref?.();
|
|
79
|
+
this.memberTimers.set(memberId, timer);
|
|
80
|
+
}
|
|
81
|
+
async reflectNow(groupId) {
|
|
82
|
+
groupId = Number(groupId);
|
|
83
|
+
const existing = this.reflectingGroups.get(groupId);
|
|
84
|
+
if (existing)
|
|
85
|
+
return existing;
|
|
86
|
+
const current = this.reflectNowInternal(groupId);
|
|
87
|
+
this.reflectingGroups.set(groupId, current);
|
|
88
|
+
return current.finally(() => {
|
|
89
|
+
if (this.reflectingGroups.get(groupId) === current)
|
|
90
|
+
this.reflectingGroups.delete(groupId);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
async reflectNowInternal(groupId) {
|
|
94
|
+
groupId = Number(groupId);
|
|
95
|
+
const route = this.routes.get(groupId);
|
|
96
|
+
if (!route)
|
|
97
|
+
throw new Error('这个群还没有可复用的 DSH 模型路由;先让 Agent 在群里完成一次回复');
|
|
98
|
+
const group = this.db.groupById(groupId);
|
|
99
|
+
if (!group)
|
|
100
|
+
throw new Error('群不存在');
|
|
101
|
+
const messages = this.db.unreflectedMessages(groupId, this.config.reflectionMaxMessages);
|
|
102
|
+
if (messages.length === 0) {
|
|
103
|
+
const view = this.memoryView(groupId);
|
|
104
|
+
if (!view)
|
|
105
|
+
throw new Error('群不存在');
|
|
106
|
+
return view;
|
|
107
|
+
}
|
|
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)
|
|
142
|
+
throw new Error('群不存在');
|
|
143
|
+
return view;
|
|
144
|
+
}
|
|
145
|
+
async reflectMemberNow(memberId) {
|
|
146
|
+
memberId = Number(memberId);
|
|
147
|
+
const existing = this.reflectingMembers.get(memberId);
|
|
148
|
+
if (existing)
|
|
149
|
+
return existing;
|
|
150
|
+
const current = this.reflectMemberNowInternal(memberId);
|
|
151
|
+
this.reflectingMembers.set(memberId, current);
|
|
152
|
+
return current.finally(() => {
|
|
153
|
+
if (this.reflectingMembers.get(memberId) === current)
|
|
154
|
+
this.reflectingMembers.delete(memberId);
|
|
155
|
+
});
|
|
156
|
+
}
|
|
157
|
+
async reflectMemberNowInternal(memberId) {
|
|
158
|
+
memberId = Number(memberId);
|
|
159
|
+
const route = this.memberRoutes.get(memberId);
|
|
160
|
+
if (!route)
|
|
161
|
+
throw new Error('这个私聊还没有可复用的 DSH 模型路由;先让 Agent 完成一次回复');
|
|
162
|
+
const member = this.db.memberById(memberId);
|
|
163
|
+
if (!member)
|
|
164
|
+
throw new Error('成员不存在');
|
|
165
|
+
const messages = this.unreflectedDirectMessages(memberId, this.config.reflectionMaxMessages);
|
|
166
|
+
if (messages.length === 0)
|
|
167
|
+
return this.db.memoryDocs('member', memberId);
|
|
168
|
+
const input = {
|
|
169
|
+
member: {
|
|
170
|
+
senderId: member.platform_user_id,
|
|
171
|
+
displayName: member.display_name || '',
|
|
172
|
+
},
|
|
173
|
+
existing: pickMemberDocs(this.db.memoryDocs('member', memberId)),
|
|
174
|
+
messages: messages.map(message => ({
|
|
175
|
+
id: Number(message.id),
|
|
176
|
+
at: message.created_at,
|
|
177
|
+
direction: message.direction,
|
|
178
|
+
senderId: message.direction === 'outbound' ? 'BOT' : member.platform_user_id,
|
|
179
|
+
senderName: message.direction === 'outbound' ? 'DSH Agent' : (member.display_name || ''),
|
|
180
|
+
text: message.content,
|
|
181
|
+
quotedText: message.quoted_text || '',
|
|
182
|
+
...(parseReflectionQuote(message.quote_json) ? { quote: parseReflectionQuote(message.quote_json) } : {}),
|
|
183
|
+
})),
|
|
184
|
+
};
|
|
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));
|
|
197
|
+
}
|
|
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
|
+
}
|
|
204
|
+
async compactDaily(scopeType, scopeKey, route, threshold, keepRecent, fallbackSessionId) {
|
|
205
|
+
const entries = this.db.dailyEntries(scopeType, scopeKey);
|
|
206
|
+
if (entries.length < threshold)
|
|
207
|
+
return false;
|
|
208
|
+
const overflow = entries.slice(0, -keepRecent);
|
|
209
|
+
const recent = entries.slice(-keepRecent);
|
|
210
|
+
if (!overflow.length)
|
|
211
|
+
return false;
|
|
212
|
+
const existingMemory = this.db.memoryDocs(scopeType, scopeKey).memory || '';
|
|
213
|
+
const profile = this.db.memoryDocs(scopeType === 'member' ? 'member' : 'group', scopeKey).profile || '';
|
|
214
|
+
const pattern = this.db.memoryDocs('member', scopeKey).pattern || '';
|
|
215
|
+
const system = scopeType === 'group' ? groupCompressionSystemPrompt() : memberCompressionSystemPrompt();
|
|
216
|
+
const input = scopeType === 'group'
|
|
217
|
+
? `已有长期记忆:\n${existingMemory || '(暂无)'}\n\n近期群聊记录(按日期保留历史,不要丢日期):\n${renderDailyEntries(overflow)}\n\n请输出整理后的完整长期记忆主档。`
|
|
218
|
+
: `已有长期记忆:\n${existingMemory || '(暂无)'}\n\n结构化 profile:\n${profile || '(暂无)'}\n\n行为模式:\n${pattern || '(暂无)'}\n\n要沉淀的近期记录(按日期保留历史,不要丢日期):\n${renderDailyEntries(overflow)}\n\n请输出整理后的完整长期记忆主档。`;
|
|
219
|
+
try {
|
|
220
|
+
const result = await this.generateReflection(route, system, { text: input }, fallbackSessionId, this.config.memoryCompressionMaxTokens);
|
|
221
|
+
const memory = typeof result?.memory === 'string' ? result.memory.trim() : '';
|
|
222
|
+
if (!memory || !preservesDailyDates(overflow, memory))
|
|
223
|
+
return false;
|
|
224
|
+
this.db.setMemoryDoc(scopeType, scopeKey, 'memory', memory);
|
|
225
|
+
this.db.setDailyEntries(scopeType, scopeKey, recent);
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
catch (error) {
|
|
229
|
+
this.logger.warn?.(`[dsh-qqchat] ${scopeType} memory compression skipped: ${errorMessage(error)}`);
|
|
230
|
+
return false;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
async generateReflection(route, system, input, fallbackSessionId, maxTokens = this.config.memoryMaxTokens) {
|
|
234
|
+
const request = createUserMessage({
|
|
235
|
+
source: { kind: 'plugin', plugin: 'dsh-qqchat' },
|
|
236
|
+
content: [{ type: 'text', text: JSON.stringify(input) }],
|
|
237
|
+
});
|
|
238
|
+
const assembler = new BlockAssembler();
|
|
239
|
+
for await (const chunk of this.ctx.llm.stream({
|
|
240
|
+
provider: route.provider,
|
|
241
|
+
model: route.model,
|
|
242
|
+
messages: [request],
|
|
243
|
+
system,
|
|
244
|
+
maxTokens,
|
|
245
|
+
sessionId: SessionId(route.sessionId || fallbackSessionId),
|
|
246
|
+
}))
|
|
247
|
+
assembler.push(chunk);
|
|
248
|
+
const text = assembler.blocks()
|
|
249
|
+
.filter(block => block.type === 'text')
|
|
250
|
+
.map(block => block.text)
|
|
251
|
+
.join('\n');
|
|
252
|
+
return parseJsonObject(text);
|
|
253
|
+
}
|
|
254
|
+
unreflectedDirectMessages(memberId, limit) {
|
|
255
|
+
const cursor = this.db.getSetting(memberCursorKey(memberId), 0);
|
|
256
|
+
const fetchLimit = Math.max(limit, this.config.reflectionMaxMessages);
|
|
257
|
+
return this.db.listDirectMessages(memberId, fetchLimit)
|
|
258
|
+
.filter(message => Number(message.id) > cursor)
|
|
259
|
+
.slice(-limit);
|
|
260
|
+
}
|
|
261
|
+
applyReflection(groupId, members, reflected) {
|
|
262
|
+
const group = reflected.group || {};
|
|
263
|
+
if (group.profile !== undefined)
|
|
264
|
+
this.db.setMemoryDoc('group', groupId, 'profile', stringifyDoc(group.profile));
|
|
265
|
+
if (group.summary !== undefined)
|
|
266
|
+
this.db.setMemoryDoc('group', groupId, 'summary', stringifyDoc(group.summary));
|
|
267
|
+
if (group.memory !== undefined)
|
|
268
|
+
this.db.setMemoryDoc('group', groupId, 'memory', stringifyMemory(group.memory));
|
|
269
|
+
if (group.daily !== undefined && stringifyDoc(group.daily).trim()) {
|
|
270
|
+
const stamp = new Date().toISOString().slice(0, 10);
|
|
271
|
+
this.db.appendDailyDoc('group', groupId, stamp, stringifyDoc(group.daily));
|
|
272
|
+
}
|
|
273
|
+
const byOpenid = new Map(members.map(member => [member.platform_user_id, Number(member.id)]));
|
|
274
|
+
const updates = Array.isArray(reflected.members) ? reflected.members : [];
|
|
275
|
+
for (const update of updates) {
|
|
276
|
+
const memberId = byOpenid.get(String(update.senderId || ''));
|
|
277
|
+
if (!memberId)
|
|
278
|
+
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));
|
|
283
|
+
if (update.summary !== undefined)
|
|
284
|
+
this.db.setMemoryDoc('member', memberId, 'summary', stringifyDoc(update.summary));
|
|
285
|
+
if (update.memory !== undefined)
|
|
286
|
+
this.db.setMemoryDoc('member', memberId, 'memory', stringifyMemory(update.memory));
|
|
287
|
+
if (Array.isArray(update.nicknames)) {
|
|
288
|
+
for (const nickname of update.nicknames) {
|
|
289
|
+
if (typeof nickname === 'string')
|
|
290
|
+
this.db.addGroupMemberNickname(groupId, memberId, nickname);
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
contextForGroup(group, currentMember) {
|
|
296
|
+
const groupDocs = this.db.memoryDocs('group', Number(group.id));
|
|
297
|
+
const memberDocs = currentMember ? this.db.memoryDocs('member', Number(currentMember.id)) : {};
|
|
298
|
+
return [
|
|
299
|
+
'[QQ 群聊上下文快照]',
|
|
300
|
+
'以下 sender ID / group ID 是可靠平台元数据。不要根据昵称猜身份,不要把其他群成员的兴趣、关系或记忆归到当前发言人。',
|
|
301
|
+
`群ID=${group.platform_group_id}`,
|
|
302
|
+
group.name ? `群名=${group.name}` : '',
|
|
303
|
+
section('群画像', groupDocs.profile),
|
|
304
|
+
section('群摘要', groupDocs.summary),
|
|
305
|
+
section('群长期记忆', groupDocs.memory),
|
|
306
|
+
section('群最近沉淀', groupDocs.daily),
|
|
307
|
+
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),
|
|
312
|
+
].filter(Boolean).join('\n');
|
|
313
|
+
}
|
|
314
|
+
contextForMember(member) {
|
|
315
|
+
const docs = this.db.memoryDocs('member', Number(member.id));
|
|
316
|
+
return [
|
|
317
|
+
'[QQ 私聊成员上下文]',
|
|
318
|
+
`成员ID=${member.platform_user_id}`,
|
|
319
|
+
`显示名=${member.display_name || ''}`,
|
|
320
|
+
'成员ID 是可靠平台身份;昵称仅供展示。',
|
|
321
|
+
section('成员画像', docs.profile),
|
|
322
|
+
section('行为模式', docs.pattern),
|
|
323
|
+
section('成员摘要', docs.summary),
|
|
324
|
+
section('成员近期沉淀', docs.daily),
|
|
325
|
+
section('成员长期记忆', docs.memory),
|
|
326
|
+
].filter(Boolean).join('\n');
|
|
327
|
+
}
|
|
328
|
+
currentMessageText(message) {
|
|
329
|
+
const quoteText = message.quote
|
|
330
|
+
? [
|
|
331
|
+
'[引用消息]',
|
|
332
|
+
message.quote.senderId ? `发送人ID=${message.quote.senderId}` : '',
|
|
333
|
+
message.quote.senderName ? `显示名=${message.quote.senderName}` : '',
|
|
334
|
+
message.quote.messageId ? `消息ID=${message.quote.messageId}` : '',
|
|
335
|
+
`正文=${message.quote.text || message.quotedText || '(空消息)'}`,
|
|
336
|
+
message.quote.attachments.length ? `附件=${message.quote.attachments.map(item => `${item.kind || 'file'}:${item.filename}${item.attachmentId ? ` (attachment_id=${item.attachmentId})` : ''}`).join('、')}` : '',
|
|
337
|
+
].filter(Boolean).join('\n')
|
|
338
|
+
: (message.quotedText ? `[引用消息]\n正文=${message.quotedText}` : '');
|
|
339
|
+
return [
|
|
340
|
+
message.chatType === 'group' ? '[当前 QQ 群消息;以下身份字段是可靠元数据]' : '[当前 QQ 私聊消息;以下身份字段是可靠元数据]',
|
|
341
|
+
`发言人ID=${message.senderId}`,
|
|
342
|
+
`显示名=${message.senderName || ''}`,
|
|
343
|
+
message.chatType === 'group' ? `群ID=${message.groupOpenid || ''}` : '',
|
|
344
|
+
`是否@机器人=${message.mentioned ? '是' : '否'}`,
|
|
345
|
+
quoteText,
|
|
346
|
+
'正文:',
|
|
347
|
+
message.text || '(空消息)',
|
|
348
|
+
].filter(Boolean).join('\n');
|
|
349
|
+
}
|
|
350
|
+
memoryView(groupId) {
|
|
351
|
+
const group = this.db.groupById(Number(groupId));
|
|
352
|
+
if (!group)
|
|
353
|
+
return undefined;
|
|
354
|
+
const members = this.db.listGroupMembers(Number(groupId)).map(member => ({
|
|
355
|
+
...member,
|
|
356
|
+
memory: this.db.memoryDocs('member', Number(member.id)),
|
|
357
|
+
}));
|
|
358
|
+
return { group, groupMemory: this.db.memoryDocs('group', Number(groupId)), members };
|
|
359
|
+
}
|
|
360
|
+
}
|
|
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 创建成员记忆。`;
|
|
372
|
+
}
|
|
373
|
+
function memberMemorySystemPrompt() {
|
|
374
|
+
return `你负责整理一个 QQ 私聊成员的长期记忆。senderId 是唯一可靠身份,displayName 只用于展示。引用内容属于 quote.senderId;除非 quote.senderId 与当前成员 senderId 相同,否则不得把引用中的个人事实归给当前成员。规则:
|
|
375
|
+
1. 只记录这个成员稳定且未来有用的信息,不根据昵称猜身份。
|
|
376
|
+
2. profile 写较稳定的个人事实,必须输出带 type/text/ts 的条目;type 只能是 name、address、pronoun、background、preference、note。pattern 写多次出现的偏好、习惯或行为模式;summary 写当前值得保留的紧凑状态;memory 写长期项目、关系、约定和背景。
|
|
377
|
+
3. 一次性寒暄、玩笑、短暂情绪和未经确认的推断不要写入长期记忆。
|
|
378
|
+
4. existing 是已有记忆,应在其基础上克制更新。
|
|
379
|
+
5. 输出严格 JSON,不要 Markdown 代码块、解释或额外文字。
|
|
380
|
+
输出结构:{"profile":[{"type":"name|address|pronoun|background|preference|note","text":"...","ts":0}],"pattern":{},"summary":"","memory":"","daily":""}`;
|
|
381
|
+
}
|
|
382
|
+
function parseReflectionQuote(value) {
|
|
383
|
+
if (!value)
|
|
384
|
+
return undefined;
|
|
385
|
+
try {
|
|
386
|
+
const raw = JSON.parse(value);
|
|
387
|
+
if (!raw || typeof raw !== 'object')
|
|
388
|
+
return undefined;
|
|
389
|
+
const quote = raw;
|
|
390
|
+
return {
|
|
391
|
+
...(typeof quote.senderId === 'string' && quote.senderId ? { senderId: quote.senderId } : {}),
|
|
392
|
+
...(typeof quote.senderName === 'string' && quote.senderName ? { senderName: quote.senderName } : {}),
|
|
393
|
+
...(typeof quote.messageId === 'string' && quote.messageId ? { messageId: quote.messageId } : {}),
|
|
394
|
+
text: typeof quote.text === 'string' ? quote.text : '',
|
|
395
|
+
attachments: Array.isArray(quote.attachments)
|
|
396
|
+
? quote.attachments.filter((item) => Boolean(item) && typeof item === 'object')
|
|
397
|
+
: [],
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
catch {
|
|
401
|
+
return undefined;
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
function groupCompressionSystemPrompt() {
|
|
405
|
+
return `你负责维护 QQ 群长期记忆主档。把近期 daily 记录合并进已有 memory,合并重复、修正矛盾,但保留有价值的历史、日期、事件背景和变化过程;不要因为旧或细而删除历史,不评判、不推测。严格只输出 JSON:{"memory":"更新后的长期记忆全文;不能清空"}`;
|
|
406
|
+
}
|
|
407
|
+
function memberCompressionSystemPrompt() {
|
|
408
|
+
return `你负责维护 QQ 成员长期记忆主档。把近期 daily 记录合并进已有 memory,结合 profile 和 pattern,保留有价值的历史、日期、项目背景和变化过程;不要重复抄写 profile/pattern,不评判、不推测,不能清空已有长期记忆。严格只输出 JSON:{"memory":"更新后的长期记忆全文;不能清空"}`;
|
|
409
|
+
}
|
|
410
|
+
function renderDailyEntries(entries) {
|
|
411
|
+
const out = [];
|
|
412
|
+
let current = '';
|
|
413
|
+
for (const entry of entries) {
|
|
414
|
+
if (!entry.date || !entry.note)
|
|
415
|
+
continue;
|
|
416
|
+
if (entry.date !== current) {
|
|
417
|
+
if (out.length)
|
|
418
|
+
out.push('');
|
|
419
|
+
out.push(`## ${entry.date}`);
|
|
420
|
+
current = entry.date;
|
|
421
|
+
}
|
|
422
|
+
out.push(`- ${entry.note}`);
|
|
423
|
+
}
|
|
424
|
+
return out.join('\n');
|
|
425
|
+
}
|
|
426
|
+
function preservesDailyDates(entries, memory) {
|
|
427
|
+
return [...new Set(entries.map(entry => entry.date))].every(date => memory.includes(date));
|
|
428
|
+
}
|
|
429
|
+
function dateForTimestamp(value) {
|
|
430
|
+
return new Date(Number(value || Date.now())).toISOString().slice(0, 10);
|
|
431
|
+
}
|
|
432
|
+
export function parseJsonObject(text) {
|
|
433
|
+
const trimmed = String(text || '').trim();
|
|
434
|
+
const unfenced = trimmed.replace(/^```(?:json)?\s*/i, '').replace(/\s*```$/, '').trim();
|
|
435
|
+
for (const candidate of [trimmed, unfenced]) {
|
|
436
|
+
try {
|
|
437
|
+
const parsed = JSON.parse(candidate);
|
|
438
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
439
|
+
return parsed;
|
|
440
|
+
}
|
|
441
|
+
catch { }
|
|
442
|
+
}
|
|
443
|
+
// Models sometimes add a short explanation before/after the JSON. Find a
|
|
444
|
+
// balanced object while respecting quoted strings and escaped characters.
|
|
445
|
+
for (let start = unfenced.indexOf('{'); start >= 0; start = unfenced.indexOf('{', start + 1)) {
|
|
446
|
+
let depth = 0;
|
|
447
|
+
let inString = false;
|
|
448
|
+
let escaped = false;
|
|
449
|
+
for (let index = start; index < unfenced.length; index += 1) {
|
|
450
|
+
const char = unfenced[index];
|
|
451
|
+
if (inString) {
|
|
452
|
+
if (escaped)
|
|
453
|
+
escaped = false;
|
|
454
|
+
else if (char === '\\')
|
|
455
|
+
escaped = true;
|
|
456
|
+
else if (char === '"')
|
|
457
|
+
inString = false;
|
|
458
|
+
continue;
|
|
459
|
+
}
|
|
460
|
+
if (char === '"') {
|
|
461
|
+
inString = true;
|
|
462
|
+
continue;
|
|
463
|
+
}
|
|
464
|
+
if (char === '{')
|
|
465
|
+
depth += 1;
|
|
466
|
+
else if (char === '}') {
|
|
467
|
+
depth -= 1;
|
|
468
|
+
if (depth === 0) {
|
|
469
|
+
try {
|
|
470
|
+
const parsed = JSON.parse(unfenced.slice(start, index + 1));
|
|
471
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
|
|
472
|
+
return parsed;
|
|
473
|
+
}
|
|
474
|
+
catch { }
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
throw new Error('记忆反思模型没有返回有效 JSON');
|
|
481
|
+
}
|
|
482
|
+
function stringifyDoc(value) {
|
|
483
|
+
if (typeof value === 'string')
|
|
484
|
+
return value;
|
|
485
|
+
return JSON.stringify(value, null, 2) ?? '';
|
|
486
|
+
}
|
|
487
|
+
function stringifyProfile(value) {
|
|
488
|
+
const entries = normalizeProfileEntries(value);
|
|
489
|
+
return JSON.stringify(entries, null, 2);
|
|
490
|
+
}
|
|
491
|
+
function normalizeProfileEntries(value) {
|
|
492
|
+
let input = value;
|
|
493
|
+
if (typeof input === 'string') {
|
|
494
|
+
try {
|
|
495
|
+
input = JSON.parse(input);
|
|
496
|
+
}
|
|
497
|
+
catch {
|
|
498
|
+
input = { note: input };
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
const now = Date.now();
|
|
502
|
+
if (Array.isArray(input)) {
|
|
503
|
+
return input.flatMap(item => {
|
|
504
|
+
if (!item || typeof item !== 'object')
|
|
505
|
+
return [];
|
|
506
|
+
const raw = item;
|
|
507
|
+
const text = typeof raw.text === 'string' ? raw.text.trim() : '';
|
|
508
|
+
if (!text)
|
|
509
|
+
return [];
|
|
510
|
+
const type = profileType(raw.type);
|
|
511
|
+
return [{ type, text, ts: Number.isSafeInteger(raw.ts) ? Number(raw.ts) : now }];
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
if (input && typeof input === 'object') {
|
|
515
|
+
return Object.entries(input).flatMap(([key, raw]) => {
|
|
516
|
+
const text = typeof raw === 'string' ? raw.trim() : JSON.stringify(raw);
|
|
517
|
+
return text ? [{ type: profileType(key), text, ts: now }] : [];
|
|
518
|
+
});
|
|
519
|
+
}
|
|
520
|
+
return typeof input === 'string' && input.trim() ? [{ type: 'note', text: input.trim(), ts: now }] : [];
|
|
521
|
+
}
|
|
522
|
+
function profileType(value) {
|
|
523
|
+
const type = String(value || '').trim();
|
|
524
|
+
if (type === 'name' || type === 'name_observed' || type === 'display_name' || type === 'nickname')
|
|
525
|
+
return 'name';
|
|
526
|
+
if (type === 'address' || type === 'pronoun' || type === 'background' || type === 'dev_env' || type === 'preference')
|
|
527
|
+
return type === 'dev_env' ? 'background' : type;
|
|
528
|
+
return 'note';
|
|
529
|
+
}
|
|
530
|
+
function parseStringList(value) {
|
|
531
|
+
if (!value)
|
|
532
|
+
return [];
|
|
533
|
+
try {
|
|
534
|
+
const parsed = JSON.parse(value);
|
|
535
|
+
return Array.isArray(parsed) ? parsed.filter(item => typeof item === 'string' && item.trim()).map(item => item.trim()) : [];
|
|
536
|
+
}
|
|
537
|
+
catch {
|
|
538
|
+
return [];
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
function stringifyMemory(value) {
|
|
542
|
+
if (Array.isArray(value)) {
|
|
543
|
+
return value.map(item => `- ${typeof item === 'string' ? item : JSON.stringify(item)}`).join('\n');
|
|
544
|
+
}
|
|
545
|
+
return stringifyDoc(value);
|
|
546
|
+
}
|
|
547
|
+
function pickDocs(docs) {
|
|
548
|
+
return Object.fromEntries(DOCS.filter(key => docs[key]).map(key => [key, docs[key]]));
|
|
549
|
+
}
|
|
550
|
+
function pickMemberDocs(docs) {
|
|
551
|
+
return {
|
|
552
|
+
...(docs.profile ? { profile: docs.profile } : {}),
|
|
553
|
+
...(docs.pattern ? { pattern: docs.pattern } : {}),
|
|
554
|
+
...(docs.summary ? { summary: docs.summary } : {}),
|
|
555
|
+
...(docs.memory ? { memory: docs.memory } : {}),
|
|
556
|
+
};
|
|
557
|
+
}
|
|
558
|
+
function memberCursorKey(memberId) {
|
|
559
|
+
return `memberReflection:${memberId}`;
|
|
560
|
+
}
|
|
561
|
+
function section(title, content) {
|
|
562
|
+
return content ? `[${title}]\n${content}` : '';
|
|
563
|
+
}
|
|
564
|
+
function errorMessage(error) {
|
|
565
|
+
return error instanceof Error ? error.message : String(error);
|
|
566
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { ConnectionRpcHandler } from '@deepseek-ai/dsh-client-connection';
|
|
2
|
+
import type { QQChatRuntime } from '../session/runtime.js';
|
|
3
|
+
type RpcSuccess<T> = {
|
|
4
|
+
ok: true;
|
|
5
|
+
value: T;
|
|
6
|
+
};
|
|
7
|
+
type RpcFailure = {
|
|
8
|
+
ok: false;
|
|
9
|
+
error: {
|
|
10
|
+
code: 'internal';
|
|
11
|
+
message: string;
|
|
12
|
+
details: Record<string, never>;
|
|
13
|
+
};
|
|
14
|
+
};
|
|
15
|
+
export type QQChatRpcResult<T = unknown> = RpcSuccess<T> | RpcFailure;
|
|
16
|
+
export type QQChatRpcHandler = ConnectionRpcHandler;
|
|
17
|
+
export declare function createQQChatRpc(runtime: QQChatRuntime): ConnectionRpcHandler;
|
|
18
|
+
export {};
|