koishi-plugin-chat-patch 1.0.18 → 1.1.1

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.
@@ -449,7 +449,7 @@ export function useChatLogic() {
449
449
  isExpanded.value = !isExpanded.value
450
450
  }
451
451
 
452
- const getPreviewText = () => {
452
+ const getPreviewText = (): { previews: string[]; messageCount: number } => {
453
453
  if (!props.element.children || props.element.children.length === 0) {
454
454
  return {
455
455
  previews: [],
@@ -488,7 +488,7 @@ export function useChatLogic() {
488
488
  }
489
489
  }
490
490
 
491
- const renderForwardedMessage = (message: any, index: number) => {
491
+ const renderForwardedMessage = (message: any, index: number): any => {
492
492
  const nickname = message.attrs?.nickname || '用户'
493
493
  const userId = message.attrs?.userId || 'unknown'
494
494
 
@@ -551,7 +551,7 @@ export function useChatLogic() {
551
551
  }
552
552
  },
553
553
  setup(props) {
554
- const renderElement = (element: MessageElement) => {
554
+ const renderElement = (element: MessageElement): any => {
555
555
  switch (element.type) {
556
556
  case 'text':
557
557
  return h('span', { class: 'message-text-content' }, element.attrs.content || '')
@@ -614,7 +614,7 @@ export function useChatLogic() {
614
614
  case 'p':
615
615
  // 处理段落元素,递归渲染子元素
616
616
  if (element.children && element.children.length > 0) {
617
- const childElements = element.children.map((child, index) =>
617
+ const childElements: any[] = element.children.map((child, index) =>
618
618
  h(MessageElement, {
619
619
  key: index,
620
620
  element: child,
@@ -654,6 +654,14 @@ export function useChatLogic() {
654
654
  // 存储每个频道的真实消息数量
655
655
  const channelMessageCounts = ref<Record<string, number>>({})
656
656
 
657
+ // 存储每个频道的分页状态
658
+ const channelPagination = ref<Record<string, {
659
+ offset: number
660
+ hasMore: boolean
661
+ loading: boolean
662
+ }>>({})
663
+
664
+ // Config默认值,实际值将从后端获取
657
665
  const pluginConfig = ref<{
658
666
  maxMessagesPerChannel: number
659
667
  keepMessagesOnClear: number
@@ -663,12 +671,14 @@ export function useChatLogic() {
663
671
  exactMatch: boolean
664
672
  }>
665
673
  chatContainerHeight: number
674
+ clearIndexedDBOnStart: boolean
666
675
  }>({
667
676
  maxMessagesPerChannel: 1000,
668
677
  keepMessagesOnClear: 50,
669
678
  loggerinfo: false,
670
679
  blockedPlatforms: [],
671
- chatContainerHeight: 80
680
+ chatContainerHeight: 80,
681
+ clearIndexedDBOnStart: true
672
682
  })
673
683
 
674
684
  // 图片缓存 - IndexedDB
@@ -682,6 +692,8 @@ export function useChatLogic() {
682
692
 
683
693
  // 内存中的URL缓存
684
694
  const imageBlobUrls = ref<Record<string, string>>({})
695
+ // 正在加载的图片URL锁,防止并发请求
696
+ const loadingImages = new Map<string, Promise<string | null>>()
685
697
 
686
698
  // 内存管理配置
687
699
  const MAX_MEMORY_USAGE = 100 * 1024 * 1024 // 100MB 最大内存使用量
@@ -736,6 +748,7 @@ export function useChatLogic() {
736
748
  const showScrollButton = ref<boolean>(false)
737
749
  const isUserScrolling = ref<boolean>(false)
738
750
  const isSending = ref<boolean>(false)
751
+ const isLoadingMore = ref<boolean>(false)
739
752
 
740
753
  // 拖拽相关状态
741
754
  const draggingChannel = ref<string>('')
@@ -1113,9 +1126,13 @@ export function useChatLogic() {
1113
1126
  mobileView.value = 'messages'
1114
1127
  }
1115
1128
 
1116
- // 先获取历史消息,然后再滚动到底部
1129
+ // 清除该频道的分页状态
1130
+ const channelKey = `${selectedBot.value}:${channelId}`
1131
+ delete channelPagination.value[channelKey]
1132
+
1133
+ // 先获取历史消息(只加载最新的50条)
1117
1134
  if (selectedBot.value) {
1118
- await loadHistoryMessages(selectedBot.value, channelId)
1135
+ await loadHistoryMessages(selectedBot.value, channelId, 50, 0)
1119
1136
  }
1120
1137
 
1121
1138
  nextTick(() => {
@@ -1366,6 +1383,59 @@ export function useChatLogic() {
1366
1383
  // 滚动到底部时,重置滚动状态
1367
1384
  isUserScrolling.value = false
1368
1385
  }
1386
+
1387
+ // 检查是否滚动到顶部,如果是则加载更多消息
1388
+ if (scrollTop <= 10 && selectedBot.value && selectedChannel.value) {
1389
+ loadMoreMessages()
1390
+ }
1391
+ }
1392
+ }
1393
+
1394
+ // 加载更多消息的函数
1395
+ async function loadMoreMessages() {
1396
+ if (!selectedBot.value || !selectedChannel.value) return
1397
+
1398
+ const channelKey = `${selectedBot.value}:${selectedChannel.value}`
1399
+
1400
+ // 检查是否正在加载或没有更多消息
1401
+ const pagination = channelPagination.value[channelKey]
1402
+ if (isLoadingMore.value || (pagination && !pagination.hasMore)) return
1403
+
1404
+ // 更新加载状态
1405
+ if (!channelPagination.value[channelKey]) {
1406
+ channelPagination.value[channelKey] = {
1407
+ offset: 0,
1408
+ hasMore: true,
1409
+ loading: false
1410
+ }
1411
+ }
1412
+
1413
+ // 设置为加载中状态
1414
+ isLoadingMore.value = true
1415
+ channelPagination.value[channelKey].loading = true
1416
+
1417
+ try {
1418
+ // 获取当前offset
1419
+ const currentOffset = pagination?.offset || 0
1420
+
1421
+ // 加载下一批消息(50条)
1422
+ const result = await loadHistoryMessages(selectedBot.value, selectedChannel.value, 50, currentOffset)
1423
+
1424
+ if (result) {
1425
+ // 更新分页状态已在loadHistoryMessages中处理
1426
+ } else {
1427
+ // 加载失败,重置加载状态
1428
+ channelPagination.value[channelKey].loading = false
1429
+ }
1430
+ } catch (error) {
1431
+ console.error('加载更多消息失败:', error)
1432
+ // 加载失败,重置加载状态
1433
+ if (channelPagination.value[channelKey]) {
1434
+ channelPagination.value[channelKey].loading = false
1435
+ }
1436
+ } finally {
1437
+ // 重置加载状态
1438
+ isLoadingMore.value = false
1369
1439
  }
1370
1440
  }
1371
1441
 
@@ -2197,102 +2267,177 @@ export function useChatLogic() {
2197
2267
 
2198
2268
  // 图片缓存管理函数
2199
2269
  async function getCachedImageUrl(channelKey: string, originalUrl: string): Promise<string | null> {
2270
+ // 检查是否正在加载中,如果是则等待加载完成
2271
+ if (loadingImages.has(originalUrl)) {
2272
+ return loadingImages.get(originalUrl) || null
2273
+ }
2274
+
2200
2275
  // 首先检查内存中的blob URL
2201
2276
  const existingBlobUrl = imageBlobUrls.value[originalUrl]
2202
2277
  if (existingBlobUrl) {
2203
2278
  return existingBlobUrl
2204
2279
  }
2205
2280
 
2206
- // 从IndexedDB获取
2207
- const cacheItem = await getImageFromDB(originalUrl)
2208
- if (!cacheItem) return null
2281
+ // 使用立即执行的异步函数来处理加载过程
2282
+ const loadingPromise = (async () => {
2283
+ try {
2284
+ // 再次检查内存中的blob URL(防止在创建Promise期间其他地方已经缓存了)
2285
+ const cachedBlobUrl = imageBlobUrls.value[originalUrl]
2286
+ if (cachedBlobUrl) {
2287
+ return cachedBlobUrl
2288
+ }
2289
+
2290
+ // 从IndexedDB获取
2291
+ const cacheItem = await getImageFromDB(originalUrl)
2292
+ if (!cacheItem) {
2293
+ return null
2294
+ }
2209
2295
 
2210
- // 检查内存使用情况
2211
- checkAndCleanupMemory()
2296
+ // 检查内存使用情况
2297
+ checkAndCleanupMemory()
2212
2298
 
2213
- // 创建blob URL并缓存到内存
2214
- const blobUrl = URL.createObjectURL(cacheItem.blob)
2215
- imageBlobUrls.value[originalUrl] = blobUrl
2299
+ // 创建blob URL并缓存到内存
2300
+ const blobUrl = URL.createObjectURL(cacheItem.blob)
2301
+ imageBlobUrls.value[originalUrl] = blobUrl
2216
2302
 
2217
- // 更新内存使用量
2218
- updateMemoryUsage(estimateBlobSize(cacheItem.blob))
2303
+ // 更新内存使用量
2304
+ updateMemoryUsage(estimateBlobSize(cacheItem.blob))
2219
2305
 
2220
- // 更新访问时间
2221
- cacheItem.timestamp = Date.now()
2222
- await saveImageToDB(cacheItem)
2306
+ // 更新访问时间
2307
+ cacheItem.timestamp = Date.now()
2308
+ await saveImageToDB(cacheItem)
2223
2309
 
2224
- return blobUrl
2310
+ return blobUrl
2311
+ } catch (error) {
2312
+ console.error('获取缓存图片失败:', error)
2313
+ return null
2314
+ } finally {
2315
+ // 加载完成后从loadingImages中移除
2316
+ loadingImages.delete(originalUrl)
2317
+ }
2318
+ })()
2319
+
2320
+ // 立即添加到loadingImages中,防止并发请求
2321
+ loadingImages.set(originalUrl, loadingPromise)
2322
+
2323
+ // 等待加载完成并返回结果
2324
+ return loadingPromise
2225
2325
  }
2226
2326
 
2227
2327
  async function cacheImage(channelKey: string, originalUrl: string): Promise<string | null> {
2228
- try {
2229
- // 检查是否已经缓存
2230
- const cached = await getCachedImageUrl(channelKey, originalUrl)
2231
- if (cached) {
2232
- return cached
2233
- }
2328
+ // 检查是否正在加载中,如果是则等待加载完成
2329
+ if (loadingImages.has(originalUrl)) {
2330
+ return loadingImages.get(originalUrl) || null
2331
+ }
2234
2332
 
2235
- // 获取图片数据
2236
- const result = await (send as any)('fetch-image', { url: originalUrl })
2333
+ // 首先检查内存中的blob URL
2334
+ const existingBlobUrl = imageBlobUrls.value[originalUrl]
2335
+ if (existingBlobUrl) {
2336
+ return existingBlobUrl
2337
+ }
2237
2338
 
2238
- if (!result.success) {
2239
- console.error('获取图片失败:', result.error)
2240
- return null
2241
- }
2339
+ // 使用立即执行的异步函数来处理加载过程
2340
+ const loadingPromise = (async () => {
2341
+ try {
2342
+ // 再次检查内存中的blob URL(防止在创建Promise期间其他地方已经缓存了)
2343
+ const cachedBlobUrl = imageBlobUrls.value[originalUrl]
2344
+ if (cachedBlobUrl) {
2345
+ return cachedBlobUrl
2346
+ }
2242
2347
 
2243
- // 将base64转换为blob
2244
- const base64Data = result.base64
2245
- const contentType = result.contentType || 'image/jpeg'
2348
+ // 从IndexedDB获取
2349
+ const cachedItem = await getImageFromDB(originalUrl)
2350
+ if (cachedItem) {
2351
+ // 检查内存使用情况
2352
+ checkAndCleanupMemory()
2246
2353
 
2247
- // 解码base64
2248
- const byteCharacters = atob(base64Data)
2249
- const byteNumbers = new Array(byteCharacters.length)
2250
- for (let i = 0; i < byteCharacters.length; i++) {
2251
- byteNumbers[i] = byteCharacters.charCodeAt(i)
2252
- }
2253
- const byteArray = new Uint8Array(byteNumbers)
2354
+ // 创建blob URL并缓存到内存
2355
+ const blobUrl = URL.createObjectURL(cachedItem.blob)
2356
+ imageBlobUrls.value[originalUrl] = blobUrl
2254
2357
 
2255
- // 保持原始MIME类型,特别是对于GIF
2256
- const blob = new Blob([byteArray], { type: contentType })
2358
+ // 更新内存使用量
2359
+ updateMemoryUsage(estimateBlobSize(cachedItem.blob))
2257
2360
 
2258
- // 检查blob大小
2259
- if (blob.size > MAX_IMAGE_SIZE) {
2260
- console.warn(`图片过大,跳过缓存: ${(blob.size / 1024 / 1024).toFixed(2)}MB`)
2261
- return null
2262
- }
2361
+ // 更新访问时间
2362
+ cachedItem.timestamp = Date.now()
2363
+ await saveImageToDB(cachedItem)
2263
2364
 
2264
- // 创建缓存项
2265
- const cacheItem: ImageCacheItem = {
2266
- url: originalUrl,
2267
- blob: blob,
2268
- timestamp: Date.now(),
2269
- size: blob.size,
2270
- channelKey: channelKey
2271
- }
2365
+ return blobUrl
2366
+ }
2272
2367
 
2273
- // 保存到IndexedDB
2274
- const saved = await saveImageToDB(cacheItem)
2275
- if (!saved) {
2276
- console.error('保存图片到IndexedDB失败')
2277
- return null
2278
- }
2368
+ // 获取图片数据
2369
+ const result = await (send as any)('fetch-image', { url: originalUrl })
2279
2370
 
2280
- // 检查内存使用情况
2281
- checkAndCleanupMemory()
2371
+ if (!result.success) {
2372
+ return null
2373
+ }
2282
2374
 
2283
- // 创建blob URL并缓存到内存
2284
- const blobUrl = URL.createObjectURL(blob)
2285
- imageBlobUrls.value[originalUrl] = blobUrl
2375
+ // 将base64转换为blob
2376
+ const base64Data = result.base64
2377
+ const contentType = result.contentType || 'image/jpeg'
2286
2378
 
2287
- // 更新内存使用量
2288
- updateMemoryUsage(estimateBlobSize(blob))
2379
+ // 解码base64
2380
+ const byteCharacters = atob(base64Data)
2381
+ const byteNumbers = new Array(byteCharacters.length)
2382
+ for (let i = 0; i < byteCharacters.length; i++) {
2383
+ byteNumbers[i] = byteCharacters.charCodeAt(i)
2384
+ }
2385
+ const byteArray = new Uint8Array(byteNumbers)
2289
2386
 
2290
- return blobUrl
2387
+ // 保持原始MIME类型,特别是对于GIF
2388
+ const blob = new Blob([byteArray], { type: contentType })
2291
2389
 
2292
- } catch (error) {
2293
- console.error('缓存图片失败:', error)
2294
- return null
2295
- }
2390
+ // 检查blob大小
2391
+ if (blob.size > MAX_IMAGE_SIZE) {
2392
+ return null
2393
+ }
2394
+
2395
+ // 再次检查内存中是否已经存在(防止并发请求)
2396
+ const concurrentBlobUrl = imageBlobUrls.value[originalUrl]
2397
+ if (concurrentBlobUrl) {
2398
+ return concurrentBlobUrl
2399
+ }
2400
+
2401
+ // 创建缓存项
2402
+ const cacheItem: ImageCacheItem = {
2403
+ url: originalUrl,
2404
+ blob: blob,
2405
+ timestamp: Date.now(),
2406
+ size: blob.size,
2407
+ channelKey: channelKey
2408
+ }
2409
+
2410
+ // 保存到IndexedDB
2411
+ const saved = await saveImageToDB(cacheItem)
2412
+ if (!saved) {
2413
+ return null
2414
+ }
2415
+
2416
+ // 检查内存使用情况
2417
+ checkAndCleanupMemory()
2418
+
2419
+ // 创建blob URL并缓存到内存
2420
+ const blobUrl = URL.createObjectURL(blob)
2421
+ imageBlobUrls.value[originalUrl] = blobUrl
2422
+
2423
+ // 更新内存使用量
2424
+ updateMemoryUsage(estimateBlobSize(blob))
2425
+
2426
+ return blobUrl
2427
+ } catch (error) {
2428
+ console.error('缓存图片失败:', error)
2429
+ return null
2430
+ } finally {
2431
+ // 加载完成后从loadingImages中移除
2432
+ loadingImages.delete(originalUrl)
2433
+ }
2434
+ })()
2435
+
2436
+ // 立即添加到loadingImages中,防止并发请求
2437
+ loadingImages.set(originalUrl, loadingPromise)
2438
+
2439
+ // 等待加载完成并返回结果
2440
+ return loadingPromise
2296
2441
  }
2297
2442
 
2298
2443
  // 清理频道的所有图片缓存
@@ -2808,13 +2953,21 @@ export function useChatLogic() {
2808
2953
  }
2809
2954
 
2810
2955
  // 获取历史消息
2811
- async function loadHistoryMessages(botId: string, channelId: string) {
2956
+ async function loadHistoryMessages(botId: string, channelId: string, limit?: number, offset?: number) {
2812
2957
  try {
2813
2958
 
2814
- const result = await (send as any)('get-history-messages', {
2959
+ const requestData: any = {
2815
2960
  selfId: botId,
2816
2961
  channelId: channelId
2817
- })
2962
+ }
2963
+
2964
+ // 如果提供了分页参数,则添加到请求中
2965
+ if (limit !== undefined) {
2966
+ requestData.limit = limit
2967
+ requestData.offset = offset || 0
2968
+ }
2969
+
2970
+ const result = await (send as any)('get-history-messages', requestData)
2818
2971
 
2819
2972
  if (result.success && result.messages) {
2820
2973
  const channelKey = `${botId}:${channelId}`
@@ -2834,14 +2987,51 @@ export function useChatLogic() {
2834
2987
  quote: msg.quote
2835
2988
  }))
2836
2989
 
2837
- // 按时间戳排序
2838
- messages.sort((a, b) => a.timestamp - b.timestamp)
2990
+ // 确保offset有默认值
2991
+ const actualOffset = offset || 0
2839
2992
 
2840
- // 设置历史消息
2841
- chatData.value.messages[channelKey] = messages
2993
+ // 如果是分页加载,需要合并到现有消息中
2994
+ if (limit !== undefined) {
2995
+ // 按时间戳排序
2996
+ messages.sort((a, b) => a.timestamp - b.timestamp)
2997
+
2998
+ // 如果是第一页(offset为0),则替换现有消息
2999
+ if (offset === 0) {
3000
+ chatData.value.messages[channelKey] = messages
3001
+ // 初始化分页状态
3002
+ channelPagination.value[channelKey] = {
3003
+ offset: messages.length,
3004
+ hasMore: messages.length >= limit && result.total > messages.length,
3005
+ loading: false
3006
+ }
3007
+ } else {
3008
+ // 否则是加载更多,需要合并到现有消息的开头(因为是历史消息)
3009
+ const existingMessages = chatData.value.messages[channelKey] || []
3010
+ chatData.value.messages[channelKey] = [...messages, ...existingMessages]
3011
+ // 更新分页状态
3012
+ const actualOffset = offset || 0
3013
+ channelPagination.value[channelKey] = {
3014
+ offset: actualOffset + messages.length,
3015
+ hasMore: messages.length >= limit && result.total > (actualOffset + messages.length),
3016
+ loading: false
3017
+ }
3018
+ }
3019
+ } else {
3020
+ // 按时间戳排序
3021
+ messages.sort((a, b) => a.timestamp - b.timestamp)
3022
+
3023
+ // 设置历史消息
3024
+ chatData.value.messages[channelKey] = messages
3025
+ // 初始化分页状态
3026
+ channelPagination.value[channelKey] = {
3027
+ offset: messages.length,
3028
+ hasMore: false, // 不分页时没有更多消息
3029
+ loading: false
3030
+ }
3031
+ }
2842
3032
 
2843
3033
  // 更新频道消息数量缓存
2844
- channelMessageCounts.value[channelKey] = messages.length
3034
+ channelMessageCounts.value[channelKey] = result.total || messages.length
2845
3035
 
2846
3036
  // 触发响应式更新
2847
3037
  chatData.value = { ...chatData.value }
@@ -3012,6 +3202,20 @@ export function useChatLogic() {
3012
3202
  // 添加点击外部关闭菜单的监听器
3013
3203
  document.addEventListener('click', handleClickOutside)
3014
3204
 
3205
+ // 首先加载插件配置
3206
+ await loadPluginConfig()
3207
+
3208
+ // 检查是否需要清空 IndexedDB
3209
+ if (pluginConfig.value.clearIndexedDBOnStart) {
3210
+ console.log('启动时清空 IndexedDB 缓存...')
3211
+ const clearResult = await clearAllIndexedDBData()
3212
+ if (clearResult) {
3213
+ console.log('IndexedDB 缓存已清空')
3214
+ } else {
3215
+ console.warn('清空 IndexedDB 缓存失败')
3216
+ }
3217
+ }
3218
+
3015
3219
  // 初始化IndexedDB
3016
3220
  const dbInitialized = await initImageDB()
3017
3221
  if (!dbInitialized) {
@@ -3030,9 +3234,6 @@ export function useChatLogic() {
3030
3234
  }, 5 * 60 * 1000)
3031
3235
  }
3032
3236
 
3033
- // 首先加载插件配置
3034
- await loadPluginConfig()
3035
-
3036
3237
  // 然后加载历史数据
3037
3238
  await loadChatData()
3038
3239
 
@@ -3120,6 +3321,7 @@ export function useChatLogic() {
3120
3321
  // 响应式数据
3121
3322
  chatData,
3122
3323
  channelMessageCounts,
3324
+ channelPagination,
3123
3325
  pluginConfig,
3124
3326
  selectedBot,
3125
3327
  selectedChannel,
@@ -3140,6 +3342,7 @@ export function useChatLogic() {
3140
3342
  showScrollButton,
3141
3343
  isUserScrolling,
3142
3344
  isSending,
3345
+ isLoadingMore,
3143
3346
  draggingChannel,
3144
3347
  dragStartPos,
3145
3348
  dragCurrentPos,
@@ -3204,11 +3407,13 @@ export function useChatLogic() {
3204
3407
  handleTouchMove,
3205
3408
  handleTouchEnd,
3206
3409
  handleInputFocus,
3410
+ loadMoreMessages,
3207
3411
 
3208
3412
  // 图片缓存相关
3209
3413
  getCachedImageUrl,
3210
3414
  cacheImage,
3211
3415
  clearChannelImageCache,
3416
+ clearAllIndexedDBData,
3212
3417
  getMemoryStats,
3213
3418
  getCacheStats,
3214
3419
 
@@ -66,6 +66,12 @@
66
66
  <div v-else class="message-content">
67
67
  <!-- 消息历史 -->
68
68
  <div class="message-history" ref="messageHistory">
69
+ <!-- 加载更多指示器 -->
70
+ <div v-if="isLoadingMore" class="loading-more-indicator">
71
+ <div class="loading-spinner"></div>
72
+ <span>加载更多消息中...</span>
73
+ </div>
74
+
69
75
  <div v-for="message in currentMessages" :key="message.id"
70
76
  :class="['message-item', { 'bot-message': message.isBot }]">
71
77
  <div class="message-avatar">
@@ -245,6 +251,7 @@ const {
245
251
  showScrollButton,
246
252
  isUserScrolling,
247
253
  isSending,
254
+ isLoadingMore,
248
255
  draggingChannel,
249
256
  dragStartPos,
250
257
  dragCurrentPos,
@@ -1918,3 +1918,27 @@ body.dragging-bubble-global {
1918
1918
  background-color: rgba(255, 255, 255, .7)
1919
1919
  }
1920
1920
  }
1921
+
1922
+ .loading-more-indicator {
1923
+ display: flex;
1924
+ align-items: center;
1925
+ justify-content: center;
1926
+ padding: 12px;
1927
+ color: #6c757d;
1928
+ font-size: 14px;
1929
+ }
1930
+
1931
+ .loading-spinner {
1932
+ width: 20px;
1933
+ height: 20px;
1934
+ border: 2px solid #f3f3f3;
1935
+ border-top: 2px solid #2196f3;
1936
+ border-radius: 50%;
1937
+ animation: spin 1s linear infinite;
1938
+ margin-right: 8px;
1939
+ }
1940
+
1941
+ @keyframes spin {
1942
+ 0% { transform: rotate(0deg); }
1943
+ 100% { transform: rotate(360deg); }
1944
+ }