koishi-plugin-chat-patch 2.2.0 → 2.4.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/client/icons/activity.vue +9 -9
- package/client/icons/index.ts +4 -4
- package/client/index.scss +90 -90
- package/client/index.ts +25 -25
- package/client/vue/composables/useChatActions.ts +1 -1
- package/client/vue/composables/useChatData.ts +1 -1
- package/client/vue/composables/useImageCache.ts +1 -1
- package/client/vue/types.ts +1 -1
- package/dist/index.js +2 -2
- package/lib/file-manager.d.ts +32 -0
- package/lib/index.js +417 -197
- package/lib/message-handler.d.ts +7 -5
- package/package.json +1 -1
- package/readme.md +25 -25
- package/src/api-handlers.ts +19 -65
- package/src/config.ts +45 -46
- package/src/file-manager.ts +367 -169
- package/src/index.ts +58 -31
- package/src/message-handler.ts +161 -134
- package/src/types.ts +62 -62
- package/src/utils.ts +4 -16
package/lib/index.js
CHANGED
|
@@ -54,7 +54,6 @@ var Utils = class {
|
|
|
54
54
|
static {
|
|
55
55
|
__name(this, "Utils");
|
|
56
56
|
}
|
|
57
|
-
// 检查平台是否被屏蔽
|
|
58
57
|
isPlatformBlocked(platform) {
|
|
59
58
|
if (!this.config.blockedPlatforms || this.config.blockedPlatforms.length === 0) {
|
|
60
59
|
return false;
|
|
@@ -72,7 +71,6 @@ var Utils = class {
|
|
|
72
71
|
}
|
|
73
72
|
return false;
|
|
74
73
|
}
|
|
75
|
-
// 递归提取所有文本内容的函数
|
|
76
74
|
extractTextContent(elements) {
|
|
77
75
|
let text = "";
|
|
78
76
|
for (const element of elements) {
|
|
@@ -88,7 +86,6 @@ var Utils = class {
|
|
|
88
86
|
}
|
|
89
87
|
return text;
|
|
90
88
|
}
|
|
91
|
-
// 检查字符串是否为base64格式
|
|
92
89
|
isBase64(str) {
|
|
93
90
|
if (!str || typeof str !== "string") return false;
|
|
94
91
|
if (str.startsWith("data:")) {
|
|
@@ -100,7 +97,6 @@ var Utils = class {
|
|
|
100
97
|
}
|
|
101
98
|
return false;
|
|
102
99
|
}
|
|
103
|
-
// 持久化 base64 图片并返回本地文件 URL
|
|
104
100
|
persistBase64Image(base64Data) {
|
|
105
101
|
if (!this.ctx || !base64Data.startsWith("data:image/")) return base64Data;
|
|
106
102
|
try {
|
|
@@ -127,8 +123,6 @@ var Utils = class {
|
|
|
127
123
|
} catch (e) {
|
|
128
124
|
}
|
|
129
125
|
}
|
|
130
|
-
// 清理对象中的base64内容,改为持久化存储
|
|
131
|
-
// isBotMessage: 是否为机器人发送的消息
|
|
132
126
|
cleanBase64Content(obj, isBotMessage = false) {
|
|
133
127
|
if (obj === null || obj === void 0) {
|
|
134
128
|
return obj;
|
|
@@ -184,95 +178,130 @@ var MessageHandler = class {
|
|
|
184
178
|
}
|
|
185
179
|
logger;
|
|
186
180
|
utils;
|
|
187
|
-
// 存储正确的 channelId 映射,key 是 selfId,value 是正确的 channelId
|
|
188
181
|
correctChannelIds = /* @__PURE__ */ new Map();
|
|
189
|
-
|
|
182
|
+
recordUserMessage(session, timestamp) {
|
|
183
|
+
setImmediate(() => {
|
|
184
|
+
this.processUserMessage(session, timestamp).catch((error) => {
|
|
185
|
+
this.logger.error("记录用户消息失败:", error);
|
|
186
|
+
});
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
recordBotMessage(session, timestamp) {
|
|
190
|
+
setImmediate(() => {
|
|
191
|
+
this.processBotMessage(session, timestamp).catch((error) => {
|
|
192
|
+
this.logger.error("记录机器人消息失败:", error);
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
}
|
|
190
196
|
setCorrectChannelId(selfId, channelId) {
|
|
191
197
|
this.correctChannelIds.set(selfId, channelId);
|
|
192
198
|
this.logInfo("设置正确的 channelId:", { selfId, channelId });
|
|
193
199
|
}
|
|
194
|
-
// 获取正确的 channelId
|
|
195
200
|
getCorrectChannelId(selfId) {
|
|
196
201
|
return this.correctChannelIds.get(selfId);
|
|
197
202
|
}
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
203
|
+
updateBotInfoToFile(session) {
|
|
204
|
+
setImmediate(() => {
|
|
205
|
+
try {
|
|
206
|
+
const data = this.fileManager.readChatDataFromFile();
|
|
207
|
+
const botInfo = {
|
|
208
|
+
selfId: session.selfId,
|
|
209
|
+
platform: session.platform || "unknown",
|
|
210
|
+
username: session.bot.user?.name || `Bot-${session.selfId}`,
|
|
211
|
+
avatar: session.bot.user?.avatar,
|
|
212
|
+
status: "online"
|
|
213
|
+
};
|
|
214
|
+
data.bots[session.selfId] = botInfo;
|
|
215
|
+
this.fileManager.writeChatDataToFile(data);
|
|
216
|
+
this.logInfo("更新机器人信息到文件:", botInfo.username);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
this.logger.error("更新机器人信息失败:", error);
|
|
219
|
+
}
|
|
220
|
+
});
|
|
211
221
|
}
|
|
212
|
-
|
|
213
|
-
async updateChannelInfoToFile(session) {
|
|
222
|
+
updateChannelInfoToFile(session) {
|
|
214
223
|
const isDirect = session.isDirect || session.channelId?.includes("private");
|
|
215
|
-
let guildName = session.channelId;
|
|
216
224
|
const directUserName = session.username || session.event?.user?.name || session.userId;
|
|
217
225
|
const data = this.fileManager.readChatDataFromFile();
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
if (session.guildId && session.bot.getGuild && typeof session.bot.getGuild === "function") {
|
|
221
|
-
const guild = await session.bot.getGuild(session.guildId);
|
|
222
|
-
guildName = guild?.name || session.channelId;
|
|
223
|
-
}
|
|
224
|
-
if (session.guildId && !session.bot.getGuild && session.bot.getChannel && typeof session.bot.getChannel === "function") {
|
|
225
|
-
try {
|
|
226
|
-
const channel = await session.bot.getChannel(session.guildId);
|
|
227
|
-
guildName = channel?.name || session.channelId;
|
|
228
|
-
} catch (channelError) {
|
|
229
|
-
this.logInfo("获取频道信息失败,使用频道ID作为备用:", channelError);
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
} catch (error) {
|
|
233
|
-
this.logInfo("获取频道信息失败,使用频道ID作为备用:", error);
|
|
234
|
-
guildName = session.channelId;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
if (!data.channels[session.selfId]) {
|
|
238
|
-
data.channels[session.selfId] = {};
|
|
239
|
-
}
|
|
240
|
-
const existingChannel = data.channels[session.selfId][session.channelId];
|
|
241
|
-
let finalName;
|
|
226
|
+
const existingChannel = data.channels[session.selfId]?.[session.channelId];
|
|
227
|
+
let immediateName = session.channelId;
|
|
242
228
|
if (isDirect) {
|
|
243
229
|
if (directUserName && directUserName !== session.userId) {
|
|
244
|
-
|
|
230
|
+
immediateName = `私聊(${directUserName})`;
|
|
245
231
|
} else if (existingChannel?.name && !existingChannel.name.includes("未知")) {
|
|
246
|
-
|
|
232
|
+
immediateName = existingChannel.name;
|
|
247
233
|
} else if (session.platform && session.platform.toLowerCase().includes("sandbox")) {
|
|
248
|
-
|
|
234
|
+
immediateName = `私聊(${session.userId})`;
|
|
249
235
|
} else {
|
|
250
|
-
|
|
236
|
+
immediateName = "私聊(未知用户)";
|
|
251
237
|
}
|
|
252
|
-
} else {
|
|
253
|
-
|
|
238
|
+
} else if (existingChannel?.guildName) {
|
|
239
|
+
immediateName = existingChannel.guildName;
|
|
254
240
|
}
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
241
|
+
setImmediate(async () => {
|
|
242
|
+
try {
|
|
243
|
+
let guildName = session.channelId;
|
|
244
|
+
if (!isDirect) {
|
|
245
|
+
try {
|
|
246
|
+
if (session.guildId && session.bot.getGuild && typeof session.bot.getGuild === "function") {
|
|
247
|
+
const guild = await session.bot.getGuild(session.guildId);
|
|
248
|
+
guildName = guild?.name || session.channelId;
|
|
249
|
+
} else if (session.guildId && !session.bot.getGuild && session.bot.getChannel && typeof session.bot.getChannel === "function") {
|
|
250
|
+
try {
|
|
251
|
+
const channel = await session.bot.getChannel(session.guildId);
|
|
252
|
+
guildName = channel?.name || session.channelId;
|
|
253
|
+
} catch (channelError) {
|
|
254
|
+
this.logInfo("获取频道信息失败,使用频道ID作为备用:", channelError);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
} catch (error) {
|
|
258
|
+
this.logInfo("获取频道信息失败,使用频道ID作为备用:", error);
|
|
259
|
+
guildName = session.channelId;
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
const freshData = this.fileManager.readChatDataFromFile();
|
|
263
|
+
if (!freshData.channels[session.selfId]) {
|
|
264
|
+
freshData.channels[session.selfId] = {};
|
|
265
|
+
}
|
|
266
|
+
const existingChannel2 = freshData.channels[session.selfId][session.channelId];
|
|
267
|
+
let finalName;
|
|
268
|
+
if (isDirect) {
|
|
269
|
+
if (directUserName && directUserName !== session.userId) {
|
|
270
|
+
finalName = `私聊(${directUserName})`;
|
|
271
|
+
} else if (existingChannel2?.name && !existingChannel2.name.includes("未知")) {
|
|
272
|
+
finalName = existingChannel2.name;
|
|
273
|
+
} else if (session.platform && session.platform.toLowerCase().includes("sandbox")) {
|
|
274
|
+
finalName = `私聊(${session.userId})`;
|
|
275
|
+
} else {
|
|
276
|
+
finalName = "私聊(未知用户)";
|
|
277
|
+
}
|
|
278
|
+
} else {
|
|
279
|
+
finalName = guildName || session.channelId;
|
|
280
|
+
}
|
|
281
|
+
const channelInfo = {
|
|
282
|
+
id: session.channelId,
|
|
283
|
+
name: finalName,
|
|
284
|
+
type: session.type || 0,
|
|
285
|
+
channelId: session.channelId,
|
|
286
|
+
guildName,
|
|
287
|
+
isDirect: !!isDirect
|
|
288
|
+
};
|
|
289
|
+
if (existingChannel2 && existingChannel2.name !== finalName) {
|
|
290
|
+
this.logInfo("更新频道名称:", {
|
|
291
|
+
channelId: session.channelId,
|
|
292
|
+
oldName: existingChannel2.name,
|
|
293
|
+
newName: finalName
|
|
294
|
+
});
|
|
295
|
+
}
|
|
296
|
+
freshData.channels[session.selfId][session.channelId] = channelInfo;
|
|
297
|
+
this.fileManager.writeChatDataToFile(freshData);
|
|
298
|
+
this.logInfo("更新频道信息到文件:", channelInfo.name);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
this.logger.error("异步更新频道信息失败:", error);
|
|
301
|
+
}
|
|
302
|
+
});
|
|
303
|
+
return immediateName;
|
|
274
304
|
}
|
|
275
|
-
// 下载并缓存媒体文件
|
|
276
305
|
async downloadAndCacheMedia(url, type) {
|
|
277
306
|
try {
|
|
278
307
|
if (!url || url.startsWith("data:") || url.startsWith("file:")) return url;
|
|
@@ -298,37 +327,44 @@ var MessageHandler = class {
|
|
|
298
327
|
return url;
|
|
299
328
|
}
|
|
300
329
|
}
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
330
|
+
processMediaElementsAsync(elements, isUserMessage = true) {
|
|
331
|
+
if (!elements) return;
|
|
332
|
+
setImmediate(async () => {
|
|
333
|
+
try {
|
|
334
|
+
for (const el of elements) {
|
|
335
|
+
if (["image", "img", "mface"].includes(el.type)) {
|
|
336
|
+
const src = el.attrs.src || el.attrs.url || el.attrs.file;
|
|
337
|
+
if (src && isUserMessage) {
|
|
338
|
+
this.downloadAndCacheMedia(src, "image").catch((e) => {
|
|
339
|
+
this.logger.warn("缓存图片失败:", e);
|
|
340
|
+
});
|
|
341
|
+
}
|
|
342
|
+
} else if (el.type === "audio") {
|
|
343
|
+
const src = el.attrs.src || el.attrs.url || el.attrs.file;
|
|
344
|
+
if (src && isUserMessage) {
|
|
345
|
+
this.downloadAndCacheMedia(src, "media").catch((e) => {
|
|
346
|
+
this.logger.warn("缓存语音失败:", e);
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
if (el.children) this.processMediaElementsAsync(el.children, isUserMessage);
|
|
315
351
|
}
|
|
352
|
+
} catch (error) {
|
|
353
|
+
this.logger.error("处理媒体元素失败:", error);
|
|
316
354
|
}
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
return elements;
|
|
355
|
+
});
|
|
320
356
|
}
|
|
321
|
-
async
|
|
357
|
+
async processUserMessage(session, timestamp) {
|
|
322
358
|
try {
|
|
323
|
-
|
|
324
|
-
|
|
359
|
+
if (!timestamp) timestamp = Date.now();
|
|
360
|
+
this.updateBotInfoToFile(session);
|
|
361
|
+
const guildName = this.updateChannelInfoToFile(session);
|
|
325
362
|
const isDirect = session.isDirect || session.channelId?.includes("private");
|
|
326
|
-
const timestamp = Date.now();
|
|
327
363
|
if (session.elements) {
|
|
328
|
-
|
|
364
|
+
this.processMediaElementsAsync(session.elements, true);
|
|
329
365
|
}
|
|
330
366
|
if (session.quote?.elements) {
|
|
331
|
-
|
|
367
|
+
this.processMediaElementsAsync(session.quote.elements, true);
|
|
332
368
|
}
|
|
333
369
|
let quoteInfo = void 0;
|
|
334
370
|
if (session.quote) {
|
|
@@ -400,18 +436,17 @@ var MessageHandler = class {
|
|
|
400
436
|
};
|
|
401
437
|
this.ctx.console.broadcast("chat-message-event", messageEvent);
|
|
402
438
|
} catch (error) {
|
|
403
|
-
this.logger.error("
|
|
439
|
+
this.logger.error("处理用户消息失败:", error);
|
|
404
440
|
}
|
|
405
441
|
}
|
|
406
|
-
|
|
407
|
-
async broadcastBotMessageEvent(session) {
|
|
442
|
+
async processBotMessage(session, timestamp) {
|
|
408
443
|
try {
|
|
444
|
+
if (!timestamp) timestamp = Date.now();
|
|
409
445
|
const correctChannelId = this.getCorrectChannelId(session.selfId);
|
|
410
446
|
const finalChannelId = correctChannelId || session.channelId;
|
|
411
|
-
|
|
412
|
-
const guildName =
|
|
447
|
+
this.updateBotInfoToFile(session);
|
|
448
|
+
const guildName = this.updateChannelInfoToFile(session);
|
|
413
449
|
const isDirect = session.isDirect || finalChannelId?.includes("private");
|
|
414
|
-
const timestamp = Date.now();
|
|
415
450
|
let content = session.content || "";
|
|
416
451
|
if (!content && session.event?.message?.elements) {
|
|
417
452
|
content = this.utils.extractTextContent(session.event.message.elements).trim();
|
|
@@ -488,7 +523,7 @@ var MessageHandler = class {
|
|
|
488
523
|
};
|
|
489
524
|
this.ctx.console.broadcast("chat-bot-message-event", messageEvent);
|
|
490
525
|
} catch (error) {
|
|
491
|
-
this.logger.error("
|
|
526
|
+
this.logger.error("处理机器人消息失败:", error);
|
|
492
527
|
}
|
|
493
528
|
}
|
|
494
529
|
logInfo(...args) {
|
|
@@ -505,62 +540,237 @@ var FileManager = class {
|
|
|
505
540
|
constructor(ctx, config) {
|
|
506
541
|
this.ctx = ctx;
|
|
507
542
|
this.config = config;
|
|
508
|
-
|
|
543
|
+
const baseDir = import_node_path2.default.resolve(ctx.baseDir, "data", "chat-patch");
|
|
544
|
+
this.chatHistoryDir = import_node_path2.default.join(baseDir, "chat-history");
|
|
545
|
+
this.metadataFilePath = import_node_path2.default.join(baseDir, "metadata.json");
|
|
509
546
|
this.logger = ctx.logger("chat-patch");
|
|
510
547
|
this.utils = new Utils(config);
|
|
548
|
+
this.cleanupOldDataFiles(baseDir);
|
|
549
|
+
this.memoryCache = this.readChatDataFromFile();
|
|
511
550
|
}
|
|
512
551
|
static {
|
|
513
552
|
__name(this, "FileManager");
|
|
514
553
|
}
|
|
515
|
-
|
|
516
|
-
|
|
554
|
+
chatHistoryDir;
|
|
555
|
+
// 聊天记录根目录
|
|
556
|
+
metadataFilePath;
|
|
557
|
+
// 元数据文件路径(存储bots、channels、pinned等信息)
|
|
517
558
|
logger;
|
|
518
559
|
utils;
|
|
560
|
+
memoryCache = null;
|
|
561
|
+
pendingMessages = /* @__PURE__ */ new Map();
|
|
562
|
+
// 按channelKey分组的待写入消息
|
|
563
|
+
writeTimers = /* @__PURE__ */ new Map();
|
|
564
|
+
// 每个频道独立的写入定时器
|
|
565
|
+
WRITE_DEBOUNCE_MS = 1e3;
|
|
566
|
+
// 清理旧版本的JSON文件
|
|
567
|
+
cleanupOldDataFiles(baseDir) {
|
|
568
|
+
const oldFiles = ["chat-data.json", "data.json", "messages.json"];
|
|
569
|
+
for (const fileName of oldFiles) {
|
|
570
|
+
const filePath = import_node_path2.default.join(baseDir, fileName);
|
|
571
|
+
if (import_node_fs2.default.existsSync(filePath)) {
|
|
572
|
+
try {
|
|
573
|
+
import_node_fs2.default.unlinkSync(filePath);
|
|
574
|
+
this.logger.info(`已删除旧版本数据文件: ${fileName}`);
|
|
575
|
+
} catch (error) {
|
|
576
|
+
this.logger.warn(`删除旧版本数据文件失败: ${fileName}`, error);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
519
581
|
// 确保目录存在
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
import_node_fs2.default.mkdirSync(dir, { recursive: true });
|
|
582
|
+
ensureDir(dirPath) {
|
|
583
|
+
if (!import_node_fs2.default.existsSync(dirPath)) {
|
|
584
|
+
import_node_fs2.default.mkdirSync(dirPath, { recursive: true });
|
|
524
585
|
}
|
|
525
586
|
}
|
|
526
|
-
//
|
|
527
|
-
|
|
587
|
+
// 获取频道消息文件路径
|
|
588
|
+
getChannelFilePath(selfId, channelId) {
|
|
589
|
+
const botDir = import_node_path2.default.join(this.chatHistoryDir, selfId);
|
|
590
|
+
this.ensureDir(botDir);
|
|
591
|
+
return import_node_path2.default.join(botDir, `${channelId}.json`);
|
|
592
|
+
}
|
|
593
|
+
// 读取单个频道的消息
|
|
594
|
+
readChannelMessages(selfId, channelId) {
|
|
595
|
+
const filePath = this.getChannelFilePath(selfId, channelId);
|
|
596
|
+
if (!import_node_fs2.default.existsSync(filePath)) {
|
|
597
|
+
return [];
|
|
598
|
+
}
|
|
528
599
|
try {
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
600
|
+
const jsonData = import_node_fs2.default.readFileSync(filePath, "utf8");
|
|
601
|
+
const messages = JSON.parse(jsonData);
|
|
602
|
+
return Array.isArray(messages) ? messages : [];
|
|
603
|
+
} catch (error) {
|
|
604
|
+
this.logger.error(`读取频道消息失败 [${selfId}:${channelId}]:`, error);
|
|
605
|
+
return [];
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
// 写入单个频道的消息
|
|
609
|
+
writeChannelMessages(selfId, channelId, messages) {
|
|
610
|
+
const filePath = this.getChannelFilePath(selfId, channelId);
|
|
611
|
+
try {
|
|
612
|
+
const jsonData = JSON.stringify(messages, null, 2);
|
|
613
|
+
import_node_fs2.default.writeFileSync(filePath, jsonData, "utf8");
|
|
614
|
+
} catch (error) {
|
|
615
|
+
this.logger.error(`写入频道消息失败 [${selfId}:${channelId}]:`, error);
|
|
616
|
+
}
|
|
617
|
+
}
|
|
618
|
+
// 读取元数据(bots、channels、pinned等)
|
|
619
|
+
readMetadata() {
|
|
620
|
+
if (!import_node_fs2.default.existsSync(this.metadataFilePath)) {
|
|
621
|
+
return {
|
|
622
|
+
bots: {},
|
|
623
|
+
channels: {},
|
|
624
|
+
pinnedBots: [],
|
|
625
|
+
pinnedChannels: []
|
|
626
|
+
};
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
const jsonData = import_node_fs2.default.readFileSync(this.metadataFilePath, "utf8");
|
|
630
|
+
const data = JSON.parse(jsonData);
|
|
631
|
+
return {
|
|
632
|
+
bots: data.bots || {},
|
|
633
|
+
channels: data.channels || {},
|
|
634
|
+
pinnedBots: data.pinnedBots || [],
|
|
635
|
+
pinnedChannels: data.pinnedChannels || [],
|
|
636
|
+
lastSaveTime: data.lastSaveTime
|
|
637
|
+
};
|
|
638
|
+
} catch (error) {
|
|
639
|
+
this.logger.error("读取元数据失败:", error);
|
|
640
|
+
return {
|
|
641
|
+
bots: {},
|
|
642
|
+
channels: {},
|
|
643
|
+
pinnedBots: [],
|
|
644
|
+
pinnedChannels: []
|
|
645
|
+
};
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
// 写入元数据
|
|
649
|
+
writeMetadata(metadata) {
|
|
650
|
+
try {
|
|
651
|
+
this.ensureDir(import_node_path2.default.dirname(this.metadataFilePath));
|
|
652
|
+
const dataToWrite = {
|
|
653
|
+
...metadata,
|
|
654
|
+
lastSaveTime: Date.now()
|
|
655
|
+
};
|
|
656
|
+
const jsonData = JSON.stringify(dataToWrite, null, 2);
|
|
657
|
+
import_node_fs2.default.writeFileSync(this.metadataFilePath, jsonData, "utf8");
|
|
658
|
+
} catch (error) {
|
|
659
|
+
this.logger.error("写入元数据失败:", error);
|
|
660
|
+
}
|
|
661
|
+
}
|
|
662
|
+
// 扫描所有频道消息文件并加载到内存
|
|
663
|
+
loadAllChannelMessages() {
|
|
664
|
+
const messages = {};
|
|
665
|
+
if (!import_node_fs2.default.existsSync(this.chatHistoryDir)) {
|
|
666
|
+
return messages;
|
|
667
|
+
}
|
|
668
|
+
try {
|
|
669
|
+
const botDirs = import_node_fs2.default.readdirSync(this.chatHistoryDir);
|
|
670
|
+
for (const botId of botDirs) {
|
|
671
|
+
const botDir = import_node_path2.default.join(this.chatHistoryDir, botId);
|
|
672
|
+
const stat = import_node_fs2.default.statSync(botDir);
|
|
673
|
+
if (!stat.isDirectory()) continue;
|
|
674
|
+
const channelFiles = import_node_fs2.default.readdirSync(botDir);
|
|
675
|
+
for (const fileName of channelFiles) {
|
|
676
|
+
if (!fileName.endsWith(".json")) continue;
|
|
677
|
+
const channelId = fileName.replace(".json", "");
|
|
678
|
+
const channelKey = `${botId}:${channelId}`;
|
|
679
|
+
messages[channelKey] = this.readChannelMessages(botId, channelId);
|
|
680
|
+
}
|
|
540
681
|
}
|
|
541
682
|
} catch (error) {
|
|
542
|
-
this.logger.error("
|
|
683
|
+
this.logger.error("加载频道消息失败:", error);
|
|
543
684
|
}
|
|
544
|
-
return
|
|
685
|
+
return messages;
|
|
686
|
+
}
|
|
687
|
+
readChatDataFromFile() {
|
|
688
|
+
if (this.memoryCache) {
|
|
689
|
+
return this.memoryCache;
|
|
690
|
+
}
|
|
691
|
+
process.nextTick(() => {
|
|
692
|
+
try {
|
|
693
|
+
const metadata = this.readMetadata();
|
|
694
|
+
const messages = this.loadAllChannelMessages();
|
|
695
|
+
this.memoryCache = {
|
|
696
|
+
...metadata,
|
|
697
|
+
messages
|
|
698
|
+
};
|
|
699
|
+
} catch (error) {
|
|
700
|
+
this.logger.error("读取聊天数据失败:", error);
|
|
701
|
+
}
|
|
702
|
+
});
|
|
703
|
+
this.memoryCache = {
|
|
545
704
|
bots: {},
|
|
546
705
|
channels: {},
|
|
547
706
|
messages: {},
|
|
548
707
|
pinnedBots: [],
|
|
549
708
|
pinnedChannels: []
|
|
550
709
|
};
|
|
710
|
+
return this.memoryCache;
|
|
551
711
|
}
|
|
552
|
-
// 写入数据到JSON文件
|
|
553
712
|
writeChatDataToFile(data) {
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
713
|
+
data.lastSaveTime = Date.now();
|
|
714
|
+
this.memoryCache = data;
|
|
715
|
+
process.nextTick(() => {
|
|
716
|
+
try {
|
|
717
|
+
const { messages, ...metadata } = data;
|
|
718
|
+
this.writeMetadata(metadata);
|
|
719
|
+
for (const [channelKey, channelMessages] of Object.entries(messages)) {
|
|
720
|
+
const [selfId, channelId] = channelKey.split(":");
|
|
721
|
+
if (selfId && channelId) {
|
|
722
|
+
this.writeChannelMessages(selfId, channelId, channelMessages);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
} catch (error) {
|
|
726
|
+
this.logger.error("写入聊天数据失败:", error);
|
|
727
|
+
}
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
// 为特定频道安排写入
|
|
731
|
+
scheduleWrite(channelKey) {
|
|
732
|
+
const existingTimer = this.writeTimers.get(channelKey);
|
|
733
|
+
if (existingTimer) {
|
|
734
|
+
existingTimer();
|
|
561
735
|
}
|
|
736
|
+
const timer = this.ctx.setTimeout(() => {
|
|
737
|
+
process.nextTick(() => {
|
|
738
|
+
this.flushPendingMessages(channelKey);
|
|
739
|
+
});
|
|
740
|
+
this.writeTimers.delete(channelKey);
|
|
741
|
+
}, this.WRITE_DEBOUNCE_MS);
|
|
742
|
+
this.writeTimers.set(channelKey, timer);
|
|
743
|
+
}
|
|
744
|
+
// 刷新特定频道的待写入消息
|
|
745
|
+
flushPendingMessages(channelKey) {
|
|
746
|
+
const messagesToWrite = this.pendingMessages.get(channelKey);
|
|
747
|
+
if (!messagesToWrite || messagesToWrite.length === 0) return;
|
|
748
|
+
this.pendingMessages.delete(channelKey);
|
|
749
|
+
const data = this.memoryCache || this.readChatDataFromFile();
|
|
750
|
+
const [selfId, channelId] = channelKey.split(":");
|
|
751
|
+
if (!selfId || !channelId) return;
|
|
752
|
+
if (!data.messages[channelKey]) {
|
|
753
|
+
data.messages[channelKey] = [];
|
|
754
|
+
}
|
|
755
|
+
for (const messageInfo of messagesToWrite) {
|
|
756
|
+
const existingMessage = data.messages[channelKey].find((m) => m.id === messageInfo.id);
|
|
757
|
+
if (existingMessage) {
|
|
758
|
+
continue;
|
|
759
|
+
}
|
|
760
|
+
if (!messageInfo.timestamp) {
|
|
761
|
+
messageInfo.timestamp = Date.now();
|
|
762
|
+
}
|
|
763
|
+
const cleanedMessageInfo = this.utils.cleanBase64Content(messageInfo);
|
|
764
|
+
data.messages[channelKey].push(cleanedMessageInfo);
|
|
765
|
+
}
|
|
766
|
+
if (data.messages[channelKey].length > this.config.maxMessagesPerChannel) {
|
|
767
|
+
data.messages[channelKey].sort((a, b) => a.timestamp - b.timestamp);
|
|
768
|
+
data.messages[channelKey] = data.messages[channelKey].slice(-this.config.maxMessagesPerChannel);
|
|
769
|
+
}
|
|
770
|
+
this.writeChannelMessages(selfId, channelId, data.messages[channelKey]);
|
|
771
|
+
this.memoryCache = data;
|
|
772
|
+
this.logInfo(`批量写入 ${messagesToWrite.length} 条消息到频道 ${channelKey}`);
|
|
562
773
|
}
|
|
563
|
-
// 清理超量消息
|
|
564
774
|
cleanExcessMessages(data) {
|
|
565
775
|
let cleanedCount = 0;
|
|
566
776
|
const cleanedMessages = {};
|
|
@@ -583,56 +793,37 @@ var FileManager = class {
|
|
|
583
793
|
messages: cleanedMessages
|
|
584
794
|
};
|
|
585
795
|
}
|
|
586
|
-
// 添加消息到JSON文件(使用锁机制防止并发冲突)
|
|
587
796
|
async addMessageToFile(messageInfo) {
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
existingType: existingMessage.type,
|
|
600
|
-
existingContent: existingMessage.content,
|
|
601
|
-
newType: messageInfo.type,
|
|
602
|
-
newContent: messageInfo.content
|
|
603
|
-
});
|
|
604
|
-
return;
|
|
605
|
-
}
|
|
797
|
+
const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`;
|
|
798
|
+
if (!this.pendingMessages.has(channelKey)) {
|
|
799
|
+
this.pendingMessages.set(channelKey, []);
|
|
800
|
+
}
|
|
801
|
+
this.pendingMessages.get(channelKey).push(messageInfo);
|
|
802
|
+
const data = this.memoryCache || this.readChatDataFromFile();
|
|
803
|
+
if (!data.messages[channelKey]) {
|
|
804
|
+
data.messages[channelKey] = [];
|
|
805
|
+
}
|
|
806
|
+
const existingMessage = data.messages[channelKey].find((m) => m.id === messageInfo.id);
|
|
807
|
+
if (!existingMessage) {
|
|
606
808
|
if (!messageInfo.timestamp) {
|
|
607
809
|
messageInfo.timestamp = Date.now();
|
|
608
810
|
}
|
|
609
811
|
const cleanedMessageInfo = this.utils.cleanBase64Content(messageInfo);
|
|
610
|
-
const beforeCount = data.messages[channelKey].length;
|
|
611
812
|
data.messages[channelKey].push(cleanedMessageInfo);
|
|
612
|
-
const afterCount = data.messages[channelKey].length;
|
|
613
813
|
if (data.messages[channelKey].length > this.config.maxMessagesPerChannel) {
|
|
614
814
|
data.messages[channelKey].sort((a, b) => a.timestamp - b.timestamp);
|
|
615
|
-
const removedCount = data.messages[channelKey].length - this.config.maxMessagesPerChannel;
|
|
616
815
|
data.messages[channelKey] = data.messages[channelKey].slice(-this.config.maxMessagesPerChannel);
|
|
617
|
-
this.logInfo(`频道 ${channelKey} 达到消息上限,清理了 ${removedCount} 条旧消息`);
|
|
618
816
|
}
|
|
619
|
-
this.
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
isCommandMessage,
|
|
630
|
-
消息数变化: `${beforeCount} -> ${afterCount} -> ${data.messages[channelKey].length}`
|
|
631
|
-
});
|
|
632
|
-
}).catch((error) => {
|
|
633
|
-
this.logger.error("保存消息时发生错误:", error);
|
|
634
|
-
});
|
|
635
|
-
await this.fileOperationLock;
|
|
817
|
+
this.memoryCache = data;
|
|
818
|
+
}
|
|
819
|
+
this.scheduleWrite(channelKey);
|
|
820
|
+
}
|
|
821
|
+
dispose() {
|
|
822
|
+
for (const [channelKey, timer] of this.writeTimers.entries()) {
|
|
823
|
+
timer();
|
|
824
|
+
this.flushPendingMessages(channelKey);
|
|
825
|
+
}
|
|
826
|
+
this.writeTimers.clear();
|
|
636
827
|
}
|
|
637
828
|
logInfo(...args) {
|
|
638
829
|
if (this.config.loggerinfo) {
|
|
@@ -658,7 +849,6 @@ var ApiHandlers = class {
|
|
|
658
849
|
__name(this, "ApiHandlers");
|
|
659
850
|
}
|
|
660
851
|
logger;
|
|
661
|
-
// 当前临时缓存的视频文件路径
|
|
662
852
|
currentTempVideo = null;
|
|
663
853
|
registerApiHandlers() {
|
|
664
854
|
this.ctx.console.addListener("clear-all-indexeddb-data", async () => {
|
|
@@ -681,7 +871,6 @@ var ApiHandlers = class {
|
|
|
681
871
|
channels: data.channels || {},
|
|
682
872
|
pinnedBots: data.pinnedBots || [],
|
|
683
873
|
pinnedChannels: data.pinnedChannels || [],
|
|
684
|
-
// 不返回 messages,由前端按需拉取
|
|
685
874
|
messages: {}
|
|
686
875
|
}
|
|
687
876
|
};
|
|
@@ -1118,7 +1307,6 @@ var ApiHandlers = class {
|
|
|
1118
1307
|
}
|
|
1119
1308
|
});
|
|
1120
1309
|
}
|
|
1121
|
-
// 检查是否为文件 URL
|
|
1122
1310
|
isFileUrl(url) {
|
|
1123
1311
|
try {
|
|
1124
1312
|
const parsedUrl = new import_node_url2.URL(url);
|
|
@@ -1127,7 +1315,6 @@ var ApiHandlers = class {
|
|
|
1127
1315
|
return false;
|
|
1128
1316
|
}
|
|
1129
1317
|
}
|
|
1130
|
-
// 创建文件 URL
|
|
1131
1318
|
createFileUrl(filePath) {
|
|
1132
1319
|
try {
|
|
1133
1320
|
return (0, import_node_url2.pathToFileURL)(filePath).href;
|
|
@@ -1136,7 +1323,6 @@ var ApiHandlers = class {
|
|
|
1136
1323
|
return `file://${filePath}`;
|
|
1137
1324
|
}
|
|
1138
1325
|
}
|
|
1139
|
-
// 处理本地文件请求
|
|
1140
1326
|
async handleLocalFileRequest(fileUrl) {
|
|
1141
1327
|
try {
|
|
1142
1328
|
const filePath = (0, import_node_url2.fileURLToPath)(fileUrl);
|
|
@@ -1158,13 +1344,11 @@ var ApiHandlers = class {
|
|
|
1158
1344
|
};
|
|
1159
1345
|
}
|
|
1160
1346
|
}
|
|
1161
|
-
// 设置定时清理临时文件
|
|
1162
1347
|
setupTempFileCleanup() {
|
|
1163
|
-
setInterval(() => {
|
|
1348
|
+
this.ctx.setInterval(() => {
|
|
1164
1349
|
this.cleanupMediaCache();
|
|
1165
1350
|
}, 5 * 60 * 1e3);
|
|
1166
1351
|
}
|
|
1167
|
-
// 统一清理媒体缓存
|
|
1168
1352
|
async cleanupMediaCache() {
|
|
1169
1353
|
const baseDir = this.ctx.baseDir + "/data/chat-patch/persist-media";
|
|
1170
1354
|
if (!require("node:fs").existsSync(baseDir)) return;
|
|
@@ -1291,23 +1475,55 @@ async function apply(ctx, config) {
|
|
|
1291
1475
|
消息频道数: Object.keys(cleanedData.messages).length,
|
|
1292
1476
|
总消息数: Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
1293
1477
|
});
|
|
1294
|
-
ctx.on("message",
|
|
1295
|
-
if (utils.isPlatformBlocked(session.platform || "unknown"))
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1478
|
+
ctx.on("message", (session) => {
|
|
1479
|
+
if (utils.isPlatformBlocked(session.platform || "unknown")) return;
|
|
1480
|
+
const timestamp = Date.now();
|
|
1481
|
+
ctx.console.broadcast("chat-message-event", {
|
|
1482
|
+
type: "message",
|
|
1483
|
+
selfId: session.selfId,
|
|
1484
|
+
platform: session.platform || "unknown",
|
|
1485
|
+
channelId: session.channelId,
|
|
1486
|
+
messageId: session.event?.message?.id || `msg-${timestamp}`,
|
|
1487
|
+
content: session.content || "",
|
|
1488
|
+
userId: session.userId || "unknown",
|
|
1489
|
+
username: session.username || session.userId || "unknown",
|
|
1490
|
+
avatar: session.event?.user?.avatar,
|
|
1491
|
+
timestamp,
|
|
1492
|
+
isDirect: session.isDirect,
|
|
1493
|
+
elements: utils.cleanBase64Content(session.elements, false),
|
|
1494
|
+
bot: {
|
|
1495
|
+
avatar: session.bot.user?.avatar,
|
|
1496
|
+
name: session.bot.user?.name
|
|
1497
|
+
}
|
|
1498
|
+
});
|
|
1499
|
+
messageHandler.recordUserMessage(session, timestamp);
|
|
1300
1500
|
});
|
|
1301
|
-
ctx.on("before-send",
|
|
1302
|
-
if (utils.isPlatformBlocked(session.platform || "unknown"))
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1501
|
+
ctx.on("before-send", (session) => {
|
|
1502
|
+
if (utils.isPlatformBlocked(session.platform || "unknown")) return;
|
|
1503
|
+
const timestamp = Date.now();
|
|
1504
|
+
ctx.console.broadcast("chat-bot-message-event", {
|
|
1505
|
+
type: "bot-message",
|
|
1506
|
+
selfId: session.selfId,
|
|
1507
|
+
platform: session.platform || "unknown",
|
|
1508
|
+
channelId: session.channelId,
|
|
1509
|
+
messageId: `bot-msg-${timestamp}`,
|
|
1510
|
+
content: session.content || "",
|
|
1511
|
+
userId: session.selfId,
|
|
1512
|
+
username: session.bot.user?.name || `Bot-${session.selfId}`,
|
|
1513
|
+
avatar: session.bot.user?.avatar,
|
|
1514
|
+
timestamp,
|
|
1515
|
+
sending: true,
|
|
1516
|
+
elements: utils.cleanBase64Content(session.event?.message?.elements, true),
|
|
1517
|
+
bot: {
|
|
1518
|
+
avatar: session.bot.user?.avatar,
|
|
1519
|
+
name: session.bot.user?.name
|
|
1520
|
+
}
|
|
1521
|
+
});
|
|
1522
|
+
messageHandler.recordBotMessage(session, timestamp);
|
|
1307
1523
|
});
|
|
1308
1524
|
ctx.on("ready", async () => {
|
|
1309
1525
|
logInfo("插件启动完成,开始监听消息");
|
|
1310
|
-
setInterval(() => {
|
|
1526
|
+
ctx.setInterval(() => {
|
|
1311
1527
|
const data = fileManager.readChatDataFromFile();
|
|
1312
1528
|
const cleanedData2 = fileManager.cleanExcessMessages(data);
|
|
1313
1529
|
const originalCount2 = Object.values(data.messages).reduce((total, msgs) => total + msgs.length, 0);
|
|
@@ -1323,6 +1539,10 @@ async function apply(ctx, config) {
|
|
|
1323
1539
|
dev: import_node_path3.default.resolve(__dirname, "../client/index.ts"),
|
|
1324
1540
|
prod: import_node_path3.default.resolve(__dirname, "../dist")
|
|
1325
1541
|
});
|
|
1542
|
+
ctx.on("dispose", () => {
|
|
1543
|
+
fileManager.dispose();
|
|
1544
|
+
logInfo("插件已卸载,所有待处理的消息已写入");
|
|
1545
|
+
});
|
|
1326
1546
|
}
|
|
1327
1547
|
__name(apply, "apply");
|
|
1328
1548
|
// Annotate the CommonJS export names for ESM import in node:
|