From 3f1c4d6c3ba4c6f4b547c4263b4e990cbd328257 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 29 Sep 2026 22:00:04 +0800 Subject: [PATCH] fix(downloads): humanized reasons and TechInfo on the download panel (DL-2b) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - board organize hint uses DEAI-2 reasonLine; add transfer_failed, blocked, blocked_by_conflict, main_failed, not_imported_kind, target_root_not_managed, download_removed to the reason table with tests - download detail / plans tab: internal ids, raw codes and JSON moved into TechInfo - library versions tech block switched to TechInfo (TODO(DEAI-2)) - copy: glossary terms (no 接管/候选), plan reason codes no longer shown raw Co-Authored-By: Claude Opus 5.5 --- apps/web/app/api/downloads.ts | 2 +- apps/web/app/api/tasks.ts | 4 +- .../components/downloads/board-format.test.ts | 18 +++++- .../app/components/downloads/board-format.ts | 6 +- .../components/downloads/download-detail.tsx | 15 ++++- apps/web/app/components/library/versions.tsx | 63 ++++++++----------- apps/web/app/lib/labels.ts | 8 ++- .../app/routes/console/downloads.plans.tsx | 37 ++++++----- docs/design/glossary.md | 1 + packages/contracts/src/reasons.ts | 8 +++ packages/contracts/test/reasons.test.ts | 8 +++ 11 files changed, 105 insertions(+), 65 deletions(-) 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 { decision: { id: "00000000-0000-4000-8000-000000000002", kind: "identity_unresolved" }, }), }); - // 原因码不直接给人看 - expect(failed).toMatchObject({ label: "入库失败", tone: "destructive", hint: null }); + // 原因码转成人话,不直接给人看 + 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("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, diff --git a/apps/web/app/components/downloads/board-format.ts b/apps/web/app/components/downloads/board-format.ts index 21078881..1d992fcb 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,10 +34,9 @@ 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; } /** 已完成任务的最近一次整理结果:已入库 / 整理中 / 待整理 / 入库失败 / 部分入库 */ 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/components/library/versions.tsx b/apps/web/app/components/library/versions.tsx index 586d7e2d..e2c29668 100644 --- a/apps/web/app/components/library/versions.tsx +++ b/apps/web/app/components/library/versions.tsx @@ -3,8 +3,9 @@ import { ChevronDown } from "lucide-react"; import { type ReactNode, useState } from "react"; import { Link } from "react-router"; import { useLibraryVersions } from "~/api/library-versions.ts"; -import { type KvItem, KvList } from "~/components/console/kv-list.tsx"; +import { JsonView } from "~/components/console/json-view.tsx"; import { consolePath } from "~/components/console/nav.ts"; +import { TechInfo } from "~/components/console/tech-info.tsx"; import { Panel } from "~/components/library/parts.tsx"; import { cn, formatBytes, formatDateTime } from "~/lib/utils.ts"; @@ -86,45 +87,31 @@ function duration(sec: number | null): string | null { const SPEC_FROM: Record = { name: "文件名", probe: "ffprobe" }; /** 技术信息:路径、音轨、字幕、入库时间、下载任务、探测原始值 */ -// TODO(DEAI-2):TechInfo 组件合入后改用 components/console/tech-info.tsx,和其他页面统一折叠样式与默认展开偏好 function VersionTech({ v }: { v: AssetVersion }) { - const items: KvItem[] = [ - { label: "文件", value: v.absolutePath ?? v.relativePath ?? v.fileName, mono: true, wide: true }, - { label: "音轨", value: audioText(v) }, - { label: "字幕", value: v.subtitleLanguages.map(langLabel).join("、") || null }, - { label: "时长", value: duration(v.durationSec) }, - { label: "色深", value: v.bitDepth ? `${v.bitDepth} bit` : null }, - { label: "入库时间", value: formatDateTime(v.organizedAt) }, - { - label: "下载任务", - value: v.downloadTaskId ? ( - - 查看下载任务 - - ) : null, - }, - { label: "规格来源", value: v.specFrom.map((f) => SPEC_FROM[f] ?? f).join(" + ") || null }, - ].filter((it) => it.value !== null && it.value !== undefined && it.value !== ""); return ( -
- - 技术信息 - - -
- - {v.probe ? ( -
- - 探测原始值 - -
-              {JSON.stringify(v.probe, null, 2)}
-            
-
- ) : null} -
-
+ + 查看下载任务 + + ) : null, + }, + { label: "规格来源", value: v.specFrom.map((f) => SPEC_FROM[f] ?? f).join(" + ") || null, mono: false }, + ]} + > + {v.probe ? : null} + ); } 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 a6cd04f5..0b9a4026 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 aaedff5c..333e676e 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("处理失败");