From 0ba947dce3cb5cbe5019a8a914d9004eb1da9570 Mon Sep 17 00:00:00 2001 From: root Date: Wed, 7 Oct 2026 01:52:23 +0800 Subject: [PATCH] =?UTF-8?q?copy(domain):=20=E5=91=BD=E5=90=8D=E6=A8=A1?= =?UTF-8?q?=E6=9D=BF=E3=80=81=E8=A7=84=E5=88=99=E5=88=A4=E5=AE=9A=E3=80=81?= =?UTF-8?q?=E6=94=B6=E5=BD=95=E7=89=88=E6=9C=AC=E8=AF=84=E4=BC=B0=E6=96=87?= =?UTF-8?q?=E6=A1=88=E6=94=B9=E5=85=A8=E8=A7=92=E6=A0=87=E7=82=B9=E5=B9=B6?= =?UTF-8?q?=E5=8E=BB=E6=8E=89=E5=86=85=E9=83=A8=E7=94=A8=E8=AF=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 阶梯 pri / 候选 / 资产交付特征等改成界面用词;同步断言。 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01Cxwgnn2pbzQ7rXv3rq7fei --- .../acquisition-organize/subscription.test.ts | 2 +- packages/domain/src/download-state/index.ts | 2 +- packages/domain/src/naming/engine.ts | 26 +++++++------- packages/domain/src/naming/layout.ts | 8 ++--- packages/domain/src/naming/variables.ts | 35 +++++++++++-------- packages/domain/src/organize/plan.ts | 2 +- packages/domain/src/release-parser/offset.ts | 2 +- packages/domain/src/rules/atoms.ts | 22 ++++++------ packages/domain/src/rules/evaluate.ts | 8 ++--- packages/domain/src/rules/expr.ts | 14 ++++---- packages/domain/src/scrape/nfo.ts | 2 +- packages/domain/src/slot/evaluate.ts | 35 +++++++++---------- .../domain/test/differential/rules.test.ts | 2 +- packages/domain/test/naming.test.ts | 6 ++-- 14 files changed, 86 insertions(+), 80 deletions(-) diff --git a/apps/server/test/acquisition-organize/subscription.test.ts b/apps/server/test/acquisition-organize/subscription.test.ts index 776fe353..dfd7df0a 100644 --- a/apps/server/test/acquisition-organize/subscription.test.ts +++ b/apps/server/test/acquisition-organize/subscription.test.ts @@ -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); }); diff --git a/packages/domain/src/download-state/index.ts b/packages/domain/src/download-state/index.ts index 42d4dfe8..82c97684 100644 --- a/packages/domain/src/download-state/index.ts +++ b/packages/domain/src/download-state/index.ts @@ -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"; diff --git a/packages/domain/src/naming/engine.ts b/packages/domain/src/naming/engine.ts index 20630a62..0d48dbeb 100644 --- a/packages/domain/src/naming/engine.ts +++ b/packages/domain/src/naming/engine.ts @@ -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; diff --git a/packages/domain/src/naming/layout.ts b/packages/domain/src/naming/layout.ts index e42e8ac4..3e580c84 100644 --- a/packages/domain/src/naming/layout.ts +++ b/packages/domain/src/naming/layout.ts @@ -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, diff --git a/packages/domain/src/naming/variables.ts b/packages/domain/src/naming/variables.ts index 4e2348c5..b0e6413c 100644 --- a/packages/domain/src/naming/variables.ts +++ b/packages/domain/src/naming/variables.ts @@ -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, }, diff --git a/packages/domain/src/organize/plan.ts b/packages/domain/src/organize/plan.ts index 16f6c99b..0cbe74fb 100644 --- a/packages/domain/src/organize/plan.ts +++ b/packages/domain/src/organize/plan.ts @@ -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", }); } diff --git a/packages/domain/src/release-parser/offset.ts b/packages/domain/src/release-parser/offset.ts index 1252f02c..d30270fc 100644 --- a/packages/domain/src/release-parser/offset.ts +++ b/packages/domain/src/release-parser/offset.ts @@ -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; diff --git a/packages/domain/src/rules/atoms.ts b/packages/domain/src/rules/atoms.ts index 78c1b12f..6905276f 100644 --- a/packages/domain/src/rules/atoms.ts +++ b/packages/domain/src/rules/atoms.ts @@ -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: "满足" }; } diff --git a/packages/domain/src/rules/evaluate.ts b/packages/domain/src/rules/evaluate.ts index 7fa5340c..44f5bdf8 100644 --- a/packages/domain/src/rules/evaluate.ts +++ b/packages/domain/src/rules/evaluate.ts @@ -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 }; } diff --git a/packages/domain/src/rules/expr.ts b/packages/domain/src/rules/expr.ts index f03a435d..66166c3a 100644 --- a/packages/domain/src/rules/expr.ts +++ b/packages/domain/src/rules/expr.ts @@ -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); } diff --git a/packages/domain/src/scrape/nfo.ts b/packages/domain/src/scrape/nfo.ts index 95ebc2b5..a3127858 100644 --- a/packages/domain/src/scrape/nfo.ts +++ b/packages/domain/src/scrape/nfo.ts @@ -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)); } diff --git a/packages/domain/src/slot/evaluate.ts b/packages/domain/src/slot/evaluate.ts index 3183dfd3..2cc9747e 100644 --- a/packages/domain/src/slot/evaluate.ts +++ b/packages/domain/src/slot/evaluate.ts @@ -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 }); diff --git a/packages/domain/test/differential/rules.test.ts b/packages/domain/test/differential/rules.test.ts index 8a46125f..9efaa8ff 100644 --- a/packages/domain/test/differential/rules.test.ts +++ b/packages/domain/test/differential/rules.test.ts @@ -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(/条件不存在/); diff --git a/packages/domain/test/naming.test.ts b/packages/domain/test/naming.test.ts index 0ca4b8ef..4eada45a 100644 --- a/packages/domain/test/naming.test.ts +++ b/packages/domain/test/naming.test.ts @@ -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([]); });