koishi-plugin-chat-patch 2.3.0 → 2.4.4

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.
@@ -35,10 +35,10 @@ export class ApiHandlers {
35
35
 
36
36
  this.ctx.console.addListener('get-chat-data' as any, async () => {
37
37
  try {
38
+ // 只读取元数据,不加载消息到内存
39
+ const data = this.fileManager.readMetadataOnly()
38
40
 
39
- const data = this.fileManager.readChatDataFromFile()
40
-
41
- this.logInfo('获取基础聊天数据')
41
+ this.logInfo('获取基础聊天数据(仅元数据)')
42
42
 
43
43
  return {
44
44
  success: true,
@@ -47,7 +47,7 @@ export class ApiHandlers {
47
47
  channels: data.channels || {},
48
48
  pinnedBots: data.pinnedBots || [],
49
49
  pinnedChannels: data.pinnedChannels || [],
50
-
50
+ // 不返回消息数据,由前端按需加载
51
51
  messages: {}
52
52
  }
53
53
  }
@@ -64,9 +64,8 @@ export class ApiHandlers {
64
64
  offset?: number
65
65
  }) => {
66
66
  try {
67
- const data = this.fileManager.readChatDataFromFile()
68
- const channelKey = `${requestData.selfId}:${requestData.channelId}`
69
- let messages = data.messages[channelKey] || []
67
+ // 直接从文件读取该频道的消息,不加载所有消息到内存
68
+ let messages = this.fileManager.readChannelMessages(requestData.selfId, requestData.channelId)
70
69
 
71
70
  const sortedMessages = messages.sort((a, b) => b.timestamp - a.timestamp)
72
71
 
@@ -77,11 +76,11 @@ export class ApiHandlers {
77
76
 
78
77
  messages = messages.sort((a, b) => a.timestamp - b.timestamp)
79
78
  } else {
80
-
79
+ // 如果没有指定limit,返回所有消息(按时间正序)
81
80
  messages = sortedMessages.sort((a, b) => a.timestamp - b.timestamp)
82
81
  }
83
82
 
84
- this.logInfo('获取历史消息:', channelKey, '共', messages.length, '条消息')
83
+ this.logInfo('获取历史消息:', `${requestData.selfId}:${requestData.channelId}`, '共', messages.length, '条消息')
85
84
 
86
85
  return {
87
86
  success: true,
@@ -120,45 +119,75 @@ export class ApiHandlers {
120
119
 
121
120
  this.ctx.console.addListener('fetch-image' as any, async (data: { url: string }) => {
122
121
  try {
122
+ // 如果已经是 Vite @fs 路径,直接返回
123
+ if (data.url.includes('/vite/@fs/')) {
124
+ return {
125
+ success: true,
126
+ viteUrl: data.url
127
+ }
128
+ }
123
129
 
130
+ // 如果是本地文件 URL,转换为 Vite @fs 路径
124
131
  if (this.isFileUrl(data.url)) {
125
132
  this.logInfo('处理本地文件请求:', data.url)
126
- return await this.handleLocalFileRequest(data.url)
133
+ const filePath = require('node:url').fileURLToPath(data.url)
134
+ const normalizedPath = filePath.replace(/\\/g, '/')
135
+ return {
136
+ success: true,
137
+ viteUrl: `/vite/@fs/${normalizedPath}`
138
+ }
139
+ }
140
+
141
+ // 网络图片:下载并缓存到本地,返回 Vite @fs 路径
142
+ const dir = require('node:path').join(this.ctx.baseDir, 'data', 'chat-patch', 'persist-media', 'images')
143
+ if (!require('node:fs').existsSync(dir)) {
144
+ require('node:fs').mkdirSync(dir, { recursive: true })
127
145
  }
128
146
 
129
- const response = await fetch(data.url, {
130
- headers: {
131
- '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',
132
- 'Referer': ''
147
+ const crypto = require('node:crypto')
148
+ const hash = crypto.createHash('md5').update(data.url).digest('hex')
149
+ const ext = require('node:path').extname(new URL(data.url).pathname) || '.jpg'
150
+ const filename = `${hash}${ext}`
151
+ const filePath = require('node:path').join(dir, filename)
152
+
153
+ // 如果文件不存在,下载并保存
154
+ if (!require('node:fs').existsSync(filePath)) {
155
+ const response = await fetch(data.url, {
156
+ headers: {
157
+ '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',
158
+ 'Referer': ''
159
+ }
160
+ })
161
+
162
+ if (!response.ok) {
163
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
133
164
  }
134
- })
135
165
 
136
- if (!response.ok) {
137
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
166
+ const buffer = await response.arrayBuffer()
167
+ require('node:fs').writeFileSync(filePath, Buffer.from(buffer))
138
168
  }
139
169
 
140
- const buffer = await response.arrayBuffer()
141
- const base64 = Buffer.from(buffer).toString('base64')
142
- const contentType = response.headers.get('content-type') || 'image/jpeg'
170
+ // 返回 Vite @fs 路径
171
+ const normalizedPath = filePath.replace(/\\/g, '/')
143
172
  return {
144
173
  success: true,
145
- base64: base64,
146
- contentType: contentType,
147
- dataUrl: `data:${contentType};base64,${base64}`
174
+ viteUrl: `/vite/@fs/${normalizedPath}`
148
175
  }
149
176
  } catch (error: any) {
177
+ this.logger.error('获取图片失败:', error)
150
178
  return { success: false, error: error?.message || String(error) }
151
179
  }
152
180
  })
153
181
 
182
+ // 清理历史记录 API 已废弃,现在直接通过右键删除频道数据
154
183
  this.ctx.console.addListener('clear-channel-history' as any, async (data: {
155
184
  selfId: string
156
185
  channelId: string
157
- keepCount?: number
158
186
  }) => {
159
187
  try {
160
- this.logInfo('收到清理历史记录请求:', data)
188
+ this.logInfo('收到清理历史记录请求(已废弃,建议使用删除频道数据):', data)
161
189
 
190
+ // 直接删除该频道的所有消息
162
191
  const chatData = this.fileManager.readChatDataFromFile()
163
192
  const channelKey = `${data.selfId}:${data.channelId}`
164
193
 
@@ -166,33 +195,19 @@ export class ApiHandlers {
166
195
  return { success: true, message: '频道没有历史消息' }
167
196
  }
168
197
 
169
- const messages = chatData.messages[channelKey]
170
- const originalCount = messages.length
171
-
172
- const keepCount = data.keepCount || this.config.keepMessagesOnClear
173
-
174
- if (keepCount > 0 && originalCount <= keepCount) {
175
- return { success: true, message: `消息数量(${originalCount})未超过保留数量(${keepCount}),无需清理` }
176
- }
177
-
178
- const sortedMessages = [...messages].sort((a, b) => a.timestamp - b.timestamp)
179
- const keptMessages = keepCount > 0 ? sortedMessages.slice(-keepCount) : []
180
- const clearedCount = originalCount - keptMessages.length
181
-
182
- chatData.messages[channelKey] = keptMessages
198
+ const originalCount = chatData.messages[channelKey].length
199
+ delete chatData.messages[channelKey]
183
200
  this.fileManager.writeChatDataToFile(chatData)
184
201
 
185
- this.logInfo(`频道 ${channelKey} 历史记录清理完成:`, {
186
- 原始消息数: originalCount,
187
- 保留消息数: keptMessages.length,
188
- 清理消息数: clearedCount
202
+ this.logInfo(`频道 ${channelKey} 历史记录已清空:`, {
203
+ 清理消息数: originalCount
189
204
  })
190
205
 
191
206
  return {
192
207
  success: true,
193
- message: `成功清理 ${clearedCount} 条历史消息,保留最新 ${keptMessages.length} 条`,
194
- clearedCount: clearedCount,
195
- keptCount: keptMessages.length
208
+ message: `成功清理 ${originalCount} 条历史消息`,
209
+ clearedCount: originalCount,
210
+ keptCount: 0
196
211
  }
197
212
  } catch (error: any) {
198
213
  this.logger.error('清理频道历史记录失败:', error)
@@ -478,7 +493,6 @@ export class ApiHandlers {
478
493
  success: true,
479
494
  config: {
480
495
  maxMessagesPerChannel: this.config.maxMessagesPerChannel,
481
- keepMessagesOnClear: this.config.keepMessagesOnClear,
482
496
  maxPersistImages: this.config.maxPersistImages,
483
497
  loggerinfo: this.config.loggerinfo,
484
498
  blockedPlatforms: this.config.blockedPlatforms || [],
@@ -579,35 +593,59 @@ export class ApiHandlers {
579
593
  try {
580
594
  this.logInfo('收到视频临时加载请求:', data.url)
581
595
 
582
- if (this.isFileUrl(data.url)) {
583
- const result = await this.handleLocalFileRequest(data.url)
584
- return result
596
+ // 如果已经是 Vite @fs 路径,直接返回
597
+ if (data.url.includes('/vite/@fs/')) {
598
+ return {
599
+ success: true,
600
+ viteUrl: data.url
601
+ }
585
602
  }
586
603
 
587
- const response = await fetch(data.url, {
588
- headers: {
589
- 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
590
- 'Referer': ''
604
+ // 如果是本地文件 URL,转换为 Vite @fs 路径
605
+ if (this.isFileUrl(data.url)) {
606
+ const filePath = require('node:url').fileURLToPath(data.url)
607
+ const normalizedPath = filePath.replace(/\\/g, '/')
608
+ return {
609
+ success: true,
610
+ viteUrl: `/vite/@fs/${normalizedPath}`
591
611
  }
592
- })
612
+ }
593
613
 
594
- if (!response.ok) {
595
- throw new Error(`HTTP ${response.status}: ${response.statusText}`)
614
+ // 网络视频:下载并缓存到本地,返回 Vite @fs 路径
615
+ const dir = require('node:path').join(this.ctx.baseDir, 'data', 'chat-patch', 'persist-media', 'media')
616
+ if (!require('node:fs').existsSync(dir)) {
617
+ require('node:fs').mkdirSync(dir, { recursive: true })
596
618
  }
597
619
 
598
- const buffer = await response.arrayBuffer()
599
- const base64 = Buffer.from(buffer).toString('base64')
620
+ const crypto = require('node:crypto')
621
+ const hash = crypto.createHash('md5').update(data.url).digest('hex')
622
+ const ext = require('node:path').extname(new URL(data.url).pathname) || '.mp4'
623
+ const filename = `${hash}${ext}`
624
+ const filePath = require('node:path').join(dir, filename)
600
625
 
601
- const contentType = response.headers.get('content-type') || 'video/mp4'
626
+ // 如果文件不存在,下载并保存
627
+ if (!require('node:fs').existsSync(filePath)) {
628
+ const response = await fetch(data.url, {
629
+ headers: {
630
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
631
+ 'Referer': ''
632
+ }
633
+ })
602
634
 
603
- this.logInfo('视频下载成功:', { size: buffer.byteLength, contentType })
635
+ if (!response.ok) {
636
+ throw new Error(`HTTP ${response.status}: ${response.statusText}`)
637
+ }
638
+
639
+ const buffer = await response.arrayBuffer()
640
+ require('node:fs').writeFileSync(filePath, Buffer.from(buffer))
641
+ this.logInfo('视频下载成功:', { size: buffer.byteLength, path: filePath })
642
+ }
604
643
 
644
+ // 返回 Vite @fs 路径
645
+ const normalizedPath = filePath.replace(/\\/g, '/')
605
646
  return {
606
647
  success: true,
607
- base64: base64,
608
- contentType: contentType,
609
- dataUrl: `data:${contentType};base64,${base64}`,
610
- size: buffer.byteLength
648
+ viteUrl: `/vite/@fs/${normalizedPath}`
611
649
  }
612
650
  } catch (error: any) {
613
651
  this.logger.error('视频临时加载失败:', error)
package/src/config.ts CHANGED
@@ -4,7 +4,6 @@ export interface Config {
4
4
  loggerinfo: boolean
5
5
  clearIndexedDBOnStart: boolean
6
6
  maxMessagesPerChannel: number
7
- keepMessagesOnClear: number
8
7
  maxPersistImages: number
9
8
  blockedPlatforms: Array<{
10
9
  platformName: string
@@ -15,7 +14,6 @@ export interface Config {
15
14
  export const Config: Schema<Config> = Schema.intersect([
16
15
  Schema.object({
17
16
  maxMessagesPerChannel: Schema.number().default(500).description('每个群组最大保存消息数量').min(50).max(1500).step(1),
18
- keepMessagesOnClear: Schema.number().default(50).description('手动清理历史记录时保留的消息数量').min(0).max(1000).step(1),
19
17
  maxPersistImages: Schema.number().default(100).description('持久化存储的图片缓存数量').min(10).max(500).step(1),
20
18
  blockedPlatforms: Schema.array(Schema.object({
21
19
  platformName: Schema.string().description('平台名称或关键词'),
@@ -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 dataFilePath: string
11
- private fileOperationLock = Promise.resolve()
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 writeTimer: (() => void) | null = null
19
+ private writeTimers: Map<string, (() => void)> = new Map() // 每个频道独立的写入定时器
20
20
 
21
21
  private readonly WRITE_DEBOUNCE_MS = 1000
22
22
 
@@ -24,45 +24,180 @@ export class FileManager {
24
24
  private ctx: Context,
25
25
  private config: Config
26
26
  ) {
27
- this.dataFilePath = path.resolve(ctx.baseDir, 'data', 'chat-patch', 'chat-data.json')
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
- private ensureDataDir() {
35
- const dir = path.dirname(this.dataFilePath)
36
- if (!fs.existsSync(dir)) {
37
- fs.mkdirSync(dir, { recursive: true })
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
- readChatDataFromFile(): ChatData {
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
+ 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
+ readMetadataOnly(): Omit<ChatData, 'messages'> {
131
+ return this.readMetadata()
132
+ }
133
+
134
+ // 写入元数据
135
+ private writeMetadata(metadata: Omit<ChatData, 'messages'>) {
136
+ try {
137
+ this.ensureDir(path.dirname(this.metadataFilePath))
138
+ const dataToWrite = {
139
+ ...metadata,
140
+ lastSaveTime: Date.now()
141
+ }
142
+ const jsonData = JSON.stringify(dataToWrite, null, 2)
143
+ fs.writeFileSync(this.metadataFilePath, jsonData, 'utf8')
144
+ } catch (error) {
145
+ this.logger.error('写入元数据失败:', error)
146
+ }
147
+ }
148
+
149
+ // 扫描所有频道消息文件并加载到内存
150
+ private loadAllChannelMessages(): Record<string, MessageInfo[]> {
151
+ const messages: Record<string, MessageInfo[]> = {}
152
+
153
+ if (!fs.existsSync(this.chatHistoryDir)) {
154
+ return messages
155
+ }
42
156
 
157
+ try {
158
+ const botDirs = fs.readdirSync(this.chatHistoryDir)
159
+ for (const botId of botDirs) {
160
+ const botDir = path.join(this.chatHistoryDir, botId)
161
+ const stat = fs.statSync(botDir)
162
+ if (!stat.isDirectory()) continue
163
+
164
+ const channelFiles = fs.readdirSync(botDir)
165
+ for (const fileName of channelFiles) {
166
+ if (!fileName.endsWith('.json')) continue
167
+
168
+ const channelId = fileName.replace('.json', '')
169
+ const channelKey = `${botId}:${channelId}`
170
+ messages[channelKey] = this.readChannelMessages(botId, channelId)
171
+ }
172
+ }
173
+ } catch (error) {
174
+ this.logger.error('加载频道消息失败:', error)
175
+ }
176
+
177
+ return messages
178
+ }
179
+
180
+ readChatDataFromFile(): ChatData {
181
+ // 如果已有缓存,直接返回
43
182
  if (this.memoryCache) {
44
183
  return this.memoryCache
45
184
  }
46
185
 
186
+ // 异步加载数据
47
187
  process.nextTick(() => {
48
188
  try {
49
- if (fs.existsSync(this.dataFilePath)) {
50
- const jsonData = fs.readFileSync(this.dataFilePath, 'utf8')
51
- const data = JSON.parse(jsonData)
52
- this.memoryCache = {
53
- bots: data.bots || {},
54
- channels: data.channels || {},
55
- messages: data.messages || {},
56
- pinnedBots: data.pinnedBots || [],
57
- pinnedChannels: data.pinnedChannels || [],
58
- lastSaveTime: data.lastSaveTime
59
- }
189
+ const metadata = this.readMetadata()
190
+ const messages = this.loadAllChannelMessages()
191
+ this.memoryCache = {
192
+ ...metadata,
193
+ messages
60
194
  }
61
195
  } catch (error) {
62
196
  this.logger.error('读取聊天数据失败:', error)
63
197
  }
64
198
  })
65
199
 
200
+ // 先返回空数据
66
201
  this.memoryCache = {
67
202
  bots: {},
68
203
  channels: {},
@@ -74,52 +209,64 @@ export class FileManager {
74
209
  }
75
210
 
76
211
  writeChatDataToFile(data: ChatData) {
77
-
78
212
  data.lastSaveTime = Date.now()
79
213
  this.memoryCache = data
80
214
 
81
215
  process.nextTick(() => {
82
216
  try {
83
- this.ensureDataDir()
84
- const jsonData = JSON.stringify(data, null, 2)
85
- fs.writeFileSync(this.dataFilePath, jsonData, 'utf8')
217
+ // 写入元数据
218
+ const { messages, ...metadata } = data
219
+ this.writeMetadata(metadata)
220
+
221
+ // 写入各个频道的消息
222
+ for (const [channelKey, channelMessages] of Object.entries(messages)) {
223
+ const [selfId, channelId] = channelKey.split(':')
224
+ if (selfId && channelId) {
225
+ this.writeChannelMessages(selfId, channelId, channelMessages)
226
+ }
227
+ }
86
228
  } catch (error) {
87
229
  this.logger.error('写入聊天数据失败:', error)
88
230
  }
89
231
  })
90
232
  }
91
233
 
92
- private scheduleWrite() {
93
-
94
- if (this.writeTimer) {
95
- this.writeTimer()
96
- this.writeTimer = null
234
+ // 为特定频道安排写入
235
+ private scheduleWrite(channelKey: string) {
236
+ // 取消之前的定时器
237
+ const existingTimer = this.writeTimers.get(channelKey)
238
+ if (existingTimer) {
239
+ existingTimer()
97
240
  }
98
241
 
99
- this.writeTimer = this.ctx.setTimeout(() => {
100
-
242
+ // 创建新的定时器
243
+ const timer = this.ctx.setTimeout(() => {
101
244
  process.nextTick(() => {
102
- this.flushPendingMessages()
245
+ this.flushPendingMessages(channelKey)
103
246
  })
104
- this.writeTimer = null
247
+ this.writeTimers.delete(channelKey)
105
248
  }, this.WRITE_DEBOUNCE_MS)
249
+
250
+ this.writeTimers.set(channelKey, timer)
106
251
  }
107
252
 
108
- private flushPendingMessages() {
109
- if (this.pendingMessages.length === 0) return
253
+ // 刷新特定频道的待写入消息
254
+ private flushPendingMessages(channelKey: string) {
255
+ const messagesToWrite = this.pendingMessages.get(channelKey)
256
+ if (!messagesToWrite || messagesToWrite.length === 0) return
110
257
 
111
- const messagesToWrite = [...this.pendingMessages]
112
- this.pendingMessages = []
258
+ this.pendingMessages.delete(channelKey)
113
259
 
114
260
  const data = this.memoryCache || this.readChatDataFromFile()
261
+ const [selfId, channelId] = channelKey.split(':')
115
262
 
116
- for (const messageInfo of messagesToWrite) {
117
- const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`
263
+ if (!selfId || !channelId) return
118
264
 
119
- if (!data.messages[channelKey]) {
120
- data.messages[channelKey] = []
121
- }
265
+ if (!data.messages[channelKey]) {
266
+ data.messages[channelKey] = []
267
+ }
122
268
 
269
+ for (const messageInfo of messagesToWrite) {
123
270
  const existingMessage = data.messages[channelKey].find(m => m.id === messageInfo.id)
124
271
  if (existingMessage) {
125
272
  continue
@@ -131,15 +278,19 @@ export class FileManager {
131
278
 
132
279
  const cleanedMessageInfo = this.utils.cleanBase64Content(messageInfo) as MessageInfo
133
280
  data.messages[channelKey].push(cleanedMessageInfo)
281
+ }
134
282
 
135
- if (data.messages[channelKey].length > this.config.maxMessagesPerChannel) {
136
- data.messages[channelKey].sort((a, b) => a.timestamp - b.timestamp)
137
- data.messages[channelKey] = data.messages[channelKey].slice(-this.config.maxMessagesPerChannel)
138
- }
283
+ // 限制消息数量
284
+ if (data.messages[channelKey].length > this.config.maxMessagesPerChannel) {
285
+ data.messages[channelKey].sort((a, b) => a.timestamp - b.timestamp)
286
+ data.messages[channelKey] = data.messages[channelKey].slice(-this.config.maxMessagesPerChannel)
139
287
  }
140
288
 
141
- this.writeChatDataToFile(data)
142
- this.logInfo(`批量写入 ${messagesToWrite.length} 条消息`)
289
+ // 只写入这个频道的消息文件
290
+ this.writeChannelMessages(selfId, channelId, data.messages[channelKey])
291
+ this.memoryCache = data
292
+
293
+ this.logInfo(`批量写入 ${messagesToWrite.length} 条消息到频道 ${channelKey}`)
143
294
  }
144
295
 
145
296
  cleanExcessMessages(data: ChatData): ChatData {
@@ -169,11 +320,16 @@ export class FileManager {
169
320
  }
170
321
 
171
322
  async addMessageToFile(messageInfo: MessageInfo) {
323
+ const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`
172
324
 
173
- this.pendingMessages.push(messageInfo)
325
+ // 添加到待写入队列
326
+ if (!this.pendingMessages.has(channelKey)) {
327
+ this.pendingMessages.set(channelKey, [])
328
+ }
329
+ this.pendingMessages.get(channelKey)!.push(messageInfo)
174
330
 
331
+ // 更新内存缓存
175
332
  const data = this.memoryCache || this.readChatDataFromFile()
176
- const channelKey = `${messageInfo.selfId}:${messageInfo.channelId}`
177
333
 
178
334
  if (!data.messages[channelKey]) {
179
335
  data.messages[channelKey] = []
@@ -195,16 +351,17 @@ export class FileManager {
195
351
  this.memoryCache = data
196
352
  }
197
353
 
198
- this.scheduleWrite()
354
+ // 安排写入
355
+ this.scheduleWrite(channelKey)
199
356
  }
200
357
 
201
358
  dispose() {
202
- if (this.writeTimer) {
203
- this.writeTimer()
204
- this.writeTimer = null
359
+ // 取消所有定时器
360
+ for (const [channelKey, timer] of this.writeTimers.entries()) {
361
+ timer()
362
+ this.flushPendingMessages(channelKey)
205
363
  }
206
-
207
- this.flushPendingMessages()
364
+ this.writeTimers.clear()
208
365
  }
209
366
 
210
367
  private logInfo(...args: any[]) {