koishi-plugin-group-memory 2.0.0 → 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 +351 -170
package/package.json
CHANGED
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-group-memory",
|
|
3
|
-
"version": "2.
|
|
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
|
@@ -17,16 +17,59 @@ const MessageFields = {
|
|
|
17
17
|
|
|
18
18
|
// --- 配置架构 ---
|
|
19
19
|
exports.Config = Schema.object({
|
|
20
|
-
//
|
|
20
|
+
// --- 存储设置 ---
|
|
21
|
+
maxContext: Schema.number()
|
|
22
|
+
.default(20)
|
|
23
|
+
.min(1)
|
|
24
|
+
.max(100)
|
|
25
|
+
.description('每个会话保留最近多少条消息作为上下文。'),
|
|
26
|
+
|
|
27
|
+
timeWindow: Schema.number()
|
|
28
|
+
.default(3600000)
|
|
29
|
+
.min(0)
|
|
30
|
+
.step(60000)
|
|
31
|
+
.description('只保留最近 N 毫秒内的消息。设置为 0 表示不限制时间。'),
|
|
32
|
+
|
|
33
|
+
// --- 记录设置 ---
|
|
34
|
+
recordPrivateChat: Schema.boolean()
|
|
35
|
+
.default(true)
|
|
36
|
+
.description('是否记录私聊消息。'),
|
|
37
|
+
|
|
38
|
+
recordGroupChat: Schema.boolean()
|
|
39
|
+
.default(true)
|
|
40
|
+
.description('是否记录群聊消息。'),
|
|
41
|
+
|
|
42
|
+
// --- 触发条件(用于中间件模式) ---
|
|
43
|
+
triggerOnAt: Schema.boolean()
|
|
44
|
+
.default(true)
|
|
45
|
+
.description('当消息中包含 @机器人 时,触发上下文注入。'),
|
|
46
|
+
|
|
47
|
+
triggerOnReply: Schema.boolean()
|
|
48
|
+
.default(true)
|
|
49
|
+
.description('当消息是回复机器人的消息时,触发上下文注入。'),
|
|
50
|
+
|
|
51
|
+
// --- 内容处理 ---
|
|
52
|
+
cleanMentions: Schema.boolean()
|
|
53
|
+
.default(true)
|
|
54
|
+
.description('发送给 AI 前,是否移除消息中的 @ 符号。'),
|
|
55
|
+
|
|
56
|
+
cleanImages: Schema.boolean()
|
|
57
|
+
.default(true)
|
|
58
|
+
.description('是否移除图片/媒体占位符。'),
|
|
59
|
+
|
|
60
|
+
// --- 调试 ---
|
|
61
|
+
verbose: Schema.boolean()
|
|
62
|
+
.default(false)
|
|
63
|
+
.description('是否在控制台打印注入的上下文 (调试用)。'),
|
|
21
64
|
|
|
22
65
|
// ============================================
|
|
23
|
-
//
|
|
66
|
+
// 新增:日志显示功能(修复版)
|
|
24
67
|
// ============================================
|
|
25
68
|
logLevel: Schema.union([
|
|
26
69
|
Schema.const('none').description('不输出日志'),
|
|
27
70
|
Schema.const('simple').description('简易模式:输出易于理解的操作摘要'),
|
|
28
71
|
Schema.const('detailed').description('详细模式:输出完整的 JSON 格式日志'),
|
|
29
|
-
]).default('
|
|
72
|
+
]).default('detailed').description('日志输出级别(默认详细,便于调试)'), // 改为默认 detailed
|
|
30
73
|
|
|
31
74
|
logDatabaseOps: Schema.boolean()
|
|
32
75
|
.default(true)
|
|
@@ -40,6 +83,10 @@ exports.Config = Schema.object({
|
|
|
40
83
|
.default(true)
|
|
41
84
|
.description('是否记录对外部插件的 API 调用'),
|
|
42
85
|
|
|
86
|
+
logMessageReceive: Schema.boolean()
|
|
87
|
+
.default(true)
|
|
88
|
+
.description('是否记录接收到的原始消息'),
|
|
89
|
+
|
|
43
90
|
logContentPreview: Schema.boolean()
|
|
44
91
|
.default(true)
|
|
45
92
|
.description('是否在简易模式下预览消息内容(只显示前30个字符)'),
|
|
@@ -51,6 +98,10 @@ exports.Config = Schema.object({
|
|
|
51
98
|
logTimestamp: Schema.boolean()
|
|
52
99
|
.default(true)
|
|
53
100
|
.description('是否在日志中显示时间戳'),
|
|
101
|
+
|
|
102
|
+
logMiddlewarePriority: Schema.boolean()
|
|
103
|
+
.default(true)
|
|
104
|
+
.description('是否记录中间件执行优先级信息'),
|
|
54
105
|
});
|
|
55
106
|
|
|
56
107
|
exports.schema = exports.Config;
|
|
@@ -61,19 +112,20 @@ exports.apply = (ctx, config) => {
|
|
|
61
112
|
const logger = ctx.logger('group-memory');
|
|
62
113
|
|
|
63
114
|
// ============================================
|
|
64
|
-
//
|
|
115
|
+
// 日志增强模块(修复版)
|
|
65
116
|
// ============================================
|
|
66
117
|
|
|
67
|
-
//
|
|
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
|
+
// 颜色代码
|
|
68
127
|
const colors = {
|
|
69
128
|
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
129
|
fg: {
|
|
78
130
|
black: '\x1b[30m',
|
|
79
131
|
red: '\x1b[31m',
|
|
@@ -83,39 +135,25 @@ exports.apply = (ctx, config) => {
|
|
|
83
135
|
magenta: '\x1b[35m',
|
|
84
136
|
cyan: '\x1b[36m',
|
|
85
137
|
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
138
|
}
|
|
99
139
|
};
|
|
100
140
|
|
|
101
|
-
// 是否启用颜色
|
|
102
141
|
const useColor = config.logColorOutput && process.stdout.isTTY;
|
|
103
142
|
|
|
104
|
-
// 颜色辅助函数
|
|
105
143
|
const color = (text, colorCode) => {
|
|
106
144
|
if (!useColor) return text;
|
|
107
145
|
return `${colorCode}${text}${colors.reset}`;
|
|
108
146
|
};
|
|
109
147
|
|
|
110
|
-
// 格式化时间戳
|
|
111
148
|
const getTimestamp = () => {
|
|
112
149
|
if (!config.logTimestamp) return '';
|
|
113
150
|
const now = new Date();
|
|
114
151
|
return `[${now.toLocaleTimeString()}.${now.getMilliseconds().toString().padStart(3, '0')}] `;
|
|
115
152
|
};
|
|
116
153
|
|
|
117
|
-
//
|
|
154
|
+
// 日志图标
|
|
118
155
|
const icons = {
|
|
156
|
+
receive: '📥',
|
|
119
157
|
db: '🗄️',
|
|
120
158
|
cache: '⚡',
|
|
121
159
|
inject: '📎',
|
|
@@ -127,137 +165,210 @@ exports.apply = (ctx, config) => {
|
|
|
127
165
|
info: 'ℹ️',
|
|
128
166
|
debug: '🔍',
|
|
129
167
|
memory: '🧠',
|
|
130
|
-
cleanup: '🧹'
|
|
168
|
+
cleanup: '🧹',
|
|
169
|
+
middleware: '🔄'
|
|
131
170
|
};
|
|
132
171
|
|
|
133
|
-
//
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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)}`);
|
|
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)}`);
|
|
162
187
|
} else {
|
|
163
|
-
|
|
188
|
+
const msg = simpleMsg || `收到消息: ${details.userName} ${details.contentPreview}`;
|
|
189
|
+
logger.info(`${timestamp}${icon} ${color(msg, colors.fg.cyan)}`);
|
|
164
190
|
}
|
|
165
|
-
}
|
|
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
|
-
};
|
|
191
|
+
},
|
|
183
192
|
|
|
184
|
-
|
|
185
|
-
};
|
|
186
|
-
|
|
187
|
-
// 统一日志接口
|
|
188
|
-
const log = {
|
|
193
|
+
// 数据库操作日志
|
|
189
194
|
db: (operation, details, simpleMsg) => {
|
|
195
|
+
if (config.logLevel === 'none') return;
|
|
190
196
|
if (!config.logDatabaseOps) return;
|
|
197
|
+
|
|
198
|
+
const timestamp = getTimestamp();
|
|
199
|
+
const icon = icons.db;
|
|
200
|
+
|
|
191
201
|
if (config.logLevel === 'detailed') {
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
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}`}`);
|
|
195
209
|
}
|
|
196
210
|
},
|
|
197
211
|
|
|
212
|
+
// 上下文注入日志
|
|
198
213
|
inject: (operation, details, simpleMsg) => {
|
|
214
|
+
if (config.logLevel === 'none') return;
|
|
199
215
|
if (!config.logContextInject) return;
|
|
216
|
+
|
|
217
|
+
const timestamp = getTimestamp();
|
|
218
|
+
const icon = icons.inject;
|
|
219
|
+
|
|
200
220
|
if (config.logLevel === 'detailed') {
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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}`}`);
|
|
204
228
|
}
|
|
205
229
|
},
|
|
206
230
|
|
|
231
|
+
// API调用日志
|
|
207
232
|
api: (operation, details, simpleMsg) => {
|
|
233
|
+
if (config.logLevel === 'none') return;
|
|
208
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
|
+
|
|
209
258
|
if (config.logLevel === 'detailed') {
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
259
|
+
logger.info(`${timestamp}${icon} ${JSON.stringify({
|
|
260
|
+
type: 'middleware',
|
|
261
|
+
...details
|
|
262
|
+
}, null, 2)}`);
|
|
263
|
+
} else {
|
|
264
|
+
logger.info(`${timestamp}${icon} ${msg}`);
|
|
213
265
|
}
|
|
214
266
|
},
|
|
215
267
|
|
|
268
|
+
// 命令日志
|
|
216
269
|
command: (cmd, user, details) => {
|
|
217
270
|
if (config.logLevel === 'none') return;
|
|
271
|
+
|
|
272
|
+
const timestamp = getTimestamp();
|
|
273
|
+
const icon = icons.command;
|
|
218
274
|
const simpleMsg = `命令 ${cmd} 由 ${user} 执行`;
|
|
275
|
+
|
|
219
276
|
if (config.logLevel === 'detailed') {
|
|
220
|
-
|
|
277
|
+
logger.info(`${timestamp}${icon} ${JSON.stringify({
|
|
278
|
+
type: 'command',
|
|
279
|
+
command: cmd,
|
|
280
|
+
user,
|
|
281
|
+
...details
|
|
282
|
+
}, null, 2)}`);
|
|
221
283
|
} else {
|
|
222
|
-
|
|
284
|
+
logger.info(`${timestamp}${icon} ${simpleMsg}`);
|
|
223
285
|
}
|
|
224
286
|
},
|
|
225
287
|
|
|
288
|
+
// 错误日志
|
|
226
289
|
error: (msg, error) => {
|
|
227
290
|
if (config.logLevel === 'none') return;
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
}
|
|
232
|
-
|
|
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);
|
|
233
300
|
}
|
|
234
301
|
},
|
|
235
302
|
|
|
303
|
+
// 成功日志
|
|
236
304
|
success: (msg, details) => {
|
|
237
305
|
if (config.logLevel === 'none') return;
|
|
306
|
+
|
|
307
|
+
const timestamp = getTimestamp();
|
|
308
|
+
const icon = icons.success;
|
|
309
|
+
|
|
238
310
|
if (config.logLevel === 'detailed') {
|
|
239
|
-
|
|
311
|
+
logger.info(`${timestamp}${icon} ${JSON.stringify({
|
|
312
|
+
type: 'success',
|
|
313
|
+
message: msg,
|
|
314
|
+
...details
|
|
315
|
+
}, null, 2)}`);
|
|
240
316
|
} else {
|
|
241
|
-
|
|
317
|
+
logger.info(`${timestamp}${icon} ${color(msg, colors.fg.green)}`);
|
|
242
318
|
}
|
|
243
319
|
},
|
|
244
320
|
|
|
321
|
+
// 缓存日志
|
|
245
322
|
cache: (operation, details) => {
|
|
246
323
|
if (config.logLevel === 'none') return;
|
|
324
|
+
|
|
325
|
+
const timestamp = getTimestamp();
|
|
326
|
+
const icon = icons.cache;
|
|
327
|
+
|
|
247
328
|
if (config.logLevel === 'detailed') {
|
|
248
|
-
|
|
329
|
+
logger.info(`${timestamp}${icon} ${JSON.stringify({
|
|
330
|
+
type: 'cache',
|
|
331
|
+
operation,
|
|
332
|
+
...details
|
|
333
|
+
}, null, 2)}`);
|
|
249
334
|
} else {
|
|
250
|
-
|
|
335
|
+
logger.info(`${timestamp}${icon} 缓存 ${operation}`);
|
|
251
336
|
}
|
|
252
337
|
},
|
|
253
338
|
|
|
339
|
+
// 清理日志
|
|
254
340
|
cleanup: (count, details) => {
|
|
255
341
|
if (config.logLevel === 'none') return;
|
|
342
|
+
|
|
343
|
+
const timestamp = getTimestamp();
|
|
344
|
+
const icon = icons.cleanup;
|
|
256
345
|
const simpleMsg = `清理了 ${count} 条过期消息`;
|
|
346
|
+
|
|
257
347
|
if (config.logLevel === 'detailed') {
|
|
258
|
-
|
|
348
|
+
logger.info(`${timestamp}${icon} ${JSON.stringify({
|
|
349
|
+
type: 'cleanup',
|
|
350
|
+
count,
|
|
351
|
+
...details
|
|
352
|
+
}, null, 2)}`);
|
|
259
353
|
} else {
|
|
260
|
-
|
|
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}`);
|
|
261
372
|
}
|
|
262
373
|
}
|
|
263
374
|
};
|
|
@@ -280,10 +391,10 @@ exports.apply = (ctx, config) => {
|
|
|
280
391
|
await new Promise(resolve => setTimeout(resolve, 100));
|
|
281
392
|
}
|
|
282
393
|
sessionLocks.set(sessionId, true);
|
|
283
|
-
log.cache('锁定', { sessionId
|
|
394
|
+
log.cache('锁定', { sessionId });
|
|
284
395
|
return () => {
|
|
285
396
|
sessionLocks.delete(sessionId);
|
|
286
|
-
log.cache('解锁', { sessionId
|
|
397
|
+
log.cache('解锁', { sessionId });
|
|
287
398
|
};
|
|
288
399
|
};
|
|
289
400
|
|
|
@@ -294,30 +405,35 @@ exports.apply = (ctx, config) => {
|
|
|
294
405
|
index: ['session_id', 'session_type', 'timestamp']
|
|
295
406
|
});
|
|
296
407
|
|
|
297
|
-
log.success('数据库表初始化完成'
|
|
408
|
+
log.success('数据库表初始化完成');
|
|
298
409
|
|
|
299
410
|
// --- 辅助函数:生成会话ID ---
|
|
300
411
|
const generateSessionId = (session) => {
|
|
301
412
|
const result = !session.groupId
|
|
302
|
-
? {
|
|
413
|
+
? {
|
|
414
|
+
sessionId: `private:${session.userId}`,
|
|
415
|
+
sessionType: 'private'
|
|
416
|
+
}
|
|
303
417
|
: {
|
|
304
418
|
sessionId: `group:${session.groupId}`,
|
|
305
|
-
separateId: config.supportSeparateContext ?
|
|
419
|
+
separateId: config.supportSeparateContext ? `group:${session.groupId}:user:${session.userId}` : undefined,
|
|
306
420
|
sessionType: 'group'
|
|
307
421
|
};
|
|
308
422
|
|
|
309
423
|
log.db('生成会话ID', {
|
|
310
424
|
input: { userId: session.userId, groupId: session.groupId },
|
|
311
425
|
output: result
|
|
312
|
-
},
|
|
426
|
+
}, `会话ID: ${result.sessionId}`);
|
|
313
427
|
|
|
314
428
|
return result;
|
|
315
429
|
};
|
|
316
430
|
|
|
317
431
|
// --- 辅助函数:清理内容 ---
|
|
318
432
|
const cleanContent = (content, session) => {
|
|
433
|
+
if (!content) return '';
|
|
434
|
+
|
|
319
435
|
let text = content;
|
|
320
|
-
const
|
|
436
|
+
const originalPreview = text.substring(0, 30);
|
|
321
437
|
|
|
322
438
|
if (config.cleanMentions && session) {
|
|
323
439
|
text = text.replace(new RegExp(`<@${session.selfId}>`, 'g'), '')
|
|
@@ -335,10 +451,9 @@ exports.apply = (ctx, config) => {
|
|
|
335
451
|
const result = text.trim() || '[空消息]';
|
|
336
452
|
|
|
337
453
|
log.db('清理内容', {
|
|
338
|
-
original:
|
|
339
|
-
result:
|
|
340
|
-
|
|
341
|
-
}, `清理内容: ${originalLength} -> ${result.length} 字符`);
|
|
454
|
+
original: originalPreview,
|
|
455
|
+
result: result.substring(0, 30)
|
|
456
|
+
}, `内容清理: ${originalPreview} -> ${result.substring(0, 30)}`);
|
|
342
457
|
|
|
343
458
|
return result;
|
|
344
459
|
};
|
|
@@ -361,29 +476,28 @@ exports.apply = (ctx, config) => {
|
|
|
361
476
|
}
|
|
362
477
|
}
|
|
363
478
|
if (cleaned > 0) {
|
|
364
|
-
log.cache('清理过期缓存', { cleaned
|
|
479
|
+
log.cache('清理过期缓存', { cleaned });
|
|
365
480
|
}
|
|
366
481
|
};
|
|
367
482
|
setInterval(cleanupCache, CACHE_TTL);
|
|
368
483
|
|
|
369
484
|
// --- 核心 API:获取上下文 ---
|
|
370
485
|
const getContext = async (sessionId, limit = null) => {
|
|
371
|
-
log.api('getContext', { sessionId, limit }
|
|
486
|
+
log.api('getContext', { sessionId, limit });
|
|
372
487
|
|
|
373
|
-
// 尝试从缓存获取
|
|
374
488
|
if (memoryCache.has(sessionId)) {
|
|
375
489
|
const cached = memoryCache.get(sessionId);
|
|
376
490
|
const age = Date.now() - cacheTimestamps.get(sessionId);
|
|
377
491
|
if (age < CACHE_TTL) {
|
|
378
|
-
log.cache('命中', { sessionId,
|
|
492
|
+
log.cache('命中', { sessionId, count: cached.length });
|
|
379
493
|
return cached;
|
|
380
494
|
} else {
|
|
381
|
-
log.cache('过期', { sessionId, age }
|
|
495
|
+
log.cache('过期', { sessionId, age });
|
|
382
496
|
}
|
|
383
497
|
}
|
|
384
498
|
|
|
385
499
|
const maxMessages = limit ? limit * 2 : config.maxContext * 2;
|
|
386
|
-
log.db('查询', { sessionId, maxMessages }
|
|
500
|
+
log.db('查询', { sessionId, maxMessages });
|
|
387
501
|
|
|
388
502
|
try {
|
|
389
503
|
let history = await ctx.database.get('group_memory_messages',
|
|
@@ -401,13 +515,12 @@ exports.apply = (ctx, config) => {
|
|
|
401
515
|
const before = history.length;
|
|
402
516
|
const cutoff = Date.now() - config.timeWindow;
|
|
403
517
|
history = history.filter(msg => msg.timestamp >= cutoff);
|
|
404
|
-
log.db('时间窗口过滤', { before, after: history.length
|
|
518
|
+
log.db('时间窗口过滤', { before, after: history.length });
|
|
405
519
|
}
|
|
406
520
|
|
|
407
|
-
// 更新缓存
|
|
408
521
|
memoryCache.set(sessionId, history);
|
|
409
522
|
cacheTimestamps.set(sessionId, Date.now());
|
|
410
|
-
log.cache('更新', { sessionId, count: history.length }
|
|
523
|
+
log.cache('更新', { sessionId, count: history.length });
|
|
411
524
|
|
|
412
525
|
return history;
|
|
413
526
|
} catch (error) {
|
|
@@ -418,7 +531,7 @@ exports.apply = (ctx, config) => {
|
|
|
418
531
|
|
|
419
532
|
// --- 核心 API:保存消息 ---
|
|
420
533
|
const saveMessage = async (sessionId, role, content, userId = 'system', userName = 'System') => {
|
|
421
|
-
log.api('saveMessage', { sessionId, role, userId }
|
|
534
|
+
log.api('saveMessage', { sessionId, role, userId });
|
|
422
535
|
|
|
423
536
|
const release = await lockSession(sessionId);
|
|
424
537
|
|
|
@@ -435,14 +548,18 @@ exports.apply = (ctx, config) => {
|
|
|
435
548
|
timestamp: Date.now(),
|
|
436
549
|
};
|
|
437
550
|
|
|
438
|
-
log.db('创建',
|
|
551
|
+
log.db('创建', {
|
|
552
|
+
sessionId,
|
|
553
|
+
role,
|
|
554
|
+
userName,
|
|
555
|
+
contentPreview: content.substring(0, 30)
|
|
556
|
+
});
|
|
439
557
|
|
|
440
558
|
await ctx.database.create('group_memory_messages', messageData);
|
|
441
559
|
|
|
442
|
-
// 使缓存失效
|
|
443
560
|
memoryCache.delete(sessionId);
|
|
444
561
|
cacheTimestamps.delete(sessionId);
|
|
445
|
-
log.cache('失效', { sessionId }
|
|
562
|
+
log.cache('失效', { sessionId });
|
|
446
563
|
|
|
447
564
|
// 异步清理旧消息
|
|
448
565
|
cleanupOldMessages(sessionId).catch(err => {
|
|
@@ -460,7 +577,7 @@ exports.apply = (ctx, config) => {
|
|
|
460
577
|
|
|
461
578
|
// --- API:删除最后一条消息 ---
|
|
462
579
|
const deleteLastMessage = async (sessionId, role) => {
|
|
463
|
-
log.api('deleteLastMessage', { sessionId, role }
|
|
580
|
+
log.api('deleteLastMessage', { sessionId, role });
|
|
464
581
|
|
|
465
582
|
const release = await lockSession(sessionId);
|
|
466
583
|
|
|
@@ -474,14 +591,13 @@ exports.apply = (ctx, config) => {
|
|
|
474
591
|
);
|
|
475
592
|
|
|
476
593
|
if (lastMessage && lastMessage.length > 0) {
|
|
477
|
-
log.db('删除', { id: lastMessage[0].id }
|
|
594
|
+
log.db('删除', { id: lastMessage[0].id });
|
|
478
595
|
await ctx.database.remove('group_memory_messages', { id: lastMessage[0].id });
|
|
479
596
|
memoryCache.delete(sessionId);
|
|
480
597
|
cacheTimestamps.delete(sessionId);
|
|
481
598
|
return true;
|
|
482
599
|
}
|
|
483
600
|
|
|
484
|
-
log.db('删除', { sessionId, role }, `未找到要删除的消息`);
|
|
485
601
|
return false;
|
|
486
602
|
} catch (error) {
|
|
487
603
|
log.error('删除消息失败', error);
|
|
@@ -493,17 +609,17 @@ exports.apply = (ctx, config) => {
|
|
|
493
609
|
|
|
494
610
|
// --- API:清空上下文 ---
|
|
495
611
|
const clearContext = async (sessionId) => {
|
|
496
|
-
log.api('clearContext', { sessionId }
|
|
612
|
+
log.api('clearContext', { sessionId });
|
|
497
613
|
|
|
498
614
|
const release = await lockSession(sessionId);
|
|
499
615
|
|
|
500
616
|
try {
|
|
501
617
|
const count = await ctx.database.remove('group_memory_messages', { session_id: sessionId });
|
|
502
|
-
log.db('清空', { sessionId, count }
|
|
618
|
+
log.db('清空', { sessionId, count });
|
|
503
619
|
|
|
504
620
|
memoryCache.delete(sessionId);
|
|
505
621
|
cacheTimestamps.delete(sessionId);
|
|
506
|
-
log.cache('失效', { sessionId }
|
|
622
|
+
log.cache('失效', { sessionId });
|
|
507
623
|
|
|
508
624
|
return true;
|
|
509
625
|
} catch (error) {
|
|
@@ -527,23 +643,18 @@ exports.apply = (ctx, config) => {
|
|
|
527
643
|
const maxMessages = config.maxContext * 2;
|
|
528
644
|
let toDelete = [];
|
|
529
645
|
|
|
530
|
-
// 按数量限制
|
|
531
646
|
if (allMessages.length > maxMessages) {
|
|
532
647
|
allMessages.sort((a, b) => a.id - b.id);
|
|
533
648
|
const excess = allMessages.slice(0, allMessages.length - maxMessages);
|
|
534
649
|
toDelete.push(...excess);
|
|
535
|
-
log.db('数量限制', { total: allMessages.length, max: maxMessages, excess: excess.length });
|
|
536
650
|
}
|
|
537
651
|
|
|
538
|
-
// 按时间窗口限制
|
|
539
652
|
if (config.timeWindow > 0) {
|
|
540
653
|
const cutoff = Date.now() - config.timeWindow;
|
|
541
654
|
const expired = allMessages.filter(msg => msg.timestamp < cutoff);
|
|
542
655
|
toDelete.push(...expired);
|
|
543
|
-
log.db('时间窗口限制', { cutoff, expired: expired.length });
|
|
544
656
|
}
|
|
545
657
|
|
|
546
|
-
// 批量删除
|
|
547
658
|
if (toDelete.length > 0) {
|
|
548
659
|
const uniqueIds = [...new Set(toDelete.map(msg => msg.id))];
|
|
549
660
|
await ctx.database.remove('group_memory_messages', { id: uniqueIds });
|
|
@@ -551,7 +662,7 @@ exports.apply = (ctx, config) => {
|
|
|
551
662
|
memoryCache.delete(sessionId);
|
|
552
663
|
cacheTimestamps.delete(sessionId);
|
|
553
664
|
|
|
554
|
-
log.cleanup(uniqueIds.length, { sessionId
|
|
665
|
+
log.cleanup(uniqueIds.length, { sessionId });
|
|
555
666
|
}
|
|
556
667
|
} catch (error) {
|
|
557
668
|
log.error('清理旧消息失败', error);
|
|
@@ -568,69 +679,140 @@ exports.apply = (ctx, config) => {
|
|
|
568
679
|
deleteLastMessage,
|
|
569
680
|
};
|
|
570
681
|
|
|
571
|
-
log.success('API 接口已暴露'
|
|
682
|
+
log.success('API 接口已暴露');
|
|
572
683
|
|
|
573
|
-
//
|
|
684
|
+
// ============================================
|
|
685
|
+
// 修复:中间件 - 记录所有消息(提高优先级)
|
|
686
|
+
// ============================================
|
|
574
687
|
ctx.middleware(async (session, next) => {
|
|
575
|
-
|
|
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
|
+
}
|
|
576
699
|
|
|
577
700
|
const trimmed = session.content?.trim() || '';
|
|
578
|
-
if (!trimmed)
|
|
701
|
+
if (!trimmed) {
|
|
702
|
+
log.middleware('忽略空消息');
|
|
703
|
+
return next();
|
|
704
|
+
}
|
|
579
705
|
|
|
580
|
-
|
|
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}`);
|
|
719
|
+
|
|
720
|
+
// 忽略命令消息
|
|
721
|
+
if (/^[./\\]/.test(trimmed)) {
|
|
722
|
+
log.middleware('忽略命令消息');
|
|
723
|
+
return next();
|
|
724
|
+
}
|
|
581
725
|
|
|
582
726
|
const sessionInfo = generateSessionId(session);
|
|
583
727
|
const sessionId = sessionInfo.separateId || sessionInfo.sessionId;
|
|
584
728
|
const sessionType = sessionInfo.sessionType;
|
|
585
729
|
|
|
586
|
-
|
|
587
|
-
if (sessionType === '
|
|
730
|
+
// 根据配置决定是否记录
|
|
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
|
+
}
|
|
588
739
|
|
|
589
740
|
const cleanText = cleanContent(session.content, session);
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
log.db('记录消息', {
|
|
741
|
+
|
|
742
|
+
log.db('准备保存消息', {
|
|
593
743
|
sessionId,
|
|
594
744
|
sessionType,
|
|
595
745
|
userId: session.userId,
|
|
596
746
|
userName,
|
|
597
|
-
|
|
598
|
-
}, `📝 ${userName}: ${cleanText.substring(0, 30)}...`);
|
|
599
|
-
|
|
600
|
-
saveMessage(sessionId, 'user', cleanText, session.userId, userName).catch(err => {
|
|
601
|
-
log.error('异步保存消息失败', err);
|
|
747
|
+
contentPreview: cleanText.substring(0, 30)
|
|
602
748
|
});
|
|
603
749
|
|
|
750
|
+
// 立即保存并等待完成,确保日志输出
|
|
751
|
+
try {
|
|
752
|
+
await saveMessage(sessionId, 'user', cleanText, session.userId, userName);
|
|
753
|
+
log.success(`消息已保存: ${sessionId}`);
|
|
754
|
+
} catch (err) {
|
|
755
|
+
log.error('保存消息失败', err);
|
|
756
|
+
}
|
|
757
|
+
|
|
604
758
|
return next();
|
|
605
|
-
},
|
|
759
|
+
}, 100); // 提高优先级到 100,确保先执行
|
|
606
760
|
|
|
607
|
-
//
|
|
761
|
+
// ============================================
|
|
762
|
+
// 中间件2: 上下文注入(保持优先级 0)
|
|
763
|
+
// ============================================
|
|
608
764
|
ctx.middleware(async (session, next) => {
|
|
609
|
-
|
|
610
|
-
|
|
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
|
+
}
|
|
611
778
|
|
|
612
779
|
let shouldInject = false;
|
|
780
|
+
let injectReason = '';
|
|
613
781
|
|
|
782
|
+
// 检查是否 @ 机器人
|
|
614
783
|
if (config.triggerOnAt) {
|
|
615
784
|
const isAtBot = session.parsed?.bots?.includes(session.selfId) ||
|
|
616
785
|
(session.content && new RegExp(`<@${session.selfId}>|@${session.selfId}\\b`).test(session.content));
|
|
617
|
-
if (isAtBot)
|
|
786
|
+
if (isAtBot) {
|
|
787
|
+
shouldInject = true;
|
|
788
|
+
injectReason = '@机器人';
|
|
789
|
+
}
|
|
618
790
|
}
|
|
619
791
|
|
|
792
|
+
// 检查是否回复机器人
|
|
620
793
|
if (!shouldInject && config.triggerOnReply && session.quote) {
|
|
621
794
|
if (session.quote.userId === session.selfId) {
|
|
622
795
|
shouldInject = true;
|
|
796
|
+
injectReason = '回复机器人';
|
|
623
797
|
}
|
|
624
798
|
}
|
|
625
799
|
|
|
626
|
-
if (!shouldInject)
|
|
800
|
+
if (!shouldInject) {
|
|
801
|
+
log.middleware('无需注入上下文');
|
|
802
|
+
return next();
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
log.info(`触发上下文注入: ${injectReason}`);
|
|
627
806
|
|
|
628
807
|
const sessionInfo = generateSessionId(session);
|
|
629
808
|
const sessionId = sessionInfo.separateId || sessionInfo.sessionId;
|
|
630
809
|
|
|
631
810
|
const history = await getContext(sessionId);
|
|
632
811
|
|
|
633
|
-
if (history.length === 0)
|
|
812
|
+
if (history.length === 0) {
|
|
813
|
+
log.middleware('无历史记录可注入');
|
|
814
|
+
return next();
|
|
815
|
+
}
|
|
634
816
|
|
|
635
817
|
// 构建上下文
|
|
636
818
|
const contextLines = history.map(msg => {
|
|
@@ -818,16 +1000,15 @@ exports.apply = (ctx, config) => {
|
|
|
818
1000
|
memoryCache.clear();
|
|
819
1001
|
cacheTimestamps.clear();
|
|
820
1002
|
sessionLocks.clear();
|
|
821
|
-
log.success('插件卸载完成'
|
|
1003
|
+
log.success('插件卸载完成');
|
|
822
1004
|
});
|
|
823
1005
|
|
|
824
|
-
//
|
|
825
|
-
log.success('Group Memory
|
|
1006
|
+
// 最终确认日志
|
|
1007
|
+
log.success('✅ Group Memory 插件已完全加载,等待消息...');
|
|
1008
|
+
log.info('当前配置:', {
|
|
1009
|
+
recordPrivateChat: config.recordPrivateChat,
|
|
1010
|
+
recordGroupChat: config.recordGroupChat,
|
|
826
1011
|
logLevel: config.logLevel,
|
|
827
|
-
|
|
828
|
-
contextInject: config.logContextInject,
|
|
829
|
-
apiCalls: config.logApiCalls
|
|
1012
|
+
maxContext: config.maxContext
|
|
830
1013
|
});
|
|
831
|
-
|
|
832
|
-
logger.info(`Group Memory 插件已加载,日志级别: ${config.logLevel}`);
|
|
833
1014
|
};
|