koishi-plugin-chat-patch 6.0.0 → 6.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.
- package/client/web/dist/assets/Chat-BduEIlJM.js +34 -0
- package/client/web/dist/assets/{Chat-D4tCqRrO.css → Chat-DgkvjRIh.css} +1 -1
- package/client/web/dist/assets/{MsgBody-BaoDyXUz.js → MsgBody-CVtXorvl.js} +1 -1
- package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-D_70Txdf.js +67 -0
- package/client/web/dist/assets/{index-X9hFOAza.css → index-BW47RWcA.css} +1 -1
- package/client/web/dist/assets/{index--7-eCx0r.js → index-N2_GDY3v.js} +5 -8
- package/client/web/dist/index.html +2 -2
- package/client/web/src/assets/css/chat.css +24 -2
- 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 +12 -6
- package/client/web/src/function/custom-element.ts +31 -0
- package/client/web/src/function/msg.ts +2 -0
- package/client/web/src/function/satori-model.ts +2 -0
- package/client/web/src/function/utils/msgUtil.ts +66 -44
- package/client/web/src/pages/Chat.vue +139 -15
- package/lib/index.js +91 -31
- package/lib/types.d.ts +142 -0
- package/package.json +1 -1
- package/src/database.ts +27 -19
- package/src/recorder.ts +2 -1
- package/src/self-message.ts +4 -1
- package/src/types.ts +4 -0
- package/src/web-error-filter.ts +30 -0
- package/src/web.ts +107 -77
- package/client/web/dist/assets/Chat-BkRnAj_r.js +0 -28
- package/client/web/dist/assets/MsgBody.vue_vue_type_script_setup_true_lang-CgpEB8mj.js +0 -67
|
@@ -11,8 +11,8 @@
|
|
|
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
|
|
15
|
-
<link rel="stylesheet" crossorigin href="./assets/index-
|
|
14
|
+
<script type="module" crossorigin src="./assets/index-N2_GDY3v.js"></script>
|
|
15
|
+
<link rel="stylesheet" crossorigin href="./assets/index-BW47RWcA.css">
|
|
16
16
|
</head>
|
|
17
17
|
|
|
18
18
|
<body>
|
|
@@ -180,12 +180,21 @@ input {
|
|
|
180
180
|
border-radius: 7px;
|
|
181
181
|
}
|
|
182
182
|
.chat-pan > div.more > div:nth-child(2) > div:first-child {
|
|
183
|
-
|
|
183
|
+
display: flex;
|
|
184
|
+
flex: 0 0 auto;
|
|
185
|
+
gap: 8px;
|
|
186
|
+
margin-right: 12px;
|
|
187
|
+
background: transparent;
|
|
188
|
+
}
|
|
189
|
+
.chat-pan > div.more > div:nth-child(2) > div:first-child > .send-tool-button {
|
|
190
|
+
background: var(--color-card-1);
|
|
191
|
+
border-radius: 7px;
|
|
184
192
|
cursor: pointer;
|
|
185
193
|
height: 50px;
|
|
194
|
+
margin: 0;
|
|
186
195
|
width: 50px;
|
|
187
196
|
}
|
|
188
|
-
.chat-pan > div.more > div:nth-child(2) > div:first-child > svg {
|
|
197
|
+
.chat-pan > div.more > div:nth-child(2) > div:first-child > .send-tool-button > svg {
|
|
189
198
|
color: var(--color-font-1);
|
|
190
199
|
margin: 15px;
|
|
191
200
|
height: 20px;
|
|
@@ -225,6 +234,19 @@ input {
|
|
|
225
234
|
resize: none;
|
|
226
235
|
border: 0;
|
|
227
236
|
}
|
|
237
|
+
.chat-pan > div.more > div:nth-child(2) > div > form.custom-element-form > input.custom-element-type-input {
|
|
238
|
+
flex: 0 1 18%;
|
|
239
|
+
width: 18%;
|
|
240
|
+
min-width: 72px;
|
|
241
|
+
max-width: 130px;
|
|
242
|
+
margin-right: 8px;
|
|
243
|
+
}
|
|
244
|
+
.chat-pan > div.more > div:nth-child(2) > div > form.custom-element-form > input.custom-element-content-input,
|
|
245
|
+
.chat-pan > div.more > div:nth-child(2) > div > form.custom-element-form > textarea.custom-element-content-input {
|
|
246
|
+
flex: 1 1 auto;
|
|
247
|
+
width: auto;
|
|
248
|
+
min-width: 0;
|
|
249
|
+
}
|
|
228
250
|
.chat-pan > div.more > div:nth-child(2) > div > form > input:disabled,
|
|
229
251
|
.chat-pan > div.more > div:nth-child(2) > div > form > textarea:disabled {
|
|
230
252
|
background: var(--color-card);
|
|
@@ -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
|
}
|
|
@@ -1303,6 +1303,8 @@ export interface SaveSelfMessagePayload {
|
|
|
1303
1303
|
forwardId?: string
|
|
1304
1304
|
forwardContent?: unknown[]
|
|
1305
1305
|
sentAt?: number
|
|
1306
|
+
timestamp?: number
|
|
1307
|
+
timestampMs?: number
|
|
1306
1308
|
source?: 'webui' | 'bot' | 'plugin'
|
|
1307
1309
|
kind?: string
|
|
1308
1310
|
}
|
|
@@ -1330,6 +1332,8 @@ function selfMessageToOneBot(record: unknown): Record<string, unknown> | null {
|
|
|
1330
1332
|
const selfId = getString(obj.selfId)
|
|
1331
1333
|
const channelId = getString(obj.channelId)
|
|
1332
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)
|
|
1333
1337
|
const messageId = getString(obj.messageId)
|
|
1334
1338
|
const localId = getString(obj.id) || `self-${sentAt}`
|
|
1335
1339
|
// 只按记录里的显式 channelType 判断,不根据频道 ID 前缀推断
|
|
@@ -1399,10 +1403,11 @@ function selfMessageToOneBot(record: unknown): Record<string, unknown> | null {
|
|
|
1399
1403
|
channel_id: channelId,
|
|
1400
1404
|
guild_id: guildId || undefined,
|
|
1401
1405
|
target_id: selfId,
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
+
timestamp,
|
|
1407
|
+
time: timestamp,
|
|
1408
|
+
local_time: timestampMs,
|
|
1409
|
+
timestamp_ms: timestampMs,
|
|
1410
|
+
time_ms: timestampMs,
|
|
1406
1411
|
sender: {
|
|
1407
1412
|
user_id: selfId,
|
|
1408
1413
|
nickname: getString(login?.name) || selfId,
|
|
@@ -1469,8 +1474,9 @@ export async function loadChatHistoryFromCache(params: {
|
|
|
1469
1474
|
const raw = getObject(recordObj.raw)
|
|
1470
1475
|
const msg = satoriEventToOneBot(raw, params.platform)
|
|
1471
1476
|
if (!msg || !msg.message_id) continue
|
|
1472
|
-
const
|
|
1473
|
-
|
|
1477
|
+
const rawTime = Number(recordObj.timestampMs ?? recordObj.timestamp ?? recordObj.receivedAt ?? 0)
|
|
1478
|
+
const localTime = rawTime > 1e12 ? rawTime : rawTime * 1000
|
|
1479
|
+
if (localTime > 0) {
|
|
1474
1480
|
msg.local_time = localTime
|
|
1475
1481
|
msg.timestamp_ms = localTime
|
|
1476
1482
|
msg.time_ms = localTime
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import h from '@satorijs/element'
|
|
2
|
+
|
|
3
|
+
// 自定义消息元素:使用 Satori 元素工厂,保留命名空间扩展类型。
|
|
4
|
+
const ELEMENT_TYPE_PATTERN = /^[a-z][a-z0-9_:-]*$/i
|
|
5
|
+
|
|
6
|
+
type CustomElement = ReturnType<typeof h>
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* 将输入的类型和内容转换为 Satori 元素。
|
|
10
|
+
* 空内容不生成元素,避免发送无意义的空消息。
|
|
11
|
+
*/
|
|
12
|
+
export function buildCustomElement(type: string, content: string): CustomElement | null {
|
|
13
|
+
const elementType = type.trim()
|
|
14
|
+
const elementContent = content.trim()
|
|
15
|
+
if (!ELEMENT_TYPE_PATTERN.test(elementType) || elementContent.length === 0) return null
|
|
16
|
+
|
|
17
|
+
const normalizedType = elementType.toLowerCase()
|
|
18
|
+
if (normalizedType === 'text') return h.text(content)
|
|
19
|
+
if (normalizedType === 'img' || normalizedType === 'image') return h.img(elementContent)
|
|
20
|
+
if (normalizedType === 'file') return h.file(elementContent)
|
|
21
|
+
if (normalizedType === 'video') return h.video(elementContent)
|
|
22
|
+
if (normalizedType === 'audio' || normalizedType === 'record') return h.audio(elementContent)
|
|
23
|
+
if (normalizedType === 'json' || normalizedType === 'xml') {
|
|
24
|
+
return h(normalizedType, { data: elementContent })
|
|
25
|
+
}
|
|
26
|
+
if (normalizedType === 'markdown') {
|
|
27
|
+
return h('markdown', elementContent)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
return h(elementType, elementContent)
|
|
31
|
+
}
|
|
@@ -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),
|
|
@@ -459,12 +459,12 @@ export function parseCQ(data: any) {
|
|
|
459
459
|
* @param preShow 是否消息预显
|
|
460
460
|
* @param echo 回显的事件名
|
|
461
461
|
*/
|
|
462
|
-
function parsePreviewMarkup(source: string): MsgItemElem[] {
|
|
463
|
-
const result: MsgItemElem[] = []
|
|
464
|
-
const tagPattern = /<(
|
|
465
|
-
let last = 0
|
|
466
|
-
let match: RegExpExecArray | null
|
|
467
|
-
while ((match = tagPattern.exec(source)) !== null) {
|
|
462
|
+
function parsePreviewMarkup(source: string): MsgItemElem[] {
|
|
463
|
+
const result: MsgItemElem[] = []
|
|
464
|
+
const tagPattern = /<([a-z][\w:-]*)\s+([^>]*?)\/?>/gi
|
|
465
|
+
let last = 0
|
|
466
|
+
let match: RegExpExecArray | null
|
|
467
|
+
while ((match = tagPattern.exec(source)) !== null) {
|
|
468
468
|
if (match.index > last) {
|
|
469
469
|
result.push({ type: 'text', text: source.slice(last, match.index) })
|
|
470
470
|
}
|
|
@@ -475,20 +475,28 @@ function parsePreviewMarkup(source: string): MsgItemElem[] {
|
|
|
475
475
|
const name = attrs.match(/name\s*=\s*(?:"([^"]*)"|'([^']*)')/i)?.[1]
|
|
476
476
|
?? attrs.match(/name\s*=\s*(?:"([^"]*)"|'([^']*)')/i)?.[2]
|
|
477
477
|
?? ''
|
|
478
|
-
const id = attrs.match(/id\s*=\s*(?:"([^"]*)"|'([^']*)')/i)?.[1]
|
|
479
|
-
?? attrs.match(/id\s*=\s*(?:"([^"]*)"|'([^']*)')/i)?.[2]
|
|
480
|
-
?? ''
|
|
481
|
-
const
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
478
|
+
const id = attrs.match(/id\s*=\s*(?:"([^"]*)"|'([^']*)')/i)?.[1]
|
|
479
|
+
?? attrs.match(/id\s*=\s*(?:"([^"]*)"|'([^']*)')/i)?.[2]
|
|
480
|
+
?? ''
|
|
481
|
+
const content = attrs.match(/content\s*=\s*(?:"([^"]*)"|'([^']*)')/i)?.[1]
|
|
482
|
+
?? attrs.match(/content\s*=\s*(?:"([^"]*)"|'([^']*)')/i)?.[2]
|
|
483
|
+
?? ''
|
|
484
|
+
const data = attrs.match(/data\s*=\s*(?:"([^"]*)"|'([^']*)')/i)?.[1]
|
|
485
|
+
?? attrs.match(/data\s*=\s*(?:"([^"]*)"|'([^']*)')/i)?.[2]
|
|
486
|
+
?? ''
|
|
487
|
+
const tag = String(match[1] ?? '').toLowerCase()
|
|
488
|
+
if (tag === 'at') {
|
|
489
|
+
result.push({ type: 'at', qq: id, text: name || id })
|
|
490
|
+
} else if (tag === 'quote') {
|
|
491
|
+
result.push({ type: 'reply', id })
|
|
492
|
+
} else if (tag === 'sharp') {
|
|
493
|
+
result.push({ type: 'text', text: name ? `#${name}` : `#${id}` })
|
|
494
|
+
} else if (tag === 'text') {
|
|
495
|
+
result.push({ type: 'text', text: content })
|
|
496
|
+
} else if (['img', 'image', 'face', 'mface', 'file', 'audio', 'video'].includes(tag)) {
|
|
497
|
+
const type = tag === 'file'
|
|
498
|
+
? 'file'
|
|
499
|
+
: tag === 'audio'
|
|
492
500
|
? 'record'
|
|
493
501
|
: tag === 'video'
|
|
494
502
|
? 'video'
|
|
@@ -497,9 +505,17 @@ function parsePreviewMarkup(source: string): MsgItemElem[] {
|
|
|
497
505
|
type,
|
|
498
506
|
file: src,
|
|
499
507
|
url: src,
|
|
500
|
-
...(name ? { name } : {}),
|
|
501
|
-
})
|
|
502
|
-
}
|
|
508
|
+
...(name ? { name } : {}),
|
|
509
|
+
})
|
|
510
|
+
} else {
|
|
511
|
+
result.push({
|
|
512
|
+
type: tag,
|
|
513
|
+
...(src ? { src, file: src, url: src } : {}),
|
|
514
|
+
...(name ? { name } : {}),
|
|
515
|
+
...(content ? { content } : {}),
|
|
516
|
+
...(data ? { data } : {}),
|
|
517
|
+
})
|
|
518
|
+
}
|
|
503
519
|
last = match.index + match[0].length
|
|
504
520
|
}
|
|
505
521
|
if (last < source.length) {
|
|
@@ -518,13 +534,24 @@ function getBase64Source(item: any): string {
|
|
|
518
534
|
return ''
|
|
519
535
|
}
|
|
520
536
|
|
|
521
|
-
function isBase64Source(source: string): boolean {
|
|
522
|
-
return source.startsWith('base64://') || source.startsWith('data:')
|
|
523
|
-
}
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
537
|
+
function isBase64Source(source: string): boolean {
|
|
538
|
+
return source.startsWith('base64://') || source.startsWith('data:')
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
function toPreviewImageSrc(item: any): string {
|
|
542
|
+
const source = String(item?.url ?? item?.file ?? '')
|
|
543
|
+
if (source.startsWith('base64://')) {
|
|
544
|
+
return `data:image/png;base64,${source.slice(9)}`
|
|
545
|
+
}
|
|
546
|
+
if (source.startsWith('data:') || source.startsWith('http:') || source.startsWith('https:')) {
|
|
547
|
+
return source
|
|
548
|
+
}
|
|
549
|
+
return getLocalMediaUrl(source)
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
export function getLocalMediaUrl(value: string): string {
|
|
553
|
+
if (!value) return value
|
|
554
|
+
const isLocalPath = value.startsWith('file:') ||
|
|
528
555
|
/^[a-z]:[\\/]/i.test(value) ||
|
|
529
556
|
value.startsWith('\\\\')
|
|
530
557
|
if (!isLocalPath) return value
|
|
@@ -637,20 +664,15 @@ export async function sendMsgRaw(
|
|
|
637
664
|
const preShowMsg = typeof msg === 'string'
|
|
638
665
|
? parsePreviewMarkup(msg)
|
|
639
666
|
: 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
|
-
}
|
|
667
|
+
preShowMsg.forEach((item: any) => {
|
|
668
|
+
// 对 base64 图片做特殊处理
|
|
669
|
+
if (item.type == 'image') {
|
|
670
|
+
item.url = toPreviewImageSrc(item)
|
|
671
|
+
}
|
|
672
|
+
if (item.type == 'record' && String(item?.file ?? '').startsWith('base64://')) {
|
|
673
|
+
const b64Str = (item.file as string).substring(9)
|
|
674
|
+
item.url = 'data:audio/webm;base64,' + b64Str
|
|
675
|
+
}
|
|
654
676
|
})
|
|
655
677
|
const showMsg = {
|
|
656
678
|
revoke: true,
|