koishi-plugin-chat-patch 2.0.1 → 2.1.1

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/lib/types.d.ts ADDED
@@ -0,0 +1,57 @@
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
+ channelId?: string;
14
+ guildName?: string;
15
+ isDirect?: boolean;
16
+ }
17
+ export interface QuoteInfo {
18
+ messageId: string;
19
+ id: string;
20
+ content: string;
21
+ elements?: h[];
22
+ user: {
23
+ id: string;
24
+ name: string;
25
+ userId: string;
26
+ avatar?: string;
27
+ username: string;
28
+ };
29
+ timestamp: number;
30
+ }
31
+ export interface MessageInfo {
32
+ id: string;
33
+ content: string;
34
+ userId: string;
35
+ username: string;
36
+ avatar?: string;
37
+ timestamp: number;
38
+ channelId: string;
39
+ selfId: string;
40
+ elements?: h[];
41
+ type: 'user' | 'bot';
42
+ guildId?: string;
43
+ guildName?: string;
44
+ platform: string;
45
+ quote?: QuoteInfo;
46
+ isDirect?: boolean;
47
+ sending?: boolean;
48
+ realId?: string;
49
+ }
50
+ export interface ChatData {
51
+ bots: Record<string, BotInfo>;
52
+ channels: Record<string, Record<string, ChannelInfo>>;
53
+ messages: Record<string, MessageInfo[]>;
54
+ pinnedBots: string[];
55
+ pinnedChannels: string[];
56
+ lastSaveTime?: number;
57
+ }
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.0.1",
4
+ "version": "2.1.1",
5
5
  "main": "lib/index.js",
6
6
  "typings": "lib/index.d.ts",
