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