koishi-plugin-chat-patch 2.4.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.
- package/client/vue/composables/useChatData.ts +0 -1
- package/client/vue/composables/useImageCache.ts +162 -21
- package/client/vue/composables/useVideoCache.ts +16 -21
- package/client/vue/types.ts +0 -1
- package/dist/index.js +2 -2
- package/lib/config.d.ts +0 -1
- package/lib/file-manager.d.ts +2 -1
- package/lib/index.js +96 -64
- package/lib/message-handler.d.ts +1 -1
- package/package.json +1 -1
- package/src/api-handlers.ts +103 -65
- package/src/config.ts +0 -2
- package/src/file-manager.ts +7 -2
- package/src/message-handler.ts +12 -7
|
@@ -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 =
|
|
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
|
-
|
|
95
|
-
|
|
141
|
+
// 如果后端返回 Vite @fs 路径,直接使用,不需要缓存到 IndexedDB
|
|
142
|
+
if (result.viteUrl) {
|
|
143
|
+
// 将 Vite 路径缓存到内存中,避免重复请求后端
|
|
144
|
+
imageBlobUrls.value[url] = result.viteUrl
|
|
145
|
+
return result.viteUrl
|
|
146
|
+
}
|
|
96
147
|
|
|
97
|
-
|
|
148
|
+
// 兼容旧的 base64 返回格式(如果有的话)
|
|
149
|
+
if (result.dataUrl) {
|
|
150
|
+
const response = await fetch(result.dataUrl)
|
|
151
|
+
const blob = await response.blob()
|
|
98
152
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
|
-
|
|
119
|
-
|
|
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
|
-
//
|
|
52
|
-
if (
|
|
53
|
-
|
|
51
|
+
// 如果返回 Vite @fs 路径,直接使用,不需要转换为 blob
|
|
52
|
+
if (result.viteUrl) {
|
|
53
|
+
loadedVideos.value[url] = result.viteUrl
|
|
54
|
+
return result.viteUrl
|
|
54
55
|
}
|
|
55
56
|
|
|
56
|
-
//
|
|
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
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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
|
-
|
|
78
|
-
loadedVideos.value[url] = blobUrl
|
|
79
|
-
return blobUrl
|
|
74
|
+
return null
|
|
80
75
|
}
|
|
81
76
|
|
|
82
77
|
return null
|