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.
- package/package.json +32 -12
- 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": "
|
|
4
|
-
"description": "
|
|
5
|
-
"main": "
|
|
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
|
-
"
|
|
21
|
+
"chat-history",
|
|
22
|
+
"group-memory",
|
|
23
|
+
"database",
|
|
24
|
+
"cache",
|
|
15
25
|
"ai",
|
|
16
|
-
"
|
|
17
|
-
"
|
|
18
|
-
"
|
|
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": "
|
|
29
|
-
"zh": "
|
|
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
|
|
8
|
-
id:
|
|
9
|
-
session_id:
|
|
10
|
-
session_type:
|
|
11
|
-
user_id:
|
|
12
|
-
user_name:
|
|
13
|
-
role:
|
|
14
|
-
content:
|
|
15
|
-
timestamp:
|
|
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)
|
|
28
|
+
.default(3600000)
|
|
30
29
|
.min(0)
|
|
31
|
-
.step(60000)
|
|
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 前,是否移除消息中的 @
|
|
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',
|
|
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
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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(
|
|
106
|
-
|
|
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
|
-
|
|
112
|
-
|
|
445
|
+
text = text.replace(/\[图片\]/g, '[图片]')
|
|
446
|
+
.replace(/\[表情.*?\]/g, '')
|
|
447
|
+
.replace(/\[CQ:.*?\]/g, '')
|
|
448
|
+
.replace(/!?\[.*?\]\(.*?\)/g, '[链接]');
|
|
113
449
|
}
|
|
114
450
|
|
|
115
|
-
|
|
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
|
-
// ---
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
571
|
+
log.error('保存消息失败', error);
|
|
172
572
|
return false;
|
|
573
|
+
} finally {
|
|
574
|
+
release();
|
|
173
575
|
}
|
|
174
576
|
};
|
|
175
577
|
|
|
176
|
-
// ---
|
|
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
|
-
|
|
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
|
|
204
|
-
|
|
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
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
682
|
+
log.success('API 接口已暴露');
|
|
683
|
+
|
|
684
|
+
// ============================================
|
|
685
|
+
// 修复:中间件 - 记录所有消息(提高优先级)
|
|
686
|
+
// ============================================
|
|
230
687
|
ctx.middleware(async (session, next) => {
|
|
231
|
-
|
|
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)
|
|
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))
|
|
721
|
+
if (/^[./\\]/.test(trimmed)) {
|
|
722
|
+
log.middleware('忽略命令消息');
|
|
723
|
+
return next();
|
|
724
|
+
}
|
|
238
725
|
|
|
239
|
-
const
|
|
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)
|
|
243
|
-
|
|
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
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
741
|
+
|
|
742
|
+
log.db('准备保存消息', {
|
|
743
|
+
sessionId,
|
|
744
|
+
sessionType,
|
|
745
|
+
userId: session.userId,
|
|
746
|
+
userName,
|
|
747
|
+
contentPreview: cleanText.substring(0, 30)
|
|
748
|
+
});
|
|
250
749
|
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
},
|
|
759
|
+
}, 100); // 提高优先级到 100,确保先执行
|
|
257
760
|
|
|
258
|
-
//
|
|
761
|
+
// ============================================
|
|
762
|
+
// 中间件2: 上下文注入(保持优先级 0)
|
|
763
|
+
// ============================================
|
|
259
764
|
ctx.middleware(async (session, next) => {
|
|
260
|
-
|
|
261
|
-
|
|
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)
|
|
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)
|
|
800
|
+
if (!shouldInject) {
|
|
801
|
+
log.middleware('无需注入上下文');
|
|
802
|
+
return next();
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
log.info(`触发上下文注入: ${injectReason}`);
|
|
280
806
|
|
|
281
|
-
const
|
|
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)
|
|
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
|
-
|
|
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
|
-
//
|
|
303
|
-
|
|
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
|
-
|
|
306
|
-
logger.info(`[GroupMemory] ${sessionId} 触发上下文注入。历史条数: ${history.length}`);
|
|
307
|
-
}
|
|
846
|
+
session.content = `${contextPrefix}\n${originalContent}`;
|
|
308
847
|
|
|
309
848
|
return next();
|
|
310
|
-
},
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
383
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
};
|