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