koishi-plugin-group-memory 1.2.0 → 2.0.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/index.js +574 -137
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "koishi-plugin-group-memory",
3
- "version": "1.2.0",
3
+ "version": "2.0.0",
4
4
  "description": "智能群聊记忆中间件:自动记录上下文,在@或回复时注入前情提要,支持时间窗口过滤与内容清洗",
5
5
  "main": "./src/index.js",
6
6
  "scripts": {
package/src/index.js CHANGED
@@ -4,219 +4,559 @@ exports.name = 'group-memory';
4
4
  exports.usage = '自动记录群聊和私聊上下文,并为 AI 插件提供历史记录查询接口。支持数据库存储,群聊共享上下文,私聊独立上下文。';
5
5
 
6
6
  // --- 数据库表结构 ---
7
- const MessageSchema = Schema.object({
8
- id: Schema.number().role('primary').required(),
9
- session_id: Schema.string().required(), // 会话ID:private:{userId} 或 group:{groupId}
10
- session_type: Schema.string().required(), // 'private' 或 'group'
11
- user_id: Schema.string().required(),
12
- user_name: Schema.string().default(''),
13
- role: Schema.string().required(), // 'user' 或 'assistant'
14
- content: Schema.string().required(),
15
- timestamp: Schema.date().required(),
16
- });
7
+ const MessageFields = {
8
+ id: 'unsigned',
9
+ session_id: 'string(255)',
10
+ session_type: 'string(50)',
11
+ user_id: 'string(100)',
12
+ user_name: 'string(255)',
13
+ role: 'string(50)',
14
+ content: 'text',
15
+ timestamp: 'unsigned',
16
+ };
17
17
 
18
18
  // --- 配置架构 ---
19
19
  exports.Config = Schema.object({
20
- // --- 存储设置 ---
21
- maxContext: Schema.number()
22
- .default(20)
23
- .min(1)
24
- .max(100)
25
- .description('每个会话保留最近多少条消息作为上下文。'),
26
-
27
- timeWindow: Schema.number()
28
- .default(3600000) // 1小时
29
- .min(0)
30
- .step(60000) // 1分钟步进
31
- .description('只保留最近 N 毫秒内的消息。设置为 0 表示不限制时间,只按条数限制。'),
32
-
33
- // --- 记录设置 ---
34
- recordPrivateChat: Schema.boolean()
20
+ // ... 其他配置保持不变 ...
21
+
22
+ // ============================================
23
+ // 新增:日志显示功能
24
+ // ============================================
25
+ logLevel: Schema.union([
26
+ Schema.const('none').description('不输出日志'),
27
+ Schema.const('simple').description('简易模式:输出易于理解的操作摘要'),
28
+ Schema.const('detailed').description('详细模式:输出完整的 JSON 格式日志'),
29
+ ]).default('simple').description('日志输出级别'),
30
+
31
+ logDatabaseOps: Schema.boolean()
35
32
  .default(true)
36
- .description('是否记录私聊消息。'),
33
+ .description('是否记录数据库操作(增删改查)'),
37
34
 
38
- recordGroupChat: Schema.boolean()
35
+ logContextInject: Schema.boolean()
39
36
  .default(true)
40
- .description('是否记录群聊消息。'),
37
+ .description('是否记录上下文注入操作'),
41
38
 
42
- // --- 触发条件(用于中间件模式) ---
43
- triggerOnAt: Schema.boolean()
39
+ logApiCalls: Schema.boolean()
44
40
  .default(true)
45
- .description('当消息中包含 @机器人 时,触发上下文注入。'),
41
+ .description('是否记录对外部插件的 API 调用'),
46
42
 
47
- triggerOnReply: Schema.boolean()
43
+ logContentPreview: Schema.boolean()
48
44
  .default(true)
49
- .description('当消息是回复机器人的消息时,触发上下文注入。'),
45
+ .description('是否在简易模式下预览消息内容(只显示前30个字符)'),
50
46
 
51
- // --- 内容处理 ---
52
- cleanMentions: Schema.boolean()
47
+ logColorOutput: Schema.boolean()
53
48
  .default(true)
54
- .description('发送给 AI 前,是否移除消息中的 @ 符号 (避免 AI 困惑)。'),
49
+ .description('是否使用颜色区分不同类型的日志(需终端支持)'),
55
50
 
56
- cleanImages: Schema.boolean()
51
+ logTimestamp: Schema.boolean()
57
52
  .default(true)
58
- .description('是否移除图片/媒体占位符 (如 [图片])。'),
59
-
60
- // --- 调试 ---
61
- verbose: Schema.boolean()
62
- .default(false)
63
- .description('是否在控制台打印注入的上下文 (调试用)。'),
53
+ .description('是否在日志中显示时间戳'),
64
54
  });
65
55
 
66
56
  exports.schema = exports.Config;
67
57
 
68
- // --- 声明依赖注入 ---
69
58
  exports.inject = ['database'];
70
59
 
71
60
  exports.apply = (ctx, config) => {
72
61
  const logger = ctx.logger('group-memory');
73
62
 
74
- // 内存缓存(用于快速访问,数据库持久化)
63
+ // ============================================
64
+ // 日志增强模块
65
+ // ============================================
66
+
67
+ // 颜色代码(仅在启用时使用)
68
+ const colors = {
69
+ reset: '\x1b[0m',
70
+ bright: '\x1b[1m',
71
+ dim: '\x1b[2m',
72
+ underscore: '\x1b[4m',
73
+ blink: '\x1b[5m',
74
+ reverse: '\x1b[7m',
75
+ hidden: '\x1b[8m',
76
+
77
+ fg: {
78
+ black: '\x1b[30m',
79
+ red: '\x1b[31m',
80
+ green: '\x1b[32m',
81
+ yellow: '\x1b[33m',
82
+ blue: '\x1b[34m',
83
+ magenta: '\x1b[35m',
84
+ cyan: '\x1b[36m',
85
+ white: '\x1b[37m',
86
+ crimson: '\x1b[38m'
87
+ },
88
+ bg: {
89
+ black: '\x1b[40m',
90
+ red: '\x1b[41m',
91
+ green: '\x1b[42m',
92
+ yellow: '\x1b[43m',
93
+ blue: '\x1b[44m',
94
+ magenta: '\x1b[45m',
95
+ cyan: '\x1b[46m',
96
+ white: '\x1b[47m',
97
+ crimson: '\x1b[48m'
98
+ }
99
+ };
100
+
101
+ // 是否启用颜色
102
+ const useColor = config.logColorOutput && process.stdout.isTTY;
103
+
104
+ // 颜色辅助函数
105
+ const color = (text, colorCode) => {
106
+ if (!useColor) return text;
107
+ return `${colorCode}${text}${colors.reset}`;
108
+ };
109
+
110
+ // 格式化时间戳
111
+ const getTimestamp = () => {
112
+ if (!config.logTimestamp) return '';
113
+ const now = new Date();
114
+ return `[${now.toLocaleTimeString()}.${now.getMilliseconds().toString().padStart(3, '0')}] `;
115
+ };
116
+
117
+ // 日志类型图标
118
+ const icons = {
119
+ db: '🗄️',
120
+ cache: '⚡',
121
+ inject: '📎',
122
+ api: '🔌',
123
+ command: '⌨️',
124
+ error: '❌',
125
+ warning: '⚠️',
126
+ success: '✅',
127
+ info: 'ℹ️',
128
+ debug: '🔍',
129
+ memory: '🧠',
130
+ cleanup: '🧹'
131
+ };
132
+
133
+ // 简易日志函数
134
+ const logSimple = (type, message, data = null) => {
135
+ if (config.logLevel === 'none') return;
136
+
137
+ const timestamp = getTimestamp();
138
+ const icon = icons[type] || '📝';
139
+ let colorFn;
140
+
141
+ switch(type) {
142
+ case 'error': colorFn = (t) => color(t, colors.fg.red); break;
143
+ case 'warning': colorFn = (t) => color(t, colors.fg.yellow); break;
144
+ case 'success': colorFn = (t) => color(t, colors.fg.green); break;
145
+ case 'db': colorFn = (t) => color(t, colors.fg.cyan); break;
146
+ case 'inject': colorFn = (t) => color(t, colors.fg.magenta); break;
147
+ case 'api': colorFn = (t) => color(t, colors.fg.blue); break;
148
+ default: colorFn = (t) => t;
149
+ }
150
+
151
+ const logMessage = `${timestamp}${icon} ${colorFn(message)}`;
152
+
153
+ if (data && config.logContentPreview) {
154
+ let preview = '';
155
+ if (typeof data === 'string') {
156
+ preview = data.length > 30 ? data.substring(0, 30) + '...' : data;
157
+ } else if (data.content) {
158
+ preview = data.content.length > 30 ? data.content.substring(0, 30) + '...' : data.content;
159
+ }
160
+ if (preview) {
161
+ logger.info(`${logMessage} ${color(`"${preview}"`, colors.fg.yellow)}`);
162
+ } else {
163
+ logger.info(logMessage);
164
+ }
165
+ } else {
166
+ logger.info(logMessage);
167
+ }
168
+ };
169
+
170
+ // 详细日志函数
171
+ const logDetailed = (type, operation, details) => {
172
+ if (config.logLevel !== 'detailed') return;
173
+
174
+ const timestamp = getTimestamp();
175
+ const icon = icons[type] || '📝';
176
+
177
+ const logEntry = {
178
+ timestamp: new Date().toISOString(),
179
+ type,
180
+ operation,
181
+ ...details
182
+ };
183
+
184
+ logger.info(`${timestamp}${icon} ${JSON.stringify(logEntry, null, 2)}`);
185
+ };
186
+
187
+ // 统一日志接口
188
+ const log = {
189
+ db: (operation, details, simpleMsg) => {
190
+ if (!config.logDatabaseOps) return;
191
+ if (config.logLevel === 'detailed') {
192
+ logDetailed('db', operation, details);
193
+ } else if (config.logLevel === 'simple') {
194
+ logSimple('db', simpleMsg || `${operation} 操作`, details);
195
+ }
196
+ },
197
+
198
+ inject: (operation, details, simpleMsg) => {
199
+ if (!config.logContextInject) return;
200
+ if (config.logLevel === 'detailed') {
201
+ logDetailed('inject', operation, details);
202
+ } else if (config.logLevel === 'simple') {
203
+ logSimple('inject', simpleMsg || `上下文注入: ${operation}`, details);
204
+ }
205
+ },
206
+
207
+ api: (operation, details, simpleMsg) => {
208
+ if (!config.logApiCalls) return;
209
+ if (config.logLevel === 'detailed') {
210
+ logDetailed('api', operation, details);
211
+ } else if (config.logLevel === 'simple') {
212
+ logSimple('api', simpleMsg || `API调用: ${operation}`, details);
213
+ }
214
+ },
215
+
216
+ command: (cmd, user, details) => {
217
+ if (config.logLevel === 'none') return;
218
+ const simpleMsg = `命令 ${cmd} 由 ${user} 执行`;
219
+ if (config.logLevel === 'detailed') {
220
+ logDetailed('command', cmd, { user, ...details });
221
+ } else {
222
+ logSimple('command', simpleMsg, details);
223
+ }
224
+ },
225
+
226
+ error: (msg, error) => {
227
+ if (config.logLevel === 'none') return;
228
+ const errorDetails = error ? { message: error.message, stack: error.stack } : {};
229
+ if (config.logLevel === 'detailed') {
230
+ logDetailed('error', '错误', { message: msg, ...errorDetails });
231
+ } else {
232
+ logSimple('error', `❌ ${msg}`, error);
233
+ }
234
+ },
235
+
236
+ success: (msg, details) => {
237
+ if (config.logLevel === 'none') return;
238
+ if (config.logLevel === 'detailed') {
239
+ logDetailed('success', '成功', { message: msg, ...details });
240
+ } else {
241
+ logSimple('success', `✅ ${msg}`, details);
242
+ }
243
+ },
244
+
245
+ cache: (operation, details) => {
246
+ if (config.logLevel === 'none') return;
247
+ if (config.logLevel === 'detailed') {
248
+ logDetailed('cache', operation, details);
249
+ } else {
250
+ logSimple('cache', `缓存 ${operation}`, details);
251
+ }
252
+ },
253
+
254
+ cleanup: (count, details) => {
255
+ if (config.logLevel === 'none') return;
256
+ const simpleMsg = `清理了 ${count} 条过期消息`;
257
+ if (config.logLevel === 'detailed') {
258
+ logDetailed('cleanup', '清理', { count, ...details });
259
+ } else {
260
+ logSimple('cleanup', simpleMsg, details);
261
+ }
262
+ }
263
+ };
264
+
265
+ // 内存缓存
75
266
  const memoryCache = new Map();
267
+ const CACHE_TTL = 5 * 60 * 1000;
268
+ const cacheTimestamps = new Map();
269
+
270
+ // 会话锁
271
+ const sessionLocks = new Map();
272
+
273
+ const lockSession = async (sessionId, timeout = 30000) => {
274
+ const startTime = Date.now();
275
+ while (sessionLocks.has(sessionId)) {
276
+ if (Date.now() - startTime > timeout) {
277
+ log.error(`获取会话锁超时: ${sessionId}`);
278
+ throw new Error(`获取会话锁超时: ${sessionId}`);
279
+ }
280
+ await new Promise(resolve => setTimeout(resolve, 100));
281
+ }
282
+ sessionLocks.set(sessionId, true);
283
+ log.cache('锁定', { sessionId, action: 'lock' });
284
+ return () => {
285
+ sessionLocks.delete(sessionId);
286
+ log.cache('解锁', { sessionId, action: 'unlock' });
287
+ };
288
+ };
76
289
 
77
290
  // 确保数据库表存在
78
- ctx.database.extend('group_memory_messages', MessageSchema, {
291
+ ctx.database.extend('group_memory_messages', MessageFields, {
79
292
  autoInc: true,
293
+ primary: 'id',
294
+ index: ['session_id', 'session_type', 'timestamp']
80
295
  });
296
+
297
+ log.success('数据库表初始化完成', { table: 'group_memory_messages' });
81
298
 
82
299
  // --- 辅助函数:生成会话ID ---
83
- // 私聊: private:{userId}
84
- // 群聊: group:{groupId}(所有人共享)
85
300
  const generateSessionId = (session) => {
86
- if (!session.groupId) {
87
- return { sessionId: `private:${session.userId}`, sessionType: 'private' };
88
- } else {
89
- return { sessionId: `group:${session.groupId}`, sessionType: 'group' };
90
- }
91
- };
92
-
93
- // --- 辅助函数:格式化时间 ---
94
- const formatTime = (timestamp) => {
95
- const date = new Date(timestamp);
96
- return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
301
+ const result = !session.groupId
302
+ ? { sessionId: `private:${session.userId}`, sessionType: 'private' }
303
+ : {
304
+ sessionId: `group:${session.groupId}`,
305
+ separateId: config.supportSeparateContext ? `${session.groupId}:user:${session.userId}` : undefined,
306
+ sessionType: 'group'
307
+ };
308
+
309
+ log.db('生成会话ID', {
310
+ input: { userId: session.userId, groupId: session.groupId },
311
+ output: result
312
+ }, `生成会话ID: ${result.sessionId}`);
313
+
314
+ return result;
97
315
  };
98
316
 
99
317
  // --- 辅助函数:清理内容 ---
100
318
  const cleanContent = (content, session) => {
101
319
  let text = content;
320
+ const originalLength = text.length;
102
321
 
103
- if (config.cleanMentions) {
104
- text = text.replace(new RegExp(`@${session.selfId}`, 'g'), '');
105
- text = text.replace(/@\S+/g, (match) => match.replace('@', ''));
322
+ if (config.cleanMentions && session) {
323
+ text = text.replace(new RegExp(`<@${session.selfId}>`, 'g'), '')
324
+ .replace(new RegExp(`@${session.selfId}\\b`, 'g'), '')
325
+ .replace(/@(\S+)/g, '$1');
106
326
  }
107
327
 
108
328
  if (config.cleanImages) {
109
- text = text.replace(/\[图片\]/g, '[图片已省略]');
110
- text = text.replace(/\[表情.*?\]/g, '');
111
- text = text.replace(/\[CQ:.*?\]/g, '');
329
+ text = text.replace(/\[图片\]/g, '[图片]')
330
+ .replace(/\[表情.*?\]/g, '')
331
+ .replace(/\[CQ:.*?\]/g, '')
332
+ .replace(/!?\[.*?\]\(.*?\)/g, '[链接]');
112
333
  }
113
334
 
114
- return text.trim();
335
+ const result = text.trim() || '[空消息]';
336
+
337
+ log.db('清理内容', {
338
+ original: { length: originalLength, preview: content.substring(0, 30) },
339
+ result: { length: result.length, preview: result.substring(0, 30) },
340
+ operations: { cleanMentions: config.cleanMentions, cleanImages: config.cleanImages }
341
+ }, `清理内容: ${originalLength} -> ${result.length} 字符`);
342
+
343
+ return result;
115
344
  };
116
345
 
117
- // --- 核心 API:获取上下文(供其他插件调用) ---
346
+ // --- 辅助函数:格式化时间 ---
347
+ const formatTime = (timestamp) => {
348
+ const date = new Date(timestamp);
349
+ return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
350
+ };
351
+
352
+ // --- 清理缓存 ---
353
+ const cleanupCache = () => {
354
+ const now = Date.now();
355
+ let cleaned = 0;
356
+ for (const [sessionId, timestamp] of cacheTimestamps.entries()) {
357
+ if (now - timestamp > CACHE_TTL) {
358
+ memoryCache.delete(sessionId);
359
+ cacheTimestamps.delete(sessionId);
360
+ cleaned++;
361
+ }
362
+ }
363
+ if (cleaned > 0) {
364
+ log.cache('清理过期缓存', { cleaned, ttl: CACHE_TTL });
365
+ }
366
+ };
367
+ setInterval(cleanupCache, CACHE_TTL);
368
+
369
+ // --- 核心 API:获取上下文 ---
118
370
  const getContext = async (sessionId, limit = null) => {
371
+ log.api('getContext', { sessionId, limit }, `获取上下文: ${sessionId}`);
372
+
373
+ // 尝试从缓存获取
374
+ if (memoryCache.has(sessionId)) {
375
+ const cached = memoryCache.get(sessionId);
376
+ const age = Date.now() - cacheTimestamps.get(sessionId);
377
+ if (age < CACHE_TTL) {
378
+ log.cache('命中', { sessionId, age, count: cached.length }, `缓存命中: ${sessionId} (${cached.length}条)`);
379
+ return cached;
380
+ } else {
381
+ log.cache('过期', { sessionId, age }, `缓存过期: ${sessionId}`);
382
+ }
383
+ }
384
+
119
385
  const maxMessages = limit ? limit * 2 : config.maxContext * 2;
386
+ log.db('查询', { sessionId, maxMessages }, `查询数据库: ${sessionId}`);
120
387
 
121
388
  try {
122
- // 从数据库获取
123
389
  let history = await ctx.database.get('group_memory_messages',
124
390
  { session_id: sessionId },
125
391
  {
126
- fields: ['id', 'session_id', 'role', 'content', 'timestamp', 'user_name'],
392
+ fields: ['id', 'session_id', 'role', 'content', 'timestamp', 'user_name', 'user_id'],
127
393
  orderBy: { id: 'desc' },
128
394
  limit: maxMessages
129
395
  }
130
396
  );
131
397
 
132
- // 反转数组以恢复时间顺序
133
398
  history.reverse();
134
399
 
135
- // 如果时间窗口设置,进行过滤
136
400
  if (config.timeWindow > 0) {
401
+ const before = history.length;
137
402
  const cutoff = Date.now() - config.timeWindow;
138
- history = history.filter(msg => new Date(msg.timestamp).getTime() >= cutoff);
403
+ history = history.filter(msg => msg.timestamp >= cutoff);
404
+ log.db('时间窗口过滤', { before, after: history.length, cutoff }, `过滤时间窗口: ${before} -> ${history.length}条`);
139
405
  }
140
406
 
407
+ // 更新缓存
408
+ memoryCache.set(sessionId, history);
409
+ cacheTimestamps.set(sessionId, Date.now());
410
+ log.cache('更新', { sessionId, count: history.length }, `更新缓存: ${sessionId} (${history.length}条)`);
411
+
141
412
  return history;
142
413
  } catch (error) {
143
- logger.error('获取上下文失败:', error.message);
414
+ log.error('获取上下文失败', error);
144
415
  return [];
145
416
  }
146
417
  };
147
418
 
148
- // --- 核心 API:保存消息(供其他插件调用) ---
419
+ // --- 核心 API:保存消息 ---
149
420
  const saveMessage = async (sessionId, role, content, userId = 'system', userName = 'System') => {
421
+ log.api('saveMessage', { sessionId, role, userId }, `保存消息: ${role} @ ${sessionId}`);
422
+
423
+ const release = await lockSession(sessionId);
424
+
150
425
  try {
151
- // 解析 sessionId 获取 sessionType
152
426
  const sessionType = sessionId.startsWith('private:') ? 'private' : 'group';
153
427
 
154
- // 保存到数据库
155
- await ctx.database.create('group_memory_messages', {
428
+ const messageData = {
156
429
  session_id: sessionId,
157
430
  session_type: sessionType,
158
431
  user_id: userId,
159
432
  user_name: userName,
160
433
  role: role,
161
434
  content: content,
162
- timestamp: new Date(),
163
- });
435
+ timestamp: Date.now(),
436
+ };
437
+
438
+ log.db('创建', messageData, `插入消息: ${role} (${content.substring(0, 30)}...)`);
164
439
 
165
- // 清理旧消息
166
- await cleanupOldMessages(sessionId);
440
+ await ctx.database.create('group_memory_messages', messageData);
441
+
442
+ // 使缓存失效
443
+ memoryCache.delete(sessionId);
444
+ cacheTimestamps.delete(sessionId);
445
+ log.cache('失效', { sessionId }, `缓存失效: ${sessionId}`);
446
+
447
+ // 异步清理旧消息
448
+ cleanupOldMessages(sessionId).catch(err => {
449
+ log.error('异步清理失败', err);
450
+ });
167
451
 
168
452
  return true;
169
453
  } catch (error) {
170
- logger.error('保存消息失败:', error.message);
454
+ log.error('保存消息失败', error);
455
+ return false;
456
+ } finally {
457
+ release();
458
+ }
459
+ };
460
+
461
+ // --- API:删除最后一条消息 ---
462
+ const deleteLastMessage = async (sessionId, role) => {
463
+ log.api('deleteLastMessage', { sessionId, role }, `删除最后一条 ${role} 消息: ${sessionId}`);
464
+
465
+ const release = await lockSession(sessionId);
466
+
467
+ try {
468
+ const lastMessage = await ctx.database.get('group_memory_messages',
469
+ { session_id: sessionId, role: role },
470
+ {
471
+ orderBy: { id: 'desc' },
472
+ limit: 1
473
+ }
474
+ );
475
+
476
+ if (lastMessage && lastMessage.length > 0) {
477
+ log.db('删除', { id: lastMessage[0].id }, `删除消息 ID: ${lastMessage[0].id}`);
478
+ await ctx.database.remove('group_memory_messages', { id: lastMessage[0].id });
479
+ memoryCache.delete(sessionId);
480
+ cacheTimestamps.delete(sessionId);
481
+ return true;
482
+ }
483
+
484
+ log.db('删除', { sessionId, role }, `未找到要删除的消息`);
485
+ return false;
486
+ } catch (error) {
487
+ log.error('删除消息失败', error);
171
488
  return false;
489
+ } finally {
490
+ release();
172
491
  }
173
492
  };
174
493
 
175
- // --- 核心 API:清空上下文(供其他插件调用) ---
494
+ // --- API:清空上下文 ---
176
495
  const clearContext = async (sessionId) => {
496
+ log.api('clearContext', { sessionId }, `清空上下文: ${sessionId}`);
497
+
498
+ const release = await lockSession(sessionId);
499
+
177
500
  try {
178
- await ctx.database.remove('group_memory_messages', { session_id: sessionId });
501
+ const count = await ctx.database.remove('group_memory_messages', { session_id: sessionId });
502
+ log.db('清空', { sessionId, count }, `清空了 ${count} 条消息`);
503
+
179
504
  memoryCache.delete(sessionId);
505
+ cacheTimestamps.delete(sessionId);
506
+ log.cache('失效', { sessionId }, `缓存失效: ${sessionId}`);
507
+
180
508
  return true;
181
509
  } catch (error) {
182
- logger.error('清空上下文失败:', error.message);
510
+ log.error('清空上下文失败', error);
183
511
  return false;
512
+ } finally {
513
+ release();
184
514
  }
185
515
  };
186
516
 
187
517
  // --- 辅助函数:清理旧消息 ---
188
518
  const cleanupOldMessages = async (sessionId) => {
519
+ const release = await lockSession(sessionId);
520
+
189
521
  try {
190
- // 获取当前消息数量
191
522
  const allMessages = await ctx.database.get('group_memory_messages',
192
523
  { session_id: sessionId },
193
524
  { fields: ['id', 'timestamp'] }
194
525
  );
195
526
 
196
527
  const maxMessages = config.maxContext * 2;
528
+ let toDelete = [];
197
529
 
198
- // 如果超过最大数量,删除最旧的消息
530
+ // 按数量限制
199
531
  if (allMessages.length > maxMessages) {
200
- // 按 ID 排序(ID 是自增的,代表时间顺序)
201
532
  allMessages.sort((a, b) => a.id - b.id);
202
- const toDelete = allMessages.slice(0, allMessages.length - maxMessages);
203
- for (const msg of toDelete) {
204
- await ctx.database.remove('group_memory_messages', { id: msg.id });
205
- }
533
+ const excess = allMessages.slice(0, allMessages.length - maxMessages);
534
+ toDelete.push(...excess);
535
+ log.db('数量限制', { total: allMessages.length, max: maxMessages, excess: excess.length });
206
536
  }
207
537
 
208
- // 如果时间窗口设置,删除过期的消息
538
+ // 按时间窗口限制
209
539
  if (config.timeWindow > 0) {
210
540
  const cutoff = Date.now() - config.timeWindow;
211
- const expiredMessages = allMessages.filter(msg =>
212
- new Date(msg.timestamp).getTime() < cutoff
213
- );
214
- for (const msg of expiredMessages) {
215
- await ctx.database.remove('group_memory_messages', { id: msg.id });
216
- }
541
+ const expired = allMessages.filter(msg => msg.timestamp < cutoff);
542
+ toDelete.push(...expired);
543
+ log.db('时间窗口限制', { cutoff, expired: expired.length });
544
+ }
545
+
546
+ // 批量删除
547
+ if (toDelete.length > 0) {
548
+ const uniqueIds = [...new Set(toDelete.map(msg => msg.id))];
549
+ await ctx.database.remove('group_memory_messages', { id: uniqueIds });
550
+
551
+ memoryCache.delete(sessionId);
552
+ cacheTimestamps.delete(sessionId);
553
+
554
+ log.cleanup(uniqueIds.length, { sessionId, ids: uniqueIds });
217
555
  }
218
556
  } catch (error) {
219
- logger.error('清理旧消息失败:', error.message);
557
+ log.error('清理旧消息失败', error);
558
+ } finally {
559
+ release();
220
560
  }
221
561
  };
222
562
 
@@ -225,8 +565,11 @@ exports.apply = (ctx, config) => {
225
565
  getContext,
226
566
  saveMessage,
227
567
  clearContext,
568
+ deleteLastMessage,
228
569
  };
229
570
 
571
+ log.success('API 接口已暴露', { methods: ['getContext', 'saveMessage', 'clearContext', 'deleteLastMessage'] });
572
+
230
573
  // --- 中间件1: 记录所有消息 ---
231
574
  ctx.middleware(async (session, next) => {
232
575
  if (session.selfId === session.userId) return next();
@@ -234,43 +577,46 @@ exports.apply = (ctx, config) => {
234
577
  const trimmed = session.content?.trim() || '';
235
578
  if (!trimmed) return next();
236
579
 
237
- // 忽略命令消息
238
580
  if (/^[./\\]/.test(trimmed)) return next();
239
581
 
240
- const { sessionId, sessionType } = generateSessionId(session);
582
+ const sessionInfo = generateSessionId(session);
583
+ const sessionId = sessionInfo.separateId || sessionInfo.sessionId;
584
+ const sessionType = sessionInfo.sessionType;
241
585
 
242
- // 根据配置决定是否记录
243
586
  if (sessionType === 'private' && !config.recordPrivateChat) return next();
244
587
  if (sessionType === 'group' && !config.recordGroupChat) return next();
245
588
 
246
589
  const cleanText = cleanContent(session.content, session);
247
590
  const userName = session.author?.nickname || session.userId;
248
591
 
249
- // 保存到数据库
250
- await saveMessage(sessionId, 'user', cleanText, session.userId, userName);
592
+ log.db('记录消息', {
593
+ sessionId,
594
+ sessionType,
595
+ userId: session.userId,
596
+ userName,
597
+ content: cleanText
598
+ }, `📝 ${userName}: ${cleanText.substring(0, 30)}...`);
251
599
 
252
- if (config.verbose) {
253
- logger.info(`记录消息 [${sessionType}] ${userName}: ${cleanText.substring(0, 50)}...`);
254
- }
600
+ saveMessage(sessionId, 'user', cleanText, session.userId, userName).catch(err => {
601
+ log.error('异步保存消息失败', err);
602
+ });
255
603
 
256
604
  return next();
257
- }, true);
605
+ }, -100);
258
606
 
259
- // --- 中间件2: 上下文注入(可选功能) ---
607
+ // --- 中间件2: 上下文注入 ---
260
608
  ctx.middleware(async (session, next) => {
261
609
  if (!session.groupId && !config.recordPrivateChat) return next();
262
610
  if (session.groupId && !config.recordGroupChat) return next();
263
611
 
264
612
  let shouldInject = false;
265
613
 
266
- // 检查是否 @ 机器人
267
614
  if (config.triggerOnAt) {
268
615
  const isAtBot = session.parsed?.bots?.includes(session.selfId) ||
269
- (session.content && new RegExp(`<@${session.selfId}>|@${session.selfId}`).test(session.content));
616
+ (session.content && new RegExp(`<@${session.selfId}>|@${session.selfId}\\b`).test(session.content));
270
617
  if (isAtBot) shouldInject = true;
271
618
  }
272
619
 
273
- // 检查是否回复机器人
274
620
  if (!shouldInject && config.triggerOnReply && session.quote) {
275
621
  if (session.quote.userId === session.selfId) {
276
622
  shouldInject = true;
@@ -279,42 +625,57 @@ exports.apply = (ctx, config) => {
279
625
 
280
626
  if (!shouldInject) return next();
281
627
 
282
- const { sessionId } = generateSessionId(session);
628
+ const sessionInfo = generateSessionId(session);
629
+ const sessionId = sessionInfo.separateId || sessionInfo.sessionId;
630
+
283
631
  const history = await getContext(sessionId);
284
632
 
285
633
  if (history.length === 0) return next();
286
634
 
287
- // 构建上下文前缀
635
+ // 构建上下文
288
636
  const contextLines = history.map(msg => {
289
637
  const timeStr = formatTime(msg.timestamp);
290
638
  const userName = msg.user_name || 'Unknown';
291
639
  return `${timeStr} ${userName}: ${msg.content}`;
292
640
  });
293
- const contextPrefix = `【对话前情提要】\n${contextLines.join('\n')}\n---\n请结合以上上下文回答以下问题:`;
641
+
642
+ const contextPrefix = `【对话历史】\n${contextLines.join('\n')}\n---`;
294
643
 
295
644
  let originalContent = session.content || '';
296
645
 
297
646
  if (config.cleanMentions) {
298
647
  originalContent = originalContent.replace(new RegExp(`<@${session.selfId}>`, 'g'), '')
299
- .replace(new RegExp(`@${session.selfId}`, 'g'), '')
648
+ .replace(new RegExp(`@${session.selfId}\\b`, 'g'), '')
300
649
  .trim();
301
650
  }
302
651
 
303
- // 修改 session.content,添加上下文前缀
304
- session.content = `${contextPrefix}\n${originalContent}`;
652
+ // 记录注入信息
653
+ log.inject('注入上下文', {
654
+ sessionId,
655
+ historyCount: history.length,
656
+ originalLength: session.content?.length || 0,
657
+ newLength: contextPrefix.length + originalContent.length,
658
+ timeRange: history.length > 0 ? {
659
+ from: formatTime(history[0].timestamp),
660
+ to: formatTime(history[history.length-1].timestamp)
661
+ } : null
662
+ }, `注入 ${history.length} 条历史到会话 ${sessionId}`);
305
663
 
306
- if (config.verbose) {
307
- logger.info(`[GroupMemory] ${sessionId} 触发上下文注入。历史条数: ${history.length}`);
308
- }
664
+ session.content = `${contextPrefix}\n${originalContent}`;
309
665
 
310
666
  return next();
311
- }, 100);
667
+ }, 0);
312
668
 
313
- // --- 命令:查看当前会话的上下文 ---
669
+ // --- 命令:查看上下文 ---
314
670
  ctx.command('memory-view [count]', '查看当前会话的历史记录')
315
671
  .alias('mem-view')
316
672
  .action(async ({ session }, count = 5) => {
317
- const { sessionId, sessionType } = generateSessionId(session);
673
+ const sessionInfo = generateSessionId(session);
674
+ const sessionId = sessionInfo.separateId || sessionInfo.sessionId;
675
+ const sessionType = sessionInfo.sessionType;
676
+
677
+ log.command('memory-view', session.userId, { sessionId, count });
678
+
318
679
  const history = await getContext(sessionId, parseInt(count));
319
680
 
320
681
  if (history.length === 0) {
@@ -332,16 +693,23 @@ exports.apply = (ctx, config) => {
332
693
  return `📜 ${sessionType === 'private' ? '私聊' : '群聊'}历史记录 (最近${history.length}条):\n${lines.join('\n')}`;
333
694
  });
334
695
 
335
- // --- 命令:清空当前会话的上下文 ---
696
+ // --- 命令:清空上下文 ---
336
697
  ctx.command('memory-clear', '清空当前会话的历史记录')
337
698
  .alias('mem-clear')
338
699
  .action(async ({ session }) => {
339
- const { sessionId, sessionType } = generateSessionId(session);
700
+ const sessionInfo = generateSessionId(session);
701
+ const sessionId = sessionInfo.separateId || sessionInfo.sessionId;
702
+ const sessionType = sessionInfo.sessionType;
703
+
704
+ log.command('memory-clear', session.userId, { sessionId });
705
+
340
706
  const result = await clearContext(sessionId);
341
707
 
342
708
  if (result) {
709
+ log.success(`清空会话 ${sessionId} 成功`);
343
710
  return `✅ 已清空当前${sessionType === 'private' ? '私聊' : '群聊'}的对话记忆!`;
344
711
  } else {
712
+ log.error(`清空会话 ${sessionId} 失败`);
345
713
  return '❌ 清空记忆失败,请查看日志。';
346
714
  }
347
715
  });
@@ -355,13 +723,25 @@ exports.apply = (ctx, config) => {
355
723
  const privateCount = await ctx.database.get('group_memory_messages', { session_type: 'private' }, { fields: ['id'] });
356
724
  const groupCount = await ctx.database.get('group_memory_messages', { session_type: 'group' }, { fields: ['id'] });
357
725
 
358
- return `🧠 Group Memory 插件状态\n` +
726
+ const status = `🧠 Group Memory 插件状态\n` +
359
727
  `总消息数: ${allMessages.length}\n` +
360
728
  `私聊消息: ${privateCount.length}\n` +
361
729
  `群聊消息: ${groupCount.length}\n` +
362
730
  `最大上下文: ${config.maxContext} 轮\n` +
363
- `时间窗口: ${config.timeWindow > 0 ? config.timeWindow / 60000 + ' 分钟' : '无限制'}`;
731
+ `时间窗口: ${config.timeWindow > 0 ? config.timeWindow / 60000 + ' 分钟' : '无限制'}\n` +
732
+ `独立上下文支持: ${config.supportSeparateContext ? '✅' : '❌'}\n` +
733
+ `日志级别: ${config.logLevel === 'none' ? '❌' : config.logLevel === 'simple' ? '📝 简易' : '📊 详细'}`;
734
+
735
+ log.command('memory-status', 'system', {
736
+ total: allMessages.length,
737
+ private: privateCount.length,
738
+ group: groupCount.length,
739
+ logLevel: config.logLevel
740
+ });
741
+
742
+ return status;
364
743
  } catch (error) {
744
+ log.error('获取状态失败', error);
365
745
  return `❌ 获取状态失败: ${error.message}`;
366
746
  }
367
747
  });
@@ -374,23 +754,80 @@ exports.apply = (ctx, config) => {
374
754
  return '⚠️ 时间窗口设置为 0,不会清理任何消息。';
375
755
  }
376
756
 
757
+ log.command('memory-cleanup', 'system', {});
758
+
377
759
  try {
378
- const allMessages = await ctx.database.get('group_memory_messages', {}, { fields: ['id', 'timestamp'] });
760
+ const allMessages = await ctx.database.get('group_memory_messages', {}, { fields: ['id', 'timestamp', 'session_id'] });
379
761
  const cutoff = Date.now() - config.timeWindow;
380
- let deletedCount = 0;
381
-
762
+ const expiredByTime = allMessages.filter(msg => msg.timestamp < cutoff);
763
+
764
+ // 按数量限制清理
765
+ const sessions = {};
382
766
  for (const msg of allMessages) {
383
- if (new Date(msg.timestamp).getTime() < cutoff) {
384
- await ctx.database.remove('group_memory_messages', { id: msg.id });
385
- deletedCount++;
767
+ if (!sessions[msg.session_id]) {
768
+ sessions[msg.session_id] = [];
386
769
  }
770
+ sessions[msg.session_id].push(msg);
771
+ }
772
+
773
+ const expiredByCount = [];
774
+ for (const [sessionId, msgs] of Object.entries(sessions)) {
775
+ msgs.sort((a, b) => a.id - b.id);
776
+ if (msgs.length > config.maxContext * 2) {
777
+ expiredByCount.push(...msgs.slice(0, msgs.length - config.maxContext * 2));
778
+ }
779
+ }
780
+
781
+ // 合并去重
782
+ const toDelete = [...new Set([...expiredByTime, ...expiredByCount].map(msg => msg.id))];
783
+
784
+ if (toDelete.length > 0) {
785
+ await ctx.database.remove('group_memory_messages', { id: toDelete });
786
+
787
+ // 清空相关缓存
788
+ const affectedSessions = [...new Set(allMessages
789
+ .filter(msg => toDelete.includes(msg.id))
790
+ .map(msg => msg.session_id))];
791
+ affectedSessions.forEach(sid => {
792
+ memoryCache.delete(sid);
793
+ cacheTimestamps.delete(sid);
794
+ });
795
+
796
+ log.cleanup(toDelete.length, { affectedSessions: affectedSessions.length });
387
797
  }
388
798
 
389
- return `🧹 清理完成,删除了 ${deletedCount} 条过期消息。`;
799
+ return `🧹 清理完成,删除了 ${toDelete.length} 条过期消息。`;
390
800
  } catch (error) {
801
+ log.error('清理失败', error);
391
802
  return `❌ 清理失败: ${error.message}`;
392
803
  }
393
804
  });
394
805
 
395
- logger.info('Group Memory 插件已加载');
806
+ // 清理定时器
807
+ const cleanupTimer = setInterval(() => {
808
+ if (config.timeWindow > 0) {
809
+ ctx.command('memory-cleanup').execute({}).catch(err => {
810
+ log.error('自动清理失败', err);
811
+ });
812
+ }
813
+ }, 3600000);
814
+
815
+ // 插件卸载时清理
816
+ ctx.on('dispose', () => {
817
+ clearInterval(cleanupTimer);
818
+ memoryCache.clear();
819
+ cacheTimestamps.clear();
820
+ sessionLocks.clear();
821
+ log.success('插件卸载完成', {});
822
+ });
823
+
824
+ // 启动日志
825
+ log.success('Group Memory 插件已加载', {
826
+ logLevel: config.logLevel,
827
+ databaseOps: config.logDatabaseOps,
828
+ contextInject: config.logContextInject,
829
+ apiCalls: config.logApiCalls
830
+ });
831
+
832
+ logger.info(`Group Memory 插件已加载,日志级别: ${config.logLevel}`);
396
833
  };