koishi-plugin-chat-patch 2.4.0 → 2.4.4
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/vue/composables/useChatData.ts +0 -1
- package/client/vue/composables/useImageCache.ts +162 -21
- package/client/vue/composables/useVideoCache.ts +16 -21
- package/client/vue/types.ts +0 -1
- package/dist/index.js +2 -2
- package/lib/config.d.ts +0 -1
- package/lib/file-manager.d.ts +2 -1
- package/lib/index.js +96 -64
- package/lib/message-handler.d.ts +1 -1
- package/package.json +1 -1
- package/src/api-handlers.ts +103 -65
- package/src/config.ts +0 -2
- package/src/file-manager.ts +7 -2
- package/src/message-handler.ts +12 -7
package/lib/config.d.ts
CHANGED
package/lib/file-manager.d.ts
CHANGED
|
@@ -16,9 +16,10 @@ export declare class FileManager {
|
|
|
16
16
|
private cleanupOldDataFiles;
|
|
17
17
|
private ensureDir;
|
|
18
18
|
private getChannelFilePath;
|
|
19
|
-
|
|
19
|
+
readChannelMessages(selfId: string, channelId: string): MessageInfo[];
|
|
20
20
|
private writeChannelMessages;
|
|
21
21
|
private readMetadata;
|
|
22
|
+
readMetadataOnly(): Omit<ChatData, 'messages'>;
|
|
22
23
|
private writeMetadata;
|
|
23
24
|
private loadAllChannelMessages;
|
|
24
25
|
readChatDataFromFile(): ChatData;
|
package/lib/index.js
CHANGED
|
@@ -304,7 +304,8 @@ var MessageHandler = class {
|
|
|
304
304
|
}
|
|
305
305
|
async downloadAndCacheMedia(url, type) {
|
|
306
306
|
try {
|
|
307
|
-
if (!url || url.startsWith("data:")
|
|
307
|
+
if (!url || url.startsWith("data:")) return url;
|
|
308
|
+
if (url.includes("/vite/@fs/")) return url;
|
|
308
309
|
let folder = "media";
|
|
309
310
|
if (type === "image") folder = "images";
|
|
310
311
|
else if (type === "avatar") folder = "avatars";
|
|
@@ -317,13 +318,14 @@ var MessageHandler = class {
|
|
|
317
318
|
const ext = require("node:path").extname(new URL(url).pathname) || (type === "image" ? ".jpg" : ".mp4");
|
|
318
319
|
const filename = `${hash}${ext}`;
|
|
319
320
|
const filePath = require("node:path").join(dir, filename);
|
|
320
|
-
if (require("node:fs").existsSync(filePath)) {
|
|
321
|
-
|
|
321
|
+
if (!require("node:fs").existsSync(filePath)) {
|
|
322
|
+
const buffer = await this.ctx.http.get(url, { responseType: "arraybuffer" });
|
|
323
|
+
require("node:fs").writeFileSync(filePath, Buffer.from(buffer));
|
|
322
324
|
}
|
|
323
|
-
const
|
|
324
|
-
|
|
325
|
-
return require("node:url").pathToFileURL(filePath).href;
|
|
325
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
326
|
+
return `/vite/@fs/${normalizedPath}`;
|
|
326
327
|
} catch (e) {
|
|
328
|
+
this.logger.warn("下载并缓存媒体失败:", e);
|
|
327
329
|
return url;
|
|
328
330
|
}
|
|
329
331
|
}
|
|
@@ -590,7 +592,7 @@ var FileManager = class {
|
|
|
590
592
|
this.ensureDir(botDir);
|
|
591
593
|
return import_node_path2.default.join(botDir, `${channelId}.json`);
|
|
592
594
|
}
|
|
593
|
-
//
|
|
595
|
+
// 读取单个频道的消息(公共方法)
|
|
594
596
|
readChannelMessages(selfId, channelId) {
|
|
595
597
|
const filePath = this.getChannelFilePath(selfId, channelId);
|
|
596
598
|
if (!import_node_fs2.default.existsSync(filePath)) {
|
|
@@ -645,6 +647,10 @@ var FileManager = class {
|
|
|
645
647
|
};
|
|
646
648
|
}
|
|
647
649
|
}
|
|
650
|
+
// 只读取元数据,不加载消息(公共方法)
|
|
651
|
+
readMetadataOnly() {
|
|
652
|
+
return this.readMetadata();
|
|
653
|
+
}
|
|
648
654
|
// 写入元数据
|
|
649
655
|
writeMetadata(metadata) {
|
|
650
656
|
try {
|
|
@@ -862,8 +868,8 @@ var ApiHandlers = class {
|
|
|
862
868
|
});
|
|
863
869
|
this.ctx.console.addListener("get-chat-data", async () => {
|
|
864
870
|
try {
|
|
865
|
-
const data = this.fileManager.
|
|
866
|
-
this.logInfo("
|
|
871
|
+
const data = this.fileManager.readMetadataOnly();
|
|
872
|
+
this.logInfo("获取基础聊天数据(仅元数据)");
|
|
867
873
|
return {
|
|
868
874
|
success: true,
|
|
869
875
|
data: {
|
|
@@ -871,6 +877,7 @@ var ApiHandlers = class {
|
|
|
871
877
|
channels: data.channels || {},
|
|
872
878
|
pinnedBots: data.pinnedBots || [],
|
|
873
879
|
pinnedChannels: data.pinnedChannels || [],
|
|
880
|
+
// 不返回消息数据,由前端按需加载
|
|
874
881
|
messages: {}
|
|
875
882
|
}
|
|
876
883
|
};
|
|
@@ -881,9 +888,7 @@ var ApiHandlers = class {
|
|
|
881
888
|
});
|
|
882
889
|
this.ctx.console.addListener("get-history-messages", async (requestData) => {
|
|
883
890
|
try {
|
|
884
|
-
|
|
885
|
-
const channelKey = `${requestData.selfId}:${requestData.channelId}`;
|
|
886
|
-
let messages = data.messages[channelKey] || [];
|
|
891
|
+
let messages = this.fileManager.readChannelMessages(requestData.selfId, requestData.channelId);
|
|
887
892
|
const sortedMessages = messages.sort((a, b) => b.timestamp - a.timestamp);
|
|
888
893
|
if (requestData.limit !== void 0) {
|
|
889
894
|
const limit = requestData.limit;
|
|
@@ -893,7 +898,7 @@ var ApiHandlers = class {
|
|
|
893
898
|
} else {
|
|
894
899
|
messages = sortedMessages.sort((a, b) => a.timestamp - b.timestamp);
|
|
895
900
|
}
|
|
896
|
-
this.logInfo("获取历史消息:",
|
|
901
|
+
this.logInfo("获取历史消息:", `${requestData.selfId}:${requestData.channelId}`, "共", messages.length, "条消息");
|
|
897
902
|
return {
|
|
898
903
|
success: true,
|
|
899
904
|
messages,
|
|
@@ -926,61 +931,72 @@ var ApiHandlers = class {
|
|
|
926
931
|
});
|
|
927
932
|
this.ctx.console.addListener("fetch-image", async (data) => {
|
|
928
933
|
try {
|
|
934
|
+
if (data.url.includes("/vite/@fs/")) {
|
|
935
|
+
return {
|
|
936
|
+
success: true,
|
|
937
|
+
viteUrl: data.url
|
|
938
|
+
};
|
|
939
|
+
}
|
|
929
940
|
if (this.isFileUrl(data.url)) {
|
|
930
941
|
this.logInfo("处理本地文件请求:", data.url);
|
|
931
|
-
|
|
942
|
+
const filePath2 = require("node:url").fileURLToPath(data.url);
|
|
943
|
+
const normalizedPath2 = filePath2.replace(/\\/g, "/");
|
|
944
|
+
return {
|
|
945
|
+
success: true,
|
|
946
|
+
viteUrl: `/vite/@fs/${normalizedPath2}`
|
|
947
|
+
};
|
|
948
|
+
}
|
|
949
|
+
const dir = require("node:path").join(this.ctx.baseDir, "data", "chat-patch", "persist-media", "images");
|
|
950
|
+
if (!require("node:fs").existsSync(dir)) {
|
|
951
|
+
require("node:fs").mkdirSync(dir, { recursive: true });
|
|
932
952
|
}
|
|
933
|
-
const
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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";
|
|
956
|
+
const filename = `${hash}${ext}`;
|
|
957
|
+
const filePath = require("node:path").join(dir, filename);
|
|
958
|
+
if (!require("node:fs").existsSync(filePath)) {
|
|
959
|
+
const response = await fetch(data.url, {
|
|
960
|
+
headers: {
|
|
961
|
+
"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",
|
|
962
|
+
"Referer": ""
|
|
963
|
+
}
|
|
964
|
+
});
|
|
965
|
+
if (!response.ok) {
|
|
966
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
937
967
|
}
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
968
|
+
const buffer = await response.arrayBuffer();
|
|
969
|
+
require("node:fs").writeFileSync(filePath, Buffer.from(buffer));
|
|
941
970
|
}
|
|
942
|
-
const
|
|
943
|
-
const base64 = Buffer.from(buffer).toString("base64");
|
|
944
|
-
const contentType = response.headers.get("content-type") || "image/jpeg";
|
|
971
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
945
972
|
return {
|
|
946
973
|
success: true,
|
|
947
|
-
|
|
948
|
-
contentType,
|
|
949
|
-
dataUrl: `data:${contentType};base64,${base64}`
|
|
974
|
+
viteUrl: `/vite/@fs/${normalizedPath}`
|
|
950
975
|
};
|
|
951
976
|
} catch (error) {
|
|
977
|
+
this.logger.error("获取图片失败:", error);
|
|
952
978
|
return { success: false, error: error?.message || String(error) };
|
|
953
979
|
}
|
|
954
980
|
});
|
|
955
981
|
this.ctx.console.addListener("clear-channel-history", async (data) => {
|
|
956
982
|
try {
|
|
957
|
-
this.logInfo("
|
|
983
|
+
this.logInfo("收到清理历史记录请求(已废弃,建议使用删除频道数据):", data);
|
|
958
984
|
const chatData = this.fileManager.readChatDataFromFile();
|
|
959
985
|
const channelKey = `${data.selfId}:${data.channelId}`;
|
|
960
986
|
if (!chatData.messages[channelKey]) {
|
|
961
987
|
return { success: true, message: "频道没有历史消息" };
|
|
962
988
|
}
|
|
963
|
-
const
|
|
964
|
-
|
|
965
|
-
const keepCount = data.keepCount || this.config.keepMessagesOnClear;
|
|
966
|
-
if (keepCount > 0 && originalCount <= keepCount) {
|
|
967
|
-
return { success: true, message: `消息数量(${originalCount})未超过保留数量(${keepCount}),无需清理` };
|
|
968
|
-
}
|
|
969
|
-
const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp);
|
|
970
|
-
const keptMessages = keepCount > 0 ? sortedMessages.slice(-keepCount) : [];
|
|
971
|
-
const clearedCount = originalCount - keptMessages.length;
|
|
972
|
-
chatData.messages[channelKey] = keptMessages;
|
|
989
|
+
const originalCount = chatData.messages[channelKey].length;
|
|
990
|
+
delete chatData.messages[channelKey];
|
|
973
991
|
this.fileManager.writeChatDataToFile(chatData);
|
|
974
|
-
this.logInfo(`频道 ${channelKey}
|
|
975
|
-
|
|
976
|
-
保留消息数: keptMessages.length,
|
|
977
|
-
清理消息数: clearedCount
|
|
992
|
+
this.logInfo(`频道 ${channelKey} 历史记录已清空:`, {
|
|
993
|
+
清理消息数: originalCount
|
|
978
994
|
});
|
|
979
995
|
return {
|
|
980
996
|
success: true,
|
|
981
|
-
message: `成功清理 ${
|
|
982
|
-
clearedCount,
|
|
983
|
-
keptCount:
|
|
997
|
+
message: `成功清理 ${originalCount} 条历史消息`,
|
|
998
|
+
clearedCount: originalCount,
|
|
999
|
+
keptCount: 0
|
|
984
1000
|
};
|
|
985
1001
|
} catch (error) {
|
|
986
1002
|
this.logger.error("清理频道历史记录失败:", error);
|
|
@@ -1192,7 +1208,6 @@ var ApiHandlers = class {
|
|
|
1192
1208
|
success: true,
|
|
1193
1209
|
config: {
|
|
1194
1210
|
maxMessagesPerChannel: this.config.maxMessagesPerChannel,
|
|
1195
|
-
keepMessagesOnClear: this.config.keepMessagesOnClear,
|
|
1196
1211
|
maxPersistImages: this.config.maxPersistImages,
|
|
1197
1212
|
loggerinfo: this.config.loggerinfo,
|
|
1198
1213
|
blockedPlatforms: this.config.blockedPlatforms || [],
|
|
@@ -1277,29 +1292,47 @@ var ApiHandlers = class {
|
|
|
1277
1292
|
this.ctx.console.addListener("fetch-video-temp", async (data) => {
|
|
1278
1293
|
try {
|
|
1279
1294
|
this.logInfo("收到视频临时加载请求:", data.url);
|
|
1295
|
+
if (data.url.includes("/vite/@fs/")) {
|
|
1296
|
+
return {
|
|
1297
|
+
success: true,
|
|
1298
|
+
viteUrl: data.url
|
|
1299
|
+
};
|
|
1300
|
+
}
|
|
1280
1301
|
if (this.isFileUrl(data.url)) {
|
|
1281
|
-
const
|
|
1282
|
-
|
|
1302
|
+
const filePath2 = require("node:url").fileURLToPath(data.url);
|
|
1303
|
+
const normalizedPath2 = filePath2.replace(/\\/g, "/");
|
|
1304
|
+
return {
|
|
1305
|
+
success: true,
|
|
1306
|
+
viteUrl: `/vite/@fs/${normalizedPath2}`
|
|
1307
|
+
};
|
|
1283
1308
|
}
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1309
|
+
const dir = require("node:path").join(this.ctx.baseDir, "data", "chat-patch", "persist-media", "media");
|
|
1310
|
+
if (!require("node:fs").existsSync(dir)) {
|
|
1311
|
+
require("node:fs").mkdirSync(dir, { recursive: true });
|
|
1312
|
+
}
|
|
1313
|
+
const crypto = require("node:crypto");
|
|
1314
|
+
const hash = crypto.createHash("md5").update(data.url).digest("hex");
|
|
1315
|
+
const ext = require("node:path").extname(new import_node_url2.URL(data.url).pathname) || ".mp4";
|
|
1316
|
+
const filename = `${hash}${ext}`;
|
|
1317
|
+
const filePath = require("node:path").join(dir, filename);
|
|
1318
|
+
if (!require("node:fs").existsSync(filePath)) {
|
|
1319
|
+
const response = await fetch(data.url, {
|
|
1320
|
+
headers: {
|
|
1321
|
+
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
|
|
1322
|
+
"Referer": ""
|
|
1323
|
+
}
|
|
1324
|
+
});
|
|
1325
|
+
if (!response.ok) {
|
|
1326
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1288
1327
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1328
|
+
const buffer = await response.arrayBuffer();
|
|
1329
|
+
require("node:fs").writeFileSync(filePath, Buffer.from(buffer));
|
|
1330
|
+
this.logInfo("视频下载成功:", { size: buffer.byteLength, path: filePath });
|
|
1292
1331
|
}
|
|
1293
|
-
const
|
|
1294
|
-
const base64 = Buffer.from(buffer).toString("base64");
|
|
1295
|
-
const contentType = response.headers.get("content-type") || "video/mp4";
|
|
1296
|
-
this.logInfo("视频下载成功:", { size: buffer.byteLength, contentType });
|
|
1332
|
+
const normalizedPath = filePath.replace(/\\/g, "/");
|
|
1297
1333
|
return {
|
|
1298
1334
|
success: true,
|
|
1299
|
-
|
|
1300
|
-
contentType,
|
|
1301
|
-
dataUrl: `data:${contentType};base64,${base64}`,
|
|
1302
|
-
size: buffer.byteLength
|
|
1335
|
+
viteUrl: `/vite/@fs/${normalizedPath}`
|
|
1303
1336
|
};
|
|
1304
1337
|
} catch (error) {
|
|
1305
1338
|
this.logger.error("视频临时加载失败:", error);
|
|
@@ -1384,7 +1417,6 @@ var import_koishi2 = require("koishi");
|
|
|
1384
1417
|
var Config = import_koishi2.Schema.intersect([
|
|
1385
1418
|
import_koishi2.Schema.object({
|
|
1386
1419
|
maxMessagesPerChannel: import_koishi2.Schema.number().default(500).description("每个群组最大保存消息数量").min(50).max(1500).step(1),
|
|
1387
|
-
keepMessagesOnClear: import_koishi2.Schema.number().default(50).description("手动清理历史记录时保留的消息数量").min(0).max(1e3).step(1),
|
|
1388
1420
|
maxPersistImages: import_koishi2.Schema.number().default(100).description("持久化存储的图片缓存数量").min(10).max(500).step(1),
|
|
1389
1421
|
blockedPlatforms: import_koishi2.Schema.array(import_koishi2.Schema.object({
|
|
1390
1422
|
platformName: import_koishi2.Schema.string().description("平台名称或关键词"),
|
package/lib/message-handler.d.ts
CHANGED
|
@@ -15,7 +15,7 @@ export declare class MessageHandler {
|
|
|
15
15
|
getCorrectChannelId(selfId: string): string | undefined;
|
|
16
16
|
updateBotInfoToFile(session: Session): void;
|
|
17
17
|
updateChannelInfoToFile(session: Session): string;
|
|
18
|
-
downloadAndCacheMedia(url: string, type: 'image' | 'media' | 'avatar'): Promise<
|
|
18
|
+
downloadAndCacheMedia(url: string, type: 'image' | 'media' | 'avatar'): Promise<string>;
|
|
19
19
|
private processMediaElementsAsync;
|
|
20
20
|
private processUserMessage;
|
|
21
21
|
private processBotMessage;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "koishi-plugin-chat-patch",
|
|
3
3
|
"description": "[<ruby>chat-patch<rp>(</rp><rt>点我预览效果</rt><rp>)</rp></ruby>](https://i0.hdslb.com/bfs/openplatform/71074dfc9e5256fc3333d8bd8478bec1af874046.png) 视奸小插件((bushi( (低性能警告)。手机端适配。灵感来自 chat 插件。",
|
|
4
|
-
"version": "2.4.
|
|
4
|
+
"version": "2.4.4",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
package/src/api-handlers.ts
CHANGED
|
@@ -35,10 +35,10 @@ export class ApiHandlers {
|
|
|
35
35
|
|
|
36
36
|
this.ctx.console.addListener('get-chat-data' as any, async () => {
|
|
37
37
|
try {
|
|
38
|
+
// 只读取元数据,不加载消息到内存
|
|
39
|
+
const data = this.fileManager.readMetadataOnly()
|
|
38
40
|
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
this.logInfo('获取基础聊天数据')
|
|
41
|
+
this.logInfo('获取基础聊天数据(仅元数据)')
|
|
42
42
|
|
|
43
43
|
return {
|
|
44
44
|
success: true,
|
|
@@ -47,7 +47,7 @@ export class ApiHandlers {
|
|
|
47
47
|
channels: data.channels || {},
|
|
48
48
|
pinnedBots: data.pinnedBots || [],
|
|
49
49
|
pinnedChannels: data.pinnedChannels || [],
|
|
50
|
-
|
|
50
|
+
// 不返回消息数据,由前端按需加载
|
|
51
51
|
messages: {}
|
|
52
52
|
}
|
|
53
53
|
}
|
|
@@ -64,9 +64,8 @@ export class ApiHandlers {
|
|
|
64
64
|
offset?: number
|
|
65
65
|
}) => {
|
|
66
66
|
try {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
let messages = data.messages[channelKey] || []
|
|
67
|
+
// 直接从文件读取该频道的消息,不加载所有消息到内存
|
|
68
|
+
let messages = this.fileManager.readChannelMessages(requestData.selfId, requestData.channelId)
|
|
70
69
|
|
|
71
70
|
const sortedMessages = messages.sort((a, b) => b.timestamp - a.timestamp)
|
|
72
71
|
|
|
@@ -77,11 +76,11 @@ export class ApiHandlers {
|
|
|
77
76
|
|
|
78
77
|
messages = messages.sort((a, b) => a.timestamp - b.timestamp)
|
|
79
78
|
} else {
|
|
80
|
-
|
|
79
|
+
// 如果没有指定limit,返回所有消息(按时间正序)
|
|
81
80
|
messages = sortedMessages.sort((a, b) => a.timestamp - b.timestamp)
|
|
82
81
|
}
|
|
83
82
|
|
|
84
|
-
this.logInfo('获取历史消息:',
|
|
83
|
+
this.logInfo('获取历史消息:', `${requestData.selfId}:${requestData.channelId}`, '共', messages.length, '条消息')
|
|
85
84
|
|
|
86
85
|
return {
|
|
87
86
|
success: true,
|
|
@@ -120,45 +119,75 @@ export class ApiHandlers {
|
|
|
120
119
|
|
|
121
120
|
this.ctx.console.addListener('fetch-image' as any, async (data: { url: string }) => {
|
|
122
121
|
try {
|
|
122
|
+
// 如果已经是 Vite @fs 路径,直接返回
|
|
123
|
+
if (data.url.includes('/vite/@fs/')) {
|
|
124
|
+
return {
|
|
125
|
+
success: true,
|
|
126
|
+
viteUrl: data.url
|
|
127
|
+
}
|
|
128
|
+
}
|
|
123
129
|
|
|
130
|
+
// 如果是本地文件 URL,转换为 Vite @fs 路径
|
|
124
131
|
if (this.isFileUrl(data.url)) {
|
|
125
132
|
this.logInfo('处理本地文件请求:', data.url)
|
|
126
|
-
|
|
133
|
+
const filePath = require('node:url').fileURLToPath(data.url)
|
|
134
|
+
const normalizedPath = filePath.replace(/\\/g, '/')
|
|
135
|
+
return {
|
|
136
|
+
success: true,
|
|
137
|
+
viteUrl: `/vite/@fs/${normalizedPath}`
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// 网络图片:下载并缓存到本地,返回 Vite @fs 路径
|
|
142
|
+
const dir = require('node:path').join(this.ctx.baseDir, 'data', 'chat-patch', 'persist-media', 'images')
|
|
143
|
+
if (!require('node:fs').existsSync(dir)) {
|
|
144
|
+
require('node:fs').mkdirSync(dir, { recursive: true })
|
|
127
145
|
}
|
|
128
146
|
|
|
129
|
-
const
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
147
|
+
const crypto = require('node:crypto')
|
|
148
|
+
const hash = crypto.createHash('md5').update(data.url).digest('hex')
|
|
149
|
+
const ext = require('node:path').extname(new URL(data.url).pathname) || '.jpg'
|
|
150
|
+
const filename = `${hash}${ext}`
|
|
151
|
+
const filePath = require('node:path').join(dir, filename)
|
|
152
|
+
|
|
153
|
+
// 如果文件不存在,下载并保存
|
|
154
|
+
if (!require('node:fs').existsSync(filePath)) {
|
|
155
|
+
const response = await fetch(data.url, {
|
|
156
|
+
headers: {
|
|
157
|
+
'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',
|
|
158
|
+
'Referer': ''
|
|
159
|
+
}
|
|
160
|
+
})
|
|
161
|
+
|
|
162
|
+
if (!response.ok) {
|
|
163
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
133
164
|
}
|
|
134
|
-
})
|
|
135
165
|
|
|
136
|
-
|
|
137
|
-
|
|
166
|
+
const buffer = await response.arrayBuffer()
|
|
167
|
+
require('node:fs').writeFileSync(filePath, Buffer.from(buffer))
|
|
138
168
|
}
|
|
139
169
|
|
|
140
|
-
|
|
141
|
-
const
|
|
142
|
-
const contentType = response.headers.get('content-type') || 'image/jpeg'
|
|
170
|
+
// 返回 Vite @fs 路径
|
|
171
|
+
const normalizedPath = filePath.replace(/\\/g, '/')
|
|
143
172
|
return {
|
|
144
173
|
success: true,
|
|
145
|
-
|
|
146
|
-
contentType: contentType,
|
|
147
|
-
dataUrl: `data:${contentType};base64,${base64}`
|
|
174
|
+
viteUrl: `/vite/@fs/${normalizedPath}`
|
|
148
175
|
}
|
|
149
176
|
} catch (error: any) {
|
|
177
|
+
this.logger.error('获取图片失败:', error)
|
|
150
178
|
return { success: false, error: error?.message || String(error) }
|
|
151
179
|
}
|
|
152
180
|
})
|
|
153
181
|
|
|
182
|
+
// 清理历史记录 API 已废弃,现在直接通过右键删除频道数据
|
|
154
183
|
this.ctx.console.addListener('clear-channel-history' as any, async (data: {
|
|
155
184
|
selfId: string
|
|
156
185
|
channelId: string
|
|
157
|
-
keepCount?: number
|
|
158
186
|
}) => {
|
|
159
187
|
try {
|
|
160
|
-
this.logInfo('
|
|
188
|
+
this.logInfo('收到清理历史记录请求(已废弃,建议使用删除频道数据):', data)
|
|
161
189
|
|
|
190
|
+
// 直接删除该频道的所有消息
|
|
162
191
|
const chatData = this.fileManager.readChatDataFromFile()
|
|
163
192
|
const channelKey = `${data.selfId}:${data.channelId}`
|
|
164
193
|
|
|
@@ -166,33 +195,19 @@ export class ApiHandlers {
|
|
|
166
195
|
return { success: true, message: '频道没有历史消息' }
|
|
167
196
|
}
|
|
168
197
|
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
const keepCount = data.keepCount || this.config.keepMessagesOnClear
|
|
173
|
-
|
|
174
|
-
if (keepCount > 0 && originalCount <= keepCount) {
|
|
175
|
-
return { success: true, message: `消息数量(${originalCount})未超过保留数量(${keepCount}),无需清理` }
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp)
|
|
179
|
-
const keptMessages = keepCount > 0 ? sortedMessages.slice(-keepCount) : []
|
|
180
|
-
const clearedCount = originalCount - keptMessages.length
|
|
181
|
-
|
|
182
|
-
chatData.messages[channelKey] = keptMessages
|
|
198
|
+
const originalCount = chatData.messages[channelKey].length
|
|
199
|
+
delete chatData.messages[channelKey]
|
|
183
200
|
this.fileManager.writeChatDataToFile(chatData)
|
|
184
201
|
|
|
185
|
-
this.logInfo(`频道 ${channelKey}
|
|
186
|
-
|
|
187
|
-
保留消息数: keptMessages.length,
|
|
188
|
-
清理消息数: clearedCount
|
|
202
|
+
this.logInfo(`频道 ${channelKey} 历史记录已清空:`, {
|
|
203
|
+
清理消息数: originalCount
|
|
189
204
|
})
|
|
190
205
|
|
|
191
206
|
return {
|
|
192
207
|
success: true,
|
|
193
|
-
message: `成功清理 ${
|
|
194
|
-
clearedCount:
|
|
195
|
-
keptCount:
|
|
208
|
+
message: `成功清理 ${originalCount} 条历史消息`,
|
|
209
|
+
clearedCount: originalCount,
|
|
210
|
+
keptCount: 0
|
|
196
211
|
}
|
|
197
212
|
} catch (error: any) {
|
|
198
213
|
this.logger.error('清理频道历史记录失败:', error)
|
|
@@ -478,7 +493,6 @@ export class ApiHandlers {
|
|
|
478
493
|
success: true,
|
|
479
494
|
config: {
|
|
480
495
|
maxMessagesPerChannel: this.config.maxMessagesPerChannel,
|
|
481
|
-
keepMessagesOnClear: this.config.keepMessagesOnClear,
|
|
482
496
|
maxPersistImages: this.config.maxPersistImages,
|
|
483
497
|
loggerinfo: this.config.loggerinfo,
|
|
484
498
|
blockedPlatforms: this.config.blockedPlatforms || [],
|
|
@@ -579,35 +593,59 @@ export class ApiHandlers {
|
|
|
579
593
|
try {
|
|
580
594
|
this.logInfo('收到视频临时加载请求:', data.url)
|
|
581
595
|
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
return
|
|
596
|
+
// 如果已经是 Vite @fs 路径,直接返回
|
|
597
|
+
if (data.url.includes('/vite/@fs/')) {
|
|
598
|
+
return {
|
|
599
|
+
success: true,
|
|
600
|
+
viteUrl: data.url
|
|
601
|
+
}
|
|
585
602
|
}
|
|
586
603
|
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
604
|
+
// 如果是本地文件 URL,转换为 Vite @fs 路径
|
|
605
|
+
if (this.isFileUrl(data.url)) {
|
|
606
|
+
const filePath = require('node:url').fileURLToPath(data.url)
|
|
607
|
+
const normalizedPath = filePath.replace(/\\/g, '/')
|
|
608
|
+
return {
|
|
609
|
+
success: true,
|
|
610
|
+
viteUrl: `/vite/@fs/${normalizedPath}`
|
|
591
611
|
}
|
|
592
|
-
}
|
|
612
|
+
}
|
|
593
613
|
|
|
594
|
-
|
|
595
|
-
|
|
614
|
+
// 网络视频:下载并缓存到本地,返回 Vite @fs 路径
|
|
615
|
+
const dir = require('node:path').join(this.ctx.baseDir, 'data', 'chat-patch', 'persist-media', 'media')
|
|
616
|
+
if (!require('node:fs').existsSync(dir)) {
|
|
617
|
+
require('node:fs').mkdirSync(dir, { recursive: true })
|
|
596
618
|
}
|
|
597
619
|
|
|
598
|
-
const
|
|
599
|
-
const
|
|
620
|
+
const crypto = require('node:crypto')
|
|
621
|
+
const hash = crypto.createHash('md5').update(data.url).digest('hex')
|
|
622
|
+
const ext = require('node:path').extname(new URL(data.url).pathname) || '.mp4'
|
|
623
|
+
const filename = `${hash}${ext}`
|
|
624
|
+
const filePath = require('node:path').join(dir, filename)
|
|
600
625
|
|
|
601
|
-
|
|
626
|
+
// 如果文件不存在,下载并保存
|
|
627
|
+
if (!require('node:fs').existsSync(filePath)) {
|
|
628
|
+
const response = await fetch(data.url, {
|
|
629
|
+
headers: {
|
|
630
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
631
|
+
'Referer': ''
|
|
632
|
+
}
|
|
633
|
+
})
|
|
602
634
|
|
|
603
|
-
|
|
635
|
+
if (!response.ok) {
|
|
636
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const buffer = await response.arrayBuffer()
|
|
640
|
+
require('node:fs').writeFileSync(filePath, Buffer.from(buffer))
|
|
641
|
+
this.logInfo('视频下载成功:', { size: buffer.byteLength, path: filePath })
|
|
642
|
+
}
|
|
604
643
|
|
|
644
|
+
// 返回 Vite @fs 路径
|
|
645
|
+
const normalizedPath = filePath.replace(/\\/g, '/')
|
|
605
646
|
return {
|
|
606
647
|
success: true,
|
|
607
|
-
|
|
608
|
-
contentType: contentType,
|
|
609
|
-
dataUrl: `data:${contentType};base64,${base64}`,
|
|
610
|
-
size: buffer.byteLength
|
|
648
|
+
viteUrl: `/vite/@fs/${normalizedPath}`
|
|
611
649
|
}
|
|
612
650
|
} catch (error: any) {
|
|
613
651
|
this.logger.error('视频临时加载失败:', error)
|
package/src/config.ts
CHANGED
|
@@ -4,7 +4,6 @@ export interface Config {
|
|
|
4
4
|
loggerinfo: boolean
|
|
5
5
|
clearIndexedDBOnStart: boolean
|
|
6
6
|
maxMessagesPerChannel: number
|
|
7
|
-
keepMessagesOnClear: number
|
|
8
7
|
maxPersistImages: number
|
|
9
8
|
blockedPlatforms: Array<{
|
|
10
9
|
platformName: string
|
|
@@ -15,7 +14,6 @@ export interface Config {
|
|
|
15
14
|
export const Config: Schema<Config> = Schema.intersect([
|
|
16
15
|
Schema.object({
|
|
17
16
|
maxMessagesPerChannel: Schema.number().default(500).description('每个群组最大保存消息数量').min(50).max(1500).step(1),
|
|
18
|
-
keepMessagesOnClear: Schema.number().default(50).description('手动清理历史记录时保留的消息数量').min(0).max(1000).step(1),
|
|
19
17
|
maxPersistImages: Schema.number().default(100).description('持久化存储的图片缓存数量').min(10).max(500).step(1),
|
|
20
18
|
blockedPlatforms: Schema.array(Schema.object({
|
|
21
19
|
platformName: Schema.string().description('平台名称或关键词'),
|
package/src/file-manager.ts
CHANGED
|
@@ -66,8 +66,8 @@ export class FileManager {
|
|
|
66
66
|
return path.join(botDir, `${channelId}.json`)
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
//
|
|
70
|
-
|
|
69
|
+
// 读取单个频道的消息(公共方法)
|
|
70
|
+
readChannelMessages(selfId: string, channelId: string): MessageInfo[] {
|
|
71
71
|
const filePath = this.getChannelFilePath(selfId, channelId)
|
|
72
72
|
if (!fs.existsSync(filePath)) {
|
|
73
73
|
return []
|
|
@@ -126,6 +126,11 @@ export class FileManager {
|
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
// 只读取元数据,不加载消息(公共方法)
|
|
130
|
+
readMetadataOnly(): Omit<ChatData, 'messages'> {
|
|
131
|
+
return this.readMetadata()
|
|
132
|
+
}
|
|
133
|
+
|
|
129
134
|
// 写入元数据
|
|
130
135
|
private writeMetadata(metadata: Omit<ChatData, 'messages'>) {
|
|
131
136
|
try {
|
package/src/message-handler.ts
CHANGED
|
@@ -168,7 +168,10 @@ export class MessageHandler {
|
|
|
168
168
|
|
|
169
169
|
public async downloadAndCacheMedia(url: string, type: 'image' | 'media' | 'avatar') {
|
|
170
170
|
try {
|
|
171
|
-
if (!url || url.startsWith('data:')
|
|
171
|
+
if (!url || url.startsWith('data:')) return url
|
|
172
|
+
|
|
173
|
+
// 如果已经是 Vite @fs 路径,直接返回
|
|
174
|
+
if (url.includes('/vite/@fs/')) return url
|
|
172
175
|
|
|
173
176
|
let folder = 'media'
|
|
174
177
|
if (type === 'image') folder = 'images'
|
|
@@ -185,15 +188,17 @@ export class MessageHandler {
|
|
|
185
188
|
const filename = `${hash}${ext}`
|
|
186
189
|
const filePath = require('node:path').join(dir, filename)
|
|
187
190
|
|
|
188
|
-
if (require('node:fs').existsSync(filePath)) {
|
|
189
|
-
|
|
191
|
+
if (!require('node:fs').existsSync(filePath)) {
|
|
192
|
+
const buffer = await this.ctx.http.get(url, { responseType: 'arraybuffer' })
|
|
193
|
+
require('node:fs').writeFileSync(filePath, Buffer.from(buffer))
|
|
190
194
|
}
|
|
191
195
|
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
return
|
|
196
|
+
// 返回 Vite @fs 路径格式,让浏览器通过 Vite 开发服务器加载本地文件
|
|
197
|
+
// Windows 路径需要转换为正斜杠
|
|
198
|
+
const normalizedPath = filePath.replace(/\\/g, '/')
|
|
199
|
+
return `/vite/@fs/${normalizedPath}`
|
|
196
200
|
} catch (e) {
|
|
201
|
+
this.logger.warn('下载并缓存媒体失败:', e)
|
|
197
202
|
return url
|
|
198
203
|
}
|
|
199
204
|
}
|