koishi-plugin-chat-patch 0.8.2 → 1.0.6

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