copy(domain): 命名模板、规则判定、收录版本评估文案改全角标点并去掉内部用语

阶梯 pri / 候选 / 资产交付特征等改成界面用词;同步断言。

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-10-07 01:52:23 +08:00
1 parent 12a8caa463
commit 0ba947dce3
14 files changed
+86 -80

No files matched your search

@@ -356,6 +356,6 @@ describe.skipIf(!hasPg)("订阅增强(真实 PG + mock qB + 临时目录)", () =
const sameEval = art
.map((a) => a.content as { sourceReleaseId: string; trace?: string[] })
.find((a) => a.sourceReleaseId === same.id);
expect(sameEval?.trace?.[0]).toBe("与当前版本同档,不反复更换");
expect(sameEval?.trace?.[0]).toBe("与现有版本同等级,不来回替换");
}, 60_000);
});
+1 -1
View File
@@ -123,7 +123,7 @@ export function applyObservation(
): AppliedObservation {
const mapped = mapQbState(obs, { ...ctx, previous: current });
if (!ALLOWED[current].includes(mapped.status))
throw new DownloadTransitionError(`非法的下载任务状态迁移: ${current} → ${mapped.status}`);
throw new DownloadTransitionError(`非法的下载任务状态迁移:${current} → ${mapped.status}`);
const out: AppliedObservation = { status: mapped.status, changed: mapped.status !== current };
if (current === "completed" && (mapped.status === "downloading" || mapped.status === "queued"))
out.anomaly = "completed_regressed";
+13 -13
View File
@@ -58,16 +58,16 @@ export interface CompileOptions {
// ── 过滤器 ──
export const NAMING_FILTERS = {
default: { args: [1, 2], doc: "值为空时用参数代替:default('未知')" },
pad: { args: [1, 1], doc: "数字左补零:pad(2) → 03;区间 1-3 → 01-03" },
default: { args: [1, 2], doc: "值为空时用参数代替:default('未知')" },
pad: { args: [1, 1], doc: "数字左补零:pad(2) → 03;区间 1-3 → 01-03" },
pad2: { args: [0, 0], doc: "等同 pad(2)" },
upper: { args: [0, 0], doc: "转大写" },
lower: { args: [0, 0], doc: "转小写" },
trim: { args: [0, 0], doc: "去首尾空白" },
replace: { args: [2, 2], doc: "替换:replace('.', ' ')" },
truncate: { args: [1, 1], doc: "按字符截断:truncate(40)" },
date: { args: [1, 1], doc: "YYYYMMDD / YYYY-MM-DD 日期格式化:date('YYYY-MM-DD'|'YYYY.MM.DD'|'YYYYMMDD')" },
join: { args: [0, 1], doc: "列表值按分隔符连接(默认空格);标量原样" },
replace: { args: [2, 2], doc: "替换:replace('.', ' ')" },
truncate: { args: [1, 1], doc: "按字符截断:truncate(40)" },
date: { args: [1, 1], doc: "YYYYMMDD / YYYY-MM-DD 日期格式化:date('YYYY-MM-DD'|'YYYY.MM.DD'|'YYYYMMDD')" },
join: { args: [0, 1], doc: "列表值按分隔符连接(默认空格);标量原样" },
} as const;
export type FilterName = keyof typeof NAMING_FILTERS;
const DATE_FORMATS = ["YYYY-MM-DD", "YYYY.MM.DD", "YYYYMMDD"];
@@ -234,10 +234,10 @@ function lex(body: string, base: number): Tok[] {
}
if (c === ".") throw new Fail("attribute_access", "不支持属性访问(a.b)", pos);
if (c === "[") throw new Fail("subscript", "不支持下标访问(a[0])", pos);
if (c === "~") throw new Fail("concat", "不支持 ~ 拼接,直接把两个 {{ }} 写在一起", pos);
if (c === "%") throw new Fail("arithmetic", "不支持 % 格式化/运算;补零用 {{ season | pad(2) }}", pos);
if (c === "~") throw new Fail("concat", "不支持 ~ 拼接,直接把两个 {{ }} 写在一起", pos);
if (c === "%") throw new Fail("arithmetic", "不支持 % 格式化/运算;补零用 {{ season | pad(2) }}", pos);
if ("+-*/".includes(c)) throw new Fail("arithmetic", "不支持算术运算", pos);
if (c === "=") throw new Fail("assignment", "不支持赋值;比较用 ==", pos);
if (c === "=") throw new Fail("assignment", "不支持赋值;比较用 ==", pos);
throw new Fail("bad_char", `无法识别的字符 ${JSON.stringify(c)}`, pos);
}
toks.push({ k: "end", pos: base + body.length });
@@ -303,7 +303,7 @@ class ExprParser {
const e = this.parseOr();
const t = this.peek();
if (t.k !== "end") {
if (t.k === "id" && t.v === "if") throw new Fail("inline_if", "不支持行内 if 表达式,改用 {% if %}", t.pos);
if (t.k === "id" && t.v === "if") throw new Fail("inline_if", "不支持行内 if 表达式,改用 {% if %}", t.pos);
if (t.k === "id" && own(KEYWORDS_UNSUPPORTED, t.v))
throw new Fail("unsupported", own(KEYWORDS_UNSUPPORTED, t.v)!, t.pos);
throw new Fail("syntax", "表达式多余内容", t.pos);
@@ -354,7 +354,7 @@ class ExprParser {
const hint = didYouMean(n.v, Object.keys(NAMING_FILTERS));
throw new Fail(
"unknown_filter",
`不支持的过滤器 ${n.v}${hint ? `,是否指 ${hint}` : ""}(可用:${Object.keys(NAMING_FILTERS).join("/")})`,
`不支持的过滤器 ${n.v}${hint ? `,是否指 ${hint}` : ""}(可用:${Object.keys(NAMING_FILTERS).join("/")})`,
n.pos,
);
}
@@ -406,7 +406,7 @@ class ExprParser {
const known = this.opts.knownVars;
if (known && !known.includes(t.v)) {
const hint = didYouMean(t.v, known);
throw new Fail("unknown_variable", `未知变量 ${t.v}${hint ? `,是否指 ${hint}` : ""}`, t.pos);
throw new Fail("unknown_variable", `未知变量 ${t.v}${hint ? `,是否指 ${hint}` : ""}`, t.pos);
}
if (!this.vars.includes(t.v)) this.vars.push(t.v);
return { t: "var", name: t.v, pos: t.pos };
@@ -495,7 +495,7 @@ function compileInner(source: string, opts: CompileOptions): CompiledNaming {
const f = stack[stack.length - 1];
if (!f) throw new Fail("stray_else", "else 没有对应的 if", c.pos);
if (f.sawElse) throw new Fail("syntax", "重复的 else", c.pos);
if (rest.trim()) throw new Fail("syntax", "else 后不能带条件,用 elif", c.pos);
if (rest.trim()) throw new Fail("syntax", "else 后不能带条件,用 elif", c.pos);
f.sawElse = true;
const body: Node[] = [];
f.node.else = body;
+4 -4
View File
@@ -129,7 +129,7 @@ export function renderNamingPath(
return fail([
at(
"no_work_root",
"识别不出作品目录:文件名之前至少要有一层目录包含 title / title_year / en_title / original_title / name,且不含季变量",
"识别不出作品目录:文件名之前至少要有一层目录包含 title / title_year / en_title / original_title / name,且不含季变量",
),
]);
const errors: NamingIssue[] = [];
@@ -148,7 +148,7 @@ export function renderNamingPath(
} else {
text = cleanSegment(r.text, SEGMENT_MAX_BYTES);
if (!text && role === "root")
errors.push(at("empty_segment", `作品目录为空(${r.vars.join("、") || "无变量"} 没有值)`));
errors.push(at("empty_segment", `作品目录为空(${r.vars.join("、") || "无变量"} 没有值)`));
if (!text && role === "season") errors.push(at("empty_segment", "季目录为空"));
}
if (text || role === "root" || role === "file" || role === "season") segments.push({ text, role, vars: r.vars });
@@ -179,7 +179,7 @@ export function validateNamingTemplate(source: string, kind: NamingKind): Naming
const lines = source.split("\n");
errors.push({
code: "missing_file_ext",
message: "文件名缺少 {{fileExt}}:模板必须以 {{fileExt}} 结尾",
message: "文件名缺少 {{fileExt}}:模板必须以 {{fileExt}} 结尾",
line: lines.length,
col: (lines[lines.length - 1]?.length ?? 0) + 1,
});
@@ -190,7 +190,7 @@ export function validateNamingTemplate(source: string, kind: NamingKind): Naming
if (kind === "tv") for (const v of ["episode", "season_episode"]) always.add(v);
const warnings = unguardedOutputs(r.compiled)
.filter((v) => !always.has(v))
.map((v) => `${v} 可能为空,建议写成 {% if ${v} %}…{{${v}}}…{% endif %}`);
.map((v) => `${v} 可能为空,建议写成 {% if ${v} %}…{{${v}}}…{% endif %}`);
return {
ok: errors.length === 0,
errors,
+21 -14
View File
@@ -19,39 +19,46 @@ const ALL: readonly NamingKind[] = ["movie", "tv", "variety"];
const SERIES: readonly NamingKind[] = ["tv", "variety"];
export const NAMING_VARIABLES: readonly NamingVariableDef[] = [
{ name: "title", group: "title", label: "作品标题(本地作品名)", example: "绝命毒师", ours: false, always: ALL },
{ name: "title", group: "title", label: "作品标题(本地作品名)", example: "绝命毒师", ours: false, always: ALL },
{ name: "en_title", group: "title", label: "英文标题", example: "Breaking Bad", ours: false },
{ name: "original_title", group: "title", label: "原语种标题", example: "Breaking Bad", ours: false },
{ name: "name", group: "title", label: "文件名识别出的标题", example: "Breaking Bad", ours: false },
{ name: "year", group: "title", label: "年份(作品年份,缺失取文件名年份)", example: "2008", ours: false },
{ name: "year", group: "title", label: "年份(作品年份,缺失取文件名年份)", example: "2008", ours: false },
{
name: "title_year",
group: "title",
label: "标题 (年份);无年份只有标题",
label: "标题(年份);无年份只有标题",
example: "绝命毒师 (2008)",
ours: false,
always: ALL,
},
{ name: "type", group: "title", label: "类型:电影 / 电视剧 / 综艺", example: "电视剧", ours: false, always: ALL },
{ name: "type", group: "title", label: "类型:电影 / 电视剧 / 综艺", example: "电视剧", ours: false, always: ALL },
{
name: "category",
group: "title",
label: "二级分类(没有则为空,所在目录段整段省略)",
label: "二级分类(没有则为空,所在目录段整段省略)",
example: "欧美剧",
ours: false,
},
{ name: "season", group: "episode", label: "季号,不补零;剧集缺季号为 1", example: "1", ours: false, always: SERIES },
{ name: "season_fmt", group: "episode", label: "S01;多季 S01-S02", example: "S01", ours: false, always: SERIES },
{
name: "season",
group: "episode",
label: "季号,不补零;剧集缺季号为 1",
example: "1",
ours: false,
always: SERIES,
},
{ name: "season_fmt", group: "episode", label: "S01;多季 S01-S02", example: "S01", ours: false, always: SERIES },
{ name: "season_year", group: "episode", label: "该季首播年份", example: "2008", ours: false },
{ name: "episode", group: "episode", label: "集号,不补零;多集 1-3", example: "1", ours: false },
{ name: "season_episode", group: "episode", label: "S01E01;多集 S01E01-E03", example: "S01E01", ours: false },
{ name: "total_episodes", group: "episode", label: "本季总集数,未知为 0", example: "7", ours: false, always: ALL },
{ name: "episode", group: "episode", label: "集号,不补零;多集 1-3", example: "1", ours: false },
{ name: "season_episode", group: "episode", label: "S01E01;多集 S01E01-E03", example: "S01E01", ours: false },
{ name: "total_episodes", group: "episode", label: "本季总集数,未知为 0", example: "7", ours: false, always: ALL },
{ name: "episode_title", group: "episode", label: "单集标题", example: "Pilot", ours: false },
{ name: "episode_date", group: "episode", label: "单集播出日期", example: "2008-01-20", ours: false },
{ name: "part", group: "episode", label: "分段(CD1、Part2)", example: "CD1", ours: false },
{ name: "videoFormat", group: "tech", label: "分辨率", example: "2160p", ours: false },
{ name: "resourceType", group: "tech", label: "来源", example: "WEB-DL", ours: false },
{ name: "effect", group: "tech", label: "特效(HDR 格式,空格分隔)", example: "DV", ours: false },
{ name: "effect", group: "tech", label: "特效(HDR 格式,空格分隔)", example: "DV", ours: false },
{ name: "edition", group: "tech", label: "来源 + 特效", example: "WEB-DL DV", ours: false },
{ name: "resource_term", group: "tech", label: "来源 + 特效 + 分辨率", example: "WEB-DL DV 2160p", ours: false },
{ name: "releaseGroup", group: "tech", label: "发布组", example: "CMCT", ours: false },
@@ -60,8 +67,8 @@ export const NAMING_VARIABLES: readonly NamingVariableDef[] = [
{ name: "audioCodec", group: "tech", label: "音频编码", example: "DDP 5.1", ours: false },
{ name: "webSource", group: "tech", label: "流媒体平台", example: "NF", ours: false },
{ name: "fps", group: "tech", label: "帧率", example: "60", ours: false },
{ name: "customization", group: "tech", label: "自定义占位符(暂恒为空)", example: "", ours: false },
{ name: "fileExt", group: "tech", label: "扩展名,带点,小写", example: ".mkv", ours: false, always: ALL },
{ name: "customization", group: "tech", label: "自定义占位符(暂恒为空)", example: "", ours: false },
{ name: "fileExt", group: "tech", label: "扩展名,带点,小写", example: ".mkv", ours: false, always: ALL },
{ name: "tmdbid", group: "id", label: "TMDB ID", example: "1396", ours: false },
{ name: "imdbid", group: "id", label: "IMDb ID", example: "tt0903747", ours: false },
{ name: "doubanid", group: "id", label: "豆瓣 ID", example: "1234567", ours: false },
@@ -71,7 +78,7 @@ export const NAMING_VARIABLES: readonly NamingVariableDef[] = [
{
name: "airDate",
group: "ours",
label: "综艺播出日期 YYYYMMDD,配合 date() 格式化",
label: "综艺播出日期 YYYYMMDD,配合 date() 格式化",
example: "20260927",
ours: true,
},
+1 -1
View File
@@ -406,7 +406,7 @@ export function planOrganize(input: OrganizeInput): OrganizePlan {
conflicts.push({
kind: h === false ? "hnr_unsatisfied" : "hnr_unknown",
blocking: true,
detail: "等待做种义务满足,或经授权改用 copy",
detail: "等待做种义务满足,或经授权改用 copy",
});
}
+1 -1
View File
@@ -43,7 +43,7 @@ function lex(src: string): Tok[] {
}
if (c === "(") out.push({ t: "lp" });
else if (c === ")") out.push({ t: "rp" });
else throw new OffsetExpressionError(`非法字符: ${c}`);
else throw new OffsetExpressionError(`非法字符:${c}`);
i++;
}
return out;
+11 -11
View File
@@ -115,7 +115,7 @@ export const BUILTIN_ATOMS: readonly BuiltinAtom[] = [
{
id: "BLU",
name: "蓝光原盘",
description: "来源为蓝光原盘(BDMV/ISO,非 Remux、非压制)",
description: "来源为蓝光原盘(BDMV/ISO,非 Remux、非压制)",
basis: "field",
supported: true,
mp: {
@@ -158,7 +158,7 @@ export const BUILTIN_ATOMS: readonly BuiltinAtom[] = [
{
id: "CNSUB",
name: "中字",
description: "含中文字幕(含 CHS/CHT/GB/BIG5);作品原语言为中文时直接满足",
description: "含中文字幕(含 CHS/CHT/GB/BIG5);作品原语言为中文时直接满足",
basis: "field",
supported: true,
mp: { include: [MP_CNSUB], tmdb: { original_language: "zh,cn" } },
@@ -173,7 +173,7 @@ export const BUILTIN_ATOMS: readonly BuiltinAtom[] = [
{
id: "GZ",
name: "官种",
description: "站点标签含 官方/官种/官组(只看标签)",
description: "站点标签含 官方/官种/官组(只看标签)",
basis: "site",
supported: true,
mp: { include: ["官方", "官种", "官组"], match: ["labels"] },
@@ -196,7 +196,7 @@ export const BUILTIN_ATOMS: readonly BuiltinAtom[] = [
{
id: "BLURAY",
name: "蓝光",
description: "蓝光来源(原盘、蓝光 Remux、蓝光压制)",
description: "蓝光来源(原盘、蓝光 Remux、蓝光压制)",
basis: "field",
supported: true,
mp: { include: ["Blu-?Ray"] },
@@ -249,7 +249,7 @@ export const BUILTIN_ATOMS: readonly BuiltinAtom[] = [
{
id: "ATMOS",
name: "杜比全景声",
description: "音轨含 Atmos(杜比全景声)",
description: "音轨含 Atmos(杜比全景声)",
basis: "field",
supported: true,
mp: { include: ["Dolby[\\s.+]+Atmos|Atmos|杜比全景[声聲]"] },
@@ -261,7 +261,7 @@ export const BUILTIN_ATOMS: readonly BuiltinAtom[] = [
{
id: "HDR",
name: "HDR",
description: "HDR10 / HDR10+ / HDR Vivid(不含只有杜比视界的)",
description: "HDR10 / HDR10+ / HDR Vivid(不含只有杜比视界的)",
basis: "field",
supported: true,
mp: { include: ["[\\s.]+HDR[\\s.]+|HDR10|HDR10\\+|HDRVivid"] },
@@ -300,7 +300,7 @@ export const BUILTIN_ATOMS: readonly BuiltinAtom[] = [
{
id: "FREE",
name: "免费",
description: "站点促销为免费(下载量不计)",
description: "站点促销为免费(下载量不计)",
basis: "site",
supported: true,
mp: { downloadvolumefactor: 0 },
@@ -315,7 +315,7 @@ export const BUILTIN_ATOMS: readonly BuiltinAtom[] = [
{
id: "CNVOI",
name: "国语配音",
description: "含国语配音;作品原语言为中文时直接满足",
description: "含国语配音;作品原语言为中文时直接满足",
basis: "regex",
supported: true,
mp: { include: [MP_CNVOI], tmdb: { original_language: "zh" } },
@@ -358,7 +358,7 @@ export const BUILTIN_ATOMS: readonly BuiltinAtom[] = [
([id, name, p]): BuiltinAtom => ({
id,
name,
description: "音乐资源条件,暂不支持",
description: "音乐资源条件,暂不支持",
basis: "regex",
supported: false,
mp: { include: [p] },
@@ -415,8 +415,8 @@ export function testCustomAtom(a: CustomAtom, i: AtomInput): AtomHit {
if (pd && (pd.min !== undefined || pd.max !== undefined)) {
const m = i.ctx.publishedMinutesAgo;
if (m === undefined) return { hit: false, why: "发布时间未知" };
if (pd.min !== undefined && m < pd.min) return { hit: false, why: `发布 ${Math.round(m)} 分钟,不足 ${pd.min}` };
if (pd.max !== undefined && m > pd.max) return { hit: false, why: `发布 ${Math.round(m)} 分钟,超过 ${pd.max}` };
if (pd.min !== undefined && m < pd.min) return { hit: false, why: `发布 ${Math.round(m)} 分钟,不足 ${pd.min}` };
if (pd.max !== undefined && m > pd.max) return { hit: false, why: `发布 ${Math.round(m)} 分钟,超过 ${pd.max}` };
}
return { hit: true, why: "满足" };
}
+4 -4
View File
@@ -88,14 +88,14 @@ export function evaluateRuleGroup(
const name = layer.name ? `第${k + 1}层「${layer.name}」` : `第${k + 1}层`;
const expr = serializeExpr(layer.expr);
if (hit) {
trace.push(`${label} ${name} ${expr}:命中,pri=${100 - k}(${notes.join(";")})`);
trace.push(`${label} ${name} ${expr}:命中,pri=${100 - k}(${notes.join(";")})`);
level = k;
break;
}
trace.push(`${label} ${name} ${expr}:未命中(${notes.join(";")})`);
trace.push(`${label} ${name} ${expr}:未命中(${notes.join(";")})`);
}
const required = g.ladderRequired !== false;
if (level === null && required) trace.push(`${label} 不命中任何一层,过滤`);
if (level === null && required) trace.push(`${label} 不命中任何一层,过滤`);
return {
...base,
excludedBy,
@@ -122,6 +122,6 @@ export function evaluateRuleChain(
const pri = deciding === null ? null : results[deciding]!.pri;
const trace = results.flatMap((x) => x.trace);
if (deciding !== null && results.filter((x) => x.pri !== null).length > 1)
trace.push(`pri 取最后一个带层的规则组「${results[deciding]!.name}」:${pri}`);
trace.push(`pri 取最后一个带层的规则组「${results[deciding]!.name}」:${pri}`);
return { passed, pri, decidingGroup: deciding, groups: results, trace };
}
+7 -7
View File
@@ -124,7 +124,7 @@ class LayerParser {
const t = this.peek();
if (!t) {
const last = this.toks[this.toks.length - 1];
this.err("unexpected_token", "表达式不完整,缺少条件", last ? last.pos + last.v.length : 0);
this.err("unexpected_token", "表达式不完整,缺少条件", last ? last.pos + last.v.length : 0);
return null;
}
if (t.t === "(") {
@@ -132,7 +132,7 @@ class LayerParser {
const e = this.or();
if (!e) return null;
if (this.peek()?.t !== ")") {
this.err("unbalanced_paren", "括号不配对:缺少 )", t.pos);
this.err("unbalanced_paren", "括号不配对:缺少 )", t.pos);
return null;
}
this.i++;
@@ -141,25 +141,25 @@ class LayerParser {
if (t.t === "atom") {
this.i++;
if (!ATOM_SHAPE.test(t.v)) {
this.err("unexpected_token", `「${t.v}」不是合法条件名(字母开头,或数字加字母如 4K)`, t.pos);
this.err("unexpected_token", `「${t.v}」不是合法条件名(字母开头,或数字加字母如 4K)`, t.pos);
return null;
}
const id = t.v.toUpperCase();
const b = BUILTIN_ATOM_MAP.get(id);
if (b && !b.supported)
this.err("unsupported_atom", `${id}(${b.name})不支持:音乐资源条件没有迁移`, t.pos, { atom: id });
this.err("unsupported_atom", `${id}(${b.name})不支持:音乐资源条件没有迁移`, t.pos, { atom: id });
else if (!b && !this.custom.has(id) && !this.opts.allowUnknown) {
const suggestions = suggestAtoms(id, this.custom);
this.err(
"unknown_atom",
`未知条件 ${id}${suggestions.length ? `,是不是 ${suggestions.join(" / ")}` : ""}`,
`未知条件 ${id}${suggestions.length ? `,是不是 ${suggestions.join(" / ")}` : ""}`,
t.pos,
{ atom: id, suggestions },
);
}
return { op: "atom", id };
}
if (t.t === ")") this.err("unbalanced_paren", "括号不配对:多余的 )", t.pos);
if (t.t === ")") this.err("unbalanced_paren", "括号不配对:多余的 )", t.pos);
else if (t.t === "bad") this.err("invalid_char", `不认识的字符「${t.v}」`, t.pos);
else this.err("unexpected_token", `「${t.v}」前缺少条件`, t.pos);
return null;
@@ -212,7 +212,7 @@ export function parseExpr(
const expr = p.or();
if (expr && p.i < toks.length && errors.length === 0) {
const t = toks[p.i]!;
if (t.t === ")") p.err("unbalanced_paren", "括号不配对:多余的 )", t.pos);
if (t.t === ")") p.err("unbalanced_paren", "括号不配对:多余的 )", t.pos);
else if (t.t === "bad") p.err("invalid_char", `不认识的字符「${t.v}」`, t.pos);
else p.err("unexpected_token", `「${t.v}」前缺少 & 或 |`, t.pos);
}
+1 -1
View File
@@ -257,7 +257,7 @@ export function episodeNfo(e: NfoEpisode): string {
* 只有一个 XML 声明。只有一集时与 episodeNfo 相同。
*/
export function multiEpisodeNfo(list: readonly NfoEpisode[]): string {
if (!list.length) throw new Error("multiEpisodeNfo: 至少一集");
if (!list.length) throw new Error("multiEpisodeNfo:至少一集");
return toXml(...list.map(episodeNode));
}
+17 -18
View File
@@ -247,7 +247,7 @@ export function describeViolation(v: Violation): string {
case "source.excluded":
return `来源 ${a} 被排除`;
case "source.not_encode":
return `要求蓝光压制,实际 ${a}`;
return `要求蓝光压制,实际 ${a}`;
case "size.too_large":
return `体积 ${fmtBytes(a)} 超过上限 ${fmtBytes(e)}`;
case "size.too_small":
@@ -257,7 +257,7 @@ export function describeViolation(v: Violation): string {
case "size.unit_too_small":
return `每集体积 ${fmtBytes(a)} 低于下限 ${fmtBytes(e)}`;
case "subtitles.missing_language":
return `缺少字幕 ${fmtList(e)}(实际 ${fmtList(a)})`;
return `缺少字幕 ${fmtList(e)}(实际 ${fmtList(a)})`;
case "cut.not_allowed":
return `版本 ${a} 不在允许列表 ${fmtList(e)}`;
case "group.excluded":
@@ -265,11 +265,11 @@ export function describeViolation(v: Violation): string {
case "contentType.not_allowed":
return `内容类型 ${a} 不在允许列表 ${fmtList(e)}`;
case "hdr.missing_required":
return `HDR 需含 ${fmtList(e)} 之一(实际 ${fmtList(a)})`;
return `HDR 需含 ${fmtList(e)} 之一(实际 ${fmtList(a)})`;
case "hdr.excluded":
return `HDR ${fmtList(a)} 被排除`;
case "hdr.not_sdr":
return `要求 SDR,实际带 ${fmtList(a)}`;
return `要求 SDR,实际带 ${fmtList(a)}`;
case "codec.not_allowed":
return `编码 ${a} 不在允许列表 ${fmtList(e)}`;
case "codec.excluded":
@@ -287,35 +287,35 @@ export function describeViolation(v: Violation): string {
case "seeders.too_few":
return `做种数 ${a} 小于 ${e}`;
case "promotion.not_free":
return `非免费(下载系数 ${a})`;
return `非免费(下载系数 ${a})`;
case "tier.no_tier_matched":
return "不命中优先级阶梯的任何一层";
return "不符合优先级规则的任何一层";
case "rule.hard_exclude":
return `命中规则组「${e}」的硬排除 ${fmtList(a)}`;
case "rule.no_layer_matched":
return `不命中规则组「${e}」的任何一层`;
case "upgrade.upgrade_disabled":
return "已有版本且未开启升级";
return "已有版本,且没开启升级";
case "upgrade.at_ceiling":
return "当前版本已达顶档,不再升级";
return "现有版本已到封顶等级,不再升级";
case "upgrade.same_tier":
return "与当前版本同档,不反复更换";
return "与现有版本同等级,不来回替换";
case "upgrade.lower_tier":
return "低于当前版本";
return "比现有版本差";
case "upgrade.below_min_improvement":
return "提升幅度不足最小改进阈值";
return "提升幅度不到最小升级幅度";
case "upgrade.encode_not_qualified":
return `压制组 ${a ?? "未知"} 不在合格名单`;
case "upgrade.not_higher_priority":
return `阶梯 pri 未严格提升(当前 ${JSON.stringify(e)},候选 ${JSON.stringify(a)})`;
return `优先级没有提高(现有 ${JSON.stringify(e)},这个资源 ${JSON.stringify(a)})`;
case "upgrade.not_full_pack":
return "全集洗版只接受整季包";
case "upgrade.no_improvable_units":
return "候选所含单元都没有可提升的";
return "这个资源包含的季集都没有可升级的";
default:
return e === undefined
? `${v.field}: ${v.code}`
: `${v.field}: ${v.code}(期望 ${JSON.stringify(e)},实际 ${JSON.stringify(a ?? null)})`;
: `${v.field}: ${v.code}(期望 ${JSON.stringify(e)},实际 ${JSON.stringify(a ?? null)})`;
}
}
@@ -361,7 +361,7 @@ export function priorityLadder(spec: SlotSpec, r: ParsedRelease, ctx: Evaluation
const name = t.name ?? `第${k + 1}层`;
const fail = tierFailures(t, r, ctx);
if (fail.length === 0) {
trace.push(`${name}:命中,pri=${100 - k}`);
trace.push(`${name}:命中,pri=${100 - k}`);
return { pri: 100 - k, level: k, levelName: name, trace };
}
trace.push(`${name}:${fail.join(";")}`);
@@ -480,7 +480,7 @@ export function evaluateCandidate(
...withRules,
};
if (missing.length > 0)
return { decision: "unknown_field", missing, trace: missing.map((f) => `字段未知,需要补充:${f}`) };
return { decision: "unknown_field", missing, trace: missing.map((f) => `字段未知,需要补充:${f}`) };
if (spec.soft?.tiers?.length && spec.soft.tiersRequired && ladder.level === null) {
const v: Violation = { field: "tier", code: "no_tier_matched" };
return {
@@ -545,8 +545,7 @@ export function evaluateCandidate(
...withRules,
};
}
if (!current.source)
return { decision: "unknown_field", missing: ["current_asset"], trace: ["当前资产交付特征未知"] };
if (!current.source) return { decision: "unknown_field", missing: ["current_asset"], trace: ["现有文件的规格未知"] };
const from = qualityTier(current, spec);
if (up.ceilingTier !== undefined && from.source >= up.ceilingTier)
return reject({ field: "upgrade", code: "at_ceiling", expected: up.ceilingTier, actual: from.source });
@@ -146,7 +146,7 @@ describe("MP 规则组过滤差分(rules-cases.json#cases)", () => {
const text = r.trace.join("\n");
if (!r.passed) expect(text, c.caseId).toMatch(/未命中|硬排除|过滤/);
const withLayers = r.groups.some((g) => g.pri !== null);
if (r.passed && withLayers) expect(text, c.caseId).toMatch(/命中,pri=\d+/);
if (r.passed && withLayers) expect(text, c.caseId).toMatch(/命中,pri=\d+/);
// MP trace 里提到的"不包含任何项"对应我们 ✗ 的原子依据
if (c.mp.trace.some((x) => x.startsWith("不包含任何项")) && !r.passed) expect(text).toMatch(/✗/);
if (c.mp.trace.some((x) => /规则 \w+ 不存在/.test(x))) expect(text).toMatch(/条件不存在/);
+3 -3
View File
@@ -86,7 +86,7 @@ describe("语法", () => {
expect(issue("{{ season | pad('x') }}")?.code).toBe("filter_arg");
expect(issue("{{ airDate | date('MM/DD') }}")?.code).toBe("filter_arg");
expect(issue("{{ title | replace('a') }}")?.code).toBe("filter_arity");
expect(issue("{{ videoformat }}")?.message).toBe("未知变量 videoformat,是否指 videoFormat");
expect(issue("{{ videoformat }}")?.message).toBe("未知变量 videoformat,是否指 videoFormat");
expect(issue("{{ constructor }}")?.code).toBe("unknown_variable");
expect(issue("{{ title | constructor }}")?.code).toBe("unknown_filter");
expect(tryCompileNaming("{% constructor %}").ok).toBe(false);
@@ -245,13 +245,13 @@ describe("校验(保存前)", () => {
it("缺 fileExt、未知变量、作品根;可能为空的提示", () => {
expect(validateNamingTemplate("{{title}}/{{title}}", "movie").errors[0]).toMatchObject({
code: "missing_file_ext",
message: "文件名缺少 {{fileExt}}:模板必须以 {{fileExt}} 结尾",
message: "文件名缺少 {{fileExt}}:模板必须以 {{fileExt}} 结尾",
});
expect(validateNamingTemplate("{{title}}/{{titel}}{{fileExt}}", "movie").errors[0]?.message).toContain(
"是否指 title",
);
expect(validateNamingTemplate("{{title}} ({{year}})/{{title}}{{fileExt}}", "movie").warnings).toEqual([
"year 可能为空,建议写成 {% if year %}…{{year}}…{% endif %}",
"year 可能为空,建议写成 {% if year %}…{{year}}…{% endif %}",
]);
expect(validateNamingTemplate(MP_DEFAULT_NAMING.tv, "tv").warnings).toEqual([]);
});