koishi-plugin-chat-patch 2.4.5 → 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.
- package/dist/index.js +11 -3
- package/lib/api-handlers.d.ts +6 -1
- package/lib/config.d.ts +2 -0
- package/lib/file-manager.d.ts +102 -13
- package/lib/index.d.ts +17 -0
- package/lib/index.js +1364 -616
- package/lib/logger.d.ts +9 -0
- package/lib/message-handler.d.ts +14 -2
- package/lib/utils.d.ts +8 -3
- package/package.json +1 -1
- package/src/api-handlers.ts +115 -211
- package/src/config.ts +4 -0
- package/src/file-manager.ts +1236 -204
- package/src/index.ts +31 -105
- package/src/logger.ts +29 -0
- package/src/message-handler.ts +216 -154
- package/src/utils.ts +73 -35
package/lib/logger.d.ts
ADDED
|
@@ -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;
|
package/lib/message-handler.d.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
11
|
-
private
|
|
12
|
-
|
|
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": "
|
|
4
|
+
"version": "3.0.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
package/src/api-handlers.ts
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
1
|
import { FileManager } from './file-manager'
|
|
2
2
|
import { MessageHandler } from './message-handler'
|
|
3
|
-
import { Context, h,
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
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:
|
|
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
|
|
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 =
|
|
143
|
-
|
|
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
|
|
148
|
-
const
|
|
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 =
|
|
135
|
+
const filePath = path.join(dir, filename)
|
|
152
136
|
|
|
153
137
|
// 如果文件不存在,下载并保存
|
|
154
|
-
if (!
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
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
|
-
清理消息数:
|
|
181
|
+
清理消息数: deletedMessages
|
|
204
182
|
})
|
|
205
183
|
|
|
206
184
|
return {
|
|
207
185
|
success: true,
|
|
208
|
-
message: `成功清理 ${
|
|
209
|
-
clearedCount:
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
440
|
-
|
|
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 =
|
|
380
|
+
const tempPath = path.join(tempDir, tempFilename)
|
|
445
381
|
|
|
446
|
-
|
|
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
|
|
469
|
-
const files =
|
|
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 =
|
|
475
|
-
if (
|
|
476
|
-
|
|
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
|
|
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 =
|
|
616
|
-
|
|
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
|
|
621
|
-
const
|
|
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 =
|
|
502
|
+
const filePath = path.join(dir, filename)
|
|
625
503
|
|
|
626
504
|
// 如果文件不存在,下载并保存
|
|
627
|
-
if (!
|
|
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
|
-
|
|
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.
|
|
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 =
|
|
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
|
|
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
|
-
|
|
734
|
-
|
|
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
|
|
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('平台名称或关键词'),
|