Files
UHD_Media_Tool/docs/design/subscriptions.md
T
rootandClaude Opus 5.5 b343dddbf6 docs: 清理过程文档,CHANGELOG 补齐 0.3.8–0.3.14
- 删除 docs/plan 下 00–18 的计划、评审、验收、发布记录与升级证据,docs/spikes 与 spikes 验证代码
  (保留 spikes/movieclaw-src 本地克隆),以及 4 份一次性审查 / 调研;全部可在 ceb66f8 查到
- MP 对照移到 docs/porting/moviepilot-v3-comparison.md;经验教训改为 docs/plan/lessons.md(补第 25 条)
- docs/plan/README.md:文档放哪、当前状态、待办
- 引用改写:代码注释指向在维护的文档,其余注明已删除与 git 位置;清掉原有 6 个失效链接
- README 开头改为当前状态与入口;CHANGELOG 补齐 0.3.8–0.3.14 与工程变更

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-02 16:08:09 +08:00

237 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 订阅重做 + 规则页(对标 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)。
- on:非主版本目标像主版本一样独立评估、搜索、下载缺失单元。
- off:非主版本目标只登记缺失,不自动建计划;RSS 来的合格资源记为"延后(未开自动补全)";手动"补全此版本"或 Agent 调 `download.plan` 仍可下。
- 主版本 = 版本目标排序第一个(可在版本目标区拖动)。
- **补全预算** `params.fillBudget {bytesPerDay?, unitsPerDay?}`:只约束非主版本的自动补全计划,计入全局每日预算;超出记延后,次日由 `slots.reevaluate` 重评。计划优先级用 `upgrade` 档(低于追更)。
## 3. 新建 / 编辑订阅表单
对话框两 Tab,同 MP 分"基础 / 进阶"。"订阅默认值"(按类型)用同一表单的默认模式,存规则页 §6.4。
| 区 | 字段(界面) | MP 字段 | 我们存放 | 默认 / 显示条件 |
| --- | --- | --- | --- | --- |
| 基础 | 作品(搜索本地作品或 TMDB) | media_id / name | `mediaEntityId` / `titleQuery` | 必填 |
| | 类型 | type | `mediaKind` | 由作品带出 |
| | 季 / 季范围 | season | `seasonUnitId` + `seasonScope` | 剧集;缺省最新季 + 接续后续季(Agent 来源缺省仅本季) |
| | 开始集 / 结束集 | start_episode | `params.episodeRange {start,end}` | 剧集 |
| | 总集数(可锁定) | total_episode / manual_total_episode | `POST /episode-total` | 剧集;显示来源 |
| | 方式 | — | `mode` once / continuous | 电影缺省 once |
| | 优先级 | — | `priority` | tracking |
| | 站点范围 | sites | `params.siteIds` | 空 = 全部启用站点 |
| | 下载器 | downloader | `params.downloaderInstanceId` | 空 = 默认实例 |
| | 库根 | save_path(整理去向部分) | `params.libraryRootId`(新) | 空 = 按类型默认库根 |
| | 版本目标 | best_version / best_version_full | 子表:名称 + 规则组 + 升级(关 / 按集 / 整季 / 电影)+ 目标 pri | 缺省取订阅默认值 |
| 进阶 | 规则组 | filter_groups | `params.ruleGroupIds`(作用于全部版本目标)+ 版本目标 `rule_group_ids` | 空 = 订阅默认组 |
| | 包含 / 排除 | include / exclude | 订阅私有规则组(`scope.subscriptionId`,不在列表页显示) | 正则白名单校验 |
| | 分辨率 / 来源 / 特效 | resolution / quality / effect | 同上,写进私有规则组 hard | |
| | 搜索关键词 | keyword | `params.keywords[]`(新,追加到标题/别名检索词) | |
| | 主动搜索间隔 | —(全局) | `params.searchIntervalHours` | 24;0 = 关 |
| | 自动补全 / 补全预算 | — | `params.autoFill`、`params.fillBudget` | inherit |
| | 截止时间 | — | `params.stopAfter` | |
| | 下载保存路径 | save_path | `params.downloadSavePath`(新,P2) | 空 = 实例默认 |
| 不做 | 剧集组 episode_group | | P2(需 TMDB episode group 适配) | |
| | ImdbID 搜索 search_imdbid | | P2 `params.searchByImdb` | |
| | 自定义类别 media_category | | 用命名模板 `category` | |
| | 识别词 custom_words | | 放身份层(实体别名/集偏移),表单给链接 | |
保存:创建时同实体同季已有 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(只剩音乐)。
## 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 加"双版本订阅一版齐一版缺 → 矩阵正确 → 手动补全"。