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.
- package/package.json +1 -1
- package/src/index.js +574 -137
package/package.json
CHANGED
package/src/index.js
CHANGED
|
@@ -4,219 +4,559 @@ 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
|
-
}
|
|
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
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
.
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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('
|
|
49
|
+
.description('是否使用颜色区分不同类型的日志(需终端支持)'),
|
|
55
50
|
|
|
56
|
-
|
|
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',
|
|
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
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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(
|
|
105
|
-
|
|
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
|
-
|
|
111
|
-
|
|
329
|
+
text = text.replace(/\[图片\]/g, '[图片]')
|
|
330
|
+
.replace(/\[表情.*?\]/g, '')
|
|
331
|
+
.replace(/\[CQ:.*?\]/g, '')
|
|
332
|
+
.replace(/!?\[.*?\]\(.*?\)/g, '[链接]');
|
|
112
333
|
}
|
|
113
334
|
|
|
114
|
-
|
|
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
|
-
// ---
|
|
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 =>
|
|
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
|
-
|
|
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:
|
|
163
|
-
}
|
|
435
|
+
timestamp: Date.now(),
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
log.db('创建', messageData, `插入消息: ${role} (${content.substring(0, 30)}...)`);
|
|
164
439
|
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
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
|
-
// ---
|
|
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
|
-
|
|
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
|
|
203
|
-
|
|
204
|
-
|
|
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
|
|
212
|
-
|
|
213
|
-
);
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
}
|
|
600
|
+
saveMessage(sessionId, 'user', cleanText, session.userId, userName).catch(err => {
|
|
601
|
+
log.error('异步保存消息失败', err);
|
|
602
|
+
});
|
|
255
603
|
|
|
256
604
|
return next();
|
|
257
|
-
},
|
|
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
|
|
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
|
-
|
|
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
|
-
//
|
|
304
|
-
|
|
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
|
-
|
|
307
|
-
logger.info(`[GroupMemory] ${sessionId} 触发上下文注入。历史条数: ${history.length}`);
|
|
308
|
-
}
|
|
664
|
+
session.content = `${contextPrefix}\n${originalContent}`;
|
|
309
665
|
|
|
310
666
|
return next();
|
|
311
|
-
},
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
381
|
-
|
|
762
|
+
const expiredByTime = allMessages.filter(msg => msg.timestamp < cutoff);
|
|
763
|
+
|
|
764
|
+
// 按数量限制清理
|
|
765
|
+
const sessions = {};
|
|
382
766
|
for (const msg of allMessages) {
|
|
383
|
-
if (
|
|
384
|
-
|
|
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 `🧹 清理完成,删除了 ${
|
|
799
|
+
return `🧹 清理完成,删除了 ${toDelete.length} 条过期消息。`;
|
|
390
800
|
} catch (error) {
|
|
801
|
+
log.error('清理失败', error);
|
|
391
802
|
return `❌ 清理失败: ${error.message}`;
|
|
392
803
|
}
|
|
393
804
|
});
|
|
394
805
|
|
|
395
|
-
|
|
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
|
};
|