koishi-plugin-chat-patch 2.0.1 → 2.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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/config.d.ts +13 -0
- package/lib/index.js +242 -140
- 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 +139 -99
- package/src/config.ts +3 -5
- package/src/message-handler.ts +163 -62
- package/src/types.ts +2 -0
package/src/message-handler.ts
CHANGED
|
@@ -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
|
-
|
|
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 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:
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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)
|