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),
|
startExport: (request) => electron.ipcRenderer.invoke("export:start", request),
|
||||||
cancelExport: (jobId) => electron.ipcRenderer.invoke("export:cancel", jobId),
|
cancelExport: (jobId) => electron.ipcRenderer.invoke("export:cancel", jobId),
|
||||||
revealExport: (path) => electron.ipcRenderer.invoke("export:reveal", path),
|
revealExport: (path) => electron.ipcRenderer.invoke("export:reveal", path),
|
||||||
|
selectExportDirectory: () => electron.ipcRenderer.invoke("export:selectDirectory"),
|
||||||
onExportProgress: (callback) => {
|
onExportProgress: (callback) => {
|
||||||
const listener = (_event, progress) => callback(progress);
|
const listener = (_event, progress) => callback(progress);
|
||||||
electron.ipcRenderer.on("export:progress", listener);
|
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'
|
request.format === 'html'
|
||||||
? options.outputFolderName || safeFilePart(request.outputName)
|
? options.outputFolderName || safeFilePart(request.outputName)
|
||||||
: `${safeFilePart(request.outputName)}_${exportStamp()}`
|
: `${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 })
|
await fs.mkdir(root, { recursive: true })
|
||||||
const outputDir = join(root, outputFolder)
|
const outputDir = join(root, outputFolder)
|
||||||
const outputPath =
|
const outputPath =
|
||||||
@@ -1607,7 +1607,7 @@ async function runAllExport(
|
|||||||
}
|
}
|
||||||
|
|
||||||
const outputFolder = safeFilePart(request.outputName)
|
const outputFolder = safeFilePart(request.outputName)
|
||||||
const exportRoot = await resolveDefaultExportRoot(outputFolder)
|
const exportRoot = request.outputDirectory || (await resolveDefaultExportRoot(outputFolder))
|
||||||
outputDir = join(exportRoot, outputFolder)
|
outputDir = join(exportRoot, outputFolder)
|
||||||
const folderNames = conversationFolderNames(targets)
|
const folderNames = conversationFolderNames(targets)
|
||||||
let lastProgressAt = 0
|
let lastProgressAt = 0
|
||||||
|
|||||||
@@ -1164,6 +1164,11 @@ app.whenReady().then(async () => {
|
|||||||
if (!window) return { success: false, error: '窗口不可用' }
|
if (!window) return { success: false, error: '窗口不可用' }
|
||||||
return runExport(request, window, voiceRecognition || undefined)
|
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) => {
|
ipcMain.handle('export:cancel', (_, jobId: string) => {
|
||||||
cancelExport(jobId)
|
cancelExport(jobId)
|
||||||
return { success: true }
|
return { success: true }
|
||||||
|
|||||||
+5
-11
@@ -216,17 +216,11 @@ export class WechatDb {
|
|||||||
if (!username) return []
|
if (!username) return []
|
||||||
if (startTime && endTime && startTime > endTime) return []
|
if (startTime && endTime && startTime > endTime) return []
|
||||||
|
|
||||||
// A bounded native cursor can omit rows stored across a message shard boundary.
|
// The async client merges cursor and shard-table results while applying the
|
||||||
// Scan without bounds first, then apply the requested range in application code.
|
// time range in SQLite. Avoid loading the entire conversation when exporting
|
||||||
const rows = await this.wcdb4Client.getMessagesAsync(username)
|
// a narrow range; this is especially important for chats with long history.
|
||||||
const messages = rows
|
const rows = await this.wcdb4Client.getMessagesAsync(username, startTime, endTime)
|
||||||
.map((message) => ({ ...message.raw, ...message }))
|
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
|
|
||||||
})
|
|
||||||
console.log(
|
console.log(
|
||||||
`[WechatDb] export scan username=${username} raw=${rows.length} filtered=${messages.length} start=${startTime || 0} end=${endTime || 0}`
|
`[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
|
httpStatus?: number
|
||||||
}>
|
}>
|
||||||
startExport: (request: ExportRequest) => Promise<ExportResult>
|
startExport: (request: ExportRequest) => Promise<ExportResult>
|
||||||
|
selectExportDirectory: () => Promise<{ canceled: boolean; path?: string }>
|
||||||
cancelExport: (jobId: string) => Promise<{ success: boolean }>
|
cancelExport: (jobId: string) => Promise<{ success: boolean }>
|
||||||
revealExport: (path: string) => Promise<{ success: boolean; error?: string }>
|
revealExport: (path: string) => Promise<{ success: boolean; error?: string }>
|
||||||
onExportProgress: (callback: (progress: ExportJobProgress) => void) => () => void
|
onExportProgress: (callback: (progress: ExportJobProgress) => void) => () => void
|
||||||
|
|||||||
@@ -209,6 +209,7 @@ const api = {
|
|||||||
startExport: (request: ExportRequest) => ipcRenderer.invoke('export:start', request),
|
startExport: (request: ExportRequest) => ipcRenderer.invoke('export:start', request),
|
||||||
cancelExport: (jobId: string) => ipcRenderer.invoke('export:cancel', jobId),
|
cancelExport: (jobId: string) => ipcRenderer.invoke('export:cancel', jobId),
|
||||||
revealExport: (path: string) => ipcRenderer.invoke('export:reveal', path),
|
revealExport: (path: string) => ipcRenderer.invoke('export:reveal', path),
|
||||||
|
selectExportDirectory: () => ipcRenderer.invoke('export:selectDirectory'),
|
||||||
onExportProgress: (callback: (progress: ExportJobProgress) => void) => {
|
onExportProgress: (callback: (progress: ExportJobProgress) => void) => {
|
||||||
const listener = (_event: Electron.IpcRendererEvent, progress: ExportJobProgress): void =>
|
const listener = (_event: Electron.IpcRendererEvent, progress: ExportJobProgress): void =>
|
||||||
callback(progress)
|
callback(progress)
|
||||||
|
|||||||
@@ -75,6 +75,7 @@ export function ExportWorkspace({
|
|||||||
const [format, setFormat] = useState<ExportFormat>(() => runningAllTask?.format || 'csv')
|
const [format, setFormat] = useState<ExportFormat>(() => runningAllTask?.format || 'csv')
|
||||||
const [zip, setZip] = useState(() => runningAllTask?.zip === true)
|
const [zip, setZip] = useState(() => runningAllTask?.zip === true)
|
||||||
const [fileName, setFileName] = useState('')
|
const [fileName, setFileName] = useState('')
|
||||||
|
const [outputDirectory, setOutputDirectory] = useState('')
|
||||||
const [status, setStatus] = useState<ExportStatus>('idle')
|
const [status, setStatus] = useState<ExportStatus>('idle')
|
||||||
const [jobId, setJobId] = useState('')
|
const [jobId, setJobId] = useState('')
|
||||||
const [progress, setProgress] = useState<ExportJobProgress | null>(null)
|
const [progress, setProgress] = useState<ExportJobProgress | null>(null)
|
||||||
@@ -334,6 +335,7 @@ export function ExportWorkspace({
|
|||||||
targets,
|
targets,
|
||||||
format: exportFormat,
|
format: exportFormat,
|
||||||
outputName,
|
outputName,
|
||||||
|
outputDirectory: outputDirectory || undefined,
|
||||||
startTime: exportAll
|
startTime: exportAll
|
||||||
? undefined
|
? undefined
|
||||||
: startOfRange
|
: startOfRange
|
||||||
@@ -439,6 +441,10 @@ export function ExportWorkspace({
|
|||||||
: `文稿/TraceMemo/导出/${outputName}/`
|
: `文稿/TraceMemo/导出/${outputName}/`
|
||||||
: `文稿/TraceMemo/导出/${outputName}.${format === 'markdown' ? 'md' : format}`
|
: `文稿/TraceMemo/导出/${outputName}.${format === 'markdown' ? 'md' : format}`
|
||||||
|
|
||||||
|
const selectedTargetPath = outputDirectory
|
||||||
|
? `${outputDirectory}/${outputName}${format === 'html' ? (zip ? '.zip' : '/') : `.${format === 'markdown' ? 'md' : format}`}`
|
||||||
|
: targetPath
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="export-workspace">
|
<div className="export-workspace">
|
||||||
<ExportContactPanel
|
<ExportContactPanel
|
||||||
@@ -757,8 +763,12 @@ export function ExportWorkspace({
|
|||||||
</label>
|
</label>
|
||||||
<div className="export-target-path">
|
<div className="export-target-path">
|
||||||
<span>保存位置</span>
|
<span>保存位置</span>
|
||||||
<strong>{targetPath}</strong>
|
<strong>{selectedTargetPath}</strong>
|
||||||
<button type="button">选择位置</button>
|
<button type="button" onClick={() => {
|
||||||
|
void window.api.selectExportDirectory().then((result) => {
|
||||||
|
if (!result.canceled && result.path) setOutputDirectory(result.path)
|
||||||
|
})
|
||||||
|
}}>选择位置</button>
|
||||||
</div>
|
</div>
|
||||||
{format === 'html' && (
|
{format === 'html' && (
|
||||||
<p className="export-helper-text">
|
<p className="export-helper-text">
|
||||||
@@ -776,7 +786,7 @@ export function ExportWorkspace({
|
|||||||
? '导出完成'
|
? '导出完成'
|
||||||
: '准备就绪'}
|
: '准备就绪'}
|
||||||
</span>
|
</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 type="button" className="export-reset-button" onClick={resetDefaults}>
|
||||||
恢复默认
|
恢复默认
|
||||||
</button>
|
</button>
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ export interface ExportRequest {
|
|||||||
targets: ExportTarget[]
|
targets: ExportTarget[]
|
||||||
format: ExportFormat
|
format: ExportFormat
|
||||||
outputName: string
|
outputName: string
|
||||||
|
outputDirectory?: string
|
||||||
startTime?: number
|
startTime?: number
|
||||||
endTime?: number
|
endTime?: number
|
||||||
kinds: ExportMessageKind[]
|
kinds: ExportMessageKind[]
|
||||||
|
|||||||
Reference in New Issue
Block a user