koishi-plugin-chat-patch 2.1.2 → 2.3.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,27 +123,39 @@ var Utils = class {
127
123
  } catch (e) {
128
124
  }
129
125
  }
130
- // 清理对象中的base64内容,改为持久化存储
131
- cleanBase64Content(obj) {
126
+ cleanBase64Content(obj, isBotMessage = false) {
132
127
  if (obj === null || obj === void 0) {
133
128
  return obj;
134
129
  }
135
130
  if (typeof obj === "string") {
136
131
  if (this.isBase64(obj)) {
132
+ if (isBotMessage && !obj.startsWith("data:image/")) {
133
+ return "[富媒体内容已省略]";
134
+ }
137
135
  return this.persistBase64Image(obj);
138
136
  }
139
137
  return obj;
140
138
  }
141
139
  if (Array.isArray(obj)) {
142
- return obj.map((item) => this.cleanBase64Content(item));
140
+ return obj.map((item) => this.cleanBase64Content(item, isBotMessage));
143
141
  }
144
142
  if (typeof obj === "object") {
145
143
  const cleaned = {};
146
144
  for (const [key, value] of Object.entries(obj)) {
145
+ if (isBotMessage && obj.type && !["text", "image", "img"].includes(obj.type)) {
146
+ if (typeof value === "string" && (key === "src" || key === "url" || key === "file") && this.isBase64(value)) {
147
+ cleaned[key] = "[富媒体内容已省略]";
148
+ continue;
149
+ }
150
+ }
147
151
  if (typeof value === "string" && (key === "src" || key === "url" || key === "file" || key === "data" || key === "content") && this.isBase64(value)) {
148
- cleaned[key] = this.persistBase64Image(value);
152
+ if (isBotMessage && !value.startsWith("data:image/")) {
153
+ cleaned[key] = "[富媒体内容已省略]";
154
+ } else {
155
+ cleaned[key] = this.persistBase64Image(value);
156
+ }
149
157
  } else {
150
- cleaned[key] = this.cleanBase64Content(value);
158
+ cleaned[key] = this.cleanBase64Content(value, isBotMessage);
151
159
  }
152
160
  }
153
161
  return cleaned;
@@ -170,95 +178,130 @@ var MessageHandler = class {
170
178
  }
171
179
  logger;
172
180
  utils;
173
- // 存储正确的 channelId 映射,key 是 selfId,value 是正确的 channelId
174
181
  correctChannelIds = /* @__PURE__ */ new Map();
175
- // 设置正确的 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
+ }
176
196
  setCorrectChannelId(selfId, channelId) {
177
197
  this.correctChannelIds.set(selfId, channelId);
178
198
  this.logInfo("设置正确的 channelId:", { selfId, channelId });
179
199
  }
180
- // 获取正确的 channelId
181
200
  getCorrectChannelId(selfId) {
182
201
  return this.correctChannelIds.get(selfId);
183
202
  }
184
- // 更新机器人信息到JSON文件
185
- async updateBotInfoToFile(session) {
186
- const data = this.fileManager.readChatDataFromFile();
187
- const botInfo = {
188
- selfId: session.selfId,
189
- platform: session.platform || "unknown",
190
- username: session.bot.user?.name || `Bot-${session.selfId}`,
191
- avatar: session.bot.user?.avatar,
192
- status: "online"
193
- };
194
- data.bots[session.selfId] = botInfo;
195
- this.fileManager.writeChatDataToFile(data);
196
- 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
+ });
197
221
  }
