koishi-plugin-chat-patch 2.4.4 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,9 @@
1
+ import { Logger } from 'koishi';
2
+ import { Config } from './config';
3
+ export interface PluginLogger {
4
+ logInfo(...args: unknown[]): void;
5
+ info(...args: unknown[]): void;
6
+ warn(...args: unknown[]): void;
7
+ error(...args: unknown[]): void;
8
+ }
9
+ export declare function createPluginLogger(logger: Logger, config: Pick<Config, 'loggerinfo'>): PluginLogger;
@@ -1,6 +1,7 @@
1
1
  import { Context, Session } from 'koishi';
2
2
  import { FileManager } from './file-manager';
3
3
  import { Config } from './config';
4
+ import { PluginLogger } from './logger';
4
5
  export declare class MessageHandler {
5
6
  private ctx;
6
7
  private config;
@@ -8,7 +9,11 @@ export declare class MessageHandler {
8
9
  private logger;
9
10
  private utils;
10
11
  private correctChannelIds;
11
- constructor(ctx: Context, config: Config, fileManager: FileManager);
12
+ private scheduledTasks;
13
+ private channelRefreshInFlight;
14
+ private lastChannelRefreshAt;
15
+ private readonly CHANNEL_REFRESH_TTL_MS;
16
+ constructor(ctx: Context, config: Config, fileManager: FileManager, logger: PluginLogger);
12
17
  recordUserMessage(session: Session, timestamp: number): void;
13
18
  recordBotMessage(session: Session, timestamp: number): void;
14
19
  setCorrectChannelId(selfId: string, channelId: string): void;
@@ -19,5 +24,12 @@ export declare class MessageHandler {
19
24
  private processMediaElementsAsync;
20
25
  private processUserMessage;
21
26
  private processBotMessage;
22
- private logInfo;
27
+ dispose(): void;
28
+ private scheduleTask;
29
+ private shouldRefreshChannelInfo;
30
+ private refreshChannelInfo;
31
+ private resolveGuildName;
32
+ private buildChannelName;
33
+ private processMediaElements;
34
+ private fileExists;
23
35
  }
package/lib/utils.d.ts CHANGED
@@ -3,11 +3,16 @@ import { Context } from 'koishi';
3
3
  export declare class Utils {
4
4
  private config;
5
5
  private ctx?;
6
+ private persistImageWriteCount;
7
+ private pendingTasks;
6
8
  constructor(config: Config, ctx?: Context);
7
9
  isPlatformBlocked(platform: string): boolean;
8
10
  extractTextContent(elements: any[]): string;
9
11
  private isBase64;
10
- persistBase64Image(base64Data: string): string;
11
- private cleanupPersistImages;
12
- cleanBase64Content(obj: any, isBotMessage?: boolean): any;
12
+ persistBase64ImageAsync(base64Data: string): Promise<string>;
13
+ private cleanupPersistImagesAsync;
14
+ cleanBase64ContentAsync<T>(obj: T, isBotMessage?: boolean): Promise<T>;
15
+ dispose(): Promise<void>;
16
+ private trackTask;
17
+ private fileExists;
13
18
  }
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",
4
+ "version": "3.0.0",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -37,16 +37,16 @@
37
37
  "koishi": "^4.16.0"
38
38
  },
39
39
  "devDependencies": {
40
+ "@element-plus/icons-vue": "^2.3.2",
40
41
  "@koishijs/client": "^5.11.0",
41
42
  "@types/mime-types": "^3.0.1",
42
43
  "autoprefixer": "^10.4.23",
44
+ "element-plus": "^2.13.0",
43
45
  "koishi": "^4.16.0",
44
46
  "postcss": "^8.5.6",
45
47
  "tailwindcss": "^3.4.17"
46
48
  },
47
49
  "dependencies": {
48
- "@element-plus/icons-vue": "^2.3.2",
49
- "element-plus": "^2.13.0",
50
50
  "mime-types": "^3.0.2"
51
51
  }
52
52
  }
@@ -1,25 +1,25 @@
1
1
  import { FileManager } from './file-manager'
2
2
  import { MessageHandler } from './message-handler'
3
- import { Context, h, Logger, Universal } from 'koishi'
3
+ import { Context, h, Universal } from 'koishi'
4
4
  import { Config } from './config'
