feat(web): 技术信息折叠、原因码人话化、待处理「指定作品」(DEAI-2)
- 新增 TechInfo 折叠组件(默认收起,标题「技术信息」);设置新增「界面」分区, 「默认展开技术信息」开关存本机浏览器 - 原因码表 @umt/contracts reasons.ts(identity_unknown、blocked:target_exists、below_min_size… → 一句人话 + 下一步),服务端整理记录 message 与冲突事项标题/摘要改用它 - 整理记录列表/详情、下载详情与时间线:原码、ID、hash、用时、变更序号进技术信息; 下载完成但入库失败时单独提示;时间线只列有记录的阶段,标题改「经过」 - 待处理:没识别出作品类事项主按钮「指定作品」(搜媒体库 + TMDB,走已有 resolve; 整理类事项打开重新整理),忽略为次要;去掉正常态徽章与「实时」指示;原始证据改「详细信息」进技术信息 - 整理成功后同时关闭该下载的「没识别出作品」事项 Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
1 parent
4480fc346d
commit
f0d4935fa7
21 files changed
+688
-78
No files matched your search
@@ -17,6 +17,7 @@ export * from "./ops.ts";
|
||||
export * from "./organization.ts";
|
||||
export * from "./pi.ts";
|
||||
export * from "./policy.ts";
|
||||
export * from "./reasons.ts";
|
||||
export * from "./redact.ts";
|
||||
export * from "./settings.ts";
|
||||
export * from "./sources.ts";
|
||||
@@ -0,0 +1,73 @@
|
||||
// 内部原因码 → 一句人话 + 下一步(DEAI-2)。整理记录、待处理、下载时间线、任务都用这张表;
|
||||
// 原码只放在界面的「技术信息」里,不直接给人看。不认识的码返回 null,由调用方决定兜底文字。
|
||||
|
||||
export type ReasonText = {
|
||||
/** 发生了什么(一句,不带句号) */
|
||||
text: string;
|
||||
/** 下一步能做什么(一句,不带句号);没有可做的就不给 */
|
||||
action?: string;
|
||||
};
|
||||
|
||||
const REASONS: Record<string, ReasonText> = {
|
||||
identity_unknown: { text: "没识别出作品", action: "指定作品后重新整理" },
|
||||
identity_ambiguous: { text: "没识别出作品", action: "指定作品" },
|
||||
identity_unresolved: { text: "没识别出作品", action: "指定作品" },
|
||||
target_exists: { text: "媒体库里已有同名文件", action: "在待处理里选择替换或跳过" },
|
||||
duplicate_target: { text: "改名后与其他文件同名", action: "改命名模板或排除其中一个" },
|
||||
replace_requires_grant: { text: "替换已有文件需要你批准", action: "在待处理里批准" },
|
||||
hnr_unsatisfied: { text: "做种时间还不够,不能移动", action: "改用复制,或等做种完成" },
|
||||
hnr_unknown: { text: "不确定做种要求是否满足", action: "改用复制,或确认站点规则" },
|
||||
orphan_sidecar: { text: "字幕等附属文件找不到对应视频" },
|
||||
no_main_video: { text: "没有可入库的主视频" },
|
||||
below_min_size: { text: "文件小于最小体积,没有识别", action: "需要时在设置里调低最小体积" },
|
||||
too_small: { text: "文件小于最小体积" },
|
||||
source_path_unknown: { text: "下载器没有报告保存路径", action: "在下载器里确认任务还在" },
|
||||
path_mapping_missing: { text: "下载目录没有路径映射", action: "在设置 → 下载器里添加路径映射" },
|
||||
file_list_unavailable: { text: "读不到下载器里的文件列表", action: "稍后重试" },
|
||||
source_missing: { text: "找不到源文件", action: "检查下载目录是否挂载" },
|
||||
source_size_mismatch: { text: "源文件大小与下载器记录不一致", action: "等下载完成后重试" },
|
||||
too_many_files: { text: "文件太多,超出单次整理上限", action: "分批整理" },
|
||||
no_source: { text: "没有要整理的文件" },
|
||||
entity_not_found: { text: "作品已不存在" },
|
||||
no_library_root: { text: "没有可用的媒体库目录", action: "在设置 → 媒体库与目录里添加" },
|
||||
library_root_offline: { text: "媒体库目录不可访问或未挂载", action: "检查挂载后重试" },
|
||||
target_dir_outside_root: { text: "目标目录不在媒体库目录里" },
|
||||
slot_entity_mismatch: { text: "收录版本和作品对不上" },
|
||||
binding_on_other_root: { text: "作品已放在另一个媒体库目录", action: "在作品详情里调整目录" },
|
||||
move_downgraded_to_copy: { text: "做种未完成,已改为复制" },
|
||||
already_in_library: { text: "已在媒体库里" },
|
||||
main_not_transferred: { text: "主文件没有整理,附属文件一起跳过" },
|
||||
timeout: { text: "操作超时", action: "稍后重试" },
|
||||
};
|
||||
|
||||
/**
|
||||
* 解析原因串:`identity_unknown`、`blocked:target_exists,hnr_unknown`、`move_downgraded_to_copy:hnr_unknown`。
|
||||
* 多个码时合并成一句(用";"分隔)。
|
||||
*/
|
||||
export function reasonText(raw: string | null | undefined): ReasonText | null {
|
||||
if (!raw) return null;
|
||||
const s = raw.trim();
|
||||
const [head, rest] = s.includes(":") ? [s.slice(0, s.indexOf(":")), s.slice(s.indexOf(":") + 1)] : [s, ""];
|
||||
if (head === "blocked" && rest) {
|
||||
const parts = rest
|
||||
.split(",")
|
||||
.map((c) => REASONS[c.trim()])
|
||||
.filter((x): x is ReasonText => !!x);
|
||||
if (!parts.length) return null;
|
||||
return {
|
||||
text: [...new Set(parts.map((p) => p.text))].join(";"),
|
||||
...(parts[0]?.action ? { action: parts[0].action } : {}),
|
||||
};
|
||||
}
|
||||
return REASONS[s] ?? REASONS[head] ?? null;
|
||||
}
|
||||
|
||||
/** 只要一句话:"没识别出作品。指定作品后重新整理";不认识的码返回 fallback */
|
||||
export function reasonSentence(raw: string | null | undefined, fallback = "处理失败"): string {
|
||||
const r = reasonText(raw);
|
||||
if (!r) return raw && !/^[a-z_:.,]+$/.test(raw) ? raw : fallback;
|
||||
return r.action ? `${r.text}。${r.action}` : r.text;
|
||||
}
|
||||
|
||||
/** 看起来像内部码(小写下划线 / 冒号)而不是给人看的句子 */
|
||||
export const looksLikeCode = (v: string | null | undefined) => !!v && /^[a-z0-9_]+([:.,][a-z0-9_:.,-]*)?$/.test(v);
|
||||
@@ -780,6 +780,7 @@ export const SettingsSectionId = z.enum([
|
||||
"metadata",
|
||||
"agent",
|
||||
"api-keys",
|
||||
"interface",
|
||||
"account",
|
||||
]);
|
||||
export type SettingsSectionId = z.infer<typeof SettingsSectionId>;
|
||||
@@ -968,6 +969,15 @@ export const SETTINGS_SECTIONS: readonly SettingsSectionDef[] = [
|
||||
fields: [],
|
||||
resources: ["api-keys"],
|
||||
},
|
||||
{
|
||||
id: "interface",
|
||||
label: "界面",
|
||||
description: "只影响当前浏览器",
|
||||
legacyTabs: [],
|
||||
anchors: [{ id: "display", label: "显示" }],
|
||||
fields: [],
|
||||
resources: ["ui-preferences"],
|
||||
},
|
||||
{
|
||||
id: "account",
|
||||
label: "账户与安全",
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { looksLikeCode, reasonSentence, reasonText } from "../src/reasons.ts";
|
||||
|
||||
describe("reasonText(DEAI-2 原因码人话化)", () => {
|
||||
it("裸码、blocked:多码、带后缀的码都能翻译", () => {
|
||||
expect(reasonText("identity_unknown")).toEqual({ text: "没识别出作品", action: "指定作品后重新整理" });
|
||||
expect(reasonText("blocked:target_exists")?.text).toBe("媒体库里已有同名文件");
|
||||
expect(reasonText("blocked:target_exists,hnr_unknown")?.text).toBe("媒体库里已有同名文件;不确定做种要求是否满足");
|
||||
expect(reasonText("move_downgraded_to_copy:hnr_unknown")?.text).toBe("做种未完成,已改为复制");
|
||||
expect(reasonText("below_min_size")?.text).toContain("最小体积");
|
||||
});
|
||||
|
||||
it("不认识的码返回 null;句子兜底不把码原样给人看", () => {
|
||||
expect(reasonText("some_new_code")).toBeNull();
|
||||
expect(reasonSentence("some_new_code")).toBe("处理失败");
|
||||
expect(reasonSentence("磁盘已满")).toBe("磁盘已满");
|
||||
expect(reasonSentence("identity_unknown")).toBe("没识别出作品。指定作品后重新整理");
|
||||
});
|
||||
|
||||
it("looksLikeCode 只认内部码", () => {
|
||||
expect(looksLikeCode("blocked:target_exists")).toBe(true);
|
||||
expect(looksLikeCode("identity_unknown")).toBe(true);
|
||||
expect(looksLikeCode("目标已存在")).toBe(false);
|
||||
expect(looksLikeCode("Connection refused")).toBe(false);
|
||||
});
|
||||
});
|
||||
@@ -8,7 +8,7 @@ import {
|
||||
} from "../src/settings.ts";
|
||||
|
||||
describe("SETTINGS_SECTIONS", () => {
|
||||
it("9 个分区,顺序与 SettingsSectionId 一致", () => {
|
||||
it("10 个分区,顺序与 SettingsSectionId 一致", () => {
|
||||
expect(SETTINGS_SECTIONS.map((s) => s.id)).toEqual(SettingsSectionId.options);
|
||||
});
|
||||
|
||||
|
||||
Reference in new issue
Block a user