import { useMutation, useQueryClient } from "@tanstack/react-query"; import { cursorPage, Task, type TaskKind, type TaskStatus } from "@umt/contracts"; import { RotateCcw, Square } from "lucide-react"; import { Link } from "react-router"; import { toast } from "sonner"; import { apiMutate } from "~/api/parse.ts"; import { DetailSection } from "~/components/console/detail-sheet.tsx"; import { KvList } from "~/components/console/kv-list.tsx"; import { consolePath } from "~/components/console/nav.ts"; import { recordPath, taskPath } from "~/components/console/record-sheet.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { Button } from "~/components/ui/button.tsx"; import { errorMessage } from "~/lib/api.ts"; import { actorText } from "~/lib/labels.ts"; import { formatDateTime } from "~/lib/utils.ts"; // 旧业务任务(task 表)的展示零件:引擎只负责推进,task 表是面向用户的投影;取消只停后续步骤,重试只对失败任务。 // 只剩日志页任务抽屉在打开旧任务 id 时用(components/tasks/legacy-task.tsx)。 const _TaskPage = cursorPage(Task); export const TASK_KIND_LABEL: Record = { ingestFeed: "抓取 RSS", resolveObservation: "识别资源", evaluateAcquisition: "评估资源", submitDownload: "提交下载", verifyAndImport: "核验整理", refreshMetadata: "刷新元数据", reconcileLibrary: "文件核对", cleanupDownloadCache: "清理缓存", resolveDecision: "处理事项", agentRun: "智能体运行", }; export const TASK_STATUS: Record< TaskStatus, { label: string; tone: "success" | "warning" | "destructive" | "neutral" } > = { pending: { label: "排队中", tone: "neutral" }, running: { label: "运行中", tone: "neutral" }, waiting: { label: "等待中", tone: "warning" }, succeeded: { label: "成功", tone: "success" }, failed: { label: "失败", tone: "destructive" }, cancelled: { label: "已取消", tone: "neutral" }, dead: { label: "已放弃", tone: "destructive" }, }; export const ACTIVE_TASK: TaskStatus[] = ["pending", "running", "waiting"]; export const SUBJECT_LABEL: Record = { download_task: "下载任务", acquisition_plan: "下载", acquisition_slot: "收录版本", organization_record: "整理记录", organize_request: "整理请求", media_entity: "作品", media_asset: "文件", source_release: "资源", feed_source: "RSS", site: "站点", library_root: "媒体库目录", cache_volume: "下载盘", }; /** 业务任务的对象 → 它的"家"页面 */ export function subjectLink(t: Pick): string | null { if (!t.subjectId) return null; switch (t.subjectType) { case "download_task": return recordPath("downloads", t.subjectId); case "acquisition_plan": return `${consolePath("downloads")}?plan=${encodeURIComponent(t.subjectId)}`; case "organization_record": return recordPath("organize", t.subjectId); case "media_entity": return consolePath("library", t.subjectId); case "site": return recordPath("sites", t.subjectId); case "feed_source": // RSS 属于站点(IA-2):按 ?feed= 查所属站点后跳到站点的 RSS Tab return `${consolePath("sources")}?feed=${encodeURIComponent(t.subjectId)}`; default: return null; } } export function subjectText(t: Pick): string { if (!t.subjectType) return "—"; const label = SUBJECT_LABEL[t.subjectType] ?? t.subjectType; return t.subjectId ? `${label} · ${t.subjectId.slice(0, 8)}` : label; } /** 取消 / 重试 */ export function useTaskAction() { const qc = useQueryClient(); return useMutation({ mutationFn: ({ id, op }: { id: string; op: "cancel" | "retry" }) => apiMutate(`/api/v1/tasks/:id/${op}`, { pathParams: { id }, body: op === "cancel" ? { reason: "控制台取消" } : {}, }).then((d) => Task.parse(d)), onSuccess: (_t, v) => { toast.success(v.op === "cancel" ? "已请求取消,后续步骤不再执行" : "已重新排队"); void qc.invalidateQueries({ queryKey: ["tasks"] }); }, onError: (e) => toast.error(errorMessage(e)), }); } /** 取消 / 重试按钮(按状态二选一或都不显示) */ export function TaskActions({ t, size = "sm" }: { t: Task; size?: "sm" | "xs" }) { const action = useTaskAction(); if (ACTIVE_TASK.includes(t.status)) return ( ); if (t.status === "failed" || t.status === "dead") return ( ); return null; } export function TaskStatusLine({ t }: { t: Task }) { return ( {TASK_STATUS[t.status].label} {t.waitReason ? {t.waitReason} : null} ); } /** 概况 + 关联对象 */ export function TaskSummary({ t }: { t: Task }) { const href = subjectLink(t); const err = t.lastError ? String((t.lastError as { message?: unknown }).message ?? JSON.stringify(t.lastError)) : null; return (
{err ? (

{err}

) : null} {subjectText(t)} ) : ( subjectText(t) ), }, { label: "上级任务", value: t.parentTaskId ? ( {t.parentTaskId.slice(0, 8)} ) : null, }, { label: "引擎运行", value: t.engineRunId ? ( {t.engineRunId.slice(0, 12)} ) : null, }, ]} />
); }