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/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
+ }