From d37f53e120ce05e010a5777a52ea1d8c1a656dc5 Mon Sep 17 00:00:00 2001 From: QingXiao <143726276+5xiao0qing5@users.noreply.github.com> Date: Fri, 23 Jan 2026 23:37:16 +0800 Subject: [PATCH 1/3] Adjust txt/excel export message formatting --- electron/services/exportService.ts | 109 +++++++++++++++++------------ 1 file changed, 66 insertions(+), 43 deletions(-) diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts index ca6c395..16c52ab 100644 --- a/electron/services/exportService.ts +++ b/electron/services/exportService.ts @@ -305,6 +305,57 @@ class ExportService { } } + private formatPlainExportContent( + content: string, + localType: number, + options: { exportVoiceAsText?: boolean }, + voiceTranscript?: string + ): string { + const safeContent = content || '' + + if (localType === 3) return '[图片]' + if (localType === 1) return this.stripSenderPrefix(safeContent) + if (localType === 34) { + if (options.exportVoiceAsText) { + return voiceTranscript || '[语音消息 - 转文字失败]' + } + return '[其他消息]' + } + if (localType === 10000 || localType === 266287972401) { + return this.cleanSystemMessage(safeContent) + } + + const normalized = this.normalizeAppMessageContent(safeContent) + const isAppMessage = normalized.includes('') + if (localType === 49 || isAppMessage) { + const typeMatch = /(\d+)<\/type>/i.exec(normalized) + const subType = typeMatch ? parseInt(typeMatch[1], 10) : 0 + const title = this.extractXmlValue(normalized, 'title') || this.extractXmlValue(normalized, 'appname') + if (subType === 6) { + const fileName = this.extractXmlValue(normalized, 'filename') || title || '文件' + return `[文件]${fileName}` + } + if (subType === 19 || normalized.includes(' [col.id, col.label])) const lines: string[] = [] - lines.push(columnOrder.map((id) => columnLabelMap.get(id) || id).join('\t')) for (let i = 0; i < sortedMessages.length; i++) { const msg = sortedMessages[i] - const mediaKey = `${msg.localType}_${msg.localId}` - const mediaItem = mediaCache.get(mediaKey) || null - - let contentValue: string - if (mediaItem) { - contentValue = mediaItem.relativePath - } else if (msg.localType === 34 && options.exportVoiceAsText) { - contentValue = voiceTranscriptMap.get(msg.localId) || '[语音消息 - 转文字失败]' - } else { - contentValue = this.parseMessageContent(msg.content, msg.localType) || '' - } + const contentValue = this.formatPlainExportContent( + msg.content, + msg.localType, + options, + voiceTranscriptMap.get(msg.localId) + ) let senderRole: string let senderWxid: string @@ -2242,21 +2277,9 @@ class ExportService { } } - const values: Record = { - index: String(i + 1), - time: this.formatTimestamp(msg.createTime), - senderRole, - senderNickname, - senderWxid, - senderRemark, - messageType: this.getMessageTypeName(msg.localType), - content: contentValue - } - - const line = columnOrder - .map((id) => this.sanitizeTxtValue(values[id] ?? '')) - .join('\t') - lines.push(line) + lines.push(`${this.formatTimestamp(msg.createTime)} '${senderRole}'`) + lines.push(contentValue) + lines.push('') if ((i + 1) % 200 === 0) { const progress = 60 + Math.floor((i + 1) / sortedMessages.length * 30) From 388923257b44b382933a6b984cbc82105d6925b7 Mon Sep 17 00:00:00 2001 From: QingXiao <143726276+5xiao0qing5@users.noreply.github.com> Date: Fri, 23 Jan 2026 23:53:33 +0800 Subject: [PATCH 2/3] Handle more message types in exports --- electron/services/exportService.ts | 61 ++++++++++++++++++++++++++++++ src/pages/SettingsPage.tsx | 55 --------------------------- 2 files changed, 61 insertions(+), 55 deletions(-) diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts index 16c52ab..37c467d 100644 --- a/electron/services/exportService.ts +++ b/electron/services/exportService.ts @@ -321,6 +321,33 @@ class ExportService { } return '[其他消息]' } + if (localType === 42) { + const normalized = this.normalizeAppMessageContent(safeContent) + const nickname = + this.extractXmlValue(normalized, 'nickname') || + this.extractXmlValue(normalized, 'displayname') || + this.extractXmlValue(normalized, 'name') + return nickname ? `[名片]${nickname}` : '[名片]' + } + if (localType === 43) { + const normalized = this.normalizeAppMessageContent(safeContent) + const lengthValue = + this.extractXmlValue(normalized, 'playlength') || + this.extractXmlValue(normalized, 'playLength') || + this.extractXmlValue(normalized, 'length') || + this.extractXmlValue(normalized, 'duration') + const seconds = lengthValue ? this.parseDurationSeconds(lengthValue) : null + return seconds ? `[视频]${seconds}s` : '[视频]' + } + if (localType === 48) { + const normalized = this.normalizeAppMessageContent(safeContent) + const location = + this.extractXmlValue(normalized, 'label') || + this.extractXmlValue(normalized, 'poiname') || + this.extractXmlValue(normalized, 'poiName') || + this.extractXmlValue(normalized, 'name') + return location ? `[定位]${location}` : '[定位]' + } if (localType === 10000 || localType === 266287972401) { return this.cleanSystemMessage(safeContent) } @@ -331,10 +358,31 @@ class ExportService { const typeMatch = /(\d+)<\/type>/i.exec(normalized) const subType = typeMatch ? parseInt(typeMatch[1], 10) : 0 const title = this.extractXmlValue(normalized, 'title') || this.extractXmlValue(normalized, 'appname') + if (subType === 3 || normalized.includes('= 1000) return Math.round(numeric / 1000) + return Math.round(numeric) + } + + private extractAmountFromText(text: string): string | null { + if (!text) return null + const match = /([¥¥]\s*\d+(?:\.\d+)?|\d+(?:\.\d+)?)/.exec(text) + return match ? match[1].replace(/\s+/g, '') : null + } + private stripSenderPrefix(content: string): string { return content.replace(/^[\s]*([a-zA-Z0-9_-]+):(?!\/\/)/, '') } diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 93c7484..18b2979 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -61,7 +61,6 @@ function SettingsPage() { const [exportDefaultMedia, setExportDefaultMedia] = useState(false) const [exportDefaultVoiceAsText, setExportDefaultVoiceAsText] = useState(true) const [exportDefaultExcelCompactColumns, setExportDefaultExcelCompactColumns] = useState(true) - const [exportDefaultTxtColumns, setExportDefaultTxtColumns] = useState(['index', 'time', 'senderRole', 'messageType', 'content']) const [isLoading, setIsLoadingState] = useState(false) const [isTesting, setIsTesting] = useState(false) @@ -142,8 +141,6 @@ function SettingsPage() { const savedExportDefaultMedia = await configService.getExportDefaultMedia() const savedExportDefaultVoiceAsText = await configService.getExportDefaultVoiceAsText() const savedExportDefaultExcelCompactColumns = await configService.getExportDefaultExcelCompactColumns() - const savedExportDefaultTxtColumns = await configService.getExportDefaultTxtColumns() - const defaultTxtColumns = ['index', 'time', 'senderRole', 'messageType', 'content'] if (savedKey) setDecryptKey(savedKey) if (savedPath) setDbPath(savedPath) @@ -161,11 +158,6 @@ function SettingsPage() { setExportDefaultMedia(savedExportDefaultMedia ?? false) setExportDefaultVoiceAsText(savedExportDefaultVoiceAsText ?? true) setExportDefaultExcelCompactColumns(savedExportDefaultExcelCompactColumns ?? true) - setExportDefaultTxtColumns( - savedExportDefaultTxtColumns && savedExportDefaultTxtColumns.length > 0 - ? savedExportDefaultTxtColumns - : defaultTxtColumns - ) // 如果语言列表为空,保存默认值 if (!savedTranscribeLanguages || savedTranscribeLanguages.length === 0) { @@ -174,9 +166,6 @@ function SettingsPage() { await configService.setTranscribeLanguages(defaultLanguages) } - if (!savedExportDefaultTxtColumns || savedExportDefaultTxtColumns.length === 0) { - await configService.setExportDefaultTxtColumns(defaultTxtColumns) - } if (savedWhisperModelDir) setWhisperModelDir(savedWhisperModelDir) } catch (e) { @@ -911,16 +900,6 @@ function SettingsPage() { { value: 'compact', label: '精简列', desc: '序号、时间、发送者身份、消息类型、内容' }, { value: 'full', label: '完整列', desc: '含发送者昵称/微信ID/备注' } ] - const exportTxtColumnOptions = [ - { value: 'index', label: '序号' }, - { value: 'time', label: '时间' }, - { value: 'senderRole', label: '发送者身份' }, - { value: 'messageType', label: '消息类型' }, - { value: 'content', label: '内容' }, - { value: 'senderNickname', label: '发送者昵称' }, - { value: 'senderWxid', label: '发送者微信ID' }, - { value: 'senderRemark', label: '发送者备注' } - ] const getOptionLabel = (options: { value: string; label: string }[], value: string) => { return options.find((option) => option.value === value)?.label ?? value @@ -1097,40 +1076,6 @@ function SettingsPage() { -
- - 默认与 Excel 精简列一致,可多选调整输出字段 -
- {exportTxtColumnOptions.map((column) => { - const checked = exportDefaultTxtColumns.includes(column.value) - return ( - - ) - })} -
-
) } From 135f4819fb350ab7a0a04f0b584a68481e78d722 Mon Sep 17 00:00:00 2001 From: QingXiao <143726276+5xiao0qing5@users.noreply.github.com> Date: Sat, 24 Jan 2026 00:07:49 +0800 Subject: [PATCH 3/3] Align HTML export parsing and voip placeholders --- electron/services/exportService.ts | 31 +++++++++--------------------- 1 file changed, 9 insertions(+), 22 deletions(-) diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts index 37c467d..88b79f3 100644 --- a/electron/services/exportService.ts +++ b/electron/services/exportService.ts @@ -348,6 +348,9 @@ class ExportService { this.extractXmlValue(normalized, 'name') return location ? `[定位]${location}` : '[定位]' } + if (localType === 50) { + return this.parseVoipMessage(safeContent) + } if (localType === 10000 || localType === 266287972401) { return this.cleanSystemMessage(safeContent) } @@ -667,31 +670,15 @@ class ExportService { private formatHtmlMessageText(content: string, localType: number): string { if (!content) return '' - const normalized = this.normalizeAppMessageContent(content) - const isAppMessage = normalized.includes('') - - if (localType === 49 || isAppMessage) { - const typeMatch = /(\d+)<\/type>/i.exec(normalized) - const subType = typeMatch ? parseInt(typeMatch[1], 10) : 0 - const title = this.extractXmlValue(normalized, 'title') || this.extractXmlValue(normalized, 'appname') - if (subType === 6) { - const fileName = this.extractXmlValue(normalized, 'filename') || title || '文件' - return `[文件] ${fileName}`.trim() - } - if (subType === 33 || subType === 36) { - const appName = this.extractXmlValue(normalized, 'appname') - const miniTitle = title || appName || '小程序' - return `[小程序] ${miniTitle}`.trim() - } - return title || '[链接]' + if (localType === 1) { + return this.stripSenderPrefix(content) } - if (localType === 42) { - const nickname = this.extractXmlValue(normalized, 'nickname') - return nickname ? `[名片] ${nickname}` : '[名片]' + if (localType === 34) { + return this.parseMessageContent(content, localType) || '' } - return this.parseMessageContent(content, localType) || '' + return this.formatPlainExportContent(content, localType, { exportVoiceAsText: false }) } /** @@ -2501,7 +2488,7 @@ class ExportService { if (msg.localType === 34 && useVoiceTranscript) { textContent = voiceTranscriptMap.get(msg.localId) || '[语音消息 - 转文字失败]' } - if (mediaItem && (msg.localType === 3 || msg.localType === 43 || msg.localType === 47)) { + if (mediaItem && (msg.localType === 3 || msg.localType === 47)) { textContent = '' }