koishi-plugin-chat-patch 0.8.2
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 -0
- package/client/icons/index.ts +4 -0
- package/client/index.scss +5 -0
- package/client/index.ts +23 -0
- package/client/vue/index.vue +2315 -0
- package/client/vue/style.css +1372 -0
- package/dist/index.js +27 -0
- package/dist/style.css +1 -0
- package/lib/index.d.ts +20 -0
- package/lib/index.js +594 -0
- package/package.json +40 -0
- package/readme.md +25 -0
- package/src/index.ts +827 -0
package/src/index.ts
ADDED
|
@@ -0,0 +1,827 @@
|
|
|
1
|
+
import { Context, Schema, Session, h } from 'koishi'
|
|
2
|
+
import { } from '@koishijs/plugin-console'
|
|
3
|
+
|
|
4
|
+
import path from 'node:path'
|
|
5
|
+
import fs from 'node:fs'
|
|
6
|
+
|
|
7
|
+
export const name = 'chat-patch'
|
|
8
|
+
export const reusable = false
|
|
9
|
+
export const filter = false
|
|
10
|
+
export const inject = {
|
|
11
|
+
required: ['console']
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export const usage = `
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
开启后,即可在koishi控制台操作机器人收发消息啦
|
|
19
|
+
|
|
20
|
+
暂时只支持接受图文消息 / 发送文字消息
|
|
21
|
+
|
|
22
|
+
---
|
|
23
|
+
`
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
export interface Config {
|
|
27
|
+
loggerinfo: boolean
|
|
28
|
+
maxMessagesPerChannel: number
|
|
29
|
+
keepMessagesOnClear: number
|
|
30
|
+
blockedPlatforms: Array<{
|
|
31
|
+
platformName: string
|
|
32
|
+
exactMatch: boolean
|
|
33
|
+
}>
|
|
34
|
+
chatContainerHeight: number
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export const Config: Schema<Config> = Schema.intersect([
|
|
38
|
+
Schema.object({
|
|
39
|
+
maxMessagesPerChannel: Schema.number().default(1000).description('每个群组最大保存消息数量').min(50).max(5000),
|
|
40
|
+
keepMessagesOnClear: Schema.number().default(0).description('手动清理历史记录时保留的消息数量').min(0).max(1000),
|
|
41
|
+
blockedPlatforms: Schema.array(Schema.object({
|
|
42
|
+
platformName: Schema.string().description('平台名称或关键词'),
|
|
43
|
+
exactMatch: Schema.boolean().default(false).description('完全匹配?如果关闭,包含关键词即屏蔽').default(true)
|
|
44
|
+
})).role('table').description('屏蔽的平台列表').default([
|
|
45
|
+
{
|
|
46
|
+
"platformName": "qq",
|
|
47
|
+
"exactMatch": true
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
"platformName": "qqguild",
|
|
51
|
+
"exactMatch": true
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
"platformName": "sandbox",
|
|
55
|
+
"exactMatch": false
|
|
56
|
+
}
|
|
57
|
+
]),
|
|
58
|
+
}).description('基础设置'),
|
|
59
|
+
|
|
60
|
+
Schema.object({
|
|
61
|
+
chatContainerHeight: Schema.number().default(80).description('手机端使用的视口高度(防止文本输入框被挡住)').min(50).max(100),
|
|
62
|
+
loggerinfo: Schema.boolean().default(false).description('日志调试模式').experimental(),
|
|
63
|
+
}).description('进阶设置'),
|
|
64
|
+
])
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
interface BotInfo {
|
|
68
|
+
selfId: string
|
|
69
|
+
platform: string
|
|
70
|
+
username: string
|
|
71
|
+
avatar?: string
|
|
72
|
+
status: 'online' | 'offline'
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
interface ChannelInfo {
|
|
76
|
+
id: string
|
|
77
|
+
name: string
|
|
78
|
+
type: number | string
|
|
79
|
+
guildId?: string
|
|
80
|
+
guildName?: string
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
interface QuoteInfo {
|
|
84
|
+
messageId: string
|
|
85
|
+
id: string
|
|
86
|
+
content: string
|
|
87
|
+
elements?: h[]
|
|
88
|
+
user: {
|
|
89
|
+
id: string
|
|
90
|
+
name: string
|
|
91
|
+
userId: string
|
|
92
|
+
avatar?: string
|
|
93
|
+
username: string
|
|
94
|
+
}
|
|
95
|
+
timestamp: number
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
interface MessageInfo {
|
|
99
|
+
id: string
|
|
100
|
+
content: string
|
|
101
|
+
userId: string
|
|
102
|
+
username: string
|
|
103
|
+
avatar?: string
|
|
104
|
+
timestamp: number
|
|
105
|
+
channelId: string
|
|
106
|
+
selfId: string
|
|
107
|
+
elements?: h[]
|
|
108
|
+
type: 'user' | 'bot'
|
|
109
|
+
guildId?: string
|
|
110
|
+
guildName?: string
|
|
111
|
+
platform: string
|
|
112
|
+
quote?: QuoteInfo
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
interface ChatData {
|
|
116
|
+
bots: Record<string, BotInfo>
|
|
117
|
+
channels: Record<string, Record<string, ChannelInfo>>
|
|
118
|
+
messages: Record<string, MessageInfo[]>
|
|
119
|
+
pinnedBots: string[]
|
|
120
|
+
pinnedChannels: string[]
|
|
121
|
+
lastSaveTime?: number
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export async function apply(ctx: Context, config: Config) {
|
|
125
|
+
const logger = ctx.logger('chat-patch')
|
|
126
|
+
const dataFilePath = path.resolve(ctx.baseDir, 'data', 'chat-patch', 'chat-data.json')
|
|
127
|
+
|
|
128
|
+
// 初始化
|
|
129
|
+
const initialData = readChatDataFromFile()
|
|
130
|
+
const cleanedData = cleanExcessMessages(initialData)
|
|
131
|
+
|
|
132
|
+
// 如果清理了数据,立即写回文件
|
|
133
|
+
const originalCount = Object.values(initialData.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
134
|
+
const cleanedCount = Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
135
|
+
|
|
136
|
+
if (originalCount !== cleanedCount) {
|
|
137
|
+
writeChatDataToFile(cleanedData)
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
logInfo('插件加载完成,数据统计:', {
|
|
141
|
+
机器人数量: Object.keys(cleanedData.bots).length,
|
|
142
|
+
频道数量: Object.keys(cleanedData.channels).reduce((total, botId) =>
|
|
143
|
+
total + Object.keys(cleanedData.channels[botId] || {}).length, 0),
|
|
144
|
+
消息频道数: Object.keys(cleanedData.messages).length,
|
|
145
|
+
总消息数: Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
// 监听消息,直接广播给前端处理
|
|
149
|
+
ctx.on('message', async (session) => {
|
|
150
|
+
// 检查平台是否被屏蔽
|
|
151
|
+
if (isPlatformBlocked(session.platform || 'unknown')) {
|
|
152
|
+
logInfo(`忽略来自被屏蔽平台的消息: ${session.platform}`)
|
|
153
|
+
return
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// 直接广播消息事件给前端处理
|
|
157
|
+
await broadcastMessageEvent(session)
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
// 插件启动时设置定期清理过期消息
|
|
161
|
+
ctx.on('ready', async () => {
|
|
162
|
+
logInfo('插件启动完成,开始监听消息')
|
|
163
|
+
|
|
164
|
+
// 定期清理超量消息
|
|
165
|
+
setInterval(() => {
|
|
166
|
+
const data = readChatDataFromFile()
|
|
167
|
+
const cleanedData = cleanExcessMessages(data)
|
|
168
|
+
|
|
169
|
+
// 如果有消息被清理,写回文件
|
|
170
|
+
const originalCount = Object.values(data.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
171
|
+
const cleanedCount = Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
172
|
+
|
|
173
|
+
if (originalCount !== cleanedCount) {
|
|
174
|
+
writeChatDataToFile(cleanedData)
|
|
175
|
+
logInfo('定期清理完成,清理了', originalCount - cleanedCount, '条超量消息')
|
|
176
|
+
}
|
|
177
|
+
}, 300000) // 每5分钟清理一次
|
|
178
|
+
})
|
|
179
|
+
|
|
180
|
+
// 获取所有聊天数据的 API
|
|
181
|
+
ctx.console.addListener('get-chat-data' as any, async () => {
|
|
182
|
+
try {
|
|
183
|
+
const data = readChatDataFromFile()
|
|
184
|
+
const cleanedData = cleanExcessMessages(data)
|
|
185
|
+
|
|
186
|
+
logInfo('获取聊天数据:', {
|
|
187
|
+
机器人数量: Object.keys(cleanedData.bots).length,
|
|
188
|
+
频道数量: Object.keys(cleanedData.channels).reduce((total, botId) =>
|
|
189
|
+
total + Object.keys(cleanedData.channels[botId] || {}).length, 0),
|
|
190
|
+
消息频道数: Object.keys(cleanedData.messages).length,
|
|
191
|
+
总消息数: Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
192
|
+
})
|
|
193
|
+
|
|
194
|
+
return {
|
|
195
|
+
success: true,
|
|
196
|
+
data: {
|
|
197
|
+
...cleanedData,
|
|
198
|
+
pinnedBots: cleanedData.pinnedBots, // 包含置顶机器人
|
|
199
|
+
pinnedChannels: cleanedData.pinnedChannels // 包含置顶频道
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
} catch (error: any) {
|
|
203
|
+
logger.error('获取聊天数据失败:', error)
|
|
204
|
+
return { success: false, error: error?.message || String(error) }
|
|
205
|
+
}
|
|
206
|
+
})
|
|
207
|
+
|
|
208
|
+
// 获取历史消息的 API
|
|
209
|
+
ctx.console.addListener('get-history-messages' as any, async (requestData: {
|
|
210
|
+
selfId: string
|
|
211
|
+
channelId: string
|
|
212
|
+
}) => {
|
|
213
|
+
try {
|
|
214
|
+
const data = readChatDataFromFile()
|
|
215
|
+
const channelKey = `${requestData.selfId}-${requestData.channelId}`
|
|
216
|
+
const messages = data.messages[channelKey] || []
|
|
217
|
+
|
|
218
|
+
// 按时间戳排序返回消息
|
|
219
|
+
const sortedMessages = messages.sort((a, b) => a.timestamp - b.timestamp)
|
|
220
|
+
|
|
221
|
+
logInfo('获取历史消息:', channelKey, '共', sortedMessages.length, '条消息')
|
|
222
|
+
|
|
223
|
+
return {
|
|
224
|
+
success: true,
|
|
225
|
+
messages: sortedMessages
|
|
226
|
+
}
|
|
227
|
+
} catch (error: any) {
|
|
228
|
+
logger.error('获取历史消息失败:', error)
|
|
229
|
+
return { success: false, error: error?.message || String(error), messages: [] }
|
|
230
|
+
}
|
|
231
|
+
})
|
|
232
|
+
|
|
233
|
+
// 获取所有频道消息数量的 API
|
|
234
|
+
ctx.console.addListener('get-all-channel-message-counts' as any, async () => {
|
|
235
|
+
try {
|
|
236
|
+
const data = readChatDataFromFile()
|
|
237
|
+
const counts: Record<string, number> = {}
|
|
238
|
+
|
|
239
|
+
// 统计每个频道的消息数量
|
|
240
|
+
for (const [channelKey, messages] of Object.entries(data.messages)) {
|
|
241
|
+
counts[channelKey] = messages.length
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
logInfo('获取所有频道消息数量:', {
|
|
245
|
+
频道数: Object.keys(counts).length,
|
|
246
|
+
总消息数: Object.values(counts).reduce((total, count) => total + count, 0)
|
|
247
|
+
})
|
|
248
|
+
|
|
249
|
+
return {
|
|
250
|
+
success: true,
|
|
251
|
+
counts: counts
|
|
252
|
+
}
|
|
253
|
+
} catch (error: any) {
|
|
254
|
+
logger.error('获取频道消息数量失败:', error)
|
|
255
|
+
return { success: false, error: error?.message || String(error), counts: {} }
|
|
256
|
+
}
|
|
257
|
+
})
|
|
258
|
+
|
|
259
|
+
// 图片获取 API
|
|
260
|
+
// 返回base64数据
|
|
261
|
+
ctx.console.addListener('fetch-image' as any, async (data: { url: string }) => {
|
|
262
|
+
try {
|
|
263
|
+
const response = await fetch(data.url, {
|
|
264
|
+
headers: {
|
|
265
|
+
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
|
|
266
|
+
'Referer': ''
|
|
267
|
+
}
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
if (!response.ok) {
|
|
271
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
const buffer = await response.arrayBuffer()
|
|
275
|
+
const base64 = Buffer.from(buffer).toString('base64')
|
|
276
|
+
const contentType = response.headers.get('content-type') || 'image/jpeg'
|
|
277
|
+
return {
|
|
278
|
+
success: true,
|
|
279
|
+
base64: base64,
|
|
280
|
+
contentType: contentType,
|
|
281
|
+
dataUrl: `data:${contentType};base64,${base64}`
|
|
282
|
+
}
|
|
283
|
+
} catch (error: any) {
|
|
284
|
+
// 无需打印错误
|
|
285
|
+
// logger.error('获取图片失败:', error)
|
|
286
|
+
return { success: false, error: error?.message || String(error) }
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
// 清理频道历史记录的 API
|
|
291
|
+
ctx.console.addListener('clear-channel-history' as any, async (data: {
|
|
292
|
+
selfId: string
|
|
293
|
+
channelId: string
|
|
294
|
+
keepCount?: number
|
|
295
|
+
}) => {
|
|
296
|
+
try {
|
|
297
|
+
logInfo('收到清理历史记录请求:', data)
|
|
298
|
+
|
|
299
|
+
const chatData = readChatDataFromFile()
|
|
300
|
+
const channelKey = `${data.selfId}-${data.channelId}`
|
|
301
|
+
|
|
302
|
+
if (!chatData.messages[channelKey]) {
|
|
303
|
+
return { success: true, message: '频道没有历史消息' }
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const messages = chatData.messages[channelKey]
|
|
307
|
+
const originalCount = messages.length
|
|
308
|
+
|
|
309
|
+
// 使用配置的保留数量,如果请求中没有指定则使用配置默认值
|
|
310
|
+
const keepCount = data.keepCount || config.keepMessagesOnClear
|
|
311
|
+
|
|
312
|
+
if (keepCount > 0 && originalCount <= keepCount) {
|
|
313
|
+
return { success: true, message: `消息数量(${originalCount})未超过保留数量(${keepCount}),无需清理` }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
// 按时间戳排序,保留最新的消息
|
|
317
|
+
const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp)
|
|
318
|
+
const keptMessages = keepCount > 0 ? sortedMessages.slice(-keepCount) : []
|
|
319
|
+
const clearedCount = originalCount - keptMessages.length
|
|
320
|
+
|
|
321
|
+
// 更新数据
|
|
322
|
+
chatData.messages[channelKey] = keptMessages
|
|
323
|
+
|
|
324
|
+
// 写入文件
|
|
325
|
+
writeChatDataToFile(chatData)
|
|
326
|
+
|
|
327
|
+
logInfo(`频道 ${channelKey} 历史记录清理完成:`, {
|
|
328
|
+
原始消息数: originalCount,
|
|
329
|
+
保留消息数: keptMessages.length,
|
|
330
|
+
清理消息数: clearedCount
|
|
331
|
+
})
|
|
332
|
+
|
|
333
|
+
return {
|
|
334
|
+
success: true,
|
|
335
|
+
message: `成功清理 ${clearedCount} 条历史消息,保留最新 ${keptMessages.length} 条`,
|
|
336
|
+
clearedCount: clearedCount,
|
|
337
|
+
keptCount: keptMessages.length
|
|
338
|
+
}
|
|
339
|
+
} catch (error: any) {
|
|
340
|
+
logger.error('清理频道历史记录失败:', error)
|
|
341
|
+
return { success: false, error: error?.message || String(error) }
|
|
342
|
+
}
|
|
343
|
+
})
|
|
344
|
+
|
|
345
|
+
// 发送消息的 API
|
|
346
|
+
ctx.console.addListener('send-message' as any, async (data: {
|
|
347
|
+
selfId: string
|
|
348
|
+
channelId: string
|
|
349
|
+
content: string
|
|
350
|
+
}) => {
|
|
351
|
+
try {
|
|
352
|
+
logInfo('收到发送消息请求:', data)
|
|
353
|
+
|
|
354
|
+
// 找到对应的机器人
|
|
355
|
+
const bot = ctx.bots.find((bot: any) => bot.selfId === data.selfId)
|
|
356
|
+
if (!bot) {
|
|
357
|
+
logger.error('未找到机器人:', data.selfId)
|
|
358
|
+
return { success: false, error: '未找到指定的机器人' }
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// 使用 bot.sendMessage 发送消息
|
|
362
|
+
const result = await bot.sendMessage(data.channelId, data.content)
|
|
363
|
+
logInfo('消息发送成功:', result)
|
|
364
|
+
|
|
365
|
+
const timestamp = Date.now()
|
|
366
|
+
|
|
367
|
+
// 更新机器人信息到文件
|
|
368
|
+
const chatData = readChatDataFromFile()
|
|
369
|
+
const botInfo: BotInfo = {
|
|
370
|
+
selfId: bot.selfId,
|
|
371
|
+
platform: bot.platform || 'unknown',
|
|
372
|
+
username: bot.user?.name || `Bot-${data.selfId}`,
|
|
373
|
+
avatar: bot.user?.avatar,
|
|
374
|
+
status: 'online'
|
|
375
|
+
}
|
|
376
|
+
chatData.bots[bot.selfId] = botInfo
|
|
377
|
+
writeChatDataToFile(chatData)
|
|
378
|
+
|
|
379
|
+
// 创建机器人消息信息对象并直接添加到文件
|
|
380
|
+
const botMessageInfo: MessageInfo = {
|
|
381
|
+
id: Array.isArray(result) ? result[0] : result || `bot-msg-${timestamp}`,
|
|
382
|
+
content: data.content,
|
|
383
|
+
userId: bot.selfId,
|
|
384
|
+
username: bot.user?.name || `Bot-${data.selfId}`,
|
|
385
|
+
avatar: bot.user?.avatar,
|
|
386
|
+
timestamp: timestamp,
|
|
387
|
+
channelId: data.channelId,
|
|
388
|
+
selfId: data.selfId,
|
|
389
|
+
type: 'bot',
|
|
390
|
+
platform: bot.platform || 'unknown'
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
// 直接添加到文件
|
|
394
|
+
addMessageToFile(botMessageInfo)
|
|
395
|
+
|
|
396
|
+
// 发送成功后,广播机器人发送的消息事件给前端
|
|
397
|
+
const sentMessageEvent = {
|
|
398
|
+
type: 'bot-message-sent',
|
|
399
|
+
selfId: data.selfId,
|
|
400
|
+
channelId: data.channelId,
|
|
401
|
+
messageId: Array.isArray(result) ? result[0] : result,
|
|
402
|
+
content: data.content,
|
|
403
|
+
timestamp: timestamp,
|
|
404
|
+
botUsername: bot.user?.name || `Bot-${data.selfId}`,
|
|
405
|
+
botAvatar: bot.user?.avatar
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
ctx.console.broadcast('bot-message-sent-event', sentMessageEvent)
|
|
409
|
+
|
|
410
|
+
return { success: true, messageId: Array.isArray(result) ? result[0] : result }
|
|
411
|
+
} catch (error: any) {
|
|
412
|
+
logger.error('发送消息失败:', error)
|
|
413
|
+
return { success: false, error: error?.message || String(error) }
|
|
414
|
+
}
|
|
415
|
+
})
|
|
416
|
+
|
|
417
|
+
// 删除机器人所有数据的 API
|
|
418
|
+
ctx.console.addListener('delete-bot-data' as any, async (data: {
|
|
419
|
+
selfId: string
|
|
420
|
+
}) => {
|
|
421
|
+
try {
|
|
422
|
+
logInfo('收到删除机器人数据请求:', data)
|
|
423
|
+
|
|
424
|
+
const chatData = readChatDataFromFile()
|
|
425
|
+
let deletedChannels = 0
|
|
426
|
+
let deletedMessages = 0
|
|
427
|
+
|
|
428
|
+
// 删除该机器人的所有频道
|
|
429
|
+
if (chatData.channels[data.selfId]) {
|
|
430
|
+
deletedChannels = Object.keys(chatData.channels[data.selfId]).length
|
|
431
|
+
delete chatData.channels[data.selfId]
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
// 删除该机器人的所有消息
|
|
435
|
+
const channelsToDelete = Object.keys(chatData.messages).filter(key => key.startsWith(`${data.selfId}-`))
|
|
436
|
+
for (const channelKey of channelsToDelete) {
|
|
437
|
+
deletedMessages += chatData.messages[channelKey].length
|
|
438
|
+
delete chatData.messages[channelKey]
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// 删除机器人信息
|
|
442
|
+
delete chatData.bots[data.selfId]
|
|
443
|
+
|
|
444
|
+
// 写入文件
|
|
445
|
+
writeChatDataToFile(chatData)
|
|
446
|
+
|
|
447
|
+
logInfo(`机器人 ${data.selfId} 数据删除完成:`, {
|
|
448
|
+
删除频道数: deletedChannels,
|
|
449
|
+
删除消息数: deletedMessages
|
|
450
|
+
})
|
|
451
|
+
|
|
452
|
+
return {
|
|
453
|
+
success: true,
|
|
454
|
+
message: `成功删除机器人数据:${deletedChannels} 个频道,${deletedMessages} 条消息`,
|
|
455
|
+
deletedChannels,
|
|
456
|
+
deletedMessages
|
|
457
|
+
}
|
|
458
|
+
} catch (error: any) {
|
|
459
|
+
logger.error('删除机器人数据失败:', error)
|
|
460
|
+
return { success: false, error: error?.message || String(error) }
|
|
461
|
+
}
|
|
462
|
+
})
|
|
463
|
+
|
|
464
|
+
// 删除频道所有数据的 API
|
|
465
|
+
ctx.console.addListener('delete-channel-data' as any, async (data: {
|
|
466
|
+
selfId: string
|
|
467
|
+
channelId: string
|
|
468
|
+
}) => {
|
|
469
|
+
try {
|
|
470
|
+
logInfo('收到删除频道数据请求:', data)
|
|
471
|
+
|
|
472
|
+
const chatData = readChatDataFromFile()
|
|
473
|
+
const channelKey = `${data.selfId}-${data.channelId}`
|
|
474
|
+
let deletedMessages = 0
|
|
475
|
+
|
|
476
|
+
// 删除频道消息
|
|
477
|
+
if (chatData.messages[channelKey]) {
|
|
478
|
+
deletedMessages = chatData.messages[channelKey].length
|
|
479
|
+
delete chatData.messages[channelKey]
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
// 删除频道信息
|
|
483
|
+
if (chatData.channels[data.selfId] && chatData.channels[data.selfId][data.channelId]) {
|
|
484
|
+
delete chatData.channels[data.selfId][data.channelId]
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
// 写入文件
|
|
488
|
+
writeChatDataToFile(chatData)
|
|
489
|
+
|
|
490
|
+
logInfo(`频道 ${channelKey} 数据删除完成:`, {
|
|
491
|
+
删除消息数: deletedMessages
|
|
492
|
+
})
|
|
493
|
+
|
|
494
|
+
return {
|
|
495
|
+
success: true,
|
|
496
|
+
message: `成功删除频道数据:${deletedMessages} 条消息`,
|
|
497
|
+
deletedMessages
|
|
498
|
+
}
|
|
499
|
+
} catch (error: any) {
|
|
500
|
+
logger.error('删除频道数据失败:', error)
|
|
501
|
+
return { success: false, error: error?.message || String(error) }
|
|
502
|
+
}
|
|
503
|
+
})
|
|
504
|
+
|
|
505
|
+
// 设置置顶机器人列表的 API
|
|
506
|
+
ctx.console.addListener('set-pinned-bots' as any, async (data: {
|
|
507
|
+
pinnedBots: string[]
|
|
508
|
+
}) => {
|
|
509
|
+
try {
|
|
510
|
+
logInfo('收到设置置顶机器人请求:', data.pinnedBots)
|
|
511
|
+
const chatData = readChatDataFromFile()
|
|
512
|
+
chatData.pinnedBots = data.pinnedBots
|
|
513
|
+
writeChatDataToFile(chatData)
|
|
514
|
+
return { success: true }
|
|
515
|
+
} catch (error: any) {
|
|
516
|
+
logger.error('设置置顶机器人失败:', error)
|
|
517
|
+
return { success: false, error: error?.message || String(error) }
|
|
518
|
+
}
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
// 设置置顶频道列表的 API
|
|
522
|
+
ctx.console.addListener('set-pinned-channels' as any, async (data: {
|
|
523
|
+
pinnedChannels: string[]
|
|
524
|
+
}) => {
|
|
525
|
+
try {
|
|
526
|
+
logInfo('收到设置置顶频道请求:', data.pinnedChannels)
|
|
527
|
+
const chatData = readChatDataFromFile()
|
|
528
|
+
chatData.pinnedChannels = data.pinnedChannels
|
|
529
|
+
writeChatDataToFile(chatData)
|
|
530
|
+
return { success: true }
|
|
531
|
+
} catch (error: any) {
|
|
532
|
+
logger.error('设置置顶频道失败:', error)
|
|
533
|
+
return { success: false, error: error?.message || String(error) }
|
|
534
|
+
}
|
|
535
|
+
})
|
|
536
|
+
|
|
537
|
+
// 获取插件配置的 API
|
|
538
|
+
ctx.console.addListener('get-plugin-config' as any, async () => {
|
|
539
|
+
try {
|
|
540
|
+
return {
|
|
541
|
+
success: true,
|
|
542
|
+
config: {
|
|
543
|
+
maxMessagesPerChannel: config.maxMessagesPerChannel,
|
|
544
|
+
keepMessagesOnClear: config.keepMessagesOnClear,
|
|
545
|
+
loggerinfo: config.loggerinfo,
|
|
546
|
+
blockedPlatforms: config.blockedPlatforms || [],
|
|
547
|
+
chatContainerHeight: config.chatContainerHeight
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
} catch (error: any) {
|
|
551
|
+
logger.error('获取插件配置失败:', error)
|
|
552
|
+
return { success: false, error: error?.message || String(error) }
|
|
553
|
+
}
|
|
554
|
+
})
|
|
555
|
+
|
|
556
|
+
// 注册控制台页面
|
|
557
|
+
ctx.console.addEntry({
|
|
558
|
+
dev: path.resolve(__dirname, '../client/index.ts'),
|
|
559
|
+
prod: path.resolve(__dirname, '../dist'),
|
|
560
|
+
})
|
|
561
|
+
|
|
562
|
+
// 日志调试
|
|
563
|
+
function logInfo(...args: any[]) {
|
|
564
|
+
if (config.loggerinfo) {
|
|
565
|
+
(logger.info as (...args: any[]) => void)(...args)
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// 检查平台是否被屏蔽
|
|
570
|
+
function isPlatformBlocked(platform: string): boolean {
|
|
571
|
+
if (!config.blockedPlatforms || config.blockedPlatforms.length === 0) {
|
|
572
|
+
return false
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
for (const blockedPlatform of config.blockedPlatforms) {
|
|
576
|
+
if (blockedPlatform.exactMatch) {
|
|
577
|
+
// 完全匹配
|
|
578
|
+
if (platform === blockedPlatform.platformName) {
|
|
579
|
+
logInfo(`平台 ${platform} 被屏蔽 (完全匹配: ${blockedPlatform.platformName})`)
|
|
580
|
+
return true
|
|
581
|
+
}
|
|
582
|
+
} else {
|
|
583
|
+
// 包含匹配
|
|
584
|
+
if (platform.includes(blockedPlatform.platformName)) {
|
|
585
|
+
logInfo(`平台 ${platform} 被屏蔽 (包含匹配: ${blockedPlatform.platformName})`)
|
|
586
|
+
return true
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
return false
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// 确保目录存在
|
|
595
|
+
function ensureDataDir() {
|
|
596
|
+
const dir = path.dirname(dataFilePath)
|
|
597
|
+
if (!fs.existsSync(dir)) {
|
|
598
|
+
fs.mkdirSync(dir, { recursive: true })
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
// 从JSON文件读取数据
|
|
603
|
+
function readChatDataFromFile(): ChatData {
|
|
604
|
+
try {
|
|
605
|
+
if (fs.existsSync(dataFilePath)) {
|
|
606
|
+
const jsonData = fs.readFileSync(dataFilePath, 'utf8')
|
|
607
|
+
const data = JSON.parse(jsonData)
|
|
608
|
+
return {
|
|
609
|
+
bots: data.bots || {},
|
|
610
|
+
channels: data.channels || {},
|
|
611
|
+
messages: data.messages || {},
|
|
612
|
+
pinnedBots: data.pinnedBots || [], // 读取置顶机器人
|
|
613
|
+
pinnedChannels: data.pinnedChannels || [], // 读取置顶频道
|
|
614
|
+
lastSaveTime: data.lastSaveTime
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
} catch (error) {
|
|
618
|
+
logger.error('读取聊天数据失败:', error)
|
|
619
|
+
}
|
|
620
|
+
return {
|
|
621
|
+
bots: {},
|
|
622
|
+
channels: {},
|
|
623
|
+
messages: {},
|
|
624
|
+
pinnedBots: [], // 默认空数组
|
|
625
|
+
pinnedChannels: [] // 默认空数组
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// 写入数据到JSON文件
|
|
630
|
+
function writeChatDataToFile(data: ChatData) {
|
|
631
|
+
try {
|
|
632
|
+
ensureDataDir()
|
|
633
|
+
data.lastSaveTime = Date.now()
|
|
634
|
+
const jsonData = JSON.stringify(data, null, 2)
|
|
635
|
+
fs.writeFileSync(dataFilePath, jsonData, 'utf8')
|
|
636
|
+
} catch (error) {
|
|
637
|
+
logger.error('写入聊天数据失败:', error)
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
// 清理超量消息
|
|
642
|
+
// 保留最新的消息
|
|
643
|
+
function cleanExcessMessages(data: ChatData): ChatData {
|
|
644
|
+
let cleanedCount = 0
|
|
645
|
+
const cleanedMessages: Record<string, MessageInfo[]> = {}
|
|
646
|
+
|
|
647
|
+
for (const [channelKey, messages] of Object.entries(data.messages)) {
|
|
648
|
+
if (messages.length > config.maxMessagesPerChannel) {
|
|
649
|
+
// 按时间戳排序,保留最新的消息
|
|
650
|
+
const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp)
|
|
651
|
+
const keptMessages = sortedMessages.slice(-config.maxMessagesPerChannel)
|
|
652
|
+
cleanedCount += messages.length - keptMessages.length
|
|
653
|
+
cleanedMessages[channelKey] = keptMessages
|
|
654
|
+
logInfo(`频道 ${channelKey} 清理了 ${messages.length - keptMessages.length} 条旧消息,保留最新 ${keptMessages.length} 条`)
|
|
655
|
+
} else {
|
|
656
|
+
cleanedMessages[channelKey] = messages
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
if (cleanedCount > 0) {
|
|
661
|
+
logInfo('总共清理超量消息:', cleanedCount, '条')
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
return {
|
|
665
|
+
...data,
|
|
666
|
+
messages: cleanedMessages
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
// 添加消息到JSON文件
|
|
671
|
+
function addMessageToFile(messageInfo: MessageInfo) {
|
|
672
|
+
const data = readChatDataFromFile()
|
|
673
|
+
const channelKey = `${messageInfo.selfId}-${messageInfo.channelId}`
|
|
674
|
+
|
|
675
|
+
if (!data.messages[channelKey]) {
|
|
676
|
+
data.messages[channelKey] = []
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// 使用统一的时间戳
|
|
680
|
+
messageInfo.timestamp = Date.now()
|
|
681
|
+
data.messages[channelKey].push(messageInfo)
|
|
682
|
+
|
|
683
|
+
// 限制消息数量 - 保留最新的消息
|
|
684
|
+
if (data.messages[channelKey].length > config.maxMessagesPerChannel) {
|
|
685
|
+
// 按时间戳排序,保留最新的消息
|
|
686
|
+
data.messages[channelKey].sort((a, b) => a.timestamp - b.timestamp)
|
|
687
|
+
const removedCount = data.messages[channelKey].length - config.maxMessagesPerChannel
|
|
688
|
+
data.messages[channelKey] = data.messages[channelKey].slice(-config.maxMessagesPerChannel)
|
|
689
|
+
logInfo(`频道 ${channelKey} 达到消息上限,清理了 ${removedCount} 条旧消息`)
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
// 立即写入文件
|
|
693
|
+
writeChatDataToFile(data)
|
|
694
|
+
logInfo('添加消息到文件:', channelKey, '当前消息数:', data.messages[channelKey].length)
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// 更新机器人信息到JSON文件
|
|
698
|
+
function updateBotInfoToFile(session: Session) {
|
|
699
|
+
const data = readChatDataFromFile()
|
|
700
|
+
|
|
701
|
+
const botInfo: BotInfo = {
|
|
702
|
+
selfId: session.selfId,
|
|
703
|
+
platform: session.platform || 'unknown',
|
|
704
|
+
username: session.bot.user?.name || `Bot-${session.selfId}`,
|
|
705
|
+
avatar: session.bot.user?.avatar,
|
|
706
|
+
status: 'online'
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
data.bots[session.selfId] = botInfo
|
|
710
|
+
writeChatDataToFile(data)
|
|
711
|
+
logInfo('更新机器人信息到文件:', botInfo.username)
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
// 更新频道信息到JSON文件
|
|
715
|
+
async function updateChannelInfoToFile(session: Session) {
|
|
716
|
+
const data = readChatDataFromFile()
|
|
717
|
+
|
|
718
|
+
// 获取群组名称,如果失败则使用频道ID作为备用
|
|
719
|
+
let guildName = session.channelId
|
|
720
|
+
try {
|
|
721
|
+
if (session.guildId && session.bot.getGuild && typeof session.bot.getGuild === 'function') {
|
|
722
|
+
const guild = await session.bot.getGuild(session.guildId)
|
|
723
|
+
guildName = guild?.name || session.channelId
|
|
724
|
+
}
|
|
725
|
+
} catch (guildError) {
|
|
726
|
+
logInfo('获取群组信息失败,使用频道ID作为备用:', guildError)
|
|
727
|
+
guildName = session.channelId
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
if (!data.channels[session.selfId]) {
|
|
731
|
+
data.channels[session.selfId] = {}
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
const channelInfo: ChannelInfo = {
|
|
735
|
+
id: session.channelId,
|
|
736
|
+
name: session.guildId
|
|
737
|
+
? `${guildName} (${session.channelId})`
|
|
738
|
+
: `私信 ${session.channelId}`,
|
|
739
|
+
type: session.type || 0,
|
|
740
|
+
guildId: session.guildId,
|
|
741
|
+
guildName: guildName
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
data.channels[session.selfId][session.channelId] = channelInfo
|
|
745
|
+
writeChatDataToFile(data)
|
|
746
|
+
logInfo('更新频道信息到文件:', channelInfo.name)
|
|
747
|
+
|
|
748
|
+
return guildName
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
async function broadcastMessageEvent(session: Session) {
|
|
752
|
+
try {
|
|
753
|
+
// 更新机器人和频道信息到文件
|
|
754
|
+
updateBotInfoToFile(session)
|
|
755
|
+
const guildName = await updateChannelInfoToFile(session)
|
|
756
|
+
|
|
757
|
+
const timestamp = Date.now()
|
|
758
|
+
|
|
759
|
+
// 处理 quote 信息
|
|
760
|
+
let quoteInfo: QuoteInfo | undefined = undefined
|
|
761
|
+
if (session.quote) {
|
|
762
|
+
quoteInfo = {
|
|
763
|
+
messageId: session.quote.messageId || session.quote.id,
|
|
764
|
+
id: session.quote.id,
|
|
765
|
+
content: session.quote.content || '',
|
|
766
|
+
elements: session.quote.elements,
|
|
767
|
+
user: {
|
|
768
|
+
id: session.quote.user?.id || session.quote.user?.userId || 'unknown',
|
|
769
|
+
name: session.quote.user?.name || session.quote.user?.username || 'unknown',
|
|
770
|
+
userId: session.quote.user?.userId || session.quote.user?.id || 'unknown',
|
|
771
|
+
avatar: session.quote.user?.avatar,
|
|
772
|
+
username: session.quote.user?.username || session.quote.user?.name || 'unknown'
|
|
773
|
+
},
|
|
774
|
+
timestamp: session.quote.timestamp || Date.now()
|
|
775
|
+
}
|
|
776
|
+
}
|
|
777
|
+
|
|
778
|
+
// 创建消息信息对象
|
|
779
|
+
const messageInfo: MessageInfo = {
|
|
780
|
+
id: session.messageId || `msg-${timestamp}`,
|
|
781
|
+
content: session.content || '',
|
|
782
|
+
userId: session.userId || 'unknown',
|
|
783
|
+
username: session.username || session.userId || 'unknown',
|
|
784
|
+
avatar: session.author?.avatar,
|
|
785
|
+
timestamp: timestamp,
|
|
786
|
+
channelId: session.channelId,
|
|
787
|
+
selfId: session.selfId,
|
|
788
|
+
elements: session.elements,
|
|
789
|
+
type: 'user',
|
|
790
|
+
guildId: session.guildId,
|
|
791
|
+
guildName: guildName,
|
|
792
|
+
platform: session.platform || 'unknown',
|
|
793
|
+
quote: quoteInfo
|
|
794
|
+
}
|
|
795
|
+
|
|
796
|
+
// 直接添加到文件
|
|
797
|
+
addMessageToFile(messageInfo)
|
|
798
|
+
|
|
799
|
+
const messageEvent = {
|
|
800
|
+
type: 'message',
|
|
801
|
+
selfId: session.selfId,
|
|
802
|
+
platform: session.platform || 'unknown',
|
|
803
|
+
channelId: session.channelId,
|
|
804
|
+
messageId: session.messageId,
|
|
805
|
+
content: session.content,
|
|
806
|
+
userId: session.userId || 'unknown',
|
|
807
|
+
username: session.username || session.userId || 'unknown',
|
|
808
|
+
avatar: session.author?.avatar,
|
|
809
|
+
timestamp: timestamp,
|
|
810
|
+
guildId: session.guildId,
|
|
811
|
+
guildName: guildName,
|
|
812
|
+
channelType: session.type || 0,
|
|
813
|
+
elements: session.elements,
|
|
814
|
+
quote: quoteInfo,
|
|
815
|
+
bot: {
|
|
816
|
+
avatar: session.bot.user?.avatar,
|
|
817
|
+
name: session.bot.user?.name,
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
ctx.console.broadcast('chat-message-event', messageEvent)
|
|
822
|
+
} catch (error) {
|
|
823
|
+
logger.error('广播消息事件失败:', error)
|
|
824
|
+
}
|
|
825
|
+
}
|
|
826
|
+
|
|
827
|
+
}
|