# 订阅重做 + 规则页(对标 MoviePilot v3) > 需求来源:`00-first-version-plan`(已删除,见 git `ceb66f8`) §8 第 3、4 条(订阅重做、收录策略 + 偏好标签合一)。命名模板另见 `naming-template.md`。 > 上游行为账本:`docs/porting/moviepilot-subscribe.md`;差距清单:`05-subscription-gap`(已删除,见 git `ceb66f8`);规则关键字核对:`docs/porting/mp-rust-logic-parity.md` §6。上游 `jxxghp/MoviePilot` v3 @ `e6820a8`,只记行为,不复制源码。 ## 0. MP 要点与我们的现状 **MP 订阅页**(`pages/subscribe.vue`、`views/subscribe/SubscribeListView.vue`、`components/cards/SubscribeCard.vue`) - 按类型分路由:电影 / 电视剧 / 音乐;每类 Tab「我的订阅 / 热门 / 分享」。 - 筛选(剧集):全部 / 洗版 / 未开始 / 订阅中 / 待定 / 暂停 / 已完成。归类顺序:洗版 → P 待定 → S 暂停 → `lack_episode==0` 已完成 → `completed_episode>0` 订阅中 → 未开始。没有分组,只有筛选。 - 排序:自定义拖拽 / 上次更新 / 添加时间 / 缺集数(剧集),按类型记在本地。批量:全选、启用、暂停、删除。 - 卡片:背景图 + 海报、年份、名称 + 季(S0 显示"特别篇")、进度 `total−lack / total`、洗版进度条(已洗版/已下载双段)、洗版角标、状态角标(只显示 暂停 / 待定)、用户名、上次更新(相对时间)。**不显示**站点、下载器、规则组。 - 菜单:编辑 / 搜索 / 暂停·启用 / 重置 / 分享 / 媒体详情 / 文件统计 / 取消订阅。 - 编辑对话框两 Tab。基础:搜索关键词、总集数、开始集数、质量/分辨率/特效、订阅站点、下载器、保存路径、洗版、全集洗版、用 ImdbID 搜索。进阶:包含、排除、优先级规则组、剧集组、指定季、自定义类别、识别词。"默认订阅规则"用同一对话框,按类型存 `Default{Movie|Tv}SubscribeConfig`。 - 文件统计对话框:按集两 Tab「下载文件 / 媒体库文件」。选季对话框:每季显示集数、缺失/已订阅,可选 普通 / 分集洗版 / 全集洗版。日历:月视图,事件按 `air_date` 聚合,入库状态 无 / 部分 / 完整。 - 进度口径:`lack_episode` 存库(普通 = 媒体库缺集;洗版 = 范围内 note 与 pri 都没有的集),`completed_episode` 响应时派生(洗版按 pri==100 计)。 **MP 规则**(`app/application/rules.py:510-656`、`modules/filter/__init__.py`、`views/setting/AccountSettingRule.vue`) - 内置规则 = 正则原子,默认匹配 `标题+副标题+标签`,可限定字段;可带 `tmdb` 条件(命中即放行,CNSUB/CNVOI 用 `original_language`)。自定义规则字段:id、name、include、exclude、size_range(MB,剧集按每集)、seeders、publish_time,同 id 覆盖内置。 - 规则组 = `name + rule_string + media_type + category`;`>` 分层,层内 `! > & > |` 与括号;第 1 层 pri=100 逐层 −1,一层不中即过滤。多组按 media_type/category 取适用组后**串联 AND**,pri 取**最后一组**。 - 默认组:订阅默认、洗版默认、搜索默认,各为组名列表;订阅 `filter_groups` 非空则覆盖默认。 - UI:三张卡片(自定义规则 / 优先级规则组 / 下载优先规则);规则组编辑 = 组名 + 媒体类型 + 分类 + 可拖拽"优先级 N"层卡片,每层多选 chips(内置 + 自定义 + "排除:X"),保存时层内 `&`、层间 `>`;`|` 与括号只能靠导入字符串。JSON 导入/分享。 **我们** - 订阅 = 本地作品 + 季单元 + `season_scope` + `params`;条件在版本目标(Slot);状态 active/paused/pending/completed/cancelled;已有 `/progress`、`/search`、`/episode-total`、历史。订阅页是表格 + 详情抽屉,进度只算**第一个**版本目标(`subscriptionProgress` 取 `slots[0]`)。 - 版本目标已有:硬条件(`SlotHardConditions`,含 include/exclude、站点、做种、免费)、优先级阶梯 `soft.tiers[]`(层内字段 AND)、三种洗版模式、分集质量账本 `slot_unit`。 - 收录策略(`policy_revision kind=acquisition`,草案→发布→回滚,含 newTitle / defaultSlots)与偏好标签(`preference_tag` + 抑制记录)是两页;**偏好标签没接入评估**(差距 #17)。 - 阶梯层内只能 AND 结构化字段;MP 的 SPECSUB/CNVOI/HKVOI/GZ/ATMOS/60FPS/3D/UHD、跨字段 OR 表达不了(parity §6:43 条样例 ✔22 ~6 ✗15)。 ## 1. 订阅总览 路由 `/console/subscriptions`,页头 Tab:**剧集 / 电影 / 综艺 / 日历(P2)**,选中项写 URL(`?kind=`)。视图切换 卡片 / 列表(记本地)。 ### 1.1 分组(派生,不新增状态值) | 分组 | 条件(按顺序判定) | 对应 MP | | --- | --- | --- | | 暂停 | `status=paused` | S | | 待定 | `status=pending`(未建档 / 无 TMDB 身份),或任一范围内季 `totalKnown=false` | P | | 已完成 | `status=completed`,或 continuous 订阅所有版本目标在范围内全覆盖且 total 已知(卡片仍追下一季时显示"等待新季") | lack=0 | | 追更中 | active 且至少一个单元已入库或在途 | 订阅中 | | 待搜索 | active 且没有任何已入库/在途单元 | 未开始 | "洗版中"不单列分组,作为徽标(任一版本目标开升级且未达顶档)与筛选项。`cancelled` 不在总览,只在"订阅历史"。 默认按分组折叠展示(待定、追更中、待搜索、已完成、暂停),组内按**上次更新**倒序。也可切成平铺 + 排序:上次更新 / 下次搜索 / 缺集数 / 添加时间 / 名称。筛选:分组、洗版中、站点、规则组、来源(用户/Agent/策略)、关键词;全部进 URL query(`use-query-state.ts`)。 **上次更新** = `subscription.last_progress_at`(新列):任一关联版本目标的单元转为 已入库 / 在途,或总集数变化时写入;为空取 `created_at`。与 MP `last_update` 同义(MP 在下载添加时更新)。 ### 1.2 卡片字段 | 区 | 字段 | 数据 | | --- | --- | --- | | 封面 | 占位块:类型图标 + 标题首字(暂无海报;有 TMDB poster_path 后再接) | — | | 标题行 | 名称、年份、类型标签;剧集加季(`S1`,S0 显示"特别篇",following_seasons 显示"S2 起") | 实体、季单元 | | 进度 | `已入库/总`,总未知显示 `?`;下方细条:已入库、在途两段;多版本时显示主版本数字,悬浮列出每个版本 | overview.progress | | 版本徽标 | 每个版本目标一个:名称 + 状态色(已齐 / 缺 N / 在途 N / 洗版中 pri a→目标 b) | overview.slots | | 元信息 | 上次更新(相对时间)、下次搜索(或"搜索已关")、站点范围(≤2 个名 + "+N";空 = 全部站点)、规则组(同样折叠) | | | 状态角标 | 仅 暂停 / 待定 / 洗版中(同 MP 只标异常态) | | | 操作菜单 | 立即搜索、查看对比、编辑、暂停/启用、重置(P2)、删除;点卡片 = 查看对比 | | 批量模式:勾选后 立即搜索 / 暂停 / 启用 / 删除(删除走确认,写审计)。空态:"还没有订阅。从媒体库或 Agent 添加。" ## 2. 对比视图(订阅详情) 路由 `/console/subscriptions/:id`(取代抽屉;抽屉保留为列表页快速预览)。页头:标题、状态、进度、操作按钮;下方三块:**覆盖矩阵**、**版本目标**、**记录**(搜索记录、候选评估、订阅历史)。 ### 2.1 覆盖矩阵 - 列 = 集,按季分段(季标题显示 `已入库/总 · 总集数来源 · 锁定`,可点"设置总集数");行 = 版本目标,每个一行;首行"库内任一版本"汇总。 - 电影:每个版本目标一格。 | 格状态 | 显示 | 来源 | | --- | --- | --- | | 已入库·达标 | 实心,文字 `2160p REMUX` | slot_unit + asset | | 已入库·可升级 | 实心 + 角点,悬浮 `pri 97 → 目标 100` | slot_unit.pri < targetPri | | 在途 | 斜纹,计划状态(已预留/下载中/核验中) | 在途计划 unit_keys | | 延后 | 空心 + 时钟 | slot_deferral | | 缺失 | 空心红框 | computeUnitGaps | | 疑似缺失 | 虚线红框 | total 未知时的集号空洞 | | 未播出 | 灰,显示播出日期 | content_unit.air_date > 今天 | | 范围外 | 淡灰 | episodeRange / season_scope | 点格打开侧栏:资产位置(链到文件管理)、质量与 pri、来源资源与下载任务、该单元的候选评估(接受/拒绝理由 trace)、操作"搜索此集""为此版本补全"。 ### 2.2 多版本与自动补全 - 每个版本目标一行,行尾:缺失数、在途数、"补全此版本"按钮、自动补全状态。 - **自动补全其他版本**:订阅级开关 `params.autoFill: "inherit" | "on" | "off"`,缺省 inherit → 取主版本收录链的 `autoFill`(后者优先,系统缺省关闭)。 - on:非主版本目标像主版本一样独立评估、搜索、下载缺失单元。 - off:非主版本目标只登记缺失,不自动建计划;RSS 来的合格资源记为"延后(未开自动补全)";手动"补全此版本"或 Agent 调 `download.plan` 仍可下。 - 主版本 = 版本目标排序第一个(可在版本目标区拖动)。 - **补全预算** `params.fillBudget {bytesPerDay?, unitsPerDay?}`:只约束非主版本的自动补全计划,计入全局每日预算;超出记延后,次日由 `slots.reevaluate` 重评。计划优先级用 `upgrade` 档(低于追更)。 ## 3. 新建 / 编辑订阅表单 对话框分「基础 / 高级」。订阅维护追踪范围、站点和下载安排,实际收录条件集中在详情的版本编辑器。 | 入口 | 配置 | 生效范围 | | --- | --- | --- | | 新建订阅 · 基础 | 作品、方式、季范围与集范围、总集数、站点、下载器、媒体库目录、保存路径 | 当前订阅 | | 新建订阅 · 收录版本 | 收录策略(缺省按分类、类型、全局匹配) | 只在创建时生成版本;模板更新不覆盖已有版本 | | 新建订阅 · 收录版本 | 其他版本自动补全与预算 | 当前订阅;已有订阅只在详情调整 | | 订阅 · 高级 | 包含/排除、搜索词、主动搜索周期、截止时间、排队优先级 | 当前订阅;共同过滤适用于全部版本 | | 订阅详情 · 收录版本 | 主版本顺序、自动补全与预算 | 当前订阅及作品共用版本;排除其他订阅的版本 | | 订阅详情 · 收录版本规则 | 分辨率等硬条件、软偏好、收录组合规则、洗版组合规则与方式、整季同套、完成条件 | 每个版本独立 | 取消订阅表单的统一分辨率、质量、特效、规则组与洗版编辑,不再向所有版本批量写入。旧 `params.ruleGroupIds/resolution/source/effect` 通过 API 继续兼容,普通保存原样保留;UHD/Atmos 私有过滤不因修改站点或周期而丢失。概览逐版本解析有效规则后合并展示,隐藏订阅私有组。自动补全继承主版本收录链的最后一个显式设置,缺省关闭;订阅显式开/关优先。 保存:创建时同实体同季已有 active 订阅 → 返回 409 并给"打开已有订阅"(差距 #18)。 ## 4. 日历(P2) `/console/subscriptions?kind=calendar`:月视图,事件 = 追踪中订阅范围内集单元的 `air_date`(同日同剧聚合为 `E03–E04`),电影取上映日期;颜色 = 该日事件 已入库 / 部分 / 未入库 / 未播出。数据来自 `refreshMetadata` 写入的集单元 `air_date`(需补:目前只写季单元总集数,不建集单元)。接口 `GET /subscriptions/calendar?from&to`。 ## 5. 后端与 contracts | 端点 | 用途 | 新/改 | | --- | --- | --- | | `GET /subscriptions/overview?kind&group&sort&siteId&ruleGroupId&q&cursor` | 卡片数据:分组、进度、每版本摘要、上次更新、下次搜索、站点、规则组 | 新 | | `GET /subscriptions/{id}/coverage` | 覆盖矩阵:季 × 集 × 版本目标 | 新(取代 `/progress`,后者保留一版兼容) | | `POST /subscriptions/batch` | `{ids, action: search\|pause\|resume\|delete}` | 新 | | `POST /subscriptions/{id}/slots/{slotId}/fill` | 手动补全某版本(可带 unitKeys) | 新 | | `POST /subscriptions/{id}/reset` | 清在途预留、重评(保留资产) | 新(差距 #24) | | `GET /subscriptions/calendar` | 日历 | 新,P2 | | `PATCH /subscriptions/{id}` | params 新键:`ruleGroupIds keywords libraryRootId autoFill fillBudget downloadSavePath` | 改 | contracts(`policy.ts`):`SubscriptionOverviewItem`、`SubscriptionCoverage {seasons[{seasonNumber,total,totalKnown,source,locked,episodes[{episode,airDate}]}], rows[{slotId|null,name,primary,autoFill,cells[{unitKey,state,quality,pri,planId,assetId}]}]}`、`SubscriptionBatch`、`SubscriptionParams` 扩键;规则页见 §6.6。 **覆盖矩阵查询**:一次取 作品全部集单元 → 左连 `asset_unit_coverage`(库内任一版本)、`slot_unit`(按版本目标)、在途计划 `unit_keys`、`slot_deferral`;在服务里用 `computeUnitGaps` 逐版本目标算状态。总览用一条聚合 SQL 出每订阅 × 版本目标的 已入库/在途/总 计数,避免 N+1;超 200 条订阅走游标分页。 DB:`subscription.last_progress_at`;`acquisition_slot.rule_group_ids uuid[]`、`acquisition_slot.position int`(主版本排序);`rule_group`、`rule_atom` 两表(§6);偏好标签迁移。 定时任务(沿用注册表):`subscriptions.search`、`subscriptions.metadata`(补:建集单元与 air_date;完成前强制刷新 = 差距 #8)、`slots.reevaluate`(补:按 `autoFill` 与 `fillBudget` 过滤非主版本)。不新增任务。 ## 6. 规则页(收录策略 + 偏好标签合一) 路由 `/console/rules`,页名"规则"。Tab:**规则组 / 自定义规则 / 默认值 / 收录策略 / 抑制记录**。`/console/preferences` 重定向到 `/console/rules?tab=groups`(带 `origin` 筛选时透传);术语表加"规则组",弃用"偏好标签"(对话与 Agent 产生的规则组以"来源"区分)。 ### 6.1 规则组结构 | 字段 | 含义 | | --- | --- | | name、description | 名称必填唯一;说明一句 | | hard | 硬条件(必须 / 排除),即 `SlotHardConditions`,再加 `atoms {require[], exclude[]}` 引用原子 | | ladder | 优先级阶梯:有序层,每层一个布尔表达式 `expr`(原子 + `! & |` + 括号);第 k 层(0 起)命中 pri = 100 − k | | ladderRequired | 一层不中即拒绝(MP 语义,缺省 true;从 MP 串导入时固定 true) | | scope | `mediaKinds[]`、`entityIds[]`、`siteIds[]`、`subscriptionId`(私有组);空 = 全部 | | origin | manual / conversation / agent_inferred(沿用偏好标签来源) | | status、revision、fingerprint、evidence | 同偏好标签;每次修改 revision+1 并写审计,评估 trace 记 `组id@revision` | | autoFill | 引用本组的订阅缺省是否自动补全其他版本 | **原子**(`rule_atom`):内置(只读)+ 自定义。自定义原子字段对齐 MP 自定义规则:`id`(大写字母数字)、`name`、`include[]`、`exclude[]`、`matchFields`(title/subtitle/labels,缺省全部)、`unitSizeMB {min,max}`(剧集按每集)、`minSeeders`、`publishDelayMinutes {min,max}`、`free`。同 id 不允许覆盖内置(偏离 MP,避免静默改义)。 ### 6.2 内置原子(对齐 MP 关键字) | 原子 | 语义 | 我们的判定 | | --- | --- | --- | | 4K / 1080P / 720P | 分辨率 | `resolution` 字段 | | BLU | 蓝光原盘 | `source = BluRay-Disc`(先修 parity our_bug:Blu-ray+AVC/HEVC 无 x26x 判原盘) | | BLURAY | 标题含 Blu-ray | 来源属蓝光族(原盘/REMUX/压制) | | REMUX / WEBDL | Remux;WEB-DL 或 WEBRip | `source` 字段 | | UHD | 标题含 UHD/UltraHD | `parsed.uhd` | | H265 / H264 | 编码 | `videoCodec` | | HDR / DOLBY / SDR | HDR 类;杜比视界;SDR | `hdr` 字段(HDR = 任一非 DV HDR,同 MP 正则口径做差分) | | ATMOS | 全景声 | `audio` 含 Atmos | | 60FPS / 3D | 帧率;3D | `fps>=60`;正则 `3D` | | CNSUB | 中字 | `subtitles` 含 zh(补 CHS/CHT/GB/BIG5);作品原语言 zh 时直接满足(MP tmdb 放行) | | SPECSUB | 特效字幕 | 正则 `特效`(标题+副标题+标签) | | CNVOI / HKVOI | 国语配音 / 粤语 | 正则;CNVOI 作品原语言 zh 时满足 | | GZ | 官种 | 只看站点标签 `官方/官种/官组` | | FREE | 免费 | `downloadVolumeFactor = 0` | | 音乐 13 条(HIRES/FLAC/…) | — | 不做 | ### 6.3 MP 规则串 ↔ 结构化 - 规则组编辑页有"规则串"输入框(高级):输入 `SPECSUB & CNSUB > CNSUB & 4K > 1080P & !BLU` 实时解析,下方同步画出层卡片;反过来编辑层卡片时规则串随之更新(规范化:原子大写、层内按 `|` 优先级加最少括号)。 - 解析错误逐条给位置:未知原子(给相近名)、括号不配对、空层。音乐原子报"不支持"。 - 层卡片编辑:每层是 chips 行,chip = 原子或取反原子;层内 chips 之间可切换 `且/或`,支持"加括号组";拖动层即改优先级(同 MP),层标题显示 `优先级 1 · pri 100`。 - 测试:"用最近资源测试"对 200 条近期资源跑本组,显示每条命中层/被拒原因,和 MP trace 同粒度;也可粘贴标题测试。 - JSON 导入/导出兼容 MP 格式(`name/rule_string/media_type`,自定义规则 7 字段),media_type 映射到 `mediaKinds`,category 丢弃并提示。 ### 6.4 引用关系与默认值 - 版本目标 `rule_group_ids`(有序)+ 订阅 `params.ruleGroupIds`;评估时编译为 SlotSpec:hard 取交集;各组按 scope 过滤后**全部须命中某层**,pri 取**最后一个**带阶梯的组(同 MP),界面在该组上标"决定优先级"。 - 版本目标自身的 hard/soft 字段保留,视为一个匿名组排在最前。 - 默认值 Tab:按类型设 **订阅默认规则组**、**洗版默认规则组**(只在洗版阶段追加,版本目标可独立指定洗版规则组)、**搜索默认规则组**(手动搜索与 Agent `release.search`)、订阅默认表单(§3 默认模式:站点、下载器、库根、版本目标模板、自动补全)。 - 收录策略 Tab:现有策略工作区原样搬入(草案→预览→发布→回滚、newTitle、defaultSlots);`SlotTemplate` 增加 `ruleGroupIds`。 - Agent:能力 `rules.list` 读规则组;新增/修改走 `rules.propose`(origin=agent_inferred,生成待处理事项,批准后生效);命中有效抑制记录的指纹直接拒绝。对话中用户明确说的偏好 origin=conversation,直接生效并可撤销。 ### 6.5 偏好标签迁移 | 旧 kind | 新规则组 | | --- | --- | | hard | hard 条件,ladder 空 | | soft | ladder 单层(或按 rule 顺序多层),`ladderRequired=false` | | version | 规则组 + 在"默认值"里挂到对应版本目标模板 | | target | 转收录策略 newTitle scope,原标签留一条只读规则组作说明 | 指纹、来源、证据、修订历史、抑制记录原样迁移(抑制按新组指纹重算一次,旧指纹保留作别名)。迁移后偏好标签接入评估,关闭差距 #17。 ### 6.6 规则接口 `GET/POST /rule-groups`、`GET/PATCH/DELETE /rule-groups/{id}`、`GET /rule-groups/{id}/revisions`、`POST /rule-groups/parse {ruleString}` → `{ladder, errors[]}`、`POST /rule-groups/render {ladder}` → `{ruleString}`、`POST /rule-groups/{id}/test {sourceReleaseIds?|titles?}`、`GET/POST/PATCH/DELETE /rule-atoms`、`GET/PUT /rule-defaults`、`POST /rule-groups/import`、`GET /rule-groups/export`。抑制记录接口沿用 `/preference-suppressions`(改名 alias `/rule-suppressions`)。 domain:`slot/rule-expr.ts`(词法/解析/求值/规范化输出)、`slot/atoms.ts`(内置原子表)、`SlotTierRule.expr?`;`mapping/slot.ts` 编译规则组 → SlotSpec。差分:`filter-cases.json#rules` 43 条全部重跑,目标 ✗ 从 15 降到 1(只剩音乐)。 ## 6.8 分类策略与两阶段收录 分类仍由已有 TMDB 分类规则计算,收录策略通过 `scope.categories.movie / tv` 指定分类名称,`scope.mediaKinds` 区分电影、剧集、综艺。默认选择为显式订阅策略、分类策略、类型默认、全局默认;同层使用发布时间顺序。一个策略模板创建一个独立保留版本,分辨率范围是该版本的硬边界。单版本可允许过渡分辨率,多版本按模板分开;手动下载不持久化这些选择。 追更与补齐允许混合版本,洗版可配置 `upgradeThreshold.ruleGroupIds` 与 `consistency: season_release`。成套身份包含分辨率、来源、编码、规范化发布组、HDR、剪辑版,覆盖由实际季集账本决定,单集同样可以组成整季。同档对齐仅用于当前季目标且不得降来源或分辨率;洗版完成要求该目标规格覆盖范围内全部单元。规格与优先级随计划冻结,在途期间不切换套装。 Agent 补充事实后重新进入原规则。新增 `release_facts` 事项沿用现有决策、调查、持续授权和恢复验证,不增加另一套自动化流程。身份和其他来源到 TMDB 的季集映射继续使用已有长效规则及证据。 ## 7. 实施拆分与估算 | 包 | 内容 | 估 | 依赖 | | --- | --- | --- | --- | | R1 domain | rule-expr 解析/求值/串互转、内置原子(含 BLU/CHS 修正)、多组编译、差分样例 | M 2 天 | — | | R2 server 规则 | 迁移(rule_group、rule_atom、slot.rule_group_ids)、CRUD/parse/render/test/import、偏好标签迁移、评估接入、Agent `rules.propose` | L 3 天 | R1 | | R3 web 规则页 | 5 个 Tab、层卡片编辑 + 规则串双向、测试面板、`/preferences` 重定向、术语表 | L 3 天 | R2 | | S1 server 订阅 | last_progress_at、overview 聚合、coverage 矩阵、batch、fill、reset、params 新键、autoFill/fillBudget 调度、查重 409、集单元与 air_date | L 3 天 | R2(ruleGroupIds) | | S2 web 总览 | 类型 Tab、分组/排序/筛选、卡片、批量 | M 1.5 天 | S1 | | S3 web 详情 | 覆盖矩阵 + 格侧栏、版本目标区(排序、补全、自动补全)、编辑表单(两 Tab、默认模式) | L 2.5 天 | S1 | | S4 日历(P2) | 接口 + 月视图 | M 1 天 | S1 | 并行:R1 与 S1 的非规则部分可同时开工;R3 与 S2/S3 可并行。合计约 15 人日(不含 P2 约 14)。 验收:`subscription.test.ts` 加 覆盖矩阵、自动补全 off 不建计划、补全预算延后、多组 pri 取最后组;e2e 加"双版本订阅一版齐一版缺 → 矩阵正确 → 手动补全"。