koishi-plugin-chat-patch 5.6.1 → 6.1.0

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.
Files changed (40) hide show
  1. package/client/web/dist/assets/Chat-BLcuHxzh.js +28 -0
  2. package/client/web/dist/assets/{Chat-BXVWj5Bd.css → Chat-Cee07eDL.css} +1 -1
  3. package/client/web/dist/assets/{MsgBody-CzrxyiFO.js → MsgBody-krZlLzRL.js} +1 -1
  4. package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-CLmUjKws.js +67 -0
  5. package/client/web/dist/assets/{index-EJ2CcODr.js → index-DOJoilEy.js} +57 -60
  6. package/client/web/dist/index.html +1 -1
  7. package/client/web/src/assets/l10n/zh-CN.po +0 -3
  8. package/client/web/src/components/MsgBody.vue +29 -20
  9. package/client/web/src/function/connect.ts +17 -18
  10. package/client/web/src/function/msg.ts +2 -0
  11. package/client/web/src/function/satori-model.ts +2 -0
  12. package/client/web/src/function/satori.ts +18 -12
  13. package/client/web/src/function/utils/appUtil.ts +27 -6
  14. package/client/web/src/function/utils/msgUtil.ts +27 -21
  15. package/client/web/src/pages/Chat.vue +35 -32
  16. package/lib/database.d.ts +5 -3
  17. package/lib/gateway.d.ts +2 -4
  18. package/lib/index.d.ts +17 -0
  19. package/lib/index.js +5409 -473
  20. package/lib/media.d.ts +1 -1
  21. package/lib/recorder.d.ts +1 -4
  22. package/lib/self-message.d.ts +1 -4
  23. package/lib/types.d.ts +4 -0
  24. package/package.json +25 -10
  25. package/src/bootstrap.ts +3 -2
  26. package/src/config.ts +1 -5
  27. package/src/database.ts +313 -139
  28. package/src/gateway.ts +21 -13
  29. package/src/index.ts +59 -54
  30. package/src/media.ts +43 -12
  31. package/src/recorder.ts +2 -12
  32. package/src/self-message.ts +7 -14
  33. package/src/server.d.ts +17 -3
  34. package/src/types.ts +4 -0
  35. package/src/web.ts +245 -151
  36. package/client/web/dist/assets/Chat-m4-U5uHa.js +0 -28
  37. package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-BBF1uxm6.js +0 -67
  38. package/lib/bootstrap.d.ts +0 -6
  39. package/lib/satori.d.ts +0 -3
  40. package/lib/web.d.ts +0 -7
@@ -11,7 +11,7 @@
11
11
  <link rel="stylesheet" href="./bcui/css/style.css">
12
12
  <link rel="stylesheet" href="./bcui/css/color-light.css">
13
13
  <link rel="stylesheet" href="./css/append-light.css">
14
- <script type="module" crossorigin src="./assets/index-EJ2CcODr.js"></script>
14
+ <script type="module" crossorigin src="./assets/index-DOJoilEy.js"></script>
15
15
  <link rel="stylesheet" crossorigin href="./assets/index-X9hFOAza.css">
16
16
  </head>
17
17
 
@@ -1047,9 +1047,6 @@ msgstr ""
1047
1047
  msgid "应用内通知和系统通知"
1048
1048
  msgstr ""
1049
1049
 
1050
- msgid "成员设置"
1051
- msgstr ""
1052
-
1053
1050
  msgid "成员信息"
1054
1051
  msgstr ""
1055
1052
 
@@ -103,17 +103,17 @@
103
103
  class="msg-md" />
104
104
  <img v-else-if="item.type == 'image' && item.file == 'marketface'"
105
105
  :class=" imgStyle(data.message.length, Number(index), true) + ' msg-mface'"
106
- :src="item.url"
106
+ :src="getImageUrl(item)"
107
107
  :alt="item.summary"
108
108
  @load="imageLoaded"
109
- @click="preImgClick(item.url || item.file)"
109
+ @click="preImgClick(getImageUrl(item))"
110
110
  @error="imgLoadFail">
111
111
  <img v-else-if="item.type == 'mface'"
112
112
  :class=" imgStyle(data.message.length, Number(index), true) + ' msg-mface'"