198
- // 更新频道信息到JSON文件
199
- async updateChannelInfoToFile(session) {
222
+ updateChannelInfoToFile(session) {
200
223
  const isDirect = session.isDirect || session.channelId?.includes("private");
201
- let guildName = session.channelId;
202
224
  const directUserName = session.username || session.event?.user?.name || session.userId;
203
225
  const data = this.fileManager.readChatDataFromFile();
204
- if (!isDirect) {
205
- try {
206
- if (session.guildId && session.bot.getGuild && typeof session.bot.getGuild === "function") {
207
- const guild = await session.bot.getGuild(session.guildId);
208
- guildName = guild?.name || session.channelId;
209
- }
210
- if (session.guildId && !session.bot.getGuild && session.bot.getChannel && typeof session.bot.getChannel === "function") {
211
- try {
212
- const channel = await session.bot.getChannel(session.guildId);
213
- guildName = channel?.name || session.channelId;
214
- } catch (channelError) {
215
- this.logInfo("获取频道信息失败,使用频道ID作为备用:", channelError);
216
- }
217
- }
218
- } catch (error) {
219
- this.logInfo("获取频道信息失败,使用频道ID作为备用:", error);
220
- guildName = session.channelId;
221
- }
222
- }
223
- if (!data.channels[session.selfId]) {
224
- data.channels[session.selfId] = {};
225
- }
226
- const existingChannel = data.channels[session.selfId][session.channelId];
227
- let finalName;
226
+ const existingChannel = data.channels[session.selfId]?.[session.channelId];
227
+ let immediateName = session.channelId;
228
228
  if (isDirect) {
229
229
  if (directUserName && directUserName !== session.userId) {
230
- finalName = `私聊(${directUserName})`;
230
+ immediateName = `私聊(${directUserName})`;
231
231
  } else if (existingChannel?.name && !existingChannel.name.includes("未知")) {
232
- finalName = existingChannel.name;
232
+ immediateName = existingChannel.name;
233
233
  } else if (session.platform && session.platform.toLowerCase().includes("sandbox")) {
234
- finalName = `私聊(${session.userId})`;
234
+ immediateName = `私聊(${session.userId})`;
235
235
  } else {
236
- finalName = "私聊(未知用户)";
236
+ immediateName = "私聊(未知用户)";
237
237
  }
238
- } else {
239
- finalName = guildName || session.channelId;
238
+ } else if (existingChannel?.guildName) {
239
+ immediateName = existingChannel.guildName;
240
240
  }
241
- const channelInfo = {
242
- id: session.channelId,
243
- name: finalName,
244
- type: session.type || 0,
245
- channelId: session.channelId,
246
- guildName,
247
- isDirect: !!isDirect
248
- };
249
- if (existingChannel && existingChannel.name !== finalName) {
250
- this.logInfo("更新频道名称:", {
251
- channelId: session.channelId,
252
- oldName: existingChannel.name,
253
- newName: finalName
254
- });
255
- }
256
- data.channels[session.selfId][session.channelId] = channelInfo;
257
- this.fileManager.writeChatDataToFile(data);
258
- this.logInfo("更新频道信息到文件:", channelInfo.name);
259
- 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;
260
304
  }
261
- // 下载并缓存媒体文件
262
305
  async downloadAndCacheMedia(url, type) {
263
306
  try {
264
307
  if (!url || url.startsWith("data:") || url.startsWith("file:")) return url;
@@ -284,32 +327,44 @@ var MessageHandler = class {
284
327
  return url;
285
328
  }
286
329
  }
287
- // 处理消息中的媒体元素并缓存
288
- async processMediaElements(elements) {
289
- if (!elements) return elements;
290
- for (const el of elements) {
291
- if (["image", "img", "mface"].includes(el.type)) {
292
- const src = el.attrs.src || el.attrs.url || el.attrs.file;
293
- if (src) el.attrs.src = await this.downloadAndCacheMedia(src, "image");
294
- } else if (["audio", "video"].includes(el.type)) {
295
- const src = el.attrs.src || el.attrs.url || el.attrs.file;
296
- if (src) 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);
351
+ }
352
+ } catch (error) {
353
+ this.logger.error("处理媒体元素失败:", error);
297
354
  }
298
- if (el.children) await this.processMediaElements(el.children);
299
- }
300
- return elements;
355
+ });
301
356
  }
