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/lib/config.d.ts CHANGED
@@ -3,7 +3,6 @@ export interface Config {
3
3
  loggerinfo: boolean;
4
4
  clearIndexedDBOnStart: boolean;
5
5
  maxMessagesPerChannel: number;
6
- keepMessagesOnClear: number;
7
6
  maxPersistImages: number;
8
7
  blockedPlatforms: Array<{
9
8
  platformName: string;
@@ -16,9 +16,10 @@ export declare class FileManager {
16
16
  private cleanupOldDataFiles;
17
17
  private ensureDir;
18
18
  private getChannelFilePath;
19
- private readChannelMessages;
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:") || url.startsWith("file:")) return url;
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
- return require("node:url").pathToFileURL(filePath).href;
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 buffer = await this.ctx.http.get(url, { responseType: "arraybuffer" });
324
- require("node:fs").writeFileSync(filePath, Buffer.from(buffer));
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.readChatDataFromFile();
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
- const data = this.fileManager.readChatDataFromFile();
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("获取历史消息:", channelKey, "共", messages.length, "条消息");
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
- return await this.handleLocalFileRequest(data.url);
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 response = await fetch(data.url, {
934
- headers: {
935
- "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",
936
- "Referer": ""
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
- if (!response.ok) {
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 buffer = await response.arrayBuffer();
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
- base64,
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("收到清理历史记录请求:", data);
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 messages = chatData.messages[channelKey];
964
- const originalCount = messages.length;
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
- 原始消息数: originalCount,
976
- 保留消息数: keptMessages.length,
977
- 清理消息数: clearedCount
992
+ this.logInfo(`频道 ${channelKey} 历史记录已清空:`, {
993
+ 清理消息数: originalCount
978
994
  });
979
995
  return {
980
996
  success: true,
981
- message: `成功清理 ${clearedCount} 条历史消息,保留最新 ${keptMessages.length} 条`,
982
- clearedCount,
983
- keptCount: keptMessages.length
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 result = await this.handleLocalFileRequest(data.url);
1282
- return result;
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 response = await fetch(data.url, {
1285
- headers: {
1286
- "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36",
1287
- "Referer": ""
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
- if (!response.ok) {
1291
- throw new Error(`HTTP ${response.status}: ${response.statusText}`);
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 buffer = await response.arrayBuffer();
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
- base64,
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("平台名称或关键词"),
@@ -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<any>;
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.0",
4
+ "version": "2.4.4",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -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
- const data = this.fileManager.readChatDataFromFile()
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
- const data = this.fileManager.readChatDataFromFile()
68
- const channelKey = `${requestData.selfId}:${requestData.channelId}`
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('获取历史消息:', channelKey, '共', messages.length, '条消息')
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
- return await this.handleLocalFileRequest(data.url)
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 response = await fetch(data.url, {
130
- headers: {
131
- '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',
132
- 'Referer': ''
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
- if (!response.ok) {
137
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
166
+ const buffer = await response.arrayBuffer()
167
+ require('node:fs').writeFileSync(filePath, Buffer.from(buffer))
138
168
  }
139
169
 
140
- const buffer = await response.arrayBuffer()
141
- const base64 = Buffer.from(buffer).toString('base64')
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
- base64: base64,
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('收到清理历史记录请求:', data)
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 messages = chatData.messages[channelKey]
170
- const originalCount = messages.length
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
- 原始消息数: originalCount,
187
- 保留消息数: keptMessages.length,
188
- 清理消息数: clearedCount
202
+ this.logInfo(`频道 ${channelKey} 历史记录已清空:`, {
203
+ 清理消息数: originalCount
189
204
  })
190
205
 
191
206
  return {
192
207
  success: true,
193
- message: `成功清理 ${clearedCount} 条历史消息,保留最新 ${keptMessages.length} 条`,
194
- clearedCount: clearedCount,
195
- keptCount: keptMessages.length
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
- if (this.isFileUrl(data.url)) {
583
- const result = await this.handleLocalFileRequest(data.url)
584
- return result
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
- const response = await fetch(data.url, {
588
- headers: {
589
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
590
- 'Referer': ''
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
- if (!response.ok) {
595
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
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 buffer = await response.arrayBuffer()
599
- const base64 = Buffer.from(buffer).toString('base64')
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
- const contentType = response.headers.get('content-type') || 'video/mp4'
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
- this.logInfo('视频下载成功:', { size: buffer.byteLength, contentType })
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
- base64: base64,
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('平台名称或关键词'),
@@ -66,8 +66,8 @@ export class FileManager {
66
66
  return path.join(botDir, `${channelId}.json`)
67
67
  }
68
68
 
69
- // 读取单个频道的消息
70
- private readChannelMessages(selfId: string, channelId: string): MessageInfo[] {
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 {
@@ -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:') || url.startsWith('file:')) return url
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
- return require('node:url').pathToFileURL(filePath).href
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
- const buffer = await this.ctx.http.get(url, { responseType: 'arraybuffer' })
193
- require('node:fs').writeFileSync(filePath, Buffer.from(buffer))
194
-
195
- return require('node:url').pathToFileURL(filePath).href
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
  }