mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-02-02 05:50:50 +08:00
feat(chat): 合并转发聊天记录支持预览与弹窗查看
- appmsg(type=19) 解析为 renderType=chatHistory,并透传 recordItem(recorditem 原文) - 修复 recorditem CDATA 内包含 <refermsg> 时误判为引用消息的问题 - 列表/导出路径统一带上 recordItem,并避免已解析的 appmsg 被二次 XML 解析覆盖 - 前端聊天页新增聊天记录卡片 + 弹窗,支持按条展示及图片/视频/引用内容预览 - 会话列表与摘要统一显示为 [聊天记录]
This commit is contained in:
@@ -318,7 +318,7 @@
|
||||
</div>
|
||||
<div v-else-if="message.renderType === 'video'" class="max-w-sm">
|
||||
<div class="msg-radius overflow-hidden relative bg-black/5" @contextmenu="openMediaContextMenu($event, message, 'video')">
|
||||
<img v-if="message.videoThumbUrl" :src="message.videoThumbUrl" alt="视频" class="max-w-[260px] max-h-[260px] object-cover">
|
||||
<img v-if="message.videoThumbUrl" :src="message.videoThumbUrl" alt="视频" class="block w-[220px] max-w-[260px] h-auto max-h-[260px] object-cover">
|
||||
<div v-else class="px-3 py-2 text-sm relative msg-bubble whitespace-pre-wrap break-words leading-relaxed"
|
||||
:class="message.isSent ? 'bg-[#95EC69] text-black bubble-tail-r' : 'bg-white text-gray-800 bubble-tail-l'">
|
||||
{{ message.content }}
|
||||
@@ -442,6 +442,29 @@
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 合并转发聊天记录(Chat History) -->
|
||||
<div
|
||||
v-else-if="message.renderType === 'chatHistory'"
|
||||
class="wechat-chat-history-card wechat-special-card msg-radius"
|
||||
:class="message.isSent ? 'wechat-special-sent-side' : ''"
|
||||
@click.stop="openChatHistoryModal(message)"
|
||||
>
|
||||
<div class="wechat-chat-history-body">
|
||||
<div class="wechat-chat-history-title">{{ message.title || '聊天记录' }}</div>
|
||||
<div class="wechat-chat-history-preview" v-if="getChatHistoryPreviewLines(message).length">
|
||||
<div
|
||||
v-for="(line, idx) in getChatHistoryPreviewLines(message)"
|
||||
:key="idx"
|
||||
class="wechat-chat-history-line"
|
||||
>
|
||||
{{ line }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="wechat-chat-history-bottom">
|
||||
<span>聊天记录</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-else-if="message.renderType === 'transfer'"
|
||||
class="wechat-transfer-card msg-radius"
|
||||
:class="[{ 'wechat-transfer-received': message.transferReceived, 'wechat-transfer-returned': isTransferReturned(message) }, message.isSent ? 'wechat-transfer-sent-side' : 'wechat-transfer-received-side']">
|
||||
@@ -927,6 +950,193 @@
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 合并转发聊天记录弹窗 -->
|
||||
<div
|
||||
v-if="chatHistoryModalVisible"
|
||||
class="fixed inset-0 z-50 bg-black/40 flex items-center justify-center"
|
||||
@click="closeChatHistoryModal"
|
||||
>
|
||||
<div
|
||||
class="w-[92vw] max-w-[560px] max-h-[80vh] bg-white rounded-xl shadow-xl overflow-hidden flex flex-col"
|
||||
@click.stop
|
||||
>
|
||||
<div class="px-4 py-3 bg-neutral-100 border-b border-gray-200 flex items-center justify-between">
|
||||
<div class="text-sm text-[#161616] truncate">{{ chatHistoryModalTitle || '聊天记录' }}</div>
|
||||
<button
|
||||
type="button"
|
||||
class="p-2 rounded hover:bg-black/5"
|
||||
@click="closeChatHistoryModal"
|
||||
aria-label="关闭"
|
||||
title="关闭"
|
||||
>
|
||||
<svg class="w-5 h-5 text-gray-700" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"/>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="flex-1 overflow-auto bg-white">
|
||||
<div v-if="!chatHistoryModalRecords.length" class="text-sm text-gray-500 text-center py-10">
|
||||
没有可显示的聊天记录
|
||||
</div>
|
||||
<template v-else>
|
||||
<div
|
||||
v-for="(rec, idx) in chatHistoryModalRecords"
|
||||
:key="rec.id || idx"
|
||||
class="px-4 py-3 flex gap-3 border-b border-gray-100"
|
||||
>
|
||||
<div class="w-9 h-9 rounded-md overflow-hidden bg-gray-200 flex-shrink-0" :class="{ 'privacy-blur': privacyMode }">
|
||||
<img
|
||||
v-if="rec.senderAvatar"
|
||||
:src="rec.senderAvatar"
|
||||
alt="头像"
|
||||
class="w-full h-full object-cover"
|
||||
/>
|
||||
<div v-else class="w-full h-full flex items-center justify-center text-xs font-bold text-gray-600">
|
||||
{{ (rec.senderDisplayName || rec.sourcename || '?').charAt(0) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="min-w-0 flex-1" :class="{ 'privacy-blur': privacyMode }">
|
||||
<div class="flex items-start gap-2">
|
||||
<div class="min-w-0 flex-1">
|
||||
<div
|
||||
v-if="chatHistoryModalInfo?.isChatRoom && (rec.senderDisplayName || rec.sourcename)"
|
||||
class="text-xs text-gray-500 leading-none truncate mb-1"
|
||||
>
|
||||
{{ rec.senderDisplayName || rec.sourcename }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="rec.fullTime || rec.sourcetime" class="text-xs text-gray-400 flex-shrink-0 leading-none">
|
||||
{{ rec.fullTime || rec.sourcetime }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-1">
|
||||
<!-- 视频 -->
|
||||
<div
|
||||
v-if="rec.renderType === 'video'"
|
||||
class="msg-radius overflow-hidden relative bg-black/5 inline-block"
|
||||
@contextmenu="openMediaContextMenu($event, rec, 'video')"
|
||||
>
|
||||
<img
|
||||
v-if="rec.videoThumbUrl && !rec._videoThumbError"
|
||||
:src="rec.videoThumbUrl"
|
||||
alt="视频"
|
||||
class="block w-[220px] max-w-[260px] h-auto max-h-[260px] object-cover"
|
||||
@error="onChatHistoryVideoThumbError(rec)"
|
||||
/>
|
||||
<div v-else class="px-3 py-2 text-sm text-gray-700">{{ rec.content || '[视频]' }}</div>
|
||||
|
||||
<a
|
||||
v-if="rec.videoThumbUrl && rec.videoUrl"
|
||||
:href="rec.videoUrl"
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
class="absolute inset-0 flex items-center justify-center"
|
||||
>
|
||||
<div class="w-12 h-12 rounded-full bg-black/45 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||
</div>
|
||||
</a>
|
||||
<div class="absolute inset-0 flex items-center justify-center" v-else-if="rec.videoThumbUrl">
|
||||
<div class="w-12 h-12 rounded-full bg-black/45 flex items-center justify-center">
|
||||
<svg class="w-6 h-6 text-white" fill="currentColor" viewBox="0 0 24 24"><path d="M8 5v14l11-7z"/></svg>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
v-if="rec.videoDuration"
|
||||
class="absolute bottom-2 right-2 text-xs text-white bg-black/55 px-1.5 py-0.5 rounded"
|
||||
>
|
||||
{{ formatChatHistoryVideoDuration(rec.videoDuration) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 图片 -->
|
||||
<div
|
||||
v-else-if="rec.renderType === 'image'"
|
||||
class="msg-radius overflow-hidden cursor-pointer inline-block"
|
||||
@click="rec.imageUrl && openImagePreview(rec.imageUrl)"
|
||||
@contextmenu="openMediaContextMenu($event, rec, 'image')"
|
||||
>
|
||||
<img
|
||||
v-if="rec.imageUrl"
|
||||
:src="rec.imageUrl"
|
||||
alt="图片"
|
||||
class="max-w-[240px] max-h-[240px] object-cover hover:opacity-90 transition-opacity"
|
||||
/>
|
||||
<div v-else class="px-3 py-2 text-sm text-gray-700">{{ rec.content || '[图片]' }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 表情 -->
|
||||
<div
|
||||
v-else-if="rec.renderType === 'emoji'"
|
||||
class="inline-block"
|
||||
@contextmenu="openMediaContextMenu($event, rec, 'emoji')"
|
||||
>
|
||||
<img v-if="rec.emojiUrl" :src="rec.emojiUrl" alt="表情" class="w-24 h-24 object-contain" />
|
||||
<div v-else class="px-3 py-2 text-sm text-gray-700">{{ rec.content || '[表情]' }}</div>
|
||||
</div>
|
||||
|
||||
<!-- 引用(回复) -->
|
||||
<div v-else-if="rec.renderType === 'quote'" class="max-w-[420px]">
|
||||
<div
|
||||
class="px-2 text-xs text-neutral-700 rounded max-w-[404px] flex items-center bg-[#e1e1e1] cursor-pointer select-none"
|
||||
@click="openChatHistoryQuote(rec)"
|
||||
@contextmenu="openMediaContextMenu($event, rec.quoteMedia || rec, rec.quote?.kind || 'message')"
|
||||
>
|
||||
<div class="w-10 h-10 rounded overflow-hidden bg-neutral-300 flex-shrink-0 mr-2">
|
||||
<img
|
||||
v-if="rec.quote?.thumbUrl && !rec._quoteThumbError"
|
||||
:src="rec.quote.thumbUrl"
|
||||
alt="引用"
|
||||
class="w-full h-full object-cover"
|
||||
@error="onChatHistoryQuoteThumbError(rec)"
|
||||
/>
|
||||
<div v-else class="w-full h-full flex items-center justify-center text-[10px] text-neutral-600">
|
||||
{{ rec.quote?.kind === 'video' ? '视频' : (rec.quote?.kind === 'image' ? '图片' : '表情') }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0 flex-1 py-2">
|
||||
<div class="line-clamp-2">
|
||||
{{ rec.quote?.label || (rec.quote?.kind === 'video' ? '[视频]' : (rec.quote?.kind === 'image' ? '[图片]' : '[表情]')) }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="rec.quote?.kind === 'video' && rec.quote?.duration" class="ml-2 flex-shrink-0 text-[11px] text-neutral-600">
|
||||
{{ formatChatHistoryVideoDuration(rec.quote.duration) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="mt-1 text-sm text-gray-900 whitespace-pre-wrap break-words leading-relaxed"
|
||||
@contextmenu="openMediaContextMenu($event, rec, 'message')"
|
||||
>
|
||||
<span v-for="(seg, sidx) in parseTextWithEmoji(rec.content)" :key="sidx">
|
||||
<span v-if="seg.type === 'text'">{{ seg.content }}</span>
|
||||
<img v-else :src="seg.emojiSrc" :alt="seg.content" class="inline-block w-[1.25em] h-[1.25em] align-text-bottom mx-px">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 文本/其它 -->
|
||||
<div
|
||||
v-else
|
||||
class="text-sm text-gray-900 whitespace-pre-wrap break-words leading-relaxed"
|
||||
@contextmenu="openMediaContextMenu($event, rec, 'message')"
|
||||
>
|
||||
<span v-for="(seg, sidx) in parseTextWithEmoji(rec.content)" :key="sidx">
|
||||
<span v-if="seg.type === 'text'">{{ seg.content }}</span>
|
||||
<img v-else :src="seg.emojiSrc" :alt="seg.content" class="inline-block w-[1.25em] h-[1.25em] align-text-bottom mx-px">
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="contextMenu.visible"
|
||||
class="fixed z-50 bg-white border border-gray-200 rounded-md shadow-lg text-sm"
|
||||
@@ -3386,6 +3596,7 @@ const normalizeMessage = (msg) => {
|
||||
voipType: msg.voipType || '',
|
||||
title: msg.title || '',
|
||||
url: msg.url || '',
|
||||
recordItem: msg.recordItem || '',
|
||||
imageMd5: msg.imageMd5 || '',
|
||||
imageFileId: msg.imageFileId || '',
|
||||
emojiMd5: msg.emojiMd5 || '',
|
||||
@@ -3468,6 +3679,371 @@ const onEmojiDownloadClick = async (message) => {
|
||||
}
|
||||
}
|
||||
|
||||
const getChatHistoryPreviewLines = (message) => {
|
||||
const raw = String(message?.content || '').trim()
|
||||
if (!raw) return []
|
||||
return raw.split(/\r?\n/).map((x) => x.trim()).filter(Boolean).slice(0, 4)
|
||||
}
|
||||
|
||||
// 合并转发聊天记录弹窗
|
||||
const chatHistoryModalVisible = ref(false)
|
||||
const chatHistoryModalTitle = ref('')
|
||||
const chatHistoryModalRecords = ref([])
|
||||
const chatHistoryModalInfo = ref({ isChatRoom: false })
|
||||
|
||||
const isMaybeMd5 = (value) => /^[0-9a-f]{32}$/i.test(String(value || '').trim())
|
||||
const pickFirstMd5 = (...values) => {
|
||||
for (const v of values) {
|
||||
const s = String(v || '').trim()
|
||||
if (isMaybeMd5(s)) return s.toLowerCase()
|
||||
}
|
||||
return ''
|
||||
}
|
||||
|
||||
const normalizeChatHistoryUrl = (value) => String(value || '').trim().replace(/\s+/g, '')
|
||||
|
||||
const parseChatHistoryRecord = (recordItemXml) => {
|
||||
if (!process.client) return { info: null, items: [] }
|
||||
const xml = String(recordItemXml || '').trim()
|
||||
if (!xml) return { info: null, items: [] }
|
||||
|
||||
const normalized = xml.replace(/ /g, ' ')
|
||||
let doc
|
||||
try {
|
||||
doc = new DOMParser().parseFromString(normalized, 'text/xml')
|
||||
} catch {
|
||||
return { info: null, items: [] }
|
||||
}
|
||||
|
||||
const parserErrors = doc.getElementsByTagName('parsererror')
|
||||
if (parserErrors && parserErrors.length) return { info: null, items: [] }
|
||||
|
||||
const getText = (node, tag) => {
|
||||
try {
|
||||
const el = node.getElementsByTagName(tag)?.[0]
|
||||
return String(el?.textContent || '').trim()
|
||||
} catch {
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
const root = doc?.documentElement
|
||||
const isChatRoom = String(getText(root, 'isChatRoom') || '').trim() === '1'
|
||||
const title = getText(root, 'title')
|
||||
const desc = getText(root, 'desc') || getText(root, 'info')
|
||||
|
||||
const items = Array.from(doc.getElementsByTagName('dataitem') || [])
|
||||
const parsed = items.map((node, idx) => {
|
||||
const datatype = String(node.getAttribute('datatype') || '').trim()
|
||||
const dataid = String(node.getAttribute('dataid') || '').trim() || String(idx)
|
||||
|
||||
const sourcename = getText(node, 'sourcename')
|
||||
const sourcetime = getText(node, 'sourcetime')
|
||||
const sourceheadurl = normalizeChatHistoryUrl(getText(node, 'sourceheadurl'))
|
||||
const datatitle = getText(node, 'datatitle')
|
||||
const datadesc = getText(node, 'datadesc')
|
||||
const datafmt = getText(node, 'datafmt')
|
||||
const duration = getText(node, 'duration')
|
||||
|
||||
const fullmd5 = getText(node, 'fullmd5')
|
||||
const thumbfullmd5 = getText(node, 'thumbfullmd5')
|
||||
const md5 = getText(node, 'md5') || getText(node, 'emoticonmd5') || getText(node, 'emojiMd5')
|
||||
|
||||
let content = datatitle || datadesc
|
||||
if (!content) {
|
||||
if (datatype === '4') content = '[视频]'
|
||||
else if (datatype === '2' || datatype === '3') content = '[图片]'
|
||||
else if (datatype === '47' || datatype === '37') content = '[表情]'
|
||||
else if (datatype) content = `[消息 ${datatype}]`
|
||||
else content = '[消息]'
|
||||
}
|
||||
|
||||
// Guess renderType using both datatype and available tags.
|
||||
const fmt = String(datafmt || '').trim().toLowerCase().replace(/^\./, '')
|
||||
const imageFormats = new Set(['jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp', 'heic', 'heif'])
|
||||
|
||||
let renderType = 'text'
|
||||
if (datatype === '4' || String(duration || '').trim() || fmt === 'mp4') {
|
||||
renderType = 'video'
|
||||
} else if (datatype === '47' || datatype === '37') {
|
||||
renderType = 'emoji'
|
||||
} else if (
|
||||
datatype === '2'
|
||||
|| datatype === '3'
|
||||
|| imageFormats.has(fmt)
|
||||
|| (datatype !== '1' && isMaybeMd5(fullmd5))
|
||||
) {
|
||||
renderType = 'image'
|
||||
} else if (isMaybeMd5(md5) && /表情/.test(String(content || ''))) {
|
||||
// Some merged-forward records use non-standard datatype but still provide emoticon md5.
|
||||
renderType = 'emoji'
|
||||
}
|
||||
|
||||
return {
|
||||
id: dataid,
|
||||
datatype,
|
||||
sourcename,
|
||||
sourcetime,
|
||||
sourceheadurl,
|
||||
datafmt,
|
||||
duration,
|
||||
fullmd5,
|
||||
thumbfullmd5,
|
||||
md5,
|
||||
renderType,
|
||||
content
|
||||
}
|
||||
})
|
||||
|
||||
return {
|
||||
info: { isChatRoom, title, desc },
|
||||
items: parsed
|
||||
}
|
||||
}
|
||||
|
||||
const formatChatHistoryVideoDuration = (value) => {
|
||||
const total = Math.max(0, parseInt(String(value || '').trim(), 10) || 0)
|
||||
const m = Math.floor(total / 60)
|
||||
const s = total % 60
|
||||
if (m <= 0) return `0:${String(s).padStart(2, '0')}`
|
||||
return `${m}:${String(s).padStart(2, '0')}`
|
||||
}
|
||||
|
||||
const normalizeChatHistoryRecordItem = (rec) => {
|
||||
const mediaBase = process.client ? 'http://localhost:8000' : ''
|
||||
const account = encodeURIComponent(selectedAccount.value || '')
|
||||
const username = encodeURIComponent(selectedContact.value?.username || '')
|
||||
|
||||
const out = { ...(rec || {}) }
|
||||
out.senderDisplayName = String(out.sourcename || '').trim()
|
||||
out.senderAvatar = normalizeChatHistoryUrl(out.sourceheadurl)
|
||||
out.fullTime = String(out.sourcetime || '').trim()
|
||||
|
||||
if (out.renderType === 'video') {
|
||||
out.videoMd5 = pickFirstMd5(out.fullmd5, out.md5)
|
||||
out.videoThumbMd5 = pickFirstMd5(out.thumbfullmd5)
|
||||
out.videoDuration = String(out.duration || '').trim()
|
||||
const thumbCandidates = []
|
||||
if (out.videoMd5) {
|
||||
thumbCandidates.push(`${mediaBase}/api/chat/media/video_thumb?account=${account}&md5=${encodeURIComponent(out.videoMd5)}&username=${username}`)
|
||||
}
|
||||
if (out.videoThumbMd5 && out.videoThumbMd5 !== out.videoMd5) {
|
||||
thumbCandidates.push(`${mediaBase}/api/chat/media/video_thumb?account=${account}&md5=${encodeURIComponent(out.videoThumbMd5)}&username=${username}`)
|
||||
}
|
||||
out._videoThumbCandidates = thumbCandidates
|
||||
out._videoThumbCandidateIndex = 0
|
||||
out._videoThumbError = false
|
||||
out.videoThumbUrl = thumbCandidates[0] || ''
|
||||
out.videoUrl = out.videoMd5
|
||||
? `${mediaBase}/api/chat/media/video?account=${account}&md5=${encodeURIComponent(out.videoMd5)}&username=${username}`
|
||||
: ''
|
||||
if (!out.content || /^\[.+\]$/.test(String(out.content || '').trim())) out.content = '[视频]'
|
||||
} else if (out.renderType === 'emoji') {
|
||||
out.emojiMd5 = pickFirstMd5(out.md5, out.fullmd5, out.thumbfullmd5)
|
||||
out.emojiUrl = out.emojiMd5
|
||||
? `${mediaBase}/api/chat/media/emoji?account=${account}&md5=${encodeURIComponent(out.emojiMd5)}&username=${username}`
|
||||
: ''
|
||||
if (!out.content || /^\[.+\]$/.test(String(out.content || '').trim())) out.content = '[表情]'
|
||||
} else if (out.renderType === 'image') {
|
||||
out.imageMd5 = pickFirstMd5(out.fullmd5, out.thumbfullmd5, out.md5)
|
||||
out.imageUrl = out.imageMd5
|
||||
? `${mediaBase}/api/chat/media/image?account=${account}&md5=${encodeURIComponent(out.imageMd5)}&username=${username}`
|
||||
: ''
|
||||
if (!out.content || /^\[.+\]$/.test(String(out.content || '').trim())) out.content = '[图片]'
|
||||
}
|
||||
|
||||
return out
|
||||
}
|
||||
|
||||
const enhanceChatHistoryRecords = (records) => {
|
||||
const list = Array.isArray(records) ? records : []
|
||||
const videoByThumbMd5 = new Map()
|
||||
const videoByMd5 = new Map()
|
||||
const imageByMd5 = new Map()
|
||||
const emojiByMd5 = new Map()
|
||||
|
||||
for (const rec of list) {
|
||||
if (!rec) continue
|
||||
if (rec.renderType === 'video' && rec.videoThumbMd5) {
|
||||
videoByThumbMd5.set(String(rec.videoThumbMd5).toLowerCase(), rec)
|
||||
}
|
||||
if (rec.renderType === 'video' && rec.videoMd5) {
|
||||
videoByMd5.set(String(rec.videoMd5).toLowerCase(), rec)
|
||||
}
|
||||
if (rec.renderType === 'image') {
|
||||
const keys = [
|
||||
pickFirstMd5(rec.imageMd5),
|
||||
pickFirstMd5(rec.fullmd5),
|
||||
pickFirstMd5(rec.thumbfullmd5),
|
||||
].filter(Boolean)
|
||||
for (const k of keys) imageByMd5.set(k, rec)
|
||||
}
|
||||
if (rec.renderType === 'emoji') {
|
||||
const keys = [
|
||||
pickFirstMd5(rec.emojiMd5),
|
||||
pickFirstMd5(rec.md5),
|
||||
pickFirstMd5(rec.fullmd5),
|
||||
pickFirstMd5(rec.thumbfullmd5),
|
||||
].filter(Boolean)
|
||||
for (const k of keys) emojiByMd5.set(k, rec)
|
||||
}
|
||||
}
|
||||
|
||||
for (const rec of list) {
|
||||
if (!rec) continue
|
||||
if (String(rec.renderType || '') !== 'text') continue
|
||||
|
||||
const refKey = pickFirstMd5(rec.thumbfullmd5) || pickFirstMd5(rec.fullmd5)
|
||||
if (!refKey) continue
|
||||
|
||||
const v = videoByThumbMd5.get(refKey) || videoByMd5.get(refKey)
|
||||
if (v) {
|
||||
const quoteThumbCandidates = Array.isArray(v._videoThumbCandidates) ? v._videoThumbCandidates.slice() : []
|
||||
rec._quoteThumbCandidates = quoteThumbCandidates
|
||||
rec._quoteThumbCandidateIndex = 0
|
||||
rec._quoteThumbError = false
|
||||
const quoteThumbUrl = quoteThumbCandidates[0] || v.videoThumbUrl || ''
|
||||
rec.renderType = 'quote'
|
||||
rec.quote = {
|
||||
kind: 'video',
|
||||
thumbUrl: quoteThumbUrl,
|
||||
url: v.videoUrl || '',
|
||||
duration: v.videoDuration || '',
|
||||
label: v.content || '[视频]',
|
||||
targetId: v.id || ''
|
||||
}
|
||||
rec.quoteMedia = {
|
||||
videoMd5: v.videoMd5,
|
||||
videoThumbMd5: v.videoThumbMd5,
|
||||
videoUrl: v.videoUrl,
|
||||
videoThumbUrl: quoteThumbUrl
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const img = imageByMd5.get(refKey)
|
||||
if (img) {
|
||||
rec.renderType = 'quote'
|
||||
rec.quote = {
|
||||
kind: 'image',
|
||||
thumbUrl: img.imageUrl || '',
|
||||
url: img.imageUrl || '',
|
||||
label: img.content || '[图片]',
|
||||
targetId: img.id || ''
|
||||
}
|
||||
rec.quoteMedia = {
|
||||
imageMd5: img.imageMd5,
|
||||
imageUrl: img.imageUrl
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
const em = emojiByMd5.get(refKey)
|
||||
if (em) {
|
||||
rec.renderType = 'quote'
|
||||
rec.quote = {
|
||||
kind: 'emoji',
|
||||
thumbUrl: em.emojiUrl || '',
|
||||
url: em.emojiUrl || '',
|
||||
label: em.content || '[表情]',
|
||||
targetId: em.id || ''
|
||||
}
|
||||
rec.quoteMedia = {
|
||||
emojiMd5: em.emojiMd5,
|
||||
emojiUrl: em.emojiUrl
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return list
|
||||
}
|
||||
|
||||
const onChatHistoryVideoThumbError = (rec) => {
|
||||
if (!rec) return
|
||||
const candidates = rec._videoThumbCandidates
|
||||
if (!Array.isArray(candidates) || candidates.length <= 1) {
|
||||
rec._videoThumbError = true
|
||||
return
|
||||
}
|
||||
|
||||
const cur = Math.max(0, Number(rec._videoThumbCandidateIndex || 0))
|
||||
const next = cur + 1
|
||||
if (next < candidates.length) {
|
||||
rec._videoThumbCandidateIndex = next
|
||||
rec.videoThumbUrl = candidates[next]
|
||||
return
|
||||
}
|
||||
rec._videoThumbError = true
|
||||
}
|
||||
|
||||
const onChatHistoryQuoteThumbError = (rec) => {
|
||||
if (!rec || !rec.quote) return
|
||||
const candidates = rec._quoteThumbCandidates
|
||||
if (!Array.isArray(candidates) || candidates.length <= 1) {
|
||||
rec._quoteThumbError = true
|
||||
return
|
||||
}
|
||||
|
||||
const cur = Math.max(0, Number(rec._quoteThumbCandidateIndex || 0))
|
||||
const next = cur + 1
|
||||
if (next < candidates.length) {
|
||||
rec._quoteThumbCandidateIndex = next
|
||||
rec.quote.thumbUrl = candidates[next]
|
||||
return
|
||||
}
|
||||
rec._quoteThumbError = true
|
||||
}
|
||||
|
||||
const openChatHistoryQuote = (rec) => {
|
||||
if (!process.client) return
|
||||
const q = rec?.quote
|
||||
if (!q) return
|
||||
|
||||
const kind = String(q.kind || '')
|
||||
const url = String(q.url || '').trim()
|
||||
if (!url) return
|
||||
|
||||
if (kind === 'video') {
|
||||
try {
|
||||
window.open(url, '_blank', 'noreferrer')
|
||||
} catch {}
|
||||
return
|
||||
}
|
||||
|
||||
if (kind === 'image' || kind === 'emoji') {
|
||||
openImagePreview(url)
|
||||
}
|
||||
}
|
||||
|
||||
const openChatHistoryModal = (message) => {
|
||||
if (!process.client) return
|
||||
chatHistoryModalTitle.value = String(message?.title || '聊天记录')
|
||||
|
||||
const recordItem = String(message?.recordItem || '').trim()
|
||||
const parsed = parseChatHistoryRecord(recordItem)
|
||||
chatHistoryModalInfo.value = parsed?.info || { isChatRoom: false }
|
||||
const records = parsed?.items
|
||||
chatHistoryModalRecords.value = Array.isArray(records) ? enhanceChatHistoryRecords(records.map(normalizeChatHistoryRecordItem)) : []
|
||||
|
||||
if (!chatHistoryModalRecords.value.length) {
|
||||
// 降级:使用摘要内容按行展示
|
||||
const lines = String(message?.content || '').trim().split(/\r?\n/).map((x) => x.trim()).filter(Boolean)
|
||||
chatHistoryModalInfo.value = { isChatRoom: false }
|
||||
chatHistoryModalRecords.value = lines.map((line, idx) => normalizeChatHistoryRecordItem({ id: String(idx), datatype: '1', sourcename: '', sourcetime: '', content: line, renderType: 'text' }))
|
||||
}
|
||||
|
||||
chatHistoryModalVisible.value = true
|
||||
document.body.style.overflow = 'hidden'
|
||||
}
|
||||
|
||||
const closeChatHistoryModal = () => {
|
||||
chatHistoryModalVisible.value = false
|
||||
chatHistoryModalTitle.value = ''
|
||||
chatHistoryModalRecords.value = []
|
||||
chatHistoryModalInfo.value = { isChatRoom: false }
|
||||
document.body.style.overflow = previewImageUrl.value ? 'hidden' : ''
|
||||
}
|
||||
|
||||
const onGlobalClick = (e) => {
|
||||
if (contextMenu.value.visible) closeContextMenu()
|
||||
if (messageSearchSenderDropdownOpen.value) {
|
||||
@@ -3504,6 +4080,7 @@ const onGlobalKeyDown = (e) => {
|
||||
if (key === 'Escape') {
|
||||
if (contextMenu.value.visible) closeContextMenu()
|
||||
if (previewImageUrl.value) closeImagePreview()
|
||||
if (chatHistoryModalVisible.value) closeChatHistoryModal()
|
||||
if (messageSearchSenderDropdownOpen.value) closeMessageSearchSenderDropdown()
|
||||
if (messageSearchOpen.value) closeMessageSearch()
|
||||
if (searchContext.value?.active) exitSearchContext()
|
||||
@@ -4105,6 +4682,65 @@ const LinkCard = defineComponent({
|
||||
right: -4px;
|
||||
}
|
||||
|
||||
.wechat-chat-history-card {
|
||||
width: 210px;
|
||||
background: #ffffff;
|
||||
border-radius: var(--message-radius);
|
||||
cursor: pointer;
|
||||
transition: background-color 0.15s ease;
|
||||
}
|
||||
|
||||
.wechat-chat-history-card:hover {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
|
||||
.wechat-chat-history-body {
|
||||
padding: 10px 12px;
|
||||
}
|
||||
|
||||
.wechat-chat-history-title {
|
||||
font-size: 14px;
|
||||
font-weight: 400;
|
||||
color: #161616;
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.wechat-chat-history-preview {
|
||||
font-size: 12px;
|
||||
color: #6b7280;
|
||||
line-height: 1.4;
|
||||
}
|
||||
|
||||
.wechat-chat-history-line {
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.wechat-chat-history-bottom {
|
||||
height: 27px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0 12px;
|
||||
border-top: none;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.wechat-chat-history-bottom::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 13px;
|
||||
right: 13px;
|
||||
height: 1.5px;
|
||||
background: #e8e8e8;
|
||||
}
|
||||
|
||||
.wechat-chat-history-bottom span {
|
||||
font-size: 12px;
|
||||
color: #b2b2b2;
|
||||
}
|
||||
|
||||
/* 转账消息样式 - 微信风格 */
|
||||
.wechat-transfer-card {
|
||||
width: 210px;
|
||||
|
||||
@@ -890,6 +890,7 @@ def _parse_message_for_export(
|
||||
content_text = raw_text
|
||||
title = ""
|
||||
url = ""
|
||||
record_item = ""
|
||||
image_md5 = ""
|
||||
image_file_id = ""
|
||||
emoji_md5 = ""
|
||||
@@ -929,6 +930,7 @@ def _parse_message_for_export(
|
||||
content_text = str(parsed.get("content") or "")
|
||||
title = str(parsed.get("title") or "")
|
||||
url = str(parsed.get("url") or "")
|
||||
record_item = str(parsed.get("recordItem") or "")
|
||||
quote_title = str(parsed.get("quoteTitle") or "")
|
||||
quote_content = str(parsed.get("quoteContent") or "")
|
||||
amount = str(parsed.get("amount") or "")
|
||||
@@ -1089,14 +1091,17 @@ def _parse_message_for_export(
|
||||
content_text = _infer_message_brief_by_local_type(local_type)
|
||||
else:
|
||||
if content_text.startswith("<") or content_text.startswith('"<'):
|
||||
parsed_special = False
|
||||
if "<appmsg" in content_text.lower():
|
||||
parsed = _parse_app_message(content_text)
|
||||
rt = str(parsed.get("renderType") or "")
|
||||
if rt and rt != "text":
|
||||
parsed_special = True
|
||||
render_type = rt
|
||||
content_text = str(parsed.get("content") or content_text)
|
||||
title = str(parsed.get("title") or title)
|
||||
url = str(parsed.get("url") or url)
|
||||
record_item = str(parsed.get("recordItem") or record_item)
|
||||
quote_title = str(parsed.get("quoteTitle") or quote_title)
|
||||
quote_content = str(parsed.get("quoteContent") or quote_content)
|
||||
amount = str(parsed.get("amount") or amount)
|
||||
@@ -1121,9 +1126,11 @@ def _parse_message_for_export(
|
||||
)
|
||||
if not content_text:
|
||||
content_text = transfer_status or "转账"
|
||||
t = _extract_xml_tag_text(content_text, "title")
|
||||
d = _extract_xml_tag_text(content_text, "des")
|
||||
content_text = t or d or _infer_message_brief_by_local_type(local_type)
|
||||
|
||||
if not parsed_special:
|
||||
t = _extract_xml_tag_text(content_text, "title")
|
||||
d = _extract_xml_tag_text(content_text, "des")
|
||||
content_text = t or d or _infer_message_brief_by_local_type(local_type)
|
||||
|
||||
if not content_text:
|
||||
content_text = _infer_message_brief_by_local_type(local_type)
|
||||
@@ -1151,6 +1158,7 @@ def _parse_message_for_export(
|
||||
"content": content_text,
|
||||
"title": title,
|
||||
"url": url,
|
||||
"recordItem": record_item,
|
||||
"thumbUrl": thumb_url,
|
||||
"imageMd5": image_md5,
|
||||
"imageFileId": image_file_id,
|
||||
|
||||
@@ -171,7 +171,7 @@ def _infer_last_message_brief(msg_type: Optional[int], sub_type: Optional[int])
|
||||
if s == 2003:
|
||||
return "[Red Packet]"
|
||||
if s == 19:
|
||||
return "[Chat History]"
|
||||
return "[聊天记录]"
|
||||
return "[App Message]"
|
||||
if t == 10000:
|
||||
return "[System]"
|
||||
@@ -209,7 +209,7 @@ def _infer_message_brief_by_local_type(local_type: Optional[int]) -> str:
|
||||
if t == 8594229559345:
|
||||
return "[Red Packet]"
|
||||
if t == 81604378673:
|
||||
return "[Chat History]"
|
||||
return "[聊天记录]"
|
||||
if t == 266287972401:
|
||||
return "[Pat]"
|
||||
if t == 8589934592049:
|
||||
@@ -698,6 +698,22 @@ def _parse_app_message(text: str) -> dict[str, Any]:
|
||||
|
||||
lower = text.lower()
|
||||
|
||||
if app_type == 19:
|
||||
# 合并转发聊天记录(Chat History)
|
||||
# 注意:recorditem 的 CDATA 内部可能包含 <refermsg> 等标签,不能据此把整条消息误判为引用消息。
|
||||
record_item = _extract_xml_tag_text(text, "recorditem")
|
||||
preview = (des or "").strip()
|
||||
if not preview:
|
||||
if record_item:
|
||||
preview = str(_extract_xml_tag_text(record_item, "desc") or "").strip()
|
||||
|
||||
return {
|
||||
"renderType": "chatHistory",
|
||||
"content": preview or "[聊天记录]",
|
||||
"title": (title or "").strip() or "聊天记录",
|
||||
"recordItem": record_item or "",
|
||||
}
|
||||
|
||||
if app_type in (5, 68) and url:
|
||||
thumb_url = _extract_xml_tag_text(text, "thumburl")
|
||||
return {
|
||||
@@ -724,7 +740,21 @@ def _parse_app_message(text: str) -> dict[str, Any]:
|
||||
"fileMd5": file_md5 or "",
|
||||
}
|
||||
|
||||
if app_type == 57 or "<refermsg" in lower:
|
||||
refermsg_probe = lower
|
||||
if "<recorditem" in lower and "<refermsg" in lower:
|
||||
# 合并转发聊天记录/其它 appmsg 里可能在 recorditem CDATA 内包含 refermsg,
|
||||
# 需要先剔除 recorditem 再判断是否为真正的引用消息。
|
||||
try:
|
||||
refermsg_probe = re.sub(
|
||||
r"(<recorditem[^>]*>.*?</recorditem>)",
|
||||
"",
|
||||
text,
|
||||
flags=re.IGNORECASE | re.DOTALL,
|
||||
).lower()
|
||||
except Exception:
|
||||
refermsg_probe = lower
|
||||
|
||||
if app_type == 57 or "<refermsg" in refermsg_probe:
|
||||
refer_block = _extract_refermsg_block(text)
|
||||
|
||||
try:
|
||||
@@ -944,6 +974,8 @@ def _build_latest_message_preview(
|
||||
rt = str(parsed.get("renderType") or "")
|
||||
content_text = str(parsed.get("content") or "")
|
||||
title_text = str(parsed.get("title") or "").strip()
|
||||
if rt == "chatHistory":
|
||||
content_text = "[聊天记录]"
|
||||
if rt == "file" and title_text:
|
||||
content_text = title_text
|
||||
if (not content_text) and rt == "transfer":
|
||||
|
||||
@@ -373,6 +373,7 @@ def _append_full_messages_from_rows(
|
||||
content_text = raw_text
|
||||
title = ""
|
||||
url = ""
|
||||
record_item = ""
|
||||
image_md5 = ""
|
||||
emoji_md5 = ""
|
||||
emoji_url = ""
|
||||
@@ -414,6 +415,7 @@ def _append_full_messages_from_rows(
|
||||
content_text = str(parsed.get("content") or "")
|
||||
title = str(parsed.get("title") or "")
|
||||
url = str(parsed.get("url") or "")
|
||||
record_item = str(parsed.get("recordItem") or "")
|
||||
quote_title = str(parsed.get("quoteTitle") or "")
|
||||
quote_content = str(parsed.get("quoteContent") or "")
|
||||
quote_username = str(parsed.get("quoteUsername") or "")
|
||||
@@ -606,14 +608,17 @@ def _append_full_messages_from_rows(
|
||||
content_text = _infer_message_brief_by_local_type(local_type)
|
||||
else:
|
||||
if content_text.startswith("<") or content_text.startswith('"<'):
|
||||
parsed_special = False
|
||||
if "<appmsg" in content_text.lower():
|
||||
parsed = _parse_app_message(content_text)
|
||||
rt = str(parsed.get("renderType") or "")
|
||||
if rt and rt != "text":
|
||||
parsed_special = True
|
||||
render_type = rt
|
||||
content_text = str(parsed.get("content") or content_text)
|
||||
title = str(parsed.get("title") or title)
|
||||
url = str(parsed.get("url") or url)
|
||||
record_item = str(parsed.get("recordItem") or record_item)
|
||||
quote_title = str(parsed.get("quoteTitle") or quote_title)
|
||||
quote_content = str(parsed.get("quoteContent") or quote_content)
|
||||
amount = str(parsed.get("amount") or amount)
|
||||
@@ -639,9 +644,11 @@ def _append_full_messages_from_rows(
|
||||
)
|
||||
if not content_text:
|
||||
content_text = transfer_status or "转账"
|
||||
t = _extract_xml_tag_text(content_text, "title")
|
||||
d = _extract_xml_tag_text(content_text, "des")
|
||||
content_text = t or d or _infer_message_brief_by_local_type(local_type)
|
||||
|
||||
if not parsed_special:
|
||||
t = _extract_xml_tag_text(content_text, "title")
|
||||
d = _extract_xml_tag_text(content_text, "des")
|
||||
content_text = t or d or _infer_message_brief_by_local_type(local_type)
|
||||
|
||||
if not content_text:
|
||||
content_text = _infer_message_brief_by_local_type(local_type)
|
||||
@@ -664,6 +671,7 @@ def _append_full_messages_from_rows(
|
||||
"content": content_text,
|
||||
"title": title,
|
||||
"url": url,
|
||||
"recordItem": record_item,
|
||||
"imageMd5": image_md5,
|
||||
"imageFileId": image_file_id,
|
||||
"emojiMd5": emoji_md5,
|
||||
@@ -1080,6 +1088,19 @@ async def list_chat_sessions(
|
||||
else:
|
||||
last_message = _infer_last_message_brief(r["last_msg_type"], r["last_msg_sub_type"])
|
||||
|
||||
# 合并转发聊天记录:左侧会话列表统一显示为 [聊天记录]
|
||||
if preview_mode != "none" and not str(last_message or "").startswith("[草稿]"):
|
||||
try:
|
||||
last_msg_type = int(r["last_msg_type"] or 0)
|
||||
except Exception:
|
||||
last_msg_type = 0
|
||||
try:
|
||||
last_msg_sub_type = int(r["last_msg_sub_type"] or 0)
|
||||
except Exception:
|
||||
last_msg_sub_type = 0
|
||||
if last_msg_type == 81604378673 or (last_msg_type == 49 and last_msg_sub_type == 19):
|
||||
last_message = "[聊天记录]"
|
||||
|
||||
last_time = _format_session_time(r["sort_timestamp"] or r["last_timestamp"])
|
||||
|
||||
sessions.append(
|
||||
@@ -1214,6 +1235,7 @@ def _collect_chat_messages(
|
||||
content_text = raw_text
|
||||
title = ""
|
||||
url = ""
|
||||
record_item = ""
|
||||
image_md5 = ""
|
||||
emoji_md5 = ""
|
||||
emoji_url = ""
|
||||
@@ -1257,6 +1279,7 @@ def _collect_chat_messages(
|
||||
content_text = str(parsed.get("content") or "")
|
||||
title = str(parsed.get("title") or "")
|
||||
url = str(parsed.get("url") or "")
|
||||
record_item = str(parsed.get("recordItem") or "")
|
||||
quote_title = str(parsed.get("quoteTitle") or "")
|
||||
quote_content = str(parsed.get("quoteContent") or "")
|
||||
quote_username = str(parsed.get("quoteUsername") or "")
|
||||
@@ -1444,14 +1467,17 @@ def _collect_chat_messages(
|
||||
content_text = _infer_message_brief_by_local_type(local_type)
|
||||
else:
|
||||
if content_text.startswith("<") or content_text.startswith('"<'):
|
||||
parsed_special = False
|
||||
if "<appmsg" in content_text.lower():
|
||||
parsed = _parse_app_message(content_text)
|
||||
rt = str(parsed.get("renderType") or "")
|
||||
if rt and rt != "text":
|
||||
parsed_special = True
|
||||
render_type = rt
|
||||
content_text = str(parsed.get("content") or content_text)
|
||||
title = str(parsed.get("title") or title)
|
||||
url = str(parsed.get("url") or url)
|
||||
record_item = str(parsed.get("recordItem") or record_item)
|
||||
quote_title = str(parsed.get("quoteTitle") or quote_title)
|
||||
quote_content = str(parsed.get("quoteContent") or quote_content)
|
||||
amount = str(parsed.get("amount") or amount)
|
||||
@@ -1477,9 +1503,11 @@ def _collect_chat_messages(
|
||||
)
|
||||
if not content_text:
|
||||
content_text = transfer_status or "转账"
|
||||
t = _extract_xml_tag_text(content_text, "title")
|
||||
d = _extract_xml_tag_text(content_text, "des")
|
||||
content_text = t or d or _infer_message_brief_by_local_type(local_type)
|
||||
|
||||
if not parsed_special:
|
||||
t = _extract_xml_tag_text(content_text, "title")
|
||||
d = _extract_xml_tag_text(content_text, "des")
|
||||
content_text = t or d or _infer_message_brief_by_local_type(local_type)
|
||||
|
||||
if not content_text:
|
||||
content_text = _infer_message_brief_by_local_type(local_type)
|
||||
@@ -1509,6 +1537,7 @@ def _collect_chat_messages(
|
||||
"content": content_text,
|
||||
"title": title,
|
||||
"url": url,
|
||||
"recordItem": record_item,
|
||||
"imageMd5": image_md5,
|
||||
"imageFileId": image_file_id,
|
||||
"emojiMd5": emoji_md5,
|
||||
@@ -1746,6 +1775,7 @@ async def list_chat_messages(
|
||||
content_text = raw_text
|
||||
title = ""
|
||||
url = ""
|
||||
record_item = ""
|
||||
image_md5 = ""
|
||||
emoji_md5 = ""
|
||||
emoji_url = ""
|
||||
@@ -1789,6 +1819,7 @@ async def list_chat_messages(
|
||||
content_text = str(parsed.get("content") or "")
|
||||
title = str(parsed.get("title") or "")
|
||||
url = str(parsed.get("url") or "")
|
||||
record_item = str(parsed.get("recordItem") or "")
|
||||
quote_title = str(parsed.get("quoteTitle") or "")
|
||||
quote_content = str(parsed.get("quoteContent") or "")
|
||||
quote_username = str(parsed.get("quoteUsername") or "")
|
||||
@@ -1976,14 +2007,17 @@ async def list_chat_messages(
|
||||
content_text = _infer_message_brief_by_local_type(local_type)
|
||||
else:
|
||||
if content_text.startswith("<") or content_text.startswith('"<'):
|
||||
parsed_special = False
|
||||
if "<appmsg" in content_text.lower():
|
||||
parsed = _parse_app_message(content_text)
|
||||
rt = str(parsed.get("renderType") or "")
|
||||
if rt and rt != "text":
|
||||
parsed_special = True
|
||||
render_type = rt
|
||||
content_text = str(parsed.get("content") or content_text)
|
||||
title = str(parsed.get("title") or title)
|
||||
url = str(parsed.get("url") or url)
|
||||
record_item = str(parsed.get("recordItem") or record_item)
|
||||
quote_title = str(parsed.get("quoteTitle") or quote_title)
|
||||
quote_content = str(parsed.get("quoteContent") or quote_content)
|
||||
amount = str(parsed.get("amount") or amount)
|
||||
@@ -2009,9 +2043,11 @@ async def list_chat_messages(
|
||||
)
|
||||
if not content_text:
|
||||
content_text = transfer_status or "转账"
|
||||
t = _extract_xml_tag_text(content_text, "title")
|
||||
d = _extract_xml_tag_text(content_text, "des")
|
||||
content_text = t or d or _infer_message_brief_by_local_type(local_type)
|
||||
|
||||
if not parsed_special:
|
||||
t = _extract_xml_tag_text(content_text, "title")
|
||||
d = _extract_xml_tag_text(content_text, "des")
|
||||
content_text = t or d or _infer_message_brief_by_local_type(local_type)
|
||||
|
||||
if not content_text:
|
||||
content_text = _infer_message_brief_by_local_type(local_type)
|
||||
@@ -2034,6 +2070,7 @@ async def list_chat_messages(
|
||||
"content": content_text,
|
||||
"title": title,
|
||||
"url": url,
|
||||
"recordItem": record_item,
|
||||
"imageMd5": image_md5,
|
||||
"imageFileId": image_file_id,
|
||||
"emojiMd5": emoji_md5,
|
||||
|
||||
Reference in New Issue
Block a user