koishi-plugin-chat-patch 2.2.0 → 2.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.
- package/client/icons/activity.vue +9 -9
- package/client/icons/index.ts +4 -4
- package/client/index.scss +90 -90
- package/client/index.ts +25 -25
- package/client/vue/composables/useChatActions.ts +1 -1
- package/client/vue/composables/useChatData.ts +1 -1
- package/client/vue/composables/useImageCache.ts +1 -1
- package/client/vue/types.ts +1 -1
- package/lib/file-manager.d.ts +25 -0
- package/lib/index.js +284 -193
- package/lib/message-handler.d.ts +7 -5
- package/package.json +1 -1
- package/readme.md +25 -25
- package/src/api-handlers.ts +19 -65
- package/src/config.ts +45 -46
- package/src/file-manager.ts +215 -169
- package/src/index.ts +58 -31
- package/src/message-handler.ts +161 -134
- package/src/types.ts +62 -62
- package/src/utils.ts +4 -16
package/lib/message-handler.d.ts
CHANGED
|
@@ -9,13 +9,15 @@ export declare class MessageHandler {
|
|
|
9
9
|
private utils;
|
|
10
10
|
private correctChannelIds;
|
|
11
11
|
constructor(ctx: Context, config: Config, fileManager: FileManager);
|
|
12
|
+
recordUserMessage(session: Session, timestamp: number): void;
|
|
13
|
+
recordBotMessage(session: Session, timestamp: number): void;
|
|
12
14
|
setCorrectChannelId(selfId: string, channelId: string): void;
|
|
13
15
|
getCorrectChannelId(selfId: string): string | undefined;
|
|
14
|
-
updateBotInfoToFile(session: Session):
|
|
15
|
-
updateChannelInfoToFile(session: Session):
|
|
16
|
+
updateBotInfoToFile(session: Session): void;
|
|
17
|
+
updateChannelInfoToFile(session: Session): string;
|
|
16
18
|
downloadAndCacheMedia(url: string, type: 'image' | 'media' | 'avatar'): Promise<any>;
|
|
17
|
-
private
|
|
18
|
-
|
|
19
|
-
|
|
19
|
+
private processMediaElementsAsync;
|
|
20
|
+
private processUserMessage;
|
|
21
|
+
private processBotMessage;
|
|
20
22
|
private logInfo;
|
|
21
23
|
}
|
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
|
+
"version": "2.3.0",
|
|
5
5
|
"main": "lib/index.js",
|
|
6
6
|
"typings": "lib/index.d.ts",
|
|
7
7
|
"files": [
|
package/readme.md
CHANGED
|
@@ -1,25 +1,25 @@
|
|
|
1
|
-
# koishi-plugin-chat-patch
|
|
2
|
-
|
|
3
|
-
[](https://www.npmjs.com/package/koishi-plugin-chat-patch) [](https://www.npmjs.com/package/koishi-plugin-chat-patch)
|
|
4
|
-
|
|
5
|
-
Koishi 控制台聊天插件,允许用户直接在控制台中管理机器人、频道和消息。
|
|
6
|
-
|
|
7
|
-
支持消息的收发、图片缓存、频道置顶等。
|
|
8
|
-
|
|
9
|
-
## ✨ 功能特性
|
|
10
|
-
|
|
11
|
-
- **聊天界面**:在 Koishi 控制台内直接查看机器人的聊天记录。
|
|
12
|
-
- **消息收发**:支持接收文本、图片等多种类型的消息。
|
|
13
|
-
- **图片缓存**:通过 IndexedDB 缓存图片,优化加载速度并减少网络请求。
|
|
14
|
-
- **手机端优化**:针对手机端进行适配,提供滑动返回等手势操作,并可配置聊天容器高度以防止输入框被遮挡。
|
|
15
|
-
- **平台屏蔽**:可配置屏蔽特定平台的消息,避免不必要的干扰。
|
|
16
|
-
|
|
17
|
-
## 🛠️ 开发与贡献
|
|
18
|
-
|
|
19
|
-
欢迎 PR ~
|
|
20
|
-
|
|
21
|
-
PR 方式请参考 -> <https://github.com/koishi-shangxue-plugins/koishi-shangxue-apps/tree/main?tab=readme-ov-file#%E5%A6%82%E4%BD%95%E5%9C%A8%E9%A1%B9%E7%9B%AE%E6%A8%A1%E6%9D%BF%E4%B8%AD%E5%BC%80%E5%8F%91%E6%AD%A4%E4%BB%93%E5%BA%93>
|
|
22
|
-
|
|
23
|
-
## 许可证
|
|
24
|
-
|
|
25
|
-
本项目采用 [MIT 许可证](LICENSE) 开源。
|
|
1
|
+
# koishi-plugin-chat-patch
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/koishi-plugin-chat-patch) [](https://www.npmjs.com/package/koishi-plugin-chat-patch)
|
|
4
|
+
|
|
5
|
+
Koishi 控制台聊天插件,允许用户直接在控制台中管理机器人、频道和消息。
|
|
6
|
+
|
|
7
|
+
支持消息的收发、图片缓存、频道置顶等。
|
|
8
|
+
|
|
9
|
+
## ✨ 功能特性
|
|
10
|
+
|
|
11
|
+
- **聊天界面**:在 Koishi 控制台内直接查看机器人的聊天记录。
|
|
12
|
+
- **消息收发**:支持接收文本、图片等多种类型的消息。
|
|
13
|
+
- **图片缓存**:通过 IndexedDB 缓存图片,优化加载速度并减少网络请求。
|
|
14
|
+
- **手机端优化**:针对手机端进行适配,提供滑动返回等手势操作,并可配置聊天容器高度以防止输入框被遮挡。
|
|
15
|
+
- **平台屏蔽**:可配置屏蔽特定平台的消息,避免不必要的干扰。
|
|
16
|
+
|
|
17
|
+
## 🛠️ 开发与贡献
|
|
18
|
+
|
|
19
|
+
欢迎 PR ~
|
|
20
|
+
|
|
21
|
+
PR 方式请参考 -> <https://github.com/koishi-shangxue-plugins/koishi-shangxue-apps/tree/main?tab=readme-ov-file#%E5%A6%82%E4%BD%95%E5%9C%A8%E9%A1%B9%E7%9B%AE%E6%A8%A1%E6%9D%BF%E4%B8%AD%E5%BC%80%E5%8F%91%E6%AD%A4%E4%BB%93%E5%BA%93>
|
|
22
|
+
|
|
23
|
+
## 许可证
|
|
24
|
+
|
|
25
|
+
本项目采用 [MIT 许可证](LICENSE) 开源。
|
package/src/api-handlers.ts
CHANGED
|
@@ -9,7 +9,7 @@ import * as mime from 'mime-types'
|
|
|
9
9
|
|
|
10
10
|
export class ApiHandlers {
|
|
11
11
|
private logger: Logger
|
|
12
|
-
|
|
12
|
+
|
|
13
13
|
private currentTempVideo: string | null = null
|
|
14
14
|
|
|
15
15
|
constructor(
|
|
@@ -25,7 +25,7 @@ export class ApiHandlers {
|
|
|
25
25
|
this.ctx.console.addListener('clear-all-indexeddb-data' as any, async () => {
|
|
26
26
|
try {
|
|
27
27
|
this.logInfo('收到清空 IndexedDB 数据请求')
|
|
28
|
-
|
|
28
|
+
|
|
29
29
|
return { success: true, message: '可以清空 IndexedDB' }
|
|
30
30
|
} catch (error: any) {
|
|
31
31
|
this.logger.error('清空 IndexedDB 数据失败:', error)
|
|
@@ -33,10 +33,9 @@ export class ApiHandlers {
|
|
|
33
33
|
}
|
|
34
34
|
})
|
|
35
35
|
|
|
36
|
-
// 获取所有聊天数据的 API
|
|
37
36
|
this.ctx.console.addListener('get-chat-data' as any, async () => {
|
|
38
37
|
try {
|
|
39
|
-
|
|
38
|
+
|
|
40
39
|
const data = this.fileManager.readChatDataFromFile()
|
|
41
40
|
|
|
42
41
|
this.logInfo('获取基础聊天数据')
|
|
@@ -48,7 +47,7 @@ export class ApiHandlers {
|
|
|
48
47
|
channels: data.channels || {},
|
|
49
48
|
pinnedBots: data.pinnedBots || [],
|
|
50
49
|
pinnedChannels: data.pinnedChannels || [],
|
|
51
|
-
|
|
50
|
+
|
|
52
51
|
messages: {}
|
|
53
52
|
}
|
|
54
53
|
}
|
|
@@ -58,7 +57,6 @@ export class ApiHandlers {
|
|
|
58
57
|
}
|
|
59
58
|
})
|
|
60
59
|
|
|
61
|
-
// 获取历史消息的 API
|
|
62
60
|
this.ctx.console.addListener('get-history-messages' as any, async (requestData: {
|
|
63
61
|
selfId: string
|
|
64
62
|
channelId: string
|
|
@@ -70,18 +68,16 @@ export class ApiHandlers {
|
|
|
70
68
|
const channelKey = `${requestData.selfId}:${requestData.channelId}`
|
|
71
69
|
let messages = data.messages[channelKey] || []
|
|
72
70
|
|
|
73
|
-
// 按时间戳降序排列(最新的消息在前面)
|
|
74
71
|
const sortedMessages = messages.sort((a, b) => b.timestamp - a.timestamp)
|
|
75
72
|
|
|
76
|
-
// 如果提供了分页参数,则进行分页处理
|
|
77
73
|
if (requestData.limit !== undefined) {
|
|
78
74
|
const limit = requestData.limit
|
|
79
75
|
const offset = requestData.offset || 0
|
|
80
76
|
messages = sortedMessages.slice(offset, offset + limit)
|
|
81
|
-
|
|
77
|
+
|
|
82
78
|
messages = messages.sort((a, b) => a.timestamp - b.timestamp)
|
|
83
79
|
} else {
|
|
84
|
-
|
|
80
|
+
|
|
85
81
|
messages = sortedMessages.sort((a, b) => a.timestamp - b.timestamp)
|
|
86
82
|
}
|
|
87
83
|
|
|
@@ -98,7 +94,6 @@ export class ApiHandlers {
|
|
|
98
94
|
}
|
|
99
95
|
})
|
|
100
96
|
|
|
101
|
-
// 获取所有频道消息数量的 API
|
|
102
97
|
this.ctx.console.addListener('get-all-channel-message-counts' as any, async () => {
|
|
103
98
|
try {
|
|
104
99
|
const data = this.fileManager.readChatDataFromFile()
|
|
@@ -123,16 +118,14 @@ export class ApiHandlers {
|
|
|
123
118
|
}
|
|
124
119
|
})
|
|
125
120
|
|
|
126
|
-
// 图片获取 API
|
|
127
121
|
this.ctx.console.addListener('fetch-image' as any, async (data: { url: string }) => {
|
|
128
122
|
try {
|
|
129
|
-
|
|
123
|
+
|
|
130
124
|
if (this.isFileUrl(data.url)) {
|
|
131
125
|
this.logInfo('处理本地文件请求:', data.url)
|
|
132
126
|
return await this.handleLocalFileRequest(data.url)
|
|
133
127
|
}
|
|
134
128
|
|
|
135
|
-
// 处理网络图片
|
|
136
129
|
const response = await fetch(data.url, {
|
|
137
130
|
headers: {
|
|
138
131
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
|
@@ -158,7 +151,6 @@ export class ApiHandlers {
|
|
|
158
151
|
}
|
|
159
152
|
})
|
|
160
153
|
|
|
161
|
-
// 清理频道历史记录的 API
|
|
162
154
|
this.ctx.console.addListener('clear-channel-history' as any, async (data: {
|
|
163
155
|
selfId: string
|
|
164
156
|
channelId: string
|
|
@@ -208,7 +200,6 @@ export class ApiHandlers {
|
|
|
208
200
|
}
|
|
209
201
|
})
|
|
210
202
|
|
|
211
|
-
// 发送消息的 API
|
|
212
203
|
this.ctx.console.addListener('send-message' as any, async (data: {
|
|
213
204
|
selfId: string
|
|
214
205
|
channelId: string
|
|
@@ -221,14 +212,12 @@ export class ApiHandlers {
|
|
|
221
212
|
try {
|
|
222
213
|
this.logInfo('收到发送消息请求:', data)
|
|
223
214
|
|
|
224
|
-
// 优化机器人查找逻辑
|
|
225
215
|
const bot = this.ctx.bots.find((bot: any) => bot.selfId === data.selfId || bot.user?.id === data.selfId)
|
|
226
216
|
if (!bot) {
|
|
227
217
|
this.logger.error('未找到机器人:', data.selfId, '当前可用机器人:', this.ctx.bots.map((b: any) => b.selfId))
|
|
228
218
|
return { success: false, error: `未找到机器人 ${data.selfId},请检查机器人是否在线` }
|
|
229
219
|
}
|
|
230
220
|
|
|
231
|
-
// 检查机器人状态
|
|
232
221
|
if (bot.status !== Universal.Status.ONLINE) {
|
|
233
222
|
this.logger.error('机器人离线:', data.selfId, '状态:', bot.status)
|
|
234
223
|
return { success: false, error: `机器人 ${data.selfId} 当前离线` }
|
|
@@ -236,7 +225,6 @@ export class ApiHandlers {
|
|
|
236
225
|
|
|
237
226
|
let messageContent = data.content
|
|
238
227
|
|
|
239
|
-
// 如果有图片,添加图片元素
|
|
240
228
|
if (data.images && data.images.length > 0) {
|
|
241
229
|
const tempDir = this.ctx.baseDir + '/data/chat-patch/temp'
|
|
242
230
|
|
|
@@ -247,7 +235,7 @@ export class ApiHandlers {
|
|
|
247
235
|
|
|
248
236
|
if (files.length > 0) {
|
|
249
237
|
const imagePath = `${tempDir}/${files[0]}`
|
|
250
|
-
|
|
238
|
+
|
|
251
239
|
const fileUrl = this.createFileUrl(imagePath)
|
|
252
240
|
messageContent += h.image(fileUrl).toString()
|
|
253
241
|
this.logInfo('添加图片到消息:', { imagePath, fileUrl })
|
|
@@ -257,7 +245,6 @@ export class ApiHandlers {
|
|
|
257
245
|
|
|
258
246
|
const parsedContent = h.parse(messageContent)
|
|
259
247
|
|
|
260
|
-
// 在发送消息前,通知 MessageHandler 正确的 channelId
|
|
261
248
|
this.messageHandler.setCorrectChannelId(data.selfId, data.channelId)
|
|
262
249
|
|
|
263
250
|
const result = await bot.sendMessage(data.channelId, parsedContent)
|
|
@@ -265,19 +252,17 @@ export class ApiHandlers {
|
|
|
265
252
|
|
|
266
253
|
const messageId = Array.isArray(result) ? result[0] : result;
|
|
267
254
|
|
|
268
|
-
// 发送成功后,更新本地存储中的虚拟消息为真实 ID
|
|
269
255
|
if (messageId) {
|
|
270
256
|
const chatData = this.fileManager.readChatDataFromFile()
|
|
271
257
|
const channelKey = `${data.selfId}:${data.channelId}`
|
|
272
258
|
const messages = chatData.messages[channelKey] || []
|
|
273
|
-
|
|
259
|
+
|
|
274
260
|
const msg = [...messages].reverse().find(m => m.type === 'bot' && m.sending)
|
|
275
261
|
if (msg) {
|
|
276
262
|
msg.realId = messageId;
|
|
277
263
|
msg.sending = false;
|
|
278
264
|
this.fileManager.writeChatDataToFile(chatData)
|
|
279
265
|
|
|
280
|
-
// 广播更新事件给前端
|
|
281
266
|
this.ctx.console.broadcast('bot-message-updated', {
|
|
282
267
|
channelKey,
|
|
283
268
|
tempId: msg.id,
|
|
@@ -297,14 +282,12 @@ export class ApiHandlers {
|
|
|
297
282
|
}
|
|
298
283
|
})
|
|
299
284
|
|
|
300
|
-
// 清理临时图片的 API(由前端在消息发送成功后调用)
|
|
301
285
|
this.ctx.console.addListener('cleanup-temp-images' as any, async (data: {
|
|
302
286
|
tempImageIds: string[]
|
|
303
287
|
}) => {
|
|
304
288
|
try {
|
|
305
289
|
this.logInfo('收到清理临时图片请求:', data.tempImageIds)
|
|
306
290
|
|
|
307
|
-
// 不立即删除,只是记录日志,让定时任务处理清理
|
|
308
291
|
this.logInfo('临时图片将由定时任务清理,保持文件可用性')
|
|
309
292
|
|
|
310
293
|
return { success: true, cleanedCount: 0 }
|
|
@@ -314,7 +297,6 @@ export class ApiHandlers {
|
|
|
314
297
|
}
|
|
315
298
|
})
|
|
316
299
|
|
|
317
|
-
// 删除机器人所有数据的 API
|
|
318
300
|
this.ctx.console.addListener('delete-bot-data' as any, async (data: {
|
|
319
301
|
selfId: string
|
|
320
302
|
}) => {
|
|
@@ -356,7 +338,6 @@ export class ApiHandlers {
|
|
|
356
338
|
}
|
|
357
339
|
})
|
|
358
340
|
|
|
359
|
-
// 删除频道所有数据的 API
|
|
360
341
|
this.ctx.console.addListener('delete-channel-data' as any, async (data: {
|
|
361
342
|
selfId: string
|
|
362
343
|
channelId: string
|
|
@@ -394,7 +375,6 @@ export class ApiHandlers {
|
|
|
394
375
|
}
|
|
395
376
|
})
|
|
396
377
|
|
|
397
|
-
// 设置置顶机器人列表的 API
|
|
398
378
|
this.ctx.console.addListener('set-pinned-bots' as any, async (data: {
|
|
399
379
|
pinnedBots: string[]
|
|
400
380
|
}) => {
|
|
@@ -410,7 +390,6 @@ export class ApiHandlers {
|
|
|
410
390
|
}
|
|
411
391
|
})
|
|
412
392
|
|
|
413
|
-
// 设置置顶频道列表的 API
|
|
414
393
|
this.ctx.console.addListener('set-pinned-channels' as any, async (data: {
|
|
415
394
|
pinnedChannels: string[]
|
|
416
395
|
}) => {
|
|
@@ -426,26 +405,22 @@ export class ApiHandlers {
|
|
|
426
405
|
}
|
|
427
406
|
})
|
|
428
407
|
|
|
429
|
-
// 上传图片的 API
|
|
430
408
|
this.ctx.console.addListener('upload-image' as any, async (data: {
|
|
431
|
-
file: string
|
|
409
|
+
file: string
|
|
432
410
|
filename: string
|
|
433
411
|
mimeType: string
|
|
434
|
-
isGif?: boolean
|
|
412
|
+
isGif?: boolean
|
|
435
413
|
}) => {
|
|
436
414
|
try {
|
|
437
415
|
this.logInfo('收到图片上传请求:', { filename: data.filename, mimeType: data.mimeType, isGif: data.isGif })
|
|
438
416
|
|
|
439
|
-
// 将base64转换为Buffer
|
|
440
417
|
const base64Data = data.file.replace(/^data:image\/\w+;base64,/, '')
|
|
441
418
|
const buffer = Buffer.from(base64Data, 'base64')
|
|
442
419
|
|
|
443
|
-
// 生成临时文件路径,保持原始扩展名以保持GIF格式
|
|
444
420
|
const tempId = Date.now() + '_' + Math.random().toString(36).substring(2, 11)
|
|
445
421
|
const extension = data.filename.split('.').pop()?.toLowerCase() || (data.isGif ? 'gif' : 'jpg')
|
|
446
422
|
const tempFilename = `temp_${tempId}.${extension}`
|
|
447
423
|
|
|
448
|
-
// 保存到临时目录
|
|
449
424
|
const tempDir = this.ctx.baseDir + '/data/chat-patch/temp'
|
|
450
425
|
if (!require('fs').existsSync(tempDir)) {
|
|
451
426
|
require('fs').mkdirSync(tempDir, { recursive: true })
|
|
@@ -453,7 +428,6 @@ export class ApiHandlers {
|
|
|
453
428
|
|
|
454
429
|
const tempPath = `${tempDir}/${tempFilename}`
|
|
455
430
|
|
|
456
|
-
// 对于GIF文件,直接保存原始数据以保持动画
|
|
457
431
|
require('fs').writeFileSync(tempPath, buffer)
|
|
458
432
|
|
|
459
433
|
this.logInfo('图片上传成功:', { tempPath, size: buffer.length, isGif: data.isGif })
|
|
@@ -472,7 +446,6 @@ export class ApiHandlers {
|
|
|
472
446
|
}
|
|
473
447
|
})
|
|
474
448
|
|
|
475
|
-
// 删除临时图片的 API
|
|
476
449
|
this.ctx.console.addListener('delete-temp-image' as any, async (data: {
|
|
477
450
|
tempId: string
|
|
478
451
|
}) => {
|
|
@@ -497,10 +470,8 @@ export class ApiHandlers {
|
|
|
497
470
|
}
|
|
498
471
|
})
|
|
499
472
|
|
|
500
|
-
// 定时清理过期临时文件(备用机制)
|
|
501
473
|
this.setupTempFileCleanup()
|
|
502
474
|
|
|
503
|
-
// 获取插件配置的 API
|
|
504
475
|
this.ctx.console.addListener('get-plugin-config' as any, async () => {
|
|
505
476
|
try {
|
|
506
477
|
return {
|
|
@@ -520,13 +491,11 @@ export class ApiHandlers {
|
|
|
520
491
|
}
|
|
521
492
|
})
|
|
522
493
|
|
|
523
|
-
// 获取用户信息 API
|
|
524
494
|
this.ctx.console.addListener('get-user-info' as any, async (data: { selfId: string, userId: string, guildId?: string }) => {
|
|
525
495
|
try {
|
|
526
496
|
const bot = this.ctx.bots.find(b => b.selfId === data.selfId)
|
|
527
497
|
if (!bot) return { success: false, error: '机器人不存在' }
|
|
528
498
|
|
|
529
|
-
// 检查平台是否支持 getUser 方法
|
|
530
499
|
if (!bot.getUser || typeof bot.getUser !== 'function') {
|
|
531
500
|
return { success: false, error: '此平台不支持查看用户信息' }
|
|
532
501
|
}
|
|
@@ -534,7 +503,7 @@ export class ApiHandlers {
|
|
|
534
503
|
const user = await bot.getUser(data.userId, data.guildId)
|
|
535
504
|
|
|
536
505
|
if (user) {
|
|
537
|
-
|
|
506
|
+
|
|
538
507
|
if (user.avatar) {
|
|
539
508
|
user.avatar = await this.messageHandler.downloadAndCacheMedia(user.avatar, 'avatar')
|
|
540
509
|
}
|
|
@@ -542,10 +511,8 @@ export class ApiHandlers {
|
|
|
542
511
|
const chatData = this.fileManager.readChatDataFromFile()
|
|
543
512
|
let changed = false
|
|
544
513
|
|
|
545
|
-
// 1. 如果是私聊,更新频道名 - 支持多种私聊ID格式
|
|
546
514
|
const botChannels = chatData.channels[data.selfId] || {}
|
|
547
515
|
|
|
548
|
-
// 查找所有可能的私聊频道ID格式
|
|
549
516
|
const possibleChannelIds = [
|
|
550
517
|
data.userId,
|
|
551
518
|
`private:${data.userId}`,
|
|
@@ -564,7 +531,6 @@ export class ApiHandlers {
|
|
|
564
531
|
}
|
|
565
532
|
}
|
|
566
533
|
|
|
567
|
-
// 2. 更新该用户在所有历史消息中的头像和昵称(持久化缓存)
|
|
568
534
|
const channelKeyPrefix = `${data.selfId}:`
|
|
569
535
|
for (const [key, messages] of Object.entries(chatData.messages)) {
|
|
570
536
|
if (key.startsWith(channelKeyPrefix)) {
|
|
@@ -585,7 +551,7 @@ export class ApiHandlers {
|
|
|
585
551
|
|
|
586
552
|
if (changed) {
|
|
587
553
|
this.fileManager.writeChatDataToFile(chatData)
|
|
588
|
-
|
|
554
|
+
|
|
589
555
|
this.ctx.console.broadcast('chat-data-updated', {})
|
|
590
556
|
}
|
|
591
557
|
}
|
|
@@ -596,7 +562,6 @@ export class ApiHandlers {
|
|
|
596
562
|
}
|
|
597
563
|
})
|
|
598
564
|
|
|
599
|
-
// 调试API:获取原始文件数据
|
|
600
565
|
this.ctx.console.addListener('debug-get-raw-data' as any, async () => {
|
|
601
566
|
try {
|
|
602
567
|
const data = this.fileManager.readChatDataFromFile()
|
|
@@ -610,18 +575,15 @@ export class ApiHandlers {
|
|
|
610
575
|
}
|
|
611
576
|
})
|
|
612
577
|
|
|
613
|
-
// 按需加载视频的临时缓存 API - 返回 base64 供前端转换为 blob
|
|
614
578
|
this.ctx.console.addListener('fetch-video-temp' as any, async (data: { url: string }) => {
|
|
615
579
|
try {
|
|
616
580
|
this.logInfo('收到视频临时加载请求:', data.url)
|
|
617
581
|
|
|
618
|
-
// 检查是否是本地文件
|
|
619
582
|
if (this.isFileUrl(data.url)) {
|
|
620
583
|
const result = await this.handleLocalFileRequest(data.url)
|
|
621
584
|
return result
|
|
622
585
|
}
|
|
623
586
|
|
|
624
|
-
// 下载视频并返回 base64
|
|
625
587
|
const response = await fetch(data.url, {
|
|
626
588
|
headers: {
|
|
627
589
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
|
|
@@ -636,7 +598,6 @@ export class ApiHandlers {
|
|
|
636
598
|
const buffer = await response.arrayBuffer()
|
|
637
599
|
const base64 = Buffer.from(buffer).toString('base64')
|
|
638
600
|
|
|
639
|
-
// 尝试从 Content-Type 获取 MIME 类型
|
|
640
601
|
const contentType = response.headers.get('content-type') || 'video/mp4'
|
|
641
602
|
|
|
642
603
|
this.logInfo('视频下载成功:', { size: buffer.byteLength, contentType })
|
|
@@ -655,7 +616,6 @@ export class ApiHandlers {
|
|
|
655
616
|
})
|
|
656
617
|
}
|
|
657
618
|
|
|
658
|
-
// 检查是否为文件 URL
|
|
659
619
|
private isFileUrl(url: string): boolean {
|
|
660
620
|
try {
|
|
661
621
|
const parsedUrl = new URL(url)
|
|
@@ -665,28 +625,24 @@ export class ApiHandlers {
|
|
|
665
625
|
}
|
|
666
626
|
}
|
|
667
627
|
|
|
668
|
-
// 创建文件 URL
|
|
669
628
|
private createFileUrl(filePath: string): string {
|
|
670
629
|
try {
|
|
671
630
|
return pathToFileURL(filePath).href
|
|
672
631
|
} catch (error) {
|
|
673
632
|
this.logger.error('创建文件URL失败:', { filePath, error })
|
|
674
|
-
|
|
633
|
+
|
|
675
634
|
return `file://${filePath}`
|
|
676
635
|
}
|
|
677
636
|
}
|
|
678
637
|
|
|
679
|
-
// 处理本地文件请求
|
|
680
638
|
private async handleLocalFileRequest(fileUrl: string) {
|
|
681
639
|
try {
|
|
682
|
-
|
|
640
|
+
|
|
683
641
|
const filePath = fileURLToPath(fileUrl)
|
|
684
642
|
|
|
685
|
-
// 改用 node:fs 直接读取文件,避免 ctx.http.file 可能存在的编码或协议处理问题
|
|
686
643
|
const buffer = readFileSync(filePath)
|
|
687
644
|
const base64 = buffer.toString('base64')
|
|
688
645
|
|
|
689
|
-
// 使用 mime-types 库精确推断 MIME 类型
|
|
690
646
|
const contentType = mime.lookup(filePath) || 'application/octet-stream'
|
|
691
647
|
|
|
692
648
|
this.logInfo('成功读取本地文件:', { fileUrl, filePath, contentType })
|
|
@@ -706,15 +662,13 @@ export class ApiHandlers {
|
|
|
706
662
|
}
|
|
707
663
|
}
|
|
708
664
|
|
|
709
|
-
// 设置定时清理临时文件
|
|
710
665
|
private setupTempFileCleanup() {
|
|
711
|
-
|
|
712
|
-
setInterval(() => {
|
|
666
|
+
|
|
667
|
+
this.ctx.setInterval(() => {
|
|
713
668
|
this.cleanupMediaCache()
|
|
714
669
|
}, 5 * 60 * 1000)
|
|
715
670
|
}
|
|
716
671
|
|
|
717
|
-
// 统一清理媒体缓存
|
|
718
672
|
private async cleanupMediaCache() {
|
|
719
673
|
const baseDir = this.ctx.baseDir + '/data/chat-patch/persist-media'
|
|
720
674
|
if (!require('node:fs').existsSync(baseDir)) return
|
|
@@ -738,8 +692,8 @@ export class ApiHandlers {
|
|
|
738
692
|
}
|
|
739
693
|
}
|
|
740
694
|
|
|
741
|
-
cleanupDir('images', 100)
|
|
742
|
-
cleanupDir('media', 20)
|
|
695
|
+
cleanupDir('images', 100)
|
|
696
|
+
cleanupDir('media', 20)
|
|
743
697
|
}
|
|
744
698
|
|
|
745
699
|
private logInfo(...args: any[]) {
|
package/src/config.ts
CHANGED
|
@@ -1,46 +1,45 @@
|
|
|
1
|
-
import { Schema } from 'koishi'
|
|
2
|
-
|
|
3
|
-
export interface Config {
|
|
4
|
-
loggerinfo: boolean
|
|
5
|
-
clearIndexedDBOnStart: boolean
|
|
6
|
-
maxMessagesPerChannel: number
|
|
7
|
-
keepMessagesOnClear: number
|
|
8
|
-
maxPersistImages: number
|
|
9
|
-
blockedPlatforms: Array<{
|
|
10
|
-
platformName: string
|
|
11
|
-
exactMatch: boolean
|
|
12
|
-
}>
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
export const Config: Schema<Config> = Schema.intersect([
|
|
16
|
-
Schema.object({
|
|
17
|
-
maxMessagesPerChannel: Schema.number().default(500).description('每个群组最大保存消息数量').min(50).max(1500).step(1),
|
|
18
|
-
keepMessagesOnClear: Schema.number().default(50).description('手动清理历史记录时保留的消息数量').min(0).max(1000).step(1),
|
|
19
|
-
maxPersistImages: Schema.number().default(100).description('持久化存储的图片缓存数量').min(10).max(500).step(1),
|
|
20
|
-
blockedPlatforms: Schema.array(Schema.object({
|
|
21
|
-
platformName: Schema.string().description('平台名称或关键词'),
|
|
22
|
-
exactMatch: Schema.boolean().default(false).description('完全匹配?如果关闭,包含关键词即屏蔽').default(true)
|
|
23
|
-
})).role('table').description('屏蔽的平台列表').default(
|
|
24
|
-
[
|
|
25
|
-
{
|
|
26
|
-
"platformName": "qq",
|
|
27
|
-
"exactMatch": true
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
"platformName": "qqguild",
|
|
31
|
-
"exactMatch": true
|
|
32
|
-
},
|
|
33
|
-
{
|
|
34
|
-
"platformName": "sandbox",
|
|
35
|
-
"exactMatch": false
|
|
36
|
-
}
|
|
37
|
-
]
|
|
38
|
-
),
|
|
39
|
-
}).description('基础设置'),
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
])
|
|
1
|
+
import { Schema } from 'koishi'
|
|
2
|
+
|
|
3
|
+
export interface Config {
|
|
4
|
+
loggerinfo: boolean
|
|
5
|
+
clearIndexedDBOnStart: boolean
|
|
6
|
+
maxMessagesPerChannel: number
|
|
7
|
+
keepMessagesOnClear: number
|
|
8
|
+
maxPersistImages: number
|
|
9
|
+
blockedPlatforms: Array<{
|
|
10
|
+
platformName: string
|
|
11
|
+
exactMatch: boolean
|
|
12
|
+
}>
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export const Config: Schema<Config> = Schema.intersect([
|
|
16
|
+
Schema.object({
|
|
17
|
+
maxMessagesPerChannel: Schema.number().default(500).description('每个群组最大保存消息数量').min(50).max(1500).step(1),
|
|
18
|
+
keepMessagesOnClear: Schema.number().default(50).description('手动清理历史记录时保留的消息数量').min(0).max(1000).step(1),
|
|
19
|
+
maxPersistImages: Schema.number().default(100).description('持久化存储的图片缓存数量').min(10).max(500).step(1),
|
|
20
|
+
blockedPlatforms: Schema.array(Schema.object({
|
|
21
|
+
platformName: Schema.string().description('平台名称或关键词'),
|
|
22
|
+
exactMatch: Schema.boolean().default(false).description('完全匹配?如果关闭,包含关键词即屏蔽').default(true)
|
|
23
|
+
})).role('table').description('屏蔽的平台列表').default(
|
|
24
|
+
[
|
|
25
|
+
{
|
|
26
|
+
"platformName": "qq",
|
|
27
|
+
"exactMatch": true
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
"platformName": "qqguild",
|
|
31
|
+
"exactMatch": true
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
"platformName": "sandbox",
|
|
35
|
+
"exactMatch": false
|
|
36
|
+
}
|
|
37
|
+
]
|
|
38
|
+
),
|
|
39
|
+
}).description('基础设置'),
|
|
40
|
+
|
|
41
|
+
Schema.object({
|
|
42
|
+
clearIndexedDBOnStart: Schema.boolean().default(true).description('启动时强制清空IndexedDB缓存(适用于紧急情况,防止浏览器卡死)'),
|
|
43
|
+
loggerinfo: Schema.boolean().default(false).description('日志调试模式').experimental(),
|
|
44
|
+
}).description('开发者选项'),
|
|
45
|
+
])
|