62 lines
6.8 KiB
Markdown
62 lines
6.8 KiB
Markdown
# ADR-006:生命周期事件与功能模块约定
|
||
|
||
- 状态:采纳(2026-10-06);2026-10-12 精简通用链式钩子,保留生命周期事件和功能模块约定。
|
||
- 关联:ADR-002(outbox 与任务引擎)、`packages/contracts/src/events.ts`(`LIFECYCLE_EVENTS`)、`packages/contracts/src/features.ts`(`FEATURES`、`SLOT_POSITIONS`)、`apps/web/app/lib/slots.ts`。
|
||
- 参照:MoviePilot v3 `app/schemas/types.py` 的 `EventType`(广播)与 `ChainEventType`(链式),分发在 `app/runtime/events.py`。
|
||
|
||
## 背景
|
||
|
||
外围功能(补字幕、以后的入库后通知、自动补字幕等)需要在业务节点上「知道发生了什么」或「插一手」。MoviePilot 用插件系统 + 两类事件解决。我们是内部工具,用户定了:
|
||
|
||
- 不做插件系统、插件市场、启停开关;
|
||
- 做「功能模块」约定:功能清单只给智能体知道;补齐生命周期事件(参考 MP 的两类事件);前端插槽要做、默认直接显示。
|
||
|
||
## 决定
|
||
|
||
### 1. 业务调用与事后通知
|
||
|
||
同步规则由所属模块 service 明确实现,路由、能力与任务共用业务入口。跨模块需要同步取得结果时显式调用 service。
|
||
|
||
需要在业务提交后可靠执行的动作(补字幕、通知、预热等)订阅生命周期事件。业务事务内调用 `ctx.outbox.emit(trx, …)`,与业务写入一起提交或回滚;payload 只放 ID 和少量判断事实。中继至少一次投递成任务,接收方按 ID 回查业务表并幂等执行。
|
||
|
||
原先参考 MP `ChainEventType` 实现的通用 rewrite/intercept 钩子没有实际业务注册者,本轮删除其注册表、挂点与调用层。以后出现真实扩展需求时再判断抽象方式。`modules/decision-hooks.ts` 是已有待处理事项的业务处理器,语义不同,继续保留。
|
||
|
||
### 2. 广播事件清单
|
||
|
||
payload schema 都在 `EVENT_PAYLOADS`(`emit` 时校验,不合法直接抛错)。已有事件保持原样;`download.status_changed` / `organization.changed` 仍是状态流(SSE 与增量游标用,每次变化都发),下表是给功能模块订阅的生命周期事件。
|
||
|
||
| 事件 | 含义 | 发出位置 | 幂等键 |
|
||
| --- | --- | --- | --- |
|
||
| `release.resolved` | 资源识别完成(已有) | sources-identity `resolve.ts` 等 | 已有 |
|
||
| `subscription.created` | 订阅已建 | 订阅 API 新建、策略自动建(`acquisition.ts`) | `subscription.created/<id>` |
|
||
| `subscription.updated` | 订阅已改(参数、暂停、恢复、重新订阅;`changes` 列字段) | 订阅 API 修改、状态动作 | 无(每次改动一条) |
|
||
| `subscription.completed` | 订阅已完成 | `completeSubscription`、停止日期到达 | `…/<id>/<历史行 id>`、`…/<id>/stop_after:<日期>` |
|
||
| `subscription.cancelled` | 订阅已取消(删除即取消) | 订阅 API 删除 / 改状态为 cancelled | 无 |
|
||
| `download.submitted` | 下载已提交(已有) | `downloads.ts` 提交 | 已有 |
|
||
| `download.completed` / `download.failed` / `download.removed` | 下载完成 / 失败 / 移除 | `downloads.ts` `emitStatus`(与 `download.status_changed` 同事务) | `<type>/<任务 id>/g<代数>` |
|
||
| `organization.finished` | 整理结束,`outcome` = succeeded / partial / failed(统一 `organization.changed` 的终态) | `emitRecord`(所有整理记录写入都经过它) | `organization.finished/<记录 id>/<change_seq>` |
|
||
| `scrape.completed` / `scrape.failed` | 刮削完成(含部分 / 跳过)/ 失败 | `scrape.ts` `runScrapeRecord` 收尾 | `<type>/<记录 id>/<结束时间>` |
|
||
| `subtitle.written` | 字幕已写入(每个文件一条) | 补字幕 `applySubtitles` | `subtitle.written/<写入 id>` |
|
||
| `media.watched` | 作品进入关注(建订阅、开始下载、用户搜索 / 手动认定) | 调用方:订阅 API、`submitDownload`、`resolveRelease` | `media.watched/<作品>/<原因>/<触发对象 id>` |
|
||
| `media.metadata_refreshed` | 作品元数据已刷新 | `refreshMetadata` 任务处理器(调用方) | `…/<作品>/<任务 run id>` |
|
||
| `media.episode_structure_changed` | 季集结构已切换(剧集组) | `setEpisodeGroup` 事务内 | `…/<作品>/<时间>` |
|
||
| `site.unreachable` / `site.recovered` | 站点不可达 / 恢复 | 列表抓取连续失败到阈值(与待处理同口径)、登录失效、站点检测结果跨越 | 列表抓取带 `…/<站点>/<快照 id>`,其余只在跨越时发 |
|
||
|
||
幂等键用斜杠分隔,与已有的 `<type>:<id>` 键分开(`download.completed:<id>` 已被 `download.status_changed` 占用,outbox 幂等键全表唯一)。
|
||
|
||
**订阅**:模块在 `outboxRoutes` 里写 `type → 路由函数`。生命周期事件可以有多个订阅方(`collectOutboxRoutes` 按模块顺序合成),其它事件(内部流程,如 `release.observed → resolveObservation`)仍只能一个模块认领。路由函数返回任务、任务数组或 `null`(不建任务);中继对第一个任务用事件的幂等键、其余用 `<键>#<任务名>`。没有订阅方的生命周期事件也会被中继认领、标记已投递,并镜像到 SSE(`worker/index.ts` 的镜像前缀)。
|
||
|
||
### 3. 功能模块约定
|
||
|
||
一个外围功能 = 服务端一个 `ServerModule` + contracts 里一份 manifest + (可选)前端插槽注册:
|
||
|
||
- **manifest**(`FEATURES`):名称、说明、怎么用、订阅的事件、提供的能力、页面入口、插槽位置。服务端模块 `manifest: featureManifest("<id>")`;`modules.test.ts` 核对模块与清单一一对应、订阅的事件与 `outboxRoutes` 一致。
|
||
- **功能清单只给智能体**:系统提示「能力地图」一节末尾列出外围功能(`renderFeatures`),能力只列本次可用的。**不做开关**:功能都直接启用。
|
||
- **前端插槽**(`apps/web/app/lib/slots.ts`):位置见 `SLOT_POSITIONS`(侧栏入口、作品页操作区、作品页面板区、整理记录行操作、设置分区)。动作类位置注册 `{ label, icon, visible?, Dialog }`,页面渲染成按钮 / 菜单项、点了打开对话框;面板类位置注册组件,挂载点按顺序渲染。注册写在 `app/features/<功能>.tsx`,`app/features/index.ts` 汇总。**默认直接显示,不做开关**。已挂载:作品页操作区、作品页面板区(`library.$id.tsx`)、整理记录行菜单(`organize.tsx`);侧栏与设置分区先只定义位置。
|
||
- 样板:补字幕(`modules/subtitles`)——manifest、订阅 `organization.finished`(认出值得补字幕的整理,自动补字幕还没做,只记调试日志不建任务)、写入后发 `subtitle.written`、前端在作品页操作区和整理记录行菜单挂「补字幕」。
|
||
|
||
## 不做
|
||
|
||
- 插件系统、热加载、插件市场、功能开关。
|
||
- 把广播事件放进内存队列(MP 做法):我们已有 outbox,事件要能跨进程、跨重启。
|