diff --git a/frontend/app/features/schedules/RunHistory.tsx b/frontend/app/features/schedules/RunHistory.tsx index daae6d9..ce9d8c5 100644 --- a/frontend/app/features/schedules/RunHistory.tsx +++ b/frontend/app/features/schedules/RunHistory.tsx @@ -1,27 +1,223 @@ -import { type ScheduleRunSummary } from "../../services/api"; -import Icon from "../../components/common/Icon"; -import { formatTime, formatDuration } from "./utils"; +import { useEffect } from "react"; -const RUN_STATUS_LABELS: Record = { - queued: "排队中", - running: "运行中", - succeeded: "成功", - failed: "失败", - cancelled: "已取消", - timed_out: "已超时", -}; +import { + type ScheduleNode, + type ScheduleRunSummary, +} from "../../services/api"; + +import { useApi } from "../../context/AuthContext"; +import Icon from "../../components/common/Icon"; +import { useSchedulesStore } from "./state/schedulesStore"; +import { + formatDuration, + formatSize, + formatTime, + NODE_STATUS_LABELS, + RUN_STATUS_LABELS, + TRIGGER_TYPE_LABELS, +} from "./utils"; export function RunHistory({ runs, + nodes, loading, disabled, onRefresh, }: { runs: ScheduleRunSummary[]; + nodes: ScheduleNode[]; loading: boolean; disabled: boolean; onRefresh: () => void; }) { + const api = useApi(); + const expandedRunId = useSchedulesStore((s) => s.expandedRunId); + const runDetails = useSchedulesStore((s) => s.runDetails); + const detailLoadingId = useSchedulesStore((s) => s.detailLoadingId); + const detailErrors = useSchedulesStore((s) => s.detailErrors); + const openLogNodeRunIds = useSchedulesStore((s) => s.openLogNodeRunIds); + const logStates = useSchedulesStore((s) => s.logStates); + const artifactBusyKey = useSchedulesStore((s) => s.artifactBusyKey); + const artifactErrors = useSchedulesStore((s) => s.artifactErrors); + const setExpandedRunId = useSchedulesStore((s) => s.setExpandedRunId); + const setRunDetails = useSchedulesStore((s) => s.setRunDetails); + const setDetailLoadingId = useSchedulesStore((s) => s.setDetailLoadingId); + const setDetailErrors = useSchedulesStore((s) => s.setDetailErrors); + const setOpenLogNodeRunIds = useSchedulesStore((s) => s.setOpenLogNodeRunIds); + const setLogStates = useSchedulesStore((s) => s.setLogStates); + const setArtifactBusyKey = useSchedulesStore((s) => s.setArtifactBusyKey); + const setArtifactErrors = useSchedulesStore((s) => s.setArtifactErrors); + + const expandedSummary = runs.find((run) => run.run_id === expandedRunId); + + useEffect(() => { + if (expandedRunId && !expandedSummary) { + setExpandedRunId(null); + setOpenLogNodeRunIds(() => new Set()); + } + }, [expandedRunId, expandedSummary]); + + useEffect(() => { + const runId = expandedRunId; + if (!runId) return undefined; + let cancelled = false; + setDetailLoadingId(runId); + setDetailErrors((current) => { + const next = { ...current }; + delete next[runId]; + return next; + }); + api.getScheduleRun(runId) + .then((detail) => { + if (!cancelled) { + setRunDetails((current) => ({ ...current, [runId]: detail })); + } + }) + .catch((error: unknown) => { + if (!cancelled) { + setDetailErrors((current) => ({ + ...current, + [runId]: error instanceof Error ? error.message : "运行详情加载失败", + })); + } + }) + .finally(() => { + if (!cancelled) setDetailLoadingId(null); + }); + return () => { + cancelled = true; + }; + }, [api, expandedRunId, expandedSummary?.state_version]); + + const toggleRun = (runId: string): void => { + setExpandedRunId(expandedRunId === runId ? null : runId); + setOpenLogNodeRunIds(() => new Set()); + }; + + const toggleLog = async (runId: string, nodeRunId: string): Promise => { + if (openLogNodeRunIds.has(nodeRunId)) { + setOpenLogNodeRunIds((current) => { + const next = new Set(current); + next.delete(nodeRunId); + return next; + }); + return; + } + setOpenLogNodeRunIds((current) => new Set(current).add(nodeRunId)); + if (logStates[nodeRunId]?.content) return; + + setLogStates((current) => ({ + ...current, + [nodeRunId]: { + loading: true, + content: null, + fileName: null, + error: null, + }, + })); + try { + const artifacts = await api.getScheduleNodeRunArtifacts(runId, nodeRunId); + if (!artifacts.log) throw new Error("本次节点执行没有可用日志"); + const response = await fetch(artifacts.log.url, { + credentials: "same-origin", + cache: "no-store", + }); + if (!response.ok) { + throw new Error(`日志读取失败(HTTP ${response.status})`); + } + const content = await response.text(); + setLogStates((current) => ({ + ...current, + [nodeRunId]: { + loading: false, + content, + fileName: artifacts.log?.file_name ?? null, + error: null, + }, + })); + } catch (error) { + setLogStates((current) => ({ + ...current, + [nodeRunId]: { + loading: false, + content: null, + fileName: null, + error: error instanceof Error ? error.message : "日志读取失败", + }, + })); + } + }; + + const downloadResult = async ( + runId: string, + nodeRunId: string, + ): Promise => { + const busyKey = `${nodeRunId}:result`; + setArtifactBusyKey(busyKey); + setArtifactErrors((current) => { + const next = { ...current }; + delete next[nodeRunId]; + return next; + }); + try { + const artifacts = await api.getScheduleNodeRunArtifacts(runId, nodeRunId); + if (!artifacts.result) throw new Error("本次节点执行没有可下载结果"); + const link = document.createElement("a"); + link.href = artifacts.result.url; + link.download = artifacts.result.file_name; + document.body.appendChild(link); + link.click(); + link.remove(); + } catch (error) { + setArtifactErrors((current) => ({ + ...current, + [nodeRunId]: error instanceof Error ? error.message : "结果下载失败", + })); + } finally { + setArtifactBusyKey(null); + } + }; + + const downloadLog = async ( + runId: string, + nodeRunId: string, + ): Promise => { + const busyKey = `${nodeRunId}:log-download`; + setArtifactBusyKey(busyKey); + setArtifactErrors((current) => { + const next = { ...current }; + delete next[nodeRunId]; + return next; + }); + try { + const artifacts = await api.getScheduleNodeRunArtifacts(runId, nodeRunId); + if (!artifacts.log) throw new Error("本次节点执行没有可下载日志"); + const response = await fetch(artifacts.log.url, { + credentials: "same-origin", + cache: "no-store", + }); + if (!response.ok) { + throw new Error(`日志下载失败(HTTP ${response.status})`); + } + const objectUrl = URL.createObjectURL(await response.blob()); + const link = document.createElement("a"); + link.href = objectUrl; + link.download = artifacts.log.file_name; + document.body.appendChild(link); + link.click(); + link.remove(); + URL.revokeObjectURL(objectUrl); + } catch (error) { + setArtifactErrors((current) => ({ + ...current, + [nodeRunId]: + error instanceof Error ? error.message : "日志下载失败", + })); + } finally { + setArtifactBusyKey(null); + } + }; + return (
@@ -42,21 +238,176 @@ export function RunHistory({ {loading && runs.length === 0 ? (

正在加载运行记录…

) : runs.length === 0 ? ( -

点击右上角"立即运行"后,这里会显示状态和耗时。

+

点击右上角“立即运行”后,这里会显示状态和耗时。

) : ( - runs.map((run) => ( -
- -
- {RUN_STATUS_LABELS[run.run_status]} - - {formatTime(run.queued_at)} · {formatDuration(run.duration_ms)} - - {run.error_message &&

{run.error_message}

} -
- {run.run_id.slice(-8)} -
- )) + runs.map((run) => { + const expanded = expandedRunId === run.run_id; + const detail = runDetails[run.run_id]; + return ( +
+
+ + + {RUN_STATUS_LABELS[run.run_status]} + + {formatTime(run.queued_at)} · {formatDuration(run.duration_ms)} + + {run.error_message && {run.error_message}} + + {run.run_id.slice(-8)} + +
+ + {expanded && ( + <> + +
+
+ {detailLoadingId === run.run_id && !detail ? ( +

正在加载运行详情…

+ ) : detailErrors[run.run_id] ? ( +

+ {detailErrors[run.run_id]} +

+ ) : detail ? ( + <> +
+
触发方式
{TRIGGER_TYPE_LABELS[detail.trigger_type]}
+
工作流版本
v{detail.workflow_version}
+
开始时间
{formatTime(detail.started_at)}
+
完成时间
{formatTime(detail.finished_at)}
+
+ {(detail.error_code || detail.error_message) && ( +
+ {detail.error_code && {detail.error_code}} + {detail.error_message &&

{detail.error_message}

} +
+ )} +
+ {detail.node_runs.length === 0 ? ( +

节点尚未开始执行

+ ) : detail.node_runs.map((nodeRun) => { + const node = nodes.find((item) => item.node_id === nodeRun.node_id); + const logState = logStates[nodeRun.node_run_id]; + const logOpen = openLogNodeRunIds.has(nodeRun.node_run_id); + return ( +
+
+ + {node?.node_name ?? nodeRun.node_id.slice(-8)} + {NODE_STATUS_LABELS[nodeRun.node_status]} +
+
+ 第 {nodeRun.attempt_no} 次 + {formatDuration(nodeRun.duration_ms)} + 退出码 {nodeRun.exit_code ?? "—"} +
+ {nodeRun.message &&

{nodeRun.message}

} +
+ + + +
+ {artifactErrors[nodeRun.node_run_id] && ( +

+ {artifactErrors[nodeRun.node_run_id]} +

+ )} + {logOpen && ( +
+ {logState?.fileName && ( +
+ {logState.fileName} + + {formatSize(new TextEncoder().encode(logState.content ?? "").length)} + +
+ )} + {logState?.error ? ( +

{logState.error}

+ ) : ( +
{logState?.content ?? "正在读取日志…"}
+ )} +
+ )} +
+ ); + })} +
+ + ) : null} +
+
+ + )} + + ); + }) )} diff --git a/frontend/app/features/schedules/SchedulePage.tsx b/frontend/app/features/schedules/SchedulePage.tsx index c2ab6ee..70a211c 100644 --- a/frontend/app/features/schedules/SchedulePage.tsx +++ b/frontend/app/features/schedules/SchedulePage.tsx @@ -262,8 +262,7 @@ export default function SchedulePage({ const [runsLoading, setRunsLoading] = useState(false); const canvasRef = useRef(null); const dragRef = useRef(null); - const positionDraftsRef = useRef>({}); - const [positionDraftCount, setPositionDraftCount] = useState(0); + const positionDraftCount = useSchedulesStore((state) => state.positionDraftCount); const api = useApi(); const selectedNode = schedule?.nodes.find( @@ -299,12 +298,8 @@ export default function SchedulePage({ window.removeEventListener("scroll", close, true); window.removeEventListener("keydown", onKeyDown); }; - }, [contextMenu]); + }, [contextMenu]); - const clearPositionDrafts = (): void => { - positionDraftsRef.current = {}; - setPositionDraftCount(0); - }; const openContextMenu = ( event: ReactMouseEvent, @@ -321,16 +316,16 @@ export default function SchedulePage({ } as ScheduleContextMenu); }; - const applyPositionDrafts = (serverSchedule: Schedule): Schedule => { - const drafts = positionDraftsRef.current; - if (Object.keys(drafts).length === 0) return serverSchedule; - return { - ...serverSchedule, - nodes: serverSchedule.nodes.map((node) => { - const draft = drafts[node.node_id]; - return draft ? { ...node, ...draft } : node; - }), - }; + const applyPositionDrafts = (serverSchedule: Schedule): Schedule => { + const drafts = useSchedulesStore.getState().getPositionDrafts(); + if (drafts.size === 0) return serverSchedule; + return { + ...serverSchedule, + nodes: serverSchedule.nodes.map((node) => { + const draft = drafts.get(node.node_id); + return draft ? { ...node, ...draft } : node; + }), + }; }; const refreshRuns = async ( @@ -484,17 +479,12 @@ export default function SchedulePage({ if (busy) return null; setBusy(label); try { - const serverUpdated = await action(); - const validNodeIds = new Set( - serverUpdated.nodes.map((node) => node.node_id), - ); - positionDraftsRef.current = Object.fromEntries( - Object.entries(positionDraftsRef.current).filter(([nodeId]) => ( - validNodeIds.has(nodeId) - )), - ); - setPositionDraftCount(Object.keys(positionDraftsRef.current).length); - const updated = applyPositionDrafts(serverUpdated); + const serverUpdated = await action(); + const validNodeIds = new Set( + serverUpdated.nodes.map((node) => node.node_id), + ); + useSchedulesStore.getState().prunePositionDrafts(validNodeIds); + const updated = applyPositionDrafts(serverUpdated); setSchedule(updated); setSchedules((current) => { const summary = { ...updated, nodes: [], edges: [] }; @@ -518,9 +508,9 @@ export default function SchedulePage({ const chooseSchedule = async (scheduleId: string): Promise => { if (scheduleId === schedule?.schedule_id || busy) return; - setBusy("load-schedule"); - clearPositionDrafts(); - setSelectedNodeId(null); + setBusy("load-schedule"); + useSchedulesStore.getState().clearAllPositionDrafts(); + setSelectedNodeId(null); setSelectedEdgeId(null); setLinkSourceId(null); setCronResult(null); @@ -554,10 +544,10 @@ export default function SchedulePage({ timezone: "Asia/Shanghai", enabled: false, }); - setSchedules((current) => [created, ...current]); - setSchedule(created); - clearPositionDrafts(); - setSelectedNodeId(null); + setSchedules((current) => [created, ...current]); + setSchedule(created); + useSchedulesStore.getState().clearAllPositionDrafts(); + setSelectedNodeId(null); setCreateDialogOpen(false); setNewScheduleName(""); onNotify({ tone: "success", message: "调度方案已创建" }); @@ -583,10 +573,10 @@ export default function SchedulePage({ (item) => item.schedule_id !== selectedSchedule.schedule_id, ); setSchedules(remaining); - if (schedule?.schedule_id === selectedSchedule.schedule_id) { - setSchedule(null); - clearPositionDrafts(); - setSelectedNodeId(null); + if (schedule?.schedule_id === selectedSchedule.schedule_id) { + setSchedule(null); + useSchedulesStore.getState().clearAllPositionDrafts(); + setSelectedNodeId(null); setSelectedEdgeId(null); if (remaining[0]) { setSchedule(await api.getSchedule(remaining[0].schedule_id)); @@ -665,12 +655,11 @@ export default function SchedulePage({ return; } setBusy("save-schedule"); - let updated = useSchedulesStore.getState().schedule ?? schedule; - try { - for (const [nodeId, position] of Object.entries( - positionDraftsRef.current, - )) { - updated = await api.updateScheduleNode(updated.schedule_id, nodeId, { + let updated = useSchedulesStore.getState().schedule ?? schedule; + try { + const drafts = useSchedulesStore.getState().getPositionDrafts(); + for (const [nodeId, position] of drafts.entries()) { + updated = await api.updateScheduleNode(updated.schedule_id, nodeId, { workflow_version: updated.workflow_version, position_x: position.position_x, position_y: position.position_y, @@ -686,11 +675,11 @@ export default function SchedulePage({ : null, timezone: scheduleForm.timezone.trim(), enabled: scheduleForm.enabled, - max_concurrency: maxConcurrency, - failure_policy: scheduleForm.failurePolicy, - }); - clearPositionDrafts(); - setSchedule(updated); + max_concurrency: maxConcurrency, + failure_policy: scheduleForm.failurePolicy, + }); + useSchedulesStore.getState().clearAllPositionDrafts(); + setSchedule(updated); setSchedules((current) => current.map((item) => ( item.schedule_id === updated.schedule_id ? { ...updated, nodes: [], edges: [] } @@ -871,16 +860,12 @@ export default function SchedulePage({ const current = useSchedulesStore.getState().schedule; const node = current?.nodes.find((item) => item.node_id === drag.nodeId); if (!current || !node) return; - const position = { - position_x: Math.round(node.position_x), - position_y: Math.round(node.position_y), - }; - positionDraftsRef.current = { - ...positionDraftsRef.current, - [node.node_id]: position, - }; - setPositionDraftCount(Object.keys(positionDraftsRef.current).length); - setSchedule((value) => { + const position = { + position_x: Math.round(node.position_x), + position_y: Math.round(node.position_y), + }; + useSchedulesStore.getState().setPositionDraft(node.node_id, position); + setSchedule((value) => { if (!value) return value; return { ...value, diff --git a/frontend/app/features/schedules/state/schedulesStore.ts b/frontend/app/features/schedules/state/schedulesStore.ts index ead9ef0..c9870aa 100644 --- a/frontend/app/features/schedules/state/schedulesStore.ts +++ b/frontend/app/features/schedules/state/schedulesStore.ts @@ -47,14 +47,17 @@ export type NodePositionDraft = { position_y: number; }; -export type ScheduleContextMenu = { - x: number; - y: number; - kind: "schedule" | "node" | "edge"; - schedule_id: string; - node_id?: string; - edge_id?: string; -}; +export type ScheduleContextMenu = + | { x: number; y: number; kind: "schedule-list" } + | { x: number; y: number; kind: "schedule"; schedule: Schedule } + | { + x: number; + y: number; + kind: "artifact"; + artifact: ScheduleArtifact; + } + | { x: number; y: number; kind: "node"; node: ScheduleNode } + | { x: number; y: number; kind: "edge"; edge: ScheduleEdge }; export const EMPTY_SCHEDULE_FORM: ScheduleForm = { scheduleName: "", @@ -219,11 +222,18 @@ type Actions = { ) => void; setLoading: (loading: boolean) => void; setBusy: (busy: string | null) => void; - setRuns: (updater: (current: ScheduleRunSummary[]) => ScheduleRunSummary[]) => void; + setRuns: ( + value: ScheduleRunSummary[] + | ((current: ScheduleRunSummary[]) => ScheduleRunSummary[]), + ) => void; + setRunsLoading: (loading: boolean) => void; + setPositionDraftCount: (count: number) => void; setRunDetails: ( updater: (current: Record) => Record, ) => void; - setOpenLogNodeRunIds: (updater: (current: Set) => Set) => void; + setOpenLogNodeRunIds: ( + value: Set | ((current: Set) => Set), + ) => void; setLogStates: ( updater: (current: Record Promise; downloadResult: (runId: string, nodeRunId: string) => Promise; downloadLog: (runId: string, nodeRunId: string) => Promise; - setExpandedRunId: (id: string | null) => void; + setExpandedRunId: ( + id: string | null | ((current: string | null) => string | null), + ) => void; // drag helpers setPositionDraft: (nodeId: string, draft: NodePositionDraft) => void; getPositionDrafts: () => Map; + clearAllPositionDrafts: () => void; + prunePositionDrafts: (validNodeIds: Set) => void; }; const initial: State = { @@ -391,10 +405,10 @@ export const useSchedulesStore = create((set, get) => { bindApi: bindSchedulesApi, reset: () => { - clearPositionDrafts(); set({ ...initial, loading: true }); }, + // ---- pure setters ---- setSchedule: (value) => @@ -422,12 +436,27 @@ export const useSchedulesStore = create((set, get) => { })), setLoading: (loading) => set({ loading }), setBusy: (busy) => set({ busy }), - setRuns: (updater) => - set((state) => ({ runs: updater(state.runs) })), + setRuns: (value) => + set((state) => ({ + runs: + typeof value === "function" + ? (value as (current: ScheduleRunSummary[]) => ScheduleRunSummary[])( + state.runs, + ) + : value, + })), + setRunsLoading: (loading) => set({ runsLoading: loading }), setRunDetails: (updater) => set((state) => ({ runDetails: updater(state.runDetails) })), - setOpenLogNodeRunIds: (updater) => - set((state) => ({ openLogNodeRunIds: updater(state.openLogNodeRunIds) })), + setOpenLogNodeRunIds: (value) => + set((state) => ({ + openLogNodeRunIds: + typeof value === "function" + ? (value as (current: Set) => Set)( + state.openLogNodeRunIds, + ) + : value, + })), setLogStates: (updater) => set((state) => ({ logStates: updater(state.logStates) })), setArtifactBusyKey: (key) => set({ artifactBusyKey: key }), @@ -522,13 +551,14 @@ export const useSchedulesStore = create((set, get) => { const current = get().schedule; if (scheduleId === current?.schedule_id || get().busy) return; set({ busy: "load-schedule" }); - clearPositionDrafts(); + get().clearAllPositionDrafts(); set({ selectedNodeId: null, selectedEdgeId: null, linkSourceId: null, cronResult: null, - positionDraftCount: 0, + runs: [], + runsLoading: false, }); try { set({ schedule: await api.getSchedule(scheduleId) }); @@ -570,8 +600,8 @@ export const useSchedulesStore = create((set, get) => { createDialogOpen: false, newScheduleName: "", })); - clearPositionDrafts(); - set({ positionDraftCount: 0, selectedNodeId: null, selectedEdgeId: null }); + get().clearAllPositionDrafts(); + set({ selectedNodeId: null, selectedEdgeId: null }); notify({ tone: "success", message: "调度方案已创建" }); } catch (error) { await handleError(error, "创建调度失败"); @@ -594,12 +624,11 @@ export const useSchedulesStore = create((set, get) => { ); set({ schedules: remaining }); if (get().schedule?.schedule_id === target_.schedule_id) { - clearPositionDrafts(); + get().clearAllPositionDrafts(); set({ schedule: null, selectedNodeId: null, selectedEdgeId: null, - positionDraftCount: 0, }); if (remaining[0]) { const detail = await api.getSchedule(remaining[0].schedule_id); @@ -714,7 +743,7 @@ export const useSchedulesStore = create((set, get) => { max_concurrency: maxConcurrency, failure_policy: scheduleForm.failurePolicy, }); - clearPositionDrafts(); + get().clearAllPositionDrafts(); set((s) => ({ schedule: updated, schedules: s.schedules.map((item) => @@ -722,8 +751,8 @@ export const useSchedulesStore = create((set, get) => { ? { ...updated, nodes: [], edges: [] } : item ), - positionDraftCount: 0, })); + notify({ tone: "success", message: "调度配置已保存" }); } catch (error) { const localDraft = applyPositionDrafts(updated); @@ -994,7 +1023,15 @@ export const useSchedulesStore = create((set, get) => { set({ expandedRunId: runId }); } }, - setExpandedRunId: (id) => set({ expandedRunId: id }), + setExpandedRunId: (id) => + set((state) => ({ + expandedRunId: + typeof id === "function" + ? (id as (current: string | null) => string | null)( + state.expandedRunId, + ) + : id, + })), toggleLog: async (runId, nodeRunId) => { const api = requireApi(); @@ -1138,7 +1175,18 @@ export const useSchedulesStore = create((set, get) => { positionDrafts.set(nodeId, draft); set({ positionDraftCount: positionDrafts.size }); }, + setPositionDraftCount: (count) => set({ positionDraftCount: count }), getPositionDrafts: () => positionDrafts, + clearAllPositionDrafts: () => { + clearPositionDrafts(); + set({ positionDraftCount: 0 }); + }, + prunePositionDrafts: (validNodeIds) => { + for (const nodeId of [...positionDrafts.keys()]) { + if (!validNodeIds.has(nodeId)) positionDrafts.delete(nodeId); + } + set({ positionDraftCount: positionDrafts.size }); + }, }; }); @@ -1157,4 +1205,4 @@ function artifactNodeKey(artifact: ScheduleArtifact, schedule: Schedule): string if (!existing.has(candidate)) return candidate; } return `${base}_${Date.now().toString(36)}`; -} \ No newline at end of file +} diff --git a/frontend/app/features/schedules/utils.ts b/frontend/app/features/schedules/utils.ts index 49a1092..befb897 100644 --- a/frontend/app/features/schedules/utils.ts +++ b/frontend/app/features/schedules/utils.ts @@ -1,4 +1,10 @@ -import { type Schedule, type ScheduleArtifact, type ScheduleNode } from "../../services/api"; +import { + type Schedule, + type ScheduleArtifact, + type ScheduleNode, + type ScheduleNodeRun, + type ScheduleRunSummary, +} from "../../services/api"; import { EMPTY_SCHEDULE_FORM, EMPTY_NODE_FORM } from "./constants"; export function formatTime(value: string | null): string { @@ -23,6 +29,33 @@ export function formatDuration(value: number | null): string { return `${Math.floor(value / 60_000)}m ${Math.round((value % 60_000) / 1000)}s`; } +export function formatSize(value: number): string { + if (value < 1024) return `${value} B`; + if (value < 1024 * 1024) return `${(value / 1024).toFixed(1)} KB`; + return `${(value / 1024 / 1024).toFixed(1)} MB`; +} + +export const RUN_STATUS_LABELS: Record = { + queued: "排队中", + running: "运行中", + succeeded: "成功", + failed: "失败", + cancelled: "已取消", + timed_out: "已超时", +}; + +export const NODE_STATUS_LABELS: Record = { + ...RUN_STATUS_LABELS, + skipped: "已跳过", +}; + +export const TRIGGER_TYPE_LABELS: Record = { + manual: "手动触发", + cron: "Cron 定时", + api: "API 触发", + retry: "失败重试", +}; + export function parseObject(text: string, label: string): Record { let value: unknown; try {