import { AlertTriangle, CheckCircle2, Circle, Clock3, Download, GitBranch, Hammer, HardDriveDownload, Loader2, PackageCheck, ShieldCheck, Terminal, Wrench, X } from "lucide-react"; import { useCallback, useState } from "react"; import { cx } from "../utils/classes"; export type RuntimeTaskStatus = "running" | "succeeded" | "failed"; export type RuntimeTaskStageStatus = "pending" | "running" | "completed" | "failed"; export interface RuntimeTaskStage { key: string; label: string; description: string; } export interface RuntimeTaskDialogState { open: boolean; title: string; description: string; status: RuntimeTaskStatus; percent: number; currentStageKey: string; stages: RuntimeTaskStage[]; stageStatus: Record; logs: string[]; summary?: string; error?: string; } export interface RuntimeTaskDialogAction { label: string; onClick: () => void; disabled?: boolean; kind?: "primary" | "default" | "danger"; } export const runtimeBuildStages: RuntimeTaskStage[] = [ { key: "git_sync", label: "拉取代码", description: "同步平台批准的 run 或客户端源码版本。" }, { key: "env_check", label: "安装环境", description: "检查 Go、系统依赖和隔离构建目录。" }, { key: "deps_download", label: "下载依赖", description: "解析模块依赖并准备构建缓存。" }, { key: "build_compile", label: "编译构建", description: "编译目标平台二进制。" }, { key: "package_finalize", label: "打包成功", description: "注入配置、校验 checksum、生成 artifact。" } ]; export const runtimeDownloadStages: RuntimeTaskStage[] = [ { key: "scope_check", label: "权限校验", description: "确认当前服务器范围和 artifact 授权。" }, { key: "artifact_lookup", label: "定位产物", description: "读取最新可下载 run 包引用。" }, { key: "download_ref", label: "生成下载", description: "创建限时下载引用和分块参数。" } ]; export const runtimeUpdateStages: RuntimeTaskStage[] = [ { key: "artifact_lookup", label: "定位产物", description: "读取最近生成或下载的 run artifact。" }, { key: "checksum_verify", label: "校验签名", description: "确认 checksum 可用于 run 自更新。" }, { key: "dispatch_job", label: "推送更新", description: "向在线 run 节点派发自更新任务。" }, { key: "job_track", label: "等待确认", description: "记录 job id 并刷新后台任务状态。" } ]; export const runtimeDependencyStages: RuntimeTaskStage[] = [ { key: "profile_read", label: "读取声明", description: "读取插件声明的 probe 和 install plan。" }, { key: "env_probe", label: "环境检查", description: "让 run 节点评估当前运行环境。" }, { key: "install_prepare", label: "安装环境", description: "准备安全、可审计的依赖安装任务。" }, { key: "job_track", label: "等待确认", description: "记录 job id 并刷新后台任务状态。" } ]; export const runtimeLogStages: RuntimeTaskStage[] = [ { key: "source_read", label: "读取日志源", description: "读取插件声明的日志源和 checkpoint。" }, { key: "cursor_prepare", label: "准备游标", description: "创建平台日志查询或历史回填游标。" }, { key: "job_track", label: "等待确认", description: "打开实时日志或记录回填任务。" } ]; export function runtimeInitialStageStatus(stages: RuntimeTaskStage[]): Record { return Object.fromEntries(stages.map((stage) => [stage.key, "pending" as RuntimeTaskStageStatus])); } interface RuntimeTaskRunOptions { title: string; description: string; stages: RuntimeTaskStage[]; execute: () => Promise; executeStageIndex?: number; } export interface RuntimeTrackedJob { id: string; state: "queued" | "accepted" | "running" | "retrying" | "succeeded" | "failed" | "cancelled"; progress: { percent: number; message?: string }; attempt?: number; retryPolicy?: { maxAttempts: number }; nextAttemptAt?: string; cancelReason?: string; reconcileOutcome?: string; } export interface RuntimeTrackedJobProjection { status: RuntimeTaskStatus; percent: number; currentStageKey: string; stageStatus: Record; message: string; } interface RuntimeTrackedTaskOptions { title: string; description: string; stages: RuntimeTaskStage[]; start: () => Promise<{ value: T; jobId: string }>; poll: (jobId: string) => Promise; pollIntervalMs?: number; } export function useRuntimeTaskController() { const [task, setTask] = useState(null); const closeTask = useCallback(() => { setTask((current) => (current ? { ...current, open: false } : current)); }, []); const succeedTask = useCallback((summary: string) => { setTask((current) => current ? { ...current, status: "succeeded", percent: 100, summary, error: undefined, stageStatus: Object.fromEntries(current.stages.map((stage) => [stage.key, "completed" as RuntimeTaskStageStatus])), logs: appendRuntimeLog(current.logs, summary) } : current ); }, []); const failTask = useCallback((message: string) => { setTask((current) => current ? { ...current, status: "failed", error: message, stageStatus: { ...current.stageStatus, [current.currentStageKey]: "failed" }, logs: appendRuntimeLog(current.logs, message) } : current ); }, []); const runTask = useCallback(async ({ title, description, stages, execute, executeStageIndex }: RuntimeTaskRunOptions): Promise => { const currentStageKey = stages[0]?.key ?? "start"; const executeIndex = Math.max(0, Math.min(stages.length - 1, executeStageIndex ?? Math.floor(stages.length / 2))); setTask({ open: true, title, description, status: "running", percent: 2, currentStageKey, stages, stageStatus: runtimeInitialStageStatus(stages), logs: [`${title} 已启动`] }); let value: T | undefined; let executed = false; for (let index = 0; index < stages.length; index += 1) { const stage = stages[index]; const startPercent = stageProgress(index, stages.length, false); setTask((current) => current ? { ...current, currentStageKey: stage.key, percent: Math.max(current.percent, startPercent), stageStatus: { ...current.stageStatus, [stage.key]: "running" }, logs: appendRuntimeLog(current.logs, `${stage.label}中`) } : current ); try { if (index === executeIndex) { executed = true; value = await execute(); } else { await wait(index === 0 ? 240 : 360); } } catch (error) { const message = error instanceof Error ? error.message : `${title}失败`; setTask((current) => current ? { ...current, status: "failed", currentStageKey: stage.key, stageStatus: { ...current.stageStatus, [stage.key]: "failed" }, error: message, logs: appendRuntimeLog(current.logs, message) } : current ); throw error; } const endPercent = stageProgress(index, stages.length, true); setTask((current) => current ? { ...current, percent: Math.max(current.percent, endPercent), stageStatus: { ...current.stageStatus, [stage.key]: "completed" }, logs: appendRuntimeLog(current.logs, `${stage.label}完成`) } : current ); } if (!executed) { value = await execute(); } return value as T; }, []); const runTrackedTask = useCallback( async ({ title, description, stages, start, poll, pollIntervalMs = 800 }: RuntimeTrackedTaskOptions): Promise => { const firstStage = stages[0]?.key ?? "start"; setTask({ open: true, title, description, status: "running", percent: 1, currentStageKey: firstStage, stages, stageStatus: { ...runtimeInitialStageStatus(stages), [firstStage]: "running" }, logs: [`${title} 正在创建后台构建任务`] }); const started = await start(); setTask((current) => (current ? { ...current, logs: appendRuntimeLog(current.logs, `后台 job ${started.jobId} 已排队`) } : current)); while (true) { const job = await poll(started.jobId); const projection = projectRuntimeTrackedJob(stages, job); setTask((current) => current ? { ...current, percent: job.state === "succeeded" ? 100 : Math.max(current.percent, projection.percent), currentStageKey: projection.currentStageKey || current.currentStageKey, stageStatus: projection.stageStatus, logs: current.logs[current.logs.length - 1] === projection.message ? current.logs : appendRuntimeLog(current.logs, projection.message) } : current ); if (job.state === "succeeded") { setTask((current) => current ? { ...current, status: "succeeded", percent: 100, stageStatus: Object.fromEntries(stages.map((item) => [item.key, "completed" as RuntimeTaskStageStatus])), logs: appendRuntimeLog(current.logs, "构建产物已由 run worker 上传") } : current ); return started.value; } if (job.state === "failed" || job.state === "cancelled") { const error = projection.message || (job.state === "cancelled" ? "构建已取消" : "构建失败"); setTask((current) => current ? { ...current, status: "failed", error, currentStageKey: projection.currentStageKey || firstStage, stageStatus: projection.stageStatus, logs: appendRuntimeLog(current.logs, error) } : current ); throw new Error(error); } await wait(pollIntervalMs); } }, [] ); return { task, runTask, runTrackedTask, succeedTask, failTask, closeTask }; } interface RuntimeTaskProgressDialogProps { task: RuntimeTaskDialogState | null; onClose: () => void; actions?: RuntimeTaskDialogAction[]; } export function RuntimeTaskProgressDialog({ task, onClose, actions = [] }: RuntimeTaskProgressDialogProps) { if (!task?.open) { return null; } const activeStage = task.stages.find((stage) => stage.key === task.currentStageKey) ?? task.stages[0]; const statusLabel = runtimeTaskStatusLabel(task.status); const closeLabel = task.status === "running" ? "后台运行" : "关闭"; return (
event.stopPropagation()}>
{task.title} {task.description} {task.status === "running" && } {task.status === "succeeded" && } {task.status === "failed" && } {statusLabel}
{activeStage?.label ?? statusLabel} {Math.round(task.percent)}%
{activeStage && task.status === "running" && (
{stageIcon(activeStage.key, "running")} {activeStage.label} {activeStage.description}
)}
    {task.stages.map((stage) => { const status = task.stageStatus[stage.key] ?? "pending"; return (
  1. {stageIcon(stage.key, status)} {stage.label} {stage.description} {runtimeStageStatusLabel(status)}
  2. ); })}
{task.logs.map((line, index) => ( {line} ))}
{(task.summary || task.error) && (
{task.error ? : } {task.error ?? task.summary}
)}
{actions.map((action) => ( ))}
); } function stageIcon(stageKey: string, status: RuntimeTaskStageStatus) { if (status === "completed") { return ; } if (status === "failed") { return ; } if (status === "running") { return ; } if (stageKey.includes("git")) { return ; } if (stageKey.includes("env") || stageKey.includes("install")) { return ; } if (stageKey.includes("deps") || stageKey.includes("download")) { return ; } if (stageKey.includes("compile") || stageKey.includes("build")) { return ; } if (stageKey.includes("scope") || stageKey.includes("checksum")) { return ; } if (stageKey.includes("package") || stageKey.includes("artifact")) { return ; } return ; } function runtimeTaskStatusLabel(status: RuntimeTaskStatus): string { switch (status) { case "running": return "构建中"; case "succeeded": return "构建成功"; case "failed": return "失败"; } } function runtimeStageStatusLabel(status: RuntimeTaskStageStatus): string { switch (status) { case "completed": return "已完成"; case "running": return "进行中"; case "failed": return "失败"; case "pending": return "等待中"; } } function stageProgress(index: number, total: number, completed: boolean): number { if (total <= 0) { return completed ? 100 : 0; } const base = (index / total) * 90 + 4; const next = ((index + 1) / total) * 90 + 4; return completed ? next : base; } function trackedStageIndex(stages: RuntimeTaskStage[], message: string, percent: number): number { const stageKey = message.split(":", 1)[0]; const explicit = stages.findIndex((stage) => stage.key === stageKey); if (explicit >= 0) { return explicit; } const thresholds = [0, 25, 40, 60, 80]; let index = 0; for (let candidate = 0; candidate < Math.min(stages.length, thresholds.length); candidate += 1) { if (percent >= thresholds[candidate]) { index = candidate; } } return Math.min(index, Math.max(0, stages.length - 1)); } export function projectRuntimeTrackedJob(stages: RuntimeTaskStage[], job: RuntimeTrackedJob): RuntimeTrackedJobProjection { const retryLabel = job.state === "retrying" ? `等待第 ${Math.min((job.attempt ?? 0) + 1, job.retryPolicy?.maxAttempts ?? (job.attempt ?? 0) + 1)} 次尝试${job.nextAttemptAt ? `(${new Date(job.nextAttemptAt).toLocaleString()})` : ""}` : ""; const message = job.cancelReason?.trim() || job.progress.message?.trim() || retryLabel || job.reconcileOutcome?.trim() || job.state; const stageIndex = trackedStageIndex(stages, message, job.progress.percent); const currentStageKey = stages[stageIndex]?.key ?? stages[0]?.key ?? "start"; const terminalFailure = job.state === "failed" || job.state === "cancelled"; const stageStatus = Object.fromEntries( stages.map((item, index) => [ item.key, job.state === "succeeded" || index < stageIndex ? "completed" : index === stageIndex ? (terminalFailure ? "failed" : "running") : "pending" ]) ) as Record; return { status: job.state === "succeeded" ? "succeeded" : terminalFailure ? "failed" : "running", percent: job.state === "succeeded" ? 100 : Math.min(99, Math.max(0, job.progress.percent)), currentStageKey, stageStatus, message }; } function appendRuntimeLog(logs: string[], line: string): string[] { return [...logs, line].slice(-8); } function wait(ms: number): Promise { return new Promise((resolve) => { window.setTimeout(resolve, ms); }); }