koishi-plugin-chat-patch 1.0.7 → 1.0.9

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.
@@ -222,7 +222,6 @@ function isFileUrl(url: string): boolean {
222
222
  }
223
223
  }
224
224
 
225
-
226
225
  // 头像组件
227
226
  const AvatarComponent = defineComponent({
228
227
  props: {
@@ -899,13 +898,35 @@ interface ImageCacheItem {
899
898
 
900
899
  // 内存中的URL缓存
901
900
  const imageBlobUrls = ref<Record<string, string>>({})
902
- const maxImagesPerChannel = 200 // 每个频道最大缓存图片数量
903
901
 
904
902
  // 内存管理配置
905
903
  const MAX_MEMORY_USAGE = 100 * 1024 * 1024 // 100MB 最大内存使用量
906
904
  const MAX_BLOB_COUNT = 50 // 最大blob URL数量
907
905
  let currentMemoryUsage = 0 // 当前内存使用量估算
908
906
 
907
+ // IndexedDB 配置和限制
908
+ let imageDB: IDBDatabase | null = null
909
+ const DB_NAME = 'ChatImageCache'
910
+ const DB_VERSION = 2 // 版本号
911
+ const STORE_NAME = 'images'
912
+
913
+ // 严格的存储限制
914
+ const MAX_DB_SIZE = 50 * 1024 * 1024 // 50MB 最大数据库大小
915
+ const MAX_IMAGES_PER_CHANNEL = 100 // 每个频道最多缓存100张图片
916
+ const MAX_TOTAL_IMAGES = 500 // 总共最多缓存500张图片
917
+ const MAX_IMAGE_SIZE = 2 * 1024 * 1024 // 单张图片最大2MB
918
+ const CLEANUP_THRESHOLD = 0.8 // 当达到80%限制时开始清理
919
+ const DB_HEALTH_CHECK_INTERVAL = 60 * 1000 // 每分钟检查一次数据库健康状态
920
+
921
+ // 数据库状态跟踪
922
+ let currentDbSize = 0
923
+ let currentImageCount = 0
924
+ let lastHealthCheck = 0
925
+
926
+ const selectedBot = ref<string>('')
927
+ const selectedChannel = ref<string>('')
928
+ const inputMessage = ref<string>('')
929
+
909
930
  // 内存管理函数
910
931
  function estimateBlobSize(blob: Blob): number {
911
932
  return blob.size || 0
@@ -954,16 +975,6 @@ function checkAndCleanupMemory() {
954
975
  }
955
976
  }
956
977
 
957
- // IndexedDB
958
- let imageDB: IDBDatabase | null = null
959
- const DB_NAME = 'ChatImageCache'
960
- const DB_VERSION = 1
961
- const STORE_NAME = 'images'
962
-
963
- const selectedBot = ref<string>('')
964
- const selectedChannel = ref<string>('')
965
- const inputMessage = ref<string>('')
966
-
967
978
  // 图片上传相关状态
968
979
  const uploadedImages = ref<Array<{
969
980
  tempId: string
@@ -971,6 +982,7 @@ const uploadedImages = ref<Array<{
971
982
  preview: string
972
983
  size: number
973
984
  }>>([])
985
+
974
986
  const showActionMenu = ref<boolean>(false)
975
987
  const fileInput = ref<HTMLInputElement>()
976
988
 
@@ -1771,7 +1783,6 @@ function resetDragState() {
1771
1783
  removeThresholdCircle()
1772
1784
  }
1773
1785
 
1774
- // getDragStyle 不再直接用于拖拽中的元素,而是用于原始元素隐藏
1775
1786
  function getDragStyle(channelId: string) {
1776
1787
  if (draggingChannel.value === channelId && isDragReady.value) {
1777
1788
  // 当拖拽开始且准备就绪时,隐藏原始气泡
@@ -1951,30 +1962,319 @@ function removeThresholdCircle() {
1951
1962
  }
1952
1963
  }
1953
1964
 
1954
- // IndexedDB初始化
1955
- async function initImageDB(): Promise<boolean> {
1956
- return new Promise((resolve, reject) => {
1957
- const request = indexedDB.open(DB_NAME, DB_VERSION)
1965
+ // 数据库健康检查
1966
+ async function checkDatabaseHealth(): Promise<boolean> {
1967
+ try {
1968
+ if (!imageDB) return false
1969
+
1970
+ const now = Date.now()
1971
+ if (now - lastHealthCheck < DB_HEALTH_CHECK_INTERVAL) {
1972
+ return true // 跳过频繁检查
1973
+ }
1974
+
1975
+ lastHealthCheck = now
1976
+
1977
+ // 获取数据库统计信息
1978
+ const stats = await getDatabaseStats()
1979
+ currentDbSize = stats.totalSize
1980
+ currentImageCount = stats.totalImages
1981
+
1982
+ console.log('数据库健康检查:', {
1983
+ 大小: `${(currentDbSize / 1024 / 1024).toFixed(2)}MB / ${(MAX_DB_SIZE / 1024 / 1024).toFixed(2)}MB`,
1984
+ 图片数量: `${currentImageCount} / ${MAX_TOTAL_IMAGES}`,
1985
+ 使用率: `${(currentDbSize / MAX_DB_SIZE * 100).toFixed(1)}%`
1986
+ })
1987
+
1988
+ // 检查是否需要清理
1989
+ const sizeRatio = currentDbSize / MAX_DB_SIZE
1990
+ const countRatio = currentImageCount / MAX_TOTAL_IMAGES
1991
+
1992
+ if (sizeRatio > CLEANUP_THRESHOLD || countRatio > CLEANUP_THRESHOLD) {
1993
+ console.warn('数据库使用率过高,开始自动清理')
1994
+ await performAutomaticCleanup()
1995
+ }
1996
+
1997
+ // 检查是否超过硬限制
1998
+ if (sizeRatio > 0.95 || countRatio > 0.95) {
1999
+ console.error('数据库接近极限,执行紧急清理')
2000
+ await performEmergencyCleanup()
2001
+ }
2002
+
2003
+ return true
2004
+ } catch (error) {
2005
+ console.error('数据库健康检查失败:', error)
2006
+ return false
2007
+ }
2008
+ }
2009
+
2010
+ // 获取数据库统计信息
2011
+ async function getDatabaseStats(): Promise<{ totalSize: number, totalImages: number, channelStats: Record<string, number> }> {
2012
+ if (!imageDB) return { totalSize: 0, totalImages: 0, channelStats: {} }
2013
+
2014
+ return new Promise((resolve) => {
2015
+ const transaction = imageDB!.transaction([STORE_NAME], 'readonly')
2016
+ const store = transaction.objectStore(STORE_NAME)
2017
+ const request = store.getAll()
2018
+
2019
+ request.onsuccess = () => {
2020
+ const items: ImageCacheItem[] = request.result || []
2021
+ let totalSize = 0
2022
+ const channelStats: Record<string, number> = {}
2023
+
2024
+ items.forEach(item => {
2025
+ totalSize += item.size || 0
2026
+ channelStats[item.channelKey] = (channelStats[item.channelKey] || 0) + 1
2027
+ })
2028
+
2029
+ resolve({
2030
+ totalSize,
2031
+ totalImages: items.length,
2032
+ channelStats
2033
+ })
2034
+ }
1958
2035
 
1959
2036
  request.onerror = () => {
1960
- console.error('IndexedDB初始化失败:', request.error)
1961
- resolve(false)
2037
+ console.error('获取数据库统计失败:', request.error)
2038
+ resolve({ totalSize: 0, totalImages: 0, channelStats: {} })
2039
+ }
2040
+ })
2041
+ }
2042
+
2043
+ // 自动清理
2044
+ async function performAutomaticCleanup() {
2045
+ try {
2046
+ console.log('开始自动清理...')
2047
+
2048
+ // 获取所有图片,按时间排序
2049
+ const allImages = await getAllImagesFromDB()
2050
+ if (allImages.length === 0) return
2051
+
2052
+ // 按频道分组
2053
+ const channelGroups: Record<string, ImageCacheItem[]> = {}
2054
+ allImages.forEach(item => {
2055
+ if (!channelGroups[item.channelKey]) {
2056
+ channelGroups[item.channelKey] = []
2057
+ }
2058
+ channelGroups[item.channelKey].push(item)
2059
+ })
2060
+
2061
+ let cleanedCount = 0
2062
+ let freedSize = 0
2063
+
2064
+ // 清理每个频道超出限制的图片
2065
+ for (const [channelKey, images] of Object.entries(channelGroups)) {
2066
+ if (images.length > MAX_IMAGES_PER_CHANNEL) {
2067
+ // 按时间排序,删除最旧的
2068
+ images.sort((a, b) => a.timestamp - b.timestamp)
2069
+ const toDelete = images.slice(0, images.length - MAX_IMAGES_PER_CHANNEL)
2070
+
2071
+ for (const item of toDelete) {
2072
+ await deleteImageFromDB(item.url)
2073
+ cleanedCount++
2074
+ freedSize += item.size || 0
2075
+
2076
+ // 清理内存中的blob URL
2077
+ if (imageBlobUrls.value[item.url]) {
2078
+ URL.revokeObjectURL(imageBlobUrls.value[item.url])
2079
+ delete imageBlobUrls.value[item.url]
2080
+ }
2081
+ }
2082
+ }
2083
+ }
2084
+
2085
+ console.log(`自动清理完成: 清理了 ${cleanedCount} 张图片,释放了 ${(freedSize / 1024 / 1024).toFixed(2)}MB`)
2086
+
2087
+ // 更新统计
2088
+ currentImageCount -= cleanedCount
2089
+ currentDbSize -= freedSize
2090
+
2091
+ } catch (error) {
2092
+ console.error('自动清理失败:', error)
2093
+ }
2094
+ }
2095
+
2096
+ // 紧急清理 // 激进
2097
+ async function performEmergencyCleanup() {
2098
+ try {
2099
+ console.log('开始紧急清理...')
2100
+
2101
+ // 获取所有图片
2102
+ const allImages = await getAllImagesFromDB()
2103
+ if (allImages.length === 0) return
2104
+
2105
+ // 按时间排序,只保留最新的一部分
2106
+ allImages.sort((a, b) => b.timestamp - a.timestamp)
2107
+ const keepCount = Math.floor(MAX_TOTAL_IMAGES * 0.3) // 只保留30%
2108
+ const toDelete = allImages.slice(keepCount)
2109
+
2110
+ let cleanedCount = 0
2111
+ let freedSize = 0
2112
+
2113
+ for (const item of toDelete) {
2114
+ await deleteImageFromDB(item.url)
2115
+ cleanedCount++
2116
+ freedSize += item.size || 0
2117
+
2118
+ // 清理内存中的blob URL
2119
+ if (imageBlobUrls.value[item.url]) {
2120
+ URL.revokeObjectURL(imageBlobUrls.value[item.url])
2121
+ delete imageBlobUrls.value[item.url]
2122
+ }
1962
2123
  }
1963
2124
 
2125
+ console.log(`紧急清理完成: 清理了 ${cleanedCount} 张图片,释放了 ${(freedSize / 1024 / 1024).toFixed(2)}MB`)
2126
+
2127
+ // 更新统计
2128
+ currentImageCount = keepCount
2129
+ currentDbSize -= freedSize
2130
+
2131
+ } catch (error) {
2132
+ console.error('紧急清理失败:', error)
2133
+ }
2134
+ }
2135
+
2136
+ // 获取所有图片
2137
+ async function getAllImagesFromDB(): Promise<ImageCacheItem[]> {
2138
+ if (!imageDB) return []
2139
+
2140
+ return new Promise((resolve) => {
2141
+ const transaction = imageDB!.transaction([STORE_NAME], 'readonly')
2142
+ const store = transaction.objectStore(STORE_NAME)
2143
+ const request = store.getAll()
2144
+
1964
2145
  request.onsuccess = () => {
1965
- imageDB = request.result
1966
- resolve(true)
2146
+ resolve(request.result || [])
2147
+ }
2148
+
2149
+ request.onerror = () => {
2150
+ console.error('获取所有图片失败:', request.error)
2151
+ resolve([])
2152
+ }
2153
+ })
2154
+ }
2155
+
2156
+ // 清理所有IndexedDB数据 //紧急情况使用
2157
+ async function clearAllIndexedDBData(): Promise<boolean> {
2158
+ return new Promise((resolve) => {
2159
+ try {
2160
+ // 先关闭现有连接
2161
+ if (imageDB) {
2162
+ imageDB.close()
2163
+ imageDB = null
2164
+ }
2165
+
2166
+ // 删除整个数据库
2167
+ const deleteRequest = indexedDB.deleteDatabase(DB_NAME)
2168
+
2169
+ deleteRequest.onsuccess = () => {
2170
+ console.log('IndexedDB数据库已完全清理')
2171
+ currentDbSize = 0
2172
+ currentImageCount = 0
2173
+ resolve(true)
2174
+ }
2175
+
2176
+ deleteRequest.onerror = () => {
2177
+ console.error('清理IndexedDB数据库失败:', deleteRequest.error)
2178
+ resolve(false)
2179
+ }
2180
+
2181
+ deleteRequest.onblocked = () => {
2182
+ console.warn('IndexedDB数据库删除被阻塞,可能有其他连接正在使用')
2183
+ // 等待一段时间后重试
2184
+ setTimeout(() => {
2185
+ resolve(false)
2186
+ }, 5000)
2187
+ }
2188
+ } catch (error) {
2189
+ console.error('清理数据库时出错:', error)
2190
+ resolve(false)
1967
2191
  }
2192
+ })
2193
+ }
2194
+
2195
+ // IndexedDB初始化
2196
+ async function initImageDB(): Promise<boolean> {
2197
+ try {
2198
+ // 首先尝试打开数据库
2199
+ const success = await openDatabase()
2200
+ if (!success) {
2201
+ console.warn('数据库打开失败,尝试清理后重新初始化')
2202
+ await clearAllIndexedDBData()
2203
+ return await openDatabase()
2204
+ }
2205
+
2206
+ // 数据库打开成功,进行初始健康检查
2207
+ setTimeout(async () => {
2208
+ const stats = await getDatabaseStats()
2209
+ console.log('数据库初始状态:', {
2210
+ 大小: `${(stats.totalSize / 1024 / 1024).toFixed(2)}MB`,
2211
+ 图片数量: stats.totalImages,
2212
+ 频道分布: stats.channelStats
2213
+ })
2214
+
2215
+ // 如果初始状态就超过限制,执行清理
2216
+ if (stats.totalSize > MAX_DB_SIZE * 0.9 || stats.totalImages > MAX_TOTAL_IMAGES * 0.9) {
2217
+ console.warn('数据库初始状态接近限制,执行清理')
2218
+ await performAutomaticCleanup()
2219
+ }
2220
+ }, 1000)
2221
+
2222
+ return true
2223
+ } catch (error) {
2224
+ console.error('IndexedDB初始化出错:', error)
2225
+ return false
2226
+ }
2227
+ }
2228
+
2229
+ // 打开数据库的内部函数
2230
+ async function openDatabase(): Promise<boolean> {
2231
+ return new Promise((resolve) => {
2232
+ try {
2233
+ const request = indexedDB.open(DB_NAME, DB_VERSION)
1968
2234
 
1969
- request.onupgradeneeded = (event) => {
1970
- const db = (event.target as IDBOpenDBRequest).result
2235
+ request.onerror = () => {
2236
+ console.error('IndexedDB打开失败:', request.error)
2237
+ resolve(false)
2238
+ }
2239
+
2240
+ request.onsuccess = () => {
2241
+ imageDB = request.result
2242
+
2243
+ // 添加错误处理
2244
+ imageDB.onerror = (event) => {
2245
+ console.error('IndexedDB运行时错误:', event)
2246
+ }
2247
+
2248
+ // 添加版本变更处理
2249
+ imageDB.onversionchange = () => {
2250
+ console.warn('IndexedDB版本变更,关闭连接')
2251
+ imageDB?.close()
2252
+ imageDB = null
2253
+ }
2254
+
2255
+ resolve(true)
2256
+ }
2257
+
2258
+ request.onupgradeneeded = (event) => {
2259
+ const db = (event.target as IDBOpenDBRequest).result
2260
+
2261
+ // 创建对象存储
2262
+ if (!db.objectStoreNames.contains(STORE_NAME)) {
2263
+ const store = db.createObjectStore(STORE_NAME, { keyPath: 'url' })
2264
+ store.createIndex('channelKey', 'channelKey', { unique: false })
2265
+ store.createIndex('timestamp', 'timestamp', { unique: false })
2266
+ store.createIndex('size', 'size', { unique: false })
2267
+ console.log('IndexedDB对象存储创建完成')
2268
+ }
2269
+ }
1971
2270
 
1972
- // 创建对象存储
1973
- if (!db.objectStoreNames.contains(STORE_NAME)) {
1974
- const store = db.createObjectStore(STORE_NAME, { keyPath: 'url' })
1975
- store.createIndex('channelKey', 'channelKey', { unique: false })
1976
- store.createIndex('timestamp', 'timestamp', { unique: false })
2271
+ request.onblocked = () => {
2272
+ console.warn('IndexedDB打开被阻塞')
2273
+ resolve(false)
1977
2274
  }
2275
+ } catch (error) {
2276
+ console.error('打开数据库时出错:', error)
2277
+ resolve(false)
1978
2278
  }
1979
2279
  })
1980
2280
  }
@@ -2003,20 +2303,60 @@ async function getImageFromDB(url: string): Promise<ImageCacheItem | null> {
2003
2303
  async function saveImageToDB(item: ImageCacheItem): Promise<boolean> {
2004
2304
  if (!imageDB) return false
2005
2305
 
2006
- return new Promise((resolve) => {
2007
- const transaction = imageDB!.transaction([STORE_NAME], 'readwrite')
2008
- const store = transaction.objectStore(STORE_NAME)
2009
- const request = store.put(item)
2306
+ try {
2307
+ // 检查单张图片大小
2308
+ if (item.size > MAX_IMAGE_SIZE) {
2309
+ console.warn(`图片过大,跳过缓存: ${(item.size / 1024 / 1024).toFixed(2)}MB > ${(MAX_IMAGE_SIZE / 1024 / 1024).toFixed(2)}MB`)
2310
+ return false
2311
+ }
2010
2312
 
2011
- request.onsuccess = () => {
2012
- resolve(true)
2313
+ // 执行健康检查
2314
+ await checkDatabaseHealth()
2315
+
2316
+ // 检查是否会超过限制
2317
+ if (currentDbSize + item.size > MAX_DB_SIZE) {
2318
+ console.warn('添加图片会超过数据库大小限制,执行清理')
2319
+ await performAutomaticCleanup()
2320
+
2321
+ // 清理后再次检查
2322
+ if (currentDbSize + item.size > MAX_DB_SIZE) {
2323
+ console.warn('清理后仍会超过限制,跳过此图片')
2324
+ return false
2325
+ }
2013
2326
  }
2014
2327
 
2015
- request.onerror = () => {
2016
- console.error('保存图片到IndexedDB失败:', request.error)
2017
- resolve(false)
2328
+ if (currentImageCount >= MAX_TOTAL_IMAGES) {
2329
+ console.warn('图片数量已达上限,执行清理')
2330
+ await performAutomaticCleanup()
2331
+
2332
+ // 清理后再次检查
2333
+ if (currentImageCount >= MAX_TOTAL_IMAGES) {
2334
+ console.warn('清理后仍达上限,跳过此图片')
2335
+ return false
2336
+ }
2018
2337
  }
2019
- })
2338
+
2339
+ return new Promise((resolve) => {
2340
+ const transaction = imageDB!.transaction([STORE_NAME], 'readwrite')
2341
+ const store = transaction.objectStore(STORE_NAME)
2342
+ const request = store.put(item)
2343
+
2344
+ request.onsuccess = () => {
2345
+ // 更新统计
2346
+ currentDbSize += item.size
2347
+ currentImageCount += 1
2348
+ resolve(true)
2349
+ }
2350
+
2351
+ request.onerror = () => {
2352
+ console.error('保存图片到IndexedDB失败:', request.error)
2353
+ resolve(false)
2354
+ }
2355
+ })
2356
+ } catch (error) {
2357
+ console.error('保存图片时出错:', error)
2358
+ return false
2359
+ }
2020
2360
  }
2021
2361
 
2022
2362
  // 从IndexedDB删除图片
@@ -2118,22 +2458,10 @@ async function cacheImage(channelKey: string, originalUrl: string): Promise<stri
2118
2458
  const byteArray = new Uint8Array(byteNumbers)
2119
2459
  const blob = new Blob([byteArray], { type: contentType })
2120
2460
 
2121
- // 检查频道缓存数量限制
2122
- const channelImages = await getChannelImagesFromDB(channelKey)
2123
-
2124
- if (channelImages.length >= maxImagesPerChannel) {
2125
- // 清理最旧的图片缓存
2126
- const sortedImages = channelImages.sort((a, b) => a.timestamp - b.timestamp)
2127
- const toDelete = sortedImages.slice(0, channelImages.length - maxImagesPerChannel + 1)
2128
-
2129
- for (const item of toDelete) {
2130
- await deleteImageFromDB(item.url)
2131
- // 清理内存中的blob URL
2132
- if (imageBlobUrls.value[item.url]) {
2133
- URL.revokeObjectURL(imageBlobUrls.value[item.url])
2134
- delete imageBlobUrls.value[item.url]
2135
- }
2136
- }
2461
+ // 检查blob大小
2462
+ if (blob.size > MAX_IMAGE_SIZE) {
2463
+ console.warn(`图片过大,跳过缓存: ${(blob.size / 1024 / 1024).toFixed(2)}MB`)
2464
+ return null
2137
2465
  }
2138
2466
 
2139
2467
  // 创建缓存项
@@ -2369,7 +2697,7 @@ function handleMessageEvent(messageEvent: any) {
2369
2697
  chatData.value = { ...chatData.value }
2370
2698
  }
2371
2699
 
2372
- // 处理机器人发送消息成功事件(通过前端发送消息API触发)
2700
+ // 处理机器人发送消息成功事件
2373
2701
  function handleBotMessageSentEvent(sentEvent: any) {
2374
2702
  const channelKey = `${sentEvent.selfId}:${sentEvent.channelId}`
2375
2703
  if (!chatData.value.messages[channelKey]) {
@@ -2436,7 +2764,7 @@ function handleBotMessageSentEvent(sentEvent: any) {
2436
2764
  chatData.value = { ...chatData.value }
2437
2765
  }
2438
2766
 
2439
- // 处理机器人消息事件(通过before-send事件监听触发)
2767
+ // 处理机器人消息事件
2440
2768
  function handleBotMessageEvent(botMessageEvent: any) {
2441
2769
  // 更新机器人信息
2442
2770
  if (!chatData.value.bots[botMessageEvent.selfId]) {
@@ -2562,7 +2890,7 @@ function handleBotMessageEvent(botMessageEvent: any) {
2562
2890
  chatData.value = { ...chatData.value }
2563
2891
  }
2564
2892
 
2565
- // 监听消息变化,只在切换频道时自动滚动到底部
2893
+ // 监听消息变化
2566
2894
  watch(currentMessages, (newMessages, oldMessages) => {
2567
2895
  // 只有在切换频道时(消息数组完全不同)才自动滚动
2568
2896
  if (oldMessages.length === 0 && newMessages.length > 0) {
@@ -2830,6 +3158,18 @@ onMounted(async () => {
2830
3158
  const dbInitialized = await initImageDB()
2831
3159
  if (!dbInitialized) {
2832
3160
  console.warn('IndexedDB初始化失败,图片缓存功能将不可用')
3161
+ } else {
3162
+ console.log('IndexedDB初始化成功')
3163
+
3164
+ // 启动时进行健康检查
3165
+ setTimeout(async () => {
3166
+ await checkDatabaseHealth()
3167
+ }, 2000) // 延迟2秒,避免影响页面加载
3168
+
3169
+ // 设置定期健康检查(每5分钟)
3170
+ setInterval(async () => {
3171
+ await checkDatabaseHealth()
3172
+ }, 5 * 60 * 1000)
2833
3173
  }
2834
3174
 
2835
3175
  // 首先加载插件配置