koishi-plugin-chat-patch 1.2.0 → 1.3.0

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