koishi-plugin-chat-patch 2.0.1 → 2.1.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/index.scss +50 -0
- package/client/vue/chat-logic.ts +471 -11
- package/client/vue/composables/useChatData.ts +4 -2
- package/client/vue/index.vue +279 -101
- package/client/vue/types.ts +2 -0
- package/dist/index.js +2 -2
- package/dist/style.css +1 -1
- package/lib/api-handlers.d.ts +19 -0
- package/lib/index.js +226 -132
- package/lib/message-handler.d.ts +21 -0
- package/lib/types.d.ts +57 -0
- package/package.json +1 -1
- package/src/api-handlers.ts +122 -94
- package/src/message-handler.ts +160 -62
- package/src/types.ts +2 -0
package/src/api-handlers.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
52
|
-
|
|
53
|
-
|
|
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) {
|
|
@@ -257,32 +254,31 @@ export class ApiHandlers {
|
|
|
257
254
|
const result = await bot.sendMessage(data.channelId, parsedContent)
|
|
258
255
|
this.logInfo('消息发送成功:', result)
|
|
259
256
|
|
|
260
|
-
// 广播机器人消息发送成功事件,包含真实的 messageId
|
|
261
257
|
const messageId = Array.isArray(result) ? result[0] : result;
|
|
258
|
+
|
|
259
|
+
// 发送成功后,更新本地存储中的虚拟消息为真实 ID
|
|
262
260
|
if (messageId) {
|
|
263
|
-
const
|
|
264
|
-
const
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
this.ctx.console.broadcast('bot-message-sent-event', botMessageEvent);
|
|
261
|
+
const chatData = this.fileManager.readChatDataFromFile()
|
|
262
|
+
const channelKey = `${data.selfId}:${data.channelId}`
|
|
263
|
+
const messages = chatData.messages[channelKey] || []
|
|
264
|
+
// 找到最近的一条正在发送的机器人消息
|
|
265
|
+
const msg = [...messages].reverse().find(m => m.type === 'bot' && m.sending)
|
|
266
|
+
if (msg) {
|
|
267
|
+
msg.realId = messageId;
|
|
268
|
+
msg.sending = false;
|
|
269
|
+
this.fileManager.writeChatDataToFile(chatData)
|
|
270
|
+
|
|
271
|
+
// 广播更新事件给前端
|
|
272
|
+
this.ctx.console.broadcast('bot-message-updated', {
|
|
273
|
+
channelKey,
|
|
274
|
+
tempId: msg.id,
|
|
275
|
+
realId: messageId
|
|
276
|
+
})
|
|
277
|
+
}
|
|
282
278
|
}
|
|
283
279
|
|
|
284
280
|
return {
|
|
285
|
-
success:
|
|
281
|
+
success: !!messageId,
|
|
286
282
|
messageId: messageId,
|
|
287
283
|
tempImageIds: data.images?.map(img => img.tempId) || []
|
|
288
284
|
}
|
|
@@ -515,6 +511,77 @@ export class ApiHandlers {
|
|
|
515
511
|
}
|
|
516
512
|
})
|
|
517
513
|
|
|
514
|
+
// 获取用户信息 API
|
|
515
|
+
this.ctx.console.addListener('get-user-info' as any, async (data: { selfId: string, userId: string, guildId?: string }) => {
|
|
516
|
+
try {
|
|
517
|
+
const bot = this.ctx.bots.find(b => b.selfId === data.selfId)
|
|
518
|
+
if (!bot) return { success: false, error: '机器人不存在' }
|
|
519
|
+
|
|
520
|
+
const user = await bot.getUser(data.userId, data.guildId)
|
|
521
|
+
|
|
522
|
+
if (user) {
|
|
523
|
+
// 缓存头像到本地
|
|
524
|
+
if (user.avatar) {
|
|
525
|
+
user.avatar = await this.messageHandler.downloadAndCacheMedia(user.avatar, 'avatar')
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
const chatData = this.fileManager.readChatDataFromFile()
|
|
529
|
+
let changed = false
|
|
530
|
+
|
|
531
|
+
// 1. 如果是私聊,更新频道名 - 支持多种私聊ID格式
|
|
532
|
+
const botChannels = chatData.channels[data.selfId] || {}
|
|
533
|
+
|
|
534
|
+
// 查找所有可能的私聊频道ID格式
|
|
535
|
+
const possibleChannelIds = [
|
|
536
|
+
data.userId,
|
|
537
|
+
`private:${data.userId}`,
|
|
538
|
+
`direct:${data.userId}`
|
|
539
|
+
]
|
|
540
|
+
|
|
541
|
+
for (const channelId of possibleChannelIds) {
|
|
542
|
+
const channel = botChannels[channelId]
|
|
543
|
+
if (channel && channel.isDirect) {
|
|
544
|
+
const newName = `私聊(${user.name})`
|
|
545
|
+
if (channel.name !== newName) {
|
|
546
|
+
channel.name = newName
|
|
547
|
+
changed = true
|
|
548
|
+
this.logInfo('更新私聊频道名称:', { channelId, oldName: channel.name, newName })
|
|
549
|
+
}
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// 2. 更新该用户在所有历史消息中的头像和昵称(持久化缓存)
|
|
554
|
+
const channelKeyPrefix = `${data.selfId}:`
|
|
555
|
+
for (const [key, messages] of Object.entries(chatData.messages)) {
|
|
556
|
+
if (key.startsWith(channelKeyPrefix)) {
|
|
557
|
+
messages.forEach(msg => {
|
|
558
|
+
if (msg.userId === data.userId) {
|
|
559
|
+
if (user.name && msg.username !== user.name) {
|
|
560
|
+
msg.username = user.name
|
|
561
|
+
changed = true
|
|
562
|
+
}
|
|
563
|
+
if (user.avatar && msg.avatar !== user.avatar) {
|
|
564
|
+
msg.avatar = user.avatar
|
|
565
|
+
changed = true
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
if (changed) {
|
|
573
|
+
this.fileManager.writeChatDataToFile(chatData)
|
|
574
|
+
// 广播更新事件,让前端实时刷新
|
|
575
|
+
this.ctx.console.broadcast('chat-data-updated', {})
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
return { success: true, data: user }
|
|
580
|
+
} catch (error: any) {
|
|
581
|
+
return { success: false, error: error?.message || '获取用户信息失败' }
|
|
582
|
+
}
|
|
583
|
+
})
|
|
584
|
+
|
|
518
585
|
// 调试API:获取原始文件数据
|
|
519
586
|
this.ctx.console.addListener('debug-get-raw-data' as any, async () => {
|
|
520
587
|
try {
|
|
@@ -585,75 +652,36 @@ export class ApiHandlers {
|
|
|
585
652
|
private setupTempFileCleanup() {
|
|
586
653
|
// 每5分钟执行一次基于数量的清理
|
|
587
654
|
setInterval(() => {
|
|
588
|
-
this.
|
|
589
|
-
}, 5 * 60 * 1000)
|
|
655
|
+
this.cleanupMediaCache()
|
|
656
|
+
}, 5 * 60 * 1000)
|
|
590
657
|
}
|
|
591
658
|
|
|
592
|
-
//
|
|
593
|
-
private async
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
if (!require('fs').existsSync(tempDir)) {
|
|
597
|
-
return
|
|
598
|
-
}
|
|
659
|
+
// 统一清理媒体缓存
|
|
660
|
+
private async cleanupMediaCache() {
|
|
661
|
+
const baseDir = this.ctx.baseDir + '/data/chat-patch/persist-media'
|
|
662
|
+
if (!require('node:fs').existsSync(baseDir)) return
|
|
599
663
|
|
|
600
|
-
|
|
601
|
-
const
|
|
664
|
+
const cleanupDir = (dirName: string, limit: number) => {
|
|
665
|
+
const dirPath = require('node:path').join(baseDir, dirName)
|
|
666
|
+
if (!require('node:fs').existsSync(dirPath)) return
|
|
602
667
|
|
|
603
|
-
const files = require('fs').readdirSync(
|
|
604
|
-
.
|
|
605
|
-
|
|
606
|
-
const
|
|
607
|
-
|
|
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
|
-
}
|
|
668
|
+
const files = require('node:fs').readdirSync(dirPath)
|
|
669
|
+
.map(file => {
|
|
670
|
+
const filePath = require('node:path').join(dirPath, file)
|
|
671
|
+
const stats = require('node:fs').statSync(filePath)
|
|
672
|
+
return { name: file, path: filePath, mtime: stats.mtimeMs }
|
|
620
673
|
})
|
|
621
|
-
.
|
|
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
|
|
627
|
-
|
|
628
|
-
// 分离受保护的文件和可删除的文件
|
|
629
|
-
const protectedFiles = files.filter((file: any) => file.protected)
|
|
630
|
-
const deletableFiles = files.filter((file: any) => !file.protected)
|
|
674
|
+
.sort((a, b) => b.mtime - a.mtime)
|
|
631
675
|
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
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} 张新上传图片`)
|
|
676
|
+
if (files.length > limit) {
|
|
677
|
+
files.slice(limit).forEach(f => {
|
|
678
|
+
try { require('node:fs').unlinkSync(f.path) } catch { }
|
|
679
|
+
})
|
|
653
680
|
}
|
|
654
|
-
} catch (error) {
|
|
655
|
-
this.logger.warn('基于数量清理临时文件失败:', error)
|
|
656
681
|
}
|
|
682
|
+
|
|
683
|
+
cleanupDir('images', 100) // 图片缓存100个
|
|
684
|
+
cleanupDir('media', 20) // 富媒体缓存20个
|
|
657
685
|
}
|
|
658
686
|
|
|
659
687
|
private logInfo(...args: any[]) {
|
package/src/message-handler.ts
CHANGED
|
@@ -50,55 +50,75 @@ export class MessageHandler {
|
|
|
50
50
|
|
|
51
51
|
// 更新频道信息到JSON文件
|
|
52
52
|
async updateChannelInfoToFile(session: Session): Promise<string> {
|
|
53
|
+
const isDirect = session.isDirect || session.channelId?.includes('private')
|
|
53
54
|
let guildName = session.channelId
|
|
54
|
-
|
|
55
|
+
// 直接使用 session.username 作为私聊用户名
|
|
56
|
+
const directUserName = session.username || session.event?.user?.name || session.userId
|
|
55
57
|
|
|
56
58
|
const data = this.fileManager.readChatDataFromFile()
|
|
57
59
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
// 获取私聊用户昵称
|
|
66
|
-
if (session.userId && session.isDirect && session.bot.getUser && typeof session.bot.getUser === 'function') {
|
|
67
|
-
try {
|
|
68
|
-
const user = await session.bot.getUser(session.userId)
|
|
69
|
-
directUserName = user?.name || session.username || session.userId || '未知用户'
|
|
70
|
-
} catch (userError) {
|
|
71
|
-
this.logInfo('获取用户信息失败,使用备用名称:', userError)
|
|
60
|
+
if (!isDirect) {
|
|
61
|
+
try {
|
|
62
|
+
// 获取群组名称
|
|
63
|
+
if (session.guildId && session.bot.getGuild && typeof session.bot.getGuild === 'function') {
|
|
64
|
+
const guild = await session.bot.getGuild(session.guildId)
|
|
65
|
+
guildName = guild?.name || session.channelId
|
|
72
66
|
}
|
|
73
|
-
}
|
|
74
67
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
68
|
+
// 如果没有getGuild方法,尝试使用getChannel方法
|
|
69
|
+
if (session.guildId && !session.bot.getGuild && session.bot.getChannel && typeof session.bot.getChannel === 'function') {
|
|
70
|
+
try {
|
|
71
|
+
const channel = await session.bot.getChannel(session.guildId)
|
|
72
|
+
guildName = channel?.name || session.channelId
|
|
73
|
+
} catch (channelError) {
|
|
74
|
+
this.logInfo('获取频道信息失败,使用频道ID作为备用:', channelError)
|
|
75
|
+
}
|
|
82
76
|
}
|
|
77
|
+
} catch (error) {
|
|
78
|
+
this.logInfo('获取频道信息失败,使用频道ID作为备用:', error)
|
|
79
|
+
guildName = session.channelId
|
|
83
80
|
}
|
|
84
|
-
} catch (error) {
|
|
85
|
-
this.logInfo('获取频道信息失败,使用频道ID作为备用:', error)
|
|
86
|
-
guildName = session.channelId
|
|
87
81
|
}
|
|
88
82
|
|
|
89
83
|
if (!data.channels[session.selfId]) {
|
|
90
84
|
data.channels[session.selfId] = {}
|
|
91
85
|
}
|
|
92
86
|
|
|
87
|
+
// 获取现有频道信息
|
|
88
|
+
const existingChannel = data.channels[session.selfId][session.channelId]
|
|
89
|
+
|
|
90
|
+
// 构造频道名称
|
|
91
|
+
let finalName: string
|
|
92
|
+
if (isDirect) {
|
|
93
|
+
// 私聊频道:优先使用真实用户名
|
|
94
|
+
if (directUserName && directUserName !== session.userId) {
|
|
95
|
+
finalName = `私聊(${directUserName})`
|
|
96
|
+
} else if (existingChannel?.name && !existingChannel.name.includes('未知')) {
|
|
97
|
+
// 如果已有名称且不是"未知用户",保持原名称
|
|
98
|
+
finalName = existingChannel.name
|
|
99
|
+
} else {
|
|
100
|
+
finalName = '私聊(未知用户)'
|
|
101
|
+
}
|
|
102
|
+
} else {
|
|
103
|
+
finalName = guildName || session.channelId
|
|
104
|
+
}
|
|
105
|
+
|
|
93
106
|
const channelInfo: ChannelInfo = {
|
|
94
107
|
id: session.channelId,
|
|
95
|
-
name:
|
|
96
|
-
? `私聊(${session.username || session.userId || directUserName})`
|
|
97
|
-
: `${guildName} (${session.channelId})`,
|
|
108
|
+
name: finalName,
|
|
98
109
|
type: session.type || 0,
|
|
99
110
|
channelId: session.channelId,
|
|
100
111
|
guildName: guildName,
|
|
101
|
-
isDirect:
|
|
112
|
+
isDirect: !!isDirect
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// 记录名称变化
|
|
116
|
+
if (existingChannel && existingChannel.name !== finalName) {
|
|
117
|
+
this.logInfo('更新频道名称:', {
|
|
118
|
+
channelId: session.channelId,
|
|
119
|
+
oldName: existingChannel.name,
|
|
120
|
+
newName: finalName
|
|
121
|
+
})
|
|
102
122
|
}
|
|
103
123
|
|
|
104
124
|
data.channels[session.selfId][session.channelId] = channelInfo
|
|
@@ -108,13 +128,72 @@ export class MessageHandler {
|
|
|
108
128
|
return guildName
|
|
109
129
|
}
|
|
110
130
|
|
|
131
|
+
// 下载并缓存媒体文件
|
|
132
|
+
public async downloadAndCacheMedia(url: string, type: 'image' | 'media' | 'avatar') {
|
|
133
|
+
try {
|
|
134
|
+
if (!url || url.startsWith('data:') || url.startsWith('file:')) return url
|
|
135
|
+
|
|
136
|
+
let folder = 'media'
|
|
137
|
+
if (type === 'image') folder = 'images'
|
|
138
|
+
else if (type === 'avatar') folder = 'avatars'
|
|
139
|
+
|
|
140
|
+
const dir = require('node:path').join(this.ctx.baseDir, 'data', 'chat-patch', 'persist-media', folder)
|
|
141
|
+
if (!require('node:fs').existsSync(dir)) {
|
|
142
|
+
require('node:fs').mkdirSync(dir, { recursive: true })
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// 使用 URL 的 hash 作为文件名,避免重复下载
|
|
146
|
+
const crypto = require('node:crypto')
|
|
147
|
+
const hash = crypto.createHash('md5').update(url).digest('hex')
|
|
148
|
+
const ext = require('node:path').extname(new URL(url).pathname) || (type === 'image' ? '.jpg' : '.mp4')
|
|
149
|
+
const filename = `${hash}${ext}`
|
|
150
|
+
const filePath = require('node:path').join(dir, filename)
|
|
151
|
+
|
|
152
|
+
if (require('node:fs').existsSync(filePath)) {
|
|
153
|
+
return require('node:url').pathToFileURL(filePath).href
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const buffer = await this.ctx.http.get(url, { responseType: 'arraybuffer' })
|
|
157
|
+
require('node:fs').writeFileSync(filePath, Buffer.from(buffer))
|
|
158
|
+
|
|
159
|
+
return require('node:url').pathToFileURL(filePath).href
|
|
160
|
+
} catch (e) {
|
|
161
|
+
return url
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// 处理消息中的媒体元素并缓存
|
|
166
|
+
private async processMediaElements(elements: h[]) {
|
|
167
|
+
if (!elements) return elements
|
|
168
|
+
for (const el of elements) {
|
|
169
|
+
if (['image', 'img', 'mface'].includes(el.type)) {
|
|
170
|
+
const src = el.attrs.src || el.attrs.url || el.attrs.file
|
|
171
|
+
if (src) el.attrs.src = await this.downloadAndCacheMedia(src, 'image')
|
|
172
|
+
} else if (['audio', 'video'].includes(el.type)) {
|
|
173
|
+
const src = el.attrs.src || el.attrs.url || el.attrs.file
|
|
174
|
+
if (src) el.attrs.src = await this.downloadAndCacheMedia(src, 'media')
|
|
175
|
+
}
|
|
176
|
+
if (el.children) await this.processMediaElements(el.children)
|
|
177
|
+
}
|
|
178
|
+
return elements
|
|
179
|
+
}
|
|
180
|
+
|
|
111
181
|
async broadcastMessageEvent(session: Session) {
|
|
112
182
|
try {
|
|
113
183
|
await this.updateBotInfoToFile(session)
|
|
114
184
|
const guildName = await this.updateChannelInfoToFile(session)
|
|
185
|
+
const isDirect = session.isDirect || session.channelId?.includes('private')
|
|
115
186
|
|
|
116
187
|
const timestamp = Date.now()
|
|
117
188
|
|
|
189
|
+
// 处理媒体缓存
|
|
190
|
+
if (session.elements) {
|
|
191
|
+
await this.processMediaElements(session.elements)
|
|
192
|
+
}
|
|
193
|
+
if (session.quote?.elements) {
|
|
194
|
+
await this.processMediaElements(session.quote.elements)
|
|
195
|
+
}
|
|
196
|
+
|
|
118
197
|
// 处理 quote 信息
|
|
119
198
|
let quoteInfo: QuoteInfo | undefined = undefined
|
|
120
199
|
if (session.quote) {
|
|
@@ -169,7 +248,7 @@ export class MessageHandler {
|
|
|
169
248
|
guildName: guildName,
|
|
170
249
|
platform: session.platform || 'unknown',
|
|
171
250
|
quote: quoteInfo ? this.utils.cleanBase64Content(quoteInfo) : undefined,
|
|
172
|
-
isDirect:
|
|
251
|
+
isDirect: !!isDirect
|
|
173
252
|
}
|
|
174
253
|
|
|
175
254
|
await this.fileManager.addMessageToFile(messageInfo)
|
|
@@ -208,24 +287,20 @@ export class MessageHandler {
|
|
|
208
287
|
// 获取正确的 channelId
|
|
209
288
|
const correctChannelId = this.getCorrectChannelId(session.selfId)
|
|
210
289
|
|
|
211
|
-
// 调试日志:检查 channelId 的来源
|
|
212
|
-
this.logInfo('机器人发送消息调试信息:', {
|
|
213
|
-
'session.channelId': session.channelId,
|
|
214
|
-
'session.event?.channel?.id': session.event?.channel?.id,
|
|
215
|
-
'correctChannelId': correctChannelId,
|
|
216
|
-
'session.guildId': session.guildId,
|
|
217
|
-
'session.isDirect': session.isDirect,
|
|
218
|
-
'session.platform': session.platform
|
|
219
|
-
})
|
|
220
|
-
|
|
221
290
|
// 使用正确的 channelId,如果没有则使用 session.channelId
|
|
222
291
|
const finalChannelId = correctChannelId || session.channelId
|
|
223
292
|
|
|
224
293
|
await this.updateBotInfoToFile(session)
|
|
225
294
|
const guildName = await this.updateChannelInfoToFile(session)
|
|
295
|
+
const isDirect = session.isDirect || finalChannelId?.includes('private')
|
|
226
296
|
|
|
227
297
|
const timestamp = Date.now()
|
|
228
298
|
|
|
299
|
+
// 处理媒体缓存
|
|
300
|
+
if (session.event?.message?.elements) {
|
|
301
|
+
await this.processMediaElements(session.event.message.elements)
|
|
302
|
+
}
|
|
303
|
+
|
|
229
304
|
// 优先使用 session.content,它包含了完整的消息内容(含标签)
|
|
230
305
|
let content = session.content || ''
|
|
231
306
|
|
|
@@ -233,6 +308,44 @@ export class MessageHandler {
|
|
|
233
308
|
content = this.utils.extractTextContent(session.event.message.elements).trim()
|
|
234
309
|
}
|
|
235
310
|
|
|
311
|
+
// 尝试从 content 中提取 quote id 并构建 quote 对象
|
|
312
|
+
let quoteInfo: QuoteInfo | undefined = undefined
|
|
313
|
+
const quoteMatch = content.match(/<quote id="([^"]+)"\/>/)
|
|
314
|
+
if (quoteMatch) {
|
|
315
|
+
const quoteId = quoteMatch[1]
|
|
316
|
+
const data = this.fileManager.readChatDataFromFile()
|
|
317
|
+
const channelKey = `${session.selfId}:${finalChannelId}`
|
|
318
|
+
// 在当前频道的消息历史中查找被引用的消息
|
|
319
|
+
const quotedMsg = data.messages[channelKey]?.find(m => m.id === quoteId)
|
|
320
|
+
|
|
321
|
+
if (quotedMsg) {
|
|
322
|
+
// 如果是虚拟 ID,尝试获取其真实的 messageId
|
|
323
|
+
const realId = quotedMsg.id.startsWith('bot-msg-') ? (quotedMsg as any).realId : quotedMsg.id
|
|
324
|
+
|
|
325
|
+
quoteInfo = {
|
|
326
|
+
messageId: realId || quotedMsg.id,
|
|
327
|
+
id: realId || quotedMsg.id,
|
|
328
|
+
content: quotedMsg.content,
|
|
329
|
+
elements: quotedMsg.elements,
|
|
330
|
+
user: {
|
|
331
|
+
id: quotedMsg.userId,
|
|
332
|
+
name: quotedMsg.username,
|
|
333
|
+
userId: quotedMsg.userId,
|
|
334
|
+
avatar: quotedMsg.avatar,
|
|
335
|
+
username: quotedMsg.username
|
|
336
|
+
},
|
|
337
|
+
timestamp: quotedMsg.timestamp
|
|
338
|
+
}
|
|
339
|
+
// 移除 content 中的 quote 标签,避免重复显示
|
|
340
|
+
content = content.replace(/<quote id="[^"]+"\/>\s*/, '')
|
|
341
|
+
|
|
342
|
+
// 修正 content 中的 quote 标签为真实 ID,以便 bot.sendMessage 能够正确识别
|
|
343
|
+
if (realId) {
|
|
344
|
+
session.content = session.content.replace(/id="[^"]+"/, `id="${realId}"`)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
236
349
|
// 创建机器人消息信息对象
|
|
237
350
|
const messageInfo: MessageInfo = {
|
|
238
351
|
id: `bot-msg-${timestamp}`,
|
|
@@ -247,7 +360,9 @@ export class MessageHandler {
|
|
|
247
360
|
type: 'bot',
|
|
248
361
|
guildName: guildName,
|
|
249
362
|
platform: session.platform || 'unknown',
|
|
250
|
-
|
|
363
|
+
quote: quoteInfo,
|
|
364
|
+
isDirect: !!isDirect,
|
|
365
|
+
sending: true // 标记为正在发送
|
|
251
366
|
}
|
|
252
367
|
|
|
253
368
|
await this.fileManager.addMessageToFile(messageInfo)
|
|
@@ -266,32 +381,15 @@ export class MessageHandler {
|
|
|
266
381
|
guildName: guildName,
|
|
267
382
|
channelType: session.event?.channel?.type || session.type || 0,
|
|
268
383
|
elements: this.utils.cleanBase64Content(session.event?.message?.elements),
|
|
269
|
-
|
|
384
|
+
quote: quoteInfo,
|
|
385
|
+
isDirect: !!isDirect,
|
|
386
|
+
sending: true,
|
|
270
387
|
bot: {
|
|
271
388
|
avatar: session.bot.user?.avatar,
|
|
272
389
|
name: session.bot.user?.name,
|
|
273
390
|
}
|
|
274
391
|
}
|
|
275
392
|
|
|
276
|
-
// 检查是否包含图片元素
|
|
277
|
-
const imageElements = session.event?.message?.elements?.filter((element: any) =>
|
|
278
|
-
element.type === 'img' || element.type === 'image' || element.type === 'mface'
|
|
279
|
-
) || []
|
|
280
|
-
|
|
281
|
-
this.logInfo('机器人发送消息 (before-send):', {
|
|
282
|
-
selfId: session.selfId,
|
|
283
|
-
channelId: messageEvent.channelId,
|
|
284
|
-
originalChannelId: session.channelId,
|
|
285
|
-
correctedChannelId: finalChannelId,
|
|
286
|
-
content: content,
|
|
287
|
-
platform: session.platform,
|
|
288
|
-
imageCount: imageElements.length,
|
|
289
|
-
imageUrls: imageElements.map((el: any) => el.attrs?.src || el.attrs?.url || el.attrs?.file)
|
|
290
|
-
})
|
|
291
|
-
|
|
292
|
-
// 清理已使用的 channelId 映射
|
|
293
|
-
this.correctChannelIds.delete(session.selfId)
|
|
294
|
-
|
|
295
393
|
this.ctx.console.broadcast('chat-bot-message-event', messageEvent)
|
|
296
394
|
} catch (error) {
|
|
297
395
|
this.logger.error('广播机器人消息事件失败:', error)
|