19 KiB
19 KiB
05 订阅业务差距与增强清单(对标 MoviePilot v3 @ e6820a8)
上游行为见
docs/porting/moviepilot-subscribe.md(下称"账本",§ 号指账本章节)。本文只比对行为,不含代码。 我们的实现范围:packages/domain/src/{slot,acquisition}、apps/server/src/modules/acquisition-organize/{services,handlers.ts,routes}、packages/contracts/src/policy.ts、迁移0004_content/0008_policy;对照需求 REQ §5.4–5.5、§6,任务书 00 §3.1 第 5 条。 核验日期 2026-09-27,基于当前工作区(含未提交改动)。
0. 结论摘要
- 架构差异是有意的:MP 是"订阅行 = 身份+季+过滤参数+下载事实(note)"的单表模型,完成即删除;我们是"订阅(追踪意图)+ Slot(收录目标/条件)+ 内容单元账本 + 资产覆盖"的分层模型,事件驱动(
release.resolved → evaluateAcquisition)。REQ 要求的分层(多 Slot、升级不覆盖、确定性评估、预算)已具备骨架。 - 可比语义 34 项(另 2 项不迁移):已覆盖/等价 11 项(32%),部分 7 项(21%),缺失 16 项(47%)。
- 最大风险不在"缺功能",而在两处会导致错误结果的缺陷:①剧集完成判定固定
totalKnown: true且只按"已见过的单元"算,once订阅会在 TMDB/发布只露出前几集时提前完成(违反 REQ §5.4);②Slot 同时只允许一个在途计划,忙时到达的发布被记slot_busy后永不重评(next_evaluate_at未使用),连续更新的剧集会漏集。
1. MP 语义 → 我们现状 → 处置
状态:✅ 相同/等价 · ◐ 部分 · ✗ 缺失 · ↔ 有意不同(REQ 已覆盖)。优先级 P0(错误结果/主链路)> P1(常用能力)> P2(增强)。工作量 S ≤0.5 天,M 1–2 天,L 3 天+。
1.1 创建与目标范围
| # | MP 语义(账本 §) | 现状(文件) | 状态 | 处置 |
|---|---|---|---|---|
| 1 | 订阅 = (source,id,季) 一行;多季拆多行(§1、§2) | 订阅指向本地实体 + season_unit_id + season_scope;条件在 Slot(routes/rules.ts subscriptions.create,0008_policy) |
↔ | REQ 增强已覆盖,不照抄 |
| 2 | 创建时补 TMDB 季集表,算 total_episode,插件可只升(§2.3) |
创建不取季集;content_unit 只在解析发布时懒建(sources-identity/service/entities.ts findOrCreateUnit);TMDB 适配器已有 seasons[].episodeCount(adapters/src/tmdb/client.ts)但无人消费 |
✗ | 需补 P0 M:refreshMetadata 写季单元 metadata.episodeTotal+来源,并按集数预建 provisional 集单元;未知则记 totalKnown=false |
| 3 | 同身份同季查重;历史存在则 Follow 不再建(§2.5、§10) | 无查重,同一作品可建多条 active 订阅 | ✗ | 需补 P2 S:创建时同实体+同季 active 订阅返回 409 或幂等返回旧行 |
| 4 | 标题识别失败则创建失败(§2.2) | titleQuery 订阅可建,但全链路只按 media_entity_id 取订阅,titleQuery 订阅永远不被处理(services/acquisition.ts evaluateRelease) |
✗ | 需补 P1 M:创建时同步 catalog.lookup/identity.resolve,需确认的进待处理中心;确认后回填 media_entity_id |
| 5 | start_episode/total_episode 裁剪范围(§3.3) |
只有季粒度(season_unit_id);无起始集、无手动总集数锁 |
✗ | 需补 P1 S:subscription.params.episodeRange {start,end?, locked},评估与完成都按范围裁剪 |
| 6 | 默认订阅配置按类型回落(§2.4) | 已发布 acquisition 策略的 defaultSlots 自动建 Slot(routes/rules.ts、AcquisitionPolicyRules) |
✅ | 照现状 |
| 7 | 订阅级下载器/保存路径(§1) | params.downloaderInstanceId;保存路径取实例(services/acquisition.ts gates) |
✅ | 照现状 |
1.2 缺集与匹配
| # | MP 语义 | 现状 | 状态 | 处置 |
|---|---|---|---|---|
| 8 | 缺集 = 媒体库缺集 ∩ 订阅范围 − note(含在途)(§3) | satisfiedUnits=该 Slot 计划产出资产+当前资产的 asset_unit_coverage(services/acquisition.ts);在途计划不算单元而是锁整个 Slot |
◐ | 需补 P0(并入 #22):domain 新增 computeUnitGaps(required, covered, inflight, range);在途单元计为"已预留"而非锁 Slot |
| 9 | 标题/年份/别名匹配 + ID 冲突复核(§5 第 3–4 步) | 身份在 sources-identity 解析,给证据等级,needs_confirm 进待识别池;评估只接 resolved 发布 |
↔ | REQ 增强已覆盖(更严格),不照抄 |
| 10 | 季匹配:多季包拒、无季号视为 S1(§5 第 5 步) | current_season + season_unit_id 时按季号过滤;实体级 Slot 接受任何季;多季包不拒 |
◐ | 需补 P1 S:current_season 未设 season_unit_id 时取创建时最新季;多季包按 Slot 范围判定覆盖而非直接拒 |
| 11 | 规则组 > 分层 → pri_order(§6.1) |
Slot hard/soft + qualityTier + softRank 确定性排序(domain/src/slot/evaluate.ts) |
↔ | REQ §6.1 已覆盖 |
| 12 | include/exclude 正则(标题+副标题+标签)(§5 第 8 步) | 只有 excludeGroups;无关键词包含/排除 |
✗ | 需补 P1 S:SlotHardConditions.include/exclude(正则白名单校验,匹配 title+subtitle+labels) |
| 13 | 订阅站点范围 ∩ 系统 RSS 站点(§5 第 1 步,get_sub_sites) |
无;任何站点的发布都参与 | ✗ | 需补 P1 S:subscription.params.siteIds,评估时 rel.site_id 不在范围 → skipped site_out_of_scope |
| 14 | 体积区间,剧集按每集体积(§6.1 __match_size) |
min/maxSizeBytes 按整包体积(evaluate.ts checkHardConditions) |
◐ | 需补 P1 S:增加 perUnitSize(体积/覆盖单元数),季包与单集同口径 |
| 15 | 做种数/促销/发布时间规则(§6.1) | 无 | ✗ | 需补 P2 S:hard 增加 minSeeders、freeOnly;站点字段已在 source_release |
| 16 | 订阅识别词 custom_words(§5 第 2 步) | 无订阅级识别词 | ✗ | 需补 P2 M:放 identity 层(实体级别名/偏移),不放订阅 |
1.3 搜索与周期
| # | MP 语义 | 现状 | 状态 | 处置 |
|---|---|---|---|---|
| 17 | RSS/首页刷新后逐订阅匹配(§5) | Feed 轮询 + 站点快照 → 识别 → release.resolved 事件逐发布评估(acquisition-organize/index.ts outboxRoutes) |
✅ | 照现状(事件驱动比批量匹配更及时) |
| 18 | 订阅主动搜索:新订阅 5 分钟、定时 24h(默认关)、手动(§4、§11) | 只在创建/改 Slot 时对本地已索引的最近 50 条重评(kickEvaluate);release.search 能力支持实时补查但无订阅调度 |
✗ | 需补 P0 M:searchSubscription 任务 = 按实体标题/别名调 snapshotSite(query) → 走同一识别评估链;新订阅立即一次,缺口 Slot 按间隔(默认 24h,站点限流器兜底),手动按钮 |
| 19 | 忙时/无结果后下次还能再匹配(批量匹配天然重试)(§5) | Slot reserved/downloading/verifying 时发布记 slot_busy 后不再评估;next_evaluate_at 有列无消费 |
✗ | 需补 P0 S:Slot 离开忙态(verify_passed/failed、release)时入队按 Slot 重评;巡检消费 next_evaluate_at |
| 20 | 元数据定时刷新 6h,完成前再刷总集数(§8) | 无订阅相关元数据刷新调度 | ✗ | 需补 P0 M(并入 #2):追踪中的剧集每 6h refreshMetadata,完成判定前强制刷新一次 |
1.4 洗版/升级
| # | MP 语义 | 现状 | 状态 | 处置 |
|---|---|---|---|---|
| 21 | 电影洗版:pri 严格提升,100 即完成(§6.2) | evaluateCandidate upgrade:minTierSteps、ceilingTier、压制组白名单、同档不换 |
✅ | REQ §6.1 已覆盖(更细) |
| 22 | 分集洗版:episode_priority 按集记录质量,只下能提升的集(§6.2) |
剧集 Slot 只有一个 current_asset_id;升级比较用"最近导入的那个资产";导入后 current_asset_id = mainAssets[0](services/organize.ts verifyAndImport) |
✗ | 需补 P1 L:按 (Slot, content_unit) 记录当前资产与质量档(新表或 coverage 加 slot_id+tier);升级按单元比较,候选只下"能提升的单元" |
| 23 | 全集洗版 / 分集洗版整包优先(整包须严格高于每集)(§6.2) | 无 | ✗ | 需补 P2 M:Slot packPolicy: per_unit / full_pack / prefer_pack,门槛照抄 MP |
| 24 | 洗版后旧文件由整理层覆盖/删除 | 新版核验入库后旧资产保留,生成"旧版本待处置" decision(organize.ts) |
↔ | REQ §6.2 增强已覆盖 |
1.5 状态、完成、回写
| # | MP 语义 | 现状 | 状态 | 处置 |
|---|---|---|---|---|
| 25 | N/R/P/S;S 不参与任何动作(§7) | active/paused/completed/cancelled;非 active 订阅的 Slot 被跳过;无订阅 Slot 仍追踪 |
✅ | 照现状 |
| 26 | P 待定:可搜索不可完成(§7) | 无对应状态;domain unitCompletion 有 keepTracking/totalKnown 但服务端未用 |
◐ | 用 #27 的 totalKnown 替代 P,不新增状态 |
| 27 | 完成判定按目标范围与 total(§8) | slotComplete 固定 { totalKnown: true },required = 已存在的 main 集单元(懒建,只含见过的集)(organize.ts slotComplete) |
◐ | 需补 P0 S:required 取季单元 episodeTotal(或预建单元);total 未知 → complete=false, keepTracking;once 订阅只在 totalKnown && complete 时完成 |
| 28 | 总集数只升;回退不低于已存在最高集;手动锁(§8) | 无 | ✗ | 需补 P0 S(并入 #2):domain 纯函数 resolveEpisodeTotal(old, fresh, injected, coveredMax, locked) |
| 29 | 下载后按集回写 note/episode_priority;整包需开种子核对(§9) | 入库后按实际文件写 asset_unit_coverage(domain unitCompletion 只认文件清单) |
↔ | REQ §6.2 已覆盖(更严格) |
| 30 | 从整季包按需选集下载(仅 qB/TR)(§9 第 4 步) | qB 适配器有 setFilePriority(adapters/src/qbittorrent/client.ts)但提交链路未用;季包整包下载 |
✗ | 需补 P1 M:提交后取文件列表,按 computeUnitGaps 只勾缺失单元文件;计划记 selectedUnits |
| 31 | 同一订阅一轮可下多个资源(batch_download 逐步扣减 lefts)(§9) | 每 Slot 同时一个计划;一个发布只给一个 Slot(planned 变量) |
✗ | 需补 P0 M:允许同 Slot 多个"单元不相交"的在途计划(预留粒度从 Slot 改为 Slot×单元);部分唯一索引相应调整 |
| 32 | 完成 = 写历史 + 删订阅 + outbox 事件(§7) | status=completed 保留行,status_reason 记原因 |
↔ | 已覆盖(保留行即历史) |
| 33 | 重新订阅 / 重置(清事实,state=R)(§7、§10) | subscriptions.update 可把 status 改回 active;无"重置" |
◐ | 需补 P2 S:POST /subscriptions/{id}/reset 把 Slot 置回 empty/保留资产并重评 |
| 34 | 优先级:无显式排队(MP 串行 + 随机休眠) | plan.priority 按 即时>追更>新作品>升级 写入,但提交/巡检不按它排序(services/downloads.ts sweepReservedPlans) |
◐ | 需补 P2 S:巡检与 deferred 恢复按 sortByPriority 出队 |
| — | 分享/Fork/Follow/热门/Seerr、完成否决插件事件(§10) | 无 | — | 不迁移(REQ 未要求;否决由 decision/approval 承担) |
2. REQ 对 MP 的增强点:实现情况
| 增强点(REQ) | 期望 | 现状(文件) | 判定 | 缺口 |
|---|---|---|---|---|
| Slot 多目标(§6.1) | 每作品多 Slot,独立状态/条件/升级;双版本须不同物理资产 | 多 Slot + 独立状态机 ✅(domain/slot/state.ts、acquisition_slot);require_distinct_asset 只存字段,checkDistinctAssets 未接入评估/入库 |
◐ | 评估时排除已被同组其他 Slot 占用的资产;一个发布可同时满足多个 Slot 时按组规则决定是否复用 |
| 升级不覆盖旧版(§6.2) | 新版核验后切换,旧版按保留策略另行处理 | 升级计划 purpose=upgrade;核验通过才切 current_asset_id;旧资产只生成 decision(organize.ts) |
✅ | 剧集按单元切换依赖 #22 |
| 追更总集数未知不结束(§5.4) | 总集数未知保持追更 | domain unitCompletion({totalKnown}) 已支持;服务端硬编码 true,且无总集数来源 |
✗ | #2、#27、#28(P0) |
| 仅本季/接续后续季(§6.5) | 用户添加默认接续;Agent 推荐默认追完当前季;特别篇/重启不算下一季 | season_scope 字段 + 用户创建默认 following_seasons ✅;current_season 仅在有 season_unit_id 时生效;agent 来源未强制默认 current_season;特别篇/同名重启无判定 |
◐ | origin=agent 默认 current_season;新季出现时对 following 订阅建季级 Slot 或记录;特别篇(S00/同名重启)进 decision |
| Agent 主观选片接口(§6.5) | 明确条件代码执行;Agent 挑选记证据理由、独立预算、不得放宽硬限制 | release.search + download.plan(slotId, releaseId) 走同一确定性闸门 ✅(acquisition-organize/capabilities.ts);无"Agent 选择理由/证据"字段,无 Agent 独立预算 |
◐ | plan 增加 selectedBy: agent + rationale/evidence;预算加 agentBytesPerDay/agentNewTitlesPerDay |
| 偏好标签与抑制(§6.7) | 标签结构化、可编辑、取消留抑制,Agent 不得据旧证据加回 | CRUD、修订历史、抑制记录、agent_inferred 遇抑制拒绝 ✅(routes/rules.ts);标签未参与任何评估(hard 排除/soft 排序/version 偏好均未映射到 SlotSpec 或新作品收录) |
◐ | 评估前把 active 标签编译进 SlotSpec(soft→softRank 加一维,hard→新作品/Slot 硬排除),拒绝理由写标签 id |
| 预算与优先级(§6.4、§6.8) | 每日新增作品/字节/站点比例风险/存储保留/并发;即时>追更>新作品>升级 | 新增作品、字节、并发 ✅(domain/acquisition/budget.ts,gates 中执行,预留计入);优先级写入 plan ✅;站点比例风险、存储保留、Agent 独立预算 ✗;队列不按优先级 |
◐ | #34;站点比例 P2(需站点用户数据) |
| 临时集单元(§5.4) | TMDB 未更新可建 provisional 单元继续下载入库 | 解析发布即建 content_unit(status=provisional) ✅(entities.ts);但完成判定把"已见单元"当全集 |
◐ | 与 #27 联动:provisional 单元参与覆盖,但不代表 total |
| 三模式判定(§6.3) | 新作品看首播日期,追更看账本,升级独立于日期 | determineMode ✅(domain/acquisition/mode.ts),新作品需已发布策略 + 授权 |
✅ | — |
| 回退条件(§6.1) | 指定等待期后接受次选 | fallback 只映射 unknownPolicy(packages/mapping/src/slot.ts) |
✗ | P2 M:fallback.after{days, relax},依赖 #19 的定时重评 |
3. 订阅增强实施清单(按优先级)
P0(错误结果/主链路,建议 W5 首批)
- 总集数来源与账本:
refreshMetadata把 TMDBseasons[].episodeCount写入季单元并预建 provisional 集单元,插件/人工值只升、可锁 —apps/server/src/modules/sources-identity/service/{metadata,entities}.ts、packages/adapters/src/tmdb/client.ts(已有字段)。 - 总集数纯函数:
resolveEpisodeTotal(old, fresh, injected, coveredMax, locked)(只升;回退不低于已覆盖最高集)—packages/domain/src/slot/(新episode-total.ts)+ 单测(账本 §13 样本 1、9)。 - 完成判定修正:
slotComplete用季单元 total 与totalKnown,未知或未齐 → 继续追踪;once订阅仅totalKnown && complete完成 —apps/server/src/modules/acquisition-organize/services/organize.ts、packages/domain/src/slot/completion.ts。 - 缺集纯函数:
computeUnitGaps(required, covered, inflight, range),评估与完成共用 —packages/domain/src/slot/(新gaps.ts)、services/acquisition.tssatisfiedUnits。 - Slot×单元预留:同 Slot 允许多个单元不相交的在途计划,
BUSY从整 Slot 改为按单元冲突;调整部分唯一索引 —services/acquisition.tsreserve、packages/db/migrations(新迁移)、domain/slot/state.ts(剧集态语义)。 - 忙后重评与定时重评:Slot 离开忙态入队
evaluateAcquisition{acquisitionSlotId};巡检消费next_evaluate_at—services/organize.ts、services/downloads.ts、acquisition-organize/index.ts循环。 - 订阅主动搜索任务:
searchSubscription(新订阅立即 + 缺口 Slot 每 24h + 手动),按实体标题/别名调snapshotSite({query})后走原识别评估链 —apps/server/src/modules/sources-identity/{tasks,scheduler}.ts、acquisition-organize/routes/rules.ts(手动端点)、packages/contracts/src/api.ts。 - 追踪剧集元数据刷新周期:active 剧集订阅每 6h
refreshMetadata,完成判定前强制一次 —sources-identity/scheduler.ts。
P1(常用过滤与季包)
- 关键词包含/排除:
SlotHardConditions.include/exclude(正则白名单校验)—packages/domain/src/slot/{types,evaluate}.ts、packages/contracts/src/policy.ts、packages/mapping/src/slot.ts。 - 订阅站点范围:
params.siteIds,评估跳过范围外站点并记理由 —services/acquisition.ts、packages/contracts/src/policy.ts。 - 每集体积口径:
perUnitSize按覆盖单元数折算 —domain/slot/evaluate.ts。 - 集范围:
params.episodeRange{start,end?,locked}裁剪评估与完成 —services/acquisition.ts、services/organize.ts、contracts。 - 季包按需选集:提交后读文件列表,
setFilePriority只下缺失单元文件,计划记selectedUnits—services/downloads.ts、packages/adapters/src/qbittorrent/client.ts。 - 按单元质量账本(分集升级):(Slot, unit) → 当前资产 + 质量档;升级按单元比较,候选只取能提升的单元 — 新迁移、
services/{acquisition,organize}.ts、domain/slot/evaluate.ts。 - titleQuery 订阅落地:创建时解析身份,需确认进待处理中心,确认后回填实体并重评 —
routes/rules.ts、sources-identity/service/resolve.ts。 - 季范围语义:
current_season无season_unit_id时固定到创建时最新季;origin=agent 默认 current_season —routes/rules.ts、services/acquisition.ts。 - 偏好标签接入评估:active 标签编译进 SlotSpec/新作品过滤,理由记标签 id —
services/acquisition.ts、packages/mapping/src/slot.ts。
P2(增强)
- 订阅查重(同实体同季 active 幂等)—
routes/rules.ts。 - 全集/整包优先升级模式
packPolicy—domain/slot/{types,evaluate}.ts、services/acquisition.ts。 - 做种数/促销硬条件 —
domain/slot/evaluate.ts。 require_distinct_asset接入评估与入库 —services/{acquisition,organize}.ts、domain/slot/completion.ts。- Agent 选片理由/证据字段与独立预算 —
acquisition-organize/capabilities.ts、domain/acquisition/budget.ts、contracts。 - 回退等待期
fallback.after—packages/mapping/src/slot.ts、domain/slot/evaluate.ts。 - 订阅重置端点 —
routes/rules.ts。 - 队列按
sortByPriority出队 —services/downloads.ts。 - 实体级识别词/集偏移(替代 MP custom_words)—
sources-identity。
不迁移:订阅分享/Fork/Follow/热门、Seerr 入口、完成否决插件事件、搜索随机休眠(由站点限流器替代)。