diff --git a/apps/web/app/api/downloads.ts b/apps/web/app/api/downloads.ts index 1ec842ce..ce18f626 100644 --- a/apps/web/app/api/downloads.ts +++ b/apps/web/app/api/downloads.ts @@ -179,7 +179,7 @@ export const DOWNLOAD_STATUS_TONE: Record { it("最近一次整理结果:已入库 / 整理中 / 待整理 / 入库失败 / 部分入库;有待处理事项时链到待处理", () => { expect(organizeState({ status: "completed", organize: null })).toMatchObject({ label: "待整理", to: null }); - expect(organizeState({ status: "importing", organize: null }).label).toBe("整理中"); - expect(organizeState({ status: "imported", organize: null }).label).toBe("已入库"); + expect(organizeState({ status: "importing", organize: null })?.label).toBe("整理中"); + expect(organizeState({ status: "imported", organize: null })?.label).toBe("已入库"); expect(organizeState({ status: "completed", organize: rec("succeeded") })).toMatchObject({ label: "已入库", tone: "success", to: `/console/organize/${RID}`, }); - expect(organizeState({ status: "completed", organize: rec("running") }).label).toBe("整理中"); + expect(organizeState({ status: "completed", organize: rec("running") })?.label).toBe("整理中"); const failed = organizeState({ status: "completed", organize: rec("failed", { @@ -42,19 +42,43 @@ describe("下载面板展示换算(DL-2)", () => { decision: { id: "00000000-0000-4000-8000-000000000002", kind: "identity_unresolved" }, }), }); - // 原因码不直接给人看 - expect(failed).toMatchObject({ label: "入库失败", tone: "destructive", hint: null }); - expect(failed.to).toBe("/console/inbox?kind=identity_unresolved"); - expect(organizeState({ status: "completed", organize: rec("failed", { reason: "磁盘已满" }) }).hint).toBe( + // 原因码转成人话,不直接给人看 + expect(failed).toMatchObject({ + label: "入库失败", + tone: "destructive", + hint: "没识别出作品。指定作品后重新整理", + }); + expect(failed?.to).toBe("/console/inbox?kind=identity_unresolved"); + expect(organizeState({ status: "completed", organize: rec("failed", { reason: "磁盘已满" }) })?.hint).toBe( "磁盘已满", ); - expect(organizeState({ status: "completed", organize: rec("partial") }).label).toBe("部分入库"); + expect(organizeState({ status: "completed", organize: rec("partial") })?.label).toBe("部分入库"); + expect(organizeState({ status: "completed", organize: rec("failed", { reason: "transfer_failed" }) })?.hint).toBe( + "文件没有整理成功。在整理记录里查看失败的文件", + ); + expect( + organizeState({ status: "completed", organize: rec("partial", { reason: "blocked:target_exists" }) })?.hint, + ).toBe("媒体库里已有同名文件。在待处理里选择替换或跳过"); + // 不认识的码不显示 + expect( + organizeState({ status: "completed", organize: rec("failed", { reason: "some_new_code" }) })?.hint, + ).toBeNull(); expect(organizeState({ status: "completed", organize: rec("cancelled") })).toMatchObject({ label: "待整理", to: null, }); }); + it("外部任务:没整理过显示 —(null),整理过的照常显示结果", () => { + expect(organizeState({ status: "completed", organize: null, managed: false })).toBeNull(); + expect(organizeState({ status: "completed", organize: null, managed: true })?.label).toBe("待整理"); + expect(organizeState({ status: "completed", organize: rec("succeeded"), managed: false })?.label).toBe("已入库"); + expect( + organizeState({ status: "completed", organize: rec("failed", { reason: "transfer_failed" }), managed: false }) + ?.label, + ).toBe("入库失败"); + }); + it("连接数:全 0 且总数未知时不显示", () => { const base = { dlSpeed: 0, diff --git a/apps/web/app/components/downloads/board-format.ts b/apps/web/app/components/downloads/board-format.ts index 21078881..9694db10 100644 --- a/apps/web/app/components/downloads/board-format.ts +++ b/apps/web/app/components/downloads/board-format.ts @@ -1,5 +1,6 @@ import type { DownloadBoardItem, DownloadBoardState, DownloadLive } from "@umt/contracts"; import { consolePath } from "~/components/console/nav.ts"; +import { reasonLine } from "~/lib/reasons.ts"; import { formatBytes } from "~/lib/utils.ts"; // 下载面板(DL-2)的展示换算:速度、剩余时间、整理状态、实时状态补充。 @@ -33,15 +34,20 @@ export type OrganizeState = { hint: string | null; }; -/** 原因:内部码(小写下划线)不显示,其余原样。TODO(DEAI-2 合并后):改用 ~/lib/reasons.ts 的 reasonLine 人话化 */ +/** 原因 → 一句人话(DEAI-2 原因码表);不认识的内部码不给人看,原码在详情的技术信息里 */ export function reasonHint(raw: string | null | undefined): string | null { - if (!raw) return null; - return /^[a-z0-9_]+([:.,][a-z0-9_:.,-]*)?$/.test(raw) ? null : raw; + return reasonLine(raw, "") || null; } -/** 已完成任务的最近一次整理结果:已入库 / 整理中 / 待整理 / 入库失败 / 部分入库 */ -export function organizeState(t: Pick): OrganizeState { +/** + * 已完成任务的最近一次整理结果:已入库 / 整理中 / 待整理 / 入库失败 / 部分入库。 + * 外部任务(managed=false)没整理过时不算"待整理",返回 null(界面显示 —);整理过的照常显示结果。 + */ +export function organizeState( + t: Pick & { managed?: boolean }, +): OrganizeState | null { const rec = t.organize; + if (t.managed === false && !rec && t.status !== "importing" && t.status !== "imported") return null; const to = rec?.decision ? `${consolePath("inbox")}?kind=${rec.decision.kind}` : rec diff --git a/apps/web/app/components/downloads/download-board.tsx b/apps/web/app/components/downloads/download-board.tsx index 2b3446f7..5cc01c2e 100644 --- a/apps/web/app/components/downloads/download-board.tsx +++ b/apps/web/app/components/downloads/download-board.tsx @@ -195,6 +195,7 @@ export function activeColumns(): DataColumn[] { function OrganizeBadge({ t }: { t: DownloadBoardItem }) { const s = organizeState(t); + if (!s) return —; const badge = {s.label}; return (
diff --git a/apps/web/app/components/downloads/download-detail.tsx b/apps/web/app/components/downloads/download-detail.tsx index 3a251488..458d3554 100644 --- a/apps/web/app/components/downloads/download-detail.tsx +++ b/apps/web/app/components/downloads/download-detail.tsx @@ -24,7 +24,7 @@ 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 { humanizeReason, reasonLine } from "~/lib/reasons.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"; @@ -162,7 +162,7 @@ export function DownloadDetail({ task, live }: { task: DownloadTask; live?: Down {task.lastError ? : null} @@ -201,7 +204,13 @@ export function DownloadDetail({ task, live }: { task: DownloadTask; live?: Down { label: "说明", value: planReasonText(plan.data.statusReason), wide: true }, ]} /> - + diff --git a/apps/web/app/lib/labels.ts b/apps/web/app/lib/labels.ts index f2d35215..c8a86107 100644 --- a/apps/web/app/lib/labels.ts +++ b/apps/web/app/lib/labels.ts @@ -1,4 +1,6 @@ +import { looksLikeCode } from "@umt/contracts"; import type { StatusTone } from "~/components/console/status-badge.tsx"; +import { reasonLine } from "~/lib/reasons.ts"; // 状态值 → 中文文案 + 语气。状态徽章必须带文字,这里集中维护常用的几组。 @@ -154,5 +156,9 @@ const PLAN_REASON: Record = { "organize:succeeded": "整理成功", "organize:partial": "部分整理成功", "organize:failed": "整理失败", + authorized: "已批准", + "reserved:agent": "智能体已预留空间", }; -export const planReasonText = (v: string | null | undefined) => (v ? (PLAN_REASON[v] ?? v) : null); +/** 不认识的内部码走原因码表,仍不认识就不显示(原码在技术信息里) */ +export const planReasonText = (v: string | null | undefined) => + v ? (PLAN_REASON[v] ?? (looksLikeCode(v) ? reasonLine(v, "") || null : v)) : null; diff --git a/apps/web/app/routes/console/downloads.plans.tsx b/apps/web/app/routes/console/downloads.plans.tsx index 76835405..39dc24ae 100644 --- a/apps/web/app/routes/console/downloads.plans.tsx +++ b/apps/web/app/routes/console/downloads.plans.tsx @@ -4,13 +4,14 @@ import { useState } from "react"; import { Link } from "react-router"; import { PLAN_STATUS_LABEL, useDownloadTasks, usePlans } from "~/api/downloads.ts"; import { type DataColumn, DataTable } from "~/components/console/data-table.tsx"; -import { DetailSection, DetailSheet } from "~/components/console/detail-sheet.tsx"; +import { DetailSheet } from "~/components/console/detail-sheet.tsx"; import { FilterBar, FilterSelect } from "~/components/console/filter-bar.tsx"; import { JsonView } from "~/components/console/json-view.tsx"; import { KvList } from "~/components/console/kv-list.tsx"; import { consolePath } from "~/components/console/nav.ts"; import { PageHeader } from "~/components/console/page-header.tsx"; import { StatusBadge } from "~/components/console/status-badge.tsx"; +import { TechInfo } from "~/components/console/tech-info.tsx"; import { useQueryState } from "~/components/console/use-query-state.ts"; import { DownloadsTabs } from "~/components/downloads/downloads-tabs.tsx"; import { PRIORITY } from "~/components/library/labels.ts"; @@ -42,12 +43,9 @@ export default function DownloadPlansPage() { key: "purpose", header: "计划", cell: (p) => ( -
-

- {PURPOSE[p.purpose]} · 优先级 {PRIORITY[p.priority as keyof typeof PRIORITY] ?? p.priority} -

-

{p.id.slice(0, 8)}

-
+

+ {PURPOSE[p.purpose]} · 优先级 {PRIORITY[p.priority as keyof typeof PRIORITY] ?? p.priority} +

), }, { @@ -103,7 +101,7 @@ export default function DownloadPlansPage() { empty={{ icon: ClipboardList, title: f.active ? "没有符合筛选的下载计划" : "还没有下载计划", - description: f.active ? undefined : "候选被选中后出现在这里。", + description: f.active ? undefined : "选中可下载的资源后出现在这里。", }} /> @@ -130,18 +128,27 @@ export default function DownloadPlansPage() { {PLAN_STATUS_LABEL[openPlan.status]} ), }, - { label: "优先级", value: openPlan.priority }, + { + label: "优先级", + value: PRIORITY[openPlan.priority as keyof typeof PRIORITY] ?? openPlan.priority, + }, { label: "预留空间", value: formatBytes(openPlan.reservedBytes, 1) }, { label: "过期于", value: formatDateTime(openPlan.expiresAt) }, { label: "创建", value: formatDateTime(openPlan.createdAt) }, - { label: "收录版本", value: openPlan.slotId, mono: true }, - { label: "资源", value: openPlan.sourceReleaseId, mono: true, wide: true }, ]} /> - - - - + + + + ) : null} diff --git a/docs/design/glossary.md b/docs/design/glossary.md index feed20a6..be8409d0 100644 --- a/docs/design/glossary.md +++ b/docs/design/glossary.md @@ -29,6 +29,7 @@ | 未确认身份的文件/资源 | 未识别 | 待识别、待识别池 | | identity_ambiguous / 整理时认不出作品的待处理事项 | 没识别出作品;主按钮"指定作品" | 身份歧义、识别有歧义、无法确定作品、待确认身份 | | 作品登记进本地库(不订阅) | 加入媒体库;状态"在媒体库 · 未下载"/"不在媒体库" | 建档、已建档、未收录 | +| 已完成下载任务的整理状态(下载面板) | 待整理 / 整理中 / 已入库 / 部分入库 / 入库失败;失败原因用原因码表的一句话 | 未整理、导入失败、转移失败 | | 本工具提交并跟踪的下载任务(managed) | 本工具管理;反面叫"外部任务";筛选"来源:本工具 / 其他" | 纳管、未纳管、接管 | | 媒体库目录里的 `.uhd-root` | 标记文件;按钮"检查目录";失败说"目录不可访问或未挂载" | 哨兵、哨兵文件 | | 作品 / 版本 / 文件 / 外部身份的本地记录总称 | 不单独命名 | 账本、本地账本 | diff --git a/packages/contracts/src/reasons.ts b/packages/contracts/src/reasons.ts index 4d6c51c4..e4200a28 100644 --- a/packages/contracts/src/reasons.ts +++ b/packages/contracts/src/reasons.ts @@ -40,6 +40,14 @@ const REASONS: Record = { timeout: { text: "操作超时", action: "稍后重试" }, downloader_unavailable: { text: "下载器连不上", action: "在设置 → 下载器里测试连接" }, downloader_error: { text: "下载器报错", action: "查看下载器日志" }, + // DL-2b:下载面板整理状态、下载计划里出现的码 + blocked: { text: "有冲突要先处理", action: "去待处理" }, + blocked_by_conflict: { text: "有冲突,暂不整理" }, + transfer_failed: { text: "文件没有整理成功", action: "在整理记录里查看失败的文件" }, + main_failed: { text: "主文件整理失败,附属文件一起跳过" }, + not_imported_kind: { text: "这类文件不入库" }, + target_root_not_managed: { text: "目标目录不是本工具管理的媒体库目录", action: "在设置 → 媒体库与目录里检查" }, + download_removed: { text: "下载任务已从下载器删除" }, }; /** diff --git a/packages/contracts/test/reasons.test.ts b/packages/contracts/test/reasons.test.ts index ddd92aa0..937c1589 100644 --- a/packages/contracts/test/reasons.test.ts +++ b/packages/contracts/test/reasons.test.ts @@ -10,6 +10,14 @@ describe("reasonText(DEAI-2 原因码人话化)", () => { expect(reasonText("below_min_size")?.text).toContain("最小体积"); }); + it("下载面板用到的码(DL-2b):整理失败、冲突、下载被删", () => { + expect(reasonSentence("transfer_failed")).toBe("文件没有整理成功。在整理记录里查看失败的文件"); + expect(reasonSentence("blocked")).toBe("有冲突要先处理。去待处理"); + expect(reasonText("download_removed")?.text).toBe("下载任务已从下载器删除"); + expect(reasonText("target_root_not_managed")?.text).toContain("本工具管理"); + for (const c of ["blocked_by_conflict", "main_failed", "not_imported_kind"]) expect(reasonText(c)).not.toBeNull(); + }); + it("不认识的码返回 null;句子兜底不把码原样给人看", () => { expect(reasonText("some_new_code")).toBeNull(); expect(reasonSentence("some_new_code")).toBe("处理失败");