This commit is contained in:
cc committed 2026-07-05 14:50:18 +08:00
1 parent aac030912b
commit d733c21145
4 files changed
+137 -46

No files matched your search

+4 -1
View File
@@ -374,7 +374,10 @@ function ExportPage() {
)}
{isAutomationModalOpen && (
<AutomationModal onClose={() => setIsAutomationModalOpen(false)} />
<AutomationModal
onClose={() => setIsAutomationModalOpen(false)}
onRunNow={enqueueAutomationTask}
/>
)}
{draftAutomationPayload && (
@@ -9,20 +9,38 @@ import './Automation.scss'
interface AutomationModalProps {
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 [editingTask, setEditingTask] = useState<ExportAutomationTask | null>(null)
const [triggeringTaskIds, setTriggeringTaskIds] = useState<Set<string>>(new Set())
const handleToggleEnable = (task: ExportAutomationTask, enabled: boolean) => {
void updateTask(task.id, prev => ({ ...prev, enabled, updatedAt: Date.now() }))
}
const handleRunNow = (_task: ExportAutomationTask) => {
// Manual trigger is handled by the automation runner's evaluateSchedules
// For now, just show a hint
alert('手动触发功能需要自动化运行器的支持,任务将在下次调度周期中执行。')
const handleRunNow = async (task: ExportAutomationTask) => {
if (triggeringTaskIds.has(task.id)) return
setTriggeringTaskIds(prev => new Set(prev).add(task.id))
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) => {
+52 -39
View File
@@ -5,6 +5,7 @@ import type { ExportAutomationTask } from '../../../types/exportAutomation'
import type { ExportTaskPayload } from '../types'
import {
resolveAutomationDueScheduleKey,
resolveAutomationDateRangeSelection,
} from '../utils/automation'
// ─── 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 ──────────────────────────────────────────
interface AutomationState {
@@ -201,30 +226,13 @@ export function useAutomationRunner(startTask: (payload: ExportTaskPayload) => v
}
// Enqueue
const outputDir = String(task.outputDir || '').trim() || await configService.getExportPath()
const outputDir = await resolveAutomationOutputDir(task)
if (!outputDir) {
markSkipped(task.id, '导出目录未设置', scheduleKey)
continue
}
const exportOptions: any = {
...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
})
const exportOptions = buildAutomationExportOptions(task)
patchTask(task.id, (prev) => ({
...prev,
@@ -238,6 +246,17 @@ export function useAutomationRunner(startTask: (payload: ExportTaskPayload) => v
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) {
console.error('Automation Schedule Evaluation Failed', error)
@@ -272,31 +291,14 @@ export function useAutomationRunner(startTask: (payload: ExportTaskPayload) => v
task: ExportAutomationTask,
_options?: { scheduleKey?: string }
): Promise<{ queued: boolean; reason?: string }> => {
const outputDir = String(task.outputDir || '').trim() || await configService.getExportPath()
const outputDir = await resolveAutomationOutputDir(task)
if (!outputDir) {
return { queued: false, reason: '导出目录未设置' }
}
const exportOptions: any = {
...task.template.optionTemplate,
exportPathStyle: task.template.optionTemplate?.exportPathStyle || 'auto',
dateRange: typeof task.template.dateRangeConfig === 'string'
? JSON.parse(task.template.dateRangeConfig)
: task.template.dateRangeConfig
}
const exportOptions = buildAutomationExportOptions(task)
startTaskRef.current({
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) => ({
await updateTask(task.id, (prev) => ({
...prev,
updatedAt: Date.now(),
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 }
}, [updateTask])
+58 -1
View File
@@ -12,7 +12,9 @@ import { useExportTaskStore } from '../../../stores/exportTaskStore'
import { buildProgressPayloadSignature } from '../utils/progress'
import { resolvePerfStageByPhase, applyProgressToTaskPerformance } from '../utils/performance'
import { emitExportSessionStatus, onExportSessionStatusRequest } from '../../../services/exportBridge'
import { useAutomationStore } from './useAutomation'
import type { ExportProgress } from '../types'
import type { ExportAutomationTask } from '../../../types/exportAutomation'
export interface ExportTasksResult {
tasks: ExportTask[]
@@ -25,6 +27,14 @@ export interface ExportTasksResult {
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 {
const [tasks, setTasks] = useState<ExportTask[]>([])
@@ -82,6 +92,20 @@ export function useExportTasks(): ExportTasksResult {
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
const run = async () => {
let progressUnsubscribe: (() => void) | null = null
@@ -138,14 +162,47 @@ export function useExportTasks(): ExportTasksResult {
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) {
console.error('[useExportTasks] Task failed:', err)
const errorMessage = err.message || '未知错误'
updateTask(taskId, (task) => ({
...task,
status: 'error',
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 {
if (progressUnsubscribe) {
progressUnsubscribe()