diff --git a/electron/services/config.ts b/electron/services/config.ts index 3ba3a14..2a496d6 100644 --- a/electron/services/config.ts +++ b/electron/services/config.ts @@ -26,6 +26,7 @@ interface ConfigSchema { whisperDownloadSource: string autoTranscribeVoice: boolean transcribeLanguages: string[] + exportDefaultConcurrency: number } export class ConfigService { @@ -54,7 +55,8 @@ export class ConfigService { whisperModelDir: '', whisperDownloadSource: 'tsinghua', autoTranscribeVoice: false, - transcribeLanguages: ['zh'] + transcribeLanguages: ['zh'], + exportDefaultConcurrency: 2 } }) } diff --git a/electron/services/exportService.ts b/electron/services/exportService.ts index 5e22b79..6df6819 100644 --- a/electron/services/exportService.ts +++ b/electron/services/exportService.ts @@ -78,6 +78,7 @@ export interface ExportOptions { txtColumns?: string[] sessionLayout?: 'shared' | 'per-session' displayNamePreference?: 'group-nickname' | 'remark' | 'nickname' + exportConcurrency?: number } const TXT_COLUMN_DEFINITIONS: Array<{ id: string; label: string }> = [ @@ -1288,6 +1289,7 @@ class ExportService { ): Promise<{ rows: any[]; memberSet: Map; firstTime: number | null; lastTime: number | null }> { const rows: any[] = [] const memberSet = new Map() + const senderSet = new Set() let firstTime: number | null = null let lastTime: number | null = null @@ -1321,16 +1323,7 @@ class ExportService { const localId = parseInt(row.local_id || row.localId || '0', 10) const actualSender = isSend ? cleanedMyWxid : (senderUsername || sessionId) - const memberInfo = await this.getContactInfo(actualSender) - if (!memberSet.has(actualSender)) { - memberSet.set(actualSender, { - member: { - platformId: actualSender, - accountName: memberInfo.displayName - }, - avatarUrl: memberInfo.avatarUrl - }) - } + senderSet.add(actualSender) // 提取媒体相关字段 let imageMd5: string | undefined @@ -1375,6 +1368,30 @@ class ExportService { await wcdbService.closeMessageCursor(cursor.cursor) } + if (senderSet.size > 0) { + const usernames = Array.from(senderSet) + const [nameResult, avatarResult] = await Promise.all([ + wcdbService.getDisplayNames(usernames), + wcdbService.getAvatarUrls(usernames) + ]) + + const nameMap = nameResult.success && nameResult.map ? nameResult.map : {} + const avatarMap = avatarResult.success && avatarResult.map ? avatarResult.map : {} + + for (const username of usernames) { + const displayName = nameMap[username] || username + const avatarUrl = avatarMap[username] + memberSet.set(username, { + member: { + platformId: username, + accountName: displayName + }, + avatarUrl + }) + this.contactCache.set(username, { displayName, avatarUrl }) + } + } + return { rows, memberSet, firstTime, lastTime } } @@ -1856,6 +1873,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, @@ -1934,6 +1961,16 @@ class ExportService { ? await this.getGroupNicknamesForRoom(sessionId) : new Map() + 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 + } + // ========== 阶段3:构建消息列表 ========== onProgress?.({ current: 55, @@ -1962,7 +1999,7 @@ class ExportService { // 获取发送者信息用于名称显示 const senderWxid = msg.senderUsername - const contact = await wcdbService.getContact(senderWxid) + const contact = await getContactCached(senderWxid) const senderNickname = contact.success && contact.contact?.nickName ? contact.contact.nickName : (senderInfo.displayName || senderWxid) @@ -2005,7 +2042,7 @@ class ExportService { const { chatlab, meta } = this.getExportMeta(sessionId, sessionInfo, isGroup) // 获取会话的昵称和备注信息 - const sessionContact = await wcdbService.getContact(sessionId) + const sessionContact = await getContactCached(sessionId) const sessionNickname = sessionContact.success && sessionContact.contact?.nickName ? sessionContact.contact.nickName : sessionInfo.displayName @@ -2098,8 +2135,18 @@ 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 + } + // 获取会话的备注信息 - const sessionContact = await wcdbService.getContact(sessionId) + const sessionContact = await getContactCached(sessionId) const sessionRemark = sessionContact.success && sessionContact.contact?.remark ? sessionContact.contact.remark : '' const sessionNickname = sessionContact.success && sessionContact.contact?.nickName ? sessionContact.contact.nickName : sessionId @@ -2328,7 +2375,7 @@ class ExportService { senderWxid = msg.senderUsername // 用 getContact 获取联系人详情,分别取昵称和备注 - const contactDetail = await wcdbService.getContact(msg.senderUsername) + const contactDetail = await getContactCached(msg.senderUsername) if (contactDetail.success && contactDetail.contact) { // nickName 才是真正的昵称 senderNickname = contactDetail.contact.nickName || msg.senderUsername @@ -2343,7 +2390,7 @@ class ExportService { } else { // 单聊对方消息 - 用 getContact 获取联系人详情 senderWxid = sessionId - const contactDetail = await wcdbService.getContact(sessionId) + const contactDetail = await getContactCached(sessionId) if (contactDetail.success && contactDetail.contact) { senderNickname = contactDetail.contact.nickName || sessionId senderRemark = contactDetail.contact.remark || '' @@ -2567,7 +2614,7 @@ class ExportService { senderNickname = myInfo.displayName || cleanedMyWxid } else if (isGroup && msg.senderUsername) { senderWxid = msg.senderUsername - const contactDetail = await wcdbService.getContact(msg.senderUsername) + const contactDetail = await getContactCached(msg.senderUsername) if (contactDetail.success && contactDetail.contact) { senderNickname = contactDetail.contact.nickName || msg.senderUsername senderRemark = contactDetail.contact.remark || '' @@ -2578,7 +2625,7 @@ class ExportService { } } else { senderWxid = sessionId - const contactDetail = await wcdbService.getContact(sessionId) + const contactDetail = await getContactCached(sessionId) if (contactDetail.success && contactDetail.contact) { senderNickname = contactDetail.contact.nickName || sessionId senderRemark = contactDetail.contact.remark || '' @@ -3005,13 +3052,20 @@ class ExportService { const sessionLayout = exportMediaEnabled ? (options.sessionLayout ?? 'per-session') : 'shared' + let completedCount = 0 + const rawConcurrency = typeof options.exportConcurrency === 'number' + ? Math.floor(options.exportConcurrency) + : 2 + const clampedConcurrency = Math.max(1, Math.min(rawConcurrency, 6)) + const sessionConcurrency = (exportMediaEnabled && sessionLayout === 'shared') + ? 1 + : clampedConcurrency - for (let i = 0; i < sessionIds.length; i++) { - const sessionId = sessionIds[i] + await parallelLimit(sessionIds, sessionConcurrency, async (sessionId) => { const sessionInfo = await this.getContactInfo(sessionId) onProgress?.({ - current: i + 1, + current: completedCount, total: sessionIds.length, currentSession: sessionInfo.displayName, phase: 'exporting' @@ -3053,7 +3107,15 @@ class ExportService { failCount++ console.error(`导出 ${sessionId} 失败:`, result.error) } - } + + completedCount++ + onProgress?.({ + current: completedCount, + total: sessionIds.length, + currentSession: sessionInfo.displayName, + phase: 'exporting' + }) + }) onProgress?.({ current: sessionIds.length, diff --git a/src/pages/ExportPage.tsx b/src/pages/ExportPage.tsx index d1f5bad..a8872cd 100644 --- a/src/pages/ExportPage.tsx +++ b/src/pages/ExportPage.tsx @@ -24,6 +24,7 @@ interface ExportOptions { excelCompactColumns: boolean txtColumns: string[] displayNamePreference: 'group-nickname' | 'remark' | 'nickname' + exportConcurrency: number } interface ExportResult { @@ -68,7 +69,8 @@ function ExportPage() { exportVoiceAsText: true, excelCompactColumns: true, txtColumns: defaultTxtColumns, - displayNamePreference: 'remark' + displayNamePreference: 'remark', + exportConcurrency: 2 }) const buildDateRangeFromPreset = (preset: string) => { @@ -133,14 +135,16 @@ function ExportPage() { savedMedia, savedVoiceAsText, savedExcelCompactColumns, - savedTxtColumns + savedTxtColumns, + savedConcurrency ] = await Promise.all([ configService.getExportDefaultFormat(), configService.getExportDefaultDateRange(), configService.getExportDefaultMedia(), configService.getExportDefaultVoiceAsText(), configService.getExportDefaultExcelCompactColumns(), - configService.getExportDefaultTxtColumns() + configService.getExportDefaultTxtColumns(), + configService.getExportDefaultConcurrency() ]) const preset = savedRange || 'today' @@ -155,7 +159,8 @@ function ExportPage() { exportMedia: savedMedia ?? false, exportVoiceAsText: savedVoiceAsText ?? true, excelCompactColumns: savedExcelCompactColumns ?? true, - txtColumns + txtColumns, + exportConcurrency: savedConcurrency ?? 2 })) } catch (e) { console.error('加载导出默认设置失败:', e) @@ -286,6 +291,7 @@ function ExportPage() { excelCompactColumns: options.excelCompactColumns, txtColumns: options.txtColumns, displayNamePreference: options.displayNamePreference, + exportConcurrency: options.exportConcurrency, sessionLayout, dateRange: options.useAllTime ? null : options.dateRange ? { start: Math.floor(options.dateRange.start.getTime() / 1000), diff --git a/src/pages/SettingsPage.tsx b/src/pages/SettingsPage.tsx index 8bd2d45..177a9fd 100644 --- a/src/pages/SettingsPage.tsx +++ b/src/pages/SettingsPage.tsx @@ -62,6 +62,7 @@ function SettingsPage() { const [exportDefaultMedia, setExportDefaultMedia] = useState(false) const [exportDefaultVoiceAsText, setExportDefaultVoiceAsText] = useState(true) const [exportDefaultExcelCompactColumns, setExportDefaultExcelCompactColumns] = useState(true) + const [exportDefaultConcurrency, setExportDefaultConcurrency] = useState(2) const [isLoading, setIsLoadingState] = useState(false) const [isTesting, setIsTesting] = useState(false) @@ -139,6 +140,7 @@ function SettingsPage() { const savedExportDefaultMedia = await configService.getExportDefaultMedia() const savedExportDefaultVoiceAsText = await configService.getExportDefaultVoiceAsText() const savedExportDefaultExcelCompactColumns = await configService.getExportDefaultExcelCompactColumns() + const savedExportDefaultConcurrency = await configService.getExportDefaultConcurrency() if (savedPath) setDbPath(savedPath) if (savedWxid) setWxid(savedWxid) @@ -166,6 +168,7 @@ function SettingsPage() { setExportDefaultMedia(savedExportDefaultMedia ?? false) setExportDefaultVoiceAsText(savedExportDefaultVoiceAsText ?? true) setExportDefaultExcelCompactColumns(savedExportDefaultExcelCompactColumns ?? true) + setExportDefaultConcurrency(savedExportDefaultConcurrency ?? 2) // 如果语言列表为空,保存默认值 if (!savedTranscribeLanguages || savedTranscribeLanguages.length === 0) { @@ -1113,6 +1116,32 @@ function SettingsPage() { + +
+ + ?????????????????? 1-3 + { + const value = Number(e.target.value) + if (Number.isNaN(value)) { + setExportDefaultConcurrency(1) + return + } + setExportDefaultConcurrency(value) + }} + onBlur={async () => { + const clamped = Math.max(1, Math.min(Math.floor(exportDefaultConcurrency || 1), 6)) + setExportDefaultConcurrency(clamped) + await configService.setExportDefaultConcurrency(clamped) + showMessage(`?????????????? ${clamped}`, true) + }} + /> +
+
控制 Excel 导出的列字段 diff --git a/src/services/config.ts b/src/services/config.ts index 063ff68..fe0153e 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -29,7 +29,8 @@ export const CONFIG_KEYS = { EXPORT_DEFAULT_MEDIA: 'exportDefaultMedia', EXPORT_DEFAULT_VOICE_AS_TEXT: 'exportDefaultVoiceAsText', EXPORT_DEFAULT_EXCEL_COMPACT_COLUMNS: 'exportDefaultExcelCompactColumns', - EXPORT_DEFAULT_TXT_COLUMNS: 'exportDefaultTxtColumns' + EXPORT_DEFAULT_TXT_COLUMNS: 'exportDefaultTxtColumns', + EXPORT_DEFAULT_CONCURRENCY: 'exportDefaultConcurrency' } as const export interface WxidConfig { @@ -352,3 +353,15 @@ export async function getExportDefaultTxtColumns(): Promise { export async function setExportDefaultTxtColumns(columns: string[]): Promise { await config.set(CONFIG_KEYS.EXPORT_DEFAULT_TXT_COLUMNS, columns) } + +// 获取导出默认并发数 +export async function getExportDefaultConcurrency(): Promise { + const value = await config.get(CONFIG_KEYS.EXPORT_DEFAULT_CONCURRENCY) + if (typeof value === 'number' && Number.isFinite(value)) return value + return null +} + +// 设置导出默认并发数 +export async function setExportDefaultConcurrency(concurrency: number): Promise { + await config.set(CONFIG_KEYS.EXPORT_DEFAULT_CONCURRENCY, concurrency) +} diff --git a/src/types/electron.d.ts b/src/types/electron.d.ts index 2622cce..3f48a8d 100644 --- a/src/types/electron.d.ts +++ b/src/types/electron.d.ts @@ -365,6 +365,7 @@ export interface ExportOptions { txtColumns?: string[] sessionLayout?: 'shared' | 'per-session' displayNamePreference?: 'group-nickname' | 'remark' | 'nickname' + exportConcurrency?: number } export interface ExportProgress {