koishi-plugin-chat-patch 2.4.5 → 3.0.1

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,333 +483,1276 @@ 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
  }
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
+ }
718
+ }
719
+ }
720
+ // 确保目录存在
721
+ async ensureDir(dirPath) {
722
+ await import_node_fs3.promises.mkdir(dirPath, { recursive: true });
723
+ }
724
+ // 读取单个频道的消息(公共方法)
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;
731
+ }
732
+ const existingPromise = this.channelLoadPromises.get(channelKey);
733
+ if (existingPromise) {
734
+ return existingPromise;
735
+ }
736
+ const loadPromise = this.loadChannelMessages(selfId, channelId);
737
+ this.channelLoadPromises.set(channelKey, loadPromise);
738
+ try {
739
+ return await loadPromise;
740
+ } finally {
741
+ this.channelLoadPromises.delete(channelKey);
742
+ }
743
+ }
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) {
748
+ return {
749
+ messages: [],
750
+ total: indexData.totalMessages
751
+ };
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() {
800
+ try {
801
+ const jsonData = await import_node_fs3.promises.readFile(this.metadataFilePath, "utf8");
802
+ const data = JSON.parse(jsonData);
803
+ return {
804
+ bots: data.bots || {},
805
+ channels: data.channels || {},
806
+ pinnedBots: data.pinnedBots || [],
807
+ pinnedChannels: data.pinnedChannels || [],
808
+ lastSaveTime: data.lastSaveTime
809
+ };
810
+ } catch (error) {
811
+ if (this.isFileMissingError(error)) {
812
+ return {
813
+ bots: {},
814
+ channels: {},
815
+ pinnedBots: [],
816
+ pinnedChannels: []
817
+ };
818
+ }
819
+ this.logger.error("读取元数据失败:", error);
820
+ return {
821
+ bots: {},
822
+ channels: {},
823
+ pinnedBots: [],
824
+ pinnedChannels: []
825
+ };
826
+ }
827
+ }
828
+ // 写入元数据
829
+ async writeMetadata(metadata) {
830
+ try {
831
+ await this.ensureDir(import_node_path3.default.dirname(this.metadataFilePath));
832
+ const dataToWrite = {
833
+ ...metadata,
834
+ lastSaveTime: Date.now()
835
+ };
836
+ const jsonData = JSON.stringify(dataToWrite, null, 2);
837
+ await this.atomicWriteTextFile(this.metadataFilePath, jsonData);
838
+ } catch (error) {
839
+ this.logger.error("写入元数据失败:", error);
840
+ }
841
+ }
842
+ // 为特定频道安排写入
843
+ scheduleWrite(channelKey) {
844
+ const existingTimer = this.writeTimers.get(channelKey);
845
+ if (existingTimer) {
846
+ existingTimer();
847
+ }
848
+ const timer = this.ctx.setTimeout(() => {
849
+ this.writeTimers.delete(channelKey);
850
+ void this.flushPendingMessages(channelKey);
851
+ }, this.WRITE_DEBOUNCE_MS);
852
+ this.writeTimers.set(channelKey, timer);
853
+ }
854
+ // 刷新特定频道的待写入消息
855
+ async flushPendingMessages(channelKey) {
856
+ const messagesToWrite = this.pendingMessages.get(channelKey);
857
+ if (!messagesToWrite || messagesToWrite.length === 0) return;
858
+ this.pendingMessages.delete(channelKey);
859
+ const cachedMessages = this.peekCachedChannelMessages(channelKey);
860
+ const [selfId, channelId] = channelKey.split(":");
861
+ if (!selfId || !channelId) return;
862
+ const uniqueMessages = this.deduplicateMessages(messagesToWrite);
863
+ if (!uniqueMessages.length) {
864
+ return;
865
+ }
866
+ if (cachedMessages) {
867
+ const nextMessages = this.mergeChannelMessages(cachedMessages, uniqueMessages);
868
+ this.setCachedChannelMessages(channelKey, this.limitChannelMessages(nextMessages));
869
+ }
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
+ });
875
+ }
876
+ cleanExcessMessages(data) {
877
+ let cleanedCount = 0;
878
+ const cleanedMessages = {};
879
+ for (const [channelKey, messages] of Object.entries(data.messages)) {
880
+ if (messages.length > this.config.maxMessagesPerChannel) {
881
+ const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp);
882
+ const keptMessages = sortedMessages.slice(-this.config.maxMessagesPerChannel);
883
+ cleanedCount += messages.length - keptMessages.length;
884
+ cleanedMessages[channelKey] = keptMessages;
885
+ this.logger.logInfo(`频道 ${channelKey} 清理了 ${messages.length - keptMessages.length} 条旧消息,保留最新 ${keptMessages.length} 条`);
886
+ } else {
887
+ cleanedMessages[channelKey] = messages;
888
+ }
889
+ }
890
+ if (cleanedCount > 0) {
891
+ this.logger.logInfo("总共清理超量消息:", cleanedCount, "条");
892
+ }
893
+ return {
894
+ ...data,
895
+ messages: cleanedMessages
896
+ };
897
+ }
898
+ async addMessageToFile(messageInfo) {
899
+ await this.ensureMetadataLoaded();
900
+ const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`;
901
+ if (!messageInfo.timestamp) {
902
+ messageInfo.timestamp = Date.now();
903
+ }
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;
908
+ }
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;
916
+ }
917
+ const existsInStorage = await this.channelMessageExists(messageInfo.selfId, messageInfo.channelId, cleanedMessageInfo.id);
918
+ if (existsInStorage) {
919
+ return;
920
+ }
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));
931
+ }
932
+ this.scheduleWrite(channelKey);
933
+ }
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;
1093
+ for (const [channelKey, timer] of this.writeTimers.entries()) {
1094
+ timer();
1095
+ await this.flushPendingMessages(channelKey);
1096
+ }
1097
+ this.writeTimers.clear();
1098
+ await this.writeQueue;
1099
+ await this.utils.dispose();
1100
+ }
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
+ }
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;
582
1280
  }
583
- // 确保目录存在
584
- ensureDir(dirPath) {
585
- if (!import_node_fs2.default.existsSync(dirPath)) {
586
- import_node_fs2.default.mkdirSync(dirPath, { recursive: true });
587
- }
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
+ };
588
1289
  }
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`);
1290
+ async writeChannelIndex(indexFilePath, indexData) {
1291
+ await this.ensureDir(import_node_path3.default.dirname(indexFilePath));
1292
+ await this.atomicWriteTextFile(indexFilePath, JSON.stringify(indexData, null, 2));
594
1293
  }
