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.
@@ -50,55 +50,78 @@ 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
- let directUserName = session.username || session.userId || '未知用户'
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
- try {
59
- // 获取群组名称
60
- if (session.guildId && session.bot.getGuild && typeof session.bot.getGuild === 'function') {
61
- const guild = await session.bot.getGuild(session.guildId)
62
- guildName = guild?.name || session.channelId
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
- // 如果没有getGuild方法,尝试使用getChannel方法
76
- if (session.guildId && !session.bot.getGuild && session.bot.getChannel && typeof session.bot.getChannel === 'function') {
77
- try {
78
- const channel = await session.bot.getChannel(session.guildId)
79
- guildName = channel?.name || session.channelId
80
- } catch (channelError) {
81
- this.logInfo('获取频道信息失败,使用频道ID作为备用:', channelError)
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 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
+
93
109
  const channelInfo: ChannelInfo = {
94
110
  id: session.channelId,
95
- name: session.isDirect
96
- ? `私聊(${session.username || session.userId || directUserName})`
97
- : `${guildName} (${session.channelId})`,
111
+ name: finalName,
98
112
  type: session.type || 0,
99
113
  channelId: session.channelId,
100
114
  guildName: guildName,
101
- isDirect: session.isDirect
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
+ })
102
125
  }
103
126
 
104
127
  data.channels[session.selfId][session.channelId] = channelInfo
@@ -108,13 +131,72 @@ export class MessageHandler {
108
131
  return guildName
109
132
  }
110
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
+
111
184
  async broadcastMessageEvent(session: Session) {
112
185
  try {
113
186
  await this.updateBotInfoToFile(session)
114
187
  const guildName = await this.updateChannelInfoToFile(session)
188
+ const isDirect = session.isDirect || session.channelId?.includes('private')
115
189
 
116
190
  const timestamp = Date.now()
117
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
+
118
200
  // 处理 quote 信息
119
201
  let quoteInfo: QuoteInfo | undefined = undefined
120
202
  if (session.quote) {
@@ -169,7 +251,7 @@ export class MessageHandler {
169
251
  guildName: guildName,
170
252
  platform: session.platform || 'unknown',
171
253
  quote: quoteInfo ? this.utils.cleanBase64Content(quoteInfo) : undefined,
172
- isDirect: session.isDirect
254
+ isDirect: !!isDirect
173
255
  }
174
256
 
175
257
  await this.fileManager.addMessageToFile(messageInfo)
@@ -208,24 +290,20 @@ export class MessageHandler {
208
290
  // 获取正确的 channelId
209
291
  const correctChannelId = this.getCorrectChannelId(session.selfId)
210
292
 
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
293
  // 使用正确的 channelId,如果没有则使用 session.channelId
222
294
  const finalChannelId = correctChannelId || session.channelId
223
295
 
224
296
  await this.updateBotInfoToFile(session)
225
297
  const guildName = await this.updateChannelInfoToFile(session)
298
+ const isDirect = session.isDirect || finalChannelId?.includes('private')
226
299
 
227
300
  const timestamp = Date.now()
228
301
 
302
+ // 处理媒体缓存
303
+ if (session.event?.message?.elements) {
304
+ await this.processMediaElements(session.event.message.elements)
305
+ }
306
+
229
307
  // 优先使用 session.content,它包含了完整的消息内容(含标签)
230
308
  let content = session.content || ''
231
309
 
@@ -233,6 +311,44 @@ export class MessageHandler {
233
311
  content = this.utils.extractTextContent(session.event.message.elements).trim()
234
312
  }
235
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
+
236
352
  // 创建机器人消息信息对象
237
353
  const messageInfo: MessageInfo = {
238
354
  id: `bot-msg-${timestamp}`,
@@ -247,7 +363,9 @@ export class MessageHandler {
247
363
  type: 'bot',
248
364
  guildName: guildName,
249
365
  platform: session.platform || 'unknown',
250
- isDirect: session.isDirect
366
+ quote: quoteInfo,
367
+ isDirect: !!isDirect,
368
+ sending: true // 标记为正在发送
251
369
  }
252
370
 
253
371
  await this.fileManager.addMessageToFile(messageInfo)
@@ -266,32 +384,15 @@ export class MessageHandler {
266
384
  guildName: guildName,
267
385
  channelType: session.event?.channel?.type || session.type || 0,
268
386
  elements: this.utils.cleanBase64Content(session.event?.message?.elements),
269
- isDirect: session.isDirect,
387
+ quote: quoteInfo,
388
+ isDirect: !!isDirect,
389
+ sending: true,
270
390
  bot: {
271
391
  avatar: session.bot.user?.avatar,
272
392
  name: session.bot.user?.name,
273
393
  }
274
394
  }
275
395
 
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
396
  this.ctx.console.broadcast('chat-bot-message-event', messageEvent)
296
397
  } catch (error) {
297
398
  this.logger.error('广播机器人消息事件失败:', error)
package/src/types.ts CHANGED
@@ -48,6 +48,8 @@ export interface MessageInfo {
48
48
  platform: string
49
49
  quote?: QuoteInfo
50
50
  isDirect?: boolean
51
+ sending?: boolean
52
+ realId?: string
51
53
  }
52
54
 
53
55
  export interface ChatData {