diff --git a/index.js b/index.js index 4ca69b4..c68ebfd 100644 --- a/index.js +++ b/index.js @@ -44,6 +44,7 @@ const api = { startExport: (request) => electron.ipcRenderer.invoke("export:start", request), cancelExport: (jobId) => electron.ipcRenderer.invoke("export:cancel", jobId), revealExport: (path) => electron.ipcRenderer.invoke("export:reveal", path), + selectExportDirectory: () => electron.ipcRenderer.invoke("export:selectDirectory"), onExportProgress: (callback) => { const listener = (_event, progress) => callback(progress); electron.ipcRenderer.on("export:progress", listener); diff --git a/public/二维码.jpg b/public/二维码.jpg index f14f826..b2ecfec 100644 Binary files a/public/二维码.jpg and b/public/二维码.jpg differ diff --git a/src/main/export-service.ts b/src/main/export-service.ts index a7fb0dd..36d7693 100644 --- a/src/main/export-service.ts +++ b/src/main/export-service.ts @@ -928,7 +928,7 @@ async function runSingleExport( request.format === 'html' ? options.outputFolderName || safeFilePart(request.outputName) : `${safeFilePart(request.outputName)}_${exportStamp()}` - const root = options.outputRoot || (await resolveDefaultExportRoot(outputFolder)) + const root = options.outputRoot || request.outputDirectory || (await resolveDefaultExportRoot(outputFolder)) await fs.mkdir(root, { recursive: true }) const outputDir = join(root, outputFolder) const outputPath = @@ -1607,7 +1607,7 @@ async function runAllExport( } const outputFolder = safeFilePart(request.outputName) - const exportRoot = await resolveDefaultExportRoot(outputFolder) + const exportRoot = request.outputDirectory || (await resolveDefaultExportRoot(outputFolder)) outputDir = join(exportRoot, outputFolder) const folderNames = conversationFolderNames(targets) let lastProgressAt = 0 diff --git a/src/main/index.ts b/src/main/index.ts index 4fd2c11..c1a1e18 100644 --- a/src/main/index.ts +++ b/src/main/index.ts @@ -1164,6 +1164,11 @@ app.whenReady().then(async () => { if (!window) return { success: false, error: '窗口不可用' } return runExport(request, window, voiceRecognition || undefined) }) + ipcMain.handle('export:selectDirectory', async (event) => { + const window = BrowserWindow.fromWebContents(event.sender) + const result = await dialog.showOpenDialog(window!, { properties: ['openDirectory', 'createDirectory'] }) + return result.canceled ? { canceled: true } : { canceled: false, path: result.filePaths[0] } + }) ipcMain.handle('export:cancel', (_, jobId: string) => { cancelExport(jobId) return { success: true } diff --git a/src/main/wechat-db.ts b/src/main/wechat-db.ts index ec2f5ae..74e3b44 100644 --- a/src/main/wechat-db.ts +++ b/src/main/wechat-db.ts @@ -216,17 +216,11 @@ export class WechatDb { if (!username) return [] if (startTime && endTime && startTime > endTime) return [] - // A bounded native cursor can omit rows stored across a message shard boundary. - // Scan without bounds first, then apply the requested range in application code. - const rows = await this.wcdb4Client.getMessagesAsync(username) - const messages = rows - .map((message) => ({ ...message.raw, ...message })) - .filter((message) => { - const createTime = Number(message.msgCreateTime || 0) - if (startTime && createTime < startTime) return false - if (endTime && createTime > endTime) return false - return true - }) + // The async client merges cursor and shard-table results while applying the + // time range in SQLite. Avoid loading the entire conversation when exporting + // a narrow range; this is especially important for chats with long history. + const rows = await this.wcdb4Client.getMessagesAsync(username, startTime, endTime) + const messages = rows.map((message) => ({ ...message.raw, ...message })) console.log( `[WechatDb] export scan username=${username} raw=${rows.length} filtered=${messages.length} start=${startTime || 0} end=${endTime || 0}` ) diff --git a/src/preload/index.d.ts b/src/preload/index.d.ts index 04fb13f..9864fdd 100644 --- a/src/preload/index.d.ts +++ b/src/preload/index.d.ts @@ -316,6 +316,7 @@ declare global { httpStatus?: number }> startExport: (request: ExportRequest) => Promise + selectExportDirectory: () => Promise<{ canceled: boolean; path?: string }> cancelExport: (jobId: string) => Promise<{ success: boolean }> revealExport: (path: string) => Promise<{ success: boolean; error?: string }> onExportProgress: (callback: (progress: ExportJobProgress) => void) => () => void diff --git a/src/preload/index.ts b/src/preload/index.ts index f57362b..4f072fa 100644 --- a/src/preload/index.ts +++ b/src/preload/index.ts @@ -209,6 +209,7 @@ const api = { startExport: (request: ExportRequest) => ipcRenderer.invoke('export:start', request), cancelExport: (jobId: string) => ipcRenderer.invoke('export:cancel', jobId), revealExport: (path: string) => ipcRenderer.invoke('export:reveal', path), + selectExportDirectory: () => ipcRenderer.invoke('export:selectDirectory'), onExportProgress: (callback: (progress: ExportJobProgress) => void) => { const listener = (_event: Electron.IpcRendererEvent, progress: ExportJobProgress): void => callback(progress) diff --git a/src/renderer/src/components/export/ExportWorkspace.tsx b/src/renderer/src/components/export/ExportWorkspace.tsx index b13e962..b486434 100644 --- a/src/renderer/src/components/export/ExportWorkspace.tsx +++ b/src/renderer/src/components/export/ExportWorkspace.tsx @@ -75,6 +75,7 @@ export function ExportWorkspace({ const [format, setFormat] = useState(() => runningAllTask?.format || 'csv') const [zip, setZip] = useState(() => runningAllTask?.zip === true) const [fileName, setFileName] = useState('') + const [outputDirectory, setOutputDirectory] = useState('') const [status, setStatus] = useState('idle') const [jobId, setJobId] = useState('') const [progress, setProgress] = useState(null) @@ -334,6 +335,7 @@ export function ExportWorkspace({ targets, format: exportFormat, outputName, + outputDirectory: outputDirectory || undefined, startTime: exportAll ? undefined : startOfRange @@ -439,6 +441,10 @@ export function ExportWorkspace({ : `文稿/TraceMemo/导出/${outputName}/` : `文稿/TraceMemo/导出/${outputName}.${format === 'markdown' ? 'md' : format}` + const selectedTargetPath = outputDirectory + ? `${outputDirectory}/${outputName}${format === 'html' ? (zip ? '.zip' : '/') : `.${format === 'markdown' ? 'md' : format}`}` + : targetPath + return (
保存位置 - {targetPath} - + {selectedTargetPath} +
{format === 'html' && (

@@ -776,7 +786,7 @@ export function ExportWorkspace({ ? '导出完成' : '准备就绪'} - 路径:{targetPath} + 路径:{selectedTargetPath} diff --git a/src/shared/export.ts b/src/shared/export.ts index 4cac2e9..0fe2bf3 100644 --- a/src/shared/export.ts +++ b/src/shared/export.ts @@ -32,6 +32,7 @@ export interface ExportRequest { targets: ExportTarget[] format: ExportFormat outputName: string + outputDirectory?: string startTime?: number endTime?: number kinds: ExportMessageKind[]