595
- // 读取单个频道的消息(公共方法)
596
- readChannelMessages(selfId, channelId) {
597
- const filePath = this.getChannelFilePath(selfId, channelId);
598
- if (!import_node_fs2.default.existsSync(filePath)) {
599
- return [];
600
- }
1294
+ async readChunkMessages(channelDirPath, fileName) {
601
1295
  try {
602
- const jsonData = import_node_fs2.default.readFileSync(filePath, "utf8");
1296
+ const jsonData = await import_node_fs3.promises.readFile(this.getChunkFilePath(channelDirPath, fileName), "utf8");
603
1297
  const messages = JSON.parse(jsonData);
604
1298
  return Array.isArray(messages) ? messages : [];
605
1299
  } catch (error) {
606
- this.logger.error(`读取频道消息失败 [${selfId}:${channelId}]:`, error);
1300
+ if (!this.isFileMissingError(error)) {
1301
+ this.logger.error(`读取消息分块失败 [${channelDirPath}/${fileName}]:`, error);
1302
+ }
607
1303
  return [];
608
1304
  }
609
1305
  }
610
- // 写入单个频道的消息
611
- writeChannelMessages(selfId, channelId, messages) {
612
- const filePath = this.getChannelFilePath(selfId, channelId);
613
- 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);
1306
+ async writeChunkMessages(channelDirPath, fileName, messages) {
1307
+ await this.ensureDir(channelDirPath);
1308
+ await this.atomicWriteTextFile(this.getChunkFilePath(channelDirPath, fileName), JSON.stringify(messages, null, 2));
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
+ }
618
1332
  }
1333
+ await this.writeChannelIndex(import_node_path3.default.join(channelDirPath, "index.json"), indexData);
619
1334
  }
620
- // 读取元数据(bots、channels、pinned等)
621
- readMetadata() {
622
- if (!import_node_fs2.default.existsSync(this.metadataFilePath)) {
623
- return {
624
- bots: {},
625
- channels: {},
626
- pinnedBots: [],
627
- pinnedChannels: []
628
- };
1335
+ async appendMessagesToChannel(selfId, channelId, messages) {
1336
+ if (!messages.length) {
1337
+ return;
629
1338
  }
630
- try {
631
- const jsonData = import_node_fs2.default.readFileSync(this.metadataFilePath, "utf8");
632
- const data = JSON.parse(jsonData);
633
- return {
634
- bots: data.bots || {},
635
- channels: data.channels || {},
636
- pinnedBots: data.pinnedBots || [],
637
- pinnedChannels: data.pinnedChannels || [],
638
- lastSaveTime: data.lastSaveTime
639
- };
640
- } catch (error) {
641
- this.logger.error("读取元数据失败:", error);
642
- return {
643
- bots: {},
644
- channels: {},
645
- pinnedBots: [],
646
- pinnedChannels: []
647
- };
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
+ }
648
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);
649
1370
  }
650
- // 只读取元数据,不加载消息(公共方法)
651
- readMetadataOnly() {
652
- return this.readMetadata();
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;
653
1411
  }
654
- // 写入元数据
655
- writeMetadata(metadata) {
1412
+ async countChannelMessages(selfId, channelId) {
1413
+ const entry = this.createStoredChannelEntry(selfId, channelId);
656
1414
  try {
657
- this.ensureDir(import_node_path2.default.dirname(this.metadataFilePath));
658
- const dataToWrite = {
659
- ...metadata,
660
- lastSaveTime: Date.now()
661
- };
662
- const jsonData = JSON.stringify(dataToWrite, null, 2);
663
- import_node_fs2.default.writeFileSync(this.metadataFilePath, jsonData, "utf8");
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
+ }
664
1420
  } catch (error) {
665
- this.logger.error("写入元数据失败:", error);
1421
+ if (!this.isFileMissingError(error)) {
1422
+ this.logger.error(`读取频道索引失败 [${entry.channelKey}]:`, error);
1423
+ }
666
1424
  }
1425
+ return 0;
667
1426
  }
668
- // 扫描所有频道消息文件并加载到内存
669
- loadAllChannelMessages() {
670
- const messages = {};
671
- if (!import_node_fs2.default.existsSync(this.chatHistoryDir)) {
672
- return messages;
673
- }
1427
+ async removeChannelStorage(selfId, channelId) {
1428
+ const entry = this.createStoredChannelEntry(selfId, channelId);
674
1429
  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);
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;
686
1453
  }
687
1454
  }