113
- :src="item.url"
113
+ :src="getImageUrl(item)"
114
114
  :alt="item.summary"
115
115
  @load="imageLoaded"
116
- @click="preImgClick(item.url || item.file)"
116
+ @click="preImgClick(getImageUrl(item))"
117
117
  @error="imgLoadFail">
118
118
  <template v-else-if="item.type == 'image'">
119
119
  <div v-show="shouldShowImagePlaceholder(item, Number(index))"
@@ -121,21 +121,21 @@
121
121
  :class="imgStyle(data.message.length, Number(index), isFace(item)) + ' msg-img-placeholder'"
122
122
  @click="loadImage(item, Number(index), $event)">
123
123
  <font-awesome-icon
124
- :icon="['fas', imageLoading(getImageKey(Number(index), item.url)) ? 'spinner' : 'image']"
125
- :spin="imageLoading(getImageKey(Number(index), item.url))" />
124
+ :icon="['fas', imageLoading(getImageKey(Number(index), getImageUrl(item))) ? 'spinner' : 'image']"
125
+ :spin="imageLoading(getImageKey(Number(index), getImageUrl(item)))" />
126
126
  <span>
127
- {{ imageLoading(getImageKey(Number(index), item.url)) ? $t('加载中') : $t('点击加载图片') }}
127
+ {{ imageLoading(getImageKey(Number(index), getImageUrl(item))) ? $t('加载中') : $t('点击加载图片') }}
128
128
  </span>
129
129
  </div>
130
130
  <img v-show="!shouldShowImagePlaceholder(item, Number(index))"
131
131
  :title="(!item.summary || item.summary == '') ? $t('预览图片') : item.summary"
132
132
  :alt="$t('图片')"
133
133
  :class=" imgStyle(data.message.length, Number(index), isFace(item))"
134
- :src="getImgSrc(item.url)"
134
+ :src="getImgSrc(getImageUrl(item))"
135
135
  data-type="image"
136
136
  @load="imageLoaded"
137
137
  @error="imgLoadFail"
138
- @click="imgClick(item.url || item.file, $event)">
138
+ @click="imgClick(getImageUrl(item), $event)">
139
139
  </template>
140
140
  <template v-else-if="item.type == 'face'">
141
141
  <EmojiFace :emoji="Emoji.get(Number(item.id))" class="msg-face" />
@@ -173,7 +173,8 @@
173
173
  <div v-if="data.fileView && Object.keys(data.fileView).length > 0"
174
174
  class="file-view">
175
175
  <img v-if="['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp'].includes(data.fileView.ext)"
176
- :src="getMediaSrc(data.fileView.url)">
176
+ :src="getMediaSrc(data.fileView.url)"
177
+ @click="preImgClick(getMediaSrc(data.fileView.url))">
177
178
  <video v-else-if="['mp4', 'avi', 'mkv', 'flv'].includes(data.fileView.ext)"
178
179
  playsinline controls muted
179
180
  autoplay>
@@ -570,11 +571,16 @@ async function loadCachedImages() {
570
571
  const selfId = authStore.loginInfo?.uin
571
572
  if (!selfId) return
572
573
  for (const seg of data.message) {
573
- if (seg.type !== 'image' || !seg.url) continue
574
- await loadCachedImage(seg.url)
574
+ const url = getImageUrl(seg)
575
+ if (seg.type !== 'image' || !url) continue
576
+ await loadCachedImage(url)
575
577
  }
576
578
  }
577
579
 
