koishi-plugin-chat-patch 1.2.0 → 1.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.
@@ -1,657 +1,657 @@
1
- import { FileManager } from './file-manager'
2
- import { MessageHandler } from './message-handler'
3
- import { Context, h, Logger } from 'koishi'
4
- import { Config } from './config'
5
- import { } from '@koishijs/plugin-console'
6
- import { URL, pathToFileURL } from 'node:url'
7
-
8
- export class ApiHandlers {
9
- private logger: Logger
10
-
11
- constructor(
12
- private ctx: Context,
13
- private config: Config,
14
- private fileManager: FileManager,
15
- private messageHandler: MessageHandler
16
- ) {
17
- this.logger = ctx.logger('chat-patch')
18
- }
19
-
20
- registerApiHandlers() {
21
- this.ctx.console.addListener('clear-all-indexeddb-data' as any, async () => {
22
- try {
23
- this.logInfo('收到清空 IndexedDB 数据请求')
24
- // 这个 API 主要用于前端调用,后端不直接操作 IndexedDB
25
- return { success: true, message: '可以清空 IndexedDB' }
26
- } catch (error: any) {
27
- this.logger.error('清空 IndexedDB 数据失败:', error)
28
- return { success: false, error: error?.message || String(error) }
29
- }
30
- })
31
-
32
- // 获取所有聊天数据的 API
33
- this.ctx.console.addListener('get-chat-data' as any, async () => {
34
- try {
35
- const data = this.fileManager.readChatDataFromFile()
36
- const cleanedData = this.fileManager.cleanExcessMessages(data)
37
-
38
- this.logInfo('获取聊天数据:', {
39
- 机器人数量: Object.keys(cleanedData.bots).length,
40
- 频道数量: Object.keys(cleanedData.channels).reduce((total, botId) =>
41
- total + Object.keys(cleanedData.channels[botId] || {}).length, 0),
42
- 消息频道数: Object.keys(cleanedData.messages).length,
43
- 总消息数: Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0)
44
- })
45
-
46
- return {
47
- success: true,
48
- data: {
49
- ...cleanedData,
50
- pinnedBots: cleanedData.pinnedBots,
51
- pinnedChannels: cleanedData.pinnedChannels
52
- }
53
- }
54
- } catch (error: any) {
55
- this.logger.error('获取聊天数据失败:', error)
56
- return { success: false, error: error?.message || String(error) }
57
- }
58
- })
59
-
60
- // 获取历史消息的 API
61
- this.ctx.console.addListener('get-history-messages' as any, async (requestData: {
62
- selfId: string
63
- channelId: string
64
- limit?: number
65
- offset?: number
66
- }) => {
67
- try {
68
- const data = this.fileManager.readChatDataFromFile()
69
- const channelKey = `${requestData.selfId}:${requestData.channelId}`
70
- let messages = data.messages[channelKey] || []
71
-
72
- // 按时间戳降序排列(最新的消息在前面)
73
- const sortedMessages = messages.sort((a, b) => b.timestamp - a.timestamp)
74
-
75
- // 如果提供了分页参数,则进行分页处理
76
- if (requestData.limit !== undefined) {
77
- const limit = requestData.limit
78
- const offset = requestData.offset || 0
79
- messages = sortedMessages.slice(offset, offset + limit)
80
- // 重新按时间戳升序排列,以保持消息显示顺序
81
- messages = messages.sort((a, b) => a.timestamp - b.timestamp)
82
- } else {
83
- // 默认返回所有消息,按时间戳升序排列
84
- messages = sortedMessages.sort((a, b) => a.timestamp - b.timestamp)
85
- }
86
-
87
- this.logInfo('获取历史消息:', channelKey, '共', messages.length, '条消息')
88
-
89
- return {
90
- success: true,
91
- messages: messages,
92
- total: sortedMessages.length
93
- }
94
- } catch (error: any) {
95
- this.logger.error('获取历史消息失败:', error)
96
- return { success: false, error: error?.message || String(error), messages: [], total: 0 }
97
- }
98
- })
99
-
100
- // 获取所有频道消息数量的 API
101
- this.ctx.console.addListener('get-all-channel-message-counts' as any, async () => {
102
- try {
103
- const data = this.fileManager.readChatDataFromFile()
104
- const counts: Record<string, number> = {}
105
-
106
- for (const [channelKey, messages] of Object.entries(data.messages)) {
107
- counts[channelKey] = messages.length
108
- }
109
-
110
- this.logInfo('获取所有频道消息数量:', {
111
- 频道数: Object.keys(counts).length,
112
- 总消息数: Object.values(counts).reduce((total, count) => total + count, 0)
113
- })
114
-
115
- return {
116
- success: true,
117
- counts: counts
118
- }
119
- } catch (error: any) {
120
- this.logger.error('获取频道消息数量失败:', error)
121
- return { success: false, error: error?.message || String(error), counts: {} }
122
- }
123
- })
124
-
125
- // 图片获取 API
126
- this.ctx.console.addListener('fetch-image' as any, async (data: { url: string }) => {
127
- try {
128
- // 检查是否是本地文件路径
129
- if (this.isFileUrl(data.url)) {
130
- this.logInfo('处理本地文件请求:', data.url)
131
- return await this.handleLocalFileRequest(data.url)
132
- }
133
-
134
- // 处理网络图片
135
- const response = await fetch(data.url, {
136
- headers: {
137
- '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',
138
- 'Referer': ''
139
- }
140
- })
141
-
142
- if (!response.ok) {
143
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
144
- }
145
-
146
- const buffer = await response.arrayBuffer()
147
- const base64 = Buffer.from(buffer).toString('base64')
148
- const contentType = response.headers.get('content-type') || 'image/jpeg'
149
- return {
150
- success: true,
151
- base64: base64,
152
- contentType: contentType,
153
- dataUrl: `data:${contentType};base64,${base64}`
154
- }
155
- } catch (error: any) {
156
- return { success: false, error: error?.message || String(error) }
157
- }
158
- })
159
-
160
- // 清理频道历史记录的 API
161
- this.ctx.console.addListener('clear-channel-history' as any, async (data: {
162
- selfId: string
163
- channelId: string
164
- keepCount?: number
165
- }) => {
166
- try {
167
- this.logInfo('收到清理历史记录请求:', data)
168
-
169
- const chatData = this.fileManager.readChatDataFromFile()
170
- const channelKey = `${data.selfId}:${data.channelId}`
171
-
172
- if (!chatData.messages[channelKey]) {
173
- return { success: true, message: '频道没有历史消息' }
174
- }
175
-
176
- const messages = chatData.messages[channelKey]
177
- const originalCount = messages.length
178
-
179
- const keepCount = data.keepCount || this.config.keepMessagesOnClear
180
-
181
- if (keepCount > 0 && originalCount <= keepCount) {
182
- return { success: true, message: `消息数量(${originalCount})未超过保留数量(${keepCount}),无需清理` }
183
- }
184
-
185
- const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp)
186
- const keptMessages = keepCount > 0 ? sortedMessages.slice(-keepCount) : []
187
- const clearedCount = originalCount - keptMessages.length
188
-
189
- chatData.messages[channelKey] = keptMessages
190
- this.fileManager.writeChatDataToFile(chatData)
191
-
192
- this.logInfo(`频道 ${channelKey} 历史记录清理完成:`, {
193
- 原始消息数: originalCount,
194
- 保留消息数: keptMessages.length,
195
- 清理消息数: clearedCount
196
- })
197
-
198
- return {
199
- success: true,
200
- message: `成功清理 ${clearedCount} 条历史消息,保留最新 ${keptMessages.length} 条`,
201
- clearedCount: clearedCount,
202
- keptCount: keptMessages.length
203
- }
204
- } catch (error: any) {
205
- this.logger.error('清理频道历史记录失败:', error)
206
- return { success: false, error: error?.message || String(error) }
207
- }
208
- })
209
-
210
- // 发送消息的 API
211
- this.ctx.console.addListener('send-message' as any, async (data: {
212
- selfId: string
213
- channelId: string
214
- content: string
215
- images?: Array<{
216
- tempId: string
217
- filename: string
218
- }>
219
- }) => {
220
- try {
221
- this.logInfo('收到发送消息请求:', data)
222
-
223
- const bot = this.ctx.bots.find((bot: any) => bot.selfId === data.selfId)
224
- if (!bot) {
225
- this.logger.error('未找到机器人:', data.selfId)
226
- return { success: false, error: '未找到指定的机器人' }
227
- }
228
-
229
- let messageContent = data.content
230
-
231
- // 如果有图片,添加图片元素
232
- if (data.images && data.images.length > 0) {
233
- const tempDir = this.ctx.baseDir + '/data/chat-patch/temp'
234
-
235
- for (const image of data.images) {
236
- const files = require('fs').readdirSync(tempDir).filter((file: string) =>
237
- file.includes(`temp_${image.tempId}`)
238
- )
239
-
240
- if (files.length > 0) {
241
- const imagePath = `${tempDir}/${files[0]}`
242
- // 使用 pathToFileURL 创建正确的文件 URL
243
- const fileUrl = this.createFileUrl(imagePath)
244
- messageContent += h.image(fileUrl).toString()
245
- this.logInfo('添加图片到消息:', { imagePath, fileUrl })
246
- }
247
- }
248
- }
249
-
250
- const parsedContent = h.parse(messageContent)
251
-
252
- // 在发送消息前,通知 MessageHandler 正确的 channelId
253
- this.messageHandler.setCorrectChannelId(data.selfId, data.channelId)
254
-
255
- const result = await bot.sendMessage(data.channelId, parsedContent)
256
- this.logInfo('消息发送成功:', result)
257
-
258
- // 广播机器人消息发送成功事件,包含真实的 messageId
259
- const messageId = Array.isArray(result) ? result[0] : result;
260
- if (messageId) {
261
- const bot = this.ctx.bots.find((b: any) => b.selfId === data.selfId);
262
- const botMessageEvent = {
263
- type: 'bot-message-sent',
264
- selfId: data.selfId,
265
- platform: bot?.platform || 'unknown',
266
- channelId: data.channelId,
267
- messageId: messageId,
268
- content: messageContent,
269
- userId: data.selfId,
270
- botUsername: bot?.user?.name || `Bot-${data.selfId}`,
271
- botAvatar: bot?.user?.avatar,
272
- timestamp: Date.now(),
273
- guildName: '', // 这个信息在前端会补充
274
- channelType: 0, // 这个信息在前端会补充
275
- elements: [], // 这个信息在前端会补充
276
- isDirect: false // 这个信息在前端会补充
277
- };
278
-
279
- this.ctx.console.broadcast('bot-message-sent-event', botMessageEvent);
280
- }
281
-
282
- return {
283
- success: true,
284
- messageId: messageId,
285
- tempImageIds: data.images?.map(img => img.tempId) || []
286
- }
287
- } catch (error: any) {
288
- this.logger.error('发送消息失败:', error)
289
- return { success: false, error: error?.message || String(error) }
290
- }
291
- })
292
-
293
- // 清理临时图片的 API(由前端在消息发送成功后调用)
294
- this.ctx.console.addListener('cleanup-temp-images' as any, async (data: {
295
- tempImageIds: string[]
296
- }) => {
297
- try {
298
- this.logInfo('收到清理临时图片请求:', data.tempImageIds)
299
-
300
- // 不立即删除,只是记录日志,让定时任务处理清理
301
- this.logInfo('临时图片将由定时任务清理,保持文件可用性')
302
-
303
- return { success: true, cleanedCount: 0 }
304
- } catch (error: any) {
305
- this.logger.error('清理临时图片失败:', error)
306
- return { success: false, error: error?.message || String(error) }
307
- }
308
- })
309
-
310
- // 删除机器人所有数据的 API
311
- this.ctx.console.addListener('delete-bot-data' as any, async (data: {
312
- selfId: string
313
- }) => {
314
- try {
315
- this.logInfo('收到删除机器人数据请求:', data)
316
-
317
- const chatData = this.fileManager.readChatDataFromFile()
318
- let deletedChannels = 0
319
- let deletedMessages = 0
320
-
321
- if (chatData.channels[data.selfId]) {
322
- deletedChannels = Object.keys(chatData.channels[data.selfId]).length
323
- delete chatData.channels[data.selfId]
324
- }
325
-
326
- const channelsToDelete = Object.keys(chatData.messages).filter(key => key.startsWith(`${data.selfId}:`))
327
- for (const channelKey of channelsToDelete) {
328
- deletedMessages += chatData.messages[channelKey].length
329
- delete chatData.messages[channelKey]
330
- }
331
-
332
- delete chatData.bots[data.selfId]
333
- this.fileManager.writeChatDataToFile(chatData)
334
-
335
- this.logInfo(`机器人 ${data.selfId} 数据删除完成:`, {
336
- 删除频道数: deletedChannels,
337
- 删除消息数: deletedMessages
338
- })
339
-
340
- return {
341
- success: true,
342
- message: `成功删除机器人数据:${deletedChannels} 个频道,${deletedMessages} 条消息`,
343
- deletedChannels,
344
- deletedMessages
345
- }
346
- } catch (error: any) {
347
- this.logger.error('删除机器人数据失败:', error)
348
- return { success: false, error: error?.message || String(error) }
349
- }
350
- })
351
-
352
- // 删除频道所有数据的 API
353
- this.ctx.console.addListener('delete-channel-data' as any, async (data: {
354
- selfId: string
355
- channelId: string
356
- }) => {
357
- try {
358
- this.logInfo('收到删除频道数据请求:', data)
359
-
360
- const chatData = this.fileManager.readChatDataFromFile()
361
- const channelKey = `${data.selfId}:${data.channelId}`
362
- let deletedMessages = 0
363
-
364
- if (chatData.messages[channelKey]) {
365
- deletedMessages = chatData.messages[channelKey].length
366
- delete chatData.messages[channelKey]
367
- }
368
-
369
- if (chatData.channels[data.selfId] && chatData.channels[data.selfId][data.channelId]) {
370
- delete chatData.channels[data.selfId][data.channelId]
371
- }
372
-
373
- this.fileManager.writeChatDataToFile(chatData)
374
-
375
- this.logInfo(`频道 ${channelKey} 数据删除完成:`, {
376
- 删除消息数: deletedMessages
377
- })
378
-
379
- return {
380
- success: true,
381
- message: `成功删除频道数据:${deletedMessages} 条消息`,
382
- deletedMessages
383
- }
384
- } catch (error: any) {
385
- this.logger.error('删除频道数据失败:', error)
386
- return { success: false, error: error?.message || String(error) }
387
- }
388
- })
389
-
390
- // 设置置顶机器人列表的 API
391
- this.ctx.console.addListener('set-pinned-bots' as any, async (data: {
392
- pinnedBots: string[]
393
- }) => {
394
- try {
395
- this.logInfo('收到设置置顶机器人请求:', data.pinnedBots)
396
- const chatData = this.fileManager.readChatDataFromFile()
397
- chatData.pinnedBots = data.pinnedBots
398
- this.fileManager.writeChatDataToFile(chatData)
399
- return { success: true }
400
- } catch (error: any) {
401
- this.logger.error('设置置顶机器人失败:', error)
402
- return { success: false, error: error?.message || String(error) }
403
- }
404
- })
405
-
406
- // 设置置顶频道列表的 API
407
- this.ctx.console.addListener('set-pinned-channels' as any, async (data: {
408
- pinnedChannels: string[]
409
- }) => {
410
- try {
411
- this.logInfo('收到设置置顶频道请求:', data.pinnedChannels)
412
- const chatData = this.fileManager.readChatDataFromFile()
413
- chatData.pinnedChannels = data.pinnedChannels
414
- this.fileManager.writeChatDataToFile(chatData)
415
- return { success: true }
416
- } catch (error: any) {
417
- this.logger.error('设置置顶频道失败:', error)
418
- return { success: false, error: error?.message || String(error) }
419
- }
420
- })
421
-
422
- // 上传图片的 API
423
- this.ctx.console.addListener('upload-image' as any, async (data: {
424
- file: string // base64 encoded image
425
- filename: string
426
- mimeType: string
427
- isGif?: boolean // 是否为GIF图片
428
- }) => {
429
- try {
430
- this.logInfo('收到图片上传请求:', { filename: data.filename, mimeType: data.mimeType, isGif: data.isGif })
431
-
432
- // 将base64转换为Buffer
433
- const base64Data = data.file.replace(/^data:image\/\w+;base64,/, '')
434
- const buffer = Buffer.from(base64Data, 'base64')
435
-
436
- // 生成临时文件路径,保持原始扩展名以保持GIF格式
437
- const tempId = Date.now() + '_' + Math.random().toString(36).substring(2, 11)
438
- const extension = data.filename.split('.').pop()?.toLowerCase() || (data.isGif ? 'gif' : 'jpg')
439
- const tempFilename = `temp_${tempId}.${extension}`
440
-
441
- // 保存到临时目录
442
- const tempDir = this.ctx.baseDir + '/data/chat-patch/temp'
443
- if (!require('fs').existsSync(tempDir)) {
444
- require('fs').mkdirSync(tempDir, { recursive: true })
445
- }
446
-
447
- const tempPath = `${tempDir}/${tempFilename}`
448
-
449
- // 对于GIF文件,直接保存原始数据以保持动画
450
- require('fs').writeFileSync(tempPath, buffer)
451
-
452
- this.logInfo('图片上传成功:', { tempPath, size: buffer.length, isGif: data.isGif })
453
-
454
- return {
455
- success: true,
456
- tempId: tempId,
457
- tempPath: tempPath,
458
- filename: data.filename,
459
- size: buffer.length,
460
- isGif: data.isGif
461
- }
462
- } catch (error: any) {
463
- this.logger.error('图片上传失败:', error)
464
- return { success: false, error: error?.message || String(error) }
465
- }
466
- })
467
-
468
- // 删除临时图片的 API
469
- this.ctx.console.addListener('delete-temp-image' as any, async (data: {
470
- tempId: string
471
- }) => {
472
- try {
473
- const tempDir = this.ctx.baseDir + '/data/chat-patch/temp'
474
- const files = require('fs').readdirSync(tempDir).filter((file: string) =>
475
- file.includes(`temp_${data.tempId}`)
476
- )
477
-
478
- for (const file of files) {
479
- const filePath = `${tempDir}/${file}`
480
- if (require('fs').existsSync(filePath)) {
481
- require('fs').unlinkSync(filePath)
482
- this.logInfo('删除临时图片:', filePath)
483
- }
484
- }
485
-
486
- return { success: true }
487
- } catch (error: any) {
488
- this.logger.error('删除临时图片失败:', error)
489
- return { success: false, error: error?.message || String(error) }
490
- }
491
- })
492
-
493
- // 定时清理过期临时文件(备用机制)
494
- this.setupTempFileCleanup()
495
-
496
- // 获取插件配置的 API
497
- this.ctx.console.addListener('get-plugin-config' as any, async () => {
498
- try {
499
- return {
500
- success: true,
501
- config: {
502
- maxMessagesPerChannel: this.config.maxMessagesPerChannel,
503
- keepMessagesOnClear: this.config.keepMessagesOnClear,
504
- keepTempImages: this.config.keepTempImages,
505
- loggerinfo: this.config.loggerinfo,
506
- blockedPlatforms: this.config.blockedPlatforms || [],
507
- chatContainerHeight: this.config.chatContainerHeight,
508
- clearIndexedDBOnStart: this.config.clearIndexedDBOnStart
509
- }
510
- }
511
- } catch (error: any) {
512
- this.logger.error('获取插件配置失败:', error)
513
- return { success: false, error: error?.message || String(error) }
514
- }
515
- })
516
-
517
- // 调试API:获取原始文件数据
518
- this.ctx.console.addListener('debug-get-raw-data' as any, async () => {
519
- try {
520
- const data = this.fileManager.readChatDataFromFile()
521
- return {
522
- success: true,
523
- data: data
524
- }
525
- } catch (error: any) {
526
- this.logger.error('获取原始数据失败:', error)
527
- return { success: false, error: error?.message || String(error) }
528
- }
529
- })
530
- }
531
-
532
- // 检查是否为文件 URL
533
- private isFileUrl(url: string): boolean {
534
- try {
535
- const parsedUrl = new URL(url)
536
- return parsedUrl.protocol === 'file:'
537
- } catch {
538
- return false
539
- }
540
- }
541
-
542
- // 创建文件 URL
543
- private createFileUrl(filePath: string): string {
544
- try {
545
- return pathToFileURL(filePath).href
546
- } catch (error) {
547
- this.logger.error('创建文件URL失败:', { filePath, error })
548
- // 回退到简单的字符串拼接
549
- return `file://${filePath}`
550
- }
551
- }
552
-
553
- // 处理本地文件请求
554
- private async handleLocalFileRequest(fileUrl: string) {
555
- try {
556
- const fileresponse = await this.ctx.http.file(fileUrl)
557
- const fileresponsebase64 = Buffer.from(fileresponse.data).toString('base64')
558
- let contentType = fileresponse.type
559
-
560
- this.logInfo('成功读取本地文件:', { fileUrl, contentType })
561
-
562
- return {
563
- success: true,
564
- base64: fileresponsebase64,
565
- contentType: contentType,
566
- dataUrl: `data:${contentType};base64,${fileresponsebase64}`
567
- }
568
- } catch (error: any) {
569
- this.logger.error('读取本地文件失败:', { fileUrl, error: error.message })
570
- return {
571
- success: false,
572
- error: `读取本地文件失败: ${error.message}`
573
- }
574
- }
575
- }
576
-
577
- // 设置定时清理临时文件
578
- private setupTempFileCleanup() {
579
- // 每5分钟执行一次基于数量的清理
580
- setInterval(() => {
581
- this.cleanupTempImagesByCount()
582
- }, 5 * 60 * 1000) // 5分钟
583
- }
584
-
585
- // 基于数量清理临时图片(保留最新的N张)
586
- private async cleanupTempImagesByCount() {
587
- try {
588
- const tempDir = this.ctx.baseDir + '/data/chat-patch/temp'
589
- if (!require('fs').existsSync(tempDir)) {
590
- return
591
- }
592
-
593
- const now = Date.now()
594
- const protectionTime = 30 * 1000 // 30秒保护期,刚上传的图片不会被删除
595
-
596
- const files = require('fs').readdirSync(tempDir)
597
- .filter((file: string) => file.startsWith('temp_'))
598
- .map((file: string) => {
599
- const filePath = `${tempDir}/${file}`
600
- try {
601
- const stats = require('fs').statSync(filePath)
602
- const fileAge = now - stats.mtime.getTime()
603
- return {
604
- name: file,
605
- path: filePath,
606
- mtime: stats.mtime.getTime(),
607
- age: fileAge,
608
- protected: fileAge < protectionTime // 是否在保护期内
609
- }
610
- } catch (error) {
611
- return null
612
- }
613
- })
614
- .filter((file: any) => file !== null)
615
- .sort((a: any, b: any) => b.mtime - a.mtime) // 按修改时间降序排列(最新的在前)
616
-
617
- const keepCount = this.config.keepTempImages
618
- let cleanedCount = 0
619
- let protectedCount = 0
620
-
621
- // 分离受保护的文件和可删除的文件
622
- const protectedFiles = files.filter((file: any) => file.protected)
623
- const deletableFiles = files.filter((file: any) => !file.protected)
624
-
625
- protectedCount = protectedFiles.length
626
-
627
- // 如果可删除的文件数量超过了保留数量,则删除多余的
628
- if (deletableFiles.length > keepCount) {
629
- const filesToDelete = deletableFiles.slice(keepCount) // 保留前N个,删除其余的
630
-
631
- for (const file of filesToDelete) {
632
- try {
633
- if (require('fs').existsSync(file.path)) {
634
- require('fs').unlinkSync(file.path)
635
- cleanedCount++
636
- this.logInfo('清理多余临时图片:', file.path)
637
- }
638
- } catch (fileError) {
639
- this.logger.warn('删除临时文件失败:', { file: file.name, error: fileError })
640
- }
641
- }
642
- }
643
-
644
- if (cleanedCount > 0 || protectedCount > 0) {
645
- this.logInfo(`基于数量清理完成,保留最新 ${keepCount} 张图片,清理了 ${cleanedCount} 张多余图片,保护了 ${protectedCount} 张新上传图片`)
646
- }
647
- } catch (error) {
648
- this.logger.warn('基于数量清理临时文件失败:', error)
649
- }
650
- }
651
-
652
- private logInfo(...args: any[]) {
653
- if (this.config.loggerinfo) {
654
- (this.logger.info as (...args: any[]) => void)(...args)
655
- }
656
- }
657
- }
1
+ import { FileManager } from './file-manager'
2
+ import { MessageHandler } from './message-handler'
3
+ import { Context, h, Logger } from 'koishi'
4
+ import { Config } from './config'
5
+ import { } from '@koishijs/plugin-console'
6
+ import { URL, pathToFileURL } from 'node:url'
7
+
8
+ export class ApiHandlers {
9
+ private logger: Logger
10
+
11
+ constructor(
12
+ private ctx: Context,
13
+ private config: Config,
14
+ private fileManager: FileManager,
15
+ private messageHandler: MessageHandler
16
+ ) {
17
+ this.logger = ctx.logger('chat-patch')
18
+ }
19
+
20
+ registerApiHandlers() {
21
+ this.ctx.console.addListener('clear-all-indexeddb-data' as any, async () => {
22
+ try {
23
+ this.logInfo('收到清空 IndexedDB 数据请求')
24
+ // 这个 API 主要用于前端调用,后端不直接操作 IndexedDB
25
+ return { success: true, message: '可以清空 IndexedDB' }
26
+ } catch (error: any) {
27
+ this.logger.error('清空 IndexedDB 数据失败:', error)
28
+ return { success: false, error: error?.message || String(error) }
29
+ }
30
+ })
31
+
32
+ // 获取所有聊天数据的 API
33
+ this.ctx.console.addListener('get-chat-data' as any, async () => {
34
+ try {
35
+ const data = this.fileManager.readChatDataFromFile()
36
+ const cleanedData = this.fileManager.cleanExcessMessages(data)
37
+
38
+ this.logInfo('获取聊天数据:', {
39
+ 机器人数量: Object.keys(cleanedData.bots).length,
40
+ 频道数量: Object.keys(cleanedData.channels).reduce((total, botId) =>
41
+ total + Object.keys(cleanedData.channels[botId] || {}).length, 0),
42
+ 消息频道数: Object.keys(cleanedData.messages).length,
43
+ 总消息数: Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0)
44
+ })
45
+
46
+ return {
47
+ success: true,
48
+ data: {
49
+ ...cleanedData,
50
+ pinnedBots: cleanedData.pinnedBots,
51
+ pinnedChannels: cleanedData.pinnedChannels
52
+ }
53
+ }
54
+ } catch (error: any) {
55
+ this.logger.error('获取聊天数据失败:', error)
56
+ return { success: false, error: error?.message || String(error) }
57
+ }
58
+ })
59
+
60
+ // 获取历史消息的 API
61
+ this.ctx.console.addListener('get-history-messages' as any, async (requestData: {
62
+ selfId: string
63
+ channelId: string
64
+ limit?: number
65
+ offset?: number
66
+ }) => {
67
+ try {
68
+ const data = this.fileManager.readChatDataFromFile()
69
+ const channelKey = `${requestData.selfId}:${requestData.channelId}`
70
+ let messages = data.messages[channelKey] || []
71
+
72
+ // 按时间戳降序排列(最新的消息在前面)
73
+ const sortedMessages = messages.sort((a, b) => b.timestamp - a.timestamp)
74
+
75
+ // 如果提供了分页参数,则进行分页处理
76
+ if (requestData.limit !== undefined) {
77
+ const limit = requestData.limit
78
+ const offset = requestData.offset || 0
79
+ messages = sortedMessages.slice(offset, offset + limit)
80
+ // 重新按时间戳升序排列,以保持消息显示顺序
81
+ messages = messages.sort((a, b) => a.timestamp - b.timestamp)
82
+ } else {
83
+ // 默认返回所有消息,按时间戳升序排列
84
+ messages = sortedMessages.sort((a, b) => a.timestamp - b.timestamp)
85
+ }
86
+
87
+ this.logInfo('获取历史消息:', channelKey, '共', messages.length, '条消息')
88
+
89
+ return {
90
+ success: true,
91
+ messages: messages,
92
+ total: sortedMessages.length
93
+ }
94
+ } catch (error: any) {
95
+ this.logger.error('获取历史消息失败:', error)
96
+ return { success: false, error: error?.message || String(error), messages: [], total: 0 }
97
+ }
98
+ })
99
+
100
+ // 获取所有频道消息数量的 API
101
+ this.ctx.console.addListener('get-all-channel-message-counts' as any, async () => {
102
+ try {
103
+ const data = this.fileManager.readChatDataFromFile()
104
+ const counts: Record<string, number> = {}
105
+
106
+ for (const [channelKey, messages] of Object.entries(data.messages)) {
107
+ counts[channelKey] = messages.length
108
+ }
109
+
110
+ this.logInfo('获取所有频道消息数量:', {
111
+ 频道数: Object.keys(counts).length,
112
+ 总消息数: Object.values(counts).reduce((total, count) => total + count, 0)
113
+ })
114
+
115
+ return {
116
+ success: true,
117
+ counts: counts
118
+ }
119
+ } catch (error: any) {
120
+ this.logger.error('获取频道消息数量失败:', error)
121
+ return { success: false, error: error?.message || String(error), counts: {} }
122
+ }
123
+ })
124
+
125
+ // 图片获取 API
126
+ this.ctx.console.addListener('fetch-image' as any, async (data: { url: string }) => {
127
+ try {
128
+ // 检查是否是本地文件路径
129
+ if (this.isFileUrl(data.url)) {
130
+ this.logInfo('处理本地文件请求:', data.url)
131
+ return await this.handleLocalFileRequest(data.url)
132
+ }
133
+
134
+ // 处理网络图片
135
+ const response = await fetch(data.url, {
136
+ headers: {
137
+ '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',
138
+ 'Referer': ''
139
+ }
140
+ })
141
+
142
+ if (!response.ok) {
143
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
144
+ }
145
+
146
+ const buffer = await response.arrayBuffer()
147
+ const base64 = Buffer.from(buffer).toString('base64')
148
+ const contentType = response.headers.get('content-type') || 'image/jpeg'
149
+ return {
150
+ success: true,
151
+ base64: base64,
152
+ contentType: contentType,
153
+ dataUrl: `data:${contentType};base64,${base64}`
154
+ }
155
+ } catch (error: any) {
156
+ return { success: false, error: error?.message || String(error) }
157
+ }
158
+ })
159
+
160
+ // 清理频道历史记录的 API
161
+ this.ctx.console.addListener('clear-channel-history' as any, async (data: {
162
+ selfId: string
163
+ channelId: string
164
+ keepCount?: number
165
+ }) => {
166
+ try {
167
+ this.logInfo('收到清理历史记录请求:', data)
168
+
169
+ const chatData = this.fileManager.readChatDataFromFile()
170
+ const channelKey = `${data.selfId}:${data.channelId}`
171
+
172
+ if (!chatData.messages[channelKey]) {
173
+ return { success: true, message: '频道没有历史消息' }
174
+ }
175
+
176
+ const messages = chatData.messages[channelKey]
177
+ const originalCount = messages.length
178
+
179
+ const keepCount = data.keepCount || this.config.keepMessagesOnClear
180
+
181
+ if (keepCount > 0 && originalCount <= keepCount) {
182
+ return { success: true, message: `消息数量(${originalCount})未超过保留数量(${keepCount}),无需清理` }
183
+ }
184
+
185
+ const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp)
186
+ const keptMessages = keepCount > 0 ? sortedMessages.slice(-keepCount) : []
187
+ const clearedCount = originalCount - keptMessages.length
188
+
189
+ chatData.messages[channelKey] = keptMessages
190
+ this.fileManager.writeChatDataToFile(chatData)
191
+
192
+ this.logInfo(`频道 ${channelKey} 历史记录清理完成:`, {
193
+ 原始消息数: originalCount,
194
+ 保留消息数: keptMessages.length,
195
+ 清理消息数: clearedCount
196
+ })
197
+
198
+ return {
199
+ success: true,
200
+ message: `成功清理 ${clearedCount} 条历史消息,保留最新 ${keptMessages.length} 条`,
201
+ clearedCount: clearedCount,
202
+ keptCount: keptMessages.length
203
+ }
204
+ } catch (error: any) {
205
+ this.logger.error('清理频道历史记录失败:', error)
206
+ return { success: false, error: error?.message || String(error) }
207
+ }
208
+ })
209
+
210
+ // 发送消息的 API
211
+ this.ctx.console.addListener('send-message' as any, async (data: {
212
+ selfId: string
213
+ channelId: string
214
+ content: string
215
+ images?: Array<{
216
+ tempId: string
217
+ filename: string
218
+ }>
219
+ }) => {
220
+ try {
221
+ this.logInfo('收到发送消息请求:', data)
222
+
223
+ const bot = this.ctx.bots.find((bot: any) => bot.selfId === data.selfId)
224
+ if (!bot) {
225
+ this.logger.error('未找到机器人:', data.selfId)
226
+ return { success: false, error: '未找到指定的机器人' }
227
+ }
228
+
229
+ let messageContent = data.content
230
+
231
+ // 如果有图片,添加图片元素
232
+ if (data.images && data.images.length > 0) {
233
+ const tempDir = this.ctx.baseDir + '/data/chat-patch/temp'
234
+
235
+ for (const image of data.images) {
236
+ const files = require('fs').readdirSync(tempDir).filter((file: string) =>
237
+ file.includes(`temp_${image.tempId}`)
238
+ )
239
+
240
+ if (files.length > 0) {
241
+ const imagePath = `${tempDir}/${files[0]}`
242
+ // 使用 pathToFileURL 创建正确的文件 URL
243
+ const fileUrl = this.createFileUrl(imagePath)
244
+ messageContent += h.image(fileUrl).toString()
245
+ this.logInfo('添加图片到消息:', { imagePath, fileUrl })
246
+ }
247
+ }
248
+ }
249
+
250
+ const parsedContent = h.parse(messageContent)
251
+
252
+ // 在发送消息前,通知 MessageHandler 正确的 channelId
253
+ this.messageHandler.setCorrectChannelId(data.selfId, data.channelId)
254
+
255
+ const result = await bot.sendMessage(data.channelId, parsedContent)
256
+ this.logInfo('消息发送成功:', result)
257
+
258
+ // 广播机器人消息发送成功事件,包含真实的 messageId
259
+ const messageId = Array.isArray(result) ? result[0] : result;
260
+ if (messageId) {
261
+ const bot = this.ctx.bots.find((b: any) => b.selfId === data.selfId);
262
+ const botMessageEvent = {
263
+ type: 'bot-message-sent',
264
+ selfId: data.selfId,
265
+ platform: bot?.platform || 'unknown',
266
+ channelId: data.channelId,
267
+ messageId: messageId,
268
+ content: messageContent,
269
+ userId: data.selfId,
270
+ botUsername: bot?.user?.name || `Bot-${data.selfId}`,
271
+ botAvatar: bot?.user?.avatar,
272
+ timestamp: Date.now(),
273
+ guildName: '', // 这个信息在前端会补充
274
+ channelType: 0, // 这个信息在前端会补充
275
+ elements: [], // 这个信息在前端会补充
276
+ isDirect: false // 这个信息在前端会补充
277
+ };
278
+
279
+ this.ctx.console.broadcast('bot-message-sent-event', botMessageEvent);
280
+ }
281
+
282
+ return {
283
+ success: true,
284
+ messageId: messageId,
285
+ tempImageIds: data.images?.map(img => img.tempId) || []
286
+ }
287
+ } catch (error: any) {
288
+ this.logger.error('发送消息失败:', error)
289
+ return { success: false, error: error?.message || String(error) }
290
+ }
291
+ })
292
+
293
+ // 清理临时图片的 API(由前端在消息发送成功后调用)
294
+ this.ctx.console.addListener('cleanup-temp-images' as any, async (data: {
295
+ tempImageIds: string[]
296
+ }) => {
297
+ try {
298
+ this.logInfo('收到清理临时图片请求:', data.tempImageIds)
299
+
300
+ // 不立即删除,只是记录日志,让定时任务处理清理
301
+ this.logInfo('临时图片将由定时任务清理,保持文件可用性')
302
+
303
+ return { success: true, cleanedCount: 0 }
304
+ } catch (error: any) {
305
+ this.logger.error('清理临时图片失败:', error)
306
+ return { success: false, error: error?.message || String(error) }
307
+ }
308
+ })
309
+
310
+ // 删除机器人所有数据的 API
311
+ this.ctx.console.addListener('delete-bot-data' as any, async (data: {
312
+ selfId: string
313
+ }) => {
314
+ try {
315
+ this.logInfo('收到删除机器人数据请求:', data)
316
+
317
+ const chatData = this.fileManager.readChatDataFromFile()
318
+ let deletedChannels = 0
319
+ let deletedMessages = 0
320
+
321
+ if (chatData.channels[data.selfId]) {
322
+ deletedChannels = Object.keys(chatData.channels[data.selfId]).length
323
+ delete chatData.channels[data.selfId]
324
+ }
325
+
326
+ const channelsToDelete = Object.keys(chatData.messages).filter(key => key.startsWith(`${data.selfId}:`))
327
+ for (const channelKey of channelsToDelete) {
328
+ deletedMessages += chatData.messages[channelKey].length
329
+ delete chatData.messages[channelKey]
330
+ }
331
+
332
+ delete chatData.bots[data.selfId]
333
+ this.fileManager.writeChatDataToFile(chatData)
334
+
335
+ this.logInfo(`机器人 ${data.selfId} 数据删除完成:`, {
336
+ 删除频道数: deletedChannels,
337
+ 删除消息数: deletedMessages
338
+ })
339
+
340
+ return {
341
+ success: true,
342
+ message: `成功删除机器人数据:${deletedChannels} 个频道,${deletedMessages} 条消息`,
343
+ deletedChannels,
344
+ deletedMessages
345
+ }
346
+ } catch (error: any) {
347
+ this.logger.error('删除机器人数据失败:', error)
348
+ return { success: false, error: error?.message || String(error) }
349
+ }
350
+ })
351
+
352
+ // 删除频道所有数据的 API
353
+ this.ctx.console.addListener('delete-channel-data' as any, async (data: {
354
+ selfId: string
355
+ channelId: string
356
+ }) => {
357
+ try {
358
+ this.logInfo('收到删除频道数据请求:', data)
359
+
360
+ const chatData = this.fileManager.readChatDataFromFile()
361
+ const channelKey = `${data.selfId}:${data.channelId}`
362
+ let deletedMessages = 0
363
+
364
+ if (chatData.messages[channelKey]) {
365
+ deletedMessages = chatData.messages[channelKey].length
366
+ delete chatData.messages[channelKey]
367
+ }
368
+
369
+ if (chatData.channels[data.selfId] && chatData.channels[data.selfId][data.channelId]) {
370
+ delete chatData.channels[data.selfId][data.channelId]
371
+ }
372
+
373
+ this.fileManager.writeChatDataToFile(chatData)
374
+
375
+ this.logInfo(`频道 ${channelKey} 数据删除完成:`, {
376
+ 删除消息数: deletedMessages
377
+ })
378
+
379
+ return {
380
+ success: true,
381
+ message: `成功删除频道数据:${deletedMessages} 条消息`,
382
+ deletedMessages
383
+ }
384
+ } catch (error: any) {
385
+ this.logger.error('删除频道数据失败:', error)
386
+ return { success: false, error: error?.message || String(error) }
387
+ }
388
+ })
389
+
390
+ // 设置置顶机器人列表的 API
391
+ this.ctx.console.addListener('set-pinned-bots' as any, async (data: {
392
+ pinnedBots: string[]
393
+ }) => {
394
+ try {
395
+ this.logInfo('收到设置置顶机器人请求:', data.pinnedBots)
396
+ const chatData = this.fileManager.readChatDataFromFile()
397
+ chatData.pinnedBots = data.pinnedBots
398
+ this.fileManager.writeChatDataToFile(chatData)
399
+ return { success: true }
400
+ } catch (error: any) {
401
+ this.logger.error('设置置顶机器人失败:', error)
402
+ return { success: false, error: error?.message || String(error) }
403
+ }
404
+ })
405
+
406
+ // 设置置顶频道列表的 API
407
+ this.ctx.console.addListener('set-pinned-channels' as any, async (data: {
408
+ pinnedChannels: string[]
409
+ }) => {
410
+ try {
411
+ this.logInfo('收到设置置顶频道请求:', data.pinnedChannels)
412
+ const chatData = this.fileManager.readChatDataFromFile()
413
+ chatData.pinnedChannels = data.pinnedChannels
414
+ this.fileManager.writeChatDataToFile(chatData)
415
+ return { success: true }
416
+ } catch (error: any) {
417
+ this.logger.error('设置置顶频道失败:', error)
418
+ return { success: false, error: error?.message || String(error) }
419
+ }
420
+ })
421
+
422
+ // 上传图片的 API
423
+ this.ctx.console.addListener('upload-image' as any, async (data: {
424
+ file: string // base64 encoded image
425
+ filename: string
426
+ mimeType: string
427
+ isGif?: boolean // 是否为GIF图片
428
+ }) => {
429
+ try {
430
+ this.logInfo('收到图片上传请求:', { filename: data.filename, mimeType: data.mimeType, isGif: data.isGif })
431
+
432
+ // 将base64转换为Buffer
433
+ const base64Data = data.file.replace(/^data:image\/\w+;base64,/, '')
434
+ const buffer = Buffer.from(base64Data, 'base64')
435
+
436
+ // 生成临时文件路径,保持原始扩展名以保持GIF格式
437
+ const tempId = Date.now() + '_' + Math.random().toString(36).substring(2, 11)
438
+ const extension = data.filename.split('.').pop()?.toLowerCase() || (data.isGif ? 'gif' : 'jpg')
439
+ const tempFilename = `temp_${tempId}.${extension}`
440
+
441
+ // 保存到临时目录
442
+ const tempDir = this.ctx.baseDir + '/data/chat-patch/temp'
443
+ if (!require('fs').existsSync(tempDir)) {
444
+ require('fs').mkdirSync(tempDir, { recursive: true })
445
+ }
446
+
447
+ const tempPath = `${tempDir}/${tempFilename}`
448
+
449
+ // 对于GIF文件,直接保存原始数据以保持动画
450
+ require('fs').writeFileSync(tempPath, buffer)
451
+
452
+ this.logInfo('图片上传成功:', { tempPath, size: buffer.length, isGif: data.isGif })
453
+
454
+ return {
455
+ success: true,
456
+ tempId: tempId,
457
+ tempPath: tempPath,
458
+ filename: data.filename,
459
+ size: buffer.length,
460
+ isGif: data.isGif
461
+ }
462
+ } catch (error: any) {
463
+ this.logger.error('图片上传失败:', error)
464
+ return { success: false, error: error?.message || String(error) }
465
+ }
466
+ })
467
+
468
+ // 删除临时图片的 API
469
+ this.ctx.console.addListener('delete-temp-image' as any, async (data: {
470
+ tempId: string
471
+ }) => {
472
+ try {
473
+ const tempDir = this.ctx.baseDir + '/data/chat-patch/temp'
474
+ const files = require('fs').readdirSync(tempDir).filter((file: string) =>
475
+ file.includes(`temp_${data.tempId}`)
476
+ )
477
+
478
+ for (const file of files) {
479
+ const filePath = `${tempDir}/${file}`
480
+ if (require('fs').existsSync(filePath)) {
481
+ require('fs').unlinkSync(filePath)
482
+ this.logInfo('删除临时图片:', filePath)
483
+ }
484
+ }
485
+
486
+ return { success: true }
487
+ } catch (error: any) {
488
+ this.logger.error('删除临时图片失败:', error)
489
+ return { success: false, error: error?.message || String(error) }
490
+ }
491
+ })
492
+
493
+ // 定时清理过期临时文件(备用机制)
494
+ this.setupTempFileCleanup()
495
+
496
+ // 获取插件配置的 API
497
+ this.ctx.console.addListener('get-plugin-config' as any, async () => {
498
+ try {
499
+ return {
500
+ success: true,
501
+ config: {
502
+ maxMessagesPerChannel: this.config.maxMessagesPerChannel,
503
+ keepMessagesOnClear: this.config.keepMessagesOnClear,
504
+ keepTempImages: this.config.keepTempImages,
505
+ loggerinfo: this.config.loggerinfo,
506
+ blockedPlatforms: this.config.blockedPlatforms || [],
507
+ chatContainerHeight: this.config.chatContainerHeight,
508
+ clearIndexedDBOnStart: this.config.clearIndexedDBOnStart
509
+ }
510
+ }
511
+ } catch (error: any) {
512
+ this.logger.error('获取插件配置失败:', error)
513
+ return { success: false, error: error?.message || String(error) }
514
+ }
515
+ })
516
+
517
+ // 调试API:获取原始文件数据
518
+ this.ctx.console.addListener('debug-get-raw-data' as any, async () => {
519
+ try {
520
+ const data = this.fileManager.readChatDataFromFile()
521
+ return {
522
+ success: true,
523
+ data: data
524
+ }
525
+ } catch (error: any) {
526
+ this.logger.error('获取原始数据失败:', error)
527
+ return { success: false, error: error?.message || String(error) }
528
+ }
529
+ })
530
+ }
531
+
532
+ // 检查是否为文件 URL
533
+ private isFileUrl(url: string): boolean {
534
+ try {
535
+ const parsedUrl = new URL(url)
536
+ return parsedUrl.protocol === 'file:'
537
+ } catch {
538
+ return false
539
+ }
540
+ }
541
+
542
+ // 创建文件 URL
543
+ private createFileUrl(filePath: string): string {
544
+ try {
545
+ return pathToFileURL(filePath).href
546
+ } catch (error) {
547
+ this.logger.error('创建文件URL失败:', { filePath, error })
548
+ // 回退到简单的字符串拼接
549
+ return `file://${filePath}`
550
+ }
551
+ }
552
+
553
+ // 处理本地文件请求
554
+ private async handleLocalFileRequest(fileUrl: string) {
555
+ try {
556
+ const fileresponse = await this.ctx.http.file(fileUrl)
557
+ const fileresponsebase64 = Buffer.from(fileresponse.data).toString('base64')
558
+ let contentType = fileresponse.type
559
+
560
+ this.logInfo('成功读取本地文件:', { fileUrl, contentType })
561
+
562
+ return {
563
+ success: true,
564
+ base64: fileresponsebase64,
565
+ contentType: contentType,
566
+ dataUrl: `data:${contentType};base64,${fileresponsebase64}`
567
+ }
568
+ } catch (error: any) {
569
+ this.logger.error('读取本地文件失败:', { fileUrl, error: error.message })
570
+ return {
571
+ success: false,
572
+ error: `读取本地文件失败: ${error.message}`
573
+ }
574
+ }
575
+ }
576
+
577
+ // 设置定时清理临时文件
578
+ private setupTempFileCleanup() {
579
+ // 每5分钟执行一次基于数量的清理
580
+ setInterval(() => {
581
+ this.cleanupTempImagesByCount()
582
+ }, 5 * 60 * 1000) // 5分钟
583
+ }
584
+
585
+ // 基于数量清理临时图片(保留最新的N张)
586
+ private async cleanupTempImagesByCount() {
587
+ try {
588
+ const tempDir = this.ctx.baseDir + '/data/chat-patch/temp'
589
+ if (!require('fs').existsSync(tempDir)) {
590
+ return
591
+ }
592
+
593
+ const now = Date.now()
594
+ const protectionTime = 30 * 1000 // 30秒保护期,刚上传的图片不会被删除
595
+
596
+ const files = require('fs').readdirSync(tempDir)
597
+ .filter((file: string) => file.startsWith('temp_'))
598
+ .map((file: string) => {
599
+ const filePath = `${tempDir}/${file}`
600
+ try {
601
+ const stats = require('fs').statSync(filePath)
602
+ const fileAge = now - stats.mtime.getTime()
603
+ return {
604
+ name: file,
605
+ path: filePath,
606
+ mtime: stats.mtime.getTime(),
607
+ age: fileAge,
608
+ protected: fileAge < protectionTime // 是否在保护期内
609
+ }
610
+ } catch (error) {
611
+ return null
612
+ }
613
+ })
614
+ .filter((file: any) => file !== null)
615
+ .sort((a: any, b: any) => b.mtime - a.mtime) // 按修改时间降序排列(最新的在前)
616
+
617
+ const keepCount = this.config.keepTempImages
618
+ let cleanedCount = 0
619
+ let protectedCount = 0
620
+
621
+ // 分离受保护的文件和可删除的文件
622
+ const protectedFiles = files.filter((file: any) => file.protected)
623
+ const deletableFiles = files.filter((file: any) => !file.protected)
624
+
625
+ protectedCount = protectedFiles.length
626
+
627
+ // 如果可删除的文件数量超过了保留数量,则删除多余的
628
+ if (deletableFiles.length > keepCount) {
629
+ const filesToDelete = deletableFiles.slice(keepCount) // 保留前N个,删除其余的
630
+
631
+ for (const file of filesToDelete) {
632
+ try {
633
+ if (require('fs').existsSync(file.path)) {
634
+ require('fs').unlinkSync(file.path)
635
+ cleanedCount++
636
+ this.logInfo('清理多余临时图片:', file.path)
637
+ }
638
+ } catch (fileError) {
639
+ this.logger.warn('删除临时文件失败:', { file: file.name, error: fileError })
640
+ }
641
+ }
642
+ }
643
+
644
+ if (cleanedCount > 0 || protectedCount > 0) {
645
+ this.logInfo(`基于数量清理完成,保留最新 ${keepCount} 张图片,清理了 ${cleanedCount} 张多余图片,保护了 ${protectedCount} 张新上传图片`)
646
+ }
647
+ } catch (error) {
648
+ this.logger.warn('基于数量清理临时文件失败:', error)
649
+ }
650
+ }
651
+
652
+ private logInfo(...args: any[]) {
653
+ if (this.config.loggerinfo) {
654
+ (this.logger.info as (...args: any[]) => void)(...args)
655
+ }
656
+ }
657
+ }