koishi-plugin-chat-patch 0.8.3 → 1.0.7
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/vue/index.vue +608 -22
- package/client/vue/style.css +470 -0
- package/dist/index.js +8 -8
- package/dist/style.css +1 -1
- package/lib/index.js +814 -450
- package/package.json +1 -1
- package/src/api-handlers.ts +593 -0
- package/src/config.ts +45 -0
- package/src/file-manager.ts +162 -0
- package/src/index.ts +45 -753
- package/src/message-handler.ts +247 -0
- package/src/types.ts +58 -0
- package/src/utils.ts +43 -0
- package/lib/index.d.ts +0 -20
package/src/index.ts
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
|
-
import { Context
|
|
1
|
+
import { Context } from 'koishi'
|
|
2
2
|
import { } from '@koishijs/plugin-console'
|
|
3
|
-
|
|
4
3
|
import path from 'node:path'
|
|
5
4
|
import fs from 'node:fs'
|
|
6
5
|
|
|
6
|
+
import { Config } from './config'
|
|
7
|
+
import { FileManager } from './file-manager'
|
|
8
|
+
import { MessageHandler } from './message-handler'
|
|
9
|
+
import { ApiHandlers } from './api-handlers'
|
|
10
|
+
import { Utils } from './utils'
|
|
11
|
+
|
|
7
12
|
export const name = 'chat-patch'
|
|
8
13
|
export const reusable = false
|
|
9
14
|
export const filter = false
|
|
@@ -22,119 +27,34 @@ export const usage = `
|
|
|
22
27
|
---
|
|
23
28
|
`
|
|
24
29
|
|
|
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
|
-
}
|
|
30
|
+
export { Config } from './config'
|
|
123
31
|
|
|
124
32
|
export async function apply(ctx: Context, config: Config) {
|
|
125
33
|
const logger = ctx.logger('chat-patch')
|
|
126
|
-
const dataFilePath = path.resolve(ctx.baseDir, 'data', 'chat-patch', 'chat-data.json')
|
|
127
34
|
|
|
128
|
-
//
|
|
129
|
-
const
|
|
130
|
-
const
|
|
35
|
+
// 初始化各个模块
|
|
36
|
+
const fileManager = new FileManager(ctx, config)
|
|
37
|
+
const messageHandler = new MessageHandler(ctx, config, fileManager)
|
|
38
|
+
const apiHandlers = new ApiHandlers(ctx, config, fileManager)
|
|
39
|
+
const utils = new Utils(config)
|
|
40
|
+
|
|
41
|
+
// 初始化数据
|
|
42
|
+
const initialData = fileManager.readChatDataFromFile()
|
|
43
|
+
const cleanedData = fileManager.cleanExcessMessages(initialData)
|
|
131
44
|
|
|
132
45
|
// 如果清理了数据,立即写回文件
|
|
133
46
|
const originalCount = Object.values(initialData.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
134
47
|
const cleanedCount = Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
135
48
|
|
|
136
49
|
if (originalCount !== cleanedCount) {
|
|
137
|
-
writeChatDataToFile(cleanedData)
|
|
50
|
+
fileManager.writeChatDataToFile(cleanedData)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// 日志调试函数
|
|
54
|
+
function logInfo(...args: any[]) {
|
|
55
|
+
if (config.loggerinfo) {
|
|
56
|
+
(logger.info as (...args: any[]) => void)(...args)
|
|
57
|
+
}
|
|
138
58
|
}
|
|
139
59
|
|
|
140
60
|
logInfo('插件加载完成,数据统计:', {
|
|
@@ -145,16 +65,27 @@ export async function apply(ctx: Context, config: Config) {
|
|
|
145
65
|
总消息数: Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
146
66
|
})
|
|
147
67
|
|
|
148
|
-
//
|
|
68
|
+
// 监听用户消息
|
|
149
69
|
ctx.on('message', async (session) => {
|
|
150
70
|
// 检查平台是否被屏蔽
|
|
151
|
-
if (isPlatformBlocked(session.platform || 'unknown')) {
|
|
71
|
+
if (utils.isPlatformBlocked(session.platform || 'unknown')) {
|
|
152
72
|
logInfo(`忽略来自被屏蔽平台的消息: ${session.platform}`)
|
|
153
73
|
return
|
|
154
74
|
}
|
|
155
75
|
|
|
156
|
-
//
|
|
157
|
-
await broadcastMessageEvent(session)
|
|
76
|
+
// 广播消息事件给前端处理
|
|
77
|
+
await messageHandler.broadcastMessageEvent(session)
|
|
78
|
+
})
|
|
79
|
+
|
|
80
|
+
// 监听机器人发送的消息
|
|
81
|
+
ctx.on('before-send', async (session) => {
|
|
82
|
+
// 检查平台是否被屏蔽
|
|
83
|
+
if (utils.isPlatformBlocked(session.platform || 'unknown')) {
|
|
84
|
+
logInfo(`忽略来自被屏蔽平台的机器人消息: ${session.platform}`)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
// 广播机器人消息事件给前端处理
|
|
88
|
+
await messageHandler.broadcastBotMessageEvent(session)
|
|
158
89
|
})
|
|
159
90
|
|
|
160
91
|
// 插件启动时设置定期清理过期消息
|
|
@@ -163,665 +94,26 @@ export async function apply(ctx: Context, config: Config) {
|
|
|
163
94
|
|
|
164
95
|
// 定期清理超量消息
|
|
165
96
|
setInterval(() => {
|
|
166
|
-
const data = readChatDataFromFile()
|
|
167
|
-
const cleanedData = cleanExcessMessages(data)
|
|
97
|
+
const data = fileManager.readChatDataFromFile()
|
|
98
|
+
const cleanedData = fileManager.cleanExcessMessages(data)
|
|
168
99
|
|
|
169
100
|
// 如果有消息被清理,写回文件
|
|
170
101
|
const originalCount = Object.values(data.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
171
102
|
const cleanedCount = Object.values(cleanedData.messages).reduce((total, msgs) => total + msgs.length, 0)
|
|
172
103
|
|
|
173
104
|
if (originalCount !== cleanedCount) {
|
|
174
|
-
writeChatDataToFile(cleanedData)
|
|
105
|
+
fileManager.writeChatDataToFile(cleanedData)
|
|
175
106
|
logInfo('定期清理完成,清理了', originalCount - cleanedCount, '条超量消息')
|
|
176
107
|
}
|
|
177
108
|
}, 300000) // 每5分钟清理一次
|
|
178
109
|
})
|
|
179
110
|
|
|
180
|
-
//
|
|
181
|
-
|
|
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
|
-
})
|
|
111
|
+
// 注册所有 API 处理器
|
|
112
|
+
apiHandlers.registerApiHandlers()
|
|
555
113
|
|
|
556
114
|
// 注册控制台页面
|
|
557
115
|
ctx.console.addEntry({
|
|
558
116
|
dev: path.resolve(__dirname, '../client/index.ts'),
|
|
559
117
|
prod: path.resolve(__dirname, '../dist'),
|
|
560
118
|
})
|
|
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
119
|
}
|