580
+ function getImageUrl(item: { url?: string; file?: string }): string {
581
+ return String(item?.url || item?.file || '')
582
+ }
583
+
578
584
  async function loadCachedImage(url: string) {
579
585
  if (resolvedImages.value[url]) return resolvedImages.value[url]
580
586
 
@@ -593,6 +599,7 @@ async function loadCachedImage(url: string) {
593
599
  }
594
600
 
595
601
  function getImgSrc(url: string): string {
602
+ if (!url) return ''
596
603
  if (url.startsWith('base64://')) {
597
604
  return `data:image/png;base64,${url.slice(9)}`
598
605
  }
@@ -617,11 +624,11 @@ function imageLoading(key: string) {
617
624
  return pendingImageLoads.value[key] === true
618
625
  }
619
626
 
620
- function shouldShowImagePlaceholder(item: { type: string, url: string }, index: number) {
627
+ function shouldShowImagePlaceholder(item: { type: string, url?: string, file?: string }, index: number) {
621
628
  if (item.type !== 'image') return false
622
629
  if (settingsStore.sysConfig.opt_no_auto_load_image !== true) return false
623
630
 
624
- return manualImageLoads.value[getImageKey(index, item.url)] !== true
631
+ return manualImageLoads.value[getImageKey(index, getImageUrl(item))] !== true
625
632
  }
626
633
 
627
634
  function getAtClass(who: number | string) {
@@ -717,8 +724,9 @@ function buildMessageImageList(): Img | undefined {
717
724
  const collect = (msg: any) => {
718
725
  if (!Array.isArray(msg?.message)) return
719
726
  for (const item of msg.message) {
720
- if (item?.type === 'image' && item?.file !== 'marketface' && item?.url) {
721
- urls.push(String(item.url))
727
+ if (item?.type === 'image' && item?.file !== 'marketface') {
728
+ const url = getImageUrl(item)
729
+ if (url) urls.push(url)
722
730
  }
723
731
  }
724
732
  }
@@ -729,15 +737,16 @@ function buildMessageImageList(): Img | undefined {
729
737
  return Img.fromList([...new Set(urls)])
730
738
  }
731
739
 
732
- async function loadImage(item: { url: string }, index: number, _event?: MouseEvent) {
740
+ async function loadImage(item: { url?: string; file?: string }, index: number, _event?: MouseEvent) {
733
741
  if (props.selecting) return
734
- const key = getImageKey(index, item.url)
742
+ const url = getImageUrl(item)
743
+ const key = getImageKey(index, url)
735
744
  if (manualImageLoads.value[key] || pendingImageLoads.value[key]) return
736
745
 
737
746
  pendingImageLoads.value[key] = true
738
747
  try {
739
748
  if (data._from_local_db) {
740
- await loadCachedImage(item.url)
749
+ await loadCachedImage(url)
741
750
  }
742
751
  manualImageLoads.value[key] = true
743
752
  } finally {
@@ -1252,7 +1261,7 @@ async function openMerge(event?: MouseEvent) {
1252
1261
  imgList.push({
1253
1262
  index: index,
1254
1263
  message_id: item.message_id,
1255
- img_url: msg.url,
1264
+ img_url: msg.url || msg.file,
1256
1265
  })
1257
1266
  index++
1258
1267
  }
@@ -6,7 +6,6 @@ import {
6
6
  botKey,
7
7
  connectBackend,
8
8
  getActiveBot,
9
- getBlockedPlatforms,
10
9
  getBootstrap,
11
10
  getLogins,
12
11
  request,
@@ -38,6 +37,7 @@ import type { ConnectionHistoryItem, LoginCacheElem } from './elements/system'
38
37
  import type { UserFriendElem, UserGroupElem } from './elements/information'
39
38
 
40
39
  const HISTORY_KEY = 'chat-patch:connection-history'
40
+ const CLIENT_MESSAGE_CACHE_LIMIT = 500
41
41
  const logger = new Logger()
42
42
  const unsupportedMethods = new Set<string>()
43
43
 
@@ -45,14 +45,6 @@ function encodeKeyPart(value: string): string {
45
45
  return encodeURIComponent(value)
46
46
  }
47
47
 
48
- function isBlockedPlatform(platform: string): boolean {
49
- return getBlockedPlatforms().some((item) => {
50
- return item.exactMatch
51
- ? platform === item.platformName
52
- : platform.includes(item.platformName)
53
- })
54
- }
55
-
56
48
  function getChannelKind(value: unknown): 'group' | 'direct' | 'unknown' {
57
49
  if (typeof value === 'number') {
58
50
  if (value === 0) return 'group'
@@ -730,6 +722,9 @@ function recordBotMessage(platform: string, selfId: string, msg: Record<string,
730
722
  const messageId = String(msg.message_id ?? '')
731
723
  if (messageId && !messages.some((item) => String(item.message_id) === messageId)) {
732
724
  messages.push(msg)
725
+ if (messages.length > CLIENT_MESSAGE_CACHE_LIMIT) {
726
+ messages.splice(0, messages.length - CLIENT_MESSAGE_CACHE_LIMIT)
727
+ }
733
728
  chatStore.sessionMessageCache.set(cacheKey, messages)
734
729
  }
735
730
  const currentShowId = String(chatStore.chatInfo.show.id ?? '')
@@ -900,7 +895,6 @@ export function restoreBotStateFromMessageCache(platform: string, selfId: string
900
895
  }
901
896
 
902
897
  function onSatoriEvent(event: SatoriEvent) {
903
- if (isBlockedPlatform(event.platform)) return
904
898
  const oneBot = satoriEventToOneBot(event.body, String(event.platform || ''))
905
899
  if (oneBot) {
906
900
  oneBot._rawSatori = {
@@ -949,7 +943,6 @@ export function flushPendingBotEvents(platform: string, selfId: string) {
949
943
  if (!events?.length) return
950
944
  pendingBotEvents.delete(key)
951
945
  for (const event of events) {
952
- if (isBlockedPlatform(event.platform)) continue
953
946
  const oneBot = satoriEventToOneBot(event.body, String(event.platform || ''))
954
947
  if (oneBot) {
955
948
  oneBot._rawSatori = {
@@ -978,7 +971,7 @@ function onSatoriReady(logins: Array<{ platform: string; selfId: string; name: s
978
971
  login.creating = false
979
972
  login.status = true
980
973
  const authStore = useAuthStore()
981
- const visibleLogins = logins.filter((item) => !isBlockedPlatform(item.platform))
974
+ const visibleLogins = logins
982
975
  for (const item of visibleLogins) {
983
976
  if (!item.platform || !item.selfId) continue
984
977
  identityInfoCache.set(identityCacheKey('user', item.platform, item.selfId, item.selfId), {
@@ -1310,6 +1303,8 @@ export interface SaveSelfMessagePayload {
1310
1303
  forwardId?: string
1311
1304
  forwardContent?: unknown[]
1312
1305
  sentAt?: number
1306
+ timestamp?: number
1307
+ timestampMs?: number
1313
1308
  source?: 'webui' | 'bot' | 'plugin'
1314
1309
  kind?: string
1315
1310
  }
@@ -1337,6 +1332,8 @@ function selfMessageToOneBot(record: unknown): Record<string, unknown> | null {
1337
1332
  const selfId = getString(obj.selfId)
1338
1333
  const channelId = getString(obj.channelId)
1339
1334
  const sentAt = Number(obj.sentAt ?? Date.now())
1335
+ const timestamp = Number(obj.timestamp) || Math.floor(sentAt / 1000)
1336
+ const timestampMs = Number(obj.timestampMs ?? sentAt)
1340
1337
  const messageId = getString(obj.messageId)
1341
1338
  const localId = getString(obj.id) || `self-${sentAt}`
1342
1339
  // 只按记录里的显式 channelType 判断,不根据频道 ID 前缀推断
@@ -1406,10 +1403,11 @@ function selfMessageToOneBot(record: unknown): Record<string, unknown> | null {
1406
1403
  channel_id: channelId,
1407
1404
  guild_id: guildId || undefined,
1408
1405
  target_id: selfId,
1409
- time: Math.floor(sentAt / 1000) || Math.floor(Date.now() / 1000),
1410
- local_time: sentAt,
1411
- timestamp_ms: sentAt,
1412
- time_ms: sentAt,
1406
+ timestamp,
1407
+ time: timestamp,
1408
+ local_time: timestampMs,
1409
+ timestamp_ms: timestampMs,
1410
+ time_ms: timestampMs,
1413
1411
  sender: {
1414
1412
  user_id: selfId,
1415
1413
  nickname: getString(login?.name) || selfId,
@@ -1476,8 +1474,9 @@ export async function loadChatHistoryFromCache(params: {
1476
1474
  const raw = getObject(recordObj.raw)
1477
1475
  const msg = satoriEventToOneBot(raw, params.platform)
1478
1476
  if (!msg || !msg.message_id) continue
1479
- const localTime = Number(recordObj.receivedAt ?? recordObj.timestampMs ?? recordObj.timestamp ?? 0)
1480
- if (localTime) {
1477
+ const rawTime = Number(recordObj.timestampMs ?? recordObj.timestamp ?? recordObj.receivedAt ?? 0)
1478
+ const localTime = rawTime > 1e12 ? rawTime : rawTime * 1000
1479
+ if (localTime > 0) {
1481
1480
  msg.local_time = localTime
1482
1481
  msg.timestamp_ms = localTime
1483
1482
  msg.time_ms = localTime
@@ -1039,6 +1039,8 @@ const msgFunctions = {
1039
1039
  message: Array.isArray(sentItem?.message) ? sentItem.message : [],
1040
1040
  source: 'webui',
1041
1041
  sentAt: Number(sentItem?.local_time ?? sentItem?.timestamp_ms ?? Date.now()),
1042
+ timestamp: Number(sentItem?.time) || Math.floor(Date.now() / 1000),
1043
+ timestampMs: Number(sentItem?.local_time ?? sentItem?.timestamp_ms ?? Date.now()),
1042
1044
  kind: String(sentItem?.message?.[0]?.type ?? 'text'),
1043
1045
  })
1044
1046
  }
@@ -456,6 +456,7 @@ export function satoriEventToOneBot(
456
456
  self_id: selfId,
457
457
  platform,
458
458
  sn: getNumber(event.sn),
459
+ timestamp: Math.floor(normalizeTimestampMs(event.timestamp) / 1000),
459
460
  timestamp_ms: getNumber(event.timestamp) ? normalizeTimestampMs(event.timestamp) : 0,
460
461
  time: Math.floor(normalizeTimestampMs(event.timestamp) / 1000),
461
462
  message_seq: getNumber(event.sn),
@@ -733,6 +734,7 @@ function messageListFromResponse(data: unknown): unknown[] {
733
734
  return {
734
735
  message_id: getString(message.id),
735
736
  sn: getNumber(message.sn),
737
+ timestamp: Math.floor(normalizeTimestampMs(message.timestamp) / 1000),
736
738
  timestamp_ms: getNumber(message.timestamp) ? normalizeTimestampMs(message.timestamp) : 0,
737
739
  time: Math.floor(normalizeTimestampMs(message.timestamp) / 1000),
738
740
  message_seq: getNumber(message.sn) || getNumber(message.seq),
@@ -57,17 +57,6 @@ function getObject(value: unknown): Record<string, unknown> {
57
57
  return typeof value === 'object' && value !== null ? value as Record<string, unknown> : {}
58
58
  }
59
59
 
60
- function isBlockedPlatform(
61
- platform: string,
62
- blockedPlatforms: SatoriBootstrap['blockedPlatforms'],
63
- ): boolean {
64
- return (blockedPlatforms ?? []).some((item) => {
65
- return item.exactMatch
66
- ? platform === item.platformName
67
- : platform.includes(item.platformName)
68
- })
69
- }
70
-
71
60
  async function parseJsonResponse(response: Response): Promise<unknown> {
72
61
  const text = await response.text()
73
62
  if (!response.ok) {
@@ -299,6 +288,7 @@ export function connect(
299
288
  let reconnectTimer = 0
300
289
  let retry = 0
301
290
  let disposed = false
291
+ let lastPongAt = 0
302
292
 
303
293
  const handleMessage = (event: MessageEvent) => {
304
294
  if (getString(event.data.source) === 'chat-patch-response') {
@@ -325,6 +315,7 @@ export function connect(
325
315
  socket = new WebSocket(wsUrl())
326
316
  socket.addEventListener('open', () => {
327
317
  retry = 0
318
+ lastPongAt = Date.now()
328
319
  socket?.send(JSON.stringify({
329
320
  op: 3,
330
321
  body: {
@@ -333,6 +324,10 @@ export function connect(
333
324
  },
334
325
  }))
335
326
  pingTimer = window.setInterval(() => {
327
+ if (Date.now() - lastPongAt > 30000) {
328
+ socket?.close()
329
+ return
330
+ }
336
331
  socket?.send(JSON.stringify({ op: 1, body: {} }))
337
332
  }, 10000)
338
333
  onStatus(true)
@@ -344,6 +339,7 @@ export function connect(
344
339
  const op = Number(raw.op)
345
340
  const body = getObject(raw.body)
346
341
  if (op === 4) {
342
+ lastPongAt = Date.now()
347
343
  const loginList = Array.isArray(body.logins) ? body.logins : []
348
344
  const seen = new Set<string>()
349
345
  const nextLogins: SatoriLogin[] = []
@@ -353,7 +349,7 @@ export function connect(
353
349
  const platform = getString(login.platform) || getString(user.platform)
354
350
  const selfId = getString(user.id) || getString(login.selfId)
355
351
  const key = [platform, selfId].map((value) => encodeURIComponent(value)).join(':')
356
- if (!platform || !selfId || seen.has(key) || isBlockedPlatform(platform, bootstrap?.blockedPlatforms ?? [])) continue
352
+ if (!platform || !selfId || seen.has(key)) continue
357
353
  seen.add(key)
358
354
  nextLogins.push({
359
355
  platform: getString(login.platform) || getString(user.platform),
@@ -371,8 +367,18 @@ export function connect(
371
367
  onReady(logins)
372
368
  return
373
369
  }
370
+ if (op === 1) {
371
+ lastPongAt = Date.now()
372
+ socket?.send(JSON.stringify({ op: 2, body: {} }))
373
+ return
374
+ }
375
+ if (op === 2) {
376
+ lastPongAt = Date.now()
377
+ return
378
+ }
374
379
  if (op === 5) return
375
380
  if (op === 0) {
381
+ lastPongAt = Date.now()
376
382
  sequence = Number(getObject(body).sn ?? sequence)
377
383
  localStorage.setItem('chat-patch:sn', String(sequence))
378
384
  onEvent({
@@ -53,6 +53,13 @@ function encodeKeyPart(value: unknown): string {
53
53
  return encodeURIComponent(String(value ?? ''))
54
54
  }
55
55
 
56
+ const HISTORY_INITIAL_LIMIT = 100
57
+ let historyRequestId = 0
58
+
59
+ function chatTargetKey(type: unknown, id: unknown, channelId: unknown): string {
60
+ return `${String(type ?? '')}:${String(id ?? '')}:${String(channelId ?? '')}`
61
+ }
62
+
56
63
  /**
57
64
  * 滚动到目标消息(不自动加载)
58
65
  * @param seqName DOM 名(chat-xx)
@@ -113,14 +120,26 @@ export async function loadHistory(info: BaseChatInfoElem) {
113
120
  const chatStore = useChatStore()
114
121
  const settingsStore = useSettingsStore()
115
122
  const uiStore = useUIStore()
123
+ const requestId = ++historyRequestId
124
+ const targetKey = chatTargetKey(info.type, info.id, info.channel_id)
125
+ const isStale = () => {
126
+ return requestId !== historyRequestId
127
+ || targetKey !== chatTargetKey(
128
+ chatStore.chatInfo.show.type,
129
+ chatStore.chatInfo.show.id,
130
+ chatStore.chatInfo.show.channel_id,
131
+ )
132
+ }
116
133
  uiStore.nowGetHistory = false
117
134
  uiStore.historyBeforeTime = undefined
135
+ uiStore.canLoadHistory = true
136
+ uiStore.loadHistoryFail = false
118
137
  chatStore.messageList = []
119
138
  const channelId = String(info.channel_id ?? info.id ?? '')
120
139
  const cacheKey = [authStore.loginInfo.platform, authStore.loginInfo.uin, channelId].map(encodeKeyPart).join(':')
121
- const cachedMessages = chatStore.sessionMessageCache.get(cacheKey)
122
- if (cachedMessages?.length) {
123
- chatStore.messageList = [...cachedMessages]
140
+ const sessionCached = chatStore.sessionMessageCache.get(cacheKey)
141
+ if (sessionCached?.length && !isStale()) {
142
+ chatStore.messageList = [...sessionCached]
124
143
  }
125
144
  // 本地有数据时立即显示,同时仍发网络请求以获取最新消息(避免遗漏)
126
145
  if (
@@ -132,17 +151,19 @@ export async function loadHistory(info: BaseChatInfoElem) {
132
151
  channelId,
133
152
  20,
134
153
  )
135
- if (localMsgs.length > 0) {
154
+ if (localMsgs.length > 0 && !isStale()) {
136
155
  chatStore.messageList = localMsgs
137
156
  }
138
157
  }
158
+ if (isStale()) return
139
159
  try {
140
160
  const cachedMessages = await loadChatHistoryFromCache({
141
161
  platform: String(authStore.loginInfo.platform ?? ''),
142
162
  selfId: String(authStore.loginInfo.uin ?? ''),
143
163
  channelId,
144
- limit: 500,
164
+ limit: HISTORY_INITIAL_LIMIT,
145
165
  })
166
+ if (isStale()) return
146
167
  if (cachedMessages.length > 0) {
147
168
  const mergedMap = new Map<string, any>()
148
169
  for (const msg of chatStore.messageList) {
@@ -167,7 +188,7 @@ export async function loadHistory(info: BaseChatInfoElem) {
167
188
  }
168
189
  return 0
169
190
  })
170
- chatStore.messageList = merged.slice(-500)
191
+ chatStore.messageList = merged.slice(-HISTORY_INITIAL_LIMIT)
171
192
  }
172
193
  } catch (error) {
173
194
  logger.error(error as Error, '[LocalHistory] 加载聊天记录缓存失败')
@@ -518,13 +518,24 @@ function getBase64Source(item: any): string {
518
518
  return ''
519
519
  }
520
520
 
521
- function isBase64Source(source: string): boolean {
522
- return source.startsWith('base64://') || source.startsWith('data:')
523
- }
524
-
525
- export function getLocalMediaUrl(value: string): string {
526
- if (!value) return value
527
- const isLocalPath = value.startsWith('file:') ||
521
+ function isBase64Source(source: string): boolean {
522
+ return source.startsWith('base64://') || source.startsWith('data:')
523
+ }
524
+
525
+ function toPreviewImageSrc(item: any): string {
526
+ const source = String(item?.url ?? item?.file ?? '')
527
+ if (source.startsWith('base64://')) {
528
+ return `data:image/png;base64,${source.slice(9)}`
529
+ }
530
+ if (source.startsWith('data:') || source.startsWith('http:') || source.startsWith('https:')) {
531
+ return source
532
+ }
533
+ return getLocalMediaUrl(source)
534
+ }
535
+
536
+ export function getLocalMediaUrl(value: string): string {
537
+ if (!value) return value
538
+ const isLocalPath = value.startsWith('file:') ||
528
539
  /^[a-z]:[\\/]/i.test(value) ||
529
540
  value.startsWith('\\\\')
530
541
  if (!isLocalPath) return value
@@ -637,20 +648,15 @@ export async function sendMsgRaw(
637
648
  const preShowMsg = typeof msg === 'string'
638
649
  ? parsePreviewMarkup(msg)
639
650
  : JSON.parse(JSON.stringify(msg));
640
- preShowMsg.forEach((item: any) => {
641
- // 对 base64 图片做特殊处理
642
- if (item.type == 'image') {
643
- if (item.file.startsWith('base64://')) {
644
- const b64Str = (item.file as string).substring(9)
645
- item.url = 'data:image/png;base64,' + b64Str
646
- } else {
647
- item.url = item.file
648
- }
649
- }
650
- if (item.type == 'record' && item.file.startsWith('base64://')) {
651
- const b64Str = (item.file as string).substring(9)
652
- item.url = 'data:audio/webm;base64,' + b64Str
653
- }
651
+ preShowMsg.forEach((item: any) => {
652
+ // 对 base64 图片做特殊处理
653
+ if (item.type == 'image') {
654
+ item.url = toPreviewImageSrc(item)
655
+ }
656
+ if (item.type == 'record' && String(item?.file ?? '').startsWith('base64://')) {
657
+ const b64Str = (item.file as string).substring(9)
658
+ item.url = 'data:audio/webm;base64,' + b64Str
659
+ }
654
660
  })
655
661
  const showMsg = {
656
662
  revoke: true,