diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts index 868a46f..fda4063 100644 --- a/electron/services/exportService.ts +++ b/electron/services/exportService.ts @@ -73,6 +73,7 @@ export interface ExportOptions { exportAvatars?: boolean exportImages?: boolean exportVoices?: boolean + exportVideos?: boolean exportEmojis?: boolean exportVoiceAsText?: boolean excelCompactColumns?: boolean @@ -186,6 +187,20 @@ class ExportService { return info } + private async preloadContacts( + usernames: Iterable, + cache: Map, + limit = 8 + ): Promise { + const unique = Array.from(new Set(Array.from(usernames).filter(Boolean))) + if (unique.length === 0) return + await parallelLimit(unique, limit, async (username) => { + if (cache.has(username)) return + const result = await wcdbService.getContact(username) + cache.set(username, result) + }) + } + /** * 解析 ext_buffer 二进制数据,提取群成员的群昵称 * ext_buffer 包含类似 protobuf 编码的数据,格式示例: @@ -859,10 +874,10 @@ class ExportService { options: { exportImages?: boolean exportVoices?: boolean + exportVideos?: boolean exportEmojis?: boolean exportVoiceAsText?: boolean includeVoiceWithTranscript?: boolean - exportVideos?: boolean } ): Promise { const localType = msg.localType @@ -877,8 +892,7 @@ class ExportService { // 语音消息 if (localType === 34) { - const shouldKeepVoiceFile = options.includeVoiceWithTranscript || !options.exportVoiceAsText - if (shouldKeepVoiceFile && options.exportVoices) { + if (options.exportVoices) { return this.exportVoice(msg, sessionId, mediaRootDir, mediaRelativePrefix) } if (options.exportVoiceAsText) { @@ -1233,7 +1247,7 @@ class ExportService { mediaRelativePrefix: string } { const exportMediaEnabled = options.exportMedia === true && - Boolean(options.exportImages || options.exportVoices || options.exportEmojis) + Boolean(options.exportImages || options.exportVoices || options.exportVideos || options.exportEmojis) const outputDir = path.dirname(outputPath) const outputBaseName = path.basename(outputPath, path.extname(outputPath)) const useSharedMediaLayout = options.sessionLayout === 'shared' @@ -1681,10 +1695,6 @@ class ExportService { phase: 'preparing' }) - if (options.exportVoiceAsText) { - await this.ensureVoiceModel(onProgress) - } - const collected = await this.collectMessages(sessionId, cleanedMyWxid, options.dateRange) const allMessages = collected.rows @@ -1693,6 +1703,14 @@ class ExportService { return { success: false, error: '该会话在指定时间范围内没有消息' } } + const voiceMessages = options.exportVoiceAsText + ? allMessages.filter(msg => msg.localType === 34) + : [] + + if (options.exportVoiceAsText && voiceMessages.length > 0) { + await this.ensureVoiceModel(onProgress) + } + if (isGroup) { await this.mergeGroupMembers(sessionId, collected.memberSet, options.exportAvatars === true) } @@ -1707,7 +1725,8 @@ class ExportService { const t = msg.localType return (t === 3 && options.exportImages) || // 图片 (t === 47 && options.exportEmojis) || // 表情 - (t === 34 && options.exportVoices && !options.exportVoiceAsText) // 语音文件(非转文字) + (t === 43 && options.exportVideos) || // 视频 + (t === 34 && options.exportVoices) // 语音文件 }) : [] @@ -1729,6 +1748,7 @@ class ExportService { const mediaItem = await this.exportMediaForMessage(msg, sessionId, mediaRootDir, mediaRelativePrefix, { exportImages: options.exportImages, exportVoices: options.exportVoices, + exportVideos: options.exportVideos, exportEmojis: options.exportEmojis, exportVoiceAsText: options.exportVoiceAsText }) @@ -1738,10 +1758,6 @@ class ExportService { } // ========== 阶段2:并行语音转文字 ========== - const voiceMessages = options.exportVoiceAsText - ? allMessages.filter(msg => msg.localType === 34) - : [] - const voiceTranscriptMap = new Map() if (voiceMessages.length > 0) { @@ -1895,10 +1911,6 @@ class ExportService { phase: 'preparing' }) - if (options.exportVoiceAsText) { - await this.ensureVoiceModel(onProgress) - } - const collected = await this.collectMessages(sessionId, cleanedMyWxid, options.dateRange) // 如果没有消息,不创建文件 @@ -1906,6 +1918,21 @@ class ExportService { return { success: false, error: '该会话在指定时间范围内没有消息' } } + const voiceMessages = options.exportVoiceAsText + ? collected.rows.filter(msg => msg.localType === 34) + : [] + + if (options.exportVoiceAsText && voiceMessages.length > 0) { + await this.ensureVoiceModel(onProgress) + } + + const senderUsernames = new Set() + for (const msg of collected.rows) { + if (msg.senderUsername) senderUsernames.add(msg.senderUsername) + } + senderUsernames.add(sessionId) + await this.preloadContacts(senderUsernames, contactCache) + const { exportMediaEnabled, mediaRootDir, mediaRelativePrefix } = this.getMediaLayout(outputPath, options) // ========== 阶段1:并行导出媒体文件 ========== @@ -1914,7 +1941,8 @@ class ExportService { const t = msg.localType return (t === 3 && options.exportImages) || (t === 47 && options.exportEmojis) || - (t === 34 && options.exportVoices && !options.exportVoiceAsText) + (t === 43 && options.exportVideos) || + (t === 34 && options.exportVoices) }) : [] @@ -1935,6 +1963,7 @@ class ExportService { const mediaItem = await this.exportMediaForMessage(msg, sessionId, mediaRootDir, mediaRelativePrefix, { exportImages: options.exportImages, exportVoices: options.exportVoices, + exportVideos: options.exportVideos, exportEmojis: options.exportEmojis, exportVoiceAsText: options.exportVoiceAsText }) @@ -1944,10 +1973,6 @@ class ExportService { } // ========== 阶段2:并行语音转文字 ========== - const voiceMessages = options.exportVoiceAsText - ? collected.rows.filter(msg => msg.localType === 34) - : [] - const voiceTranscriptMap = new Map() if (voiceMessages.length > 0) { @@ -1988,10 +2013,10 @@ class ExportService { const mediaKey = `${msg.localType}_${msg.localId}` const mediaItem = mediaCache.get(mediaKey) - if (mediaItem) { - content = mediaItem.relativePath - } else if (msg.localType === 34 && options.exportVoiceAsText) { + if (msg.localType === 34 && options.exportVoiceAsText) { content = voiceTranscriptMap.get(msg.localId) || '[语音消息 - 转文字失败]' + } else if (mediaItem) { + content = mediaItem.relativePath } else { content = this.parseMessageContent(msg.content, msg.localType) } @@ -2156,10 +2181,6 @@ class ExportService { phase: 'preparing' }) - if (options.exportVoiceAsText) { - await this.ensureVoiceModel(onProgress) - } - const collected = await this.collectMessages(sessionId, cleanedMyWxid, options.dateRange) // 如果没有消息,不创建文件 @@ -2167,6 +2188,21 @@ class ExportService { return { success: false, error: '该会话在指定时间范围内没有消息' } } + const voiceMessages = options.exportVoiceAsText + ? collected.rows.filter(msg => msg.localType === 34) + : [] + + if (options.exportVoiceAsText && voiceMessages.length > 0) { + await this.ensureVoiceModel(onProgress) + } + + const senderUsernames = new Set() + for (const msg of collected.rows) { + if (msg.senderUsername) senderUsernames.add(msg.senderUsername) + } + senderUsernames.add(sessionId) + await this.preloadContacts(senderUsernames, contactCache) + onProgress?.({ current: 30, total: 100, @@ -2297,7 +2333,8 @@ class ExportService { const t = msg.localType return (t === 3 && options.exportImages) || (t === 47 && options.exportEmojis) || - (t === 34 && options.exportVoices && !options.exportVoiceAsText) + (t === 43 && options.exportVideos) || + (t === 34 && options.exportVoices) }) : [] @@ -2318,6 +2355,7 @@ class ExportService { const mediaItem = await this.exportMediaForMessage(msg, sessionId, mediaRootDir, mediaRelativePrefix, { exportImages: options.exportImages, exportVoices: options.exportVoices, + exportVideos: options.exportVideos, exportEmojis: options.exportEmojis, exportVoiceAsText: options.exportVoiceAsText }) @@ -2327,10 +2365,6 @@ class ExportService { } // ========== 并行预处理:语音转文字 ========== - const voiceMessages = options.exportVoiceAsText - ? sortedMessages.filter(msg => msg.localType === 34) - : [] - const voiceTranscriptMap = new Map() if (voiceMessages.length > 0) { @@ -2416,13 +2450,21 @@ class ExportService { const mediaKey = `${msg.localType}_${msg.localId}` const mediaItem = mediaCache.get(mediaKey) - const contentValue = mediaItem?.relativePath - || this.formatPlainExportContent( + const shouldUseTranscript = msg.localType === 34 && options.exportVoiceAsText + const contentValue = shouldUseTranscript + ? this.formatPlainExportContent( msg.content, msg.localType, options, voiceTranscriptMap.get(msg.localId) ) + : (mediaItem?.relativePath + || this.formatPlainExportContent( + msg.content, + msg.localType, + options, + voiceTranscriptMap.get(msg.localId) + )) // 调试日志 if (msg.localType === 3 || msg.localType === 47) { @@ -2549,6 +2591,16 @@ class ExportService { const sessionInfo = await this.getContactInfo(sessionId) const myInfo = await this.getContactInfo(cleanedMyWxid) + const contactCache = new Map() + const getContactCached = async (username: string) => { + if (contactCache.has(username)) { + return contactCache.get(username)! + } + const result = await wcdbService.getContact(username) + contactCache.set(username, result) + return result + } + onProgress?.({ current: 0, total: 100, @@ -2556,10 +2608,6 @@ class ExportService { phase: 'preparing' }) - if (options.exportVoiceAsText) { - await this.ensureVoiceModel(onProgress) - } - const collected = await this.collectMessages(sessionId, cleanedMyWxid, options.dateRange) // 如果没有消息,不创建文件 @@ -2567,6 +2615,21 @@ class ExportService { return { success: false, error: '该会话在指定时间范围内没有消息' } } + const voiceMessages = options.exportVoiceAsText + ? collected.rows.filter(msg => msg.localType === 34) + : [] + + if (options.exportVoiceAsText && voiceMessages.length > 0) { + await this.ensureVoiceModel(onProgress) + } + + const senderUsernames = new Set() + for (const msg of collected.rows) { + if (msg.senderUsername) senderUsernames.add(msg.senderUsername) + } + senderUsernames.add(sessionId) + await this.preloadContacts(senderUsernames, contactCache) + const sortedMessages = collected.rows.sort((a, b) => a.createTime - b.createTime) const { exportMediaEnabled, mediaRootDir, mediaRelativePrefix } = this.getMediaLayout(outputPath, options) @@ -2575,7 +2638,8 @@ class ExportService { const t = msg.localType return (t === 3 && options.exportImages) || (t === 47 && options.exportEmojis) || - (t === 34 && options.exportVoices && !options.exportVoiceAsText) + (t === 43 && options.exportVideos) || + (t === 34 && options.exportVoices) }) : [] @@ -2596,6 +2660,7 @@ class ExportService { const mediaItem = await this.exportMediaForMessage(msg, sessionId, mediaRootDir, mediaRelativePrefix, { exportImages: options.exportImages, exportVoices: options.exportVoices, + exportVideos: options.exportVideos, exportEmojis: options.exportEmojis, exportVoiceAsText: options.exportVoiceAsText }) @@ -2604,9 +2669,6 @@ class ExportService { }) } - const voiceMessages = options.exportVoiceAsText - ? sortedMessages.filter(msg => msg.localType === 34) - : [] const voiceTranscriptMap = new Map() if (voiceMessages.length > 0) { @@ -2637,13 +2699,21 @@ class ExportService { const msg = sortedMessages[i] const mediaKey = `${msg.localType}_${msg.localId}` const mediaItem = mediaCache.get(mediaKey) - const contentValue = mediaItem?.relativePath - || this.formatPlainExportContent( + const shouldUseTranscript = msg.localType === 34 && options.exportVoiceAsText + const contentValue = shouldUseTranscript + ? this.formatPlainExportContent( msg.content, msg.localType, options, voiceTranscriptMap.get(msg.localId) ) + : (mediaItem?.relativePath + || this.formatPlainExportContent( + msg.content, + msg.localType, + options, + voiceTranscriptMap.get(msg.localId) + )) let senderRole: string let senderWxid: string @@ -2763,7 +2833,7 @@ class ExportService { return (t === 3 && options.exportImages) || (t === 47 && options.exportEmojis) || (t === 34 && options.exportVoices) || - t === 43 + (t === 43 && options.exportVideos) }) : [] @@ -2787,7 +2857,7 @@ class ExportService { exportEmojis: options.exportEmojis, exportVoiceAsText: options.exportVoiceAsText, includeVoiceWithTranscript: true, - exportVideos: true + exportVideos: options.exportVideos }) mediaCache.set(mediaKey, mediaItem) } @@ -3094,7 +3164,7 @@ class ExportService { } const exportMediaEnabled = options.exportMedia === true && - Boolean(options.exportImages || options.exportVoices || options.exportEmojis) + Boolean(options.exportImages || options.exportVoices || options.exportVideos || options.exportEmojis) const sessionLayout = exportMediaEnabled ? (options.sessionLayout ?? 'per-session') : 'shared' diff --git a/package-lock.json b/package-lock.json index 9412eb3..abbf9b6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "weflow", - "version": "1.4.1", + "version": "1.4.2", "lockfileVersion": 3, "requires": true, "packages": { diff --git a/package.json b/package.json index 46ab5a0..3163e57 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "weflow", - "version": "1.4.1", + "version": "1.4.2", "description": "WeFlow", "main": "dist-electron/main.js", "author": "cc", diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index 4f86aa4..67d89fc 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -19,6 +19,7 @@ interface ExportOptions { exportMedia: boolean exportImages: boolean exportVoices: boolean + exportVideos: boolean exportEmojis: boolean exportVoiceAsText: boolean excelCompactColumns: boolean @@ -65,6 +66,7 @@ function ExportPage() { exportMedia: false, exportImages: true, exportVoices: true, + exportVideos: true, exportEmojis: true, exportVoiceAsText: true, excelCompactColumns: true, @@ -257,6 +259,7 @@ function ExportPage() { exportMedia: true, exportImages: true, exportVoices: true, + exportVideos: true, exportEmojis: true, exportVoiceAsText: true } @@ -286,6 +289,7 @@ function ExportPage() { exportMedia: options.exportMedia, exportImages: options.exportMedia && options.exportImages, exportVoices: options.exportMedia && options.exportVoices, + exportVideos: options.exportMedia && options.exportVideos, exportEmojis: options.exportMedia && options.exportEmojis, exportVoiceAsText: options.exportVoiceAsText, // 即使不导出媒体,也可以导出语音转文字内容 excelCompactColumns: options.excelCompactColumns, @@ -609,7 +613,7 @@ function ExportPage() { )}

媒体文件

-

导出图片/语音/表情并在记录内写入相对路径

+

导出图片/语音/视频/表情并在记录内写入相对路径

@@ -661,7 +665,7 @@ function ExportPage() {
+ + +
+
) }