From e1243522b055bdcab92485badab95ab7396ba6e7 Mon Sep 17 00:00:00 2001 From: tisonhuang Date: Tue, 3 Mar 2026 18:31:11 +0800 Subject: [PATCH] feat(export): enrich arkme json for card/location/music --- electron/services/exportService.ts | 175 ++++++++++++++++++++++++++++- 1 file changed, 174 insertions(+), 1 deletion(-) diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts index bc49a5d..61f38a8 100644 --- a/electron/services/exportService.ts +++ b/electron/services/exportService.ts @@ -867,6 +867,7 @@ class ExportService { case 49: { const title = this.extractXmlValue(content, 'title') const type = this.extractXmlValue(content, 'type') + const songName = this.extractXmlValue(content, 'songname') // 转账消息特殊处理 if (type === '2000') { @@ -879,6 +880,7 @@ class ExportService { return transferPrefix } + if (type === '3') return songName ? `[音乐] ${songName}` : (title ? `[音乐] ${title}` : '[音乐]') if (type === '6') return title ? `[文件] ${title}` : '[文件]' if (type === '19') return title ? `[聊天记录] ${title}` : '[聊天记录]' if (type === '33' || type === '36') return title ? `[小程序] ${title}` : '[小程序]' @@ -920,6 +922,7 @@ class ExportService { } // 其他类型 + if (xmlType === '3') return title ? `[音乐] ${title}` : '[音乐]' if (xmlType === '6') return title ? `[文件] ${title}` : '[文件]' if (xmlType === '19') return title ? `[聊天记录] ${title}` : '[聊天记录]' if (xmlType === '33' || xmlType === '36') return title ? `[小程序] ${title}` : '[小程序]' @@ -1303,6 +1306,7 @@ class ExportService { if (xmlType) { switch (xmlType) { + case '3': return '音乐消息' case '87': return '群公告' case '2000': return '转账消息' case '5': return '链接消息' @@ -1513,6 +1517,11 @@ class ExportService { normalized.includes('') || + normalized.includes('') if (!looksLikeAppMsg && !isFinder) return null @@ -1521,7 +1530,7 @@ class ExportService { appMsgKind = 'finder' } else if (xmlType === '2001') { appMsgKind = 'red-packet' - } else if (xmlType === '3') { + } else if (isMusic) { appMsgKind = 'music' } else if (xmlType === '33' || xmlType === '36') { appMsgKind = 'miniapp' @@ -1545,6 +1554,46 @@ class ExportService { if (xmlType) meta.appMsgType = xmlType if (appMsgKind) meta.appMsgKind = appMsgKind + if (isMusic) { + const musicTitle = + this.extractXmlValue(normalized, 'songname') || + this.extractXmlValue(normalized, 'title') + const musicUrl = + this.extractXmlValue(normalized, 'musicurl') || + this.extractXmlValue(normalized, 'playurl') || + this.extractXmlValue(normalized, 'songalbumurl') + const musicDataUrl = + this.extractXmlValue(normalized, 'dataurl') || + this.extractXmlValue(normalized, 'lowurl') + const musicAlbumUrl = this.extractXmlValue(normalized, 'songalbumurl') + const musicCoverUrl = + this.extractXmlValue(normalized, 'thumburl') || + this.extractXmlValue(normalized, 'cdnthumburl') || + this.extractXmlValue(normalized, 'coverurl') || + this.extractXmlValue(normalized, 'cover') + const musicSinger = + this.extractXmlValue(normalized, 'singername') || + this.extractXmlValue(normalized, 'artist') || + this.extractXmlValue(normalized, 'albumartist') + const musicAppName = this.extractXmlValue(normalized, 'appname') + const musicSourceName = this.extractXmlValue(normalized, 'sourcename') + const durationRaw = + this.extractXmlValue(normalized, 'playlength') || + this.extractXmlValue(normalized, 'play_length') || + this.extractXmlValue(normalized, 'duration') + const musicDuration = durationRaw ? this.parseDurationSeconds(durationRaw) : null + + if (musicTitle) meta.musicTitle = musicTitle + if (musicUrl) meta.musicUrl = musicUrl + if (musicDataUrl) meta.musicDataUrl = musicDataUrl + if (musicAlbumUrl) meta.musicAlbumUrl = musicAlbumUrl + if (musicCoverUrl) meta.musicCoverUrl = musicCoverUrl + if (musicSinger) meta.musicSinger = musicSinger + if (musicAppName) meta.musicAppName = musicAppName + if (musicSourceName) meta.musicSourceName = musicSourceName + if (musicDuration != null) meta.musicDuration = musicDuration + } + if (!isFinder) { return Object.keys(meta).length > 0 ? meta : null } @@ -1590,6 +1639,49 @@ class ExportService { return Object.keys(meta).length > 0 ? meta : null } + private extractArkmeContactCardMeta(content: string, localType: number): Record | null { + if (!content || localType !== 42) return null + + const normalized = this.normalizeAppMessageContent(content) + const readAttr = (attrName: string): string => + this.extractXmlAttribute(normalized, 'msg', attrName) || this.extractXmlValue(normalized, attrName) + + const contactCardWxid = + readAttr('username') || + readAttr('encryptusername') || + readAttr('encrypt_user_name') + const contactCardNickname = readAttr('nickname') + const contactCardAlias = readAttr('alias') + const contactCardRemark = readAttr('remark') + const contactCardProvince = readAttr('province') + const contactCardCity = readAttr('city') + const contactCardSignature = readAttr('sign') || readAttr('signature') + const contactCardAvatar = + readAttr('smallheadimgurl') || + readAttr('bigheadimgurl') || + readAttr('headimgurl') || + readAttr('avatar') + const sexRaw = readAttr('sex') + const contactCardGender = sexRaw ? parseInt(sexRaw, 10) : NaN + + const meta: Record = { + cardKind: 'contact-card' + } + if (contactCardWxid) meta.contactCardWxid = contactCardWxid + if (contactCardNickname) meta.contactCardNickname = contactCardNickname + if (contactCardAlias) meta.contactCardAlias = contactCardAlias + if (contactCardRemark) meta.contactCardRemark = contactCardRemark + if (contactCardProvince) meta.contactCardProvince = contactCardProvince + if (contactCardCity) meta.contactCardCity = contactCardCity + if (contactCardSignature) meta.contactCardSignature = contactCardSignature + if (contactCardAvatar) meta.contactCardAvatar = contactCardAvatar + if (Number.isFinite(contactCardGender) && contactCardGender >= 0) { + meta.contactCardGender = contactCardGender + } + + return Object.keys(meta).length > 0 ? meta : null + } + private getInlineEmojiDataUrl(name: string): string | null { if (!name) return null const cached = this.inlineEmojiCache.get(name) @@ -2050,6 +2142,46 @@ class ExportService { return tagMatch?.[1]?.toLowerCase() } + private extractLocationMeta(content: string, localType: number): { + locationLat?: number + locationLng?: number + locationPoiname?: string + locationLabel?: string + } | null { + if (!content || localType !== 48) return null + + const normalized = this.normalizeAppMessageContent(content) + const rawLat = this.extractXmlAttribute(normalized, 'location', 'x') || this.extractXmlAttribute(normalized, 'location', 'latitude') + const rawLng = this.extractXmlAttribute(normalized, 'location', 'y') || this.extractXmlAttribute(normalized, 'location', 'longitude') + const locationPoiname = + this.extractXmlAttribute(normalized, 'location', 'poiname') || + this.extractXmlValue(normalized, 'poiname') || + this.extractXmlValue(normalized, 'poiName') + const locationLabel = + this.extractXmlAttribute(normalized, 'location', 'label') || + this.extractXmlValue(normalized, 'label') + + const meta: { + locationLat?: number + locationLng?: number + locationPoiname?: string + locationLabel?: string + } = {} + + if (rawLat) { + const parsed = parseFloat(rawLat) + if (Number.isFinite(parsed)) meta.locationLat = parsed + } + if (rawLng) { + const parsed = parseFloat(rawLng) + if (Number.isFinite(parsed)) meta.locationLng = parsed + } + if (locationPoiname) meta.locationPoiname = locationPoiname + if (locationLabel) meta.locationLabel = locationLabel + + return Object.keys(meta).length > 0 ? meta : null + } + /** * 从 data URL 获取扩展名 */ @@ -2256,8 +2388,22 @@ class ExportService { let emojiCdnUrl: string | undefined let emojiMd5: string | undefined let videoMd5: string | undefined + let locationLat: number | undefined + let locationLng: number | undefined + let locationPoiname: string | undefined + let locationLabel: string | undefined let chatRecordList: any[] | undefined + if (localType === 48 && content) { + const locationMeta = this.extractLocationMeta(content, localType) + if (locationMeta) { + locationLat = locationMeta.locationLat + locationLng = locationMeta.locationLng + locationPoiname = locationMeta.locationPoiname + locationLabel = locationMeta.locationLabel + } + } + if (collectMode === 'full' || collectMode === 'media-fast') { // 优先复用游标返回的字段,缺失时再回退到 XML 解析。 imageMd5 = String(row.image_md5 || row.imageMd5 || '').trim() || undefined @@ -2298,6 +2444,10 @@ class ExportService { emojiCdnUrl, emojiMd5, videoMd5, + locationLat, + locationLng, + locationPoiname, + locationLabel, chatRecordList }) @@ -3628,6 +3778,10 @@ class ExportService { if (appMsgMeta) { Object.assign(msgObj, appMsgMeta) } + const contactCardMeta = this.extractArkmeContactCardMeta(msg.content, msg.localType) + if (contactCardMeta) { + Object.assign(msgObj, contactCardMeta) + } } if (content && this.isTransferExportContent(content) && msg.content) { @@ -3819,6 +3973,25 @@ class ExportService { if (message.finderDuration != null) compactMessage.finderDuration = message.finderDuration if (message.finderObjectId) compactMessage.finderObjectId = message.finderObjectId if (message.finderUrl) compactMessage.finderUrl = message.finderUrl + if (message.musicTitle) compactMessage.musicTitle = message.musicTitle + if (message.musicUrl) compactMessage.musicUrl = message.musicUrl + if (message.musicDataUrl) compactMessage.musicDataUrl = message.musicDataUrl + if (message.musicAlbumUrl) compactMessage.musicAlbumUrl = message.musicAlbumUrl + if (message.musicCoverUrl) compactMessage.musicCoverUrl = message.musicCoverUrl + if (message.musicSinger) compactMessage.musicSinger = message.musicSinger + if (message.musicAppName) compactMessage.musicAppName = message.musicAppName + if (message.musicSourceName) compactMessage.musicSourceName = message.musicSourceName + if (message.musicDuration != null) compactMessage.musicDuration = message.musicDuration + if (message.cardKind) compactMessage.cardKind = message.cardKind + if (message.contactCardWxid) compactMessage.contactCardWxid = message.contactCardWxid + if (message.contactCardNickname) compactMessage.contactCardNickname = message.contactCardNickname + if (message.contactCardAlias) compactMessage.contactCardAlias = message.contactCardAlias + if (message.contactCardRemark) compactMessage.contactCardRemark = message.contactCardRemark + if (message.contactCardGender != null) compactMessage.contactCardGender = message.contactCardGender + if (message.contactCardProvince) compactMessage.contactCardProvince = message.contactCardProvince + if (message.contactCardCity) compactMessage.contactCardCity = message.contactCardCity + if (message.contactCardSignature) compactMessage.contactCardSignature = message.contactCardSignature + if (message.contactCardAvatar) compactMessage.contactCardAvatar = message.contactCardAvatar return compactMessage })