diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index 661a6d814..db50921c5 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -685,11 +685,8 @@ flowchart LR 物理文件仍是稳定入口的,不应为了目录规整新增“自己映射自己”的别名,也不应在 canonical 包中 保留多余导出。 -详细的迁移批次、风险、验证命令和插件兼容矩阵见 -[`docs/refactor/backend-architecture-governance.md`](refactor/backend-architecture-governance.md) 与 -[`docs/refactor/backend-module-refactor-compatibility.md`](refactor/backend-module-refactor-compatibility.md)。 -第一阶段分层收口后的进程拓扑、事务所有权、类型化运行时契约、后台可靠性和可观测性路线见 -[`docs/refactor/backend-architecture-next-stage.md`](refactor/backend-architecture-next-stage.md)。 +当前架构现状差距、优化点与建议实施顺序见 +[`docs/refactor/backend-architecture-review.md`](refactor/backend-architecture-review.md)。 --- @@ -706,6 +703,5 @@ flowchart LR | [`docs/subscribe-lifecycle.md`](subscribe-lifecycle.md) | 订阅生命周期详解 | | [`docs/mcp-api.md`](mcp-api.md) | MCP 工具端点说明 | | [`docs/v3t-runtime-governance.md`](v3t-runtime-governance.md) | V3/V3t 运行依赖、故障恢复、GIL 可观测性与兼容退场门禁 | -| [`docs/refactor/backend-architecture-governance.md`](refactor/backend-architecture-governance.md) | 分阶段架构治理、边界门禁与迁移验收 | -| [`docs/refactor/backend-module-refactor-compatibility.md`](refactor/backend-module-refactor-compatibility.md) | 模块迁移与插件兼容层实施矩阵 | -| [`docs/refactor/backend-architecture-next-stage.md`](refactor/backend-architecture-next-stage.md) | 对标优秀 Python 后端后的二阶段任务、验收与回滚方案 | +| [`docs/refactor/backend-architecture-review.md`](refactor/backend-architecture-review.md) | 2026-08 架构评审:现状差距、优化点与实施顺序 | +| [`docs/adr/0007-background-action-reliability.md`](adr/0007-background-action-reliability.md) | 后台动作 E0–E3 可靠性分级与完成语义决策 | diff --git a/docs/refactor/backend-architecture-governance.md b/docs/refactor/backend-architecture-governance.md deleted file mode 100644 index d79d87095..000000000 --- a/docs/refactor/backend-architecture-governance.md +++ /dev/null @@ -1,1477 +0,0 @@ -# MoviePilot V3 后端架构提升与分阶段治理方案 - -> 文档性质:现状审计、目标约束、迁移路线和 AI 实施手册 -> 适用仓库:`MoviePilot`,分支 `v3` -> 审计基线:`7c97d1742`(2026-08-24) -> 相关规范:`AGENTS.md`、`docs/rules/05-architecture.md`、`docs/architecture-overview.md`、`docs/refactor/backend-module-refactor-compatibility.md` - -## 1. 文档目的 - -本文件不是另一份目录说明,也不是一次大规模重构设计稿。它解决四个更具体的问题: - -1. 区分已经完成的物理目录迁移与仍未解决的职责、依赖和运行时契约问题。 -2. 把问题定位到具体模块、类、方法和调用边界,给出可逐批落地的迁移方向。 -3. 为其他 AI 提供可以直接执行的任务边界、兼容约束、验证命令和完成标准。 -4. 在不破坏 V3 插件生态的前提下,逐步收敛宿主内部结构,而不是用一次性改名制造新的兼容层。 - -本文同时记录治理方案和当前工作树的实施状态。2026-08-24 的当前代码已经完成阶段 0-7 的跨层边界收口,并继续完成模块契约、生命周期 owner、命名数据端口、Outbox 和插件运行时治理切片;仍保留的千行级文件属于同一职责域内的兼容 Facade、厂商协议实现或第三方移植代码,不再作为跨层混合问题处理。每个阶段是否完成必须以本文件的机器基线、聚焦测试、插件兼容扫描和完整测试门禁为准,不能只凭目录已经创建判断。 - -### 2026-08-18 收口结论 - -本批次的“全部拆完”指跨层职责和依赖边界完成收敛,不指把所有历史 ABI 类名删除或把每个厂商实现机械切成小文件。当前已验证的关键收口如下: - -1. API、Agent、Workflow、Chain 不再直接构造插件/模块 Runtime 管理器;入口通过 `app.application.plugin.runtime.get_plugin_manager()`、`app.application.module.get_module_manager()` 和 `app.application.scheduling.get_scheduler()` 等端口访问,启动层负责实例装配。 -2. `ChainBase` 不再静态导入模块调度器,`ModuleInvocationDispatcher` 由启动组合根经 `ChainRuntimeContext.module_dispatcher_factory` 注入。 -3. `PluginManager` 的加载、生命周期、注册表、投影、存储、目录、路径、同步、依赖、克隆和文件监控分别由 `app/runtime/extensions/plugin/` 下的单职责组件承担;旧管理器只保留 V3 ABI 门面和兼容调用顺序。 -4. 动态插件 API 使用专用 raw 路由;主程序统一响应信封不进入插件 `get_api()`。前端 `pluginApi` 对非 `Response` envelope 的 payload 原样交付调用方。 -5. 旧插件导入仅由 `app/runtime/compat/manifest.py` 精确映射;canonical 模块不复制旧 Manager/Helper/Oper 导出。`app/plugins/` 仍是运行时副本,继续排除在宿主架构扫描之外。 -6. 2026-08-24 当前机器基线为 811 个宿主 Python 模块、6,572 条内部导入边;数据库边界、Adapter→DB、Runtime→DB、Application→DB 及新增 API/Agent/Chain 目标边均为 0。架构门禁、插件兼容快照和基线脚本均已重新生成。 -7. 订阅写入统一归入 `app/application/subscription/write.py`;插件动态路由和文件夹操作统一归入 `app/application/plugin/routes.py`、`folders.py`。重构期间新增且未形成插件 ABI 的 `app/application/subscribe.py`、`app/application/plugins.py` 已直接删除,不进入 compat manifest。 -8. 2026-08-24 完成 Module Contract V2 宿主观察面收口:212 个 spec 均使用可执行的显式 aggregation, - `legacy` 只保留为未知第三方自定义方法的开放 fallback;插件方法名、kwargs、优先级和异常隔离 ABI 不变。 -9. 订阅及其它关键业务副作用已通过 `app/application/outbox.py`、`app/db/adapters/outbox.py` - 和启动组合根形成同事务 durable intent、claim/lease、有限重试与 dead-letter 边界;TaskRegistry - 仍只负责进程内任务 owner 和关停,不被当作持久队列。 - -## 2. 范围与明确排除项 - -### 2.1 纳入范围 - -- FastAPI 入口、路由、响应封装和动态路由注册。 -- `chain` 编排层、`application` 应用能力、`domain` 领域语义。 -- `runtime` 进程级基础设施、事件、模块、插件和服务生命周期。 -- `adapters` 技术适配与命名外部系统。 -- `db/models`、`db/oper`、会话与事务边界。 -- `modules` 宿主模块 SPI 及其与应用层的交互。 -- Agent、LLM Provider、工具注册和流式 API 的职责边界。 -- `sdk` 与 `runtime/compat` 形成的插件公开 ABI。 -- 启动、关闭、安全模式、热重载和后台任务的组合关系。 - -### 2.2 排除项 - -- **不审计、不迁移 `app/plugins/` 中的代码。**该目录是已安装插件副本,不是后端架构源代码,也不能作为插件兼容性的唯一事实来源。 -- 插件兼容基线应读取同工作区独立仓库 `../MoviePilot-Plugins` 的 `plugins.v3/`、`plugins.v2/` 和 V3 实际会从默认索引回退加载的 `plugins/` 实现,再配合宿主的 SDK、兼容清单和插件管理器契约判断。 -- 不把 `app/modules/themoviedb/` 内部第三方或移植代码的局部循环,直接等同于 MoviePilot 自有架构失败。它需要被隔离,但不应优先重写上游库。 -- 本轮不主张数据库表结构变更。纯架构批次不得夹带 Alembic 迁移、字段重命名或数据回填。 -- 本轮不主张删除 V3 兼容映射。任何删除都应作为显式破坏性变更另行决策。 - -## 3. 结论摘要 - -MoviePilot V3 已经完成一轮重要基础工作:原 `app/core`、`app/helper`、`app/utils` 已转为虚拟兼容入口;`foundation`、`domain`、`runtime`、`adapters`、`application`、`chain`、`startup`、`sdk` 的目标方向也已经写入规范;现有架构门禁通过。 - -以下八类是本轮治理开始时的审计问题清单,不代表 2026-08-18 收口后的未完成项;当前剩余工作以“3.1 当前未完成项”和各阶段收口表为准: - -1. **规范比门禁严格(历史基线)。**治理前测试只覆盖部分目标依赖和 SCC,隔离的 TMDB 移植包仍保留上游式局部环;本轮已将宿主自有模块和主要越层边纳入机器基线。 -2. **核心运行契约是字符串和约定(历史基线)。**`ChainBase.run_module()` 依赖方法名、签名探测、返回值形态和执行顺序;当前已为 212 个宿主观察方法建立统一可执行契约,未知第三方方法继续保留开放 fallback。 -3. **编排类和端点承担过多职责。**订阅、搜索、整理、下载、Agent、插件管理、外部市场和服务端客户端均出现千行级文件、百行级方法和多种基础设施混合。 -4. **数据库边界没有收口(历史基线)。**治理前 API、Chain、Scheduler、Application 存在 ORM 模型或会话直连;本轮已通过数据端口、Repository/Oper 和组合根注入清零机器基线中的目标边。 -5. **组合根仍有泄漏(历史基线)。**治理前存在导入期 app、事件解析器兜底实例化和 Chain 隐式抓取管理器;本轮已改为生命周期/运行时上下文显式装配。 -6. **Adapter、Application、Runtime 之间仍有历史职责混合(历史基线)。**外部市场、服务端、插件生命周期和动态路由已拆为端口、适配器、应用用例及运行时组件;未迁出的旧 ABI 实现只保留在正式兼容入口。 -7. **插件兼容面大且缺少版本化。**旧导入、SDK、管理器具体类型、动态 API、事件装饰器、模块方法和热重载行为共同构成 ABI;目前主要靠兼容清单和测试样例保护。 -8. **治理缺少可量化收敛目标(历史基线)。**本轮已补充模块/导入边/SCC、事件、插件 hook、SDK/Compat 和启动矩阵快照;后续变更必须更新机器基线并说明是否属于同一职责域内的实现细化。 - -### 3.1 持续治理边界 - -按“全部拆完”的边界,宿主跨层职责已经收口;以下三类是已明确边界的持续治理工作,不构成当前跨层重构遗漏: - -1. `app/runtime/extensions/plugin_manager.py` 与 `app/adapters/external/market.py` 仍保留正式 V3 ABI 的兼容 Facade/算法实现;已拆出的生命周期、投影、目录、安装、包和依赖职责均走 canonical 组件,未迁出的私有算法只有在获得真实命中数据和行为快照后才可逐项内移,不能复制旧类或删除旧路径。 -2. `app/modules/themoviedb/` 等第三方移植代码的局部 SCC 属于上游实现隔离项,不纳入宿主跨层拆分目标。 -3. 新增业务能力仍需遵守端口、组合根、单词文件命名和插件 raw 响应约束;这些是持续门禁,不是本轮遗留拆分任务。普通用户自定义通知和第三方插件自行写库/上报不可能由宿主事务自动包裹,宿主只对可追踪的主链业务写入提供 outbox durable 语义,并在文档中明确其外部边界。 - -治理顺序必须是:**先冻结行为契约和补门禁,再拆环和依赖,再拆职责,最后才讨论缩减兼容面。** - -## 4. 审计方法与当前基线 - -### 4.1 方法 - -本次基线使用以下方式获得: - -- 读取仓库与后端架构规则。 -- 运行 `tests/test_architecture_dependencies.py`。 -- 复用该测试的 AST 模块解析逻辑,统计 `app/` 内部依赖;排除 `app/plugins/`。 -- 统计文件规模、类和方法规模、入度、出度、SCC。 -- 沿启动、事件、模块、插件、Chain、API、Oper、Agent 的真实调用路径阅读。 -- 扫描独立插件仓的导入路径和插件钩子定义;不读取 `app/plugins/` 副本作为设计依据。 - -### 4.2 已验证结果 - -```text -./.venv/bin/python -m pytest \ - tests/test_architecture_dependencies.py \ - tests/test_architecture_contract_baseline.py -q -68 passed -``` - -这只能证明当前代码符合现有门禁,不能证明符合本文件提出的更完整目标。 - -### 4.3 模块规模 - -排除 `app/plugins/` 后,2026-08-24 当前静态扫描得到 811 个 Python 模块、6,572 条内部导入边。下表保留 2026-08-18 收口时的一级目录规模快照(代码行数包含注释和空行,用于趋势比较而非质量评分): - -| 一级目录 | 约代码行数 | Python 文件数 | 判断 | -| --- | ---: | ---: | --- | -| `app/modules` | 67,526 | 151 | 体量最大,具体平台协议和第三方移植代码留在模块族内部 | -| `app/agent` | 40,510 | 141 | Provider、工具、编排和策略各自有子域;后续只做域内优化 | -| `app/chain` | 29,703 | 36 | 大型用例链保留历史行为,跨层依赖已经由端口收口 | -| `app/api` | 16,882 | 44 | 端点保留传输映射和协议特例,业务/持久化经 Application 端口完成 | -| `app/application` | 19,273 | 81 | 应用用例、端口和兼容门面集中,禁止反向依赖 Runtime 实现 | -| `app/runtime` | 14,620 | 58 | 进程机制、扩展生命周期和插件单职责组件集中 | -| `app/adapters` | 12,995 | 37 | 技术 I/O 和命名外部生态适配,禁止直接持久化 | -| `app/db` | 8,416 | 51 | 只保留模型、Oper、会话、事务和健康实现 | -| `app/domain` | 7,654 | 21 | 相对可控,后续应继续保持纯语义 | -| `app/schemas` | 7,698 | 39 | 根入口已改为生成清单和惰性兼容导出 | - -### 4.4 高出度模块 - -| 模块 | 静态出度 | 主要原因 | -| --- | ---: | --- | -| `app.agent.tools.factory` | 99 | 一次性导入全部内置工具并维护集中注册表 | -| `app.startup.initializers.modules` | 55 | 组合根职责,这是合理高出度,但仍需声明式管理 | -| `app.api.endpoints.system` | 54 | 系统设置、规则测试、日志、网络测试、运行控制混合 | -| `app.api.deps` | 49 | 认证、插件配置和跨端点依赖装配集中 | -| `app.agent.orchestrator` | 48 | Agent 构建、执行、工具、记忆、审计、用量混合 | -| `app.chain.message` | 48 | 消息路由和多个业务域耦合 | -| `app.chain.subscribe` | 48 | 写入、识别、搜索、匹配、完成、分享混合 | -| `app.chain.download` | 45 | 下载选择、客户端调用、字幕和历史混合 | -| `app.scheduler` | 42 | 调度定义、业务调用和运行控制仍混合,清理已迁出 | -| `app.chain.transfer` | 41 | 计划、执行、刮削、通知、回调、清理混合 | - -`app.runtime`、`app.schemas`、`app.db` 等包入口具有很高入度。高入度本身不等于错误,但意味着它们是兼容和回归风险集中的枢纽,不能随意改变导出行为。 - -### 4.5 当前循环依赖 - -静态扫描共发现 1 个 SCC。`schemas`、`db`、订阅音乐、filemanager、Agent policy/LLM、Doctor/Monitor 和四个平台模块的自有环均已消除,当前只剩明确隔离的移植包局部环: - -| SCC | 类型 | 优先级 | 处理原则 | -| --- | --- | --- | --- | -| `app.modules.themoviedb` 及其对象模型环 | 移植/第三方局部环 | 隔离 | 保持包内封闭,不让环越出模块边界,不优先重写 | - -现有架构测试已经用机器基线锁定全量 SCC,并额外限制 `foundation/domain/runtime/adapters/application` 实现根和进程级跨包环。后续迁移必须继续满足“自有代码 SCC 不增长”和“目标 SCC 逐项归零”。 - -### 4.6 阶段 0-5 实施后的机器基线 - -当前工作树重新生成 `tests/fixtures/architecture/dependency-baseline.json` 后得到: - -| 指标 | 初始审计 | 当前基线 | 说明 | -| --- | ---: | ---: | --- | -| Python 模块数 | 约 654 | 811 | 增量来自单一职责的 Application、Runtime、Adapter、插件组件和维护用例模块 | -| 内部导入边 | 约 5,623 | 6,572 | 显式端口增加模块数但移除了反向边;边数不作为单独质量目标 | -| SCC 数 | 14 | 1 | 自有代码 SCC 已归零,仅保留 TMDB 移植包内部隔离例外 | -| `adapters -> db` | 存在 | 0 | `PluginHelper`、`MoviePilotServerHelper` 的本地数据读取已移到组合根/Application | -| `runtime -> db` | 存在 | 0 | 插件存储、服务配置均改为启动注入 | - -Doctor/Monitor 改为惰性公开门面;QQBot、Telegram、TriMedia、UGreen 的宿主实现迁入单词命名的 `module.py`,包根继续保持 manifest 入口和类 identity。插件生命周期监控已进一步归入 `plugin/monitor.py` 的 `PluginMonitorController`,Chain 调度器改为组合根注入。剩余第三方/TMDB 局部环只要求不越过 Facade,不为归零指标仓促改写上游式代码。 - -机器基线来源: - -- `tests/fixtures/architecture/dependency-baseline.json`:模块、边、SCC 和目标边。 -- `tests/fixtures/architecture/runtime-contract-baseline.json`:SDK、兼容清单、事件和 `run_module` 合同。 -- `tests/fixtures/architecture/official-plugin-baseline.json`:独立官方插件仓中 V3 实际可加载的 V3/V2/default 实现导入及钩子快照。 -- `app/schemas/exports.py`:Schema 根入口的生成式兼容导出清单。 - -## 5. 目标架构与依赖方向 - -既有架构规则继续是规范来源。本文件补充的是可执行边界。 - -### 5.1 目标调用路径 - -```text -HTTP / CLI / Event / Scheduler / Plugin Hook - | - v - Transport / Runtime Adapter - | - v - Application Use Case / Chain Facade - | - +-------+--------+ - | | - v v - Domain Policy Application Port - | - v - Adapter / Oper / External Client -``` - -### 5.2 各层应承担的职责 - -| 层 | 应承担 | 不应承担 | -| --- | --- | --- | -| `foundation` | 无状态通用算法、值归一化、基础类型工具 | 配置、日志、数据库、HTTP、单例、业务流程 | -| `domain` | 媒体身份、规则、匹配、领域值和纯策略 | FastAPI、SQLAlchemy 会话、网络、调度器、插件管理器 | -| `runtime` | 事件循环、进程资源、扩展生命周期、执行上下文 | 具体业务查询、外部市场业务、页面 DTO 拼装 | -| `adapters` | HTTP、浏览器、文件系统、系统、命名外部服务的具体 I/O | 直接决定用例、直接持久化业务状态 | -| `application` | 有状态单能力、用例、端口协议、跨 adapter 的短流程 | 动态抓取全局管理器、长期进程生命周期、巨型多域编排 | -| `chain` | 面向用户目标的多域编排和向后兼容门面 | 直接写 SQL、实现底层协议、复制纯领域算法 | -| `modules` | 可替换宿主能力 Provider,实现模块 SPI | 反向控制 Chain、直接掌管宿主生命周期 | -| `api` | 参数解析、鉴权、传输 DTO、状态码、流协议 | 直接会话事务、调度细节、业务分支和外部上报 | -| `startup` | 唯一组合根、创建并连接实例、决定启停顺序 | 业务规则和常态请求处理 | -| `sdk` | 稳定、文档化、受测试保护的插件公开门面 | 随意导出内部单例和具体实现的新符号 | -| `runtime/compat` | 精确恢复旧路径和旧符号 | 承载新业务逻辑或模糊吞掉所有导入错误 | - -### 5.3 强制依赖规则 - -后续新增代码应满足: - -1. `foundation` 不依赖其他 MoviePilot 层。 -2. `domain` 只依赖 `foundation` 和纯类型;必要 DTO 应移动到领域或契约模块,而不是依赖运行时 schema 聚合入口。 -3. `runtime` 不直接依赖 `db.oper`、具体外部服务或 Chain。 -4. `adapters` 不直接使用业务 Oper;外部结果通过返回值交给 Application 决定是否持久化。 -5. `application` 不直接依赖 `api`、`startup`,不引用 Agent/Module 的具体类;通过 Protocol 或组合根注入。 -6. `api` 不新增 `app.db.models`、`Session`、`AsyncSession`、`Scheduler`、`PluginManager` 的直接使用。 -7. `chain` 不新增裸会话或直接 SQL,不新增通过延迟导入掩盖的环。 -8. `modules` 可实现宿主 SPI,可调用稳定的 Application 能力,但不得让 Application 反向依赖具体模块类。 -9. 只有 `startup` 和非常薄的兼容门面可以装配具体实现。 -10. 兼容入口不反向成为宿主内部新代码的首选导入路径。 - -## 6. 详细问题与治理要求 - -本章保留治理前的证据、目标设计和验收标准,便于其他 AI 复用迁移方法;其中标注“历史基线”的条目不是当前未完成项。当前是否仍存在跨层问题,以第 3.1 节、4.5/4.6 节机器基线、阶段 6-7 收口表和第 11.4 节验证快照为准。 - -### 6.1 架构规则与门禁存在空档 - -#### 历史基线与当前收口 - -- `tests/test_architecture_dependencies.py` 当前有 28 项测试,能保护虚拟兼容根、核心实现根、插件组件和禁止边。 -- `_music`/`subscribe`、Schema、DB、filemanager、Agent policy/LLM、Doctor/Monitor 和四个平台模块等自有 SCC 已消除;当前基线只保留隔离的 TMDB 移植包环。 -- `app/application/messaging/skill.py` 已改为依赖 `SkillCatalogPort`,由启动组合根注入 Agent 技能目录;当前目标 Application→具体 Runtime/Adapter 边已由架构门禁锁定为零。 -- `app/adapters/external/market.py`、`app/adapters/external/server.py` 的旧 Oper 直连是治理前证据;当前宿主 canonical 路径已改为组合根注入的数据 Provider,兼容 Facade 的旧算法不作为新调用入口。 -- API 端点曾直接持有 `Scheduler`、ORM 模型和数据库会话;当前目标 endpoint→Scheduler/Model/Session 边均为零。 - -#### 风险 - -- 新改动只要没有触发已有少数模式,就可能继续扩大架构债务。 -- “测试通过”容易被误读为“架构迁移完成”。 -- 后续 AI 会复制当前调用方式,造成错误模式扩散。 - -#### 治理动作 - -1. 在现有测试中增加“趋势型门禁”,先用基线白名单锁住现状,再逐项减小白名单。 -2. 对以下依赖设置零新增: - - `app.adapters..* -> app.db..*` - - `app.runtime..* -> app.db..*` - - `app.api.endpoints..* -> sqlalchemy.orm.Session/sqlalchemy.ext.asyncio.AsyncSession` - - `app.api.endpoints..* -> app.db.models..*` - - `app.application..* -> app.agent..*`,唯一例外必须是明确稳定门面。 -3. 增加自有 SCC 基线文件。白名单必须写明负责人、原因和目标阶段,不得只列模块名。 -4. 对第三方/移植包使用路径级豁免,不使用整个 `app.modules` 豁免。 -5. 每个架构批次输出变更前后:SCC、目标边数量、出度、受影响公开导入。 - -#### 完成标准 - -- 新代码不能增加上述禁止边。 -- 每个阶段至少消除一个明确 SCC 或一类越层调用。 -- 门禁失败信息打印“调用方、被调用方、允许的替代入口”。 - -### 6.2 `ChainBase` 是隐式服务定位器和字符串协议总线 - -#### 历史基线与当前收口 - -- `app/chain/__init__.py:53-64` 中,每个 Chain 默认构造 `ModuleManager`、`EventManager`、`MessageOper`、`MessageHelper`、`MessageQueueManager`、`PluginManager` 和两种缓存。 -- `run_module()` 位于 `app/chain/__init__.py:370-390`,先执行插件模块,再执行系统模块。 -- 插件返回非空且不是列表时直接短路;列表结果继续合并。 -- 系统模块按优先级执行;可能根据 `ObjectUtils.check_signature()` 把前一结果作为下一处理器唯一参数。 -- AST 扫描发现约 211 个不同的字面量方法名、259 处调用。这已经是一套大型内部和插件协议,而不只是工具函数。 - -#### 不可破坏的行为 - -1. 插件模块先于系统模块执行。 -2. 非空非列表结果短路。 -3. 列表结果按现有规则合并。 -4. 系统模块按 `get_priority()` 排序。 -5. 同步方法在异步路径中进入线程池。 -6. `raise_exception`、限流和系统错误通知语义保持。 -7. 无参数 `Chain()` 构造仍可用,至少在 V3 兼容期内保持。 - -#### 目标设计 - -- 把调度算法提取为一个可单测的 `ModuleInvocationDispatcher`,只接收模块目录、插件模块目录、错误策略和执行器。 -- 建立 `ModuleMethodContract` 清单,记录方法名、调用方式、参数模型、结果聚合策略、同步/异步能力、是否允许插件短路。 -- `ChainBase` 保留兼容门面和公共辅助方法,但不再在每个实例构造时自行发现所有全局服务。 -- 由 `startup` 创建 `ChainRuntimeContext`;无参构造从兼容 provider 取默认上下文,测试和新代码显式注入。 -- 不把 211 个方法一次性改成枚举。先生成清单和测试,再按能力族引入 Typed Protocol。 - -#### 建议目标模块 - -```text -app/runtime/extensions/module/contracts.py -app/runtime/extensions/module/dispatcher.py -app/application/chain/context.py -app/chain/__init__.py # 保留 ChainBase 兼容门面 -``` - -#### 完成标准 - -- 调度器可在不创建真实 PluginManager、ModuleManager、DB 和消息队列时独立测试。 -- 现有 211 个方法名均被扫描清单覆盖,新增方法必须登记。 -- 对插件优先、短路、列表聚合、签名接力、同步/异步、异常六类行为建立参数化契约测试。 - -### 6.3 巨型 Chain 混合了用例、策略、I/O 和展示副作用 - -#### 重点文件 - -| 文件 | 规模/热点 | 当前混合职责 | 首批拆分方向 | -| --- | --- | --- | --- | -| `app/chain/subscribe.py` | 约 3,794 行,70 个方法;`match()` 约 417 行 | 订阅写入、识别、搜索、匹配、缺失判断、完成、分享、历史、通知 | 命令、查询、匹配策略、完成策略、对外 Facade | -| `app/chain/search.py` | 约 2,901 行;结果解析约 195 行 | 搜索计划、站点并发、结果解析、规则过滤、流式回调 | 计划器、执行器、结果归一化、流式进度 | -| `app/chain/transfer.py` | 约 2,685 行;`do_transfer()` 约 885 行 | 计划、文件操作、刮削、历史、消息、媒体库刷新、回调 | 传输计划、执行、后处理、结果提交 | -| `app/chain/download.py` | 约 2,100 行;批量下载约 572 行 | 资源选择、客户端选择、提交、字幕、历史、通知 | 选择策略、提交服务、字幕流程、审计记录 | -| `app/chain/media.py` | 约 2,097 行 | 识别、缓存、身份转换、同步/异步重复 | 识别用例、身份解析、Provider 网关、缓存策略 | - -#### 当前真实循环 - -`app/chain/_music.py:103-104`、`:134-135`、`:222-223` 通过延迟导入访问 `app.chain.subscribe` 的 `build_subscribe_meta`、`_subscribe_media_key`,而 `subscribe.py` 又导入 `MusicSubscribeMixin`。注释已经明确说明它是在回避模块级循环。 - -延迟导入只改变出错时机,不会恢复正确依赖方向。 - -#### 拆分原则 - -1. 保持 `app.chain.subscribe.SubscribeChain` 等公开路径和类名。 -2. 优先提取纯函数和只依赖 DTO 的策略,再提取有状态用例。 -3. 不在一次提交中同时改同步与异步全链路;先建立共享核心,再让两条入口委托。 -4. 原 Facade 的参数默认值、返回类型、事件时机和消息副作用必须保持。 -5. 不为了缩短文件把相互调用的方法机械分散到多个 `helper.py`。 - -#### 已落地的订阅应用拆分 - -```text -app/application/subscription/ - write.py # 新增订阅、媒体翻译和写入端口 - query.py # 存在性、来源定位和公开查询 - mutation.py # 更新、重置和历史删除 - delete.py # 单条删除与事务端口 - identity.py # 按媒体身份批量删除 - search.py # 手工搜索调度 - contract.py # Chain 共用的媒体元数据与媒体键契约 - -app/chain/subscribe.py # V3 Facade,继续暴露 SubscribeChain 与旧辅助符号 -``` - -`app/application/subscribe.py` 是 V3 重构期间新增的内部过渡文件,插件仓与运行时插件均无导入; -在宿主、测试和旧 `app.db.subscribe_oper` 行为适配切换到 `subscription/write.py` 后直接删除, -不保留门面,也不在 `manifest.py` 中新增没有历史消费者的映射。 - -#### 建议的整理拆分 - -```text -app/domain/transfer/ - plan.py - naming.py - result.py - -app/application/transfer_pipeline/ - planner.py - executor.py - metadata.py - commit.py - ports.py - -app/chain/transfer.py # 保持 TransferChain 兼容门面 -``` - -`do_transfer()` 应先被改造成显式阶段流水线,每个阶段接受不可变上下文并返回新结果。不能在第一步就重写文件移动算法。 - -#### 完成标准 - -- 消除 `_music` ↔ `subscribe` SCC,不再用新增延迟导入维持。 -- 目标大方法拆为有名称、可独立验证的阶段;单个用例方法原则上不超过 150 行。 -- Chain Facade 的外部路径、方法名、参数和关键副作用测试保持。 -- 每次只迁移一个垂直用例,例如“删除订阅”或“传输后处理”,不得一次搬完整个 Chain。 - -### 6.4 数据访问边界与事务所有权不一致 - -#### 现状证据 - -- `app/api/endpoints/subscribe.py` 直接持有 Session、模型和 Oper 是治理前证据;当前 endpoint→Session/Model 目标边已清零。 -- Chain、Scheduler、Application 的模型直连属于治理前扫描结果;当前目标 Application/Chain/Runtime→DB 边均为零。 -- ORM Model 仍保留贴近表结构的查询原语,但已全部要求调用方显式传入 Session;Model/Base 的查询、写入和 legacy 事务装饰器均已清零。 -- `app/db/__init__.py` 的根入口和模型回流曾参与 DB SCC;该自有 SCC 已消除,旧根入口仅作为兼容边界保留。 - -#### 问题本质 - -治理前同时存在三种数据访问风格: - -1. `db/oper` 服务。 -2. ORM 模型类方法。 -3. API/业务代码直接持有 Session。 - -这会让事务边界、权限过滤、事件发送和外部上报的先后次序散落在不同层。出现失败时很难判断哪些副作用已提交。 - -#### 目标边界 - -- ORM 模型只描述表、关系、约束和极少量无 I/O 的实体辅助。 -- `db/oper` 是当前 V3 的持久化实现边界,不在本轮强制引入完整 Repository 框架。 -- Application 用例拥有事务语义;API 只调用用例。 -- 复杂跨 Oper 事务可引入小型 `UnitOfWork` Protocol,但不要为单表查询套通用框架。 -- Event、Scheduler、Server 上报只在提交成功后触发;必要时用显式 after-commit 动作清单。 -- 需要跨进程恢复、重试或幂等交付的 after-commit 动作进入 - `app/application/outbox.py`,由 `app/db/adapters/outbox.py` 持久化 intent 并负责 claim/lease; - `TaskRegistry` 只覆盖进程内任务的取消与关停等待。 - -#### 迁移顺序 - -1. 统计宿主内部所有 `app.db.models` 和 Session 直接调用,建立基线。 -2. 先迁移写操作,因为事务和副作用风险最高;读操作可稍后处理。 -3. 为每个端点提取 Application command,例如 `DeleteSubscriptionCommand`。 -4. Command 调用 Oper,并返回待发送事件/待调度动作;提交成功后执行。 -5. 宿主内部调用切到 Oper 后,模型旧类方法继续保留为兼容转发,不在 V3 直接删除。 -6. 内部 DB 模块改为从 `app.db.base`、`decorators`、`session` 精确导入,不经 `app.db` 根入口。 - -#### 插件兼容约束 - -- 独立插件仓仍有 `app.db.*`、`app.db.site_oper` 等直接导入。 -- 旧模型类方法、`DbOper`、事务装饰器和惰性 `Engine`/`AsyncEngine` 符号不能因宿主内部收口而删除。 -- 兼容转发不得改变同步/异步类型、装饰器提交行为和返回对象类型。 -- 新 SDK 应提供更窄的数据/配置服务,但不能强迫现有插件同步迁移。 - -#### 完成标准 - -- `app/api/endpoints` 不再新增裸 Session 和模型写入。 -- 第一阶段写端点全部由 Application command 负责事务。 -- Adapter、Runtime 对 `app.db` 的直接依赖归零。 -- DB 自有 SCC 消除;兼容根入口的外部导入测试保持通过。 - -### 6.5 启动组合根已经形成,但全局构造与隐式取实例仍然存在 - -#### 已有进展 - -`app/startup/initializers/modules.py` 承担托管资源、壁纸 Provider、认证载荷、DoH、站点、事件错误通知、模块、Agent 和前端的组合工作。`app/startup/lifecycle/` 显式规定数据库预热、路由、模块、插件、调度器、监控器、命令和工作流的顺序。这是正确方向。 - -#### 历史泄漏与当前收口 - -- `app/factory.py`、`app/main.py`、`ChainBase` 和事件 resolver 的隐式构造是治理前泄漏证据;当前启动组合根负责注册动态路由、Chain dispatcher、插件 Runtime 和模块能力。 -- 兼容入口仍保留 `EventManager()`、settings、global_vars 和 Singleton 的对象身份,但新宿主路径不再通过它们临时创建未托管组件。 -- 正常/安全模式组件清单、导入冷启动和生命周期顺序已纳入机器快照;后续仅允许补充观测和同职责域实现细化。 - -#### 目标设计 - -```text -ApplicationRuntime - - event_bus - - module_registry - - plugin_registry - - scheduler - - command_runtime - - workflow_runtime - - message_gateway - - cache_registry - - db_runtime - - agent_runtime -``` - -- `startup` 创建一个 `ApplicationRuntime` 或等价的显式组件注册表。 -- 生命周期步骤声明名称、依赖、start、stop、safe-mode 策略、超时和失败策略。 -- 老的单例入口继续返回该注册表中的实例,保持对象身份。 -- 新代码显式接收所需最小依赖,不接收整个容器。 -- Event handler 必须由模块/插件/服务 resolver 解析。未绑定类的自动构造先告警并记录命中,完成迁移后改为拒绝。 - -#### 迁移要求 - -1. 先增加生命周期快照测试,记录正常模式、安全模式、关闭顺序和失败继续策略。 -2. 再把单个资源改为注册表所有;一次只迁移一个资源。 -3. 保留 `EventManager()`、`PluginManager()` 等现有入口的同一实例语义。 -4. 禁止在迁移批次顺带改变 uvicorn/gunicorn 入口和 Docker 启动方式。 -5. 测试 `app.factory:app` 直接挂载路径,因为它与 `main.py` 路径不同。 - -#### 完成标准 - -- 启动时能打印或导出已启用组件及其依赖顺序。 -- Event handler 无未登记的运行时构造。 -- 正常、安全模式、启动中断和部分关闭失败均有测试。 -- 导入模块不建立数据库连接、不启动线程、不启动调度器。 - -### 6.6 事件总线同时承担注册、解析、调度、隔离和错误再广播 - -#### 现状证据 - -- `app/runtime/events.py` 约 801 行,包含装饰器注册、订阅快照、实例解析、同步/异步/广播调度、插件目标过滤、限流和错误通知。 -- 链式事件按优先级顺序执行;广播事件通过线程池或 `asyncio.run_coroutine_threadsafe()` 并发执行。 -- 广播事件对 `event_data` 仅做顶层浅拷贝;嵌套可变对象仍共享。 -- `MessageAction` 使用 `__mp_target_plugin_id` 作为内部定向字段。 -- 错误处理在通知后再次发送 `SystemError` 事件,存在错误处理链再次出错的递归风险。 -- 未被 resolver 管理的类处理器可被临时实例化。 - -#### 必须冻结的语义 - -1. `EventType` 与 `ChainEventType` 的区别。 -2. 链式事件的优先级、顺序和返回行为。 -3. 广播事件的并发模型和“订阅快照从下一次事件生效”。 -4. 插件定向消息不能被其他插件观察。 -5. 同步处理器在线程池执行的条件。 -6. 插件热加载/卸载时 handler 的启用和移除时机。 - -#### 目标拆分 - -```text -app/runtime/events.py # 兼容门面与 eventmanager -app/runtime/event/registry.py # 注册、快照、启停 -app/runtime/event/binding.py # resolver 与实例绑定 -app/runtime/event/dispatch.py # chain/broadcast 调度算法 -app/runtime/event/errors.py # 限流、错误隔离、通知降级 -app/domain/events/ # 逐步增加 Typed payload,不承载总线实现 -``` - -#### 实施顺序 - -1. 为所有现有事件枚举生成 producer/consumer 清单。 -2. 对高风险事件增加 payload model,但入口继续接受 dict,并在边界校验/转换。 -3. 提取纯调度器,不改变 `EventManager` 公共方法和全局实例。 -4. 为 resolver 未命中增加 DEBUG 诊断和测试;清零后移除自动构造兜底。 -5. `SystemError` 增加递归保护和不可再次广播的降级日志路径。 -6. 对需要深隔离的事件定义不可变 payload,不全局使用 `deepcopy`。 - -#### 完成标准 - -- 事件注册、实例绑定、调度和错误策略可分别测试。 -- 高风险事件 producer/consumer 的 payload 契约一致。 -- 热加载、定向插件、广播并发、错误递归保护均有回归测试。 -- `app.core.event`、`app.sdk.events` 的对象身份和装饰器用法不变。 - -### 6.7 API 层包含用例、事务、调度和长流协议 - -#### 重点文件 - -| 文件 | 典型问题 | -| --- | --- | -| `app/api/endpoints/agent.py` | 约 2,315 行,`web_agent_stream()` 约 400 行,上传、队列、Agent 执行、SSE 映射和清理混合 | -| `app/api/endpoints/system.py` | 约 1,493 行,网络测试、规则测试、日志、配置、运行控制混合 | -| `app/api/endpoints/plugin.py` | 市场、安装、状态、详情、动态 API 注册和文件操作耦合 | -| `app/api/endpoints/subscribe.py` | 鉴权、查询、事务、事件、调度、共享上报混合 | -| `app/api/endpoints/site.py` | 站点 CRUD、认证、统计、图标和资源更新混合 | -| `app/api/endpoints/transfer.py` | `manual_transfer()` 约 293 行,解析、计划、执行和响应混合 | -| `app/api/endpoints/openai.py` | OpenAI 兼容协议、流式适配、业务执行混合 | - -#### 目标设计 - -- endpoint 只负责传输参数、认证依赖、调用用例、映射响应。 -- 业务权限检查进入 Application policy/use case;FastAPI 的 token 解码仍留在 API/security adapter。 -- 后台任务不直接抓取 Scheduler 单例;调用 Application command 返回一个可提交的任务请求。 -- SSE/OpenAI 流协议由独立 transport adapter 映射领域/Agent 事件。 -- API 路径、HTTP method、状态码、响应模型和流事件格式保持。 - -#### 动态插件 API 的 P0 兼容冲突 - -这是治理前发现并已完成的 P0 兼容修复。主应用仍使用 `ResponseAPIRoute`,但 `app/adapters/web/plugin/routes.py` 在动态插件注册时显式使用原生 `APIRoute`;`app/application/plugin/routes.py` 定义 `DynamicRouteRegistry` 端口并承载注册/移除用例,不依赖 FastAPI。因此插件 `get_api()` 返回的 dict、Pydantic model、原生 `Response`、文件/流响应和自定义状态码均不进入主 API envelope。前端 `pluginApi` 也只在检测到严格 `Response` envelope 时解包,否则原样交付。 - -真实运行验证已覆盖:官方 V3 `TvdbDiscover` 插件加载后生成 `/api/v1/plugin/TvdbDiscover/tvdb_discover` 动态路由,未认证请求返回插件路由自己的认证错误体而非主 API 404/统一路由包装;对应 route class、raw 响应和前端 pass-through 均有测试。 - -#### 完成标准 - -- 主 API 继续统一信封。 -- 动态插件 API 的 raw 返回、原生 Response、文件/流响应和自定义状态码保持。 -- 每个重点端点文件逐批只保留 transport 逻辑。 -- API 层不再直接提交数据库事务或调用具体外部上报 Helper。 - -### 6.8 `PluginManager` 同时承担宿主生命周期、契约聚合、UI 投影和市场安装 - -#### 现状证据 - -`app/runtime/extensions/plugin_manager.py` 当前约 999 行、80 个方法;它仍包含兼容门面和少量运行时编排,但职责实现已拆到: - -- 插件扫描、选择性加载、实例化、`init_plugin`、停止和热重载。 -- 文件监控和本地变化处理。 -- 配置和数据访问。 -- 命令、API、服务、模块、动作、Agent tools 聚合。 -- 页面、表单、侧栏、仪表板、授权 Provider 等 UI/交互投影。 -- 插件状态、更新入口和兼容 Facade;市场、包、依赖的宿主调用已经改为经注入系统服务。 - -因此 PluginManager 仍是 V3 ABI 的运行时 Facade,但不再直接承担 market service、包/依赖安装或 FastAPI presentation 适配;这些职责由下列组件和启动组合根连接。 - -#### 目标拆分 - -```text -app/runtime/extensions/plugin_manager.py # 保留公共 Facade 和实例身份 -app/runtime/extensions/plugin/lifecycle.py # 后续提取 load/start/stop/reload -app/runtime/extensions/plugin/registry.py # 实例、状态、元数据 -app/runtime/extensions/plugin/contracts.py # hook 解析与校验 -app/runtime/extensions/plugin/projection.py # commands/apis/services/modules/actions 投影 -app/runtime/extensions/plugin/storage.py # 运行时持久化窄端口 -app/application/plugin/catalog.py # 市场目录查询、代际合并和来源去重 -app/application/plugin/install.py # 安装用例与阶段结果 -app/application/plugin/routes.py # 动态 API 注册端口与用例 -app/application/plugin/folders.py # 插件文件夹清理用例 -``` - -#### 插件钩子契约 - -独立插件仓当前高频钩子包括: - -| 钩子 | 扫描到的插件文件数(约) | -| --- | ---: | -| `init_plugin`、`stop_service`、`get_state`、`get_form`、`get_page`、`get_api` | 81-82 | -| `get_command` | 79 | -| `get_service` | 47 | -| `get_render_mode` | 11 | -| `get_dashboard` | 10 | -| `get_module` | 5 | -| `get_agent_tools` | 3 | - -这些方法的存在性、参数、返回形态和异常隔离方式都是 ABI。目标 `plugin/contracts.py` 应定义 Protocol 和运行时 validator,但不能要求旧插件显式继承新 Protocol。 - -#### 已完成拆分与后续边界 - -1. hook contract snapshot、registry、projection、storage、catalog、install、routes、package、dependency 已落地。 -2. 生命周期和文件 watcher 已分别由 `plugin/lifecycle.py`、`plugin/monitor.py`、`PluginMonitorController` 承担;旧 Facade 只保留调用顺序、对象身份和 V3 公共方法。 -3. 后续只允许在同一职责域内优化算法和可观测性,禁止重新把市场、数据库、FastAPI 或具体 Manager 导入 Runtime/API。 - -#### 完成标准 - -- `PluginManager()` 仍返回同一实例,`app.sdk.plugins.PluginManager` 身份测试保持。 -- 启停、更新、热重载、配置更新、动态路由刷新顺序不变。 -- PluginManager 本身不再直接导入 DB、市场 client、包管理器、压缩包和备份实现;具体安装阶段由 Application command 和注入的包/依赖端口完成。 -- 所有旧公共方法在 V3 保留,内部只做委托。 - -### 6.9 外部 Adapter 直接持久化并承载业务用例 - -#### `PluginHelper` - -`app/adapters/external/market.py` 当前约 3,066 行、112 个方法,仍保留以下正式 V3 ABI 实现: - -- 市场索引和发布信息请求。 -- 插件包下载、解压、校验、备份和恢复。 -- 插件 `pyproject.toml` / `requirements.txt` 选择、约束判断、uv 安装与降级策略。 -- 同步/异步重复实现。 -- 市场缓存、旧同步/异步安装入口和旧私有方法兼容。 - -它当前不再导入 `SystemConfigOper`;已拆出的 canonical 入口由 `PluginMarketClient`、`PluginPackageManager`、`PluginDependencyInstaller`、`PluginCatalogService` 和 `PluginInstallCommand` 承担。为了不破坏旧插件对 `PluginHelper` 的类名、静态方法和私有兼容调用,本轮没有把 3,066 行旧实现机械搬走,也没有在新模块中复制一套同名旧导出。后续阶段可继续把旧实现的具体算法逐步内移到这些组件。 - -建议拆为: - -```text -app/adapters/external/plugin/client.py -app/adapters/system/plugin/package.py -app/adapters/system/plugin/dependency.py -app/application/plugin/catalog.py -app/application/plugin/install.py -app/adapters/external/market.py # PluginHelper 正式 ABI 与过渡实现 -``` - -外部 client 只返回结构化结果;Application 决定版本选择、安装事务、备份和重载。 - -#### `MoviePilotServerHelper` - -`app/adapters/external/server.py` 当前约 1,836 行、137 个方法,同时承担: - -- 通用请求签名和 HTTP 调用。 -- 使用统计和插件统计。 -- 订阅、工作流、识别共享。 -- 本地 Oper 查询和 payload 拼装。 -- 多类响应解析与缓存。 - -当前文件不再直接导入 `SubscribeOper`、`SystemConfigOper` 或 `WorkflowOper`;本地数据读取和 payload 组装已由启动层注入的 `report.py`、`share.py` 用例提供。 - -建议拆为: - -```text -app/adapters/external/server.py # HTTP transport 与旧 Helper Facade -app/application/server/report.py # 插件/订阅统计和首次上报 -app/application/server/share.py # 订阅/工作流等分享用例 -``` - -`server.py` 暂时同时保留底层 transport 和旧公开 Facade,但不再读取 Oper;启动组合根把数据读取 Provider、Application 用例和 transport 回调连接起来。后续如果 transport 继续增长,再建立 `app/adapters/external/server/` 主题目录并使用 `client.py`、`contracts.py` 等单词文件名,不能新增 `moviepilot_server.py` 一类多词实现模块。 - -#### 完成标准 - -- `app/adapters` 对 `app.db` 的静态导入为零。 -- 外部 client 可用 fake transport 测试,不需要真实 DB。 -- 业务用例可用 fake client 测试,不需要网络。 -- 旧 Helper 路径和方法在 V3 内继续工作。 - -### 6.10 Schema 聚合入口和本地化产生运行时耦合 - -#### 现状证据 - -- `app/schemas/__init__.py` 已改为由 `app/schemas/exports.py` 驱动的惰性兼容入口;任意 `from app import schemas` 不再主动加载全部 schema 子模块。 -- 仍需注意 `from app.schemas import X` 首次访问会加载该符号的所有者模块,不能把惰性入口误解为 schema 本身已经完全解耦。 -- `app/schemas/dashboard.py:5`、`app/schemas/response.py:5` 直接依赖 `app.runtime.localization.LocaleHelper`。 -- `Response.message` 的 Pydantic validator 在构造模型时读取当前请求 locale,序列化模型不再是纯数据操作。 - -#### 风险 - -- 小范围 schema 导入会触发大量模块加载,放大循环和启动时间。 -- 同一个 Response 在不同上下文构造可能得到不同文本,后台任务和测试受 ContextVar/全局上下文影响。 -- Domain/Application 依赖 schema 聚合入口时,被动依赖展示层和本地化运行时。 - -#### 目标设计 - -1. 宿主内部改用精确子模块导入。 -2. `app.schemas` 根入口保留兼容,但用显式导出表和惰性 `__getattr__`,不再全量星号加载。 -3. 建立导出符号冲突检查,避免不同 schema 同名时依赖导入顺序。 -4. 本地化发生在 API/消息 presentation mapper,不发生在通用 DTO 构造阶段。 -5. V3 内保持最终 API `message` 字段和语言行为,迁移时用请求级快照测试锁定。 - -#### 完成标准 - -- `app.schemas` 自有 SCC 消除。 -- 内部新增代码不得 `from app.schemas import *`。 -- 根入口公开符号集合有快照测试。 -- schema 子模块不再依赖 `runtime.localization`;最终返回文本仍符合现有 locale 行为。 - -### 6.11 Application 仍依赖具体实现,能力边界不稳定 - -#### 典型证据 - -- `app/application/messaging/skill.py` 通过 `SkillCatalogPort` 消费技能目录,`app.startup.initializers.agent` 才导入并注入 `SkillHelper`。 -- `app/application/plugin/routes.py` 持有 `DynamicRouteRegistry` Protocol 和注册/移除用例;FastAPI app、`app.routes`、`openapi_schema` 和 `setup()` 均封装在 `app/adapters/web/plugin/routes.py`。 -- 多个 `modules` 直接导入 `app.application.messaging.agent`、`mediaserver`、`storage` 等;其中一部分是合理 SPI 消费,一部分表明应用能力接口和具体实现未区分。 -- `SystemConfigOper()` 在大量文件中被直接构造,形成持久化配置服务定位器。 - -#### 目标边界 - -- Application 能依赖自己定义的端口,不依赖 Agent registry、FastAPI app、PluginManager 具体类。 -- 端口定义靠近消费者,例如 `SkillCatalog` 定义在 messaging use case 一侧,由 Agent adapter 实现。 -- 动态路由操作应定义 `DynamicRouteRegistry` Protocol,FastAPI 实现在 API adapter,插件应用服务只提交路由描述。 -- `SystemConfigReader/Writer` 作为窄协议注入用例,默认实现可继续包装 `SystemConfigOper`。 -- Modules 只消费稳定 Application facade;需要长期保留的接口进入 SDK/Host SPI,而不是随意导入内部文件。 - -#### 完成标准 - -- `app.application` 不直接导入 `app.agent`、FastAPI 和具体 Module 类。 -- Application 单测可通过 fake port 完成。 -- Module 依赖的 Application 能力有 Protocol、生命周期和异常语义说明。 - -### 6.12 Agent 子系统存在集中注册、Provider 巨型对象和编排混合 - -#### 现状证据 - -- `app/agent/tools/factory.py` 静态出度约 99,一次性导入大量内置工具并维护集中列表。 -- `app/agent/llm/provider.py` 约 3,527 行,内置 Provider 规格段约 700 行,并混合配置、授权、模型发现、协议兼容和运行实例创建。 -- `app/agent/orchestrator.py` 约 3,116 行,混合 Agent 创建、执行、工具选择、用量记录、记忆和流式事件。 -- `app/agent/llm/helper.py` 约 1,699 行,包含多种供应商兼容修补。 -- Agent 已通过 `runtime_loader.py` 延迟物化,因此“让 Agent 延迟启动”不是下一阶段主要任务。 - -#### 目标拆分 - -```text -app/agent/llm/specs/ # Provider 静态规格,数据化并校验唯一 ID -app/agent/llm/auth/ # OAuth/设备码/会话状态 -app/agent/llm/catalog.py # 模型发现和缓存 -app/agent/llm/protocols/ # OpenAI/Anthropic/Gemini 等适配 -app/agent/llm/runtime.py # 选定配置到运行客户端 -app/agent/tools/manifests/ # 按能力域声明工具,不在工厂顶层全量导入 -app/agent/execution/ # 执行、流事件、用量、恢复 -``` - -#### 兼容要求 - -- Provider ID、配置 key、已保存授权状态、模型 ID 和默认选择不能变化。 -- 工具名称、参数 schema、权限、用户确认语义不能变化。 -- 插件 `get_agent_tools()` 和 `MoviePilotTool` 继承/注册机制保持。 -- OpenAI 兼容 API 的事件顺序、finish reason、error 形态和 usage 保持。 - -#### 完成标准 - -- 工具工厂不再静态导入全部工具;按 manifest 或域 registry 延迟加载。 -- `provider.py` 只保留兼容 Facade 和运行时入口。 -- 每个 Provider 协议适配可单独做录制响应/fixture 测试。 -- Agent 编排不直接处理 HTTP/SSE 格式。 - -### 6.13 `modules` 既是 Provider 集合,又出现模块内环和跨层扩散 - -#### 判断原则 - -`app/modules` 的高体量并不意味着应该整体改造成 Application。它是宿主可替换 Provider 的主要实现区,正确目标是: - -- 每个模块实现明确 SPI。 -- 模块自己的平台协议和对象留在模块内。 -- 宿主只通过 ModuleManager/HostModuleAdapter 调用。 -- 共享语义不藏在某个具体模块中。 -- 模块不反向驱动 Chain 和宿主生命周期。 - -#### 当前重点 - -- `filemanager` 与 `transhandler` 形成双向依赖,应先提取传输 DTO、回调 Protocol 和文件操作结果。 -- 消息平台模块重复依赖 `application.messaging.agent` 等能力,应固化消息网关 SPI,避免每个平台了解 Agent 细节。 -- 媒体服务器模块直接消费 `application.mediaserver`,需要区分“宿主下发能力”与“模块反调宿主”的方向。 -- TMDB 移植包内部大 SCC 应包内隔离,通过单一 Facade 对外,不开展无收益重写。 - -#### 完成标准 - -- 每个模块族有一份 SPI 清单和返回契约。 -- 自有模块内部 SCC 逐项消除;第三方局部环不越过 Facade。 -- ModuleManager 不再通过任意 `hasattr` 发现无限制能力;能力必须进入 method contract 清单。 -- 插件 `get_module()` 仍可提供同名方法并参与现有聚合。 - -### 6.14 SDK 与兼容层是正式 ABI,但当前过宽 - -#### 当前事实 - -`app/runtime/compat/manifest.py` 当前约包含: - -- 113 个模块别名。 -- 1 个包别名。 -- 10 个模块、55 个符号别名。 -- 3 个虚拟包。 - -独立插件仓中仍高频使用: - -| 导入面 | 使用文件数(约) | -| --- | ---: | -| `app.log` | 97 | -| `app.plugins` | 81 | -| `app.core.config` | 71 | -| `app.schemas.types` | 67 | -| `app.schemas` | 49 | -| `app.utils.http` / `app.utils.string` | 45 / 43 | -| `app.core.event` | 42 | -| `app.sdk.media` | 33 | -| `app.sdk.logging` | 24 | -| `app.sdk.config` / `app.sdk.network` | 20 / 18 | -| `app.core.context` | 17 | -| `app.helper.downloader` / `app.helper.sites` | 14 / 13 | -| `app.chain.download` / `subscribe` / `media` | 11 / 10 / 9 | -| `app.db.site_oper` | 10 | - -现有 SDK 也直接导出 settings/global_vars、具体 PluginManager/ModuleManager、具体 EventManager 和多个跨层 Helper。它能维持兼容,但不是新插件应无限扩张依赖的依据。 - -#### 治理策略 - -1. 把 SDK 和兼容清单视为版本化公开产品,不是临时代码。 -2. 建立 `sdk-public-api.json` 或等价测试清单,记录模块、符号、类型身份和行为测试。 -3. 新增 SDK 能力优先导出 Protocol/Facade,不新增内部 manager 的可变状态。 -4. 宿主内部不因兼容存在而继续使用旧 `app.core.*`、`app.helper.*`、`app.utils.*` 路径。 -5. V3 默认只增不删。弃用必须包含:替代入口、诊断、至少一个完整发布周期、官方插件仓扫描、样例第三方插件验证。 -6. 兼容模块必须精确路由,不能用宽泛 `__getattr__` 吞掉拼写错误。 -7. 需要保持类/单例身份的符号必须测试 `is`,不能只测试能导入。 - -#### 完成标准 - -- SDK 公开面有机器可读清单和变更审查。 -- 每次迁移明确列出旧路径、新路径、身份要求和保留期限。 -- 独立插件仓中 V3 实际可加载实现的静态导入扫描通过。 -- V3 治理批次不删除现有 113 个模块别名、1 个包别名和 55 个符号别名。 - -### 6.15 配置、缓存和错误策略分散 - -#### 现状 - -- `settings` 在大量模块中直接读取,这是运行配置的事实 API。 -- `SystemConfigOper()` 在几十个文件中直接构造,运行配置与持久化用户配置边界模糊。 -- 缓存装饰器、文件缓存、Redis、内存状态由调用方自行选择,缺少能力级一致失效策略。 -- 部分层把异常转成 `schemas.Response`,部分抛异常,部分发送 `SystemError`,部分只记录日志。 - -#### 目标 - -- `settings` 仅表示启动时环境配置;用例接收所需配置快照,而不是读取整个 settings。 -- 持久化系统配置通过窄 `SystemConfigReader/Writer`。 -- 每个能力明确缓存所有者、key、TTL、负缓存、失效事件和降级策略。 -- Domain/Application 返回领域错误;API 映射 HTTP;Event/Background runtime 决定重试、通知和死信。 -- 不在第一阶段引入统一“万能 Result”类型;先统一错误所有权。 - -## 7. 插件兼容治理专章 - -### 7.1 插件是外部消费者,不是内部实现目录 - -本次后端重构必须同时接受两个事实: - -1. `app/plugins/` 是运行时副本,不能按其当前内容决定宿主架构。 -2. 插件运行时仍依赖宿主提供的 `_PluginBase`、旧导入、SDK、事件、模块、API、调度和配置能力,这些必须作为黑盒 ABI 保护。 - -兼容审计至少包含: - -- 独立官方插件仓 `plugins.v2/`、`plugins.v3/`。 -- `runtime/compat/manifest.py`。 -- `app/sdk/` 公开导出。 -- PluginManager 实际消费的 hook。 -- `tests/test_legacy_import_compat.py`、`tests/test_legacy_plugin_resource_imports.py`、`tests/test_plugin_sdk.py` 等。 -- 一组最小第三方插件 fixture,覆盖旧导入、事件、动态 API、模块、服务和 Agent tool。 - -### 7.2 必须保持的兼容维度 - -| 维度 | 必须验证 | -| --- | --- | -| 导入 | 旧模块和旧符号可导入;包/模块形态与子模块导入不冲突 | -| 身份 | Singleton、Manager、EventManager、公开类在旧新路径下按要求保持 `is` | -| 构造 | 插件基类和 Chain 的无参构造仍工作 | -| Hook | 方法名、参数、同步/异步、None/空列表语义、异常隔离不变 | -| Module | 插件优先级、短路、列表合并、签名接力语义不变 | -| Event | 注册装饰器、目标插件过滤、链式顺序、热卸载清理不变 | -| API | 路径、鉴权默认值、raw 返回、原生 Response、流式返回不被主 API 信封改变 | -| Service | 定时任务描述、Cron、启动/停止和去重语义不变 | -| UI | form/page/dashboard/sidebar DTO 形态不变 | -| Data | 插件配置和 PluginData 的 key、序列化、隔离和迁移行为不变 | -| Reload | 本地开发 watcher、更新、备份、重新实例化和路由刷新顺序不变 | - -### 7.3 兼容迁移模式 - -每个公开模块迁移采用以下模式: - -```text -旧入口(永久或长期 Facade) - | - v -新 Application/Runtime/Adapter 实现 - ^ - | -startup 注入具体依赖 -``` - -规则: - -1. 先新增实现和契约测试。 -2. 旧入口改为薄委托,但保留公开名称。 -3. 宿主内部调用切换到新入口。 -4. 官方插件无需修改即可通过。 -5. 新 SDK 入口可逐步推广,但不以删除旧路径作为同一批次完成条件。 -6. 若类的 `__module__`、pickle、反射或前端模块名会变化,必须显式增加兼容测试。 -7. 已废弃的模块路径统一登记到 `app/runtime/compat/manifest.py`,不得在新实现模块内复制导出旧对象。 -8. `PluginManager`、`PluginHelper`、`MoviePilotServerHelper` 等仍被插件直接依赖的正式公共路径必须保留原有公共合同和对象身份;其中已经完成职责拆分的入口可以委托新实现,但尚未迁出的算法仍可能留在原类中,不能把它们笼统描述成纯薄 Facade。 -9. 新实现包默认不增加 `__all__`、惰性 `__getattr__` 或模块级旧类别名;确需公开时进入 `app/sdk` 导出清单和架构快照。 - -### 7.4 插件兼容禁止事项 - -- 不扫描 `app/plugins/` 后批量改写插件源码。 -- 不把插件 API 自动包装成主 API 统一信封。 -- 不改变 `get_module()` 返回字典的方法名或 `run_module()` 聚合顺序。 -- 不因新 Protocol 存在就要求旧插件继承它。 -- 不把热重载问题用“重启生效”替代。 -- 不把 SDK 改成全新对象,导致旧路径和新路径的 Singleton 身份分裂。 -- 不在 V3 普通架构 PR 中删除兼容 manifest 项。 - -## 8. 分阶段实施路线 - -每个阶段可以拆成多个小 PR/提交。阶段之间有依赖,阶段内部按风险从低到高推进。 - -### 阶段 0:冻结契约、纠正 P0 兼容边界 - -#### 目标 - -先知道什么不能变,并修复会阻碍后续治理的契约冲突。 - -#### 工作项 - -1. 生成并提交当前架构基线:模块、导入边、自有 SCC、禁止边白名单。 -2. 生成 `run_module` 方法清单,覆盖约 211 个方法名及调用位置。 -3. 生成插件 hook、SDK 导出、compat manifest 和官方插件导入快照。 -4. 增加动态插件 API 真实请求测试,恢复/确认 raw free-return 边界。 -5. 增加启动矩阵:`app.factory:app`、主入口、安全模式、正常模式、关闭失败。 -6. 增加 Event/Module/PluginManager 对象身份测试。 -7. 记录当前导入耗时和启动关键阶段耗时,作为后续非功能基线。 - -#### 不做 - -- 不拆巨型文件。 -- 不移动公开类。 -- 不删除兼容映射。 -- 不改数据库结构。 - -#### 验收 - -- 行为契约成为测试或机器可读清单。 -- 动态插件 API 的返回边界有明确、可执行测试。 -- 架构基线可以在 CI 中稳定复现。 - -### 阶段 1:补架构门禁并消除低风险环 - -#### 目标 - -先让依赖图停止恶化,再处理不涉及业务算法的环。 - -#### 工作项 - -1. `app.schemas` 改为显式/惰性兼容导出;宿主内部使用精确子模块导入。 -2. 移除重复 `system` 导出,增加公开符号快照和冲突检查。 -3. DB 内部模块从具体 `app.db.base/decorators/session/engine` 导入,不经根入口回流。 -4. 消除 `app.chain._music` ↔ `subscribe`:把订阅媒体 key、meta 构造移到 Domain/Application 的单向依赖模块。 -5. 消除 `filemanager` ↔ `transhandler`:提取共享 DTO/Protocol。 -6. 新门禁设为禁止新增 Adapter→DB、Runtime→DB、API→Session/Model、Application→Agent 具体实现。 - -#### 兼容方式 - -- `app.schemas.X` 和 `app.db` 旧导出继续工作。 -- `app.chain.subscribe` 的旧辅助函数保留转发,直到插件扫描证明可移除;V3 默认不移除。 -- 文件改包时保持完整导入路径和类名。 - -#### 验收 - -- 自有目标 SCC 至少减少 3 个。 -- 新门禁无无期限宽泛豁免。 -- 官方插件仓静态导入和宿主兼容测试通过。 - -### 阶段 2:显式运行时组合与事件/模块调度契约 - -#### 目标 - -把隐藏在 Singleton、装饰器和字符串里的宿主运行机制变成可组合、可测试的基础设施。 - -#### 工作项 - -1. 提取 `ModuleInvocationDispatcher`,由 `ChainBase` 委托。 -2. 引入 method contract registry,先覆盖高频能力族。 -3. 提取 Event registry、binding resolver、dispatcher、error policy。 -4. 引入生命周期组件描述,逐个登记 start/stop/safe-mode/timeout。 -5. Event resolver 未命中增加诊断;迁移宿主 handler 到显式 resolver。 -6. 让 Chain 新代码可注入 `ChainRuntimeContext`,保留无参兼容 provider。 - -#### 风险控制 - -- 先复制现有算法到可测试组件,再让 Facade 委托,不能边提取边重写规则。 -- 同步和异步聚合测试必须成对。 -- 对广播事件使用可控 executor 和 loop fixture。 -- PluginManager/EventManager/ModuleManager 身份保持。 - -#### 验收 - -- 调度算法不依赖真实插件、数据库和线程即可单测。 -- Event handler 不再由总线隐式构造,或剩余命中有明确白名单和日志。 -- 生命周期顺序由测试锁定。 - -### 阶段 3:数据访问与 API 用例收口 - -#### 目标 - -让事务、权限和提交后副作用拥有清晰所有者。 - -#### 工作项 - -1. 从订阅删除/修改、站点修改、工作流修改等写端点开始,建立 Application command。 -2. 把 ORM 直接写入、commit/rollback、事件、调度、外部上报迁入用例。 -3. 建立必要的 Oper/UnitOfWork 端口。 -4. 模型类方法在宿主内部逐步停用,保留兼容转发。 -5. 端点只做 FastAPI 参数和结果映射。 -6. Scheduler 的数据库清理逻辑迁到 Application maintenance use case,Scheduler 只触发。 - -#### 推荐垂直切片顺序 - -1. 删除订阅。 -2. 手工触发订阅搜索。 -3. 站点启停/修改。 -4. 工作流启停/删除。 -5. 历史删除与清理。 -6. 插件状态与配置更新。 - -每个切片单独验证,不等待所有端点一起完成。 - -#### 验收 - -- 已迁移端点不持有 Session、不直接调用 Model/Oper/Scheduler/ServerHelper。 -- commit 失败时不发送成功事件、不上报、不调度后续任务。 -- 同步/异步路径和权限结果不变。 - -### 阶段 4:按用例拆分巨型 Chain - -#### 目标 - -在数据和运行时边界已经稳定后,拆解业务编排。 - -#### 工作项 - -1. Subscribe:身份、命令、识别、搜索、匹配、完成。 -2. Search:计划、并发执行、归一化、过滤、流式进度。 -3. Transfer:计划、执行、元数据、提交、后处理。 -4. Download:候选选择、客户端提交、字幕、审计。 -5. Media:身份解析、Provider 识别、缓存和同步/异步共核。 -6. Message:通道解析、路由、交互状态和业务 handler。 - -#### 拆分方式 - -- 每次选一个公开方法作为纵向切片。 -- 先做 characterization test。 -- 新服务返回结构化结果,Facade 负责兼容旧返回。 -- 事件、通知、历史和缓存失效点写入时序测试。 -- 纯策略下沉 Domain;短用例进入 Application;多域串联保留 Chain。 - -#### 验收 - -- 目标 Chain 文件规模和出度持续下降。 -- 不新增 `misc.py`、`common.py`、`helper.py` 式无边界收纳文件。 -- Facade 兼容测试覆盖所有被迁移公开方法。 - -#### 阶段 0-4 当前落地索引 - -| 阶段 | 已落地入口 | 已锁定的关键语义 | -| --- | --- | --- | -| 0 | `scripts/architecture/baseline.py`、`scripts/schema/exports.py`、architecture fixtures | 模块/边/SCC、显式 `__all__` SDK/compat、事件、`run_module`、官方插件 V3/V2/default 有效实现的导入和钩子快照 | -| 0 | `app/adapters/web/plugin/routes.py`、`app/application/plugin/routes.py` | 主程序继续统一 envelope;动态插件 API 默认 raw,自定义状态码、原生 Response、文件/流响应不被改写 | -| 0 | `MoviePilot-Frontend/src/api/client.ts` | 联邦插件公共客户端遇到非 `Response` payload 时原样返回;合法 envelope 仍保留统一错误反馈 | -| 1 | `app/schemas/exports.py`、`app/schemas/__init__.py` | Schema 根入口惰性兼容导出,宿主内部使用精确子模块,公开符号由生成清单锁定 | -| 1 | `app/application/subscription/contract.py`、`app/modules/filemanager/module.py` | 订阅身份/元数据和文件管理共享合同改为单向依赖,目标 SCC 不再靠延迟导入维持 | -| 1 | `tests/test_architecture_dependencies.py`、dependency baseline | Adapter/Runtime 到 DB 零新增,API/Session/Model 与 Application/Agent 采用趋势基线治理 | -| 2 | `app/runtime/extensions/module/contracts.py`、`dispatcher.py` | 插件优先、短路、列表合并、参数签名和同步/异步执行顺序保持 | -| 2 | `app/runtime/event/{registry,binding,dispatch,errors}.py` | 事件注册、实例解析、分发和错误降级拆开;总线不再隐式构造未绑定处理器 | -| 2 | `app/application/chain/context.py`、`app/startup/lifecycle/components.py`、`scripts/startup/performance.py` | Chain 依赖可注入;正常/安全模式启停顺序、超时、阶段耗时及隔离资源快照可导出测试 | -| 3 | `app/db/uow.py`、`app/application/subscription/{delete,identity}.py` | 订阅删除事务、权限、提交后事件/上报时序归 Application 所有;端点只做传输映射 | -| 3 | `app/application/subscription/query.py`、`app/application/maintenance.py`、`app/db/maintenance.py` | 订阅查询三条垂直切片和六张维护表的保留期/批次/失败汇总归 Application;Scheduler 只触发 | -| 4 | `app/application/search/state.py` | 搜索状态查询和控制从巨型 Chain 提取,保留原同步/异步状态语义 | -| 4 | `app/application/download/tasks.py` | 下载任务查询/控制形成窄用例,Chain 保留用户目标编排 Facade | -| 4 | `app/application/music/catalog.py` | 多来源音乐目录聚合形成可用 fake Provider 测试的应用服务,不改变原搜索命中/回退行为 | -| 4 | `app/application/transfer.py`、`app/application/messaging/session.py` | Transfer、Message 各三条以上状态/控制切片由窄服务承接,旧 Chain 方法保留兼容委托 | - -这些切片记录阶段 0-4 的实施历史;当前工作树机器基线中的 API endpoint→Model、endpoint→Session、Application→DB、Application→Agent 具体实现边均为 0。后续新增端点仍必须通过 Application/Repository 端口,不能把已清零的边重新引入。 - -### 阶段 5:拆分插件宿主与外部服务适配 - -#### 目标 - -让 PluginManager 只管理扩展运行,让 Adapter 只做 I/O。 - -#### 工作项 - -1. Plugin hook contract/registry/projection 从 PluginManager 提取。 -2. 插件市场查询和安装进入 Application 用例。 -3. `PluginHelper` 拆 market client、包管理、依赖安装。 -4. `MoviePilotServerHelper` 拆 transport client 与分享/统计用例。 -5. 动态路由以 `DynamicRouteRegistry` 端口连接 FastAPI adapter。 -6. Runtime 的系统配置访问改为启动注入的 reader。 - -#### 验收 - -- Runtime 和 Adapter 不再导入 DB Oper。 -- PluginManager 仍保持完整 V3 公共方法和实例身份。 -- 插件安装失败可以明确回滚文件、依赖、实例和路由中的哪些步骤。 -- 热重载与在线更新测试覆盖。 - -#### 当前已落地切片 - -| 职责 | Canonical 实现 | 旧入口/兼容方式 | -| --- | --- | --- | -| 插件钩子契约 | `app/runtime/extensions/plugin/contracts.py` | 旧插件仍按鸭子类型实现,不要求继承 Protocol 或基类 | -| 插件类与运行实例注册 | `app/runtime/extensions/plugin/registry.py` | `PluginManager.plugins`、`running_plugins` 仍返回原有可变映射 | -| 命令/API/服务/模块/动作/联邦/认证/侧栏/仪表板元数据投影 | `app/runtime/extensions/plugin/projection.py` | `PluginManager.get_plugin_*()` 原方法委托,异常隔离和 DTO 不变 | -| 插件配置和数据持久化 | `app/runtime/extensions/plugin/storage.py` | 启动层用 `SystemConfigOper`、`PluginDataOper` 注入;Runtime 不导入 Oper | -| 市场目录和版本/来源合并 | `app/application/plugin/catalog.py` | `PluginManager.get_online_plugins()` 等公开方法经启动注入的目录工厂委托 | -| 插件安装阶段编排 | `app/application/plugin/install.py` | API 和 Agent 共用命令;旧管理器/Helper 安装入口保留 | -| 动态插件路由 | `app/application/plugin/routes.py` + `app/adapters/web/plugin/routes.py` | 过渡聚合文件无插件 ABI,已删除;插件响应默认 raw | -| 插件文件夹清理 | `app/application/plugin/folders.py` | API 与 Agent 直接调用 canonical 用例,兼容新旧配置存储形态 | -| 市场读取 | `app/adapters/external/plugin/client.py` | `app.adapters.external.market.PluginHelper` 保留正式公共实现路径 | -| 包与依赖安装 | `app/adapters/system/plugin/package.py`、`dependency.py` | PluginManager 原方法只做委托和日志/上报 | -| 中心服务统计/分享 | `app/application/server/report.py`、`share.py` | `MoviePilotServerHelper` 保留 transport 和公开静态/类方法,由启动层注入用例 | - -阶段 5 的“拆分”是职责入口和组合依赖的拆分,不等于本轮把旧 `PluginHelper` 的全部 3,066 行算法复制到新文件。旧类仍是正式 V3 ABI,保留原类名、对象/静态方法和旧私有调用;新宿主路径使用上述 canonical client、package、dependency 和 Application command。后续如需继续内移算法,必须先增加旧私有调用命中统计和逐方法行为快照。 - -**实施记录(2026-08-23)**:`app.runtime.observability.observe_compat_facade()` 为 -`PluginManager`、`PluginHelper`、`MoviePilotServerHelper` 的公开及旧私有方法记录 -`compat.facade.hit`。指标只使用 Facade 名称、稳定方法名、公开/私有可见性和固定 ABI 来源,保留 -同步/异步 descriptor、签名和对象身份;三类入口的离线测试已覆盖命中记录。该统计是迁移取证,不代表 -算法已全部内移,后续仍需按命中最高的方法建立行为快照后逐项迁移。 - -这里的“兼容”分为两类,后续 AI 不得混淆: - -1. 已迁移、只需恢复旧模块路径的入口,统一登记到 `app/runtime/compat/manifest.py`,新实现模块不复制旧对象导出。 -2. 插件直接依赖其对象身份或静态方法的正式 ABI,如 `PluginManager`、`PluginHelper`、`MoviePilotServerHelper`,继续留在原路径;已拆出的职责由 canonical 组件承接,未迁出的实现仍由原类承担。它们不是在新模块里额外定义一份别名,也不能为了“看起来统一”复制一套旧类。 - -`app.sdk.plugins` 只显式导出 `ModuleManager`、`PluginManager`。阶段 5 新实现包没有增加 `__all__`、惰性 `__getattr__`、旧 Manager/Helper/Oper 别名;任何新增插件公开能力必须先进入 SDK 清单和快照测试。 - -### 阶段 6:Agent 与模块族治理 - -#### 目标 - -处理高体量但相对独立的垂直子系统,避免阻塞前面主链路治理。 - -#### 工作项 - -1. Provider 规格数据化并与授权、模型目录、协议 client 分离。 -2. Agent 执行事件与 HTTP/SSE 映射分离。 -3. 工具注册按能力域延迟加载,降低工厂出度。 -4. 消息模块、媒体服务器模块、下载器模块分别固化 SPI。 -5. 消除自有模块内部 SCC;隔离第三方包局部环。 - -#### 验收 - -- Provider ID/配置和工具 schema 快照不变。 -- 工具工厂出度显著下降,目标不高于 20。 -- Agent 单元测试不需要启动完整 MoviePilot runtime。 -- 模块族可以用 host contract fixture 独立验证。 - -### 阶段 7:SDK 收敛、兼容治理与长期预算 - -#### 目标 - -让兼容从“永久扩张”变成“有版本、有观测、有替代入口”的产品能力。 - -#### 工作项 - -1. 发布 SDK public manifest 和变更规则。 -2. 为旧入口增加 DEBUG 级命中统计,不记录插件敏感数据。 -3. 标记推荐的新 SDK Facade;文档和新官方插件优先使用。 -4. 建立弃用决策模板,但 V3 普通版本不删除旧映射。 -5. 将架构指标纳入 CI 报告:SCC、禁止边、目标直接 DB 调用、巨型文件、SDK 变化。 - -#### 验收 - -- 新插件可以只依赖 SDK/Host SPI 完成常见能力。 -- 旧插件无需修改继续工作。 -- 每个弃用项有真实命中数据和替代方案,不按时间自动删除。 - -### 阶段 6-7 收口记录(2026-08-18) - -本轮不再把阶段 6-7 留作“以后再拆”的跨层债务,已完成以下可执行项: - -| 主题 | 收口结果 | 兼容边界 | -| --- | --- | --- | -| Agent / API / Workflow 访问插件运行时 | 改为 `app.application.plugin.runtime.get_plugin_manager()` 端口;入口文件不再静态依赖 `runtime.extensions.plugin_manager` | `app.sdk.plugins.PluginManager` 的真实类身份不变 | -| API 访问模块与调度器 | 改为 `app.application.module`、`app.application.scheduling` 端口;由启动组合根注册实现 | 端点测试和旧调用顺序不变 | -| Chain 模块调度 | `ChainRuntimeContext.module_dispatcher_factory` 注入 `ModuleInvocationDispatcher` | `run_module`/`async_run_module` 方法名、短路、列表聚合和异常语义不变 | -| Agent 插件工具目录 | 工具工厂与 Agent 编排通过窄函数读取插件投影和 revision,不再直接依赖具体 Manager 类型 | 插件 `get_agent_tools()`、工具 schema、名称和 revision 快照不变 | -| PluginManager 文件监控 | `PluginMonitorController` 持有线程和停止事件,`PluginChangeMonitor` 只处理变化归并 | `reload_monitor`、`stop_monitor`、本地同步/热重载顺序不变 | -| SDK / Compat | 新模块不复制旧 Manager/Helper/Oper;删除的 `service_registry` 通过 `manifest.py` 精确映射到 SDK | V3 旧导入、对象 identity 和动态 API raw 合同保留 | - -阶段 6-7 后续只允许做同一职责域的性能、可观测性和实现细化,不得重新引入跨层具体导入;新增能力必须先进入端口、SDK 清单或兼容清单,再接入宿主。 - -## 9. 推荐的首批实施任务 - -以下任务粒度适合其他 AI 独立执行,并且互相依赖清晰。 - -### 任务 A:插件动态 API raw 契约 - -**范围**:`app/application/plugin/routes.py`、`app/adapters/web/plugin/routes.py`、`app/api/response.py`、`app/factory.py`、对应测试。 -**目标**:主 API 统一信封,插件动态 API 默认自由返回。 -**禁止**:修改插件副本、修改普通 API 响应格式、修改鉴权默认值。 -**验证**:dict、Pydantic model、Response、StreamingResponse、204、自定义状态码、OpenAPI。 - -### 任务 B:`_music`/`subscribe` 环拆除 - -**范围**:`app/chain/_music.py`、`app/chain/subscribe.py`、订阅身份相关 Domain/Application 文件和测试。 -**目标**:迁移 `build_subscribe_meta`、`_subscribe_media_key(s)` 的真正所有权,消除延迟导入。 -**禁止**:改变音乐搜索、订阅完成判定、媒体身份字段、旧辅助函数路径。 -**验证**:音乐单曲/专辑、缺少远端 ID、同步/异步识别、旧路径导入、SCC。 - -### 任务 C:Schema 根入口惰性兼容导出 - -**范围**:`app/schemas/__init__.py`、内部精确导入、导出清单和测试。 -**目标**:消除全量星号导入和 schema SCC。 -**禁止**:删除 `app.schemas.X`、改变 Pydantic schema 和 OpenAPI。 -**验证**:公开符号快照、重复名、冷导入、全部 schema model rebuild、API OpenAPI。 - -### 任务 D:Chain 模块调度器提取 - -**范围**:`app/chain/__init__.py`、`app/runtime/extensions` 新调度组件、契约测试。 -**目标**:原样提取插件/系统模块调度算法。 -**禁止**:改变执行顺序、异常、限流、聚合、线程池策略。 -**验证**:参数化契约矩阵及 PluginManager/ModuleManager fake。 - -### 任务 E:订阅删除垂直切片 - -**范围**:`app/api/endpoints/subscribe.py` 删除端点、Application command、Oper 和测试。 -**目标**:API 不直接管理事务;提交成功后才发送事件和上报。 -**禁止**:改变路由、权限、响应、媒体身份、事件 payload。 -**验证**:存在/不存在、普通用户、管理员、commit 失败、事件失败、上报失败。 - -### 任务 F:外部服务 client 与分享用例分离 - -**范围**:`app/adapters/external/server.py` 选一个低风险能力,例如 workflow 分享。 -**目标**:client 不导入 Oper,Application 负责数据读取和 DTO。 -**禁止**:一次拆完整个 1,900 行文件。 -**验证**:旧 Helper 方法、请求参数、缓存、错误降级和 fake transport。 - -## 10. AI 实施标准作业流程 - -其他 AI 接到本文件中的任务时,必须按以下顺序执行。 - -### 10.1 开始前 - -1. 读取根 `AGENTS.md`、仓库 `AGENTS.md` 和所涉及目录规则。 -2. 检查分支、工作树、上游差异;不得覆盖用户或其他进程改动。 -3. 阅读公开入口、所有调用方、相关测试和兼容 manifest。 -4. 如果涉及插件契约,扫描 `../MoviePilot-Plugins/plugins.v2` 与 `plugins.v3`;不要把 `app/plugins` 当源码。 -5. 记录迁移前静态依赖、公开符号和行为快照。 - -### 10.2 任务说明必须包含 - -```yaml -objective: 单一可验证目标 -scope: - allowed_files: [] - affected_modules: [] -out_of_scope: [] -current_evidence: [] -public_contracts: - imports: [] - methods: [] - events: [] - api: [] -plugin_compatibility: - old_paths: [] - identity_requirements: [] - runtime_behaviors: [] -migration_steps: [] -tests: - focused: [] - architecture: [] - compatibility: [] -rollback: 如何恢复委托而不丢数据 -done_when: [] -``` - -### 10.3 编码规则 - -1. 一个批次只修一个依赖方向或一个垂直用例。 -2. 先建新实现,再让旧入口委托;不能先删除旧入口。 -3. 新增类和方法按仓库规则写类级、方法级中文注释,说明原因和关键约束。 -4. 注释不能只复述代码;兼容转发必须注明保留原因和不可改变的语义。 -5. 不用延迟导入作为最终环修复;它只能作为短期过渡且必须有清理任务。 -6. 不引入 `Manager2`、`HelperNew` 等无所有权名称。 -7. 不创建通用 `common.py`/`misc.py` 收纳不相关逻辑。 -8. 同步和异步逻辑优先共享纯核心,不用复制粘贴维持两套算法。 -9. 不把异常全部捕获后返回 False;错误类型和降级责任由边界决定。 -10. 不顺带格式化或重排无关大文件。 -11. 新增生产 Python 模块的文件名只使用一个小写单词;同一主题需要多个模块时,建立主题子目录,并在其中使用单词文件名。 -12. 已存在的多词公开导入路径只有在插件或兼容扫描证明不能迁移时才保留为薄门面,不得继续作为新模块命名模板。 -13. 测试文件继续使用 pytest 的描述性 `test_.py` 命名,不受生产模块单词命名约束。 - -### 10.4 每次迁移的七步闭环 - -1. **刻画**:补现有行为测试。 -2. **建契约**:定义 Protocol、DTO 或机器可读清单。 -3. **提取**:不改行为地移动单一职责。 -4. **委托**:旧入口调用新实现。 -5. **切换**:宿主内部新代码改用 canonical 入口。 -6. **兼容**:运行旧导入、对象身份和插件 fixture。 -7. **度量**:报告环、禁止边、出度、文件规模的变化。 - -任何一步没有验证,任务都不能标记为完成。 - -## 11. 验证矩阵 - -### 11.1 每个架构批次的最低门禁 - -```bash -./.venv/bin/python -m pytest tests/test_architecture_dependencies.py -q -./.venv/bin/python -m pytest tests/test_legacy_import_compat.py -q -./.venv/bin/python -m pytest tests/test_legacy_plugin_resource_imports.py -q -./.venv/bin/python -m pytest tests/test_plugin_sdk.py -q -./.venv/bin/python scripts/architecture/task_ownership.py -``` - -再运行本批次聚焦测试。涉及发布级公共行为时,使用仓库完整门禁: - -```bash -./.venv/bin/python tests/run.py -``` - -本地若遇到已知二进制 `sites` 扩展导致的 `137/SIGKILL`,应按仓库既有测试 Stub 方案隔离;不能把进程被杀误报为断言失败,也不能因此跳过所有验证。 - -### 11.2 按边界追加的测试 - -| 变更边界 | 必测内容 | -| --- | --- | -| Event | 顺序、优先级、并发、handler 快照、目标插件、异常、热卸载 | -| Module | 插件优先、短路、列表合并、签名接力、sync/async、限流 | -| Plugin | hook 空值/异常、状态、配置、服务、API、页面、更新、热重载 | -| API | 路径、鉴权、响应信封/raw、状态码、OpenAPI、stream disconnect | -| DB | commit/rollback、并发、权限过滤、提交后副作用、同步/异步 | -| Startup | 主入口、`app.factory:app`、安全模式、部分失败、逆序关闭 | -| SDK/Compat | 旧路径、新路径、符号集合、对象身份、pickle/反射(如适用) | -| Agent | Provider 配置、工具 schema、流事件、取消、usage、插件工具 | - -### 11.3 非功能回归 - -每个阶段至少记录,并将结果写入 `tests/fixtures/architecture/`: - -- 冷导入 `app.factory` 耗时。 -- 正常和安全模式生命周期耗时;当前基线使用 `scripts/startup/performance.py` 的 no-op 组件采样,明确不启动真实插件、网络或用户数据库。 -- 隔离采样的线程数、后台任务数和数据库连接数范围;真实生产连接数由部署监控另行采集。 -- 架构模块数、边数、自有 SCC、目标禁止边数量。 -- 目标文件行数、方法最大行数、出度。 - -默认不要求每项立即变小,但不得无解释显著恶化。启动和请求关键路径超过 10% 的回归必须调查。 - -当前可复现命令: - -```bash -./.venv/bin/python scripts/startup/performance.py --write --repeat 3 -./.venv/bin/python scripts/architecture/baseline.py --check-host -./.venv/bin/python scripts/architecture/baseline.py \ - --check-plugins --plugin-repo ../MoviePilot-Plugins -``` - -### 11.4 2026-08-18 当前验证快照(收口批次) - -| 范围 | 命令 | 结果 | -| --- | --- | --- | -| 后端完整门禁 | `./.venv/bin/python tests/run.py` | 4,914 passed、2 failed、3 skipped(2026-08-18);失败为未修改的 Agent 图片能力测试,架构专项不受影响 | -| 架构与插件快照 | 分别运行 `--check-host` 与 `--check-plugins --plugin-repo ../MoviePilot-Plugins` | 已通过,基线已更新为 746 模块 / 6,024 边 | -| 前端联邦 API 客户端 | `yarn test:run src/api/__tests__/client.spec.ts src/api/__tests__/index.spec.ts` | 36 passed | -| 前端类型检查 | `yarn typecheck` | 通过 | -| V3 插件契约与版本门禁 | `../MoviePilot/.venv/bin/python -m pytest tests/ci/test_v3_contract.py tests/ci/test_plugin_release_gate.py -q` | 16 passed | -| 本次 IMDb/TVDB 插件适配 | `../MoviePilot/.venv/bin/python -m pytest tests/v3/imdbsource tests/v3/tvdbdiscover -q` | 14 passed | - -架构专项复核:`tests/test_architecture_dependencies.py`、`tests/test_architecture_contract_baseline.py`、插件 API/注册/SDK 相关聚焦用例共 71 passed。全量门禁中的 2 个失败均来自未修改的 `tests/test_agent_image_capability.py`:其一依赖当前模型目录未提供的 MiniMax 图片能力元数据,其二直接调用消息链时未装配 Agent service;它们不是本批次的层间依赖或插件兼容回归。 - -独立插件仓分代回归已使用主仓 `.venv` 通过 `tests/run.py` 复现:CI 35 passed、V3 80 passed、V2 30 passed,共 145 passed。插件仓自身 `.venv` 直接运行时因缺少主程序依赖 `httpx2` 在收集阶段失败;该问题属于测试环境依赖边界,CI 应统一使用主仓锁定运行环境或在插件仓补齐同版本依赖。官方插件语义基线未变化,仅 provenance HEAD 更新,已审查后不刷新 fixture。 - -## 12. 量化治理目标 - -### 12.1 已达成的边界指标(阶段 0-2) - -- 动态插件 API 返回契约明确并有真实请求测试。 -- `run_module` 方法名和插件 hook 100% 进入契约快照。 -- 212 个宿主观察模块 spec 的 legacy aggregation 为 0;未知第三方方法继续兼容并记录真实命中。 -- 自有 SCC 不增长,消除 `_music`/`subscribe`、schemas、DB 根回流等首批环。 -- Adapter→DB、Runtime→DB、Application→DB、API/Agent/Chain/Workflow→DB 新增裸依赖均为零。 -- 生命周期组件和 Event resolver 命中可观测。 - -### 12.2 持续门禁与同职责域细化(阶段 3-5) - -- 本轮纳入阶段 3 的写端点不再直接持有数据库事务;当前机器基线中的 endpoint→Session、endpoint→Model、Application→DB 和目标 Adapter/Runtime→DB 边均为 0。后续只允许防止这些边重新引入,不再把历史边数量当作未完成任务。 -- PluginManager 不直接做市场、包管理、压缩包和备份实现;外部 Adapter 不导入 Oper。 -- 重点 Chain 的垂直切片和 `ChainBase` 脱离真实 Runtime 的单测属于同一职责域内的持续细化,不再作为跨层拆分阻塞项。 - -### 12.3 长期 ABI、性能与实现预算(阶段 6-7) - -- 除明确第三方局部豁免外,自有 Python 模块 SCC 保持归零。 -- `app.agent.tools.factory` 出度从约 99 降至不高于 20,属于 Agent 同一职责域内的实现预算,不是本轮层间拆分的遗留边。 -- 新 API endpoint 原则上不超过 80 行,新 Application 用例原则上不超过 150 行。 -- 新插件常用能力只依赖 `app.sdk`/Host SPI;旧插件仍可运行。 -- 兼容面有版本、命中数据、替代入口和机器可读清单。 - -这些是治理指标,不是为了达标而机械切文件。任何指标变化都要结合职责是否真正单一判断。 - -## 13. 风险清单与回滚策略 - -| 风险 | 典型触发 | 防护 | 回滚 | -| --- | --- | --- | --- | -| 插件模块结果变化 | 改写 `run_module` | 契约矩阵、记录调用序列 | Facade 切回旧 dispatcher | -| 事件顺序/并发变化 | 拆 EventManager | 可控 loop/executor 测试 | 保留旧 dispatcher 注入 | -| 插件 API 被包装 | 共用主 RouteClass | 真实请求 raw 测试 | 动态路由强制 raw | -| Singleton 身份分裂 | 新旧入口各自实例化 | `is` 测试、startup provider | 旧入口转回同一 provider | -| DB 副作用提前 | 事务迁移 | commit 失败测试、after-commit | 用例切回旧端点实现 | -| 热重载残留 | 拆 PluginManager | handler/route/service 快照 | 切回旧 lifecycle Facade | -| Provider 配置失效 | 拆 LLM provider | 配置/ID 快照与真实 fixture | 保留旧 resolver | -| 启动死锁或提前 I/O | 组合根迁移 | import/startup 线程连接快照 | 单资源恢复旧 initializer | -| Pickle/反射路径变化 | 文件改包/类移动 | `__module__`/反序列化测试 | 旧类留在原模块作门面 | -| 缓存不一致 | 调用层迁移 | key/TTL/失效时序测试 | Facade 继续使用旧缓存策略 | - -架构改造的回滚单位必须是“旧 Facade 的委托切换”,不能依赖回滚数据库迁移或清理用户数据。 - -## 14. 明确禁止的重构方式 - -1. 把大文件机械切成多个互相任意导入的小文件。 -2. 用函数内导入、`TYPE_CHECKING` 或字符串模块名掩盖真实运行依赖,并把它当作完成。 -3. 新建另一个全局 Service Locator 取代 Singleton。 -4. 为追求纯层级而复制相同 DTO、枚举和媒体身份规则。 -5. 一次性重写 Chain、PluginManager、EventManager 或 Agent orchestrator。 -6. 在同一批次同时移动类、改参数、改返回、改异常和改缓存。 -7. 删除旧导入后批量修改官方插件来“证明兼容”。 -8. 以 `app/plugins` 当前副本扫描结果代替独立插件生态审计。 -9. 把主 API 的 `{success, message, data}` 信封强加给动态插件 API。 -10. 以 build/pytest 通过代替依赖图、ABI 和启动副作用验证。 -11. 用 LOC 作为唯一目标,导致职责更分散但依赖没有变少。 -12. 架构批次夹带数据库 schema、前端协议或资源文件变更。 - -## 15. 完成定义 - -单个治理任务只有同时满足以下条件才算完成: - -1. 目标职责有明确所有者和 canonical 路径。 -2. 旧公开入口按兼容要求保留。 -3. 宿主内部调用已经切到正确入口,不继续扩大旧模式。 -4. 静态依赖方向改善,有前后数据。 -5. 行为、错误、同步/异步和生命周期测试通过。 -6. 插件导入、hook、对象身份和动态 API 相关测试通过。 -7. 没有把问题转移成新的延迟导入、全局容器或无边界 Helper。 -8. 相关架构规则、compat manifest、SDK 清单和文档已同步。 -9. 变更范围可独立回滚,不依赖数据降级。 -10. 汇报中明确区分已验证、未验证和剩余风险。 - -## 16. 后续文档维护 - -- 每完成一个阶段,在本文对应工作项后记录实际提交、指标变化和剩余例外。 -- 若目标目录与 `docs/rules/05-architecture.md` 冲突,以更新后的正式规则为准,并在同一提交同步本文。 -- 新增兼容入口必须更新 SDK/compat 机器清单,不只更新文字。 -- 新发现的越层依赖先进入基线并给出清理阶段,不能用永久全局豁免消音。 -- 本文不记录 `app/plugins/` 副本内容;插件生态数据应以独立插件仓的可重复扫描为准。 - ---- - -本轮收口后,后续治理顺序调整为:**协议观测与性能预算 → 同一职责域内的垂直切片 → 兼容命中数据驱动的长期弃用评估**。不得以继续拆文件替代职责、事务和生命周期所有权迁移;每批仍按“契约快照、提取、旧入口委托、独立插件仓扫描、完整门禁”的顺序实施,且不得删除 V3 兼容入口。 diff --git a/docs/refactor/backend-architecture-next-stage.md b/docs/refactor/backend-architecture-next-stage.md deleted file mode 100644 index 67d04a15b..000000000 --- a/docs/refactor/backend-architecture-next-stage.md +++ /dev/null @@ -1,2235 +0,0 @@ -# MoviePilot V3 后端架构二阶段提升方案 - -> 文档性质:当前架构复核、优秀 Python 后端实践对标、AI 可执行任务手册 -> 适用仓库:`MoviePilot`,分支 `v3` -> 审计基线:`41b1460b`(2026-08-24) -> 审计范围:宿主后端;排除 `app/plugins/**` 运行时插件副本 -> 规范优先级:`AGENTS.md` 与 `docs/rules/` 高于本文 -> 相关文档:`docs/architecture-overview.md`、`docs/refactor/backend-architecture-governance.md`、`docs/refactor/backend-module-refactor-compatibility.md` -> 实施进度:阶段 0~6 的宿主架构能力已完成收口;API/Application 公共复杂度基线已清零,启动组合根的 SystemConfigOper 构造点已由 14 降至 1;API 进程内后台任务已完成首批统一登记,插件仓适配和 Outbox 外围扩展仍按风险切片推进。Model/Base 查询与写装饰器、legacy 隐式会话外壳均已清零,插件 SDK 也不再导出宿主 Model。2026-08-23 的长期整改阶段 0 已恢复宿主、启动性能、官方插件和 SDK 契约门禁的可信基线;阶段 1a 已补齐 TaskRegistry owner 零债务门禁和诚实的关停超时语义;阶段 1b1 已收口整理 worker、pending 回放、失败通知、进程内 AI 重试、插件监控与事件投递的生命周期所有权;2026-08-24 的阶段 2 已将 212 个已观察宿主模块方法的 legacy aggregation 清零,并补齐可执行 fanout 与下载器文件 DTO 边界;阶段 3 已将消息交互和远程命令的订阅删除统一到 Application/UoW/outbox,宿主不再调用裸线程统计入口;阶段 4 已统一七种消息渠道的宿主回环与后台执行边界;阶段 5 已补齐事件窗口聚合任务的生命周期所有权;阶段 6 已统一插件文件操作的取消完成语义;阶段 7 已统一插件协程补偿的终态等待;阶段 8 已统一宿主同步函数的异步线程池入口;阶段 9 已统一工作流运行时的宿主获取路径;阶段 10 已统一模块、插件与调度运行时的显式 getter 调用;阶段 11 已清除系统配置 getter 的 Oper 形别名;阶段 12 已完成工作流域的显式 Chain 数据端口迁移;阶段 13 已收口用户、交互与消息链的数据端口;阶段 14 已收口音乐订阅数据端口;阶段 15 已收口站点数据端口;阶段 16 已收口媒体服务器数据端口;阶段 17 已收口下载数据端口;阶段 18 已收口主订阅数据端口;阶段 19 已收口整理数据端口;阶段 20 已收口 Agent 数据端口;阶段 21 已收口监控历史端口;阶段 22 已统一服务配置应用边界;阶段 23 已补齐媒体服务器 API 遗留的类形配置读取路径;阶段 24 已清除 Scheduler 内部无 owner 的协程提交双轨;阶段 25 已补齐 TaskRegistry 跨线程 owner 并迁移整理 AI 接管;阶段 26 已统一 Agent 会话清理提交;阶段 27 已统一历史 AI 进度 owner;阶段 28 已托管旧插件订阅统计线程;阶段 29 已统一 Emby 系条目转换并清零重复代码白名单;阶段 30 已收口插件市场请求级子任务;阶段 31 已托管搜索 AI 推荐任务;阶段 32 已清除事件调度器绕过生命周期 owner 的投递回退;阶段 33 已统一宿主 Agent 运行时的获取路径;阶段 34 已统一 durable-required 事件与 Outbox topic 事实源;阶段 35 已统一 LLM provider 管理 API 的运行时解析路径;阶段 36 已统一 WebAgent 音频能力访问边界;阶段 37 已统一插件输入事件发布路径;阶段 38 已统一 WebAgent 通知事件监听与队列边界;阶段 39 已补齐搜索 SSE 断线时的上游任务清理;阶段 40 已补齐异步防抖取消的终态所有权;阶段 41 已统一优雅重启兜底线程的唯一所有权;阶段 42 已补齐 Telegram typing 的多实例隔离和终态 owner;阶段 43 已统一 Discord typing 的异步 owner 和 shutdown 收尾;阶段 44 已清除 WebAgent 测试临时事件循环提前关闭产生的 CI 红注解;阶段 45 已统一影视与字幕搜索的请求级逐页任务编排;阶段 46 已收口启动性能门禁的托管 runner 假失败与诊断输出;阶段 47 已补齐 Agent 渠道流式刷新任务的重入 owner;阶段 48 已统一工件上传 action 的 Node 24 主版本;阶段 49 已统一插件安装的同步/异步代际解析事实源;阶段 50 已统一插件市场 GitHub 请求降级策略;阶段 51 已统一插件索引请求与响应三态策略;阶段 52 已统一插件 Release 分页策略;阶段 53 已统一远端插件安装模式决策;阶段 54 已补齐同步安装成功后的临时回滚备份清理;阶段 55~56 已收口官方插件观察基线与报告保留策略;阶段 57 已统一进程级运行时 Facade 门禁并补齐 ModuleManager 边界;阶段 58 已消除 AgentTask 关闭回归的跨线程零时长等待竞态;阶段 59 已统一 Feishu 多实例长连接的 SDK 循环路由;阶段 60 已清除命令服务虚假的关停 owner 声明;阶段 61 已统一 Capability Runtime 同步/异步关闭的诚实收敛结果;阶段 62 已统一消息渠道长连接的多实例关闭收敛合同;阶段 63 已补齐应用消息队列线程的关闭收敛合同;阶段 64 已统一共享线程池的有界关闭 owner;阶段 65 已统一异步文件日志的单一有界写入和关闭 owner;阶段 66 已统一 DoH 与共享线程池的有界 executor owner;阶段 67 已补齐工作流活动执行的生命周期 owner。 -> 当前 canonical 状态:API/Application 公共复杂度基线已清零,组合根外 `SystemConfigOper()` 构造和 Model/Oper 隐式事务均为 0;命名 Chain/Agent 数据端口、TaskRegistry owner、Module Contract V2、typed Event、Outbox durable intent、请求关联和插件运行时 getter 已形成当前路径。插件仓适配、未知第三方 fallback 和其它 E1/E3 副作用仍按风险持续治理。 -> 最新阶段:阶段 67 已补齐工作流活动执行的生命周期 owner。 - -## 当前复核结论(2026-08-24) - -本节是本轮全面复核后的当前事实源。本文后续的阶段实施记录保留历史审计证据, -其中的数量和判断以当时审计提交为准,不能直接当作当前未完成项。 - -### 长期整改阶段 0:治理门禁恢复(2026-08-23) - -- 宿主依赖基线已审查 TaskRegistry、有界后台 owner 与插件变更准入接入后的语义差异:当前为 `810` 个模块、`6564` 条内部导入边,12 组重点禁止边继续全部为 `0`,唯一非平凡 SCC 仍是隔离的 TMDB 移植包。 -- 启动性能探针会在隔离生命周期中真实创建并释放 TaskRegistry;normal/safe 组件数分别为 `23`/`11`,CI 只读检查使用稳定的宿主模块集合和生命周期组件顺序,不再把 Python/平台模块数量当作硬合同。 -- 官方插件快照覆盖 `plugins.v3`、`plugins.v2` 以及 V3 实际会从 `package.json` 回退加载的 31 个默认实现;`app/plugins/**` 仍只是宿主运行副本,不进入扫描。 -- SDK 快照以各模块显式 `__all__` 为公开合同,能够记录赋值别名;`typing`、`__future__` 等实现期导入不再被误冻结,既有数据库备份门面已补精确导出清单。 -- async 阻塞实际债务已由 fixture 中的 10 项归零;Scheduler Agent task 收尾查询已复用 - `AgentTaskOper.async_get` 的统一 AsyncSession 边界,CI 继续以零债务基线拒绝回退。 - -本阶段只修复治理信号和事实源,不把基线刷新当作业务重构完成。后台任务所有权、typed runtime、durable 副作用和质量规模化仍按下列 P1/P2 顺序推进;Module Contract V2 的宿主观察面已在阶段 2 收口。 - -### 长期整改阶段 1a:TaskRegistry owner 与关停契约(2026-08-23) - -- 手工订阅搜索曾因 `create_sync()` 缺少必填 `owner` 在真实命令路径抛出 `TypeError`;现以 - `api.subscription.search_schedule` 登记,并由命令级测试冻结既有 scheduler 参数和立即返回语义。 -- 新增符号感知的 `scripts/architecture/task_ownership.py`:宿主中所有可证明为 TaskRegistry 的 - `create`、`create_sync`、`register` 调用必须显式传入非空字符串字面量 owner,当前债务为零;CI - 只读执行该门禁。插件、SDK、`runtime/compat` 和测试运行时目录明确排除,不扩大插件 ABI 约束。 -- TaskRegistry 关停超过预算时不再取消不可中断的同步线程包装任务或清空其记录;尚未真正结束的任务 - 保留 owner 并通过事件循环异常处理器报告。可取消协程在整个关停周期只收到一次取消请求,重复或并发 - 关停不会再次打断其异步清理,超时诊断也按任务去重。 -- 本子阶段仍只覆盖 TaskRegistry。Transfer worker/replay、Agent blocking executor、Event handler - drain、通道线程和 E2/E3 durable 完成点继续作为阶段 1 后续切片,不能因 owner 门禁通过而宣称完成。 - -### 长期整改阶段 1b1:整理后台生命周期所有权(2026-08-23) - -- `TransferChain` 为每一代整理 worker 使用独立停止信号;配置热更新只让旧代完成已经进入同步 I/O 的 - 工作,不再让旧线程重新领取新任务。超时旧线程继续由 `_retiring_threads` 持有,重复关闭可以继续等待, - 不把无法强制取消的文件操作伪装成已结束。生命周期锁等待与 worker/replay join 共用同一个 deadline, - 停止哨兵也不再被误算成真实队列任务而阻止最后一批进度结算。 -- pending 回放改为单一受管线程,启动重复调用不会并发扫描;关停信号会在查询、`stat` 和逐条回放边界 - 重新检查,尚未处理的 `TransferPending` 登记保持不变。关闭与 `queue.get()` 竞争时,尚未开始的任务会 - 原样放回队列并保持 `task_done()` 计数平衡。 -- 失败通知聚合器和 AI 重试 scheduler 现在拥有 timer、buffer 与 flush task 的显式 `close()`;分组使用 - generation 阻止旧 timer 在新静默窗口尚未 armed 时提前消费新批次。跨线程提交 AI 重试产生的 Future - 也会观察最终异常,不再只用提交调用外层的 `try/except` 假设异步执行成功。 -- 生命周期新增常驻“整理后台服务” owner:正常模式和安全模式都只关闭已存在的单例,不会在 shutdown - 反向创建 worker。插件文件监控、Scheduler、Agent 和整理 owner 先停止宿主生产任务;随后封口插件变更、 - 停用插件事件入口并结算全部在途 handler,最后才调用插件私有 timer、scheduler、watcher 的旧停机 hook。 - 任一阶段返回 `False` 或超时,`FAIL_FAST` 屏障都会停止释放仍可能被活线程使用的后续依赖。 -- EventManager 同时持有线程池同步 handler Future 和事件循环异步 handler completion;“事件投递屏障”会 - 等待队列、handler 及 handler 派生事件自然收敛,再原子封住新的广播提交。为兼容无法声明资源依赖的 - 旧插件,宿主先停用其新 handler 投递,再用非封口屏障等待已经开始的 handler 退出,之后按原顺序调用 - `close`/`stop_service`;hook 产生的尾事件仍可由其他宿主 handler 消费,最终屏障后才卸载插件实例。 - 事件消费、共享线程池和模块资源仍在插件之后关闭。 -- lifespan 启动中途失败会记录已完成及当前部分启动的组件,递归纳入已激活依赖对应的 stop-only owner, - 并复用正常停机的顺序、超时和 `FAIL_FAST` 策略。后段初始化失败不再只停止 TaskRegistry 后遗留 Scheduler、 - Transfer、Event、插件或 HTTP 资源。 -- 兼容边界没有变化:`do_transfer`、队列与手工整理公开签名、模块方法 kwargs、插件整理事件类型及 payload、 - 同步插件 ABI 和动态 API 原生返回结构均未改名或包裹;本阶段也没有 schema/Alembic 变更。 -- 本阶段只证明进程内 owner、取消、等待和依赖释放顺序。失败通知仍可能在业务提交后、消息接受前随进程 - 崩溃而丢失;五分钟 AI 重试缓冲仍未持久化;`TransferPending` 仍只有 `storage + src_path`,不能表达文件 - 副作用 checkpoint、lease 和未知完成状态。它们分别留给阶段 1b2、1b3、1b4,不能据此宣称 E2/E3 完成。 - -### 长期整改阶段 2:Module Contract V2 宿主收口(2026-08-24) - -- 212 个显式 spec 当前分布为 `first_non_empty=94`、`ordered_list_merge=108`、 - `ordered_mapping_merge=1`、`pipeline_relay=2`、`fan_out=7`,已观察宿主方法的 `legacy` aggregation 为 `0`。 -- 媒体发现、豆瓣、Bangumi、AniList、TMDB、音乐、媒体服务器、存储、消息、识别和下载器能力均按真实 - 同步/异步入口复用契约;15 个无 required parameters 的方法均为真实无参调用,不再是 family 默认值遗漏。 -- `plugin_short_circuit` 已接入同步与异步 dispatcher;缓存清理、命令注册、调度、下载新增和整理完成等 - 副作用方法使用 `fan_out`,忽略 provider 返回值并执行全部插件和宿主实现,异常仍按 provider 隔离。 -- `torrent_files` 不再把 qBittorrent SDK 集合、Transmission 对象和 rTorrent 字典泄漏给 Chain;三个宿主 - provider 在共同适配边界投影为 `DownloaderFile`,修复 rTorrent 字典与调用方 `.name` 假设并存的双实现。 -- 兼容边界不变:未知第三方自定义方法继续走开放 legacy fallback;旧插件签名和结果不匹配仍只诊断、 - 不拒绝加载。已登记方法的名称、kwargs、插件优先级、同步/异步入口和异常隔离 ABI 均保留。 - -### 长期整改阶段 3:订阅删除生产路径统一(2026-08-24) - -- `/subscribes` 交互删除与 `/subscribe_delete` 远程命令不再直接调用 `SubscribeOper.delete` 后启动 - `sub_done_async` 裸线程;两个同步入口统一委托 `app.application.subscription.delete` 的权限、事务、 - 事件、统计与 outbox 协议,和 API、Agent 的异步删除路径共享候选快照、授权规则及 durable intent。 -- 启动组合根为同步消息入口提供独占 Session、UoW 和 outbox;订阅行与 `subscribe.deleted`、 - `subscribe.deleted.report` 在同一事务提交,暂存或提交失败都会回滚,成功后仍按事件、统计顺序收口。 -- 消息交互层只保留 ID 解析、名称展示和结果提示,不再拥有写库或统计实现;事务内目标已消失时按既有 - “未找到”语义返回,避免读取与删除竞态被误报为成功。 -- 插件兼容边界保持不变:`MoviePilotServerHelper.sub_reg_async` 与 `sub_done_async` 的类方法、签名和返回值 - 继续保留给旧插件;仅宿主生产调用清零,因此没有改动插件仓、SDK/Compat 映射或事件 payload。 - -### 长期整改阶段 4:消息渠道回环与线程所有权统一(2026-08-24) - -- Slack、Telegram、Discord、飞书、QQBot、企业微信与 WeChatClawBot 原先分别拼接本地消息 API URL、 - 执行 HTTP 并判断响应;其中飞书、QQBot、企业微信还为每条入站消息创建不可追踪的 daemon 线程。现在七者统一复用 - `app.application.messaging.ingress` 的 URL 编码、请求、状态确认、异常日志和响应释放语义。 -- SDK 回调需要同步确认的 Slack/Telegram/WeChatClawBot 保留同步调用,Discord 保留自有事件循环内的 - 异步调用;飞书、QQBot、企业微信继续立即返回,但任务改由现有 `ThreadHelper` 共享执行器承载,应用 - 关闭时由既有线程池生命周期等待,不再产生逐消息游离线程。HTTP 到达后仍由 `/api/v1/message` 的 - TaskRegistry owner 执行消息主链。 -- 新门禁扫描全部宿主 `app/modules/**/*.py`,禁止渠道再次硬编码 `/api/v1/message`;新增渠道必须使用统一 - ingress。source、token 统一通过 query encoder 处理,修复配置名含 `&` 等字符时被拆成额外参数的问题。 -- 兼容边界不变:七个渠道类、模块方法、配置字段、消息 payload、同步/异步 SDK 回调方式和 - `/api/v1/message` HTTP 合同均未改;没有修改插件仓或向 SDK/Compat 新增宿主内部入口。 - -### 长期整改阶段 9:工作流运行时获取路径统一(2026-08-24) - -- API 列表端点、请求级工作流写用例装配和 `WorkflowChain` 原先分别直接构造 - `app.workflow.WorkFlowManager` Singleton;现在统一通过 `app.application.workflow` 的 typed runtime - provider 获取启动组合根登记的同一实例,不再由消费者自行定位 concrete 管理器。 -- 架构门禁禁止 `app.workflow/**` 实现包和 `app.startup/initializers/workflow.py` 以外的宿主模块直接依赖 - `app.workflow`。依赖图总边数保持 `6544`:移除 3 条 API/Chain concrete 边,同时新增 Chain 的 - Application 端口边和 startup 的两条装配边;重点禁止边与自有 SCC 均未增长。 -- 兼容边界不变:`app.workflow.WorkFlowManager` 的类路径、Singleton identity、公开方法、事件监听和 - action 加载保持原样,旧插件仍可直接使用 concrete 类;本阶段只收口 canonical 宿主消费者。 - -### 长期整改阶段 10:运行时 Facade 获取方式统一(2026-08-24) - -- API、CLI、Scheduler 和工作流动作原先同时存在显式 `get_*` 调用、Application 兼容类构造,以及 - `get_plugin_manager as PluginManager` 的类形别名。canonical 宿主消费者现统一显式调用 - `get_module_manager()`、`get_plugin_manager()` 和 `get_scheduler()`,不再把 Service Locator 伪装成 - concrete 类构造。 -- 架构门禁扫描全部宿主 Python 源码,禁止重新导入 `app.application.module.ModuleManager`、 - `app.application.scheduling.Scheduler`,或为插件 getter 建立别名。startup 仍负责创建 concrete - 管理器并注册 provider,运行时实例身份、初始化顺序和依赖图边均不改变。 -- 兼容边界不变:Application 的 `ModuleManager`、`Scheduler` 类形 Facade 和 concrete 插件管理器类路径 - 继续保留,旧插件、V1/V2/V3 索引加载及 SDK/Compat 映射无需迁移;本阶段仅统一宿主生产路径。 - -### 长期整改阶段 11:系统配置端口命名统一(2026-08-24) - -- Agent、Chain、Module 和 Workflow 的 17 个 canonical 文件原先把 - `get_configured_system_config()` 别名或赋值为 `SystemConfigOper`,使 Application 配置端口在调用处 - 看起来仍像数据库 Oper。宿主生产路径现统一显式调用 getter,测试也改为替换真实组合根接缝。 -- 架构门禁禁止为 `get_configured_system_config` 建立别名,也禁止把它赋给本地 - `SystemConfigOper`;真正的 `app.db.oper.systemconfig.SystemConfigOper` 只留在 DB 实现、startup 装配和 - testing bootstrap,不再形成第二种 canonical 获取方式。 -- 兼容边界不变:DB Oper 类、`app.db.oper` 懒导出、SDK/Compat 旧路径和 V1/V2/V3 插件加载均未改动; - 已注入 `SystemConfigReader/SystemConfigService` 的 Agent 工具构造合同保持原样。 - -### 长期整改阶段 12:工作流域 Chain 数据端口收口(2026-08-24) - -- `app.application.chain.data` 已提供命名 `get_chain_*_port()`,但 `WorkFlowManager`、`WorkflowChain`、 - 添加订阅和整理文件动作仍把 `*PortProxy` 别名为数据库 Oper,形成 getter 与代理双轨。四个工作流消费者 - 现统一使用显式 getter,测试替换同一个组合根接缝。 -- 架构门禁禁止 `app/workflow/**` 和 `app/chain/workflow.py` 重新导入任何 `*PortProxy`。其它 Chain 域仍按 - 独立风险切片迁移,不能因本阶段通过而宣称全部 Chain 数据代理已清零。 -- 兼容边界不变:全部 `*PortProxy` 类、`WorkFlowManager` 类路径/Singleton identity、动作类型与参数、 - 工作流事件和数据库 `WorkflowOper/SubscribeOper/TransferHistoryOper` 均保留,插件无需迁移。 - -### 长期整改阶段 13:用户与消息 Chain 数据端口收口(2026-08-24) - -- `UserChain`、`InteractionChain` 和消息发送 mixin 原先都把 `UserPortProxy` 别名为 `UserOper`;现在统一 - 通过 `get_chain_user_port()` 获取组合根登记的数据端口,登录、用户绑定查询和通知收件人解析不再存在 - 第二种伪 Oper 获取路径。 -- 架构门禁覆盖这三个用户身份消费者,禁止重新导入 `UserPortProxy`。站点、媒体服务器、音乐、订阅、 - 下载和整理 Chain 仍保留独立迁移清单,继续按行为风险分阶段推进。 -- 兼容边界不变:数据库 `UserOper`、`UserPortProxy`、User/Interaction/Message Chain 公开方法、消息渠道 - payload 和插件调用方式均未改动。 - -### 长期整改阶段 14:音乐订阅 Chain 数据端口收口(2026-08-24) - -- 音乐订阅辅助链原先通过 `SubscribePortProxy as SubscribeOper` 更新音乐元数据、质量和当前订阅;现在 - 统一调用 `get_chain_subscribe_port()`,测试改为替换同一个命名 getter。 -- 架构门禁单独覆盖 `_music.py`,禁止重新引入 `SubscribePortProxy`。主订阅 Chain 仍有更多数据端口和 - 事务/副作用耦合,继续作为独立高风险阶段处理。 -- 兼容边界不变:音乐订阅公开方法、数据库 `SubscribeOper`、`SubscribePortProxy`、字段写入与插件调用 - 合同均未修改。 - -### 长期整改阶段 15:站点 Chain 数据端口收口(2026-08-24) - -- `SiteChain` 与 `TorrentsChain` 原先把 `SitePortProxy` 别名为 `SiteOper`,覆盖 CookieCloud、用户数据、 - 站点增删改、认证后刷新和 RSS 地址更新。现在全部统一通过 `get_chain_site_port()` 获取同一组合根端口。 -- `/sites` 交互测试不再修改 Proxy 类属性,而是替换 getter 返回的 repository;架构门禁同时覆盖两个 - 消费者,禁止站点域重新引入 `SitePortProxy`。 -- 兼容边界不变:数据库 `SiteOper`、`SitePortProxy`、`SiteChain/TorrentsChain` 公开方法、站点事件、 - CookieCloud/RSS 语义和插件调用方式均未改动。 - -### 长期整改阶段 16:媒体服务器 Chain 数据端口收口(2026-08-24) - -- `MediaServerChain` 原先通过 `MediaServerPortProxy as MediaServerOper` 清理已移除服务器、写入媒体项并 - 清理陈旧记录;现在统一调用 `get_chain_media_server_port()`,同步阶段继续共享同一个端口实例。 -- 五个增量同步测试接缝改为替换命名 getter,架构门禁禁止媒体服务器 Chain 重新导入 - `MediaServerPortProxy`。 -- 兼容边界不变:数据库 `MediaServerOper`、Proxy 类、媒体库同步公开方法、进度合同、停止语义和插件调用 - 方式均未修改。 - -### 长期整改阶段 17:下载 Chain 数据端口收口(2026-08-24) - -- `DownloadChain` 原先把下载失败、下载历史和媒体服务器三个 `*PortProxy` 别名为数据库 Oper,用于失败 - 抑制、重复下载查询、下载结果结算和媒体库判重;现在全部统一调用对应 `get_chain_*_port()`。 -- 五个下载/音乐测试接缝改为替换命名 getter,架构门禁禁止下载 Chain 重新导入这三个 Proxy。 -- 兼容边界不变:三个 DB Oper、Proxy 类、下载公开入口、失败指纹、历史 hash 查询、媒体库判重和插件 - 调用合同均未修改。 - -### 长期整改阶段 18:主订阅 Chain 数据端口收口(2026-08-24) - -- `SubscribeChain` 原先把订阅、站点和下载历史三个 `*PortProxy` 别名为数据库 Oper,查询、搜索、匹配、 - 完成检查与订阅文件视图因此和命名数据端口形成双轨;现在统一调用对应 `get_chain_*_port()`。 -- 订阅、音乐与交互测试改为替换命名 getter,架构门禁禁止主订阅 Chain 重新导入这三个 Proxy。 -- 兼容边界不变:三个 DB Oper、Proxy 类、订阅 Chain 公开方法、订阅字段和事件合同、V2/V3 插件调用 - 方式均未修改。 - -### 长期整改阶段 19:整理 Chain 数据端口收口(2026-08-24) - -- `TransferChain` 与 `_transfer` mixin 原先把 pending、下载历史和整理历史三个 `*PortProxy` 别名为 - 数据库 Oper;现在 worker 回放、查重、历史解析、手动重整和失败重试统一调用对应 `get_chain_*_port()`。 -- 整理主流程、同步附属文件、音乐整理和手动历史测试改为替换命名 getter;架构门禁同时覆盖主链和 - mixin,禁止重新引入三个 Proxy。 -- 兼容边界不变:三个 DB Oper、Proxy 类、整理公开入口、队列和生命周期、历史字段、事件 payload 以及 - V2/V3 插件调用方式均未修改。 - -### 长期整改阶段 20:Agent 数据端口收口(2026-08-24) - -- Agent 编排、会话记忆和 22 个工具实现原先把十种 `AgentDataPort` 代理重新别名为数据库 Oper,形成与 - Chain 命名 getter 不同的第二套数据端口用法;现在统一调用 `get_agent_*_port()`。 -- Agent 测试接缝改为替换 getter 返回的端口实例,架构门禁遍历 `app/agent/**`,禁止生产模块重新导入 - 十个兼容 Port 代理。 -- 兼容边界不变:`AgentDataPorts` 注册表、十个旧 Port 代理类、DB Oper、Agent/工具公开参数和返回值、 - 权限判断以及 V2/V3 插件调用方式均未修改。 - -### 长期整改阶段 21:监控历史端口收口(2026-08-24) - -- `TransferDispatcher` 原先把应用层兼容 `TransferHistoryPort` 再别名为 `TransferHistoryOper`;现在通过 - `get_transfer_history_port()` 直接取得组合根登记的整理历史端口。 -- 监控历史与文件事件测试改为替换命名 getter,架构门禁禁止监控分发器重新导入兼容 Facade。 -- 兼容边界不变:`TransferHistoryPort`、DB Oper、监控候选判定、历史查重、失败重试和整理触发语义均 - 未修改。 - -### 长期整改阶段 22:服务配置应用边界统一(2026-08-24) - -- 启动组合根已注入应用层服务目录,但 Chain、消息 API、Scheduler 和 Agent 仍直接读取 runtime - `ServiceConfigHelper`,形成两个应用入口;现在统一通过通知与媒体服务器应用模块的命名函数读取。 -- 命名函数显式区分仅启用配置和包含禁用配置,保持定向同步、定时注册、企业微信模式和渠道管理员判断 - 的原语义;架构门禁覆盖七个生产消费者。 -- 依赖基线经语义诊断后从 `6544` 条边降为 `6539`:七个消费者移除 runtime service-config 边并改为 - Application 边,12 组禁止边和唯一隔离 TMDB SCC 均未变化。 -- `ServiceConfigHelper` 继续保留在 startup、runtime module adapter、模块实例初始化和 `app.sdk.services` - 插件兼容出口;V2/V3 插件导入、配置 Schema 和热更新读取器均未修改。 - -### 长期整改阶段 23:媒体服务器 API 配置路径收口(2026-08-24) - -- 在线播放端点是阶段 22 后唯一仍以 `MediaServerHelper().get_configs()` 读取配置的 canonical API;现统一 - 调用 `get_mediaserver_configs()`,配置过滤、遍历顺序和播放地址响应保持不变。 -- 架构门禁覆盖该遗留点,拒绝端点重新导入类形 Helper;测试也替换命名 Application 接缝,不再伪造 - Helper 实例。 -- `MediaServerHelper` 继续服务于运行实例发现和插件 SDK 兼容,类路径、方法、配置 Schema、路由及响应 - 均未修改,V2/V3 插件无需迁移。 - -### 长期整改阶段 24:Scheduler 内部协程所有权收口(2026-08-24) - -- `_submit_to_loop()` 的三个生产调用点都已携带 job owner,但私有签名仍允许省略 `job_id`,并为当前循环 - 与跨线程主循环各保留一条不登记句柄的 fire-and-forget 分支;现将 owner 收紧为必填并删除双轨。 -- 进度更新、同步作业收尾和协程作业继续按 generation 登记同一 Scheduler 句柄表;停止接收后拒绝提交, - shutdown 取消并等待真实终态,跨线程代理不被误认作完成。 -- `Scheduler.start()`、同步 `stop()`、作业定义、插件调度方法、进度 payload 和 SDK/Compat 均未修改, - V2/V3 插件行为保持兼容。 - -### 长期整改阶段 25:跨线程 TaskRegistry owner 收口(2026-08-24) - -- TaskRegistry 原先只有事件循环内 `create()`,同步 Chain 无法原子登记异步后台动作;新增 - `submit_threadsafe()`,在目标循环内完成 accepting 检查、任务创建和 owner 登记,再镜像真实终态。 -- shutdown 与跨线程提交竞态时,先登记的任务进入既有取消/等待;后到任务被拒绝并关闭协程,立即投递 - 失败向调用方抛出。owner 静态门禁同步覆盖新入口。 -- 整理失败按钮的 AI 接管从裸 `run_coroutine_threadsafe` 迁为 `chain.transfer.ai_takeover`,消息文案、Agent - prompt/参数、回调返回和 V2/V3 插件 ABI 保持不变;该动作仍为 E0,不宣称跨进程恢复。 - -### 长期整改阶段 26:Agent 会话清理提交路径统一(2026-08-24) - -- 过期会话回收与远程清理命令原先各自构造 `clear_session()` 协程并裸提交主循环,形成同一目标的两套 - 实现;远程命令现在复用 `_schedule_agent_session_clear()` 唯一入口。 -- 唯一入口通过 TaskRegistry 以 `chain.message.agent_session_clear` owner 跨线程提交,目标循环先登记再执行, - shutdown 可取消并等待;调度失败仍关闭协程并记录告警,不泄漏 Agent 资源清理对象。 -- `/clear_session` 命令、会话映射、成功/空会话提示、Agent manager 合同及 V2/V3 插件 ABI 均未修改;实时 - Agent 消息和停止命令仍保留各自的 Future 结果观察,不被机械改成 fire-and-forget。 -- 依赖基线仅新增 `app.chain.message -> app.runtime.tasks`,模块数保持 `806`,内部边为 `6541`;12 组禁止 - 边与唯一隔离 TMDB SCC 均未变化。 - -### 长期整改阶段 27:历史 AI 进度任务所有权统一(2026-08-24) - -- 整理历史 AI 重做的外层 runner 已进入 TaskRegistry,但单条与批量输出回调仍各复制一套裸 - `run_coroutine_threadsafe`,进度缓存写入绕过了同一 shutdown owner 边界。 -- 两条路径现在复用唯一进度回调工厂和外层同一个 registry,分别登记 - `api.history.ai_redo.progress` 与 `api.history.ai_redo_batch.progress`;停止接收、取消和有限等待语义统一。 -- API 路由、权限、进度 key/payload、Agent prompt 与完成/失败文案均未修改;该进度仍按 E0 允许进程崩溃 - 丢失,不把 UI 进度误报为 durable 完成。既有 `app.api.endpoints.history -> app.runtime.tasks` 依赖边不变, - V2/V3 插件 SDK/Compat 无改动。 - -### 长期整改阶段 28:插件兼容统计线程托管(2026-08-24) - -- canonical 订阅新增/删除/完成统计已由事务 Outbox 驱动,但 `MoviePilotServerHelper.sub_reg_async()` 与 - `sub_done_async()` 仍各自创建裸线程;V3 官方插件仍通过旧 `app.helper.server` 映射调用后者,不能删除 ABI。 -- 两个旧同步入口保留类、方法、参数与立即布尔返回,内部改经 TaskRegistry 跨线程提交 `asyncio.to_thread`, - 分别登记 `compat.server.subscribe_added_report` / `compat.server.subscribe_done_report`;同步网络工作开始后 - 不在 shutdown 时取消,宿主会等待真实完成,生命周期不可用时拒绝并关闭未执行协程。 -- 插件仓没有改动,V1/V2/V3 旧导入映射保持不变;主链也不回退兼容入口。依赖基线仅新增 - `app.adapters.external.server -> app.runtime.config/tasks` 两条允许边,模块保持 `806`、内部边为 `6543`, - 12 组禁止边与唯一隔离 TMDB SCC 均未变化。 - -### 长期整改阶段 29:Emby 系条目转换协议统一(2026-08-24) - -- Emby、Jellyfin 与极影视原先各自维护一套用户播放状态、Provider ID、音乐备注和媒体服务器条目投影; - Jellyfin/极影视的同构函数还占用了重复代码门禁中最后一组“待后续 Phase 清理”白名单。 -- 转换规则现统一由 `app.application.mediaserver.format_emby_family_item()` 所有,三个服务类只保留带各自 - `server` 标识的薄适配器;Emby 独有的 `ServerId` 投影继续显式开启,Jellyfin/极影视仍保持不投影该字段。 -- 三个私有静态入口的名称、参数与返回类型均保留,服务查询、统一媒体身份和音乐匹配行为不变;插件仓、 - SDK/Compat 映射和 V1/V2/V3 插件公开合同均未修改。重复代码白名单因此清零,后续新增同构实现会直接失败。 -- 依赖基线只新增 `app.application.mediaserver -> app.runtime.log` 及父包边,用于保持原有转换异常日志;模块 - 保持 `806`、内部边为 `6545`,12 组禁止边与唯一隔离 TMDB SCC 均未变化。 - -### 长期整改阶段 30:插件市场请求级子任务收口(2026-08-24) - -- 插件目录异步聚合原先用 `asyncio.create_task()` 并发读取多个市场和 V1/V2/V3 代际,正常路径会逐个 - 等待,但父请求取消或进度回调抛错时会直接退出,尚未完成的 loader 继续在事件循环中运行。 -- 这些任务只服务当前 API/Agent 请求,不进入 lifespan TaskRegistry;`async_collect()` 现在通过 - `try/finally` 持有完整任务集合,异常退出时取消未完成 loader,并用 `gather(return_exceptions=True)` - 观察所有终态。单 loader 失败隔离、完成顺序进度和稳定市场/代际合并顺序均保持不变。 -- 回归测试覆盖父任务取消和进度消费者失败,均证明阻塞 loader 收到取消后才允许父调用结束;插件管理器 - 方法、目录 DTO、市场参数、缓存、V1/V2/V3 索引与插件 SDK/Compat 均未修改。依赖图仍为 `806/6545`。 - -### 长期整改阶段 31:搜索 AI 推荐生命周期托管(2026-08-24) - -- 搜索 AI 推荐原先用类级 `_ai_recommend_task` 持有裸 `asyncio.create_task()`:新搜索和手工取消能停止 - 旧任务,但 lifespan shutdown 不知道该任务存在,可能在 Agent/LLM 调用尚未结束时越过关停预算。 -- 同步 `start_recommend_task()` 入口现在通过 TaskRegistry 创建 `chain.search.ai_recommend` owner,仍把同一个 - Task 保存到原类状态供轮询、强制重启和取消入口使用;任务结果、错误、缓存索引和旧请求丢弃逻辑不变。 -- 回归测试使用真实 Registry 阻塞推荐调用,证明 shutdown 会取消并等待任务终态,随后 running/task 状态 - 正常复位。API 参数与响应、SearchChain 方法签名、Agent 调用协议和插件 SDK/Compat 均未修改。 -- 依赖基线仅新增 `app.chain.search -> app.runtime.tasks`,模块保持 `806`、内部边为 `6546`,12 组禁止边 - 与唯一隔离 TMDB SCC 均未变化。 - -### 长期整改阶段 32:事件投递所有权单路径(2026-08-24) - -- `EventManager` 的正式组合早已向 `EventDispatcher` 注入同步和异步 handle sink,但调度器仍 - 允许不注入 sink,并直接调用线程池或 `run_coroutine_threadsafe()`;该回退无法进入事件总线 - 的 owner 句柄表,因而绕过 stop/drain 的取消、等待和超时诊断。 -- 调度器现在把两类 sink 收紧为必需的生命周期依赖,并删除自行提交的第二套实现;所有广播 - handler 都必须先被 `EventManager` 登记,停止状态下由同一个 sink 拒绝并关闭协程。 -- 事件类型、payload、优先级、同步/异步 handler 签名、插件监听注册和 SDK/Compat 映射均未修改; - `EventDispatcher` 仍是不对外公开的宿主内部算法类。 - -### 长期整改阶段 33:宿主 Agent 运行时获取单路径(2026-08-24) - -- Chain 早已通过 `app.application.agent` 获取 Agent 服务,但 WebAgent、OpenAI、Anthropic、整理 - 历史 API 和 Scheduler 仍直接调用 `app.agent.runtime_loader`,对同一运行实例形成两条 - 宿主服务定位路径。 -- 五个宿主消费模块现在统一经已有 running manager provider 获取实例;门面在 lifespan - 尚未装配时保持旧的 `None` 查询语义,API 仍返回 503,也不会提前物化 Agent、LLM 或工具树。 -- 架构 ratchet 精确禁止 `app.agent/**` 和 `app.startup/**` 之外的宿主模块直接导入两个 manager - getter。WebAgent/OpenAI 构造具体 Agent 的类型入口仍留在 Agent 展示实现边界;loader 原函数、 - 内部工具工厂、启动/热更新/关停语义、API 参数与插件 SDK/Compat 均未修改。 -- 依赖边集合按新门面路径刷新,模块数仍为 `806`、内部边仍为 `6546`;12 组禁止边 - 与唯一隔离 TMDB SCC 均未变化。 - -### 长期整改阶段 34:durable-required 事件事实源收口(2026-08-24) - -- 当前 Event Contract 将订阅新增/修改/删除、下载添加、整理成功/失败六个事件标为 - `durable_required`;它们的宿主正式生产者早已与业务写入同事务暂存 Outbox intent,但 topic - 字符串在订阅、Chain adapter 和 startup dispatcher 中分散重复,当前复核结论仍误写成六个事件待实现。 -- `DURABLE_EVENT_TOPICS` 现在是六个 EventType 与版本化 topic 的单一映射;订阅命令、下载/整理 - writer 和恢复 dispatcher 共用该映射,不再各自维护同义字符串。启动恢复器也会拒绝缺失任一 - durable topic handler 的配置。 -- 事件契约测试保证全部 `durable_required` 事件与 topic 键集合一致、topic 不重复,并冻结恢复 - handler 完整性。原 topic、payload、幂等键、at-least-once 语义、无 writer 的测试/嵌入式兼容分支 - 以及插件 SDK/Compat 均未修改;第三方插件自行写库或发事件仍不在宿主事务边界内。 -- 单一映射新增 `app.application.outbox -> app.schemas.types` 与 `app.chain.transfer -> app.application.outbox` - 语义边,模块仍为 `806`、内部边为 `6549`;12 组禁止边与唯一隔离 TMDB SCC 均未变化。 - -### 长期整改阶段 35:LLM provider 管理运行时收口(2026-08-24) - -- 启动组合根早已把唯一 `LLMProviderManager` 工厂注册到 `app.agent.llm.gateway`,LLM helper 也从该端口 - 获取同一运行时;但 `/llm/manage` 与 OAuth 回调仍在端点内再次惰性导入并实例化 concrete manager, - 同一目标形成 gateway 与 Singleton 两条解析路径。 -- 管理 API 现在与 helper 共用 `resolve_llm_provider_runtime()`;gateway 合同补齐统一管理与 OAuth 回调 - 能力,架构测试拒绝 startup/agent 之外的宿主代码直接导入 `LLMProviderManager`。原 API 路径、请求与响应 - 数据、OAuth HTML、manager Singleton identity 及 `app.agent.llm` 兼容导出均保持不变。 -- API 的运行时依赖由 concrete provider 边替换为 gateway 边,模块仍为 `806`、内部边仍为 `6549`; - 12 组禁止边与唯一隔离 TMDB SCC 均未变化。 - -### 长期整改阶段 36:WebAgent 音频能力边界收口(2026-08-24) - -- startup 已把唯一 `AgentCapabilityManager` 注册到 `app.application.agent`,Chain 和其他宿主消费者也已 - 使用该门面;WebAgent 音频转写却仍直接静态调用 concrete manager,形成两条能力访问路径。 -- WebAgent 现在调用 application 层的 `is_audio_input_available()` 与 `transcribe_audio()`;文件解析与读取、 - AnyIO 线程边界、provider 选择和返回语义均未变化。架构测试拒绝 startup/agent 之外的宿主代码重新 - 导入 `AgentCapabilityManager`,但保留 `app.agent.llm` 的公开惰性导出供 Agent 内部与既有消费者使用。 -- 宿主依赖图模块仍为 `806`,内部边由 `6549` 降至 `6547`;12 组禁止边与唯一隔离 TMDB SCC 均未变化。 - -### 长期整改阶段 37:插件输入事件发布边界收口(2026-08-24) - -- `PluginInputInteractionHandler` 原先位于 Application 层,却在超时、取消和正常输入三条分支重新构造 - `EventManager`;两个生产调用方所在的 MessageChain 已持有同一事件管理器,形成注入端口与全局定位双轨。 -- handler 现在必须接收显式事件发布端口,MessageChain 统一注入自身 `eventmanager`。事件类型、 - payload、`__mp_target_plugin_id` 定向字段、消息提示及消费返回值均未变化;架构门禁把 Application 到 - `app.runtime.events` 的直接依赖锁为零,插件公开事件合同和 V1/V2/V3 加载路径保持不变。 -- 宿主依赖图模块仍为 `806`,内部边由 `6547` 降至 `6545`;事件 producer 合同仍为 `78`,12 组禁止边 - 与唯一隔离 TMDB SCC 均未变化。 - -### 长期整改阶段 38:WebAgent 通知事件监听边界收口(2026-08-24) - -- WebAgent 原消息编辑队列已经属于 `app.application.messaging.agent`,但 NoticeMessage 的解析、通知队列 - 和进程级监听仍位于 HTTP endpoint,并由首个请求惰性注册,形成同一请求的两套队列归属和注册时机。 -- 通知解析、按用户路由及队列挂载/释放现与编辑队列统一归入 messaging Application;startup 组合根在 - 事件消费启动前登记唯一 NoticeMessage listener。API 只持有当前请求队列,不再构造 EventManager 或 - 注册进程监听;通知 schema、用户广播语义、SSE 事件顺序和传统命令等待窗口均未变化。 -- 架构门禁拒绝 HTTP endpoint 新增 `register`/`add_event_listener`,V1/V2/V3 插件事件类型和 payload - 保持不变;事件合同仍跟踪同一个 NoticeMessage consumer,只把 owner 从 API 更正为 startup。 -- 宿主依赖图模块仍为 `806`,内部边由 `6545` 调整为 `6546`:删除 API 到事件总线的隐式边,并由 - Application 显式持有消息 schema 与诊断日志依赖;12 组禁止边与唯一隔离 TMDB SCC 均未变化。 - -### 长期整改阶段 39:搜索 SSE 断线清理收口(2026-08-24) - -- 搜索链的站点并发任务已由各异步生成器在 `finally` 中取消并等待,但 API 批处理和传输包装器在 - 客户端断开时只退出循环,上游关闭依赖异步生成器回收时机;现在两层包装器都显式关闭可关闭的 - 上游迭代器,断线返回前即可触发站点任务清理。 -- 回归测试锁定客户端在首个事件到达时断开后,上游 `finally` 已在流函数返回前执行;心跳、批量 - append/replace、字幕签名、SSE payload 与缓存头均未修改,V1/V2/V3 插件合同不受影响。 - -### 长期整改阶段 40:异步防抖取消终态收口(2026-08-24) - -- `AsyncDebouncer` 在后续调用替换旧任务时只发出取消并覆盖句柄,公开 `cancel()` 也在任务真正退出前 - 清空引用;现在被替换的任务保留为 retired owner,`cancel()` 会取消并等待活动任务和 retired 任务 - 全部进入终态后再返回。 -- 回归测试覆盖旧任务在取消清理中阻塞、后续任务同时存在的场景。`debounce()` 装饰器、leading/trailing - 触发规则、同步 `Debouncer`、`app.utils.debounce` 兼容映射和 V1/V2/V3 插件导入路径均未修改。 - -### 长期整改阶段 41:优雅重启兜底线程所有权收口(2026-08-24) - -- API、命令链、升级和启动期资源更新都可能调用 `SystemHelper.restart()`;Docker 优雅退出路径原先每次 - 请求都会创建一个独立 daemon monitor,连续请求会在 180 秒后并行触发多次强制容器重启。现在 monitor - 由 `SystemHelper` 以锁和唯一句柄持有,存活期间的重复请求复用同一 owner,线程启动失败或执行结束都会 - 释放句柄,后续真实重启仍可创建新的兜底监控。 -- 回归测试用事件屏障验证首线程存活期间不会重复创建,并验证强制重启只调用一次、终态 owner 被释放; - 同时将触发说明从过期的 30 秒更正为实际的 180 秒。SIGTERM 优雅退出、Docker API 回退、意图标记、 - 本地 CLI 与一次性升级语义均未改变。 -- 该 daemon monitor 故意跨越正常 shutdown drain,以便进程卡死时仍能请求 Docker 重启,因此不纳入 - `TaskRegistry` 或普通线程池等待。`SystemHelper` 公开方法、SDK/Compat 映射和 V1/V2/V3 插件 ABI 均未修改。 - -### 长期整改阶段 42:Telegram typing 多实例与终态所有权收口(2026-08-24) - -- `TelegramModule` 支持多个通知配置实例,但用户到 chat 的映射、typing 线程、停止信号和锁原先都在 - `Telegram` 类级共享;两个配置遇到相同 chat ID 时会相互停止或覆盖 owner。生产 client 现在各自持有 - 完整运行状态,同一配置内则由 lifecycle 锁串行替换,保留一个 chat 一个 typing owner。 -- 停止等待超过预算时不再提前删除仍存活的线程句柄,新请求会拒绝覆盖阻塞中的旧 owner;线程启动失败会 - 回滚 owner 和停止信号,client 停止时先封住新增任务再取得完整快照。SDK 请求恢复后,线程仍由自己的 - `finally` 在真实终态释放登记。 -- `Telegram` 类路径、构造参数、`start_typing()`/`stop_typing()` 布尔合同、消息格式、模块方法及配置字段 - 均保持不变;私有类级可变状态已清除,正常 V1/V2/V3 模块实例不再共享运行状态。本阶段未修改插件仓、 - SDK 或 Compat 映射。 - -### 长期整改阶段 43:Discord typing 异步 owner 与 shutdown 收口(2026-08-24) - -- Discord typing 原先在等待旧 task 进入终态前就删除字典 owner;`trigger_typing()` 阻塞超过一秒时, - 新请求会覆盖仍运行的 task,模块停止也无法再取得它。现在同一实例通过异步 lifecycle 锁串行替换, - 超时后保留 owner 并拒绝并行启动,task 只在自己的 `finally` 或已确认终态后释放登记。 -- client shutdown 先封住新增 typing,再按统一预算通知全部 owner;未自然结束的 task 会被取消并再次等待, - 已完成 task 的异常也会被读取。Discord 长连接意外退出时,线程 runner 同样执行这条收尾路径,事件循环 - 不再直接关闭仍登记的 typing task。 -- `Discord` 类路径、构造参数、同步 `start_typing()`/`stop_typing()` 布尔合同、模块方法、消息格式和配置 - 字段均保持不变;V1/V2/V3 插件仍通过原模块能力调用。本阶段未修改插件仓、SDK 或 Compat 映射。 - -### 长期整改阶段 44:WebAgent 测试后台 owner 与 CI 红注解收口(2026-08-24) - -- GitHub 单测作业虽然成功,WebAgent SSE 用例却会在临时 `asyncio.run()` 关闭事件循环时取消仍登记的快照 - owner;Linux 下 `aiosqlite` worker 随后回送结果会触发 `PytestUnhandledThreadExceptionWarning`,Actions 将 - traceback 中两处 `Event loop is closed` 各自显示为错误级 annotation,形成“绿作业带红错误”。 -- WebAgent 流测试现在复用生产 `wait_web_agent_background_tasks()` 合同,在关闭临时循环前等待正常 owner; - 断线和慢快照用例仍先证明后台执行与 `done` 发送不受落库阻塞,再释放并等待真实终态,不把生产异步语义 - 改成同步等待。`wait_web_agent_background_tasks()` 的文档也改为反映其同时服务正常与取消收尾。 -- `pytest.ini` 将未处理线程异常提升为测试失败,后续不会再以绿色结果掩盖后台线程越过事件循环生命周期。 - 本阶段不改变 WebAgent API/SSE、Agent 模块、SDK/Compat 或 V1/V2/V3 插件合同,也未修改插件仓。 - -### 长期整改阶段 45:影视与字幕搜索逐页任务编排统一(2026-08-24) - -- 影视普通搜索、影视流式搜索、字幕普通搜索和字幕流式搜索原先各自维护一套站点 task 字典、并发信号量、 - 续页判断以及取消 `gather`;四套近似实现使断线和异常收尾修复容易只覆盖部分入口。 -- `SearchChain._iter_site_page_results()` 现在统一持有请求级站点 task,使用稳定 owner 名称,集中执行并发 - 限制、逐站点续页、系统停止检查和 `cancel + gather` 终态等待;四个既有入口只保留各自的结果累计、进度 - 文案和 SSE 包装。外层使用 `aclosing`,消费者提前关闭流时会立即触发统一收尾。 -- 回归测试覆盖迭代器提前关闭时取消并等待其他站点请求,以及字幕普通/流式入口的页序、续页条件和结果 - 一致性。公开搜索方法、SSE 字段、插件资源源、站点模块方法、SDK/Compat 与 V1/V2/V3 插件合同均未改变; - 本阶段未修改插件仓。 - -### 长期整改阶段 46:启动性能门禁托管 runner 波动收口(2026-08-24) - -- 阶段 45 首轮 Actions 的四个单测分片全部成功,Architecture Contract Gate 仅在冷导入耗时失败: - `app.factory` 为 `2031.389ms / 1924.455ms`,`app.startup.lifecycle` 为 - `1972.696ms / 1921.477ms`;宿主模块数、生命周期组件顺序、线程和 task 资源合同均未变化。同一失败 - job 原提交复跑直接成功,启动性能步骤由约 40 秒降至约 26 秒,证明是 Linux 托管 runner 的共同波动。 -- 冷导入仍以“维护者基线的 2 倍或增加 500ms,取较大者”为硬预算,只把最终跨平台/调度抖动带由 5% - 调整为 15%,足以覆盖首轮实测但不会掩盖模块数、组件集合、资源泄漏或超过约 2.3 倍基线的真实回退。 -- `--check` 现在逐目标打印实测中位数与预算;后续 CI 失败可直接判断越界幅度,不再只有结论而缺少通过 - 样本。脚本 CLI 的只读/显式写入边界和启动基线文件均未改变;本阶段不涉及运行时 API、SDK/Compat、 - 插件 ABI 或插件仓。 - -### 长期整改阶段 47:Agent 渠道流式刷新重入 owner 收口(2026-08-24) - -- `StreamingHandler.start_streaming()` 原先会直接覆盖已有 `_flush_task`;旧任务若仍在线程池消息发送或编辑 - 中,会继续读取已被新一轮改写的 channel/source/message 状态,且后续 `stop_streaming()` 只能等待新句柄。 -- start/stop 现在共享实例级异步生命周期锁。重复 start 会先关闭接收并等待旧 flush owner 真实结束,再 - 发布新一轮消息上下文和新 owner;并发 stop 的最终刷新、消息 finalize 与状态重置完成前,新 start 也不能 - 进入。定时刷新本身仍保持原有 0.3 秒节奏和自然完成语义,不在同步发送中途伪装成已取消。 -- 回归测试覆盖旧 owner 阻塞时重复 start 不覆盖上下文,以及 stop 最终刷新阻塞时新 start 必须等待。 - `StreamingHandler` 类路径、公开 `start_streaming()` / `stop_streaming()` 参数与返回值、渠道能力、消息格式、 - SDK/Compat 和 V1/V2/V3 插件合同均未改变;本阶段未修改插件仓。 - -### 长期整改阶段 48:CI 工件上传 action 版本统一(2026-08-24) - -- Pylint 报告仍使用 `actions/upload-artifact@v4`,在 GitHub 托管 runner 上持续产生 Node 20 弃用注解;覆盖率与 - 架构观察使用 v6,站点适配器使用 v7,同一工件上传职责存在三套主版本。 -- 四个工件上传入口现统一到官方 Node 24 的 `actions/upload-artifact@v7`;名称、路径、保留期、缺失文件策略和 - 默认归档上传语义均保持不变。架构 CI 静态合同枚举所有入口并锁定统一主版本,防止后续再次分叉。 -- 本阶段只修改主仓工作流与治理合同,不涉及运行时、构建产物格式、SDK/Compat、V1/V2/V3 插件 ABI 或插件仓。 - -### 长期整改阶段 49:插件包代际解析双轨统一(2026-08-24) - -- `PluginHelper.get_plugin_package_version()` 与异步版本原先分别手写当前代、向后兼容代和共享索引选择; - V3 临时兼容 V2 的顺序与 `v3:false` 排除规则因此有两处漂移风险,V1 空版本还会重复读取基础索引。 -- 同步和异步入口现在共用 `_package_version_candidates()` 与 `_select_compatible_package_version()`:候选顺序、 - 去重和兼容判定只有一份,网络 I/O 仍分别走原同步/异步适配器并在首个命中处停止。 -- 回归测试覆盖 V3、显式 V2、V1 基础索引去重,以及同步/异步访问顺序和结果一致。公开类路径、方法签名、 - 返回的 `v3`/`v2`/空字符串/`None` 语义、安装流程、SDK/Compat 和 V1/V2/V3 插件合同均未改变;未修改插件仓。 - -### 长期整改阶段 50:插件市场 GitHub 请求策略统一(2026-08-24) - -- 同步 `__request_with_fallback()` 与异步版本原先各自维护镜像、代理、直连三段策略;是否跳过 API 镜像、 - 代理参数和超时传递存在两处漂移风险,而 I/O 客户端的同步/异步差异并不要求复制策略事实。 -- `_build_github_request_strategies()` 现在唯一生成有序请求计划;同步 `RequestUtils` 与异步 - `AsyncRequestUtils` 仍独立执行并保持原错误隔离、取消和返回合同。回归测试强制两条入口遍历完全相同的 - URL/参数,并覆盖 GitHub API 跳过 raw 内容镜像。 -- 本阶段不改市场地址、缓存键、请求成功判定、安装接口、SDK/Compat、V1/V2/V3 插件 ABI 或插件仓。 - -### 长期整改阶段 51:插件市场索引请求与响应策略统一(2026-08-24) - -- 同步 `get_plugins()` 与异步版本原先各自解析仓库、拼接代际索引文件、生成请求头,并分别维护 - `404 -> {}`、其他失败 `-> None`、成功解析字典的三态响应;任何一侧调整都可能改变安装版本选择。 -- `_build_plugin_index_request()` 现在唯一持有仓库解析、`package[.vN].json` 命名、缓存戳和请求头策略, - `_resolve_plugin_index_response()` 唯一持有 HTTP 状态与 JSON 字典解析;同步与异步入口只保留各自 I/O。 -- 回归测试覆盖两条入口的 URL、请求头和结果一致,并锁定 404、非 200、有效字典、非法 JSON 和非字典 JSON - 的既有区别。缓存装饰器、公开签名、安装返回语义、SDK/Compat、V1/V2/V3 插件 ABI 均未改变;未修改插件仓。 - -### 长期整改阶段 52:插件 Release 分页策略统一(2026-08-24) - -- 同步 `_get_plugin_repo_releases()` 与异步版本原先各自维护仓库解析、十页上限、请求头、HTTP 状态、JSON - 类型校验、缓存快照规范化以及满 100 条才续页的规则;Release 安装能力因此有两份易漂移的事实源。 -- `_iter_plugin_release_page_requests()` 现在按需生成唯一分页计划,`_merge_plugin_release_page()` 唯一解释页面 - 响应并返回继续、自然结束或整次失败三态;同步与异步方法只保留各自 HTTP await 差异。 -- 回归测试覆盖两条入口的两页 URL、参数和 101 条规范化结果一致,并锁定空页、短页、满页、坏状态、坏 - payload 与 JSON 异常。仓库级共享缓存、强刷合并、TaskRegistry owner、取消传播、公开 Release 列表、 - SDK/Compat 和 V1/V2/V3 插件 ABI 均未改变;未修改插件仓。 - -### 长期整改阶段 53:远端插件安装模式决策统一(2026-08-24) - -- 同步 `install()` 与异步版本原先分别判定指定 Release、当前 Release 回退、文件列表安装、Release 版本 - 存在性和主系统版本限制;相同插件元数据可能因后续单侧修改而落入不同安装模式。 -- `_build_remote_plugin_install_plan()` 现在唯一产出不可变 `_RemotePluginInstallPlan`:指定历史 Release 不回退, - 指定当前 Release 继续校验系统版本,当前索引 Release 失败可回退,未声明 Release 使用文件列表。同步与异步 - 入口只负责各自索引/Release I/O 和内容准备实现。 -- 回归测试锁定四种计划与拒绝文案,既有同步/异步安装测试继续覆盖备份、清理、下载、文件列表回退和依赖 - 安装顺序。公开安装方法、返回值、缓存、取消、SDK/Compat、V1/V2/V3 插件 ABI 均未改变;未修改插件仓。 - -### 长期整改阶段 54:同步安装临时回滚备份清理收口(2026-08-24) - -- 同步和异步安装失败时都会恢复并删除临时回滚副本;异步成功路径也会在 `finally` 清理,但同步成功路径 - 原先保留 `TEMP_PATH/plugin_backup/`,直到同一插件下次安装才删除,形成生命周期分叉和无界陈旧副本。 -- 同步安装现在仅在内容准备、依赖安装和持久化备份刷新全部成功后删除临时回滚副本;失败路径仍先恢复, - 未知异常行为也未扩大。对照测试证明同步与异步成功都返回原 `(True, "")` 并删除各自临时副本。 -- 本阶段不改 Docker `CONFIG_PATH/plugins_backup` 持久化备份、失败恢复、强制安装、下载、依赖、取消、 - SDK/Compat 或 V1/V2/V3 插件 ABI;未修改插件仓。 - -### 长期整改阶段 55:官方插件观察基线跟进(2026-08-24) - -- GitHub 定时观察任务检测到官方插件仓从 `7d2d676d` 前进到 `8af03a0f`;V3 索引新增 - CourseOrganizer 和 LunaTVSource,因主仓冻结契约尚未审查跟进而按设计失败。 -- 语义报告为 46 项新增、23 个聚合计数增长、0 删除;复核的宿主导入、Hook 和 Bearer API 路由 - 均已存在,两个插件均明确限制 `system_version >= 3.0.0`,不扩大 V1/V2 承诺。 -- `official-plugin-baseline.json` 只更新主仓对外部契约的已审查快照,不同步或改写插件源码。 - 后续定时任务仍会对新增、删除或路由合同变化继续失败,不把外部仓漂移静默吸收。 - -### 长期整改阶段 56:观察报告保留策略收口(2026-08-24) - -- 官方插件观察已恢复成功,但工件上传仍请求保留 14 天,超过当前 GitHub 仓库允许的 3 天上限, - 因此每次成功运行都会留下无法执行的黄色注解。 -- `architecture-observe.yml` 现在显式使用 3 天保留期;报告内容、定时频率、失败条件、宿主及插件 - 契约都不变,仅清除已知的 CI 策略噪音。 - -### 长期整改阶段 57:进程级运行时 Facade 门禁统一(2026-08-24) - -- Scheduler、PluginManager、Command 和 WorkFlowManager 已经分别存在四套近似的依赖断言,但已经建立 - `app.application.module` Facade 的 ModuleManager 未被同等全宿主 ratchet 保护,且新运行时边界仍可能 - 复制第五套测试实现。 -- `scripts/architecture/service_locator.py` 现在以一份策略表统一扫描五类 concrete 运行时依赖, - ModuleManager 只允许 startup 组合根与既有 `app.sdk.plugins` 兼容导出直连;Scheduler、PluginManager、 - Command 和 Workflow 保留原批准边界。 -- Architecture CI 直接执行新门禁,合成回归同时证明五类越界均会失败,而 `app.sdk.plugins`、 - startup 组合根和 `app.workflow` 实现包仍可通过。公开 getter、Singleton identity、SDK/Compat 及 - V1/V2/V3 插件 ABI 均未修改;未修改插件仓。 -- 衍合远端 `feff545b` 后,新增的 `app.application.agenttask` 使三个启动入口的宿主模块数均精确增加 1; - 已用仓库生成器刷新启动性能基线并保留原有严格模块计数和耗时预算,未通过放宽门禁掩盖该变更。 - -### 长期整改阶段 58:AgentTask 关闭回归确定性等待(2026-08-24) - -- 远端 `feff545b` 新增的同会话关闭回归以 50 次 `asyncio.sleep(0)` 等待两个异步数据库 worker - 完成任务认领;GitHub Linux runner 上零时长让步不能保证工作线程获得 CPU,导致第二个任务仍未进入 - `running` 便触发失败,而四个分片中的其余 5917 项和全部架构门禁均已通过。 -- 回归现在以 2 秒硬上限和 10ms 间隔等待真实持久化状态,既保留失败超时,也不把线程调度速度误当成 - 生产语义。`execute_scheduled_task`、`AgentTaskExecutionService`、`AgentManager.close()` 与插件 ABI - 均未修改。 - -### 长期整改阶段 59:Feishu 多实例 SDK 循环路由统一(2026-08-24) - -- `FeishuModule` 支持多个配置实例,但 lark SDK 以模块级 `loop` 与 `_select` 驱动客户端;原实现由每个 - 实例线程临时覆盖并在退出时恢复这两个全局,两个实例并发启动或停止时会互相窃取循环或恢复旧全局。 -- 宿主现在只安装一份线程本地 loop 代理和停止选择器:绑定线程内继续使用实例独立事件循环,未绑定的 - SDK 调用仍委托原始 loop/select;每个实例原有 `_ws_tasks`、静默断连和有限 join 语义不变。 -- 并发回归用屏障强制两个模拟 SDK Client 同时读取模块级入口,证明二者运行在不同循环且均可独立停止。 - Feishu 配置、消息/卡片 API、模块类身份、插件 SDK/Compat 与其他 V1/V2/V3 插件行为均未修改。 - -### 长期整改阶段 60:命令重建任务关停 owner 统一(2026-08-24) - -- 命令服务的初始化和热更新通过 `ThreadHelper` 共享线程池提交,真实任务 owner 始终是 - 模块服务关闭阶段的线程池;原生命周期清单又为命令服务登记了一个空 `stop_command()`, - 形成声明上的第二套 owner,却无法拒绝、取消或等待任何任务。 -- 当前删除空关闭入口及其 stop/timeout/order 声明;命令服务仍是正常模式的显式启动组件, - 命令重建 Future 仍由共享线程池在模块、事件和插件资源收口后统一等待。生命周期快照 - 不再把无行为的回调计为已关闭 owner。 -- `Command` 类身份、Application 命令门面、插件命令 Hook、热更新时序和 SDK/Compat 均未修改; - V1/V2/V3 插件仍通过原有注册链路生效,且未修改插件仓。 - -### 长期整改阶段 61:Capability Runtime 关闭收敛事实源统一(2026-08-24) - -- Capability Runtime 原本会在 stop 异常时保留 `pending_stop`,但同步 `shutdown()` 把结果丢弃; - `HostModuleAdapter` 同时忽略模块显式返回的 `False`,startup 内部 `run_step()` 又把异常视为成功。 - 这三层会让未释放资源仅留下日志,对外却报告整体关闭完成。 -- 当前以 Capability Runtime 的同步/异步 stop 作为单一事实源:Host Module `stop() is False` 会进入失败 - 路径并保留原 owner,Runtime/ModuleManager/startup 逐层返回未收敛;Agent 与 Managed Resource 关闭入口 - 也直接传播 Runtime 的整体布尔结果,不再用单个 service 快照或无返回包装器形成并行判断。其余模块、 - 数据库和临时资源仍继续尽力关闭,但失败不再被伪装成成功;后续重试成功后同一 owner 才会进入终态。 -- Telegram polling 是首个接入该合同的长连接:无界 `join()` 改为 10 秒预算,超时时保留 SDK 与线程句柄并 - 返回 `False`,不会因清空句柄而丢失重试能力。Telegram 配置、菜单、消息、typing 语义与类 identity 未变。 -- 本阶段收口宿主 Capability Runtime 生命周期结果;未修改插件仓、SDK/Compat 映射或 V1/V2/V3 插件 Hook。 - -### 长期整改阶段 62:消息渠道长连接关闭收敛合同统一(2026-08-24) - -- 阶段 61 只让 Telegram polling 接入了 Host Module 的布尔收敛合同;QQBot、企业微信、 - WeChatClawBot、飞书和 Discord 的 Gateway/WebSocket/polling 线程仍在 join 超时后返回 `None`, - Slack 的 Socket Mode close 异常也只写日志。WeChatClawBot 还会无条件清空仍存活的轮询线程句柄,形成同一目标的两套实现。 -- 当前七个消息渠道模块统一复用 `_MessageChannelModuleBase._stop_service_instances()`:逐实例继续尽力停止, - 任一客户端异常或显式 `False` 都聚合为模块未收敛,再经 HostModuleAdapter 与 Capability Runtime 向 startup - 传播。各长连接客户端使用既有有界等待预算,只有线程真实终止才返回成功;超时 owner 和句柄继续保留供 - 后续 shutdown 重试。 -- 渠道名称、配置字段、优先级、消息解析、发送与命令 Hook、类 identity 均未改变;抽象 Module `stop()` - 只扩展为可选布尔结果,既有返回 `None` 的宿主模块和 V1/V2/V3 插件仍按成功兼容处理。未修改插件仓、 - SDK/Compat 映射或事件 payload。 - -### 长期整改阶段 63:应用消息队列线程关闭收敛合同补齐(2026-08-24) - -- `MessageQueueManager` 的监控线程仍使用无界 `join()`;渠道同步发送回调一旦阻塞,`stop_modules()` 所在 - 事件循环也会同步卡住,外层生命周期的 300 秒协程预算无法中断它。`stop_message()` 同时丢弃关闭结果, - 形成阶段 61 布尔收敛事实源之外的遗漏。 -- 当前停止入口使用 10 秒默认预算并返回真实线程终态;超时时保留原线程 owner,后续可在回调返回后重试。 - 监控循环收到停止请求后不再继续取新的排队消息,模板缓存仍会独立尽力关闭,任一资源失败统一向 - `stop_modules()` 返回 `False`,且不跳过其余模块资源收口。 -- `MessageQueueManager()`、无参数 `stop()`、`stop_message()` 与 `app.helper.message` 精确映射全部保留;新增 - 返回值对忽略结果的旧调用兼容,未修改消息内容、调度时段、发送回调、SDK/Compat 映射或插件仓, - V1/V2/V3 插件边界不变。 -- 阻塞回调、有限返回、保留 owner、重试终止、模板缓存继续清理和 startup 失败传播均有故障注入; - 消息/生命周期专项 58 项、架构与兼容专项 122 项、Pylint 10.00/10、strict mypy 39 文件、宿主与质量 - ratchet 以及四分片全量 `5941 passed, 3 skipped` 均通过。 - -### 长期整改阶段 64:共享线程池有界关闭 owner 统一(2026-08-24) - -- 阶段 4 的消息渠道回环和阶段 60 的命令重建都声明由 `ThreadHelper` 统一持有,但原实现没有登记 Future, - `shutdown()` 直接执行无界 `ThreadPoolExecutor.shutdown(wait=True)`。任一同步任务阻塞时会卡住 - `stop_modules()` 所在事件循环,外层生命周期预算无法取消,且无法向阶段 61 的布尔合同报告未收敛。 -- 当前共享 executor 在 Future 达到终态前保留 owner,先封口新提交,再使用 10 秒默认预算有限等待; - 超时返回 `False` 且不取消正在执行或排队的历史工作,任务完成后同一 owner 可重试到真实终态。 - 追踪同时覆盖宿主 `ThreadHelper.submit()` 和旧调用方直接使用的 `.pool.submit()`,避免形成第二套旁路。 -- `ThreadHelper()`、`submit()`、无参数 `shutdown()`、公开 `.pool` 及 `app.helper.thread` 精确映射均保留; - `.pool` 仍是 `ThreadPoolExecutor` 子类,上下文传播、任务返回和异常语义不变。未修改 SDK/Compat 清单、 - 插件 Hook 或插件仓,V1/V2/V3 插件边界保持兼容。 -- 宿主/旧兼容提交、阻塞任务、排队任务、阻塞完成回调、提交封口、重试终态和 startup 失败传播均有 - 故障注入;线程池/生命周期专项 78 项、架构与兼容调用链 164 项、Pylint 10.00/10、strict mypy - 40 文件、宿主与质量 ratchet 及四分片全量 `5946 passed, 3 skipped` 均通过。 - -### 长期整改阶段 65:异步文件日志单一有界写入与关闭 owner 统一(2026-08-24) - -- 协程环境的日志运行时原先同时保留批量队列 writer 和“队列满后提交独立 `ThreadPoolExecutor` 直写”两条路径; - 配置的队列容量并不是真实上限,溢出任务会进入 executor 的无界队列,写入顺序、资源 owner 和关闭 - 语义也分裂。当前异步文件日志只由一个有界队列 writer 执行;达到容量时拒绝新增文件副本,既有 - 控制台输出保持不变;无事件循环的同步调用仍保留直接写入语义,不再以第二套线程池掩盖 E1 过载。 -- 原 `shutdown()` 会无界等待批量线程、executor 和文件处理器;任一文件系统写入或 `close()` 阻塞都会 - 卡住 lifespan 所在事件循环。当前默认使用 10 秒共享 deadline,先封口新日志并排空已接受队列,再有限 - 等待 writer 和一次性文件处理器关闭 owner;超时返回 `False`,阻塞 owner 与 handler 均保留,释放后 - 可由同一实例重试到真实终态。 -- `LoggerManager` 不再先清空 writer 引用再关闭;未收敛时保留原 writer,重新装配也拒绝覆盖活动 owner。 - 日志仍在全部宿主组件之后最后关闭,但 `False` 会让 lifespan 以关闭失败结束,测试会话收尾也会报告 - 同一结果,不再把记录清理动作当成资源已经终止。 -- `NonBlockingFileHandler`、`LoggerManager`、无参数 `shutdown()`、历史 `ASYNC_FILE_WORKERS` 配置解析、 - `configure_log_writer()` 及 `app.log` 精确兼容映射均保留;新增 timeout 和布尔结果为向后兼容扩展, - 未修改 SDK/Compat 清单、插件 Hook 或插件仓,V1/V2/V3 插件观察面不变。 -- writer/handler 阻塞、队列过载、重试终态、重新装配拒绝覆盖和 lifespan 失败传播均有故障注入; - 日志/生命周期/旧导入专项 83 项、架构合同 94 项、兼容调用链 102 项、Pylint 10.00/10、strict mypy - 41 文件、全部宿主与质量 ratchet 及四分片全量 `5952 passed, 3 skipped` 均通过。 - -### 长期整改阶段 66:DoH 与共享线程池有界 executor owner 统一(2026-08-24) - -- DoH 仍单独创建标准库 `ThreadPoolExecutor`,关闭时先清空全局句柄,再执行无界 `shutdown(wait=True)`; - 任一底层 DNS/HTTPS 调用未返回都会阻塞 `stop_modules()` 所在事件循环,外层生命周期预算无法介入, - 且已丢失的 executor 无法重试收敛。阻塞探针确认旧关闭 50ms 内不返回,同时 owner 已从全局移除。 -- 阶段 64 的 Future 追踪和有界 worker join 已抽取为 - `app.runtime.execution.OwnedThreadPoolExecutor` 唯一实现;`ThreadHelper` 与 DoH 共同复用,旧私有类名 - 保留精确别名。标准 `shutdown(wait=True)` 仍可用于旧 `.pool` 调用,但采用锁内封口、锁外等待,避免 - worker 完成回调与 owner 锁互锁。 -- DoH 关闭现在先恢复系统 DNS,再使用 10 秒默认预算有限等待;超时返回 `False` 并保留已封口 executor, - startup 继续收口其它资源并向上报告失败。释放阻塞查询后可在同一 owner 上重试成功,只有真实终止后 - 才清空句柄;配置重启也不得覆盖未收敛 owner,关闭期间完成的旧查询不会回填下一轮缓存。 -- `DohHelper()`、`enable_doh()`、无参数 `shutdown()`、socket 补丁、惰性建池、解析器并发、缓存命中及 - `app.helper.doh` 精确映射全部保留;新增 timeout 和布尔收敛结果为向后兼容扩展。未修改 SDK/Compat - 清单、插件 Hook 或插件仓,V1/V2/V3 插件导入和调用边界不变。 -- 阻塞查询、有限返回、owner 保留、拒绝替换、重试终态、标准 shutdown 兼容和 startup 失败传播均有 - 故障注入;DoH/线程池/生命周期专项 66 项、旧导入与插件兼容联合专项 174 项、架构合同 94 项、 - Pylint 10.00/10、strict mypy 41 文件、全部宿主与质量 ratchet 及四分片全量 - `5955 passed, 3 skipped` 均通过。 - -### 长期整改阶段 67:工作流活动执行生命周期 owner 收口(2026-08-24) - -- 工作流生命周期组件原本先于 Scheduler、插件和模块关闭,但 concrete `WorkFlowManager.stop()` 只注销 - 事件监听并清空动作表,不登记或等待 `WorkflowExecutor`。阻塞动作探针确认旧 stop 立即返回 `None`, - 活动执行线程仍存活且动作表已经清空,后续组件会继续释放仍被节点使用的运行时依赖。 -- Application 工作流运行时协议现在声明最小 `WorkflowExecutionOwner`;concrete manager 是全部活动执行的 - 唯一注册表,停机先原子封口新准入,再向快照中的每个 owner 请求本地取消并使用 10 秒共享 deadline - 有限等待。单个 owner 抛错或超时不会跳过其它执行的停止尝试;未收敛 owner 和动作表均保留供同实例重试。 -- `WorkflowExecutor` 的节点池不再维护独立标准库实现,统一复用阶段 66 的 - `OwnedThreadPoolExecutor`;正常路径在节点完成后即时收敛,异常/阻塞路径保留执行 owner。manager 的本地 - 停止事件同时进入 `WorkflowCancelToken`,支持取消的重试和循环动作可尽快退出,不响应取消的第三方同步 - 动作则诚实返回未收敛。 -- `WorkflowChain.process()` 在写入运行中状态前取得执行准入,停机后的 API、事件、Agent 或 Scheduler - 新触发不会制造新的 `R` 状态。工作流组件改为 fail-fast owner;活动执行未终止时不得继续关闭监控器、 - Scheduler、插件、模块或 HTTP 依赖。 -- `WorkflowExecutor`、`WorkflowCancelToken(workflow_id)`、`WorkFlowManager()`、无参数 `stop()`、 - `WorkflowChain.process()`、旧自定义 runtime provider、事件/定时/API/Agent 调用路径均保留;新增可选取消 - 信号、准入和布尔收敛结果为兼容扩展。未修改 SDK/Compat 清单、插件 Hook 或插件仓,V1/V2/V3 边界不变。 -- 阻塞动作、有限返回、本地取消、owner/动作表保留、拒绝新执行、释放后重试、多 owner 异常隔离和生命周期 - fail-fast 均有故障注入;工作流/生命周期专项 91 项、工作流与旧导入联合专项 213 项、架构合同 94 项、 - Pylint 10.00/10、strict mypy 41 文件、全部宿主与质量 ratchet 及四分片全量 - `5959 passed, 3 skipped` 均通过。 - -### 总体判断 - -当前架构总体合理,已经从跨层混合的遗留单体收敛为**边界清晰的模块化单体**: - -- 继续采用单进程控制面是正确选择,不建议现在拆成微服务;插件、调度器、工作流、事件和数据库共享进程内状态,拆分会放大部署、事务和兼容成本。 -- `foundation/domain/runtime/adapters/application/chain/api/startup` 的职责方向基本成立;宿主架构基线、复杂度 ratchet、异步阻塞 ratchet 当前均通过。 -- 依赖图当前为 `810` 个 Python 模块、`6564` 条内部导入边;唯一非平凡 SCC 位于隔离的 TMDB 第三方移植包内部,不应为了指标归零重写。 -- 当前主要风险已经从“目录和依赖失控”转移到运行时协议、后台副作用的可靠性和遗留兼容面。换言之,下一阶段重点应是**语义收口和可验证性**,而不是继续搬文件或机械拆大文件。 - -综合评价:架构方向可持续,生产可用性较高;可演进性仍处于中等水平。现阶段没有静态审计发现必须立即推倒重来的 P0 架构问题,但存在需要按 P1/P2 计划治理的真实债务。 - -### P1:需要优先治理的真实债务 - -1. **后台任务的统一所有权已覆盖 API 入口,但仍有更深层任务机制待分级。** `app/runtime/tasks.py` 已建立 lifespan 级 TaskRegistry,启动收尾、插件 Release 刷新、Webhook E0 广播、CookieCloud E1 手工调度、消息入口、Seerr 订阅、整理历史 AI 重做、OpenAI/Anthropic 协议流和 WebAgent 断线后执行/快照保存均不再维护端点模块级任务集合或 Starlette 回调,shutdown 会停止接收、取消并有限等待,且生命周期清单明确登记其顺序。主仓 `app/` 已无裸 FastAPI `BackgroundTasks`;当前仍有约 `50` 个更底层 `create_task`/等价任务创建点,与线程池和 APScheduler 并存,后续需逐项确认 owner、取消、等待、重试、幂等和是否 durable,关键业务副作用优先接入已有 Outbox/恢复表。 - - Agent 活动摘要已完成一个深层 owner 切片:中间件仍保留自己的完成回调与非阻塞语义,但任务创建统一 - 经 lifespan `TaskRegistry` 登记为 `agent.activity_log.record`,宿主关停会取消并有限等待,不再形成 - 绕过全局关停预算的第二套后台任务集合。该摘要属于可丢弃 E1 观测数据,不宣称 durable。 - 模块同步清缓存的异步桥接也统一复用同一模式:Fanart 不再保留裸 `loop.create_task`,与 IMDb 一样登记 - 稳定 owner,并继续保留无事件循环时 `asyncio.run` 和原同步 ABI。 - 插件市场 Release 合并层的普通读取和强刷子任务也已登记稳定 owner;API 外层任务被关停取消时,内部 - `shield` 不再让网络请求逃逸生命周期预算,仓库级并发合并、缓存键和 V1/V2/V3 返回兼容保持不变。 - 请求作用域的结构化并发不进入全局登记器:传统 WebAgent SSE 的 collection 子任务改由生成器 - `finally` 取消并等待清理,断线和 ASGI 取消均不会留下请求级 task。 - 插件市场目录并发也采用同一责任边界:API/Agent 父请求取消或进度回调失败时,聚合服务会取消并等待 - 全部市场/代际 loader;正常单源失败仍隔离,不把请求级读取错误登记成 lifespan 后台任务。 - 搜索结果 AI 推荐则属于跨请求轮询任务,继续由 SearchChain 状态暴露进度,但任务创建已登记 - `chain.search.ai_recommend` owner;新搜索、手工取消和 lifespan shutdown 现在共享同一个 Task 终态。 - 订阅删除的宿主生产者也已完成 durable 分级:消息交互和远程删除不再调用裸线程统计入口,而是 - 与业务删除原子暂存事件和统计 intent;旧类方法只作为插件 ABI 保留,不纳入宿主可靠性证明。 - 消息渠道回环也不再各自拥有 URL、HTTP 判定和逐消息线程:七种宿主渠道共享 ingress,三种立即返回 - 渠道交给生命周期持有的共享线程池;这仍是 E0 投递,不宣称跨进程恢复。 - 图片代理安全日志的窗口聚合也已补齐内部任务所有权:timer 到期创建的 flush task 由 coalescer 持有, - `stop_modules()` 会刷新未到期摘要并等待已启动回调;它属于 E1 观测,不扩大 TaskRegistry 或 Outbox 范围。 - 插件市场与插件包适配器原有两套线程取消 wrapper 也已统一到 `runtime.execution`:连续取消必须等同步 - 文件 worker 到达终态后再传播,避免提前释放 mutation owner;旧模块内私有名称保留为 canonical 别名。 - 插件安装快照、取消补偿与市场临时文件清理原有两套协程终态循环也已合并为 - `await_task_to_terminal`;数据库 worker 的 interruptible 等待属于队列 owner,未被机械合并。 - canonical 宿主原有 11 处 FastAPI `run_in_threadpool` 导入也已统一到 `runtime.execution`,AST 门禁 - 防止再次出现框架直连;模块局部符号及调用参数不变,不改变插件运行和 monkeypatch 接缝。 - Feishu 长连接原先由每个配置实例分别覆盖 lark SDK 的模块级 loop 与阻塞选择函数,多实例并发会把 - 先启动实例的 SDK 任务路由到后启动实例的循环;现在以单一线程本地代理分派各实例 loop 和停止信号, - 任务集合、连接清理和模块 stop 仍由各实例持有。 -2. **Model/Base 的数据库装饰器和隐式会话 ABI 已全部清零。** 查询、写事务和 `legacy_*` 装饰器均为 `0`;所有 Model `db` 参数要求显式 Session,Base CRUD 仅在调用方事务内查询或 stage。可无会话构造的入口统一留在 Oper,经组合根事务执行器运行;插件 SDK 不再导出宿主 Model。后续重点转为减少 ORM 对象跨层流转,并保持 Model 隐式事务零回退。 - - Oper 内部的执行入口也已统一:最后一处 `AgentTaskOper` 直接 transaction runner 调用已迁入 - `DbOper._execute_sync_query`,静态门禁禁止 `app/db/oper` 再绕过统一的 Session 类型分派。 -3. **组合根和全局状态仍形成复杂的隐式运行时图。** Singleton 实例、模块级 provider、`configure_*` 注册函数和兼容 Facade 同时存在;它们解决了旧 ABI 和启动顺序问题,但增加测试污染、重复装配、实例身份和初始化顺序风险。`app/startup/lifecycle/__init__.py` 已有声明式生命周期,`app/startup/initializers/modules.py` 也有分阶段关闭,但尚未做到所有进程级资源都只通过 typed HostRuntime 访问。后续应以“新代码禁止新增 Service Locator/Singleton 依赖、旧入口有命中观测”为 ratchet。 - - Scheduler 已先完成一个可验证切片:API、Agent 与 Command 统一经 - `app.application.scheduling` Facade 获取实例,只有 `app.scheduler` 实现本身及 startup 组合根允许 - 依赖 concrete Scheduler;架构测试拒绝普通宿主消费者重新引入第二条实例化路径。 - 插件运行时也采用相同边界:factory 的动态路由投影和 Scheduler 插件任务统一延迟调用 - `app.application.plugin.runtime`,只有 startup 组合根与 `app.sdk.plugins` 兼容面允许直接引用 - concrete `PluginManager`,不改变 V1/V2/V3 插件加载与自由响应 API。 - Command 的 API 消费点也已完成原计划迁移:WebAgent 查询和站点认证后的刷新统一使用 - `app.application.commands`,只有 startup 组合根注册 concrete Command;门面保留原命令对象与插件命令语义。 - 宿主 Module 的部署配置读取也已统一到 `RuntimeSettingsCompat`:PostgreSQL、Redis、qBittorrent、 - rTorrent 和 Transmission 不再直接导入全局 Settings,模块目录由架构测试保持零直连;兼容代理仍保留 - 启动早期与旧插件/测试的动态 Settings 注入语义。 - 配置治理基线进一步区分债务与批准边界:canonical 未批准 Settings 直连和非组合根 - `SystemConfigOper()` 构造均为 `0`;`app/db/base.py`、`engine.py`、`session.py` 的启动前数据库基础设施 - 读取及 startup 唯一 Oper 构造点以带理由的固定边界登记。四个集合都只能减少,不能新增或换位置。 - 工作流运行时也已按相同模式收口:API、请求依赖与 Chain 只依赖 - `app.application.workflow.get_workflow_manager()`;只有工作流实现包和 startup 组合根可直接依赖 - concrete `WorkFlowManager`,架构测试拒绝宿主重新引入第二条实例获取路径。 - -### P2:中长期可演进性债务 - -- **大型职责域仍偏重。** 代表性热点包括 `app/chain/subscribe.py`(约 `4141` 行)、`app/chain/transfer.py`(约 `2944` 行)、`app/agent/orchestrator.py`(约 `3540` 行)、`app/agent/llm/provider.py`(约 `3529` 行)、`app/adapters/external/market.py`(约 `3139` 行)和 `app/api/endpoints/agent.py`(约 `2489` 行)。复杂度 ratchet 只保证不超过当前基线,不代表这些文件已经易维护。只有在行为快照、调用命中和事务边界明确后,才值得按用例拆分。 -- **类型门禁覆盖面不足。** `mypy.ini` strict 文件清单目前为 `41` 个文件,Agent、Chain、Module、Adapter 大量代码仍依赖动态类型。应从模块契约、生命周期、Repository/Port 和关键 Chain 返回值开始扩展,而不是直接开启全仓 strict。 -- **Pylint 仍是增量硬门禁。** `.github/workflows/pylint.yml` 对改动 Python 文件执行硬检查,但全仓报告使用 `|| true` 仅作 advisory。该策略适合存量迁移,却没有形成全仓质量趋势约束;应增加按目录和新增问题数的 ratchet。 -- **测试风格存在历史混用。** 当前有 `527` 个测试文件,仍有 `70` 个 `unittest.TestCase` 文件。它不是生产架构缺陷,但会增加 fixture、状态隔离和异步测试迁移成本,应在触碰相关模块时渐进迁移。 -- **跨仓治理链路尚未完全闭环。** 前端已有 lint、typecheck、分片 Vitest 和构建门禁;插件仓有 V1/V2/V3 索引及版本/依赖检查;资源和 Rust 仓有独立构建发布链路。但插件 CI 本地复核因插件仓环境缺少主仓依赖 `httpx2` 无法完成收集,说明“插件仓测试环境与主仓锁定依赖”的可复现性仍需加强。资源构建通过 PR 同步到 `MoviePilot-Resources`,Rust 发布后自动向主仓发依赖 bump PR,链路合理但仍是多仓异步发布,需保留版本 provenance 和回滚点。 - -### 已解决、不应重复治理的问题 - -- 分层依赖和重点禁止边已建立门禁;不要再以“减少目录数量”作为目标。 -- 全功能多 worker 的误导性配置已由 `app/runtime/topology.py` 和 `app/main.py` 拒绝;V3 默认单 worker 的部署事实已经明确。 -- 写事务已由组合根/UoW/Outbox 方向收口,`db_update/async_db_update` 为零;不要重新引入 Model 自动提交。 -- 已具备 correlation ID、`/health/live`、`/health/ready`、模块/事件/调度观测端口和兼容 Facade 命中指标;历史文档中“完全缺少观测能力”的描述已过时。 -- 212 个已观察宿主模块方法已全部使用可执行的非 legacy aggregation;只为未知第三方自定义方法保留开放 fallback,不再重复按能力族补聚合标签。 -- 旧导入路径、显式 `__all__` SDK 合同、插件 manifest 和 V3 实际可加载的三层索引实现均有白名单或版本约束;兼容层应继续保持“薄、可观测、只增不删”,不应为了清理目录直接删除。 -- TMDB 移植包内部 SCC 属于第三方隔离代码,按现状豁免是合理的技术决策。 - -### 刻意保留的兼容成本 - -以下内容不是遗漏,而是当前产品 ABI 的有意成本: - -1. 未知第三方插件自定义模块方法继续走 `legacy` fallback,不能因宿主契约收口而拒绝加载旧插件。 -2. `PluginManager`、`PluginHelper`、`MoviePilotServerHelper` 等 Facade 继续保留旧公开/私有调用面,并通过 `compat.facade.hit` 统计迁移命中。 -3. `app/runtime/compat` 的精确旧导入映射、`app.sdk._legacy` 薄门面和插件 V1/V2/V3 三代索引继续存在,直到命中数据和发行策略支持删除。 -4. 插件访问宿主持久化必须经过 Oper 或稳定 SDK;不再保留直接调用宿主 Model 的事务兼容。 - -### 建议的后续治理顺序 - -1. **先做后台任务审计与统一登记**:建立 TaskOwner/生命周期协议,区分请求后非关键通知、可重试 Outbox 副作用和必须在请求内完成的业务写入;为断线、崩溃、重复执行和 shutdown 补测试。 -2. **随后收敛组合根和全局状态**:为新代码禁止新增 Service Locator/Singleton 依赖,逐步让关键服务只通过 typed HostRuntime 获取;旧 Facade 继续保留命中观测和插件 ABI。 -3. **再扩展类型和复杂度预算**:每次触碰大型职责域时拆一个可回滚垂直切片,同时扩大 mypy strict 清单和 Pylint 新增问题 ratchet;不要为追求行数指标进行无行为收益的拆分。Module Contract V2 只继续保持新增方法门禁和第三方 fallback 观测。 -4. **跨仓发布以契约为中心**:保持插件索引、前端远程组件、资源版本、Rust wheel 和主仓依赖的 provenance;将插件测试环境固定为主仓 `uv.lock` 可复现安装,避免本地和 CI 依赖漂移。 - -本轮复核结论:**当前架构不需要推倒重来,真正未完成的是运行时可靠性和协议收口。** 下一轮治理完成上述 P1 后,再评估是否值得继续拆分大型文件或扩大严格类型范围。 - -## 1. 结论先行 - -MoviePilot V3 当前不是“目录混乱、必须推倒重来”的状态。第一阶段治理已经取得实质成果: - -- `foundation/domain/runtime/adapters/application` 等实现根没有自有循环依赖; -- Adapter、Runtime、Application、Chain、API 等重点边界到 DB 的禁止边为零; -- `app.core`、`app.helper`、`app.utils` 已经是精确兼容入口,不再是宿主实现目录; -- 插件 raw API、SDK/Compat、生命周期清单、模块调度快照和零真实网络测试均已有门禁; -- 当前唯一 SCC 位于隔离的 TMDB 移植包内部,不应为了指标归零重写第三方风格代码。 - -因此,下一阶段不应继续以“搬文件、拆目录、减少行数”为主目标。八类运行时和演进问题的当前状态如下; -其中已完成的门禁不能再作为待办重复实施,部分完成项则继续按低水位推进: - -1. **架构基线工具的事实源分离已完成。**宿主、官方插件和启动性能使用独立 check/write;稳定语义与源码位置诊断分开,配置/事务低水位采用单向 ratchet,不能再用一次全量 `--write` 掩盖跨域变化。 -2. **V3 部署拓扑边界已完成。**全功能模式在 startup、launcher 和 Doctor 共同拒绝 `API_WORKERS > 1`,生产入口固定单 worker;开发 reload/监督模式使用 `app.factory:create_app` import-string factory,不再把 app 实例交给多进程 supervisor。旧配置键继续可解析,未来只有拆出 control role 后才重新评估全功能多 worker。 -3. **事务所有权已完成装饰器层收口,但 ORM 对象跨层流转仍需治理。**正式 Model 查询/写装饰器均已清零,宿主 Oper 查询统一接收显式 Session;调用方仍需继续明确 ORM 对象生命周期、懒加载和业务提交后副作用边界。 -4. **组合根之后仍存在全局服务定位,但配置和 API 数据主路径已收口。**canonical 未批准 Settings 导入与非组合根 `SystemConfigOper()` 构造均为 `0`;数据库基础设施 3 处和 startup 唯一构造点作为不可扩张边界登记。正式 FastAPI 依赖只读取 AppState `HostRuntime` 的命名领域,字符串 API 数据注册表仅允许 startup 注入和旧 Facade 转发;后续对象是 Singleton 与模块级 `configure/get` provider,不应再迁移已类型化 API 依赖。 -5. **模块与事件契约登记均已完成。**当前 212 个模块 spec 的宿主观察面已无 legacy aggregation,53 个事件全部绑定 typed payload,可见性、投递等级、错误行为和敏感字段均有基线,legacy event payload 为 `0`。六个 durable-required 事件的宿主正式生产者已通过业务同事务 Outbox 提供真实持久投递,事件与 topic 映射及恢复 handler 完整性已纳入 ratchet。后续重点是保持新增能力门禁和观察未知第三方 fallback 命中,不是重复创建契约、DTO 或 Outbox。 -6. **后台副作用已有统一可靠性分类,但其他 E1/E3 机制仍需逐项收口。** ADR-0007 已分类事件队列、APScheduler、进程内任务、Agent task 与 transfer pending 的完成点、恢复和失败表达;不能因六个关键事件已接 Outbox,就把仍需定时重建、持久任务表或人工恢复的其他 E1/E3 机制误报为全部完成。 -7. **核心关联与健康边界已落地,指标导出仍未收口。**HTTP/SSE correlation ID 已传播到线程池、事件、工作流、子进程、外部请求和日志;`/health/live`、`/health/ready` 已由部署入口消费,事件/数据库队列深度及模块/事件耗时使用低基数指标登记。当前缺口是稳定 exporter、运维查询面和跨进程聚合,而不是重新实现 request ID 或健康路由。 -8. **质量门禁已具备增量硬约束,但覆盖面仍需扩大。**push/PR 对变更 Python 文件执行 Pylint,CI 同时运行 host architecture、41 个 strict mypy 文件、复杂度、async 阻塞和 task owner ratchet;全仓 Pylint 仍是 advisory,strict 类型和复杂度拆分仍应随业务切片渐进扩展。 - -建议保持**模块化单体**,按以下顺序治理: - -```text -先修治理工具和部署真相 - -> 再统一事务边界和运行时装配 - -> 再类型化模块/事件协议 - -> 再为关键副作用补持久可靠性 - -> 最后收敛可观测性、类型和复杂度预算 -``` - -不建议在本轮引入微服务、通用 DI 框架、Celery/Kafka、全仓 ORM 重写或全仓强类型。这些动作会显著扩大插件兼容、部署和回滚面,但不能直接解决当前最重要的问题。 - -## 2. 当前审计基线 - -### 2.1 取证方法 - -本次复核执行或检查了: - -- 仓库、分支、上游和工作树状态; -- `AGENTS.md`、架构/设计/测试规则和两份既有重构文档; -- 当前宿主 AST 依赖图、SCC、禁止边和运行契约快照; -- FastAPI 创建、路由聚合、异常处理、生命周期和 Uvicorn 入口; -- SQLAlchemy Session、事务装饰器、UnitOfWork、Model 与 Oper 的真实调用关系; -- Event、Module dispatcher、Scheduler、插件运行时与后台线程; -- 方法规模、端点规模、全局配置读取和类型注解近似统计; -- 独立 `MoviePilot-Plugins` 仓的当前版本与宿主内插件兼容基线关系。 - -### 2.2 已验证数据 - -| 指标 | 当前值 | 判断 | -| --- | ---: | --- | -| 宿主 Python 模块数 | 810 | 排除 `app/plugins/**` | -| 宿主内部导入边 | 6,560 | 边数本身不是质量目标 | -| 非平凡 SCC | 1 | 仅 TMDB 移植包内部 | -| 重点禁止边 | 0 | Adapter/Runtime/Application/API/Chain 等到 DB 的既有门禁均通过 | -| 架构专项测试 | 68 passed | `test_architecture_dependencies` + `test_architecture_contract_baseline` | -| 宿主 Python 代码行 | 约 241,227 | 含注释和空行,仅用于趋势 | -| 已登记模块调用方法 | 211 | 212 个宿主 spec,其中 211 个进入 `run_module` 观察面 | -| legacy 默认模块契约 | 0 个宿主观察方法;未知动态方法保留 fallback | 所有静态宿主方法已有显式 V2 spec;真实 fallback 命中由 `module.contract.legacy_hit` 观测 | -| 事件枚举 | 53 | 78 个 producer、15 个 consumer(含动态观察) | -| 专用 EventData model | 53 | Event Contract Registry 已为全部事件登记 typed payload/fallback 原因 | -| 直接读取 `settings` 的文件 | 0 | 当前宿主基线已清零;部署配置通过组合根快照/窄端口提供 | -| `SystemConfigOper()` | 1 个 | 仅组合根创建 `SystemConfigService` 时保留 | -| Model/Base 上的 DB 装饰器 | 0 | 正式与 legacy 查询/写装饰器全部为 0;`db` 参数必须显式传入 | -| API/Application/Chain 公共复杂度超限 | 0 | 当前 `scripts/architecture/complexity.py` ratchet 无新增或增长债务 | -| 公共函数缺少返回注解 | 约 1,592 / 7,442 | AST 近似值,适合做 ratchet,不适合直接作为失败阈值 | -| 公共参数缺少注解 | 约 858 / 12,763 | 主要集中在 `app/modules` | - -当前复杂度门禁只对新增/增长负责;同一职责域中的大型兼容 Facade、厂商协议实现和第三方移植代码仍以 -行为快照、依赖边界和增量 ratchet 为主要治理尺度,不以机械拆文件代替所有权迁移。 - -### 2.3 本次检查暴露的基线问题 - -宿主架构测试通过,但下面的跨仓命令失败: - -```bash -./.venv/bin/python scripts/architecture/baseline.py \ - --check \ - --plugin-repo ../MoviePilot-Plugins -``` - -失败对象是 `tests/fixtures/architecture/official-plugin-baseline.json`。宿主内基线记录的插件仓提交为 -`ddb41dbcbbea21196154a7f6d5fdba3aa34a5e4a`,当前独立插件仓为 -`217c8d25ffe6ff0b3f6352c4278fd6896def442e`。这不是宿主依赖环回归,但当前命令无法单独表达“宿主硬门禁通过、外部插件仓观察值已变化”。 - -另一个风险是 `scripts/startup/performance.py` 在不传 `--output` 时会直接覆盖已提交基线。其他 AI 在只想读取当前数据时,很容易制造未审查的基线变更。 - -**本次审计没有更新任何基线文件。**上述意外写入已恢复,最终工作树只包含本文和文档索引改动。 - -阶段 0 实施后,宿主与插件基线已使用独立 check/write 入口;运行契约行号只进入按需诊断, -插件 commit、源码摘要和文件数只作为 provenance。当前宿主和官方插件语义检查均通过,CI 会在 -主仓 PR/push 执行宿主硬门禁,并在定时/手工工作流中上传最新插件仓的语义差异报告。 - -## 3. 优秀 Python 后端实践对标 - -本节只采用与 MoviePilot 当前形态相近、能转化为具体约束的实践。参考不是为了照抄目录,而是为了验证职责、生命周期和失败语义。 - -| 对标来源 | 可复用实践 | MoviePilot 当前差距 | 采用方式 | -| --- | --- | --- | --- | -| [FastAPI:Bigger Applications](https://fastapi.tiangolo.com/tutorial/bigger-applications/) | Router、依赖和主应用分离;路由按领域聚合 | Router 已分文件,但 `app/api/deps.py` 集中 33 个依赖工厂,部分端点仍编排完整用例 | 保留现有 Router;按垂直切片拆依赖和 presentation mapper,不重做目录树 | -| [FastAPI 官方 Full Stack Template](https://github.com/fastapi/full-stack-fastapi-template/tree/master/backend/app) | 请求依赖提供 Session,测试和迁移入口明确 | MoviePilot 已有请求 Session 和 UoW,Model 隐式事务已清零;仍需继续减少 ORM 对象跨层流转 | 将 Session 生命周期留在请求/作业边界,Repository 只登记变更 | -| [SQLAlchemy Session Basics](https://docs.sqlalchemy.org/en/20/orm/session_basics.html) | Session/事务生命周期应与具体数据操作分离;Session per thread、AsyncSession per task | Model/Base 已要求显式 Session;无会话 Oper 仍依赖组合根事务执行器 | 新写用例强制请求/任务级 UoW;持续禁止 Model 重新拥有事务 | -| [Starlette Lifespan](https://www.starlette.io/lifespan/) | Lifespan 完成前不接流量;用 typed state 共享进程资源;用 task group 管理异步任务 | 已有声明式生命周期,但仍依赖多个模块全局注册表和裸 `create_task`/线程 | 建立类型化 `HostRuntime/AppState`,旧 provider 继续作兼容门面 | -| [Uvicorn Deployment](https://www.uvicorn.org/deployment/) 与 [Lifespan](https://www.uvicorn.org/concepts/lifespan/) | reload/workers 使用 import string/factory;每个 worker 独立执行 lifespan | 当前 app 实例与 reload/workers 配置并存,多 worker 会重复控制面 | V3 先明确只支持单 worker;开发 reload 改为 factory/import string;未来再拆 control role | -| [Home Assistant:Integration Quality Scale](https://developers.home-assistant.io/docs/core/integration-quality-scale/) | 插件/集成按可测试性、错误处理、异步安全、类型和文档分级;豁免必须说明 | Module 能力差异大,只有统一发现和方法名快照,没有每个集成的质量状态 | 为宿主 Module 建立轻量质量清单和逐项 ratchet,不阻塞历史模块运行 | -| [Home Assistant:Blocking operations with asyncio](https://developers.home-assistant.io/docs/asyncio_blocking_operations/) | 阻塞 I/O 必须移出事件循环,并提供检测 | 项目已有 ThreadHelper/异步 HTTP,但没有统一的阻塞调用检测门禁 | 先对 API、Agent、Application 新代码加调试/测试检测,不开展全仓 async 重写 | -| [Home Assistant:Fetching data](https://developers.home-assistant.io/docs/integration_fetching_data/) | 外部集成统一刷新协调、并发限制、退避和认证失效语义 | 各 Module 自行决定轮询、缓存、限流和错误降级 | 先建立 Module quality/contract 字段,再为同一模块族复用 coordinator | -| [OpenTelemetry Python](https://opentelemetry.io/docs/languages/python/) | Trace/Metric 稳定,支持标准上下文传播和框架/HTTP client instrumentation | 当前缺少跨 API、Chain、Module 和外部请求的关联标识 | 先实现无依赖 request ID;OTel 作为可选 Adapter,不能成为核心层依赖 | - -### 3.1 明确不照抄的内容 - -- FastAPI 示例中的直接 endpoint → ORM 只适合较小 CRUD 服务;MoviePilot 有插件、调度、工作流和多入口,仍应使用 Application/Repository。 -- Home Assistant 的完整 Integration Framework 不能直接替换现有 Module/Plugin 体系;只借鉴质量清单、异步边界和刷新协调思想。 -- OpenTelemetry 不应在第一步强制进入所有环境;应先定义内部观测端口和 request ID,再提供可选 exporter。 -- 不因 SQLAlchemy 官方示例使用同步 Session 就把现有 async 查询全部改回同步;关键是事务范围清晰,不是统一一种 I/O 风格。 - -## 4. 目标架构 - -目标仍是单仓、单部署单元的模块化单体,但把 API 数据面、宿主控制面、持久可靠性和观测边界区分开: - -```mermaid -flowchart TB - Entry["API / CLI / Agent / Scheduler / Workflow"] - ApiState["Typed HostRuntime / AppState"] - UseCase["Application Command / Query"] - Domain["Domain rules"] - Ports["Repository / Module / External Ports"] - Adapters["DB Oper / Module Dispatcher / External Adapters"] - Control["Control Plane: Plugin / Scheduler / Monitor / Event"] - Durable["Durable Job / Outbox / Existing recovery tables"] - Observe["Request ID / Metrics / Trace Adapter / Health"] - - Entry --> ApiState --> UseCase - UseCase --> Domain - UseCase --> Ports --> Adapters - Control --> Ports - UseCase --> Durable - Control --> Durable - Entry -.correlation.-> Observe - UseCase -.correlation.-> Observe - Adapters -.correlation.-> Observe -``` - -强制原则: - -1. API/CLI/Agent/Scheduler 只是不同入口,事务和业务完成定义归 Application 用例。 -2. Model 负责映射、数据库约束和必要的同表纯条件表达;不拥有 Session 生命周期和自动提交。 -3. Oper/Repository 负责查询和登记变更;不决定整个业务动作何时 commit。 -4. 宿主运行对象由 Startup 创建;FastAPI 通过 typed state/Depends 读取,不新增字符串 Service Locator。 -5. Module 与 Event 的动态兼容继续存在,但宿主高频能力必须有可检查签名、结果和错误语义。 -6. 普通进程内通知可以丢失;影响用户数据完成状态的副作用必须显式选择 durable 语义。 -7. V3 默认单 worker。未拆出控制面前,不允许通过多 worker 复制插件、调度和监控运行时。 -8. 插件公开 ABI 只增不删;宿主内部改造不能要求同步修改所有第三方插件。 - -## 5. 分阶段实施路线 - -每个任务均应独立提交、独立回滚。除非任务明确说明,不允许同时改数据库 schema、前端协议和插件仓。 - -### 阶段 0:先让治理工具可信 - -#### ARCH-201:拆分宿主与插件基线命令 - -**目标**:基线工具能够分别检查/写入宿主依赖、宿主运行契约、官方插件兼容和启动性能,禁止一次操作无差别覆盖全部文件。 - -**允许范围**: - -- `scripts/architecture/baseline.py` -- `scripts/startup/performance.py` -- `tests/test_architecture_contract_baseline.py` -- 新增的脚本 CLI 测试 -- 对应文档 - -**实施步骤**: - -1. 为架构脚本增加互斥的细粒度参数,例如: - `--check-host`、`--check-plugins`、`--write-host`、`--write-plugins`。 -2. `--check-host` 不要求 `../MoviePilot-Plugins` 存在。 -3. `--write-*` 必须显式指定目标;不带写参数只能输出到 stdout 或退出。 -4. 性能脚本改为 `--print`/`--check`/`--write` 三种明确行为;默认只打印,不能覆盖 fixture。 -5. 写入前在 stdout 列出将修改的文件;写入后仍由 Git diff 供人工审查。 -6. 保留旧 `--check/--write` 一小段兼容期时,只允许它们给出弃用提示并要求显式 scope,不能继续静默全写。 - -**禁止**: - -- 不因当前插件仓已变化而直接刷新 baseline; -- 不在本任务改变依赖规则、SDK 导出或插件 hook; -- 不把 Git commit hash 变化等同于 ABI 变化。 - -**验证**: - -```bash -./.venv/bin/python -m pytest \ - tests/test_architecture_contract_baseline.py \ - tests/test_architecture_baseline_cli.py -q - -./.venv/bin/python scripts/architecture/baseline.py --check-host -./.venv/bin/python scripts/architecture/baseline.py \ - --check-plugins --plugin-repo ../MoviePilot-Plugins -``` - -**完成标准**:只改插件仓时宿主门禁仍能独立通过;只读命令不会修改工作树;每个 fixture 都有独立写入口。 - -**回滚**:恢复旧 CLI 解析器即可;不得回滚已审查的 fixture 内容。 - -**实施记录(2026-08-21)**: - -- `baseline.py` 已提供 host/plugin 的显式 check/write scope,性能基线提供 print/check/write;默认行为只读, - write 前列出目标,宿主检查不依赖外部插件仓存在。 -- CLI、只读工作树和 fixture 定向写入测试已覆盖,提交为 `7bc3ea83`。 - -#### ARCH-202:把语义基线与诊断位置分开 - -**目标**:正常行号移动、时间戳或插件仓 HEAD 变化不再触发“架构语义变化”;真实方法、事件、导入、签名和结果契约变化仍失败。 - -**实施步骤**: - -1. 将 `caller + line` 拆为 gate key(`caller + operation/method/event`)和 diagnostic(当前 line,仅用于报告)。 -2. `generated_at`、平台、源码 HEAD、源码摘要属于 provenance,不参与 semantic equality。 -3. 插件基线分别保存公开导入、hook、动态 API 路由契约和扫描来源 revision。 -4. CI 比较语义集合;revision 改变但语义不变时只输出 notice。 -5. 对 import edge 继续保留完整集合,但将“禁止边”测试与“全图快照”测试分开,前者是硬门禁,后者要求审查变化原因。 -6. 为旧 fixture 写一次 schema migration 读取器,避免直接删除历史字段导致维护脚本失效。 - -**完成标准**:只插入空行不改变 semantic fixture;改 `run_module("...")`、EventType、SDK 导出或 Compat 映射仍稳定失败。 - -**实施记录(2026-08-21)**: - -- semantic key 与行号/来源 revision 诊断已分离;时间、位置和外部仓 HEAD 不再参与硬比较,真实 import、method、 - event、SDK/compat 变化仍产生精确 diff。 -- 旧 fixture 可兼容读取,语义稳定性与真实变更失败测试通过,提交为 `37c442b0`。 - -#### ARCH-203:把架构与跨仓兼容纳入持续 CI - -**目标**:当前只手工执行的架构/插件观察变成分层 CI。 - -**实施步骤**: - -1. 每个主仓 PR 必跑宿主架构测试和 `--check-host`。 -2. 官方插件兼容分为 PR 硬门禁(仓库内固定 fixture/样例插件)和定时/手工观察(独立插件仓最新默认分支)。 -3. 跨仓观察失败不能由机器人自动 `--write`;必须创建可审查结果,说明新增/删除的导入、hook、API 契约。 -4. Pylint 工作流至少对主仓 PR 运行改动文件严重错误检查;全仓报告仍可手工生成。 - -**完成标准**:宿主 PR 不因外部仓普通版本变化随机红灯;真实兼容破坏能定位到插件和符号。 - -**实施记录(2026-08-21)**: - -- 主测试 workflow 增加独立宿主 Architecture Contract Gate,跨仓插件检查进入定时/手工 observation workflow, - 只上传报告、不自动写 baseline。 -- Pylint 硬门禁只检查本次改动 Python 文件,全仓结果保留 advisory artifact;提交为 `6c7c54d2`、`0959831b`。 - -### 阶段 1:固定部署拓扑和统一入口 - -#### ARCH-210:V3 全功能模式强制单 worker - -**现状证据**: - -- `app/runtime/config.py` 暴露 `API_WORKERS`; -- `app/main.py` 将具体 `app` 实例和 `workers=settings.API_WORKERS` 同时交给 Uvicorn; -- `app/startup/lifecycle/__init__.py` 在 lifespan 中启动插件、APScheduler、监控器、命令和工作流; -- Uvicorn 官方说明每个 worker 都会独立执行 lifespan。 - -**目标**:在控制面拆分完成前,拒绝 `API_WORKERS != 1`,避免重复调度、重复插件事件、重复文件监控和进程内状态分裂。 - -**实施步骤**: - -1. 新增位于 Startup/入口边界的 `validate_process_topology()`,不要放进 Domain/Application。 -2. 全功能模式下 `API_WORKERS != 1` 直接给出可操作错误;不能只打 warning 后继续启动。 -3. Doctor 增加同一诊断,说明为什么不是“多开几个 API worker 就能扩容”。 -4. 文档明确:V3 当前扩容单位是完整 MoviePilot 实例,单一配置/数据库只应有一个控制面实例。 -5. 补回归测试:worker=1、worker>1、safe mode,以及环境变量解析错误。 - -**禁止**: - -- 不用数据库锁“快速解决”全部多进程问题;动态插件路由、Singleton、内存交互状态和监控器仍会分裂; -- 不在本任务引入 Redis leader election; -- 不删除 `API_WORKERS` 配置键,以免旧配置解析失败。 - -**完成标准**:不再存在看似支持、实际重复运行控制面的多 worker 配置。 - -**实施记录(2026-08-21)**: - -- Startup 边界、launcher 与 Doctor 共用单 worker 拓扑合同;全功能模式拒绝 worker>1,safe mode 与旧配置键保持 - 兼容,部署文档解释控制面重复风险。 -- worker=1/>1、safe mode 和配置解析专项测试通过,提交为 `d89d2961`。 - -#### ARCH-211:修正 Uvicorn app factory 与开发 reload - -**目标**:生产、开发 reload、测试和外部 ASGI supervisor 使用明确、可验证的入口,不依赖 app 实例在 multiprocessing/reload 下的未支持行为。 - -**实施步骤**: - -1. 冻结四种入口行为:`python -m app.main`、本地 `start-local.sh`、`app.factory:create_app`、`TestClient`。 -2. 开发 reload 使用 import string + factory 形式,不直接传 app 实例。 -3. 生产入口保持自定义优雅停止语义,但只启动一个 worker。 -4. `create_app()` 只做 ASGI 结构创建;插件运行实例、数据库连接和后台线程不得在 import/create 阶段物化。 -5. `app.factory:app` 若需保留,作为薄兼容入口调用同一个 factory,测试对象 identity 和副作用。 -6. 对每种入口记录:import 是否联网、是否开线程、是否建 DB 连接、lifespan start/stop 次数。 - -**验证**: - -```bash -./.venv/bin/python -m pytest \ - tests/test_moviepilot_launcher.py \ - tests/test_lifecycle_shutdown.py \ - tests/test_testing_bootstrap.py -q -``` - -**实施记录(2026-08-21)**: - -- reload/监督进程统一使用 `app.factory:create_app` import-string factory;生产入口保留单 worker 和既有优雅停止, - import/create 阶段不启动 DB、插件或后台线程。 -- launcher、lifespan、factory/TestClient 与信号关停测试通过,提交为 `bb57b229`。 - -#### ARCH-212:统一数据库准备与健康语义 - -**现状证据**:`run_application()` 会调用 `prepare_database()`,外部 supervisor 直接加载 -`app.factory:app` 时不会;lifespan 当前只预热引擎,不执行迁移准备。 - -**目标**:所有受支持入口对数据库迁移、备份、head 校验和 ready 状态具有同一语义。 - -**推荐方案**:在 V3 单 worker 前提下,把数据库准备作为最早的声明式生命周期组件;若未来拆多 worker,再改为独立 prestart/control role。 - -**实施步骤**: - -1. 为现有 `prepare_database()` 补入口矩阵测试,不先改算法。 -2. 新增“数据库准备”生命周期组件,顺序早于 Router、Module、Plugin 和 Scheduler。 -3. 删除 `app.main` 的重复调用,保证一个进程只走一个事实入口。 -4. 数据库准备失败必须阻止 readiness 和服务接流量;不能降级成后台日志。 -5. 新增最小公开探针: - - `/health/live`:进程和事件循环存活,不查外部系统; - - `/health/ready`:生命周期完成、数据库 revision/head 可用、控制面未处于不可恢复启动失败。 -6. 公开探针只返回最小状态,不泄露路径、版本链、插件名和异常栈;详细诊断仍需管理员鉴权。 - -**回滚**:生命周期组件可暂时委托回 `app.main` 旧调用,但同一版本不能同时保留两个主动迁移入口。 - -**实施记录(2026-08-21)**: - -- 数据库准备已成为最早生命周期组件,`app.main` 不再重复迁移;所有受支持入口共享 prepare/head/readiness 状态。 -- `/health/live` 与 `/health/ready` 使用最小响应,准备失败阻止 ready;入口矩阵、失败和探针测试通过,提交为 - `dd1c4c32`。 - -### 阶段 2:统一数据访问和事务所有权 - -#### ARCH-220:建立 Model/Repository 事务 ratchet - -**目标**:先禁止债务增长,再按用例迁移;不要求一个 PR 清除 178 个 Model 装饰器。 - -**实施步骤**: - -1. 在架构 fixture 记录 `app/db/models/**` 中: - - `@db_query` / `@db_update` / async 变体数量; - - 每个 Model 的装饰方法清单; - - 直接 `Session.commit/rollback` 调用。 -2. 新增硬规则:新 Model 不得新增会话生命周期/自动提交方法;修改到的旧写方法应优先迁移到 Oper。 -3. `app/db/oper/**` 允许 SQLAlchemy 查询和 stage mutation,但禁止自己创建独立 Session 或在可组合方法中 commit。 -4. Application command 持有 UnitOfWork;API、Scheduler、Agent 分别在自己的逻辑操作起点创建 Session/UoW。 -5. 读操作允许短会话自动 close,但不得让返回的 ORM lazy attribute 在 Session 外才加载。 -6. 对同步线程和异步任务分别验证 Session 独占,禁止跨线程/跨 task 共享同一个 Session。 - -**完成标准**:装饰器基线不增长;新写用例可从测试中明确观察 `stage -> commit -> after-commit effect` 顺序。 - -**实施记录(2026-08-21)**: - -- host architecture baseline 新增 transaction debt 域,记录 Model decorator 和直接 commit/rollback;新增或增长会 - 失败,减少允许通过。 -- Repository/Oper 的 Session 所有权与可组合 stage 规则已有静态和生命周期测试,提交为 `de2957b9`。 - -#### ARCH-221:以订阅写入做首个完整事务切片 - -**范围**: - -- `app/application/subscription/` -- `app/db/oper/subscribe.py` -- `app/db/models/subscribe.py` -- `app/api/deps.py` 中对应依赖工厂 -- 订阅 API/Agent/Scheduler 聚焦测试 - -**目标**:同一个“新增或修改订阅”用例无论从 API、Agent 还是 Scheduler 进入,都由 Application command 决定事务完成和提交后副作用。 - -**实施顺序**: - -1. 列出所有写入口和旧返回/事件/上报顺序。 -2. 先补 commit 失败、事件失败、上报失败和重复请求测试。 -3. 把所需 SQL 从 Model classmethod 移到 `SubscribeOper`;Model 保留字段、约束和无 I/O 条件表达。 -4. Repository 方法只 `add/update/delete/flush`,不 commit。 -5. Command 统一 commit/rollback;只有 commit 成功后才发送事件、刷新调度和上报。 -6. 旧 Chain/API 方法委托新 Command,保留返回值、消息、事件 payload 和插件可见行为。 -7. 度量本切片迁移前后 Model 装饰器、方法长度和事务测试数量。 - -**禁止**: - -- 不顺便改订阅表字段或媒体身份; -- 不同时重写订阅搜索/匹配算法; -- 不让事件失败回滚已经提交的数据库事务并伪装成“数据库未写入”。 - -**完成标准**:一个业务动作只有一个事务所有者;任意入口都不会因内部 Model 方法提前 commit 而产生部分写入。 - -**实施记录(2026-08-21)**: - -- `app/application/subscription/write.py` 定义用例 Port,`app/db/adapters/subscription.py` 为每次规范新增创建独占同步/异步 Session,`app/startup/composition/subscription.py` 只负责注入; - `CreateSubscriptionCommand` / `AsyncCreateSubscriptionCommand` 持有 UoW,Oper 只执行 - 查重、`add` 与 `flush`。 -- `SubscribeOper.stage_add()` 的查重 SQL已收口到 Oper;无会话构造 `SubscribeOper()` 时由 - Oper 的 `_execute_*` 委托组合根事务执行器,Model 不再创建会话。 -- Chain 把原有“成功消息 → `SubscribeAdded` 事件 → Server 统计”作为显式 post-commit - 回调交给 Command;commit/flush 失败回滚,事件或上报失败只传播原异常,不回滚已提交记录。 -- 同步/异步 `SubscribeChain.add` 方法长度从各 203 行降至 183/186 行;新增 9 个事务边界测试, - 覆盖成功顺序、commit/flush 失败、重复请求、Oper 不提交、事件失败、上报失败与真实落库。 -- Model 查询装饰器曾有 123 个,分切片迁移后已连同 Base 的 12 个 legacy 查询/写装饰器全部删除。 - `AgentTask`、PassKey 等 Model 方法保留查询语义,但签名统一要求显式 Session;无 Session 使用方式 - 只在对应 Oper 上存在,由组合根事务执行器承接。归属过滤、启用状态过滤和稳定排序继续由显式 - Session 的 Model 测试与无会话 Oper 测试共同覆盖。 - -#### ARCH-222:按风险迁移其余写用例 - -推荐顺序: - -1. 站点配置写入; -2. 下载/整理历史删除与恢复; -3. 工作流定义和状态变更; -4. Agent task/chat 写入; -5. 插件配置与安装状态。 - -每个切片沿用 ARCH-221,不允许批量移动全部 Model 方法。查询方法可在写边界稳定后再迁移。 - -**实施记录(2026-08-21)**: - -| 风险域 | 规范事务入口 | 结果 | -| --- | --- | --- | -| 站点配置 | `SiteMutationCommand` + Async UoW | create/update/priorities/delete/reset 均先 stage 再 commit | -| 下载/整理历史 | `DownloadHistoryMutationCommand`、`TransferHistoryMutationCommand` + Sync UoW | 多表删除与文件副作用顺序已有聚焦回归 | -| 工作流 | `WorkflowMutationCommand`、`WorkflowDefinitionCommand` + Sync/Async UoW | 定义写入提交后才刷新 timer/event | -| Agent chat | `AgentChatService` + 请求级 Async UoW | API 会话删除改为 `async_stage_delete()`;失败回滚、缺失不提交 | -| 插件数据重置 | `DeletePluginDataCommand` + 独占 Sync Session/UoW | `PluginDataOper.stage_delete()` 只 DELETE/flush;重置链由 startup 装配 | - -旧插件与宿主存量代码直接构造 `PluginDataOper`、`AgentChatOper` 的行为继续保留;新 API 和插件 -重置链不得回退到这些自动提交兼容方法。五类矩阵聚焦测试共 57 项通过,事务 ratchet 仍为 -178 且没有新增或搬移 Model 装饰器。 - -### 阶段 3:类型化运行时装配,减少全局服务定位 - -#### ARCH-230:建立类型化 HostRuntime / AppState - -**目标**:用 Startup 创建的显式运行时对象替代全局字符串注册表,同时保留旧 provider 兼容入口。 - -**建议结构**: - -```text -app/startup/composition/context.py # HostRuntime 及构建结果 -app/api/context.py # API 可见的最小 AppState / 读取依赖 -app/api/dependencies/ # 按领域拆分依赖工厂 - auth.py - subscription.py - site.py - workflow.py -``` - -以上文件名均为单个小写单词,符合仓库命名规则。 - -**实施步骤**: - -1. 定义 slots dataclass 或 TypedDict,字段使用具体 Protocol 类型,禁止 `dict[str, Any]` 仓储表。 -2. Startup 构建 HostRuntime;lifespan 通过 `app.state` 或 yield state 暴露给请求。 -3. FastAPI Depends 从 Request/AppState 取精确能力,不直接读取模块全局 `_ports`。 -4. `ApiDataPorts` 和 `configure_api_data_ports()` 暂时保留为兼容 Facade,内部委托同一个 HostRuntime,不形成第二份实例。 -5. 先迁移一个垂直切片,验证测试可以传 fake runtime,不加载真实 PluginManager/DB engine。 -6. 每迁移一个领域就删除对应字符串 key;禁止新增新 key。 - -**禁止**: - -- 不引入第三方 DI container; -- 不创建一个更大的全局 `services: dict[str, Any]`; -- 不把完整 HostRuntime 传入 Domain 或每个小函数。 - -**实施记录(2026-08-21)**: - -- `app/startup/composition/context.py` 定义 frozen slots `HostRuntime` 与首个窄能力 - `AgentChatRuntime`,仓储、Session、UoW 字段均为具体 Protocol 工厂,不是字符串字典。 -- `init_modules()` 保留零参数兼容签名并返回本次 lifespan 唯一 Runtime;生命周期组件把结果挂到 - `app.state.host_runtime`。`app/api/context.py` 只向 Depends 暴露 Agent chat 的最小能力。 -- `get_agent_chat_service` 不再读取全局 `_ports` 或 `"agent_chat"` key;该 key 已从宿主和测试 - `ApiDataPorts.repositories` 删除。未迁移领域仍通过 `compatibility_api_data` 使用同一个实例。 -- fake Runtime 请求测试证明仓储与 UoW 共享同一请求会话,且无需加载真实 DB engine、 - PluginManager 或其他运行时服务;旧 `configure_api_data_ports()` 调用形态继续可用。 - -**收口记录(2026-08-22)**: - -- `HostRuntime` 已覆盖认证/用户/PassKey、消息、下载与整理历史、媒体服务器、站点、订阅、 - 工作流、请求 Session/UoW 和配置快照等全部正式 API 业务领域。每个能力均为命名字段, - 不再由 `repository("name")` 或 `transaction("name")` 在运行时猜测。 -- `app/api/dependencies/` 的正式领域模块已清除 `app.api.data` 与 - `app.api.dependencies.data` 依赖,并增加静态测试防止回退。旧 `ApiDataPorts` 只作为旧导入 - ABI 的全局转发保留,不再挂入 `HostRuntime`,也不参与正式 FastAPI 请求装配。 - -#### ARCH-231:按领域拆分 API dependency 与 presentation - -**目标**:`app/api/deps.py` 从 512 行集中装配点变成兼容聚合入口,端点只负责 HTTP 解析、鉴权依赖和结果映射。 - -**实施步骤**: - -1. 不做纯机械切文件;随 ARCH-221/222 的垂直用例迁移对应依赖。 -2. 每个依赖模块只组装本领域 command/query 和身份依赖。 -3. 协议特例(OpenAI/Anthropic/MCP/plugin raw)保留独立 presentation mapper,不进入通用 Response 逻辑猜测。 -4. 新端点原则上不超过 80 行;超过时必须在 PR 中说明流协议、资源清理或兼容原因。 -5. SSE 端点拆为请求校验、执行 service、event → wire mapper、disconnect/cancel 清理。 - -**优先切片**:`manual_transfer()`、`web_agent_stream()`、OpenAI/Anthropic streaming adapter。 - -**实施记录(2026-08-21)**: - -- `app/api/deps.py` 已由 524 行集中装配点收敛为 88 行兼容聚合入口;认证、Agent、订阅、站点、 - 工作流、历史和插件依赖分别由 `app/api/dependencies/` 下的领域模块拥有。宿主 API 端点全部改为 - 直接导入领域依赖,旧入口只为外部兼容消费者保留。 -- 新增 `app/api/presentation/sse.py`,统一 non-buffered SSE transport 策略,并分别提供 unnamed data - 与 named event wire mapper。WebAgent、OpenAI 和 Anthropic 继续保留各自协议 payload 与错误结构, - 不进入通用 `Response` 包装。 -- `manual_transfer()` 已缩为 HTTP/鉴权/依赖入口,历史恢复、批量预览和旧 `TransferChain` 参数兼容 - 由内部处理器承接;WebAgent 的拒绝响应、stream headers 与协议映射已从主控制流抽离。长生命周期 - generator 仍保留在端点模块,因为它直接拥有 request disconnect、后台 task cancel 与敏感结果关闭时序, - 后续只能在保持现有取消测试的前提下继续下沉。 -- 125 个鉴权、手动整理、WebAgent、OpenAI/Anthropic 生命周期、API 响应和 typed runtime 专项测试通过; - 61 个架构/基线 CLI 测试通过。依赖基线变化只反映集中边拆为领域边,runtime contract 变化只反映 - dependency callable 的新模块路径;禁止边与插件 raw API 均未变化。 - -#### ARCH-232:配置快照与窄配置端口 - -**目标**:阻止 `settings` 和 `SystemConfigOper()` 继续扩散,不要求一次清除 180 个文件。 - -**实施步骤**: - -1. 将 180/45 作为趋势基线,新增调用必须说明所属边界。 -2. `settings` 只保存启动环境/部署配置;Application 用例接收所需字段组成的 frozen config snapshot。 -3. 持久化用户配置使用 `SystemConfigReader/Writer` Protocol;默认实现包装 `SystemConfigOper`。 -4. 长生命周期 Module 在初始化/配置变更时接收配置快照,不在每个方法中全局读取。 -5. Agent tool 通过注入的设置服务读取可授权字段,不直接构造 Oper。 -6. 保留 `app.sdk.config.settings` 给旧插件;宿主 canonical 新代码不得因此继续扩大直接依赖。 - -**实施记录(2026-08-21)**: - -- 新增 `configuration-debt-baseline.json` 与单向 ratchet。基线排除 `app/plugins`、`app/sdk` 和 - `app/runtime/compat`,当前 canonical 宿主为 169 个直接导入 `settings` 的文件、15 个真实 - `app.db.oper.systemconfig.SystemConfigOper` 构造点;删除旧债务继续通过,新增或换位置均失败。 -- `SystemConfigReader` / `SystemConfigWriter` 已成为持久用户配置的窄端口;`SystemConfigService` - 支持分别注入 reader/writer,同时保留 `repository=` 兼容装配。Agent 系统设置查询与修改工具支持 - 显式注入授权配置端口,旧工具构造签名与密钥确认/脱敏行为不变。 -- 整理失败重试从 Application 直接读取全局 `settings` 改为 `TransferRetryConfig` frozen snapshot; - 启动组合根和测试组合根负责生成每次用例快照,reload 后新调用读取新 generation,旧调用不漂移。 -- Bangumi 模块作为长生命周期样板,在 `init_module()` / `on_config_changed()` 时更新不可变网络快照, - `test()` 不再逐次读取全局代理。该模式先验证后推广,不批量改写 169 个存量调用方。 -- 219 个架构、配置、Agent 安全、整理重试、Module reload 专项测试通过,Pylint 10/10;依赖基线 - 仅把 `app.application.history -> app.runtime.config` 替换为窄配置端口边,禁止边不变。 - -**扩展实施记录(2026-08-22)**: - -- `HostRuntime.configuration` 现在提供 API、Scheduler、Chain 三类 frozen snapshot 工厂。API 每个请求、 - Scheduler 每次初始化/任务注册都取得新快照,因此配置 reload 后的新调用可见新值,已经开始执行的调用 - 不会在中途漂移;Chain 基础文件后缀由启动上下文一次注入。 -- 登录、仪表板和整理历史 API 不再直接导入 `settings`;`Scheduler` 已清除全部直接 `settings` 访问, - 用户认证配置改走 `SystemConfigService`;`StorageChain` 的媒体后缀改走 Chain snapshot。canonical 配置债务 - 从 169/15 降到 164 个 settings import 文件/14 个 SystemConfigOper 构造点。 -- Chain snapshot 继续覆盖超级用户、共享识别、辅助认证、全局图片缓存、自动下载用户和资源页链接; - 消息、识别、交互、推荐和用户链的 5 个直接 `settings` 导入被移除,当前低水位进一步降到 - 161 个 settings import 文件,插件 SDK 与兼容入口未改。 -- API snapshot 继续覆盖 API token、临时目录、识别共享与订阅模式;Chain snapshot 覆盖站点请求、 - 代理、CookieCloud 黑名单和种子缓存配额。Agent/OpenAI/Anthropic/TMDB/种子缓存 API 以及 Site、 - Torrents Chain 共 7 个直接 `settings` 导入被移除,canonical 低水位降到 154 个文件;独立协议 - 测试显式注入快照,不再依赖 endpoint 模块中的全局配置别名。 -- 直接调用 endpoint 和显式构造 `ChainRuntimeContext` 的旧测试/兼容入口仍有 fallback;正式 FastAPI 与 - Startup 路径始终使用 HostRuntime 注入。插件 SDK 的 `app.sdk.config.settings`、动态 API 返回和事件字段未改。 -- 收尾批次把 API 与 Chain 余下直接配置读取全部迁入类型化 snapshot;Scheduler 继续保持为零。 - `HostRuntime` 新增可变部署设置服务,只供系统设置管理 API 使用,业务 API/Chain 只接收 frozen 字段。 - snapshot 构造集中到 `app/startup/composition/configuration.py`,生产启动与测试组合根复用同一映射,避免测试默认值 - 漂移。canonical `settings` 直接导入低水位从 154 降到 137,`SystemConfigOper()` 保持 14 个。 -- `ApiRuntimeConfig` 已覆盖搜索来源、媒体/字幕/音频后缀、重命名格式、WebPush、CookieCloud、根目录和 - 版本标识;`ChainRuntimeConfig` 覆盖搜索、下载、整理、刮削、AI、代理、缓存、链接、路径和 TMDB 图片域。 - 元数据缓存 TTL 使用动态 provider,在保留热更新语义的同时不再让 Chain 导入全局 settings。 - -### 阶段 4:把动态模块和事件变成可演进契约 - -#### ARCH-240:Module Contract V2 - -**目标**:兼容字符串分发,但让宿主高频能力具备签名、结果、并发和错误语义。 - -**建议契约字段**: - -```python -ModuleMethodSpec( - name="search_music", - family="music", - version=1, - input_contract="SearchMusicRequest", - result_contract="list[MediaInfo]", - aggregation="ordered_list_merge", - plugin_short_circuit=False, - execution="sync_or_async", - timeout_policy="caller_budget", - error_policy="isolate_provider", - public_to_plugins=True, -) -``` - -**实施步骤**: - -1. 先选择 20 个高价值能力:识别、搜索、下载、存储、消息发送、媒体服务器查询。 -2. 冻结当前参数、返回、排序、空值、异常、插件优先和聚合行为。 -3. 扩展 `ModuleMethodContract`,不能只记录 family。 -4. Module/Plugin 注册时检查 callable 和基础签名;第一阶段对旧插件不匹配只诊断,不拒绝加载。 -5. 宿主 Module 和新插件 SDK 提供 Protocol/DTO;`run_module()` 继续作为兼容执行器。 -6. 当一个能力全部宿主实现和官方插件均通过后,再把不匹配升级为宿主硬错误、第三方插件可读错误。 -7. 未登记的第三方自定义方法继续走 legacy,不得删除开放扩展能力。 - -**量化目标**:legacy 方法数从 96 开始只降不升;新增宿主调用必须先有显式 spec。 - -**实施记录(2026-08-21)**: - -- `ModuleMethodContract` 已升级为 V2,显式记录 version、input/result contract、aggregation、 - execution、timeout、error、plugin visibility 与基础签名要求;首批 22 个识别、搜索、媒体服务器、 - 存储、消息和调度/集成能力完成登记。 -- `run_module()` 与插件优先、短路、列表顺序合并、空值和异常隔离算法保持不变。Dispatcher 在真实 - provider 调用边界执行基础签名诊断;旧插件不匹配只写可读 warning,不拒绝加载或执行,未知自定义 - 方法继续使用 legacy contract。 -- runtime contract baseline 现包含稳定的 `module_method_specs`,后续字段或显式方法变化必须审查; - `ModuleCapability` Protocol 为宿主和新插件提供静态声明入口,但不替换字符串 dispatcher ABI。 -- 22 个显式方法进一步登记宿主真实传入的 required parameter 名称,覆盖识别、搜索、媒体服务器、存储、 - 消息收尾、命令注册和 webhook;dispatcher 仍只输出诊断 warning,不阻断缺少参数的旧插件或未知自定义方法。 -- 契约清单现覆盖静态扫描到的 211 个宿主字符串调用,并保留一个暂未被宿主调用的 `send_message` 公开能力, - 共 212 个显式 V2 spec。原先仅按 prefix 分类或落入默认 legacy 的宿主方法均获得稳定 family、输入合同、 - 结果合同、执行、超时和错误语义;未知第三方自定义方法仍走开放 legacy fallback,不拒绝加载或执行。 -- 未知动态方法在真实 provider 命中时记录 `module.contract.legacy_hit`,区分插件/宿主调用方和 ABI 来源; - 该指标只在 callable 实际存在并准备执行时递增,不改变未知第三方方法的开放 fallback、聚合或异常语义。 -- 2026-08-23 增加 `result_shape` 基础结果形状合同,首批覆盖存储列表、媒体服务器列表/剧集、播放 URL、 - 快照映射和无返回值能力。Dispatcher 在 provider 返回边界记录 - `module.contract.result_mismatch` 与期望形状;该阶段只观测和告警,不拒绝旧插件、不改写返回值, - 也不把业务对象类型强行导入动态调度器。未知第三方方法继续完全使用 legacy fallback。 -- 2026-08-24 将已登记的 `first_non_empty` 与 `ordered_list_merge` 接入同步、异步 dispatcher 的统一 - provider 决策函数,避免契约字段只存在于快照而运行时仍执行另一套隐式算法。未登记方法和仍声明 - `legacy` 的宿主/插件能力继续保留原签名接力、列表合并、异常隔离和短路行为;旧 provider 的签名或 - 结果偏差仍只诊断,不拒绝插件加载和执行。 -- 消息附件下载的 13 个宿主能力不再因 `download_*` 名称误归到下载器族,已按真实 `file/image/media` - 参数、bytes/string 结果和首个非空语义登记;`list_torrents`、`downloader_info` 按三个下载器宿主实现 - 冻结为有序列表合并。`torrent_files` 因 qBittorrent 的 `TorrentFilesList` 与其他下载器普通列表并存, - 当时只补真实参数与异构结果合同,继续显式保留 legacy 聚合;该临时状态已由下方 2026-08-24 的 - `DownloaderFile` 宿主投影取代。 -- `get_torrent_trackers` 新增有序映射聚合:未指定下载器时按宿主优先级合并 qBittorrent、Transmission、 - rTorrent 的名称到 Tracker 列表映射,不再由首个非空 dict 隐式截断;插件 provider 返回映射后仍按旧 ABI - 优先短路宿主,未知方法和其他 legacy 映射不受该策略影响。 -- qBittorrent、Transmission、rTorrent 共享的 `download`、删除、启停、标签与更新 6 个目标选择动作已冻结 - 一致参数和首个非空结果语义;bool/dict 结果启用基础形状诊断,tuple 下载结果保持业务合同标签而不强制 - Python 形状。当时广播型 `download_added` / `transfer_completed` 和 `filter_torrents` 继续保留 legacy; - 该临时状态已由下方 2026-08-24 的 `fan_out` 和有序列表契约取代。 -- 识别与搜索的 sync/async 方法现在复用同一个不可变契约对象,`async_recognize_media` 与 - `async_search_medias` 不再落入不同 family/aggregation;同步、异步 `obtain_images` 也统一登记为显式 - `pipeline_relay`,按宿主优先级把同一 `MediaInfo` 交给后续图片 provider。未知插件方法和 legacy 接力 - 仍使用原算法,插件先返回非空对象时继续优先短路宿主。 -- 2026-08-24 完成全部宿主观察方法的聚合收口:此前逐族登记的发现、元数据、存储、消息、识别、音乐和 - 下载器契约均切换到真实可执行语义;`filter_torrents` 按现有原参数调用与列表合并 ABI 登记,而不是误用 - 单参数 pipeline。7 个副作用 hook 新增 `fan_out` 并启用非短路执行;`torrent_files` 在三个内置下载器 - 边界统一投影为 `DownloaderFile`。最终 212 个 spec 的 legacy aggregation 为 0,未知第三方方法仍由 - `_DEFAULT_CONTRACT` 兼容,签名或结果差异只记录诊断。 - -#### ARCH-241:Event Contract Registry - -**目标**:为每个 EventType/ChainEventType 明确 payload、可见范围、投递和可靠性,不改变旧装饰器 API。 - -**建议字段**: - -- event type; -- payload model 或 legacy dict; -- broadcast / chain; -- host-only / plugin-public / target-plugin; -- sync/async handler 规则; -- ordering/priority; -- delivery:ephemeral / durable-required; -- error:isolate / stop-chain / notify; -- sensitive fields; -- producer/consumer owner。 - -**实施步骤**: - -1. 先登记已有 53 个事件,不要求同时补齐 53 个 model;legacy 项必须显式标记原因。 -2. 首批类型化配置、订阅、整理、下载、插件生命周期和 Agent usage 事件。 -3. 发送边界接受旧 dict,并转换/校验;插件收到的 dict 形状保持。 -4. 基线比较事件与 payload spec,不比较行号。 -5. 报告“宿主无 consumer”时区分插件公开事件、预留事件和真正死事件。 -6. `SystemError` 递归保护继续保留并补 contract;错误通知不能再次构造无限错误链。 - -**实施记录(2026-08-21)**: - -- 新增 `app/runtime/event/contracts.py`,53 个 `EventType` / `ChainEventType` 全量登记 payload、 - broadcast/chain、可见性、顺序、错误策略、敏感字段和 ephemeral/durable-required 语义;尚未模型化的 - 事件显式记录 legacy dict 原因,不把“未登记”当成兼容策略。 -- 首批 20 个已有 Pydantic payload 的配置、订阅、整理、资源、认证、插件和 Agent 事件绑定具体 model。 - `Event` 创建边界对 dict/model 做诊断校验,但继续投递原对象,因此插件 dict 形状和链式原地修改语义不变。 -- 订阅变更、下载添加、整理成功/失败等用户副作用标记为 `durable_required`,只表达完成语义要求; - 在 ARCH-251 pilot 完成前不虚构当前已具备持久投递。SystemError 仍沿用既有递归保护和异常通知路径。 -- runtime contract baseline 新增稳定 `event_specs`,后续 enum 新增必须同步登记,且不比较源码行号。 -- 53 个事件现已全部绑定 typed payload,原有 28 个 `legacy_dict` 项归零。插件动作/触发等开放事件使用 - “公共字段类型化 + `extra=allow`”模型,Webhook 与 Workflow execution 复用既有 DTO;验证仍只诊断并投递 - 同一个原始 dict/model,因此插件字段、对象引用和链式原地修改语义未改变。 - -#### ARCH-242:Module/Integration 质量清单 - -**目标**:借鉴 Home Assistant Integration Quality Scale,为 `app/modules` 建立可检查但渐进的质量视图。 - -**建议规则**: - -- 有 fake client 或录制 fixture; -- 零真实网络测试; -- sync/async 边界明确; -- 阻塞 I/O 不进入事件循环; -- 鉴权失效、限流、超时和离线语义明确; -- 并发上限/轮询间隔明确; -- 配置重载与 stop 可重复; -- Module Contract V2 覆盖; -- 敏感日志脱敏; -- 维护 owner 和豁免原因。 - -质量清单只约束新模块和被修改模块;历史模块以 `legacy`/`exempt + reason` 进入,不允许一次性阻断全部功能。 - -**实施记录(2026-08-21)**: - -- `app/runtime/extensions/module/quality.py` 提供十项统一规则、`legacy/assessed` 等级、owner、已验证 - 规则和精确豁免原因;所有存量模块均能生成有 owner/原因的 legacy 视图,不一次性阻断。 -- 本轮修改的 `bangumi` 首个进入 assessed:fake client、零真实网络、sync/async 边界、reload/stop、 - Contract V2、敏感日志和 owner 已登记;限流/并发继续复用通用 HTTP adapter 并明确豁免范围。 -- 详细规则和验收证据见 `docs/refactor/module-quality-scale.md`;自动测试阻止 profile 使用未登记规则, - 并要求今后修改模块时将对应 profile 纳入同一提交。 - -**收口记录(2026-08-22)**:39 个宿主 Module 已全部显式进入 assessed,不再以通用 fallback 把 -37 个模块标成“尚未审查”。所有模块共同由零真实网络、async 阻塞扫描、Module Contract V2 和 owner -四项机器门禁覆盖;能力专属的鉴权、限流、并发、敏感日志与 reload/stop 仍按 profile 精确豁免, -不会把 assessed 误读为十项满分。未知第三方模块继续使用 legacy 兼容视图,Module ABI 未变。 - -### 阶段 5:定义后台可靠性,不先引入分布式队列 - -#### ARCH-250:后台动作可靠性分类 ADR - -**目标**:先决定哪些动作允许丢失,哪些必须恢复,再选择实现。 - -分类建议: - -| 等级 | 例子 | 允许语义 | -| --- | --- | --- | -| E0 即时通知 | UI 进度、缓存刷新提示、非关键统计 | 进程内、允许丢失、错误记录 | -| E1 可重建任务 | 推荐缓存、站点数据刷新、市场刷新 | 幂等、定时重建、有限重试 | -| E2 用户动作后置副作用 | 订阅变更事件、下载提交后的历史/通知 | commit 后必须可重放或明确补偿 | -| E3 数据完成状态 | 文件整理、迁移、备份、恢复 | 持久任务记录、幂等步骤、崩溃恢复 | - -ADR 必须逐个映射当前 Event、BackgroundTasks、Scheduler job、Agent task 和 transfer pending,不允许笼统写“全部可靠”。 - -**实施记录(2026-08-21)**: - -- `docs/adr/0007-background-action-reliability.md` 已接受:逐项覆盖 53 个事件,并分别映射 - BackgroundTasks、Scheduler、Agent task 与 transfer pending 的 E0~E3、完成点、恢复、重试、 - 幂等、关停和失败表达。 -- ADR 明确区分“Registry 标记 durable-required”与“当前已经 durable”;ARCH-251 前仍如实保留 - commit 后进程崩溃窗口,不用日志或 BackgroundTasks 冒充交付保证。 -- 首个 pilot 选择 `SubscribeAdded`,因为 ARCH-221 已有事务所有权与 post-commit 样板;文件整理 - 继续保持 E3,不在本任务中被降格为普通事件重试。 - -**扩展实施记录(2026-08-23)**: - -- 新增 `app/runtime/tasks.py` 的 `TaskRegistry`,作为当前 lifespan 的进程内后台任务所有权边界; - 生命周期清单新增“后台任务登记器”组件,启动失败和正常关闭均会停止接收新任务、取消存量任务并 - 在有限等待窗口内收口,登记器不承担 durable queue 语义。 -- 插件 Release 后台刷新、WebAgent 断线后 Agent 执行以及消息展示快照保存均接入登记器,旧插件 API、 - SSE 协议和测试直接调用入口保持不变;未启动完整 ASGI lifespan 的旧调用继续使用兼容回退登记器。 -- Webhook E0 广播和站点 CookieCloud E1 手工调度已从 Starlette `BackgroundTasks` 迁入同一登记器; - 同步函数在线程池执行,关停时优先等待而非假设线程可强制取消。响应成功仍只表示本进程已接受, - 不能因为进程内任务已统一登记而宣称崩溃可恢复。 -- 订阅手工搜索、消息入口和 Seerr 订阅均已按 E0/E1 登记;其他关键业务副作用继续按等级逐项迁移, - 需要可靠交付的路径仍走 ARCH-251 的 Outbox/幂等切片,不扩大插件事件或 API payload。 -- 整理历史单条与批量 AI 重做分别登记为 `api.history.ai_redo` 和 - `api.history.ai_redo_batch`;请求响应、进度键、Agent prompt、输出回调与旧直接调用入口保持不变。 - 两类任务随 lifespan shutdown 取消并有限等待,但仍属于进程内 E1 工作,不宣称崩溃后自动恢复。 -- OpenAI Chat Completions 与 Anthropic Messages 的流式 Agent 执行分别登记为 - `api.openai.stream` 和 `api.anthropic.stream`。SSE payload、断线取消、临时会话清理和非流式入口保持 - 原语义;未启动完整 lifespan 的协议校验和旧直接调用通过兼容依赖回退到默认登记器。 -- IMDb 同步清缓存兼容入口在运行事件循环时改由 TaskRegistry 登记异步缓存清理任务,owner 为 - `module.imdb.cache_clear`;同步签名、模块调用方式和无事件循环时的立即清理语义保持不变。 -- Scheduler 的协程作业和异步进度收尾不再使用无主 `create_task` 或丢弃跨线程 Future;由 Scheduler 自有 - 句柄表登记并在停止时取消,completion 只在目标事件循环确认真实收尾后完成。关闭总预算由宿主生命周期 - 统一控制,保留旧同步 `Scheduler.start()` / `Scheduler.stop()` 与插件调度 ABI。 - -#### ARCH-251:用现有数据库做首个 durable side-effect pilot - -**前置**:ARCH-220/221 与 ARCH-241 完成。 - -**目标**:为一个 E2 用例消除“DB 已提交,但事件/上报尚未执行时进程崩溃”的窗口。 - -**实施要求**: - -1. 单独 ADR 决定 outbox 表或复用现有任务表;需要 schema 时必须新增 Alembic 迁移。 -2. 同一事务内写业务行和 outbox;提交后 dispatcher 执行。 -3. 记录 event key、payload version、attempt、next retry、last error、created/completed time。 -4. handler 必须按稳定 idempotency key 去重。 -5. 失败采用有上限指数退避,最终进入可诊断 dead-letter 状态,不无限刷日志。 -6. 插件事件 payload 仍按 V3 dict 发送;durability 是宿主内部实现,不改变 SDK。 -7. 先选订阅写入或整理完成中的一个用例,不建立万能消息总线。 - -**实施记录(2026-08-21)**: - -- 新增 `outboxmessage` 表与 Alembic revision `c7d9a1e4f2b6`。订阅新增行和 - `subscribe.added` version 1 intent 在同一 Session/UoW 中 stage/flush/commit;outbox 写失败会回滚 - 订阅。降级会删除未投递 intent,执行前必须确认 pending/dead 均已处理或备份。 -- event key 由订阅 ID、`media_source`、`media_id` 和 payload version 构成;即时事件 payload 同步 - 暴露 `idempotency_key`。正常 post-commit 编排全部完成后收口 intent;进程在 commit 后崩溃或回调 - 失败时,记录保持 pending,由恢复 dispatcher 重放。 -- SQLAlchemy adapter 使用 attempt 条件更新和 lease 做原子 claim;dispatcher 最多 5 次指数退避, - 错误截断后持久化,最终进入 `dead`。30 秒 Scheduler job 每批恢复最多 20 条,批次 Session 始终关闭。 -- pilot 只恢复 `SubscribeAdded` 事件;消息和外部统计仍执行旧 post-commit 编排,不能据此宣称所有订阅 - 副作用均 durable。后续 topic 必须另做幂等 handler 与崩溃窗口测试。 -- 66 个订阅/调度专项测试和 40 个数据库、迁移、Session/outbox 测试通过(1 个环境条件 skip); - fresh schema 先 create_all 再升级与重复迁移均保持幂等。 - -**扩展实施记录(2026-08-22)**: - -- 宿主自有的 `SubscribeModified`、`SubscribeDeleted` 生产路径已扩展到同一 outbox:订阅行更新/删除与 - version 1 intent 使用同一 `AsyncSession`、UoW 和 commit;即时广播失败时 intent 保持 pending,恢复 - dispatcher 分别按 `subscribe.modified`、`subscribe.deleted` topic 重放。 -- API、Agent 更新/删除工具以及按媒体身份批量删除均复用请求级或独占事务作用域。API 中保留的 - `event_published=False` 分支只服务测试替身和旧依赖注入,不是正式装配路径;正式 `HostRuntime` - 同时提供订阅 repository、history repository、transaction 与 outbox factory。 -- `SubscribeAddedEventData`、`SubscribeModifiedEventData`、`SubscribeDeletedEventData` 已进入 Event Contract; - 对插件仍投递原有 dict 字段,只新增可选 `idempotency_key`,不把 Pydantic 实例传给插件。 -- 保证边界只覆盖主仓可追踪的宿主生产者。运行时安装在 `app/plugins/**` 的第三方插件未被主仓改写; - 插件若自行直接发送同名事件,该发送仍由插件负责,无法与插件自己的数据库写入自动组成原子事务。 -- `SubscribeChain` 完成流程现由 `app/application/subscription/complete.py` 统一收口:订阅历史新增、 - 原订阅删除、`subscribe.complete` 事件 intent 与 `subscribe.complete.report` 统计 intent 在同一同步 - Session/UoW 中提交。提交后仍按通知、事件、统计的历史顺序执行;事件和统计分别按幂等键收口,任一步 - 失败都会留下独立 pending intent,由 outbox dispatcher 有限重试并最终进入 dead-letter。完成事件仍向插件 - 投递原有 `subscribe_id`、`subscribe_info`、`mediainfo` 字段,仅增加可选 `idempotency_key`。 -- 普通订阅新增/修改/删除路径的用户通知与第三方插件自行发送的事件仍不自动纳入宿主事务;本切片只覆盖 - 主仓可追踪的 `SubscribeChain` 完成生产者。 -- 2026-08-23 将主仓可追踪的订阅新增与完成用户通知冻结为已渲染 `Message` JSON 快照,并分别写入 - `subscribe.added.notification`、`subscribe.complete.notification` outbox intent。即时发送成功后按稳定 - 幂等键收口,崩溃或发送失败由现有 dispatcher 恢复;旧插件收到的事件字段和同步/异步入口保持不变。 - 第三方插件自行调用通知或自行写库的副作用仍不在宿主原子事务边界内。 -- `DownloadAdded`、`TransferComplete`、`TransferFailed` 也已逐项接入,而不是复用一个不分业务语义的 - “万能消息总线”。下载历史、下载文件清单或整理历史与各自 intent 在独占同步 Session/UoW 中原子提交; - 即时广播失败时 intent 保持 pending,三种恢复 handler 均继续使用有限重试与 dead-letter 策略。 -- 下载和整理事件保留插件原有运行时对象 ABI:即时发送仍含 `Context`、`FileItem`、`MetaInfo`、 - `MediaInfo`、`TransferInfo`;outbox 单独存 JSON 快照,恢复 handler 无远端调用地重建这些对象。 - `idempotency_key` 仍是唯一新增的可选公开字段,提醒插件按 at-least-once 语义自行去重。 -- 本切片同时把 `DownloadChain.download_single` 的提交后通知/任务编排抽成独立方法,并删除已经被 - Application 删除命令替代的两个 `Subscribe` Model 级删除事务装饰器;Model decorator 基线从 - 178 降到 176,Oper 内显式 commit/rollback 仍为 0。strict mypy 门禁新增 Chain durable context、 - payload 转换和启动适配器。 -- 下载失败冷却切片继续迁移到 `TransactionalDownloadFailureRepository`:Chain 每次读写使用独立短会话, - 写成功由显式 `SqlAlchemyUnitOfWork` commit,异常 rollback;`DownloadFailure` 查询和记录方法不再拥有 - 自动会话/提交装饰器。Model decorator 基线继续从 176 降到 174,Oper 内显式 commit/rollback 仍为 0。 -- Workflow 执行状态切片新增 `WorkflowExecutionCommand` 与短会话事务适配器;运行中、动作进度、成功、 - 失败和重置均由 Application command 显式 commit/rollback。`WorkflowOper()` 的旧方法名、参数和返回值 - 继续可用,无 Session 调用委托组合根服务,显式 Session 调用只暂存;同步 Model 自动提交装饰器移除 6 个, - 事务低水位从 174 降到 168,Oper 仍不创建 Session、也不直接 commit/rollback。 -- 剩余同步/异步 Model 写装饰器已全部迁移:AgentTask、PassKey、User、消息、历史清理、 - 站点快照、媒体服务器、插件数据、TransferPending 等写入由调用方 Session 和 UoW 收口;无 Session - 的旧 Oper ABI 委托 Startup 注入的短事务执行器。当前 Model 正式查询装饰器仅剩 30 个(同步 16、异步 14), - `db_update` 与 `async_db_update` 均为 0,Oper 自建 Session/直接提交仍为 0。 -- 数据清理按批次显式提交 UoW,单表失败先回滚会话再继续汇总后续表;不再依赖删除 Model 的隐式提交。 -- 收尾批次进一步移除宿主 Oper 对 `Base.create/update/delete/truncate` 隐式提交语义的依赖:显式 - Session 只 stage,由 Application UoW 提交;无 Session 的 Oper 入口委托 Startup 的短事务执行器。 - Base 方法最终改成纯显式 Session 原语,AST 门禁禁止 Model/Base 再引入装饰器或可选 Session。 - -**禁止**:本阶段不引入 Celery、Kafka、RabbitMQ 等新基础设施。 - -#### ARCH-252:Scheduler 拆成声明、执行和状态 - -**目标**:缩小 `Scheduler.init()`,统一重入、并发、超时、取消和进度语义。 - -**建议结构**: - -```text -app/application/scheduling/ - contract.py # JobSpec / trigger / overlap / retry / timeout - catalog.py # 业务 job 声明 - execution.py # 执行状态和幂等 -app/scheduler.py # APScheduler 兼容 Facade -``` - -**步骤**:先把 job 定义数据化,再提执行状态;不在第一步替换 APScheduler。每个 job 必须声明 overlap policy、timeout、manual、recovery 和 owner。 - -**实施记录(2026-08-21)**: - -- `app.application.scheduling` 新增 `JobSpec`、`JobCatalog`、`JobExecutionState` 以及 overlap/recovery 枚举; - 系统、媒体服务器、Agent、工作流、插件和 outbox 动态任务均由同一合同生成兼容运行状态。 -- 保留 APScheduler 和既有 `Scheduler` Facade;重入判断、开始/结束/失败状态统一由 execution state 收敛, - job 状态稳定暴露 owner、overlap、timeout、manual、recovery 五项策略。 -- coroutine job 的非空 timeout 使用 `asyncio.wait_for`,超时会取消底层任务并记录明确终态;同步 job 默认 - `timeout=None`,避免用线程强杀制造不可控的半完成副作用。一次性 Agent 任务重启后保持 manual-only,durable - outbox/备份/整理与 next-schedule 任务的恢复语义可审计。 -- 61 个 Scheduler、Agent 定时任务、备份、进度和媒体服务器专项测试通过,覆盖重复 ID、overlap skip、 - timeout cancel、restart/manual recovery 与兼容状态字段。 - -### 阶段 6:可观测性、类型和复杂度预算 - -#### ARCH-260:统一 request/correlation ID - -**目标**:一个请求进入后,API、Application、Chain、Module dispatcher、Event 和 `RequestUtils` 日志能够使用同一个关联 ID。 - -**实施步骤**: - -1. 中间件接受合法 `X-Request-ID`,否则生成;限制长度和字符集,防止日志注入。 -2. 使用 ContextVar 保存;线程池/异步 task 必须验证上下文传播,跨进程任务写入 payload。 -3. 响应回写 `X-Request-ID`;SSE 在握手响应和错误事件中保持同一 ID。 -4. 日志 formatter 增加结构字段,不在消息字符串中到处手拼。 -5. 外部请求可传标准 trace headers 或项目 correlation header,但不得泄露用户 token。 - -**实施记录(2026-08-21)**: - -- 新增受 64 字符安全字符集约束的 `moviepilot_correlation_id` ContextVar 和纯 ASGI middleware;合法 - `X-Request-ID` 原样使用,非法值重新生成,`request.state`、普通响应和 SSE 握手响应回写同一个 ID。 -- 平台日志 formatter 以独立 `correlation_id` 字段输出;`app.runtime.execution`、共享 `ThreadHelper`、 - Event 生产/消费均显式复制或恢复上下文。Event 在生产时固化 ID,广播线程不能用自己的空上下文覆盖它。 -- Scheduler 多进程入口把关联 ID 作为显式可序列化参数传入,不依赖 fork 继承;`RequestUtils` 和 - `AsyncRequestUtils` 在调用方未指定时传播 `X-Request-ID`,不读取或复制任何鉴权 token。 -- 并发请求、非法头、线程池、事件处理、SSE、同步/异步外呼和显式外呼头覆盖均有专项测试;原 API - 响应、健康探针、日志和搜索流式测试保持通过。 - -#### ARCH-261:指标与可选 OpenTelemetry Adapter - -先定义内部观测端口和低基数指标: - -- HTTP route/status/latency; -- DB pool wait/checked-out/timeout; -- Event queue depth、handler latency/error; -- Module provider latency/error/timeout; -- Scheduler job duration、overlap skip、retry/dead-letter; -- Plugin load/reload/settling duration; -- Agent active task、cancel、provider latency、token usage。 - -OTel 初始化只能位于 Startup/Adapter;Domain/Application 只依赖 no-op-capable observation Protocol。插件 ID、用户 ID、媒体标题等高基数字段不得直接作为 metric label。 - -**实施记录(2026-08-21)**: - -- `app.runtime.observability` 定义单一 `ObservationPort`、默认 no-op、指标类型/目录、标签白名单和统一耗时 - 作用域;没有 exporter 时所有调用仍可执行,未登记标签在进入 Adapter 前直接拒绝。 -- 指标目录覆盖 HTTP、DB pool、Event、Module、Scheduler、Plugin lifecycle 和 Agent 所列能力;标签审计 - 明确禁止 user/plugin/media/request/job 实例 ID、标题和 URL。首批实际接线覆盖 HTTP route/status/latency、 - Event queue/handler、Module provider 与 Scheduler duration/overlap,剩余能力可按相同端口逐个接入。 -- `app.adapters.observability.otel` 只在组合根显式读取 `MOVIEPILOT_OTEL_METRICS=1` 后懒加载 OTel API; - 未安装可选包时稳定回退 no-op,不给核心层增加 SDK 依赖。HTTP Adapter 通过路由匹配输出模板,绝不以原始 - request path 充当 label。 -- 2026-08-22 扩展接线覆盖 SQLAlchemy checkout/checkin、异步回退配额 wait/timeout、Module 真实 - `TimeoutError`、插件 start/initialize/stop/reload,以及 Agent 活跃任务、取消结果、供应商耗时和输入/ - 输出 token。自定义 Agent provider 统一归类为 `custom`,不会暴露配置名称。 -- Outbox dispatcher 的有限重试和 dead-letter 已分别接入 `scheduler.job.retry` 与 - `scheduler.job.dead_letter`,只使用固定 `owner=outbox` 低基数标签;观测失败端口由 Startup 注入, - Application 不依赖具体 OTel SDK。 -- 2026-08-23 增加 `compat.facade.hit` 低基数指标和 `observe_compat_facade()` 装饰器,覆盖 - `PluginManager`、`PluginHelper`、`MoviePilotServerHelper` 三个正式 V3 ABI 入口。装饰器保留同步/ - 异步 descriptor、原方法签名和对象身份,并同时记录公开方法与旧私有方法的命中,标签仅包含 - facade、稳定方法名、可见性和固定 ABI 来源,不包含插件/用户/媒体实例数据。专项测试验证三类 - Facade 的公开与私有命中,后续可按真实命中量和行为快照逐项内移算法。 -- 专项测试覆盖 exporter 缺失、非法标签、全目录高基数审计、成功/失败 outcome、动态 URL 路由模板; - 既有 API、Event、Module、Scheduler 与健康探针回归保持通过。 - -#### ARCH-270:渐进式类型门禁 - -**目标**:不要求全仓一次通过 mypy/pyright;只保证新 canonical contract 和被治理模块完整类型化。 - -**实施步骤**: - -1. 选定一个类型检查器并写入 dev dependency/lock;不要同时引入两套。 -2. 首批严格目录: - - `app/domain/` - - 新增 Application command/port - - `app/runtime/event/` - - `app/runtime/extensions/module/contracts.py` - - `app/startup/composition/context.py` / `app/api/context.py` -3. 对第三方移植包、旧插件 Facade 和动态 SDK 设置精确豁免,不允许 `app.* = ignore_errors`。 -4. CI 先检查严格目录;每次迁移扩大 include 范围。 -5. 类型错误不能用无界 `Any`、`cast(Any, ...)` 或全文件 ignore 消音。 - -**实施记录(2026-08-21)**: - -- 选定 mypy 1.18.x 并写入 `pyproject.toml`/`uv.lock`,仓库只保留这一套新增类型门禁;CI architecture job - 使用锁定环境运行 `mypy --config-file mypy.ini`。 -- 首批 strict 清单包含 4 个已满足合同的 Domain value 文件、correlation/observation、Event Contract V2、 - Module Contract V2、typed HostRuntime startup context 和 API context,共 10 个 canonical 文件。 -- `mypy.ini` 不包含 `app.* = ignore_errors`、全文件 ignore、无界 `Any` 或 `cast(Any, ...)` 消音;历史 - `domain/meta` 动态模型只有在逐文件修正后才可加入清单,当前错误不能被 baseline 当作“已通过”。 -- 配置约束测试会检查 strict、关键合同文件和至少一个 Domain 文件均在清单中,并实际启动锁定版本 mypy; - 当前 10 个源文件零错误通过。 - -**扩展实施记录(2026-08-22)**:mypy 目标运行时更新到 Python 3.14,严格清单扩大到 20 个源文件; -新增纳管配置快照和下载失败事务适配器,仍保持零错误、无全局 ignore。 - -Workflow 执行状态 UoW 切片将 `app/application/workflow.py` 与 `app/db/adapters/workflow.py` 纳入 strict 清单, -治理范围扩大到 22 个源文件;事务命令、仓储 Protocol 和短会话适配器保持零错误。 - -异步安全与契约收口继续纳管 scheduling facade、Event error policy、Module dispatcher 和 async blocking -scanner,strict 清单扩大到 26 个源文件;已登记范围保持零错误,未使用全文件 ignore 或 `cast(Any, ...)`。 - -收尾批次继续纳管 Startup 配置快照、Module quality、Compat manifest/diagnostics、插件运行时窄端口、 -Outbox adapter、DB 装饰器、Base 与 UoW,strict 清单扩大到 37 个源文件并保持零错误。 - -#### ARCH-271:复杂度和端点预算 ratchet - -**目标**:阻止大方法继续增长,并让拆分对应真实阶段,而不是机械 helper 化。 - -**初始规则**: - -- 新 API endpoint 原则上 ≤ 80 行; -- 新 Application command/query 方法原则上 ≤ 150 行; -- 新 Chain public use-case 方法原则上 ≤ 150 行; -- 既有超限方法进入 baseline,只允许不增; -- 修改超限方法时,PR 必须列出阶段划分、共享状态和回归测试。 - -优先拆分对象:`do_transfer`、`batch_download`、`SubscribeChain.match`、`web_agent_stream`、`Scheduler.init`。先提取 phase object/DTO/port,再缩短入口;不创建一批互相读写同一个大 dict 的私有函数来“达标”。 - -**实施记录(2026-08-21)**: - -- 新增 `scripts/architecture/complexity.py`,通过 AST 只统计 API HTTP endpoint、Application public method - 和 Chain public use-case,预算分别为 80/150/150 行;嵌套 helper 不会被机械重复计数。 -- `complexity-baseline.json` 只保存当前超限入口和行数,不把达标方法写成永久快照。check 允许缩短、达标或删除, - 精确拒绝既有超限增长和任何新增超限;CI architecture job 每次执行。 -- 当前债务清单明确包含 `web_agent_stream`、`batch_download`、`SubscribeChain.match`、`do_transfer`; - `Scheduler.init` 已在 ARCH-252 通过 JobSpec/catalog 拆分退出超限清单,调度专项测试是该代表性拆分的回归证据。 -- 单元测试覆盖删除/缩短放行和增长/新增拒绝,当前仓库 baseline check 通过。 -- 2026-08-22 将 MCP JSON-RPC 分派、无媒体信息下载识别、缺集结果合并拆成具有独立输入/输出的私有阶段; - 对应 `mcp_jsonrpc`、`download.add`、`DownloadChain.get_no_exists_info` 退出超限清单,总债务从 28 降到 25。 -- 2026-08-22 将 `SiteChain.sync_cookies` 拆为单域名处理、黑名单判断、索引器地址解析和连接重试阶段,入口降至预算内; - 保留已有站点健康、黑名单、失败重试时的事件与进度回调语义,站点专项测试通过。 -- 继续将 `TorrentsChain.refresh` 拆为单站点抓取、上下文构造和缓存写入阶段,入口退出超限清单; - 音乐双缓存、去重、停止信号和订阅匹配专项测试通过,当前复杂度债务由 25 项降至 21 项。 - -配置债务继续按模块族收敛:`app/application/image.py` 的壁纸模式、图片缓存、代理和安全后缀读取已接入 -`ChainRuntimeConfig`,canonical `settings` 直接读取文件数从 137 降至 136;配置/依赖基线已更新,壁纸与图片专项测试通过。 -随后将 `app/application/torrent.py` 的代理和媒体后缀读取迁移到同一快照,canonical 配置债务进一步降至 134 个文件; -下载/种子专项测试与架构门禁通过。 -`app/application/rss.py` 的代理和编码检测选项也已迁移到快照,配置债务降至 133 个文件;RSS、Rust 解析和音乐资源专项测试通过。 -数据维护策略随后接入同一快照,`app/application/maintenance.py` 的直接配置读取移除,债务降至 132 个文件; -清理服务与 Chain 专项测试通过。 -Passkey 的 APP_DOMAIN、NGINX_PORT 和用户验证要求也已接入 API 配置快照,配置债务降至 131 个文件; -MFA/Passkey 专项测试与架构门禁通过,密钥类配置仍保留在安全端口范围内。 -认证服务的超级用户、向导开关和访问令牌过期时间也改用配置快照,直接 `settings` 读取债务降至 130 个文件;启动组合根的 `SystemConfigOper()` 构造点进一步由 14 降至 1(唯一保留点为创建 `SystemConfigService` 本身)。 -鉴权与 MFA 专项测试通过。 -`DownloadChain.download_single` 的下载成功结算已提取为独立阶段,入口从 255 行降至 167 行; -历史、文件明细、durable intent、post-commit 通知和旧测试 fallback 语义保持,下载专项测试通过。 -`SubscribeChain.add/async_add` 的同步/异步重复编排随后收口到显式创建上下文和阶段方法:输入规范化、媒体识别、 -电视剧集数准备、默认字段/图片处理、事务提交和失败反馈分别拥有明确边界;订阅重复检测、owner scope、 -`SubscribeAdded` payload、outbox stage/commit/post-commit 顺序仍由既有 `application/subscription/write.py` 负责。 -两个公开入口均降至 150 行预算内,复杂度基线移除对应债务项;订阅识别、音乐订阅、写入事务和搜索来源专项 -共 280 项测试通过,架构、复杂度与异步阻塞门禁通过。 - -2026-08-23 将工作流动作 `FetchRssAction`、`ScanFileAction` 和 `AddSubscribeAction` 接入 `ChainRuntimeConfig` 快照,分别移除代理、媒体后缀和超级用户的全局 `settings` 读取;保留动作公开入口与工作流上下文行为,新增快照注入测试覆盖。配置债务由 130 个文件降至 127 个文件,宿主依赖与配置基线已更新。 -2026-08-23 将工作流动作 `FetchMediasAction` 和 `SendMessageAction` 接入 `ChainRuntimeConfig` 快照,分别移除内部 API 端口/令牌及工作流链接的全局 `settings` 读取;保留动作公开入口与消息载荷行为,新增快照注入测试覆盖。配置债务由 127 个文件降至 125 个文件,宿主依赖与配置基线已更新。 -2026-08-23 将 API 路由前缀作为组合根参数传入 `init_routers`,移除路由初始化模块对全局 `settings` 的直接读取;默认参数保留旧调用兼容性,并补充自定义前缀测试。配置债务由 125 个文件降至 124 个文件。 -2026-08-23 将令牌编解码的密钥与过期策略接入 `TokenRuntimeConfig` 快照;启动组合根统一装配,未装配时保留 SDK/旧插件的动态回退,公开令牌函数签名不变。配置债务由 124 个文件降至 123 个文件,并补充资源/认证令牌回归测试。 -2026-08-23 将 URL 资源签名改为复用 `TokenRuntimeConfig` 的资源密钥快照;未装配时保留旧模块动态回退,签名公开 API 与密钥轮换语义不变。配置债务由 123 个文件降至 122 个文件,并补充安全 URL、媒体服务器和字幕下载回归测试。 -2026-08-23 将 `AgentRuntimeManager` 的默认 Agent 目录改为通过 `RuntimeSettingsService` 读取配置快照;显式目录参数和导入早期的旧设置回退保持不变。配置债务由 122 个文件降至 121 个文件,并补充默认目录注入回归测试。 -2026-08-23 将 `OcrHelper` 的服务地址改为通过 `RuntimeSettingsService` 或显式构造参数取得;导入早期保留旧模块回退,自动补齐 `/captcha/base64` 路径。配置债务由 121 个文件降至 120 个文件,并补充 OCR 地址注入回归测试。 -2026-08-23 将 `CookieCloudHelper` 的五项运行配置改为通过 `RuntimeSettingsService` 读取;同步期间仍获取最新值,未装配时保留旧 Settings ABI。配置债务由 120 个文件降至 119 个文件,并通过 CookieCloud 路由与站点回归测试。 -2026-08-23 将 DoH 开关、域名和解析器配置改为通过 `RuntimeSettingsService` 动态读取;socket 补丁、热更新、缓存和线程池关闭语义保持不变,未装配时保留旧 Settings ABI。配置债务由 119 个文件降至 118 个文件,并通过 DoH 与生命周期回归测试。 -2026-08-23 将 Rust 加速开关改为通过 `RuntimeSettingsService` 动态读取;扩展可用性、异常回退和公开适配器 API 保持不变,未装配时保留旧 Settings ABI。配置债务由 118 个文件降至 117 个文件,并通过 Rust 解析与开关回归测试。 -2026-08-23 将目录监控的快照、整理分发、系统限制、监控门面和本地 watcher 配置读取统一改为 -`app.runtime.settings.get_runtime_setting()`;保留未装配时的旧 Settings 回退和热更新读取语义,监控专项 -87 项测试、Pylint 与架构基线通过。配置债务由 117 个文件降至 112 个文件。 -随后将插件依赖扫描、插件包事务和 V3 资源安装适配器的部署配置读取迁移到同一 runtime 端口;资源适配器 -保留模块级 `settings` 兼容入口供旧插件覆盖,实际逻辑动态读取 runtime 配置。插件/资源专项 141 项测试、 -Pylint 与架构基线通过,配置债务由 112 个文件降至 109 个文件。 -缓存 Redis 连接池、内存限制和文件缓存工厂随后改用 runtime 配置端口,保留旧模块级 Settings 覆盖入口; -缓存专项 41 项测试与 Pylint 通过,配置债务由 109 个文件降至 107 个文件。 -同时为兼容市场、服务端和插件包适配器增加 `RuntimeSettingsCompat` 动态代理:组合根已装配时读取 -runtime provider,旧插件或测试替换模块级 `settings` 时仍保持原覆盖语义。相关插件市场、插件本地同步、 -服务端和评分专项 184 项测试与 Pylint 通过,配置债务由 107 个文件降至 105 个文件。 -用户模型的 `get_by_name` 与 `get_by_id` 同步查询改为显式 Session 执行,并以一次性短会话保留旧插件 -无 Session ABI;用户查询与兼容专项 75 项测试、Pylint 及架构基线通过,查询装饰器由 119 个降至 117 个。 -随后将 Agent 系统设置查询/更新工具切换到已装配的 `RuntimeSettingsService` 窄端口,保留工具构造和 -设置更新返回 ABI;配置债务由 103 个文件降至 101 个文件,系统设置工具专项测试与架构基线通过。 -站点图标、站点统计和用户配置的只读 Model 方法随后改为显式 Session 执行,异步无 Session 旧 ABI 由 -一次性兼容查询会话承接;查询装饰器由 117 个降至 112 个,站点查询专项测试与架构基线通过。 -插件数据的六个只读入口也改为显式 Session/AsyncSession,旧插件无会话读取通过一次性兼容查询会话保留; -查询装饰器由 112 个降至 106 个,插件数据与事务专项测试及架构基线通过。 -Agent 能力适配器、记忆和提示词模块改用 `RuntimeSettingsCompat` 动态运行时端口,保留模块级覆盖和 -导入早期回退;配置债务由 101 个文件降至 98 个,Agent 能力、提示词与运行时专项测试通过。 -随后将 LLM capability/helper/provider、Agent orchestrator 和工具基础类统一切换到同一动态运行时端口; -配置债务由 98 个文件降至 93 个,LLM、Agent 生命周期与工具专项测试通过。 -技能注册表、插件工具辅助、终端会话和语音工具也已切换到动态运行时端口,技能市场写入改走配置服务; -配置债务由 93 个文件降至 89 个,技能、工具和安全专项测试通过。 -运行时状态、线程池、插件目录/管理器和模块管理器随后切换到同一动态运行时端口,保留旧模块覆盖语义; -配置债务由 89 个文件降至 84 个,插件、模块生命周期与线程安全专项测试通过。 -Agent 下载、任务、媒体识别、刮削和 Web 搜索工具也切换到动态运行时端口,保留原工具输入与输出 ABI; -配置债务由 84 个文件降至 77 个,Agent 任务、搜索与识别专项测试通过。 -AcoustID、AniList、Bangumi、Douban、Fanart 和 IMDb 模块族改用动态运行时端口,模块公开类与配置热读保持; -配置债务由 77 个文件降至 67 个,相关识别、浏览与刮削专项测试通过。 -MusicBrainz、ListenBrainz、LrcLib 与 TheAudioDB 模块族也完成同样迁移,配置债务由 67 个文件降至 62 个, -音乐元数据专项测试通过。 -Emby、Jellyfin、Feishu、Discord 与 Slack 模块切换到动态运行时配置,模块测试与消息生命周期 ABI 保持; -配置债务由 62 个文件降至 57 个,媒体服务器和消息模块专项测试通过。 -文件管理器及 Alist、Rclone、U115、SMB、Alipan 存储实现切换到动态运行时配置,保留模块级 global_vars 与 -存储公开方法;配置债务由 57 个文件降至 50 个,文件管理与存储专项测试通过。 -Indexer parser/spider 模块族切换到动态运行时配置,站点搜索 URL、解析与分类行为保持;配置债务由 50 个文件 -降至 39 个,Indexer 专项测试通过。 -QQ、Telegram、WeChat、WeChatClawBot 与字幕模块切换到动态运行时配置,消息回调、代理和字幕下载行为保持; -配置债务由 39 个文件降至 34 个,消息与字幕专项测试通过。 -TMDB、TVDB 与 TriMedia 模块切换到动态运行时配置,缓存、重试、媒体源和登录兼容行为保持;配置债务由 34 个 -文件降至 24 个,TMDB、媒体源与 TriMedia 专项测试通过。 -Doctor、factory、main 与 CLI 的部署读取切换到动态运行时端口,CLI 仍保留 `Settings` 类型清单和旧命令 ABI; -配置债务由 24 个文件降至 19 个,Doctor、启动、CLI 与数据库迁移专项测试通过。 -FS proxy、Local/WebPush 存储适配以及 Module host adapter 切换到动态运行时端口,保留 global_vars、能力 -快照与模块契约行为;配置债务由 19 个文件降至 15 个,文件存储、WebPush 与 Module contract 专项测试通过。 -启动 Agent、数据库、领域、生命周期和插件初始化模块切换到动态运行时端口;组合根 provider 明确绑定原始 -Settings,避免代理自递归,配置债务由 15 个文件降至 8 个。数据库引擎/Session 与下载器模块保留底层 -Settings 读取作为基础设施边界,架构基线已明确记录该例外,启动与架构专项测试通过。 - -2026-08-24 将 configuration baseline 升级为 schema v2:待整改的直接 Settings 与 Oper 构造均清零; -数据库模型/引擎/Session 的 3 个启动前读取和 startup 的唯一 Oper 构造点进入带理由的批准边界。ratchet -同时冻结债务与批准边界,批准项不能扩张,因此不会通过新增“例外”掩盖配置回流。 - -同日修正适配器配置下沉边界:OCR、CookieCloud、DoH、Rust 和资源签名等低层实现不再直接依赖 -`app.application`,由 `app.runtime.settings` 端口承接组合根注入;未启动装配时仍回退旧 Settings ABI, -架构依赖专项和官方插件语义观察均通过。 - -**收口记录(2026-08-22)**:`reidentify_cache`、`nettest`、`scrape`、OpenAI `chat_completions/responses`、`get_logging` 和 Web Agent SSE 均改为稳定公开入口委托私有编排实现;四个消息交互 Handler 的公开方法也保留 ABI 并委托私有状态机。复杂度基线已清零,API/Application/Chain 入口预算、异步阻塞 ratchet 均通过;复杂度及兼容专项合计 252 项测试通过。 -随后将 `TransferChain.do_transfer` 的公开入口收口为稳定兼容 Facade,先提取媒体身份规范化阶段,保留显式 -`media_source/media_id` 校验、识别失败文案和所有原有调用参数;整理专项 80 项测试通过,复杂度基线移除该入口, -后续继续拆分其批次规划与执行阶段。 -2026-08-22 继续完成入口垂直切片:`DownloadChain.download_single`、`SubscribeChain.search` 和 -`SubscribeChain.match` 均改为稳定兼容 Facade,分别委托下载执行、搜索执行、资源预处理和订阅匹配阶段; -保留原参数、对象类型、锁、进度回调、停止信号、候选过滤、失败冷却日志和下载结算语义。 -`MediaServerChain.sync` 补回停止信号后的立即退出,避免系统停止后继续发送服务器/全局完成进度。 -下载、订阅、媒体服务器及 durable/outbox 专项共 370 项测试通过,复杂度基线移除上述三个订阅/下载入口。 -该阶段当时尚未把普通用户通知和 MoviePilot Server 外部统计标记为 durable;后续仅将宿主可追踪的 -`SubscribeAdded` 与订阅完成通知/统计同业务写入一起登记为独立 outbox intent。通用自定义通知、非订阅类 -MoviePilot Server 调用及第三方插件自行写库、通知或上报仍不在宿主事务边界内,不能从订阅切片外推为 -全部副作用都已 durable。 - -2026-08-23 收口兼容回归:`RuntimeSettingsCompat` 补齐 `update_setting`、`update_settings` 和 -`model_dump` 旧 Settings ABI,并由应用组合根注入服务对象,低层 runtime 不再反向导入 `app.application`; -`SkillHelper` 的技能市场写入继续经过兼容代理,旧插件/测试的模块级替换语义保持。`UserConfigOper` 的 -无 Session 查询由组合根事务执行器创建一次性会话,显式 Session 仍由调用方持有。配置债务稳定为 -8 个文件;Model 查询装饰器在消息、用户和订阅查询切片后曾降至 75 个,随后已全部清零。该阶段 -四分片全量测试 `5492 passed, 3 skipped`,mypy、复杂度、异步阻塞、 -host/plugin 架构基线均通过。 - -2026-08-23 分阶段完成下载/整理历史、Workflow、MediaServer、SiteUserData、AgentChat、AgentTaskRun、 -TransferPending、SystemConfig、PassKey 与 SubscribeHistory 查询切片:宿主 Oper 统一通过 -`_execute_sync_query` / `_execute_async_query` 复用调用方 Session,正式 Model 查询装饰器由 75 逐步降至 -0。各阶段显式 Session 查询、过滤语义、架构基线和全量测试均有回归记录。 - -2026-08-23 在正式装饰器清零后继续删除过渡性的 `legacy_db_query`、`legacy_async_db_query`、 -`legacy_db_update`、`legacy_async_db_update`:Base 与全部 Model 只接受显式 Session,不再替无会话调用 -创建或提交事务。原先把 `self._db=None` 直传 Model 的 User、PluginData、Subscribe、Site、配置和下载失败 -Oper 已迁到 `_execute_*`,插件 SDK 也移除了 User、Subscribe、TransferHistory Model 导出。架构测试新增 -三项硬约束:Model/Base 不得导入 DB 装饰器、`db` 参数不得可选、插件 SDK 不得导入 `app.db.models`。 - -#### ARCH-272:异步阻塞检测 - -**目标**:对新 API/Agent/Application async 路径检测 `open`、文件遍历、同步 HTTP、阻塞 sleep 和重 CPU 解析。 - -**步骤**: - -1. 开发/测试启用 asyncio debug 和慢 callback 诊断; -2. 为已知同步 I/O adapter 提供统一 `run_in_threadpool` 入口; -3. 添加针对改动模块的阻塞调用测试/AST 规则; -4. 同步 Module 由 dispatcher 线程池兼容,不要求第三方插件立刻 async 化; -5. 只在测量证明有收益时改用 async 第三方 client。 - -**实施记录(2026-08-21)**: - -- `scripts/architecture/async_blocking.py` 扫描 canonical 主程序目录和顶层运行入口中的 async 函数,覆盖 - 同步 HTTP、Oper、Path、`shutil`、`subprocess`、`os`、`time.sleep` 与 `open`。 -- scanner 按 import 来源、局部别名、互斥分支和嵌套函数定义点解析符号;`AsyncRequestUtils`、 - `anyio.Path`、延迟回调及受控 worker 内执行的同步函数不记为 async 直接阻塞。函数和 lambda 的默认值、 - decorator 等定义时表达式仍在所在 async 执行体中检查。 -- baseline 只允许调用减少或删除,新增调用及次数增长均使 CI architecture job 失败;当前记录 10 条已确认 - 存量,包括 8 条文件元数据访问、1 条目录删除和 1 条同步 Oper 读取,由后续数据库与文件 adapter 叶迁移。 -- pytest 全局启用 `asyncio_debug`,专项测试验证实际 loop debug 状态;AST ratchet 与 46 个 Agent 流式回归 - 通过。同步第三方 Module 仍由 dispatcher 的 `app.runtime.execution.run_in_threadpool` 兼容。 - -## 6. 推荐执行队列 - -下表是默认的提交顺序,不表示所有任务必须由同一个 AI 连续完成。一个 AI 一次只领取一行;如果发现前置条件未满足,应停止实施并回报证据,不得顺手扩大范围。 - -| 顺序 | 任务 | 前置 | 主要产物 | 风险 | 最小验证 | -| ---: | --- | --- | --- | --- | --- | -| 1 | ARCH-201 基线 CLI 分域 | 无 | host/plugin/performance 的独立 check/write | 低 | CLI 测试 + 工作树不变断言 | -| 2 | ARCH-202 语义与位置分离 | ARCH-201 | 稳定语义 fixture + 诊断报告 | 低 | 架构 fixture 精确 diff | -| 3 | ARCH-203 CI 分层 | ARCH-201 | PR 快门禁、跨仓观察 job | 低 | 本地复现 workflow 命令 | -| 4 | ARCH-210 单 worker 约束 | 无 | 配置校验、启动错误文案、部署文档 | 中 | worker=1/2 启动测试 | -| 5 | ARCH-211 factory/reload | ARCH-210 | import-string/factory 入口 | 中 | 实际启动、reload smoke、信号关停 | -| 6 | ARCH-212 DB 准备与健康 | ARCH-211 | 唯一 DB prepare 入口、live/ready | 中 | 迁移失败/DB 断开/安全模式测试 | -| 7 | ARCH-220 事务 ratchet | 无 | 禁止新 Model 自提交的门禁 | 中 | DB decorator 与 Session 生命周期测试 | -| 8 | ARCH-221 订阅完整切片 | ARCH-220 | 订阅 command + UoW + post-commit | 高 | SQLite/PostgreSQL 语义测试、事件次数 | -| 9 | ARCH-222 其余写切片 | ARCH-221 | 迁移批次,不是一次全仓改写 | 高 | 每个业务切片独立回归 | -| 10 | ARCH-230 Typed HostRuntime | ARCH-211 | typed state、兼容 provider | 高 | 生命周期顺序、重复启动/关停测试 | -| 11 | ARCH-231 API 依赖拆分 | ARCH-230 | 领域 dependency/presentation | 中 | OpenAPI 快照、鉴权、SSE/流式响应 | -| 12 | ARCH-232 配置快照/端口 | ARCH-230 | 窄配置对象与 reload 订阅 | 中 | reload 前后行为、敏感配置测试 | -| 13 | ARCH-240 Module Contract V2 | ARCH-201 | 高频方法签名/结果/错误协议 | 高 | 宿主 + 官方插件 dispatcher 测试 | -| 14 | ARCH-241 Event Registry | ARCH-201 | EventType 到 payload/reliability 映射 | 高 | producer/consumer 静态和运行测试 | -| 15 | ARCH-242 Module 质量清单 | ARCH-240 | 能力族分级与豁免清单 | 中 | 选定模块族验收 | -| 16 | ARCH-250 可靠性 ADR | ARCH-241 | E0-E3 分类和完成语义 | 低 | 文档评审 + 现状映射无遗漏 | -| 17 | ARCH-251 durable pilot | ARCH-221、250 | outbox/job 表、claim/retry/幂等 | 高 | 崩溃窗口、重复投递、并发 claim | -| 18 | ARCH-252 Scheduler 分层 | ARCH-230、250 | JobSpec/catalog/execution state | 高 | overlap/timeout/restart/manual | -| 19 | ARCH-260 correlation ID | ARCH-230 | ContextVar、中间件、传播 | 中 | 并发请求、线程池、SSE、外部请求 | -| 20 | ARCH-261 Metrics/OTel | ARCH-260 | 低基数指标、可选 adapter | 中 | exporter 缺失时 no-op、label 审计 | -| 21 | ARCH-270 类型门禁 | ARCH-230、240、241 | 严格目录和精确豁免 | 中 | 选定 type checker | -| 22 | ARCH-271 复杂度 ratchet | ARCH-201 | 只降不增的规模基线 | 低 | AST 门禁 + 代表性拆分测试 | -| 23 | ARCH-272 async 阻塞检测 | ARCH-203 | debug/AST/专项测试 | 中 | API/Agent/Application 新改动路径 | - -可并行关系:ARCH-210 与 ARCH-220 可并行;ARCH-240 与 ARCH-230 可在接口冻结后并行;ARCH-260 可在 typed state 稳定后独立进行。不可并行关系:ARCH-221 与同一订阅写路径上的其他重构、ARCH-230 与生命周期大改、ARCH-251 与目标副作用的业务修改。 - -## 7. 给实施 AI 的任务卡模板 - -领取任务时先复制并填写下面模板。`allowed_paths` 不是提示,而是本次改动白名单;需要越界时先停下说明原因。 - -```yaml -task_id: ARCH-xxx -objective: 一句话描述用户可见或架构可验证的结果 -baseline_commit: 实施开始时的 git rev-parse HEAD -must_read: - - AGENTS.md - - docs/rules/04-design-patterns.md - - docs/rules/05-architecture.md - - docs/testing.md - - docs/refactor/backend-architecture-next-stage.md#对应任务 -allowed_paths: - - app/... - - tests/... - - docs/... -forbidden_scope: - - 第三方插件 ABI 删除或改名 - - 无关 schema、前端协议或插件仓修改 -contracts_to_preserve: - - REST 路径、状态码、响应结构 - - 动态插件 API 原生返回结构 - - SDK/compat manifest 中已发布符号 - - 启停顺序和安全模式语义 -preflight: - - git status --short - - git branch --show-current - - git rev-list --left-right --count HEAD...@{upstream} -evidence_to_collect: - - 真实调用链和所有入口 - - 修改前失败/缺口测试 - - 兼容消费者和回滚点 -acceptance: - - 新增或更新的专项测试通过 - - 架构门禁通过 - - 工作树仅包含白名单文件 -rollback: - - 描述代码、配置、迁移各自如何恢复 -``` - -任务卡还必须回答四个问题: - -1. **完成点在哪里?**例如订阅写入的完成是 DB commit,还是事件处理成功;不能只写“接口返回成功”。 -2. **谁拥有资源?**Session、task、thread、client、plugin instance 由谁创建、谁关闭、失败时谁回收。 -3. **兼容边界是什么?**宿主内部可以改,插件 SDK/Compat、动态 API 和持久数据不能被无意改变。 -4. **如何证明没有扩大范围?**列出路径 diff、测试命令和未执行的验证,不用“应该没问题”代替证据。 - -## 8. AI 标准执行循环 - -### 8.1 开始前 - -1. 确认仓库是 `MoviePilot`、分支是 `v3`,记录 `HEAD`、上游差异和现有工作树;不清理、不 stash、不覆盖用户改动。 -2. 阅读任务映射到的规则文件;若涉及插件兼容,再读 `docs/refactor/backend-module-refactor-compatibility.md`。 -3. 使用 `rg` 从所有入口追踪到实现、持久化、事件和外部调用;不能只查看报错文件或同名类。 -4. 先运行最小现状测试。基线本来失败时,记录精确失败并区分“当前已存在”和“本任务引入”。 -5. 对照本任务的前置任务;未满足时只做调查,不伪造兼容层绕过。 - -### 8.2 实施中 - -1. 先写或更新契约测试,再做最小实现;新增类和方法按仓库规则补类级、方法级注释。 -2. 每个提交只解决一个 Task ID。数据迁移、行为迁移和删除兼容入口至少拆成不同提交。 -3. 新路径先双轨兼容并增加计数/日志,再迁移调用方,最后在门禁证明无消费者后删除旧路径。 -4. 事务任务必须显式测试:成功提交、任一步失败回滚、重复调用、并发冲突和 post-commit 副作用。 -5. 生命周期任务必须显式测试:部分启动失败、重复 shutdown、取消传播和资源最终释放。 -6. 不运行无 scope 的 baseline write;fixture 变化必须能从语义 diff 解释,不能因为测试红就刷新。 -7. 不为通过行数门禁机械切私有函数;拆分后的对象必须拥有独立输入、输出、错误和测试边界。 - -### 8.3 完成前 - -1. 先跑目标模块专项测试,再跑架构/兼容门禁;高风险任务最后运行仓库全量门禁。 -2. 检查 `git diff --check`、`git status --short` 和逐文件 diff,确认没有生成物、秘密或无关格式化。 -3. 若 baseline 变化,逐字段说明原因;若涉及插件仓,分别报告宿主门禁与跨仓观察结果。 -4. 汇报必须包含:结果、修改路径、行为变化、兼容性、验证命令和结果、未验证项、风险/回滚。 -5. 未通过高风险验证时不得宣称完成,也不得用“仅环境问题”笼统归因。 - -## 9. 验证矩阵 - -以下命令是最低集合;实施 AI 应先用 `rg --files tests` 确认文件仍存在。新增任务测试名可以调整,但必须覆盖表中的行为。 - -| 变更类型 | 必须验证 | 重点故障注入 | -| --- | --- | --- | -| 架构规则/基线 | `tests/test_architecture_dependencies.py`、`tests/test_architecture_contract_baseline.py`、新 CLI 测试 | fixture 缺失、只读误写、插件仓不存在/漂移 | -| 启动/worker/factory | 新增 factory、worker 与 lifespan 测试;真实 Uvicorn smoke | workers=2、reload、端口占用、初始化中断、二次 shutdown | -| DB 事务/Repository | `tests/test_db_session_lifecycle.py`、`tests/test_db_decorator_error_paths.py`、目标 Oper/用例测试 | 中途异常、commit 异常、重复请求、并发更新、事件失败 | -| 订阅切片 | `tests/test_subscription_query_service.py`、`tests/test_subscribe_modified_event.py` 与新增 command 测试 | 唯一键冲突、回滚后无事件、commit 后只发一次 | -| Runtime/AppState | `tests/test_module_lifecycle.py`、启动专项测试 | 缺失依赖、部分初始化、重复关停、safe mode | -| Module 契约 | `tests/test_module_invocation_dispatcher.py`、`tests/test_module_method_contracts.py` | legacy provider、同步/异步、timeout、坏返回值、第三方异常 | -| Event 契约 | `tests/test_event_dispatch_snapshot.py`、`tests/test_event_runtime_components.py`、`tests/test_event_plugin_errors.py` | 非法 payload、handler 超时、插件异常、关停 drain | -| Scheduler/可靠任务 | scheduler 现有测试 + 新 execution/outbox 测试 | 进程在 commit 后崩溃、重复 claim、overlap、超时、重启恢复 | -| request ID/观测 | 新中间件和传播测试、`tests/test_async_request_utils.py` | 并发隔离、非法 header、线程池、SSE、外部 client 异常 | -| async 安全 | asyncio debug、目标 API/Agent/Application 测试 | 同步文件/HTTP/sleep、取消、慢 callback | - -通用架构门禁: - -```bash -./.venv/bin/python -m pytest \ - tests/test_architecture_dependencies.py \ - tests/test_architecture_contract_baseline.py -q -``` - -高风险 Python 改动的最终门禁以 `docs/testing.md` 为准,通常应在仓库目录运行: - -```bash -./.venv/bin/python tests/run.py -``` - -如果本机的编译站点资源导致进程以 `137`/`SIGKILL` 退出,应按项目既有测试隔离方案使用 `_SitesHelperStub`,并明确记录退出码和隔离方式;不得将进程被杀描述成断言失败或测试通过。 - -## 10. 可量化验收目标 - -指标用于证明方向,不用于鼓励刷数字。达到一项必须同时保留业务和插件兼容测试。 - -| 领域 | 当前基线 | 二阶段目标 | -| --- | ---: | --- | -| 宿主架构 SCC | 1 个隔离 TMDB SCC | 不新增;隔离包继续豁免,不强拆 | -| 重点禁止依赖边 | 0 | 持续为 0 | -| 基线写入行为 | 默认命令可能覆盖 fixture | 所有默认/check 命令保证工作树不变;write 必须显式 scope | -| 全功能 worker | 配置允许 >1,控制面会复制 | 启动期明确拒绝 >1;文档与配置一致 | -| 健康接口 | 认证 `/system/ping` 为主 | 分离公开 live 与受限/安全 ready;失败原因可诊断 | -| Model/Base 事务装饰器 | 正式与 legacy 查询/写装饰器均为 0 | 持续保持为 0;`db` 参数保持显式必传 | -| 新写用例事务 | 宿主写 Oper 已脱离 Base 隐式提交 | 100% 由入口/Application 边界拥有 Session/UoW | -| 高频 Module 契约 | 212 个宿主能力显式登记 | 新观察到的宿主方法必须同步登记完整契约 | -| Event payload | 53 类型全部登记 typed payload 与可靠性 | 新事件必须同步登记,不回退裸 dict | -| 超长新端点/用例 | 无增量门禁 | 新代码不越预算;旧 baseline 只降不增 | -| Request 关联 | 无统一 ID | HTTP → Application → Module/Event/外部请求可关联 | -| 关键后台副作用 | commit 后存在崩溃窗口 | 选定 pilot 可恢复、幂等、可查询失败和重试次数 | -| 类型门禁 | 无渐进严格目录 | 新 contract、typed state、event/module contract 进入 CI | - -## 11. 风险与回滚策略 - -| 风险 | 预防 | 回滚触发 | 回滚方式 | -| --- | --- | --- | --- | -| 单 worker 校验阻断既有部署 | 启动错误列出原因和替代配置;先发弃用告警再硬拒绝 | 已有用户无法按单 worker 启动 | 临时恢复告警模式;不声称多 worker 已安全 | -| factory 改造改变导入副作用 | 分离 `create_app` 与 DB prepare;真实进程 smoke | reload、CLI 或容器入口失败 | 恢复原入口,保留已通过的纯函数拆分 | -| 事务迁移改变提交时机 | 一个垂直切片、双数据库语义测试、事件次数断言 | 重复事件、部分写入或锁冲突上升 | 切回旧 facade;schema 若未变无需数据回滚 | -| Typed runtime 破坏插件启动 | 旧 provider 作为兼容门面;SDK/manifest 快照 | 官方/第三方插件找不到宿主服务 | 切回旧 provider 读取,保留 typed 对象但不强制 | -| Module Contract V2 误拒绝 legacy | adapter 做输入归一和返回验证;按 method 逐个开启 | 合法旧插件调用被拒绝 | 对该 method 关闭严格模式,不删除 spec/观测 | -| Event model 阻断插件自定义数据 | 区分宿主 strict 与插件 opaque/extension payload | 插件事件无法投递 | 对该事件恢复兼容解析并记录未知字段 | -| Durable pilot 重复执行 | 幂等键、原子 claim、lease 超时和执行记录 | 同一副作用多次发生 | 停止 worker,保留表和待处理记录,切回人工恢复流程 | -| 指标造成高基数或泄密 | label 白名单、值截断/散列、敏感字段测试 | 指标存储暴涨或出现用户数据 | 关闭 exporter;no-op adapter 保持业务可运行 | - -涉及 Alembic 的任务必须遵守扩展—迁移—收缩:先增加兼容 schema,再部署双读/双写或回填,最后在确认回滚窗口关闭后删旧字段。任何不可逆 downgrade 都要单独说明数据损失,不能把代码回滚等同于数据库回滚。 - -## 12. 明确禁止的“改进” - -- 不创建新的 `app/common`、`app/shared`、`app/utils` 万能目录;无法说明所有者的代码不应迁入公共层。 -- 不引入通用 DI 容器来隐藏依赖图;Startup 显式装配和窄 Protocol 已足够。 -- 不把所有同步代码改成 async,也不在没有压测证据时更换数据库驱动。 -- 不在控制面拆分前开启多 worker;也不通过文件锁“临时保证”所有后台组件只启动一次。 -- 不把进程内 Event 全部升级为消息队列;先按 E0-E3 业务完成语义分类。 -- 不让 Repository/Model 发布业务事件;事件由完成用例的 Application/Chain 在正确提交点触发。 -- 不把动态插件 API 强制包成宿主 `{success, message, data}` 响应。 -- 不删除 SDK/Compat symbol、旧模块方法或旧事件字段来换取类型整洁;必须先有消费者证据和弃用期。 -- 不编辑 `app/plugins/**` 运行时副本来代表官方插件修复;插件源码应在 `MoviePilot-Plugins` 独立仓处理。 -- 不因行号、commit hash 或采样耗时变化直接刷新 fixture;必须先解释语义 diff。 -- 不用全局 `Any`、全文件 ignore、吞异常或无界重试来通过门禁。 -- 不将用户 ID、媒体名、URL、插件配置等敏感或高基数字段作为 metric label。 - -## 13. 整体完成定义 - -本方案只有同时满足以下条件才算完成,而不是“23 个任务都有提交”即完成: - -1. 宿主硬门禁与跨仓观察门禁可独立运行,所有只读检查不会修改 fixture。 -2. 文档、启动校验和实际 Uvicorn 进程职责一致;全功能 V3 不会悄悄复制控制面。 -3. 至少订阅写入完成一个端到端事务样板,证明请求、CLI/Job 可复用同一用例且失败原子回滚。 -4. typed runtime、Module Contract V2、Event Registry 均保留现有插件 ABI,并由官方插件仓快照验证。 -5. 至少一个用户数据相关副作用具备持久恢复、幂等、失败可查询和安全重试能力。 -6. 请求关联、健康、核心指标能解释 API → 用例 → Module/Event → 外部 I/O 的主要失败点。 -7. 新代码受类型、复杂度、异步阻塞和架构方向门禁约束;旧债务的 baseline 只降不增。 -8. 每个高风险切片都存在可执行回滚方案;数据库变更另有明确升级/降级说明。 -9. `./.venv/bin/python tests/run.py` 通过,或如实记录可复现的环境阻塞与仍未验证范围;不得以专项测试代替全量结果。 -10. 最终复核 `docs/rules/`、架构总览、部署文档和实际代码一致,并删除已经过期的临时兼容说明。 diff --git a/docs/refactor/backend-architecture-review.md b/docs/refactor/backend-architecture-review.md new file mode 100644 index 000000000..56366ca53 --- /dev/null +++ b/docs/refactor/backend-architecture-review.md @@ -0,0 +1,131 @@ +# 后端架构优化点评审(2026-08) + +> 本文档是 2026-08-24 基于当前 `v3` 分支代码的全面架构评审结论,取代此前 +> `docs/refactor/` 下的分阶段治理文档(`backend-architecture-governance.md`、 +> `backend-module-refactor-compatibility.md`、`backend-architecture-next-stage.md`、 +> `module-quality-scale.md`,历史内容见对应提交的 git 记录)。分层权威规范仍以 +> [`docs/rules/05-architecture.md`](../rules/05-architecture.md) 为准,后台动作可靠性分级 +> (E0–E3)的决策依据见 [`docs/adr/0007-background-action-reliability.md`](../adr/0007-background-action-reliability.md)。 +> 本文档不重复既有约束,只记录现状差距与改进方向。 + +## 总体结论 + +v3 重构骨架健康:架构测试 `tests/test_architecture_dependencies.py` 全部通过, +chain 层零 `app.db` / `app.modules` 内部直连,domain 与 chain 层配置注入纪律近乎完全落实。 +当前的主要优化空间不是"分层错误",而是三类问题: + +1. **上帝类**——领域算法埋在编排链里; +2. **迁移过半的全局状态**——注入通道已建好,存量未迁完; +3. **收缩的质量门禁**——静态检查名义严格、实际覆盖面很小。 + +## 一、Chain 层上帝类:领域算法埋在编排类里(优先级最高) + +| 文件 | 行数 | 问题 | +|---|---|---| +| `app/chain/subscribe.py` | 4155 | 约 15 个职责域:洗版优先级纯计算约 650 行(L201-847)、订阅创建状态机(L976-1406)、搜索编排(单方法 285 行 L1505-1790)、匹配编排(L2032-2421)、进度/完成事实管理(L2735-3106)、分享跟随、日历缓存、远程交互删除等 | +| `app/chain/download.py` | 2293 | `_execute_batch_download`(L1533-2052)是"电影→整季→按集→拆包"四轮择优策略引擎,本质是领域算法却埋在链里;字幕下载子系统约 500 行;失败冷却指纹(L774-946) | +| `app/chain/transfer.py` | 3208 | 线程池基础设施 + 内存队列/落盘回放 + 整理编排三类职责同居一个单例;MRO 深达 10 个类;`_execute_transfer` 单方法约 820 行 | +| `app/chain/media.py` | 2060 | 音乐识别子域(约 900 行路径专辑推断)与影视识别门面强行同居一个类 | + +**建议**:按项目已有的 topic-package 先例(参照 `application/subscription/` 的拆分方式), +把纯策略计算下沉到 `domain` 或 `application`: + +* 洗版优先级/缺集计算 → 如 `application/subscription/priority.py` 或 domain; +* 批量择优下载策略 → 如 `application/download/strategy.py`; +* 音乐识别 → 并入既有 `application/music/` 目录; +* 链本身只保留编排职责。 + +## 二、sync/async 手工双写造成系统性重复 + +* `chain/media.py` 有 **11 对**同步/异步孪生方法; + `_recognize_with_fallback_by_meta`(L647-723)与其异步版(L1596-1669)结构逐行对应。 +* `chain/search.py` 的 `process / async_process / async_process_stream` 三份实现 + (L1665/1758/1848),站点并发搜索三份(L2301/2486/2602),字幕搜索再两份。 +* 过滤规则组解析同一表达式出现 **5 处**;`_media_recognize_kwargs` 三胞胎逐行相同; + "未识别到媒体信息"告警模式全目录 **18 处**。 +* 死代码残留:`transfer.py` L2146-2173 变量初始化两次;`search.py` L2366-2378 + if/else 两分支提交完全相同的调用。 + +**建议**:统一封装"同步包装异步"的基础设施(复用 `app/runtime/execution.py` 的跨线程提交边界), +新代码只写 async 版本;重复告警/解析模式收敛到共享 mixin 或 application 服务。 + +## 三、Mixin 组合缺契约 + 两处 dispatch 绕过 + +* mixin 大量使用 `self.messageoper` / `self.run_module` 等,但无 Protocol/ABC 声明依赖, + 靠 docstring 书面承认(`_music.py:42-46`);每个链无差别继承全部基础能力, + TransferChain MRO 达 10 个类。 +* `_music.py:9-11`、`_transfer.py:22-24` mixin 反向 import 具体链,形成耦合网。 +* 正面样板是 `_interaction.py` 的 `InteractionChainMixin`(显式 `_interaction_handler_type` + 注入点 + 抽象方法),值得推广为所有 mixin 的标准姿势。 +* 违反"chains reach modules only through run_module dispatch"约束的两处实锤: + * `media.py:626-638` 硬编码 `get_running_module("TheMovieDbModule")` 直调其方法; + * `scraping.py:584-598` 自行聚合 `metadata_img` 多模块结果 + (dispatcher 已有 aggregation contract 可表达)。 + +## 四、全局状态:"通道已建、存量过半" + +| 全局点 | 现状 | 建议 | +|---|---|---| +| `eventmanager` 单例 | 36 个文件直连 import;其中 chain 层 8 个文件绕过已注入的 `context.event_manager`(search/download/media/subscribe/transfer/site/scraping/workflow) | chain 层直连改为使用注入上下文,改动机械、风险低 | +| `global_vars` 容器 | 47 个文件 147 处引用,workflow + chain 占近半,被当"停止信号总线"广泛直读 | 停止信号演进为 `runtime/state.py` 的显式契约 | +| `RuntimeSettingsCompat` | 119 个文件 import(modules 占 60),形式上是端口、用法上仍是每模块全局对象 | modules 层逐步改为注入快照 | +| Singleton 元类 | 41 处 class 使用,与 getter 门面双轨并存 | 维持双轨兼容,新增能力一律走 getter 门面 | + +## 五、启动生命周期欠账(违反自家规则) + +规则明确"新增进程级资源不得只在 lifespan() 中追加过程代码",但 `startup/lifecycle/__init__.py` +仍有 4 处过程式资源管理: + +1. 主事件循环注册/清理(global_vars set_loop/clear_loop)未进组件清单; +2. 插件同步与启动收尾任务(init_extra)游离在清单之外,不参与依赖排序和清理集合推导; +3. 停止标志设置; +4. 日志关闭靠注释约定顺序。 + +另有两个组合根脆弱点: + +* `initializers/command.py`、`initializers/scheduler.py`、`initializers/agent.py` + 在 **import 时即注册**全局服务,构成隐式时序契约,任何提前 import 都会改变注册顺序; +* `initializers/modules.py`(908 行、约 68 处 configure 调用)事实上成为第二过程式组合根, + 与 `composition/` 的纯函数式装配存在职责重叠。 + +## 六、质量门禁名义严格、实际收缩(投入产出比最高) + +| 工具 | 现状 | 建议 | +|---|---|---| +| mypy | `strict=True` 但 `files=` 白名单仅 41 个文件;全量扫描约 10072 个错误被白名单挡在门外;`follow_imports = skip` 架空跨模块检查 | 引入棘轮机制:新增文件必须达标,白名单只减不增 | +| ruff | 完全不在工具链中 | 引入并启用 import 排序等检查,与架构测试互补 | +| pylint | `disable=all` 后仅 enable 12 项高确定性检查 | 维持现状可接受,不指望它兜底 | +| 覆盖率 | 无 `fail_under` 门禁,coverage job 仅手动触发 | 至少给 `app/application`、`app/domain` 设阈值 | + +## 七、测试隔离的风险点 + +四道防线(CONFIG_DIR 隔离、网络守卫双重断言、水位回收、会话收尾)设计精细,但有三个隐患: + +* 单一 SQLite 库按主键水位回收,正确性依赖每个用例自觉登记模型表,漏登记即污染后续用例, + 且清理失败被静默吞掉; +* 约 290 行巨型 autouse fixture 装配几十个进程级服务槽位,teardown 只复位 + `reset_plugin_system()` 一个,其余槽位跨用例残留(164 处手动 `reset_*` 说明恢复靠约定而非机制); +* 54 个 `unittest.TestCase` 残留文件(占测试文件的 9.9%),按 AGENTS.md 策略在触碰时机会性转换。 + +## 八、其他次要点 + +* **非单例链反复实例化**:`MediaChain()` 全仓构造 54 处,`DownloadChain().batch_download()` + 在订阅循环内反复构造重跑 init;建议统一走 getter 门面。 +* `app/scheduler.py`(2096 行):入边已收敛到组合根,但调度器 + GC + 壁纸 + 媒体库同步等 + job 实现混在一个文件,建议按 job 域拆分。 +* `app/agent/orchestrator.py`(3655 行):`MoviePilotAgent` 与 `AgentManager` 同居, + 含会话快照、任务队列、脱敏确认等多个内部类,可按 `agent/` 已有子包惯例拆分。 + +## 建议实施顺序 + +1. **补质量门禁棘轮**(mypy/ruff/覆盖率)——先止血,防止新增债务; +2. **完成 chain 层 eventmanager/global_vars 存量迁移**——机械性工作,风险低; +3. **按 domain 下沉方式拆 subscribe/download 的领域算法**——收益最大; +4. **修两处 dispatch 绕过与 lifespan 清单欠账**——对齐自家规则。 + +## 附:评审方法 + +基于 2026-08-24 对 `v3` 分支的证据收集:架构测试运行结果、全仓静态扫描 +(mypy 全量 vs 白名单对比)、大文件方法级职责盘点(wc/rg/class 清单)、 +全局符号引用统计(eventmanager/global_vars/settings import 分布)、 +lifecycle 组件清单与 lifespan 过程式代码比对、tests/conftest 隔离机制审查。 diff --git a/docs/refactor/backend-module-refactor-compatibility.md b/docs/refactor/backend-module-refactor-compatibility.md deleted file mode 100644 index f8126ceeb..000000000 --- a/docs/refactor/backend-module-refactor-compatibility.md +++ /dev/null @@ -1,529 +0,0 @@ -# 后端模块重构与旧导入路径兼容层设计 - -> 状态:已实施。初始依赖基线取自 `v3` 分支提交 `895635c27792` 的 AST 静态扫描;2026-08-14 已完成物理迁移、拆环、兼容层、插件 SDK、资源链路和静态门禁。 - -## 1. 背景与目标 - -MoviePilot 后端计划重新划分 `core`、`helper`、`utils` 等目录的职责,消除反向依赖和循环导入,同时不能要求数量众多、版本不一的插件同步修改既有导入语句。 - -本方案同时解决两个问题: - -1. **主程序内部结构治理**:主程序代码只使用新的规范路径,并通过静态依赖门禁维持单向依赖。 -2. **插件导入兼容**:插件仍可通过旧路径导入相同对象,旧路径不需要保留同名 Python 文件。 - -兼容层是插件 ABI 的适配边界,不是主程序内部绕过分层规则的工具。导入成功不代表依赖方向合理;主程序一旦迁移到新路径,禁止再新增或保留旧路径引用。 - -### 1.1 设计目标 - -- 旧插件不修改源码即可继续加载。 -- 旧业务模块和新路径导入得到同一个模块对象、类对象、单例和模块级状态;仅用于路由的合成父包除外。 -- 物理源码只保留在新位置,不在旧目录生成大量转发文件。 -- 映射按需加载,不因安装兼容层而预导入全部目标模块。 -- Debug 模式下明确提示插件仍在使用的旧路径,生产环境不产生兼容警告噪声。 -- 兼容行为可观测、可测试、可分批上线、可快速停用或回滚。 -- 不改变插件热重载、动态导入、事件注册和打包发布的既有语义。 - -### 1.2 非目标 - -- 不用导入钩子掩盖新的循环依赖。 -- 第一阶段不同时进行模块搬迁和插件公开符号改名。 -- 不支持模糊匹配、正则猜测或任意旧路径重写。 -- 不代理第三方包、`app.plugins.*` 或插件自己的相对导入。 -- 不承诺所有 `app.*` 内部对象永久都是插件公共 API;长期公共接口应逐步收敛到 `app.sdk`。 - -## 2. 当前问题基线 - -对当前源码的静态导入图检查显示,`core`、`helper`、`utils` 并不是单向分层: - -| 依赖方向 | 静态导入边数量 | -| --- | ---: | -| `core -> helper` | 9 | -| `helper -> core` | 46 | -| `utils -> core` | 5 | - -当前还存在一个至少包含以下模块的强连通分量: - -```text -app.core.cache -app.core.event -app.core.module -app.core.plugin -app.helper.message -app.helper.plugin -app.helper.redis -app.helper.server -app.utils.mixins -``` - -因此不能简单地把文件移动到新目录后依赖兼容钩子维持运行。正确顺序是先定义职责和依赖方向,拆开运行时环,再移动模块并为插件保留旧导入 ABI。 - -插件仓库也有大量直接导入旧目录的代码。高频入口包括 `app.core.config`、`app.core.event`、`app.utils.http`、`app.utils.string`、`app.core.context` 和 `app.core.metainfo`。兼容必须在插件首次加载之前全局可用,不能依赖逐个插件适配。 - -## 3. 目标架构 - -目录名称应表达职责,而不是继续维护三个边界含混的公共杂物目录。建议的目标依赖方向如下: - -```text -Entrypoints / Plugins --> Application / Chain --> Domain + Ports --> Foundation - | ^ ^ - v | | - Runtime Composition ------------+----> Infrastructure / Adapters / Persistence -``` - -其中 startup 是组合根,负责把跨层 callback、resolver、配置读取器和 adapter 注入低层。 -本次迁移以“canonical 模块不进入任何导入 SCC”为硬约束。识别领域不直接调用 -基础设施或读取数据库/settings;可选 Rust 加速器、文件后缀、媒体来源和持久化规则 -均由启动层显式注入。 - -实施后的 canonical 边界如下: - -| 目标包 | 职责 | 允许依赖 | -| --- | --- | --- | -| `app.foundation` | 不读取 MoviePilot 业务或运行配置、也不执行 I/O 的反射/动态加载、DOM、通用结构、加密、URL、版本和文本基础能力 | 标准库、第三方库、同层代码 | -| `app.domain` | 媒体、识别、媒体服务器身份、站点和种子业务语义 | `foundation` 和 schemas;不得依赖 DB、settings、基础设施、扩展、消息、安全或应用服务 | -| `app.chain`、`app.application` | 用例编排、跨模块业务流程和聚焦应用服务 | 领域、平台能力和适配器;不得形成模块级依赖环 | -| `app.adapters` | 按 cache/network/system/external 分类的 Redis、HTTP、浏览器、DNS、资源、包、OS、Rust 和命名外部生态适配 | `foundation`、`domain`、schemas 和必要 runtime 契约;不得依赖 application、runtime extensions/compat 或 SDK | -| `app.runtime` | 配置、事件总线、缓存契约/内存策略、并发、GC 和进程级协调 | `foundation` 及少量明确的 OS 适配器 | -| `app.runtime.extensions` | 模块、插件和服务的运行时发现及生命周期 | 领域、平台及适配器;依赖由 startup 注入 | -| `app.agent.skills` | Agent Skill 元数据、市场和本地生命周期 | Agent、平台及适配器;不归入通用扩展层 | -| `app.adapters.external`、`app.application.messaging`、`app.application.security` | 插件市场、IP 归属等外部生态、消息和安全边界 | 领域、平台及基础设施能力 | -| `app.sdk` | 明确承诺给插件使用的稳定类型、事件和服务门面 | 只通过显式导出依赖受控 canonical 对象 | -| `app.runtime.compat` | 旧导入路径兼容机制和声明式映射 | 仅 Python 标准库;不能导入业务目标模块 | - -源码已按这些边界迁移完成。后续新增模块以所有权和无环依赖为验收标准,不能重新创建 `core/helper/utils` 物理源码目录。 - -### 3.1 重点拆环原则 - -- 配置重载 mixin 不应在底层模块导入全局事件单例。可改为由 `runtime` 装配阶段注册监听,或只依赖一个事件订阅协议。 -- 事件总线不应通过类名猜测 `core`、`chain`、`helper` 路径并动态实例化对象。处理器注册时应携带明确的实例解析器,或由插件/模块管理器在装配阶段注册。 -- 模块和插件管理器可以依赖插件安装、服务报告等接口,但不能直接依赖包含完整业务流程的 helper 实现。实现应注入或在更高层编排。 -- 缓存抽象与 Redis 实现分离:缓存协议/本地缓存位于低层,Redis 是 infrastructure adapter,运行时选择具体实现。 -- 消息通知失败处理不能从底层事件总线直接反向调用消息业务实现,应发布结构化错误事件,由上层订阅者处理。 - -### 3.2 重点内容的实施归属 - -下表记录本次逐文件评审后的职责结论。一个旧文件同时承担多种职责时先拆分,再分别迁入所有者目录,不能为了减少改动把整份文件直接换目录。 - -| 现有内容 | 实施归属 | 边界说明 | -| --- | --- | --- | -| `core.context`、`core.meta*`、`core.metainfo` | `app.domain.context` / `app.domain.meta` / `app.domain.metainfo` | 已去除 DB、settings、平台日志实现和 Rust adapter 直接依赖,由 startup 注入 | -| `helper.nfo`、`helper.scraper` | `app.domain.scraper` | NFO 读取与媒体元数据文档生成属于同一领域能力;旧 `app.helper.nfo` 精确映射到合并后的模块 | -| `app.log` | `app.runtime.log` | 日志策略、控制台/插件路由、异步滚动文件写入和关闭集中在一个模块;插件入口为 `app.sdk.logging` | -| `core.config` | `runtime.config` | 纯 URL、网络和系统操作下沉到 foundation/adapters,避免 runtime 承担具体 I/O | -| `core.event` 中的 `Event` 契约 | `domain.events` 或稳定 SDK contract | 与事件队列、线程、处理器实例解析分离 | -| `core.event` 中的 EventManager | `runtime.events` | 移除按类名猜路径及直接实例化 PluginManager/ModuleManager/MessageHelper | -| `core.module`、`core.plugin` | `runtime.extensions` | 安装、发现、生命周期和业务上报通过接口/装配连接 | -| `core.cache` | `app.runtime.cache` + `app.adapters.cache.backends` | runtime 保留契约、内存策略和装饰器;cache adapters 实现 Redis/文件 I/O;SDK 维持旧完整符号集 | -| `utils.string` 聚合类 | `foundation.text/size/temporal/url/dom/crypto/version` + `domain.title/episode/site/torrent` | 宿主按真实职责直接调用;完整 `StringUtils` 静态方法面只在 `app.sdk.string` 组合,旧 `app.utils.string` 和 `app.domain.string` 精确映射到该 SDK 模块 | -| `utils.url/identity/coalesce/structures` 等纯函数 | `foundation` 对应能力文件 | 确认不读取全局配置、不执行 I/O、不导入高层模块 | -| `utils.http` | `app.adapters.network.http` | 去除对 `settings` 的反向读取,由启动层注入宿主 User-Agent | -| `utils.web` | `app.adapters.external.location` | 外部 IP 归属服务是具体生态集成,不是通用网络基础设施 | -| `utils.gc` | `app.runtime.gc` | 进程内存观测和回收是运行平台策略,不是外部适配器 | -| `utils.rust_accel/system/stdio` | `app.adapters` | 具体扩展、系统调用和 stdio I/O 保留在适配器层 | -| `utils.mixins` | 按能力拆分,配置重载部分归 `runtime` | 消除 mixin 对全局事件单例的导入期注册 | -| `helper.redis/browser/doh/display/thread/package` 等 | `adapters/cache`、`adapters/network`、`adapters/system` 或 `runtime/thread.py` | 生命周期由 startup 装配,不在适配器内部反向获取管理器 | -| `helper.module` | `foundation.reflection` | 只保留通用 Python 反射、模块发现与动态加载,不承担模块生命周期 | -| `helper.downloader/mediaserver/service` | `app.application` + `app.application.service` / `app.sdk.services` | 媒体服务器身份/匹配规则与配置化服务发现统一归入 application;旧 `app.runtime.extensions.service_registry` 由 `app/runtime/compat/manifest.py` 精确映射到 `app.sdk.services`,不在新模块复制旧导出 | -| `helper.message/interaction` | `app.application.messaging` | 负责消息渲染、路由和交互,不承担配置化服务发现 | -| `helper.notification` | `app.application.notification` | 通知模块发现依赖持久化配置,属于应用服务 | -| `helper.webpush` | `app.api.endpoints.message` | Web Push 订阅和手动发送只服务消息 HTTP API,直接归入对应 endpoint | -| `helper.server` | `app.adapters.external.server` | MoviePilot 远端服务是命名外部生态集成 | -| `helper.torrent/audio/directory/format/nfo/rule/scraper` | `domain` 纯规则 + `application` 用例 + I/O adapter | 逐函数区分纯转换、业务流程和文件/网络访问 | -| `helper.rss` | `app.application.rss` | RSS 同时负责 Feed/种子语义、站点规则和浏览器回退,不把它简化为网络传输适配器 | -| `helper.sites` 与二进制资源 | `app.application.site.sites` + `app/application/site/` 资源目录 | 站点目录、认证和索引属于应用能力;完成 Build、Resources、Docker、本地安装及 CI 的跨仓同步迁移 | - -`app.chain` 已经承担 application orchestration,可继续保留,不必仅为追求目录命名整齐而整体改名。`app.modules` 继续作为可插拔 adapter 集合,但模块间编排仍由 chain/application 完成。 - -## 4. 兼容层总体方案 - -### 4.1 为什么使用导入钩子 - -每个旧模块保留一个转发文件虽然简单,但会留下大量虚假目录和文件,容易被主程序继续误用,也需要维护重复的 `__all__`、模块元数据和符号转发。统一导入钩子更符合“源码只存在于新位置”的目标。 - -兼容层使用 Python 标准导入协议: - -- 一个 `MetaPathFinder` 仅匹配声明过的旧路径; -- 一个 `Loader` 在真正命中旧路径时按需导入目标模块; -- 加载完成后让旧路径和新路径指向同一个模块对象; -- 映射表是代码仓内唯一事实来源,并经过启动前校验和测试。 - -### 4.2 建议目录 - -```text -app/ - compat/ - __init__.py - imports.py # Finder、Loader、安装和卸载入口 - manifest.py # 不导入业务模块的静态映射数据 - diagnostics.py # Debug 诊断与插件源码扫描 - sdk/ - __init__.py - events.py - media.py - services.py -``` - -`app.runtime.compat` 自身只使用标准库,尤其不能导入 `settings`、logger、事件总线、插件管理器或映射目标。`app/__init__.py` 只负责无业务依赖地安装钩子;配置初始化完成后、插件加载前,再由启动装配代码调用类似 `configure_diagnostics(enabled=settings.DEBUG, emit=logger.warning)` 的入口注入 Debug 状态和日志回调,避免兼容层再次进入当前依赖环。 - -### 4.3 声明式映射 - -映射必须精确到完整模块路径,并携带治理元数据: - -```python -MODULE_ALIASES = { - "app.core.event": ModuleAlias( - target="app.runtime.events", - introduced="3.x.y", - owner="runtime", - ), - "app.utils.http": ModuleAlias( - target="app.adapters.network.http", - introduced="3.x.y", - owner="infrastructure", - ), -} -``` - -以上路径只展示映射格式,不代表已经确定这些模块的最终归属。正式映射必须在对应领域完成依赖拆分和所有权评审后加入。 - -约束如下: - -- 旧路径和目标路径都必须是完整绝对模块名。 -- 不允许 `app.core.* -> app.runtime.*` 这类通配规则自动覆盖未知模块。 -- 旧路径不能仍有真实 `.py` 文件,避免标准查找器绕过兼容 Finder。 -- 目标不能再指向另一个旧路径;启动校验应将别名链视为错误。 -- 一个旧模块只能映射到一个目标模块。 -- 多个旧模块只有在历史上本就代表同一公共模块时才能映射到同一目标。 -- 物理搬迁阶段保持插件可见符号名称不变;符号改名另行显式登记,不能由 `__getattr__` 猜测。 - -建议同时维护机器可读的兼容清单,CI、文档生成和插件扫描均读取同一数据源,不再维护第二份路径列表。 - -### 4.4 模块身份必须唯一 - -兼容层的核心不只是“能导入”,而是保证实际承载业务对象的模块身份一致: - -```python -import app.core.event as legacy -import app.runtime.events as canonical - -assert legacy is canonical -assert legacy.Event is canonical.Event -``` - -如果分别执行同一份源码生成两个模块对象,会产生严重问题: - -- `isinstance` 对同名类判断失败; -- 单例元类在两个模块命名空间各创建一个实例; -- 装饰器、事件监听器和模块级缓存重复注册; -- pickle、Pydantic 类型路径、日志和调试信息不一致。 - -Loader 因此不能在旧模块名下再次 `exec` 目标源码,而应导入 canonical 模块,并将旧键绑定到该对象。实现需要覆盖“先导入旧路径”和“先导入新路径”两种顺序,以及并发导入时的锁语义。 - -建议的加载算法是: - -1. Finder 精确命中旧业务模块并返回 alias spec。 -2. Loader 的 `create_module()` 在 Python 导入锁内导入 canonical 模块并返回该对象。 -3. `exec_module()` 不重复执行目标源码,只校验 canonical 模块已经完整初始化。 -4. 导入结束后 `sys.modules[legacy]` 与 `sys.modules[canonical]` 指向同一对象;canonical 的 `__name__`、`__spec__` 和 `__package__` 不被旧路径覆盖。 -5. 兼容层独立记录 legacy 名称用于诊断,不把旧身份写回 canonical 模块。 - -实现阶段必须用目标 Python 版本验证上述元数据行为;如果自定义 Loader 无法在所有支持版本上保持 canonical spec,允许改用等价的受锁 `sys.modules` alias 实现,但仍禁止二次执行源码。 - -### 4.5 包和子模块处理 - -模块别名存在父包导入语义。例如导入 `app.core.meta.words` 时,Python 会依次处理父包。采用以下规则: - -1. 优先逐个登记实际被插件使用的叶子模块。 -2. 旧父包仍有物理 `__init__.py` 时沿用该父包;目录完全迁空后,由 Finder 创建 `__path__` 为空的合成兼容包,不保留散落的物理转发文件。 -3. 合成父包只是路由容器,不承载业务状态,不要求与 canonical 父包是同一对象;实际叶子模块和公开符号仍必须保持 canonical 身份。 -4. 旧包 `__init__.py` 曾公开导出的符号,要在 manifest 中登记精确的包级符号映射,由合成包惰性解析。 -5. 不把 canonical 包的真实文件系统 `__path__` 暴露给旧包,否则标准 `PathFinder` 可能把未登记的新子模块以旧名称再次执行。 -6. 兼容层不得根据目标包文件系统自动开放未登记的新内部模块给旧命名空间。 -7. 测试必须覆盖 `from old.package import child`、`from old.package import PublicName`、`import old.package.child`、`find_spec()` 和相对导入。 - -兼容承诺覆盖 Python 模块协议下的普通 `import`、使用常量旧路径的 `importlib.import_module()`,以及 pickle 等通过模块名重新导入公开符号的场景。以下行为不由通用钩子模拟: - -- 按旧模块 `__file__` 拼接数据文件路径; -- 用 `pkgutil.iter_modules()` 或旧包 `__path__` 枚举已经迁走的内部文件; -- 通过绝对磁盘路径直接加载已经删除的旧 `.py` 文件; -- 依赖旧模块 repr、traceback 或对象 `__module__` 永久保持旧名称。 - -这类插件如确属有效公共用例,应迁到 `app.sdk` 的资源/发现 API,或增加经过评审的专用 adapter,不能扩大通用 Finder 的文件系统伪装范围。 - -### 4.6 安装时机 - -钩子应在 `app/__init__.py` 最早期安装,早于 `app.factory`、启动生命周期、模块初始化和 `PluginManager.start()`。安装过程必须满足: - -- 幂等,多次调用只保留一个 Finder; -- 放在 `sys.meta_path` 中标准 `PathFinder` 之前,但只拦截白名单旧路径; -- 不预导入映射目标; -- 提供仅供测试使用的卸载和状态复原能力; -- 安装失败应在启动阶段明确失败,不能等某个插件加载后才随机暴露。 - -Finder 在诊断回调尚未配置时仍可暂存命中的旧路径和调用模块;`configure_diagnostics()` 完成后仅刷新能够确认来自插件/扩展的记录。这样不需要在 `app/__init__.py` 导入配置,又不会漏掉非常早期的插件式扩展导入。 - -不建议只在 `PluginManager` 中临时安装钩子。主程序启动、CLI、脚本、插件依赖扫描和测试都可能在插件管理器初始化前导入旧路径。 - -## 5. Debug 模式旧路径警告 - -### 5.1 运行时警告行为 - -当 `settings.DEBUG` 为真且兼容 Finder 命中旧路径时,记录一次 Debug 兼容警告: - -```text -[兼容导入] 插件 AutoSignIn 使用旧路径 app.utils.http,已映射到 app.adapters.network.http;请迁移到 app.sdk.network -``` - -警告应包含: - -- 旧模块路径; -- 当前实际目标路径; -- 推荐的插件稳定路径; -- 能识别时的插件 ID 或触发模块; -- 兼容规则引入版本。 - -日志级别建议用 `WARNING`,但仅在 `settings.DEBUG=true` 时启用。`DEV` 仍只控制热重载等开发行为,不作为本兼容警告的开关;本地启动脚本目前会同时打开两者,但实现和测试必须保持语义独立。 - -去重键使用 `(plugin_id, legacy_module)`;同一插件同一路径每个进程只提示一次。插件热重载清理 `app.plugins.` 时不清除这份诊断去重集合,避免每次保存文件都重复刷屏。测试可以显式清空诊断状态。 - -### 5.2 为什么还要做插件源码扫描 - -运行时钩子本身不能完整识别所有旧引用:如果目标旧模块已经由另一个插件加载并存在于 `sys.modules`,后续插件导入可能直接命中缓存,不再调用 Finder。只靠运行时钩子会漏报。 - -因此在 DEBUG 模式下,`PluginManager` 导入插件前应对该插件的 Python 文件执行一次轻量 AST 扫描: - -- 识别 `import app.core.xxx`、`from app.core.xxx import Name`,以及 `from app.core import xxx` 这类包级写法; -- 对照同一份兼容映射表生成警告; -- 报告插件 ID、文件相对路径和行号; -- 结果按文件修改时间或内容摘要缓存; -- 解析失败只警告,不阻止插件加载; -- 不执行插件源码,也不通过正则猜测 Python 语法。 - -动态字符串导入如 `importlib.import_module(variable)` 无法全部静态识别,仍由运行时 Finder 兜底。两种诊断共享去重/聚合器,避免产生重复日志。 - -### 5.3 不要使用 `DeprecationWarning` 作为唯一通道 - -Python 默认通常隐藏 `DeprecationWarning`,并且难以稳定带出插件 ID。兼容层可以额外调用标准 `warnings.warn()` 方便测试或 IDE 捕获,但 MoviePilot 的 DEBUG 日志警告才是插件开发者可依赖的诊断通道。 - -### 5.4 生产环境行为 - -- 兼容映射继续生效; -- 不扫描插件源码; -- 不输出旧路径警告; -- 可维护内部计数,但不得产生高基数日志或遥测; -- 兼容导入失败仍按普通 `ModuleNotFoundError`/`ImportError` 记录真实错误。 - -## 6. 插件公共接口策略 - -导入兼容可以保证旧插件继续运行,但不能让插件永久依赖重构后的内部目录。应建立 `app.sdk` 作为新的插件稳定入口: - -```python -from app.sdk.events import Event, EventType, eventmanager -from app.sdk.media import MediaInfo, MetaInfo -from app.sdk.services import RequestClient -``` - -`app.sdk` 的原则: - -- 只暴露有兼容承诺的对象; -- 不通过 `from internal_module import *` 无限制导出内部实现; -- SDK 门面尽量依赖协议和稳定数据结构; -- 新插件文档只展示 `app.sdk` 路径; -- 旧路径映射到当前 canonical 实现,但警告中的推荐路径优先指向 `app.sdk`; -- 将来内部位置再次变化时,只维护 SDK 门面和映射目标,不要求插件再迁移。 - -第一阶段不强制现有插件改为 SDK;官方插件可在后续常规版本中逐步消除警告。 - -## 7. 符号级兼容 - -模块搬迁和符号改名应拆成不同批次。绝大多数迁移只做模块别名,并保持原有类/函数名称。 - -确实需要改名时,使用显式的符号映射: - -```python -SYMBOL_ALIASES = { - ("app.core.context", "MediaInfo"): SymbolAlias( - target_module="app.domain.media", - target_name="MediaDescriptor", - ), -} -``` - -符号兼容仅支持 `from old.module import OldName` 和 `old_module.OldName` 等明确场景,并保证返回 canonical 对象。禁止自动遍历、相似名称匹配或静默参数转换。构造参数/返回值契约发生变化时,应增加真正的 adapter,并单独评审行为兼容性。 - -## 8. 分阶段迁移计划 - -### 阶段 0:冻结基线和生成清单 - -- 用 AST 构建主仓和官方插件仓库的导入图。 -- 输出 `core/helper/utils` 的强连通分量、反向边和插件使用频率。 -- 区分插件公共契约、主程序内部实现和资源二进制落点。 -- 为首批迁移建立精确的 `old -> canonical -> sdk` 清单。 -- CI 保存依赖图摘要,后续批次不得增加反向边或新环。 - -### 阶段 1:先落兼容基础设施 - -- 新增只依赖标准库的 `app.runtime.compat`。 -- 在 `app/__init__.py` 最早安装 Finder。 -- 实现 DEBUG 运行时告警与插件 AST 扫描。 -- 映射表先为空或只放一个无副作用的试点模块。 -- 完成模块身份、并发导入、包语义、诊断去重和状态恢复测试。 - -### 阶段 2:拆环,不急于搬所有文件 - -- 优先拆 `event/module/plugin/cache/redis/message/mixins` 强连通分量。 -- 用协议、注册表和 startup composition root 替代类名猜测及底层反向实例化。 -- 每拆一条环都增加静态依赖测试和相关生命周期测试。 -- 此阶段允许部分文件暂留旧目录,但主程序新代码必须遵守目标依赖方向。 - -### 阶段 3:按垂直批次搬迁 - -每个 PR 只处理一个可独立验证的领域,例如“HTTP 基础设施”或“媒体领域模型”: - -1. 在新位置建立 canonical 模块。 -2. 将主程序、脚本和测试切换到新路径。 -3. 删除旧物理源码文件。 -4. 添加旧路径映射。 -5. 验证旧插件导入、主程序新导入和对象身份。 -6. 更新依赖图,确认没有新增 SCC。 - -不要在一个 PR 中同时移动几十个不相关模块。Git 能识别文件移动,但运行时副作用、插件 API 和依赖方向必须逐域验证。 - -### 阶段 4:建设 SDK 并迁移官方插件 - -- 从实际高频插件入口开始建立 `app.sdk`。 -- 更新插件开发文档和模板。 -- 官方插件在正常版本发布中逐步改用 SDK;第三方插件继续由兼容层支持。 -- DEBUG 扫描报告可输出剩余旧路径统计,用于安排迁移优先级。 - -### 阶段 5:兼容策略长期维护 - -- V3 生命周期内默认不删除已发布的旧路径映射,除非明确宣布大版本破坏性变更。 -- 映射只能新增或纠正目标,不随内部清理随意删除。 -- 删除规则前必须确认官方插件仓库、已知第三方插件样本和文档均已迁移,并经过至少一个明确弃用周期。 - -## 9. 静态门禁和测试 - -### 9.1 主程序依赖门禁 - -新增 AST 级测试或独立脚本,至少检查: - -- `app.runtime.compat` 不导入任何 MoviePilot 业务模块; -- `app.foundation` 不导入其他 MoviePilot 能力包; -- `domain` 不导入 DB、runtime、adapters、application、SDK 或兼容层; -- `adapters` 不导入 application、runtime extensions/compat 或 SDK; -- `foundation` 不打印日志,也不导入任何其他 `app.*` 包;`runtime.cache` 不导入具体 cache adapter; -- `adapters.system.resource` 不导入或调用 `runtime.state`; -- 低层不导入 `PluginManager`、`ModuleManager` 等运行时实现; -- `app/` 主程序代码不再导入已登记的旧路径,插件目录除外; -- 完整导入图中不存在包含 canonical 迁移模块、SDK 或兼容层的强连通分量; -- 映射目标真实存在,旧物理文件不存在,无别名链和重复冲突。 - -检查应解析 AST,不用文本正则替代 Python 导入语义。 - -### 9.2 兼容层单元测试 - -- 新路径先导入、旧路径后导入; -- 旧路径先导入、新路径后导入; -- `import old.module` 和 `from old.module import Name`; -- 模块、类、单例、枚举身份一致; -- 模块初始化副作用只执行一次; -- 多线程并发导入不会得到半初始化模块; -- 物理父包和合成父包下的子模块、包级公开符号、相对导入、`find_spec()` 行为正确; -- 未登记的新子模块不能通过旧合成父包的 `__path__` 泄漏或被重复执行; -- 未登记的旧路径仍抛出正常 `ModuleNotFoundError`; -- Finder 对第三方包和 `app.plugins.*` 零干扰; -- 安装幂等,测试卸载后完整恢复 `sys.meta_path`、`sys.modules` 和诊断状态。 - -### 9.3 Debug 警告测试 - -- DEBUG=false 时不扫描、不告警,即使 DEV=true 也一样; -- DEBUG=true 时告警包含插件、旧路径、新路径和推荐 SDK 路径; -- 同一插件同一路径只告警一次; -- 不同插件使用同一路径分别可见; -- 模块已在 `sys.modules` 时,AST 扫描仍能发现后加载插件的旧导入; -- 热重载不重复刷屏,源文件新增旧导入后可被重新扫描; -- AST 语法错误不会阻止插件正常走原有加载错误处理。 - -### 9.4 集成与回归测试 - -- 选择高频旧入口构造一个未改源码的兼容插件样本并启动。 -- 覆盖插件首次启动、停止、单插件热重载、全部插件重载。 -- 覆盖事件装饰器、配置重载、模块枚举、缓存/Redis 和消息错误路径。 -- 覆盖 CLI、FastAPI lifespan、safe mode 和本地插件同步。 -- 按仓库规则运行聚焦测试、Pylint 和完整 `python tests/run.py`。 - -## 10. 资源、构建和跨仓影响 - -`MoviePilot-Resources/resources.v3`、Docker 更新脚本、本地安装脚本以及 `MoviePilot-Build` 现已把站点扩展和数据文件同步到 `app/application/site/`,编译扩展模块名为 `app.application.site.sites`。 - -跨仓资源迁移已按以下约束完成: - -- 站点运行时扩展 canonical 路径为 `app.application.site.sites`; -- 数据文件与其唯一消费者放到聚焦的站点应用目录,不再混入通用网络适配器; -- 同步修改 `MoviePilot-Build` 的扩展名和输出参数; -- 同步修改 Build CI 的认证扩展、站点数据发布流程和 manifest target; -- 同步修改 `MoviePilot-Resources` 的 package target; -- 修改 Dockerfile、`docker/update.sh`、entrypoint、本地安装/卸载和相关文档; -- 为 `app.helper.sites` 保留旧路径兼容,并验证 CPython 扩展通过别名加载时保持同一模块身份; -- 分别验证 macOS/Linux 和当前支持的 Python 版本产物。 - -该跨仓迁移不能混在普通纯 Python 模块搬迁 PR 中,否则发布镜像、本地安装和源码开发环境会出现不同结果。 - -## 11. 可观测性与故障处理 - -兼容层建议提供只读诊断快照,至少包含: - -- 已安装 Finder 数量和版本; -- 当前映射表版本/摘要; -- 本进程已命中的旧路径集合; -- DEBUG 模式下按插件聚合的旧路径列表; -- 最近一次兼容导入失败及原始异常链。 - -不要让兼容层吞掉目标模块自己的 `ImportError`。需要区分: - -- **旧路径未登记**:`ModuleNotFoundError` 指向旧路径; -- **映射目标不存在**:兼容配置错误,应包含 old/target 信息并在测试或启动校验失败; -- **目标内部导入失败**:保留原始 traceback,附加兼容上下文但不改写根因; -- **循环初始化**:明确报告当前别名解析栈,不能重试后返回半初始化模块。 - -## 12. 回滚方案 - -每个迁移批次必须可以独立回滚: - -- 映射表带批次或版本元数据,便于定位新规则。 -- 兼容层本身提供全局禁用开关仅用于故障诊断;生产默认开启,不能要求用户手动开启才能兼容插件。 -- 单个错误映射可被精确禁用,不影响其他已迁移路径。 -- 搬迁 PR 不删除旧实现逻辑,只移动 canonical 所有权;Git 回滚后旧文件和映射可以一起恢复。 -- 数据库、配置格式和插件存储不应在纯模块搬迁批次变化。 -- 跨仓资源迁移保留一个发布周期的旧产物回退能力,并验证旧镜像/新资源及新镜像/旧资源组合的兼容边界。 - -## 13. 验收标准 - -单个迁移批次只有同时满足以下条件才算完成: - -1. 主程序只导入 canonical 新路径,静态门禁无新增反向依赖或循环。 -2. 旧路径不存在同名业务源码文件,映射表是唯一兼容定义。 -3. 未修改源码的旧插件样本可以正常启动、执行和热重载。 -4. 旧业务模块与新路径的模块、类、枚举和单例身份一致;合成父包只承担白名单路由。 -5. DEBUG 模式会对每个插件的每个旧路径首次发出可行动的警告,生产模式无警告噪声。 -6. 映射安装不预导入目标模块,不增加可感知的启动副作用。 -7. 聚焦测试、静态检查、完整测试以及涉及的构建/资源验证通过。 -8. 文档、插件 SDK 推荐路径和跨仓发布脚本与真实运行路径一致。 - -## 14. 实施结果 - -1. `app/core`、`app/helper`、`app/utils` 物理目录均已删除,宿主全部使用 canonical 路径,插件旧导入只由虚拟兼容包解析。 -2. `app.runtime.compat` 在 `app` 包初始化时安装精确白名单 Finder,旧叶子模块与 canonical 模块保持同一身份。 -3. DEBUG 诊断通过运行时命中和插件 AST 扫描互补发现旧引用,生产模式静默。 -4. Event、模块、插件和安全边界改为由 startup composition root 注入 resolver、回调和错误处理器,迁移模块不再处于强连通分量。 -5. 插件稳定入口收敛到 `app.sdk`;存量插件无需同步修改,官方插件可以按正常发布节奏迁移。 -6. 站点二进制和数据资源迁到 `app/application/site`,Build 直接生成 `app.application.site.sites`,Build CI、Resources V3 manifest、Docker 和本地 CLI 使用同一目标路径。 -7. 媒体识别领域不再直接读取 DB/settings,也不导入 Rust 适配器;`startup/initializers/domain.py` 统一注入实时规则、后缀策略、TMDB 图片地址、默认媒体来源和加速器。 -8. 缓存按职责拆为 `runtime/cache.py`(契约、内存实现、装饰器、代理)和 `adapters/cache/backends.py`(Redis、文件 I/O);旧 `app.core.cache` 指向完整 `app.sdk.cache` 门面。 -9. `application/mediaserver.py` 集中负责媒体服务器的配置化服务发现、Provider ID 规范化和音乐库匹配;通用媒体身份规则继续复用 `domain/media.py`。 -10. GC 归入 `runtime/gc.py`,外部 IP 归属查询归入 `adapters/external/location.py`,安全能力统一在 `app/application/security/`,URL 安全策略为 `url.py`,二次认证文件为 `twofactor.py`。 -11. 资源适配器只负责检测、下载和安装,成功后是否重启由 startup 决策。 -12. 日志策略、控制台/插件路由、异步滚动文件写入和关闭集中在 `runtime/log.py`;该模块不得导入任何 `app.*` 模块,foundation 不打印日志,运行期诊断由上层调用方负责。插件使用 `app.sdk.logging`,旧 `app.log` 继续精确兼容。 -13. `domain/nfo.py` 已合并进 `domain/scraper.py`,NFO 读取与元数据文档生成由同一领域模块负责,旧 `app.helper.nfo` 仍可导入。 -14. 通知服务发现归入 `application/notification.py`;Web Push API 辅助逻辑归入 `api/endpoints/message.py`,不再保留旧顶级 messaging 中的 notification/webpush 模块。 diff --git a/docs/refactor/module-quality-scale.md b/docs/refactor/module-quality-scale.md deleted file mode 100644 index 478a4f5db..000000000 --- a/docs/refactor/module-quality-scale.md +++ /dev/null @@ -1,37 +0,0 @@ -# Module / Integration 渐进质量清单 - -本清单对应 ARCH-242。机器可检查定义位于 -`app/runtime/extensions/module/quality.py`;它不改变 Module ABI,也不把“已评估”误写成“所有规则满分”。 - -## 使用规则 - -- 当前 39 个宿主模块都必须显式登记 `ModuleQualityProfile`;未知第三方扩展才解析为 `legacy`。 -- 新模块必须在同一提交新增 profile,只可使用登记规则;宿主目录与 profile 集合不一致时测试失败。 -- `assessed` 表示已明确检查的规则集合,不等于所有规则满分;未覆盖项必须写精确原因。 -- 测试不得访问真实网络。外部错误、限流和超时通过 fake client、fixture 或 adapter stub 验证。 -- profile 不能替代 Module Contract V2;对外能力仍须在 contract registry 单独登记。 - -## 规则说明 - -| 规则 | 验收证据 | -| --- | --- | -| `fake-client-or-fixture` | provider 测试使用 fake client 或稳定录制 fixture | -| `zero-real-network-tests` | 网络守卫下专项测试通过 | -| `sync-async-boundary` | 同步 I/O 与 async 入口的调度策略明确 | -| `no-blocking-io-in-event-loop` | async 专项测试或受控线程池证据 | -| `auth-rate-timeout-offline-semantics` | 鉴权过期、限流、超时、离线结果分别测试 | -| `bounded-concurrency-or-polling` | 并发上限、轮询周期或不适用理由明确 | -| `reload-stop-idempotent` | init/reload/stop 可重复且资源最终释放 | -| `module-contract-v2` | 公开能力进入 Module Contract V2 | -| `sensitive-log-redaction` | token/cookie/password 不进入日志 | -| `owner-declared` | profile 有维护 owner | - -## 当前 assessed 范围 - -全部 39 个宿主模块已经完成显式 assessed 登记。所有模块共同具备四项机器证据:全测试真实网络 -守卫、覆盖 `app/modules` 的 async 阻塞扫描、宿主已观察能力的 Module Contract V2、明确的 -`MoviePilot core` owner。鉴权、限流、并发、敏感日志和 reload/stop 等能力相关规则不做虚假 -“全通过”声明,仍由对应模块专项测试证明,并在 profile 中保留豁免边界。 - -`bangumi` 与 `dingtalk` 已登记更细的专项证据;其他模块先完成“已审查、通用门禁已覆盖、专属规则 -按能力适用”的收口。测试会阻止宿主模块退回无法区分是否审查过的 `legacy` 状态。