Merge branch 'dl2b'
Co-Authored-By: Claude Fable 5.1 <[email protected]> # Conflicts: # apps/web/app/components/library/versions.tsx
This commit is contained in:
commit
606e97815f
11 files changed
+105
-35
No files matched your search
@@ -179,7 +179,7 @@ export const DOWNLOAD_STATUS_TONE: Record<DownloadStatus, "success" | "warning"
|
||||
};
|
||||
|
||||
export const PAUSE_REASON_LABEL = {
|
||||
user: "用户暂停",
|
||||
user: "手动暂停",
|
||||
threshold: "下载盘满暂停",
|
||||
policy: "策略暂停",
|
||||
agent: "智能体暂停",
|
||||
|
||||
@@ -191,7 +191,7 @@ export function buildTimeline(input: {
|
||||
key: "dl-submit",
|
||||
stage: "download",
|
||||
at: task.createdAt,
|
||||
title: task.origin === "adopted" ? "接管已有下载" : "提交下载",
|
||||
title: task.origin === "adopted" ? "开始管理已有下载" : "提交下载",
|
||||
detail: task.name,
|
||||
tone: "neutral",
|
||||
});
|
||||
@@ -230,7 +230,7 @@ export function buildTimeline(input: {
|
||||
stage: "download",
|
||||
at: task.updatedAt,
|
||||
title: "下载器报错",
|
||||
detail: String((task.lastError as { message?: unknown }).message ?? JSON.stringify(task.lastError)),
|
||||
detail: humanizeReason(String((task.lastError as { message?: unknown }).message ?? "")) || null,
|
||||
tone: "destructive",
|
||||
});
|
||||
for (const r of records) {
|
||||
|
||||
@@ -27,14 +27,14 @@ describe("下载面板展示换算(DL-2)", () => {
|
||||
|
||||
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,
|
||||
|
||||
@@ -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<DownloadBoardItem, "status" | "organize">): OrganizeState {
|
||||
/**
|
||||
* 已完成任务的最近一次整理结果:已入库 / 整理中 / 待整理 / 入库失败 / 部分入库。
|
||||
* 外部任务(managed=false)没整理过时不算"待整理",返回 null(界面显示 —);整理过的照常显示结果。
|
||||
*/
|
||||
export function organizeState(
|
||||
t: Pick<DownloadBoardItem, "status" | "organize"> & { 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
|
||||
|
||||
@@ -195,6 +195,7 @@ export function activeColumns(): DataColumn<DownloadBoardItem>[] {
|
||||
|
||||
function OrganizeBadge({ t }: { t: DownloadBoardItem }) {
|
||||
const s = organizeState(t);
|
||||
if (!s) return <span className="text-meta text-muted-foreground">—</span>;
|
||||
const badge = <StatusBadge tone={s.tone}>{s.label}</StatusBadge>;
|
||||
return (
|
||||
<div className="flex max-w-56 flex-col items-start gap-0.5">
|
||||
|
||||
@@ -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
|
||||
<KvList
|
||||
className="mt-3"
|
||||
items={[
|
||||
{ label: "下载器", value: names.data?.get(task.instanceId) ?? task.instanceId.slice(0, 8) },
|
||||
{ label: "下载器", value: names.data?.get(task.instanceId) ?? "未知" },
|
||||
{ label: "大小", value: task.sizeBytes === null ? "未知" : formatBytes(task.sizeBytes, 2) },
|
||||
{ label: "剩余", value: task.remainingBytes === null ? "未知" : formatBytes(task.remainingBytes, 2) },
|
||||
...liveItems(live),
|
||||
@@ -181,6 +181,9 @@ export function DownloadDetail({ task, live }: { task: DownloadTask; live?: Down
|
||||
{ label: "内容路径", value: task.contentPath, wide: true },
|
||||
{ label: "torrent hash", value: task.torrentHash, wide: true },
|
||||
{ label: "任务 ID", value: task.id, wide: true },
|
||||
{ label: "下载器 ID", value: task.instanceId, wide: true },
|
||||
importFailed ? { label: "整理失败原因码", value: reasonCode(importFailed.reason) } : null,
|
||||
importFailed ? { label: "整理记录 ID", value: importFailed.id, wide: true } : null,
|
||||
]}
|
||||
>
|
||||
{task.lastError ? <JsonView label="下载器错误原文" value={task.lastError} tone="destructive" /> : null}
|
||||
@@ -201,7 +204,13 @@ export function DownloadDetail({ task, live }: { task: DownloadTask; live?: Down
|
||||
{ label: "说明", value: planReasonText(plan.data.statusReason), wide: true },
|
||||
]}
|
||||
/>
|
||||
<TechInfo className="mt-2">
|
||||
<TechInfo
|
||||
className="mt-2"
|
||||
items={[
|
||||
{ label: "计划 ID", value: plan.data.id, wide: true },
|
||||
{ label: "说明原码", value: plan.data.statusReason },
|
||||
]}
|
||||
>
|
||||
<JsonView label="评估依据" value={plan.data.evaluation} />
|
||||
</TechInfo>
|
||||
</DetailSection>
|
||||
|
||||
@@ -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<string, string> = {
|
||||
"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;
|
||||
@@ -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) => (
|
||||
<div className="min-w-0">
|
||||
<p className="text-foreground">
|
||||
{PURPOSE[p.purpose]} · 优先级 {PRIORITY[p.priority as keyof typeof PRIORITY] ?? p.priority}
|
||||
</p>
|
||||
<p className="truncate font-mono text-meta text-muted-foreground">{p.id.slice(0, 8)}</p>
|
||||
</div>
|
||||
<p className="text-foreground">
|
||||
{PURPOSE[p.purpose]} · 优先级 {PRIORITY[p.priority as keyof typeof PRIORITY] ?? p.priority}
|
||||
</p>
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -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() {
|
||||
<StatusBadge tone={planTone(openPlan.status)}>{PLAN_STATUS_LABEL[openPlan.status]}</StatusBadge>
|
||||
),
|
||||
},
|
||||
{ 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 },
|
||||
]}
|
||||
/>
|
||||
<DetailSection title="评估依据">
|
||||
<JsonView value={openPlan.evaluation} />
|
||||
</DetailSection>
|
||||
<JsonView label="预算预留" value={openPlan.budgetReservation} />
|
||||
<TechInfo
|
||||
className="mt-4"
|
||||
items={[
|
||||
{ label: "计划 ID", value: openPlan.id, wide: true },
|
||||
{ label: "收录版本 ID", value: openPlan.slotId, wide: true },
|
||||
{ label: "资源 ID", value: openPlan.sourceReleaseId, wide: true },
|
||||
{ label: "说明原码", value: openPlan.statusReason },
|
||||
]}
|
||||
>
|
||||
<JsonView label="评估依据" value={openPlan.evaluation} defaultOpen={false} />
|
||||
<JsonView label="预算预留" value={openPlan.budgetReservation} defaultOpen={false} />
|
||||
</TechInfo>
|
||||
</>
|
||||
) : null}
|
||||
</DetailSheet>
|
||||
|
||||
@@ -29,6 +29,7 @@
|
||||
| 未确认身份的文件/资源 | 未识别 | 待识别、待识别池 |
|
||||
| identity_ambiguous / 整理时认不出作品的待处理事项 | 没识别出作品;主按钮"指定作品" | 身份歧义、识别有歧义、无法确定作品、待确认身份 |
|
||||
| 作品登记进本地库(不订阅) | 加入媒体库;状态"在媒体库 · 未下载"/"不在媒体库" | 建档、已建档、未收录 |
|
||||
| 已完成下载任务的整理状态(下载面板) | 待整理 / 整理中 / 已入库 / 部分入库 / 入库失败;失败原因用原因码表的一句话 | 未整理、导入失败、转移失败 |
|
||||
| 本工具提交并跟踪的下载任务(managed) | 本工具管理;反面叫"外部任务";筛选"来源:本工具 / 其他" | 纳管、未纳管、接管 |
|
||||
| 媒体库目录里的 `.uhd-root` | 标记文件;按钮"检查目录";失败说"目录不可访问或未挂载" | 哨兵、哨兵文件 |
|
||||
| 作品 / 版本 / 文件 / 外部身份的本地记录总称 | 不单独命名 | 账本、本地账本 |
|
||||
|
||||
@@ -40,6 +40,14 @@ const REASONS: Record<string, ReasonText> = {
|
||||
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: "下载任务已从下载器删除" },
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -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("处理失败");
|
||||
|
||||
Reference in new issue
Block a user