koishi-plugin-chat-patch 0.8.2 → 1.0.6

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/config.ts ADDED
@@ -0,0 +1,45 @@
1
+ import { Schema } from 'koishi'
2
+
3
+ export interface Config {
4
+ loggerinfo: boolean
5
+ maxMessagesPerChannel: number
6
+ keepMessagesOnClear: number
7
+ keepTempImages: number
8
+ blockedPlatforms: Array<{
9
+ platformName: string
10
+ exactMatch: boolean
11
+ }>
12
+ chatContainerHeight: number
13
+ }
14
+
15
+ export const Config: Schema<Config> = Schema.intersect([
16
+ Schema.object({
17
+ maxMessagesPerChannel: Schema.number().default(1000).description('每个群组最大保存消息数量').min(50).max(5000),
18
+ keepMessagesOnClear: Schema.number().default(50).description('手动清理历史记录时保留的消息数量').min(0).max(1000),
19
+ keepTempImages: Schema.number().default(50).description('发送消息保留的临时图片数量(最新的N张)').min(10).max(200),
20
+ blockedPlatforms: Schema.array(Schema.object({
21
+ platformName: Schema.string().description('平台名称或关键词'),
22
+ exactMatch: Schema.boolean().default(false).description('完全匹配?如果关闭,包含关键词即屏蔽').default(true)
23
+ })).role('table').description('屏蔽的平台列表').default(
24
+ [
25
+ {
26
+ "platformName": "qq",
27
+ "exactMatch": true
28
+ },
29
+ {
30
+ "platformName": "qqguild",
31
+ "exactMatch": true
32
+ },
33
+ {
34
+ "platformName": "sandbox",
35
+ "exactMatch": false
36
+ }
37
+ ]
38
+ ),
39
+ }).description('基础设置'),
40
+
41
+ Schema.object({
42
+ chatContainerHeight: Schema.number().default(80).description('手机端使用的视口高度(防止文本输入框被挡住)').min(50).max(100),
43
+ loggerinfo: Schema.boolean().default(false).description('日志调试模式').experimental(),
44
+ }).description('进阶设置'),
45
+ ])
@@ -0,0 +1,162 @@
1
+ import path from 'node:path'
2
+ import fs from 'node:fs'
3
+ import { Context, Logger } from 'koishi'
4
+ import { ChatData, MessageInfo } from './types'
5
+ import { Config } from './config'
6
+
7
+ export class FileManager {
8
+ private dataFilePath: string
9
+ private fileOperationLock = Promise.resolve()
10
+ private logger: Logger
11
+
12
+ constructor(
13
+ private ctx: Context,
14
+ private config: Config
15
+ ) {
16
+ this.dataFilePath = path.resolve(ctx.baseDir, 'data', 'chat-patch', 'chat-data.json')
17
+ this.logger = ctx.logger('chat-patch')
18
+ }
19
+
20
+ // 确保目录存在
21
+ private ensureDataDir() {
22
+ const dir = path.dirname(this.dataFilePath)
23
+ if (!fs.existsSync(dir)) {
24
+ fs.mkdirSync(dir, { recursive: true })
25
+ }
26
+ }
27
+
28
+ // 从JSON文件读取数据
29
+ readChatDataFromFile(): ChatData {
30
+ try {
31
+ if (fs.existsSync(this.dataFilePath)) {
32
+ const jsonData = fs.readFileSync(this.dataFilePath, 'utf8')
33
+ const data = JSON.parse(jsonData)
34
+ return {
35
+ bots: data.bots || {},
36
+ channels: data.channels || {},
37
+ messages: data.messages || {},
38
+ pinnedBots: data.pinnedBots || [],
39
+ pinnedChannels: data.pinnedChannels || [],
40
+ lastSaveTime: data.lastSaveTime
41
+ }
42
+ }
43
+ } catch (error) {
44
+ this.logger.error('读取聊天数据失败:', error)
45
+ }
46
+ return {
47
+ bots: {},
48
+ channels: {},
49
+ messages: {},
50
+ pinnedBots: [],
51
+ pinnedChannels: []
52
+ }
53
+ }
54
+
55
+ // 写入数据到JSON文件
56
+ writeChatDataToFile(data: ChatData) {
57
+ try {
58
+ this.ensureDataDir()
59
+ data.lastSaveTime = Date.now()
60
+ const jsonData = JSON.stringify(data, null, 2)
61
+ fs.writeFileSync(this.dataFilePath, jsonData, 'utf8')
62
+ } catch (error) {
63
+ this.logger.error('写入聊天数据失败:', error)
64
+ }
65
+ }
66
+
67
+ // 清理超量消息
68
+ cleanExcessMessages(data: ChatData): ChatData {
69
+ let cleanedCount = 0
70
+ const cleanedMessages: Record<string, MessageInfo[]> = {}
71
+
72
+ for (const [channelKey, messages] of Object.entries(data.messages)) {
73
+ if (messages.length > this.config.maxMessagesPerChannel) {
74
+ const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp)
75
+ const keptMessages = sortedMessages.slice(-this.config.maxMessagesPerChannel)
76
+ cleanedCount += messages.length - keptMessages.length
77
+ cleanedMessages[channelKey] = keptMessages
78
+ this.logInfo(`频道 ${channelKey} 清理了 ${messages.length - keptMessages.length} 条旧消息,保留最新 ${keptMessages.length} 条`)
79
+ } else {
80
+ cleanedMessages[channelKey] = messages
81
+ }
82
+ }
83
+
84
+ if (cleanedCount > 0) {
85
+ this.logInfo('总共清理超量消息:', cleanedCount, '条')
86
+ }
87
+
88
+ return {
89
+ ...data,
90
+ messages: cleanedMessages
91
+ }
92
+ }
93
+
94
+ // 添加消息到JSON文件(使用锁机制防止并发冲突)
95
+ async addMessageToFile(messageInfo: MessageInfo) {
96
+ this.fileOperationLock = this.fileOperationLock.then(async () => {
97
+ const data = this.readChatDataFromFile()
98
+ const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`
99
+
100
+ if (!data.messages[channelKey]) {
101
+ data.messages[channelKey] = []
102
+ }
103
+
104
+ // 检查消息是否已存在
105
+ const existingMessage = data.messages[channelKey].find(m => m.id === messageInfo.id)
106
+ if (existingMessage) {
107
+ this.logInfo('消息已存在,跳过保存:', {
108
+ channelKey: channelKey,
109
+ messageId: messageInfo.id,
110
+ existingType: existingMessage.type,
111
+ existingContent: existingMessage.content,
112
+ newType: messageInfo.type,
113
+ newContent: messageInfo.content
114
+ })
115
+ return
116
+ }
117
+
118
+ if (!messageInfo.timestamp) {
119
+ messageInfo.timestamp = Date.now()
120
+ }
121
+
122
+ const beforeCount = data.messages[channelKey].length
123
+ data.messages[channelKey].push(messageInfo)
124
+ const afterCount = data.messages[channelKey].length
125
+
126
+ // 限制消息数量 - 保留最新的消息
127
+ if (data.messages[channelKey].length > this.config.maxMessagesPerChannel) {
128
+ data.messages[channelKey].sort((a, b) => a.timestamp - b.timestamp)
129
+ const removedCount = data.messages[channelKey].length - this.config.maxMessagesPerChannel
130
+ data.messages[channelKey] = data.messages[channelKey].slice(-this.config.maxMessagesPerChannel)
131
+ this.logInfo(`频道 ${channelKey} 达到消息上限,清理了 ${removedCount} 条旧消息`)
132
+ }
133
+
134
+ this.writeChatDataToFile(data)
135
+
136
+ const isCommandMessage = messageInfo.content?.startsWith('++') || messageInfo.content?.startsWith('.')
137
+
138
+ this.logInfo('添加消息到文件:', {
139
+ channelKey: channelKey,
140
+ messageId: messageInfo.id,
141
+ content: messageInfo.content,
142
+ type: messageInfo.type,
143
+ userId: messageInfo.userId,
144
+ username: messageInfo.username,
145
+ timestamp: messageInfo.timestamp,
146
+ isCommandMessage: isCommandMessage,
147
+ 消息数变化: `${beforeCount} -> ${afterCount} -> ${data.messages[channelKey].length}`
148
+ })
149
+
150
+ }).catch(error => {
151
+ this.logger.error('保存消息时发生错误:', error)
152
+ })
153
+
154
+ await this.fileOperationLock
155
+ }
156
+
157
+ private logInfo(...args: any[]) {
158
+ if (this.config.loggerinfo) {
159
+ (this.logger.info as (...args: any[]) => void)(...args)
160
+ }
161
+ }
162
+ }