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.
@@ -11,7 +11,6 @@ export function useChatData() {
11
11
 
12
12
  const pluginConfig = ref<PluginConfig>({
13
13
  maxMessagesPerChannel: 1000,
14
- keepMessagesOnClear: 50,
15
14
  loggerinfo: false,
16
15
  blockedPlatforms: [],
17
16
  chatContainerHeight: 80,
@@ -14,10 +14,11 @@ export function useImageCache() {
14
14
  const imageBlobUrls = ref<Record<string, string>>({})
15
15
  const loadingImages = new Map<string, Promise<string | null>>()
16
16
 
17
- // 内存管理配置
18
- const MAX_MEMORY_USAGE = 100 * 1024 * 1024 // 100MB
19
- const MAX_BLOB_COUNT = 50
17
+ // 内存管理配置 - 减少内存占用
18
+ const MAX_MEMORY_USAGE = 50 * 1024 * 1024 // 50MB(从100MB降低)
19
+ const MAX_BLOB_COUNT = 30 // 从50降低到30
20
20
  let currentMemoryUsage = 0
21
+ let blobCount = 0
21
22
 
22
23
  // IndexedDB 配置
23
24
  let imageDB: IDBDatabase | null = null
@@ -28,8 +29,28 @@ export function useImageCache() {
28
29
  const MAX_TOTAL_IMAGES = 500
29
30
  const MAX_IMAGE_SIZE = 12 * 1024 * 1024 // 12MB
30
31
 
32
+ // 清理 IndexedDB
33
+ async function clearIndexedDB(): Promise<boolean> {
34
+ return new Promise((resolve) => {
35
+ const request = indexedDB.deleteDatabase(DB_NAME)
36
+ request.onsuccess = () => {
37
+ console.log('[ImageCache] IndexedDB 已清空')
38
+ resolve(true)
39
+ }
40
+ request.onerror = () => {
41
+ console.error('[ImageCache] 清空 IndexedDB 失败')
42
+ resolve(false)
43
+ }
44
+ })
45
+ }
46
+
31
47
  // 初始化数据库
32
- async function initDB(): Promise<boolean> {
48
+ async function initDB(shouldClear: boolean = false): Promise<boolean> {
49
+ // 如果需要清空,先删除数据库
50
+ if (shouldClear) {
51
+ await clearIndexedDB()
52
+ }
53
+
33
54
  return new Promise((resolve) => {
34
55
  const request = indexedDB.open(DB_NAME, DB_VERSION)
35
56
  request.onupgradeneeded = (event) => {
@@ -48,8 +69,28 @@ export function useImageCache() {
48
69
  })
49
70
  }
50
71
 
72
+ // 清理内存中的 Blob URLs
73
+ function cleanupMemoryBlobs() {
74
+ if (blobCount > MAX_BLOB_COUNT || currentMemoryUsage > MAX_MEMORY_USAGE) {
75
+ // 按时间戳排序,删除最旧的
76
+ const entries = Object.entries(imageBlobUrls.value)
77
+ const toRemove = Math.ceil(entries.length * 0.3) // 删除30%最旧的
78
+
79
+ for (let i = 0; i < toRemove && entries.length > 0; i++) {
80
+ const [url, blobUrl] = entries[i]
81
+ URL.revokeObjectURL(blobUrl)
82
+ delete imageBlobUrls.value[url]
83
+ blobCount--
84
+ }
85
+
86
+ currentMemoryUsage = Math.floor(currentMemoryUsage * 0.7)
87
+ console.log(`[ImageCache] 清理内存,剩余 ${blobCount} 个图片`)
88
+ }
89
+ }
90
+
51
91
  // 获取缓存图片
52
92
  async function getCachedImageUrl(channelKey: string, url: string): Promise<string | null> {
93
+ // 先检查内存缓存
53
94
  if (imageBlobUrls.value[url]) return imageBlobUrls.value[url]
54
95
  if (loadingImages.has(url)) return loadingImages.get(url)!
55
96
 
@@ -58,8 +99,12 @@ export function useImageCache() {
58
99
  if (!imageDB) await initDB()
59
100
  const item = await getImageFromDB(url)
60
101
  if (item) {
102
+ // 从 IndexedDB 加载到内存
61
103
  const blobUrl = URL.createObjectURL(item.blob)
62
104
  imageBlobUrls.value[url] = blobUrl
105
+ currentMemoryUsage += item.size
106
+ blobCount++
107
+ cleanupMemoryBlobs() // 检查是否需要清理
63
108
  return blobUrl
64
109
  }
65
110
  return null
@@ -84,50 +129,145 @@ export function useImageCache() {
84
129
  }
85
130
 
86
131
  async function cacheImage(channelKey: string, url: string): Promise<string | null> {
132
+ // 先检查是否已缓存
87
133
  const existing = await getCachedImageUrl(channelKey, url)
88
134
  if (existing) return existing
89
135
 
90
136
  try {
137
+ // 通过后端获取图片(现在返回 Vite @fs 路径)
91
138
  const result = await (send as any)('fetch-image', { url })
92
139
  if (!result.success) return null
93
140
 
94
- const response = await fetch(result.dataUrl)
95
- const blob = await response.blob()
141
+ // 如果后端返回 Vite @fs 路径,直接使用,不需要缓存到 IndexedDB
142
+ if (result.viteUrl) {
143
+ // 将 Vite 路径缓存到内存中,避免重复请求后端
144
+ imageBlobUrls.value[url] = result.viteUrl
145
+ return result.viteUrl
146
+ }
96
147
 
97
- if (blob.size > MAX_IMAGE_SIZE) return null
148
+ // 兼容旧的 base64 返回格式(如果有的话)
149
+ if (result.dataUrl) {
150
+ const response = await fetch(result.dataUrl)
151
+ const blob = await response.blob()
98
152
 
99
- const item: ImageCacheItem = {
100
- url,
101
- blob,
102
- timestamp: Date.now(),
103
- size: blob.size,
104
- channelKey
153
+ // 检查图片大小
154
+ if (blob.size > MAX_IMAGE_SIZE) {
155
+ console.warn(`[ImageCache] 图片过大 (${blob.size} bytes),不缓存`)
156
+ return result.dataUrl
157
+ }
158
+
159
+ const item: ImageCacheItem = {
160
+ url,
161
+ blob,
162
+ timestamp: Date.now(),
163
+ size: blob.size,
164
+ channelKey
165
+ }
166
+
167
+ await saveToDB(item)
168
+
169
+ const blobUrl = URL.createObjectURL(blob)
170
+ imageBlobUrls.value[url] = blobUrl
171
+ currentMemoryUsage += blob.size
172
+ blobCount++
173
+ cleanupMemoryBlobs()
174
+
175
+ return blobUrl
105
176
  }
106
177
 
107
- await saveToDB(item)
108
- const blobUrl = URL.createObjectURL(blob)
109
- imageBlobUrls.value[url] = blobUrl
110
- return blobUrl
178
+ return null
111
179
  } catch (e) {
180
+ console.error('[ImageCache] 缓存图片失败:', e)
112
181
  return null
113
182
  }
114
183
  }
115
184
 
116
185
  async function saveToDB(item: ImageCacheItem) {
117
186
  if (!imageDB) return
118
- const transaction = imageDB.transaction([STORE_NAME], 'readwrite')
119
- transaction.objectStore(STORE_NAME).put(item)
187
+
188
+ try {
189
+ // 检查数据库大小,如果超过限制则清理旧数据
190
+ const count = await getDBCount()
191
+ if (count >= MAX_TOTAL_IMAGES) {
192
+ await cleanupOldestImages(Math.floor(MAX_TOTAL_IMAGES * 0.2)) // 清理20%最旧的
193
+ }
194
+
195
+ const transaction = imageDB.transaction([STORE_NAME], 'readwrite')
196
+ transaction.objectStore(STORE_NAME).put(item)
197
+ } catch (e) {
198
+ console.error('[ImageCache] 保存到 IndexedDB 失败:', e)
199
+ }
200
+ }
201
+
202
+ // 获取数据库中的图片数量
203
+ async function getDBCount(): Promise<number> {
204
+ if (!imageDB) return 0
205
+ return new Promise((resolve) => {
206
+ const transaction = imageDB!.transaction([STORE_NAME], 'readonly')
207
+ const store = transaction.objectStore(STORE_NAME)
208
+ const request = store.count()
209
+ request.onsuccess = () => resolve(request.result)
210
+ request.onerror = () => resolve(0)
211
+ })
212
+ }
213
+
214
+ // 清理最旧的图片
215
+ async function cleanupOldestImages(count: number) {
216
+ if (!imageDB) return
217
+
218
+ return new Promise<void>((resolve) => {
219
+ const transaction = imageDB!.transaction([STORE_NAME], 'readwrite')
220
+ const store = transaction.objectStore(STORE_NAME)
221
+ const index = store.index('timestamp')
222
+ const request = index.openCursor()
223
+
224
+ let deleted = 0
225
+ request.onsuccess = (event) => {
226
+ const cursor = (event.target as IDBRequest).result
227
+ if (cursor && deleted < count) {
228
+ cursor.delete()
229
+ deleted++
230
+ cursor.continue()
231
+ } else {
232
+ console.log(`[ImageCache] 清理了 ${deleted} 张旧图片`)
233
+ resolve()
234
+ }
235
+ }
236
+ request.onerror = () => resolve()
237
+ })
120
238
  }
121
239
 
122
240
  async function clearChannelCache(channelKey: string) {
123
- // 简化实现
241
+ // 清理内存中的 Blob URLs
124
242
  Object.keys(imageBlobUrls.value).forEach(url => {
125
243
  URL.revokeObjectURL(imageBlobUrls.value[url])
126
244
  delete imageBlobUrls.value[url]
127
245
  })
246
+ blobCount = 0
247
+ currentMemoryUsage = 0
128
248
  }
129
249
 
250
+ // 初始化时检查是否需要清空 IndexedDB
251
+ onMounted(async () => {
252
+ try {
253
+ // 获取插件配置
254
+ const configResult = await (send as any)('get-plugin-config')
255
+ const shouldClear = configResult?.success && configResult?.config?.clearIndexedDBOnStart
256
+
257
+ if (shouldClear) {
258
+ console.log('[ImageCache] 配置要求清空 IndexedDB,正在清理...')
259
+ await initDB(true) // 清空并重新初始化
260
+ } else {
261
+ await initDB(false) // 正常初始化
262
+ }
263
+ } catch (e) {
264
+ console.error('[ImageCache] 初始化失败:', e)
265
+ await initDB(false)
266
+ }
267
+ })
268
+
130
269
  onUnmounted(() => {
270
+ // 清理所有内存中的 Blob URLs
131
271
  Object.values(imageBlobUrls.value).forEach(URL.revokeObjectURL)
132
272
  if (imageDB) imageDB.close()
133
273
  })
@@ -135,6 +275,7 @@ export function useImageCache() {
135
275
  return {
136
276
  getCachedImageUrl,
137
277
  cacheImage,
138
- clearChannelCache
278
+ clearChannelCache,
279
+ clearIndexedDB // 导出清理函数供外部使用
139
280
  }
140
281
  }
@@ -48,35 +48,30 @@ export function useVideoCache() {
48
48
  const result = await (send as any)('fetch-video-temp', { url })
49
49
 
50
50
  if (result.success) {
51
- // 清理上一个 blob URL
52
- if (currentBlobUrl.value) {
53
- URL.revokeObjectURL(currentBlobUrl.value)
51
+ // 如果返回 Vite @fs 路径,直接使用,不需要转换为 blob
52
+ if (result.viteUrl) {
53
+ loadedVideos.value[url] = result.viteUrl
54
+ return result.viteUrl
54
55
  }
55
56
 
56
- // 如果返回的是 dataUrl,转换为 blob
57
- let blobUrl: string
57
+ // 兼容旧的 dataUrl 格式(如果有的话)
58
58
  if (result.dataUrl) {
59
+ // 清理上一个 blob URL
60
+ if (currentBlobUrl.value) {
61
+ URL.revokeObjectURL(currentBlobUrl.value)
62
+ }
63
+
59
64
  // 从 data URL 创建 blob
60
65
  const response = await fetch(result.dataUrl)
61
66
  const blob = await response.blob()
62
- blobUrl = URL.createObjectURL(blob)
63
- } else if (result.fileUrl) {
64
- // 如果是文件 URL,也尝试转换为 blob
65
- try {
66
- const response = await fetch(result.fileUrl)
67
- const blob = await response.blob()
68
- blobUrl = URL.createObjectURL(blob)
69
- } catch (e) {
70
- // 如果转换失败,直接使用文件 URL
71
- blobUrl = result.fileUrl
72
- }
73
- } else {
74
- return null
67
+ const blobUrl = URL.createObjectURL(blob)
68
+
69
+ currentBlobUrl.value = blobUrl
70
+ loadedVideos.value[url] = blobUrl
71
+ return blobUrl
75
72
  }
76
73
 
77
- currentBlobUrl.value = blobUrl
78
- loadedVideos.value[url] = blobUrl
79
- return blobUrl
74
+ return null
80
75
  }
81
76
 
82
77
  return null
@@ -62,7 +62,6 @@ export interface ChatData {
62
62
 
63
63
  export interface PluginConfig {
64
64
  maxMessagesPerChannel: number
65
- keepMessagesOnClear: number
66
65
  loggerinfo: boolean
67
66
  blockedPlatforms: Array<{
68
67
  platformName: string