koishi-plugin-chat-patch 2.1.2 → 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.
@@ -1,407 +1,440 @@
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
+
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
+ recordUserMessage(session: Session, timestamp: number) {
24
+
25
+ setImmediate(() => {
26
+ this.processUserMessage(session, timestamp).catch(error => {
27
+ this.logger.error('记录用户消息失败:', error)
28
+ })
29
+ })
30
+ }
31
+
32
+ recordBotMessage(session: Session, timestamp: number) {
33
+
34
+ setImmediate(() => {
35
+ this.processBotMessage(session, timestamp).catch(error => {
36
+ this.logger.error('记录机器人消息失败:', error)
37
+ })
38
+ })
39
+ }
40
+
41
+ setCorrectChannelId(selfId: string, channelId: string) {
42
+ this.correctChannelIds.set(selfId, channelId)
43
+ this.logInfo('设置正确的 channelId:', { selfId, channelId })
44
+ }
45
+
46
+ getCorrectChannelId(selfId: string): string | undefined {
47
+ return this.correctChannelIds.get(selfId)
48
+ }
49
+
50
+ updateBotInfoToFile(session: Session) {
51
+
52
+ setImmediate(() => {
53
+ try {
54
+ const data = this.fileManager.readChatDataFromFile()
55
+
56
+ const botInfo: BotInfo = {
57
+ selfId: session.selfId,
58
+ platform: session.platform || 'unknown',
59
+ username: session.bot.user?.name || `Bot-${session.selfId}`,
60
+ avatar: session.bot.user?.avatar,
61
+ status: 'online'
62
+ }
63
+
64
+ data.bots[session.selfId] = botInfo
65
+ this.fileManager.writeChatDataToFile(data)
66
+ this.logInfo('更新机器人信息到文件:', botInfo.username)
67
+ } catch (error) {
68
+ this.logger.error('更新机器人信息失败:', error)
69
+ }
70
+ })
71
+ }
72
+
73
+ updateChannelInfoToFile(session: Session): string {
74
+ const isDirect = session.isDirect || session.channelId?.includes('private')
75
+ const directUserName = session.username || session.event?.user?.name || session.userId
76
+
77
+ const data = this.fileManager.readChatDataFromFile()
78
+ const existingChannel = data.channels[session.selfId]?.[session.channelId]
79
+
80
+ let immediateName = session.channelId
81
+ if (isDirect) {
82
+ if (directUserName && directUserName !== session.userId) {
83
+ immediateName = `私聊(${directUserName})`
84
+ } else if (existingChannel?.name && !existingChannel.name.includes('未知')) {
85
+ immediateName = existingChannel.name
86
+ } else if (session.platform && session.platform.toLowerCase().includes('sandbox')) {
87
+ immediateName = `私聊(${session.userId})`
88
+ } else {
89
+ immediateName = '私聊(未知用户)'
90
+ }
91
+ } else if (existingChannel?.guildName) {
92
+ immediateName = existingChannel.guildName
93
+ }
94
+
95
+ setImmediate(async () => {
96
+ try {
97
+ let guildName = session.channelId
98
+
99
+ if (!isDirect) {
100
+ try {
101
+
102
+ if (session.guildId && session.bot.getGuild && typeof session.bot.getGuild === 'function') {
103
+ const guild = await session.bot.getGuild(session.guildId)
104
+ guildName = guild?.name || session.channelId
105
+ } else if (session.guildId && !session.bot.getGuild && session.bot.getChannel && typeof session.bot.getChannel === 'function') {
106
+ try {
107
+ const channel = await session.bot.getChannel(session.guildId)
108
+ guildName = channel?.name || session.channelId
109
+ } catch (channelError) {
110
+ this.logInfo('获取频道信息失败,使用频道ID作为备用:', channelError)
111
+ }
112
+ }
113
+ } catch (error) {
114
+ this.logInfo('获取频道信息失败,使用频道ID作为备用:', error)
115
+ guildName = session.channelId
116
+ }
117
+ }
118
+
119
+ const freshData = this.fileManager.readChatDataFromFile()
120
+ if (!freshData.channels[session.selfId]) {
121
+ freshData.channels[session.selfId] = {}
122
+ }
123
+
124
+ const existingChannel = freshData.channels[session.selfId][session.channelId]
125
+
126
+ let finalName: string
127
+ if (isDirect) {
128
+ if (directUserName && directUserName !== session.userId) {
129
+ finalName = `私聊(${directUserName})`
130
+ } else if (existingChannel?.name && !existingChannel.name.includes('未知')) {
131
+ finalName = existingChannel.name
132
+ } else if (session.platform && session.platform.toLowerCase().includes('sandbox')) {
133
+ finalName = `私聊(${session.userId})`
134
+ } else {
135
+ finalName = '私聊(未知用户)'
136
+ }
137
+ } else {
138
+ finalName = guildName || session.channelId
139
+ }
140
+
141
+ const channelInfo: ChannelInfo = {
142
+ id: session.channelId,
143
+ name: finalName,
144
+ type: session.type || 0,
145
+ channelId: session.channelId,
146
+ guildName: guildName,
147
+ isDirect: !!isDirect
148
+ }
149
+
150
+ if (existingChannel && existingChannel.name !== finalName) {
151
+ this.logInfo('更新频道名称:', {
152
+ channelId: session.channelId,
153
+ oldName: existingChannel.name,
154
+ newName: finalName
155
+ })
156
+ }
157
+
158
+ freshData.channels[session.selfId][session.channelId] = channelInfo
159
+ this.fileManager.writeChatDataToFile(freshData)
160
+ this.logInfo('更新频道信息到文件:', channelInfo.name)
161
+ } catch (error) {
162
+ this.logger.error('异步更新频道信息失败:', error)
163
+ }
164
+ })
165
+
166
+ return immediateName
167
+ }
168
+
169
+ public async downloadAndCacheMedia(url: string, type: 'image' | 'media' | 'avatar') {
170
+ try {
171
+ if (!url || url.startsWith('data:') || url.startsWith('file:')) return url
172
+
173
+ let folder = 'media'
174
+ if (type === 'image') folder = 'images'
175
+ else if (type === 'avatar') folder = 'avatars'
176
+
177
+ const dir = require('node:path').join(this.ctx.baseDir, 'data', 'chat-patch', 'persist-media', folder)
178
+ if (!require('node:fs').existsSync(dir)) {
179
+ require('node:fs').mkdirSync(dir, { recursive: true })
180
+ }
181
+
182
+ const crypto = require('node:crypto')
183
+ const hash = crypto.createHash('md5').update(url).digest('hex')
184
+ const ext = require('node:path').extname(new URL(url).pathname) || (type === 'image' ? '.jpg' : '.mp4')
185
+ const filename = `${hash}${ext}`
186
+ const filePath = require('node:path').join(dir, filename)
187
+
188
+ if (require('node:fs').existsSync(filePath)) {
189
+ return require('node:url').pathToFileURL(filePath).href
190
+ }
191
+
192
+ const buffer = await this.ctx.http.get(url, { responseType: 'arraybuffer' })
193
+ require('node:fs').writeFileSync(filePath, Buffer.from(buffer))
194
+
195
+ return require('node:url').pathToFileURL(filePath).href
196
+ } catch (e) {
197
+ return url
198
+ }
199
+ }
200
+
201
+ private processMediaElementsAsync(elements: h[], isUserMessage: boolean = true) {
202
+ if (!elements) return
203
+
204
+ setImmediate(async () => {
205
+ try {
206
+ for (const el of elements) {
207
+ if (['image', 'img', 'mface'].includes(el.type)) {
208
+ const src = el.attrs.src || el.attrs.url || el.attrs.file
209
+ if (src && isUserMessage) {
210
+
211
+ this.downloadAndCacheMedia(src, 'image').catch(e => {
212
+ this.logger.warn('缓存图片失败:', e)
213
+ })
214
+ }
215
+ } else if (el.type === 'audio') {
216
+ const src = el.attrs.src || el.attrs.url || el.attrs.file
217
+ if (src && isUserMessage) {
218
+
219
+ this.downloadAndCacheMedia(src, 'media').catch(e => {
220
+ this.logger.warn('缓存语音失败:', e)
221
+ })
222
+ }
223
+ }
224
+
225
+ if (el.children) this.processMediaElementsAsync(el.children, isUserMessage)
226
+ }
227
+ } catch (error) {
228
+ this.logger.error('处理媒体元素失败:', error)
229
+ }
230
+ })
231
+ }
232
+
233
+ private async processUserMessage(session: Session, timestamp?: number) {
234
+ try {
235
+ if (!timestamp) timestamp = Date.now()
236
+
237
+ this.updateBotInfoToFile(session)
238
+
239
+ const guildName = this.updateChannelInfoToFile(session)
240
+ const isDirect = session.isDirect || session.channelId?.includes('private')
241
+
242
+ if (session.elements) {
243
+ this.processMediaElementsAsync(session.elements, true)
244
+ }
245
+ if (session.quote?.elements) {
246
+ this.processMediaElementsAsync(session.quote.elements, true)
247
+ }
248
+
249
+ let quoteInfo: QuoteInfo | undefined = undefined
250
+ if (session.quote) {
251
+ quoteInfo = {
252
+ messageId: session.quote.messageId || session.quote.id,
253
+ id: session.quote.id,
254
+ content: session.quote.content || '',
255
+ elements: session.quote.elements,
256
+ user: {
257
+ id: session.quote.user?.id || session.quote.user?.userId || 'unknown',
258
+ name: session.quote.user?.name || session.quote.user?.username || 'unknown',
259
+ userId: session.quote.user?.userId || session.quote.user?.id || 'unknown',
260
+ avatar: session.quote.user?.avatar,
261
+ username: session.quote.user?.username || session.quote.user?.name || 'unknown'
262
+ },
263
+ timestamp: session.quote.timestamp || Date.now()
264
+ }
265
+ }
266
+
267
+ let content = ''
268
+ let elements: h[] = []
269
+
270
+ if (session.content) {
271
+ content = session.content
272
+ } else if (session.stripped?.content) {
273
+ content = session.stripped.content
274
+ }
275
+
276
+ if (session.elements) {
277
+ elements = session.elements
278
+ if (!content) {
279
+ content = session.elements
280
+ .filter((element: any) => element.type === 'text')
281
+ .map((element: any) => element.attrs?.content || '')
282
+ .join('')
283
+ }
284
+ }
285
+
286
+ const messageInfo: MessageInfo = {
287
+ id: session.event?.message?.id || `msg-${timestamp}`,
288
+ content: content || session.content || '',
289
+ userId: session.userId || session.event?.user?.id || 'unknown',
290
+ username: session.username || session.event?.user?.name || session.userId || 'unknown',
291
+ avatar: session.event?.user?.avatar,
292
+ timestamp: timestamp,
293
+ channelId: session.channelId,
294
+ selfId: session.selfId,
295
+ elements: this.utils.cleanBase64Content(elements, false),
296
+ type: 'user',
297
+ guildName: guildName,
298
+ platform: session.platform || 'unknown',
299
+ quote: quoteInfo ? this.utils.cleanBase64Content(quoteInfo, false) : undefined,
300
+ isDirect: !!isDirect
301
+ }
302
+
303
+ await this.fileManager.addMessageToFile(messageInfo)
304
+
305
+ const messageEvent = {
306
+ type: 'message',
307
+ selfId: session.selfId,
308
+ platform: session.platform || 'unknown',
309
+ channelId: session.channelId,
310
+ messageId: session.event?.message?.id || `msg-${timestamp}`,
311
+ content: content || session.content || '',
312
+ userId: session.userId || session.event?.user?.id || 'unknown',
313
+ username: session.username || session.event?.user?.name || session.userId || 'unknown',
314
+ avatar: session.event?.user?.avatar,
315
+ timestamp: timestamp,
316
+ guildName: guildName,
317
+ channelType: session.type || 0,
318
+ elements: this.utils.cleanBase64Content(elements, false),
319
+ quote: quoteInfo ? this.utils.cleanBase64Content(quoteInfo, false) : undefined,
320
+ isDirect: session.isDirect,
321
+ bot: {
322
+ avatar: session.bot.user?.avatar,
323
+ name: session.bot.user?.name,
324
+ }
325
+ }
326
+
327
+ this.ctx.console.broadcast('chat-message-event', messageEvent)
328
+ } catch (error) {
329
+ this.logger.error('处理用户消息失败:', error)
330
+ }
331
+ }
332
+
333
+ private async processBotMessage(session: Session, timestamp?: number) {
334
+ try {
335
+ if (!timestamp) timestamp = Date.now()
336
+
337
+ const correctChannelId = this.getCorrectChannelId(session.selfId)
338
+ const finalChannelId = correctChannelId || session.channelId
339
+
340
+ this.updateBotInfoToFile(session)
341
+
342
+ const guildName = this.updateChannelInfoToFile(session)
343
+ const isDirect = session.isDirect || finalChannelId?.includes('private')
344
+
345
+ let content = session.content || ''
346
+
347
+ if (!content && session.event?.message?.elements) {
348
+ content = this.utils.extractTextContent(session.event.message.elements).trim()
349
+ }
350
+
351
+ let quoteInfo: QuoteInfo | undefined = undefined
352
+ const quoteMatch = content.match(/<quote id="([^"]+)"\/>/)
353
+ if (quoteMatch) {
354
+ const quoteId = quoteMatch[1]
355
+ const data = this.fileManager.readChatDataFromFile()
356
+ const channelKey = `${session.selfId}:${finalChannelId}`
357
+ const quotedMsg = data.messages[channelKey]?.find(m => m.id === quoteId)
358
+
359
+ if (quotedMsg) {
360
+ const realId = quotedMsg.id.startsWith('bot-msg-') ? (quotedMsg as any).realId : quotedMsg.id
361
+
362
+ quoteInfo = {
363
+ messageId: realId || quotedMsg.id,
364
+ id: realId || quotedMsg.id,
365
+ content: quotedMsg.content,
366
+ elements: quotedMsg.elements,
367
+ user: {
368
+ id: quotedMsg.userId,
369
+ name: quotedMsg.username,
370
+ userId: quotedMsg.userId,
371
+ avatar: quotedMsg.avatar,
372
+ username: quotedMsg.username
373
+ },
374
+ timestamp: quotedMsg.timestamp
375
+ }
376
+ content = content.replace(/<quote id="[^"]+"\/>\s*/, '')
377
+
378
+ if (realId) {
379
+ session.content = session.content.replace(/id="[^"]+"/, `id="${realId}"`)
380
+ }
381
+ }
382
+ }
383
+
384
+ // 创建机器人消息信息对象
385
+ const messageInfo: MessageInfo = {
386
+ id: `bot-msg-${timestamp}`,
387
+ content: content,
388
+ userId: session.selfId,
389
+ username: session.bot.user?.name || `Bot-${session.selfId}`,
390
+ avatar: session.bot.user?.avatar,
391
+ timestamp: timestamp,
392
+ channelId: finalChannelId,
393
+ selfId: session.selfId,
394
+ elements: this.utils.cleanBase64Content(session.event?.message?.elements, true),
395
+ type: 'bot',
396
+ guildName: guildName,
397
+ platform: session.platform || 'unknown',
398
+ quote: quoteInfo,
399
+ isDirect: !!isDirect,
400
+ sending: true // 标记为正在发送
401
+ }
402
+
403
+ // 异步保存消息(不阻塞)
404
+ await this.fileManager.addMessageToFile(messageInfo)
405
+
406
+ const messageEvent = {
407
+ type: 'bot-message',
408
+ selfId: session.selfId,
409
+ platform: session.platform || 'unknown',
410
+ channelId: finalChannelId,
411
+ messageId: `bot-msg-${timestamp}`,
412
+ content: content,
413
+ userId: session.selfId,
414
+ username: session.bot.user?.name || `Bot-${session.selfId}`,
415
+ avatar: session.bot.user?.avatar,
416
+ timestamp: timestamp,
417
+ guildName: guildName,
418
+ channelType: session.event?.channel?.type || session.type || 0,
419
+ elements: this.utils.cleanBase64Content(session.event?.message?.elements, true),
420
+ quote: quoteInfo,
421
+ isDirect: !!isDirect,
422
+ sending: true,
423
+ bot: {
424
+ avatar: session.bot.user?.avatar,
425
+ name: session.bot.user?.name,
426
+ }
427
+ }
428
+
429
+ this.ctx.console.broadcast('chat-bot-message-event', messageEvent)
430
+ } catch (error) {
431
+ this.logger.error('处理机器人消息失败:', error)
432
+ }
433
+ }
434
+
435
+ private logInfo(...args: any[]) {
436
+ if (this.config.loggerinfo) {
437
+ (this.logger.info as (...args: any[]) => void)(...args)
438
+ }
439
+ }
440
+ }