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}
);
}