688
- } catch (error) {
689
- this.logger.error("加载频道消息失败:", error);
1455
+ if (!chunkChanged) {
1456
+ continue;
1457
+ }
1458
+ await this.writeChunkMessages(entry.channelDirPath, chunk.fileName, messages);
1459
+ changed = true;
690
1460
  }
691
- return messages;
1461
+ return changed;
692
1462
  }
693
- readChatDataFromFile() {
694
- if (this.memoryCache) {
695
- return this.memoryCache;
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;
696
1477
  }
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);
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;
707
1506
  }
708
- });
709
- this.memoryCache = {
710
- bots: {},
711
- channels: {},
712
- messages: {},
713
- pinnedBots: [],
714
- pinnedChannels: []
715
- };
716
- return this.memoryCache;
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;
717
1523
  }
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);
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;
733
1530
  }
734
- });
1531
+ seen.add(message.id);
1532
+ deduplicated.push(message);
1533
+ }
1534
+ return deduplicated;
735
1535
  }
736
- // 为特定频道安排写入
737
- scheduleWrite(channelKey) {
738
- const existingTimer = this.writeTimers.get(channelKey);
739
- if (existingTimer) {
740
- existingTimer();
1536
+ getCachedChannelMessages(channelKey) {
1537
+ const cached = this.channelMessagesCache.get(channelKey);
1538
+ if (!cached) {
1539
+ return void 0;
741
1540
  }
742
- const timer = this.ctx.setTimeout(() => {
743
- process.nextTick(() => {
744
- this.flushPendingMessages(channelKey);
745
- });
746
- this.writeTimers.delete(channelKey);
747
- }, this.WRITE_DEBOUNCE_MS);
748
- this.writeTimers.set(channelKey, timer);
1541
+ this.channelMessagesCache.delete(channelKey);
1542
+ this.channelMessagesCache.set(channelKey, cached);
1543
+ this.memoryCache.messages = this.getChannelMessagesCacheSnapshot();
1544
+ return cached;
749
1545
  }
750
- // 刷新特定频道的待写入消息
751
- flushPendingMessages(channelKey) {
752
- const messagesToWrite = this.pendingMessages.get(channelKey);
753
- if (!messagesToWrite || messagesToWrite.length === 0) return;
754
- this.pendingMessages.delete(channelKey);
755
- const data = this.memoryCache || this.readChatDataFromFile();
756
- const [selfId, channelId] = channelKey.split(":");
757
- if (!selfId || !channelId) return;
758
- if (!data.messages[channelKey]) {
759
- data.messages[channelKey] = [];
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);
760
1561
  }
761
- for (const messageInfo of messagesToWrite) {
762
- const existingMessage = data.messages[channelKey].find((m) => m.id === messageInfo.id);
763
- if (existingMessage) {
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)) {
764
1576
  continue;
765
1577
  }
766
- if (!messageInfo.timestamp) {
767
- messageInfo.timestamp = Date.now();
768
- }
769
- const cleanedMessageInfo = this.utils.cleanBase64Content(messageInfo);
770
- data.messages[channelKey].push(cleanedMessageInfo);
1578
+ knownIds.add(message.id);
1579
+ merged.push(message);
771
1580
  }
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);
1581
+ return merged;
1582
+ }
1583
+ limitChannelMessages(messages) {
1584
+ if (messages.length <= this.config.maxMessagesPerChannel) {
1585
+ return messages;
775
1586
  }
776
- this.writeChannelMessages(selfId, channelId, data.messages[channelKey]);
777
- this.memoryCache = data;
778
- this.logInfo(`批量写入 ${messagesToWrite.length} 条消息到频道 ${channelKey}`);
1587
+ return [...messages].sort((left, right) => left.timestamp - right.timestamp).slice(-this.config.maxMessagesPerChannel);
779
1588
  }
780
- cleanExcessMessages(data) {
781
- let cleanedCount = 0;
782
- const cleanedMessages = {};
783
- for (const [channelKey, messages] of Object.entries(data.messages)) {
784
- if (messages.length > this.config.maxMessagesPerChannel) {
785
- const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp);
786
- const keptMessages = sortedMessages.slice(-this.config.maxMessagesPerChannel);
787
- cleanedCount += messages.length - keptMessages.length;
788
- cleanedMessages[channelKey] = keptMessages;
789
- this.logInfo(`频道 ${channelKey} 清理了 ${messages.length - keptMessages.length} 条旧消息,保留最新 ${keptMessages.length} 条`);
790
- } else {
791
- cleanedMessages[channelKey] = messages;
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;
792
1602
  }
793
1603
  }
794
- if (cleanedCount > 0) {
795
- this.logInfo("总共清理超量消息:", cleanedCount, "条");
1604
+ return false;
1605
+ }
1606
+ syncDirtyChannelState(channelKey, totalMessages) {
1607
+ if (totalMessages > this.config.maxMessagesPerChannel) {
1608
+ this.dirtyChannelKeys.add(channelKey);
1609
+ return;
796
1610
  }
797
- return {
798
- ...data,
799
- messages: cleanedMessages
800
- };
1611
+ this.dirtyChannelKeys.delete(channelKey);
801
1612
  }
