This commit is contained in:
1 parent
aac030912b
commit
d733c21145
4 files changed
+137
-46
No files matched your search
@@ -374,7 +374,10 @@ function ExportPage() {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{isAutomationModalOpen && (
|
{isAutomationModalOpen && (
|
||||||
<AutomationModal onClose={() => setIsAutomationModalOpen(false)} />
|
<AutomationModal
|
||||||
|
onClose={() => setIsAutomationModalOpen(false)}
|
||||||
|
onRunNow={enqueueAutomationTask}
|
||||||
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{draftAutomationPayload && (
|
{draftAutomationPayload && (
|
||||||
|
|||||||
@@ -9,20 +9,38 @@ import './Automation.scss'
|
|||||||
|
|
||||||
interface AutomationModalProps {
|
interface AutomationModalProps {
|
||||||
onClose: () => void
|
onClose: () => void
|
||||||
|
onRunNow: (task: ExportAutomationTask) => Promise<{ queued: boolean; reason?: string }>
|
||||||
}
|
}
|
||||||
|
|
||||||
export const AutomationModal: React.FC<AutomationModalProps> = ({ onClose }) => {
|
export const AutomationModal: React.FC<AutomationModalProps> = ({ onClose, onRunNow }) => {
|
||||||
const { tasks, updateTask, deleteTask } = useAutomationStore()
|
const { tasks, updateTask, deleteTask } = useAutomationStore()
|
||||||
const [editingTask, setEditingTask] = useState<ExportAutomationTask | null>(null)
|
const [editingTask, setEditingTask] = useState<ExportAutomationTask | null>(null)
|
||||||
|
const [triggeringTaskIds, setTriggeringTaskIds] = useState<Set<string>>(new Set())
|
||||||
|
|
||||||
const handleToggleEnable = (task: ExportAutomationTask, enabled: boolean) => {
|
const handleToggleEnable = (task: ExportAutomationTask, enabled: boolean) => {
|
||||||
void updateTask(task.id, prev => ({ ...prev, enabled, updatedAt: Date.now() }))
|
void updateTask(task.id, prev => ({ ...prev, enabled, updatedAt: Date.now() }))
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleRunNow = (_task: ExportAutomationTask) => {
|
const handleRunNow = async (task: ExportAutomationTask) => {
|
||||||
// Manual trigger is handled by the automation runner's evaluateSchedules
|
if (triggeringTaskIds.has(task.id)) return
|
||||||
// For now, just show a hint
|
setTriggeringTaskIds(prev => new Set(prev).add(task.id))
|
||||||
alert('手动触发功能需要自动化运行器的支持,任务将在下次调度周期中执行。')
|
|
||||||
|
try {
|
||||||
|
const result = await onRunNow(task)
|
||||||
|
if (result.queued) {
|
||||||
|
alert('已手动触发自动化导出,任务已加入导出队列。')
|
||||||
|
} else {
|
||||||
|
alert(result.reason || '手动触发失败')
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
alert(error instanceof Error ? error.message : '手动触发失败')
|
||||||
|
} finally {
|
||||||
|
setTriggeringTaskIds(prev => {
|
||||||
|
const next = new Set(prev)
|
||||||
|
next.delete(task.id)
|
||||||
|
return next
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = (task: ExportAutomationTask) => {
|
const handleDelete = (task: ExportAutomationTask) => {
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import type { ExportAutomationTask } from '../../../types/exportAutomation'
|
|||||||
import type { ExportTaskPayload } from '../types'
|
import type { ExportTaskPayload } from '../types'
|
||||||
import {
|
import {
|
||||||
resolveAutomationDueScheduleKey,
|
resolveAutomationDueScheduleKey,
|
||||||
|
resolveAutomationDateRangeSelection,
|
||||||
} from '../utils/automation'
|
} from '../utils/automation'
|
||||||
|
|
||||||
// ─── Core Logic Helpers ─────────────────────────────────────
|
// ─── Core Logic Helpers ─────────────────────────────────────
|
||||||
@@ -48,6 +49,30 @@ const resolveAutomationHasNewMessages = async (task: ExportAutomationTask): Prom
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const buildAutomationExportOptions = (task: ExportAutomationTask): any => {
|
||||||
|
const dateRangeSelection = resolveAutomationDateRangeSelection(task.template.dateRangeConfig)
|
||||||
|
return {
|
||||||
|
...task.template.optionTemplate,
|
||||||
|
exportPathStyle: task.template.optionTemplate?.exportPathStyle || 'auto',
|
||||||
|
useAllTime: dateRangeSelection.useAllTime,
|
||||||
|
dateRange: dateRangeSelection.useAllTime ? null : dateRangeSelection.dateRange
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolveAutomationOutputDir = async (task: ExportAutomationTask): Promise<string> => {
|
||||||
|
const taskOutputDir = String(task.outputDir || '').trim()
|
||||||
|
if (taskOutputDir) return taskOutputDir
|
||||||
|
|
||||||
|
const configuredOutputDir = String(await configService.getExportPath() || '').trim()
|
||||||
|
if (configuredOutputDir) return configuredOutputDir
|
||||||
|
|
||||||
|
try {
|
||||||
|
return String(await window.electronAPI.app.getDownloadsPath() || '').trim()
|
||||||
|
} catch {
|
||||||
|
return ''
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ─── Zustand Store ──────────────────────────────────────────
|
// ─── Zustand Store ──────────────────────────────────────────
|
||||||
|
|
||||||
interface AutomationState {
|
interface AutomationState {
|
||||||
@@ -201,30 +226,13 @@ export function useAutomationRunner(startTask: (payload: ExportTaskPayload) => v
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Enqueue
|
// Enqueue
|
||||||
const outputDir = String(task.outputDir || '').trim() || await configService.getExportPath()
|
const outputDir = await resolveAutomationOutputDir(task)
|
||||||
if (!outputDir) {
|
if (!outputDir) {
|
||||||
markSkipped(task.id, '导出目录未设置', scheduleKey)
|
markSkipped(task.id, '导出目录未设置', scheduleKey)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportOptions: any = {
|
const exportOptions = buildAutomationExportOptions(task)
|
||||||
...task.template.optionTemplate,
|
|
||||||
exportPathStyle: task.template.optionTemplate?.exportPathStyle || 'auto',
|
|
||||||
dateRange: typeof task.template.dateRangeConfig === 'string'
|
|
||||||
? JSON.parse(task.template.dateRangeConfig)
|
|
||||||
: task.template.dateRangeConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
startTaskRef.current({
|
|
||||||
sessionIds: task.sessionIds,
|
|
||||||
sessionNames: task.sessionNames,
|
|
||||||
outputDir,
|
|
||||||
options: exportOptions,
|
|
||||||
scope: task.template.scope,
|
|
||||||
source: 'automation',
|
|
||||||
automationTaskId: task.id,
|
|
||||||
contentType: task.template.contentType
|
|
||||||
})
|
|
||||||
|
|
||||||
patchTask(task.id, (prev) => ({
|
patchTask(task.id, (prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
@@ -238,6 +246,17 @@ export function useAutomationRunner(startTask: (payload: ExportTaskPayload) => v
|
|||||||
lastScheduleKey: scheduleKey
|
lastScheduleKey: scheduleKey
|
||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
startTaskRef.current({
|
||||||
|
sessionIds: task.sessionIds,
|
||||||
|
sessionNames: task.sessionNames,
|
||||||
|
outputDir,
|
||||||
|
options: exportOptions,
|
||||||
|
scope: task.template.scope,
|
||||||
|
source: 'automation',
|
||||||
|
automationTaskId: task.id,
|
||||||
|
contentType: task.template.contentType
|
||||||
|
})
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Automation Schedule Evaluation Failed', error)
|
console.error('Automation Schedule Evaluation Failed', error)
|
||||||
@@ -272,31 +291,14 @@ export function useAutomationRunner(startTask: (payload: ExportTaskPayload) => v
|
|||||||
task: ExportAutomationTask,
|
task: ExportAutomationTask,
|
||||||
_options?: { scheduleKey?: string }
|
_options?: { scheduleKey?: string }
|
||||||
): Promise<{ queued: boolean; reason?: string }> => {
|
): Promise<{ queued: boolean; reason?: string }> => {
|
||||||
const outputDir = String(task.outputDir || '').trim() || await configService.getExportPath()
|
const outputDir = await resolveAutomationOutputDir(task)
|
||||||
if (!outputDir) {
|
if (!outputDir) {
|
||||||
return { queued: false, reason: '导出目录未设置' }
|
return { queued: false, reason: '导出目录未设置' }
|
||||||
}
|
}
|
||||||
|
|
||||||
const exportOptions: any = {
|
const exportOptions = buildAutomationExportOptions(task)
|
||||||
...task.template.optionTemplate,
|
|
||||||
exportPathStyle: task.template.optionTemplate?.exportPathStyle || 'auto',
|
|
||||||
dateRange: typeof task.template.dateRangeConfig === 'string'
|
|
||||||
? JSON.parse(task.template.dateRangeConfig)
|
|
||||||
: task.template.dateRangeConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
startTaskRef.current({
|
await updateTask(task.id, (prev) => ({
|
||||||
sessionIds: task.sessionIds,
|
|
||||||
sessionNames: task.sessionNames,
|
|
||||||
outputDir,
|
|
||||||
options: exportOptions,
|
|
||||||
scope: task.template.scope,
|
|
||||||
source: 'automation',
|
|
||||||
automationTaskId: task.id,
|
|
||||||
contentType: task.template.contentType
|
|
||||||
})
|
|
||||||
|
|
||||||
void updateTask(task.id, (prev) => ({
|
|
||||||
...prev,
|
...prev,
|
||||||
updatedAt: Date.now(),
|
updatedAt: Date.now(),
|
||||||
runState: {
|
runState: {
|
||||||
@@ -309,6 +311,17 @@ export function useAutomationRunner(startTask: (payload: ExportTaskPayload) => v
|
|||||||
}
|
}
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
startTaskRef.current({
|
||||||
|
sessionIds: task.sessionIds,
|
||||||
|
sessionNames: task.sessionNames,
|
||||||
|
outputDir,
|
||||||
|
options: exportOptions,
|
||||||
|
scope: task.template.scope,
|
||||||
|
source: 'automation',
|
||||||
|
automationTaskId: task.id,
|
||||||
|
contentType: task.template.contentType
|
||||||
|
})
|
||||||
|
|
||||||
return { queued: true }
|
return { queued: true }
|
||||||
}, [updateTask])
|
}, [updateTask])
|
||||||
|
|
||||||
|
|||||||
@@ -12,7 +12,9 @@ import { useExportTaskStore } from '../../../stores/exportTaskStore'
|
|||||||
import { buildProgressPayloadSignature } from '../utils/progress'
|
import { buildProgressPayloadSignature } from '../utils/progress'
|
||||||
import { resolvePerfStageByPhase, applyProgressToTaskPerformance } from '../utils/performance'
|
import { resolvePerfStageByPhase, applyProgressToTaskPerformance } from '../utils/performance'
|
||||||
import { emitExportSessionStatus, onExportSessionStatusRequest } from '../../../services/exportBridge'
|
import { emitExportSessionStatus, onExportSessionStatusRequest } from '../../../services/exportBridge'
|
||||||
|
import { useAutomationStore } from './useAutomation'
|
||||||
import type { ExportProgress } from '../types'
|
import type { ExportProgress } from '../types'
|
||||||
|
import type { ExportAutomationTask } from '../../../types/exportAutomation'
|
||||||
|
|
||||||
export interface ExportTasksResult {
|
export interface ExportTasksResult {
|
||||||
tasks: ExportTask[]
|
tasks: ExportTask[]
|
||||||
@@ -25,6 +27,14 @@ export interface ExportTasksResult {
|
|||||||
|
|
||||||
const generateTaskId = () => `task-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`
|
const generateTaskId = () => `task-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`
|
||||||
|
|
||||||
|
const updateAutomationRunState = (
|
||||||
|
automationTaskId: string | undefined,
|
||||||
|
updater: (prev: ExportAutomationTask) => ExportAutomationTask
|
||||||
|
) => {
|
||||||
|
if (!automationTaskId) return
|
||||||
|
void useAutomationStore.getState().updateTask(automationTaskId, updater)
|
||||||
|
}
|
||||||
|
|
||||||
export function useExportTasks(): ExportTasksResult {
|
export function useExportTasks(): ExportTasksResult {
|
||||||
const [tasks, setTasks] = useState<ExportTask[]>([])
|
const [tasks, setTasks] = useState<ExportTask[]>([])
|
||||||
|
|
||||||
@@ -82,6 +92,20 @@ export function useExportTasks(): ExportTasksResult {
|
|||||||
|
|
||||||
setTasks(prev => [newTask, ...prev])
|
setTasks(prev => [newTask, ...prev])
|
||||||
|
|
||||||
|
if (payload.source === 'automation') {
|
||||||
|
updateAutomationRunState(payload.automationTaskId, (prev) => ({
|
||||||
|
...prev,
|
||||||
|
updatedAt: Date.now(),
|
||||||
|
runState: {
|
||||||
|
...(prev.runState || {}),
|
||||||
|
lastRunStatus: 'running',
|
||||||
|
lastStartedAt: Date.now(),
|
||||||
|
lastSkipReason: undefined,
|
||||||
|
lastError: undefined
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
// Kick off via electron API
|
// Kick off via electron API
|
||||||
const run = async () => {
|
const run = async () => {
|
||||||
let progressUnsubscribe: (() => void) | null = null
|
let progressUnsubscribe: (() => void) | null = null
|
||||||
@@ -138,14 +162,47 @@ export function useExportTasks(): ExportTasksResult {
|
|||||||
sessionOutputPaths: result?.sessionOutputPaths
|
sessionOutputPaths: result?.sessionOutputPaths
|
||||||
}))
|
}))
|
||||||
|
|
||||||
|
if (payload.source === 'automation') {
|
||||||
|
const finishedAt = Date.now()
|
||||||
|
updateAutomationRunState(payload.automationTaskId, (prev) => {
|
||||||
|
const previousSuccessCount = Math.max(0, Math.floor(Number(prev.runState?.successCount || 0)))
|
||||||
|
return {
|
||||||
|
...prev,
|
||||||
|
updatedAt: finishedAt,
|
||||||
|
runState: {
|
||||||
|
...(prev.runState || {}),
|
||||||
|
lastRunStatus: result?.success ? 'success' : 'error',
|
||||||
|
lastFinishedAt: finishedAt,
|
||||||
|
lastSuccessAt: result?.success ? finishedAt : prev.runState?.lastSuccessAt,
|
||||||
|
lastError: result?.success ? undefined : (result?.error || '导出失败'),
|
||||||
|
successCount: result?.success ? previousSuccessCount + 1 : previousSuccessCount
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
} catch (err: any) {
|
} catch (err: any) {
|
||||||
console.error('[useExportTasks] Task failed:', err)
|
console.error('[useExportTasks] Task failed:', err)
|
||||||
|
const errorMessage = err.message || '未知错误'
|
||||||
updateTask(taskId, (task) => ({
|
updateTask(taskId, (task) => ({
|
||||||
...task,
|
...task,
|
||||||
status: 'error',
|
status: 'error',
|
||||||
finishedAt: Date.now(),
|
finishedAt: Date.now(),
|
||||||
error: err.message || '未知错误'
|
error: errorMessage
|
||||||
}))
|
}))
|
||||||
|
if (payload.source === 'automation') {
|
||||||
|
const finishedAt = Date.now()
|
||||||
|
updateAutomationRunState(payload.automationTaskId, (prev) => ({
|
||||||
|
...prev,
|
||||||
|
updatedAt: finishedAt,
|
||||||
|
runState: {
|
||||||
|
...(prev.runState || {}),
|
||||||
|
lastRunStatus: 'error',
|
||||||
|
lastFinishedAt: finishedAt,
|
||||||
|
lastError: errorMessage
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
if (progressUnsubscribe) {
|
if (progressUnsubscribe) {
|
||||||
progressUnsubscribe()
|
progressUnsubscribe()
|
||||||
|
|||||||
Reference in new issue
Block a user