mirror of
https://github.com/LifeArchiveProject/WeChatDataAnalysis.git
synced 2026-02-02 22:10: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>
|
||||||
<div v-else-if="message.renderType === 'video'" class="max-w-sm">
|
<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')">
|
<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"
|
<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'">
|
:class="message.isSent ? 'bg-[#95EC69] text-black bubble-tail-r' : 'bg-white text-gray-800 bubble-tail-l'">
|
||||||
{{ message.content }}
|
{{ message.content }}
|
||||||
@@ -442,6 +442,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</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'"
|
<div v-else-if="message.renderType === 'transfer'"
|
||||||
class="wechat-transfer-card msg-radius"
|
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']">
|
: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>
|
</button>
|
||||||
</div>
|
</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
|
<div
|
||||||
v-if="contextMenu.visible"
|
v-if="contextMenu.visible"
|
||||||
class="fixed z-50 bg-white border border-gray-200 rounded-md shadow-lg text-sm"
|
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 || '',
|
voipType: msg.voipType || '',
|
||||||
title: msg.title || '',
|
title: msg.title || '',
|
||||||
url: msg.url || '',
|
url: msg.url || '',
|
||||||
|
recordItem: msg.recordItem || '',
|
||||||
imageMd5: msg.imageMd5 || '',
|
imageMd5: msg.imageMd5 || '',
|
||||||
imageFileId: msg.imageFileId || '',
|
imageFileId: msg.imageFileId || '',
|
||||||
emojiMd5: msg.emojiMd5 || '',
|
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) => {
|
const onGlobalClick = (e) => {
|
||||||
if (contextMenu.value.visible) closeContextMenu()
|
if (contextMenu.value.visible) closeContextMenu()
|
||||||
if (messageSearchSenderDropdownOpen.value) {
|
if (messageSearchSenderDropdownOpen.value) {
|
||||||
@@ -3504,6 +4080,7 @@ const onGlobalKeyDown = (e) => {
|
|||||||
if (key === 'Escape') {
|
if (key === 'Escape') {
|
||||||
if (contextMenu.value.visible) closeContextMenu()
|
if (contextMenu.value.visible) closeContextMenu()
|
||||||
if (previewImageUrl.value) closeImagePreview()
|
if (previewImageUrl.value) closeImagePreview()
|
||||||
|
if (chatHistoryModalVisible.value) closeChatHistoryModal()
|
||||||
if (messageSearchSenderDropdownOpen.value) closeMessageSearchSenderDropdown()
|
if (messageSearchSenderDropdownOpen.value) closeMessageSearchSenderDropdown()
|
||||||
if (messageSearchOpen.value) closeMessageSearch()
|
if (messageSearchOpen.value) closeMessageSearch()
|
||||||
if (searchContext.value?.active) exitSearchContext()
|
if (searchContext.value?.active) exitSearchContext()
|
||||||
@@ -4105,6 +4682,65 @@ const LinkCard = defineComponent({
|
|||||||
right: -4px;
|
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 {
|
.wechat-transfer-card {
|
||||||
width: 210px;
|
width: 210px;
|
||||||
|
|||||||
@@ -890,6 +890,7 @@ def _parse_message_for_export(
|
|||||||
content_text = raw_text
|
content_text = raw_text
|
||||||
title = ""
|
title = ""
|
||||||
url = ""
|
url = ""
|
||||||
|
record_item = ""
|
||||||
image_md5 = ""
|
image_md5 = ""
|
||||||
image_file_id = ""
|
image_file_id = ""
|
||||||
emoji_md5 = ""
|
emoji_md5 = ""
|
||||||
@@ -929,6 +930,7 @@ def _parse_message_for_export(
|
|||||||
content_text = str(parsed.get("content") or "")
|
content_text = str(parsed.get("content") or "")
|
||||||
title = str(parsed.get("title") or "")
|
title = str(parsed.get("title") or "")
|
||||||
url = str(parsed.get("url") or "")
|
url = str(parsed.get("url") or "")
|
||||||
|
record_item = str(parsed.get("recordItem") or "")
|
||||||
quote_title = str(parsed.get("quoteTitle") or "")
|
quote_title = str(parsed.get("quoteTitle") or "")
|
||||||
quote_content = str(parsed.get("quoteContent") or "")
|
quote_content = str(parsed.get("quoteContent") or "")
|
||||||
amount = str(parsed.get("amount") 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)
|
content_text = _infer_message_brief_by_local_type(local_type)
|
||||||
else:
|
else:
|
||||||
if content_text.startswith("<") or content_text.startswith('"<'):
|
if content_text.startswith("<") or content_text.startswith('"<'):
|
||||||
|
parsed_special = False
|
||||||
if "<appmsg" in content_text.lower():
|
if "<appmsg" in content_text.lower():
|
||||||
parsed = _parse_app_message(content_text)
|
parsed = _parse_app_message(content_text)
|
||||||
rt = str(parsed.get("renderType") or "")
|
rt = str(parsed.get("renderType") or "")
|
||||||
if rt and rt != "text":
|
if rt and rt != "text":
|
||||||
|
parsed_special = True
|
||||||
render_type = rt
|
render_type = rt
|
||||||
content_text = str(parsed.get("content") or content_text)
|
content_text = str(parsed.get("content") or content_text)
|
||||||
title = str(parsed.get("title") or title)
|
title = str(parsed.get("title") or title)
|
||||||
url = str(parsed.get("url") or url)
|
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_title = str(parsed.get("quoteTitle") or quote_title)
|
||||||
quote_content = str(parsed.get("quoteContent") or quote_content)
|
quote_content = str(parsed.get("quoteContent") or quote_content)
|
||||||
amount = str(parsed.get("amount") or amount)
|
amount = str(parsed.get("amount") or amount)
|
||||||
@@ -1121,9 +1126,11 @@ def _parse_message_for_export(
|
|||||||
)
|
)
|
||||||
if not content_text:
|
if not content_text:
|
||||||
content_text = transfer_status or "转账"
|
content_text = transfer_status or "转账"
|
||||||
t = _extract_xml_tag_text(content_text, "title")
|
|
||||||
d = _extract_xml_tag_text(content_text, "des")
|
if not parsed_special:
|
||||||
content_text = t or d or _infer_message_brief_by_local_type(local_type)
|
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:
|
if not content_text:
|
||||||
content_text = _infer_message_brief_by_local_type(local_type)
|
content_text = _infer_message_brief_by_local_type(local_type)
|
||||||
@@ -1151,6 +1158,7 @@ def _parse_message_for_export(
|
|||||||
"content": content_text,
|
"content": content_text,
|
||||||
"title": title,
|
"title": title,
|
||||||
"url": url,
|
"url": url,
|
||||||
|
"recordItem": record_item,
|
||||||
"thumbUrl": thumb_url,
|
"thumbUrl": thumb_url,
|
||||||
"imageMd5": image_md5,
|
"imageMd5": image_md5,
|
||||||
"imageFileId": image_file_id,
|
"imageFileId": image_file_id,
|
||||||
|
|||||||
@@ -171,7 +171,7 @@ def _infer_last_message_brief(msg_type: Optional[int], sub_type: Optional[int])
|
|||||||
if s == 2003:
|
if s == 2003:
|
||||||
return "[Red Packet]"
|
return "[Red Packet]"
|
||||||
if s == 19:
|
if s == 19:
|
||||||
return "[Chat History]"
|
return "[聊天记录]"
|
||||||
return "[App Message]"
|
return "[App Message]"
|
||||||
if t == 10000:
|
if t == 10000:
|
||||||
return "[System]"
|
return "[System]"
|
||||||
@@ -209,7 +209,7 @@ def _infer_message_brief_by_local_type(local_type: Optional[int]) -> str:
|
|||||||
if t == 8594229559345:
|
if t == 8594229559345:
|
||||||
return "[Red Packet]"
|
return "[Red Packet]"
|
||||||
if t == 81604378673:
|
if t == 81604378673:
|
||||||
return "[Chat History]"
|
return "[聊天记录]"
|
||||||
if t == 266287972401:
|
if t == 266287972401:
|
||||||
return "[Pat]"
|
return "[Pat]"
|
||||||
if t == 8589934592049:
|
if t == 8589934592049:
|
||||||
@@ -698,6 +698,22 @@ def _parse_app_message(text: str) -> dict[str, Any]:
|
|||||||
|
|
||||||
lower = text.lower()
|
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:
|
if app_type in (5, 68) and url:
|
||||||
thumb_url = _extract_xml_tag_text(text, "thumburl")
|
thumb_url = _extract_xml_tag_text(text, "thumburl")
|
||||||
return {
|
return {
|
||||||
@@ -724,7 +740,21 @@ def _parse_app_message(text: str) -> dict[str, Any]:
|
|||||||
"fileMd5": file_md5 or "",
|
"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)
|
refer_block = _extract_refermsg_block(text)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -944,6 +974,8 @@ def _build_latest_message_preview(
|
|||||||
rt = str(parsed.get("renderType") or "")
|
rt = str(parsed.get("renderType") or "")
|
||||||
content_text = str(parsed.get("content") or "")
|
content_text = str(parsed.get("content") or "")
|
||||||
title_text = str(parsed.get("title") or "").strip()
|
title_text = str(parsed.get("title") or "").strip()
|
||||||
|
if rt == "chatHistory":
|
||||||
|
content_text = "[聊天记录]"
|
||||||
if rt == "file" and title_text:
|
if rt == "file" and title_text:
|
||||||
content_text = title_text
|
content_text = title_text
|
||||||
if (not content_text) and rt == "transfer":
|
if (not content_text) and rt == "transfer":
|
||||||
|
|||||||
@@ -373,6 +373,7 @@ def _append_full_messages_from_rows(
|
|||||||
content_text = raw_text
|
content_text = raw_text
|
||||||
title = ""
|
title = ""
|
||||||
url = ""
|
url = ""
|
||||||
|
record_item = ""
|
||||||
image_md5 = ""
|
image_md5 = ""
|
||||||
emoji_md5 = ""
|
emoji_md5 = ""
|
||||||
emoji_url = ""
|
emoji_url = ""
|
||||||
@@ -414,6 +415,7 @@ def _append_full_messages_from_rows(
|
|||||||
content_text = str(parsed.get("content") or "")
|
content_text = str(parsed.get("content") or "")
|
||||||
title = str(parsed.get("title") or "")
|
title = str(parsed.get("title") or "")
|
||||||
url = str(parsed.get("url") or "")
|
url = str(parsed.get("url") or "")
|
||||||
|
record_item = str(parsed.get("recordItem") or "")
|
||||||
quote_title = str(parsed.get("quoteTitle") or "")
|
quote_title = str(parsed.get("quoteTitle") or "")
|
||||||
quote_content = str(parsed.get("quoteContent") or "")
|
quote_content = str(parsed.get("quoteContent") or "")
|
||||||
quote_username = str(parsed.get("quoteUsername") 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)
|
content_text = _infer_message_brief_by_local_type(local_type)
|
||||||
else:
|
else:
|
||||||
if content_text.startswith("<") or content_text.startswith('"<'):
|
if content_text.startswith("<") or content_text.startswith('"<'):
|
||||||
|
parsed_special = False
|
||||||
if "<appmsg" in content_text.lower():
|
if "<appmsg" in content_text.lower():
|
||||||
parsed = _parse_app_message(content_text)
|
parsed = _parse_app_message(content_text)
|
||||||
rt = str(parsed.get("renderType") or "")
|
rt = str(parsed.get("renderType") or "")
|
||||||
if rt and rt != "text":
|
if rt and rt != "text":
|
||||||
|
parsed_special = True
|
||||||
render_type = rt
|
render_type = rt
|
||||||
content_text = str(parsed.get("content") or content_text)
|
content_text = str(parsed.get("content") or content_text)
|
||||||
title = str(parsed.get("title") or title)
|
title = str(parsed.get("title") or title)
|
||||||
url = str(parsed.get("url") or url)
|
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_title = str(parsed.get("quoteTitle") or quote_title)
|
||||||
quote_content = str(parsed.get("quoteContent") or quote_content)
|
quote_content = str(parsed.get("quoteContent") or quote_content)
|
||||||
amount = str(parsed.get("amount") or amount)
|
amount = str(parsed.get("amount") or amount)
|
||||||
@@ -639,9 +644,11 @@ def _append_full_messages_from_rows(
|
|||||||
)
|
)
|
||||||
if not content_text:
|
if not content_text:
|
||||||
content_text = transfer_status or "转账"
|
content_text = transfer_status or "转账"
|
||||||
t = _extract_xml_tag_text(content_text, "title")
|
|
||||||
d = _extract_xml_tag_text(content_text, "des")
|
if not parsed_special:
|
||||||
content_text = t or d or _infer_message_brief_by_local_type(local_type)
|
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:
|
if not content_text:
|
||||||
content_text = _infer_message_brief_by_local_type(local_type)
|
content_text = _infer_message_brief_by_local_type(local_type)
|
||||||
@@ -664,6 +671,7 @@ def _append_full_messages_from_rows(
|
|||||||
"content": content_text,
|
"content": content_text,
|
||||||
"title": title,
|
"title": title,
|
||||||
"url": url,
|
"url": url,
|
||||||
|
"recordItem": record_item,
|
||||||
"imageMd5": image_md5,
|
"imageMd5": image_md5,
|
||||||
"imageFileId": image_file_id,
|
"imageFileId": image_file_id,
|
||||||
"emojiMd5": emoji_md5,
|
"emojiMd5": emoji_md5,
|
||||||
@@ -1080,6 +1088,19 @@ async def list_chat_sessions(
|
|||||||
else:
|
else:
|
||||||
last_message = _infer_last_message_brief(r["last_msg_type"], r["last_msg_sub_type"])
|
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"])
|
last_time = _format_session_time(r["sort_timestamp"] or r["last_timestamp"])
|
||||||
|
|
||||||
sessions.append(
|
sessions.append(
|
||||||
@@ -1214,6 +1235,7 @@ def _collect_chat_messages(
|
|||||||
content_text = raw_text
|
content_text = raw_text
|
||||||
title = ""
|
title = ""
|
||||||
url = ""
|
url = ""
|
||||||
|
record_item = ""
|
||||||
image_md5 = ""
|
image_md5 = ""
|
||||||
emoji_md5 = ""
|
emoji_md5 = ""
|
||||||
emoji_url = ""
|
emoji_url = ""
|
||||||
@@ -1257,6 +1279,7 @@ def _collect_chat_messages(
|
|||||||
content_text = str(parsed.get("content") or "")
|
content_text = str(parsed.get("content") or "")
|
||||||
title = str(parsed.get("title") or "")
|
title = str(parsed.get("title") or "")
|
||||||
url = str(parsed.get("url") or "")
|
url = str(parsed.get("url") or "")
|
||||||
|
record_item = str(parsed.get("recordItem") or "")
|
||||||
quote_title = str(parsed.get("quoteTitle") or "")
|
quote_title = str(parsed.get("quoteTitle") or "")
|
||||||
quote_content = str(parsed.get("quoteContent") or "")
|
quote_content = str(parsed.get("quoteContent") or "")
|
||||||
quote_username = str(parsed.get("quoteUsername") 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)
|
content_text = _infer_message_brief_by_local_type(local_type)
|
||||||
else:
|
else:
|
||||||
if content_text.startswith("<") or content_text.startswith('"<'):
|
if content_text.startswith("<") or content_text.startswith('"<'):
|
||||||
|
parsed_special = False
|
||||||
if "<appmsg" in content_text.lower():
|
if "<appmsg" in content_text.lower():
|
||||||
parsed = _parse_app_message(content_text)
|
parsed = _parse_app_message(content_text)
|
||||||
rt = str(parsed.get("renderType") or "")
|
rt = str(parsed.get("renderType") or "")
|
||||||
if rt and rt != "text":
|
if rt and rt != "text":
|
||||||
|
parsed_special = True
|
||||||
render_type = rt
|
render_type = rt
|
||||||
content_text = str(parsed.get("content") or content_text)
|
content_text = str(parsed.get("content") or content_text)
|
||||||
title = str(parsed.get("title") or title)
|
title = str(parsed.get("title") or title)
|
||||||
url = str(parsed.get("url") or url)
|
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_title = str(parsed.get("quoteTitle") or quote_title)
|
||||||
quote_content = str(parsed.get("quoteContent") or quote_content)
|
quote_content = str(parsed.get("quoteContent") or quote_content)
|
||||||
amount = str(parsed.get("amount") or amount)
|
amount = str(parsed.get("amount") or amount)
|
||||||
@@ -1477,9 +1503,11 @@ def _collect_chat_messages(
|
|||||||
)
|
)
|
||||||
if not content_text:
|
if not content_text:
|
||||||
content_text = transfer_status or "转账"
|
content_text = transfer_status or "转账"
|
||||||
t = _extract_xml_tag_text(content_text, "title")
|
|
||||||
d = _extract_xml_tag_text(content_text, "des")
|
if not parsed_special:
|
||||||
content_text = t or d or _infer_message_brief_by_local_type(local_type)
|
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:
|
if not content_text:
|
||||||
content_text = _infer_message_brief_by_local_type(local_type)
|
content_text = _infer_message_brief_by_local_type(local_type)
|
||||||
@@ -1509,6 +1537,7 @@ def _collect_chat_messages(
|
|||||||
"content": content_text,
|
"content": content_text,
|
||||||
"title": title,
|
"title": title,
|
||||||
"url": url,
|
"url": url,
|
||||||
|
"recordItem": record_item,
|
||||||
"imageMd5": image_md5,
|
"imageMd5": image_md5,
|
||||||
"imageFileId": image_file_id,
|
"imageFileId": image_file_id,
|
||||||
"emojiMd5": emoji_md5,
|
"emojiMd5": emoji_md5,
|
||||||
@@ -1746,6 +1775,7 @@ async def list_chat_messages(
|
|||||||
content_text = raw_text
|
content_text = raw_text
|
||||||
title = ""
|
title = ""
|
||||||
url = ""
|
url = ""
|
||||||
|
record_item = ""
|
||||||
image_md5 = ""
|
image_md5 = ""
|
||||||
emoji_md5 = ""
|
emoji_md5 = ""
|
||||||
emoji_url = ""
|
emoji_url = ""
|
||||||
@@ -1789,6 +1819,7 @@ async def list_chat_messages(
|
|||||||
content_text = str(parsed.get("content") or "")
|
content_text = str(parsed.get("content") or "")
|
||||||
title = str(parsed.get("title") or "")
|
title = str(parsed.get("title") or "")
|
||||||
url = str(parsed.get("url") or "")
|
url = str(parsed.get("url") or "")
|
||||||
|
record_item = str(parsed.get("recordItem") or "")
|
||||||
quote_title = str(parsed.get("quoteTitle") or "")
|
quote_title = str(parsed.get("quoteTitle") or "")
|
||||||
quote_content = str(parsed.get("quoteContent") or "")
|
quote_content = str(parsed.get("quoteContent") or "")
|
||||||
quote_username = str(parsed.get("quoteUsername") 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)
|
content_text = _infer_message_brief_by_local_type(local_type)
|
||||||
else:
|
else:
|
||||||
if content_text.startswith("<") or content_text.startswith('"<'):
|
if content_text.startswith("<") or content_text.startswith('"<'):
|
||||||
|
parsed_special = False
|
||||||
if "<appmsg" in content_text.lower():
|
if "<appmsg" in content_text.lower():
|
||||||
parsed = _parse_app_message(content_text)
|
parsed = _parse_app_message(content_text)
|
||||||
rt = str(parsed.get("renderType") or "")
|
rt = str(parsed.get("renderType") or "")
|
||||||
if rt and rt != "text":
|
if rt and rt != "text":
|
||||||
|
parsed_special = True
|
||||||
render_type = rt
|
render_type = rt
|
||||||
content_text = str(parsed.get("content") or content_text)
|
content_text = str(parsed.get("content") or content_text)
|
||||||
title = str(parsed.get("title") or title)
|
title = str(parsed.get("title") or title)
|
||||||
url = str(parsed.get("url") or url)
|
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_title = str(parsed.get("quoteTitle") or quote_title)
|
||||||
quote_content = str(parsed.get("quoteContent") or quote_content)
|
quote_content = str(parsed.get("quoteContent") or quote_content)
|
||||||
amount = str(parsed.get("amount") or amount)
|
amount = str(parsed.get("amount") or amount)
|
||||||
@@ -2009,9 +2043,11 @@ async def list_chat_messages(
|
|||||||
)
|
)
|
||||||
if not content_text:
|
if not content_text:
|
||||||
content_text = transfer_status or "转账"
|
content_text = transfer_status or "转账"
|
||||||
t = _extract_xml_tag_text(content_text, "title")
|
|
||||||
d = _extract_xml_tag_text(content_text, "des")
|
if not parsed_special:
|
||||||
content_text = t or d or _infer_message_brief_by_local_type(local_type)
|
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:
|
if not content_text:
|
||||||
content_text = _infer_message_brief_by_local_type(local_type)
|
content_text = _infer_message_brief_by_local_type(local_type)
|
||||||
@@ -2034,6 +2070,7 @@ async def list_chat_messages(
|
|||||||
"content": content_text,
|
"content": content_text,
|
||||||
"title": title,
|
"title": title,
|
||||||
"url": url,
|
"url": url,
|
||||||
|
"recordItem": record_item,
|
||||||
"imageMd5": image_md5,
|
"imageMd5": image_md5,
|
||||||
"imageFileId": image_file_id,
|
"imageFileId": image_file_id,
|
||||||
"emojiMd5": emoji_md5,
|
"emojiMd5": emoji_md5,
|
||||||
|
|||||||
Reference in New Issue
Block a user