Files

8.3 KiB
Raw Permalink Blame History

规则运行手册(W9-D)

实现:packages/domain/src/rules/(规则串解析/序列化、内置关键字、规则组链求值)、packages/domain/src/slot/evaluate.ts(接入版本目标评估)、 apps/server/src/modules/acquisition-organize/{routes/rule-groups.ts, routes/rules.ts, services/rules.ts, services/acquisition.ts}、迁移 0033_rule_groups。 设计:docs/design/subscriptions.md §6;MP 对照:docs/porting/mp-rust-logic-parity.md §6、docs/porting/moviepilot-subscribe.md §6。

模型

规则组(rule_group)= 名称 + 类型 + 适用范围 + 结构化层 + 硬排除。规则串(SPECSUB & CNSUB > 4K)只是层的文本表示,存库的是结构化表达式,rule_string 是规范化后的缓存。

字段 含义
kind acquisition 订阅 / upgrade 洗版 / search 搜索
scope mediaTypes[](movie/series/variety)、entityIds[]、siteIds[]、subscriptionId(订阅私有组);空 = 全部
layers[] 每层 {name?, expr},expr 是 {op:atom,id} / {op:not,arg} / {op:and|or,args[]} 树;第 k 层(0 起)命中 pri = 100 − k
hardExcludes[] 表达式列表,任一命中即拒绝
ladderRequired 一层都不中即拒绝(MP 语义,缺省 true);false = 软偏好,不中只是 pri=0
source manual 手动 / chat 对话中用户明确说的 / agent Agent 推断
enabled、revision、fingerprint、suppressedAt、deletedAt 每次修改 revision+1 并写审计 rule_group.*;指纹 = hash(kind+scope+规范化层+硬排除+ladderRequired)
defaultOrder 非 null = 属于该类默认链
legacyKind 由偏好标签迁来或经旧 /preference-tags 写入

自定义条件(rule_atom):对齐 MP 自定义规则 7 字段 —— key(大写)、include[]/exclude[](正则,MP 语法,(?i) 会去掉,统一不区分大小写;过防灾难回溯白名单)、matchFields、unitSizeMb {min,max}(剧集按每集)、minSeeders、publishDelayMinutes {min,max}、free。不允许与内置关键字同名(偏离 MP 的"同 id 覆盖",避免静默改义)。

求值语义(对齐 MP FilterModule)

  1. 组内:硬排除任一命中 → 拒绝(rule.hard_exclude);逐层匹配,首个命中层给 pri;有层且 ladderRequired 而一层不中 → 拒绝(rule.no_layer_matched)。
  2. 多组串联取交集:任一组拒绝即拒绝;pri 取链上最后一个带层的组(没有层的组只起过滤作用)。
  3. 层内 ! > & > |,可加括号;表达式短路求值。未知条件视为不匹配(只可能出现在迁移前的旧数据里,新写入会被拒)。
  4. 版本目标自身的 hardConditions / softPreferences.tiers 保留,视为链首的匿名组;规则组有层时 pri 以规则组为准,升级比较口径自动切到 pri(对照 MP 洗版 pri_order > current_priority)。

引用链(services/rules.ts resolveRuleChain):订阅私有组 → 版本目标 ruleGroupIds(非空时)|| 订阅 params.ruleGroupIds || 订阅默认链 → 版本目标开了升级时再叠加洗版默认链;逐个按 scope 过滤,跳过停用/已删。搜索用 kind: "search" 取搜索默认链(给 release.search / 手动搜索接入)。

候选评估记录(artifact candidate_evaluation)里:pri 为最终 pri;reasons 带 rule_group:<id>@<revision>;trace 逐组逐层写明命中/未命中及每个原子的依据,如 规则组「片源」@r1 第1层 4K:未命中(4K✗ 分辨率 1080p)、规则组「片源」@r1 硬排除 REMUX 命中(REMUX✓ 来源 REMUX)。

内置关键字

GET /rules/keywords 返回全表(含 MP 上游正则/条件原文与我们的判定依据 basis)。

关键字 我们的判定 basis
4K / 1080P / 720P 解析出的分辨率(1080P 含 1080i) field
BLU / REMUX / WEBDL 解析出的来源(原盘 / Remux / WEB-DL+WEBRip) field
BLURAY 来源属蓝光族(原盘、压制、标题带 Blu-ray 的 Remux) field
UHD 标题 UHD / UltraHD field
H265 / H264 视频编码 field
HDR / DOLBY HDR10/HDR10+/HDR Vivid;杜比视界 field
ATMOS / 60FPS 音轨 Atmos;帧率 ≥ 60(均兼容 MP 正则) field
CNSUB 字幕含中文;作品原语言 zh/cn 直接满足(MP tmdb 条件) field
CNVOI / HKVOI / SPECSUB / SDR / 3D MP 正则;CNVOI 原语言 zh 直接满足 regex
GZ / FREE 站点标签 官方/官种/官组;下载系数 = 0 site
音乐 13 条(HIRES、FLAC、AAC…) 不支持,规则串里出现报 unsupported_atom —

