koishi-plugin-group-memory 1.2.1 → 2.1.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 +32 -12
  2. package/src/index.js +733 -114
package/package.json CHANGED
@@ -1,9 +1,16 @@
1
1
  {
2
2
  "name": "koishi-plugin-group-memory",
3
- "version": "1.2.1",
4
- "description": "智能群聊记忆中间件:自动记录上下文,在@或回复时注入前情提要,支持时间窗口过滤与内容清洗",
5
- "main": "./src/index.js",
3
+ "version": "2.1.0",
4
+ "description": "群聊和私聊上下文记忆插件,为 AI 插件提供历史记录查询接口。支持数据库持久化、自动清理、上下文注入,可与其他 AI 插件无缝协作",
5
+ "main": "lib/index.js",
6
+ "typings": "lib/index.d.ts",
7
+ "files": [
8
+ "lib",
9
+ "src"
10
+ ],
6
11
  "scripts": {
12
+ "build": "atsc -b",
13
+ "build:watch": "atsc -b -w",
7
14
  "test": "echo \"Error: no test specified\" && exit 1"
8
15
  },
9
16
  "keywords": [
@@ -11,32 +18,45 @@
11
18
  "plugin",
12
19
  "memory",
13
20
  "context",
14
- "group-chat",
21
+ "chat-history",
22
+ "group-memory",
23
+ "database",
24
+ "cache",
15
25
  "ai",
16
- "middleware",
17
- "deepseek",
18
- "openai"
26
+ "llm",
27
+ "conversation",
28
+ "history"
19
29
  ],
20
30
  "author": "YourName",
21
31
  "license": "MIT",
22
32
  "repository": {
23
33
  "type": "git",
24
- "url": ""
34
+ "url": "https://github.com/yourusername/koishi-plugin-group-memory"
35
+ },
36
+ "bugs": {
37
+ "url": "https://github.com/yourusername/koishi-plugin-group-memory/issues"
25
38
  },
39
+ "homepage": "https://github.com/yourusername/koishi-plugin-group-memory#readme",
26
40
  "koishi": {
27
41
  "description": {
28
- "en": "Smart group chat memory middleware. Automatically records context and injects summaries when @mentioned or replied. Supports time-window filtering, content cleaning (mentions/images), and custom templates. Perfect for making AI bots aware of ongoing conversations.",
29
- "zh": "智能群聊记忆中间件。自动记录群内闲聊上下文,当用户 @机器人 或回复机器人时,自动将“前情提要”注入给 AI 插件。支持按时间/条数过滤记忆、自动清洗 @符号和图片、自定义提示词模板。是让 AI 机器人拥有“群聊感知力”的必备插件。"
42
+ "en": "Group and private chat memory plugin. Provides history query interface for AI plugins. Supports database persistence, auto-cleanup, context injection, and seamless collaboration with other AI plugins.",
43
+ "zh": "群聊和私聊上下文记忆插件,为 AI 插件提供历史记录查询接口。支持数据库持久化、自动清理、上下文注入,可与其他 AI 插件无缝协作。"
30
44
  },
31
45
  "service": {
32
- "required": [],
46
+ "required": ["database"],
33
47
  "optional": []
34
48
  }
35
49
  },
36
50
  "peerDependencies": {
37
51
  "koishi": "^4.16.0"
38
52
  },
53
+ "devDependencies": {
54
+ "@types/node": "^18.0.0",
55
+ "atsc": "^1.2.0",
56
+ "typescript": "^5.0.0"
57
+ },
39
58
  "engines": {
40
59
  "node": ">=18.0.0"
41
- }
60
+ },
61
+ "dependencies": {}
42
62
  }
package/src/index.js CHANGED
@@ -4,17 +4,16 @@ 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(),
10
- session_type: Schema.string().required(),
11
- user_id: Schema.string().required(),
12
- user_name: Schema.string().default(''),
13
- role: Schema.string().required(),
14
- content: Schema.string().required(),
15
- timestamp: Schema.number().required(), // 改为 number
16
- });
17
-
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
+ };
18
17
 
19
18
  // --- 配置架构 ---
