koishi-plugin-chat-patch 2.1.2 → 2.2.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.
@@ -1,407 +1,413 @@
1
- import { BotInfo, ChannelInfo, MessageInfo, QuoteInfo } from './types'
2
- import { Context, Session, h, Logger } from 'koishi'
3
- import { FileManager } from './file-manager'
4
- import { Config } from './config'
5
- import { Utils } from './utils'
6
- import { } from '@koishijs/plugin-console'
7
-
8
- export class MessageHandler {
9
- private logger: Logger
10
- private utils: Utils
11
- // 存储正确的 channelId 映射,key 是 selfId,value 是正确的 channelId
12
- private correctChannelIds: Map<string, string> = new Map()
13
-
14
- constructor(
15
- private ctx: Context,
16
- private config: Config,
17
- private fileManager: FileManager
18
- ) {
19
- this.logger = ctx.logger('chat-patch')
20
- this.utils = new Utils(config)
21
- }
22
-
23
- // 设置正确的 channelId
24
- setCorrectChannelId(selfId: string, channelId: string) {
25
- this.correctChannelIds.set(selfId, channelId)
26
- this.logInfo('设置正确的 channelId:', { selfId, channelId })
27
- }
28
-
29
- // 获取正确的 channelId
30
- getCorrectChannelId(selfId: string): string | undefined {
31
- return this.correctChannelIds.get(selfId)
32
- }
33
-
34
- // 更新机器人信息到JSON文件
35
- async updateBotInfoToFile(session: Session) {
36
- const data = this.fileManager.readChatDataFromFile()
37
-
38
- const botInfo: BotInfo = {
39
- selfId: session.selfId,
40
- platform: session.platform || 'unknown',
41
- username: session.bot.user?.name || `Bot-${session.selfId}`,
42
- avatar: session.bot.user?.avatar,
43
- status: 'online'
44
- }
45
-
46
- data.bots[session.selfId] = botInfo
47
- this.fileManager.writeChatDataToFile(data)
48
- this.logInfo('更新机器人信息到文件:', botInfo.username)
49
- }
50
-
51
- // 更新频道信息到JSON文件
52
- async updateChannelInfoToFile(session: Session): Promise<string> {
53
- const isDirect = session.isDirect || session.channelId?.includes('private')
54
- let guildName = session.channelId
55
- // 直接使用 session.username 作为私聊用户名
56
- const directUserName = session.username || session.event?.user?.name || session.userId
57
-
58
- const data = this.fileManager.readChatDataFromFile()
59
-
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
66
- }
67
-
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
- }
76
- }
77
- } catch (error) {
78
- this.logInfo('获取频道信息失败,使用频道ID作为备用:', error)
79
- guildName = session.channelId
80
- }
81
- }
82
-
83
- if (!data.channels[session.selfId]) {
84
- data.channels[session.selfId] = {}
85
- }
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 if (session.platform && session.platform.toLowerCase().includes('sandbox')) {
100
- // sandbox 平台直接使用 userId 作为频道名称
101
- finalName = `私聊(${session.userId})`
102
- } else {
103
- finalName = '私聊(未知用户)'
104
- }
105
- } else {
106
- finalName = guildName || session.channelId
107
- }
108
-
109
- const channelInfo: ChannelInfo = {
110
- id: session.channelId,
111
- name: finalName,
112
- type: session.type || 0,
113
- channelId: session.channelId,
114
- guildName: guildName,
115
- isDirect: !!isDirect
116
- }
117
-
118
- // 记录名称变化
119
- if (existingChannel && existingChannel.name !== finalName) {
120
- this.logInfo('更新频道名称:', {
121
- channelId: session.channelId,
122
- oldName: existingChannel.name,
123
- newName: finalName
124
- })
125
- }
126
-
127
- data.channels[session.selfId][session.channelId] = channelInfo
128
- this.fileManager.writeChatDataToFile(data)
129
- this.logInfo('更新频道信息到文件:', channelInfo.name)
130
-
131
- return guildName
132
- }
133
-
134
- // 下载并缓存媒体文件
135
- public async downloadAndCacheMedia(url: string, type: 'image' | 'media' | 'avatar') {
136
- try {
137
- if (!url || url.startsWith('data:') || url.startsWith('file:')) return url
138
-
139
- let folder = 'media'
140
- if (type === 'image') folder = 'images'
141
- else if (type === 'avatar') folder = 'avatars'
142
-
143
- const dir = require('node:path').join(this.ctx.baseDir, 'data', 'chat-patch', 'persist-media', folder)
144
- if (!require('node:fs').existsSync(dir)) {
145
- require('node:fs').mkdirSync(dir, { recursive: true })
146
- }
147
-
148
- // 使用 URL 的 hash 作为文件名,避免重复下载
149
- const crypto = require('node:crypto')
150
- const hash = crypto.createHash('md5').update(url).digest('hex')
151
- const ext = require('node:path').extname(new URL(url).pathname) || (type === 'image' ? '.jpg' : '.mp4')
152
- const filename = `${hash}${ext}`
153
- const filePath = require('node:path').join(dir, filename)
154
-
155
- if (require('node:fs').existsSync(filePath)) {
156
- return require('node:url').pathToFileURL(filePath).href
157
- }
158
-
159
- const buffer = await this.ctx.http.get(url, { responseType: 'arraybuffer' })
160
- require('node:fs').writeFileSync(filePath, Buffer.from(buffer))
161
-
162
- return require('node:url').pathToFileURL(filePath).href
163
- } catch (e) {
164
- return url
165
- }
166
- }
167
-
168
- // 处理消息中的媒体元素并缓存
169
- private async processMediaElements(elements: h[]) {
170
- if (!elements) return elements
171
- for (const el of elements) {
172
- if (['image', 'img', 'mface'].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, 'image')
175
- } else if (['audio', 'video'].includes(el.type)) {
176
- const src = el.attrs.src || el.attrs.url || el.attrs.file
177
- if (src) el.attrs.src = await this.downloadAndCacheMedia(src, 'media')
178
- }
179
- if (el.children) await this.processMediaElements(el.children)
180
- }
181
- return elements
182
- }
183
-
184
- async broadcastMessageEvent(session: Session) {
185
- try {
186
- await this.updateBotInfoToFile(session)
187
- const guildName = await this.updateChannelInfoToFile(session)
188
- const isDirect = session.isDirect || session.channelId?.includes('private')
189
-
190
- const timestamp = Date.now()
191
-
192
- // 处理媒体缓存
193
- if (session.elements) {
194
- await this.processMediaElements(session.elements)
195
- }
196
- if (session.quote?.elements) {
197
- await this.processMediaElements(session.quote.elements)
198
- }
199
-
200
- // 处理 quote 信息
201
- let quoteInfo: QuoteInfo | undefined = undefined
202
- if (session.quote) {
203
- quoteInfo = {
204
- messageId: session.quote.messageId || session.quote.id,
205
- id: session.quote.id,
206
- content: session.quote.content || '',
207
- elements: session.quote.elements,
208
- user: {
209
- id: session.quote.user?.id || session.quote.user?.userId || 'unknown',
210
- name: session.quote.user?.name || session.quote.user?.username || 'unknown',
211
- userId: session.quote.user?.userId || session.quote.user?.id || 'unknown',
212
- avatar: session.quote.user?.avatar,
213
- username: session.quote.user?.username || session.quote.user?.name || 'unknown'
214
- },
215
- timestamp: session.quote.timestamp || Date.now()
216
- }
217
- }
218
-
219
- // session 中提取用户消息内容
220
- let content = ''
221
- let elements: h[] = []
222
-
223
- if (session.content) {
224
- content = session.content
225
- } else if (session.stripped?.content) {
226
- content = session.stripped.content
227
- }
228
-
229
- if (session.elements) {
230
- elements = session.elements
231
- if (!content) {
232
- content = session.elements
233
- .filter((element: any) => element.type === 'text')
234
- .map((element: any) => element.attrs?.content || '')
235
- .join('')
236
- }
237
- }
238
-
239
- // 创建消息信息对象
240
- const messageInfo: MessageInfo = {
241
- id: session.event?.message?.id || `msg-${timestamp}`,
242
- content: content || session.content || '',
243
- userId: session.userId || session.event?.user?.id || 'unknown',
244
- username: session.username || session.event?.user?.name || session.userId || 'unknown',
245
- avatar: session.event?.user?.avatar,
246
- timestamp: timestamp,
247
- channelId: session.channelId,
248
- selfId: session.selfId,
249
- elements: this.utils.cleanBase64Content(elements),
250
- type: 'user',
251
- guildName: guildName,
252
- platform: session.platform || 'unknown',
253
- quote: quoteInfo ? this.utils.cleanBase64Content(quoteInfo) : undefined,
254
- isDirect: !!isDirect
255
- }
256
-
257
- await this.fileManager.addMessageToFile(messageInfo)
258
-
259
- const messageEvent = {
260
- type: 'message',
261
- selfId: session.selfId,
262
- platform: session.platform || 'unknown',
263
- channelId: session.channelId,
264
- messageId: session.event?.message?.id || `msg-${timestamp}`,
265
- content: content || session.content || '',
266
- userId: session.userId || session.event?.user?.id || 'unknown',
267
- username: session.username || session.event?.user?.name || session.userId || 'unknown',
268
- avatar: session.event?.user?.avatar,
269
- timestamp: timestamp,
270
- guildName: guildName,
271
- channelType: session.type || 0,
272
- elements: this.utils.cleanBase64Content(elements),
273
- quote: quoteInfo ? this.utils.cleanBase64Content(quoteInfo) : undefined,
274
- isDirect: session.isDirect,
275
- bot: {
276
- avatar: session.bot.user?.avatar,
277
- name: session.bot.user?.name,
278
- }
279
- }
280
-
281
- this.ctx.console.broadcast('chat-message-event', messageEvent)
282
- } catch (error) {
283
- this.logger.error('广播消息事件失败:', error)
284
- }
285
- }
286
-
287
- // 处理机器人发送的消息
288
- async broadcastBotMessageEvent(session: Session) {
289
- try {
290
- // 获取正确的 channelId
291
- const correctChannelId = this.getCorrectChannelId(session.selfId)
292
-
293
- // 使用正确的 channelId,如果没有则使用 session.channelId
294
- const finalChannelId = correctChannelId || session.channelId
295
-
296
- await this.updateBotInfoToFile(session)
297
- const guildName = await this.updateChannelInfoToFile(session)
298
- const isDirect = session.isDirect || finalChannelId?.includes('private')
299
-
300
- const timestamp = Date.now()
301
-
302
- // 处理媒体缓存
303
- if (session.event?.message?.elements) {
304
- await this.processMediaElements(session.event.message.elements)
305
- }
306
-
307
- // 优先使用 session.content,它包含了完整的消息内容(含标签)
308
- let content = session.content || ''
309
-
310
- if (!content && session.event?.message?.elements) {
311
- content = this.utils.extractTextContent(session.event.message.elements).trim()
312
- }
313
-
314
- // 尝试从 content 中提取 quote id 并构建 quote 对象
315
- let quoteInfo: QuoteInfo | undefined = undefined
316
- const quoteMatch = content.match(/<quote id="([^"]+)"\/>/)
317
- if (quoteMatch) {
318
- const quoteId = quoteMatch[1]
319
- const data = this.fileManager.readChatDataFromFile()
320
- const channelKey = `${session.selfId}:${finalChannelId}`
321
- // 在当前频道的消息历史中查找被引用的消息
322
- const quotedMsg = data.messages[channelKey]?.find(m => m.id === quoteId)
323
-
324
- if (quotedMsg) {
325
- // 如果是虚拟 ID,尝试获取其真实的 messageId
326
- const realId = quotedMsg.id.startsWith('bot-msg-') ? (quotedMsg as any).realId : quotedMsg.id
327
-
328
- quoteInfo = {
329
- messageId: realId || quotedMsg.id,
330
- id: realId || quotedMsg.id,
331
- content: quotedMsg.content,
332
- elements: quotedMsg.elements,
333
- user: {
334
- id: quotedMsg.userId,
335
- name: quotedMsg.username,
336
- userId: quotedMsg.userId,
337
- avatar: quotedMsg.avatar,
338
- username: quotedMsg.username
339
- },
340
- timestamp: quotedMsg.timestamp
341
- }
342
- // 移除 content 中的 quote 标签,避免重复显示
343
- content = content.replace(/<quote id="[^"]+"\/>\s*/, '')
344
-
345
- // 修正 content 中的 quote 标签为真实 ID,以便 bot.sendMessage 能够正确识别
346
- if (realId) {
347
- session.content = session.content.replace(/id="[^"]+"/, `id="${realId}"`)
348
- }
349
- }
350
- }
351
-
352
- // 创建机器人消息信息对象
353
- const messageInfo: MessageInfo = {
354
- id: `bot-msg-${timestamp}`,
355
- content: content,
356
- userId: session.selfId,
357
- username: session.bot.user?.name || `Bot-${session.selfId}`,
358
- avatar: session.bot.user?.avatar,
359
- timestamp: timestamp,
360
- channelId: finalChannelId,
361
- selfId: session.selfId,
362
- elements: this.utils.cleanBase64Content(session.event?.message?.elements),
363
- type: 'bot',
364
- guildName: guildName,
365
- platform: session.platform || 'unknown',
366
- quote: quoteInfo,
367
- isDirect: !!isDirect,
368
- sending: true // 标记为正在发送
369
- }
370
-
371
- await this.fileManager.addMessageToFile(messageInfo)
372
-
373
- const messageEvent = {
374
- type: 'bot-message',
375
- selfId: session.selfId,
376
- platform: session.platform || 'unknown',
377
- channelId: finalChannelId,
378
- messageId: `bot-msg-${timestamp}`,
379
- content: content,
380
- userId: session.selfId,
381
- username: session.bot.user?.name || `Bot-${session.selfId}`,
382
- avatar: session.bot.user?.avatar,
383
- timestamp: timestamp,
384
- guildName: guildName,
385
- channelType: session.event?.channel?.type || session.type || 0,
386
- elements: this.utils.cleanBase64Content(session.event?.message?.elements),
387
- quote: quoteInfo,
388
- isDirect: !!isDirect,
389
- sending: true,
390
- bot: {
391
- avatar: session.bot.user?.avatar,
392
- name: session.bot.user?.name,
393
- }
394
- }
395
-
396
- this.ctx.console.broadcast('chat-bot-message-event', messageEvent)
397
- } catch (error) {
398
- this.logger.error('广播机器人消息事件失败:', error)
399
- }
400
- }
401
-
402
- private logInfo(...args: any[]) {
403
- if (this.config.loggerinfo) {
404
- (this.logger.info as (...args: any[]) => void)(...args)
405
- }
406
- }
407
- }
1
+ import { BotInfo, ChannelInfo, MessageInfo, QuoteInfo } from './types'
2
+ import { Context, Session, h, Logger } from 'koishi'
3
+ import { FileManager } from './file-manager'
4
+ import { Config } from './config'
5
+ import { Utils } from './utils'
6
+ import { } from '@koishijs/plugin-console'
7
+
8
+ export class MessageHandler {
9
+ private logger: Logger
10
+ private utils: Utils
11
+ // 存储正确的 channelId 映射,key 是 selfId,value 是正确的 channelId
12
+ private correctChannelIds: Map<string, string> = new Map()
13
+
14
+ constructor(
15
+ private ctx: Context,
16
+ private config: Config,
17
+ private fileManager: FileManager
18
+ ) {
19
+ this.logger = ctx.logger('chat-patch')
20
+ this.utils = new Utils(config)
21
+ }
22
+
23
+ // 设置正确的 channelId
24
+ setCorrectChannelId(selfId: string, channelId: string) {
25
+ this.correctChannelIds.set(selfId, channelId)
26
+ this.logInfo('设置正确的 channelId:', { selfId, channelId })
27
+ }
28
+
29
+ // 获取正确的 channelId
30
+ getCorrectChannelId(selfId: string): string | undefined {
31
+ return this.correctChannelIds.get(selfId)
32
+ }
33
+
34
+ // 更新机器人信息到JSON文件
35
+ async updateBotInfoToFile(session: Session) {
36
+ const data = this.fileManager.readChatDataFromFile()
37
+
38
+ const botInfo: BotInfo = {
39
+ selfId: session.selfId,
40
+ platform: session.platform || 'unknown',
41
+ username: session.bot.user?.name || `Bot-${session.selfId}`,
42
+ avatar: session.bot.user?.avatar,
43
+ status: 'online'
44
+ }
45
+
46
+ data.bots[session.selfId] = botInfo
47
+ this.fileManager.writeChatDataToFile(data)
48
+ this.logInfo('更新机器人信息到文件:', botInfo.username)
49
+ }
50
+
51
+ // 更新频道信息到JSON文件
52
+ async updateChannelInfoToFile(session: Session): Promise<string> {
53
+ const isDirect = session.isDirect || session.channelId?.includes('private')
54
+ let guildName = session.channelId
55
+ // 直接使用 session.username 作为私聊用户名
56
+ const directUserName = session.username || session.event?.user?.name || session.userId
57
+
58
+ const data = this.fileManager.readChatDataFromFile()
59
+
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
66
+ }
67
+
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
+ }
76
+ }
77
+ } catch (error) {
78
+ this.logInfo('获取频道信息失败,使用频道ID作为备用:', error)
79
+ guildName = session.channelId
80
+ }
81
+ }
82
+
83
+ if (!data.channels[session.selfId]) {
84
+ data.channels[session.selfId] = {}
85
+ }
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 if (session.platform && session.platform.toLowerCase().includes('sandbox')) {
100
+ // sandbox 平台直接使用 userId 作为频道名称
101
+ finalName = `私聊(${session.userId})`
102
+ } else {
103
+ finalName = '私聊(未知用户)'
104
+ }
105
+ } else {
106
+ finalName = guildName || session.channelId
107
+ }
108
+
109
+ const channelInfo: ChannelInfo = {
110
+ id: session.channelId,
111
+ name: finalName,
112
+ type: session.type || 0,
113
+ channelId: session.channelId,
114
+ guildName: guildName,
115
+ isDirect: !!isDirect
116
+ }
117
+
118
+ // 记录名称变化
119
+ if (existingChannel && existingChannel.name !== finalName) {
120
+ this.logInfo('更新频道名称:', {
121
+ channelId: session.channelId,
122
+ oldName: existingChannel.name,
123
+ newName: finalName
124
+ })
125
+ }
126
+
127
+ data.channels[session.selfId][session.channelId] = channelInfo
128
+ this.fileManager.writeChatDataToFile(data)
129
+ this.logInfo('更新频道信息到文件:', channelInfo.name)
130
+
131
+ return guildName
132
+ }
133
+
134
+ // 下载并缓存媒体文件
135
+ public async downloadAndCacheMedia(url: string, type: 'image' | 'media' | 'avatar') {
136
+ try {
137
+ if (!url || url.startsWith('data:') || url.startsWith('file:')) return url
138
+
139
+ let folder = 'media'
140
+ if (type === 'image') folder = 'images'
141
+ else if (type === 'avatar') folder = 'avatars'
142
+
143
+ const dir = require('node:path').join(this.ctx.baseDir, 'data', 'chat-patch', 'persist-media', folder)
144
+ if (!require('node:fs').existsSync(dir)) {
145
+ require('node:fs').mkdirSync(dir, { recursive: true })
146
+ }
147
+
148
+ // 使用 URL 的 hash 作为文件名,避免重复下载
149
+ const crypto = require('node:crypto')
150
+ const hash = crypto.createHash('md5').update(url).digest('hex')
151
+ const ext = require('node:path').extname(new URL(url).pathname) || (type === 'image' ? '.jpg' : '.mp4')
152
+ const filename = `${hash}${ext}`
153
+ const filePath = require('node:path').join(dir, filename)
154
+
155
+ if (require('node:fs').existsSync(filePath)) {
156
+ return require('node:url').pathToFileURL(filePath).href
157
+ }
158
+
159
+ const buffer = await this.ctx.http.get(url, { responseType: 'arraybuffer' })
160
+ require('node:fs').writeFileSync(filePath, Buffer.from(buffer))
161
+
162
+ return require('node:url').pathToFileURL(filePath).href
163
+ } catch (e) {
164
+ return url
165
+ }
166
+ }
167
+
168
+ // 处理消息中的媒体元素并缓存
169
+ // 只缓存用户的图片和语音,不缓存视频和文件
170
+ private async processMediaElements(elements: h[], isUserMessage: boolean = true) {
171
+ if (!elements) return elements
172
+ for (const el of elements) {
173
+ if (['image', 'img', 'mface'].includes(el.type)) {
174
+ const src = el.attrs.src || el.attrs.url || el.attrs.file
175
+ if (src && isUserMessage) {
176
+ // 只缓存用户消息的图片
177
+ el.attrs.src = await this.downloadAndCacheMedia(src, 'image')
178
+ }
179
+ } else if (el.type === 'audio') {
180
+ const src = el.attrs.src || el.attrs.url || el.attrs.file
181
+ if (src && isUserMessage) {
182
+ // 只缓存用户消息的语音
183
+ el.attrs.src = await this.downloadAndCacheMedia(src, 'media')
184
+ }
185
+ }
186
+ // 视频和文件不再缓存,保持原始URL
187
+ if (el.children) await this.processMediaElements(el.children, isUserMessage)
188
+ }
189
+ return elements
190
+ }
191
+
192
+ async broadcastMessageEvent(session: Session) {
193
+ try {
194
+ await this.updateBotInfoToFile(session)
195
+ const guildName = await this.updateChannelInfoToFile(session)
196
+ const isDirect = session.isDirect || session.channelId?.includes('private')
197
+
198
+ const timestamp = Date.now()
199
+
200
+ // 处理媒体缓存 - 用户消息只缓存图片和语音
201
+ if (session.elements) {
202
+ await this.processMediaElements(session.elements, true)
203
+ }
204
+ if (session.quote?.elements) {
205
+ await this.processMediaElements(session.quote.elements, true)
206
+ }
207
+
208
+ // 处理 quote 信息
209
+ let quoteInfo: QuoteInfo | undefined = undefined
210
+ if (session.quote) {
211
+ quoteInfo = {
212
+ messageId: session.quote.messageId || session.quote.id,
213
+ id: session.quote.id,
214
+ content: session.quote.content || '',
215
+ elements: session.quote.elements,
216
+ user: {
217
+ id: session.quote.user?.id || session.quote.user?.userId || 'unknown',
218
+ name: session.quote.user?.name || session.quote.user?.username || 'unknown',
219
+ userId: session.quote.user?.userId || session.quote.user?.id || 'unknown',
220
+ avatar: session.quote.user?.avatar,
221
+ username: session.quote.user?.username || session.quote.user?.name || 'unknown'
222
+ },
223
+ timestamp: session.quote.timestamp || Date.now()
224
+ }
225
+ }
226
+
227
+ // 从 session 中提取用户消息内容
228
+ let content = ''
229
+ let elements: h[] = []
230
+
231
+ if (session.content) {
232
+ content = session.content
233
+ } else if (session.stripped?.content) {
234
+ content = session.stripped.content
235
+ }
236
+
237
+ if (session.elements) {
238
+ elements = session.elements
239
+ if (!content) {
240
+ content = session.elements
241
+ .filter((element: any) => element.type === 'text')
242
+ .map((element: any) => element.attrs?.content || '')
243
+ .join('')
244
+ }
245
+ }
246
+
247
+ // 创建消息信息对象
248
+ const messageInfo: MessageInfo = {
249
+ id: session.event?.message?.id || `msg-${timestamp}`,
250
+ content: content || session.content || '',
251
+ userId: session.userId || session.event?.user?.id || 'unknown',
252
+ username: session.username || session.event?.user?.name || session.userId || 'unknown',
253
+ avatar: session.event?.user?.avatar,
254
+ timestamp: timestamp,
255
+ channelId: session.channelId,
256
+ selfId: session.selfId,
257
+ elements: this.utils.cleanBase64Content(elements, false),
258
+ type: 'user',
259
+ guildName: guildName,
260
+ platform: session.platform || 'unknown',
261
+ quote: quoteInfo ? this.utils.cleanBase64Content(quoteInfo, false) : undefined,
262
+ isDirect: !!isDirect
263
+ }
264
+
265
+ await this.fileManager.addMessageToFile(messageInfo)
266
+
267
+ const messageEvent = {
268
+ type: 'message',
269
+ selfId: session.selfId,
270
+ platform: session.platform || 'unknown',
271
+ channelId: session.channelId,
272
+ messageId: session.event?.message?.id || `msg-${timestamp}`,
273
+ content: content || session.content || '',
274
+ userId: session.userId || session.event?.user?.id || 'unknown',
275
+ username: session.username || session.event?.user?.name || session.userId || 'unknown',
276
+ avatar: session.event?.user?.avatar,
277
+ timestamp: timestamp,
278
+ guildName: guildName,
279
+ channelType: session.type || 0,
280
+ elements: this.utils.cleanBase64Content(elements, false),
281
+ quote: quoteInfo ? this.utils.cleanBase64Content(quoteInfo, false) : undefined,
282
+ isDirect: session.isDirect,
283
+ bot: {
284
+ avatar: session.bot.user?.avatar,
285
+ name: session.bot.user?.name,
286
+ }
287
+ }
288
+
289
+ this.ctx.console.broadcast('chat-message-event', messageEvent)
290
+ } catch (error) {
291
+ this.logger.error('广播消息事件失败:', error)
292
+ }
293
+ }
294
+
295
+ // 处理机器人发送的消息
296
+ async broadcastBotMessageEvent(session: Session) {
297
+ try {
298
+ // 获取正确的 channelId
299
+ const correctChannelId = this.getCorrectChannelId(session.selfId)
300
+
301
+ // 使用正确的 channelId,如果没有则使用 session.channelId
302
+ const finalChannelId = correctChannelId || session.channelId
303
+
304
+ await this.updateBotInfoToFile(session)
305
+ const guildName = await this.updateChannelInfoToFile(session)
306
+ const isDirect = session.isDirect || finalChannelId?.includes('private')
307
+
308
+ const timestamp = Date.now()
309
+
310
+ // 机器人消息不缓存任何媒体
311
+ // 保持原始URL,不进行缓存处理
312
+
313
+ // 优先使用 session.content,它包含了完整的消息内容(含标签)
314
+ let content = session.content || ''
315
+
316
+ if (!content && session.event?.message?.elements) {
317
+ content = this.utils.extractTextContent(session.event.message.elements).trim()
318
+ }
319
+
320
+ // 尝试从 content 中提取 quote id 并构建 quote 对象
321
+ let quoteInfo: QuoteInfo | undefined = undefined
322
+ const quoteMatch = content.match(/<quote id="([^"]+)"\/>/)
323
+ if (quoteMatch) {
324
+ const quoteId = quoteMatch[1]
325
+ const data = this.fileManager.readChatDataFromFile()
326
+ const channelKey = `${session.selfId}:${finalChannelId}`
327
+ // 在当前频道的消息历史中查找被引用的消息
328
+ const quotedMsg = data.messages[channelKey]?.find(m => m.id === quoteId)
329
+
330
+ if (quotedMsg) {
331
+ // 如果是虚拟 ID,尝试获取其真实的 messageId
332
+ const realId = quotedMsg.id.startsWith('bot-msg-') ? (quotedMsg as any).realId : quotedMsg.id
333
+
334
+ quoteInfo = {
335
+ messageId: realId || quotedMsg.id,
336
+ id: realId || quotedMsg.id,
337
+ content: quotedMsg.content,
338
+ elements: quotedMsg.elements,
339
+ user: {
340
+ id: quotedMsg.userId,
341
+ name: quotedMsg.username,
342
+ userId: quotedMsg.userId,
343
+ avatar: quotedMsg.avatar,
344
+ username: quotedMsg.username
345
+ },
346
+ timestamp: quotedMsg.timestamp
347
+ }
348
+ // 移除 content 中的 quote 标签,避免重复显示
349
+ content = content.replace(/<quote id="[^"]+"\/>\s*/, '')
350
+
351
+ // 修正 content 中的 quote 标签为真实 ID,以便 bot.sendMessage 能够正确识别
352
+ if (realId) {
353
+ session.content = session.content.replace(/id="[^"]+"/, `id="${realId}"`)
354
+ }
355
+ }
356
+ }
357
+
358
+ // 创建机器人消息信息对象
359
+ const messageInfo: MessageInfo = {
360
+ id: `bot-msg-${timestamp}`,
361
+ content: content,
362
+ userId: session.selfId,
363
+ username: session.bot.user?.name || `Bot-${session.selfId}`,
364
+ avatar: session.bot.user?.avatar,
365
+ timestamp: timestamp,
366
+ channelId: finalChannelId,
367
+ selfId: session.selfId,
368
+ elements: this.utils.cleanBase64Content(session.event?.message?.elements, true),
369
+ type: 'bot',
370
+ guildName: guildName,
371
+ platform: session.platform || 'unknown',
372
+ quote: quoteInfo,
373
+ isDirect: !!isDirect,
374
+ sending: true // 标记为正在发送
375
+ }
376
+
377
+ await this.fileManager.addMessageToFile(messageInfo)
378
+
379
+ const messageEvent = {
380
+ type: 'bot-message',
381
+ selfId: session.selfId,
382
+ platform: session.platform || 'unknown',
383
+ channelId: finalChannelId,
384
+ messageId: `bot-msg-${timestamp}`,
385
+ content: content,
386
+ userId: session.selfId,
387
+ username: session.bot.user?.name || `Bot-${session.selfId}`,
388
+ avatar: session.bot.user?.avatar,
389
+ timestamp: timestamp,
390
+ guildName: guildName,
391
+ channelType: session.event?.channel?.type || session.type || 0,
392
+ elements: this.utils.cleanBase64Content(session.event?.message?.elements, true),
393
+ quote: quoteInfo,
394
+ isDirect: !!isDirect,
395
+ sending: true,
396
+ bot: {
397
+ avatar: session.bot.user?.avatar,
398
+ name: session.bot.user?.name,
399
+ }
400
+ }
401
+
402
+ this.ctx.console.broadcast('chat-bot-message-event', messageEvent)
403
+ } catch (error) {
404
+ this.logger.error('广播机器人消息事件失败:', error)
405
+ }
406
+ }
407
+
408
+ private logInfo(...args: any[]) {
409
+ if (this.config.loggerinfo) {
410
+ (this.logger.info as (...args: any[]) => void)(...args)
411
+ }
412
+ }
413
+ }