802
- async addMessageToFile(messageInfo) {
803
- const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`;
804
- if (!this.pendingMessages.has(channelKey)) {
805
- this.pendingMessages.set(channelKey, []);
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;
806
1626
  }
807
- this.pendingMessages.get(channelKey).push(messageInfo);
808
- const data = this.memoryCache || this.readChatDataFromFile();
809
- if (!data.messages[channelKey]) {
810
- data.messages[channelKey] = [];
1627
+ const nextMessageIds = messageIds.filter((id) => id !== messageId);
1628
+ if (nextMessageIds.length) {
1629
+ this.pendingBotMessageIds.set(channelKey, nextMessageIds);
1630
+ return;
811
1631
  }
812
- const existingMessage = data.messages[channelKey].find((m) => m.id === messageInfo.id);
813
- if (!existingMessage) {
814
- if (!messageInfo.timestamp) {
815
- messageInfo.timestamp = Date.now();
816
- }
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);
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);
822
1649
  }
823
- this.memoryCache = data;
1650
+ nextMessageIds.push(messageId);
824
1651
  }
825
- this.scheduleWrite(channelKey);
1652
+ const maxSize = this.getRecentMessageIdCacheLimit();
1653
+ this.recentMessageIdsCache.set(channelKey, nextMessageIds.slice(-maxSize));
826
1654
  }
827
- dispose() {
828
- for (const [channelKey, timer] of this.writeTimers.entries()) {
829
- timer();
830
- this.flushPendingMessages(channelKey);
1655
+ forgetRecentMessageIds(channelKey, messageIds) {
1656
+ const currentMessageIds = this.recentMessageIdsCache.get(channelKey);
1657
+ if (!currentMessageIds?.length || !messageIds.length) {
1658
+ return;
831
1659
  }
832
- this.writeTimers.clear();
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);
833
1666
  }
834
- logInfo(...args) {
835
- if (this.config.loggerinfo) {
836
- this.logger.info(...args);
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";
1710
+ }
1711
+ async atomicWriteTextFile(filePath, content) {
1712
+ const tempFilePath = `${filePath}.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}.tmp`;
1713
+ await import_node_fs3.promises.writeFile(
1714
+ tempFilePath,
1715
+ content,
1716
+ "utf8"
1717
+ );
1718
+ try {
1719
+ await import_node_fs3.promises.rename(tempFilePath, filePath);
1720
+ } catch (error) {
1721
+ if (this.isAtomicRenameReplaceError(error)) {
1722
+ await import_node_fs3.promises.rm(filePath, { force: true });
1723
+ await import_node_fs3.promises.rename(tempFilePath, filePath);
1724
+ return;
1725
+ }
1726
+ try {
1727
+ await import_node_fs3.promises.rm(tempFilePath, { force: true });
1728
+ } catch {
1729
+ }
1730
+ throw error;
837
1731
  }
838
1732
  }
1733
+ isAtomicRenameReplaceError(error) {
1734
+ return typeof error === "object" && error !== null && "code" in error && (error.code === "EEXIST" || error.code === "EPERM");
1735
+ }
839
1736
  };
840
1737
 
841
1738
  // src/api-handlers.ts
842
1739
  var import_koishi = require("koishi");
843
1740
  var import_node_url2 = require("node:url");
844
- var import_node_fs3 = require("node:fs");
1741
+ var import_node_fs4 = require("node:fs");
1742
+ var import_node_path4 = __toESM(require("node:path"));
1743
+ var import_node_crypto3 = require("node:crypto");
845
1744
  var mime = __toESM(require("mime-types"));
846
1745
  var ApiHandlers = class {
847
- constructor(ctx, config, fileManager, messageHandler) {
1746
+ constructor(ctx, config, fileManager, messageHandler, logger) {
848
1747
  this.ctx = ctx;
849
1748
  this.config = config;
850
1749
  this.fileManager = fileManager;
851
1750
  this.messageHandler = messageHandler;
852
- this.logger = ctx.logger("chat-patch");
1751
+ this.logger = logger;
853
1752
  }
854
1753
  static {
855
1754
  __name(this, "ApiHandlers");
856
1755
  }
857
- logger;
858
1756
  currentTempVideo = null;
859
1757
  registerApiHandlers() {
860
1758
  this.ctx.console.addListener("clear-all-indexeddb-data", async () => {
@@ -868,7 +1766,7 @@ var ApiHandlers = class {
868
1766
  });
869
1767
  this.ctx.console.addListener("get-chat-data", async () => {
870
1768
  try {
871
- const data = this.fileManager.readMetadataOnly();
1769
+ const data = await this.fileManager.readMetadataOnly();
872
1770
  this.logInfo("获取基础聊天数据(仅元数据)");
873
1771
  return {
874
1772
  success: true,
@@ -888,21 +1786,18 @@ var ApiHandlers = class {
888
1786
  });
889
1787
  this.ctx.console.addListener("get-history-messages", async (requestData) => {
890
1788
  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, "条消息");
1789
+ const limit = Math.max(1, requestData.limit ?? this.config.messageChunkSize);
1790
+ const result = await this.fileManager.readChannelMessagesPage(
1791
+ requestData.selfId,
1792
+ requestData.channelId,
1793
+ limit,
1794
+ requestData.offset || 0
1795
+ );
1796
+ this.logInfo("获取历史消息:", `${requestData.selfId}:${requestData.channelId}`, "共", result.messages.length, "条消息");
902
1797
  return {
903
1798
  success: true,
904
- messages,
905
- total: sortedMessages.length
1799
+ messages: result.messages,
1800
+ total: result.total
906
1801
  };
907
1802
  } catch (error) {
908
1803
  this.logger.error("获取历史消息失败:", error);
@@ -911,11 +1806,7 @@ var ApiHandlers = class {
911
1806
  });
912
1807
  this.ctx.console.addListener("get-all-channel-message-counts", async () => {
913
1808
  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
- }
1809
+ const counts = await this.fileManager.getAllChannelMessageCounts();
919
1810
  this.logInfo("获取所有频道消息数量:", {
920
1811
  频道数: Object.keys(counts).length,
921
1812
  总消息数: Object.values(counts).reduce((total, count) => total + count, 0)
@@ -946,16 +1837,13 @@ var ApiHandlers = class {
946
1837
  viteUrl: `/vite/@fs/${normalizedPath2}`
947
1838
  };
948
1839
  }
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";
1840
+ const dir = import_node_path4.default.join(this.ctx.baseDir, "data", "chat-patch", "persist-media", "images");
1841
+ await import_node_fs4.promises.mkdir(dir, { recursive: true });
1842
+ const hash = (0, import_node_crypto3.createHash)("md5").update(data.url).digest("hex");
1843
+ const ext = import_node_path4.default.extname(new import_node_url2.URL(data.url).pathname) || ".jpg";
956
1844
  const filename = `${hash}${ext}`;
957
- const filePath = require("node:path").join(dir, filename);
958
- if (!require("node:fs").existsSync(filePath)) {
1845
+ const filePath = import_node_path4.default.join(dir, filename);
1846
+ if (!await this.fileExists(filePath)) {
959
1847
  const response = await fetch(data.url, {
960
1848
  headers: {
961
1849
  "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 +1854,7 @@ var ApiHandlers = class {
966
1854
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
967
1855
  }
968
1856
  const buffer = await response.arrayBuffer();
969
- require("node:fs").writeFileSync(filePath, Buffer.from(buffer));
1857
+ await import_node_fs4.promises.writeFile(filePath, Buffer.from(buffer));
970
1858
  }
971
1859
  const normalizedPath = filePath.replace(/\\/g, "/");
972
1860
  return {
@@ -974,28 +1862,24 @@ var ApiHandlers = class {
974
1862
  viteUrl: `/vite/@fs/${normalizedPath}`
975
1863
  };
976
1864
  } catch (error) {
977
- this.logger.error("获取图片失败:", error);
978
- return { success: false, error: error?.message || String(error) };
1865
+ return { success: false, error: this.getClientErrorMessage(error) };
979
1866
  }
980
1867
  });
981
1868
  this.ctx.console.addListener("clear-channel-history", async (data) => {
982
1869
  try {
983
1870
  this.logInfo("收到清理历史记录请求(已废弃,建议使用删除频道数据):", data);
984
- const chatData = this.fileManager.readChatDataFromFile();
985
1871
  const channelKey = `${data.selfId}:${data.channelId}`;
986
- if (!chatData.messages[channelKey]) {
1872
+ const { deletedMessages } = await this.fileManager.deleteChannelData(data.selfId, data.channelId);
1873
+ if (!deletedMessages) {
987
1874
  return { success: true, message: "频道没有历史消息" };
988
1875
  }
989
- const originalCount = chatData.messages[channelKey].length;
990
- delete chatData.messages[channelKey];
991
- this.fileManager.writeChatDataToFile(chatData);
992
1876
  this.logInfo(`频道 ${channelKey} 历史记录已清空:`, {
993
- 清理消息数: originalCount
1877
+ 清理消息数: deletedMessages
994
1878
  });
995
1879
  return {
996
1880
  success: true,
997
- message: `成功清理 ${originalCount} 条历史消息`,
998
- clearedCount: originalCount,
1881
+ message: `成功清理 ${deletedMessages} 条历史消息`,
1882
+ clearedCount: deletedMessages,
999
1883
  keptCount: 0
1000
1884
  };
1001
1885
  } catch (error) {
@@ -1017,13 +1901,12 @@ var ApiHandlers = class {
1017
1901
  }
1018
1902
  let messageContent = data.content;
1019
1903
  if (data.images && data.images.length > 0) {
1020
- const tempDir = this.ctx.baseDir + "/data/chat-patch/temp";
1904
+ const tempDir = import_node_path4.default.join(this.ctx.baseDir, "data", "chat-patch", "temp");
1905
+ const tempFiles = await this.safeReadDir(tempDir);
1021
1906
  for (const image of data.images) {
1022
- const files = require("fs").readdirSync(tempDir).filter(
1023
- (file) => file.includes(`temp_${image.tempId}`)
1024
- );
1907
+ const files = tempFiles.filter((file) => file.includes(`temp_${image.tempId}`));
1025
1908
  if (files.length > 0) {
1026
- const imagePath = `${tempDir}/${files[0]}`;
1909
+ const imagePath = import_node_path4.default.join(tempDir, files[0]);
1027
1910
  const fileUrl = this.createFileUrl(imagePath);
1028
1911
  messageContent += import_koishi.h.image(fileUrl).toString();
1029
1912
  this.logInfo("添加图片到消息:", { imagePath, fileUrl });
@@ -1036,14 +1919,9 @@ var ApiHandlers = class {
1036
1919
  this.logInfo("消息发送成功:", result);
1037
1920
  const messageId = Array.isArray(result) ? result[0] : result;
1038
1921
  if (messageId) {
1039
- const chatData = this.fileManager.readChatDataFromFile();
1040
1922
  const channelKey = `${data.selfId}:${data.channelId}`;
1041
- const messages = chatData.messages[channelKey] || [];
1042
- const msg = [...messages].reverse().find((m) => m.type === "bot" && m.sending);
1923
+ const msg = await this.fileManager.markLatestBotMessageAsSent(data.selfId, data.channelId, messageId);
1043
1924
  if (msg) {
1044
- msg.realId = messageId;
1045
- msg.sending = false;
1046
- this.fileManager.writeChatDataToFile(chatData);
1047
1925
  this.ctx.console.broadcast("bot-message-updated", {
1048
1926
  channelKey,
1049
1927
  tempId: msg.id,
@@ -1074,20 +1952,7 @@ var ApiHandlers = class {
1074
1952
  this.ctx.console.addListener("delete-bot-data", async (data) => {
1075
1953
  try {
1076
1954
  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);
1955
+ const { deletedChannels, deletedMessages } = await this.fileManager.deleteBotData(data.selfId);
1091
1956
  this.logInfo(`机器人 ${data.selfId} 数据删除完成:`, {
1092
1957
  删除频道数: deletedChannels,
1093
1958
  删除消息数: deletedMessages
@@ -1106,17 +1971,8 @@ var ApiHandlers = class {
1106
1971
  this.ctx.console.addListener("delete-channel-data", async (data) => {
1107
1972
  try {
1108
1973
  this.logInfo("收到删除频道数据请求:", data);
1109
- const chatData = this.fileManager.readChatDataFromFile();
1110
1974
  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);
1975
+ const { deletedMessages } = await this.fileManager.deleteChannelData(data.selfId, data.channelId);
1120
1976
  this.logInfo(`频道 ${channelKey} 数据删除完成:`, {
1121
1977
  删除消息数: deletedMessages
1122
1978
  });
@@ -1133,9 +1989,7 @@ var ApiHandlers = class {
1133
1989
  this.ctx.console.addListener("set-pinned-bots", async (data) => {
1134
1990
  try {
1135
1991
  this.logInfo("收到设置置顶机器人请求:", data.pinnedBots);
1136
- const chatData = this.fileManager.readChatDataFromFile();
1137
- chatData.pinnedBots = data.pinnedBots;
1138
- this.fileManager.writeChatDataToFile(chatData);
1992
+ await this.fileManager.setPinnedBots(data.pinnedBots);
1139
1993
  return { success: true };
1140
1994
  } catch (error) {
1141
1995
  this.logger.error("设置置顶机器人失败:", error);
@@ -1145,9 +1999,7 @@ var ApiHandlers = class {
1145
1999
  this.ctx.console.addListener("set-pinned-channels", async (data) => {
1146
2000
  try {
1147
2001
  this.logInfo("收到设置置顶频道请求:", data.pinnedChannels);
1148
- const chatData = this.fileManager.readChatDataFromFile();
1149
- chatData.pinnedChannels = data.pinnedChannels;
1150
- this.fileManager.writeChatDataToFile(chatData);
2002
+ await this.fileManager.setPinnedChannels(data.pinnedChannels);
1151
2003
  return { success: true };
1152
2004
  } catch (error) {
1153
2005
  this.logger.error("设置置顶频道失败:", error);
@@ -1162,12 +2014,10 @@ var ApiHandlers = class {
1162
2014
  const tempId = Date.now() + "_" + Math.random().toString(36).substring(2, 11);
1163
2015
  const extension = data.filename.split(".").pop()?.toLowerCase() || (data.isGif ? "gif" : "jpg");
1164
2016
  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);
2017
+ const tempDir = import_node_path4.default.join(this.ctx.baseDir, "data", "chat-patch", "temp");
2018
+ await import_node_fs4.promises.mkdir(tempDir, { recursive: true });
2019
+ const tempPath = import_node_path4.default.join(tempDir, tempFilename);
2020
+ await import_node_fs4.promises.writeFile(tempPath, buffer);
1171
2021
  this.logInfo("图片上传成功:", { tempPath, size: buffer.length, isGif: data.isGif });
1172
2022
  return {
1173
2023
  success: true,
@@ -1184,14 +2034,12 @@ var ApiHandlers = class {
1184
2034
  });
1185
2035
  this.ctx.console.addListener("delete-temp-image", async (data) => {
1186
2036
  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
- );
2037
+ const tempDir = import_node_path4.default.join(this.ctx.baseDir, "data", "chat-patch", "temp");
2038
+ const files = (await this.safeReadDir(tempDir)).filter((file) => file.includes(`temp_${data.tempId}`));
1191
2039
  for (const file of files) {
1192
- const filePath = `${tempDir}/${file}`;
1193
- if (require("fs").existsSync(filePath)) {
1194
- require("fs").unlinkSync(filePath);
2040
+ const filePath = import_node_path4.default.join(tempDir, file);
2041
+ if (await this.fileExists(filePath)) {
2042
+ await import_node_fs4.promises.unlink(filePath);
1195
2043
  this.logInfo("删除临时图片:", filePath);
1196
2044
  }
1197
2045
  }
@@ -1208,6 +2056,8 @@ var ApiHandlers = class {
1208
2056
  success: true,
1209
2057
  config: {
1210
2058
  maxMessagesPerChannel: this.config.maxMessagesPerChannel,
2059
+ messageChunkSize: this.config.messageChunkSize,
2060
+ channelCacheLimit: this.config.channelCacheLimit,
1211
2061
  maxPersistImages: this.config.maxPersistImages,
1212
2062
  loggerinfo: this.config.loggerinfo,
1213
2063
  blockedPlatforms: this.config.blockedPlatforms || [],
@@ -1231,44 +2081,8 @@ var ApiHandlers = class {
1231
2081
  if (user.avatar) {
1232
2082
  user.avatar = await this.messageHandler.downloadAndCacheMedia(user.avatar, "avatar");
1233
2083
  }
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
- }
2084
+ const changed = await this.fileManager.updateUserProfileInBotData(data.selfId, data.userId, user.name, user.avatar);
1270
2085
  if (changed) {
1271
- this.fileManager.writeChatDataToFile(chatData);
1272
2086
  this.ctx.console.broadcast("chat-data-updated", {});
1273
2087
  }
1274
2088
  }
@@ -1277,18 +2091,6 @@ var ApiHandlers = class {
1277
2091
  return { success: false, error: error?.message || "获取用户信息失败" };
1278
2092
  }
1279
2093
  });
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
2094
  this.ctx.console.addListener("fetch-video-temp", async (data) => {
1293
2095
  try {
1294
2096
  this.logInfo("收到视频临时加载请求:", data.url);
@@ -1306,16 +2108,13 @@ var ApiHandlers = class {
1306
2108
  viteUrl: `/vite/@fs/${normalizedPath2}`
1307
2109
  };
1308
2110
  }
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";
2111
+ const dir = import_node_path4.default.join(this.ctx.baseDir, "data", "chat-patch", "persist-media", "media");
2112
+ await import_node_fs4.promises.mkdir(dir, { recursive: true });
2113
+ const hash = (0, import_node_crypto3.createHash)("md5").update(data.url).digest("hex");
2114
+ const ext = import_node_path4.default.extname(new import_node_url2.URL(data.url).pathname) || ".mp4";
1316
2115
  const filename = `${hash}${ext}`;
1317
- const filePath = require("node:path").join(dir, filename);
1318
- if (!require("node:fs").existsSync(filePath)) {
2116
+ const filePath = import_node_path4.default.join(dir, filename);
2117
+ if (!await this.fileExists(filePath)) {
1319
2118
  const response = await fetch(data.url, {
1320
2119
  headers: {
1321
2120
  "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
@@ -1326,7 +2125,7 @@ var ApiHandlers = class {
1326
2125
  throw new Error(`HTTP ${response.status}: ${response.statusText}`);
1327
2126
  }
1328
2127
  const buffer = await response.arrayBuffer();
1329
- require("node:fs").writeFileSync(filePath, Buffer.from(buffer));
2128
+ await import_node_fs4.promises.writeFile(filePath, Buffer.from(buffer));
1330
2129
  this.logInfo("视频下载成功:", { size: buffer.byteLength, path: filePath });
1331
2130
  }
1332
2131
  const normalizedPath = filePath.replace(/\\/g, "/");
@@ -1335,8 +2134,7 @@ var ApiHandlers = class {
1335
2134
  viteUrl: `/vite/@fs/${normalizedPath}`
1336
2135
  };
1337
2136
  } catch (error) {
1338
- this.logger.error("视频临时加载失败:", error);
1339
- return { success: false, error: error?.message || String(error) };
2137
+ return { success: false, error: this.getClientErrorMessage(error) };
1340
2138
  }
1341
2139
  });
1342
2140
  }
@@ -1359,7 +2157,7 @@ var ApiHandlers = class {
1359
2157
  async handleLocalFileRequest(fileUrl) {
1360
2158
  try {
1361
2159
  const filePath = (0, import_node_url2.fileURLToPath)(fileUrl);
1362
- const buffer = (0, import_node_fs3.readFileSync)(filePath);
2160
+ const buffer = await import_node_fs4.promises.readFile(filePath);
1363
2161
  const base64 = buffer.toString("base64");
1364
2162
  const contentType = mime.lookup(filePath) || "application/octet-stream";
1365
2163
  this.logInfo("成功读取本地文件:", { fileUrl, filePath, contentType });
@@ -1370,7 +2168,6 @@ var ApiHandlers = class {
1370
2168
  dataUrl: `data:${contentType};base64,${base64}`
1371
2169
  };
1372
2170
  } catch (error) {
1373
- this.logger.error("读取本地文件失败:", { fileUrl, error: error.message });
1374
2171
  return {
1375
2172
  success: false,
1376
2173
  error: `读取本地文件失败: ${error.message}`
@@ -1383,40 +2180,85 @@ var ApiHandlers = class {
1383
2180
  }, 5 * 60 * 1e3);
1384
2181
  }
1385
2182
  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);
2183
+ const baseDir = import_node_path4.default.join(this.ctx.baseDir, "data", "chat-patch", "persist-media");
2184
+ await this.cleanupMediaCacheDir(import_node_path4.default.join(baseDir, "images"), 100);
2185
+ await this.cleanupMediaCacheDir(import_node_path4.default.join(baseDir, "media"), 20);
1407
2186
  }
1408
2187
  logInfo(...args) {
1409
2188
  if (this.config.loggerinfo) {
1410
- this.logger.info(...args);
2189
+ Reflect.apply(this.logger.info, this.logger, args);
2190
+ }
2191
+ }
2192
+ async fileExists(filePath) {
2193
+ try {
2194
+ await import_node_fs4.promises.access(filePath);
2195
+ return true;
2196
+ } catch {
2197
+ return false;
1411
2198
  }
1412
2199
  }
2200
+ async safeReadDir(dirPath) {
2201
+ try {
2202
+ return await import_node_fs4.promises.readdir(dirPath);
2203
+ } catch {
2204
+ return [];
2205
+ }
2206
+ }
2207
+ async cleanupMediaCacheDir(dirPath, limit) {
2208
+ const files = await this.safeReadDir(dirPath);
2209
+ if (!files.length) {
2210
+ return;
2211
+ }
2212
+ const fileStats = await Promise.all(files.map(async (fileName) => {
2213
+ const filePath = import_node_path4.default.join(dirPath, fileName);
2214
+ const stats = await import_node_fs4.promises.stat(filePath);
2215
+ return { path: filePath, mtime: stats.mtimeMs };
2216
+ }));
2217
+ fileStats.sort((left, right) => right.mtime - left.mtime);
2218
+ for (const file of fileStats.slice(limit)) {
2219
+ try {
2220
+ await import_node_fs4.promises.unlink(file.path);
2221
+ } catch {
2222
+ continue;
2223
+ }
2224
+ }
2225
+ }
2226
+ getClientErrorMessage(error) {
2227
+ if (typeof error === "object" && error !== null && "message" in error && typeof error.message === "string") {
2228
+ return error.message;
2229
+ }
2230
+ return String(error);
2231
+ }
1413
2232
  };
1414
2233
 
2234
+ // src/logger.ts
2235
+ function createPluginLogger(logger, config) {
2236
+ return {
2237
+ logInfo(...args) {
2238
+ if (config.loggerinfo) {
2239
+ Reflect.apply(logger.info, logger, args);
2240
+ }
2241
+ },
2242
+ info(...args) {
2243
+ Reflect.apply(logger.info, logger, args);
2244
+ },
2245
+ warn(...args) {
2246
+ Reflect.apply(logger.warn, logger, args);
2247
+ },
2248
+ error(...args) {
2249
+ Reflect.apply(logger.error, logger, args);
2250
+ }
2251
+ };
2252
+ }
2253
+ __name(createPluginLogger, "createPluginLogger");
2254
+
1415
2255
  // src/config.ts
1416
2256
  var import_koishi2 = require("koishi");
1417
2257
  var Config = import_koishi2.Schema.intersect([
1418
2258
  import_koishi2.Schema.object({
1419
2259
  maxMessagesPerChannel: import_koishi2.Schema.number().default(500).description("每个群组最大保存消息数量").min(50).max(1500).step(1),
2260
+ messageChunkSize: import_koishi2.Schema.number().default(100).description("单个消息分块文件最大消息数量").min(20).max(500).step(1),
2261
+ channelCacheLimit: import_koishi2.Schema.number().default(50).description("内存中最多缓存的频道消息数量").min(1).max(200).step(1),
1420
2262
  maxPersistImages: import_koishi2.Schema.number().default(100).description("持久化存储的图片缓存数量").min(10).max(500).step(1),
1421
2263
  blockedPlatforms: import_koishi2.Schema.array(import_koishi2.Schema.object({
1422
2264
  platformName: import_koishi2.Schema.string().description("平台名称或关键词"),
@@ -1462,118 +2304,49 @@ var usage = `
1462
2304
  ---
