koishi-plugin-chat-patch 2.3.0 → 2.4.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/dist/index.js +2 -2
- package/lib/file-manager.d.ts +11 -4
- package/lib/index.js +181 -52
- package/package.json +1 -1
- package/src/file-manager.ts +209 -57
package/src/file-manager.ts
CHANGED
|
@@ -7,16 +7,16 @@ import path from 'node:path'
|
|
|
7
7
|
import fs from 'node:fs'
|
|
8
8
|
|
|
9
9
|
export class FileManager {
|
|
10
|
-
private
|
|
11
|
-
private
|
|
10
|
+
private chatHistoryDir: string // 聊天记录根目录
|
|
11
|
+
private metadataFilePath: string // 元数据文件路径(存储bots、channels、pinned等信息)
|
|
12
12
|
private logger: Logger
|
|
13
13
|
private utils: Utils
|
|
14
14
|
|
|
15
15
|
private memoryCache: ChatData | null = null
|
|
16
16
|
|
|
17
|
-
private pendingMessages: MessageInfo[] =
|
|
17
|
+
private pendingMessages: Map<string, MessageInfo[]> = new Map() // 按channelKey分组的待写入消息
|
|
18
18
|
|
|
19
|
-
private
|
|
19
|
+
private writeTimers: Map<string, (() => void)> = new Map() // 每个频道独立的写入定时器
|
|
20
20
|
|
|
21
21
|
private readonly WRITE_DEBOUNCE_MS = 1000
|
|
22
22
|
|
|
@@ -24,45 +24,175 @@ export class FileManager {
|
|
|
24
24
|
private ctx: Context,
|
|
25
25
|
private config: Config
|
|
26
26
|
) {
|
|
27
|
-
|
|
27
|
+
const baseDir = path.resolve(ctx.baseDir, 'data', 'chat-patch')
|
|
28
|
+
this.chatHistoryDir = path.join(baseDir, 'chat-history')
|
|
29
|
+
this.metadataFilePath = path.join(baseDir, 'metadata.json')
|
|
28
30
|
this.logger = ctx.logger('chat-patch')
|
|
29
31
|
this.utils = new Utils(config)
|
|
30
32
|
|
|
33
|
+
// 清理旧版本的单文件存储
|
|
34
|
+
this.cleanupOldDataFiles(baseDir)
|
|
35
|
+
|
|
31
36
|
this.memoryCache = this.readChatDataFromFile()
|
|
32
37
|
}
|
|
33
38
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
39
|
+
// 清理旧版本的JSON文件
|
|
40
|
+
private cleanupOldDataFiles(baseDir: string) {
|
|
41
|
+
const oldFiles = ['chat-data.json', 'data.json', 'messages.json']
|
|
42
|
+
for (const fileName of oldFiles) {
|
|
43
|
+
const filePath = path.join(baseDir, fileName)
|
|
44
|
+
if (fs.existsSync(filePath)) {
|
|
45
|
+
try {
|
|
46
|
+
fs.unlinkSync(filePath)
|
|
47
|
+
this.logger.info(`已删除旧版本数据文件: ${fileName}`)
|
|
48
|
+
} catch (error) {
|
|
49
|
+
this.logger.warn(`删除旧版本数据文件失败: ${fileName}`, error)
|
|
50
|
+
}
|
|
51
|
+
}
|
|
38
52
|
}
|
|
39
53
|
}
|
|
40
54
|
|
|
41
|
-
|
|
55
|
+
// 确保目录存在
|
|
56
|
+
private ensureDir(dirPath: string) {
|
|
57
|
+
if (!fs.existsSync(dirPath)) {
|
|
58
|
+
fs.mkdirSync(dirPath, { recursive: true })
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 获取频道消息文件路径
|
|
63
|
+
private getChannelFilePath(selfId: string, channelId: string): string {
|
|
64
|
+
const botDir = path.join(this.chatHistoryDir, selfId)
|
|
65
|
+
this.ensureDir(botDir)
|
|
66
|
+
return path.join(botDir, `${channelId}.json`)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// 读取单个频道的消息
|
|
70
|
+
private readChannelMessages(selfId: string, channelId: string): MessageInfo[] {
|
|
71
|
+
const filePath = this.getChannelFilePath(selfId, channelId)
|
|
72
|
+
if (!fs.existsSync(filePath)) {
|
|
73
|
+
return []
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const jsonData = fs.readFileSync(filePath, 'utf8')
|
|
78
|
+
const messages = JSON.parse(jsonData)
|
|
79
|
+
return Array.isArray(messages) ? messages : []
|
|
80
|
+
} catch (error) {
|
|
81
|
+
this.logger.error(`读取频道消息失败 [${selfId}:${channelId}]:`, error)
|
|
82
|
+
return []
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// 写入单个频道的消息
|
|
87
|
+
private writeChannelMessages(selfId: string, channelId: string, messages: MessageInfo[]) {
|
|
88
|
+
const filePath = this.getChannelFilePath(selfId, channelId)
|
|
89
|
+
try {
|
|
90
|
+
const jsonData = JSON.stringify(messages, null, 2)
|
|
91
|
+
fs.writeFileSync(filePath, jsonData, 'utf8')
|
|
92
|
+
} catch (error) {
|
|
93
|
+
this.logger.error(`写入频道消息失败 [${selfId}:${channelId}]:`, error)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// 读取元数据(bots、channels、pinned等)
|
|
98
|
+
private readMetadata(): Omit<ChatData, 'messages'> {
|
|
99
|
+
if (!fs.existsSync(this.metadataFilePath)) {
|
|
100
|
+
return {
|
|
101
|
+
bots: {},
|
|
102
|
+
channels: {},
|
|
103
|
+
pinnedBots: [],
|
|
104
|
+
pinnedChannels: []
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
try {
|
|
109
|
+
const jsonData = fs.readFileSync(this.metadataFilePath, 'utf8')
|
|
110
|
+
const data = JSON.parse(jsonData)
|
|
111
|
+
return {
|
|
112
|
+
bots: data.bots || {},
|
|
113
|
+
channels: data.channels || {},
|
|
114
|
+
pinnedBots: data.pinnedBots || [],
|
|
115
|
+
pinnedChannels: data.pinnedChannels || [],
|
|
116
|
+
lastSaveTime: data.lastSaveTime
|
|
117
|
+
}
|
|
118
|
+
} catch (error) {
|
|
119
|
+
this.logger.error('读取元数据失败:', error)
|
|
120
|
+
return {
|
|
121
|
+
bots: {},
|
|
122
|
+
channels: {},
|
|
123
|
+
pinnedBots: [],
|
|
124
|
+
pinnedChannels: []
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 写入元数据
|
|
130
|
+
private writeMetadata(metadata: Omit<ChatData, 'messages'>) {
|
|
131
|
+
try {
|
|
132
|
+
this.ensureDir(path.dirname(this.metadataFilePath))
|
|
133
|
+
const dataToWrite = {
|
|
134
|
+
...metadata,
|
|
135
|
+
lastSaveTime: Date.now()
|
|
136
|
+
}
|
|
137
|
+
const jsonData = JSON.stringify(dataToWrite, null, 2)
|
|
138
|
+
fs.writeFileSync(this.metadataFilePath, jsonData, 'utf8')
|
|
139
|
+
} catch (error) {
|
|
140
|
+
this.logger.error('写入元数据失败:', error)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// 扫描所有频道消息文件并加载到内存
|
|
145
|
+
private loadAllChannelMessages(): Record<string, MessageInfo[]> {
|
|
146
|
+
const messages: Record<string, MessageInfo[]> = {}
|
|
147
|
+
|
|
148
|
+
if (!fs.existsSync(this.chatHistoryDir)) {
|
|
149
|
+
return messages
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
try {
|
|
153
|
+
const botDirs = fs.readdirSync(this.chatHistoryDir)
|
|
154
|
+
for (const botId of botDirs) {
|
|
155
|
+
const botDir = path.join(this.chatHistoryDir, botId)
|
|
156
|
+
const stat = fs.statSync(botDir)
|
|
157
|
+
if (!stat.isDirectory()) continue
|
|
42
158
|
|
|
159
|
+
const channelFiles = fs.readdirSync(botDir)
|
|
160
|
+
for (const fileName of channelFiles) {
|
|
161
|
+
if (!fileName.endsWith('.json')) continue
|
|
162
|
+
|
|
163
|
+
const channelId = fileName.replace('.json', '')
|
|
164
|
+
const channelKey = `${botId}:${channelId}`
|
|
165
|
+
messages[channelKey] = this.readChannelMessages(botId, channelId)
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
} catch (error) {
|
|
169
|
+
this.logger.error('加载频道消息失败:', error)
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
return messages
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
readChatDataFromFile(): ChatData {
|
|
176
|
+
// 如果已有缓存,直接返回
|
|
43
177
|
if (this.memoryCache) {
|
|
44
178
|
return this.memoryCache
|
|
45
179
|
}
|
|
46
180
|
|
|
181
|
+
// 异步加载数据
|
|
47
182
|
process.nextTick(() => {
|
|
48
183
|
try {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
channels: data.channels || {},
|
|
55
|
-
messages: data.messages || {},
|
|
56
|
-
pinnedBots: data.pinnedBots || [],
|
|
57
|
-
pinnedChannels: data.pinnedChannels || [],
|
|
58
|
-
lastSaveTime: data.lastSaveTime
|
|
59
|
-
}
|
|
184
|
+
const metadata = this.readMetadata()
|
|
185
|
+
const messages = this.loadAllChannelMessages()
|
|
186
|
+
this.memoryCache = {
|
|
187
|
+
...metadata,
|
|
188
|
+
messages
|
|
60
189
|
}
|
|
61
190
|
} catch (error) {
|
|
62
191
|
this.logger.error('读取聊天数据失败:', error)
|
|
63
192
|
}
|
|
64
193
|
})
|
|
65
194
|
|
|
195
|
+
// 先返回空数据
|
|
66
196
|
this.memoryCache = {
|
|
67
197
|
bots: {},
|
|
68
198
|
channels: {},
|
|
@@ -74,52 +204,64 @@ export class FileManager {
|
|
|
74
204
|
}
|
|
75
205
|
|
|
76
206
|
writeChatDataToFile(data: ChatData) {
|
|
77
|
-
|
|
78
207
|
data.lastSaveTime = Date.now()
|
|
79
208
|
this.memoryCache = data
|
|
80
209
|
|
|
81
210
|
process.nextTick(() => {
|
|
82
211
|
try {
|
|
83
|
-
|
|
84
|
-
const
|
|
85
|
-
|
|
212
|
+
// 写入元数据
|
|
213
|
+
const { messages, ...metadata } = data
|
|
214
|
+
this.writeMetadata(metadata)
|
|
215
|
+
|
|
216
|
+
// 写入各个频道的消息
|
|
217
|
+
for (const [channelKey, channelMessages] of Object.entries(messages)) {
|
|
218
|
+
const [selfId, channelId] = channelKey.split(':')
|
|
219
|
+
if (selfId && channelId) {
|
|
220
|
+
this.writeChannelMessages(selfId, channelId, channelMessages)
|
|
221
|
+
}
|
|
222
|
+
}
|
|
86
223
|
} catch (error) {
|
|
87
224
|
this.logger.error('写入聊天数据失败:', error)
|
|
88
225
|
}
|
|
89
226
|
})
|
|
90
227
|
}
|
|
91
228
|
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
229
|
+
// 为特定频道安排写入
|
|
230
|
+
private scheduleWrite(channelKey: string) {
|
|
231
|
+
// 取消之前的定时器
|
|
232
|
+
const existingTimer = this.writeTimers.get(channelKey)
|
|
233
|
+
if (existingTimer) {
|
|
234
|
+
existingTimer()
|
|
97
235
|
}
|
|
98
236
|
|
|
99
|
-
|
|
100
|
-
|
|
237
|
+
// 创建新的定时器
|
|
238
|
+
const timer = this.ctx.setTimeout(() => {
|
|
101
239
|
process.nextTick(() => {
|
|
102
|
-
this.flushPendingMessages()
|
|
240
|
+
this.flushPendingMessages(channelKey)
|
|
103
241
|
})
|
|
104
|
-
this.
|
|
242
|
+
this.writeTimers.delete(channelKey)
|
|
105
243
|
}, this.WRITE_DEBOUNCE_MS)
|
|
244
|
+
|
|
245
|
+
this.writeTimers.set(channelKey, timer)
|
|
106
246
|
}
|
|
107
247
|
|
|
108
|
-
|
|
109
|
-
|
|
248
|
+
// 刷新特定频道的待写入消息
|
|
249
|
+
private flushPendingMessages(channelKey: string) {
|
|
250
|
+
const messagesToWrite = this.pendingMessages.get(channelKey)
|
|
251
|
+
if (!messagesToWrite || messagesToWrite.length === 0) return
|
|
110
252
|
|
|
111
|
-
|
|
112
|
-
this.pendingMessages = []
|
|
253
|
+
this.pendingMessages.delete(channelKey)
|
|
113
254
|
|
|
114
255
|
const data = this.memoryCache || this.readChatDataFromFile()
|
|
256
|
+
const [selfId, channelId] = channelKey.split(':')
|
|
115
257
|
|
|
116
|
-
|
|
117
|
-
const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`
|
|
258
|
+
if (!selfId || !channelId) return
|
|
118
259
|
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
260
|
+
if (!data.messages[channelKey]) {
|
|
261
|
+
data.messages[channelKey] = []
|
|
262
|
+
}
|
|
122
263
|
|
|
264
|
+
for (const messageInfo of messagesToWrite) {
|
|
123
265
|
const existingMessage = data.messages[channelKey].find(m => m.id === messageInfo.id)
|
|
124
266
|
if (existingMessage) {
|
|
125
267
|
continue
|
|
@@ -131,15 +273,19 @@ export class FileManager {
|
|
|
131
273
|
|
|
132
274
|
const cleanedMessageInfo = this.utils.cleanBase64Content(messageInfo) as MessageInfo
|
|
133
275
|
data.messages[channelKey].push(cleanedMessageInfo)
|
|
276
|
+
}
|
|
134
277
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
278
|
+
// 限制消息数量
|
|
279
|
+
if (data.messages[channelKey].length > this.config.maxMessagesPerChannel) {
|
|
280
|
+
data.messages[channelKey].sort((a, b) => a.timestamp - b.timestamp)
|
|
281
|
+
data.messages[channelKey] = data.messages[channelKey].slice(-this.config.maxMessagesPerChannel)
|
|
139
282
|
}
|
|
140
283
|
|
|
141
|
-
|
|
142
|
-
this.
|
|
284
|
+
// 只写入这个频道的消息文件
|
|
285
|
+
this.writeChannelMessages(selfId, channelId, data.messages[channelKey])
|
|
286
|
+
this.memoryCache = data
|
|
287
|
+
|
|
288
|
+
this.logInfo(`批量写入 ${messagesToWrite.length} 条消息到频道 ${channelKey}`)
|
|
143
289
|
}
|
|
144
290
|
|
|
145
291
|
cleanExcessMessages(data: ChatData): ChatData {
|
|
@@ -169,11 +315,16 @@ export class FileManager {
|
|
|
169
315
|
}
|
|
170
316
|
|
|
171
317
|
async addMessageToFile(messageInfo: MessageInfo) {
|
|
318
|
+
const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`
|
|
172
319
|
|
|
173
|
-
|
|
320
|
+
// 添加到待写入队列
|
|
321
|
+
if (!this.pendingMessages.has(channelKey)) {
|
|
322
|
+
this.pendingMessages.set(channelKey, [])
|
|
323
|
+
}
|
|
324
|
+
this.pendingMessages.get(channelKey)!.push(messageInfo)
|
|
174
325
|
|
|
326
|
+
// 更新内存缓存
|
|
175
327
|
const data = this.memoryCache || this.readChatDataFromFile()
|
|
176
|
-
const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`
|
|
177
328
|
|
|
178
329
|
if (!data.messages[channelKey]) {
|
|
179
330
|
data.messages[channelKey] = []
|
|
@@ -195,16 +346,17 @@ export class FileManager {
|
|
|
195
346
|
this.memoryCache = data
|
|
196
347
|
}
|
|
197
348
|
|
|
198
|
-
|
|
349
|
+
// 安排写入
|
|
350
|
+
this.scheduleWrite(channelKey)
|
|
199
351
|
}
|
|
200
352
|
|
|
201
353
|
dispose() {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
354
|
+
// 取消所有定时器
|
|
355
|
+
for (const [channelKey, timer] of this.writeTimers.entries()) {
|
|
356
|
+
timer()
|
|
357
|
+
this.flushPendingMessages(channelKey)
|
|
205
358
|
}
|
|
206
|
-
|
|
207
|
-
this.flushPendingMessages()
|
|
359
|
+
this.writeTimers.clear()
|
|
208
360
|
}
|
|
209
361
|
|
|
210
362
|
private logInfo(...args: any[]) {
|