302
- async broadcastMessageEvent(session) {
357
+ async processUserMessage(session, timestamp) {
303
358
  try {
304
- await this.updateBotInfoToFile(session);
305
- const guildName = await this.updateChannelInfoToFile(session);
359
+ if (!timestamp) timestamp = Date.now();
360
+ this.updateBotInfoToFile(session);
361
+ const guildName = this.updateChannelInfoToFile(session);
306
362
  const isDirect = session.isDirect || session.channelId?.includes("private");
307
- const timestamp = Date.now();
308
363
  if (session.elements) {
309
- await this.processMediaElements(session.elements);
364
+ this.processMediaElementsAsync(session.elements, true);
310
365
  }
311
366
  if (session.quote?.elements) {
312
- await this.processMediaElements(session.quote.elements);
367
+ this.processMediaElementsAsync(session.quote.elements, true);
313
368
  }
314
369
  let quoteInfo = void 0;
315
370
  if (session.quote) {
@@ -350,11 +405,11 @@ var MessageHandler = class {
350
405
  timestamp,
351
406
  channelId: session.channelId,
352
407
  selfId: session.selfId,
353
- elements: this.utils.cleanBase64Content(elements),
408
+ elements: this.utils.cleanBase64Content(elements, false),
354
409
  type: "user",
355
410
  guildName,
356
411
  platform: session.platform || "unknown",
357
- quote: quoteInfo ? this.utils.cleanBase64Content(quoteInfo) : void 0,
412
+ quote: quoteInfo ? this.utils.cleanBase64Content(quoteInfo, false) : void 0,
358
413
  isDirect: !!isDirect
359
414
  };
360
415
  await this.fileManager.addMessageToFile(messageInfo);
@@ -371,8 +426,8 @@ var MessageHandler = class {
371
426
  timestamp,
372
427
  guildName,
373
428
  channelType: session.type || 0,
374
- elements: this.utils.cleanBase64Content(elements),
375
- quote: quoteInfo ? this.utils.cleanBase64Content(quoteInfo) : void 0,
429
+ elements: this.utils.cleanBase64Content(elements, false),
430
+ quote: quoteInfo ? this.utils.cleanBase64Content(quoteInfo, false) : void 0,
376
431
  isDirect: session.isDirect,
377
432
  bot: {
378
433
  avatar: session.bot.user?.avatar,
@@ -381,21 +436,17 @@ var MessageHandler = class {
381
436
  };
382
437
  this.ctx.console.broadcast("chat-message-event", messageEvent);
383
438
  } catch (error) {
384
- this.logger.error("广播消息事件失败:", error);
439
+ this.logger.error("处理用户消息失败:", error);
385
440
  }
386
441
  }
387
- // 处理机器人发送的消息
388
- async broadcastBotMessageEvent(session) {
442
+ async processBotMessage(session, timestamp) {
389
443
  try {
444
+ if (!timestamp) timestamp = Date.now();
390
445
  const correctChannelId = this.getCorrectChannelId(session.selfId);
391
446
  const finalChannelId = correctChannelId || session.channelId;
392
- await this.updateBotInfoToFile(session);
393
- const guildName = await this.updateChannelInfoToFile(session);
447
+ this.updateBotInfoToFile(session);
448
+ const guildName = this.updateChannelInfoToFile(session);
394
449
  const isDirect = session.isDirect || finalChannelId?.includes("private");
395
- const timestamp = Date.now();
396
- if (session.event?.message?.elements) {
397
- await this.processMediaElements(session.event.message.elements);
398
- }
399
450
  let content = session.content || "";
400
451
  if (!content && session.event?.message?.elements) {
401
452
  content = this.utils.extractTextContent(session.event.message.elements).trim();
@@ -438,7 +489,7 @@ var MessageHandler = class {
438
489
  timestamp,
439
490
  channelId: finalChannelId,
440
491
  selfId: session.selfId,
441
- elements: this.utils.cleanBase64Content(session.event?.message?.elements),
492
+ elements: this.utils.cleanBase64Content(session.event?.message?.elements, true),
442
493
  type: "bot",
443
494
  guildName,
444
495
  platform: session.platform || "unknown",
@@ -461,7 +512,7 @@ var MessageHandler = class {
461
512
  timestamp,
462
513
  guildName,
463
514
  channelType: session.event?.channel?.type || session.type || 0,
464
- elements: this.utils.cleanBase64Content(session.event?.message?.elements),
515
+ elements: this.utils.cleanBase64Content(session.event?.message?.elements, true),
465
516
  quote: quoteInfo,
466
517
  isDirect: !!isDirect,
467
518
  sending: true,
@@ -472,7 +523,7 @@ var MessageHandler = class {
472
523
  };
473
524
  this.ctx.console.broadcast("chat-bot-message-event", messageEvent);
474
525
  } catch (error) {
475
- this.logger.error("广播机器人消息事件失败:", error);
526
+ this.logger.error("处理机器人消息失败:", error);
476
527
  }
477
528
  }
478
529
  logInfo(...args) {
@@ -492,6 +543,7 @@ var FileManager = class {
492
543
  this.dataFilePath = import_node_path2.default.resolve(ctx.baseDir, "data", "chat-patch", "chat-data.json");
493
544
  this.logger = ctx.logger("chat-patch");
494
545
  this.utils = new Utils(config);
546
+ this.memoryCache = this.readChatDataFromFile();
495
547
  }
496
548
  static {
497
549
  __name(this, "FileManager");
@@ -500,51 +552,99 @@ var FileManager = class {
500
552
  fileOperationLock = Promise.resolve();
501
553
  logger;
502
554
  utils;
503
- // 确保目录存在
555
+ memoryCache = null;
556
+ pendingMessages = [];
557
+ writeTimer = null;
558
+ WRITE_DEBOUNCE_MS = 1e3;
504
559
  ensureDataDir() {
505
560
  const dir = import_node_path2.default.dirname(this.dataFilePath);
506
561
  if (!import_node_fs2.default.existsSync(dir)) {
507
562
  import_node_fs2.default.mkdirSync(dir, { recursive: true });
508
563
  }
509
564
  }
510
- // 从JSON文件读取数据
511
565
  readChatDataFromFile() {
512
- try {
513
- if (import_node_fs2.default.existsSync(this.dataFilePath)) {
514
- const jsonData = import_node_fs2.default.readFileSync(this.dataFilePath, "utf8");
515
- const data = JSON.parse(jsonData);
516
- return {
517
- bots: data.bots || {},
518
- channels: data.channels || {},
519
- messages: data.messages || {},
520
- pinnedBots: data.pinnedBots || [],
521
- pinnedChannels: data.pinnedChannels || [],
522
- lastSaveTime: data.lastSaveTime
523
- };
524
- }
525
- } catch (error) {
526
- this.logger.error("读取聊天数据失败:", error);
566
+ if (this.memoryCache) {
567
+ return this.memoryCache;
527
568
  }
528
- return {
569
+ process.nextTick(() => {
570
+ try {
571
+ if (import_node_fs2.default.existsSync(this.dataFilePath)) {
572
+ const jsonData = import_node_fs2.default.readFileSync(this.dataFilePath, "utf8");
573
+ const data = JSON.parse(jsonData);
574
+ this.memoryCache = {
575
+ bots: data.bots || {},
576
+ channels: data.channels || {},
577
+ messages: data.messages || {},
578
+ pinnedBots: data.pinnedBots || [],
579
+ pinnedChannels: data.pinnedChannels || [],
580
+ lastSaveTime: data.lastSaveTime
581
+ };
582
+ }
583
+ } catch (error) {
584
+ this.logger.error("读取聊天数据失败:", error);
585
+ }
586
+ });
587
+ this.memoryCache = {
529
588
  bots: {},
530
589
  channels: {},
531
590
  messages: {},
532
591
  pinnedBots: [],
533
592
  pinnedChannels: []
534
593
  };
594
+ return this.memoryCache;
535
595
  }
536
- // 写入数据到JSON文件
537
596
  writeChatDataToFile(data) {
538
- try {
539
- this.ensureDataDir();
540
- data.lastSaveTime = Date.now();
541
- const jsonData = JSON.stringify(data, null, 2);
542
- import_node_fs2.default.writeFileSync(this.dataFilePath, jsonData, "utf8");
543
- } catch (error) {
544
- this.logger.error("写入聊天数据失败:", error);
597
+ data.lastSaveTime = Date.now();
598
+ this.memoryCache = data;
599
+ process.nextTick(() => {
600
+ try {
601
+ this.ensureDataDir();
602
+ const jsonData = JSON.stringify(data, null, 2);
603
+ import_node_fs2.default.writeFileSync(this.dataFilePath, jsonData, "utf8");
604
+ } catch (error) {
605
+ this.logger.error("写入聊天数据失败:", error);
606
+ }
607
+ });
608
+ }
609
+ scheduleWrite() {
610
+ if (this.writeTimer) {
611
+ this.writeTimer();
612
+ this.writeTimer = null;
545
613
  }
614
+ this.writeTimer = this.ctx.setTimeout(() => {
615
+ process.nextTick(() => {
616
+ this.flushPendingMessages();
617
+ });
618
+ this.writeTimer = null;
619
+ }, this.WRITE_DEBOUNCE_MS);
620
+ }
621
+ flushPendingMessages() {
622
+ if (this.pendingMessages.length === 0) return;
623
+ const messagesToWrite = [...this.pendingMessages];
624
+ this.pendingMessages = [];
625
+ const data = this.memoryCache || this.readChatDataFromFile();
626
+ for (const messageInfo of messagesToWrite) {
627
+ const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`;
628
+ if (!data.messages[channelKey]) {
629
+ data.messages[channelKey] = [];
630
+ }
631
+ const existingMessage = data.messages[channelKey].find((m) => m.id === messageInfo.id);
632
+ if (existingMessage) {
633
+ continue;
634
+ }
635
+ if (!messageInfo.timestamp) {
636
+ messageInfo.timestamp = Date.now();
637
+ }
638
+ const cleanedMessageInfo = this.utils.cleanBase64Content(messageInfo);
639
+ data.messages[channelKey].push(cleanedMessageInfo);
640
+ if (data.messages[channelKey].length > this.config.maxMessagesPerChannel) {
641
+ data.messages[channelKey].sort((a, b) => a.timestamp - b.timestamp);
642
+ data.messages[channelKey] = data.messages[channelKey].slice(-this.config.maxMessagesPerChannel);
643
+ }
644
+ }
645
+ this.writeChatDataToFile(data);
646
+ this.logInfo(`批量写入 ${messagesToWrite.length} 条消息`);
546
647
  }
547
- // 清理超量消息
548
648
  cleanExcessMessages(data) {
549
649
  let cleanedCount = 0;
550
650
  const cleanedMessages = {};
@@ -567,56 +667,34 @@ var FileManager = class {
567
667
  messages: cleanedMessages
568
668
  };
569
669
  }
570
- // 添加消息到JSON文件(使用锁机制防止并发冲突)
571
670
  async addMessageToFile(messageInfo) {
572
- this.fileOperationLock = this.fileOperationLock.then(async () => {
573
- const data = this.readChatDataFromFile();
574
- const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`;
575
- if (!data.messages[channelKey]) {
576
- data.messages[channelKey] = [];
577
- }
578
- const existingMessage = data.messages[channelKey].find((m) => m.id === messageInfo.id);
579
- if (existingMessage) {
580
- this.logInfo("消息已存在,跳过保存:", {
581
- channelKey,
582
- messageId: messageInfo.id,
583
- existingType: existingMessage.type,
584
- existingContent: existingMessage.content,
585
- newType: messageInfo.type,
586
- newContent: messageInfo.content
587
- });
588
- return;
589
- }
671
+ this.pendingMessages.push(messageInfo);
672
+ const data = this.memoryCache || this.readChatDataFromFile();
673
+ const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`;
674
+ if (!data.messages[channelKey]) {
675
+ data.messages[channelKey] = [];
676
+ }
677
+ const existingMessage = data.messages[channelKey].find((m) => m.id === messageInfo.id);
678
+ if (!existingMessage) {
590
679
  if (!messageInfo.timestamp) {
591
680
  messageInfo.timestamp = Date.now();
592
681
  }
593
682
  const cleanedMessageInfo = this.utils.cleanBase64Content(messageInfo);
594
- const beforeCount = data.messages[channelKey].length;
595
683
  data.messages[channelKey].push(cleanedMessageInfo);
596
- const afterCount = data.messages[channelKey].length;
597
684
  if (data.messages[channelKey].length > this.config.maxMessagesPerChannel) {
598
685
  data.messages[channelKey].sort((a, b) => a.timestamp - b.timestamp);
599
- const removedCount = data.messages[channelKey].length - this.config.maxMessagesPerChannel;
600
686
  data.messages[channelKey] = data.messages[channelKey].slice(-this.config.maxMessagesPerChannel);
601
- this.logInfo(`频道 ${channelKey} 达到消息上限,清理了 ${removedCount} 条旧消息`);
602
687
  }
603
- this.writeChatDataToFile(data);
604
- const isCommandMessage = messageInfo.content?.startsWith("++") || messageInfo.content?.startsWith(".");
605
- this.logInfo("添加消息到文件:", {
606
- channelKey,
607
- messageId: messageInfo.id,
608
- content: messageInfo.content,
609
- type: messageInfo.type,
610
- userId: messageInfo.userId,
611
- username: messageInfo.username,
612
- timestamp: messageInfo.timestamp,
613
- isCommandMessage,
614
- 消息数变化: `${beforeCount} -> ${afterCount} -> ${data.messages[channelKey].length}`
615
- });
616
- }).catch((error) => {
617
- this.logger.error("保存消息时发生错误:", error);
618
- });
619
- await this.fileOperationLock;
688
+ this.memoryCache = data;
689
+ }
690
+ this.scheduleWrite();
691
+ }
692
+ dispose() {
693
+ if (this.writeTimer) {
694
+ this.writeTimer();
695
+ this.writeTimer = null;
696
+ }
697
+ this.flushPendingMessages();
620
698
  }
621
699
  logInfo(...args) {
622
700
  if (this.config.loggerinfo) {
@@ -642,6 +720,7 @@ var ApiHandlers = class {
642
720
  __name(this, "ApiHandlers");
643
721
  }
644
722
  logger;
723
+ currentTempVideo = null;
645
724
  registerApiHandlers() {
646
725
  this.ctx.console.addListener("clear-all-indexeddb-data", async () => {
647
726
  try {
@@ -663,7 +742,6 @@ var ApiHandlers = class {
663
742
  channels: data.channels || {},
664
743
  pinnedBots: data.pinnedBots || [],
665
744
  pinnedChannels: data.pinnedChannels || [],
666
- // 不返回 messages,由前端按需拉取
667
745
  messages: {}
668
746
  }
669
747
  };
@@ -1067,8 +1145,39 @@ var ApiHandlers = class {
1067
1145
  return { success: false, error: error?.message || String(error) };
1068
1146
  }
1069
1147
  });
1148
+ this.ctx.console.addListener("fetch-video-temp", async (data) => {
1149
+ try {
1150
+ this.logInfo("收到视频临时加载请求:", data.url);
1151
+ if (this.isFileUrl(data.url)) {
1152
+ const result = await this.handleLocalFileRequest(data.url);
1153
+ return result;
1154
+ }
1155
+ const response = await fetch(data.url, {
1156
+ headers: {
1157
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
1158
+ "Referer": ""
1159
+ }
1160
+ });
1161
+ if (!response.ok) {
1162
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1163
+ }
1164
+ const buffer = await response.arrayBuffer();
1165
+ const base64 = Buffer.from(buffer).toString("base64");
1166
+ const contentType = response.headers.get("content-type") || "video/mp4";
1167
+ this.logInfo("视频下载成功:", { size: buffer.byteLength, contentType });
1168
+ return {
1169
+ success: true,
1170
+ base64,
1171
+ contentType,
1172
+ dataUrl: `data:${contentType};base64,${base64}`,
1173
+ size: buffer.byteLength
1174
+ };
1175
+ } catch (error) {
1176
+ this.logger.error("视频临时加载失败:", error);
1177
+ return { success: false, error: error?.message || String(error) };
1178
+ }
1179
+ });
1070
1180
  }
1071
- // 检查是否为文件 URL
1072
1181
  isFileUrl(url) {
1073
1182
  try {
1074
1183
  const parsedUrl = new import_node_url2.URL(url);
@@ -1077,7 +1186,6 @@ var ApiHandlers = class {
1077
1186
  return false;
1078
1187
  }
1079
1188
  }
1080
- // 创建文件 URL
1081
1189
  createFileUrl(filePath) {
1082
1190
  try {
1083
1191
  return (0, import_node_url2.pathToFileURL)(filePath).href;
@@ -1086,7 +1194,6 @@ var ApiHandlers = class {
1086
1194
  return `file://${filePath}`;
1087
1195
  }
1088
1196
  }
1089
- // 处理本地文件请求
1090
1197
  async handleLocalFileRequest(fileUrl) {
1091
1198
  try {
1092
1199
  const filePath = (0, import_node_url2.fileURLToPath)(fileUrl);
@@ -1108,13 +1215,11 @@ var ApiHandlers = class {
1108
1215
  };
1109
1216
  }
1110
1217
  }
1111
- // 设置定时清理临时文件
1112
1218
  setupTempFileCleanup() {
1113
- setInterval(() => {
1219
+ this.ctx.setInterval(() => {
1114
1220
  this.cleanupMediaCache();
1115
1221
  }, 5 * 60 * 1e3);
1116
1222
  }
1117
- // 统一清理媒体缓存
1118
1223
  async cleanupMediaCache() {
1119
1224
  const baseDir = this.ctx.baseDir + "/data/chat-patch/persist-media";
1120
1225
  if (!require("node:fs").existsSync(baseDir)) return;
@@ -1197,6 +1302,27 @@ var usage = `
1197
1302
  `;
1198
1303
  async function apply(ctx, config) {
1199
1304
  const logger = ctx.logger("chat-patch");
1305
+ const mediaDir = import_node_path3.default.join(ctx.baseDir, "data", "chat-patch", "persist-media", "media");
1306
+ if (require("node:fs").existsSync(mediaDir)) {
1307
+ try {
1308
+ const files = require("node:fs").readdirSync(mediaDir);
1309
+ let deletedCount = 0;
1310
+ for (const file of files) {
1311
+ const filePath = import_node_path3.default.join(mediaDir, file);
1312
+ try {
1313
+ require("node:fs").unlinkSync(filePath);
1314
+ deletedCount++;
1315
+ } catch (e) {
1316
+ logger.warn("删除媒体文件失败:", filePath, e);
1317
+ }
1318
+ }
1319
+ if (deletedCount > 0) {
1320
+ logger.info(`启动时清理了 ${deletedCount} 个旧版本的媒体缓存文件`);
1321
+ }
1322
+ } catch (e) {
1323
+ logger.warn("清理媒体文件夹失败:", e);
1324
+ }
1325
+ }
1200
1326
  const fileManager = new FileManager(ctx, config);
1201
1327
  const messageHandler = new MessageHandler(ctx, config, fileManager);
1202
1328
  const apiHandlers = new ApiHandlers(ctx, config, fileManager, messageHandler);
@@ -1220,23 +1346,55 @@ async function apply(ctx, config) {
1220
1346
  消息频道数: Object.keys(cleanedData.messages).length,
1221
1347
  总消息数: Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0)
1222
1348
  });
1223
- ctx.on("message", async (session) => {
1224
- if (utils.isPlatformBlocked(session.platform || "unknown")) {
1225
- logInfo(`忽略来自被屏蔽平台的消息: ${session.platform}`);
1226
- return;
1227
- }
1228
- await messageHandler.broadcastMessageEvent(session);
1349
+ ctx.on("message", (session) => {
1350
+ if (utils.isPlatformBlocked(session.platform || "unknown")) return;
1351
+ const timestamp = Date.now();
1352
+ ctx.console.broadcast("chat-message-event", {
1353
+ type: "message",
1354
+ selfId: session.selfId,
1355
+ platform: session.platform || "unknown",
1356
+ channelId: session.channelId,
1357
+ messageId: session.event?.message?.id || `msg-${timestamp}`,
1358
+ content: session.content || "",
1359
+ userId: session.userId || "unknown",
1360
+ username: session.username || session.userId || "unknown",
1361
+ avatar: session.event?.user?.avatar,
1362
+ timestamp,
1363
+ isDirect: session.isDirect,
1364
+ elements: utils.cleanBase64Content(session.elements, false),
1365
+ bot: {
1366
+ avatar: session.bot.user?.avatar,
1367
+ name: session.bot.user?.name
1368
+ }
1369
+ });
1370
+ messageHandler.recordUserMessage(session, timestamp);
1229
1371
  });
1230
- ctx.on("before-send", async (session) => {
1231
- if (utils.isPlatformBlocked(session.platform || "unknown")) {
1232
- logInfo(`忽略来自被屏蔽平台的机器人消息: ${session.platform}`);
1233
- return;
1234
- }
1235
- await messageHandler.broadcastBotMessageEvent(session);
1372
+ ctx.on("before-send", (session) => {
1373
+ if (utils.isPlatformBlocked(session.platform || "unknown")) return;
1374
+ const timestamp = Date.now();
1375
+ ctx.console.broadcast("chat-bot-message-event", {
1376
+ type: "bot-message",
1377
+ selfId: session.selfId,
1378
+ platform: session.platform || "unknown",
1379
+ channelId: session.channelId,
1380
+ messageId: `bot-msg-${timestamp}`,
1381
+ content: session.content || "",
1382
+ userId: session.selfId,
1383
+ username: session.bot.user?.name || `Bot-${session.selfId}`,
1384
+ avatar: session.bot.user?.avatar,
1385
+ timestamp,
1386
+ sending: true,
1387
+ elements: utils.cleanBase64Content(session.event?.message?.elements, true),
1388
+ bot: {
1389
+ avatar: session.bot.user?.avatar,
1390
+ name: session.bot.user?.name
1391
+ }
1392
+ });
1393
+ messageHandler.recordBotMessage(session, timestamp);
1236
1394
  });
1237
1395
  ctx.on("ready", async () => {
1238
1396
  logInfo("插件启动完成,开始监听消息");
1239
- setInterval(() => {
1397
+ ctx.setInterval(() => {
1240
1398
  const data = fileManager.readChatDataFromFile();
1241
1399
  const cleanedData2 = fileManager.cleanExcessMessages(data);
1242
1400
  const originalCount2 = Object.values(data.messages).reduce((total, msgs) => total + msgs.length, 0);
@@ -1252,6 +1410,10 @@ async function apply(ctx, config) {
1252
1410
  dev: import_node_path3.default.resolve(__dirname, "../client/index.ts"),
1253
1411
  prod: import_node_path3.default.resolve(__dirname, "../dist")
1254
1412
  });
1413
+ ctx.on("dispose", () => {
1414
+ fileManager.dispose();
1415
+ logInfo("插件已卸载,所有待处理的消息已写入");
1416
+ });
1255
1417
  }
1256
1418
  __name(apply, "apply");
1257
1419
  // Annotate the CommonJS export names for ESM import in node: