159 lines
25 KiB
Markdown
159 lines
25 KiB
Markdown
# 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 首批)
|
||
|
||
1. ✅(W5-C)**总集数来源与账本**:`refreshMetadata` 把 TMDB `seasons[].episodeCount` 写入季单元并预建 provisional 集单元,插件/人工值只升、可锁 — `apps/server/src/modules/sources-identity/service/{metadata,entities}.ts`、`packages/adapters/src/tmdb/client.ts`(已有字段)。
|
||
2. ✅(W5-C)**总集数纯函数**:`resolveEpisodeTotal(old, fresh, injected, coveredMax, locked)`(只升;回退不低于已覆盖最高集)— `packages/domain/src/slot/`(新 `episode-total.ts`)+ 单测(账本 §13 样本 1、9)。
|
||
3. ✅(W5-C)**完成判定修正**:`slotComplete` 用季单元 total 与 `totalKnown`,未知或未齐 → 继续追踪;`once` 订阅仅 `totalKnown && complete` 完成 — `apps/server/src/modules/acquisition-organize/services/organize.ts`、`packages/domain/src/slot/completion.ts`。
|
||
4. ✅(W5-C)**缺集纯函数**:`computeUnitGaps(required, covered, inflight, range)`,评估与完成共用 — `packages/domain/src/slot/`(新 `gaps.ts`)、`services/acquisition.ts` satisfiedUnits。
|
||
5. ✅(W5-C)**Slot×单元预留**:同 Slot 允许多个单元不相交的在途计划,`BUSY` 从整 Slot 改为按单元冲突;调整部分唯一索引 — `services/acquisition.ts` reserve、`packages/db/migrations`(新迁移)、`domain/slot/state.ts`(剧集态语义)。
|
||
6. ✅(W5-C)**忙后重评与定时重评**:Slot 离开忙态入队 `evaluateAcquisition{acquisitionSlotId}`;巡检消费 `next_evaluate_at` — `services/organize.ts`、`services/downloads.ts`、`acquisition-organize/index.ts` 循环。
|
||
7. ✅(W5-C)**订阅主动搜索任务**:`searchSubscription`(新订阅立即 + 缺口 Slot 每 24h + 手动),按实体标题/别名调 `snapshotSite({query})` 后走原识别评估链 — `apps/server/src/modules/sources-identity/{tasks,scheduler}.ts`、`acquisition-organize/routes/rules.ts`(手动端点)、`packages/contracts/src/api.ts`。
|
||
8. ◐(W5-C)**追踪剧集元数据刷新周期**:active 剧集订阅每 6h `refreshMetadata`,完成判定前强制一次 — `sources-identity/scheduler.ts`。
|
||
|
||
### P1(常用过滤与季包)
|
||
|
||
9. ✅(W5-C)**关键词包含/排除**:`SlotHardConditions.include/exclude`(正则白名单校验)— `packages/domain/src/slot/{types,evaluate}.ts`、`packages/contracts/src/policy.ts`、`packages/mapping/src/slot.ts`。
|
||
10. ✅(W5-C)**订阅站点范围**:`params.siteIds`,评估跳过范围外站点并记理由 — `services/acquisition.ts`、`packages/contracts/src/policy.ts`。
|
||
11. ✅(W5-C)**每集体积口径**:`perUnitSize` 按覆盖单元数折算 — `domain/slot/evaluate.ts`。
|
||
12. ✅(W5-C)**集范围**:`params.episodeRange{start,end?,locked}` 裁剪评估与完成 — `services/acquisition.ts`、`services/organize.ts`、contracts。
|
||
13. ✅(W5-C)**季包按需选集**:提交后读文件列表,`setFilePriority` 只下缺失单元文件,计划记 `selectedUnits` — `services/downloads.ts`、`packages/adapters/src/qbittorrent/client.ts`。
|
||
14. ✅(W5-C)**按单元质量账本(分集升级)**:(Slot, unit) → 当前资产 + 质量档;升级按单元比较,候选只取能提升的单元 — 新迁移、`services/{acquisition,organize}.ts`、`domain/slot/evaluate.ts`。
|
||
15. ◐(W5-C 仅状态)**titleQuery 订阅落地**:创建时解析身份,需确认进待处理中心,确认后回填实体并重评 — `routes/rules.ts`、`sources-identity/service/resolve.ts`。
|
||
16. ✗ **季范围语义**:`current_season` 无 `season_unit_id` 时固定到创建时最新季;origin=agent 默认 current_season — `routes/rules.ts`、`services/acquisition.ts`。
|
||
17. ✗ **偏好标签接入评估**:active 标签编译进 SlotSpec/新作品过滤,理由记标签 id — `services/acquisition.ts`、`packages/mapping/src/slot.ts`。
|
||
|
||
### P2(增强)
|
||
|
||
18. ✗ 订阅查重(同实体同季 active 幂等)— `routes/rules.ts`。
|
||
19. ◐(W5-C:`upgrade.mode=full_pack` 只收整季包;prefer_pack 未做)全集/整包优先升级模式 `packPolicy` — `domain/slot/{types,evaluate}.ts`、`services/acquisition.ts`。
|
||
20. ✅(W5-C)做种数/促销硬条件 — `domain/slot/evaluate.ts`。
|
||
21. ✗ `require_distinct_asset` 接入评估与入库 — `services/{acquisition,organize}.ts`、`domain/slot/completion.ts`。
|
||
22. ✗ Agent 选片理由/证据字段与独立预算 — `acquisition-organize/capabilities.ts`、`domain/acquisition/budget.ts`、contracts。
|
||
23. ✗ 回退等待期 `fallback.after` — `packages/mapping/src/slot.ts`、`domain/slot/evaluate.ts`。
|
||
24. ◐(W5-C:完成保留行 + PATCH status=active 重新订阅;无清账重置)订阅重置端点 — `routes/rules.ts`。
|
||
25. ✗ 队列按 `sortByPriority` 出队 — `services/downloads.ts`。
|
||
26. 实体级识别词/集偏移(替代 MP custom_words)— `sources-identity`。
|
||
|
||
不迁移:订阅分享/Fork/Follow/热门、Seerr 入口、完成否决插件事件、搜索随机休眠(由站点限流器替代)。
|
||
|
||
## 4. W5-C 实施记录(2026-09-27)
|
||
|
||
对应 §3 的状态已在条目前标注(✅ 完成 / ◐ 部分 / ✗ 未做)。实现要点与落点:
|
||
|
||
| 能力 | 实现 | 落点 |
|
||
| --- | --- | --- |
|
||
| 总集数账本 | 季单元 `content_unit.episode_total/_source/_locked/_at`(迁移 0025);`resolveEpisodeTotal`:变大采用、注入只升、TMDB 回退取 max(新值, 已覆盖最高集)、锁定不改、未公布保持未知;`refreshMetadata` 写入(只建季单元,不预建集单元);手动 `POST /subscriptions/{id}/episode-total`(不锁只升,锁定可调低) | `domain/slot/episode-total.ts`、`sources-identity/service/metadata-totals.ts`、`acquisition-organize/services/subscriptions.ts` |
|
||
| 完成判定 | `slotComplete`:按季 `seasonProgress`(总集数已知且范围内全覆盖才完成;未知 → 继续追踪 + "疑似缺集"待处理 info);pending 订阅不完成;once 订阅完成、continuous+仅本季剧集季齐也完成;完成写 `subscription_history`(行保留,改回 active 即重新订阅) | `services/subscriptions.ts`(经 `applySlotEvent(verify_passed)` 钩入,覆盖 organize 传入的 complete) |
|
||
| 缺集/在途 | `computeUnitGaps(units, covered, inflight, range)`;在途 = 该 Slot reserved/submitted 计划的 `unit_keys`(提交即计入,失败/移除释放);季包总集数已知按缺集展开,未知用整季通配 `s{季}*` | `domain/slot/gaps.ts`、`services/acquisition.ts` |
|
||
| Slot×单元多在途 | 去掉 `acquisition_plan_slot_active_key` 唯一索引;剧集 Slot 在行锁内校验单元不相交后可再预留;Slot 状态 = 在途计划最靠后阶段(`transitionWithInflight`),最后一个结束才落静止态;电影 Slot 仍单在途 | `domain/slot/inflight.ts`、`services/downloads.ts applySlotEvent`、迁移 0025 |
|
||
| 延后与重评 | Slot 忙/单元在途冲突 → `slot_deferral` + 候选评估 `decision=deferred`;计划结束(释放/失败/核验/下载被移除)→ `next_evaluate_at=now`;编排循环 `sweepDueSlots` 入队 `evaluateAcquisition{acquisitionSlotId}`(先按集号评 deferred 再评最近发布);未满足 Slot 按 `slotReevaluateIntervalMs`(缺省 6h)定时重评 | `services/subscriptions.ts`、`index.ts planTick` |
|
||
| 主动搜索 | `searchSubscription`(sources-identity 注册):标题/原名/别名 ≤3 个检索词 × 订阅站点范围内已开启且非 failing 的站点,经 `liveSearch`(适配器 search + 共享限流 + 合并/退避 + `site_snapshot kind=live_search`);新结果走 release.observed → resolved → evaluate,旧结果由任务末尾的订阅重评覆盖;用户新建订阅立即一次,之后 `next_search_at`(`params.searchIntervalHours`,缺省 24h,0 关)由编排循环 `sweepSubscriptionSearch` 到期入队;手动 `POST /subscriptions/{id}/search` | `sources-identity/tasks.ts`、`services/subscriptions.ts`、`routes/rules.ts` |
|
||
| 元数据周期刷新 | 追踪中剧集(active/pending 且有 TMDB 身份)`last_metadata_refresh_at` 超过 `metadataRefreshIntervalMs`(缺省 6h)→ 入队 `refreshMetadata`;**完成前强制刷新未做**(完成用当前账本) | `services/subscriptions.ts sweepMetadataRefresh` |
|
||
| 季包选文件 | 提交后 `files()`(重试 3 次),视频文件按文件名解析集号:需要 = 计划单元(通配 = 未覆盖且不在其它在途计划)→ 其余视频与有集号的附属文件 `setFilePriority(0)`;拿不到清单/有视频认不出集号/包里没有需要的集 → 整包并在 `acquisition_plan.file_selection` 记理由;`unit_keys` 收敛为实际选中集;接管的已有任务不改选择 | `services/downloads.ts selectPackFiles` |
|
||
| 分集质量账本 | `slot_unit`(Slot × 内容单元 → 当前资产 + 质量摘要 + 阶梯 pri);核验通过时登记;升级计划只替换计划内单元;全部已覆盖时按 `improvableUnits` 只取能提升的单元建 `purpose=upgrade` 计划 | `services/subscriptions.ts afterSlotVerified`、`services/acquisition.ts` |
|
||
| 硬条件扩展 | `excludeSources`、`hdr{anyOf,noneOf,requireSdr}`、`codecs/excludeCodecs`、`min/maxUnitSizeBytes`(每集体积,季包按账本总集数折算)、`include/exclude` 正则(白名单校验:长度、反向引用、后行断言、嵌套量词)、`sites/excludeSites`、`minSeeders`、`freeOnly`;站点事实经 `EvaluationContext` 传入,缺失按 unknownPolicy | `domain/slot/{types,evaluate}.ts` |
|
||
| 优先级阶梯 | `soft.tiers[]`(`>` 分层,层内条件同时满足),pri = 100 − 层号,未命中 0(`tiersRequired` 时拒绝);排序先比 pri;`upgrade.basis=priority`(配置阶梯时缺省)要求 pri 严格提升,`targetPri` 顶档;`upgrade.mode` movie / per_unit / full_pack | `domain/slot/evaluate.ts priorityLadder/currentPri/improvableUnits` |
|
||
| trace | 所有评估结果带 `trace`(逐条违规中文说明 + 逐层阶梯),`describeViolation` 统一文案;候选评估记录新增 `pri/trace/unitKeys` | `domain/slot/evaluate.ts`、`mapping/slot.ts`、`contracts/policy.ts CandidateEvaluation` |
|
||
| 状态 | `subscription.status` 新增 `pending`(未建档,或剧集无已接受 TMDB 身份;可搜索/评估,不可完成;`refreshMetadata` 成功后转 active);暂停不评估不搜索 | 迁移 0025、`routes/rules.ts`、`metadata-totals.ts` |
|
||
| API/UI | `GET /subscriptions/{id}/progress`(季总集数 + 每集状态 + Slot 在途/延后)、`POST /subscriptions/{id}/search`、`POST /subscriptions/{id}/episode-total`、`GET /subscription-history`;订阅详情加"立即搜索"、主动搜索时间、追更进度(每集色块 + 设置/锁定总集数) | `contracts/api.ts`、`apps/web/app/routes/console/subscriptions.tsx` |
|
||
|
||
测试:`packages/domain/test/subscription.test.ts`(账本/缺集/多在途迁移/硬条件/阶梯/洗版,覆盖账本 §13 样本 1、3、6、9、11、12);
|
||
`acquisition-organize/__tests__/subscription.test.ts`(多在途 + 延后重评 + 移除释放、季包选文件与整包兜底、总集数未知不结束 + 疑似缺集 + once 完成写历史、pending 不完成/暂停不评估、分集洗版);
|
||
`test/sources-identity/subscription-search.test.ts`(mock NexusPHP 主动搜索、站点范围、手动/暂停、季总集数写入与 pending→active);
|
||
`test/e2e/e9-continuous-series.test.ts`(连续更新 3 集在 Slot 忙时到达全部入库,另一版本延后不重复下载,once 完成写历史);E7 保持通过。
|
||
|
||
遗留:#8 完成前强制刷新;#15 titleQuery 订阅建档回填;#16 季范围语义;#17 偏好标签接入评估;P2 #18/#21–#23/#25/#26;
|
||
`organize.ts verifyAndImport` 仍保留其本地 `slotComplete` 与"once 订阅完成"更新(W4-E4 改执行侧期间不动),实际完成与历史由 `applySlotEvent` 钩子按总集数账本判定;执行侧稳定后应删掉本地判定,改调 `services/subscriptions.ts`。
|