mirror of
https://github.com/hicccc77/WeFlow.git
synced 2026-03-24 23:06:51 +00:00
导出方面优化
This commit is contained in:
@@ -73,6 +73,7 @@ export interface ExportOptions {
|
|||||||
exportAvatars?: boolean
|
exportAvatars?: boolean
|
||||||
exportImages?: boolean
|
exportImages?: boolean
|
||||||
exportVoices?: boolean
|
exportVoices?: boolean
|
||||||
|
exportVideos?: boolean
|
||||||
exportEmojis?: boolean
|
exportEmojis?: boolean
|
||||||
exportVoiceAsText?: boolean
|
exportVoiceAsText?: boolean
|
||||||
excelCompactColumns?: boolean
|
excelCompactColumns?: boolean
|
||||||
@@ -186,6 +187,20 @@ class ExportService {
|
|||||||
return info
|
return info
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async preloadContacts(
|
||||||
|
usernames: Iterable<string>,
|
||||||
|
cache: Map<string, { success: boolean; contact?: any; error?: string }>,
|
||||||
|
limit = 8
|
||||||
|
): Promise<void> {
|
||||||
|
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 二进制数据,提取群成员的群昵称
|
||||||
* ext_buffer 包含类似 protobuf 编码的数据,格式示例:
|
* ext_buffer 包含类似 protobuf 编码的数据,格式示例:
|
||||||
@@ -859,10 +874,10 @@ class ExportService {
|
|||||||
options: {
|
options: {
|
||||||
exportImages?: boolean
|
exportImages?: boolean
|
||||||
exportVoices?: boolean
|
exportVoices?: boolean
|
||||||
|
exportVideos?: boolean
|
||||||
exportEmojis?: boolean
|
exportEmojis?: boolean
|
||||||
exportVoiceAsText?: boolean
|
exportVoiceAsText?: boolean
|
||||||
includeVoiceWithTranscript?: boolean
|
includeVoiceWithTranscript?: boolean
|
||||||
exportVideos?: boolean
|
|
||||||
}
|
}
|
||||||
): Promise<MediaExportItem | null> {
|
): Promise<MediaExportItem | null> {
|
||||||
const localType = msg.localType
|
const localType = msg.localType
|
||||||
@@ -877,8 +892,7 @@ class ExportService {
|
|||||||
|
|
||||||
// 语音消息
|
// 语音消息
|
||||||
if (localType === 34) {
|
if (localType === 34) {
|
||||||
const shouldKeepVoiceFile = options.includeVoiceWithTranscript || !options.exportVoiceAsText
|
if (options.exportVoices) {
|
||||||
if (shouldKeepVoiceFile && options.exportVoices) {
|
|
||||||
return this.exportVoice(msg, sessionId, mediaRootDir, mediaRelativePrefix)
|
return this.exportVoice(msg, sessionId, mediaRootDir, mediaRelativePrefix)
|
||||||
}
|
}
|
||||||
if (options.exportVoiceAsText) {
|
if (options.exportVoiceAsText) {
|
||||||
@@ -1233,7 +1247,7 @@ class ExportService {
|
|||||||
mediaRelativePrefix: string
|
mediaRelativePrefix: string
|
||||||
} {
|
} {
|
||||||
const exportMediaEnabled = options.exportMedia === true &&
|
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 outputDir = path.dirname(outputPath)
|
||||||
const outputBaseName = path.basename(outputPath, path.extname(outputPath))
|
const outputBaseName = path.basename(outputPath, path.extname(outputPath))
|
||||||
const useSharedMediaLayout = options.sessionLayout === 'shared'
|
const useSharedMediaLayout = options.sessionLayout === 'shared'
|
||||||
@@ -1681,10 +1695,6 @@ class ExportService {
|
|||||||
phase: 'preparing'
|
phase: 'preparing'
|
||||||
})
|
})
|
||||||
|
|
||||||
if (options.exportVoiceAsText) {
|
|
||||||
await this.ensureVoiceModel(onProgress)
|
|
||||||
}
|
|
||||||
|
|
||||||
const collected = await this.collectMessages(sessionId, cleanedMyWxid, options.dateRange)
|
const collected = await this.collectMessages(sessionId, cleanedMyWxid, options.dateRange)
|
||||||
const allMessages = collected.rows
|
const allMessages = collected.rows
|
||||||
|
|
||||||
@@ -1693,6 +1703,14 @@ class ExportService {
|
|||||||
return { success: false, error: '该会话在指定时间范围内没有消息' }
|
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) {
|
if (isGroup) {
|
||||||
await this.mergeGroupMembers(sessionId, collected.memberSet, options.exportAvatars === true)
|
await this.mergeGroupMembers(sessionId, collected.memberSet, options.exportAvatars === true)
|
||||||
}
|
}
|
||||||
@@ -1707,7 +1725,8 @@ class ExportService {
|
|||||||
const t = msg.localType
|
const t = msg.localType
|
||||||
return (t === 3 && options.exportImages) || // 图片
|
return (t === 3 && options.exportImages) || // 图片
|
||||||
(t === 47 && options.exportEmojis) || // 表情
|
(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, {
|
const mediaItem = await this.exportMediaForMessage(msg, sessionId, mediaRootDir, mediaRelativePrefix, {
|
||||||
exportImages: options.exportImages,
|
exportImages: options.exportImages,
|
||||||
exportVoices: options.exportVoices,
|
exportVoices: options.exportVoices,
|
||||||
|
exportVideos: options.exportVideos,
|
||||||
exportEmojis: options.exportEmojis,
|
exportEmojis: options.exportEmojis,
|
||||||
exportVoiceAsText: options.exportVoiceAsText
|
exportVoiceAsText: options.exportVoiceAsText
|
||||||
})
|
})
|
||||||
@@ -1738,10 +1758,6 @@ class ExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========== 阶段2:并行语音转文字 ==========
|
// ========== 阶段2:并行语音转文字 ==========
|
||||||
const voiceMessages = options.exportVoiceAsText
|
|
||||||
? allMessages.filter(msg => msg.localType === 34)
|
|
||||||
: []
|
|
||||||
|
|
||||||
const voiceTranscriptMap = new Map<number, string>()
|
const voiceTranscriptMap = new Map<number, string>()
|
||||||
|
|
||||||
if (voiceMessages.length > 0) {
|
if (voiceMessages.length > 0) {
|
||||||
@@ -1895,10 +1911,6 @@ class ExportService {
|
|||||||
phase: 'preparing'
|
phase: 'preparing'
|
||||||
})
|
})
|
||||||
|
|
||||||
if (options.exportVoiceAsText) {
|
|
||||||
await this.ensureVoiceModel(onProgress)
|
|
||||||
}
|
|
||||||
|
|
||||||
const collected = await this.collectMessages(sessionId, cleanedMyWxid, options.dateRange)
|
const collected = await this.collectMessages(sessionId, cleanedMyWxid, options.dateRange)
|
||||||
|
|
||||||
// 如果没有消息,不创建文件
|
// 如果没有消息,不创建文件
|
||||||
@@ -1906,6 +1918,21 @@ class ExportService {
|
|||||||
return { success: false, error: '该会话在指定时间范围内没有消息' }
|
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<string>()
|
||||||
|
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)
|
const { exportMediaEnabled, mediaRootDir, mediaRelativePrefix } = this.getMediaLayout(outputPath, options)
|
||||||
|
|
||||||
// ========== 阶段1:并行导出媒体文件 ==========
|
// ========== 阶段1:并行导出媒体文件 ==========
|
||||||
@@ -1914,7 +1941,8 @@ class ExportService {
|
|||||||
const t = msg.localType
|
const t = msg.localType
|
||||||
return (t === 3 && options.exportImages) ||
|
return (t === 3 && options.exportImages) ||
|
||||||
(t === 47 && options.exportEmojis) ||
|
(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, {
|
const mediaItem = await this.exportMediaForMessage(msg, sessionId, mediaRootDir, mediaRelativePrefix, {
|
||||||
exportImages: options.exportImages,
|
exportImages: options.exportImages,
|
||||||
exportVoices: options.exportVoices,
|
exportVoices: options.exportVoices,
|
||||||
|
exportVideos: options.exportVideos,
|
||||||
exportEmojis: options.exportEmojis,
|
exportEmojis: options.exportEmojis,
|
||||||
exportVoiceAsText: options.exportVoiceAsText
|
exportVoiceAsText: options.exportVoiceAsText
|
||||||
})
|
})
|
||||||
@@ -1944,10 +1973,6 @@ class ExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========== 阶段2:并行语音转文字 ==========
|
// ========== 阶段2:并行语音转文字 ==========
|
||||||
const voiceMessages = options.exportVoiceAsText
|
|
||||||
? collected.rows.filter(msg => msg.localType === 34)
|
|
||||||
: []
|
|
||||||
|
|
||||||
const voiceTranscriptMap = new Map<number, string>()
|
const voiceTranscriptMap = new Map<number, string>()
|
||||||
|
|
||||||
if (voiceMessages.length > 0) {
|
if (voiceMessages.length > 0) {
|
||||||
@@ -1988,10 +2013,10 @@ class ExportService {
|
|||||||
const mediaKey = `${msg.localType}_${msg.localId}`
|
const mediaKey = `${msg.localType}_${msg.localId}`
|
||||||
const mediaItem = mediaCache.get(mediaKey)
|
const mediaItem = mediaCache.get(mediaKey)
|
||||||
|
|
||||||
if (mediaItem) {
|
if (msg.localType === 34 && options.exportVoiceAsText) {
|
||||||
content = mediaItem.relativePath
|
|
||||||
} else if (msg.localType === 34 && options.exportVoiceAsText) {
|
|
||||||
content = voiceTranscriptMap.get(msg.localId) || '[语音消息 - 转文字失败]'
|
content = voiceTranscriptMap.get(msg.localId) || '[语音消息 - 转文字失败]'
|
||||||
|
} else if (mediaItem) {
|
||||||
|
content = mediaItem.relativePath
|
||||||
} else {
|
} else {
|
||||||
content = this.parseMessageContent(msg.content, msg.localType)
|
content = this.parseMessageContent(msg.content, msg.localType)
|
||||||
}
|
}
|
||||||
@@ -2156,10 +2181,6 @@ class ExportService {
|
|||||||
phase: 'preparing'
|
phase: 'preparing'
|
||||||
})
|
})
|
||||||
|
|
||||||
if (options.exportVoiceAsText) {
|
|
||||||
await this.ensureVoiceModel(onProgress)
|
|
||||||
}
|
|
||||||
|
|
||||||
const collected = await this.collectMessages(sessionId, cleanedMyWxid, options.dateRange)
|
const collected = await this.collectMessages(sessionId, cleanedMyWxid, options.dateRange)
|
||||||
|
|
||||||
// 如果没有消息,不创建文件
|
// 如果没有消息,不创建文件
|
||||||
@@ -2167,6 +2188,21 @@ class ExportService {
|
|||||||
return { success: false, error: '该会话在指定时间范围内没有消息' }
|
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<string>()
|
||||||
|
for (const msg of collected.rows) {
|
||||||
|
if (msg.senderUsername) senderUsernames.add(msg.senderUsername)
|
||||||
|
}
|
||||||
|
senderUsernames.add(sessionId)
|
||||||
|
await this.preloadContacts(senderUsernames, contactCache)
|
||||||
|
|
||||||
onProgress?.({
|
onProgress?.({
|
||||||
current: 30,
|
current: 30,
|
||||||
total: 100,
|
total: 100,
|
||||||
@@ -2297,7 +2333,8 @@ class ExportService {
|
|||||||
const t = msg.localType
|
const t = msg.localType
|
||||||
return (t === 3 && options.exportImages) ||
|
return (t === 3 && options.exportImages) ||
|
||||||
(t === 47 && options.exportEmojis) ||
|
(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, {
|
const mediaItem = await this.exportMediaForMessage(msg, sessionId, mediaRootDir, mediaRelativePrefix, {
|
||||||
exportImages: options.exportImages,
|
exportImages: options.exportImages,
|
||||||
exportVoices: options.exportVoices,
|
exportVoices: options.exportVoices,
|
||||||
|
exportVideos: options.exportVideos,
|
||||||
exportEmojis: options.exportEmojis,
|
exportEmojis: options.exportEmojis,
|
||||||
exportVoiceAsText: options.exportVoiceAsText
|
exportVoiceAsText: options.exportVoiceAsText
|
||||||
})
|
})
|
||||||
@@ -2327,10 +2365,6 @@ class ExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ========== 并行预处理:语音转文字 ==========
|
// ========== 并行预处理:语音转文字 ==========
|
||||||
const voiceMessages = options.exportVoiceAsText
|
|
||||||
? sortedMessages.filter(msg => msg.localType === 34)
|
|
||||||
: []
|
|
||||||
|
|
||||||
const voiceTranscriptMap = new Map<number, string>()
|
const voiceTranscriptMap = new Map<number, string>()
|
||||||
|
|
||||||
if (voiceMessages.length > 0) {
|
if (voiceMessages.length > 0) {
|
||||||
@@ -2416,13 +2450,21 @@ class ExportService {
|
|||||||
|
|
||||||
const mediaKey = `${msg.localType}_${msg.localId}`
|
const mediaKey = `${msg.localType}_${msg.localId}`
|
||||||
const mediaItem = mediaCache.get(mediaKey)
|
const mediaItem = mediaCache.get(mediaKey)
|
||||||
const contentValue = mediaItem?.relativePath
|
const shouldUseTranscript = msg.localType === 34 && options.exportVoiceAsText
|
||||||
|| this.formatPlainExportContent(
|
const contentValue = shouldUseTranscript
|
||||||
|
? this.formatPlainExportContent(
|
||||||
msg.content,
|
msg.content,
|
||||||
msg.localType,
|
msg.localType,
|
||||||
options,
|
options,
|
||||||
voiceTranscriptMap.get(msg.localId)
|
voiceTranscriptMap.get(msg.localId)
|
||||||
)
|
)
|
||||||
|
: (mediaItem?.relativePath
|
||||||
|
|| this.formatPlainExportContent(
|
||||||
|
msg.content,
|
||||||
|
msg.localType,
|
||||||
|
options,
|
||||||
|
voiceTranscriptMap.get(msg.localId)
|
||||||
|
))
|
||||||
|
|
||||||
// 调试日志
|
// 调试日志
|
||||||
if (msg.localType === 3 || msg.localType === 47) {
|
if (msg.localType === 3 || msg.localType === 47) {
|
||||||
@@ -2549,6 +2591,16 @@ class ExportService {
|
|||||||
const sessionInfo = await this.getContactInfo(sessionId)
|
const sessionInfo = await this.getContactInfo(sessionId)
|
||||||
const myInfo = await this.getContactInfo(cleanedMyWxid)
|
const myInfo = await this.getContactInfo(cleanedMyWxid)
|
||||||
|
|
||||||
|
const contactCache = new Map<string, { success: boolean; contact?: any; error?: string }>()
|
||||||
|
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?.({
|
onProgress?.({
|
||||||
current: 0,
|
current: 0,
|
||||||
total: 100,
|
total: 100,
|
||||||
@@ -2556,10 +2608,6 @@ class ExportService {
|
|||||||
phase: 'preparing'
|
phase: 'preparing'
|
||||||
})
|
})
|
||||||
|
|
||||||
if (options.exportVoiceAsText) {
|
|
||||||
await this.ensureVoiceModel(onProgress)
|
|
||||||
}
|
|
||||||
|
|
||||||
const collected = await this.collectMessages(sessionId, cleanedMyWxid, options.dateRange)
|
const collected = await this.collectMessages(sessionId, cleanedMyWxid, options.dateRange)
|
||||||
|
|
||||||
// 如果没有消息,不创建文件
|
// 如果没有消息,不创建文件
|
||||||
@@ -2567,6 +2615,21 @@ class ExportService {
|
|||||||
return { success: false, error: '该会话在指定时间范围内没有消息' }
|
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<string>()
|
||||||
|
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 sortedMessages = collected.rows.sort((a, b) => a.createTime - b.createTime)
|
||||||
|
|
||||||
const { exportMediaEnabled, mediaRootDir, mediaRelativePrefix } = this.getMediaLayout(outputPath, options)
|
const { exportMediaEnabled, mediaRootDir, mediaRelativePrefix } = this.getMediaLayout(outputPath, options)
|
||||||
@@ -2575,7 +2638,8 @@ class ExportService {
|
|||||||
const t = msg.localType
|
const t = msg.localType
|
||||||
return (t === 3 && options.exportImages) ||
|
return (t === 3 && options.exportImages) ||
|
||||||
(t === 47 && options.exportEmojis) ||
|
(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, {
|
const mediaItem = await this.exportMediaForMessage(msg, sessionId, mediaRootDir, mediaRelativePrefix, {
|
||||||
exportImages: options.exportImages,
|
exportImages: options.exportImages,
|
||||||
exportVoices: options.exportVoices,
|
exportVoices: options.exportVoices,
|
||||||
|
exportVideos: options.exportVideos,
|
||||||
exportEmojis: options.exportEmojis,
|
exportEmojis: options.exportEmojis,
|
||||||
exportVoiceAsText: options.exportVoiceAsText
|
exportVoiceAsText: options.exportVoiceAsText
|
||||||
})
|
})
|
||||||
@@ -2604,9 +2669,6 @@ class ExportService {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const voiceMessages = options.exportVoiceAsText
|
|
||||||
? sortedMessages.filter(msg => msg.localType === 34)
|
|
||||||
: []
|
|
||||||
const voiceTranscriptMap = new Map<number, string>()
|
const voiceTranscriptMap = new Map<number, string>()
|
||||||
|
|
||||||
if (voiceMessages.length > 0) {
|
if (voiceMessages.length > 0) {
|
||||||
@@ -2637,13 +2699,21 @@ class ExportService {
|
|||||||
const msg = sortedMessages[i]
|
const msg = sortedMessages[i]
|
||||||
const mediaKey = `${msg.localType}_${msg.localId}`
|
const mediaKey = `${msg.localType}_${msg.localId}`
|
||||||
const mediaItem = mediaCache.get(mediaKey)
|
const mediaItem = mediaCache.get(mediaKey)
|
||||||
const contentValue = mediaItem?.relativePath
|
const shouldUseTranscript = msg.localType === 34 && options.exportVoiceAsText
|
||||||
|| this.formatPlainExportContent(
|
const contentValue = shouldUseTranscript
|
||||||
|
? this.formatPlainExportContent(
|
||||||
msg.content,
|
msg.content,
|
||||||
msg.localType,
|
msg.localType,
|
||||||
options,
|
options,
|
||||||
voiceTranscriptMap.get(msg.localId)
|
voiceTranscriptMap.get(msg.localId)
|
||||||
)
|
)
|
||||||
|
: (mediaItem?.relativePath
|
||||||
|
|| this.formatPlainExportContent(
|
||||||
|
msg.content,
|
||||||
|
msg.localType,
|
||||||
|
options,
|
||||||
|
voiceTranscriptMap.get(msg.localId)
|
||||||
|
))
|
||||||
|
|
||||||
let senderRole: string
|
let senderRole: string
|
||||||
let senderWxid: string
|
let senderWxid: string
|
||||||
@@ -2763,7 +2833,7 @@ class ExportService {
|
|||||||
return (t === 3 && options.exportImages) ||
|
return (t === 3 && options.exportImages) ||
|
||||||
(t === 47 && options.exportEmojis) ||
|
(t === 47 && options.exportEmojis) ||
|
||||||
(t === 34 && options.exportVoices) ||
|
(t === 34 && options.exportVoices) ||
|
||||||
t === 43
|
(t === 43 && options.exportVideos)
|
||||||
})
|
})
|
||||||
: []
|
: []
|
||||||
|
|
||||||
@@ -2787,7 +2857,7 @@ class ExportService {
|
|||||||
exportEmojis: options.exportEmojis,
|
exportEmojis: options.exportEmojis,
|
||||||
exportVoiceAsText: options.exportVoiceAsText,
|
exportVoiceAsText: options.exportVoiceAsText,
|
||||||
includeVoiceWithTranscript: true,
|
includeVoiceWithTranscript: true,
|
||||||
exportVideos: true
|
exportVideos: options.exportVideos
|
||||||
})
|
})
|
||||||
mediaCache.set(mediaKey, mediaItem)
|
mediaCache.set(mediaKey, mediaItem)
|
||||||
}
|
}
|
||||||
@@ -3094,7 +3164,7 @@ class ExportService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const exportMediaEnabled = options.exportMedia === true &&
|
const exportMediaEnabled = options.exportMedia === true &&
|
||||||
Boolean(options.exportImages || options.exportVoices || options.exportEmojis)
|
Boolean(options.exportImages || options.exportVoices || options.exportVideos || options.exportEmojis)
|
||||||
const sessionLayout = exportMediaEnabled
|
const sessionLayout = exportMediaEnabled
|
||||||
? (options.sessionLayout ?? 'per-session')
|
? (options.sessionLayout ?? 'per-session')
|
||||||
: 'shared'
|
: 'shared'
|
||||||
|
|||||||
2
package-lock.json
generated
2
package-lock.json
generated
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "weflow",
|
"name": "weflow",
|
||||||
"version": "1.4.1",
|
"version": "1.4.2",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "weflow",
|
"name": "weflow",
|
||||||
"version": "1.4.1",
|
"version": "1.4.2",
|
||||||
"description": "WeFlow",
|
"description": "WeFlow",
|
||||||
"main": "dist-electron/main.js",
|
"main": "dist-electron/main.js",
|
||||||
"author": "cc",
|
"author": "cc",
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ interface ExportOptions {
|
|||||||
exportMedia: boolean
|
exportMedia: boolean
|
||||||
exportImages: boolean
|
exportImages: boolean
|
||||||
exportVoices: boolean
|
exportVoices: boolean
|
||||||
|
exportVideos: boolean
|
||||||
exportEmojis: boolean
|
exportEmojis: boolean
|
||||||
exportVoiceAsText: boolean
|
exportVoiceAsText: boolean
|
||||||
excelCompactColumns: boolean
|
excelCompactColumns: boolean
|
||||||
@@ -65,6 +66,7 @@ function ExportPage() {
|
|||||||
exportMedia: false,
|
exportMedia: false,
|
||||||
exportImages: true,
|
exportImages: true,
|
||||||
exportVoices: true,
|
exportVoices: true,
|
||||||
|
exportVideos: true,
|
||||||
exportEmojis: true,
|
exportEmojis: true,
|
||||||
exportVoiceAsText: true,
|
exportVoiceAsText: true,
|
||||||
excelCompactColumns: true,
|
excelCompactColumns: true,
|
||||||
@@ -257,6 +259,7 @@ function ExportPage() {
|
|||||||
exportMedia: true,
|
exportMedia: true,
|
||||||
exportImages: true,
|
exportImages: true,
|
||||||
exportVoices: true,
|
exportVoices: true,
|
||||||
|
exportVideos: true,
|
||||||
exportEmojis: true,
|
exportEmojis: true,
|
||||||
exportVoiceAsText: true
|
exportVoiceAsText: true
|
||||||
}
|
}
|
||||||
@@ -286,6 +289,7 @@ function ExportPage() {
|
|||||||
exportMedia: options.exportMedia,
|
exportMedia: options.exportMedia,
|
||||||
exportImages: options.exportMedia && options.exportImages,
|
exportImages: options.exportMedia && options.exportImages,
|
||||||
exportVoices: options.exportMedia && options.exportVoices,
|
exportVoices: options.exportMedia && options.exportVoices,
|
||||||
|
exportVideos: options.exportMedia && options.exportVideos,
|
||||||
exportEmojis: options.exportMedia && options.exportEmojis,
|
exportEmojis: options.exportMedia && options.exportEmojis,
|
||||||
exportVoiceAsText: options.exportVoiceAsText, // 即使不导出媒体,也可以导出语音转文字内容
|
exportVoiceAsText: options.exportVoiceAsText, // 即使不导出媒体,也可以导出语音转文字内容
|
||||||
excelCompactColumns: options.excelCompactColumns,
|
excelCompactColumns: options.excelCompactColumns,
|
||||||
@@ -609,7 +613,7 @@ function ExportPage() {
|
|||||||
)}
|
)}
|
||||||
<div className="setting-section">
|
<div className="setting-section">
|
||||||
<h3>媒体文件</h3>
|
<h3>媒体文件</h3>
|
||||||
<p className="setting-subtitle">导出图片/语音/表情并在记录内写入相对路径</p>
|
<p className="setting-subtitle">导出图片/语音/视频/表情并在记录内写入相对路径</p>
|
||||||
<div className="media-options-card">
|
<div className="media-options-card">
|
||||||
<div className="media-switch-row">
|
<div className="media-switch-row">
|
||||||
<div className="media-switch-info">
|
<div className="media-switch-info">
|
||||||
@@ -661,7 +665,7 @@ function ExportPage() {
|
|||||||
<label className="media-checkbox-row">
|
<label className="media-checkbox-row">
|
||||||
<div className="media-checkbox-info">
|
<div className="media-checkbox-info">
|
||||||
<span className="media-checkbox-title">语音转文字</span>
|
<span className="media-checkbox-title">语音转文字</span>
|
||||||
<span className="media-checkbox-desc">将语音消息转换为文字导出(不导出语音文件)</span>
|
<span className="media-checkbox-desc">将语音消息转换为文字导出</span>
|
||||||
</div>
|
</div>
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
@@ -672,6 +676,21 @@ function ExportPage() {
|
|||||||
|
|
||||||
<div className="media-option-divider"></div>
|
<div className="media-option-divider"></div>
|
||||||
|
|
||||||
|
<label className={`media-checkbox-row ${!options.exportMedia ? 'disabled' : ''}`}>
|
||||||
|
<div className="media-checkbox-info">
|
||||||
|
<span className="media-checkbox-title">视频</span>
|
||||||
|
<span className="media-checkbox-desc">直接复制视频文件到导出目录</span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={options.exportVideos}
|
||||||
|
disabled={!options.exportMedia}
|
||||||
|
onChange={e => setOptions({ ...options, exportVideos: e.target.checked })}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<div className="media-option-divider"></div>
|
||||||
|
|
||||||
<label className={`media-checkbox-row ${!options.exportMedia ? 'disabled' : ''}`}>
|
<label className={`media-checkbox-row ${!options.exportMedia ? 'disabled' : ''}`}>
|
||||||
<div className="media-checkbox-info">
|
<div className="media-checkbox-info">
|
||||||
<span className="media-checkbox-title">表情</span>
|
<span className="media-checkbox-title">表情</span>
|
||||||
|
|||||||
@@ -62,9 +62,11 @@ function SettingsPage() {
|
|||||||
const [showExportFormatSelect, setShowExportFormatSelect] = useState(false)
|
const [showExportFormatSelect, setShowExportFormatSelect] = useState(false)
|
||||||
const [showExportDateRangeSelect, setShowExportDateRangeSelect] = useState(false)
|
const [showExportDateRangeSelect, setShowExportDateRangeSelect] = useState(false)
|
||||||
const [showExportExcelColumnsSelect, setShowExportExcelColumnsSelect] = useState(false)
|
const [showExportExcelColumnsSelect, setShowExportExcelColumnsSelect] = useState(false)
|
||||||
|
const [showExportConcurrencySelect, setShowExportConcurrencySelect] = useState(false)
|
||||||
const exportFormatDropdownRef = useRef<HTMLDivElement>(null)
|
const exportFormatDropdownRef = useRef<HTMLDivElement>(null)
|
||||||
const exportDateRangeDropdownRef = useRef<HTMLDivElement>(null)
|
const exportDateRangeDropdownRef = useRef<HTMLDivElement>(null)
|
||||||
const exportExcelColumnsDropdownRef = useRef<HTMLDivElement>(null)
|
const exportExcelColumnsDropdownRef = useRef<HTMLDivElement>(null)
|
||||||
|
const exportConcurrencyDropdownRef = useRef<HTMLDivElement>(null)
|
||||||
const [cachePath, setCachePath] = useState('')
|
const [cachePath, setCachePath] = useState('')
|
||||||
const [logEnabled, setLogEnabled] = useState(false)
|
const [logEnabled, setLogEnabled] = useState(false)
|
||||||
const [whisperModelName, setWhisperModelName] = useState('base')
|
const [whisperModelName, setWhisperModelName] = useState('base')
|
||||||
@@ -144,10 +146,13 @@ function SettingsPage() {
|
|||||||
if (showExportExcelColumnsSelect && exportExcelColumnsDropdownRef.current && !exportExcelColumnsDropdownRef.current.contains(target)) {
|
if (showExportExcelColumnsSelect && exportExcelColumnsDropdownRef.current && !exportExcelColumnsDropdownRef.current.contains(target)) {
|
||||||
setShowExportExcelColumnsSelect(false)
|
setShowExportExcelColumnsSelect(false)
|
||||||
}
|
}
|
||||||
|
if (showExportConcurrencySelect && exportConcurrencyDropdownRef.current && !exportConcurrencyDropdownRef.current.contains(target)) {
|
||||||
|
setShowExportConcurrencySelect(false)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
document.addEventListener('mousedown', handleClickOutside)
|
document.addEventListener('mousedown', handleClickOutside)
|
||||||
return () => document.removeEventListener('mousedown', handleClickOutside)
|
return () => document.removeEventListener('mousedown', handleClickOutside)
|
||||||
}, [showExportFormatSelect, showExportDateRangeSelect, showExportExcelColumnsSelect])
|
}, [showExportFormatSelect, showExportDateRangeSelect, showExportExcelColumnsSelect, showExportConcurrencySelect])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const removeDb = window.electronAPI.key.onDbKeyStatus((payload) => {
|
const removeDb = window.electronAPI.key.onDbKeyStatus((payload) => {
|
||||||
@@ -1110,6 +1115,15 @@ function SettingsPage() {
|
|||||||
{ value: 'full', label: '完整列', desc: '含发送者昵称/微信ID/备注' }
|
{ value: 'full', label: '完整列', desc: '含发送者昵称/微信ID/备注' }
|
||||||
]
|
]
|
||||||
|
|
||||||
|
const exportConcurrencyOptions = [
|
||||||
|
{ value: 1, label: '1' },
|
||||||
|
{ value: 2, label: '2' },
|
||||||
|
{ value: 3, label: '3' },
|
||||||
|
{ value: 4, label: '4' },
|
||||||
|
{ value: 5, label: '5' },
|
||||||
|
{ value: 6, label: '6' }
|
||||||
|
]
|
||||||
|
|
||||||
const getOptionLabel = (options: { value: string; label: string }[], value: string) => {
|
const getOptionLabel = (options: { value: string; label: string }[], value: string) => {
|
||||||
return options.find((option) => option.value === value)?.label ?? value
|
return options.find((option) => option.value === value)?.label ?? value
|
||||||
}
|
}
|
||||||
@@ -1119,6 +1133,7 @@ function SettingsPage() {
|
|||||||
const exportFormatLabel = getOptionLabel(exportFormatOptions, exportDefaultFormat)
|
const exportFormatLabel = getOptionLabel(exportFormatOptions, exportDefaultFormat)
|
||||||
const exportDateRangeLabel = getOptionLabel(exportDateRangeOptions, exportDefaultDateRange)
|
const exportDateRangeLabel = getOptionLabel(exportDateRangeOptions, exportDefaultDateRange)
|
||||||
const exportExcelColumnsLabel = getOptionLabel(exportExcelColumnOptions, exportExcelColumnsValue)
|
const exportExcelColumnsLabel = getOptionLabel(exportExcelColumnOptions, exportExcelColumnsValue)
|
||||||
|
const exportConcurrencyLabel = String(exportDefaultConcurrency)
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="tab-content">
|
<div className="tab-content">
|
||||||
@@ -1133,6 +1148,7 @@ function SettingsPage() {
|
|||||||
setShowExportFormatSelect(!showExportFormatSelect)
|
setShowExportFormatSelect(!showExportFormatSelect)
|
||||||
setShowExportDateRangeSelect(false)
|
setShowExportDateRangeSelect(false)
|
||||||
setShowExportExcelColumnsSelect(false)
|
setShowExportExcelColumnsSelect(false)
|
||||||
|
setShowExportConcurrencySelect(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="select-value">{exportFormatLabel}</span>
|
<span className="select-value">{exportFormatLabel}</span>
|
||||||
@@ -1172,6 +1188,7 @@ function SettingsPage() {
|
|||||||
setShowExportDateRangeSelect(!showExportDateRangeSelect)
|
setShowExportDateRangeSelect(!showExportDateRangeSelect)
|
||||||
setShowExportFormatSelect(false)
|
setShowExportFormatSelect(false)
|
||||||
setShowExportExcelColumnsSelect(false)
|
setShowExportExcelColumnsSelect(false)
|
||||||
|
setShowExportConcurrencySelect(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="select-value">{exportDateRangeLabel}</span>
|
<span className="select-value">{exportDateRangeLabel}</span>
|
||||||
@@ -1256,6 +1273,7 @@ function SettingsPage() {
|
|||||||
setShowExportExcelColumnsSelect(!showExportExcelColumnsSelect)
|
setShowExportExcelColumnsSelect(!showExportExcelColumnsSelect)
|
||||||
setShowExportFormatSelect(false)
|
setShowExportFormatSelect(false)
|
||||||
setShowExportDateRangeSelect(false)
|
setShowExportDateRangeSelect(false)
|
||||||
|
setShowExportConcurrencySelect(false)
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<span className="select-value">{exportExcelColumnsLabel}</span>
|
<span className="select-value">{exportExcelColumnsLabel}</span>
|
||||||
@@ -1285,6 +1303,45 @@ function SettingsPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="form-group">
|
||||||
|
<label>导出并发数</label>
|
||||||
|
<span className="form-hint">导出多个会话时的最大并发(1~6)</span>
|
||||||
|
<div className="select-field" ref={exportConcurrencyDropdownRef}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className={`select-trigger ${showExportConcurrencySelect ? 'open' : ''}`}
|
||||||
|
onClick={() => {
|
||||||
|
setShowExportConcurrencySelect(!showExportConcurrencySelect)
|
||||||
|
setShowExportFormatSelect(false)
|
||||||
|
setShowExportDateRangeSelect(false)
|
||||||
|
setShowExportExcelColumnsSelect(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="select-value">{exportConcurrencyLabel}</span>
|
||||||
|
<ChevronDown size={16} />
|
||||||
|
</button>
|
||||||
|
{showExportConcurrencySelect && (
|
||||||
|
<div className="select-dropdown">
|
||||||
|
{exportConcurrencyOptions.map((option) => (
|
||||||
|
<button
|
||||||
|
key={option.value}
|
||||||
|
type="button"
|
||||||
|
className={`select-option ${exportDefaultConcurrency === option.value ? 'active' : ''}`}
|
||||||
|
onClick={async () => {
|
||||||
|
setExportDefaultConcurrency(option.value)
|
||||||
|
await configService.setExportDefaultConcurrency(option.value)
|
||||||
|
showMessage(`已将导出并发数设为 ${option.value}`, true)
|
||||||
|
setShowExportConcurrencySelect(false)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<span className="option-label">{option.label}</span>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user