koishi-plugin-chat-patch 2.4.4 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/lib/index.js CHANGED
@@ -39,13 +39,13 @@ __export(src_exports, {
39
39
  usage: () => usage
40
40
  });
41
41
  module.exports = __toCommonJS(src_exports);
42
- var import_node_path3 = __toESM(require("node:path"));
42
+ var import_node_path5 = __toESM(require("node:path"));
43
43
 
44
44
  // src/utils.ts
45
- var import_node_fs = require("node:fs");
46
45
  var import_node_path = require("node:path");
47
46
  var import_node_crypto = require("node:crypto");
48
47
  var import_node_url = require("node:url");
48
+ var import_node_fs = require("node:fs");
49
49
  var Utils = class {
50
50
  constructor(config, ctx) {
51
51
  this.config = config;
@@ -54,6 +54,8 @@ var Utils = class {
54
54
  static {
55
55
  __name(this, "Utils");
56
56
  }
57
+ persistImageWriteCount = 0;
58
+ pendingTasks = /* @__PURE__ */ new Set();
57
59
  isPlatformBlocked(platform) {
58
60
  if (!this.config.blockedPlatforms || this.config.blockedPlatforms.length === 0) {
59
61
  return false;
@@ -97,33 +99,48 @@ var Utils = class {
97
99
  }
98
100
  return false;
99
101
  }
100
- persistBase64Image(base64Data) {
102
+ async persistBase64ImageAsync(base64Data) {
101
103
  if (!this.ctx || !base64Data.startsWith("data:image/")) return base64Data;
102
104
  try {
103
105
  const dir = (0, import_node_path.join)(this.ctx.baseDir, "data", "chat-patch", "persist-images");
104
- if (!(0, import_node_fs.existsSync)(dir)) (0, import_node_fs.mkdirSync)(dir, { recursive: true });
106
+ await import_node_fs.promises.mkdir(dir, { recursive: true });
105
107
  const hash = (0, import_node_crypto.createHash)("md5").update(base64Data).digest("hex");
106
108
  const ext = base64Data.split(";")[0].split("/")[1] || "png";
107
- const filename = `${Date.now()}_${hash}.${ext}`;
109
+ const filename = `${hash}.${ext}`;
108
110
  const filePath = (0, import_node_path.join)(dir, filename);
109
- const base64Content = base64Data.split(",")[1];
110
- (0, import_node_fs.writeFileSync)(filePath, Buffer.from(base64Content, "base64"));
111
- this.cleanupPersistImages(dir);
111
+ if (!await this.fileExists(filePath)) {
112
+ const base64Content = base64Data.split(",")[1];
113
+ await import_node_fs.promises.writeFile(filePath, Buffer.from(base64Content, "base64"));
114
+ }
115
+ this.persistImageWriteCount += 1;
116
+ if (this.persistImageWriteCount % 20 === 0) {
117
+ this.trackTask(this.cleanupPersistImagesAsync(dir));
118
+ }
112
119
  return (0, import_node_url.pathToFileURL)(filePath).href;
113
- } catch (e) {
120
+ } catch {
114
121
  return base64Data;
115
122
  }
116
123
  }
117
- cleanupPersistImages(dir) {
124
+ async cleanupPersistImagesAsync(dir) {
118
125
  try {
119
- const files = (0, import_node_fs.readdirSync)(dir).map((name2) => ({ name: name2, path: (0, import_node_path.join)(dir, name2), mtime: (0, import_node_fs.statSync)((0, import_node_path.join)(dir, name2)).mtimeMs })).sort((a, b) => b.mtime - a.mtime);
120
- if (files.length > this.config.maxPersistImages) {
121
- files.slice(this.config.maxPersistImages).forEach((f) => (0, import_node_fs.unlinkSync)(f.path));
126
+ const fileNames = await import_node_fs.promises.readdir(dir);
127
+ const files = await Promise.all(fileNames.map(async (name2) => {
128
+ const filePath = (0, import_node_path.join)(dir, name2);
129
+ const stats = await import_node_fs.promises.stat(filePath);
130
+ return { path: filePath, mtime: stats.mtimeMs };
131
+ }));
132
+ files.sort((a, b) => b.mtime - a.mtime);
133
+ for (const file of files.slice(this.config.maxPersistImages)) {
134
+ try {
135
+ await import_node_fs.promises.unlink(file.path);
136
+ } catch {
137
+ continue;
138
+ }
122
139
  }
123
- } catch (e) {
140
+ } catch {
124
141
  }
125
142
  }
126
- cleanBase64Content(obj, isBotMessage = false) {
143
+ async cleanBase64ContentAsync(obj, isBotMessage = false) {
127
144
  if (obj === null || obj === void 0) {
128
145
  return obj;
129
146
  }
@@ -132,17 +149,20 @@ var Utils = class {
132
149
  if (isBotMessage && !obj.startsWith("data:image/")) {
133
150
  return "[富媒体内容已省略]";
134
151
  }
135
- return this.persistBase64Image(obj);
152
+ return await this.persistBase64ImageAsync(obj);
136
153
  }
137
154
  return obj;
138
155
  }
139
156
  if (Array.isArray(obj)) {
140
- return obj.map((item) => this.cleanBase64Content(item, isBotMessage));
157
+ const cleanedItems = await Promise.all(obj.map((item) => this.cleanBase64ContentAsync(item, isBotMessage)));
158
+ return cleanedItems;
141
159
  }
142
160
  if (typeof obj === "object") {
143
161
  const cleaned = {};
144
- for (const [key, value] of Object.entries(obj)) {
145
- if (isBotMessage && obj.type && !["text", "image", "img"].includes(obj.type)) {
162
+ const source = obj;
163
+ for (const [key, value] of Object.entries(source)) {
164
+ const type = typeof source.type === "string" ? source.type : void 0;
165
+ if (isBotMessage && type && !["text", "image", "img"].includes(type)) {
146
166
  if (typeof value === "string" && (key === "src" || key === "url" || key === "file") && this.isBase64(value)) {
147
167
  cleaned[key] = "[富媒体内容已省略]";
148
168
  continue;
@@ -152,78 +172,90 @@ var Utils = class {
152
172
  if (isBotMessage && !value.startsWith("data:image/")) {
153
173
  cleaned[key] = "[富媒体内容已省略]";
154
174
  } else {
155
- cleaned[key] = this.persistBase64Image(value);
175
+ cleaned[key] = await this.persistBase64ImageAsync(value);
156
176
  }
157
177
  } else {
158
- cleaned[key] = this.cleanBase64Content(value, isBotMessage);
178
+ cleaned[key] = await this.cleanBase64ContentAsync(value, isBotMessage);
159
179
  }
160
180
  }
161
181
  return cleaned;
162
182
  }
163
183
  return obj;
164
184
  }
185
+ async dispose() {
186
+ await Promise.allSettled([...this.pendingTasks]);
187
+ }
188
+ trackTask(task) {
189
+ this.pendingTasks.add(task);
190
+ void task.finally(() => {
191
+ this.pendingTasks.delete(task);
192
+ });
193
+ }
194
+ async fileExists(filePath) {
195
+ try {
196
+ await import_node_fs.promises.access(filePath);
197
+ return true;
198
+ } catch {
199
+ return false;
200
+ }
201
+ }
165
202
  };
166
203
 
167
204
  // src/message-handler.ts
205
+ var import_node_fs2 = require("node:fs");
206
+ var import_node_path2 = __toESM(require("node:path"));
207
+ var import_node_crypto2 = require("node:crypto");
168
208
  var MessageHandler = class {
169
- constructor(ctx, config, fileManager) {
209
+ constructor(ctx, config, fileManager, logger) {
170
210
  this.ctx = ctx;
171
211
  this.config = config;
172
212
  this.fileManager = fileManager;
173
- this.logger = ctx.logger("chat-patch");
213
+ this.logger = logger;
174
214
  this.utils = new Utils(config);
175
215
  }
176
216
  static {
177
217
  __name(this, "MessageHandler");
178
218
  }
179
- logger;
180
219
  utils;
181
220
  correctChannelIds = /* @__PURE__ */ new Map();
221
+ scheduledTasks = /* @__PURE__ */ new Set();
222
+ channelRefreshInFlight = /* @__PURE__ */ new Set();
223
+ lastChannelRefreshAt = /* @__PURE__ */ new Map();
224
+ CHANNEL_REFRESH_TTL_MS = 10 * 60 * 1e3;
182
225
  recordUserMessage(session, timestamp) {
183
- setImmediate(() => {
184
- this.processUserMessage(session, timestamp).catch((error) => {
185
- this.logger.error("记录用户消息失败:", error);
186
- });
226
+ this.scheduleTask("记录用户消息", async () => {
227
+ await this.processUserMessage(session, timestamp);
187
228
  });
188
229
  }
189
230
  recordBotMessage(session, timestamp) {
190
- setImmediate(() => {
191
- this.processBotMessage(session, timestamp).catch((error) => {
192
- this.logger.error("记录机器人消息失败:", error);
193
- });
231
+ this.scheduleTask("记录机器人消息", async () => {
232
+ await this.processBotMessage(session, timestamp);
194
233
  });
195
234
  }
196
235
  setCorrectChannelId(selfId, channelId) {
197
236
  this.correctChannelIds.set(selfId, channelId);
198
- this.logInfo("设置正确的 channelId:", { selfId, channelId });
237
+ this.logger.logInfo("设置正确的 channelId:", { selfId, channelId });
199
238
  }
200
239
  getCorrectChannelId(selfId) {
201
240
  return this.correctChannelIds.get(selfId);
202
241
  }
203
242
  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
- }
243
+ this.scheduleTask("更新机器人信息", async () => {
244
+ const botInfo = {
245
+ selfId: session.selfId,
246
+ platform: session.platform || "unknown",
247
+ username: session.bot.user?.name || `Bot-${session.selfId}`,
248
+ avatar: session.bot.user?.avatar,
249
+ status: "online"
250
+ };
251
+ await this.fileManager.upsertBotInfo(botInfo);
252
+ this.logger.logInfo("更新机器人信息到文件:", botInfo.username);
220
253
  });
221
254
  }
222
255
  updateChannelInfoToFile(session) {
223
256
  const isDirect = session.isDirect || session.channelId?.includes("private");
224
257
  const directUserName = session.username || session.event?.user?.name || session.userId;
225
- const data = this.fileManager.readChatDataFromFile();
226
- const existingChannel = data.channels[session.selfId]?.[session.channelId];
258
+ const existingChannel = this.fileManager.getCachedChannelInfo(session.selfId, session.channelId);
227
259
  let immediateName = session.channelId;
228
260
  if (isDirect) {
229
261
  if (directUserName && directUserName !== session.userId) {
@@ -238,68 +270,12 @@ var MessageHandler = class {
238
270
  } else if (existingChannel?.guildName) {
239
271
  immediateName = existingChannel.guildName;
240
272
  }
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
- });
273
+ const channelKey = `${session.selfId}:${session.channelId}`;
274
+ if (this.shouldRefreshChannelInfo(channelKey, existingChannel, isDirect, session.channelId, directUserName)) {
275
+ this.scheduleTask("更新频道信息", async () => {
276
+ await this.refreshChannelInfo(session, existingChannel, isDirect, directUserName);
277
+ });
278
+ }
303
279
  return immediateName;
304
280
  }
305
281
  async downloadAndCacheMedia(url, type) {
@@ -309,18 +285,15 @@ var MessageHandler = class {
309
285
  let folder = "media";
310
286
  if (type === "image") folder = "images";
311
287
  else if (type === "avatar") folder = "avatars";
312
- const dir = require("node:path").join(this.ctx.baseDir, "data", "chat-patch", "persist-media", folder);
313
- if (!require("node:fs").existsSync(dir)) {
314
- require("node:fs").mkdirSync(dir, { recursive: true });
315
- }
316
- const crypto = require("node:crypto");
317
- const hash = crypto.createHash("md5").update(url).digest("hex");
318
- const ext = require("node:path").extname(new URL(url).pathname) || (type === "image" ? ".jpg" : ".mp4");
288
+ const dir = import_node_path2.default.join(this.ctx.baseDir, "data", "chat-patch", "persist-media", folder);
289
+ await import_node_fs2.promises.mkdir(dir, { recursive: true });
290
+ const hash = (0, import_node_crypto2.createHash)("md5").update(url).digest("hex");
291
+ const ext = import_node_path2.default.extname(new URL(url).pathname) || (type === "image" ? ".jpg" : ".mp4");
319
292
  const filename = `${hash}${ext}`;
320
- const filePath = require("node:path").join(dir, filename);
321
- if (!require("node:fs").existsSync(filePath)) {
293
+ const filePath = import_node_path2.default.join(dir, filename);
294
+ if (!await this.fileExists(filePath)) {
322
295
  const buffer = await this.ctx.http.get(url, { responseType: "arraybuffer" });
323
- require("node:fs").writeFileSync(filePath, Buffer.from(buffer));
296
+ await import_node_fs2.promises.writeFile(filePath, Buffer.from(buffer));
324
297
  }
325
298
  const normalizedPath = filePath.replace(/\\/g, "/");
326
299
  return `/vite/@fs/${normalizedPath}`;
@@ -331,29 +304,8 @@ var MessageHandler = class {
331
304
  }
332
305
  processMediaElementsAsync(elements, isUserMessage = true) {
333
306
  if (!elements) return;
334
- setImmediate(async () => {
335
- try {
336
- for (const el of elements) {
337
- if (["image", "img", "mface"].includes(el.type)) {
338
- const src = el.attrs.src || el.attrs.url || el.attrs.file;
339
- if (src && isUserMessage) {
340
- this.downloadAndCacheMedia(src, "image").catch((e) => {
341
- this.logger.warn("缓存图片失败:", e);
342
- });
343
- }
344
- } else if (el.type === "audio") {
345
- const src = el.attrs.src || el.attrs.url || el.attrs.file;
346
- if (src && isUserMessage) {
347
- this.downloadAndCacheMedia(src, "media").catch((e) => {
348
- this.logger.warn("缓存语音失败:", e);
349
- });
350
- }
351
- }
352
- if (el.children) this.processMediaElementsAsync(el.children, isUserMessage);
353
- }
354
- } catch (error) {
355
- this.logger.error("处理媒体元素失败:", error);
356
- }
307
+ this.scheduleTask("处理媒体元素", async () => {
308
+ await this.processMediaElements(elements, isUserMessage);
357
309
  });
358
310
  }
359
311
  async processUserMessage(session, timestamp) {
@@ -407,14 +359,18 @@ var MessageHandler = class {
407
359
  timestamp,
408
360
  channelId: session.channelId,
409
361
  selfId: session.selfId,
410
- elements: this.utils.cleanBase64Content(elements, false),
362
+ elements,
411
363
  type: "user",
412
364
  guildName,
413
365
  platform: session.platform || "unknown",
414
- quote: quoteInfo ? this.utils.cleanBase64Content(quoteInfo, false) : void 0,
366
+ quote: quoteInfo,
415
367
  isDirect: !!isDirect
416
368
  };
369
+ messageInfo.elements = await this.utils.cleanBase64ContentAsync(messageInfo.elements, false);
370
+ messageInfo.quote = messageInfo.quote ? await this.utils.cleanBase64ContentAsync(messageInfo.quote, false) : void 0;
417
371
  await this.fileManager.addMessageToFile(messageInfo);
372
+ const eventElements = await this.utils.cleanBase64ContentAsync(elements, false);
373
+ const eventQuote = quoteInfo ? await this.utils.cleanBase64ContentAsync(quoteInfo, false) : void 0;
418
374
  const messageEvent = {
419
375
  type: "message",
420
376
  selfId: session.selfId,
@@ -428,8 +384,8 @@ var MessageHandler = class {
428
384
  timestamp,
429
385
  guildName,
430
386
  channelType: session.type || 0,
431
- elements: this.utils.cleanBase64Content(elements, false),
432
- quote: quoteInfo ? this.utils.cleanBase64Content(quoteInfo, false) : void 0,
387
+ elements: eventElements,
388
+ quote: eventQuote,
433
389
  isDirect: session.isDirect,
434
390
  bot: {
435
391
  avatar: session.bot.user?.avatar,
@@ -457,9 +413,7 @@ var MessageHandler = class {
457
413
  const quoteMatch = content.match(/<quote id="([^"]+)"\/>/);
458
414
  if (quoteMatch) {
459
415
  const quoteId = quoteMatch[1];
460
- const data = this.fileManager.readChatDataFromFile();
461
- const channelKey = `${session.selfId}:${finalChannelId}`;
462
- const quotedMsg = data.messages[channelKey]?.find((m) => m.id === quoteId);
416
+ const quotedMsg = await this.fileManager.findChannelMessageById(session.selfId, finalChannelId, quoteId);
463
417
  if (quotedMsg) {
464
418
  const realId = quotedMsg.id.startsWith("bot-msg-") ? quotedMsg.realId : quotedMsg.id;
465
419
  quoteInfo = {
@@ -491,7 +445,7 @@ var MessageHandler = class {
491
445
  timestamp,
492
446
  channelId: finalChannelId,
493
447
  selfId: session.selfId,
494
- elements: this.utils.cleanBase64Content(session.event?.message?.elements, true),
448
+ elements: await this.utils.cleanBase64ContentAsync(session.event?.message?.elements, true),
495
449
  type: "bot",
496
450
  guildName,
497
451
  platform: session.platform || "unknown",
@@ -501,6 +455,7 @@ var MessageHandler = class {
501
455
  // 标记为正在发送
502
456
  };
503
457
  await this.fileManager.addMessageToFile(messageInfo);
458
+ const eventElements = await this.utils.cleanBase64ContentAsync(session.event?.message?.elements, true);
504
459
  const messageEvent = {
505
460
  type: "bot-message",
506
461
  selfId: session.selfId,
@@ -514,7 +469,7 @@ var MessageHandler = class {
514
469
  timestamp,
515
470
  guildName,
516
471
  channelType: session.event?.channel?.type || session.type || 0,
517
- elements: this.utils.cleanBase64Content(session.event?.message?.elements, true),
472
+ elements: eventElements,
518
473
  quote: quoteInfo,
519
474
  isDirect: !!isDirect,
520
475
  sending: true,
@@ -528,107 +483,322 @@ var MessageHandler = class {
528
483
  this.logger.error("处理机器人消息失败:", error);
529
484
  }
530
485
  }
531
- logInfo(...args) {
532
- if (this.config.loggerinfo) {
533
- this.logger.info(...args);
486
+ dispose() {
487
+ void this.utils.dispose();
488
+ for (const dispose of this.scheduledTasks) {
489
+ dispose();
490
+ }
491
+ this.scheduledTasks.clear();
492
+ }
493
+ scheduleTask(label, task) {
494
+ const dispose = this.ctx.setTimeout(() => {
495
+ this.scheduledTasks.delete(dispose);
496
+ void task().catch((error) => {
497
+ this.logger.error(`${label}失败:`, error);
498
+ });
499
+ }, 0);
500
+ this.scheduledTasks.add(dispose);
501
+ }
502
+ shouldRefreshChannelInfo(channelKey, existingChannel, isDirect, channelId, directUserName) {
503
+ if (this.channelRefreshInFlight.has(channelKey)) {
504
+ return false;
505
+ }
506
+ const lastRefresh = this.lastChannelRefreshAt.get(channelKey) || 0;
507
+ if (Date.now() - lastRefresh < this.CHANNEL_REFRESH_TTL_MS) {
508
+ return false;
509
+ }
510
+ if (isDirect) {
511
+ return !directUserName && (!existingChannel || existingChannel.name.includes("未知"));
512
+ }
513
+ return !existingChannel?.guildName || existingChannel.guildName === channelId;
514
+ }
515
+ async refreshChannelInfo(session, existingChannel, isDirect, directUserName) {
516
+ const channelKey = `${session.selfId}:${session.channelId}`;
517
+ this.channelRefreshInFlight.add(channelKey);
518
+ try {
519
+ let guildName = existingChannel?.guildName || session.channelId;
520
+ if (!isDirect) {
521
+ guildName = await this.resolveGuildName(session);
522
+ }
523
+ const finalName = this.buildChannelName(session, existingChannel, isDirect, directUserName, guildName);
524
+ const channelInfo = {
525
+ id: session.channelId,
526
+ name: finalName,
527
+ type: session.type || 0,
528
+ channelId: session.channelId,
529
+ guildName,
530
+ isDirect: !!isDirect
531
+ };
532
+ await this.fileManager.upsertChannelInfo(session.selfId, session.channelId, channelInfo);
533
+ this.lastChannelRefreshAt.set(channelKey, Date.now());
534
+ this.logger.logInfo("更新频道信息到文件:", channelInfo.name);
535
+ } finally {
536
+ this.channelRefreshInFlight.delete(channelKey);
537
+ }
538
+ }
539
+ async resolveGuildName(session) {
540
+ try {
541
+ if (session.guildId && session.bot.getGuild && typeof session.bot.getGuild === "function") {
542
+ const guild = await session.bot.getGuild(session.guildId);
543
+ return guild?.name || session.channelId;
544
+ }
545
+ if (session.guildId && session.bot.getChannel && typeof session.bot.getChannel === "function") {
546
+ const channel = await session.bot.getChannel(session.guildId);
547
+ return channel?.name || session.channelId;
548
+ }
549
+ } catch (error) {
550
+ this.logger.logInfo("获取频道信息失败,使用频道ID作为备用:", error);
551
+ }
552
+ return session.channelId;
553
+ }
554
+ buildChannelName(session, existingChannel, isDirect, directUserName, guildName) {
555
+ if (isDirect) {
556
+ if (directUserName && directUserName !== session.userId) {
557
+ return `私聊(${directUserName})`;
558
+ }
559
+ if (existingChannel?.name && !existingChannel.name.includes("未知")) {
560
+ return existingChannel.name;
561
+ }
562
+ if (session.platform && session.platform.toLowerCase().includes("sandbox")) {
563
+ return `私聊(${session.userId})`;
564
+ }
565
+ return "私聊(未知用户)";
566
+ }
567
+ return guildName || session.channelId;
568
+ }
569
+ async processMediaElements(elements, isUserMessage) {
570
+ for (const el of elements) {
571
+ if (["image", "img", "mface"].includes(el.type)) {
572
+ const src = el.attrs.src || el.attrs.url || el.attrs.file;
573
+ if (src && isUserMessage) {
574
+ try {
575
+ await this.downloadAndCacheMedia(src, "image");
576
+ } catch (error) {
577
+ this.logger.warn("缓存图片失败:", error);
578
+ }
579
+ }
580
+ } else if (el.type === "audio") {
581
+ const src = el.attrs.src || el.attrs.url || el.attrs.file;
582
+ if (src && isUserMessage) {
583
+ try {
584
+ await this.downloadAndCacheMedia(src, "media");
585
+ } catch (error) {
586
+ this.logger.warn("缓存语音失败:", error);
587
+ }
588
+ }
589
+ }
590
+ if (el.children?.length) {
591
+ await this.processMediaElements(el.children, isUserMessage);
592
+ }
593
+ }
594
+ }
595
+ async fileExists(filePath) {
596
+ try {
597
+ await import_node_fs2.promises.access(filePath);
598
+ return true;
599
+ } catch {
600
+ return false;
534
601
  }
535
602
  }
536
603
  };
537
604
 
538
605
  // src/file-manager.ts
539
- var import_node_path2 = __toESM(require("node:path"));
540
- var import_node_fs2 = __toESM(require("node:fs"));
606
+ var import_node_path3 = __toESM(require("node:path"));
607
+ var import_node_fs3 = require("node:fs");
541
608
  var FileManager = class {
542
- constructor(ctx, config) {
609
+ constructor(ctx, config, logger) {
543
610
  this.ctx = ctx;
544
611
  this.config = config;
545
- const baseDir = import_node_path2.default.resolve(ctx.baseDir, "data", "chat-patch");
546
- this.chatHistoryDir = import_node_path2.default.join(baseDir, "chat-history");
547
- this.metadataFilePath = import_node_path2.default.join(baseDir, "metadata.json");
548
- this.logger = ctx.logger("chat-patch");
549
- this.utils = new Utils(config);
550
- this.cleanupOldDataFiles(baseDir);
551
- this.memoryCache = this.readChatDataFromFile();
612
+ this.logger = logger;
613
+ const baseDir = import_node_path3.default.resolve(ctx.baseDir, "data", "chat-patch");
614
+ this.storageBaseDir = import_node_path3.default.join(baseDir, "v2");
615
+ this.chatHistoryDir = import_node_path3.default.join(this.storageBaseDir, "chat-history");
616
+ this.metadataFilePath = import_node_path3.default.join(this.storageBaseDir, "metadata.json");
617
+ this.utils = new Utils(config, ctx);
618
+ this.ctx.setTimeout(() => {
619
+ void this.cleanupLegacyStorage(baseDir);
620
+ }, 0);
552
621
  }
553
622
  static {
554
623
  __name(this, "FileManager");
555
624
  }
625
+ storageBaseDir;
556
626
  chatHistoryDir;
557
627
  // 聊天记录根目录
558
628
  metadataFilePath;
559
629
  // 元数据文件路径(存储bots、channels、pinned等信息)
560
- logger;
561
630
  utils;
562
- memoryCache = null;
631
+ memoryCache = this.createEmptyChatData();
632
+ channelMessagesCache = /* @__PURE__ */ new Map();
633
+ recentMessageIdsCache = /* @__PURE__ */ new Map();
634
+ pendingBotMessageIds = /* @__PURE__ */ new Map();
635
+ messageChunkLocationCache = /* @__PURE__ */ new Map();
636
+ dirtyChannelKeys = /* @__PURE__ */ new Set();
563
637
  pendingMessages = /* @__PURE__ */ new Map();
564
638
  // 按channelKey分组的待写入消息
565
639
  writeTimers = /* @__PURE__ */ new Map();
566
640
  // 每个频道独立的写入定时器
641
+ metadataLoadPromise = null;
642
+ channelLoadPromises = /* @__PURE__ */ new Map();
643
+ writeQueue = Promise.resolve();
644
+ disposed = false;
567
645
  WRITE_DEBOUNCE_MS = 1e3;
568
- // 清理旧版本的JSON文件
569
- cleanupOldDataFiles(baseDir) {
646
+ RECENT_MESSAGE_ID_CACHE_SIZE = 200;
647
+ async initialize() {
648
+ await this.ensureMetadataLoaded();
649
+ }
650
+ readChatDataFromFile() {
651
+ this.memoryCache.messages = this.getChannelMessagesCacheSnapshot();
652
+ return this.memoryCache;
653
+ }
654
+ getCachedChannelInfo(selfId, channelId) {
655
+ return this.memoryCache.channels[selfId]?.[channelId];
656
+ }
657
+ async readMetadataOnly() {
658
+ await this.ensureMetadataLoaded();
659
+ const { messages, ...metadata } = this.memoryCache;
660
+ return { ...metadata };
661
+ }
662
+ async upsertBotInfo(botInfo) {
663
+ await this.ensureMetadataLoaded();
664
+ const current = this.memoryCache.bots[botInfo.selfId];
665
+ if (current && this.isSameBotInfo(current, botInfo)) {
666
+ return;
667
+ }
668
+ this.memoryCache.bots[botInfo.selfId] = botInfo;
669
+ this.scheduleMetadataWrite();
670
+ }
671
+ async upsertChannelInfo(selfId, channelId, channelInfo) {
672
+ await this.ensureMetadataLoaded();
673
+ if (!this.memoryCache.channels[selfId]) {
674
+ this.memoryCache.channels[selfId] = {};
675
+ }
676
+ const current = this.memoryCache.channels[selfId][channelId];
677
+ if (current && this.isSameChannelInfo(current, channelInfo)) {
678
+ return;
679
+ }
680
+ this.memoryCache.channels[selfId][channelId] = channelInfo;
681
+ this.scheduleMetadataWrite();
682
+ }
683
+ async setPinnedBots(pinnedBots) {
684
+ await this.ensureMetadataLoaded();
685
+ this.memoryCache.pinnedBots = [...pinnedBots];
686
+ this.scheduleMetadataWrite();
687
+ }
688
+ async setPinnedChannels(pinnedChannels) {
689
+ await this.ensureMetadataLoaded();
690
+ this.memoryCache.pinnedChannels = [...pinnedChannels];
691
+ this.scheduleMetadataWrite();
692
+ }
693
+ // 清理旧版本数据目录
694
+ async cleanupLegacyStorage(baseDir) {
570
695
  const oldFiles = ["chat-data.json", "data.json", "messages.json"];
571
696
  for (const fileName of oldFiles) {
572
- const filePath = import_node_path2.default.join(baseDir, fileName);
573
- if (import_node_fs2.default.existsSync(filePath)) {
574
- try {
575
- import_node_fs2.default.unlinkSync(filePath);
576
- this.logger.info(`已删除旧版本数据文件: ${fileName}`);
577
- } catch (error) {
697
+ const filePath = import_node_path3.default.join(baseDir, fileName);
698
+ try {
699
+ await import_node_fs3.promises.unlink(filePath);
700
+ this.logger.logInfo(`已删除旧版本数据文件: ${fileName}`);
701
+ } catch (error) {
702
+ if (!this.isFileMissingError(error)) {
578
703
  this.logger.warn(`删除旧版本数据文件失败: ${fileName}`, error);
579
704
  }
580
705
  }
581
706
  }
582
- }
583
- // 确保目录存在
584
- ensureDir(dirPath) {
585
- if (!import_node_fs2.default.existsSync(dirPath)) {
586
- import_node_fs2.default.mkdirSync(dirPath, { recursive: true });
707
+ const legacyPaths = [
708
+ import_node_path3.default.join(baseDir, "metadata.json"),
709
+ import_node_path3.default.join(baseDir, "chat-history")
710
+ ];
711
+ for (const legacyPath of legacyPaths) {
712
+ try {
713
+ await import_node_fs3.promises.rm(legacyPath, { recursive: true, force: true });
714
+ this.logger.logInfo(`已清理旧版数据路径: ${legacyPath}`);
715
+ } catch (error) {
716
+ this.logger.warn(`清理旧版数据路径失败: ${legacyPath}`, error);
717
+ }
587
718
  }
588
719
  }
589
- // 获取频道消息文件路径
590
- getChannelFilePath(selfId, channelId) {
591
- const botDir = import_node_path2.default.join(this.chatHistoryDir, selfId);
592
- this.ensureDir(botDir);
593
- return import_node_path2.default.join(botDir, `${channelId}.json`);
720
+ // 确保目录存在
721
+ async ensureDir(dirPath) {
722
+ await import_node_fs3.promises.mkdir(dirPath, { recursive: true });
594
723
  }
595
724
  // 读取单个频道的消息(公共方法)
596
- readChannelMessages(selfId, channelId) {
597
- const filePath = this.getChannelFilePath(selfId, channelId);
598
- if (!import_node_fs2.default.existsSync(filePath)) {
599
- return [];
725
+ async readChannelMessages(selfId, channelId) {
726
+ await this.ensureMetadataLoaded();
727
+ const channelKey = `${selfId}:${channelId}`;
728
+ const cached = this.getCachedChannelMessages(channelKey);
729
+ if (cached) {
730
+ return cached;
600
731
  }
601
- try {
602
- const jsonData = import_node_fs2.default.readFileSync(filePath, "utf8");
603
- const messages = JSON.parse(jsonData);
604
- return Array.isArray(messages) ? messages : [];
605
- } catch (error) {
606
- this.logger.error(`读取频道消息失败 [${selfId}:${channelId}]:`, error);
607
- return [];
732
+ const existingPromise = this.channelLoadPromises.get(channelKey);
733
+ if (existingPromise) {
734
+ return existingPromise;
608
735
  }
609
- }
610
- // 写入单个频道的消息
611
- writeChannelMessages(selfId, channelId, messages) {
612
- const filePath = this.getChannelFilePath(selfId, channelId);
736
+ const loadPromise = this.loadChannelMessages(selfId, channelId);
737
+ this.channelLoadPromises.set(channelKey, loadPromise);
613
738
  try {
614
- const jsonData = JSON.stringify(messages, null, 2);
615
- import_node_fs2.default.writeFileSync(filePath, jsonData, "utf8");
616
- } catch (error) {
617
- this.logger.error(`写入频道消息失败 [${selfId}:${channelId}]:`, error);
739
+ return await loadPromise;
740
+ } finally {
741
+ this.channelLoadPromises.delete(channelKey);
618
742
  }
619
743
  }
620
- // 读取元数据(bots、channels、pinned等)
621
- readMetadata() {
622
- if (!import_node_fs2.default.existsSync(this.metadataFilePath)) {
744
+ async readChannelMessagesPage(selfId, channelId, limit, offset = 0) {
745
+ await this.ensureMetadataLoaded();
746
+ const indexData = await this.loadOrCreateChannelIndex(selfId, channelId);
747
+ if (limit <= 0 || offset >= indexData.totalMessages) {
623
748
  return {
624
- bots: {},
625
- channels: {},
626
- pinnedBots: [],
627
- pinnedChannels: []
749
+ messages: [],
750
+ total: indexData.totalMessages
628
751
  };
629
752
  }
753
+ const entry = this.createStoredChannelEntry(selfId, channelId);
754
+ const collected = [];
755
+ let skipped = 0;
756
+ for (let chunkIndex = indexData.chunks.length - 1; chunkIndex >= 0; chunkIndex -= 1) {
757
+ const chunk = indexData.chunks[chunkIndex];
758
+ const chunkMessages = await this.readChunkMessages(entry.channelDirPath, chunk.fileName);
759
+ for (let messageIndex = chunkMessages.length - 1; messageIndex >= 0; messageIndex -= 1) {
760
+ if (skipped < offset) {
761
+ skipped += 1;
762
+ continue;
763
+ }
764
+ collected.push(chunkMessages[messageIndex]);
765
+ if (collected.length >= limit) {
766
+ return {
767
+ messages: collected.reverse(),
768
+ total: indexData.totalMessages
769
+ };
770
+ }
771
+ }
772
+ }
773
+ return {
774
+ messages: collected.reverse(),
775
+ total: indexData.totalMessages
776
+ };
777
+ }
778
+ async findChannelMessageById(selfId, channelId, messageId) {
779
+ await this.ensureMetadataLoaded();
780
+ const channelKey = `${selfId}:${channelId}`;
781
+ const cachedMessages = this.peekCachedChannelMessages(channelKey);
782
+ const cachedMatched = cachedMessages?.find((message) => message.id === messageId);
783
+ if (cachedMatched) {
784
+ return cachedMatched;
785
+ }
786
+ const entry = this.createStoredChannelEntry(selfId, channelId);
787
+ const indexData = await this.loadOrCreateChannelIndex(selfId, channelId);
788
+ for (let chunkIndex = indexData.chunks.length - 1; chunkIndex >= 0; chunkIndex -= 1) {
789
+ const chunk = indexData.chunks[chunkIndex];
790
+ const messages = await this.readChunkMessages(entry.channelDirPath, chunk.fileName);
791
+ const matched = messages.find((message) => message.id === messageId);
792
+ if (matched) {
793
+ return matched;
794
+ }
795
+ }
796
+ return null;
797
+ }
798
+ // 读取元数据(bots、channels、pinned等)
799
+ async readMetadata() {
630
800
  try {
631
- const jsonData = import_node_fs2.default.readFileSync(this.metadataFilePath, "utf8");
801
+ const jsonData = await import_node_fs3.promises.readFile(this.metadataFilePath, "utf8");
632
802
  const data = JSON.parse(jsonData);
633
803
  return {
634
804
  bots: data.bots || {},
@@ -638,6 +808,14 @@ var FileManager = class {
638
808
  lastSaveTime: data.lastSaveTime
639
809
  };
640
810
  } catch (error) {
811
+ if (this.isFileMissingError(error)) {
812
+ return {
813
+ bots: {},
814
+ channels: {},
815
+ pinnedBots: [],
816
+ pinnedChannels: []
817
+ };
818
+ }
641
819
  this.logger.error("读取元数据失败:", error);
642
820
  return {
643
821
  bots: {},
@@ -647,92 +825,20 @@ var FileManager = class {
647
825
  };
648
826
  }
649
827
  }
650
- // 只读取元数据,不加载消息(公共方法)
651
- readMetadataOnly() {
652
- return this.readMetadata();
653
- }
654
828
  // 写入元数据
655
- writeMetadata(metadata) {
829
+ async writeMetadata(metadata) {
656
830
  try {
657
- this.ensureDir(import_node_path2.default.dirname(this.metadataFilePath));
831
+ await this.ensureDir(import_node_path3.default.dirname(this.metadataFilePath));
658
832
  const dataToWrite = {
659
833
  ...metadata,
660
834
  lastSaveTime: Date.now()
661
835
  };
662
836
  const jsonData = JSON.stringify(dataToWrite, null, 2);
663
- import_node_fs2.default.writeFileSync(this.metadataFilePath, jsonData, "utf8");
837
+ await import_node_fs3.promises.writeFile(this.metadataFilePath, jsonData, "utf8");
664
838
  } catch (error) {
665
839
  this.logger.error("写入元数据失败:", error);
666
840
  }
667
841
  }
668
- // 扫描所有频道消息文件并加载到内存
669
- loadAllChannelMessages() {
670
- const messages = {};
671
- if (!import_node_fs2.default.existsSync(this.chatHistoryDir)) {
672
- return messages;
673
- }
674
- try {
675
- const botDirs = import_node_fs2.default.readdirSync(this.chatHistoryDir);
676
- for (const botId of botDirs) {
677
- const botDir = import_node_path2.default.join(this.chatHistoryDir, botId);
678
- const stat = import_node_fs2.default.statSync(botDir);
679
- if (!stat.isDirectory()) continue;
680
- const channelFiles = import_node_fs2.default.readdirSync(botDir);
681
- for (const fileName of channelFiles) {
682
- if (!fileName.endsWith(".json")) continue;
683
- const channelId = fileName.replace(".json", "");
684
- const channelKey = `${botId}:${channelId}`;
685
- messages[channelKey] = this.readChannelMessages(botId, channelId);
686
- }
687
- }
688
- } catch (error) {
689
- this.logger.error("加载频道消息失败:", error);
690
- }
691
- return messages;
692
- }
693
- readChatDataFromFile() {
694
- if (this.memoryCache) {
695
- return this.memoryCache;
696
- }
697
- process.nextTick(() => {
698
- try {
699
- const metadata = this.readMetadata();
700
- const messages = this.loadAllChannelMessages();
701
- this.memoryCache = {
702
- ...metadata,
703
- messages
704
- };
705
- } catch (error) {
706
- this.logger.error("读取聊天数据失败:", error);
707
- }
708
- });
709
- this.memoryCache = {
710
- bots: {},
711
- channels: {},
712
- messages: {},
713
- pinnedBots: [],
714
- pinnedChannels: []
715
- };
716
- return this.memoryCache;
717
- }
718
- writeChatDataToFile(data) {
719
- data.lastSaveTime = Date.now();
720
- this.memoryCache = data;
721
- process.nextTick(() => {
722
- try {
723
- const { messages, ...metadata } = data;
724
- this.writeMetadata(metadata);
725
- for (const [channelKey, channelMessages] of Object.entries(messages)) {
726
- const [selfId, channelId] = channelKey.split(":");
727
- if (selfId && channelId) {
728
- this.writeChannelMessages(selfId, channelId, channelMessages);
729
- }
730
- }
731
- } catch (error) {
732
- this.logger.error("写入聊天数据失败:", error);
733
- }
734
- });
735
- }
736
842
  // 为特定频道安排写入
737
843
  scheduleWrite(channelKey) {
738
844
  const existingTimer = this.writeTimers.get(channelKey);
@@ -740,42 +846,32 @@ var FileManager = class {
740
846
  existingTimer();
741
847
  }
742
848
  const timer = this.ctx.setTimeout(() => {
743
- process.nextTick(() => {
744
- this.flushPendingMessages(channelKey);
745
- });
746
849
  this.writeTimers.delete(channelKey);
850
+ void this.flushPendingMessages(channelKey);
747
851
  }, this.WRITE_DEBOUNCE_MS);
748
852
  this.writeTimers.set(channelKey, timer);
749
853
  }
750
854
  // 刷新特定频道的待写入消息
751
- flushPendingMessages(channelKey) {
855
+ async flushPendingMessages(channelKey) {
752
856
  const messagesToWrite = this.pendingMessages.get(channelKey);
753
857
  if (!messagesToWrite || messagesToWrite.length === 0) return;
754
858
  this.pendingMessages.delete(channelKey);
755
- const data = this.memoryCache || this.readChatDataFromFile();
859
+ const cachedMessages = this.peekCachedChannelMessages(channelKey);
756
860
  const [selfId, channelId] = channelKey.split(":");
757
861
  if (!selfId || !channelId) return;
758
- if (!data.messages[channelKey]) {
759
- data.messages[channelKey] = [];
760
- }
761
- for (const messageInfo of messagesToWrite) {
762
- const existingMessage = data.messages[channelKey].find((m) => m.id === messageInfo.id);
763
- if (existingMessage) {
764
- continue;
765
- }
766
- if (!messageInfo.timestamp) {
767
- messageInfo.timestamp = Date.now();
768
- }
769
- const cleanedMessageInfo = this.utils.cleanBase64Content(messageInfo);
770
- data.messages[channelKey].push(cleanedMessageInfo);
862
+ const uniqueMessages = this.deduplicateMessages(messagesToWrite);
863
+ if (!uniqueMessages.length) {
864
+ return;
771
865
  }
772
- if (data.messages[channelKey].length > this.config.maxMessagesPerChannel) {
773
- data.messages[channelKey].sort((a, b) => a.timestamp - b.timestamp);
774
- data.messages[channelKey] = data.messages[channelKey].slice(-this.config.maxMessagesPerChannel);
866
+ if (cachedMessages) {
867
+ const nextMessages = this.mergeChannelMessages(cachedMessages, uniqueMessages);
868
+ this.setCachedChannelMessages(channelKey, this.limitChannelMessages(nextMessages));
775
869
  }
776
- this.writeChannelMessages(selfId, channelId, data.messages[channelKey]);
777
- this.memoryCache = data;
778
- this.logInfo(`批量写入 ${messagesToWrite.length} 条消息到频道 ${channelKey}`);
870
+ this.rememberRecentMessageIds(channelKey, uniqueMessages.map((message) => message.id));
871
+ this.enqueueWrite(async () => {
872
+ await this.appendMessagesToChannel(selfId, channelId, uniqueMessages);
873
+ this.logger.logInfo(`批量写入 ${uniqueMessages.length} 条消息到频道 ${channelKey}`);
874
+ });
779
875
  }
780
876
  cleanExcessMessages(data) {
781
877
  let cleanedCount = 0;
@@ -786,13 +882,13 @@ var FileManager = class {
786
882
  const keptMessages = sortedMessages.slice(-this.config.maxMessagesPerChannel);
787
883
  cleanedCount += messages.length - keptMessages.length;
788
884
  cleanedMessages[channelKey] = keptMessages;
789
- this.logInfo(`频道 ${channelKey} 清理了 ${messages.length - keptMessages.length} 条旧消息,保留最新 ${keptMessages.length} 条`);
885
+ this.logger.logInfo(`频道 ${channelKey} 清理了 ${messages.length - keptMessages.length} 条旧消息,保留最新 ${keptMessages.length} 条`);
790
886
  } else {
791
887
  cleanedMessages[channelKey] = messages;
792
888
  }
793
889
  }
794
890
  if (cleanedCount > 0) {
795
- this.logInfo("总共清理超量消息:", cleanedCount, "条");
891
+ this.logger.logInfo("总共清理超量消息:", cleanedCount, "条");
796
892
  }
797
893
  return {
798
894
  ...data,
@@ -800,61 +896,838 @@ var FileManager = class {
800
896
  };
801
897
  }
802
898
  async addMessageToFile(messageInfo) {
899
+ await this.ensureMetadataLoaded();
803
900
  const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`;
804
- if (!this.pendingMessages.has(channelKey)) {
805
- this.pendingMessages.set(channelKey, []);
901
+ if (!messageInfo.timestamp) {
902
+ messageInfo.timestamp = Date.now();
806
903
  }
807
- this.pendingMessages.get(channelKey).push(messageInfo);
808
- const data = this.memoryCache || this.readChatDataFromFile();
809
- if (!data.messages[channelKey]) {
810
- data.messages[channelKey] = [];
904
+ const cleanedMessageInfo = await this.utils.cleanBase64ContentAsync(messageInfo, false);
905
+ const pendingMessages = this.pendingMessages.get(channelKey) || [];
906
+ if (pendingMessages.some((message) => message.id === cleanedMessageInfo.id)) {
907
+ return;
811
908
  }
812
- const existingMessage = data.messages[channelKey].find((m) => m.id === messageInfo.id);
813
- if (!existingMessage) {
814
- if (!messageInfo.timestamp) {
815
- messageInfo.timestamp = Date.now();
909
+ const cachedMessages = this.peekCachedChannelMessages(channelKey);
910
+ if (cachedMessages?.some((message) => message.id === cleanedMessageInfo.id)) {
911
+ return;
912
+ }
913
+ if (!cachedMessages) {
914
+ if (this.hasRecentMessageId(channelKey, cleanedMessageInfo.id)) {
915
+ return;
816
916
  }
817
- const cleanedMessageInfo = this.utils.cleanBase64Content(messageInfo);
818
- data.messages[channelKey].push(cleanedMessageInfo);
819
- if (data.messages[channelKey].length > this.config.maxMessagesPerChannel) {
820
- data.messages[channelKey].sort((a, b) => a.timestamp - b.timestamp);
821
- data.messages[channelKey] = data.messages[channelKey].slice(-this.config.maxMessagesPerChannel);
917
+ const existsInStorage = await this.channelMessageExists(messageInfo.selfId, messageInfo.channelId, cleanedMessageInfo.id);
918
+ if (existsInStorage) {
919
+ return;
822
920
  }
823
- this.memoryCache = data;
921
+ }
922
+ pendingMessages.push(cleanedMessageInfo);
923
+ this.pendingMessages.set(channelKey, pendingMessages);
924
+ this.rememberRecentMessageIds(channelKey, [cleanedMessageInfo.id]);
925
+ if (cleanedMessageInfo.type === "bot" && cleanedMessageInfo.sending) {
926
+ this.registerPendingBotMessage(channelKey, cleanedMessageInfo.id);
927
+ }
928
+ if (cachedMessages) {
929
+ const nextMessages = this.mergeChannelMessages(cachedMessages, [cleanedMessageInfo]);
930
+ this.setCachedChannelMessages(channelKey, this.limitChannelMessages(nextMessages));
824
931
  }
825
932
  this.scheduleWrite(channelKey);
826
933
  }
827
- dispose() {
934
+ async cleanupExcessMessagesInStorage() {
935
+ let cleanedCount = 0;
936
+ for (const channelKey of [...this.dirtyChannelKeys]) {
937
+ const [selfId, channelId] = channelKey.split(":");
938
+ if (!selfId || !channelId) {
939
+ this.dirtyChannelKeys.delete(channelKey);
940
+ continue;
941
+ }
942
+ const indexData = await this.loadOrCreateChannelIndex(selfId, channelId);
943
+ if (indexData.totalMessages <= this.config.maxMessagesPerChannel) {
944
+ this.dirtyChannelKeys.delete(channelKey);
945
+ continue;
946
+ }
947
+ const removedCount = await this.trimChannelToLimit(selfId, channelId, indexData, this.config.maxMessagesPerChannel);
948
+ if (!removedCount) {
949
+ continue;
950
+ }
951
+ cleanedCount += removedCount;
952
+ if (this.peekCachedChannelMessages(channelKey)) {
953
+ this.setCachedChannelMessages(channelKey, await this.loadChannelMessagesNoCache(selfId, channelId));
954
+ }
955
+ this.dirtyChannelKeys.delete(channelKey);
956
+ this.logger.logInfo(`频道 ${channelKey} 清理了 ${removedCount} 条旧消息,保留最新 ${indexData.totalMessages} 条`);
957
+ }
958
+ if (cleanedCount > 0) {
959
+ this.logger.logInfo("定期清理完成,清理了", cleanedCount, "条超量消息");
960
+ }
961
+ }
962
+ async getAllChannelMessageCounts() {
963
+ const counts = {};
964
+ await this.scanStoredChannels(async (entry) => {
965
+ counts[entry.channelKey] = await this.countChannelMessages(entry.selfId, entry.channelId);
966
+ });
967
+ return counts;
968
+ }
969
+ async deleteChannelData(selfId, channelId) {
970
+ await this.ensureMetadataLoaded();
971
+ const channelKey = `${selfId}:${channelId}`;
972
+ const deletedMessages = await this.countChannelMessages(selfId, channelId);
973
+ this.deleteCachedChannelMessages(channelKey);
974
+ this.deleteRecentMessageIds(channelKey);
975
+ this.deletePendingBotMessages(channelKey);
976
+ this.deleteMessageChunkLocations(channelKey);
977
+ this.dirtyChannelKeys.delete(channelKey);
978
+ this.pendingMessages.delete(channelKey);
979
+ const timer = this.writeTimers.get(channelKey);
980
+ if (timer) {
981
+ timer();
982
+ this.writeTimers.delete(channelKey);
983
+ }
984
+ if (this.memoryCache.channels[selfId]?.[channelId]) {
985
+ delete this.memoryCache.channels[selfId][channelId];
986
+ if (!Object.keys(this.memoryCache.channels[selfId]).length) {
987
+ delete this.memoryCache.channels[selfId];
988
+ }
989
+ }
990
+ this.memoryCache.pinnedChannels = this.memoryCache.pinnedChannels.filter((item) => item !== channelKey);
991
+ await this.removeChannelStorage(selfId, channelId);
992
+ this.scheduleMetadataWrite();
993
+ return { deletedMessages };
994
+ }
995
+ async deleteBotData(selfId) {
996
+ await this.ensureMetadataLoaded();
997
+ const botDir = import_node_path3.default.join(this.chatHistoryDir, selfId);
998
+ const channels = await this.listBotChannels(selfId);
999
+ let deletedMessages = 0;
1000
+ for (const entry of channels) {
1001
+ deletedMessages += await this.countChannelMessages(entry.selfId, entry.channelId);
1002
+ this.deleteCachedChannelMessages(entry.channelKey);
1003
+ this.deleteRecentMessageIds(entry.channelKey);
1004
+ this.deletePendingBotMessages(entry.channelKey);
1005
+ this.deleteMessageChunkLocations(entry.channelKey);
1006
+ this.dirtyChannelKeys.delete(entry.channelKey);
1007
+ this.pendingMessages.delete(entry.channelKey);
1008
+ const timer = this.writeTimers.get(entry.channelKey);
1009
+ if (timer) {
1010
+ timer();
1011
+ this.writeTimers.delete(entry.channelKey);
1012
+ }
1013
+ }
1014
+ const deletedChannels = channels.length;
1015
+ delete this.memoryCache.bots[selfId];
1016
+ delete this.memoryCache.channels[selfId];
1017
+ this.memoryCache.pinnedBots = this.memoryCache.pinnedBots.filter((item) => item !== selfId);
1018
+ this.memoryCache.pinnedChannels = this.memoryCache.pinnedChannels.filter((item) => !item.startsWith(`${selfId}:`));
1019
+ try {
1020
+ await import_node_fs3.promises.rm(botDir, { recursive: true, force: true });
1021
+ } catch (error) {
1022
+ this.logger.error(`删除机器人目录失败 [${selfId}]:`, error);
1023
+ }
1024
+ this.scheduleMetadataWrite();
1025
+ return {
1026
+ deletedChannels,
1027
+ deletedMessages
1028
+ };
1029
+ }
1030
+ async updateUserProfileInBotData(selfId, userId, userName, avatar) {
1031
+ await this.ensureMetadataLoaded();
1032
+ let changed = false;
1033
+ const botChannels = this.memoryCache.channels[selfId] || {};
1034
+ const possibleChannelIds = [
1035
+ userId,
1036
+ `private:${userId}`,
1037
+ `direct:${userId}`
1038
+ ];
1039
+ for (const channelId of possibleChannelIds) {
1040
+ const channel = botChannels[channelId];
1041
+ if (!channel || !channel.isDirect || !userName) {
1042
+ continue;
1043
+ }
1044
+ const newName = `私聊(${userName})`;
1045
+ if (channel.name !== newName) {
1046
+ channel.name = newName;
1047
+ changed = true;
1048
+ }
1049
+ }
1050
+ const channels = await this.listBotChannels(selfId);
1051
+ for (const entry of channels) {
1052
+ const entryChanged = await this.updateUserProfileInChannel(entry.selfId, entry.channelId, userId, userName, avatar);
1053
+ if (!entryChanged) {
1054
+ continue;
1055
+ }
1056
+ if (this.peekCachedChannelMessages(entry.channelKey)) {
1057
+ this.setCachedChannelMessages(entry.channelKey, await this.loadChannelMessagesNoCache(entry.selfId, entry.channelId));
1058
+ }
1059
+ changed = true;
1060
+ }
1061
+ if (changed) {
1062
+ this.scheduleMetadataWrite();
1063
+ }
1064
+ return changed;
1065
+ }
1066
+ async markLatestBotMessageAsSent(selfId, channelId, realId) {
1067
+ const channelKey = `${selfId}:${channelId}`;
1068
+ const tempMessageId = this.peekLatestPendingBotMessageId(channelKey);
1069
+ let matched;
1070
+ if (tempMessageId) {
1071
+ matched = await this.findAndUpdateBotMessageByTempId(selfId, channelId, tempMessageId, realId);
1072
+ this.consumePendingBotMessageId(channelKey, tempMessageId);
1073
+ }
1074
+ if (!matched) {
1075
+ matched = await this.findAndUpdateLatestBotMessage(selfId, channelId, realId);
1076
+ }
1077
+ if (!matched) {
1078
+ return void 0;
1079
+ }
1080
+ const cachedMessages = this.peekCachedChannelMessages(channelKey);
1081
+ if (cachedMessages) {
1082
+ const cachedMatched = cachedMessages.find((message) => message.id === matched?.id);
1083
+ if (cachedMatched) {
1084
+ cachedMatched.realId = realId;
1085
+ cachedMatched.sending = false;
1086
+ this.setCachedChannelMessages(channelKey, cachedMessages);
1087
+ }
1088
+ }
1089
+ return matched;
1090
+ }
1091
+ async dispose() {
1092
+ this.disposed = true;
828
1093
  for (const [channelKey, timer] of this.writeTimers.entries()) {
829
1094
  timer();
830
- this.flushPendingMessages(channelKey);
1095
+ await this.flushPendingMessages(channelKey);
831
1096
  }
832
1097
  this.writeTimers.clear();
1098
+ await this.writeQueue;
1099
+ await this.utils.dispose();
833
1100
  }
834
- logInfo(...args) {
835
- if (this.config.loggerinfo) {
836
- this.logger.info(...args);
1101
+ createEmptyChatData() {
1102
+ return {
1103
+ bots: {},
1104
+ channels: {},
1105
+ messages: {},
1106
+ pinnedBots: [],
1107
+ pinnedChannels: []
1108
+ };
1109
+ }
1110
+ async ensureMetadataLoaded() {
1111
+ if (!this.metadataLoadPromise) {
1112
+ this.metadataLoadPromise = this.loadMetadataIntoCache();
1113
+ }
1114
+ await this.metadataLoadPromise;
1115
+ }
1116
+ async loadMetadataIntoCache() {
1117
+ const metadata = await this.readMetadata();
1118
+ this.memoryCache = {
1119
+ ...metadata,
1120
+ messages: this.getChannelMessagesCacheSnapshot()
1121
+ };
1122
+ }
1123
+ async loadChannelMessages(selfId, channelId) {
1124
+ const messages = await this.loadChannelMessagesNoCache(selfId, channelId);
1125
+ this.setCachedChannelMessages(`${selfId}:${channelId}`, messages);
1126
+ return messages;
1127
+ }
1128
+ async loadChannelMessagesNoCache(selfId, channelId) {
1129
+ const indexData = await this.loadOrCreateChannelIndex(selfId, channelId);
1130
+ const channelDirPath = this.getChannelDirPath(selfId, channelId);
1131
+ const messages = [];
1132
+ const channelKey = `${selfId}:${channelId}`;
1133
+ for (const chunk of indexData.chunks) {
1134
+ const chunkMessages = await this.readChunkMessages(channelDirPath, chunk.fileName);
1135
+ messages.push(...chunkMessages);
1136
+ this.rememberMessageChunkLocation(channelKey, chunk.fileName, chunkMessages.map((message) => message.id));
1137
+ }
1138
+ this.rememberRecentMessageIds(channelKey, messages.slice(-this.getRecentMessageIdCacheLimit()).map((message) => message.id));
1139
+ return messages;
1140
+ }
1141
+ scheduleMetadataWrite() {
1142
+ this.enqueueWrite(async () => {
1143
+ const { messages, ...metadata } = this.readChatDataFromFile();
1144
+ await this.writeMetadata(metadata);
1145
+ });
1146
+ }
1147
+ enqueueWrite(task) {
1148
+ if (this.disposed) {
1149
+ return;
1150
+ }
1151
+ this.writeQueue = this.writeQueue.then(task).catch((error) => {
1152
+ this.logger.error("写入任务失败:", error);
1153
+ });
1154
+ }
1155
+ async listStoredChannels() {
1156
+ const result = [];
1157
+ await this.scanStoredChannels(async (entry) => {
1158
+ result.push(entry);
1159
+ });
1160
+ return result;
1161
+ }
1162
+ async listBotChannels(selfId) {
1163
+ const result = [];
1164
+ await this.scanStoredChannels(async (entry) => {
1165
+ if (entry.selfId === selfId) {
1166
+ result.push(entry);
1167
+ }
1168
+ }, selfId);
1169
+ return result;
1170
+ }
1171
+ async scanStoredChannels(visitor, botIdFilter) {
1172
+ try {
1173
+ const botDirs = await import_node_fs3.promises.readdir(this.chatHistoryDir, { withFileTypes: true });
1174
+ for (const botEntry of botDirs) {
1175
+ const botName = this.normalizeDirentName(botEntry.name);
1176
+ if (!botEntry.isDirectory()) continue;
1177
+ if (botIdFilter && botName !== botIdFilter) continue;
1178
+ const botDir = import_node_path3.default.join(this.chatHistoryDir, botName);
1179
+ const channelEntries = await import_node_fs3.promises.readdir(botDir, { withFileTypes: true });
1180
+ for (const channelEntry of channelEntries) {
1181
+ const channelId = await this.resolveStoredChannelId(botName, channelEntry);
1182
+ if (!channelId) {
1183
+ continue;
1184
+ }
1185
+ await visitor(this.createStoredChannelEntry(botName, channelId));
1186
+ }
1187
+ }
1188
+ } catch (error) {
1189
+ if (!this.isFileMissingError(error)) {
1190
+ this.logger.error("扫描频道文件失败:", error);
1191
+ }
1192
+ }
1193
+ }
1194
+ createStoredChannelEntry(selfId, channelId) {
1195
+ const channelDirPath = this.getChannelDirPath(selfId, channelId);
1196
+ return {
1197
+ selfId,
1198
+ channelId,
1199
+ channelKey: `${selfId}:${channelId}`,
1200
+ channelDirPath,
1201
+ indexFilePath: this.getChannelIndexPath(selfId, channelId)
1202
+ };
1203
+ }
1204
+ async resolveStoredChannelId(selfId, entry) {
1205
+ const entryName = this.normalizeDirentName(entry.name);
1206
+ if (!entry.isDirectory()) {
1207
+ return void 0;
1208
+ }
1209
+ const encodedChannelId = entryName;
1210
+ const channelDirPath = import_node_path3.default.join(this.chatHistoryDir, selfId, encodedChannelId);
1211
+ const indexFilePath = import_node_path3.default.join(channelDirPath, "index.json");
1212
+ try {
1213
+ const jsonData = await import_node_fs3.promises.readFile(indexFilePath, "utf8");
1214
+ const indexData = JSON.parse(jsonData);
1215
+ if (typeof indexData.channelId === "string") {
1216
+ return indexData.channelId;
1217
+ }
1218
+ } catch (error) {
1219
+ if (!this.isFileMissingError(error)) {
1220
+ this.logger.error(`读取频道索引失败 [${selfId}:${entryName}]:`, error);
1221
+ }
837
1222
  }
1223
+ return this.decodeChannelId(encodedChannelId);
1224
+ }
1225
+ getBotDirPath(selfId) {
1226
+ return import_node_path3.default.join(this.chatHistoryDir, selfId);
1227
+ }
1228
+ getEncodedChannelId(channelId) {
1229
+ return encodeURIComponent(channelId);
1230
+ }
1231
+ decodeChannelId(encodedChannelId) {
1232
+ try {
1233
+ return decodeURIComponent(encodedChannelId);
1234
+ } catch {
1235
+ return encodedChannelId;
1236
+ }
1237
+ }
1238
+ normalizeDirentName(name2) {
1239
+ return typeof name2 === "string" ? name2 : name2.toString("utf8");
1240
+ }
1241
+ getChannelDirPath(selfId, channelId) {
1242
+ return import_node_path3.default.join(this.getBotDirPath(selfId), this.getEncodedChannelId(channelId));
1243
+ }
1244
+ getChannelIndexPath(selfId, channelId) {
1245
+ return import_node_path3.default.join(this.getChannelDirPath(selfId, channelId), "index.json");
1246
+ }
1247
+ getChunkFilePath(channelDirPath, fileName) {
1248
+ return import_node_path3.default.join(channelDirPath, fileName);
1249
+ }
1250
+ createEmptyChannelIndex(channelId) {
1251
+ return {
1252
+ version: 1,
1253
+ channelId,
1254
+ totalMessages: 0,
1255
+ nextChunkId: 1,
1256
+ chunks: []
1257
+ };
1258
+ }
1259
+ createChunkFileName(chunkId) {
1260
+ return `chunk-${String(chunkId).padStart(6, "0")}.json`;
1261
+ }
1262
+ async loadOrCreateChannelIndex(selfId, channelId) {
1263
+ const entry = this.createStoredChannelEntry(selfId, channelId);
1264
+ try {
1265
+ const jsonData = await import_node_fs3.promises.readFile(entry.indexFilePath, "utf8");
1266
+ const indexData2 = JSON.parse(jsonData);
1267
+ const normalizedIndexData = this.normalizeChannelIndex(channelId, indexData2);
1268
+ this.syncDirtyChannelState(entry.channelKey, normalizedIndexData.totalMessages);
1269
+ return normalizedIndexData;
1270
+ } catch (error) {
1271
+ if (!this.isFileMissingError(error)) {
1272
+ this.logger.error(`读取频道索引失败 [${entry.channelKey}]:`, error);
1273
+ }
1274
+ }
1275
+ const indexData = this.createEmptyChannelIndex(channelId);
1276
+ await this.ensureDir(entry.channelDirPath);
1277
+ await this.writeChannelIndex(entry.indexFilePath, indexData);
1278
+ this.syncDirtyChannelState(entry.channelKey, indexData.totalMessages);
1279
+ return indexData;
1280
+ }
1281
+ normalizeChannelIndex(channelId, indexData) {
1282
+ return {
1283
+ version: 1,
1284
+ channelId,
1285
+ totalMessages: indexData.totalMessages || 0,
1286
+ nextChunkId: indexData.nextChunkId || (indexData.chunks?.length || 0) + 1,
1287
+ chunks: Array.isArray(indexData.chunks) ? indexData.chunks : []
1288
+ };
1289
+ }
1290
+ async writeChannelIndex(indexFilePath, indexData) {
1291
+ await this.ensureDir(import_node_path3.default.dirname(indexFilePath));
1292
+ await import_node_fs3.promises.writeFile(indexFilePath, JSON.stringify(indexData, null, 2), "utf8");
1293
+ }
1294
+ async readChunkMessages(channelDirPath, fileName) {
1295
+ try {
1296
+ const jsonData = await import_node_fs3.promises.readFile(this.getChunkFilePath(channelDirPath, fileName), "utf8");
1297
+ const messages = JSON.parse(jsonData);
1298
+ return Array.isArray(messages) ? messages : [];
1299
+ } catch (error) {
1300
+ if (!this.isFileMissingError(error)) {
1301
+ this.logger.error(`读取消息分块失败 [${channelDirPath}/${fileName}]:`, error);
1302
+ }
1303
+ return [];
1304
+ }
1305
+ }
1306
+ async writeChunkMessages(channelDirPath, fileName, messages) {
1307
+ await this.ensureDir(channelDirPath);
1308
+ await import_node_fs3.promises.writeFile(this.getChunkFilePath(channelDirPath, fileName), JSON.stringify(messages, null, 2), "utf8");
1309
+ }
1310
+ async writeMessagesToChunks(channelDirPath, indexData, messages) {
1311
+ const oldChunks = [...indexData.chunks];
1312
+ indexData.chunks = [];
1313
+ indexData.totalMessages = 0;
1314
+ indexData.nextChunkId = 1;
1315
+ for (let offset = 0; offset < messages.length; offset += this.config.messageChunkSize) {
1316
+ const chunkMessages = messages.slice(offset, offset + this.config.messageChunkSize);
1317
+ const chunkId = indexData.nextChunkId;
1318
+ const fileName = this.createChunkFileName(chunkId);
1319
+ await this.writeChunkMessages(channelDirPath, fileName, chunkMessages);
1320
+ indexData.chunks.push({ id: chunkId, fileName, messageCount: chunkMessages.length });
1321
+ indexData.nextChunkId += 1;
1322
+ indexData.totalMessages += chunkMessages.length;
1323
+ }
1324
+ for (const chunk of oldChunks) {
1325
+ try {
1326
+ await import_node_fs3.promises.unlink(this.getChunkFilePath(channelDirPath, chunk.fileName));
1327
+ } catch (error) {
1328
+ if (!this.isFileMissingError(error)) {
1329
+ this.logger.warn(`删除旧消息分块失败 [${channelDirPath}/${chunk.fileName}]:`, error);
1330
+ }
1331
+ }
1332
+ }
1333
+ await this.writeChannelIndex(import_node_path3.default.join(channelDirPath, "index.json"), indexData);
1334
+ }
1335
+ async appendMessagesToChannel(selfId, channelId, messages) {
1336
+ if (!messages.length) {
1337
+ return;
1338
+ }
1339
+ const entry = this.createStoredChannelEntry(selfId, channelId);
1340
+ const indexData = await this.loadOrCreateChannelIndex(selfId, channelId);
1341
+ await this.ensureDir(entry.channelDirPath);
1342
+ let remainingMessages = [...messages];
1343
+ const lastChunk = indexData.chunks[indexData.chunks.length - 1];
1344
+ if (lastChunk && lastChunk.messageCount < this.config.messageChunkSize) {
1345
+ const chunkMessages = await this.readChunkMessages(entry.channelDirPath, lastChunk.fileName);
1346
+ const writableCount = this.config.messageChunkSize - chunkMessages.length;
1347
+ const appendMessages = remainingMessages.slice(0, writableCount);
1348
+ if (appendMessages.length) {
1349
+ chunkMessages.push(...appendMessages);
1350
+ lastChunk.messageCount = chunkMessages.length;
1351
+ indexData.totalMessages += appendMessages.length;
1352
+ remainingMessages = remainingMessages.slice(appendMessages.length);
1353
+ await this.writeChunkMessages(entry.channelDirPath, lastChunk.fileName, chunkMessages);
1354
+ this.rememberMessageChunkLocation(entry.channelKey, lastChunk.fileName, appendMessages.map((message) => message.id));
1355
+ }
1356
+ }
1357
+ while (remainingMessages.length) {
1358
+ const chunkMessages = remainingMessages.slice(0, this.config.messageChunkSize);
1359
+ const chunkId = indexData.nextChunkId;
1360
+ const fileName = this.createChunkFileName(chunkId);
1361
+ await this.writeChunkMessages(entry.channelDirPath, fileName, chunkMessages);
1362
+ indexData.chunks.push({ id: chunkId, fileName, messageCount: chunkMessages.length });
1363
+ indexData.nextChunkId += 1;
1364
+ indexData.totalMessages += chunkMessages.length;
1365
+ this.rememberMessageChunkLocation(entry.channelKey, fileName, chunkMessages.map((message) => message.id));
1366
+ remainingMessages = remainingMessages.slice(chunkMessages.length);
1367
+ }
1368
+ this.syncDirtyChannelState(entry.channelKey, indexData.totalMessages);
1369
+ await this.writeChannelIndex(entry.indexFilePath, indexData);
1370
+ }
1371
+ async trimChannelToLimit(selfId, channelId, indexData, limit) {
1372
+ const entry = this.createStoredChannelEntry(selfId, channelId);
1373
+ let overflow = indexData.totalMessages - limit;
1374
+ if (overflow <= 0) {
1375
+ return 0;
1376
+ }
1377
+ let removedCount = 0;
1378
+ while (overflow > 0 && indexData.chunks.length) {
1379
+ const chunk = indexData.chunks[0];
1380
+ if (chunk.messageCount <= overflow) {
1381
+ const removedChunkMessages = await this.readChunkMessages(entry.channelDirPath, chunk.fileName);
1382
+ overflow -= chunk.messageCount;
1383
+ removedCount += chunk.messageCount;
1384
+ indexData.totalMessages -= chunk.messageCount;
1385
+ indexData.chunks.shift();
1386
+ this.forgetRecentMessageIds(entry.channelKey, removedChunkMessages.map((message) => message.id));
1387
+ this.forgetMessageChunkLocation(entry.channelKey, removedChunkMessages.map((message) => message.id));
1388
+ try {
1389
+ await import_node_fs3.promises.unlink(this.getChunkFilePath(entry.channelDirPath, chunk.fileName));
1390
+ } catch (error) {
1391
+ if (!this.isFileMissingError(error)) {
1392
+ this.logger.warn(`删除消息分块失败 [${entry.channelKey}:${chunk.fileName}]:`, error);
1393
+ }
1394
+ }
1395
+ continue;
1396
+ }
1397
+ const chunkMessages = await this.readChunkMessages(entry.channelDirPath, chunk.fileName);
1398
+ const keptMessages = chunkMessages.slice(overflow);
1399
+ const removedMessages = chunkMessages.slice(0, overflow);
1400
+ removedCount += overflow;
1401
+ indexData.totalMessages -= overflow;
1402
+ chunk.messageCount = keptMessages.length;
1403
+ overflow = 0;
1404
+ this.forgetRecentMessageIds(entry.channelKey, removedMessages.map((message) => message.id));
1405
+ this.forgetMessageChunkLocation(entry.channelKey, removedMessages.map((message) => message.id));
1406
+ await this.writeChunkMessages(entry.channelDirPath, chunk.fileName, keptMessages);
1407
+ }
1408
+ this.syncDirtyChannelState(entry.channelKey, indexData.totalMessages);
1409
+ await this.writeChannelIndex(entry.indexFilePath, indexData);
1410
+ return removedCount;
1411
+ }
1412
+ async countChannelMessages(selfId, channelId) {
1413
+ const entry = this.createStoredChannelEntry(selfId, channelId);
1414
+ try {
1415
+ const jsonData = await import_node_fs3.promises.readFile(entry.indexFilePath, "utf8");
1416
+ const indexData = JSON.parse(jsonData);
1417
+ if (typeof indexData.totalMessages === "number") {
1418
+ return indexData.totalMessages;
1419
+ }
1420
+ } catch (error) {
1421
+ if (!this.isFileMissingError(error)) {
1422
+ this.logger.error(`读取频道索引失败 [${entry.channelKey}]:`, error);
1423
+ }
1424
+ }
1425
+ return 0;
1426
+ }
1427
+ async removeChannelStorage(selfId, channelId) {
1428
+ const entry = this.createStoredChannelEntry(selfId, channelId);
1429
+ try {
1430
+ await import_node_fs3.promises.rm(entry.channelDirPath, { recursive: true, force: true });
1431
+ } catch (error) {
1432
+ this.logger.error(`删除频道目录失败 [${entry.channelKey}]:`, error);
1433
+ }
1434
+ }
1435
+ async updateUserProfileInChannel(selfId, channelId, userId, userName, avatar) {
1436
+ const entry = this.createStoredChannelEntry(selfId, channelId);
1437
+ const indexData = await this.loadOrCreateChannelIndex(selfId, channelId);
1438
+ let changed = false;
1439
+ for (const chunk of indexData.chunks) {
1440
+ const messages = await this.readChunkMessages(entry.channelDirPath, chunk.fileName);
1441
+ let chunkChanged = false;
1442
+ for (const message of messages) {
1443
+ if (message.userId !== userId) {
1444
+ continue;
1445
+ }
1446
+ if (userName && message.username !== userName) {
1447
+ message.username = userName;
1448
+ chunkChanged = true;
1449
+ }
1450
+ if (avatar && message.avatar !== avatar) {
1451
+ message.avatar = avatar;
1452
+ chunkChanged = true;
1453
+ }
1454
+ }
1455
+ if (!chunkChanged) {
1456
+ continue;
1457
+ }
1458
+ await this.writeChunkMessages(entry.channelDirPath, chunk.fileName, messages);
1459
+ changed = true;
1460
+ }
1461
+ return changed;
1462
+ }
1463
+ async findAndUpdateLatestBotMessage(selfId, channelId, realId) {
1464
+ const entry = this.createStoredChannelEntry(selfId, channelId);
1465
+ const indexData = await this.loadOrCreateChannelIndex(selfId, channelId);
1466
+ for (let index = indexData.chunks.length - 1; index >= 0; index -= 1) {
1467
+ const chunk = indexData.chunks[index];
1468
+ const messages = await this.readChunkMessages(entry.channelDirPath, chunk.fileName);
1469
+ const matched = [...messages].reverse().find((message) => message.type === "bot" && message.sending);
1470
+ if (!matched) {
1471
+ continue;
1472
+ }
1473
+ matched.realId = realId;
1474
+ matched.sending = false;
1475
+ await this.writeChunkMessages(entry.channelDirPath, chunk.fileName, messages);
1476
+ return matched;
1477
+ }
1478
+ return void 0;
1479
+ }
1480
+ async findAndUpdateBotMessageByTempId(selfId, channelId, tempMessageId, realId) {
1481
+ const channelKey = `${selfId}:${channelId}`;
1482
+ const pendingMessages = this.pendingMessages.get(channelKey);
1483
+ const pendingMatched = pendingMessages?.find((message) => message.id === tempMessageId);
1484
+ if (pendingMatched) {
1485
+ pendingMatched.realId = realId;
1486
+ pendingMatched.sending = false;
1487
+ return pendingMatched;
1488
+ }
1489
+ const cachedMessages = this.peekCachedChannelMessages(channelKey);
1490
+ const cachedMatched = cachedMessages?.find((message) => message.id === tempMessageId);
1491
+ if (cachedMatched) {
1492
+ cachedMatched.realId = realId;
1493
+ cachedMatched.sending = false;
1494
+ this.setCachedChannelMessages(channelKey, cachedMessages);
1495
+ }
1496
+ const entry = this.createStoredChannelEntry(selfId, channelId);
1497
+ const chunkFileName = this.getMessageChunkLocation(channelKey, tempMessageId);
1498
+ if (chunkFileName) {
1499
+ const chunkMessages = await this.readChunkMessages(entry.channelDirPath, chunkFileName);
1500
+ const matched = chunkMessages.find((message) => message.id === tempMessageId);
1501
+ if (matched) {
1502
+ matched.realId = realId;
1503
+ matched.sending = false;
1504
+ await this.writeChunkMessages(entry.channelDirPath, chunkFileName, chunkMessages);
1505
+ return matched;
1506
+ }
1507
+ }
1508
+ const indexData = await this.loadOrCreateChannelIndex(selfId, channelId);
1509
+ for (let index = indexData.chunks.length - 1; index >= 0; index -= 1) {
1510
+ const chunk = indexData.chunks[index];
1511
+ const chunkMessages = await this.readChunkMessages(entry.channelDirPath, chunk.fileName);
1512
+ const matched = chunkMessages.find((message) => message.id === tempMessageId);
1513
+ if (!matched) {
1514
+ continue;
1515
+ }
1516
+ matched.realId = realId;
1517
+ matched.sending = false;
1518
+ await this.writeChunkMessages(entry.channelDirPath, chunk.fileName, chunkMessages);
1519
+ this.rememberMessageChunkLocation(channelKey, chunk.fileName, [matched.id]);
1520
+ return matched;
1521
+ }
1522
+ return cachedMatched;
1523
+ }
1524
+ deduplicateMessages(messages) {
1525
+ const seen = /* @__PURE__ */ new Set();
1526
+ const deduplicated = [];
1527
+ for (const message of messages) {
1528
+ if (seen.has(message.id)) {
1529
+ continue;
1530
+ }
1531
+ seen.add(message.id);
1532
+ deduplicated.push(message);
1533
+ }
1534
+ return deduplicated;
1535
+ }
1536
+ getCachedChannelMessages(channelKey) {
1537
+ const cached = this.channelMessagesCache.get(channelKey);
1538
+ if (!cached) {
1539
+ return void 0;
1540
+ }
1541
+ this.channelMessagesCache.delete(channelKey);
1542
+ this.channelMessagesCache.set(channelKey, cached);
1543
+ this.memoryCache.messages = this.getChannelMessagesCacheSnapshot();
1544
+ return cached;
1545
+ }
1546
+ peekCachedChannelMessages(channelKey) {
1547
+ return this.channelMessagesCache.get(channelKey);
1548
+ }
1549
+ setCachedChannelMessages(channelKey, messages) {
1550
+ this.channelMessagesCache.delete(channelKey);
1551
+ this.channelMessagesCache.set(channelKey, messages);
1552
+ this.rememberRecentMessageIds(channelKey, messages.slice(-this.getRecentMessageIdCacheLimit()).map((message) => message.id));
1553
+ while (this.channelMessagesCache.size > this.config.channelCacheLimit) {
1554
+ const oldestKey = this.channelMessagesCache.keys().next().value;
1555
+ if (!oldestKey) {
1556
+ break;
1557
+ }
1558
+ this.channelMessagesCache.delete(oldestKey);
1559
+ this.deleteRecentMessageIds(oldestKey);
1560
+ this.deleteMessageChunkLocations(oldestKey);
1561
+ }
1562
+ this.memoryCache.messages = this.getChannelMessagesCacheSnapshot();
1563
+ }
1564
+ deleteCachedChannelMessages(channelKey) {
1565
+ this.channelMessagesCache.delete(channelKey);
1566
+ delete this.memoryCache.messages[channelKey];
1567
+ }
1568
+ getChannelMessagesCacheSnapshot() {
1569
+ return Object.fromEntries(this.channelMessagesCache.entries());
1570
+ }
1571
+ mergeChannelMessages(baseMessages, appendedMessages) {
1572
+ const merged = [...baseMessages];
1573
+ const knownIds = new Set(baseMessages.map((message) => message.id));
1574
+ for (const message of appendedMessages) {
1575
+ if (knownIds.has(message.id)) {
1576
+ continue;
1577
+ }
1578
+ knownIds.add(message.id);
1579
+ merged.push(message);
1580
+ }
1581
+ return merged;
1582
+ }
1583
+ limitChannelMessages(messages) {
1584
+ if (messages.length <= this.config.maxMessagesPerChannel) {
1585
+ return messages;
1586
+ }
1587
+ return [...messages].sort((left, right) => left.timestamp - right.timestamp).slice(-this.config.maxMessagesPerChannel);
1588
+ }
1589
+ async channelMessageExists(selfId, channelId, messageId) {
1590
+ const entry = this.createStoredChannelEntry(selfId, channelId);
1591
+ const channelKey = entry.channelKey;
1592
+ if (this.hasRecentMessageId(channelKey, messageId)) {
1593
+ return true;
1594
+ }
1595
+ const indexData = await this.loadOrCreateChannelIndex(selfId, channelId);
1596
+ for (let chunkIndex = indexData.chunks.length - 1; chunkIndex >= 0; chunkIndex -= 1) {
1597
+ const chunk = indexData.chunks[chunkIndex];
1598
+ const messages = await this.readChunkMessages(entry.channelDirPath, chunk.fileName);
1599
+ if (messages.some((message) => message.id === messageId)) {
1600
+ this.rememberRecentMessageIds(channelKey, [messageId]);
1601
+ return true;
1602
+ }
1603
+ }
1604
+ return false;
1605
+ }
1606
+ syncDirtyChannelState(channelKey, totalMessages) {
1607
+ if (totalMessages > this.config.maxMessagesPerChannel) {
1608
+ this.dirtyChannelKeys.add(channelKey);
1609
+ return;
1610
+ }
1611
+ this.dirtyChannelKeys.delete(channelKey);
1612
+ }
1613
+ registerPendingBotMessage(channelKey, messageId) {
1614
+ const messageIds = this.pendingBotMessageIds.get(channelKey) || [];
1615
+ messageIds.push(messageId);
1616
+ this.pendingBotMessageIds.set(channelKey, messageIds);
1617
+ }
1618
+ peekLatestPendingBotMessageId(channelKey) {
1619
+ const messageIds = this.pendingBotMessageIds.get(channelKey);
1620
+ return messageIds?.[messageIds.length - 1];
1621
+ }
1622
+ consumePendingBotMessageId(channelKey, messageId) {
1623
+ const messageIds = this.pendingBotMessageIds.get(channelKey);
1624
+ if (!messageIds?.length) {
1625
+ return;
1626
+ }
1627
+ const nextMessageIds = messageIds.filter((id) => id !== messageId);
1628
+ if (nextMessageIds.length) {
1629
+ this.pendingBotMessageIds.set(channelKey, nextMessageIds);
1630
+ return;
1631
+ }
1632
+ this.pendingBotMessageIds.delete(channelKey);
1633
+ }
1634
+ deletePendingBotMessages(channelKey) {
1635
+ this.pendingBotMessageIds.delete(channelKey);
1636
+ }
1637
+ getRecentMessageIdCacheLimit() {
1638
+ return Math.max(this.RECENT_MESSAGE_ID_CACHE_SIZE, this.config.messageChunkSize * 2);
1639
+ }
1640
+ rememberRecentMessageIds(channelKey, messageIds) {
1641
+ if (!messageIds.length) {
1642
+ return;
1643
+ }
1644
+ const nextMessageIds = [...this.recentMessageIdsCache.get(channelKey) || []];
1645
+ for (const messageId of messageIds) {
1646
+ const existingIndex = nextMessageIds.indexOf(messageId);
1647
+ if (existingIndex !== -1) {
1648
+ nextMessageIds.splice(existingIndex, 1);
1649
+ }
1650
+ nextMessageIds.push(messageId);
1651
+ }
1652
+ const maxSize = this.getRecentMessageIdCacheLimit();
1653
+ this.recentMessageIdsCache.set(channelKey, nextMessageIds.slice(-maxSize));
1654
+ }
1655
+ forgetRecentMessageIds(channelKey, messageIds) {
1656
+ const currentMessageIds = this.recentMessageIdsCache.get(channelKey);
1657
+ if (!currentMessageIds?.length || !messageIds.length) {
1658
+ return;
1659
+ }
1660
+ const nextMessageIds = currentMessageIds.filter((messageId) => !messageIds.includes(messageId));
1661
+ if (nextMessageIds.length) {
1662
+ this.recentMessageIdsCache.set(channelKey, nextMessageIds);
1663
+ return;
1664
+ }
1665
+ this.recentMessageIdsCache.delete(channelKey);
1666
+ }
1667
+ hasRecentMessageId(channelKey, messageId) {
1668
+ const currentMessageIds = this.recentMessageIdsCache.get(channelKey);
1669
+ return !!currentMessageIds?.includes(messageId);
1670
+ }
1671
+ deleteRecentMessageIds(channelKey) {
1672
+ this.recentMessageIdsCache.delete(channelKey);
1673
+ }
1674
+ rememberMessageChunkLocation(channelKey, chunkFileName, messageIds) {
1675
+ if (!messageIds.length) {
1676
+ return;
1677
+ }
1678
+ const currentLocations = this.messageChunkLocationCache.get(channelKey) || /* @__PURE__ */ new Map();
1679
+ for (const messageId of messageIds) {
1680
+ currentLocations.set(messageId, chunkFileName);
1681
+ }
1682
+ this.messageChunkLocationCache.set(channelKey, currentLocations);
1683
+ }
1684
+ forgetMessageChunkLocation(channelKey, messageIds) {
1685
+ const currentLocations = this.messageChunkLocationCache.get(channelKey);
1686
+ if (!currentLocations || !messageIds.length) {
1687
+ return;
1688
+ }
1689
+ for (const messageId of messageIds) {
1690
+ currentLocations.delete(messageId);
1691
+ }
1692
+ if (!currentLocations.size) {
1693
+ this.messageChunkLocationCache.delete(channelKey);
1694
+ }
1695
+ }
1696
+ getMessageChunkLocation(channelKey, messageId) {
1697
+ return this.messageChunkLocationCache.get(channelKey)?.get(messageId);
1698
+ }
1699
+ deleteMessageChunkLocations(channelKey) {
1700
+ this.messageChunkLocationCache.delete(channelKey);
1701
+ }
1702
+ isSameBotInfo(left, right) {
1703
+ return left.selfId === right.selfId && left.platform === right.platform && left.username === right.username && left.avatar === right.avatar && left.status === right.status;
1704
+ }
1705
+ isSameChannelInfo(left, right) {
1706
+ return left.id === right.id && left.name === right.name && left.type === right.type && left.channelId === right.channelId && left.guildName === right.guildName && left.isDirect === right.isDirect;
1707
+ }
1708
+ isFileMissingError(error) {
1709
+ return typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT";
838
1710
  }
839
1711
  };
840
1712
 
841
1713
  // src/api-handlers.ts
842
1714
  var import_koishi = require("koishi");
843
1715
  var import_node_url2 = require("node:url");
844
- var import_node_fs3 = require("node:fs");
1716
+ var import_node_fs4 = require("node:fs");
1717
+ var import_node_path4 = __toESM(require("node:path"));
1718
+ var import_node_crypto3 = require("node:crypto");
845
1719
  var mime = __toESM(require("mime-types"));
846
1720
  var ApiHandlers = class {
847
- constructor(ctx, config, fileManager, messageHandler) {
1721
+ constructor(ctx, config, fileManager, messageHandler, logger) {
848
1722
  this.ctx = ctx;
849
1723
  this.config = config;
850
1724
  this.fileManager = fileManager;
851
1725
  this.messageHandler = messageHandler;
852
- this.logger = ctx.logger("chat-patch");
1726
+ this.logger = logger;
853
1727
  }
854
1728
  static {
855
1729
  __name(this, "ApiHandlers");
856
1730
  }
857
- logger;
858
1731
  currentTempVideo = null;
859
1732
  registerApiHandlers() {
860
1733
  this.ctx.console.addListener("clear-all-indexeddb-data", async () => {
@@ -868,7 +1741,7 @@ var ApiHandlers = class {
868
1741
  });
869
1742
  this.ctx.console.addListener("get-chat-data", async () => {
870
1743
  try {
871
- const data = this.fileManager.readMetadataOnly();
1744
+ const data = await this.fileManager.readMetadataOnly();
872
1745
  this.logInfo("获取基础聊天数据(仅元数据)");
873
1746
  return {
874
1747
  success: true,
@@ -888,21 +1761,18 @@ var ApiHandlers = class {
888
1761
  });
889
1762
  this.ctx.console.addListener("get-history-messages", async (requestData) => {
890
1763
  try {
891
- let messages = this.fileManager.readChannelMessages(requestData.selfId, requestData.channelId);
892
- const sortedMessages = messages.sort((a, b) => b.timestamp - a.timestamp);
893
- if (requestData.limit !== void 0) {
894
- const limit = requestData.limit;
895
- const offset = requestData.offset || 0;
896
- messages = sortedMessages.slice(offset, offset + limit);
897
- messages = messages.sort((a, b) => a.timestamp - b.timestamp);
898
- } else {
899
- messages = sortedMessages.sort((a, b) => a.timestamp - b.timestamp);
900
- }
901
- this.logInfo("获取历史消息:", `${requestData.selfId}:${requestData.channelId}`, "共", messages.length, "条消息");
1764
+ const limit = Math.max(1, requestData.limit ?? this.config.messageChunkSize);
1765
+ const result = await this.fileManager.readChannelMessagesPage(
1766
+ requestData.selfId,
1767
+ requestData.channelId,
1768
+ limit,
1769
+ requestData.offset || 0
1770
+ );
1771
+ this.logInfo("获取历史消息:", `${requestData.selfId}:${requestData.channelId}`, "共", result.messages.length, "条消息");
902
1772
  return {
903
1773
  success: true,
904
- messages,
905
- total: sortedMessages.length
1774
+ messages: result.messages,
1775
+ total: result.total
906
1776
  };
907
1777
  } catch (error) {
908
1778
  this.logger.error("获取历史消息失败:", error);
@@ -911,11 +1781,7 @@ var ApiHandlers = class {
911
1781
  });
912
1782
  this.ctx.console.addListener("get-all-channel-message-counts", async () => {
913
1783
  try {
914
- const data = this.fileManager.readChatDataFromFile();
915
- const counts = {};
916
- for (const [channelKey, messages] of Object.entries(data.messages)) {
917
- counts[channelKey] = messages.length;
918
- }
1784
+ const counts = await this.fileManager.getAllChannelMessageCounts();
919
1785
  this.logInfo("获取所有频道消息数量:", {
920
1786
  频道数: Object.keys(counts).length,
921
1787
  总消息数: Object.values(counts).reduce((total, count) => total + count, 0)
@@ -946,16 +1812,13 @@ var ApiHandlers = class {
946
1812
  viteUrl: `/vite/@fs/${normalizedPath2}`
947
1813
  };
948
1814
  }
949
- const dir = require("node:path").join(this.ctx.baseDir, "data", "chat-patch", "persist-media", "images");
950
- if (!require("node:fs").existsSync(dir)) {
951
- require("node:fs").mkdirSync(dir, { recursive: true });
952
- }
953
- const crypto = require("node:crypto");
954
- const hash = crypto.createHash("md5").update(data.url).digest("hex");
955
- const ext = require("node:path").extname(new import_node_url2.URL(data.url).pathname) || ".jpg";
1815
+ const dir = import_node_path4.default.join(this.ctx.baseDir, "data", "chat-patch", "persist-media", "images");
1816
+ await import_node_fs4.promises.mkdir(dir, { recursive: true });
1817
+ const hash = (0, import_node_crypto3.createHash)("md5").update(data.url).digest("hex");
1818
+ const ext = import_node_path4.default.extname(new import_node_url2.URL(data.url).pathname) || ".jpg";
956
1819
  const filename = `${hash}${ext}`;
957
- const filePath = require("node:path").join(dir, filename);
958
- if (!require("node:fs").existsSync(filePath)) {
1820
+ const filePath = import_node_path4.default.join(dir, filename);
1821
+ if (!await this.fileExists(filePath)) {
959
1822
  const response = await fetch(data.url, {
960
1823
  headers: {
961
1824
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36",
@@ -966,7 +1829,7 @@ var ApiHandlers = class {
966
1829
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
967
1830
  }
968
1831
  const buffer = await response.arrayBuffer();
969
- require("node:fs").writeFileSync(filePath, Buffer.from(buffer));
1832
+ await import_node_fs4.promises.writeFile(filePath, Buffer.from(buffer));
970
1833
  }
971
1834
  const normalizedPath = filePath.replace(/\\/g, "/");
972
1835
  return {
@@ -974,28 +1837,24 @@ var ApiHandlers = class {
974
1837
  viteUrl: `/vite/@fs/${normalizedPath}`
975
1838
  };
976
1839
  } catch (error) {
977
- this.logger.error("获取图片失败:", error);
978
- return { success: false, error: error?.message || String(error) };
1840
+ return { success: false, error: this.getClientErrorMessage(error) };
979
1841
  }
980
1842
  });
981
1843
  this.ctx.console.addListener("clear-channel-history", async (data) => {
982
1844
  try {
983
1845
  this.logInfo("收到清理历史记录请求(已废弃,建议使用删除频道数据):", data);
984
- const chatData = this.fileManager.readChatDataFromFile();
985
1846
  const channelKey = `${data.selfId}:${data.channelId}`;
986
- if (!chatData.messages[channelKey]) {
1847
+ const { deletedMessages } = await this.fileManager.deleteChannelData(data.selfId, data.channelId);
1848
+ if (!deletedMessages) {
987
1849
  return { success: true, message: "频道没有历史消息" };
988
1850
  }
989
- const originalCount = chatData.messages[channelKey].length;
990
- delete chatData.messages[channelKey];
991
- this.fileManager.writeChatDataToFile(chatData);
992
1851
  this.logInfo(`频道 ${channelKey} 历史记录已清空:`, {
993
- 清理消息数: originalCount
1852
+ 清理消息数: deletedMessages
994
1853
  });
995
1854
  return {
996
1855
  success: true,
997
- message: `成功清理 ${originalCount} 条历史消息`,
998
- clearedCount: originalCount,
1856
+ message: `成功清理 ${deletedMessages} 条历史消息`,
1857
+ clearedCount: deletedMessages,
999
1858
  keptCount: 0
1000
1859
  };
1001
1860
  } catch (error) {
@@ -1017,13 +1876,12 @@ var ApiHandlers = class {
1017
1876
  }
1018
1877
  let messageContent = data.content;
1019
1878
  if (data.images && data.images.length > 0) {
1020
- const tempDir = this.ctx.baseDir + "/data/chat-patch/temp";
1879
+ const tempDir = import_node_path4.default.join(this.ctx.baseDir, "data", "chat-patch", "temp");
1880
+ const tempFiles = await this.safeReadDir(tempDir);
1021
1881
  for (const image of data.images) {
1022
- const files = require("fs").readdirSync(tempDir).filter(
1023
- (file) => file.includes(`temp_${image.tempId}`)
1024
- );
1882
+ const files = tempFiles.filter((file) => file.includes(`temp_${image.tempId}`));
1025
1883
  if (files.length > 0) {
1026
- const imagePath = `${tempDir}/${files[0]}`;
1884
+ const imagePath = import_node_path4.default.join(tempDir, files[0]);
1027
1885
  const fileUrl = this.createFileUrl(imagePath);
1028
1886
  messageContent += import_koishi.h.image(fileUrl).toString();
1029
1887
  this.logInfo("添加图片到消息:", { imagePath, fileUrl });
@@ -1036,14 +1894,9 @@ var ApiHandlers = class {
1036
1894
  this.logInfo("消息发送成功:", result);
1037
1895
  const messageId = Array.isArray(result) ? result[0] : result;
1038
1896
  if (messageId) {
1039
- const chatData = this.fileManager.readChatDataFromFile();
1040
1897
  const channelKey = `${data.selfId}:${data.channelId}`;
1041
- const messages = chatData.messages[channelKey] || [];
1042
- const msg = [...messages].reverse().find((m) => m.type === "bot" && m.sending);
1898
+ const msg = await this.fileManager.markLatestBotMessageAsSent(data.selfId, data.channelId, messageId);
1043
1899
  if (msg) {
1044
- msg.realId = messageId;
1045
- msg.sending = false;
1046
- this.fileManager.writeChatDataToFile(chatData);
1047
1900
  this.ctx.console.broadcast("bot-message-updated", {
1048
1901
  channelKey,
1049
1902
  tempId: msg.id,
@@ -1074,20 +1927,7 @@ var ApiHandlers = class {
1074
1927
  this.ctx.console.addListener("delete-bot-data", async (data) => {
1075
1928
  try {
1076
1929
  this.logInfo("收到删除机器人数据请求:", data);
1077
- const chatData = this.fileManager.readChatDataFromFile();
1078
- let deletedChannels = 0;
1079
- let deletedMessages = 0;
1080
- if (chatData.channels[data.selfId]) {
1081
- deletedChannels = Object.keys(chatData.channels[data.selfId]).length;
1082
- delete chatData.channels[data.selfId];
1083
- }
1084
- const channelsToDelete = Object.keys(chatData.messages).filter((key) => key.startsWith(`${data.selfId}:`));
1085
- for (const channelKey of channelsToDelete) {
1086
- deletedMessages += chatData.messages[channelKey].length;
1087
- delete chatData.messages[channelKey];
1088
- }
1089
- delete chatData.bots[data.selfId];
1090
- this.fileManager.writeChatDataToFile(chatData);
1930
+ const { deletedChannels, deletedMessages } = await this.fileManager.deleteBotData(data.selfId);
1091
1931
  this.logInfo(`机器人 ${data.selfId} 数据删除完成:`, {
1092
1932
  删除频道数: deletedChannels,
1093
1933
  删除消息数: deletedMessages
@@ -1106,17 +1946,8 @@ var ApiHandlers = class {
1106
1946
  this.ctx.console.addListener("delete-channel-data", async (data) => {
1107
1947
  try {
1108
1948
  this.logInfo("收到删除频道数据请求:", data);
1109
- const chatData = this.fileManager.readChatDataFromFile();
1110
1949
  const channelKey = `${data.selfId}:${data.channelId}`;
1111
- let deletedMessages = 0;
1112
- if (chatData.messages[channelKey]) {
1113
- deletedMessages = chatData.messages[channelKey].length;
1114
- delete chatData.messages[channelKey];
1115
- }
1116
- if (chatData.channels[data.selfId] && chatData.channels[data.selfId][data.channelId]) {
1117
- delete chatData.channels[data.selfId][data.channelId];
1118
- }
1119
- this.fileManager.writeChatDataToFile(chatData);
1950
+ const { deletedMessages } = await this.fileManager.deleteChannelData(data.selfId, data.channelId);
1120
1951
  this.logInfo(`频道 ${channelKey} 数据删除完成:`, {
1121
1952
  删除消息数: deletedMessages
1122
1953
  });
@@ -1133,9 +1964,7 @@ var ApiHandlers = class {
1133
1964
  this.ctx.console.addListener("set-pinned-bots", async (data) => {
1134
1965
  try {
1135
1966
  this.logInfo("收到设置置顶机器人请求:", data.pinnedBots);
1136
- const chatData = this.fileManager.readChatDataFromFile();
1137
- chatData.pinnedBots = data.pinnedBots;
1138
- this.fileManager.writeChatDataToFile(chatData);
1967
+ await this.fileManager.setPinnedBots(data.pinnedBots);
1139
1968
  return { success: true };
1140
1969
  } catch (error) {
1141
1970
  this.logger.error("设置置顶机器人失败:", error);
@@ -1145,9 +1974,7 @@ var ApiHandlers = class {
1145
1974
  this.ctx.console.addListener("set-pinned-channels", async (data) => {
1146
1975
  try {
1147
1976
  this.logInfo("收到设置置顶频道请求:", data.pinnedChannels);
1148
- const chatData = this.fileManager.readChatDataFromFile();
1149
- chatData.pinnedChannels = data.pinnedChannels;
1150
- this.fileManager.writeChatDataToFile(chatData);
1977
+ await this.fileManager.setPinnedChannels(data.pinnedChannels);
1151
1978
  return { success: true };
1152
1979
  } catch (error) {
1153
1980
  this.logger.error("设置置顶频道失败:", error);
@@ -1162,12 +1989,10 @@ var ApiHandlers = class {
1162
1989
  const tempId = Date.now() + "_" + Math.random().toString(36).substring(2, 11);
1163
1990
  const extension = data.filename.split(".").pop()?.toLowerCase() || (data.isGif ? "gif" : "jpg");
1164
1991
  const tempFilename = `temp_${tempId}.${extension}`;
1165
- const tempDir = this.ctx.baseDir + "/data/chat-patch/temp";
1166
- if (!require("fs").existsSync(tempDir)) {
1167
- require("fs").mkdirSync(tempDir, { recursive: true });
1168
- }
1169
- const tempPath = `${tempDir}/${tempFilename}`;
1170
- require("fs").writeFileSync(tempPath, buffer);
1992
+ const tempDir = import_node_path4.default.join(this.ctx.baseDir, "data", "chat-patch", "temp");
1993
+ await import_node_fs4.promises.mkdir(tempDir, { recursive: true });
1994
+ const tempPath = import_node_path4.default.join(tempDir, tempFilename);
1995
+ await import_node_fs4.promises.writeFile(tempPath, buffer);
1171
1996
  this.logInfo("图片上传成功:", { tempPath, size: buffer.length, isGif: data.isGif });
1172
1997
  return {
1173
1998
  success: true,
@@ -1184,14 +2009,12 @@ var ApiHandlers = class {
1184
2009
  });
1185
2010
  this.ctx.console.addListener("delete-temp-image", async (data) => {
1186
2011
  try {
1187
- const tempDir = this.ctx.baseDir + "/data/chat-patch/temp";
1188
- const files = require("fs").readdirSync(tempDir).filter(
1189
- (file) => file.includes(`temp_${data.tempId}`)
1190
- );
2012
+ const tempDir = import_node_path4.default.join(this.ctx.baseDir, "data", "chat-patch", "temp");
2013
+ const files = (await this.safeReadDir(tempDir)).filter((file) => file.includes(`temp_${data.tempId}`));
1191
2014
  for (const file of files) {
1192
- const filePath = `${tempDir}/${file}`;
1193
- if (require("fs").existsSync(filePath)) {
1194
- require("fs").unlinkSync(filePath);
2015
+ const filePath = import_node_path4.default.join(tempDir, file);
2016
+ if (await this.fileExists(filePath)) {
2017
+ await import_node_fs4.promises.unlink(filePath);
1195
2018
  this.logInfo("删除临时图片:", filePath);
1196
2019
  }
1197
2020
  }
@@ -1208,6 +2031,8 @@ var ApiHandlers = class {
1208
2031
  success: true,
1209
2032
  config: {
1210
2033
  maxMessagesPerChannel: this.config.maxMessagesPerChannel,
2034
+ messageChunkSize: this.config.messageChunkSize,
2035
+ channelCacheLimit: this.config.channelCacheLimit,
1211
2036
  maxPersistImages: this.config.maxPersistImages,
1212
2037
  loggerinfo: this.config.loggerinfo,
1213
2038
  blockedPlatforms: this.config.blockedPlatforms || [],
@@ -1231,44 +2056,8 @@ var ApiHandlers = class {
1231
2056
  if (user.avatar) {
1232
2057
  user.avatar = await this.messageHandler.downloadAndCacheMedia(user.avatar, "avatar");
1233
2058
  }
1234
- const chatData = this.fileManager.readChatDataFromFile();
1235
- let changed = false;
1236
- const botChannels = chatData.channels[data.selfId] || {};
1237
- const possibleChannelIds = [
1238
- data.userId,
1239
- `private:${data.userId}`,
1240
- `direct:${data.userId}`
1241
- ];
1242
- for (const channelId of possibleChannelIds) {
1243
- const channel = botChannels[channelId];
1244
- if (channel && channel.isDirect) {
1245
- const newName = `私聊(${user.name})`;
1246
- if (channel.name !== newName) {
1247
- channel.name = newName;
1248
- changed = true;
1249
- this.logInfo("更新私聊频道名称:", { channelId, oldName: channel.name, newName });
1250
- }
1251
- }
1252
- }
1253
- const channelKeyPrefix = `${data.selfId}:`;
1254
- for (const [key, messages] of Object.entries(chatData.messages)) {
1255
- if (key.startsWith(channelKeyPrefix)) {
1256
- messages.forEach((msg) => {
1257
- if (msg.userId === data.userId) {
1258
- if (user.name && msg.username !== user.name) {
1259
- msg.username = user.name;
1260
- changed = true;
1261
- }
1262
- if (user.avatar && msg.avatar !== user.avatar) {
1263
- msg.avatar = user.avatar;
1264
- changed = true;
1265
- }
1266
- }
1267
- });
1268
- }
1269
- }
2059
+ const changed = await this.fileManager.updateUserProfileInBotData(data.selfId, data.userId, user.name, user.avatar);
1270
2060
  if (changed) {
1271
- this.fileManager.writeChatDataToFile(chatData);
1272
2061
  this.ctx.console.broadcast("chat-data-updated", {});
1273
2062
  }
1274
2063
  }
@@ -1277,18 +2066,6 @@ var ApiHandlers = class {
1277
2066
  return { success: false, error: error?.message || "获取用户信息失败" };
1278
2067
  }
1279
2068
  });
1280
- this.ctx.console.addListener("debug-get-raw-data", async () => {
1281
- try {
1282
- const data = this.fileManager.readChatDataFromFile();
1283
- return {
1284
- success: true,
1285
- data
1286
- };
1287
- } catch (error) {
1288
- this.logger.error("获取原始数据失败:", error);
1289
- return { success: false, error: error?.message || String(error) };
1290
- }
1291
- });
1292
2069
  this.ctx.console.addListener("fetch-video-temp", async (data) => {
1293
2070
  try {
1294
2071
  this.logInfo("收到视频临时加载请求:", data.url);
@@ -1306,16 +2083,13 @@ var ApiHandlers = class {
1306
2083
  viteUrl: `/vite/@fs/${normalizedPath2}`
1307
2084
  };
1308
2085
  }
1309
- const dir = require("node:path").join(this.ctx.baseDir, "data", "chat-patch", "persist-media", "media");
1310
- if (!require("node:fs").existsSync(dir)) {
1311
- require("node:fs").mkdirSync(dir, { recursive: true });
1312
- }
1313
- const crypto = require("node:crypto");
1314
- const hash = crypto.createHash("md5").update(data.url).digest("hex");
1315
- const ext = require("node:path").extname(new import_node_url2.URL(data.url).pathname) || ".mp4";
2086
+ const dir = import_node_path4.default.join(this.ctx.baseDir, "data", "chat-patch", "persist-media", "media");
2087
+ await import_node_fs4.promises.mkdir(dir, { recursive: true });
2088
+ const hash = (0, import_node_crypto3.createHash)("md5").update(data.url).digest("hex");
2089
+ const ext = import_node_path4.default.extname(new import_node_url2.URL(data.url).pathname) || ".mp4";
1316
2090
  const filename = `${hash}${ext}`;
1317
- const filePath = require("node:path").join(dir, filename);
1318
- if (!require("node:fs").existsSync(filePath)) {
2091
+ const filePath = import_node_path4.default.join(dir, filename);
2092
+ if (!await this.fileExists(filePath)) {
1319
2093
  const response = await fetch(data.url, {
1320
2094
  headers: {
1321
2095
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
@@ -1326,7 +2100,7 @@ var ApiHandlers = class {
1326
2100
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1327
2101
  }
1328
2102
  const buffer = await response.arrayBuffer();
1329
- require("node:fs").writeFileSync(filePath, Buffer.from(buffer));
2103
+ await import_node_fs4.promises.writeFile(filePath, Buffer.from(buffer));
1330
2104
  this.logInfo("视频下载成功:", { size: buffer.byteLength, path: filePath });
1331
2105
  }
1332
2106
  const normalizedPath = filePath.replace(/\\/g, "/");
@@ -1335,8 +2109,7 @@ var ApiHandlers = class {
1335
2109
  viteUrl: `/vite/@fs/${normalizedPath}`
1336
2110
  };
1337
2111
  } catch (error) {
1338
- this.logger.error("视频临时加载失败:", error);
1339
- return { success: false, error: error?.message || String(error) };
2112
+ return { success: false, error: this.getClientErrorMessage(error) };
1340
2113
  }
1341
2114
  });
1342
2115
  }
@@ -1359,7 +2132,7 @@ var ApiHandlers = class {
1359
2132
  async handleLocalFileRequest(fileUrl) {
1360
2133
  try {
1361
2134
  const filePath = (0, import_node_url2.fileURLToPath)(fileUrl);
1362
- const buffer = (0, import_node_fs3.readFileSync)(filePath);
2135
+ const buffer = await import_node_fs4.promises.readFile(filePath);
1363
2136
  const base64 = buffer.toString("base64");
1364
2137
  const contentType = mime.lookup(filePath) || "application/octet-stream";
1365
2138
  this.logInfo("成功读取本地文件:", { fileUrl, filePath, contentType });
@@ -1370,7 +2143,6 @@ var ApiHandlers = class {
1370
2143
  dataUrl: `data:${contentType};base64,${base64}`
1371
2144
  };
1372
2145
  } catch (error) {
1373
- this.logger.error("读取本地文件失败:", { fileUrl, error: error.message });
1374
2146
  return {
1375
2147
  success: false,
1376
2148
  error: `读取本地文件失败: ${error.message}`
@@ -1383,40 +2155,85 @@ var ApiHandlers = class {
1383
2155
  }, 5 * 60 * 1e3);
1384
2156
  }
1385
2157
  async cleanupMediaCache() {
1386
- const baseDir = this.ctx.baseDir + "/data/chat-patch/persist-media";
1387
- if (!require("node:fs").existsSync(baseDir)) return;
1388
- const cleanupDir = /* @__PURE__ */ __name((dirName, limit) => {
1389
- const dirPath = require("node:path").join(baseDir, dirName);
1390
- if (!require("node:fs").existsSync(dirPath)) return;
1391
- const files = require("node:fs").readdirSync(dirPath).map((file) => {
1392
- const filePath = require("node:path").join(dirPath, file);
1393
- const stats = require("node:fs").statSync(filePath);
1394
- return { name: file, path: filePath, mtime: stats.mtimeMs };
1395
- }).sort((a, b) => b.mtime - a.mtime);
1396
- if (files.length > limit) {
1397
- files.slice(limit).forEach((f) => {
1398
- try {
1399
- require("node:fs").unlinkSync(f.path);
1400
- } catch {
1401
- }
1402
- });
1403
- }
1404
- }, "cleanupDir");
1405
- cleanupDir("images", 100);
1406
- cleanupDir("media", 20);
2158
+ const baseDir = import_node_path4.default.join(this.ctx.baseDir, "data", "chat-patch", "persist-media");
2159
+ await this.cleanupMediaCacheDir(import_node_path4.default.join(baseDir, "images"), 100);
2160
+ await this.cleanupMediaCacheDir(import_node_path4.default.join(baseDir, "media"), 20);
1407
2161
  }
1408
2162
  logInfo(...args) {
1409
2163
  if (this.config.loggerinfo) {
1410
- this.logger.info(...args);
2164
+ Reflect.apply(this.logger.info, this.logger, args);
2165
+ }
2166
+ }
2167
+ async fileExists(filePath) {
2168
+ try {
2169
+ await import_node_fs4.promises.access(filePath);
2170
+ return true;
2171
+ } catch {
2172
+ return false;
1411
2173
  }
1412
2174
  }
2175
+ async safeReadDir(dirPath) {
2176
+ try {
2177
+ return await import_node_fs4.promises.readdir(dirPath);
2178
+ } catch {
2179
+ return [];
2180
+ }
2181
+ }
2182
+ async cleanupMediaCacheDir(dirPath, limit) {
2183
+ const files = await this.safeReadDir(dirPath);
2184
+ if (!files.length) {
2185
+ return;
2186
+ }
2187
+ const fileStats = await Promise.all(files.map(async (fileName) => {
2188
+ const filePath = import_node_path4.default.join(dirPath, fileName);
2189
+ const stats = await import_node_fs4.promises.stat(filePath);
2190
+ return { path: filePath, mtime: stats.mtimeMs };
2191
+ }));
2192
+ fileStats.sort((left, right) => right.mtime - left.mtime);
2193
+ for (const file of fileStats.slice(limit)) {
2194
+ try {
2195
+ await import_node_fs4.promises.unlink(file.path);
2196
+ } catch {
2197
+ continue;
2198
+ }
2199
+ }
2200
+ }
2201
+ getClientErrorMessage(error) {
2202
+ if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") {
2203
+ return error.message;
2204
+ }
2205
+ return String(error);
2206
+ }
1413
2207
  };
1414
2208
 
2209
+ // src/logger.ts
2210
+ function createPluginLogger(logger, config) {
2211
+ return {
2212
+ logInfo(...args) {
2213
+ if (config.loggerinfo) {
2214
+ Reflect.apply(logger.info, logger, args);
2215
+ }
2216
+ },
2217
+ info(...args) {
2218
+ Reflect.apply(logger.info, logger, args);
2219
+ },
2220
+ warn(...args) {
2221
+ Reflect.apply(logger.warn, logger, args);
2222
+ },
2223
+ error(...args) {
2224
+ Reflect.apply(logger.error, logger, args);
2225
+ }
2226
+ };
2227
+ }
2228
+ __name(createPluginLogger, "createPluginLogger");
2229
+
1415
2230
  // src/config.ts
1416
2231
  var import_koishi2 = require("koishi");
1417
2232
  var Config = import_koishi2.Schema.intersect([
1418
2233
  import_koishi2.Schema.object({
1419
2234
  maxMessagesPerChannel: import_koishi2.Schema.number().default(500).description("每个群组最大保存消息数量").min(50).max(1500).step(1),
2235
+ messageChunkSize: import_koishi2.Schema.number().default(100).description("单个消息分块文件最大消息数量").min(20).max(500).step(1),
2236
+ channelCacheLimit: import_koishi2.Schema.number().default(50).description("内存中最多缓存的频道消息数量").min(1).max(200).step(1),
1420
2237
  maxPersistImages: import_koishi2.Schema.number().default(100).description("持久化存储的图片缓存数量").min(10).max(500).step(1),
1421
2238
  blockedPlatforms: import_koishi2.Schema.array(import_koishi2.Schema.object({
1422
2239
  platformName: import_koishi2.Schema.string().description("平台名称或关键词"),
@@ -1462,118 +2279,49 @@ var usage = `
1462
2279
  ---
1463
2280
  `;
1464
2281
  async function apply(ctx, config) {
1465
- const logger = ctx.logger("chat-patch");
1466
- const mediaDir = import_node_path3.default.join(ctx.baseDir, "data", "chat-patch", "persist-media", "media");
1467
- if (require("node:fs").existsSync(mediaDir)) {
1468
- try {
1469
- const files = require("node:fs").readdirSync(mediaDir);
1470
- let deletedCount = 0;
1471
- for (const file of files) {
1472
- const filePath = import_node_path3.default.join(mediaDir, file);
1473
- try {
1474
- require("node:fs").unlinkSync(filePath);
1475
- deletedCount++;
1476
- } catch (e) {
1477
- logger.warn("删除媒体文件失败:", filePath, e);
1478
- }
1479
- }
1480
- if (deletedCount > 0) {
1481
- logger.info(`启动时清理了 ${deletedCount} 个旧版本的媒体缓存文件`);
1482
- }
1483
- } catch (e) {
1484
- logger.warn("清理媒体文件夹失败:", e);
1485
- }
1486
- }
1487
- const fileManager = new FileManager(ctx, config);
1488
- const messageHandler = new MessageHandler(ctx, config, fileManager);
1489
- const apiHandlers = new ApiHandlers(ctx, config, fileManager, messageHandler);
2282
+ const pluginLogger = createPluginLogger(ctx.logger("chat-patch"), config);
2283
+ const fileManager = new FileManager(ctx, config, pluginLogger);
2284
+ await fileManager.initialize();
2285
+ const messageHandler = new MessageHandler(ctx, config, fileManager, pluginLogger);
2286
+ const apiHandlers = new ApiHandlers(ctx, config, fileManager, messageHandler, pluginLogger);
1490
2287
  const utils = new Utils(config, ctx);
1491
- const initialData = fileManager.readChatDataFromFile();
1492
- const cleanedData = fileManager.cleanExcessMessages(initialData);
1493
- const originalCount = Object.values(initialData.messages).reduce((total, msgs) => total + msgs.length, 0);
1494
- const cleanedCount = Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0);
1495
- if (originalCount !== cleanedCount) {
1496
- fileManager.writeChatDataToFile(cleanedData);
1497
- }
1498
- function logInfo(...args) {
1499
- if (config.loggerinfo) {
1500
- logger.info(...args);
1501
- }
1502
- }
1503
- __name(logInfo, "logInfo");
1504
- logInfo("插件加载完成,数据统计:", {
1505
- 机器人数量: Object.keys(cleanedData.bots).length,
1506
- 频道数量: Object.keys(cleanedData.channels).reduce((total, botId) => total + Object.keys(cleanedData.channels[botId] || {}).length, 0),
1507
- 消息频道数: Object.keys(cleanedData.messages).length,
1508
- 总消息数: Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0)
2288
+ const metadata = await fileManager.readMetadataOnly();
2289
+ pluginLogger.logInfo("插件加载完成,元数据统计:", {
2290
+ 机器人数量: Object.keys(metadata.bots).length,
2291
+ 频道数量: Object.keys(metadata.channels).reduce((total, botId) => total + Object.keys(metadata.channels[botId] || {}).length, 0),
2292
+ 置顶机器人数量: metadata.pinnedBots.length,
2293
+ 置顶频道数量: metadata.pinnedChannels.length
1509
2294
  });
1510
2295
  ctx.on("message", (session) => {
1511
2296
  if (utils.isPlatformBlocked(session.platform || "unknown")) return;
1512
- const timestamp = Date.now();
1513
- ctx.console.broadcast("chat-message-event", {
1514
- type: "message",
1515
- selfId: session.selfId,
1516
- platform: session.platform || "unknown",
1517
- channelId: session.channelId,
1518
- messageId: session.event?.message?.id || `msg-${timestamp}`,
1519
- content: session.content || "",
1520
- userId: session.userId || "unknown",
1521
- username: session.username || session.userId || "unknown",
1522
- avatar: session.event?.user?.avatar,
1523
- timestamp,
1524
- isDirect: session.isDirect,
1525
- elements: utils.cleanBase64Content(session.elements, false),
1526
- bot: {
1527
- avatar: session.bot.user?.avatar,
1528
- name: session.bot.user?.name
1529
- }
1530
- });
1531
- messageHandler.recordUserMessage(session, timestamp);
2297
+ messageHandler.recordUserMessage(session, Date.now());
1532
2298
  });
1533
2299
  ctx.on("before-send", (session) => {
1534
2300
  if (utils.isPlatformBlocked(session.platform || "unknown")) return;
1535
- const timestamp = Date.now();
1536
- ctx.console.broadcast("chat-bot-message-event", {
1537
- type: "bot-message",
1538
- selfId: session.selfId,
1539
- platform: session.platform || "unknown",
1540
- channelId: session.channelId,
1541
- messageId: `bot-msg-${timestamp}`,
1542
- content: session.content || "",
1543
- userId: session.selfId,
1544
- username: session.bot.user?.name || `Bot-${session.selfId}`,
1545
- avatar: session.bot.user?.avatar,
1546
- timestamp,
1547
- sending: true,
1548
- elements: utils.cleanBase64Content(session.event?.message?.elements, true),
1549
- bot: {
1550
- avatar: session.bot.user?.avatar,
1551
- name: session.bot.user?.name
1552
- }
1553
- });
1554
- messageHandler.recordBotMessage(session, timestamp);
2301
+ messageHandler.recordBotMessage(session, Date.now());
1555
2302
  });
2303
+ let cleanupDelayTimer;
2304
+ let cleanupInterval;
1556
2305
  ctx.on("ready", async () => {
1557
- logInfo("插件启动完成,开始监听消息");
1558
- ctx.setInterval(() => {
1559
- const data = fileManager.readChatDataFromFile();
1560
- const cleanedData2 = fileManager.cleanExcessMessages(data);
1561
- const originalCount2 = Object.values(data.messages).reduce((total, msgs) => total + msgs.length, 0);
1562
- const cleanedCount2 = Object.values(cleanedData2.messages).reduce((total, msgs) => total + msgs.length, 0);
1563
- if (originalCount2 !== cleanedCount2) {
1564
- fileManager.writeChatDataToFile(cleanedData2);
1565
- logInfo("定期清理完成,清理了", originalCount2 - cleanedCount2, "条超量消息");
1566
- }
2306
+ pluginLogger.logInfo("插件启动完成,开始监听消息");
2307
+ cleanupDelayTimer = ctx.setTimeout(() => {
2308
+ void fileManager.cleanupExcessMessagesInStorage();
2309
+ }, 15e3);
2310
+ cleanupInterval = ctx.setInterval(() => {
2311
+ void fileManager.cleanupExcessMessagesInStorage();
1567
2312
  }, 3e5);
1568
2313
  });
1569
2314
  apiHandlers.registerApiHandlers();
1570
2315
  ctx.console.addEntry({
1571
- dev: import_node_path3.default.resolve(__dirname, "../client/index.ts"),
1572
- prod: import_node_path3.default.resolve(__dirname, "../dist")
2316
+ dev: import_node_path5.default.resolve(__dirname, "../client/index.ts"),
2317
+ prod: import_node_path5.default.resolve(__dirname, "../dist")
1573
2318
  });
1574
2319
  ctx.on("dispose", () => {
1575
- fileManager.dispose();
1576
- logInfo("插件已卸载,所有待处理的消息已写入");
2320
+ cleanupDelayTimer?.();
2321
+ cleanupInterval?.();
2322
+ messageHandler.dispose();
2323
+ void fileManager.dispose();
2324
+ pluginLogger.logInfo("插件已卸载,所有待处理的消息已写入");
1577
2325
  });
1578
2326
  }
1579
2327
  __name(apply, "apply");