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