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 && ( {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) => {
+52 -39
View File
@@ -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])
+58 -1
View File
@@ -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()