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/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
- // 设置正确的 channelId
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
- // 更新机器人信息到JSON文件
199
- async updateBotInfoToFile(session) {
200
- const data = this.fileManager.readChatDataFromFile();
201
- const botInfo = {
202
- selfId: session.selfId,
203
- platform: session.platform || "unknown",
204
- username: session.bot.user?.name || `Bot-${session.selfId}`,
205
- avatar: session.bot.user?.avatar,
206
- status: "online"
207
- };
208
- data.bots[session.selfId] = botInfo;
209
- this.fileManager.writeChatDataToFile(data);
210
- this.logInfo("更新机器人信息到文件:", botInfo.username);
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
- // 更新频道信息到JSON文件
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
- if (!isDirect) {
219
- try {
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
- finalName = `私聊(${directUserName})`;
230
+ immediateName = `私聊(${directUserName})`;
245
231
  } else if (existingChannel?.name && !existingChannel.name.includes("未知")) {
246
- finalName = existingChannel.name;
232
+ immediateName = existingChannel.name;
247
233
  } else if (session.platform && session.platform.toLowerCase().includes("sandbox")) {
248
- finalName = `私聊(${session.userId})`;
234
+ immediateName = `私聊(${session.userId})`;
249
235
  } else {
250
- finalName = "私聊(未知用户)";
236
+ immediateName = "私聊(未知用户)";
251
237
  }
252
- } else {
253
- finalName = guildName || session.channelId;
238
+ } else if (existingChannel?.guildName) {
239
+ immediateName = existingChannel.guildName;
254
240
  }
255
- const channelInfo = {
256
- id: session.channelId,
257
- name: finalName,
258
- type: session.type || 0,
259
- channelId: session.channelId,
260
- guildName,
261
- isDirect: !!isDirect
262
- };
263
- if (existingChannel && existingChannel.name !== finalName) {
264
- this.logInfo("更新频道名称:", {
265
- channelId: session.channelId,
266
- oldName: existingChannel.name,
267
- newName: finalName
268
- });
269
- }
270
- data.channels[session.selfId][session.channelId] = channelInfo;
271
- this.fileManager.writeChatDataToFile(data);
272
- this.logInfo("更新频道信息到文件:", channelInfo.name);
273
- return guildName;
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
- async processMediaElements(elements, isUserMessage = true) {
304
- if (!elements) return elements;
305
- for (const el of elements) {
306
- if (["image", "img", "mface"].includes(el.type)) {
307
- const src = el.attrs.src || el.attrs.url || el.attrs.file;
308
- if (src && isUserMessage) {
309
- el.attrs.src = await this.downloadAndCacheMedia(src, "image");
310
- }
311
- } else if (el.type === "audio") {
312
- const src = el.attrs.src || el.attrs.url || el.attrs.file;
313
- if (src && isUserMessage) {
314
- el.attrs.src = await this.downloadAndCacheMedia(src, "media");
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
- if (el.children) await this.processMediaElements(el.children, isUserMessage);
318
- }
319
- return elements;
355
+ });
320
356
  }
321
- async broadcastMessageEvent(session) {
357
+ async processUserMessage(session, timestamp) {
322
358
  try {
323
- await this.updateBotInfoToFile(session);
324
- const guildName = await this.updateChannelInfoToFile(session);
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
- await this.processMediaElements(session.elements, true);
364
+ this.processMediaElementsAsync(session.elements, true);
329
365
  }
330
366
  if (session.quote?.elements) {
331
- await this.processMediaElements(session.quote.elements, true);
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("广播消息事件失败:", 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
- await this.updateBotInfoToFile(session);
412
- const guildName = await this.updateChannelInfoToFile(session);
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("广播机器人消息事件失败:", 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
- this.dataFilePath = import_node_path2.default.resolve(ctx.baseDir, "data", "chat-patch", "chat-data.json");
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
- dataFilePath;
516
- fileOperationLock = Promise.resolve();
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
- ensureDataDir() {
521
- const dir = import_node_path2.default.dirname(this.dataFilePath);
522
- if (!import_node_fs2.default.existsSync(dir)) {
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
- // 从JSON文件读取数据
527
- readChatDataFromFile() {
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
- if (import_node_fs2.default.existsSync(this.dataFilePath)) {
530
- const jsonData = import_node_fs2.default.readFileSync(this.dataFilePath, "utf8");
531
- const data = JSON.parse(jsonData);
532
- return {
533
- bots: data.bots || {},
534
- channels: data.channels || {},
535
- messages: data.messages || {},
536
- pinnedBots: data.pinnedBots || [],
537
- pinnedChannels: data.pinnedChannels || [],
538
- lastSaveTime: data.lastSaveTime
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("读取聊天数据失败:", 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
- try {
555
- this.ensureDataDir();
556
- data.lastSaveTime = Date.now();
557
- const jsonData = JSON.stringify(data, null, 2);
558
- import_node_fs2.default.writeFileSync(this.dataFilePath, jsonData, "utf8");
559
- } catch (error) {
560
- this.logger.error("写入聊天数据失败:", error);
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
- this.fileOperationLock = this.fileOperationLock.then(async () => {
589
- const data = this.readChatDataFromFile();
590
- const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`;
591
- if (!data.messages[channelKey]) {
592
- data.messages[channelKey] = [];
593
- }
594
- const existingMessage = data.messages[channelKey].find((m) => m.id === messageInfo.id);
595
- if (existingMessage) {
596
- this.logInfo("消息已存在,跳过保存:", {
597
- channelKey,
598
- messageId: messageInfo.id,
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.writeChatDataToFile(data);
620
- const isCommandMessage = messageInfo.content?.startsWith("++") || messageInfo.content?.startsWith(".");
621
- this.logInfo("添加消息到文件:", {
622
- channelKey,
623
- messageId: messageInfo.id,
624
- content: messageInfo.content,
625
- type: messageInfo.type,
626
- userId: messageInfo.userId,
627
- username: messageInfo.username,
628
- timestamp: messageInfo.timestamp,
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", async (session) => {
1295
- if (utils.isPlatformBlocked(session.platform || "unknown")) {
1296
- logInfo(`忽略来自被屏蔽平台的消息: ${session.platform}`);
1297
- return;
1298
- }
1299
- await messageHandler.broadcastMessageEvent(session);
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", async (session) => {
1302
- if (utils.isPlatformBlocked(session.platform || "unknown")) {
1303
- logInfo(`忽略来自被屏蔽平台的机器人消息: ${session.platform}`);
1304
- return;
1305
- }
1306
- await messageHandler.broadcastBotMessageEvent(session);
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: