@zhin.js/ai 0.0.1

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.
@@ -0,0 +1,439 @@
1
+ /**
2
+ * @zhin.js/ai - Context Manager
3
+ * 上下文管理器,负责消息记录、历史读取和智能总结
4
+ *
5
+ * 特性:
6
+ * - 所有平台消息自动落表
7
+ * - 按场景(scene_id)读取历史消息
8
+ * - 智能总结,保持上下文简洁
9
+ * - 支持多平台、多场景
10
+ */
11
+
12
+ import { Logger } from '@zhin.js/core';
13
+ import type { ChatMessage, AIProvider } from './types.js';
14
+
15
+ const logger = new Logger(null, 'ContextManager');
16
+
17
+ // ============================================================================
18
+ // 数据库模型定义
19
+ // ============================================================================
20
+
21
+ /**
22
+ * 聊天消息记录模型定义
23
+ */
24
+ export const CHAT_MESSAGE_MODEL = {
25
+ platform: { type: 'text' as const, nullable: false }, // 平台:icqq, kook, discord 等
26
+ scene_id: { type: 'text' as const, nullable: false }, // 场景ID:群号/频道ID/用户ID
27
+ scene_type: { type: 'text' as const, nullable: false }, // 场景类型:group, private, channel
28
+ scene_name: { type: 'text' as const, default: '' }, // 场景名称
29
+ sender_id: { type: 'text' as const, nullable: false }, // 发送者ID
30
+ sender_name: { type: 'text' as const, default: '' }, // 发送者名称
31
+ message: { type: 'text' as const, nullable: false }, // 消息内容
32
+ time: { type: 'integer' as const, nullable: false }, // 时间戳(毫秒)
33
+ };
34
+
35
+ /**
36
+ * 上下文总结模型定义
37
+ */
38
+ export const CONTEXT_SUMMARY_MODEL = {
39
+ scene_id: { type: 'text' as const, nullable: false }, // 场景ID
40
+ summary: { type: 'text' as const, nullable: false }, // 总结内容
41
+ message_count: { type: 'integer' as const, default: 0 }, // 包含的消息数量
42
+ start_time: { type: 'integer' as const, default: 0 }, // 总结的起始时间
43
+ end_time: { type: 'integer' as const, default: 0 }, // 总结的结束时间
44
+ created_at: { type: 'integer' as const, default: 0 }, // 创建时间
45
+ };
46
+
47
+ // ============================================================================
48
+ // 类型定义
49
+ // ============================================================================
50
+
51
+ /**
52
+ * 消息记录
53
+ */
54
+ export interface MessageRecord {
55
+ id?: number;
56
+ platform: string;
57
+ scene_id: string;
58
+ scene_type: 'group' | 'private' | 'channel' | string;
59
+ scene_name: string;
60
+ sender_id: string;
61
+ sender_name: string;
62
+ message: string;
63
+ time: number;
64
+ }
65
+
66
+ /**
67
+ * 上下文总结记录
68
+ */
69
+ export interface SummaryRecord {
70
+ id?: number;
71
+ scene_id: string;
72
+ summary: string;
73
+ message_count: number;
74
+ start_time: number;
75
+ end_time: number;
76
+ created_at: number;
77
+ }
78
+
79
+ /**
80
+ * 上下文配置
81
+ */
82
+ export interface ContextConfig {
83
+ /** 是否启用上下文管理(默认 true) */
84
+ enabled?: boolean;
85
+ /** 读取的最近消息数量(默认 100) */
86
+ maxRecentMessages?: number;
87
+ /** 触发总结的消息数量阈值(默认 50) */
88
+ summaryThreshold?: number;
89
+ /** 总结后保留的消息数量(默认 10) */
90
+ keepAfterSummary?: number;
91
+ /** 上下文最大 token 估算(默认 4000) */
92
+ maxContextTokens?: number;
93
+ /** 总结提示词 */
94
+ summaryPrompt?: string;
95
+ }
96
+
97
+ /**
98
+ * 场景上下文
99
+ */
100
+ export interface SceneContext {
101
+ sceneId: string;
102
+ sceneType: string;
103
+ sceneName: string;
104
+ platform: string;
105
+ /** 历史总结 */
106
+ summaries: string[];
107
+ /** 最近消息 */
108
+ recentMessages: MessageRecord[];
109
+ /** 格式化的聊天消息 */
110
+ chatMessages: ChatMessage[];
111
+ }
112
+
113
+ // ============================================================================
114
+ // 上下文管理器
115
+ // ============================================================================
116
+
117
+ /**
118
+ * 上下文管理器
119
+ * 负责消息记录、历史读取和智能总结
120
+ */
121
+ export class ContextManager {
122
+ private messageModel: any;
123
+ private summaryModel: any;
124
+ private config: Required<ContextConfig>;
125
+ private aiProvider?: AIProvider;
126
+
127
+ constructor(
128
+ messageModel: any,
129
+ summaryModel: any,
130
+ config: ContextConfig = {}
131
+ ) {
132
+ this.messageModel = messageModel;
133
+ this.summaryModel = summaryModel;
134
+ this.config = {
135
+ enabled: config.enabled ?? true,
136
+ maxRecentMessages: config.maxRecentMessages ?? 100,
137
+ summaryThreshold: config.summaryThreshold ?? 50,
138
+ keepAfterSummary: config.keepAfterSummary ?? 10,
139
+ maxContextTokens: config.maxContextTokens ?? 4000,
140
+ summaryPrompt: config.summaryPrompt ?? this.getDefaultSummaryPrompt(),
141
+ };
142
+ }
143
+
144
+ /**
145
+ * 设置 AI 提供商(用于自动总结)
146
+ */
147
+ setAIProvider(provider: AIProvider): void {
148
+ this.aiProvider = provider;
149
+ }
150
+
151
+ /**
152
+ * 记录消息
153
+ */
154
+ async recordMessage(record: Omit<MessageRecord, 'id'>): Promise<void> {
155
+ try {
156
+ await this.messageModel.create(record);
157
+ } catch (error) {
158
+ logger.debug('记录消息失败:', error);
159
+ }
160
+ }
161
+
162
+ /**
163
+ * 获取场景的最近消息
164
+ */
165
+ async getRecentMessages(
166
+ sceneId: string,
167
+ limit: number = this.config.maxRecentMessages
168
+ ): Promise<MessageRecord[]> {
169
+ try {
170
+ // 查询最近的消息,按时间倒序
171
+ const messages = await this.messageModel.select(
172
+ { scene_id: sceneId },
173
+ { orderBy: { time: 'desc' }, limit }
174
+ );
175
+ // 反转为时间正序
176
+ return (messages as MessageRecord[]).reverse();
177
+ } catch (error) {
178
+ logger.debug('获取最近消息失败:', error);
179
+ return [];
180
+ }
181
+ }
182
+
183
+ /**
184
+ * 获取场景的总结历史
185
+ */
186
+ async getSummaries(sceneId: string): Promise<SummaryRecord[]> {
187
+ try {
188
+ const summaries = await this.summaryModel.select(
189
+ { scene_id: sceneId },
190
+ { orderBy: { created_at: 'asc' } }
191
+ );
192
+ return summaries as SummaryRecord[];
193
+ } catch (error) {
194
+ logger.debug('获取总结失败:', error);
195
+ return [];
196
+ }
197
+ }
198
+
199
+ /**
200
+ * 构建场景上下文
201
+ * 用于 AI 对话,包含历史总结和最近消息
202
+ */
203
+ async buildContext(sceneId: string, platform: string): Promise<SceneContext> {
204
+ // 获取最近消息
205
+ const recentMessages = await this.getRecentMessages(sceneId);
206
+
207
+ // 获取历史总结
208
+ const summaries = await this.getSummaries(sceneId);
209
+
210
+ // 获取场景信息
211
+ const sceneInfo = recentMessages.length > 0
212
+ ? { sceneType: recentMessages[0].scene_type, sceneName: recentMessages[0].scene_name }
213
+ : { sceneType: 'unknown', sceneName: '' };
214
+
215
+ // 构建聊天消息格式
216
+ const chatMessages = this.formatToChatMessages(
217
+ summaries.map(s => s.summary),
218
+ recentMessages
219
+ );
220
+
221
+ return {
222
+ sceneId,
223
+ sceneType: sceneInfo.sceneType,
224
+ sceneName: sceneInfo.sceneName,
225
+ platform,
226
+ summaries: summaries.map(s => s.summary),
227
+ recentMessages,
228
+ chatMessages,
229
+ };
230
+ }
231
+
232
+ /**
233
+ * 格式化为 ChatMessage 格式
234
+ */
235
+ formatToChatMessages(
236
+ summaries: string[],
237
+ messages: MessageRecord[]
238
+ ): ChatMessage[] {
239
+ const chatMessages: ChatMessage[] = [];
240
+
241
+ // 添加历史总结作为系统上下文
242
+ if (summaries.length > 0) {
243
+ chatMessages.push({
244
+ role: 'system',
245
+ content: `以下是之前对话的总结:\n\n${summaries.join('\n\n---\n\n')}\n\n请基于这些背景信息继续对话。`,
246
+ });
247
+ }
248
+
249
+ // 添加最近消息
250
+ for (const msg of messages) {
251
+ // 判断是否是机器人消息(通常 sender_id 包含 bot 标识)
252
+ const isBot = msg.sender_id.includes('bot') || msg.sender_name.toLowerCase().includes('bot');
253
+
254
+ chatMessages.push({
255
+ role: isBot ? 'assistant' : 'user',
256
+ content: `[${msg.sender_name}]: ${msg.message}`,
257
+ name: msg.sender_id,
258
+ });
259
+ }
260
+
261
+ return chatMessages;
262
+ }
263
+
264
+ /**
265
+ * 估算 token 数量(粗略估算)
266
+ */
267
+ estimateTokens(text: string): number {
268
+ // 中文约 1 字 = 2 tokens,英文约 4 字符 = 1 token
269
+ const chineseChars = (text.match(/[\u4e00-\u9fff]/g) || []).length;
270
+ const otherChars = text.length - chineseChars;
271
+ return Math.ceil(chineseChars * 2 + otherChars / 4);
272
+ }
273
+
274
+ /**
275
+ * 检查是否需要总结
276
+ */
277
+ async shouldSummarize(sceneId: string): Promise<boolean> {
278
+ const messages = await this.getRecentMessages(sceneId, this.config.summaryThreshold + 10);
279
+
280
+ if (messages.length < this.config.summaryThreshold) {
281
+ return false;
282
+ }
283
+
284
+ // 估算 token 数量
285
+ const totalText = messages.map(m => m.message).join('\n');
286
+ const estimatedTokens = this.estimateTokens(totalText);
287
+
288
+ return estimatedTokens > this.config.maxContextTokens;
289
+ }
290
+
291
+ /**
292
+ * 执行总结
293
+ */
294
+ async summarize(sceneId: string): Promise<string | null> {
295
+ if (!this.aiProvider) {
296
+ logger.warn('未设置 AI provider,无法总结');
297
+ return null;
298
+ }
299
+
300
+ const messages = await this.getRecentMessages(sceneId, this.config.summaryThreshold);
301
+
302
+ if (messages.length < this.config.keepAfterSummary) {
303
+ return null;
304
+ }
305
+
306
+ // 需要总结的消息(排除最近的几条)
307
+ const toSummarize = messages.slice(0, -this.config.keepAfterSummary);
308
+
309
+ if (toSummarize.length === 0) {
310
+ return null;
311
+ }
312
+
313
+ // 格式化消息用于总结
314
+ const conversationText = toSummarize
315
+ .map(m => `[${m.sender_name}] (${new Date(m.time).toLocaleString()}): ${m.message}`)
316
+ .join('\n');
317
+
318
+ try {
319
+ // 调用 AI 进行总结
320
+ const response = await this.aiProvider.chat({
321
+ model: this.aiProvider.models[0],
322
+ messages: [
323
+ { role: 'system', content: this.config.summaryPrompt },
324
+ { role: 'user', content: conversationText },
325
+ ],
326
+ temperature: 0.3,
327
+ max_tokens: 500,
328
+ });
329
+
330
+ const summary = response.choices[0]?.message?.content;
331
+ if (typeof summary !== 'string' || !summary.trim()) {
332
+ return null;
333
+ }
334
+
335
+ // 保存总结
336
+ await this.saveSummary({
337
+ scene_id: sceneId,
338
+ summary: summary.trim(),
339
+ message_count: toSummarize.length,
340
+ start_time: toSummarize[0].time,
341
+ end_time: toSummarize[toSummarize.length - 1].time,
342
+ created_at: Date.now(),
343
+ });
344
+
345
+ // 删除已总结的消息(可选,保持数据库精简)
346
+ // await this.deleteMessages(sceneId, toSummarize.map(m => m.id!));
347
+
348
+ return summary;
349
+ } catch (error) {
350
+ logger.error('总结失败:', error);
351
+ return null;
352
+ }
353
+ }
354
+
355
+ /**
356
+ * 保存总结
357
+ */
358
+ private async saveSummary(record: Omit<SummaryRecord, 'id'>): Promise<void> {
359
+ try {
360
+ await this.summaryModel.create(record);
361
+ } catch (error) {
362
+ logger.error('保存总结失败:', error);
363
+ }
364
+ }
365
+
366
+ /**
367
+ * 自动检查并总结(在每次对话后调用)
368
+ */
369
+ async autoSummarizeIfNeeded(sceneId: string): Promise<void> {
370
+ const needSummary = await this.shouldSummarize(sceneId);
371
+ if (needSummary) {
372
+ logger.debug(`总结场景 ${sceneId}...`);
373
+ await this.summarize(sceneId);
374
+ }
375
+ }
376
+
377
+ /**
378
+ * 获取场景统计信息
379
+ */
380
+ async getSceneStats(sceneId: string): Promise<{
381
+ messageCount: number;
382
+ summaryCount: number;
383
+ firstMessageTime?: number;
384
+ lastMessageTime?: number;
385
+ }> {
386
+ const messages = await this.getRecentMessages(sceneId, 1000);
387
+ const summaries = await this.getSummaries(sceneId);
388
+
389
+ return {
390
+ messageCount: messages.length,
391
+ summaryCount: summaries.length,
392
+ firstMessageTime: messages.length > 0 ? messages[0].time : undefined,
393
+ lastMessageTime: messages.length > 0 ? messages[messages.length - 1].time : undefined,
394
+ };
395
+ }
396
+
397
+ /**
398
+ * 清理过期消息
399
+ */
400
+ async cleanupOldMessages(maxAge: number = 30 * 24 * 60 * 60 * 1000): Promise<number> {
401
+ const cutoff = Date.now() - maxAge;
402
+ try {
403
+ // 这里假设数据库支持条件删除
404
+ // 实际实现可能需要根据数据库适配器调整
405
+ const result = await this.messageModel.delete({ time: { $lt: cutoff } });
406
+ return typeof result === 'number' ? result : 0;
407
+ } catch (error) {
408
+ logger.debug('清理旧消息失败:', error);
409
+ return 0;
410
+ }
411
+ }
412
+
413
+ /**
414
+ * 默认总结提示词
415
+ */
416
+ private getDefaultSummaryPrompt(): string {
417
+ return `你是一个对话总结助手。请将以下对话内容总结为简洁的要点,保留关键信息、人物关系和重要事件。
418
+
419
+ 要求:
420
+ 1. 使用第三人称描述
421
+ 2. 保留重要的人名、事件、决定
422
+ 3. 忽略日常寒暄和无意义的对话
423
+ 4. 总结应该简洁,不超过 200 字
424
+ 5. 如果对话中有明确的结论或决定,务必保留
425
+
426
+ 请直接输出总结内容,不要添加额外的标题或格式。`;
427
+ }
428
+ }
429
+
430
+ /**
431
+ * 创建上下文管理器
432
+ */
433
+ export function createContextManager(
434
+ messageModel: any,
435
+ summaryModel: any,
436
+ config?: ContextConfig
437
+ ): ContextManager {
438
+ return new ContextManager(messageModel, summaryModel, config);
439
+ }