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.
- package/client/web/dist/assets/Chat-BLcuHxzh.js +28 -0
- package/client/web/dist/assets/{Chat-BXVWj5Bd.css → Chat-Cee07eDL.css} +1 -1
- package/client/web/dist/assets/{MsgBody-CzrxyiFO.js → MsgBody-krZlLzRL.js} +1 -1
- package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-CLmUjKws.js +67 -0
- package/client/web/dist/assets/{index-EJ2CcODr.js → index-DOJoilEy.js} +57 -60
- package/client/web/dist/index.html +1 -1
- package/client/web/src/assets/l10n/zh-CN.po +0 -3
- package/client/web/src/components/MsgBody.vue +29 -20
- package/client/web/src/function/connect.ts +17 -18
- package/client/web/src/function/msg.ts +2 -0
- package/client/web/src/function/satori-model.ts +2 -0
- package/client/web/src/function/satori.ts +18 -12
- package/client/web/src/function/utils/appUtil.ts +27 -6
- package/client/web/src/function/utils/msgUtil.ts +27 -21
- package/client/web/src/pages/Chat.vue +35 -32
- package/lib/database.d.ts +5 -3
- package/lib/gateway.d.ts +2 -4
- package/lib/index.d.ts +17 -0
- package/lib/index.js +5409 -473
- package/lib/media.d.ts +1 -1
- package/lib/recorder.d.ts +1 -4
- package/lib/self-message.d.ts +1 -4
- package/lib/types.d.ts +4 -0
- package/package.json +25 -10
- package/src/bootstrap.ts +3 -2
- package/src/config.ts +1 -5
- package/src/database.ts +313 -139
- package/src/gateway.ts +21 -13
- package/src/index.ts +59 -54
- package/src/media.ts +43 -12
- package/src/recorder.ts +2 -12
- package/src/self-message.ts +7 -14
- package/src/server.d.ts +17 -3
- package/src/types.ts +4 -0
- package/src/web.ts +245 -151
- package/client/web/dist/assets/Chat-m4-U5uHa.js +0 -28
- package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-BBF1uxm6.js +0 -67
- package/lib/bootstrap.d.ts +0 -6
- package/lib/satori.d.ts +0 -3
- 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-
|
|
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
|
|
|
@@ -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
|
|
106
|
+
:src="getImageUrl(item)"
|
|
107
107
|
:alt="item.summary"
|
|
108
108
|
@load="imageLoaded"
|
|
109
|
-
@click="preImgClick(item
|
|
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
|
|
113
|
+
:src="getImageUrl(item)"
|
|
114
114
|
:alt="item.summary"
|
|
115
115
|
@load="imageLoaded"
|
|
116
|
-
@click="preImgClick(item
|
|
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
|
|
125
|
-
:spin="imageLoading(getImageKey(Number(index), item
|
|
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
|
|
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
|
|
134
|
+
:src="getImgSrc(getImageUrl(item))"
|
|
135
135
|
data-type="image"
|
|
136
136
|
@load="imageLoaded"
|
|
137
137
|
@error="imgLoadFail"
|
|
138
|
-
@click="imgClick(item
|
|
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
|
-
|
|
574
|
-
|
|
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
|
|
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
|
|
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'
|
|
721
|
-
|
|
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
|
|
740
|
+
async function loadImage(item: { url?: string; file?: string }, index: number, _event?: MouseEvent) {
|
|
733
741
|
if (props.selecting) return
|
|
734
|
-
const
|
|
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(
|
|
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
|
|
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
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
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
|
|
1480
|
-
|
|
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)
|
|
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
|
|
122
|
-
if (
|
|
123
|
-
chatStore.messageList = [...
|
|
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:
|
|
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(-
|
|
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
|
-
|
|
526
|
-
|
|
527
|
-
|
|
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
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
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,
|