171 lines
5.6 KiB
TypeScript
171 lines
5.6 KiB
TypeScript
/**
|
|
* ExportV2 — useExportTasks hook
|
|
*
|
|
* Manages the queue and state of manual export tasks.
|
|
* Includes starting tasks via electronAPI, tracking progress, and aborting.
|
|
*/
|
|
|
|
import { useState, useCallback, useRef, useEffect } from 'react'
|
|
import type { ExportTask, ExportTaskPayload, TaskStatus } from '../types'
|
|
import { createEmptyProgress } from '../constants'
|
|
import { useExportTaskStore } from '../../../stores/exportTaskStore'
|
|
import { buildProgressPayloadSignature } from '../utils/progress'
|
|
import { resolvePerfStageByPhase, applyProgressToTaskPerformance } from '../utils/performance'
|
|
import type { ExportProgress } from '../types'
|
|
|
|
export interface ExportTasksResult {
|
|
tasks: ExportTask[]
|
|
activeTasks: ExportTask[]
|
|
completedTasks: ExportTask[]
|
|
startTask: (payload: ExportTaskPayload) => void
|
|
cancelTask: (taskId: string) => void
|
|
clearCompletedTasks: () => void
|
|
}
|
|
|
|
const generateTaskId = () => `task-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`
|
|
|
|
export function useExportTasks(): ExportTasksResult {
|
|
const [tasks, setTasks] = useState<ExportTask[]>([])
|
|
|
|
const tasksRef = useRef<ExportTask[]>([])
|
|
tasksRef.current = tasks
|
|
|
|
const { setSessionStatus } = useExportTaskStore()
|
|
|
|
// Track the ongoing tasks to update the global zustand store badge
|
|
useEffect(() => {
|
|
const activeTasks = tasksRef.current.filter(t => t.status === 'running' || t.status === 'cancel_requested')
|
|
const inProgressSessionIds = new Set<string>()
|
|
activeTasks.forEach(task => {
|
|
task.payload.sessionIds.forEach(id => inProgressSessionIds.add(id))
|
|
})
|
|
|
|
setSessionStatus({
|
|
activeTaskCount: activeTasks.length,
|
|
inProgressSessionIds: Array.from(inProgressSessionIds)
|
|
})
|
|
}, [tasks, setSessionStatus])
|
|
|
|
const updateTask = useCallback((taskId: string, updater: (prev: ExportTask) => ExportTask) => {
|
|
setTasks(prev => prev.map(t => t.id === taskId ? updater(t) : t))
|
|
}, [])
|
|
|
|
const startTask = useCallback((payload: ExportTaskPayload) => {
|
|
const taskId = generateTaskId()
|
|
const title = payload.sessionIds.length > 1
|
|
? `批量导出 ${payload.sessionIds.length} 个会话`
|
|
: `导出 ${payload.sessionNames[0] || '会话'}`
|
|
|
|
const newTask: ExportTask = {
|
|
id: taskId,
|
|
title,
|
|
status: 'running',
|
|
createdAt: Date.now(),
|
|
startedAt: Date.now(),
|
|
payload,
|
|
progress: createEmptyProgress()
|
|
}
|
|
|
|
setTasks(prev => [newTask, ...prev])
|
|
|
|
// Kick off via electron API
|
|
const run = async () => {
|
|
let progressUnsubscribe: (() => void) | null = null
|
|
let lastProgressSig = ''
|
|
|
|
try {
|
|
// Subscribe to progress
|
|
progressUnsubscribe = window.electronAPI.export.onProgress((progressPayload: ExportProgress) => {
|
|
if ((progressPayload as any).taskId && (progressPayload as any).taskId !== taskId) return
|
|
|
|
const sig = buildProgressPayloadSignature(progressPayload)
|
|
if (sig === lastProgressSig) return
|
|
lastProgressSig = sig
|
|
|
|
updateTask(taskId, (task) => {
|
|
if (task.status !== 'running') return task
|
|
|
|
const nextProgress = { ...task.progress, ...progressPayload }
|
|
// The original expected performance from task or undefined.
|
|
// The signature is applyProgressToTaskPerformance(task: ExportTask, payload: ExportProgress, now: number)
|
|
const nextPerf = applyProgressToTaskPerformance(task, progressPayload, Date.now())
|
|
|
|
return {
|
|
...task,
|
|
progress: nextProgress,
|
|
performance: nextPerf
|
|
}
|
|
})
|
|
})
|
|
|
|
const frontendOptions = payload.options as any
|
|
const electronOptions = { ...frontendOptions }
|
|
if (frontendOptions?.useAllTime) {
|
|
electronOptions.dateRange = null
|
|
} else if (frontendOptions?.dateRange) {
|
|
electronOptions.dateRange = {
|
|
start: frontendOptions.dateRange.start.getTime(),
|
|
end: frontendOptions.dateRange.end.getTime()
|
|
}
|
|
}
|
|
|
|
const result = await window.electronAPI.export.exportSessions(
|
|
payload.sessionIds,
|
|
payload.outputDir,
|
|
electronOptions,
|
|
{ taskId }
|
|
)
|
|
|
|
updateTask(taskId, (task) => ({
|
|
...task,
|
|
status: result?.success ? 'success' : 'error',
|
|
finishedAt: Date.now(),
|
|
error: result?.error || undefined,
|
|
sessionOutputPaths: result?.sessionOutputPaths
|
|
}))
|
|
|
|
} catch (err: any) {
|
|
console.error('[useExportTasks] Task failed:', err)
|
|
updateTask(taskId, (task) => ({
|
|
...task,
|
|
status: 'error',
|
|
finishedAt: Date.now(),
|
|
error: err.message || '未知错误'
|
|
}))
|
|
} finally {
|
|
if (progressUnsubscribe) {
|
|
progressUnsubscribe()
|
|
}
|
|
}
|
|
}
|
|
|
|
void run()
|
|
}, [updateTask])
|
|
|
|
const cancelTask = useCallback((taskId: string) => {
|
|
updateTask(taskId, (task) => {
|
|
if (task.status === 'running') {
|
|
window.electronAPI.export.cancelTask(taskId)
|
|
return { ...task, status: 'cancel_requested' }
|
|
}
|
|
return task
|
|
})
|
|
}, [updateTask])
|
|
|
|
const clearCompletedTasks = useCallback(() => {
|
|
setTasks(prev => prev.filter(t => t.status === 'running' || t.status === 'cancel_requested'))
|
|
}, [])
|
|
|
|
const activeTasks = tasks.filter(t => t.status === 'running' || t.status === 'cancel_requested')
|
|
const completedTasks = tasks.filter(t => t.status === 'success' || t.status === 'error')
|
|
|
|
return {
|
|
tasks,
|
|
activeTasks,
|
|
completedTasks,
|
|
startTask,
|
|
cancelTask,
|
|
clearCompletedTasks
|
|
}
|
|
}
|