20
19
  exports.Config = Schema.object({
@@ -26,10 +25,10 @@ exports.Config = Schema.object({
26
25
  .description('每个会话保留最近多少条消息作为上下文。'),
27
26
 
28
27
  timeWindow: Schema.number()
29
- .default(3600000) // 1小时
28
+ .default(3600000)
30
29
  .min(0)
31
- .step(60000) // 1分钟步进
32
- .description('只保留最近 N 毫秒内的消息。设置为 0 表示不限制时间,只按条数限制。'),
30
+ .step(60000)
31
+ .description('只保留最近 N 毫秒内的消息。设置为 0 表示不限制时间。'),
33
32
 
34
33
  // --- 记录设置 ---
35
34
  recordPrivateChat: Schema.boolean()
@@ -52,170 +51,623 @@ exports.Config = Schema.object({
52
51
  // --- 内容处理 ---
53
52
  cleanMentions: Schema.boolean()
54
53
  .default(true)
55
- .description('发送给 AI 前,是否移除消息中的 @ 符号 (避免 AI 困惑)。'),
54
+ .description('发送给 AI 前,是否移除消息中的 @ 符号。'),
56
55
 
57
56
  cleanImages: Schema.boolean()
58
57
  .default(true)
59
- .description('是否移除图片/媒体占位符 (如 [图片])。'),
58
+ .description('是否移除图片/媒体占位符。'),
60
59
 
61
60
  // --- 调试 ---
62
61
  verbose: Schema.boolean()
63
62
  .default(false)
64
63
  .description('是否在控制台打印注入的上下文 (调试用)。'),
64
+
65
+ // ============================================
66
+ // 新增:日志显示功能(修复版)
67
+ // ============================================
68
+ logLevel: Schema.union([
69
+ Schema.const('none').description('不输出日志'),
70
+ Schema.const('simple').description('简易模式:输出易于理解的操作摘要'),
71
+ Schema.const('detailed').description('详细模式:输出完整的 JSON 格式日志'),
72
+ ]).default('detailed').description('日志输出级别(默认详细,便于调试)'), // 改为默认 detailed
73
+
74
+ logDatabaseOps: Schema.boolean()
75
+ .default(true)
76
+ .description('是否记录数据库操作(增删改查)'),
77
+
78
+ logContextInject: Schema.boolean()
79
+ .default(true)
80
+ .description('是否记录上下文注入操作'),
81
+
82
+ logApiCalls: Schema.boolean()
83
+ .default(true)
84
+ .description('是否记录对外部插件的 API 调用'),
85
+
86
+ logMessageReceive: Schema.boolean()
87
+ .default(true)
88
+ .description('是否记录接收到的原始消息'),
89
+
90
+ logContentPreview: Schema.boolean()
91
+ .default(true)
92
+ .description('是否在简易模式下预览消息内容(只显示前30个字符)'),
93
+
94
+ logColorOutput: Schema.boolean()
95
+ .default(true)
96
+ .description('是否使用颜色区分不同类型的日志(需终端支持)'),
97
+
98
+ logTimestamp: Schema.boolean()
99
+ .default(true)
100
+ .description('是否在日志中显示时间戳'),
101
+
102
+ logMiddlewarePriority: Schema.boolean()
103
+ .default(true)
104
+ .description('是否记录中间件执行优先级信息'),
65
105
  });
66
106
 
67
107
  exports.schema = exports.Config;
68
108
 
69
- // --- 声明依赖注入 ---
70
109
  exports.inject = ['database'];
71
110
 
72
111
  exports.apply = (ctx, config) => {
73
112
  const logger = ctx.logger('group-memory');
74
113
 
75
- // 内存缓存(用于快速访问,数据库持久化)
114
+ // ============================================
115
+ // 日志增强模块(修复版)
116
+ // ============================================
117
+
118
+ // 立即输出启动日志,确认插件已加载
119
+ logger.info('='.repeat(50));
120
+ logger.info('🧠 Group Memory 插件正在初始化...');
121
+ logger.info(`日志级别: ${config.logLevel}`);
122
+ logger.info(`记录私聊: ${config.recordPrivateChat}`);
123
+ logger.info(`记录群聊: ${config.recordGroupChat}`);
124
+ logger.info('='.repeat(50));
125
+
126
+ // 颜色代码
127
+ const colors = {
128
+ reset: '\x1b[0m',
129
+ fg: {
130
+ black: '\x1b[30m',
131
+ red: '\x1b[31m',
132
+ green: '\x1b[32m',
133
+ yellow: '\x1b[33m',
134
+ blue: '\x1b[34m',
135
+ magenta: '\x1b[35m',
136
+ cyan: '\x1b[36m',
137
+ white: '\x1b[37m',
138
+ }
139
+ };
140
+
141
+ const useColor = config.logColorOutput && process.stdout.isTTY;
142
+
143
+ const color = (text, colorCode) => {
144
+ if (!useColor) return text;
145
+ return `${colorCode}${text}${colors.reset}`;
146
+ };
147
+
148
+ const getTimestamp = () => {
149
+ if (!config.logTimestamp) return '';
150
+ const now = new Date();
151
+ return `[${now.toLocaleTimeString()}.${now.getMilliseconds().toString().padStart(3, '0')}] `;
152
+ };
153
+
154
+ // 日志图标
155
+ const icons = {
156
+ receive: '📥',
157
+ db: '🗄️',
158
+ cache: '⚡',
159
+ inject: '📎',
160
+ api: '🔌',
161
+ command: '⌨️',
162
+ error: '❌',
163
+ warning: '⚠️',
164
+ success: '✅',
165
+ info: 'ℹ️',
166
+ debug: '🔍',
167
+ memory: '🧠',
168
+ cleanup: '🧹',
169
+ middleware: '🔄'
170
+ };
171
+
172
+ // 增强的日志函数 - 确保立即输出
173
+ const log = {
174
+ // 接收消息日志
175
+ receive: (session, details, simpleMsg) => {
176
+ if (config.logLevel === 'none') return;
177
+ if (!config.logMessageReceive) return;
178
+
179
+ const timestamp = getTimestamp();
180
+ const icon = icons.receive;
181
+
182
+ if (config.logLevel === 'detailed') {
183
+ logger.info(`${timestamp}${icon} ${JSON.stringify({
184
+ type: 'receive',
185
+ ...details
186
+ }, null, 2)}`);
187
+ } else {
188
+ const msg = simpleMsg || `收到消息: ${details.userName} ${details.contentPreview}`;
189
+ logger.info(`${timestamp}${icon} ${color(msg, colors.fg.cyan)}`);
190
+ }
191
+ },
192
+
193
+ // 数据库操作日志
194
+ db: (operation, details, simpleMsg) => {
195
+ if (config.logLevel === 'none') return;
196
+ if (!config.logDatabaseOps) return;
197
+
198
+ const timestamp = getTimestamp();
199
+ const icon = icons.db;
200
+
201
+ if (config.logLevel === 'detailed') {
202
+ logger.info(`${timestamp}${icon} ${JSON.stringify({
203
+ type: 'database',
204
+ operation,
205
+ ...details
206
+ }, null, 2)}`);
207
+ } else {
208
+ logger.info(`${timestamp}${icon} ${simpleMsg || `数据库 ${operation}`}`);
209
+ }
210
+ },
211
+
212
+ // 上下文注入日志
213
+ inject: (operation, details, simpleMsg) => {
214
+ if (config.logLevel === 'none') return;
215
+ if (!config.logContextInject) return;
216
+
217
+ const timestamp = getTimestamp();
218
+ const icon = icons.inject;
219
+
220
+ if (config.logLevel === 'detailed') {
221
+ logger.info(`${timestamp}${icon} ${JSON.stringify({
222
+ type: 'inject',
223
+ operation,
224
+ ...details
225
+ }, null, 2)}`);
226
+ } else {
227
+ logger.info(`${timestamp}${icon} ${simpleMsg || `上下文注入: ${operation}`}`);
228
+ }
229
+ },
230
+
231
+ // API调用日志
232
+ api: (operation, details, simpleMsg) => {
233
+ if (config.logLevel === 'none') return;
234
+ if (!config.logApiCalls) return;
235
+
236
+ const timestamp = getTimestamp();
237
+ const icon = icons.api;
238
+
239
+ if (config.logLevel === 'detailed') {
240
+ logger.info(`${timestamp}${icon} ${JSON.stringify({
241
+ type: 'api',
242
+ operation,
243
+ ...details
244
+ }, null, 2)}`);
245
+ } else {
246
+ logger.info(`${timestamp}${icon} ${simpleMsg || `API: ${operation}`}`);
247
+ }
248
+ },
249
+
250
+ // 中间件日志
251
+ middleware: (msg, details) => {
252
+ if (config.logLevel === 'none') return;
253
+ if (!config.logMiddlewarePriority) return;
254
+
255
+ const timestamp = getTimestamp();
256
+ const icon = icons.middleware;
257
+
258
+ if (config.logLevel === 'detailed') {
259
+ logger.info(`${timestamp}${icon} ${JSON.stringify({
260
+ type: 'middleware',
261
+ ...details
262
+ }, null, 2)}`);
263
+ } else {
264
+ logger.info(`${timestamp}${icon} ${msg}`);
265
+ }
266
+ },
267
+
268
+ // 命令日志
269
+ command: (cmd, user, details) => {
270
+ if (config.logLevel === 'none') return;
271
+
272
+ const timestamp = getTimestamp();
273
+ const icon = icons.command;
274
+ const simpleMsg = `命令 ${cmd} 由 ${user} 执行`;
275
+
276
+ if (config.logLevel === 'detailed') {
277
+ logger.info(`${timestamp}${icon} ${JSON.stringify({
278
+ type: 'command',
279
+ command: cmd,
280
+ user,
281
+ ...details
282
+ }, null, 2)}`);
283
+ } else {
284
+ logger.info(`${timestamp}${icon} ${simpleMsg}`);
285
+ }
286
+ },
287
+
288
+ // 错误日志
289
+ error: (msg, error) => {
290
+ if (config.logLevel === 'none') return;
291
+
292
+ const timestamp = getTimestamp();
293
+ const icon = icons.error;
294
+ const errorMsg = error ? `${msg}: ${error.message}` : msg;
295
+
296
+ logger.error(`${timestamp}${icon} ${color(errorMsg, colors.fg.red)}`);
297
+
298
+ if (config.logLevel === 'detailed' && error?.stack) {
299
+ logger.error(error.stack);
300
+ }
301
+ },
302
+
303
+ // 成功日志
304
+ success: (msg, details) => {
305
+ if (config.logLevel === 'none') return;
306
+
307
+ const timestamp = getTimestamp();
308
+ const icon = icons.success;
309
+
310
+ if (config.logLevel === 'detailed') {
311
+ logger.info(`${timestamp}${icon} ${JSON.stringify({
312
+ type: 'success',
313
+ message: msg,
314
+ ...details
315
+ }, null, 2)}`);
316
+ } else {
317
+ logger.info(`${timestamp}${icon} ${color(msg, colors.fg.green)}`);
318
+ }
319
+ },
320
+
321
+ // 缓存日志
322
+ cache: (operation, details) => {
323
+ if (config.logLevel === 'none') return;
324
+
325
+ const timestamp = getTimestamp();
326
+ const icon = icons.cache;
327
+
328
+ if (config.logLevel === 'detailed') {
329
+ logger.info(`${timestamp}${icon} ${JSON.stringify({
330
+ type: 'cache',
331
+ operation,
332
+ ...details
333
+ }, null, 2)}`);
334
+ } else {
335
+ logger.info(`${timestamp}${icon} 缓存 ${operation}`);
336
+ }
337
+ },
338
+
339
+ // 清理日志
340
+ cleanup: (count, details) => {
341
+ if (config.logLevel === 'none') return;
342
+
343
+ const timestamp = getTimestamp();
344
+ const icon = icons.cleanup;
345
+ const simpleMsg = `清理了 ${count} 条过期消息`;
346
+
347
+ if (config.logLevel === 'detailed') {
348
+ logger.info(`${timestamp}${icon} ${JSON.stringify({
349
+ type: 'cleanup',
350
+ count,
351
+ ...details
352
+ }, null, 2)}`);
353
+ } else {
354
+ logger.info(`${timestamp}${icon} ${simpleMsg}`);
355
+ }
356
+ },
357
+
358
+ // 信息日志
359
+ info: (msg, details) => {
360
+ if (config.logLevel === 'none') return;
361
+
362
+ const timestamp = getTimestamp();
363
+ const icon = icons.info;
364
+
365
+ if (config.logLevel === 'detailed' && details) {
366
+ logger.info(`${timestamp}${icon} ${JSON.stringify({
367
+ message: msg,
368
+ ...details
369
+ }, null, 2)}`);
370
+ } else {
371
+ logger.info(`${timestamp}${icon} ${msg}`);
372
+ }
373
+ }
374
+ };
375
+
376
+ // 内存缓存
76
377
  const memoryCache = new Map();
378
+ const CACHE_TTL = 5 * 60 * 1000;
379
+ const cacheTimestamps = new Map();
380
+
381
+ // 会话锁
382
+ const sessionLocks = new Map();
383
+
384
+ const lockSession = async (sessionId, timeout = 30000) => {
385
+ const startTime = Date.now();
386
+ while (sessionLocks.has(sessionId)) {
387
+ if (Date.now() - startTime > timeout) {
388
+ log.error(`获取会话锁超时: ${sessionId}`);
389
+ throw new Error(`获取会话锁超时: ${sessionId}`);
390
+ }
391
+ await new Promise(resolve => setTimeout(resolve, 100));
392
+ }
393
+ sessionLocks.set(sessionId, true);
394
+ log.cache('锁定', { sessionId });
395
+ return () => {
396
+ sessionLocks.delete(sessionId);
397
+ log.cache('解锁', { sessionId });
398
+ };
399
+ };
77
400
 
78
401
  // 确保数据库表存在
79
- ctx.database.extend('group_memory_messages', MessageSchema, {
402
+ ctx.database.extend('group_memory_messages', MessageFields, {
80
403
  autoInc: true,
404
+ primary: 'id',
405
+ index: ['session_id', 'session_type', 'timestamp']
81
406
  });
407
+
408
+ log.success('数据库表初始化完成');
82
409
 
83
410
  // --- 辅助函数:生成会话ID ---
84
- // 私聊: private:{userId}
85
- // 群聊: group:{groupId}(所有人共享)
86
411
  const generateSessionId = (session) => {
87
- if (!session.groupId) {
88
- return { sessionId: `private:${session.userId}`, sessionType: 'private' };
89
- } else {
90
- return { sessionId: `group:${session.groupId}`, sessionType: 'group' };
91
- }
92
- };
93
-
94
- // --- 辅助函数:格式化时间 ---
95
- const formatTime = (timestamp) => {
96
- const date = new Date(timestamp);
97
- return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
412
+ const result = !session.groupId
413
+ ? {
414
+ sessionId: `private:${session.userId}`,
415
+ sessionType: 'private'
416
+ }
417
+ : {
418
+ sessionId: `group:${session.groupId}`,
419
+ separateId: config.supportSeparateContext ? `group:${session.groupId}:user:${session.userId}` : undefined,
420
+ sessionType: 'group'
421
+ };
422
+
423
+ log.db('生成会话ID', {
424
+ input: { userId: session.userId, groupId: session.groupId },
425
+ output: result
426
+ }, `会话ID: ${result.sessionId}`);
427
+
428
+ return result;
98
429
  };
99
430
 
100
431
  // --- 辅助函数:清理内容 ---
101
432
  const cleanContent = (content, session) => {
433
+ if (!content) return '';
434
+
102
435
  let text = content;
436
+ const originalPreview = text.substring(0, 30);
103
437
 
104
- if (config.cleanMentions) {
105
- text = text.replace(new RegExp(`@${session.selfId}`, 'g'), '');
106
- text = text.replace(/@\S+/g, (match) => match.replace('@', ''));
438
+ if (config.cleanMentions && session) {
439
+ text = text.replace(new RegExp(`<@${session.selfId}>`, 'g'), '')
440
+ .replace(new RegExp(`@${session.selfId}\\b`, 'g'), '')
441
+ .replace(/@(\S+)/g, '$1');
107
442
  }
108
443
 
109
444
  if (config.cleanImages) {
110
- text = text.replace(/\[图片\]/g, '[图片已省略]');
111
- text = text.replace(/\[表情.*?\]/g, '');
112
- text = text.replace(/\[CQ:.*?\]/g, '');
445
+ text = text.replace(/\[图片\]/g, '[图片]')
446
+ .replace(/\[表情.*?\]/g, '')
447
+ .replace(/\[CQ:.*?\]/g, '')
448
+ .replace(/!?\[.*?\]\(.*?\)/g, '[链接]');
113
449
  }
114
450
 
115
- return text.trim();
451
+ const result = text.trim() || '[空消息]';
452
+
453
+ log.db('清理内容', {
454
+ original: originalPreview,
455
+ result: result.substring(0, 30)
456
+ }, `内容清理: ${originalPreview} -> ${result.substring(0, 30)}`);
457
+
458
+ return result;
459
+ };
460
+
461
+ // --- 辅助函数:格式化时间 ---
462
+ const formatTime = (timestamp) => {
463
+ const date = new Date(timestamp);
464
+ return `${date.getHours().toString().padStart(2, '0')}:${date.getMinutes().toString().padStart(2, '0')}`;
116
465
  };
117
466
 
118
- // --- 核心 API:获取上下文(供其他插件调用) ---
467
+ // --- 清理缓存 ---
468
+ const cleanupCache = () => {
469
+ const now = Date.now();
470
+ let cleaned = 0;
471
+ for (const [sessionId, timestamp] of cacheTimestamps.entries()) {
472
+ if (now - timestamp > CACHE_TTL) {
473
+ memoryCache.delete(sessionId);
474
+ cacheTimestamps.delete(sessionId);
475
+ cleaned++;
476
+ }
477
+ }
478
+ if (cleaned > 0) {
479
+ log.cache('清理过期缓存', { cleaned });
480
+ }
481
+ };
482
+ setInterval(cleanupCache, CACHE_TTL);
483
+
484
+ // --- 核心 API:获取上下文 ---
119
485
  const getContext = async (sessionId, limit = null) => {
486
+ log.api('getContext', { sessionId, limit });
487
+
488
+ if (memoryCache.has(sessionId)) {
489
+ const cached = memoryCache.get(sessionId);
490
+ const age = Date.now() - cacheTimestamps.get(sessionId);
491
+ if (age < CACHE_TTL) {
492
+ log.cache('命中', { sessionId, count: cached.length });
493
+ return cached;
494
+ } else {
495
+ log.cache('过期', { sessionId, age });
496
+ }
497
+ }
498
+
120
499
  const maxMessages = limit ? limit * 2 : config.maxContext * 2;
500
+ log.db('查询', { sessionId, maxMessages });
121
501
 
122
502
  try {
123
- // 从数据库获取
124
503
  let history = await ctx.database.get('group_memory_messages',
125
504
  { session_id: sessionId },
126
505
  {
127
- fields: ['id', 'session_id', 'role', 'content', 'timestamp', 'user_name'],
506
+ fields: ['id', 'session_id', 'role', 'content', 'timestamp', 'user_name', 'user_id'],
128
507
  orderBy: { id: 'desc' },
129
508
  limit: maxMessages
130
509
  }
131
510
  );
132
511
 
133
- // 反转数组以恢复时间顺序
134
512
  history.reverse();
135
513
 
136
- // 如果时间窗口设置,进行过滤
137
514
  if (config.timeWindow > 0) {
515
+ const before = history.length;
138
516
  const cutoff = Date.now() - config.timeWindow;
139
517
  history = history.filter(msg => msg.timestamp >= cutoff);
518
+ log.db('时间窗口过滤', { before, after: history.length });
140
519
  }
141
520
 
521
+ memoryCache.set(sessionId, history);
522
+ cacheTimestamps.set(sessionId, Date.now());
523
+ log.cache('更新', { sessionId, count: history.length });
524
+
142
525
  return history;
143
526
  } catch (error) {
144
- logger.error('获取上下文失败:', error.message);
527
+ log.error('获取上下文失败', error);
145
528
  return [];
146
529
  }
147
530
  };
148
531
 
149
- // --- 核心 API:保存消息(供其他插件调用) ---
532
+ // --- 核心 API:保存消息 ---
150
533
  const saveMessage = async (sessionId, role, content, userId = 'system', userName = 'System') => {
534
+ log.api('saveMessage', { sessionId, role, userId });
535
+
536
+ const release = await lockSession(sessionId);
537
+
151
538
  try {
152
- // 解析 sessionId 获取 sessionType
153
539
  const sessionType = sessionId.startsWith('private:') ? 'private' : 'group';
154
540
 
155
- // 保存到数据库
156
- await ctx.database.create('group_memory_messages', {
541
+ const messageData = {
157
542
  session_id: sessionId,
158
543
  session_type: sessionType,
159
544
  user_id: userId,
160
545
  user_name: userName,
161
546
  role: role,
162
547
  content: content,
163
- timestamp: Date.now(), // ← 修改这里
548
+ timestamp: Date.now(),
549
+ };
550
+
551
+ log.db('创建', {
552
+ sessionId,
553
+ role,
554
+ userName,
555
+ contentPreview: content.substring(0, 30)
164
556
  });
165
557
 
166
- // 清理旧消息
167
- await cleanupOldMessages(sessionId);
558
+ await ctx.database.create('group_memory_messages', messageData);
559
+
560
+ memoryCache.delete(sessionId);
561
+ cacheTimestamps.delete(sessionId);
562
+ log.cache('失效', { sessionId });
563
+
564
+ // 异步清理旧消息
565
+ cleanupOldMessages(sessionId).catch(err => {
566
+ log.error('异步清理失败', err);
567
+ });
168
568
 
169
569
  return true;
170
570
  } catch (error) {
171
- logger.error('保存消息失败:', error.message);
571
+ log.error('保存消息失败', error);
172
572
  return false;
573
+ } finally {
574
+ release();
173
575
  }
174
576
  };
175
577
 
176
- // --- 核心 API:清空上下文(供其他插件调用) ---
578
+ // --- API:删除最后一条消息 ---
579
+ const deleteLastMessage = async (sessionId, role) => {
580
+ log.api('deleteLastMessage', { sessionId, role });
581
+
582
+ const release = await lockSession(sessionId);
583
+
584
+ try {
585
+ const lastMessage = await ctx.database.get('group_memory_messages',
586
+ { session_id: sessionId, role: role },
587
+ {
588
+ orderBy: { id: 'desc' },
589
+ limit: 1
590
+ }
591
+ );
592
+
593
+ if (lastMessage && lastMessage.length > 0) {
594
+ log.db('删除', { id: lastMessage[0].id });
595
+ await ctx.database.remove('group_memory_messages', { id: lastMessage[0].id });
596
+ memoryCache.delete(sessionId);
597
+ cacheTimestamps.delete(sessionId);
598
+ return true;
599
+ }
600
+
601
+ return false;
602
+ } catch (error) {
603
+ log.error('删除消息失败', error);
604
+ return false;
605
+ } finally {
606
+ release();
607
+ }
608
+ };
609
+
610
+ // --- API:清空上下文 ---
177
611
  const clearContext = async (sessionId) => {
612
+ log.api('clearContext', { sessionId });
613
+
614
+ const release = await lockSession(sessionId);
615
+
178
616
  try {
179
- await ctx.database.remove('group_memory_messages', { session_id: sessionId });
617
+ const count = await ctx.database.remove('group_memory_messages', { session_id: sessionId });
618
+ log.db('清空', { sessionId, count });
619
+
180
620
  memoryCache.delete(sessionId);
621
+ cacheTimestamps.delete(sessionId);
622
+ log.cache('失效', { sessionId });
623
+
181
624
  return true;
182
625
  } catch (error) {
183
- logger.error('清空上下文失败:', error.message);
626
+ log.error('清空上下文失败', error);
184
627
  return false;
628
+ } finally {
629
+ release();
185
630
  }
186
631
  };
187
632
 
188
633
  // --- 辅助函数:清理旧消息 ---
189
634
  const cleanupOldMessages = async (sessionId) => {
635
+ const release = await lockSession(sessionId);
636
+
190
637
  try {
191
- // 获取当前消息数量
192
638
  const allMessages = await ctx.database.get('group_memory_messages',
193
639
  { session_id: sessionId },
194
640
  { fields: ['id', 'timestamp'] }
195
641
  );
196
642
 
197
643
  const maxMessages = config.maxContext * 2;
644
+ let toDelete = [];
198
645
 
199
- // 如果超过最大数量,删除最旧的消息
200
646
  if (allMessages.length > maxMessages) {
201
- // 按 ID 排序(ID 是自增的,代表时间顺序)
202
647
  allMessages.sort((a, b) => a.id - b.id);
203
- const toDelete = allMessages.slice(0, allMessages.length - maxMessages);
204
- for (const msg of toDelete) {
205
- await ctx.database.remove('group_memory_messages', { id: msg.id });
206
- }
648
+ const excess = allMessages.slice(0, allMessages.length - maxMessages);
649
+ toDelete.push(...excess);
207
650
  }
208
651
 
209
- // 如果时间窗口设置,删除过期的消息
210
652
  if (config.timeWindow > 0) {
211
653
  const cutoff = Date.now() - config.timeWindow;
212
- const expiredMessages = allMessages.filter(msg => msg.timestamp < cutoff);
213
- for (const msg of expiredMessages) {
214
- await ctx.database.remove('group_memory_messages', { id: msg.id });
215
- }
654
+ const expired = allMessages.filter(msg => msg.timestamp < cutoff);
655
+ toDelete.push(...expired);
656
+ }
657
+
658
+ if (toDelete.length > 0) {
659
+ const uniqueIds = [...new Set(toDelete.map(msg => msg.id))];
660
+ await ctx.database.remove('group_memory_messages', { id: uniqueIds });
661
+
662
+ memoryCache.delete(sessionId);
663
+ cacheTimestamps.delete(sessionId);
664
+
665
+ log.cleanup(uniqueIds.length, { sessionId });
216
666
  }
217
667
  } catch (error) {
218
- logger.error('清理旧消息失败:', error.message);
668
+ log.error('清理旧消息失败', error);
669
+ } finally {
670
+ release();
219
671
  }
220
672
  };
221
673
 
@@ -224,96 +676,188 @@ exports.apply = (ctx, config) => {
224
676
  getContext,
225
677
  saveMessage,
226
678
  clearContext,
679
+ deleteLastMessage,
227
680
  };
228
681
 
229
- // --- 中间件1: 记录所有消息 ---
682
+ log.success('API 接口已暴露');
683
+
684
+ // ============================================
685
+ // 修复:中间件 - 记录所有消息(提高优先级)
686
+ // ============================================
230
687
  ctx.middleware(async (session, next) => {
231
- if (session.selfId === session.userId) return next();
688
+ // 记录中间件执行
689
+ log.middleware('执行消息记录中间件', {
690
+ userId: session.userId,
691
+ groupId: session.groupId,
692
+ content: session.content?.substring(0, 30)
693
+ });
694
+
695
+ if (session.selfId === session.userId) {
696
+ log.middleware('忽略机器人自身消息');
697
+ return next();
698
+ }
232
699
 
233
700
  const trimmed = session.content?.trim() || '';
234
- if (!trimmed) return next();
701
+ if (!trimmed) {
702
+ log.middleware('忽略空消息');
703
+ return next();
704
+ }
705
+
706
+ // 记录原始消息接收
707
+ const contentPreview = trimmed.substring(0, 50) + (trimmed.length > 50 ? '...' : '');
708
+ const userName = session.author?.nickname || session.userId;
709
+ const chatType = !session.groupId ? '私聊' : '群聊';
710
+
711
+ log.receive(session, {
712
+ userId: session.userId,
713
+ userName,
714
+ groupId: session.groupId,
715
+ content: trimmed,
716
+ contentPreview,
717
+ chatType
718
+ }, `${chatType} ${userName}: ${contentPreview}`);
235
719
 
236
720
  // 忽略命令消息
237
- if (/^[./\\]/.test(trimmed)) return next();
721
+ if (/^[./\\]/.test(trimmed)) {
722
+ log.middleware('忽略命令消息');
723
+ return next();
724
+ }
238
725
 
239
- const { sessionId, sessionType } = generateSessionId(session);
726
+ const sessionInfo = generateSessionId(session);
727
+ const sessionId = sessionInfo.separateId || sessionInfo.sessionId;
728
+ const sessionType = sessionInfo.sessionType;
240
729
 
241
730
  // 根据配置决定是否记录
242
- if (sessionType === 'private' && !config.recordPrivateChat) return next();
243
- if (sessionType === 'group' && !config.recordGroupChat) return next();
731
+ if (sessionType === 'private' && !config.recordPrivateChat) {
732
+ log.middleware('私聊记录已禁用');
733
+ return next();
734
+ }
735
+ if (sessionType === 'group' && !config.recordGroupChat) {
736
+ log.middleware('群聊记录已禁用');
737
+ return next();
738
+ }
244
739
 
245
740
  const cleanText = cleanContent(session.content, session);
246
- const userName = session.author?.nickname || session.userId;
247
-
248
- // 保存到数据库
249
- await saveMessage(sessionId, 'user', cleanText, session.userId, userName);
741
+
742
+ log.db('准备保存消息', {
743
+ sessionId,
744
+ sessionType,
745
+ userId: session.userId,
746
+ userName,
747
+ contentPreview: cleanText.substring(0, 30)
748
+ });
250
749
 
251
- if (config.verbose) {
252
- logger.info(`记录消息 [${sessionType}] ${userName}: ${cleanText.substring(0, 50)}...`);
750
+ // 立即保存并等待完成,确保日志输出
751
+ try {
752
+ await saveMessage(sessionId, 'user', cleanText, session.userId, userName);
753
+ log.success(`消息已保存: ${sessionId}`);
754
+ } catch (err) {
755
+ log.error('保存消息失败', err);
253
756
  }
254
757
 
255
758
  return next();
256
- }, true);
759
+ }, 100); // 提高优先级到 100,确保先执行
257
760
 
258
- // --- 中间件2: 上下文注入(可选功能) ---
761
+ // ============================================
762
+ // 中间件2: 上下文注入(保持优先级 0)
763
+ // ============================================
259
764
  ctx.middleware(async (session, next) => {
260
- if (!session.groupId && !config.recordPrivateChat) return next();
261
- if (session.groupId && !config.recordGroupChat) return next();
765
+ log.middleware('执行上下文注入中间件', {
766
+ userId: session.userId,
767
+ groupId: session.groupId
768
+ });
769
+
770
+ if (!session.groupId && !config.recordPrivateChat) {
771
+ log.middleware('私聊注入已禁用');
772
+ return next();
773
+ }
774
+ if (session.groupId && !config.recordGroupChat) {
775
+ log.middleware('群聊注入已禁用');
776
+ return next();
777
+ }
262
778
 
263
779
  let shouldInject = false;
780
+ let injectReason = '';
264
781
 
265
782
  // 检查是否 @ 机器人
266
783
  if (config.triggerOnAt) {
267
784
  const isAtBot = session.parsed?.bots?.includes(session.selfId) ||
268
- (session.content && new RegExp(`<@${session.selfId}>|@${session.selfId}`).test(session.content));
269
- if (isAtBot) shouldInject = true;
785
+ (session.content && new RegExp(`<@${session.selfId}>|@${session.selfId}\\b`).test(session.content));
786
+ if (isAtBot) {
787
+ shouldInject = true;
788
+ injectReason = '@机器人';
789
+ }
270
790
  }
271
791
 
272
792
  // 检查是否回复机器人
273
793
  if (!shouldInject && config.triggerOnReply && session.quote) {
274
794
  if (session.quote.userId === session.selfId) {
275
795
  shouldInject = true;
796
+ injectReason = '回复机器人';
276
797
  }
277
798
  }
278
799
 
279
- if (!shouldInject) return next();
800
+ if (!shouldInject) {
801
+ log.middleware('无需注入上下文');
802
+ return next();
803
+ }
804
+
805
+ log.info(`触发上下文注入: ${injectReason}`);
280
806
 
281
- const { sessionId } = generateSessionId(session);
807
+ const sessionInfo = generateSessionId(session);
808
+ const sessionId = sessionInfo.separateId || sessionInfo.sessionId;
809
+
282
810
  const history = await getContext(sessionId);
283
811
 
284
- if (history.length === 0) return next();
812
+ if (history.length === 0) {
813
+ log.middleware('无历史记录可注入');
814
+ return next();
815
+ }
285
816
 
286
- // 构建上下文前缀
817
+ // 构建上下文
287
818
  const contextLines = history.map(msg => {
288
819
  const timeStr = formatTime(msg.timestamp);
289
820
  const userName = msg.user_name || 'Unknown';
290
821
  return `${timeStr} ${userName}: ${msg.content}`;
291
822
  });
292
- const contextPrefix = `【对话前情提要】\n${contextLines.join('\n')}\n---\n请结合以上上下文回答以下问题:`;
823
+
824
+ const contextPrefix = `【对话历史】\n${contextLines.join('\n')}\n---`;
293
825
 
294
826
  let originalContent = session.content || '';
295
827
 
296
828
  if (config.cleanMentions) {
297
829
  originalContent = originalContent.replace(new RegExp(`<@${session.selfId}>`, 'g'), '')
298
- .replace(new RegExp(`@${session.selfId}`, 'g'), '')
830
+ .replace(new RegExp(`@${session.selfId}\\b`, 'g'), '')
299
831
  .trim();
300
832
  }
301
833
 
302
- // 修改 session.content,添加上下文前缀
303
- session.content = `${contextPrefix}\n${originalContent}`;
834
+ // 记录注入信息
835
+ log.inject('注入上下文', {
836
+ sessionId,
837
+ historyCount: history.length,
838
+ originalLength: session.content?.length || 0,
839
+ newLength: contextPrefix.length + originalContent.length,
840
+ timeRange: history.length > 0 ? {
841
+ from: formatTime(history[0].timestamp),
842
+ to: formatTime(history[history.length-1].timestamp)
843
+ } : null
844
+ }, `注入 ${history.length} 条历史到会话 ${sessionId}`);
304
845
 
305
- if (config.verbose) {
306
- logger.info(`[GroupMemory] ${sessionId} 触发上下文注入。历史条数: ${history.length}`);
307
- }
846
+ session.content = `${contextPrefix}\n${originalContent}`;
308
847
 
309
848
  return next();
310
- }, 100);
849
+ }, 0);
311
850
 
312
- // --- 命令:查看当前会话的上下文 ---
851
+ // --- 命令:查看上下文 ---
313
852
  ctx.command('memory-view [count]', '查看当前会话的历史记录')
314
853
  .alias('mem-view')
315
854
  .action(async ({ session }, count = 5) => {
316
- const { sessionId, sessionType } = generateSessionId(session);
855
+ const sessionInfo = generateSessionId(session);
856
+ const sessionId = sessionInfo.separateId || sessionInfo.sessionId;
857
+ const sessionType = sessionInfo.sessionType;
858
+
859
+ log.command('memory-view', session.userId, { sessionId, count });
860
+
317
861
  const history = await getContext(sessionId, parseInt(count));
318
862
 
319
863
  if (history.length === 0) {
@@ -331,16 +875,23 @@ exports.apply = (ctx, config) => {
331
875
  return `📜 ${sessionType === 'private' ? '私聊' : '群聊'}历史记录 (最近${history.length}条):\n${lines.join('\n')}`;
332
876
  });
333
877
 
334
- // --- 命令:清空当前会话的上下文 ---
878
+ // --- 命令:清空上下文 ---
335
879
  ctx.command('memory-clear', '清空当前会话的历史记录')
336
880
  .alias('mem-clear')
337
881
  .action(async ({ session }) => {
338
- const { sessionId, sessionType } = generateSessionId(session);
882
+ const sessionInfo = generateSessionId(session);
883
+ const sessionId = sessionInfo.separateId || sessionInfo.sessionId;
884
+ const sessionType = sessionInfo.sessionType;
885
+
886
+ log.command('memory-clear', session.userId, { sessionId });
887
+
339
888
  const result = await clearContext(sessionId);
340
889
 
341
890
  if (result) {
891
+ log.success(`清空会话 ${sessionId} 成功`);
342
892
  return `✅ 已清空当前${sessionType === 'private' ? '私聊' : '群聊'}的对话记忆!`;
343
893
  } else {
894
+ log.error(`清空会话 ${sessionId} 失败`);
344
895
  return '❌ 清空记忆失败,请查看日志。';
345
896
  }
346
897
  });
@@ -354,13 +905,25 @@ exports.apply = (ctx, config) => {
354
905
  const privateCount = await ctx.database.get('group_memory_messages', { session_type: 'private' }, { fields: ['id'] });
355
906
  const groupCount = await ctx.database.get('group_memory_messages', { session_type: 'group' }, { fields: ['id'] });
356
907
 
357
- return `🧠 Group Memory 插件状态\n` +
908
+ const status = `🧠 Group Memory 插件状态\n` +
358
909
  `总消息数: ${allMessages.length}\n` +
359
910
  `私聊消息: ${privateCount.length}\n` +
360
911
  `群聊消息: ${groupCount.length}\n` +
361
912
  `最大上下文: ${config.maxContext} 轮\n` +
362
- `时间窗口: ${config.timeWindow > 0 ? config.timeWindow / 60000 + ' 分钟' : '无限制'}`;
913
+ `时间窗口: ${config.timeWindow > 0 ? config.timeWindow / 60000 + ' 分钟' : '无限制'}\n` +
914
+ `独立上下文支持: ${config.supportSeparateContext ? '✅' : '❌'}\n` +
915
+ `日志级别: ${config.logLevel === 'none' ? '❌' : config.logLevel === 'simple' ? '📝 简易' : '📊 详细'}`;
916
+
917
+ log.command('memory-status', 'system', {
918
+ total: allMessages.length,
919
+ private: privateCount.length,
920
+ group: groupCount.length,
921
+ logLevel: config.logLevel
922
+ });
923
+
924
+ return status;
363
925
  } catch (error) {
926
+ log.error('获取状态失败', error);
364
927
  return `❌ 获取状态失败: ${error.message}`;
365
928
  }
366
929
  });
@@ -373,23 +936,79 @@ exports.apply = (ctx, config) => {
373
936
  return '⚠️ 时间窗口设置为 0,不会清理任何消息。';
374
937
  }
375
938
 
939
+ log.command('memory-cleanup', 'system', {});
940
+
376
941
  try {
377
- const allMessages = await ctx.database.get('group_memory_messages', {}, { fields: ['id', 'timestamp'] });
942
+ const allMessages = await ctx.database.get('group_memory_messages', {}, { fields: ['id', 'timestamp', 'session_id'] });
378
943
  const cutoff = Date.now() - config.timeWindow;
379
- let deletedCount = 0;
380
-
944
+ const expiredByTime = allMessages.filter(msg => msg.timestamp < cutoff);
945
+
946
+ // 按数量限制清理
947
+ const sessions = {};
381
948
  for (const msg of allMessages) {
382
- if (msg.timestamp < cutoff) {
383
- await ctx.database.remove('group_memory_messages', { id: msg.id });
384
- deletedCount++;
949
+ if (!sessions[msg.session_id]) {
950
+ sessions[msg.session_id] = [];
385
951
  }
952
+ sessions[msg.session_id].push(msg);
386
953
  }
387
954
 
388
- return `🧹 清理完成,删除了 ${deletedCount} 条过期消息。`;
955
+ const expiredByCount = [];
956
+ for (const [sessionId, msgs] of Object.entries(sessions)) {
957
+ msgs.sort((a, b) => a.id - b.id);
958
+ if (msgs.length > config.maxContext * 2) {
959
+ expiredByCount.push(...msgs.slice(0, msgs.length - config.maxContext * 2));
960
+ }
961
+ }
962
+
963
+ // 合并去重
964
+ const toDelete = [...new Set([...expiredByTime, ...expiredByCount].map(msg => msg.id))];
965
+
966
+ if (toDelete.length > 0) {
967
+ await ctx.database.remove('group_memory_messages', { id: toDelete });
968
+
969
+ // 清空相关缓存
970
+ const affectedSessions = [...new Set(allMessages
971
+ .filter(msg => toDelete.includes(msg.id))
972
+ .map(msg => msg.session_id))];
973
+ affectedSessions.forEach(sid => {
974
+ memoryCache.delete(sid);
975
+ cacheTimestamps.delete(sid);
976
+ });
977
+
978
+ log.cleanup(toDelete.length, { affectedSessions: affectedSessions.length });
979
+ }
980
+
981
+ return `🧹 清理完成,删除了 ${toDelete.length} 条过期消息。`;
389
982
  } catch (error) {
983
+ log.error('清理失败', error);
390
984
  return `❌ 清理失败: ${error.message}`;
391
985
  }
392
986
  });
393
987
 
394
- logger.info('Group Memory 插件已加载');
988
+ // 清理定时器
989
+ const cleanupTimer = setInterval(() => {
990
+ if (config.timeWindow > 0) {
991
+ ctx.command('memory-cleanup').execute({}).catch(err => {
992
+ log.error('自动清理失败', err);
993
+ });
994
+ }
995
+ }, 3600000);
996
+
997
+ // 插件卸载时清理
998
+ ctx.on('dispose', () => {
999
+ clearInterval(cleanupTimer);
1000
+ memoryCache.clear();
1001
+ cacheTimestamps.clear();
1002
+ sessionLocks.clear();
1003
+ log.success('插件卸载完成');
1004
+ });
1005
+
1006
+ // 最终确认日志
1007
+ log.success('✅ Group Memory 插件已完全加载,等待消息...');
1008
+ log.info('当前配置:', {
1009
+ recordPrivateChat: config.recordPrivateChat,
1010
+ recordGroupChat: config.recordGroupChat,
1011
+ logLevel: config.logLevel,
1012
+ maxContext: config.maxContext
1013
+ });
395
1014
  };