koishi-plugin-chat-patch 2.2.0 → 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.
- package/client/icons/activity.vue +9 -9
- package/client/icons/index.ts +4 -4
- package/client/index.scss +90 -90
- package/client/index.ts +25 -25
- package/client/vue/composables/useChatActions.ts +1 -1
- package/client/vue/composables/useChatData.ts +1 -1
- package/client/vue/composables/useImageCache.ts +1 -1
- package/client/vue/types.ts +1 -1
- package/lib/file-manager.d.ts +25 -0
- package/lib/index.js +284 -193
- package/lib/message-handler.d.ts +7 -5
- package/package.json +1 -1
- package/readme.md +25 -25
- package/src/api-handlers.ts +19 -65
- package/src/config.ts +45 -46
- package/src/file-manager.ts +215 -169
- package/src/index.ts +58 -31
- package/src/message-handler.ts +161 -134
- package/src/types.ts +62 -62
- package/src/utils.ts +4 -16
package/src/message-handler.ts
CHANGED
|
@@ -8,7 +8,7 @@ import { } from '@koishijs/plugin-console'
|
|
|
8
8
|
export class MessageHandler {
|
|
9
9
|
private logger: Logger
|
|
10
10
|
private utils: Utils
|
|
11
|
-
|
|
11
|
+
|
|
12
12
|
private correctChannelIds: Map<string, string> = new Map()
|
|
13
13
|
|
|
14
14
|
constructor(
|
|
@@ -20,118 +20,152 @@ export class MessageHandler {
|
|
|
20
20
|
this.utils = new Utils(config)
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
|
|
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
|
+
|
|
24
41
|
setCorrectChannelId(selfId: string, channelId: string) {
|
|
25
42
|
this.correctChannelIds.set(selfId, channelId)
|
|
26
43
|
this.logInfo('设置正确的 channelId:', { selfId, channelId })
|
|
27
44
|
}
|
|
28
45
|
|
|
29
|
-
// 获取正确的 channelId
|
|
30
46
|
getCorrectChannelId(selfId: string): string | undefined {
|
|
31
47
|
return this.correctChannelIds.get(selfId)
|
|
32
48
|
}
|
|
33
49
|
|
|
34
|
-
|
|
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
|
|
50
|
+
updateBotInfoToFile(session: Session) {
|
|
57
51
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
if (!isDirect) {
|
|
52
|
+
setImmediate(() => {
|
|
61
53
|
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
|
-
}
|
|
54
|
+
const data = this.fileManager.readChatDataFromFile()
|
|
67
55
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.logInfo('获取频道信息失败,使用频道ID作为备用:', channelError)
|
|
75
|
-
}
|
|
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'
|
|
76
62
|
}
|
|
63
|
+
|
|
64
|
+
data.bots[session.selfId] = botInfo
|
|
65
|
+
this.fileManager.writeChatDataToFile(data)
|
|
66
|
+
this.logInfo('更新机器人信息到文件:', botInfo.username)
|
|
77
67
|
} catch (error) {
|
|
78
|
-
this.
|
|
79
|
-
guildName = session.channelId
|
|
68
|
+
this.logger.error('更新机器人信息失败:', error)
|
|
80
69
|
}
|
|
81
|
-
}
|
|
70
|
+
})
|
|
71
|
+
}
|
|
82
72
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
86
76
|
|
|
87
|
-
|
|
88
|
-
const existingChannel = data.channels[session.selfId][session.channelId]
|
|
77
|
+
const data = this.fileManager.readChatDataFromFile()
|
|
78
|
+
const existingChannel = data.channels[session.selfId]?.[session.channelId]
|
|
89
79
|
|
|
90
|
-
|
|
91
|
-
let finalName: string
|
|
80
|
+
let immediateName = session.channelId
|
|
92
81
|
if (isDirect) {
|
|
93
|
-
// 私聊频道:优先使用真实用户名
|
|
94
82
|
if (directUserName && directUserName !== session.userId) {
|
|
95
|
-
|
|
83
|
+
immediateName = `私聊(${directUserName})`
|
|
96
84
|
} else if (existingChannel?.name && !existingChannel.name.includes('未知')) {
|
|
97
|
-
|
|
98
|
-
finalName = existingChannel.name
|
|
85
|
+
immediateName = existingChannel.name
|
|
99
86
|
} else if (session.platform && session.platform.toLowerCase().includes('sandbox')) {
|
|
100
|
-
|
|
101
|
-
finalName = `私聊(${session.userId})`
|
|
87
|
+
immediateName = `私聊(${session.userId})`
|
|
102
88
|
} else {
|
|
103
|
-
|
|
89
|
+
immediateName = '私聊(未知用户)'
|
|
104
90
|
}
|
|
105
|
-
} else {
|
|
106
|
-
|
|
91
|
+
} else if (existingChannel?.guildName) {
|
|
92
|
+
immediateName = existingChannel.guildName
|
|
107
93
|
}
|
|
108
94
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
type: session.type || 0,
|
|
113
|
-
channelId: session.channelId,
|
|
114
|
-
guildName: guildName,
|
|
115
|
-
isDirect: !!isDirect
|
|
116
|
-
}
|
|
95
|
+
setImmediate(async () => {
|
|
96
|
+
try {
|
|
97
|
+
let guildName = session.channelId
|
|
117
98
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
this.logInfo('更新频道名称:', {
|
|
121
|
-
channelId: session.channelId,
|
|
122
|
-
oldName: existingChannel.name,
|
|
123
|
-
newName: finalName
|
|
124
|
-
})
|
|
125
|
-
}
|
|
99
|
+
if (!isDirect) {
|
|
100
|
+
try {
|
|
126
101
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
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
|
+
}
|
|
130
123
|
|
|
131
|
-
|
|
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
|
|
132
167
|
}
|
|
133
168
|
|
|
134
|
-
// 下载并缓存媒体文件
|
|
135
169
|
public async downloadAndCacheMedia(url: string, type: 'image' | 'media' | 'avatar') {
|
|
136
170
|
try {
|
|
137
171
|
if (!url || url.startsWith('data:') || url.startsWith('file:')) return url
|
|
@@ -145,7 +179,6 @@ export class MessageHandler {
|
|
|
145
179
|
require('node:fs').mkdirSync(dir, { recursive: true })
|
|
146
180
|
}
|
|
147
181
|
|
|
148
|
-
// 使用 URL 的 hash 作为文件名,避免重复下载
|
|
149
182
|
const crypto = require('node:crypto')
|
|
150
183
|
const hash = crypto.createHash('md5').update(url).digest('hex')
|
|
151
184
|
const ext = require('node:path').extname(new URL(url).pathname) || (type === 'image' ? '.jpg' : '.mp4')
|
|
@@ -165,47 +198,54 @@ export class MessageHandler {
|
|
|
165
198
|
}
|
|
166
199
|
}
|
|
167
200
|
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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)
|
|
184
226
|
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
this.logger.error('处理媒体元素失败:', error)
|
|
185
229
|
}
|
|
186
|
-
|
|
187
|
-
if (el.children) await this.processMediaElements(el.children, isUserMessage)
|
|
188
|
-
}
|
|
189
|
-
return elements
|
|
230
|
+
})
|
|
190
231
|
}
|
|
191
232
|
|
|
192
|
-
async
|
|
233
|
+
private async processUserMessage(session: Session, timestamp?: number) {
|
|
193
234
|
try {
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
235
|
+
if (!timestamp) timestamp = Date.now()
|
|
236
|
+
|
|
237
|
+
this.updateBotInfoToFile(session)
|
|
197
238
|
|
|
198
|
-
const
|
|
239
|
+
const guildName = this.updateChannelInfoToFile(session)
|
|
240
|
+
const isDirect = session.isDirect || session.channelId?.includes('private')
|
|
199
241
|
|
|
200
|
-
// 处理媒体缓存 - 用户消息只缓存图片和语音
|
|
201
242
|
if (session.elements) {
|
|
202
|
-
|
|
243
|
+
this.processMediaElementsAsync(session.elements, true)
|
|
203
244
|
}
|
|
204
245
|
if (session.quote?.elements) {
|
|
205
|
-
|
|
246
|
+
this.processMediaElementsAsync(session.quote.elements, true)
|
|
206
247
|
}
|
|
207
248
|
|
|
208
|
-
// 处理 quote 信息
|
|
209
249
|
let quoteInfo: QuoteInfo | undefined = undefined
|
|
210
250
|
if (session.quote) {
|
|
211
251
|
quoteInfo = {
|
|
@@ -224,7 +264,6 @@ export class MessageHandler {
|
|
|
224
264
|
}
|
|
225
265
|
}
|
|
226
266
|
|
|
227
|
-
// 从 session 中提取用户消息内容
|
|
228
267
|
let content = ''
|
|
229
268
|
let elements: h[] = []
|
|
230
269
|
|
|
@@ -244,7 +283,6 @@ export class MessageHandler {
|
|
|
244
283
|
}
|
|
245
284
|
}
|
|
246
285
|
|
|
247
|
-
// 创建消息信息对象
|
|
248
286
|
const messageInfo: MessageInfo = {
|
|
249
287
|
id: session.event?.message?.id || `msg-${timestamp}`,
|
|
250
288
|
content: content || session.content || '',
|
|
@@ -288,47 +326,37 @@ export class MessageHandler {
|
|
|
288
326
|
|
|
289
327
|
this.ctx.console.broadcast('chat-message-event', messageEvent)
|
|
290
328
|
} catch (error) {
|
|
291
|
-
this.logger.error('
|
|
329
|
+
this.logger.error('处理用户消息失败:', error)
|
|
292
330
|
}
|
|
293
331
|
}
|
|
294
332
|
|
|
295
|
-
|
|
296
|
-
async broadcastBotMessageEvent(session: Session) {
|
|
333
|
+
private async processBotMessage(session: Session, timestamp?: number) {
|
|
297
334
|
try {
|
|
298
|
-
|
|
299
|
-
const correctChannelId = this.getCorrectChannelId(session.selfId)
|
|
335
|
+
if (!timestamp) timestamp = Date.now()
|
|
300
336
|
|
|
301
|
-
|
|
337
|
+
const correctChannelId = this.getCorrectChannelId(session.selfId)
|
|
302
338
|
const finalChannelId = correctChannelId || session.channelId
|
|
303
339
|
|
|
304
|
-
|
|
305
|
-
const guildName = await this.updateChannelInfoToFile(session)
|
|
306
|
-
const isDirect = session.isDirect || finalChannelId?.includes('private')
|
|
307
|
-
|
|
308
|
-
const timestamp = Date.now()
|
|
340
|
+
this.updateBotInfoToFile(session)
|
|
309
341
|
|
|
310
|
-
|
|
311
|
-
|
|
342
|
+
const guildName = this.updateChannelInfoToFile(session)
|
|
343
|
+
const isDirect = session.isDirect || finalChannelId?.includes('private')
|
|
312
344
|
|
|
313
|
-
// 优先使用 session.content,它包含了完整的消息内容(含标签)
|
|
314
345
|
let content = session.content || ''
|
|
315
346
|
|
|
316
347
|
if (!content && session.event?.message?.elements) {
|
|
317
348
|
content = this.utils.extractTextContent(session.event.message.elements).trim()
|
|
318
349
|
}
|
|
319
350
|
|
|
320
|
-
// 尝试从 content 中提取 quote id 并构建 quote 对象
|
|
321
351
|
let quoteInfo: QuoteInfo | undefined = undefined
|
|
322
352
|
const quoteMatch = content.match(/<quote id="([^"]+)"\/>/)
|
|
323
353
|
if (quoteMatch) {
|
|
324
354
|
const quoteId = quoteMatch[1]
|
|
325
355
|
const data = this.fileManager.readChatDataFromFile()
|
|
326
356
|
const channelKey = `${session.selfId}:${finalChannelId}`
|
|
327
|
-
// 在当前频道的消息历史中查找被引用的消息
|
|
328
357
|
const quotedMsg = data.messages[channelKey]?.find(m => m.id === quoteId)
|
|
329
358
|
|
|
330
359
|
if (quotedMsg) {
|
|
331
|
-
// 如果是虚拟 ID,尝试获取其真实的 messageId
|
|
332
360
|
const realId = quotedMsg.id.startsWith('bot-msg-') ? (quotedMsg as any).realId : quotedMsg.id
|
|
333
361
|
|
|
334
362
|
quoteInfo = {
|
|
@@ -345,10 +373,8 @@ export class MessageHandler {
|
|
|
345
373
|
},
|
|
346
374
|
timestamp: quotedMsg.timestamp
|
|
347
375
|
}
|
|
348
|
-
// 移除 content 中的 quote 标签,避免重复显示
|
|
349
376
|
content = content.replace(/<quote id="[^"]+"\/>\s*/, '')
|
|
350
377
|
|
|
351
|
-
// 修正 content 中的 quote 标签为真实 ID,以便 bot.sendMessage 能够正确识别
|
|
352
378
|
if (realId) {
|
|
353
379
|
session.content = session.content.replace(/id="[^"]+"/, `id="${realId}"`)
|
|
354
380
|
}
|
|
@@ -374,6 +400,7 @@ export class MessageHandler {
|
|
|
374
400
|
sending: true // 标记为正在发送
|
|
375
401
|
}
|
|
376
402
|
|
|
403
|
+
// 异步保存消息(不阻塞)
|
|
377
404
|
await this.fileManager.addMessageToFile(messageInfo)
|
|
378
405
|
|
|
379
406
|
const messageEvent = {
|
|
@@ -401,7 +428,7 @@ export class MessageHandler {
|
|
|
401
428
|
|
|
402
429
|
this.ctx.console.broadcast('chat-bot-message-event', messageEvent)
|
|
403
430
|
} catch (error) {
|
|
404
|
-
this.logger.error('
|
|
431
|
+
this.logger.error('处理机器人消息失败:', error)
|
|
405
432
|
}
|
|
406
433
|
}
|
|
407
434
|
|
package/src/types.ts
CHANGED
|
@@ -1,62 +1,62 @@
|
|
|
1
|
-
import { h } from 'koishi'
|
|
2
|
-
|
|
3
|
-
export interface BotInfo {
|
|
4
|
-
selfId: string
|
|
5
|
-
platform: string
|
|
6
|
-
username: string
|
|
7
|
-
avatar?: string
|
|
8
|
-
status: 'online' | 'offline'
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export interface ChannelInfo {
|
|
12
|
-
id: string
|
|
13
|
-
name: string
|
|
14
|
-
type: number | string
|
|
15
|
-
channelId?: string
|
|
16
|
-
guildName?: string
|
|
17
|
-
isDirect?: boolean
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export interface QuoteInfo {
|
|
21
|
-
messageId: string
|
|
22
|
-
id: string
|
|
23
|
-
content: string
|
|
24
|
-
elements?: h[]
|
|
25
|
-
user: {
|
|
26
|
-
id: string
|
|
27
|
-
name: string
|
|
28
|
-
userId: string
|
|
29
|
-
avatar?: string
|
|
30
|
-
username: string
|
|
31
|
-
}
|
|
32
|
-
timestamp: number
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface MessageInfo {
|
|
36
|
-
id: string
|
|
37
|
-
content: string
|
|
38
|
-
userId: string
|
|
39
|
-
username: string
|
|
40
|
-
avatar?: string
|
|
41
|
-
timestamp: number
|
|
42
|
-
channelId: string
|
|
43
|
-
selfId: string
|
|
44
|
-
elements?: h[]
|
|
45
|
-
type: 'user' | 'bot'
|
|
46
|
-
guildId?: string
|
|
47
|
-
guildName?: string
|
|
48
|
-
platform: string
|
|
49
|
-
quote?: QuoteInfo
|
|
50
|
-
isDirect?: boolean
|
|
51
|
-
sending?: boolean
|
|
52
|
-
realId?: string
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
export interface ChatData {
|
|
56
|
-
bots: Record<string, BotInfo>
|
|
57
|
-
channels: Record<string, Record<string, ChannelInfo>>
|
|
58
|
-
messages: Record<string, MessageInfo[]>
|
|
59
|
-
pinnedBots: string[]
|
|
60
|
-
pinnedChannels: string[]
|
|
61
|
-
lastSaveTime?: number
|
|
62
|
-
}
|
|
1
|
+
import { h } from 'koishi'
|
|
2
|
+
|
|
3
|
+
export interface BotInfo {
|
|
4
|
+
selfId: string
|
|
5
|
+
platform: string
|
|
6
|
+
username: string
|
|
7
|
+
avatar?: string
|
|
8
|
+
status: 'online' | 'offline'
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface ChannelInfo {
|
|
12
|
+
id: string
|
|
13
|
+
name: string
|
|
14
|
+
type: number | string
|
|
15
|
+
channelId?: string
|
|
16
|
+
guildName?: string
|
|
17
|
+
isDirect?: boolean
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface QuoteInfo {
|
|
21
|
+
messageId: string
|
|
22
|
+
id: string
|
|
23
|
+
content: string
|
|
24
|
+
elements?: h[]
|
|
25
|
+
user: {
|
|
26
|
+
id: string
|
|
27
|
+
name: string
|
|
28
|
+
userId: string
|
|
29
|
+
avatar?: string
|
|
30
|
+
username: string
|
|
31
|
+
}
|
|
32
|
+
timestamp: number
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export interface MessageInfo {
|
|
36
|
+
id: string
|
|
37
|
+
content: string
|
|
38
|
+
userId: string
|
|
39
|
+
username: string
|
|
40
|
+
avatar?: string
|
|
41
|
+
timestamp: number
|
|
42
|
+
channelId: string
|
|
43
|
+
selfId: string
|
|
44
|
+
elements?: h[]
|
|
45
|
+
type: 'user' | 'bot'
|
|
46
|
+
guildId?: string
|
|
47
|
+
guildName?: string
|
|
48
|
+
platform: string
|
|
49
|
+
quote?: QuoteInfo
|
|
50
|
+
isDirect?: boolean
|
|
51
|
+
sending?: boolean
|
|
52
|
+
realId?: string
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ChatData {
|
|
56
|
+
bots: Record<string, BotInfo>
|
|
57
|
+
channels: Record<string, Record<string, ChannelInfo>>
|
|
58
|
+
messages: Record<string, MessageInfo[]>
|
|
59
|
+
pinnedBots: string[]
|
|
60
|
+
pinnedChannels: string[]
|
|
61
|
+
lastSaveTime?: number
|
|
62
|
+
}
|
package/src/utils.ts
CHANGED
|
@@ -8,7 +8,6 @@ import { pathToFileURL } from 'node:url'
|
|
|
8
8
|
export class Utils {
|
|
9
9
|
constructor(private config: Config, private ctx?: Context) { }
|
|
10
10
|
|
|
11
|
-
// 检查平台是否被屏蔽
|
|
12
11
|
isPlatformBlocked(platform: string): boolean {
|
|
13
12
|
if (!this.config.blockedPlatforms || this.config.blockedPlatforms.length === 0) {
|
|
14
13
|
return false
|
|
@@ -29,7 +28,6 @@ export class Utils {
|
|
|
29
28
|
return false
|
|
30
29
|
}
|
|
31
30
|
|
|
32
|
-
// 递归提取所有文本内容的函数
|
|
33
31
|
extractTextContent(elements: any[]): string {
|
|
34
32
|
let text = ''
|
|
35
33
|
for (const element of elements) {
|
|
@@ -46,16 +44,13 @@ export class Utils {
|
|
|
46
44
|
return text
|
|
47
45
|
}
|
|
48
46
|
|
|
49
|
-
// 检查字符串是否为base64格式
|
|
50
47
|
private isBase64(str: string): boolean {
|
|
51
48
|
if (!str || typeof str !== 'string') return false
|
|
52
49
|
|
|
53
|
-
// 检查是否以data:开头的base64格式
|
|
54
50
|
if (str.startsWith('data:')) {
|
|
55
51
|
return str.includes('base64,')
|
|
56
52
|
}
|
|
57
53
|
|
|
58
|
-
// 检查纯base64字符串(长度大于100且符合base64格式)
|
|
59
54
|
if (str.length > 100) {
|
|
60
55
|
const base64Regex = /^[A-Za-z0-9+/]*={0,2}$/
|
|
61
56
|
return base64Regex.test(str)
|
|
@@ -64,7 +59,6 @@ export class Utils {
|
|
|
64
59
|
return false
|
|
65
60
|
}
|
|
66
61
|
|
|
67
|
-
// 持久化 base64 图片并返回本地文件 URL
|
|
68
62
|
persistBase64Image(base64Data: string): string {
|
|
69
63
|
if (!this.ctx || !base64Data.startsWith('data:image/')) return base64Data
|
|
70
64
|
|
|
@@ -72,17 +66,14 @@ export class Utils {
|
|
|
72
66
|
const dir = join(this.ctx.baseDir, 'data', 'chat-patch', 'persist-images')
|
|
73
67
|
if (!existsSync(dir)) mkdirSync(dir, { recursive: true })
|
|
74
68
|
|
|
75
|
-
// 生成文件名:时间戳 + 内容哈希
|
|
76
69
|
const hash = createHash('md5').update(base64Data).digest('hex')
|
|
77
70
|
const ext = base64Data.split(';')[0].split('/')[1] || 'png'
|
|
78
71
|
const filename = `${Date.now()}_${hash}.${ext}`
|
|
79
72
|
const filePath = join(dir, filename)
|
|
80
73
|
|
|
81
|
-
// 写入文件
|
|
82
74
|
const base64Content = base64Data.split(',')[1]
|
|
83
75
|
writeFileSync(filePath, Buffer.from(base64Content, 'base64'))
|
|
84
76
|
|
|
85
|
-
// 清理旧图片
|
|
86
77
|
this.cleanupPersistImages(dir)
|
|
87
78
|
|
|
88
79
|
return pathToFileURL(filePath).href
|
|
@@ -103,8 +94,6 @@ export class Utils {
|
|
|
103
94
|
} catch (e) { }
|
|
104
95
|
}
|
|
105
96
|
|
|
106
|
-
// 清理对象中的base64内容,改为持久化存储
|
|
107
|
-
// isBotMessage: 是否为机器人发送的消息
|
|
108
97
|
cleanBase64Content(obj: any, isBotMessage: boolean = false): any {
|
|
109
98
|
if (obj === null || obj === undefined) {
|
|
110
99
|
return obj
|
|
@@ -112,7 +101,7 @@ export class Utils {
|
|
|
112
101
|
|
|
113
102
|
if (typeof obj === 'string') {
|
|
114
103
|
if (this.isBase64(obj)) {
|
|
115
|
-
|
|
104
|
+
|
|
116
105
|
if (isBotMessage && !obj.startsWith('data:image/')) {
|
|
117
106
|
return '[富媒体内容已省略]'
|
|
118
107
|
}
|
|
@@ -128,16 +117,15 @@ export class Utils {
|
|
|
128
117
|
if (typeof obj === 'object') {
|
|
129
118
|
const cleaned: any = {}
|
|
130
119
|
for (const [key, value] of Object.entries(obj)) {
|
|
131
|
-
|
|
120
|
+
|
|
132
121
|
if (isBotMessage && obj.type && !['text', 'image', 'img'].includes(obj.type)) {
|
|
133
|
-
|
|
122
|
+
|
|
134
123
|
if (typeof value === 'string' && (key === 'src' || key === 'url' || key === 'file') && this.isBase64(value)) {
|
|
135
124
|
cleaned[key] = '[富媒体内容已省略]'
|
|
136
125
|
continue
|
|
137
126
|
}
|
|
138
127
|
}
|
|
139
128
|
|
|
140
|
-
// 特别处理常见的base64字段
|
|
141
129
|
if (typeof value === 'string' && (
|
|
142
130
|
key === 'src' ||
|
|
143
131
|
key === 'url' ||
|
|
@@ -145,7 +133,7 @@ export class Utils {
|
|
|
145
133
|
key === 'data' ||
|
|
146
134
|
key === 'content'
|
|
147
135
|
) && this.isBase64(value)) {
|
|
148
|
-
|
|
136
|
+
|
|
149
137
|
if (isBotMessage && !value.startsWith('data:image/')) {
|
|
150
138
|
cleaned[key] = '[富媒体内容已省略]'
|
|
151
139
|
} else {
|