端点

端点 说明
GET /rules/keywords 内置 + 自定义条件
POST /rules/parse {ruleString} → {layers, ruleString(规范化,有错为 null), errors[{code, message, position, layer, atom?, suggestions?}]};code:empty empty_layer unknown_atom unsupported_atom unbalanced_paren unexpected_token invalid_char
POST /rules/serialize {layers} → {ruleString}(原子大写,只加必要括号)
GET / PUT /rules/defaults {acquisition[], upgrade[], search[]},按数组顺序串联;类型不符 400
GET/POST /rules、GET/PATCH/DELETE /rules/{id} 列表筛选 kind source enabled includeDeleted q;写入可给 ruleString(优先)或 layers,hardExcludes 元素可以是表达式或单层规则串;PATCH 可带 expectedRevision(不匹配 409);同名 409;条件无效 422;DELETE 被版本目标/订阅引用 409
GET /rules/{id}/revisions 审计回放(含迁移前的 preference_tag.*)
POST /rules/{id}/preview {limit?, sourceReleaseIds?, titles?[{title, subtitle?}], draft?};缺省最近 50 条已识别资源;draft 试算未保存的层/硬排除 → {sampleSize, passed, rejected, priHistogram[{pri,count}], items[{title, passed, pri, level, excludedBy, reasons, trace}]}
GET/POST /rules/atoms、GET/PATCH/DELETE /rules/atoms/{id} 自定义条件;与内置同名 409;被规则组引用不能删 409
/rules/policies* 收录策略版本(草案→预览→发布→回滚),同 /policy-revisions
/rules/suppressions* 抑制记录,同 /preference-suppressions;行多了 ruleGroupId
版本目标 ruleGroupIds POST/PATCH /acquisition-slots 可写,引用不存在的组 400;SlotTemplate.ruleGroupIds 建目标时写入
订阅 params.ruleGroupIds PATCH /subscriptions/{id} 的 params 键

抑制记录(偏好标签语义保留)

  • 停用(enabled: false)或删除规则组 → 写抑制记录(指纹 = 停用前的指纹),suppressedAt 置位。
  • 对话/Agent 来源的组被用户改掉语义 → 旧指纹也写抑制("edited");手动组的编辑不写。
  • POST /rules source=agent 命中有效抑制 → 409;manual/chat 新建同指纹 → 解除抑制。非手动来源的同指纹重复新建 409。

旧端点(弃用,保留到 v0.3)

旧 新 备注
/preference-tags* /rules* 别名直接读写 rule_group(legacy_kind 非空的行);rule.ruleString 会编译:hard → 硬排除,soft → 单层软偏好;其余 rule 键只作记录、不参与评估。指纹沿用旧口径 hash(kind+scope+rule),旧抑制记录照常命中
/policy-revisions* /rules/policies* 同一张表、同一套处理器
/preference-suppressions* /rules/suppressions* 同上;preferenceTagId 对新建规则组回填为 ruleGroupId

preference_tag 表在 0033 后不再写入(数据已按原 id 迁到 rule_group,抑制记录按 preference_tag_id 回填 rule_group_id),回滚迁移前保留。

差分测试

  • 生成:MP_ROOT=/opt/moviepilot-plus/upstream/MoviePilot $MP_ROOT/.venv/bin/python scripts/mp-sync/gen-rules-differential.py,写 packages/domain/test/differential/rules-cases.json(登记的 deviations 重跑保留)。
  • 覆盖:66 条规则链(旧 filter-cases 的 42 条 + 19 条新规则串 + 5 条多组串联)× 32 个种子 = 2112 case;64 个规则串的 pyparsing 结构。
  • 结果:解析结构 64/64 一致、序列化往返一致;pri 2090/2112 一致(98.96%),22 处全是 mp_bug:无来源 2160p HEVC 被 BLU 正则误判原盘(t08)、副标题"国语/国配"被 CNSUB 当中字(t26/t32)、.HDR- 不被 HDR 正则命中(t02)。原 filter-cases 里 15 条"表达不了"的规则现在全部可表达,只剩音乐条件不支持。
  • 临时看差异:UMT_RULES_DUMP=1 deno run -A npm:vitest run test/differential/rules.test.ts(在 packages/domain 下)。