mirror of
https://wget.la/https://github.com/Wxw-Gu/WechatExplorer
synced 2026-08-17 03:27:00 +08:00
feat: 支持导出自定义目录并优化聊天记录读取
接入导出保存目录选择功能 修复导出路径摘要未同步更新 按时间范围查询聊天记录,避免全量扫描历史消息 增加跨分片消息查询支持验证
This commit is contained in:
@@ -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);
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 158 KiB After Width: | Height: | Size: 158 KiB |
@@ -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
|
||||
|
||||
@@ -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 }
|
||||
|
||||
+5
-11
@@ -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}`
|
||||
)
|
||||
|
||||
Vendored
+1
@@ -316,6 +316,7 @@ declare global {
|
||||
httpStatus?: number
|
||||
}>
|
||||
startExport: (request: ExportRequest) => Promise<ExportResult>
|
||||
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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -75,6 +75,7 @@ export function ExportWorkspace({
|
||||
const [format, setFormat] = useState<ExportFormat>(() => runningAllTask?.format || 'csv')
|
||||
const [zip, setZip] = useState(() => runningAllTask?.zip === true)
|
||||
const [fileName, setFileName] = useState('')
|
||||
const [outputDirectory, setOutputDirectory] = useState('')
|
||||
const [status, setStatus] = useState<ExportStatus>('idle')
|
||||
const [jobId, setJobId] = useState('')
|
||||
const [progress, setProgress] = useState<ExportJobProgress | null>(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 (
|
||||
<div className="export-workspace">
|
||||
<ExportContactPanel
|
||||
@@ -757,8 +763,12 @@ export function ExportWorkspace({
|
||||
</label>
|
||||
<div className="export-target-path">
|
||||
<span>保存位置</span>
|
||||
<strong>{targetPath}</strong>
|
||||
<button type="button">选择位置</button>
|
||||
<strong>{selectedTargetPath}</strong>
|
||||
<button type="button" onClick={() => {
|
||||
void window.api.selectExportDirectory().then((result) => {
|
||||
if (!result.canceled && result.path) setOutputDirectory(result.path)
|
||||
})
|
||||
}}>选择位置</button>
|
||||
</div>
|
||||
{format === 'html' && (
|
||||
<p className="export-helper-text">
|
||||
@@ -776,7 +786,7 @@ export function ExportWorkspace({
|
||||
? '导出完成'
|
||||
: '准备就绪'}
|
||||
</span>
|
||||
<span className="export-target-summary">路径:{targetPath}</span>
|
||||
<span className="export-target-summary">路径:{selectedTargetPath}</span>
|
||||
<button type="button" className="export-reset-button" onClick={resetDefaults}>
|
||||
恢复默认
|
||||
</button>
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface ExportRequest {
|
||||
targets: ExportTarget[]
|
||||
format: ExportFormat
|
||||
outputName: string
|
||||
outputDirectory?: string
|
||||
startTime?: number
|
||||
endTime?: number
|
||||
kinds: ExportMessageKind[]
|
||||
|
||||
Reference in New Issue
Block a user