1463
2305
  `;
1464
2306
  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);
2307
+ const pluginLogger = createPluginLogger(ctx.logger("chat-patch"), config);
2308
+ const fileManager = new FileManager(ctx, config, pluginLogger);
2309
+ await fileManager.initialize();
2310
+ const messageHandler = new MessageHandler(ctx, config, fileManager, pluginLogger);
2311
+ const apiHandlers = new ApiHandlers(ctx, config, fileManager, messageHandler, pluginLogger);
1490
2312
  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)
2313
+ const metadata = await fileManager.readMetadataOnly();
2314
+ pluginLogger.logInfo("插件加载完成,元数据统计:", {
2315
+ 机器人数量: Object.keys(metadata.bots).length,
2316
+ 频道数量: Object.keys(metadata.channels).reduce((total, botId) => total + Object.keys(metadata.channels[botId] || {}).length, 0),
2317
+ 置顶机器人数量: metadata.pinnedBots.length,
2318
+ 置顶频道数量: metadata.pinnedChannels.length
1509
2319
  });
1510
2320
  ctx.on("message", (session) => {
1511
2321
  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);
2322
+ messageHandler.recordUserMessage(session, Date.now());
1532
2323
  });
1533
2324
  ctx.on("before-send", (session) => {
1534
2325
  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);
2326
+ messageHandler.recordBotMessage(session, Date.now());
1555
2327
  });
2328
+ let cleanupDelayTimer;
2329
+ let cleanupInterval;
1556
2330
  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
- }
2331
+ pluginLogger.logInfo("插件启动完成,开始监听消息");
2332
+ cleanupDelayTimer = ctx.setTimeout(() => {
2333
+ void fileManager.cleanupExcessMessagesInStorage();
2334
+ }, 15e3);
2335
+ cleanupInterval = ctx.setInterval(() => {
2336
+ void fileManager.cleanupExcessMessagesInStorage();
1567
2337
  }, 3e5);
1568
2338
  });
1569
2339
  apiHandlers.registerApiHandlers();
1570
2340
  ctx.console.addEntry({
1571
- dev: import_node_path3.default.resolve(__dirname, "../client/index.ts"),
1572
- prod: import_node_path3.default.resolve(__dirname, "../dist")
2341
+ dev: import_node_path5.default.resolve(__dirname, "../client/index.ts"),
2342
+ prod: import_node_path5.default.resolve(__dirname, "../dist")
1573
2343
  });
1574
2344
  ctx.on("dispose", () => {
1575
- fileManager.dispose();
1576
- logInfo("插件已卸载,所有待处理的消息已写入");
2345
+ cleanupDelayTimer?.();
2346
+ cleanupInterval?.();
2347
+ messageHandler.dispose();
2348
+ void fileManager.dispose();
2349
+ pluginLogger.logInfo("插件已卸载,所有待处理的消息已写入");
1577
2350
  });
1578
2351
  }
1579
2352
  __name(apply, "apply");