import type { DownloadTask } from "@umt/contracts"; import { Bot, FolderInput, Pause, Play, ShieldAlert } from "lucide-react"; import { useState } from "react"; import { toast } from "sonner"; import { DOWNLOAD_STATUS_LABEL, DOWNLOAD_STATUS_TONE, PAUSE_REASON_LABEL, PLAN_STATUS_LABEL, useDownloadAction, useDownloaderNames, usePlan, } from "~/api/downloads.ts"; import { useDownloadTrace } from "~/api/tasks.ts"; import { openAgentDrawer } from "~/components/agent/agent-store.ts"; import { DetailSection } from "~/components/console/detail-sheet.tsx"; import { JsonView } from "~/components/console/json-view.tsx"; import { KvList } from "~/components/console/kv-list.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { OrganizeWizard } from "~/components/organize-wizard/organize-wizard.tsx"; import { Button } from "~/components/ui/button.tsx"; import { relTime } from "~/components/workbench/release-fields.ts"; import { errorMessage } from "~/lib/api.ts"; import { planReasonText } from "~/lib/labels.ts"; import { formatBytes, formatDateTime } from "~/lib/utils.ts"; import { BusinessTimeline } from "./business-timeline.tsx"; const PLAN_PURPOSE_LABEL: Record = { fill: "补缺", upgrade: "升级", replace_source: "换源" }; /** 进度:细线 + 文字;总量未知时不伪造百分比 */ export function ProgressLine({ t }: { t: DownloadTask }) { const known = t.sizeBytes !== null; return (

{known ? `${(t.progress * 100).toFixed(1)}%` : `已下 ${formatBytes(t.downloadedBytes, 1)}`}

); } export function DownloadStatusBadges({ t }: { t: DownloadTask }) { return ( {DOWNLOAD_STATUS_LABEL[t.status]} {t.pauseReason === "threshold" ? ( 阈值暂停 ) : t.pauseReason ? ( {PAUSE_REASON_LABEL[t.pauseReason]} ) : null} {!t.managed ? 未纳管 : null} {t.origin === "adopted" ? 接管 : null} ); } /** 暂停/恢复:阈值暂停在水位回落前不可手动恢复(服务端强制) */ export function PauseResume({ t, size = "sm" }: { t: DownloadTask; size?: "sm" | "xs" }) { const act = useDownloadAction(); const canPause = ["queued", "downloading", "stalled"].includes(t.status); const canResume = t.status === "paused"; if (!canPause && !canResume) return null; const threshold = t.pauseReason === "threshold"; return ( ); } /** 下载任务详情:状态、阈值说明、业务时间线、文件、关联计划 */ export function DownloadDetail({ task }: { task: DownloadTask }) { const plan = usePlan(task.acquisitionPlanId); const names = useDownloaderNames(); const trace = useDownloadTrace(task, plan.data); const [organizing, setOrganizing] = useState(false); const canOrganize = ["completed", "importing", "imported"].includes(task.status); return (
{task.pauseReason === "threshold" ? (

缓存卷达到硬阈值,任务已暂停,水位回落到恢复线后自动恢复。手动恢复和 Agent 都不能绕过阈值。

) : null} {task.noProgressSince ? (

自 {formatDateTime(task.noProgressSince)} 起无进展,已记入待处理,不会自动换源。

) : null} {plan.data ? ( ) : null} {task.fileList?.length ? (
    {task.fileList.map((f) => (
  • {f.path} {formatBytes(f.size, 1)} {f.progress !== undefined ? ` · ${Math.round(f.progress * 100)}%` : ""}
  • ))}
) : null} {task.lastError ? : null} {organizing ? ( setOrganizing(false)} /> ) : null}
); }