import type { DownloadLive, 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 { RecordLink } from "~/components/console/record-sheet.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; import { TechInfo } from "~/components/console/tech-info.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 { humanizeReason, reasonCode, reasonLine } from "~/lib/reasons.ts"; import { formatBytes, formatDateTime } from "~/lib/utils.ts"; import { liveItems } from "./board-format.ts"; import { BusinessTimeline } from "./business-timeline.tsx"; import { WhySection } from "./plan-row.tsx"; /** 进度:细线 + 文字;总量未知时不伪造百分比 */ 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} ); } /** 暂停/恢复:阈值暂停在水位回落前不可手动恢复(服务端强制) */ 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 ( ); } /** 下载任务详情:状态、阈值说明、业务时间线、为什么下这个(DL-3)、文件 */ export function DownloadDetail({ task, live }: { task: DownloadTask; live?: DownloadLive | null }) { 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); // 下载完成但最近一次整理失败:状态行只写"下载完成"会误导,单独说明整理失败 const lastRecord = [...(trace.records.data?.items ?? [])].sort((a, b) => (b.startedAt ?? b.createdAt).localeCompare(a.startedAt ?? a.createdAt), )[0]; const importFailed = task.status === "completed" && lastRecord?.status === "failed" ? lastRecord : null; return (
{task.pauseReason === "threshold" ? (

下载盘超过暂停线,任务已暂停;降到恢复线以下后自动恢复。

) : null} {importFailed ? (

整理失败:{reasonLine(importFailed.reason)}。 去整理记录处理

) : null} {task.noProgressSince ? (

{formatDateTime(task.noProgressSince)}{" "} 起没有进展,不会自动换源。开启自动调查时,智能体会在持续授权范围内尝试恢复。

) : null} {task.acquisitionPlanId ? ( ) : null} {task.lastError ? : 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 ? (

下载器报错: {humanizeReason(String((task.lastError as { message?: unknown }).message ?? "")) || "原文见技术信息"}

) : null} {organizing ? ( setOrganizing(false)} /> ) : null}
); }