5
+ import { PluginLogger } from './logger'
5
6
  import { } from '@koishijs/plugin-console'
6
7
  import { URL, pathToFileURL, fileURLToPath } from 'node:url'
7
- import { readFileSync } from 'node:fs'
8
+ import { promises as fs } from 'node:fs'
9
+ import path from 'node:path'
10
+ import { createHash } from 'node:crypto'
8
11
  import * as mime from 'mime-types'
9
12
 
10
13
  export class ApiHandlers {
11
- private logger: Logger
12
-
13
14
  private currentTempVideo: string | null = null
14
15
 
15
16
  constructor(
16
17
  private ctx: Context,
17
18
  private config: Config,
18
19
  private fileManager: FileManager,
19
- private messageHandler: MessageHandler
20
- ) {
21
- this.logger = ctx.logger('chat-patch')
22
- }
20
+ private messageHandler: MessageHandler,
21
+ private logger: PluginLogger
22
+ ) { }
23
23
 
24
24
  registerApiHandlers() {
25
25
  this.ctx.console.addListener('clear-all-indexeddb-data' as any, async () => {
@@ -36,7 +36,7 @@ export class ApiHandlers {
36
36
  this.ctx.console.addListener('get-chat-data' as any, async () => {
37
37
  try {
38
38
  // 只读取元数据,不加载消息到内存
39
- const data = this.fileManager.readMetadataOnly()
39
+ const data = await this.fileManager.readMetadataOnly()
40
40
 
41
41
  this.logInfo('获取基础聊天数据(仅元数据)')
42
42
 
@@ -64,28 +64,20 @@ export class ApiHandlers {
64
64
  offset?: number
65
65
  }) => {
66
66
  try {
67
- // 直接从文件读取该频道的消息,不加载所有消息到内存
68
- let messages = this.fileManager.readChannelMessages(requestData.selfId, requestData.channelId)
69
-
70
- const sortedMessages = messages.sort((a, b) => b.timestamp - a.timestamp)
71
-
72
- if (requestData.limit !== undefined) {
73
- const limit = requestData.limit
74
- const offset = requestData.offset || 0
75
- messages = sortedMessages.slice(offset, offset + limit)
76
-
77
- messages = messages.sort((a, b) => a.timestamp - b.timestamp)
78
- } else {
79
- // 如果没有指定limit,返回所有消息(按时间正序)
80
- messages = sortedMessages.sort((a, b) => a.timestamp - b.timestamp)
81
- }
67
+ const limit = Math.max(1, requestData.limit ?? this.config.messageChunkSize)
68
+ const result = await this.fileManager.readChannelMessagesPage(
69
+ requestData.selfId,
70
+ requestData.channelId,
71
+ limit,
72
+ requestData.offset || 0
73
+ )
82
74
 
83
- this.logInfo('获取历史消息:', `${requestData.selfId}:${requestData.channelId}`, '共', messages.length, '条消息')
75
+ this.logInfo('获取历史消息:', `${requestData.selfId}:${requestData.channelId}`, '共', result.messages.length, '条消息')
84
76
 
85
77
  return {
86
78
  success: true,
87
- messages: messages,
88
- total: sortedMessages.length
79
+ messages: result.messages,
80
+ total: result.total
89
81
  }
90
82
  } catch (error: any) {
91
83
  this.logger.error('获取历史消息失败:', error)
@@ -95,12 +87,7 @@ export class ApiHandlers {
95
87
 
96
88
  this.ctx.console.addListener('get-all-channel-message-counts' as any, async () => {
97
89
  try {
98
- const data = this.fileManager.readChatDataFromFile()
99
- const counts: Record<string, number> = {}
100
-
101
- for (const [channelKey, messages] of Object.entries(data.messages)) {
102
- counts[channelKey] = messages.length
103
- }
90
+ const counts = await this.fileManager.getAllChannelMessageCounts()
104
91
 
105
92
  this.logInfo('获取所有频道消息数量:', {
106
93
  频道数: Object.keys(counts).length,
@@ -139,19 +126,16 @@ export class ApiHandlers {
139
126
  }
140
127
 
141
128
  // 网络图片:下载并缓存到本地,返回 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 })
145
- }
129
+ const dir = path.join(this.ctx.baseDir, 'data', 'chat-patch', 'persist-media', 'images')
130
+ await fs.mkdir(dir, { recursive: true })
146
131
 
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'
132
+ const hash = createHash('md5').update(data.url).digest('hex')
133
+ const ext = path.extname(new URL(data.url).pathname) || '.jpg'
150
134
  const filename = `${hash}${ext}`
151
- const filePath = require('node:path').join(dir, filename)
135
+ const filePath = path.join(dir, filename)
152
136
 
153
137
  // 如果文件不存在,下载并保存
154
- if (!require('node:fs').existsSync(filePath)) {
138
+ if (!(await this.fileExists(filePath))) {
155
139
  const response = await fetch(data.url, {
156
140
  headers: {
157
141
  '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',
@@ -164,7 +148,7 @@ export class ApiHandlers {
164
148
  }
165
149
 
166
150
  const buffer = await response.arrayBuffer()
167
- require('node:fs').writeFileSync(filePath, Buffer.from(buffer))
151
+ await fs.writeFile(filePath, Buffer.from(buffer))
168
152
  }
169
153
 
170
154
  // 返回 Vite @fs 路径
@@ -174,8 +158,7 @@ export class ApiHandlers {
174
158
  viteUrl: `/vite/@fs/${normalizedPath}`
175
159
  }
176
160
  } catch (error: any) {
177
- this.logger.error('获取图片失败:', error)
178
- return { success: false, error: error?.message || String(error) }
161
+ return { success: false, error: this.getClientErrorMessage(error) }
179
162
  }
180
163
  })
181
164
 
@@ -187,26 +170,21 @@ export class ApiHandlers {
187
170
  try {
188
171
  this.logInfo('收到清理历史记录请求(已废弃,建议使用删除频道数据):', data)
189
172
 
190
- // 直接删除该频道的所有消息
191
- const chatData = this.fileManager.readChatDataFromFile()
192
173
  const channelKey = `${data.selfId}:${data.channelId}`
193
174
 
194
- if (!chatData.messages[channelKey]) {
175
+ const { deletedMessages } = await this.fileManager.deleteChannelData(data.selfId, data.channelId)
176
+ if (!deletedMessages) {
195
177
  return { success: true, message: '频道没有历史消息' }
196
178
  }
197
179
 
198
- const originalCount = chatData.messages[channelKey].length
199
- delete chatData.messages[channelKey]
200
- this.fileManager.writeChatDataToFile(chatData)
201
-
202
180
  this.logInfo(`频道 ${channelKey} 历史记录已清空:`, {
203
- 清理消息数: originalCount
181
+ 清理消息数: deletedMessages
204
182
  })
205
183
 
206
184
  return {
207
185
  success: true,
208
- message: `成功清理 ${originalCount} 条历史消息`,
209
- clearedCount: originalCount,
186
+ message: `成功清理 ${deletedMessages} 条历史消息`,
187
+ clearedCount: deletedMessages,
210
188
  keptCount: 0
211
189
  }
212
190
  } catch (error: any) {
@@ -241,15 +219,14 @@ export class ApiHandlers {
241
219
  let messageContent = data.content
242
220
 
243
221
  if (data.images && data.images.length > 0) {
244
- const tempDir = this.ctx.baseDir + '/data/chat-patch/temp'
222
+ const tempDir = path.join(this.ctx.baseDir, 'data', 'chat-patch', 'temp')
223
+ const tempFiles = await this.safeReadDir(tempDir)
245
224
 
246
225
  for (const image of data.images) {
247
- const files = require('fs').readdirSync(tempDir).filter((file: string) =>
248
- file.includes(`temp_${image.tempId}`)
249
- )
226
+ const files = tempFiles.filter((file) => file.includes(`temp_${image.tempId}`))
250
227
 
251
228
  if (files.length > 0) {
252
- const imagePath = `${tempDir}/${files[0]}`
229
+ const imagePath = path.join(tempDir, files[0])
253
230
 
254
231
  const fileUrl = this.createFileUrl(imagePath)
255
232
  messageContent += h.image(fileUrl).toString()
@@ -268,16 +245,9 @@ export class ApiHandlers {
268
245
  const messageId = Array.isArray(result) ? result[0] : result;
269
246
 
270
247
  if (messageId) {
271
- const chatData = this.fileManager.readChatDataFromFile()
272
248
  const channelKey = `${data.selfId}:${data.channelId}`
273
- const messages = chatData.messages[channelKey] || []
274
-
275
- const msg = [...messages].reverse().find(m => m.type === 'bot' && m.sending)
249
+ const msg = await this.fileManager.markLatestBotMessageAsSent(data.selfId, data.channelId, messageId)
276
250
  if (msg) {
277
- msg.realId = messageId;
278
- msg.sending = false;
279
- this.fileManager.writeChatDataToFile(chatData)
280
-
281
251
  this.ctx.console.broadcast('bot-message-updated', {
282
252
  channelKey,
283
253
  tempId: msg.id,
@@ -318,23 +288,7 @@ export class ApiHandlers {
318
288
  try {
319
289
  this.logInfo('收到删除机器人数据请求:', data)
320
290
 
321
- const chatData = this.fileManager.readChatDataFromFile()
322
- let deletedChannels = 0
323
- let deletedMessages = 0
324
-
325
- if (chatData.channels[data.selfId]) {
326
- deletedChannels = Object.keys(chatData.channels[data.selfId]).length
327
- delete chatData.channels[data.selfId]
328
- }
329
-
330
- const channelsToDelete = Object.keys(chatData.messages).filter(key => key.startsWith(`${data.selfId}:`))
331
- for (const channelKey of channelsToDelete) {
332
- deletedMessages += chatData.messages[channelKey].length
333
- delete chatData.messages[channelKey]
334
- }
335
-
336
- delete chatData.bots[data.selfId]
337
- this.fileManager.writeChatDataToFile(chatData)
291
+ const { deletedChannels, deletedMessages } = await this.fileManager.deleteBotData(data.selfId)
338
292
 
339
293
  this.logInfo(`机器人 ${data.selfId} 数据删除完成:`, {
340
294
  删除频道数: deletedChannels,
@@ -360,20 +314,8 @@ export class ApiHandlers {
360
314
  try {
361
315
  this.logInfo('收到删除频道数据请求:', data)
362
316
 
363
- const chatData = this.fileManager.readChatDataFromFile()
364
317
  const channelKey = `${data.selfId}:${data.channelId}`
365
- let deletedMessages = 0
366
-
367
- if (chatData.messages[channelKey]) {
368
- deletedMessages = chatData.messages[channelKey].length
369
- delete chatData.messages[channelKey]
370
- }
371
-
372
- if (chatData.channels[data.selfId] && chatData.channels[data.selfId][data.channelId]) {
373
- delete chatData.channels[data.selfId][data.channelId]
374
- }
375
-
376
- this.fileManager.writeChatDataToFile(chatData)
318
+ const { deletedMessages } = await this.fileManager.deleteChannelData(data.selfId, data.channelId)
377
319
 
378
320
  this.logInfo(`频道 ${channelKey} 数据删除完成:`, {
379
321
  删除消息数: deletedMessages
@@ -395,9 +337,7 @@ export class ApiHandlers {
395
337
  }) => {
396
338
  try {
397
339
  this.logInfo('收到设置置顶机器人请求:', data.pinnedBots)
398
- const chatData = this.fileManager.readChatDataFromFile()
399
- chatData.pinnedBots = data.pinnedBots
400
- this.fileManager.writeChatDataToFile(chatData)
340
+ await this.fileManager.setPinnedBots(data.pinnedBots)
401
341
  return { success: true }
402
342
  } catch (error: any) {
403
343
  this.logger.error('设置置顶机器人失败:', error)
@@ -410,9 +350,7 @@ export class ApiHandlers {
410
350
  }) => {
411
351
  try {
412
352
  this.logInfo('收到设置置顶频道请求:', data.pinnedChannels)
413
- const chatData = this.fileManager.readChatDataFromFile()
414
- chatData.pinnedChannels = data.pinnedChannels
415
- this.fileManager.writeChatDataToFile(chatData)
353
+ await this.fileManager.setPinnedChannels(data.pinnedChannels)
416
354
  return { success: true }
417
355
  } catch (error: any) {
418
356
  this.logger.error('设置置顶频道失败:', error)
@@ -436,14 +374,12 @@ export class ApiHandlers {
436
374
  const extension = data.filename.split('.').pop()?.toLowerCase() || (data.isGif ? 'gif' : 'jpg')
437
375
  const tempFilename = `temp_${tempId}.${extension}`
438
376
 
439
- const tempDir = this.ctx.baseDir + '/data/chat-patch/temp'
440
- if (!require('fs').existsSync(tempDir)) {
441
- require('fs').mkdirSync(tempDir, { recursive: true })
442
- }
377
+ const tempDir = path.join(this.ctx.baseDir, 'data', 'chat-patch', 'temp')
378
+ await fs.mkdir(tempDir, { recursive: true })
443
379
 
444
- const tempPath = `${tempDir}/${tempFilename}`
380
+ const tempPath = path.join(tempDir, tempFilename)
445
381
 
446
- require('fs').writeFileSync(tempPath, buffer)
382
+ await fs.writeFile(tempPath, buffer)
447
383
 
448
384
  this.logInfo('图片上传成功:', { tempPath, size: buffer.length, isGif: data.isGif })
449
385
 
@@ -465,15 +401,13 @@ export class ApiHandlers {
465
401
  tempId: string
466
402
  }) => {
467
403
  try {
468
- const tempDir = this.ctx.baseDir + '/data/chat-patch/temp'
469
- const files = require('fs').readdirSync(tempDir).filter((file: string) =>
470
- file.includes(`temp_${data.tempId}`)
471
- )
404
+ const tempDir = path.join(this.ctx.baseDir, 'data', 'chat-patch', 'temp')
405
+ const files = (await this.safeReadDir(tempDir)).filter((file) => file.includes(`temp_${data.tempId}`))
472
406
 
473
407
  for (const file of files) {
474
- const filePath = `${tempDir}/${file}`
475
- if (require('fs').existsSync(filePath)) {
476
- require('fs').unlinkSync(filePath)
408
+ const filePath = path.join(tempDir, file)
409
+ if (await this.fileExists(filePath)) {
410
+ await fs.unlink(filePath)
477
411
  this.logInfo('删除临时图片:', filePath)
478
412
  }
479
413
  }
@@ -493,6 +427,8 @@ export class ApiHandlers {
493
427
  success: true,
494
428
  config: {
495
429
  maxMessagesPerChannel: this.config.maxMessagesPerChannel,
430
+ messageChunkSize: this.config.messageChunkSize,
431
+ channelCacheLimit: this.config.channelCacheLimit,
496
432
  maxPersistImages: this.config.maxPersistImages,
497
433
  loggerinfo: this.config.loggerinfo,
498
434
  blockedPlatforms: this.config.blockedPlatforms || [],
@@ -522,50 +458,8 @@ export class ApiHandlers {
522
458
  user.avatar = await this.messageHandler.downloadAndCacheMedia(user.avatar, 'avatar')
523
459
  }
524
460
 
525
- const chatData = this.fileManager.readChatDataFromFile()
526
- let changed = false
527
-
528
- const botChannels = chatData.channels[data.selfId] || {}
529
-
530
- const possibleChannelIds = [
531
- data.userId,
532
- `private:${data.userId}`,
533
- `direct:${data.userId}`
534
- ]
535
-
536
- for (const channelId of possibleChannelIds) {
537
- const channel = botChannels[channelId]
538
- if (channel && channel.isDirect) {
539
- const newName = `私聊(${user.name})`
540
- if (channel.name !== newName) {
541
- channel.name = newName
542
- changed = true
543
- this.logInfo('更新私聊频道名称:', { channelId, oldName: channel.name, newName })
544
- }
545
- }
546
- }
547
-
548
- const channelKeyPrefix = `${data.selfId}:`
549
- for (const [key, messages] of Object.entries(chatData.messages)) {
550
- if (key.startsWith(channelKeyPrefix)) {
551
- messages.forEach(msg => {
552
- if (msg.userId === data.userId) {
553
- if (user.name && msg.username !== user.name) {
554
- msg.username = user.name
555
- changed = true
556
- }
557
- if (user.avatar && msg.avatar !== user.avatar) {
558
- msg.avatar = user.avatar
559
- changed = true
560
- }
561
- }
562
- });
563
- }
564
- }
565
-
461
+ const changed = await this.fileManager.updateUserProfileInBotData(data.selfId, data.userId, user.name, user.avatar)
566
462
  if (changed) {
567
- this.fileManager.writeChatDataToFile(chatData)
568
-
569
463
  this.ctx.console.broadcast('chat-data-updated', {})
570
464
  }
571
465
  }
@@ -576,19 +470,6 @@ export class ApiHandlers {
576
470
  }
577
471
  })
578
472
 
579
- this.ctx.console.addListener('debug-get-raw-data' as any, async () => {
580
- try {
581
- const data = this.fileManager.readChatDataFromFile()
582
- return {
583
- success: true,
584
- data: data
585
- }
586
- } catch (error: any) {
587
- this.logger.error('获取原始数据失败:', error)
588
- return { success: false, error: error?.message || String(error) }
589
- }
590
- })
591
-
592
473
  this.ctx.console.addListener('fetch-video-temp' as any, async (data: { url: string }) => {
593
474
  try {
594
475
  this.logInfo('收到视频临时加载请求:', data.url)
@@ -612,19 +493,16 @@ export class ApiHandlers {
612
493
  }
613
494
 
614
495
  // 网络视频:下载并缓存到本地,返回 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 })
618
- }
496
+ const dir = path.join(this.ctx.baseDir, 'data', 'chat-patch', 'persist-media', 'media')
497
+ await fs.mkdir(dir, { recursive: true })
619
498
 
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'
499
+ const hash = createHash('md5').update(data.url).digest('hex')
500
+ const ext = path.extname(new URL(data.url).pathname) || '.mp4'
623
501
  const filename = `${hash}${ext}`
624
- const filePath = require('node:path').join(dir, filename)
502
+ const filePath = path.join(dir, filename)
625
503
 
626
504
  // 如果文件不存在,下载并保存
627
- if (!require('node:fs').existsSync(filePath)) {
505
+ if (!(await this.fileExists(filePath))) {
628
506
  const response = await fetch(data.url, {
629
507
  headers: {
630
508
  'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
@@ -637,7 +515,7 @@ export class ApiHandlers {
637
515
  }
638
516
 
639
517
  const buffer = await response.arrayBuffer()
640
- require('node:fs').writeFileSync(filePath, Buffer.from(buffer))
518
+ await fs.writeFile(filePath, Buffer.from(buffer))
641
519
  this.logInfo('视频下载成功:', { size: buffer.byteLength, path: filePath })
642
520
  }
643
521
 
@@ -648,8 +526,7 @@ export class ApiHandlers {
648
526
  viteUrl: `/vite/@fs/${normalizedPath}`
649
527
  }
650
528
  } catch (error: any) {
651
- this.logger.error('视频临时加载失败:', error)
652
- return { success: false, error: error?.message || String(error) }
529
+ return { success: false, error: this.getClientErrorMessage(error) }
653
530
  }
654
531
  })
655
532
  }
@@ -678,7 +555,7 @@ export class ApiHandlers {
678
555
 
679
556
  const filePath = fileURLToPath(fileUrl)
680
557
 
681
- const buffer = readFileSync(filePath)
558
+ const buffer = await fs.readFile(filePath)
682
559
  const base64 = buffer.toString('base64')
683
560
 
684
561
  const contentType = mime.lookup(filePath) || 'application/octet-stream'
@@ -692,7 +569,6 @@ export class ApiHandlers {
692
569
  dataUrl: `data:${contentType};base64,${base64}`
693
570
  }
694
571
  } catch (error: any) {
695
- this.logger.error('读取本地文件失败:', { fileUrl, error: error.message })
696
572
  return {
697
573
  success: false,
698
574
  error: `读取本地文件失败: ${error.message}`
@@ -708,35 +584,63 @@ export class ApiHandlers {
708
584
  }
709
585
 
710
586
  private async cleanupMediaCache() {
711
- const baseDir = this.ctx.baseDir + '/data/chat-patch/persist-media'
712
- if (!require('node:fs').existsSync(baseDir)) return
713
-
714
- const cleanupDir = (dirName: string, limit: number) => {
715
- const dirPath = require('node:path').join(baseDir, dirName)
716
- if (!require('node:fs').existsSync(dirPath)) return
717
-
718
- const files = require('node:fs').readdirSync(dirPath)
719
- .map(file => {
720
- const filePath = require('node:path').join(dirPath, file)
721
- const stats = require('node:fs').statSync(filePath)
722
- return { name: file, path: filePath, mtime: stats.mtimeMs }
723
- })
724
- .sort((a, b) => b.mtime - a.mtime)
725
-
726
- if (files.length > limit) {
727
- files.slice(limit).forEach(f => {
728
- try { require('node:fs').unlinkSync(f.path) } catch { }
729
- })
730
- }
731
- }
587
+ const baseDir = path.join(this.ctx.baseDir, 'data', 'chat-patch', 'persist-media')
732
588
 
733
- cleanupDir('images', 100)
734
- cleanupDir('media', 20)
589
+ await this.cleanupMediaCacheDir(path.join(baseDir, 'images'), 100)
590
+ await this.cleanupMediaCacheDir(path.join(baseDir, 'media'), 20)
735
591
  }
736
592
 
737
593
  private logInfo(...args: any[]) {
738
594
  if (this.config.loggerinfo) {
739
- (this.logger.info as (...args: any[]) => void)(...args)
595
+ Reflect.apply(this.logger.info, this.logger, args)
596
+ }
597
+ }
598
+
599
+ private async fileExists(filePath: string) {
600
+ try {
601
+ await fs.access(filePath)
602
+ return true
603
+ } catch {
604
+ return false
740
605
  }
741
606
  }
607
+
608
+ private async safeReadDir(dirPath: string): Promise<string[]> {
609
+ try {
610
+ return await fs.readdir(dirPath)
611
+ } catch {
612
+ return []
613
+ }
614
+ }
615
+
616
+ private async cleanupMediaCacheDir(dirPath: string, limit: number) {
617
+ const files = await this.safeReadDir(dirPath)
618
+ if (!files.length) {
619
+ return
620
+ }
621
+
622
+ const fileStats = await Promise.all(files.map(async (fileName) => {
623
+ const filePath = path.join(dirPath, fileName)
624
+ const stats = await fs.stat(filePath)
625
+ return { path: filePath, mtime: stats.mtimeMs }
626
+ }))
627
+
628
+ fileStats.sort((left, right) => right.mtime - left.mtime)
629
+
630
+ for (const file of fileStats.slice(limit)) {
631
+ try {
632
+ await fs.unlink(file.path)
633
+ } catch {
634
+ continue
635
+ }
636
+ }
637
+ }
638
+
639
+ private getClientErrorMessage(error: unknown) {
640
+ if (typeof error === 'object' && error !== null && 'message' in error && typeof error.message === 'string') {
641
+ return error.message
642
+ }
643
+
644
+ return String(error)
645
+ }
742
646
  }
package/src/config.ts CHANGED
@@ -4,6 +4,8 @@ export interface Config {
4
4
  loggerinfo: boolean
5
5
  clearIndexedDBOnStart: boolean
6
6
  maxMessagesPerChannel: number
7
+ messageChunkSize: number
8
+ channelCacheLimit: number
7
9
  maxPersistImages: number
8
10
  blockedPlatforms: Array<{
9
11
  platformName: string
@@ -14,6 +16,8 @@ export interface Config {
14
16
  export const Config: Schema<Config> = Schema.intersect([
15
17
  Schema.object({
16
18
  maxMessagesPerChannel: Schema.number().default(500).description('每个群组最大保存消息数量').min(50).max(1500).step(1),
19
+ messageChunkSize: Schema.number().default(100).description('单个消息分块文件最大消息数量').min(20).max(500).step(1),
20
+ channelCacheLimit: Schema.number().default(50).description('内存中最多缓存的频道消息数量').min(1).max(200).step(1),
17
21
  maxPersistImages: Schema.number().default(100).description('持久化存储的图片缓存数量').min(10).max(500).step(1),
18
22
  blockedPlatforms: Schema.array(Schema.object({
19
23
  platformName: Schema.string().description('平台名称或关键词'),