koishi-plugin-chat-patch 2.2.0 → 2.4.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/dist/index.js +2 -2
- package/lib/file-manager.d.ts +32 -0
- package/lib/index.js +417 -197
- 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 +367 -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/index.ts
CHANGED
|
@@ -38,7 +38,6 @@ export { Config } from './config'
|
|
|
38
38
|
export async function apply(ctx: Context, config: Config) {
|
|
39
39
|
const logger = ctx.logger('chat-patch')
|
|
40
40
|
|
|
41
|
-
// 启动时清空 media 文件夹(为旧版本做清理工作)
|
|
42
41
|
const mediaDir = path.join(ctx.baseDir, 'data', 'chat-patch', 'persist-media', 'media')
|
|
43
42
|
if (require('node:fs').existsSync(mediaDir)) {
|
|
44
43
|
try {
|
|
@@ -61,17 +60,14 @@ export async function apply(ctx: Context, config: Config) {
|
|
|
61
60
|
}
|
|
62
61
|
}
|
|
63
62
|
|
|
64
|
-
// 初始化各个模块
|
|
65
63
|
const fileManager = new FileManager(ctx, config)
|
|
66
64
|
const messageHandler = new MessageHandler(ctx, config, fileManager)
|
|
67
65
|
const apiHandlers = new ApiHandlers(ctx, config, fileManager, messageHandler)
|
|
68
66
|
const utils = new Utils(config, ctx)
|
|
69
67
|
|
|
70
|
-
// 初始化数据
|
|
71
68
|
const initialData = fileManager.readChatDataFromFile()
|
|
72
69
|
const cleanedData = fileManager.cleanExcessMessages(initialData)
|
|
73
70
|
|
|
74
|
-
// 如果清理了数据,立即写回文件
|
|
75
71
|
const originalCount = Object.values(initialData.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
76
72
|
const cleanedCount = Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
77
73
|
|
|
@@ -79,7 +75,6 @@ export async function apply(ctx: Context, config: Config) {
|
|
|
79
75
|
fileManager.writeChatDataToFile(cleanedData)
|
|
80
76
|
}
|
|
81
77
|
|
|
82
|
-
// 日志调试函数
|
|
83
78
|
function logInfo(...args: any[]) {
|
|
84
79
|
if (config.loggerinfo) {
|
|
85
80
|
(logger.info as (...args: any[]) => void)(...args)
|
|
@@ -94,39 +89,67 @@ export async function apply(ctx: Context, config: Config) {
|
|
|
94
89
|
总消息数: Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
95
90
|
})
|
|
96
91
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
92
|
+
ctx.on('message', (session) => {
|
|
93
|
+
if (utils.isPlatformBlocked(session.platform || 'unknown')) return
|
|
94
|
+
|
|
95
|
+
const timestamp = Date.now()
|
|
96
|
+
|
|
97
|
+
ctx.console.broadcast('chat-message-event', {
|
|
98
|
+
type: 'message',
|
|
99
|
+
selfId: session.selfId,
|
|
100
|
+
platform: session.platform || 'unknown',
|
|
101
|
+
channelId: session.channelId,
|
|
102
|
+
messageId: session.event?.message?.id || `msg-${timestamp}`,
|
|
103
|
+
content: session.content || '',
|
|
104
|
+
userId: session.userId || 'unknown',
|
|
105
|
+
username: session.username || session.userId || 'unknown',
|
|
106
|
+
avatar: session.event?.user?.avatar,
|
|
107
|
+
timestamp: timestamp,
|
|
108
|
+
isDirect: session.isDirect,
|
|
109
|
+
elements: utils.cleanBase64Content(session.elements, false),
|
|
110
|
+
bot: {
|
|
111
|
+
avatar: session.bot.user?.avatar,
|
|
112
|
+
name: session.bot.user?.name,
|
|
113
|
+
}
|
|
114
|
+
})
|
|
115
|
+
|
|
116
|
+
messageHandler.recordUserMessage(session, timestamp)
|
|
107
117
|
})
|
|
108
118
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
119
|
+
ctx.on('before-send', (session) => {
|
|
120
|
+
if (utils.isPlatformBlocked(session.platform || 'unknown')) return
|
|
121
|
+
|
|
122
|
+
const timestamp = Date.now()
|
|
123
|
+
|
|
124
|
+
ctx.console.broadcast('chat-bot-message-event', {
|
|
125
|
+
type: 'bot-message',
|
|
126
|
+
selfId: session.selfId,
|
|
127
|
+
platform: session.platform || 'unknown',
|
|
128
|
+
channelId: session.channelId,
|
|
129
|
+
messageId: `bot-msg-${timestamp}`,
|
|
130
|
+
content: session.content || '',
|
|
131
|
+
userId: session.selfId,
|
|
132
|
+
username: session.bot.user?.name || `Bot-${session.selfId}`,
|
|
133
|
+
avatar: session.bot.user?.avatar,
|
|
134
|
+
timestamp: timestamp,
|
|
135
|
+
sending: true,
|
|
136
|
+
elements: utils.cleanBase64Content(session.event?.message?.elements, true),
|
|
137
|
+
bot: {
|
|
138
|
+
avatar: session.bot.user?.avatar,
|
|
139
|
+
name: session.bot.user?.name,
|
|
140
|
+
}
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
messageHandler.recordBotMessage(session, timestamp)
|
|
118
144
|
})
|
|
119
145
|
|
|
120
|
-
// 插件启动时设置定期清理过期消息
|
|
121
146
|
ctx.on('ready', async () => {
|
|
122
147
|
logInfo('插件启动完成,开始监听消息')
|
|
123
148
|
|
|
124
|
-
|
|
125
|
-
setInterval(() => {
|
|
149
|
+
ctx.setInterval(() => {
|
|
126
150
|
const data = fileManager.readChatDataFromFile()
|
|
127
151
|
const cleanedData = fileManager.cleanExcessMessages(data)
|
|
128
152
|
|
|
129
|
-
// 如果有消息被清理,写回文件
|
|
130
153
|
const originalCount = Object.values(data.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
131
154
|
const cleanedCount = Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
132
155
|
|
|
@@ -134,15 +157,19 @@ export async function apply(ctx: Context, config: Config) {
|
|
|
134
157
|
fileManager.writeChatDataToFile(cleanedData)
|
|
135
158
|
logInfo('定期清理完成,清理了', originalCount - cleanedCount, '条超量消息')
|
|
136
159
|
}
|
|
137
|
-
}, 300000)
|
|
160
|
+
}, 300000)
|
|
138
161
|
})
|
|
139
162
|
|
|
140
|
-
// 注册所有 API 处理器
|
|
141
163
|
apiHandlers.registerApiHandlers()
|
|
142
164
|
|
|
143
|
-
// 注册控制台页面
|
|
144
165
|
ctx.console.addEntry({
|
|
145
166
|
dev: path.resolve(__dirname, '../client/index.ts'),
|
|
146
167
|
prod: path.resolve(__dirname, '../dist'),
|
|
147
168
|
})
|
|
169
|
+
|
|
170
|
+
ctx.on('dispose', () => {
|
|
171
|
+
|
|
172
|
+
fileManager.dispose()
|
|
173
|
+
logInfo('插件已卸载,所有待处理的消息已写入')
|
|
174
|
+
})
|
|
148
175
|
}
|
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
|
|