7
7
  "files": [
@@ -1,6 +1,6 @@
1
1
  import { FileManager } from './file-manager'
2
2
  import { MessageHandler } from './message-handler'
3
- import { Context, h, Logger } from 'koishi'
3
+ import { Context, h, Logger, Universal } from 'koishi'
4
4
  import { Config } from './config'
5
5
  import { } from '@koishijs/plugin-console'
6
6
  import { URL, pathToFileURL, fileURLToPath } from 'node:url'
@@ -34,23 +34,20 @@ export class ApiHandlers {
34
34
  // 获取所有聊天数据的 API
35
35
  this.ctx.console.addListener('get-chat-data' as any, async () => {
36
36
  try {
37
+ // 优化性能:只读取基础信息,不读取庞大的消息体
37
38
  const data = this.fileManager.readChatDataFromFile()
38
- const cleanedData = this.fileManager.cleanExcessMessages(data)
39
-
40
- this.logInfo('获取聊天数据:', {
41
- 机器人数量: Object.keys(cleanedData.bots).length,
42
- 频道数量: Object.keys(cleanedData.channels).reduce((total, botId) =>
43
- total + Object.keys(cleanedData.channels[botId] || {}).length, 0),
44
- 消息频道数: Object.keys(cleanedData.messages).length,
45
- 总消息数: Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0)
46
- })
39
+
40
+ this.logInfo('获取基础聊天数据')
47
41
 
48
42
  return {
49
43
  success: true,
50
44
  data: {
51
- ...cleanedData,
52
- pinnedBots: cleanedData.pinnedBots,
53
- pinnedChannels: cleanedData.pinnedChannels
45
+ bots: data.bots || {},
46
+ channels: data.channels || {},
47
+ pinnedBots: data.pinnedBots || [],
48
+ pinnedChannels: data.pinnedChannels || [],
49
+ // 不返回 messages,由前端按需拉取
50
+ messages: {}
54
51
  }
55
52
  }
56
53
  } catch (error: any) {
@@ -222,10 +219,17 @@ export class ApiHandlers {
222
219
  try {
223
220
  this.logInfo('收到发送消息请求:', data)
224
221
 
225
- const bot = this.ctx.bots.find((bot: any) => bot.selfId === data.selfId)
222
+ // 优化机器人查找逻辑
223
+ const bot = this.ctx.bots.find((bot: any) => bot.selfId === data.selfId || bot.user?.id === data.selfId)
226
224
  if (!bot) {
227
- this.logger.error('未找到机器人:', data.selfId)
228
- return { success: false, error: '未找到指定的机器人' }
225
+ this.logger.error('未找到机器人:', data.selfId, '当前可用机器人:', this.ctx.bots.map((b: any) => b.selfId))
226
+ return { success: false, error: `未找到机器人 ${data.selfId},请检查机器人是否在线` }
227
+ }
228
+
229
+ // 检查机器人状态
230
+ if (bot.status !== Universal.Status.ONLINE) {
231
+ this.logger.error('机器人离线:', data.selfId, '状态:', bot.status)
232
+ return { success: false, error: `机器人 ${data.selfId} 当前离线` }
229
233
  }
230
234
 
231
235
  let messageContent = data.content
@@ -257,32 +261,31 @@ export class ApiHandlers {
257
261
  const result = await bot.sendMessage(data.channelId, parsedContent)
258
262
  this.logInfo('消息发送成功:', result)
259
263
 
260
- // 广播机器人消息发送成功事件,包含真实的 messageId
261
264
  const messageId = Array.isArray(result) ? result[0] : result;
265
+
266
+ // 发送成功后,更新本地存储中的虚拟消息为真实 ID
262
267
  if (messageId) {
263
- const bot = this.ctx.bots.find((b: any) => b.selfId === data.selfId);
264
- const botMessageEvent = {
265
- type: 'bot-message-sent',
266
- selfId: data.selfId,
267
- platform: bot?.platform || 'unknown',
268
- channelId: data.channelId,
269
- messageId: messageId,
270
- content: messageContent,
271
- userId: data.selfId,
272
- username: bot?.user?.name || `Bot-${data.selfId}`,
273
- avatar: bot?.user?.avatar,
274
- timestamp: Date.now(),
275
- guildName: '', // 这个信息在前端会补充
276
- channelType: 0, // 这个信息在前端会补充
277
- elements: [], // 这个信息在前端会补充
278
- isDirect: false // 这个信息在前端会补充
279
- };
280
-
281
- this.ctx.console.broadcast('bot-message-sent-event', botMessageEvent);
268
+ const chatData = this.fileManager.readChatDataFromFile()
269
+ const channelKey = `${data.selfId}:${data.channelId}`
270
+ const messages = chatData.messages[channelKey] || []
271
+ // 找到最近的一条正在发送的机器人消息
272
+ const msg = [...messages].reverse().find(m => m.type === 'bot' && m.sending)
273
+ if (msg) {
274
+ msg.realId = messageId;
275
+ msg.sending = false;
276
+ this.fileManager.writeChatDataToFile(chatData)
277
+
278
+ // 广播更新事件给前端
279
+ this.ctx.console.broadcast('bot-message-updated', {
280
+ channelKey,
281
+ tempId: msg.id,
282
+ realId: messageId
283
+ })
284
+ }
282
285
  }
283
286
 
284
287
  return {
285
- success: true,
288
+ success: !!messageId,
286
289
  messageId: messageId,
287
290
  tempImageIds: data.images?.map(img => img.tempId) || []
288
291
  }
@@ -503,7 +506,7 @@ export class ApiHandlers {
503
506
  config: {
504
507
  maxMessagesPerChannel: this.config.maxMessagesPerChannel,
505
508
  keepMessagesOnClear: this.config.keepMessagesOnClear,
506
- keepTempImages: this.config.keepTempImages,
509
+ maxPersistImages: this.config.maxPersistImages,
507
510
  loggerinfo: this.config.loggerinfo,
508
511
  blockedPlatforms: this.config.blockedPlatforms || [],
509
512
  clearIndexedDBOnStart: this.config.clearIndexedDBOnStart
@@ -515,6 +518,82 @@ export class ApiHandlers {
515
518
  }
516
519
  })
517
520
 
521
+ // 获取用户信息 API
522
+ this.ctx.console.addListener('get-user-info' as any, async (data: { selfId: string, userId: string, guildId?: string }) => {
523
+ try {
524
+ const bot = this.ctx.bots.find(b => b.selfId === data.selfId)
525
+ if (!bot) return { success: false, error: '机器人不存在' }
526
+
527
+ // 检查平台是否支持 getUser 方法
528
+ if (!bot.getUser || typeof bot.getUser !== 'function') {
529
+ return { success: false, error: '此平台不支持查看用户信息' }
530
+ }
531
+
532
+ const user = await bot.getUser(data.userId, data.guildId)
533
+
534
+ if (user) {
535
+ // 缓存头像到本地
536
+ if (user.avatar) {
537
+ user.avatar = await this.messageHandler.downloadAndCacheMedia(user.avatar, 'avatar')
538
+ }
539
+
540
+ const chatData = this.fileManager.readChatDataFromFile()
541
+ let changed = false
542
+
543
+ // 1. 如果是私聊,更新频道名 - 支持多种私聊ID格式
544
+ const botChannels = chatData.channels[data.selfId] || {}
545
+
546
+ // 查找所有可能的私聊频道ID格式
547
+ const possibleChannelIds = [
548
+ data.userId,
549
+ `private:${data.userId}`,
550
+ `direct:${data.userId}`
551
+ ]
552
+
553
+ for (const channelId of possibleChannelIds) {
554
+ const channel = botChannels[channelId]
555
+ if (channel && channel.isDirect) {
556
+ const newName = `私聊(${user.name})`
557
+ if (channel.name !== newName) {
558
+ channel.name = newName
559
+ changed = true
560
+ this.logInfo('更新私聊频道名称:', { channelId, oldName: channel.name, newName })
561
+ }
562
+ }
563
+ }
564
+
565
+ // 2. 更新该用户在所有历史消息中的头像和昵称(持久化缓存)
566
+ const channelKeyPrefix = `${data.selfId}:`
567
+ for (const [key, messages] of Object.entries(chatData.messages)) {
568
+ if (key.startsWith(channelKeyPrefix)) {
569
+ messages.forEach(msg => {
570
+ if (msg.userId === data.userId) {
571
+ if (user.name && msg.username !== user.name) {
572
+ msg.username = user.name
573
+ changed = true
574
+ }
575
+ if (user.avatar && msg.avatar !== user.avatar) {
576
+ msg.avatar = user.avatar
577
+ changed = true
578
+ }
579
+ }
580
+ });
581
+ }
582
+ }
583
+
584
+ if (changed) {
585
+ this.fileManager.writeChatDataToFile(chatData)
586
+ // 广播更新事件,让前端实时刷新
587
+ this.ctx.console.broadcast('chat-data-updated', {})
588
+ }
589
+ }
590
+
591
+ return { success: true, data: user }
592
+ } catch (error: any) {
593
+ return { success: false, error: error?.message || '获取用户信息失败' }
594
+ }
595
+ })
596
+
518
597
  // 调试API:获取原始文件数据
519
598
  this.ctx.console.addListener('debug-get-raw-data' as any, async () => {
520
599
  try {
@@ -585,75 +664,36 @@ export class ApiHandlers {
585
664
  private setupTempFileCleanup() {
586
665
  // 每5分钟执行一次基于数量的清理
587
666
  setInterval(() => {
588
- this.cleanupTempImagesByCount()
589
- }, 5 * 60 * 1000) // 5分钟
667
+ this.cleanupMediaCache()
668
+ }, 5 * 60 * 1000)
590
669
  }
591
670
 
592
- // 基于数量清理临时图片(保留最新的N张)
593
- private async cleanupTempImagesByCount() {
594
- try {
595
- const tempDir = this.ctx.baseDir + '/data/chat-patch/temp'
596
- if (!require('fs').existsSync(tempDir)) {
597
- return
598
- }
671
+ // 统一清理媒体缓存
672
+ private async cleanupMediaCache() {
673
+ const baseDir = this.ctx.baseDir + '/data/chat-patch/persist-media'
674
+ if (!require('node:fs').existsSync(baseDir)) return
599
675
 
600
- const now = Date.now()
601
- const protectionTime = 30 * 1000 // 30秒保护期,刚上传的图片不会被删除
676
+ const cleanupDir = (dirName: string, limit: number) => {
677
+ const dirPath = require('node:path').join(baseDir, dirName)
678
+ if (!require('node:fs').existsSync(dirPath)) return
602
679
 
603
- const files = require('fs').readdirSync(tempDir)
604
- .filter((file: string) => file.startsWith('temp_'))
605
- .map((file: string) => {
606
- const filePath = `${tempDir}/${file}`
607
- try {
608
- const stats = require('fs').statSync(filePath)
609
- const fileAge = now - stats.mtime.getTime()
610
- return {
611
- name: file,
612
- path: filePath,
613
- mtime: stats.mtime.getTime(),
614
- age: fileAge,
615
- protected: fileAge < protectionTime // 是否在保护期内
616
- }
617
- } catch (error) {
618
- return null
619
- }
680
+ const files = require('node:fs').readdirSync(dirPath)
681
+ .map(file => {
682
+ const filePath = require('node:path').join(dirPath, file)
683
+ const stats = require('node:fs').statSync(filePath)
684
+ return { name: file, path: filePath, mtime: stats.mtimeMs }
620
685
  })
621
- .filter((file: any) => file !== null)
622
- .sort((a: any, b: any) => b.mtime - a.mtime) // 按修改时间降序排列(最新的在前)
623
-
624
- const keepCount = this.config.keepTempImages
625
- let cleanedCount = 0
626
- let protectedCount = 0
686
+ .sort((a, b) => b.mtime - a.mtime)
627
687
 
628
- // 分离受保护的文件和可删除的文件
629
- const protectedFiles = files.filter((file: any) => file.protected)
630
- const deletableFiles = files.filter((file: any) => !file.protected)
631
-
632
- protectedCount = protectedFiles.length
633
-
634
- // 如果可删除的文件数量超过了保留数量,则删除多余的
635
- if (deletableFiles.length > keepCount) {
636
- const filesToDelete = deletableFiles.slice(keepCount) // 保留前N个,删除其余的
637
-
638
- for (const file of filesToDelete) {
639
- try {
640
- if (require('fs').existsSync(file.path)) {
641
- require('fs').unlinkSync(file.path)
642
- cleanedCount++
643
- this.logInfo('清理多余临时图片:', file.path)
644
- }
645
- } catch (fileError) {
646
- this.logger.warn('删除临时文件失败:', { file: file.name, error: fileError })
647
- }
648
- }
649
- }
650
-
651
- if (cleanedCount > 0 || protectedCount > 0) {
652
- this.logInfo(`基于数量清理完成,保留最新 ${keepCount} 张图片,清理了 ${cleanedCount} 张多余图片,保护了 ${protectedCount} 张新上传图片`)
688
+ if (files.length > limit) {
689
+ files.slice(limit).forEach(f => {
690
+ try { require('node:fs').unlinkSync(f.path) } catch { }
691
+ })
653
692
  }
654
- } catch (error) {
655
- this.logger.warn('基于数量清理临时文件失败:', error)
656
693
  }
694
+
695
+ cleanupDir('images', 100) // 图片缓存100个
696
+ cleanupDir('media', 20) // 富媒体缓存20个
657
697
  }
658
698
 
659
699
  private logInfo(...args: any[]) {
package/src/config.ts CHANGED
@@ -5,7 +5,6 @@ export interface Config {
5
5
  clearIndexedDBOnStart: boolean
6
6
  maxMessagesPerChannel: number
7
7
  keepMessagesOnClear: number
8
- keepTempImages: number
9
8
  maxPersistImages: number
10
9
  blockedPlatforms: Array<{
11
10
  platformName: string
@@ -15,10 +14,9 @@ export interface Config {
15
14
 
16
15
  export const Config: Schema<Config> = Schema.intersect([
17
16
  Schema.object({
18
- maxMessagesPerChannel: Schema.number().default(500).description('每个群组最大保存消息数量').min(50).max(1500),
19
- keepMessagesOnClear: Schema.number().default(50).description('手动清理历史记录时保留的消息数量').min(0).max(1000),
20
- keepTempImages: Schema.number().default(50).description('发送消息保留的临时图片数量(最新的N张)').min(10).max(200),
21
- maxPersistImages: Schema.number().default(100).description('持久化存储的机器人发送图片数量').min(10).max(500),
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),
22
20
  blockedPlatforms: Schema.array(Schema.object({
23
21
  platformName: Schema.string().description('平台名称或关键词'),
24
22
  exactMatch: Schema.boolean().default(false).description('完全匹配?如果关闭,包含关键词即屏蔽').default(true)