import type { EngineStepAttempt } from "@umt/contracts"; import { ArrowLeft, Ban, Repeat, RotateCw } from "lucide-react"; import { Link, useNavigate, useParams } from "react-router"; import { toast } from "sonner"; import { ENGINE_TERMINAL, useCancelEngineRun, useEngineRun, useRetryEngineRun } from "~/api/engine.ts"; import { ConfirmDialog } from "~/components/console/confirm-dialog.tsx"; import { DetailSection } from "~/components/console/detail-sheet.tsx"; import { EmptyState, ErrorState, LoadingState } from "~/components/console/empty-state.tsx"; import { JsonView } from "~/components/console/json-view.tsx"; import { KvList } from "~/components/console/kv-list.tsx"; import { consolePath } from "~/components/console/nav.ts"; import { PageHeader } from "~/components/console/page-header.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { Timeline, type TimelineItem } from "~/components/console/timeline.tsx"; import { Button, buttonVariants } from "~/components/ui/button.tsx"; import { errorMessage } from "~/lib/api.ts"; import { engineRunLabel, formatDuration, formatRelative, spanSec as runDurationSec } from "~/lib/labels.ts"; import { formatDateTime } from "~/lib/utils.ts"; export const meta = () => [{ title: "运行详情 · 引擎面板" }]; const KIND_LABEL: Record = { function: "函数", sleep: "休眠", workflow: "子工作流", "signal-send": "发送信号", "signal-wait": "等待信号", }; function stepTone(s: string) { if (s === "completed" || s === "succeeded") return "success" as const; if (s === "failed") return "destructive" as const; return "neutral" as const; } function stepItems(steps: EngineStepAttempt[]): TimelineItem[] { const seen = new Map(); return steps.map((st) => { const n = (seen.get(st.stepName) ?? 0) + 1; seen.set(st.stepName, n); const status = st.status === "running" ? "运行中" : st.status === "failed" ? "失败" : st.status === "completed" || st.status === "succeeded" ? "完成" : st.status; const hasError = st.error !== null && st.error !== undefined; return { id: st.id, at: st.startedAt ?? st.createdAt, tone: stepTone(st.status), title: ( {st.stepName} {KIND_LABEL[st.kind] ?? st.kind} · {status} {n > 1 ? ` · 第 ${n} 次` : ""} ), meta: formatDuration(runDurationSec(st)), description: ( <> {st.waitSignal ? (

等待信号 {st.waitSignal} {st.timeoutAt ? ` · 超时于 ${formatDateTime(st.timeoutAt)}` : ""}

) : null} {st.childWorkflowRunId ? (

子工作流{" "} {st.childWorkflowRunId}

) : null}

{st.id}

), children: hasError ? ( ) : st.output !== null && st.output !== undefined ? ( ) : undefined, }; }); } export default function EngineRunPage() { const { runId } = useParams(); const q = useEngineRun(runId); const cancel = useCancelEngineRun(); const retry = useRetryEngineRun(); const navigate = useNavigate(); const back = ( 返回列表 ); if (q.isPending) { return ( <> {back} ); } if (q.isError) { return ( <> {back} q.refetch()} /> ); } const r = q.data; const st = engineRunLabel(r.status); const terminal = ENGINE_TERMINAL.has(r.status); const hasError = r.error !== null && r.error !== undefined; return ( <> {back} {st.label} {r.version ? {r.version} : null} {r.id} } actions={ <> {!terminal ? ( 取消运行 } title="取消这次运行?" description="正在执行的步骤会在下一个检查点退出。" confirmLabel="取消运行" cancelLabel="不取消" destructive onConfirm={async () => { try { const res = await cancel.mutateAsync({ id: r.id, reason: "控制台手动取消" }); if (res.canceled) toast.success("已取消"); else toast.warning("运行已结束"); } catch (err) { toast.error(errorMessage(err, "取消失败")); throw err; } }} /> ) : ( 重跑 } title="重跑这次运行?" description="用原始输入新建一次运行,原记录不变。已完成的文件操作不会重复执行。" confirmLabel="重跑" cancelLabel="不重跑" onConfirm={async () => { try { const res = await retry.mutateAsync({ id: r.id }); toast.success(`已重新入队(第 ${res.attempt} 次重跑)`); navigate(consolePath("engine", res.runId)); } catch (err) { toast.error(errorMessage(err, "重跑失败")); throw err; } }} /> )} } />
{hasError ? : null} {!hasError ? : null}
{r.signals.length === 0 ? (

没有信号。

) : (
    {r.signals.map((sg) => (
  • {sg.signal} {formatDateTime(sg.createdAt)}

  • ))}
)}
{r.stepAttempts.length === 0 ? ( ) : ( )}
); }