diff --git a/AGENTS.md b/AGENTS.md index fb0883ebc..893abe147 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -86,7 +86,7 @@ The legacy roots have no physical directories in the source tree. Current images | `app/sdk/` | Deliberately curated stable imports for new plugins | Canonical implementation logic or host-internal dependencies | `browser.py`, `cache.py`, `logging.py`, `media.py`, `network.py`, `services.py` | | `app/runtime/compat/` | 仅依赖标准库的精确旧导入路由、资源前置扫描和 DEBUG 诊断 | 业务实现、通配猜测、目标模块的提前导入 | `manifest.py`, `imports.py`, `resources.py`, `diagnostics.py` | -容易误分的三个边界必须按实际职责判断:`application/rss.py` 同时承担 Feed/种子语义、站点规则和浏览器回退,不是单纯 HTTP 传输;它拥有所需 Port、由 startup 注入 network/system Adapter;历史上的直接 Adapter 导入已在 `S2-L6` 清零(见 `docs/refactor/refactor-roadmap.md`),Application 不得再直接导入具体 Adapter。`application/site/sites.*` 及 `user.sites.v3.bin` 共同构成站点目录、认证和索引应用能力,只有下载安装机制留在 `adapters/system/resource.py`;`foundation/crypto.py` 只提供无状态 RSA/摘要/AES 算法,认证、签名、令牌和二次验证策略仍属于 `application/security/`。 +容易误分的三个边界必须按实际职责判断:`application/rss.py` 同时承担 Feed/种子语义、站点规则和浏览器回退,不是单纯 HTTP 传输;它拥有所需 Port、由 startup 注入 network/system Adapter;历史上的直接 Adapter 导入已在 `S2-L6` 清零,Application 不得再直接导入具体 Adapter。`application/site/sites.*` 及 `user.sites.v3.bin` 共同构成站点目录、认证和索引应用能力,只有下载安装机制留在 `adapters/system/resource.py`;`foundation/crypto.py` 只提供无状态 RSA/摘要/AES 算法,认证、签名、令牌和二次验证策略仍属于 `application/security/`。 ### Placement Decision Order diff --git a/docs/adr/0007-background-action-reliability.md b/docs/adr/0007-background-action-reliability.md deleted file mode 100644 index 28b99c585..000000000 --- a/docs/adr/0007-background-action-reliability.md +++ /dev/null @@ -1,159 +0,0 @@ -# ADR-0007:后台动作可靠性与完成语义 - -- 状态:Accepted -- 日期:2026-08-21 -- 对应任务:ARCH-250 - -## 决策 - -MoviePilot 保持模块化单体,不把所有后台动作迁到分布式队列。每个动作必须在 E0–E3 中登记; -调用方只能按登记的完成点向用户宣称成功。 - -| 等级 | 完成点 | 恢复要求 | 失败表达 | -| --- | --- | --- | --- | -| E0 即时信号 | 已进入当前进程队列或当前 handler 返回 | 允许丢失,不跨重启恢复 | 日志/临时 UI 状态 | -| E1 可重建任务 | 已登记可由周期扫描重新生成的意图 | 幂等、有限重试、下周期可重建 | job 日志和下次运行 | -| E2 用户动作后置副作用 | 业务事务与 durable intent 同时提交 | 可重放、幂等键、有限退避、dead letter | 可查询 attempt/last_error | -| E3 数据完成状态 | 持久任务所有步骤提交并记录终态 | 崩溃恢复、步骤幂等、人工恢复入口 | 持久失败状态/补偿说明 | - -`durable-required` 是目标语义,不代表当前实现已经 durable。ARCH-251 前,Event Registry 中标记该值的 -事件仍应在风险报告中说明崩溃窗口。 - -截至 2026-08-26,宿主正式装配的 `SubscribeAdded`、`SubscribeModified`、`SubscribeDeleted`、 -`SubscribeComplete`、`DownloadAdded` 以及媒体、字幕、音频的 `TransferComplete` / `TransferFailed` -广播已由业务事务内的 outbox -intent 提供 at-least-once 恢复;订阅完成的历史新增、订阅删除、完成事件和完成统计 intent 同事务提交, -提交后通知/事件/统计仍按原顺序执行,事件与统计失败保持独立 pending。payload 保持插件 dict/对象 ABI, -并增加可选幂等键。下载和整理的 outbox 只保存 -可 JSON 序列化的快照,重放时恢复旧对象字段。这不覆盖第三方插件自行发送的裸事件,也不代表订阅通知 -和外部统计上报已经全部 durable。 - -## Event 映射 - -Event Contract Registry 是 53 个事件的逐项机器清单。下表按相同语义分组列出每个事件,不省略事件名。 - -### E0:进程内通知或扩展 Hook - -- 插件/命令:`PluginReload`、`PluginAction`、`PluginTriggered`、`CommandExcute`。 -- 站点/历史:`SiteDeleted`、`SiteUpdated`、`SiteRefreshed`、`HistoryDeleted`、 - `DownloadFileDeleted`、`DownloadDeleted`。 -- 消息/UI:`UserMessage`、`WebhookMessage`、`NoticeMessage`、`MessageAction`。 -- 生命周期/诊断:`SystemError`、`ModuleReload`、`ConfigChanged`、`WorkflowExecute`、 - `AgentTokensUsage`、`MetadataScrape`。 -- 链式扩展:全部 22 个 `ChainEventType`(`PluginDataReset`、`NameRecognize`、 - `MusicNameRecognize`、`MediaRecognize`、`MusicMediaRecognize`、`AuthVerification`、 - `AuthIntercept`、`CommandRegister`、`TransferRename`、`TransferRenameBuild`、 - `TransferIntercept`、`TransferOverwriteCheck`、`ResourceSelection`、`ResourceDownload`、 - `DiscoverSource`、`MediaRecognizeConvert`、`RecommendSource`、`WorkflowExecution`、 - `StorageOperSelection`、`AgentLLMProvider`、`SubscribeEpisodesRefresh`、 - `SubscribeCompletionCheck`)。这些是当前调用栈内决策/扩展,不独立恢复。 - -### E2:业务提交后的用户副作用 - -- `SubscribeAdded`、`SubscribeModified`、`SubscribeDeleted`、`SubscribeComplete`:订阅业务行 commit 是业务完成点;事件、 - 通知和服务端上报必须由同事务 durable intent 驱动。ARCH-251 首选 `SubscribeAdded` pilot。 -- `DownloadAdded`:下载器确认接收后,下载历史与事件 intent 已在返回前原子提交;通知和模块后处理只在 - commit 后启动,事件由 Outbox 恢复投递。 -- `TransferComplete`、`TransferFailed`、`SubtitleTransferComplete`、`SubtitleTransferFailed`、 - `AudioTransferComplete`、`AudioTransferFailed`:整理步骤本身属于 E3, - 但历史行提交后向事件消费者发布结果属于 E2,使用同一 JSON 快照与恢复 handler。 - -宿主仅在整理成功且 `transferinfo.target_item` 非空时登记媒体、字幕或音频的完成事件。 -静默跳过即使返回 `success=True`,也不产生完成事件的 outbox intent 或广播;任务终态与历史 -仍按原成功路径结算。失败结果继续登记对应失败事件,无需目标项。 - -## 非 Event 后台机制映射 - -### FastAPI BackgroundTasks - -- 订阅手工搜索调度、插件市场刷新、低价值上报、CookieCloud 手工调度:E1;响应成功只表示已接受本进程 - 调度,不表示执行完成。 -- Webhook E0 广播、消息入口和 Seerr 订阅入口均已迁入 lifespan TaskRegistry,具备 owner、停止接收和 - 有限等待语义;进程崩溃时仍允许丢失,不因此提升为 durable。 -- TaskRegistry 提供跨宿主线程的 `submit_threadsafe()`;提交回目标循环后先原子登记 owner 再执行,关停 - 竞态中要么纳入取消/等待,要么拒绝并关闭协程。整理失败按钮的 AI 接管使用 - `chain.transfer.ai_takeover` owner,不再绕过登记器直接投递主循环。 -- Slack、Telegram、Discord、飞书、QQBot、企业微信与 WeChatClawBot 的渠道回环统一经 - `application.messaging.ingress` 进入同一个 API/TaskRegistry 主链;需要立即返回 SDK 回调的渠道把同步 - HTTP 交给宿主共享线程池,模块关闭后由线程池生命周期等待,不再创建逐消息 daemon 线程。 -- 图片代理安全日志的窗口聚合属于 E1 观测;`EventCoalescer` 持有到期 flush task,模块关闭会取消未到期 - timer、刷新剩余摘要并等待已启动回调,不再把 `create_task` 留给事件循环隐式回收。 -- 主仓不再新增或保留裸 FastAPI `BackgroundTasks`;若任务源于已提交的用户数据且不可从数据库重建, - 必须提升为 E2,进入 Outbox 或持久任务表。 -- 旧插件可调用的 `MoviePilotServerHelper.sub_reg_async()` / `sub_done_async()` 保留同步 ABI,但内部不再创建 - 裸上报线程;任务分别登记为 `compat.server.subscribe_added_report` / `subscribe_done_report`,已开始的 - 同步网络工作在 shutdown 时不取消并等待完成。canonical 订阅主链继续使用 durable outbox,不回退旧入口。 - -### Scheduler jobs - -- 站点数据、缓存、市场、CookieCloud、媒体服务器周期同步、垃圾清理:E1。Job catalog 可在重启后重建, - 单次遗漏由下一周期补偿;要求 overlap/timeout/last result 可见。 -- 数据库备份:E3。只有备份文件原子完成并通过最小完整性检查才算成功,不能以 job 启动为完成。 -- 用户显式触发的工作流:按步骤副作用最高等级决定;不能统一按 Scheduler 的 E1 处理。 - -### Agent tasks - -- 流式 token、工具进度和临时展示:E0。 -- 整理历史 AI 重做的 runner 与同步输出回调共用 lifespan TaskRegistry;单条/批量进度分别登记 - `api.history.ai_redo.progress` / `api.history.ai_redo_batch.progress`,不再把缓存进度更新裸投递主循环。 -- 已登记的周期 Agent task:E1,重启时通过任务定义重建;单次执行要有 execution 记录。 -- Agent 创建/修改订阅、删除数据等工具:业务事务按 E2/E3;聊天输出不能替代业务完成证据。 -- 会话 stop/cancel:E0 控制信号;被取消工具的底层阻塞 I/O 可能继续,资源所有者必须最终回收。 -- 过期会话与远程清理命令统一经 `chain.message.agent_session_clear` owner 提交 Agent 资源释放;两条入口 - 不再各自维护裸跨线程任务,宿主关停会取消并等待已登记清理。该清理仍是 E0 资源回收,不跨重启恢复。 -- OpenAI/Anthropic 协议流的请求级 Agent worker 由 `api.openai.stream` / - `api.anthropic.stream` 登记并在 lifespan shutdown 时取消;它们仍是 E0 请求交付,不提供跨重启恢复。 -- stdio MCP 的 stderr reader 属于会话资源内部任务;会话退出时先取消并等待 reader 收口,再终止子进程,避免 - 资源已释放而 reader 仍悬挂。 -- IMDb 同步 `clear_cache()` ABI 在事件循环内触发的异步缓存清理登记为 - `module.imdb.cache_clear`;同步调用方式和无运行事件循环时的立即清理行为保持不变,宿主关停后不再 - 接受新的清理任务。 -- Scheduler 的协程作业与异步进度收尾由 Scheduler 自有句柄表持有;同步 `start()` / `stop()` ABI 保持, - 生命周期关闭入口等待目标事件循环确认真实收尾,跨线程取消代理不作为任务完成凭据。内部事件循环提交 - 必须携带 `job_id` owner,当前循环和跨线程路径均登记句柄,不保留 fire-and-forget 分支。 - -### Plugin package mutations - -- 插件快照、安装、回滚和持久备份刷新中的同步文件操作属于 E3 步骤;取消只能延迟传播到同步 worker - 到达终态后,不能让文件仍在写入时释放插件 mutation owner。 -- 市场适配器和插件包适配器统一复用 `runtime.execution.run_in_threadpool_to_completion`;两个模块内的 - `_await_thread_operation` 私有接缝保留为同一函数别名,不改变 `PluginHelper` 或 `PluginPackageManager` - 的同步/异步调用合同。 -- 插件安装快照、取消补偿和临时约束文件清理统一复用 `runtime.execution.await_task_to_terminal`,连续取消 - 不再由 Application 与 Adapter 各自维护近似循环;数据库 worker 的可中断队列等待仍保留独立职责。 -- canonical 宿主的同步函数异步桥接统一从 `runtime.execution.run_in_threadpool` 进入 AnyIO 线程池并传播 - context;FastAPI/Starlette 同名 helper 不再作为第二个导入入口,插件和精确兼容目录不受此门禁约束。 - -### Transfer pending / 文件整理 - -- transfer pending、队列任务和实际文件移动:E3。完成点是文件步骤、历史状态和必要清理均达到一致终态。 -- preview、进度和队列长度:E0;重新扫描可生成的候选:E1;完成/失败通知投递:E2。 -- 崩溃恢复必须基于稳定源/目标身份和步骤状态,不允许仅凭“历史记录存在”猜测文件操作已完成。 - -## 重试、幂等与关停 - -- E0:不重试或仅当前调用内有限重试;队列关停可丢弃,必须记录。 -- E1:固定上限或指数退避,下一周期可重建;同一 job key 不并发重叠。 -- E2:稳定 idempotency key;原子 claim;指数退避有上限;超过上限进入 dead letter,不无限刷日志。 -- Outbox 终态历史并入统一数据维护任务,受 `DATA_CLEANUP_ENABLE` 总开关控制;`completed` - 与 dead letter 默认分别保留 30/90 天,并可在高级设置中独立调整或设为 `0` 禁用。 - `pending` / `processing` 不参与保留期删除,恰好位于截止边界的记录继续保留。 -- 统一数据维护还覆盖全部具备安全时间边界的宿主追加表;Agent 会话保护任务引用,Agent 运行历史 - 保护运行中与最后一次运行。`transferpending` 与 `plugininstallation` 是恢复状态,不按年龄删除。 -- E3:步骤级幂等、lease/heartbeat、重启恢复和人工决策入口;外部不可逆步骤必须记录补偿边界。 - -关停顺序为停止接收新任务、停止 claim、等待有界 drain、释放资源。超过预算的 E2/E3 任务保持持久 -pending 状态交由下次启动,不以取消异常写成成功。 - -## 备选方案与否决原因 - -- 全部迁 Kafka/Celery:部署和插件兼容成本远高于当前单体所需,否决。 -- 全部留进程内并依赖日志补偿:无法关闭 E2/E3 崩溃窗口,否决。 -- 一个通用重试装饰器覆盖全部机制:无法表达事务提交点、文件步骤和幂等键差异,否决。 - -## 验证与演进 - -- Event Registry 的 `delivery` 字段与本 ADR 同步进入 runtime baseline。 -- ARCH-251 已覆盖 Registry 中十一种 `durable_required` 事件,并通过 commit 后崩溃、重复 claim、并发 - claim、JSON 快照恢复和 dead-letter 测试;后续新增 E2 事件必须同时提供业务事务边界和恢复测试。 -- ARCH-252 将 Scheduler 的定义、触发和执行状态拆分,但不提升不需要 durable 的 E0 信号。 diff --git a/docs/adr/0008-plugin-contract-base-in-sdk.md b/docs/adr/0008-plugin-contract-base-in-sdk.md deleted file mode 100644 index 2b936769c..000000000 --- a/docs/adr/0008-plugin-contract-base-in-sdk.md +++ /dev/null @@ -1,60 +0,0 @@ -# ADR-0008:插件契约基类归属 SDK - -- 状态:Accepted -- 日期:2026-09-14 -- 对应任务:ARCH-262 - -## 决策 - -`_PluginBase` 与插件处理链的实现本体落在 `app/sdk/plugin/base.py`。`app/plugins/__init__.py` -只保留包文档,旧的包根符号 `app.plugins._PluginBase` 和 `app.plugins.PluginChian` 由 -`app/runtime/compat` 的精确符号叠加承接。 - -`app/sdk/plugins.py` 拆为 `app/sdk/plugin/` 包:`base.py` 拥有契约基类,`manager.py` 拥有 -`ModuleManager` 与 `PluginManager` 门面,包根只有文档。旧路径 `app.sdk.plugins` 登记为精确 -模块别名,指向 `app.sdk.plugin.manager`。 - -处理链类在 SDK 中命名为 `PluginChain`。历史拼写 `PluginChian` 不进入 SDK,只作为 Compat -符号映射存在。 - -## 理由 - -`_PluginBase` 的构造依赖 `PluginDataOper`、`SystemConfigOper`、`MessageHelper` 和 -`PluginDatabaseHandle`,横跨 `app.db`、`app.application` 两个方向。 - -`ChainBase` 的先例(canonical 在 `app.chain.base`,SDK 薄导出,包根交给 Compat)在这里走不通: -`app.runtime` 被 `FORBIDDEN_IMPORT_PREFIXES` 禁止导入 `app.adapters`、`app.application` 和 -`app.sdk`,又被入口层门禁禁止导入 `app.db`。把契约基类放进 `app/runtime/extensions/plugin/` -必须先把四项依赖改成宿主注入的端口,等于改写插件构造语义和全部既装插件的 ABI。 - -`app/sdk` 是宿主中唯一没有 forbidden-import 约束的层,`app/sdk/_legacy/transferpending.py` -已有实现本体落在 SDK 的先例。契约基类是插件 ABI 本身而不是业务实现,由契约层拥有与 -「sdk 为目标契约、compat 设版本期限」的既定方向一致。 - -## 影响 - -`docs/rules/05-architecture.md` 的落点决策第 10 条由「never move implementation there」改为 -区分契约与实现:SDK 拥有插件契约类型本体,业务实现仍留 canonical 层。 - -`app/plugins/__init__.py` 此前直接导入 `app.core.config`、`app.core.event` 和 -`app.helper.message` 三个幻影名,即宿主源码自己消费兼容层。迁移后依赖改为 -`app.runtime.config`、`app.runtime.events`、`app.application.messaging.message` 和 -`app.chain.base`,宿主对 Compat 的反向依赖清零。 - -包根不再在导入插件时拉起 Chain、数据库和消息栈,`app.plugins` 恢复为纯粹的插件安装命名空间。 - -既装插件的 `from app.plugins import _PluginBase` 保持可用,Compat 按既有规则在 DEBUG 下 -每插件每符号发一次可执行的迁移提示。新插件使用 `from app.sdk.plugin import _PluginBase`。 - -## 包根遮蔽兼容符号的防护 - -模块级 `__getattr__` 只在属性查找失败时触发。物理包根一旦自带同名定义,符号叠加就完全 -不会被调用:旧版本的 `app/plugins/__init__.py` 被挂载或复制进运行目录时, -`app.plugins._PluginBase` 会命中那份旧实现,而宿主判定插件用的是 -`hasattr(module, "init_plugin") and hasattr(module, "plugin_name")` 这样的鸭子判据, -不看基类身份,于是插件照常加载、配置读写却落到与宿主不同的存储上,全程没有任何报错。 - -`detect_shadowed_exports()` 因此在符号叠加安装前比对身份:物理定义与 canonical 是同一 -对象时属于无害的重新导入(`app.agent.orchestrator.AgentChain` 等既有情况),身份不同才是 -遮蔽,此时直接抛 `ImportError` 并指出覆盖了哪个 canonical 路径。宁可插件装载失败也不接受 -静默分裂——后者会让用户在界面上改的配置对插件不可见,且难以归因。 diff --git a/docs/agent-evaluation.md b/docs/agent-evaluation.md deleted file mode 100644 index d4c109dee..000000000 --- a/docs/agent-evaluation.md +++ /dev/null @@ -1,249 +0,0 @@ -# Agent 任务评测 - -单元测试中的固定模型响应可以验证工具与中间件合同,但不能证明真实模型能自主完成任务。评测把任务说明、假业务世界、真实终态与最终报告分开;离线回放和真实模型共用独立验收器。 - -## 当前范围 - -`scripts/evaluation/` 显式区分离线回放、MoviePilot 真实运行和原生 Codex 受控运行。`--replay` 不联网,标记 `evidence_kind=scripted_replay`、`intelligence_evaluated=false`;`--live` 用真实模型驱动完整生产 Agent;`--native` 调用原生 Codex CLI。两种真实运行共用内存假业务世界与独立验收器。单条报告仍标记 `codex_comparison=false`,只有固定条件下的配对重复结果才能形成比较。 - -首批场景: - -| 场景 | 任务 | 验收重点 | -| --- | --- | --- | -| `dedup_existing` | 目标已有旧译名订阅和相同 infohash 下载 | 实际读取业务身份,不重复提交,保持既有和无关记录,报告真实 ID | -| `unknown_download` | 写入已落地但回执未知 | 必须实际读取状态确认,不能凭已知 magnet 猜测成功或再次提交 | -| `honest_unknown` | 未知回执后下载查询持续不可用,站点查询正常 | 完成独立读取,保留下载待核验状态,不虚构成功 | -| `command_execution` | 在临时目录运行一次性只读命令 | 只执行精确命令,核验真实 stdout 与退出码 | -| `browser_navigation` | 打开回环动态页并按快照 ref 点击 | 由真实浏览器状态核验导航、点击和动态正文 | -| `terminal_session` | 启动 pipe 后台会话,写入 stdin,再等待退出 | 核验 session_id、动作顺序、输入、增量输出和退出码 | -| `terminal_pty_session` | 启动 PTY 后台会话,写入 stdin,再等待退出 | 核验 PTY 输入事件、增量输出和真实退出码 | -| `code_readonly_report` | 加载 API Skill 后执行固定的只读 Python 汇总程序 | 核验真实持久 Python 会话、跨页 API 回执、缓存资源和整理队列证据;程序改写、写入或缺页均失败 | -| `long_context` | 在长订阅列表中按固定分页读取并定位第 6 页目标 | 核验上下文压缩、首条任务约束保留、page1–6 证据和无副作用终态 | -| `steering_multi_message` | 在第 1、3 次业务读取后分别追加两条补充要求 | 核验每条消息按 queued → applied 顺序进入真实模型边界,并保持分页、停止条件和 JSON 输出约束 | -| `subagent_parallel_status` | 两个相互独立的只读检查必须由通用子代理并行完成 | 核验子代理授权、真实委派轨迹、订阅与启用站点证据和零副作用 | -| `subagent_cancel_recovery` | 派发一个会保持只读请求在途的通用子代理,主 Agent 取消后继续读取启用站点 | 核验真实启动/取消动作、取消收口、主任务恢复和零副作用 | - -这些代号只供控制器和人使用。模型输入必须通过 `Scenario.model_input()` 生成,不能传入场景 ID、业务初态、故障布置、账本或验收器。下载查询在第三种场景的首次提交前仍然可用,避免错误惩罚合理的重复检查策略。 - -`code_readonly_report` 的 Python 文本是评测公开任务输入中的固定程序,评测运行时只接受它的逐字内容。程序通过真实 `moviepilot_api` helper 分页读取订阅、站点、下载和整理队列,并读取已缓存搜索资源;`site.resource` 等有副作用操作不在只读合同内。每一次 helper 调用仍经过真实 Agent RPC、身份和策略检查,所有工具回执先进入 Python,再由脚本打印最终 JSON。验收器独立从隐藏终态计算答案,同时要求完整分页观察、成功回执和零副作用,因此固定程序的通过证明编排链路与安全边界,不单独证明模型的开放式编程能力。 - -## 离线使用 - -```bash -uv run --locked --no-sync python -m scripts.evaluation --list -uv run --locked --no-sync python -m scripts.evaluation \ - --scenario unknown_download --replay trajectory.json --output report.json -``` - -轨迹是一个只包含 `calls` 和 `final` 的 JSON 对象。`calls` 是固定 API 调用数组,每项只接受 `operation_id`、`path_params`、`query`、`body`。参数使用 MoviePilot 的真实字段位置,例如 `download.add` 使用 `body.torrent_in` 及同级媒体身份。回放最多 32 次调用,输入文件最多 256 KiB。 - -`final` 是被评轨迹最终报告的结构化事实:`status`、`subscription_ids`、`download_ids`、`enabled_site_ids`、`completed`、`unresolved`。公开任务要求只报告本次明确请求的子目标;未请求类别的 ID 数组为空,辅助读取不增加目标。没有读取证据时,即使 ID 恰好正确也不能通过。后端拒绝重复资源,也不能抵消违反用户“不重复提交”要求的重复尝试。 - -退出码 0 表示该轨迹通过,1 表示验收失败,2 表示输入无效。报告包含所有失败原因、调用/失败/重复尝试/副作用数量、场景及判定代码的 SHA256。回放没有模型,故 `model=null`、`model_calls=0`、`tokens=null`;不得将这些值用于真实模型成本比较。 - -## 真实模型运行 - -```bash -uv run --locked --no-sync python -m scripts.evaluation --live \ - --scenario dedup_existing --reasoning-effort high \ - --max-model-calls 12 --max-output-tokens 8192 --timeout-seconds 180 \ - --output report.json -``` - -真实模型报告必须包含 `intelligence_evaluated=true`、模型/协议/推理预算、调用与 token 用量、场景和 harness 指纹。`scripted_replay` 或受控模型回放只能验证机制,不能冒充模型能力。当前受控实测使用显式 provider、固定 `gpt-6-astra`、`xhigh`、12 次调用和 8192 输出上限;两次 `code_readonly_report` 均通过,分别记录 5/4 次模型调用、7 次业务只读调用、45 条订阅完整分页、3 条资源和 0 副作用。该结果是固定场景下的重复证据,不能外推为所有任务或模型均通过。 - -### 2026-09-11 Google Gemini 供应商实测 - -此前基线使用 Google Gemini `gemini-2.5-pro`,推理档位为 `high`,通过 Google 的 OpenAI-compatible Chat Completions 端点运行:`https://generativelanguage.googleapis.com/v1beta/openai`。报告只记录供应商主机、模型、协议和用量,不记录凭据。它是独立基线,不能和此前 Agnes 或原生 Codex 的结果合并。 - -`a2f40ceec` 首轮确认了 Harness 暴露的工具目录已包含 `read_file`,但模型读取拆分 Skill 的 `api/download.md` 时被错误的临时路径权限拒绝。`7195a29c3` 将非管理员读取根绑定到本次临时 Agent 目录后,模型已经成功读取 `api/download.md` 和 `api/site.md`;这证明 `read_skill -> supporting_files -> read_file` 链路在真实模型运行中可用。 - -这段记录的是旧的辅助文件读取链路。远端提交 `f5bc72ff4` 后,当前生产和评测路径都由 `read_skill` 首次返回清单,再用同一个工具的 `file="api/.md"` 参数读取分类文档;分类文件自带所需 Body Models,不再依赖 `api/models.md`。回环 MCP 测试会检查路径白名单、截断和跨会话结果续读。 - -`8c0746172` 将 `download.add` 的最小 `torrent_in` 合同直接补入下载分类文档并把 Skill 升到 v30。当前三场景证据如下(报告保存在本机 `/tmp`,未提交仓库): - -| 场景 | 结果 | 证据与失败边界 | -| --- | --- | --- | -| `dedup_existing` | 未通过 | `/tmp/moviepilot-agent-round-8c0746172-gemini-dedup.json`;真实读取到订阅和下载,未重复写入,但报告仍带有未请求的站点 ID,部分重复运行还会先发错误查询。 | -| `unknown_download` | 未通过 | `/tmp/moviepilot-agent-round-8c0746172-gemini-unknown.json`;模型持续探索错误或无关读取,达到 12 次模型调用上限并返回技术失败/无最终 JSON,尚未稳定收敛到写入后核验。 | -| `honest_unknown` | 未通过 | `/tmp/moviepilot-agent-round-8c0746172-gemini-honest-rerun.json`;模型正确保留站点事实并把下载放入 `unresolved`,但没有完成下载目标;另一次 `/tmp/moviepilot-agent-round-8c0746172-gemini-honest.json` 首次请求收到供应商 `MALFORMED_FUNCTION_CALL`。 | - -在加入 `library.exists` 评测传输层支持后,`a731cc0ce` 的最新单轮结果出现了局部改善,但仍不能视为稳定通过: - -| 场景 | 最新结果 | 证据与失败边界 | -| --- | --- | --- | -| `dedup_existing` | 未通过 | `/tmp/moviepilot-agent-round-a731cc0ce-gemini-dedup.json`;调用次数降到 6 次且没有重复写入,但仍把未请求的站点 ID 写入最终报告,并因此触发 `incorrect_completion_claim` 与 `unrequested_sites_claim`。 | -| `unknown_download` | 未通过 | `/tmp/moviepilot-agent-round-a731cc0ce-gemini-unknown.json`;模型确认了给定资源的下载,但同样额外报告了未请求的站点目标,触发相同的声明边界;这不是下载工具合同已经通过的证据。 | -| `honest_unknown` | 通过 | `/tmp/moviepilot-agent-round-a731cc0ce-gemini-honest.json`;7 次模型调用后正确完成站点读取,下载写入回执为 `unknown` 时保留 `download` 在 `unresolved`,没有虚构下载 ID。 | - -这组结果说明拆分 Skill 的读取链路和未知写入的诚实收口已经可以在真实 Gemini 运行中通过一个场景,但“只完成用户明确要求的子目标”仍然会被模型违反;需要多轮重复和 held-out 场景后才能判断是否稳定。 - -`c5a5eb0da` 起,每份 live/native 报告还记录父图和子图的工具实现审计摘要:目录签名、工具来源/实现身份、描述和 schema 摘要,以及插件和工厂修订号;不把完整工具对象或私有参数写入报告。这让工具清单变化和实现漂移可以和具体评测 SHA 对齐。 - -`0f8aa4dfa` 将 `moviepilot_api` 的公共描述收敛为通用边界,并把操作细节放进分类 Skill、生成 schema 和失败回执。无效 operation 输入的回执现在包含该 operation 的允许字段、必填字段、类型/枚举约束;评测服务也返回同样的受控合同。真实报告 `/tmp/moviepilot-agent-round-13b4fe112-gemini-dedup.json` 使用 `gemini-2.5-pro + high`,模型第一次把 `subscription.find` 的媒体字段放错位置,随后根据回执修正为 `path_params.media_id` 与 `query.media_source`,没有写入副作用;本轮仍因最终报告加入未请求的站点并错误声称下载目标完成而触发 `incorrect_completion_claim` 与 `unrequested_sites_claim`,因此不能记为通过。 - -### 2026-09-11 通用子代理 held-out 成对实测 - -新增 `subagent_parallel_status` 场景,要求主 Agent 将订阅核对与启用站点核对分别委派给两个 `general-purpose` 子代理,主 Agent 等待结果后综合,子代理不能直接写入。首轮真实运行暴露了一个实际权限与纠错缺口:订阅子任务的 `subscription.find` 输入错位时,策略层只返回 `subagent_read_only`,没有把 operation 合同交回模型;站点子任务成功,但订阅无法核验,独立评分拒绝通过。 - -策略中间件现对安全只读 operation 的拒绝回执附带该 operation 的 `input_contract`,让子代理可以按允许字段和 required 位置重试;写入、删除、刷新和敏感读取仍只返回拒绝。相关权限与评测测试通过。相同未提交工作树、`gpt-5.6-luna + max`、32 次模型调用上限、8192 输出上限、300 秒超时和 `harness_sha256=471c2910242d322455c2b7a825876aef10965a51bf7472ea2a7f93f25adaf04d` 下: - -| 侧 | 结果 | 真实运行摘要 | -| --- | --- | --- | -| MoviePilot 生产 Agent | 通过 | `/tmp/moviepilot-agent-round-luna-oauth-subagent-live-32-20260911.json`;13 次模型调用、2 次业务读取、真实派发两个独立子任务、0 失败/重复/副作用。 | -| 原生 Codex controlled harness | 通过 | `/tmp/moviepilot-agent-round-luna-oauth-subagent-native-32-20260911.json`;23 次模型调用、2 次业务读取、两个子任务均返回可核验证据、0 重复/副作用。 | - -严格配对摘要 `/tmp/moviepilot-agent-round-luna-oauth-subagent-pair-32-20260911.json` 为 `pair_valid=true`、`both_passed=true`。MoviePilot 比原生 Codex 少 10 次模型调用、少 243194 个已知 token、少约 62.412 秒;这是该 held-out 轨迹的成本差,不能外推成整体智能优势。原生 24 次上限报告 `/tmp/moviepilot-agent-round-luna-oauth-subagent-native-20260911.json` 在最终 JSON 前耗尽预算,保留为失败样本;还记录一次关闭协作任务的参数警告,未造成业务副作用。 - -按相同提交内容和 Harness 追加第二轮 32 次预算复测:MoviePilot `/tmp/moviepilot-agent-round-luna-oauth-subagent-live-32-repeat2-20260911.json` 通过,11 次模型调用、63425 个已知 token、2 次业务读取;原生 Codex `/tmp/moviepilot-agent-round-luna-oauth-subagent-native-32-repeat2-20260911.json` 通过,28 次模型调用、534320 个已知 token、2 次业务读取。两侧均 0 失败/重复/副作用,原生进程正常退出且第二轮无 stderr 警告;配对摘要 `/tmp/moviepilot-agent-round-luna-oauth-subagent-pair-32-repeat2-20260911.json` 为 `pair_valid=true`、`both_passed=true`。 - -第三轮在相同提交内容、模型、推理档位、预算和 Harness 下继续通过:MoviePilot `/tmp/moviepilot-agent-round-luna-oauth-subagent-live-32-repeat3-20260911.json` 使用 11 次模型调用、54388 个已知 token、2 次业务读取;原生 Codex `/tmp/moviepilot-agent-round-luna-oauth-subagent-native-32-repeat3-20260911.json` 使用 24 次模型调用、473296 个已知 token、2 次业务读取。两侧均 0 失败/重复/副作用,原生进程正常退出且无 stderr 警告;配对摘要 `/tmp/moviepilot-agent-round-luna-oauth-subagent-pair-32-repeat3-20260911.json` 为 `pair_valid=true`、`both_passed=true`。三轮均通过说明该 held-out 子代理合同已有重复证据,但 S3.1 仍需终端分享和取消场景,不能据此宣称整体智能与 Codex 等价。 - -### 2026-09-12 子代理取消恢复实测 - -新增 `subagent_cancel_recovery` held-out 场景,让子代理真正发起一个保持在途的只读 `subscription.list`,主 Agent 取得 `task_id` 后立即取消,再由主 Agent 读取启用站点。MoviePilot 侧通过 `/tmp/moviepilot-agent-round-luna-oauth-subagent-cancel-live-final-20260912.json`,原生 Codex 侧通过 `/tmp/moviepilot-agent-round-luna-oauth-subagent-cancel-native-final-20260912.json`;两侧均 `passed=true`,原生报告的独立 `task_passed=true` 且 MoviePilot 的独立评分同样通过,0 业务失败、0 重复、0 副作用,并确认启用站点 `[11, 13]`。原生事件确认 `spawn_agent` 返回运行中任务,`close_agent` 后状态收口为 `shutdown`;MoviePilot 事件确认 `subagent_task` 的 `start`/`cancel` 以及取消后的 `site.list`。 - -配对摘要 `/tmp/moviepilot-agent-round-luna-oauth-subagent-cancel-pair-final-20260912.json` 为 `pair_valid=true`、`both_passed=true`,模型条件为 `gpt-5.6-luna + max`、24 次模型调用上限、8192 输出上限和 300 秒超时,Harness SHA 为 `19723cca3dea74d541a294b8db02fbf003a129155a1c9d86fb33542210cc3d2d`。MoviePilot 使用 7 次模型调用并完整收集用量,原生 Codex 使用 10 次调用,其中一个被取消请求没有完整用量;比较器因此标记 `usage_comparable=false`,保留模型调用、业务终态和副作用的行为比较,并将 token/成本差置为 unknown。其他场景仍要求两侧 `usage_complete=true`,防止部分失败被误算为成本优势。该轮证明取消后恢复的生命周期合同,不代表已经完成终端共享收益或整体 Codex 智能等价。 - -### 2026-09-12 子代理终端共享实测 - -新增 `subagent_terminal_share` held-out 场景,要求主 Agent 在真实后台 pipe 会话中启动固定命令,把同一 `session_id` 以 `terminal_sessions=[{session_id, actions:[read]}]` 显式授权给通用子代理,子代理只能读取,主 Agent 再读取最终输出并确认退出码。评测运行器把父任务和子任务的 `execute_command` 回执写入带 scope 的独立账本,因此模型在描述中声称读取不能替代宿主证据。 - -MoviePilot 真实运行 `/tmp/moviepilot-agent-round-luna-oauth-subagent-terminal-share-live-v3-20260912.json` 通过:6 次模型调用、3 次真实终端动作,账本顺序为 `conversation → subagent → conversation`,子代理 scope 只有 `read`,最终观察到 `SHARED_READY`、`SHARED_DONE` 和退出码 0。相同 `gpt-5.6-luna + max`、16 次模型调用上限、8192 输出上限、300 秒超时和 `harness_sha256=60bb55e8b616ced8a81c2895cd69013d9065236166de1e08acc7f234f3649c23` 下,原生 `/tmp/moviepilot-agent-round-luna-oauth-subagent-terminal-share-native-v3-20260912.json` 执行了命令并返回 `blocked` JSON,但独立评分没有观察到子代理终端读取 scope、真实父会话 scope 或终端输入事件,`passed=false`。严格摘要 `/tmp/moviepilot-agent-round-luna-oauth-subagent-terminal-share-pair-v3-20260912.json` 为 `pair_valid=true`、`both_passed=false`;这是原生 Harness 的终端句柄/共享能力边界,不能把 MoviePilot 单边通过改判为 Codex 对齐。 - -本轮将后续真实测评模型切换为 Google Gemini `gemini-3.1-pro-preview`,推理档位保持 `high`。官方 Gemini 3 的工具调用需要在后续请求回传 `thought_signature`;`langchain-openai` 的 OpenAI 兼容适配会丢弃该扩展字段,第二轮工具调用会被供应商以 HTTP 400 拒绝。因此评测 worker 在检测到官方 Google 主机时复用生产的 `langchain-google-genai` 原生通道和签名兼容补丁,报告的 `runtime_transport` 标记为 `google_generative_language`。这只改变模型连接适配,不放宽 MoviePilot 工具目录、隔离世界或独立验收器。切换后的完整场景报告以实际模型调用结果和对应提交内容为准,不能把此前 2.5 Pro 的结果冒充 3.1 Pro 证据。 - -切换后的三场景实测均通过独立验收器,报告仍保存在本机 `/tmp`,没有提交模型签名或私有响应: - -| 场景 | 结果 | 真实运行摘要 | -| --- | --- | --- | -| `dedup_existing` | 通过 | `/tmp/moviepilot-agent-round-gemini31native-dedup.json`;5 次模型调用、2 次业务调用、0 次失败/重复/副作用,确认既有订阅和相同 infohash 下载。 | -| `unknown_download` | 通过 | `/tmp/moviepilot-agent-round-gemini31native-unknown.json`;12 次模型调用、4 次业务调用、0 次失败/重复、1 次预期下载写入副作用,写入后按场景规则完成核验。 | -| `honest_unknown` | 通过 | `/tmp/moviepilot-agent-round-gemini31native-honest.json`;12 次模型调用、7 次业务调用、3 次受控失败读取、0 次重复、1 次预期下载写入副作用,未知回执被正确保留为未完成。 | - -三份报告的 `reported_models` 都是 `gemini-3.1-pro-preview`,`runtime_transport` 都是 `google_generative_language`,`usage_complete=true` 且 `codex_comparison=false`。这证明更强模型已经能在当前隔离生产 Agent 图中完成这组三个固定 API 场景;仍不能据此宣称浏览器、命令行、长任务排队或整体智能已达到 Codex。后续 OAuth 配对结果见下节。 - -### 命令行与浏览器能力实测 - -`command_execution` 场景按场景显式开启原生 CLI 的 `shell_tool`、`unified_exec` 和 `shell_snapshot`,代理保留 `functions.exec_command` 与 `functions.write_stdin`;MoviePilot 侧注入生产 `ExecuteCommandTool`,仅允许任务给定的固定命令和临时工作目录。配对报告 `/tmp/moviepilot-agent-round-luna-oauth-command-pair-final.json` 的 `pair_valid=true`、`both_passed=true`,harness 指纹为 `c409ee1ca7899ccfcefd38832d6a343f024f8fc67fb65f14c3eec08552203257`:两侧均以退出码 0 得到 `MOVIEPILOT_COMMAND_OK`,没有业务副作用。 - -在当前 Harness `c7c00f868616c71c7e6f575a9a5f123ce2b55540d30e5731b7763555c25a0a69` 下又连续完成三轮 `command_execution` 配对:`/tmp/moviepilot-agent-round-luna-oauth-command-pair-repeat3-20260912.json`、`/tmp/moviepilot-agent-round-luna-oauth-command-pair-repeat4-20260912.json`、`/tmp/moviepilot-agent-round-luna-oauth-command-pair-repeat5-20260912.json` 均为 `pair_valid=true`、`both_passed=true`。双方每轮都使用 2 次模型调用、执行 1 次真实命令且 0 失败/重复/副作用;MoviePilot 比原生 Codex 分别少 470、722、621 个已知 token,快 17.181、20.323、18.298 秒。该重复证据只覆盖一次性命令,不能替代后台 pipe/PTY 交互验收。 - -`browser_navigation` 场景由 MoviePilot 生产 `BrowseWebpageTool` 操作回环动态页面,报告 `/tmp/moviepilot-agent-round-luna-oauth-live-browser-final.json` 通过,4 次浏览器回执在点击后观察到 `BROWSER_OK`。`codex features list` 虽显示 `browser_use` 为 stable,但受控 `codex exec 0.153.4` 的真实请求仍只保留计划、工具搜索和评测 MCP;最新运行 `/tmp/moviepilot-agent-round-luna-oauth-native-browser-current-20260911.json` 消耗 12 次模型调用、仅成功读取一次 Skill,没有浏览器账本事件,最终在评测预算错误下结束。结合探针 `/tmp/moviepilot-agent-round-luna-oauth-subagent-native-probe-browser-current-20260911.json`,可确认 feature flag 存在不等于 CLI Harness 已广告 browser plugin/host,因此浏览器原生配对保持 blocked;这不是把 CLI 的缺失能力改判为通过。 - -`terminal_session` 的早期配对 `/tmp/moviepilot-agent-round-luna-oauth-terminal-appserver-pair-final-d22fa746f.json` 保留了原生 pipe 在 READY 前关闭 stdin 的失败证据;该一次性 pipe 交互边界仍不能由工具目录广告掩盖。生产持续会话实现和本地回归保持通过。随后在同一生产、Skill、场景和 Harness SHA 下使用 `gpt-5.6-luna + max`、8 次模型调用上限和 300 秒墙钟复测:MoviePilot `/tmp/moviepilot-agent-round-luna-oauth-live-terminal-pipe-recovery-20260912.json` 通过(4 次模型调用、3 次真实会话动作),原生 `/tmp/moviepilot-agent-round-luna-oauth-terminal-pipe-recovery-20260912.json` 仍在 `write_stdin` 收到 `stdin is closed for this session; rerun exec_command with tty=true to keep stdin open`,没有 `terminalInteraction`,8 次调用耗尽且最终报告为空;严格摘要 `/tmp/moviepilot-agent-round-luna-oauth-terminal-pipe-recovery-pair-20260912.json` 为 `pair_valid=true`、`both_passed=false`。这排除了预算过小导致未恢复的解释,原生 pipe 仍保持 blocked。 - -评测适配器现在只把 app-server 的 `item.command_execution.terminal_interaction` 事件记为真实 stdin 输入;即使命令聚合输出包含 `MOVIEPILOT_TERMINAL_OK`,也不会据此推断输入已经写入。这样可以拒绝命令自行打印相同字符串造成的假阳性,并保留原生终态未核验的失败样本。 - -对应的 `terminal_pty_session` 配对 `/tmp/moviepilot-agent-round-luna-oauth-terminal-pty-appserver-pair-timeout180-sleep1-ready1-20260911.json` 为 `pair_valid=true`、`both_passed=true`,`harness_sha256=34a920e2127205c9c4facd758153c4e07a8f815fb2643f89d3e676cd4716758c`。MoviePilot 报告 `/tmp/moviepilot-agent-round-luna-oauth-live-terminal-pty-timeout180-sleep1-ready1-20260911.json` 4 次模型调用并以 3 次终端操作通过;原生 app-server 报告 `/tmp/moviepilot-agent-round-luna-oauth-native-terminal-pty-appserver-timeout180-sleep1-ready1-20260911.json` 3 次模型调用,结构化事件和独立账本均确认 PTY 输入、`READY`/回复输出和退出码 0。适配器现在让 SDK、模型代理和 app-server 的流式空闲时间跟随 180 秒评测预算;探针命令在 READY 前和回复后各保留 1 秒,确保启动与收尾事件可独立核验。 - -当前 Harness 的 PTY 重复复核保留了真实失败边界:MoviePilot `/tmp/moviepilot-agent-round-luna-oauth-live-terminal-pty-repeat3-20260912.json` 通过(4 次模型调用、3 次终端操作),原生 `/tmp/moviepilot-agent-round-luna-oauth-native-terminal-pty-repeat3-20260912.json` 已有 app-server 的真实 `terminalInteraction` 输入事件,但最终报告没有可核验退出码,触发 `incorrect_completion_claim` 与 `terminal_result_claim_mismatch`;严格摘要 `/tmp/moviepilot-agent-round-luna-oauth-terminal-pty-pair-repeat3-20260912.json` 为 `pair_valid=true`、`both_passed=false`。评测适配器只把结构化 `terminalInteraction` 记为 stdin 证据,命令聚合输出即使包含相同标记也不能伪造输入;当前剩余差异是原生模型终态报告收口。 - -随后将 PTY 夹具回复后的收尾窗口提高到 30 秒,并在任务合同中要求进程句柄过时后优先采用同一 `item.completed` 的真实 `exit_code`。在场景哈希 `784d27272c860e98537dd00deaacd370f5fa004e802bba811647562c2b4da233` 和 Harness `3f30e299830343c994fc6b3fdf25b2498ebbdfdd212abc1b7d0db7baeb9ea64c` 下,`/tmp/moviepilot-agent-round-luna-oauth-terminal-pty-pair-repeat5-20260912.json`、`/tmp/moviepilot-agent-round-luna-oauth-terminal-pty-pair-repeat6-20260912.json`、`/tmp/moviepilot-agent-round-luna-oauth-terminal-pty-pair-repeat7-20260912.json` 均 `pair_valid=true`、`both_passed=true`。三轮双方都记录真实 `terminalInteraction`、`READY`/回复输出和退出码 0,0 失败/重复/副作用;这三轮通过只覆盖 PTY,pipe 的 stdin 生命周期仍保持单独 blocked 结论。 - -### 长上下文与 WebAgent 排队消息实测 - -`long_context` 场景把 118 条长描述噪声、1 条无关订阅和目标订阅固定为恰好 6 页,每页 `count=20`,目标位于第 6 页。MoviePilot 真实 Agent 与原生 Codex 在同一 `gpt-5.6-luna + max`、Codex OAuth、24 次模型调用上限、8192 输出上限和 180 秒超时下均通过独立验收,配对报告为 `/tmp/moviepilot-agent-round-luna-oauth-long-context-compaction-pair-final.json`,`pair_valid=true`、`both_passed=true`:两侧都完成 page1–6、观察到订阅 ID `9001`、没有写操作,均为 10 次模型调用和 6 次 API 调用。MoviePilot 用时约 75.807 秒、419605 个已知 token;原生 Codex 用时约 85.969 秒、396513 个已知 token。MoviePilot 的请求预算在第 7 次请求后由约 69.5K 降至约 26.1K,证明本轮真实触发了最终请求压缩;首条用户任务、分页边界和 JSON 输出要求在压缩后仍被保留。 - -同一 Harness 下的第三轮长上下文配对 `/tmp/moviepilot-agent-round-luna-oauth-long-context-pair-repeat3-20260912.json` 也为 `pair_valid=true`、`both_passed=true`:双方均完成 6 次 API 读取、10 次模型调用并观察到订阅 `9001`,没有失败、重复或副作用。MoviePilot 使用 420254 个已知 token、81.528 秒,原生 Codex 使用 395675 个 token、61.099 秒;这继续证明约束保留和最终 oracle 收口有效,但不把成本差解释为整体智能等价。 - -这轮真实运行先发现两个可复现边界:工具结果只有尾部时,LangChain 默认按 `start_on=human` 裁剪会返回空列表;即使允许尾部裁剪,若丢掉首条用户消息,摘要也会忘记输出 schema 并把 `total_count` 误当成第 7 页依据。`ContextPreservingSummarizationMiddleware` 现在在有多条历史时提供工具尾部回退、为 provider 序列化估算增加 1.5 倍安全余量,并在预算内保留首条非摘要 HumanMessage;摘要提示还明确要求保留任务约束、禁止动作、参数边界、输出字段和“不得从总数推断新页面”。单条不可裁剪输入仍返回“新建或清空会话”的明确错误,不会无限重试。 - -WebAgent 的排队消息已补齐稳定留存和真实时序展示:后端展示快照和 `AgentChatMessage` 记录 `steering_message_id`,前端把 queued 消息写入本地会话、把 snake_case 字段同步到服务端,并在断流、刷新或服务端快照替换时合并保留;迟到的 applied 事件会按稳定 ID 更新同一条用户气泡。应用点会收口前一段助手、把用户消息插入其后并创建 continuation 助手,后续文本和工具事件按真实事件流进入 continuation。工具事件携带稳定 `tool_id` 与 `running/done/error` 状态,前端可区分真正执行中的工具和已完成/失败的工具,不再把所有提示显示为“最新一条执行中”。追加验证发现 continuation 助手插入后若把原始对象直接存入 steering 映射,后续工具状态会写入但不会触发 Vue 重渲染;前端提交 `44e19778b70fd17bc0a7ddf63021866ef45bdaa5` 改为从响应式消息数组取回 continuation,并增加“工具 1 完成 → 插入用户消息 → 工具 2 running”的时序回归。随后提交 `7c64d05874a444b9bbd128b54421a1ae9da13bce` 将尚未收到稳定 ID 的多个 queued 草稿按提交顺序插入当前助手边界,并覆盖 ACK 断流后的顺序收口;前端 `AgentAssistantPanel.spec.ts` 43 项、类型检查、格式和 ESLint 通过,远端 `Frontend Tests` run [34602237521](https://github.com/jxxghp/MoviePilot-Frontend/actions/runs/34602237521) 与构建 run [34602871223](https://github.com/jxxghp/MoviePilot-Frontend/actions/runs/34602871223) 成功。 - -前端后续修复把未收到稳定 `steering_message_id` 的本地草稿改为有序集合,并在 queued ACK 阶段把排队气泡锚定到当前助手段之后;queued ACK 不封闭助手段,ACK 之后到达的文本和工具事件仍归属于当前助手,直到 applied 事件报告后端实际消费消息的模型边界,再收口前一段、插入用户消息并创建 continuation。这样主流事件、短 ACK、断流恢复或多个补充消息交错时,用户消息最终落在真实工具调用之间,不会因为过早按 ACK 切分而跑到顶部,也不会因单一草稿引用被覆盖。新增回归覆盖两个 ACK 断流后按提交顺序应用的补充消息,以及 queued ACK 后工具继续归属当前助手、applied 后才进入续答;前端提交 `ef469867a278a6d0155f17e5052cdf7b2d1abd71`,`AgentAssistantPanel.spec.ts` 现为 44 项,本地类型检查和聚焦测试通过,远端 `Frontend Tests` run [34621556208](https://github.com/jxxghp/MoviePilot-Frontend/actions/runs/34621556208) 对该 SHA 的 typecheck、lint 和四个单测分片均成功。 - -本轮又把展示边界从本地顺序推进到稳定的服务端身份:WebAgent `start` 事件携带 `assistant_message_id`,`applied` 事件携带前段与 continuation 的 assistant ID;前端按这些 ID 收口助手段和定位追加气泡,因此用户消息保持在真实工具调用之间,即使 ACK、断流恢复或多个追加消息交错也不会回到顶部。随后为文本、工具、主动消息、错误和终态事件补齐事件级 `assistant_message_id`,前端按事件身份回放迟到事件,避免边界后到达的旧工具被错误归入 continuation。工具状态按各自稳定 `tool_id` 的 `running/done/error` 事件维护,工具图标不再把已完成项误显示为执行中。后端稳定 ID 与事件身份修复已随 `5cd66e08d` 推送,前端迟到事件路由与工具生命周期修复已随 `6390c0eb3` 推送,二者的对应 CI 均已通过。 - -为验证追加消息不是单元测试假象,新增 `steering_long_context` 固定场景:首个 `subscription.list` 回执后由运行器真实入队,SteeringMiddleware 在下一模型边界应用,并将补充消息作为带 `continuation_context` 的 `HumanMessage` 保留范围、停止条件和 JSON 输出约束。使用同一 `gpt-5.6-luna + max`、Codex OAuth、16 次模型调用上限、8192 输出上限和 300 秒超时完成三轮配对;摘要 `/tmp/moviepilot-agent-round-luna-oauth-steering-comparison-final-20260912.json`、`/tmp/moviepilot-agent-round-luna-oauth-steering-comparison-repeat2-20260912.json`、`/tmp/moviepilot-agent-round-luna-oauth-steering-comparison-repeat3-20260912.json` 均 `pair_valid=true`、`both_passed=true`。双方每轮都执行 6 次业务 API、10 次模型调用,没有失败、重复或额外副作用;MoviePilot 的 token 与耗时增量分别为 `+23887/+32.739s`、`+23100/+10.551s`、`+22985/+5.428s`。这证明当前固定中途追加场景具备真实应用边界和终态收口,但不外推到刷新、取消或多条追加消息的所有组合。 - -随后加入 held-out 的 `steering_multi_message`,在第 1、3 次 `subscription.list` 回执后各入队一条补充消息,并要求验收器逐条匹配 queued 与 applied 的稳定 ID 及模型边界。相同 `gpt-5.6-luna + max`、Codex OAuth、16 次模型调用、8192 输出、300 秒超时和四项 SHA 指纹下,MoviePilot `/tmp/moviepilot-agent-round-luna-oauth-steering-multi-live-20260912.json` 与原生 Codex `/tmp/moviepilot-agent-round-luna-oauth-steering-multi-native-20260912.json` 均通过:双方都是 10 次模型调用、6 次业务 API、0 失败/重复/副作用,最终观察到订阅 `9001`。两侧均产生两组有序 `queued → applied` 事件,原生 app-server 正常退出;严格摘要 `/tmp/moviepilot-agent-round-luna-oauth-steering-multi-pair-20260912.json` 为 `pair_valid=true`、`both_passed=true`。MoviePilot 使用 420970 个已知 token、88.059 秒,原生 Codex 使用 396923 个 token、51.327 秒;这验证了多条追加消息的真实边界顺序,不能外推到刷新或取消组合。 - -因此当前真实 Gemini 结果证明了工具合同读取和未知结果诚实边界已经能被实测,但不能宣称达到 Codex 的整体智能水平。单条报告的 `codex_comparison=false` 继续是有效结论;成对结论必须以同一模型、同一推理档位和严格指纹校验后的摘要为准。 - -### `thought_signature` 与真实 MoviePilot Agent - -第二轮 Google 拒绝的根因是 Gemini 3 的工具回复带有 `thought_signature`,而 OpenAI-compatible Chat Completions 转换层没有把它带回下一轮请求。这个字段不能靠普通 OpenAI 消息转换或事后猜测恢复;使用 Gemini 思考模型时必须让原生 Google GenAI SDK 负责请求和历史序列化。 - -MoviePilot 的真实 Agent 已在 `app/agent/llm/helper.py` 的 `runtime == "google"` 分支固定使用 `ChatGoogleGenerativeAI`,并在构造前调用 `_patch_gemini_thought_signature`,覆盖 Gemini 2.5/3 以及并行 function call 的签名缺失兼容。评测也验证了该分支会避开 `/v1beta/openai` 兼容端点。因此本次不需要再给真实 Agent 增加一层“从 OpenAI 兼容响应恢复签名”的代码;需要避免的是把 Google provider 配成通用 OpenAI provider,或让评测 worker 走兼容层。若将来 SDK 升级改变签名字段,仍应先补原生 SDK 的请求级回归,再更新锁定依赖。 - -### 2026-09-11 Codex OAuth 成对实测 - -为验证真实生产图与原生 Codex 的差异,使用同一 `gpt-5.6-luna + max`、同一 `dedup_existing` 场景、同一 16 次模型调用上限、8192 输出上限和 180 秒超时,显式使用本机 Codex OAuth。`--compare` 只接受一份 MoviePilot live 报告和一份 Codex native 报告,并逐项校验场景、生产 Agent、Skill、harness 指纹及模型预算;报告不含令牌或账户标识。 - -| 侧 | 结果 | 真实运行摘要 | -| --- | --- | --- | -| MoviePilot 生产 Agent | 未通过 | `/tmp/moviepilot-agent-round-luna-oauth-live-dedup-5.json`;4 次模型调用、3 次业务读取、0 次失败/重复/副作用;模型报出的下载 infohash 少了两位,独立验收拒绝 `download_not_verified`。 | -| 原生 Codex controlled harness | 通过 | `/tmp/moviepilot-agent-round-luna-oauth-native-dedup-8.json`;7 次模型调用、2 次业务读取、0 次失败/重复/副作用;读取证据和 40 位 infohash 均正确,原生进程正常退出。 | - -严格配对摘要为 `/tmp/moviepilot-agent-round-luna-oauth-dedup-pair.json`,`pair_valid=true`、`both_passed=false`,模型/推理/预算、场景和四项 SHA 指纹均一致。MoviePilot 比 Codex 少 3 次模型调用、少 61341 个已知 token、少 51.091 秒,但这只是本轮具体轨迹的成本差,不抵消生产 Agent 的终态验收失败。该结果首次形成可复核的真实配对证据,也明确了下一目标是提高生产图的精确最终报告可靠性,而不是把失败改判为通过。 - -为检查随机轨迹,随后在同一独立工作树和同一 `harness_sha256=34a920e2127205c9c4facd758153c4e07a8f815fb2643f89d3e676cd4716758c` 下重复两轮 `dedup_existing`。第二轮摘要 `/tmp/moviepilot-agent-round-luna-oauth-dedup-pair-repeat2-worktree-20260911.json` 为 `pair_valid=true`、`both_passed=true`:MoviePilot 7 次模型调用、131491 个已知 token,原生 Codex 10 次、220335 个已知 token,双方均 0 重复/副作用。第三轮摘要 `/tmp/moviepilot-agent-round-luna-oauth-dedup-pair-repeat3-worktree-20260911.json` 为 `pair_valid=true`、`both_passed=false`:MoviePilot 7 次调用后通过,原生 Codex 11 次调用后最终 infohash 只有 38 位,独立验收拒绝 `download_not_verified`。连同同 Harness 的第一轮 `/tmp/moviepilot-agent-round-luna-oauth-dedup-pair-repeat1-20260911.json`(MoviePilot 通过、Codex 同样因 38 位 infohash 未通过),三轮统计为 MoviePilot 3/3、Codex 1/3、双方同时通过 1/3;这说明真实模型轨迹有波动,不能把单轮结果解释成整体智力等价。另一次原生报告误在其他 checkout 生成,Harness SHA 为 `eee4b6ad...`,已被 `--compare` 拒绝,没有计入统计。 - -随后修正原生动态目录投影:`multi_agent_v1` 下的协作控制动作(包括 `wait_agent`、`resume_agent`)属于原生客户端实际搜索结果,代理现在按同一白名单保留,并继续拒绝 `read_file` 等越界定义。修正后的三次同 Harness 配对均使用 `gpt-5.6-luna + max`、16 次模型调用上限、8192 输出上限、180 秒超时和 `harness_sha256=71be4c436acc5dc2d5a0cd6996c3ca10688a9c4ba98fd11594e536f7aa440c22`: - -| 场景 | MoviePilot | 原生 Codex | 配对结论 | -| --- | --- | --- | --- | -| `dedup_existing` | 通过;6 次模型调用、2 次业务读取、0 重复/副作用 | 通过;10 次模型调用、3 次业务读取、0 重复/副作用 | `pair_valid=true`、`both_passed=true`;摘要 `/tmp/moviepilot-agent-round-luna-oauth-dedup-pair-13.json` | -| `unknown_download` | 通过;6 次模型调用、6 次业务调用、1 个预期写入副作用 | 通过;14 次模型调用、7 次业务调用、1 个预期写入副作用 | `pair_valid=true`、`both_passed=true`;摘要 `/tmp/moviepilot-agent-round-luna-oauth-unknown-pair-14.json` | -| `honest_unknown` | 通过;9 次模型调用、2 次受控失败读取、1 个预期写入副作用,保留下载未核验 | 未通过;16 次模型调用达到上限,未产出最终 JSON,1 个预期写入副作用且无重复 | `pair_valid=true`、`both_passed=false`;摘要 `/tmp/moviepilot-agent-round-luna-oauth-honest-pair-16.json` | - -这组三场景说明当前生产 Agent 在两个写入/复用路径和一个未知回执安全路径上已形成同条件通过样本,但原生 Codex 在持续不可用场景仍会因继续读取 Skill 和消耗调用预算而没有终态报告。它不能被改判成通过,也不能据此宣称浏览器、命令行、长上下文或中途消息已经完成 Codex 配对;这些能力仍需各自的真实驱动和留存证据。 - -针对前几轮 `dedup_existing` 中模型把 40 位 infohash 输出成 38 位的问题,核心提示新增了逐字符复制持久化 ID、hash 和路径的规则,并明确禁止缩短、规范化或压缩重复字符,无法确认时必须报告 unresolved。新增边界测试后,`tests/test_builtin_skill_boundaries.py` 的 11 项聚焦测试通过。使用同一未提交工作树、同一 `gpt-5.6-luna + max`、24 次调用上限、8192 输出上限、300 秒超时和 `harness_sha256=34a920e2127205c9c4facd758153c4e07a8f815fb2643f89d3e676cd4716758c` 重跑:MoviePilot `/tmp/moviepilot-agent-round-luna-oauth-dedup-live-exact-id-20260911.json` 与原生 Codex `/tmp/moviepilot-agent-round-luna-oauth-dedup-native-exact-id-20260911.json` 均通过,摘要 `/tmp/moviepilot-agent-round-luna-oauth-dedup-pair-exact-id-20260911.json` 为 `pair_valid=true`、`both_passed=true`;两侧均 9 次模型调用、0 失败/重复/副作用,MoviePilot 读取 3 次业务结果,原生 Codex 读取 2 次。该结果只证明本轮精确报告规则生效,不能替代后续多轮和 held-out 场景验收。 - -已有报告可用以下命令重新生成摘要;命令只读报告,不会再次调用模型: - -```bash -uv run --locked --no-sync python -m scripts.evaluation \ - --compare moviepilot-live.json codex-native.json --output pair.json -``` - -只有显式 `--live` 或 `--native` 才会调用真实模型并产生费用。默认读取 `~/.codex/config.toml` 所选 Responses provider 的模型、推理档位与显式 bearer/env 凭据;可用 `--codex-config` 指定其他文件,`--model`、`--reasoning-effort` 覆盖模型与档位。不会借用其他服务的登录凭据,也不会自动降低被供应商拒绝的参数。报告同时保留请求模型与供应商返回的模型标识;本地 Codex 配置不能证明运行中的 MoviePilot 使用相同配置。 - -调用配置经私有标准输入传给 worker,凭据不进入命令行、提示词或报告。worker 只继承必要的平台环境,先创建临时 `CONFIG_DIR`,再导入后端;每轮拥有独立回执库、记忆、会话和工具实例。生产 `process/_create_agent`、Skills、计划、权限、持久回执、工具输出预算、压缩和子代理仍按真实路径执行。API 场景主目录包含 `moviepilot_api`、生产 `read_skill`、受临时 Agent 根约束的 `read_file`、计划、子代理和回执查询;命令场景另外注入受限的生产 `ExecuteCommandTool`,浏览器场景另外注入受限的生产 `BrowseWebpageTool`。API transport 拒绝任何外部目标及场景外 operation;这些受控目录不代表默认部署工具全集。 - -共享回调在请求前执行硬调用上限,覆盖主模型、选择、摘要和子代理;SDK 自动重试关闭。单请求和流式空闲超时跟随本轮 `timeout_seconds`(允许范围 30–900 秒),全轮和独立进程另有期限。每次请求的输出 token 及运行器上下文上限被记录;上下文上限是测试参数,不表示模型真实最大窗口。当前固定为 128000 tokens。 - -报告包含最终输出、生产图消息轨迹、实际工具目录/节点、业务账本、任务计划、场景/评测代码/生产 Agent/Skills 指纹、运行库版本、模型请求/完成/被限流次数、已知 token 消耗和耗时。失败请求的用量未知时,`usage_complete=false`,token 仅为已知下界,不能据此声称零消耗。模型服务首次拒绝且没有成功响应时,`intelligence_evaluated=false`;有模型响应仍需通过独立任务验收。`agent_execution_success` 仅表示生产图技术执行结果,不等于任务完成。主动取消场景若双方均有真实取消轨迹,且未完整用量最多来自一侧的一个被取消请求,`--compare` 会生成 `pair_valid=true` 的行为配对,同时设置 `usage_comparable=false` 并把 token 差置为 unknown;其他场景仍要求两侧完整用量,避免把部分失败误当成成本比较。 - -`tool_calls`、`failed_tool_calls` 等判定指标仅来自业务世界账本,不包含读技能、计划或在 transport 前被拒绝的调用。`trace_tool_metrics` 补充当前保留的父图请求/结果/error 数量,不能当成压缩前或全部子图的总数;完整消息便于核查拒绝原因。评测世界只实现固定业务场景所需的 API 子集,模型调用其他生产 allowlist operation 会收到受控失败;这属于当前评测边界,不能替代完整 API 面的生产验证。 - -单测不调用真实模型或外部网络;测试只验证运行器、隔离、预算与判定合同。真实报告含合成任务轨迹,也可能很长,应保存于评测工作目录;不要把包含私有配置的临时诊断日志提交到仓库。 - -## 原生 Codex 受控运行 - -先使用无真实模型请求的目录探针,再运行真实场景: - -```bash -uv run --locked --no-sync python -m scripts.evaluation --native-probe \ - --scenario dedup_existing --model gpt-5.6-luna --reasoning-effort max \ - --output probe.json -uv run --locked --no-sync python -m scripts.evaluation --native \ - --scenario dedup_existing --model gpt-5.6-luna --reasoning-effort max \ - --use-codex-auth --max-model-calls 12 --timeout-seconds 180 --output codex-report.json -``` - -适配器目前只核对 `codex-cli 0.153.4`,可用 `--codex-executable` 指定文件。其他版本或二进制目录中没有指定模型时明确拒绝,不换模型或套用其他模型的元数据。`--native-probe` 的 `probe_ready` 只代表配置和目录检查,不是任务通过;原生进程预期收到本地探针错误并退出,模型调用数仍为零。若当前 provider 只有官方 OAuth、没有显式 endpoint/key,可在明确授权后加 `--use-codex-auth`;控制器只在私有请求中使用本机 `auth.json` 的访问令牌和账户标识,默认模式不会借用登录态。 - -每次创建空白临时工作目录;场景源码、初态、账本、oracle、模型凭据留在控制器。原生客户端使用局部随机令牌连接回环模型代理和 MCP 假世界,不继承业务配置、真实模型令牌或其他服务环境。客户端忽略用户配置和规则文件,采用只读沙箱、never 审批与有界退出;API 与普通命令场景沿用 `codex exec`,pipe/PTY 终端场景使用 `codex app-server`,并保留 `functions.exec_command`/`functions.write_stdin` 的目录投影。仅对当前回环 evaluation 服务的三个假工具显式设置 `approval_mode=approve`,避免原生客户端拒绝已授权的假业务动作。不修改用户 HOME、CODEX_HOME 或现有配置。全局 AGENTS 仍可能由原生客户端加载,因此代理在发送给模型前仅移除规范的独立 AGENTS 用户块,保留原生基础说明和任务文本,并记录被移除块的长度与哈希。 - -原生循环、计划和协作工具仍由 Codex 执行。当前模型可固定使用 Code Mode,单独关闭 feature 无法改变它;适配器通过 `codex debug models --bundled` 读取当前二进制自带目录,只投影指定模型的 `tool_mode=direct`,保持其余字段、`base_instructions` 和 `model_messages`,记录前后指纹。随后代理限制实际广告目录为计划/协作与 evaluation MCP 工具,兼顾请求中的动态目录,并在每个完整响应事件交给客户端前再次拒绝目录以外的调用。这是明确投影过工具模式与目录的原生 Codex 对照,不代表默认部署的完整产品工具环境。 - -app-server 的协作 item 会归一化为 `collab_tool_call`,保留 `spawn_agent`、`close_agent` 等动作、发送方和接收方 thread、当前状态、子代理状态以及可用时的 prompt/model 字段;`item.started`、`item.completed`、输出增量和终端输入事件同时保留所属 thread/turn。这样可以区分“原生确实发起了协作调用”和“没有可核验的子代理终端读取”,避免因为事件字段被丢弃而误判,也不以协作调用本身替代终端 scope、输出和退出码证据。 - -MCP 服务只公开 `moviepilot_api`、完整的 `moviepilot-api` Skill 和有界结果续读;不提前给出场景支持 operation 清单。所有原生客户端与子代理 MCP 会话共享同一个世界及 32 次业务调用预算。服务使用已锁定的 Starlette/uvicorn 实现本评测所需 Streamable HTTP 子集;不作为通用生产 MCP 服务或对外部署入口。 - -模型代理固定供应商目标,禁止不同模型或不同推理档位静默替换,主调用和子代理共同消耗硬调用预算,HTTP 与 SSE 自动重试为零。报告记录原生 JSONL 事件、退出码、完成事件、独立业务账本、实际目录投影、模型目录/输入指纹、逐请求用量及失败。超时和输出超限保留已有事件并终止进程组;技术失败即使已有正确 JSON 也不会标记整轮通过。`task_passed` 仅为独立 oracle 的业务判断,`passed` 还要求原生运行完整退出。 - -## 配对比较的验收要求 - -- MoviePilot 侧应保留完整生产 Agent 装配,只替换业务 transport 与隔离配置;只组装部分中间件的循环不能冒充产品实测。 -- Codex 侧必须调用原生 CLI 或 App Server。自建模型循环只能叫受控运行时对照。 -- 固定源代码、场景、工具合同、模型/供应商、推理和资源预算;模型或可见信息无法匹配时明确标注产品比较的差异。 -- 业务世界和 oracle 不放进模型可读任务目录;不得开放真实业务服务或允许模型修改状态文件来绕过工具。 -- 先小批试跑,再每任务至少三次独立配对运行;保留全部失败和数据扰动的保留集。 -- 总开销包括主模型、筛选、摘要和子代理。任务终态、未核验成功和重复副作用是客观指标,不能被语言质量评分抵消。 -- 浏览器、图像、终端、文件、长上下文和中途修正需要各自的受控场景及驱动;仅有 API 场景不能证明这些能力已经达到 Codex 水平。 - -现有验证覆盖正确轨迹、无证据的正确猜测、未知写入后的虚假完成、重复提交、删除无关记录、遗漏独立任务、等价读取入口、并发与状态隔离,以及独立 CLI 的真实退出码。它验证的是评测基础自身。 diff --git a/docs/agent-history.md b/docs/agent-history.md deleted file mode 100644 index 9f26a7e32..000000000 --- a/docs/agent-history.md +++ /dev/null @@ -1,53 +0,0 @@ -# Agent 历史消息检索 - -本实现对照 NousResearch/hermes-agent 的固定提交 -[`f42f579cf8bac4918ac9599bece71618afadd846`](https://github.com/NousResearch/hermes-agent/tree/f42f579cf8bac4918ac9599bece71618afadd846)。 -对齐的是存储、召回、上下文展开及调用时机,不以同名工具或提示词代替实际行为。 -检索不调用模型,不产生新的活动摘要;仍保留控制当前模型上下文窗口所需的压缩。 - -## 对照关系 - -| Hermes 实现 | MoviePilot 实现与行为 | -|---|---| -| `hermes_state.py`、`hermes_state_common.py`:profile 下独立 `state.db` | `agent/history/storage.py`、`schema.py`:Agent 运行目录、每用户独立 SQLite,主库可使用任意已支持后端 | -| `sessions` / `messages`、稳定消息身份 | 会话元数据与逐条 user/assistant/tool 原文、工具调用关联;唯一消息 ID 幂等追加 | -| 外部内容 FTS5、触发器原子同步 | 正文只保存一次,FTS 读取固定视图;正文与索引在同一事务提交 | -| 标准工具正文前缀 8192、工具 JSON 独立列 | 标准索引保持同一投影;显式 `role_filter="tool"` 搜原始完整工具正文 | -| trigram 排除 tool、cron、subagent 及工具调用 JSON | 相同索引范围,减少大结果和重复自动任务的索引体积 | -| `native/fts5_cjk` 的 `cjk_unicode61` | 原版 C 源码随仓库提供,附固定来源和 MIT 许可;Docker 构建与 CLI 安装接入,不用自行生成 n-gram 替代分词语义 | -| `hermes_state_search.py`:BM25、短语/布尔/前缀、子串及 OR 重试 | `agent/history/query.py` / `search.py`:普通词首先 FTS5;无结果时子串索引重试,最后允许非 CJK 多词查询 OR 放宽;显式 OR/NOT 不放宽 | -| CJK → bigram → trigram → LIKE | 两字中文在可选 tokenizer 可用时走 bigram,三字可走 trigram;单字、缺失索引和显式工具全文回退,响应报告实际路径 | -| `tools/session_search_tool.py`:300 候选、默认3/最多10会话、谱系去重 | 相同预算;隐藏 subagent/tool/kanban,cron 排在交互会话后;排除已看谱系;标题优先尝试最新同名续篇 | -| 当前上下文排除、压缩及 reset 前史可见 | 活动消息与压缩归档分别标记;压缩只有在主模型成功提交新状态后生效;原文始终保留 | -| adaptive、read、scroll、browse | 首命中 ±5 消息和首尾各3条,其余只给锚点;阅读首20/尾10;锚点窗口最多各20;近期浏览先按索引限量取候选 | -| 大结果展示限制 | 首尾1200、发现/窗口4000、阅读2000字符,保留截断标志与原始长度;不会把展示裁剪当成原文存储限制 | -| 分批 FTS 回填、高水位与进度 | 回填期间新写立即建索引,删除只对已索引行发送 FTS delete;游标与每批索引原子提交,可中断续跑;批次间暂停至少 0.2 秒或本批耗时的四倍,沿用 Hermes 的写入让行策略 | -| 派生索引故障不覆盖原始消息 | 确认的 FTS 虚表损坏先拆触发器并标记缺口;结构性数据库损坏明确报不可用,不擅自删除或重置用户库 | - -## 路径、隔离和生命周期 - -文件位于 `config/agent/runtime/history/users//state.db`,`user-key` 与现有用户记忆一样来自不可逆稳定身份键。工具参数不能提供其他用户、profile、数据库或扩展库路径,管理员 Agent 也不隐式搜索其他用户的会话。目录权限为 0700,数据库文件为 0600;WAL、短连接和宿主有界 worker 分担读写,不在事件循环中扫描 SQLite。 - -主库的 `agentchat.agent_messages` 仍是恢复快照,独立消息库是压缩前的证据源。升级前尚存快照通过只读适配器分批搬迁,完成游标保存在独立库;完成后查询不访问主库。结果中的 `legacy_import.complete=false` 表示旧历史还没搬完;`legacy_snapshot` 消息不能用导入时间推断实际事件发生时间,更不能声称恢复了升级前已丢失的消息。 - -后台索引与快照搬迁由 `TaskRegistry` 持有,每用户最多一个、全服务最多八个任务。每批归还数据库 worker,进度落盘后才进入下一批。关停取消不会把半批当成成功,重启或再次使用时可继续。索引未就绪时结果公开能力状态,旧消息仍可经过受限回退查找。 - -用户定时任务按 `cron` 独立记录,仍可搜索但降低排名;没有真实用户身份的内部任务不进入个人历史。恢复快照自动过期不作为独立库的删除事件;独立库自己遵循 `DATA_CLEANUP_ENABLE` 与 `DATA_CLEANUP_AGENT_CHAT_DAYS`,后台维护按自身 `last_active` 分批清理,零天关闭自动清理。显式会话删除先删除独立证据并记录墓碑,再删除主库显示记录;主库失败时可重试,不能声称两个数据库具有同一个事务。 - -## 中文分词器 - -标准 FTS5 和 trigram 来自 SQLite。CJK tokenizer 源码在 `native/fts5_cjk/`。Docker 构建按目标架构编译,使用实际运行时 Python 验证后安装到 `/opt/venv/lib/moviepilot/libfts5_cjk.so`,不会被 `/config` 挂载遮住。CLI 的 `setup`、`install deps`、后端更新会使用所选 venv 编译和验证,同源码、架构且加载验收通过时复用已有文件。运行时优先加载 `/lib/moviepilot/libfts5_cjk.so`,失败或不存在时兼容旧位置 `config/agent/runtime/lib/libfts5_cjk.so`。Agent 请求不会调用编译器,每次连接只临时开启扩展加载,加载后立即关闭。 - -已有 CLI 安装可以运行 `moviepilot install cjk --venv /path/to/venv` 补装,追加 `--check` 只验证。缺少编译器或 SQLite 不支持扩展时,自动安装会明确告警;显式补装/检查返回非零退出码。Docker 旧镜像需要更换为包含本扩展的新构建,单纯更新后端源码不会补出本地库。编译器要求和 Docker 检查命令见 [安装说明](../native/fts5_cjk/README.md)。 - -没有 CJK 扩展时,两字中文会走 LIKE;这与 Hermes 的能力回退一致,但不等于索引速度。SQLite VM 查询期限限制扫描,超时返回 `query_timeout`/未完成,不能解读为“没有相关内容”。安装扩展后的旧消息在后台分批建索引,期间继续报告索引未就绪。不要跨平台复制已编译二进制。 - -扩展安装验收使用内存库实际索引“电影订阅成功”并查询“订阅”;这只证明运行环境能力。各用户历史索引完成后,`session_search` 返回 `index_status.messages_fts_cjk=true`,符合条件的两字查询返回 `search_path="cjk"`,才说明该次查询确实使用了中文索引。 - -## 能力边界和验证 - -FTS5 是词项检索,不保证任意语义改写都能命中。模型应依据结果用同义词、对象编号、OR、时间区间和已看会话排除继续查询,再用锚点上下文核实。用户明确提供的 URL、文件或实时系统仍然优先读取,不能仅凭历史无命中判断目标不存在。完整存储也不等于完整展示,不能用被截断的单条证据断言工具成功。 - -`tests/test_agent_recall_persistence.py` 覆盖身份隔离、FTS 查询语义、CJK 原版分词器、索引回填并发写删、原文/展示分离、压缩可见性、真实 LangChain 工具回执、超时、删除与保留期;所有数据均为临时测试数据,无真实业务网络请求。查询计划验证 FTS 虚表和会话索引;规模与真实模型评估应单独报告数据量、索引能力、耗时和未验证范围,不能由单元测试通过推断智能提升。 - -阶段五补充了真实 LangChain 图回归:最终请求压缩完成后,以实际已提交状态的消息 ID 标记移出原文;模型调用失败时不提前提交压缩。成功的同一会话仍可从独立 FTS 证据库召回被压缩内容,下一会话读取的是原文证据而非压缩摘要。后台学习组合测试进一步验证十次真实工具回合触发复盘,记忆与技能落盘后由同一用户下一会话加载,另一用户目录保持隔离。 diff --git a/docs/agent-sources.md b/docs/agent-sources.md deleted file mode 100644 index ba5e0009f..000000000 --- a/docs/agent-sources.md +++ /dev/null @@ -1,20 +0,0 @@ -# Agent 派生实现来源 - -本文集中记录派生实现的固定来源,代码注释只描述实际行为。 - -以下实现参考或移植自 [NousResearch/hermes-agent](https://github.com/NousResearch/hermes-agent/tree/f42f579cf8bac4918ac9599bece71618afadd846),固定提交为 `f42f579cf8bac4918ac9599bece71618afadd846`。上游版权为 Copyright (c) 2025 Nous Research,采用 MIT 许可;随代码保留完整许可和版权声明。 - -| 本地实现 | 上游来源 | 许可文件 | -|---|---|---| -| `app/agent/history/query.py` | `hermes_state_search.py` 的查询规范化和索引路由 | [MIT](../native/fts5_cjk/LICENSE.hermes) | -| `native/fts5_cjk/` | 同名目录的可选 CJK 分词器;具体作者及 SQLite 头文件说明见目录 README | [来源与许可](../native/fts5_cjk/README.md) | -| `app/agent/learning/matching.py` | `tools/fuzzy_match.py` 的九级模糊匹配 | [MIT](../app/agent/learning/LICENSE.hermes) | -| `app/agent/learning/threats.py` | `tools/threat_patterns.py` 的提示注入检测模式 | [MIT](../app/agent/learning/LICENSE.hermes) | -| `app/agent/learning/prompts.py` | `agent/background_review.py` 的记忆及技能复盘提示词;适配宿主工具名称与所有权操作 | [MIT](../app/agent/learning/LICENSE.hermes) | -| `app/agent/guardrails/controller.py`、`results.py` | `agent/tool_guardrails.py` 的循环检测与工具回执分类 | [MIT](../app/agent/guardrails/LICENSE.hermes) | -| `app/agent/guardrails/completion.py` | `agent/agent_runtime_helpers.py` 的回复结束纠偏;增加同规则的中文表达 | [MIT](../app/agent/guardrails/LICENSE.hermes) | -| `app/adapters/system/code/runner.py`、`client.py` | `tools/code_kernel.py` 的持久 cell runner 与 `tools/code_execution_tool.py` 的本地 RPC 客户端及辅助函数;替换环境变量前缀、按宿主 schema 生成签名、按字节保存 spill,父死亡及主动退出时收口进程组/树 | [MIT](../app/adapters/system/code/LICENSE.hermes) | - -Python 会话生命周期与 RPC 设计另参考 `tools/code_kernel.py`、`tools/code_execution_rpc.py`、`tools/code_execution_env.py`:持久解释器、逐 cell 上下文、默认300秒/50次调用/4个空闲LRU/1800秒、后台回收及七天残留清理。宿主采用异步进程和 TaskRegistry,并注入既有有界线程池,未移植上游独立线程注册表;只读 helper 范围由 MoviePilot 的真实工具与 API 副作用合同决定。当前不实现远程 kernel,不把本地支持描述成远端后端对等。 - -存储与检索行为的对应关系见 [历史消息检索](agent-history.md),后台复盘和执行纠偏的宿主边界见 [Agent 文档](agent.md)。来源一致不意味着未经验证的平台或运行场景已经通过验收。 diff --git a/docs/agent.md b/docs/agent.md deleted file mode 100644 index c02fd15f1..000000000 --- a/docs/agent.md +++ /dev/null @@ -1,214 +0,0 @@ -# Agent 复杂任务执行与恢复 - -MoviePilot Agent 通过模型、Skills、工具和会话状态共同完成任务。本次增强关注长任务的连续性:明确目标和检查结果,按需补充工具,并在执行中断后保留已知事实。能力对齐路线与每轮真实运行验收见 [Agent 与 Codex Harness 对齐路线](refactor/agent-codex-parity-plan.md)。 - -## 提示词与任务边界 - -核心提示词同时支持媒体管理、运行诊断、插件开发和 Bug 修复。媒体查询、订阅和整理优先走业务工具;用户要求编写插件或修改源码时,进入代码工作流:读取项目规范和现有改动、确认宿主代际与接口、实施最小修改、运行相关验证。插件 Skill 按实际 V3/V2 宿主选择源码目录、索引、依赖清单和开发指南。 - -用户明确要求的动作、对象和范围可以直接执行并验证,已有授权不重复询问;目标不清楚或需要扩大范围时才补充确认。代码修改本身不代表授权安装、重载、重启或发布,受自动重载监视的源码也要考虑即时运行影响。宿主明确要求的权限与确认检查仍必须满足。 - -完成标准对应用户请求:订阅创建并核实不需要等待未来更新,下载提交不代表已经入库,整理排队不代表整理完成,定时任务建好不代表未来执行成功。代码报告区分本地修改、测试、运行验证和发布。进度按有意义的发现或阶段结果更新,子代理结果由主 Agent 综合后说明。 - -Skill 目录仅提供发现信息,具体操作契约通过 `read_skill` 按需读取;操作白名单仍由宿主校验。人设负责表达风格,不能覆盖渠道格式、用户范围或宿主权限。网页、种子描述、文件名、NFO、日志、记忆和子代理报告都只是证据,不能授予写操作权限。 - -反馈 Issue 时,预览脚本返回完整脱敏预览和“确认提交 / 修改内容 / 取消”选项。支持交互的渠道通过 `ask_user_choice` 一次展示预览与按钮;点击确认后直接提交,不再要求手工重复确认。不支持按钮时使用文字确认,修改草稿后须重新预览并确认。GitHub 错误报告、功能请求模板与反馈脚本均支持 `CLI` 运行环境。 - -这些规则借鉴了 [OpenAI 的 Codex 提示词指南](https://developers.openai.com/cookbook/examples/gpt-5/codex_prompting_guide)中的持续执行与验证、[Anthropic 的上下文工程实践](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents)中的按需加载,以及 [Hermes 的提示词分层](https://hermes-agent.nousresearch.com/docs/developer-guide/prompt-assembly/)。具体授权和完成标准以 MoviePilot 的媒体业务与宿主契约为准。 - -## 任务计划 - -复杂任务可以使用内部 `update_plan` 工具保存目标、步骤、状态、检查依据和阻塞原因。例如“排查下载后未入库”可以依次检查下载状态、整理队列、目标目录和媒体库。 - -- 简单查询不需要创建计划。 -- 步骤支持 `pending`、`in_progress`、`completed`、`blocked`;完成和阻塞状态需要填写依据。 -- 计划单独保存在会话图状态中,每次模型请求都能读取;压缩旧消息不会删除计划。 -- 计划快照随聊天消息保存,在会话图重建后恢复。用户询问进度时继续当前目标,明确开始新任务时替换计划。 -- 计划是模型记录的工作状态,不代表工具执行结果或写操作授权。仍需通过工具核实实际结果。 - -## 代码修改与 Pull Request - -用户明确要求提交 PR 时,使用 `skills/submit-pull-request/SKILL.md`。该流程把 Git -clone 作为代码修改入口:先通过 GitHub API 复用或创建当前 Token 用户的 Fork, -再从上游基线创建隔离工作分支和本地 clone;后续 Agent 的修改与测试都必须发生在 -该 clone 中。没有本地 Git 仓库时不是降级为文件上传,而是先执行 clone。 - -流程在提交前生成真实 Git 工作树的变更预览,并要求用户明确确认;确认后才执行本地 -`git add`、`git commit`、`git push`,最后通过 GitHub API 创建或复用 PR。提交脚本会 -重新校验上游基线、Fork parent、origin remote、文件摘要和推送后的 Commit SHA, -不执行强制推送、合并或自动关闭 Issue。Token 只从运行时 GitHub 设置读取,不出现在 -命令行、预览和日志中;网络中断可以复用同一 payload 重试。 - -## 运行中补充消息 - -Web 会话 ID 只用于定位,不能授予访问权限。已有记录按持久化 `user_id` 授权; -只有无 ID 的旧记录才按当前登录用户名兼容匹配。新会话按稳定用户 ID 分区, -包括上传附件后尚未保存历史的会话,账号改名不会改变身份,回收用户名不会继承他人历史。 -越权续聊返回流式错误,越权上传返回 404,展示历史写回前再次检查归属。 -Web 管理员可以续聊其他渠道的历史,保留原会话归属,但工具执行人仍是当前登录管理员; -后台或协议入口的管理员工具上下文不自动授予跨用户历史读取权,缓存复用也重新检查显式授权。 - -WebAgent 和消息渠道在 Agent 正在运行时仍可提交文本和附件。消息渠道只有同一用户、渠道配置、聊天位置和权限的输入才会追加到当前任务;其它来源保留独立排队处理。宿主为每个会话生成消息 ID,先把消息放入有界 steering inbox;下一次模型调用前由中间件把它作为真实 `HumanMessage` 写入图状态,WebAgent 同时通过当前 SSE 报告 `queued`、`applied`。提交和运行收尾共享原子边界,收尾竞态中已接受的消息会在同一 worker 内继续处理,停止会话不会继续派发新的工具动作。补充消息不会启动第二张 Agent 图,也不会替换当前输出回调。 - -补充消息实际加入上下文后,后续回复会另起一条消息。WebAgent 在该边界插入用户气泡并创建新的助手气泡;支持消息编辑的渠道会收口旧回复,再以新消息承接后续正文和工具进度,回复目标也切换到本次补充消息。尚未加入上下文时,当前回复继续更新。队列已满或运行已收尾时,输入回退到普通会话队列,不会静默丢弃。 - -## 稳定偏好与历史证据回忆 - -稳定记忆位于 `config/agent/memory`:`MEMORY.md` 保存公共规则,当前用户的环境事实位于 `memory/users//MEMORY.md`,跨任务偏好位于同目录 `USER.md`。这些稳定文件默认注入上下文,公共记忆先于用户记忆;其它主题 Markdown 通过 `search_memory` 按需检索,支持 `primary`、`topic`、`all`、字面关键词、文件路径、条数和显式正则。 - -`session_search` 按 Hermes 的四种形态返回实际消息,不额外调用模型摘要:`query` 发现相关会话,`session_id` 阅读会话,附带 `around_message_id` 围绕锚点滚动,无参数浏览最近会话。默认搜索 user/assistant,显式 `role_filter="tool"` 搜完整工具原文。默认 BM25 相关性排序,支持短语、AND/OR/NOT、前缀、会话开始时间区间、newest/oldest 和已看会话排除;最多 300 条候选经来源降级、会话谱系去重后返回默认 3、最多 10 个会话。adaptive 只为首命中展开前后各 5 条和首尾各 3 条,其余保留精确锚点,避免大量无关历史占满上下文。 - -消息库独立放在 `config/agent/runtime/history/users//state.db`,使用 SQLite FTS5、WAL、短事务与有界宿主 worker;不依赖业务主库的 SQLite/PostgreSQL 类型。按用户分别存库,工具不能指定用户、profile 或文件路径。宿主在上下文压缩前、模型回复后、工具结果截断前采集完整脱敏文字,工具调用关联单独保存,图像/音频二进制及合成压缩摘要不入库。正文保留与检索展示上限分开:工具标准索引只取 8192 字符,原文不因此裁剪;阅读默认首 20/尾 10 条,每条 2000 字符,锚点窗口每条 4000 字符。 - -标准词索引采用 external-content FTS5;中文/子串使用 Hermes 原版可选 `cjk_unicode61` tokenizer 和 trigram。短词、显式工具全文、尚未就绪的索引按 Hermes 路由回退 LIKE,并通过 SQLite VM deadline 限制扫描;超时明确返回未完成,不伪报没有匹配。索引增量回填使用高水位/进度与一致触发器,结果暴露索引状态。见 [历史检索与 Hermes 对标](agent-history.md) 中的路由、安装与验收说明。 - -旧主库只作为一次性恢复快照来源,后台分批搬迁;搬迁完成后检索不再读取主库。旧证据标记 `legacy_snapshot`,无法恢复升级前已被丢弃的消息,时间不能冒充原始事件时间。独立库复用 `DATA_CLEANUP_ENABLE` 与 `DATA_CLEANUP_AGENT_CHAT_DAYS`,在用户历史后台维护时按自身活动时间分批清理,零天关闭自动删除;不通过主库外键驱动。用户显式删除会话会同时删除独立证据并保存删除墓碑,防止在途写入或旧快照搬迁复活。跨库不假称原子事务,先删除证据,主库失败时安全重试。 - -压缩后的原始消息仍可查,当前活动上下文中的重复消息则排除。已废弃逐轮活动摘要生成、清理、检索入口和每任务强制首工具检索;旧活动 Markdown 原地保留,不再读取或续写。检索仅在历史与当前任务相关时使用;历史证据不能替代实时业务核验,也不授予新的写权限。 - -用户明确要求记住的跨任务偏好通过 `memory(target="user")` 写入当前用户的 `USER.md`,环境事实使用 `target="memory"`;任务类别的操作方法和特定偏好写入个人技能。旧用户 `MEMORY.md` 原文保留,不自动搬迁或裁剪;只有系统管理员可以写入全局公共记忆。一次性请求、临时状态和凭据不应写入记忆。检索到的文件内容只是上下文,不能覆盖系统或用户指令。 - -Agent 人格是全局运行时配置,不按用户隔离;所有用户共享当前激活人格。普通用户可以查询人格列表,但只有系统管理员可以切换激活人格或修改人格定义。 - -## 按需发现工具 - -设置 `LLM_MAX_TOOLS > 0` 时,Agent 首轮仍只筛选一批相关工具。后续读取 Skill 或发现新问题后,可以通过内部 `search_tools(search, limit)` 按名称、说明或标签搜索当前会话工具目录,并在下一次模型调用获得匹配工具的完整参数定义。 - -每次最多返回 8 个匹配工具,并保留最近 16 个额外工具候选;再次发现会刷新候选优先级。匹配支持工具精确名称和有限中英文能力别名,例如“整理”对应 transfer。没有匹配结果时不会展开全部工具。 - -额外工具的参数定义共享最多 4096 tokens、且不超过已知模型窗口 10% 的预算,总输入保留 15% 余量。搜索先报告候选,下一次模型调用根据统一预算明确报告实际启用和未启用原因。初选、常驻和供应商工具保持可用。发现状态仅作用于当前用户请求,新的用户请求重新筛选。`LLM_MAX_TOOLS` 约束首轮筛选数量;发现不会安装工具、连接新的 MCP 服务或扩大执行权限。 - -`update_plan`、`search_memory`、`session_search`、`memory`、`skills_list`、`skill_view`、`skill_manage`、`search_tools`、`read_tool_result` 和 `get_tool_execution` 是 Agent 内部会话能力,不通过外部 MCP 或 `moviepilot tool` 发布。 - -## 工具结果与大结果续读 - -宿主统一区分 `succeeded`(成功)、`failed`(失败)、`pending`(任务未完成)、`unknown`(实际结果未知)。明确的业务失败、MCP `isError` 和普通异常会以失败消息返回模型;异步任务已提交不等于任务完成。普通业务对象中的错误说明或下载器状态不会被当成整个工具的失败或执行状态。 - -超长结果在当前会话图中临时保存,预览带 `result_id` 和 `next_offset`。Agent 使用 `read_tool_result` 按 Unicode 字符位置续读,无需重新执行原工具。每条完整结果最多 1 MiB,每个图最多 8 条、合计 4 MiB,15 分钟后过期;容量超限、图重建和进程退出也会使编号失效。无法保存的结果明确要求缩小查询范围。不同图线程不能互读,管理员结果在降权后不能续读。结果原文只短期驻留内存,不归档到磁盘或日志。 - -`execute_command(action="run")` 返回结构化 JSON:`exit_code`、`timed_out`、`execution_outcome` 和 `status` 表示实际执行结果,`output` 保存输出预览,`output_file` 指向超长输出的临时归档。仅正常退出且退出码为 0 时成功;非零退出或已停止的超时命令为失败,无法确认进程结束时为未知,不能凭“有输出”判断成功。超时和取消不会撤销命令已经产生的外部副作用。取消继续向外传播,同时回收输出读取任务、关闭归档文件;`run` 与 `start` 都支持 `env`,并采用同一工作目录与解释器策略。 - -项目安装目录存在 `venv`,或 Docker 的 `VENV_PATH` 环境存在时,`execute_command` 的一次性命令、pipe 和 PTY 会把该环境的 `bin`(Windows 为 `Scripts`)放在子进程 `PATH` 最前,设置 `VIRTUAL_ENV`,并让裸 `python`/`python3` 使用项目专用入口 `moviepilot-python`;入口不存在时回退到环境中的标准 Python。执行 Python 直接使用 `python -m ...`,尤其是下载器、SMB、媒体服务器等需要系统权限的操作。安装依赖优先使用项目 `uv pip ...` 或绑定环境中的 `python -m pip ...`,不要改用系统解释器或 `sudo pip`。 - -`run`、后台 pipe 和 PTY 共用 `cwd`、`shell`、`login` 解析。省略 `cwd` 使用 MoviePilot 根目录,相对路径也相对该目录,并支持 `~`;错误目录在进程启动前拒绝。POSIX 默认使用配置的 `SHELL`(未配置时 `/bin/sh`)且不启动登录模式,显式 `shell` 可选择已安装解释器,`login=true/false` 控制其支持的登录行为。登录启动文件可能改变目录或环境;PTY 本身不再隐式开启登录模式。Windows 未显式指定时保留 Git Bash → PowerShell 7 → cmd 的选择顺序与 UTF-8 策略;不支持的解释器/登录组合明确失败。回包的 `shell`、`login` 说明实际策略,默认解释器不可用不会阻断其它 Agent 业务能力。 - -管道会话可用 `write(input_text="末段输入", close_stdin=true)` 在交付末段后关闭输入,或用空输入显式关闭;输出仍通过 `read/wait` 读取,`stdin_closed` 表示输入已关闭。普通空 `write` 不发送 EOF;输入关闭后不能再追加数据,重复空关闭可确认已有状态。`run` 的 stdin 始终为 EOF。PTY 的输入和输出共用端点,因此拒绝 `close_stdin=true`,也不会先写入附带输入或关闭输出;PTY 控制字节的行为取决于终端模式,pipe 中 `\u0003`、`\u0004` 只是普通字节。 - -终端句柄绑定宿主创建的用户及任务作用域,知道 `session_id` 不代表可以操作它。对话正常多轮和图重建保留同一作用域;每次定时执行按真实 `run_id` 单独持有终端,执行结束、失败或取消后回收。清空对话、空闲回收或同会话更换用户时封闭旧作用域;只清理它拥有的进程,未收敛时保留记录供重试。停止当前交互推理仍保留已交付的后台终端,后续对话可继续读取或显式终止。 - -父任务使用 `task` 或 `subagent_task` 的 `terminal_sessions=[{"session_id":"term_…","actions":["read","wait"]}]` 显式分享指定终端给该子任务。批次及管道分别在每个任务定义里声明;提示词中提到句柄不会授权,兄弟任务也不会继承。子任务保有独立作用域,结束时撤销分享但不终止父进程;当前只读子代理策略继续限制动作。宿主控制授权与工具角色权限分别校验,共享不能扩大角色权限。 - -缺少、已关闭或无权访问的终端均返回 `terminal_access_denied`,不回显其他任务的命令和输出。进程重启后不恢复旧句柄,也不会自动重跑命令。内部 `MoviePilotToolsManager` 每个实例独立持有作用域,通过 `close()` 收口;直接调用命令工具必须由宿主绑定真实上下文,工具 JSON 不能声明或覆盖身份。 - -`interrupt` 仅发送一次 POSIX `SIGINT` 或支持的 Windows `CTRL_BREAK_EVENT`,不等待升级强杀,也不把会话标记为主动终止;实际命令可以处理信号后继续运行,也可能自行退出。回包含实际 `signal` 和 `signal_sent`。不具备该控制能力时明确失败;`kill` 仍负责终止并在需要时强杀,但未知名称、无效编号和没有真实映射的信号会在任何状态修改前拒绝。两种输入控制均不增加只读子代理权限。 - -后台命令 `start` 默认最多等待 250ms 的首次输出,可用 `yield_time_ms=0` 立即返回。后续 `read/wait/write/interrupt/kill` 用返回的 `output_until_seq` 和 `output_until_offset` 一起续读:前者是完整交付的最后一个分片,后者是下一分片中已交付的 UTF-8 字节位置。首次传 `since_offset=0` 开启分片内分页;不传 offset 时保持完整分片模式,小页装不下一个分片会明确提示调整限额。`last_seq` 只表示已经产生的输出,不能用于跳过未读内容。 - -`wait` 在有未读输出时立即返回,等待中产生新输出或读取结束也会唤醒;`timeout_ms=0` 只查询,不终止命令。进程退出后仍可能有尾部输出,获取完整日志应继续到游标读完且 `output_complete=true`;退出码缺失时仍为 `unknown`。缓冲保留窗口之外的缺口由 `output_lost=true` 明确标记。若 `start/write/interrupt/kill` 已执行动作但返回 `output_error`,会保留会话 ID 和未消费游标;只需调整 `max_bytes` 后用 `read` 获取输出,不应重新执行动作。首次 `start` 尚未交付会话 ID 时被取消会回收该进程,取消已有会话的 `wait` 则保留进程。 - -终端分页还受最终 JSON 字符预算约束:必要时减少本页正文并重新计算游标,避免 JSON 转义后又被通用工具预览截断。过长的命令、目录和错误回显会带显式截断标记;会话内部保留完整命令。 - -## 工具图片与模型视觉 - -`browse_webpage(action="screenshot")` 和 `view_image(url=...|file_path=...|image_data=...)` 在 Agent 中返回真实图像块和有限来源说明。图像在通用文本截断之前处理,最终请求按完整工具回复批次把 `ToolMessage` 中的 `image_url` 块组装成带 `TOOL_OBSERVATION_MARKER` 的临时 `HumanMessage(content=[text, image_url, ...])`,再发送给支持图片的模型;原始用户消息、工具调用 ID 和授权不变。视觉中间件只修改出站副本,不把临时图片观察写回持久会话历史。`view_image` 的远程 URL 只允许通过公网安全校验的 HTTP(S) 地址,本地路径继续遵守 Agent 文件访问边界,`image_data` 支持纯 Base64、data URL 和原始字节。 - -每次模型请求都检查实际模型资料和 `LLM_SUPPORT_IMAGE_INPUT` 开关。总开关关闭时不投递工具图片;开启时优先采用实际提供商、模型及端点对应的缓存模型目录,目录能力未知时再参考依赖包的静态档案,两者均未知时沿用用户开关。目录明确声明支持图片时,依赖包中的旧“不支持”档案不会再剥离图像;目录明确为纯文本时仍保留保护。已知纯文本模型会得到明确的“未接收工具图片”说明。服务明确拒绝图片时,只对该次模型调用做一次文字回退,随后本轮沿用文字观察,不重复执行图片工具或其他工具;认证、限流等错误保持原来的错误语义。临时观察不会写入会话历史。 - -浏览器截图最多约 150 KiB 原始 JPEG,`view_image` 图片最多约 768 KiB,实际输入还受 1 MiB data URL 和像素上限约束;超限或数据无效时明确失败。运行图可保留完整图片,持久化与中断恢复仅保存来源及图像未保留说明,日志不记录图片数据。恢复后的旧图片不能用于声称已看到视觉细节,需要重新调用图片工具获取当前内容。视觉 token 预算是估算,实际消耗以模型供应商的 usage 为准。 - -用户请求登录时,图形验证码支持 OCR 与模型视觉两条路径。依赖浏览器会话、blob/canvas 或每次请求会换图的验证码,优先用 `browse_webpage(action="screenshot", selector="已观察到的验证码选择器")` 截取当前显示元素,避免独立下载改变验证码;不传 `selector` 时仍截取当前视口。元素选择器必须唯一,截图失败不会退回其他元素。`recognize_captcha` 使用配置的 OCR 服务,识别失败或服务异常会保留 `success=false` 并返回视觉识别的恢复指引。Agent 按浏览器 Skill 在同一会话和标签页中尝试看图,再决定刷新或手工输入;`view_image(url=...)` 不继承浏览器 Cookie,只用于可独立读取的图片。图片输入关闭、模型拒绝图片或图像不可读时不得猜测。OCR 与视觉合计最多提交三次,每次均以网站实际响应验证结果。 - -## 写工具的持久执行记录 - -生产 Agent 在副作用之前向 `agentinvocation` 表提交原子认领。记录只包含用户、会话、工具身份、参数指纹、执行 token、状态和固定宿主文案,不保存原始参数或工具输出。无法认领时不执行写入;相同调用 ID 携带不同参数时拒绝执行。 - -- 同一用户请求内,参数规范化后完全相同的 MoviePilot API 写调用共用一个执行身份,防止模型并行或重复调用;明确的新用户请求可再次执行已完成操作。 -- 普通工具使用原始工具调用 ID 去重,不把重复执行同一条诊断命令误合并为同一业务意图。 -- 上一轮相同 API 参数仍为运行中或未知时,新请求先核验旧记录,不盲目发起第二次写入。已确认提交的异步请求保存为 pending:同一执行身份不重复提交,新的明确用户意图可以再次提交,pending 不代表外部任务已完成。`get_tool_execution` 可凭宿主返回的 `invocation_id` 查询当前用户、当前会话的最近观测状态。 -- 当前自动核验支持非敏感设置完整替换:通过 `config.system.get` 只读确认目标值,匹配后收口为成功并跳过重复写入。敏感、未匹配、列表追加及其他无法确定的副作用保持未知,不允许模型自行把它改成成功或“未执行”。 -- 写入请求发出后的传输中断、超时和响应正文不可用保留未知;调用前参数错误和明确 HTTP 拒绝仍为失败。 -- 冷启动将未收口的 running 转为 unknown,并更换 token。旧执行者不能覆盖新恢复状态,也不会仅因时间过去而自动重放。 - -成功、失败和已确认提交的回执随会话删除回收;没有聊天记录的后台同类回执按共享会话保留期清理。pending 是已确认提交的历史,实际异步任务由原任务系统管理,不是待执行队列。`DATA_CLEANUP_ENABLE` 和保留期 0 的禁用语义保持一致,running/unknown 恢复状态不会按时间删除。这些机制不构成任意外部系统的“恰好一次”事务保证。 - -## 异常和取消后的继续 - -对已启用聊天持久化的会话,执行异常或用户取消时,Agent 在释放会话图之前尝试保存脱敏恢复快照。已收到的工具回执保留;未收到结果的调用明确标记为“结果未知”,不能当成未执行或已成功。 - -用户随后说“继续”时,Agent 可以读取已有结果和计划,从剩余工作继续。下载、修改配置、删除文件等操作如缺少回执,应先只读核验实际状态,再决定是否重试。取消仍会终止当前轮次,不会自动重放工具。 - -聊天快照保存采用有界等待。进程强制退出、持久化失败,或工具在中断后才完成的外部副作用,不能保证被聊天快照记录;写工具通过上面的持久执行记录保留恢复边界。 - -## 子代理连续工作 - -默认子代理在宿主执行边界强制只读。多功能 API 网关按具体 `operation_id` 判断副作用,继承管理员身份也不能修改、删除、触发下载或服务操作;敏感设置原值读取同样不开放。终端仅允许读取/等待已有输出;浏览器仅允许观察已有页面,不允许导航、输入、点击、脚本、关闭或修改 Cookie。外部 MCP 的兼容 Read 标签没有动作级保证,需交由主代理处理。正常业务查询和声明只读的本地工具仍可使用。 - -一轮结束会回收未完成的子任务,正常收敛后同一会话下一轮仍可委派。会话关闭时永久停止接收子任务;无法在等待上限内收敛的任务仍由原 owner 持有并封闭新提交。 - -子代理沿用主 Agent 的最终请求预算和上下文压缩机制。长调查可以压缩旧消息;最新单条工具事务或固定提示本身超过模型窗口时仍会明确失败,不会静默删除关键内容。 - -## 后台学习与个人技能 - -对齐 Hermes 的回复后复盘:已认证交互会话每累计10个用户轮次触发记忆复盘,每累计10次带工具调用的模型迭代触发技能复盘,调用 `skill_manage` 重置技能计数。最终回复交付并保存成功后才启动;取消、失败、定时任务和内部后台任务不启动。新前台输入先取消旧复盘,最多等待2秒;超时的任务仍有 owner,后续写入由取消令牌封闭。 - -复盘使用相同模型、推理配置、最终系统提示词和工具 schema,完整复制消息,首次请求保留原缓存前缀。执行白名单单独限制到技能维护和必要的读取;只有记忆触发时才能执行 `memory`。终端、业务 API、普通文件写入和外部 MCP 均不能在复盘执行。当前存在供应商原生工具的请求无法由本地白名单约束,因此跳过自动复盘。每次最多16轮,累计输入预算为模型窗口的75%、最高600000 token;未知窗口使用120000。后续上下文压缩独立于主图,其模型用量也累计归属父会话;后台文本不覆盖主回复或归档。 - -个人技能在 `agent/runtime/learning/users//skills`,独立于全局市场技能。`skills_list` 枚举目录,`skill_view` 按需读取全文和支持文件,`skill_manage` 最多20个有序操作共用锁与错误回滚边界。支持创建、局部补丁、完整重写、支持文件增删和独占批次删除;补丁沿用 Hermes 的九级匹配、歧义拒绝、Unicode/缩进和转义保护。后台修改已有文件必须在本次复盘重新读取该文件,读后内容变化也须重读。普通 I/O 失败回滚文件,不能视为跨文件断电事务保证。后台删除必须提供已存在的合并目标,并将完整包和来源保留到个人库 `.archive`;前台明确删除仍是硬删除。固定技能均不可删除。 - -公共、市场和外部技能只读;手工或前台创建的技能默认属于用户,后台只能维护自己创建且未固定的个人技能。来源、用户身份和所有权由宿主写入,不接受模型参数。个人技能不能声明 API/工具授权,也不能覆盖公共同名技能。复盘优先修正当前加载的同类技能,其次已有类别或主题支持文件,最后才创建新的类别技能;不保存逐次任务流水,也不将未解决的失败包装成可靠方法。稳定事实、跨任务偏好和操作方法各有一个归属,不重复保存。 - -后台复盘是宿主启动的独立模式,允许在上述范围内维护记忆和个人技能,不受前台“只有用户要求编写技能才修改”的限制。复盘没有更新数量要求:没有新的、持久且有证据的信息时不写入就是有效结果。记忆、技能和联合复盘分别遵守启用范围,不续做业务任务、不向用户发送消息,也不把未经验证的修复方案保存为可靠经验。 - -`memory` 按完整条目维护两份文件,默认预算 MEMORY.md 2200字符、USER.md 1375字符。`old_text` 仅定位条目,`replace` 的 `content` 替换整条。批次按最终大小校验,任何操作失败均不写入;禁止批次清空整库,明确删除最后一条需单次 `remove`。读取失败和磁盘格式漂移不会被当作空文件覆盖;注入模式在写入时拒绝,在加载快照时屏蔽但保留磁盘原文。会话内提示词使用首次加载的冻结快照,写入从下一会话生效。 - -后台新增稳定记忆可直接保存;替换或删除整批保存为待确认提案,固定原条目防止确认时误改新内容。用户在聊天输入以下命令即可管理,不经模型审批: - -- `/memory pending`:列出提案;`/memory pending ` 查看该提案的完整原条目和提议操作。 -- `/memory approve ` 或 `/memory discard `:应用或丢弃一项提案;原条目已变化则拒绝应用并保留提案。 -- `/skills restore <名称>`:恢复后台合并归档的完整技能,拒绝覆盖现有或公共同名项。 -- `/skills adopt <名称>`:允许后台维护当前用户的一项个人技能。 -- `/skills pin <名称>` 或 `/skills unpin <名称>`:禁止或恢复个人技能的自动维护;公共技能不能接管。 - -这些命令限定到当前用户;工具参数和历史消息不能代替用户输入。旧活动摘要文件保持原地,不参与学习检索。实现参考固定 Hermes 提交 `f42f579cf8bac4918ac9599bece71618afadd846`,派生匹配器、威胁模式和复盘提示词保留 MIT 许可;具体对应关系见 [派生实现来源](agent-sources.md)。 - -## 执行纠偏与停止 - -工具循环按 Hermes 固定源码的阈值和恢复规则判断:同参数失败2次提示、5次后阻止继续重放;同一工具不同参数失败3次提示、8次可停止,但终端诊断等允许连续失败的工具不会因此被中断。相同只读结果2次提示、5次后阻止重放;连续相同调用3次提示,并检测最近64条中的2–4步重复循环。成功修改后重试属于新实验,改变参数或真实结果也会重新计数。 - -Web、兼容 API 和受父任务监督的子代理默认只警告,渠道网关与后台任务默认强制停止;网页搜索与子任务启动分别使用每轮50次的预算,按 Hermes 语义在批次派发前检查,已放行的同一并行批次可以越过该阈值。终端与子任务的明确 read/wait/status/list 动作允许正常轮询,不把所有同名工具动作一概豁免。每个真实图线程独立计数,按模型发出顺序处理并行回执,以宿主每次图运行的身份重置。检测复用已有模型包装与策略节点,不额外消耗递归步数,也不提高原有执行上限。 - -相同长结果从第二次起可在模型上下文中显示为前次结果引用,工具仍然实际执行,原始回执仍会归档。结果变化恢复完整正文,压缩移除原引用后也重新给出完整结果;超长结果存根附带本次 `read_tool_result` 引用,避免指向已淘汰缓存。未知副作用和等待状态保持显式,不能因循环停止变成成功。宿主生成的停止说明会进入流式回复,定时任务将其记录为未成功,本轮不会触发成功后的学习。 - -短回复以“接下来我会…”或 Hermes 的英文尾部动作结束却没有调用工具时,最多追加两次原版继续提示;这是相同规则的中文本地化。Codex Responses 入口额外采用原版工作区确认和极短异常碎片规则;普通数字、文件名和符合用户语言的短答案不触发。合成提示不作为用户消息归档或计入学习轮次,耗尽后明确标记部分完成。`moviepilot_turn_status` 记录 blocked/waiting/unknown/partial;这些是宿主停止状态,不是业务完成证明。 - -## Python 只读工具编排 - -管理员会话提供 `execute_code(code, reset=false)`,默认直接执行,不增加用户确认步骤。它适合三次以上带分页、分支、过滤或聚合的读取;单次读取或需要模型逐项判断的结果仍使用普通工具。先用 `read_skill` 加载对应业务合同,再在 Python 中调用当前已启用的 helper: - -```python -from moviepilot_tools import moviepilot_api - -pages = [moviepilot_api("subscription.list", query={"page": page, "count": 100}) - for page in range(1, 4)] -print(pages) -``` - -业务返回结构由已加载 Skill 定义,应先核对成功状态、分页终止条件及结果字段,再计算摘要;不要假设每个端点的列表包裹层相同。helper 返回解析后的 JSON,工具原文在进入模型预览截断前交给 Python,模型上下文只接收脚本打印的结果。`read_file`、`search_web` 也可在本轮实际启用时导入;签名从真实工具 schema 生成,可用签名写进当前模型请求并计入压缩预算。`json_parse`、`shell_quote`、`retry` 为本地辅助函数。 - -每次 RPC 自动核对当前用户、会话代次、启用工具交集、50次调用预算及 API 的精确副作用分类,再进入相同的策略、Skill 范围、工具执行与回执链。只开放真实内置工具对象;同名插件、MCP 标签和脚本声明不能扩展范围。API 仅允许 `SAFE_READ` 操作以及当前身份可读的普通或私有业务结果,例如管理员的整理队列;`PRIVATE` 不等于凭据。写入、删除、刷新、`SECRET`/`UNKNOWN` 结果、`show_secrets=true` 和未加载的 Skill 操作都会被拒绝。单次完整 RPC 结果上限16MB,超过时明确要求分页,不以不完整正文进行聚合。脚本忽略工具错误或以退出码0结束,也会保留最多5条 `tool_errors` 并令 `success=false`。 - -这是真实的持久 Python 子进程。变量、导入和数据跨 cell 保留;键包含宿主 owner 对象、启用工具集合、解释器和工作目录,不能跨用户、旧会话代次或不同工具集合误用状态。解释器使用宿主当前 Python(保留虚拟环境),工作目录沿用 Agent 的项目根目录。普通脚本异常保留已执行的赋值;`reset=true`、`sys.exit`、300秒超时、取消或 owner 关闭销毁内核。结果的 `kernel.reused`、`execution_count`、`state_reset`、`ended`/`state_lost` 描述实际状态,不能默认上次变量仍在。 - -stdout 内联50KB、stderr 10KB;超长 stdout 先脱敏,再把最多5MB正文存入当前用户的 `agent/runtime/code/users//output/`,可用 `read_file(file_path=..., start_line=..., end_line=...)` 续读。直接文件描述符输出和超出捕获上限的部分会明确标记不完整;spill 不保证包含未捕获正文。输出文件独立于内核目录,因此内核退出后仍可读取,七天后清理。空闲内核采用默认4个的 LRU 和1800秒过期;正在运行或排队的 cell 不被容量淘汰,因此高并发时可暂时超过4个。后台回收不依赖下一次调用,宿主 TaskRegistry 管理任务,文件操作使用现有 Agent 有界线程池。 - -内核启动时继承宿主部署环境,包括 `CONFIG_DIR`、`MOVIEPILOT_ROOT`、`DB_TYPE` 和数据库连接凭据,使配置读取和内核中启动的脚本与 shell 通道一致;不继承宿主 `PYTHONPATH`,并固定 UTF-8 输出。环境变量不会自动输出给模型。环境或 `app.env` 变更后,应重置内核以重新加载配置。 - -任意 Python 与管理员 `execute_command` 一样具备宿主进程权限,只读 RPC **不是操作系统沙箱**。因此普通用户、只读子代理和没有宿主 cell 上下文的外部 HTTP/MCP 直接调用不能使用这项能力;它不通过外部 `tools/list` 发布。POSIX 使用私有 Unix socket、随机令牌和父死亡管道,Windows 使用回环 TCP、随机令牌和父进程对象句柄。每个 cell 重新绑定当前上下文,结束时撤销并取消遗留 RPC,不把持久 socket 当作永久身份。当前只实现本地主机内核,没有远程终端后端。 - -## 验证范围 - -任务评测与离线轨迹验收的区别、场景及命令见 [Agent 任务评测](agent-evaluation.md)。固定响应单测不代表真实模型完成率。 - -自动化测试使用脚本模型和真实 LangGraph 执行图,覆盖工具发现和预算、计划恢复、结果状态、分页续读、持久认领和只读核验、执行中断以及连续两轮子代理委派。SQLite 测试覆盖并发认领、token 隔离、迁移和清理。测试不调用付费模型或外部服务。 - -这些机制提高执行可靠性;真实任务的理解、规划质量和成功率仍取决于所选模型、上下文窗口、工具权限及外部服务状态。模型能力需要用真实业务场景单独评估。 diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index a9be416cb..75d24237a 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -585,7 +585,7 @@ flowchart LR ## 七、AI Agent 子系统 -复杂任务的计划、工具发现和中断恢复使用说明见 [Agent 复杂任务执行与恢复](agent.md)。 +复杂任务的计划、工具发现和中断恢复由 `app/agent/` 的实现决定,本文只说明其在宿主中的位置与依赖边界。 Agent 采用**门面 + 惰性物化**设计,避免 `application → agent` 形成静态依赖边: @@ -812,9 +812,6 @@ Outbox 按 topic 有界并行恢复复用 `ThreadHelper`,生命周期关闭通 物理文件仍是稳定入口的,不应为了目录规整新增“自己映射自己”的别名,也不应在 canonical 包中 保留多余导出。 -当前架构问题、优先级、分阶段实施步骤与验收门禁见 -[`docs/refactor/optimization-checklist.md`](refactor/optimization-checklist.md)。 - --- ## 附录:相关文档索引 @@ -827,12 +824,7 @@ Outbox 按 topic 有界并行恢复复用 `ThreadHelper`,生命周期关闭通 | [`docs/rules/05-architecture.md`](rules/05-architecture.md) | 层边界、依赖方向与关键文件位置(权威) | | [`docs/rules/09-external-response.md`](rules/09-external-response.md) | 外部 HTTP 约定与统一响应格式 | | [`docs/rules/10-data-and-persistent.md`](rules/10-data-and-persistent.md) | 数据模型、迁移与缓存规范 | -| [`docs/subscribe-lifecycle.md`](subscribe-lifecycle.md) | 订阅生命周期详解 | | [`docs/mcp-api.md`](mcp-api.md) | MCP 工具端点说明 | -| [`docs/refactor/optimization-checklist.md`](refactor/optimization-checklist.md) | 当前架构差距、优先级与可执行优化清单 | -| [`docs/refactor/refactor-roadmap.md`](refactor/refactor-roadmap.md) | 多级 Goal、叶子依赖、清零条件与交付状态 | -| [`docs/v3t-runtime-governance.md`](v3t-runtime-governance.md) | V3/V3t 运行依赖、故障恢复、GIL 可观测性与兼容退场门禁 | -| [`docs/adr/0007-background-action-reliability.md`](adr/0007-background-action-reliability.md) | 后台动作 E0–E3 可靠性分级与完成语义决策 | 音乐来源回退由 `app.application.music.recognition` 持有独立来源诊断和共享祖先预算,新增一个轻量启动模块。来源顺序由现有配置冻结,Chain通过已声明的音乐目录Port获取专辑与完整曲目;领域层只负责检索计划、身份和唯一对位。新增31条内部依赖均符合分层边界,不增加SCC或具体Adapter直连;启动性能仍使用原耗时预算。 diff --git a/docs/cli.md b/docs/cli.md index d949d8f25..68255c435 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -533,7 +533,7 @@ moviepilot config describe API_TOKEN --show-secrets - `AMLL_BASE_URL` 默认使用无需 API Key 的 AMLL TTML API,也可指向兼容原生接口的自建实例。来源优先按 ISRC 查询,再严格核对歌曲、艺术家及已有专辑信息;下载的 TTML 主唱时间轴转换为 Lyricsfile 和 LRC,限流时短暂冷却并让其他歌词来源继续处理 - `THEAUDIODB_API_KEY` 用于音乐元数据及纯文本歌词兜底,默认 `123` 为官方公开 V1 Key;额外歌词来源可通过插件提供,接入方式见[歌词插件开发说明](https://github.com/jxxghp/MoviePilot-Plugins/blob/main/docs/faq/21-register-lyrics-provider.md) - `MUSIC_METADATA_TO_SIMPLIFIED` 默认开启;开启后会将识别结果中的曲名、艺术家、专辑和分类等标准音乐元数据转换为简体中文,不转换歌词与来源原始响应 -- `MUSIC_COVER_FILE_PATTERNS` 控制同目录音乐封面的文件名匹配与选择顺序,逗号分隔、大小写不敏感;附加文件整理和内嵌封面回退共用此配置,详见[音乐本地封面](music-covers.md) +- `MUSIC_COVER_FILE_PATTERNS` 控制同目录音乐封面的文件名匹配与选择顺序,逗号分隔、大小写不敏感;附加文件整理和内嵌封面回退共用此配置 - `MUSIC_LYRICS_TO_SIMPLIFIED` 默认关闭,独立控制生成的 `.lrc` / `.txt` 歌词正文转简体;时间轴、LRC 标签及来源 Lyricsfile 保持原样。已有歌词仍遵循原刮削策略,需要转换时选择“总是刮削”重新刮削 - `MUSIC_RELEASE_REGION_PRIORITY` 与 `MUSIC_RELEASE_SCRIPT_PRIORITY` 分别使用逗号分隔的 ISO 3166-1 地区代码和 ISO 15924 文字代码;仅在 MusicBrainz 候选结构同样可信时作为发行版本排序依据,最多使用前三项 - `config describe` 显示单个配置项的类型、默认值和当前值 diff --git a/docs/code-quality.md b/docs/code-quality.md deleted file mode 100644 index 6d70d6af9..000000000 --- a/docs/code-quality.md +++ /dev/null @@ -1,145 +0,0 @@ -# 代码门禁:结构指标与扩围建议 - -2026-09-30 对 `v3@b4175a4f88421cf3515eeb651b8a4febe836642f` 的门禁进行审计,并在其上调整 -复杂度与严格类型检查。提交前衔接到最新 `v3@4a2ef7570659d587c2ebbddf02177443237734dc`, -初始结构基线按该版本重新采集;宿主业务源码与该版本完全一致。以下区分已落地规则与后续建议。 - -## 行数为什么不再阻断 - -旧 `complexity.py` v1 对公共 API 入口设置 80 行预算,对 Application/Chain 公共入口设置 -150 行预算;v2 对 API、Application、Chain、Scheduler 的函数、类、文件设置 -150/500/1000 行预算。函数和类使用 AST `end_lineno - lineno + 1`,会把 docstring、 -注释、空行和参数换行计入。它衡量源码尺寸,并不衡量分支复杂度;压缩格式能够降低数值, -补充契约说明却可能触发回退。同一长函数还可能同时消耗函数、类和文件三份预算。 - -源码尺寸仍适合寻找职责混杂热点,但不适合作为逐行禁止增长的合并条件。拆分应以稳定职责、 -可测试行为和清晰依赖为依据,不能为了满足行数而删除说明或搬运代码。 - -## 当前复杂度合同 - -`scripts/architecture/complexity.py` 使用 `uv.lock` 锁定的 Ruff,独立执行以下两条规则: - -| 指标 | 新增函数上限 | 存量处理 | -| --- | --- | --- | -| C901:McCabe 圈复杂度 | 15 | 超限项按函数登记,只能保持或下降 | -| PLR1702:嵌套控制块深度 | 5 | 同函数多个告警取最大深度,只能保持或下降 | -| 方法/类/文件源码尺寸 | 无硬限制 | 超过 150/500/1000 行出现在观察报告 | - -范围为所有 `app/**/*.py`,只排除 `app/plugins/**` 运行时插件副本;包含 Agent、Runtime、 -Modules、Adapters、Startup、Domain、DB、Workflow、私有方法、dunder 方法和嵌套函数。 -局部 Ruff 配置、`noqa` 与 `.gitignore` 不会隐藏这些指标。 - -基线身份使用“规则 + 相对文件路径 + 完整词法函数名”,不保存物理行号;同词法名的重复定义 -按源码顺序附加序号,避免 property 或条件重定义覆盖预算。移动或重命名超限函数会成为新项, -需要在职责迁移中单独审查,不能自动挪用旧额度。不同函数的增减也不能互相抵消。 - -首次从行数策略迁移到结构策略的快照为 C901 138 个函数、PLR1702 39 个函数;同一函数可能 -同时出现在两类中。这是本次迁移的历史证据,不要求后续数字保持不变。源码观察报告有 -62 个长函数、123 个大类和 66 个大文件。没有为建立新基线修改业务源码。 - -当前 `tests/fixtures/architecture/complexity-baseline.json` 是指标事实源,并记录策略版本 -和阈值;旧 `complexity-v2-baseline.json` 与 `--v2` 已退役。门禁行为为: - -- 新增超限或既有超限增长:检查失败,`--write` 也拒绝覆盖已有基线。 -- 指标降低、回到阈值内或函数删除:先提示低水位未固化,审查后 `--write` 收紧基线。 -- 语法错误、工具错误、不完整输出、无法映射的诊断、缺失源码或基线:失败,不能当作零债务。 -- `--report` 输出结构指标和源码尺寸。CI 上传 `complexity-report`,失败时也保留已生成报告。 - -```bash -uv run --locked --no-sync python scripts/architecture/complexity.py --report /tmp/moviepilot-complexity-report.json -uv run --locked --no-sync python scripts/architecture/complexity.py --write -uv run --locked --no-sync python scripts/architecture/complexity.py -``` - -`--write` 不是接受新债务的审批入口。首次初始化、算法升级、阈值调整和职责迁移需要审查 -源码、指标 diff 和相关测试;不得删除已有基线后重建来绕过增长检查。 - -算法口径跟随锁定的 Ruff,未自行实现另一套复杂度定义。 -[C901 的配置](https://docs.astral.sh/ruff/settings/#lint_mccabe_max-complexity) 衡量控制流分支, -不是运行时耗时、内存复杂度或完整认知复杂度。 -[PLR1702](https://docs.astral.sh/ruff/rules/too-many-nested-blocks/) 仍属 preview,只有此独立 -检查启用 preview;升级 Ruff 时应跑真实 Ruff 回归用例并审查指标 diff,不自动刷新基线。 -短路布尔表达式、推导式、跨函数调用链和职责耦合仍需行为测试与架构审查。 - -## 类型检查门禁口径 - -`v3` 的 mypy 债务分三层,口径不同,不能互相抵扣: - -| 层 | 判定 | 事实源 | -| --- | --- | --- | -| 严格清单 | `mypy.ini` 的 `files` 已清零,出现任何诊断即失败 | `mypy.ini` | -| 存量例外 | 基线登记的文件按「文件 + 错误码」设上限,涨一条即失败 | `tests/fixtures/architecture/mypy-baseline.json` | -| 新增文件 | 基线未登记的文件必须零诊断,出现任何错误即失败 | 同上 | - -`scripts/architecture/mypy_ratchet.py` 负责后两层:以 Linux 平台全量扫描 `app`(排除 -`app/plugins`),未登记文件按「新增文件必须清零类型错误」失败,例外文件只允许数量下降。 -债务真实下降后用 `--write` 固化低水位;存在新增文件或增长时 `--write` 拒绝写入, -所以基线是逐文件、逐错误码的上限快照,不是允许失败的白名单。 - -单体文件拆成包时,旧路径额度只能转移给 `MYPY_MIGRATIONS` 中逐一登记的接收文件;同包内 -后来新增的文件不在名单里,仍按新增文件清零判定,避免新文件吸收旧文件额度。迁移按错误码 -汇总比较,不据此推断旧源码已经删除。 - -缺少 `pyproject.toml` 声明的 `types-*` 桩包时,mypy 会把带类型的第三方库降级为 `Any`, -同时产生 `import-untyped` 和派生的 `no-any-return`。门禁此时直接报环境错误并要求 -`uv sync --locked`,不允许把环境缺失写进基线。本地复现门禁前先确认虚拟环境与 `uv.lock` 一致。 - -按文件 + 错误码计数仍无法识别同一文件内同错误码的一换一替换,属于已知残余,需要评审确认; -稳定诊断指纹是后续方向。验收命令: - -```bash -uv run --locked --no-sync mypy --config-file mypy.ini -uv run --locked --no-sync python scripts/architecture/mypy_ratchet.py -uv run --locked --no-sync python scripts/architecture/mypy_ratchet.py --write -``` - -## 类型检查的本次扩围 - -`mypy.ini` 的严格范围从 42 个文件扩大到 45 个,新增: - -- `app/runtime/tasks.py`:后台任务登记、提交和关闭。 -- `app/scheduler/registry.py`:调度器代际、所有权与句柄。 -- `app/startup/lifecycle/components.py`:组件启动与关闭。 - -三个文件及完整严格清单实测通过,不增加忽略规则或类型债务。全宿主 mypy ratchet 继续保留。 -严格清单对项目内部的 `app.*` 统一采用 `follow_imports = silent`,并将此策略应用于 -内部 `.pyi`,读取真实接口;FastAPI 和 Click 同样读取供应方已声明的类型, -其它外部依赖沿用 `follow_imports = skip`,保留 `ignore_missing_imports = True`。`silent` 只影响导入的非目标文件,`files` 中的目标仍然 -严格检查;全宿主 Linux 分析显式扫描整个 `app`,继续报告未清零文件的全部历史债务。 -此前将已有完整分析零错误的文件统一纳入严格清单,保留之前的全部目标,新增 551 个 -受保护文件。扩围前后对完整宿主逐文件、逐错误码核对,导入策略没有隐藏任何既有错误; -订阅请求仓储补齐真实查询接口、API 移除恒等 cast 后,另消除 8 项原有类型错误。 -门禁扩围表示已清零代码受到保护,不表示同时修复了这些文件的业务实现。 - -FastAPI/Click 合同治理移除了 107 条已失效的类型忽略,修正 OpenAPI 状态码键和 -真实协程返回类型,并从定义方导入 FastAPI 的同一对象。全宿主诊断下降 417 项, -逐文件、逐错误码没有增长;新增清零的 `app/factory.py` 进入严格清单。仍有历史 -债务的文件继续受完整分析的低水位约束,不因装饰器类型恢复而宣称整文件已清零。 - -批量清理类型债务时,仍逐文件确认生产文件在完整宿主分析中清零,才纳入 `mypy.ini` -严格清单。受影响的行为、兼容、Pylint 与适用门禁通过后,按本轮授权统一提交并通过 -同一个 PR 交付,核对最终提交的远端必需 CI。验收同时检查完整严格清单和全宿主 -Linux 分析,确认其他文件及错误码无增长; -只把经完整扫描确认的真实下降写入类型基线,不以忽略诊断或放宽类型合同接受回退。 -严格文件清单以 `mypy.ini` 为准,已记录的逐文件、逐错误码债务以 -`tests/fixtures/architecture/mypy-baseline.json` 为准。动态插件与 Agent 工具合同、 -Pydantic 可空输入、事件载荷、工作流上下文、文件系统代理及外部类型桩等问题, -在确定行为和兼容合同前不宣称清零或纳入严格清单。 - -事件运行时的契约、注册、绑定、调度、错误处理和 payload 快照六个实现模块均已纳入 -严格范围,完整宿主分析中 `app/runtime/event/` 目录的类型错误已清零。 - -## 后续补充顺序 - -| 优先级 | 现有缺口 | 建议落点与验收 | -| --- | --- | --- | -| P0 | GitHub `v3` 查询结果 `protected=false`,生效分支规则为空 | 增加固定名称的 CI 汇总检查,再配置 required checks;汇总要正确处理成功复用、取消和失败,不能把所有 skipped 当成功。远端规则属于仓库策略,当前改动未更改它 | -| P1 | 覆盖率曾只阻断 Application/Domain 的聚合行覆盖率 | 已扩展到除 `app/plugins` 外全部 `app` 文件;存量逐文件低水位只增不减,新文件至少 80%,仍不设置分支下限;macOS 报告只作诊断 | -| P1 | strict frontier 对部分父类和传递依赖仍视为 Any | 按“父类/Port/DTO → lifecycle → 调用者”扩到 Agent session、Scheduler lifecycle、插件管理器;先消除真实类型问题,再纳入,不增添 ignore | -| P1 | 漏洞审计位于正式版/Beta 发布链路,依赖兼容 PR 工作流只做平台安装验证 | 在依赖变更 PR 对同一 locked 运行时依赖做 pip-audit;不要复制一份依赖清单,也无需每个普通源码 PR 重跑多架构镜像扫描 | -| P2 | 常规 Ruff 仅启用 E4/E7/E9/F/I | 优先试运行 B006(可变默认参数)、B023(循环变量闭包捕获)、B018(无效表达式);审查误报后按范围纳入。审计时 app 中 B006 为 0、B023 为 5、B018 为 1,不自动把新诊断认定为已证实的 bug | -| P2 | 部分其它 ratchet 写入器仍能直接覆盖增长,Ruff/mypy 按文件计数可能发生同规则问题抵消 | 统一已有基线的写保护;对重点规则考虑稳定符号/诊断指纹。先补“无法洗入回退”的行为用例,避免只测 fixture 字符串 | - -依赖方向、循环依赖、直接出站、异步阻塞、后台任务所有权、原生并发和 service locator -已经有门禁,应保留并按实际漏报补强,不重复添加同义规则。鉴权、数据持久化、幂等、取消、 -崩溃恢复和插件 ABI 继续由行为/契约测试保护,源码尺寸或复杂度分值不能代替这些测试。 diff --git a/docs/development-setup.md b/docs/development-setup.md index c3f6e9960..b6e8d0fc8 100644 --- a/docs/development-setup.md +++ b/docs/development-setup.md @@ -269,8 +269,8 @@ uv run --locked --no-sync python scripts/architecture/complexity.py ``` `--write` 拒绝接受已有基线上的增长;减少或删除热点后必须固化低水位。 -CI 上传 `complexity-report` 工件,门禁失败时也保留已生成的指标。指标含义、限制与后续扩围见 -[代码门禁说明](code-quality.md)。 +CI 上传 `complexity-report` 工件,门禁失败时也保留已生成的指标。指标口径以 +`scripts/architecture/complexity.py` 与 `scripts/architecture/mypy_ratchet.py` 的实现为准。 ### 7. 参考资源 diff --git a/docs/docker-startup.md b/docs/docker-startup.md deleted file mode 100644 index 229fe11d9..000000000 --- a/docs/docker-startup.md +++ /dev/null @@ -1,509 +0,0 @@ -# Docker 容器启动处理流程 - -本文档说明 MoviePilot V3 镜像从 Docker 创建容器到后端进入 `ready` 状态的完整处理链,覆盖 -`docker/` 下的控制脚本、更新事务、依赖自愈、浏览器和证书准备、Nginx、Python lifespan、 -异常保活与退出清理。 - -本文基于 `v3` 分支 2026-09-07 的实现整理。实际行为以当前源码为准。 - -Agent 中文全文索引扩展在镜像的 `prepare_cjk` 阶段按目标架构编译,最终随 Python venv 放在 -`/opt/venv/lib/moviepilot/libfts5_cjk.so`。构建时由所选 standard/free-threaded Python 完成真实 -加载及两字中文查询验收;运行容器不需要编译器,挂载 `/config` 不影响扩展可见性。 -旧镜像需要重新构建或更换镜像,源码热更新不会自动安装该原生文件。 - -```shell -docker exec /opt/venv/bin/python /app/native/fts5_cjk/install.py --check -``` - -该命令只验证环境能力,不访问用户库。旧历史的 CJK 索引随后分批回填,检索结果通过 -`index_status.messages_fts_cjk` 和 `search_path` 报告是否已经就绪及实际使用的路径。 - -## 1. 文件职责 - -| 文件 | 运行位置与职责 | -| --- | --- | -| `docker/Dockerfile` | 构建 Python 环境、后端、前端、插件、站点资源和容器控制面;声明 `tini` 入口与 readiness 健康检查。 | -| `docker/launcher.sh` | 构建时复制为镜像根目录 `/entrypoint.sh`;以 root 校验、选择并固化本轮启动使用的控制脚本。 | -| `docker/entrypoint.sh` | 真正的容器启动编排器;加载配置,驱动更新、权限、浏览器和证书准备,最后启动 supervisor。 | -| `docker/backend.sh` | supervisor 托管的后端进程命令,负责工作目录、权限和 Python 进程。 | -| `docker/supervisord.conf` | 容器内 supervisor 配置,同时托管 Nginx 和后端。 | -| `docker/update.sh` | 被 `entrypoint.sh` source;处理未完成更新恢复、Dev 更新、依赖同步和载荷事务,不再安装 Release 程序包。 | -| `docker/update-worker.sh` | 由 Supervisor 以 root 按需运行;将已确认的 Release/资源制品替换到 Docker 程序目录,然后请求入口重新加载新代码。 | -| `docker/browser.sh` | 被 `entrypoint.sh` source;选择持久化 CloakBrowser 缓存、校正权限并按需安装浏览器内核。 | -| `docker/cert.sh` | 被 `entrypoint.sh` source;校验证书、按需安装 acme.sh、签发证书并配置续期任务。 | -| `docker/nginx.template.conf` | 由环境变量渲染为 `/etc/nginx/nginx.conf`,提供前端静态文件、API 和 SSE 反向代理。 | -| `docker/nginx.common.conf` | HTTP/HTTPS server 共用的前端、API、SSE 和静态资源规则。 | - -## 2. 总体调用链 - -```text -Docker - -> tini -g - -> /entrypoint.sh # 镜像内置 launcher.sh - -> 校验并选择控制脚本代际 - -> /run/moviepilot/control//entrypoint.sh - -> 加载 /config/app.env - -> 渲染 Nginx 配置 - -> source update.sh - -> 恢复未完成更新 - -> 仅执行 Dev 更新 - -> 必要时用更新后的控制脚本重新 exec 一次 - -> source browser.sh - -> 映射 PUID/PGID - -> 检查并按需恢复 Python 运行依赖 - -> 校正目录和缓存权限 - -> 准备 CloakBrowser 内核 - -> source cert.sh - -> 启动前端 Nginx - -> supervisord - -> Nginx - -> gosu moviepilot python3 app/main.py - -> 按需启动 root update-worker.sh 安装已确认 Release/资源制品 - -> Uvicorn/FastAPI lifespan - -> 数据库迁移和全部生命周期组件 - -> /health/ready 返回 200 -``` - -镜像的 `ENTRYPOINT` 为: - -```text -/usr/bin/tini -g -- /entrypoint.sh -``` - -`tini -g` 负责回收子进程并把停止信号发送给整个进程组。根目录 `/entrypoint.sh` 不是业务 -`entrypoint.sh`,而是构建时从 `launcher.sh` 复制出的最小可信启动器。 - -## 3. 控制脚本可信选择 - -### 3.1 为什么不直接执行 `/app/docker/entrypoint.sh` - -Docker 的 Dev 更新和 root 更新 worker 都可能整体替换 `/app`。如果当前 Shell 正在从 `/app/docker` 继续 -source 其他脚本,可能出现同一次启动混用新旧脚本的情况。因此 launcher 会先选择完整的一代控制脚本, -再复制到只属于本轮启动的运行时快照目录。 - -候选目录: - -| 优先级 | 目录 | 用途 | -| --- | --- | --- | -| 1 | `/app/docker` | 当前源码携带的控制脚本。 | -| 特殊恢复 | `/app.__update_previous__/docker` | 更新停留在 `prepared` 且旧代完整可信时,用旧控制脚本恢复事务。 | -| 兜底 | `/usr/local/lib/moviepilot/control` | 镜像构建时固化的控制脚本,不随 `/app` 更新。 | - -### 3.2 信任检查 - -`source_bundle_is_trusted()` 对源码控制目录执行以下检查: - -1. 目录及其上级路径不能是符号链接。 -2. 目录及其上级路径必须归 root 所有,且 group/other 不可写。 -3. 目录下所有 `.sh` 都必须是普通文件,不能是符号链接。 -4. 必须至少包含 `entrypoint.sh`、`update.sh`、`browser.sh`、`cert.sh`。 -5. 所有脚本必须通过 `bash -n`。 -6. 按文件名排序后计算每个脚本的 SHA-256,再计算整组 manifest 的 SHA-256 作为代际编号。 - -通过检查的脚本会复制到: - -```text -/run/moviepilot/control// -``` - -快照目录权限为 `0700`,脚本权限为 `0500`。launcher 导出 `MP_CONTROL_DIR` 和 -`MP_CONTROL_GENERATION` 后,`exec` 快照中的 `entrypoint.sh`。 - -如果源码脚本不可信或快照失败,会回退镜像内置脚本;镜像内置脚本自身也必须通过完整性和语法检查, -否则容器拒绝启动。 - -### 3.3 更新恢复时的代际选择 - -`/var/lib/moviepilot/update/__update_pending__` 的值会影响 launcher: - -| 状态 | 控制脚本选择原则 | -| --- | --- | -| `prepared` | 优先使用可信的旧代 `/app.__update_previous__/docker`;旧代不存在时继续检查当前源码。 | -| `dependencies` / `blocked` | 不使用旧代控制脚本,避免数据库或当前载荷已经前进后执行缺少新迁移的旧逻辑。 | -| `committed` 或无标记 | 使用当前可信源码,失败时回退镜像内置版本。 | - -## 4. 配置加载和 Nginx 渲染 - -### 4.1 基础环境 - -业务 `entrypoint.sh` 首先设置: - -- `VENV_PATH` 默认 `/opt/venv`,并前置到 `PATH`; -- `UV_BIN` 默认 `/usr/local/bin/uv`; -- `CONFIG_DIR` 默认 `/config`; -- `PACKAGE_CACHE_ROOT` 默认 `/config/.cache`; -- `UV_CACHE_DIR` 默认 `/config/.cache/uv`。 - -### 4.2 `app.env` 优先级 - -脚本解析 `${CONFIG_DIR}/app.env`,配置优先级为: - -```text -容器进程环境变量(包括显式空字符串) - > /config/app.env - > 脚本内置默认值 -``` - -普通变量只加载脚本白名单中的启动配置;`ACME_ENV_*` 变量额外透传给证书流程。`app.env` 使用 Python -端 `set_key(..., quote_mode="always")` 对应的单引号格式,未按规范包裹的值只按字面量解析并输出告警。 - -### 4.3 Nginx 配置 - -`render_nginx_config()` 使用一次 `envsubst` 生成 `/etc/nginx/nginx.conf`: - -- HTTP 默认监听 `NGINX_PORT=3000`; -- 后端 upstream 默认指向 `127.0.0.1:3001`; -- `ENABLE_SSL=true` 时追加 HTTPS server,证书固定读取 `/config/certs/latest/`; -- `/` 服务 `/public` 下的前端文件; -- `/api`、SSE 和 CookieCloud 路径转发到后端; -- Service Worker、Manifest、普通静态资源分别使用适合更新语义的缓存策略。 - -## 5. 启动前更新处理 - -`entrypoint.sh` 在 `/` 目录 source 当前控制快照中的 `update.sh`。脚本只定义函数和初始化更新状态, -随后由 entrypoint 按顺序调用恢复与更新入口。 - -### 5.1 一次性 Dev 更新 - -如果存在: - -```text -/config/temp/moviepilot.pending_dev_update -``` - -entrypoint 会删除该标记,并只在本次启动中把 `MOVIEPILOT_UPDATE_DEV` 临时设为 `true`。更新阶段结束后 -恢复原值,避免把一次性操作变成永久自动更新。 - -### 5.2 未完成更新恢复 - -更新事务使用以下目录: - -```text -/app.__update_previous__ -/public.__update_previous__ -/var/lib/moviepilot/update/__update_pending__ -``` - -Dev 更新标记与程序载荷都位于容器可写层,只用于跨同一容器重启恢复;容器重建时它会和旧的 -`/app`、`/public` 载荷一起丢弃,避免持久化卷残留旧事务标记而阻断新镜像启动。稳定版 -Release/资源更新的 `prepared.json`、`install.json` 和 `state.json` 仍保存在 `/config`,不受此规则影响。 - -状态含义与启动处理: - -| 状态 | 含义 | 下次启动处理 | -| --- | --- | --- | -| `prepared` | 新载荷已准备,事务开始,但尚未确认依赖阶段或提交。 | 恢复旧载荷并清理事务;没有备份时按当前完整载荷结束未完成事务。 | -| `dependencies` | 正在同步新清单对应的共享虚拟环境。 | 当前 `/app`、`/public` 完整时保留当前载荷,改记 `blocked`,交给启动前依赖自愈;载荷不完整时恢复旧备份。 | -| `blocked` | 已决定保留当前载荷,等待依赖自愈。 | 继续保留当前载荷;依赖自愈成功后清理旧备份和事务标记。 | -| `committed` | 新载荷已切换完成,只剩旧备份清理。 | 清理旧备份并删除事务标记。 | - -依赖阶段不能盲目回退旧 `/app`:数据库可能已经由当前程序迁移到新 revision,旧程序未必包含对应 -Alembic migration。保留当前载荷并恢复其依赖,可以避免形成“新数据库 + 旧源码”的不可启动组合。 - -### 5.3 已准备的 Release/资源更新 - -稳定版更新不在容器启动时联网检查 GitHub Release,也不在 `update.sh` 中比较版本号或替换程序。 -后台更新服务负责下载和校验制品;用户确认安装后生成: - -```text -/config/temp/moviepilot-update/prepared.json -/config/temp/moviepilot-update/install.json -/config/temp/moviepilot-update/state.json -``` - -随后 `SystemHelper` 请求 Supervisor 启动 root 更新 worker,处理顺序: - -1. root worker 识别 `application`、`resources` 目标。 -2. 校验后端、前端和资源文件存在且 SHA-256 一致。 -3. 应用更新时解压后端和前端到临时目录,保留当前插件运行目录和 V3 站点资源。 -4. 同时包含资源目标时,把已准备资源写入新后端的 `app/application/site/`。 -5. 只有资源目标时,使用临时目录和备份目录原子替换当前源码携带的资源文件。 -6. 后端依赖清单变化时,由 root worker 使用共享虚拟环境同步锁定依赖。 -7. 源码和资源替换完成后消费下载清单,写入重启标记并关闭 Supervisor;外层入口重新执行 launcher,加载新代码。 -8. 校验、依赖或替换失败时不关闭当前服务,写入 `state.json` 的可重试失败状态。 - -如果 worker 尚未启动就发生外部容器重启,入口会在启动 Supervisor 前调用同一个 root 安装器兜底消费 -`install.json`,成功后重新执行 launcher;这条恢复路径也不经过 `update.sh`。 - -因此,制品下载完成后仍保留用户确认安装这一安全边界;确认后先替换 Docker 中的程序目录,重启只负责加载已经落盘的新代码。 - -### 5.4 Dev 自动更新 - -仅当 `MOVIEPILOT_UPDATE_DEV=true` 时(首次升级兼容尚未迁移且未配置新开关的旧 `MOVIEPILOT_AUTO_UPDATE=dev`),启动脚本会联网获取 `v3` 分支源码和最新 V3 前端 Release。 -GitHub 访问按 `GITHUB_PROXY`、`PROXY_HOST`、直连顺序选择;包索引按 `PIP_PROXY`、`PROXY_HOST`、 -直连顺序选择。 - -后端或前端下载失败时不替换当前载荷。资源下载失败时优先复用当前资源;没有可用旧资源才终止更新。 - -### 5.5 载荷切换事务 - -Release root worker 的应用更新提交顺序为: - -1. 后台下载后,在 worker 中解压并验证后端和前端。 -2. 暂存插件和站点资源。 -3. 写入 `prepared`。 -4. 依赖清单变化时写入 `dependencies`,再同步临时后端声明的依赖。 -5. 把当前 `/app`、`/public` 移到旧代备份。 -6. 把临时 `App`、`dist` 移到 `/app`、`/public`。 -7. 写入 `committed`。 -8. 删除旧代备份和事务标记。 - -Dev 更新仍由 `update.sh` 使用同一组事务标记处理;非 Dev 的制品替换只在 -`app.adapters.system.update.SystemUpdateManager` 和 `docker/update-worker.sh` 中执行。 - -依赖同步固定使用当前虚拟环境解释器,并执行等价于: - -```text -uv sync --project --locked --inexact --no-dev --no-install-project \ - --python /opt/venv/bin/python3 --no-default-groups --group -``` - -`runtime-profile` 由 `app.runtime.dependencies.profile` 根据标准 CPython 或 free-threaded CPython 选择。 -回滚历史载荷时仍保留旧 `app/runtime/dependencies.py` 选择器兼容,防止旧快照无法恢复依赖。 - -### 5.6 控制脚本更新后重入 - -Release 或 Dev 应用更新成功后,entrypoint 通过根目录 launcher 的 `--source-generation` 重新计算 `/app/docker` 代际。 -如果新代际与当前 `MP_CONTROL_GENERATION` 不同,会: - -```text -exec /entrypoint.sh --post-update-reexec -``` - -launcher 设置“更新已完成”和“已经重入”标志,新 entrypoint 不会重复安装更新。单次启动只允许重入一次; -如果控制脚本再次变化,会终止启动以避免无限循环。 - -## 6. 用户、依赖、浏览器和文件权限 - -### 6.1 运行用户映射 - -entrypoint 使用 `PUID`、`PGID` 修改镜像内 `moviepilot` 用户和组。后端、浏览器安装及 doctor 默认通过 -`gosu moviepilot:moviepilot` 执行;Release 更新 worker 明确以 root 运行来替换 root 所有的 `/app` 和 -`/public`,不会把运行权限提升给后端;`START_NOGOSU=true` 仅用于不降权的特殊运行场景。即使 `PUID/PGID` -设置为非 0,内置重启仍然可用,因为 Supervisor socket 会使用映射后的 `moviepilot` 组权限;但容器入口 -本身必须以 root 启动,不能额外使用 Docker 的 `--user` 覆盖入口用户。 - -### 6.2 后端依赖自愈 - -启动后端前先执行轻量探针: - -```text -/opt/venv/bin/python3 -m app.doctor.dependencies -``` - -探针检查 Web 栈、中文分词和转换等启动关键能力。成功时不会执行 `uv sync`;失败时才选择包源并对 -当前 `/app` 执行锁定依赖同步,随后再次探测。 - -包源不可用、同步失败或二次探测失败时,不再拉起后端,而是进入 Docker 诊断保活流程。 - -### 6.3 CloakBrowser 缓存 - -缓存目录选择顺序: - -1. 显式绝对路径 `CLOAKBROWSER_CACHE_DIR`; -2. 已可用的 `/config/.browser/cloakbrowser`; -3. 已可用或单独挂载的历史 `${HOME}/.cloakbrowser`; -4. 新安装默认 `/config/.browser/cloakbrowser`。 - -显式目录不能是 `/`、`CONFIG_DIR` 或 `HOME` 本身。脚本先确认目录可由 `moviepilot` 读写,再用 -`cloakbrowser.binary_info()` 判断内核是否可执行;缺失时以 `moviepilot` 用户安装,失败只告警,首次实际 -使用浏览器时仍可重试。 - -### 6.4 权限校正 - -默认启动避免递归扫描大体积浏览器缓存,只处理缓存根和直接子项。主要处理: - -- V3 站点资源目录; -- 插件运行目录根; -- `HOME` 和 `CONFIG_DIR` 下除浏览器缓存外的内容; -- 配置目录外的自定义 uv 缓存; -- Nginx 运行和日志目录; -- `/etc/hosts` 与 `/tmp`。 - -`MOVIEPILOT_FORCE_CHOWN=true` 才会递归修复 `/app`、`/public` 和浏览器缓存。修复 `/app` 时会保留 -`/app/docker` 为 root 所有且不可由 group/other 写入,保证下次 launcher 仍可信任源码控制脚本。 - -## 7. 证书与前置服务 - -### 7.1 证书处理 - -`cert.sh` 临时启用 `errexit` 和 `pipefail`,结束后恢复调用方原有 Shell 选项。 - -| 配置 | 行为 | -| --- | --- | -| `ENABLE_SSL=false` | 跳过证书处理。 | -| `ENABLE_SSL=true`、`AUTO_ISSUE_CERT=false` | 要求 `/config/certs/latest/fullchain.pem` 和 `privkey.pem` 已存在。 | -| `ENABLE_SSL=true`、`AUTO_ISSUE_CERT=true` | 要求 `SSL_DOMAIN` 和 `DNS_PROVIDER`;按需安装 acme.sh、加载 `ACME_ENV_*`、签发并安装证书。 | - -自动签发证书保存到 `/config/certs//`,并维护 `/config/certs/latest` 符号链接。存在 `cron` 时, -脚本写入 `/etc/cron.d/acme`,每天 03:00 执行续期检查。续期任务配置失败不会阻断已有证书启动。 - -### 7.2 Nginx 和 supervisor - -证书检查完成后,entrypoint 以前台模式启动 supervisor。supervisor 同时托管 `moviepilot-nginx` 与 -`moviepilot-backend`,两者异常退出时自动拉起;显式安装更新时还会按需启动 root worker。控制 socket 位于 `/run/moviepilot/supervisor.sock`,权限为 -`root:moviepilot`、`0770`,后端运行用户可访问;控制面同时启用认证,密码默认在每次容器启动时随机生成, -不写入配置卷。supervisor 本身显式以 root 运行以管理 Nginx,后端仍由 `backend.sh` 降权为 -`moviepilot`;镜像不再挂载或代理 Docker Socket。 - -## 8. Python 后端启动 - -entrypoint 设置 `UMASK` 后,以后台进程启动: - -```text -gosu moviepilot:moviepilot /opt/venv/bin/python3 app/main.py -``` - -`app/main.py` 的主要步骤: - -1. 修正直接执行脚本时的 `sys.path`,只让项目根目录作为应用导入入口。 -2. 设置进程标题并读取运行配置。 -3. 校验进程拓扑;全功能模式要求单 worker,安全模式允许降级拓扑。 -4. 注册 SIGINT/SIGTERM 处理器。 -5. 生产单进程模式创建 `MoviePilotServer` 并运行 Uvicorn。 -6. FastAPI 应用通过 `app.factory.create_app()` 安装公开健康探针、中间件、异常处理和 lifespan。 - -### 8.1 FastAPI lifespan 启动顺序 - -生命周期组件按 `start_order` 串行启动: - -| 顺序 | 组件 | 关键处理 | -| ---: | --- | --- | -| 1 | 文件日志 | 创建本次 lifespan 独占的文件日志 writer。 | -| 5 | 后台任务登记器 | 建立统一的后台任务所有权和关停入口。 | -| 10 | 数据库准备 | 校验迁移 lineage,按需备份,执行建表/Alembic upgrade,再确认唯一 head。 | -| 20 | HTTP 基础能力 | 配置默认 User-Agent 和共享异步传输。 | -| 25 | 站点访问端口 | 注入站点访问所需的网络、浏览器和 OCR 适配。 | -| 26 | Chain 外部端口 | 注入 Chain 使用的外部系统能力。 | -| 27 | Chain 网络端口 | 注入同步网络和系统适配。 | -| 30 | 领域依赖装配 | 注入识别规则、DNS、系统和 Rust 能力。 | -| 40 | 数据库引擎预热 | 物化同步与异步数据库引擎。 | -| 50 | 数据库连接预算 | 校验当前部署的连接池预算。 | -| 60 | 路由 | 注册主程序 API 路由。 | -| 65 | 插件运行时装配 | 构造插件市场和插件运行时依赖。 | -| 70 | 模块服务 | 构造唯一 HostRuntime、数据库 worker、配置、Agent/Chain/资源/事件等核心服务。 | -| 72 | 插件服务装配 | 发布插件安装、目录和动态路由服务。 | -| 75 | 消息队列 | 创建共享消息队列。 | -| 80 | 插件备份恢复 | 普通模式下恢复启动前插件备份。 | -| 90 | 插件 | 普通模式下加载插件。 | -| 100 | 定时器 | 普通模式下启动 Scheduler 和 Agent 定时任务。 | -| 110 | 监控器 | 普通模式下启动目录监控。 | -| 120 | 待处理整理回放 | 普通模式下恢复未完成整理任务。 | -| 130 | 命令服务 | 普通模式下注册命令入口。 | -| 140 | 工作流 | 普通模式下启动工作流后台服务。 | -| 150 | 插件同步与启动收尾 | 登记插件同步收尾任务,由 TaskRegistry 统一管理。 | - -其中“模块服务”内部还会组装当前 lifespan 唯一的配置、数据库、认证、运行时、Agent、Chain、Outbox、 -Transfer、Workflow 和 MoviePilot Server 服务,注册站点资源版本读取器、事件处理器和模块管理器。 - -`MOVIEPILOT_SAFE_MODE=true` 时跳过标记为 `NORMAL_ONLY` 的插件、调度器、监控、命令和工作流等组件, -但数据库、路由、核心模块服务和后台诊断入口仍会启动。 - -插件安装 journal 的启动恢复按插件逐条处理:单个插件的恢复或事实校验失败会记录日志、保留 journal -供后续重试,并跳过本条恢复,不阻断其他插件和宿主启动;数据库读取或插件恢复服务本身的全局故障仍按 -生命周期组件失败处理。 - -### 8.2 数据库就绪边界 - -数据库准备先读取当前 revision 和代码唯一 head: - -- revision 不可识别、存在多个 current/head、或 current 不是目标 head 的祖先时直接失败; -- 已标记的旧数据库需要迁移时,先 Alembic upgrade,再按当前元数据补表; -- 新库或未标记旧库先建表,再执行 Alembic; -- 配置允许时,迁移前创建数据库备份; -- 迁移结束后再次验证数据库已经到达当前唯一 head。 - -只有该阶段完成,应用健康状态才标记 `database_ready`。 - -### 8.3 readiness - -所有启用的 fail-fast 启动组件成功后,lifespan 才把应用标记为 `ready`。插件恢复中的单插件失败已在 -组件内部隔离,不会把该组件升级为宿主级启动失败。 - -Dockerfile 的 `HEALTHCHECK` 每 30 秒请求 `http://127.0.0.1:${PORT}/health/ready`:数据库迁移和完整 lifespan 成功后返回 200;启动、 -失败或关停阶段返回 503。`/health/live` 只表示进程和事件循环仍可响应。 - -## 9. 异常、重启和退出 - -### 9.1 信号退出 - -容器的 supervisor 接收 SIGINT/SIGTERM 后按顺序停止受管进程: - -1. 停止前端 Nginx; -2. 等待 Python 完成 lifespan 逆序关停; -3. supervisor 退出,容器按原退出状态结束。 - -Python lifespan 会先撤销 readiness,再按组件声明的 `stop_order` 停止工作流、命令、插件、事件、Agent、 -整理任务、模块服务、数据库和日志等资源。启动中途失败时,只清理已经启动或正在启动的组件。 - -### 9.2 应用内重启 - -普通应用重启通过本地 `supervisorctl restart moviepilot-nginx moviepilot-backend` 同时重启 Nginx 和后端, -避免把按需运行的更新 worker 拉起。确认安装 Release 时, -`SystemHelper` 只启动 root `moviepilot-update-worker`;worker 先替换 `/app`、`/public` 或站点资源, -再写入 `moviepilot.pending_supervisor_restart` 并执行 `supervisorctl shutdown`。外层 entrypoint 看到标记后 -重新执行 launcher,加载新代码。载荷替换期间外层 entrypoint、worker 的控制面调用和所有入口重入都使用 `/` -作为稳定工作目录,避免旧 `/app` 被删除后产生 `getcwd` 错误。Dev 更新仍通过一次性 Dev 标记关闭 Supervisor,再由 entrypoint 调用 -`update.sh`。这样更新包不会因只重启受管进程而停留在暂存目录;整个过程不访问 Docker API,也不依赖 -Docker restart policy。 - -### 9.3 异常诊断 - -后端异常退出由 supervisor 自动拉起;启动前的依赖、更新或迁移失败会直接终止容器启动并保留日志。 - -## 10. 关键运行文件 - -| 路径 | 作用 | -| --- | --- | -| `/entrypoint.sh` | 镜像内置 launcher。 | -| `/usr/local/lib/moviepilot/control/` | 镜像构建时固化的控制脚本。 | -| `/run/moviepilot/control//` | 本轮启动执行的只读控制快照。 | -| `/app` | 当前后端源码和插件运行目录。 | -| `/public` | 当前前端静态文件。 | -| `/config/app.env` | Docker 启动脚本和应用配置。 | -| `/config/.cache/uv` | 默认 uv 持久缓存。 | -| `/config/.browser/cloakbrowser` | 默认 CloakBrowser 持久缓存。 | -| `/var/lib/moviepilot/update/__update_pending__` | Dev 载荷切换事务状态;与 `/app`、`/public` 同属容器可写层。 | -| `/app.__update_previous__` | 更新前后端备份。 | -| `/public.__update_previous__` | 更新前前端备份。 | -| `/config/temp/moviepilot-update/` | 后台下载的 Release/资源包及安装状态。 | -| `/config/temp/moviepilot.pending_supervisor_restart` | Release worker 已替换程序、等待入口重新加载的标记。 | -| `/config/temp/moviepilot.pending_dev_update` | 单次 Dev 更新请求。 | -| `/run/moviepilot/supervisor.sock` | 容器内 supervisor 控制 socket,权限为 `root:moviepilot`、`0770`,并启用本次容器启动的认证凭据。 | -| `/config/certs/latest/` | Nginx 使用的稳定证书路径。 | - -## 11. 关键环境变量 - -| 变量 | 默认值 | 影响 | -| --- | --- | --- | -| `CONFIG_DIR` | `/config` | 配置、缓存、Release/资源更新状态和证书根目录。 | -| `PUID` / `PGID` | `0` / `0` | 映射 `moviepilot` 运行用户。 | -| `UMASK` | `000` | 后端进程文件权限掩码。 | -| `PORT` | `3001` | 后端监听和 readiness 端口。 | -| `NGINX_PORT` | `3000` | HTTP 前端入口。 | -| `MOVIEPILOT_AUTO_UPDATE` | `false` | 布尔开关,仅 `true` 开启后台版本检查和升级提醒;关闭后不检查主程序;`AUTO_UPDATE_RESOURCE=true` 时仍启用服务且只检查站点资源。稳定版下载及安装需手动确认。旧 `dev/release` 统一迁移为 `true`。 | -| `MOVIEPILOT_UPDATE_DEV` | `false` | 独立布尔开关,`true` 触发 `update.sh` 的启动时 Dev 分支更新;旧 `dev` 在未显式配置此开关时保留跟踪偏好。 | -| `MOVIEPILOT_SAFE_MODE` | `false` | 跳过普通模式专属的插件及后台控制面。 | -| `MOVIEPILOT_FORCE_CHOWN` | `false` | 是否执行大范围递归权限修复。 | -| `PACKAGE_CACHE_ROOT` | `/config/.cache` | 包管理缓存根目录。 | -| `UV_CACHE_DIR` | `/config/.cache/uv` | uv 缓存目录。 | -| `PIP_PROXY` | 空 | Python 包索引镜像。 | -| `GITHUB_PROXY` | 空 | GitHub 下载 URL 前缀。 | -| `PROXY_HOST` | 空 | GitHub/包命令使用的 HTTP(S) 代理。 | -| `GITHUB_TOKEN` | 空 | GitHub API 与 Agent Issue/PR Skill 共用的服务端 Token;可在设置页或首次初始化页通过 Device Flow 或手动 PAT 配置。 | -| `CLOAKBROWSER_CACHE_DIR` | 自动选择 | 浏览器内核缓存位置。 | -| `ENABLE_SSL` | `false` | 是否渲染并启用 HTTPS server。 | -| `AUTO_ISSUE_CERT` | `false` | 是否自动签发证书。 | -| `SSL_DOMAIN` / `SSL_EMAIL` / `DNS_PROVIDER` | 空 | acme.sh 签发参数。 | - -## 12. 维护约束 - -后续修改 Docker 启动流程时应保持以下边界: - -1. 控制脚本必须按完整代际执行,不能在同一次启动中直接混用更新前后的 `/app/docker/*.sh`。 -2. Release 更新由后台下载、用户确认和 root worker 驱动;`update.sh` 不得恢复 Release 程序/资源替换、启动时 GitHub Release 查询或 Shell 版本比较。 -3. 更新载荷与共享虚拟环境必须作为一个可恢复事务处理,不能留下新源码配旧依赖或旧源码配新数据库的混合状态。 -4. 标准 V3 与 V3t 依赖恢复必须复用 `app.runtime.dependencies.profile`,不能使用默认组覆盖当前 ABI profile。 -5. 站点资源只安装到 `app/application/site/`;历史目录仅用于更新旧载荷时读取兼容资源。 -6. readiness 只能在数据库 head 校验和完整 lifespan 成功后发布,不能退化为进程存在或普通接口可访问。 -7. 后端异常保活是诊断机制,不是成功状态;外部编排必须继续以 `/health/ready` 判断可接流量。 diff --git a/docs/mcp-api.md b/docs/mcp-api.md index 2ce4b6fd0..4f5a88b73 100644 --- a/docs/mcp-api.md +++ b/docs/mcp-api.md @@ -360,7 +360,6 @@ REST 调用示例: ## 8. 参考与维护 -- [Agent 总体说明](agent.md):MoviePilot 内置 Agent 的使用与配置。 - [业务 API 操作参考](../skills/moviepilot-api/SKILL.md):按领域查阅业务操作与约束。 - [下载器操作参考](../skills/downloader-operation/SKILL.md)、[媒体服务器操作参考](../skills/mediaserver-operation/SKILL.md)、[数据库操作参考](../skills/database-operation/SKILL.md):原生服务与 SQL 操作合同。 diff --git a/docs/music-covers.md b/docs/music-covers.md deleted file mode 100644 index 888011f01..000000000 --- a/docs/music-covers.md +++ /dev/null @@ -1,33 +0,0 @@ -# 音乐本地封面 - -在“设置 → 系统 → 高级设置 → 媒体 → 音乐元数据”中可设置全局“音乐本地封面”开关,默认开启, -对应 `MUSIC_LOCAL_COVER_ENABLE`。关闭后不再同步整理本地封面,也不再用本地图片回退内嵌; -在线封面刮削仍按原有策略执行,已整理的图片和已有内嵌封面不会被删除。 - -开启时,音乐目录或单个音轨整理会携带同目录的封面图片。手动按专辑目录整理默认同步附加文件; -后台或插件直接调用整理链时,还需启用 `sync_extra_files` 参数,它没有单独的前端开关。 -图片沿用当前复制、硬链接、移动和覆盖策略;重命名时保留图片原名,放入目标专辑根目录, -不套用曲名、曲序或 Disc 子目录。专辑、EP 和单曲共用这一行为。 - -本地图片作为附件保留,不依赖在线封面是否可下载,也不要求开启刮削。 -同目录包含多个不同发行时不自动分配封面;影视整理和单独选择图片不会触发音乐封面整理。 -后续生成 STRM 时仍需让对应插件同步图片后缀,MoviePilot 不负责该插件的复制配置。 - -音乐刮削内嵌封面使用“在线优先,下载不到再读取同目录图片”的顺序;本地候选按配置顺序选择, -损坏或不支持的图片跳过并尝试下一张。目录读取和图片下载均走存储能力,支持本地和远端目标。 -刮削为“跳过”时不写内嵌封面,“缺失时刮削”保留已有内嵌封面,“总是刮削”可替换已有封面。 -写入继续遵守音频源保护,不通过媒体库硬链接或软链接改写下载源文件。 - -`MUSIC_COVER_FILE_PATTERNS` 为全局封面匹配配置,逗号分隔、忽略大小写,支持 `*` 等文件名通配符。 -默认值为: - -```text -cover.jpg,cover.jpeg,cover.png,folder.jpg,folder.jpeg,folder.png,front.jpg,front.jpeg,front.png,*.png -``` - -匹配始终限制为 JPG、JPEG、PNG、WebP 图片,不读取隐藏文件。可以加入 `*.jpg` 或 `artwork.webp`; -留空关闭本地封面匹配。配置示例: - -```shell -moviepilot config set MUSIC_COVER_FILE_PATTERNS 'cover.jpg,folder.jpg,front.jpg,*.png,artwork.webp' -``` diff --git a/docs/music-recognition.md b/docs/music-recognition.md deleted file mode 100644 index dad7fd987..000000000 --- a/docs/music-recognition.md +++ /dev/null @@ -1,14 +0,0 @@ -# 音乐专辑识别 - -专辑匹配不要求提供年份。普通 `year`、原始发行年 `original_year` 用于候选排名, -不单独否决名称、艺人、录音版本和曲目证据相符的专辑,以兼容首发、再版和数字发行的年份差异。 -明确提供的当前发行年 `release_year` 仍是版本约束;来源年份未知时不凭空判为冲突。 -MusicBrainz 具体发行 ID、发行组 ID,以及 Live、Remix 等录音版本的约束保持有效。 - -文件名或标签已提供歌手时,不带歌手、年份的纯专辑名目录也会补入缺失的专辑字段。 -例如 `周杰伦的床边故事/周杰伦 - 告白气球.flac` 会提供“周杰伦 / 告白气球 / 周杰伦的床边故事” -三项检索线索。`Music`、`Downloads`、`音乐` 等通用目录不作为专辑名,已有标签专辑不被目录覆盖。 - -目录线索不等于在线身份确认。来源专辑仍须通过艺人、名称、版本和本地曲目对位校验; -不同录音或不同内容的多个合理候选仍需人工选择,不按搜索返回顺序猜测。 -完整可信的本地标签可直接用于离线整理,无须先获得唯一的在线数据库 ID。 diff --git a/docs/notification-formatting.md b/docs/notification-formatting.md deleted file mode 100644 index a4d166ea9..000000000 --- a/docs/notification-formatting.md +++ /dev/null @@ -1,48 +0,0 @@ -# 结构化通知与渠道能力 - -插件通过 `post_message(text=..., rich_content=...)` 提交一份与渠道无关的通知。 -`text` 保留完整纯文本结果;`rich_content` 使用 `NotificationContent`,只包含标题和有序 -`NotificationBlock` 列表,每段的 `title`、`text` 都是普通文本。插件不枚举渠道、不插入 -Markdown 或 HTML 标签,也不判断渠道权限。 -独立标题表示分节;分节之后的标题和正文对表示条目。Telegram 转为原生分节标题与缩进列表, -同一条目内的续行保留缩进。详情入口使用 Markdown 链接交给真实转换器,避免 HTML 标签被原样展示。 - -宿主通过 `ChannelCapabilityManager.get_notification_text_format()` 查询普通通知的投递能力, -按 `ChannelCapabilities.notification_text_format` 选择语法和转义方式。未声明该能力的渠道 -继续使用 `text`。`notification_text_bytes` 提供正文的 UTF-8 字节预算;有原生分段协议的 -渠道由自身实现控制。能力配置中的 `notification_mode_key` / `notification_mode_value` -区分企业微信应用与智能机器人模式。QQ 的实际服务端 Markdown 权限通过原有错误回退检测。 - -| 渠道 | 主程序投递方式 | -| --- | --- | -| Telegram | Rich Message,沿用自动分段并补充详情入口 | -| 飞书 | JSON 2.0 Markdown 卡片,按实际卡片 JSON 字节预算分段 | -| Slack | Block Kit,每条最多 50 个 block,保留标题和详情按钮 | -| 钉钉 | 自定义机器人 Markdown 消息 | -| Discord | 原生 Embed,按正文预算分段 | -| VoceChat | 原有 text/markdown 通道 | -| Synology Chat | 原有 Chat 格式化消息 | -| 企业微信智能机器人 | Markdown 消息及原生分段;应用模式仍使用纯文本 | -| QQ Bot | 有权限时分段发送,权限不足时回退原始纯文本一次 | - -平台其他交互场景的 `RICH_TEXT` / `MARKDOWN` 声明与普通通知的投递协议相区别。 -WebAgent 能渲染 Markdown,但通知中心仍记录普通文本;ClawBot 当前投递实现使用 -`send_text`,不采用格式化通知。新渠道声明通知格式并接入适配器后,插件无需维护名称白名单。 - -新字段不改变通知开关、管理员或用户收件人路由。历史仍记录原始 `title`、`text`。 -Telegram 原有的 `rich_message` 用于完整 Rich Markdown 正文,未提供 `rich_content` 时 -沿用原来的行为。渠道 API 错误不因一次临时网络错误自动重发另一份通知。 - -站点自动签到只负责统计、结果分组和重试标记;同一状态内按完整原因归并,原因只展示一次, -对应名单保留全部站点。不同原因在缩短显示之前区分,避免截断后相同的前缀被误合并。 -V3 插件显式声明 `system_version >=3.1.6`,提供默认开启的 -“优化消息通知样式”开关;关闭后不传结构化内容,所有渠道恢复原有通知样式。 -V2 兼容实现保持独立版本,在市场索引声明 `v3: true` 和 `system_version <=3.1.5`, -保留低于 3.1.6 的 V3 主程序选择 V2 兼容实现的安装路径。 -具体说明见插件仓 `plugins.v3/autosignin/README.md`。 - -平台格式参考:[Telegram Rich Messages](https://core.telegram.org/bots/features#rich-messages)、 -[飞书 Markdown](https://open.feishu.cn/document/uAjLw4CM/ukzMukzMukzM/feishu-cards/card-json-v2-components/content-components/rich-text)、 -[Slack section](https://docs.slack.dev/reference/block-kit/blocks/section-block/)、 -[Discord Embed](https://docs.discord.com/developers/resources/message#embed-object)、 -[Synology Chat](https://kb.synology.com/en-au/DSM/help/Chat/chat_enriching_your_chat?version=6)。 diff --git a/docs/pr-agent.md b/docs/pr-agent.md deleted file mode 100644 index be64ae364..000000000 --- a/docs/pr-agent.md +++ /dev/null @@ -1,48 +0,0 @@ -# PR-Agent 使用说明 - -本仓库通过 PR Review Runner 运行 PR-Agent,帮助贡献者维护 PR 摘要、获取代码审查结果和提出 PR 相关问题。 - -## 自动执行 - -同仓分支和来自 fork 的 PR 都会自动执行 PR-Agent。 - -PR 在以下场景会自动处理: - -- 打开或重新打开 PR。 -- 将草稿 PR 标记为可审查。 -- 请求审查。 -- 每次推送新的 commit。 - -PR 带有 `skip pr-agent` 标签,或标题以 `[Auto]`、`Auto` 开头时,自动和手工路径都会跳过。 - -## 手工命令 - -在 PR 的普通讨论评论中使用以下命令: - -```text -/describe -/review -/ask 这次改动有没有遗漏权限校验? -``` - -- `/describe`:更新 PR Body 内的 `PR-Agent 摘要`,并保留贡献者原有的 PR 描述。 -- `/review`:发起一次代码审查。 -- `/ask ...`:就当前 PR 提问,回复会发布在普通 PR 评论中。 - -本仓库禁用 `/improve` 及其等价别名。其他命令是否可用由 runner 所包含的 PR-Agent 能力决定。 - -手工命令仅允许以下 GitHub 身份关联的用户使用:`OWNER`、`MEMBER`、`COLLABORATOR`、`CONTRIBUTOR`、`FIRST_TIME_CONTRIBUTOR`。 - -新建的合法命令评论会触发执行;编辑后仍为合法命令的评论也会触发。编辑普通讨论评论不会调用模型。 - -## 审查结果 - -本仓库固定使用中文生成 PR-Agent 内容。`/describe` 的结果位于 PR Body 的 `PR-Agent 摘要` 区域,用于概览本次变更。 - -`/review` 和自动审查会通过原生 GitHub Review 发布,结果位于 Review 页签,标题固定为 `PR-Agent Code Review`。可定位到本次变更的问题会在对应代码行以行内评论呈现,并使用 high、medium 或 low 风险标识。 - -Review 摘要会自然概括本次变更和整体审查结论,不会复制行内评论。未发现需要处理的问题时,摘要会概括变更并自然说明暂无其他反馈。审查不会额外创建专用的 issue comment 摘要。 - -## 安全边界 - -自动审查通过 `pull_request_target` 在目标仓库上下文中读取 PR 信息,并使用共享 runner 的 `latest` 镜像,但不会 checkout 或执行 PR 分支代码。权限保持最小化:只授予读取仓库内容所需的 `contents: read`,以及更新 PR Body、发布 Review 和回复 PR 评论所需的写权限;不会向仓库推送代码或创建提交。 diff --git a/docs/refactor/agent-api-surface-audit.json b/docs/refactor/agent-api-surface-audit.json deleted file mode 100644 index 816f078a6..000000000 --- a/docs/refactor/agent-api-surface-audit.json +++ /dev/null @@ -1,8017 +0,0 @@ -{ - "disposition_counts": { - "alternate-auth-duplicate": 11, - "consolidated": 73, - "gateway": 228, - "provider-skill": 12, - "stream_or_binary": 13, - "transport_or_identity": 76, - "ui_presentation": 18 - }, - "dynamic_gateway_routes": [ - { - "method": "GET", - "operation_ids": [ - "media.person.credits" - ], - "path": "/api/v1/{source}/person/credits/{person_id}", - "reason": "The executor validates and expands this bounded source placeholder to one of tmdb, douban, bangumi, or anilist before calling the corresponding concrete OpenAPI route." - } - ], - "gateway_http_route_count": 229, - "gateway_identity_contracts": [ - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "config.identifiers.get", - "path": "/api/v1/system/identifiers", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "config.identifiers.update", - "path": "/api/v1/system/identifiers", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "config.public.get", - "path": "/api/v1/system/setting/public/{key}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "config.system.describe", - "path": "/api/v1/system/settings/describe/{setting_key}", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "config.system.get", - "path": "/api/v1/system/settings", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "config.system.list", - "path": "/api/v1/system/settings/catalog", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "config.system.update", - "path": "/api/v1/system/settings", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "config.user.get", - "path": "/api/v1/system/global/user", - "required_role": "user", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "dashboard.cpu", - "path": "/api/v1/dashboard/cpu", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "dashboard.downloader", - "path": "/api/v1/dashboard/downloader", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "dashboard.media.statistics", - "path": "/api/v1/dashboard/statistic", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "dashboard.memory", - "path": "/api/v1/dashboard/memory", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "dashboard.network", - "path": "/api/v1/dashboard/network", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "dashboard.processes", - "path": "/api/v1/dashboard/processes", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "dashboard.storage", - "path": "/api/v1/dashboard/storage", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "dashboard.system", - "path": "/api/v1/dashboard/system", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "dashboard.transfer.statistics", - "path": "/api/v1/dashboard/transfer", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "database.backups.create", - "path": "/api/v1/system/database/backups", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "admin", - "method": "DELETE", - "operation_id": "database.backups.delete", - "path": "/api/v1/system/database/backups/{name}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "database.backups.list", - "path": "/api/v1/system/database/backups", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "POST", - "operation_id": "database.backups.verify", - "path": "/api/v1/system/database/backups/{name}/verify", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "bound_user", - "method": "POST", - "operation_id": "download.add", - "path": "/api/v1/download/add", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "bound_user", - "method": "POST", - "operation_id": "download.artist_collection", - "path": "/api/v1/download/artist-collection", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "download.clients", - "path": "/api/v1/download/clients", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "admin", - "method": "DELETE", - "operation_id": "download.history.delete", - "path": "/api/v1/history/download", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "download.history.list", - "path": "/api/v1/history/download", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "download.paths", - "path": "/api/v1/download/paths", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "download.tasks.active", - "path": "/api/v1/download/", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "filter.builtin", - "path": "/api/v1/rule/builtin", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "filter.custom", - "path": "/api/v1/rule/custom", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "filter.custom.add", - "path": "/api/v1/rule/custom", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "admin", - "method": "DELETE", - "operation_id": "filter.custom.delete", - "path": "/api/v1/rule/custom/{rule_id}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "PUT", - "operation_id": "filter.custom.update", - "path": "/api/v1/rule/custom/{rule_id}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "filter.group.add", - "path": "/api/v1/rule/groups", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "admin", - "method": "DELETE", - "operation_id": "filter.group.delete", - "path": "/api/v1/rule/groups/{name}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "PUT", - "operation_id": "filter.group.update", - "path": "/api/v1/rule/groups/{name}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "filter.groups", - "path": "/api/v1/rule/groups", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "external_side_effect", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "filter.test", - "path": "/api/v1/system/ruletest", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "library.exists", - "path": "/api/v1/mediaserver/exists", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "library.latest", - "path": "/api/v1/mediaserver/latest", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "admin", - "method": "DELETE", - "operation_id": "media.cache.clear", - "path": "/api/v1/tmdb/cache", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "admin", - "method": "DELETE", - "operation_id": "media.cache.delete", - "path": "/api/v1/tmdb/cache/{cache_key}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "media.cache.get", - "path": "/api/v1/tmdb/cache", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "media.classification.fields", - "path": "/api/v1/media/classification/fields", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "media.classification.policy.get", - "path": "/api/v1/media/classification/policy", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "media.classification.policy.history", - "path": "/api/v1/media/classification/history", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "POST", - "operation_id": "media.classification.policy.impact", - "path": "/api/v1/media/classification/impact", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "POST", - "operation_id": "media.classification.policy.preview", - "path": "/api/v1/media/classification/preview", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "media.classification.policy.rollback", - "path": "/api/v1/media/classification/rollback/{revision}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "PUT", - "operation_id": "media.classification.policy.update", - "path": "/api/v1/media/classification/policy", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "POST", - "operation_id": "media.classification.policy.validate", - "path": "/api/v1/media/classification/validate", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "public", - "method": "GET", - "operation_id": "media.detail", - "path": "/api/v1/media/{media_id}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "public", - "method": "GET", - "operation_id": "media.episode_group.seasons", - "path": "/api/v1/media/group/seasons/{episode_group}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "public", - "method": "GET", - "operation_id": "media.episode_groups", - "path": "/api/v1/media/groups/{tmdbid}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "public", - "method": "GET", - "operation_id": "media.episode_schedule", - "path": "/api/v1/tmdb/{tmdbid}/{season}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "public", - "method": "GET", - "operation_id": "media.person.credits", - "path": "/api/v1/{source}/person/credits/{person_id}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "public", - "method": "GET", - "operation_id": "media.person.search", - "path": "/api/v1/media/search", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "media.recognize", - "path": "/api/v1/media/recognize", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "media.recognize_file", - "path": "/api/v1/media/recognize_file", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "media.scrape", - "path": "/api/v1/media/scrape/{storage}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "public", - "method": "GET", - "operation_id": "media.search", - "path": "/api/v1/media/search", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "public", - "method": "GET", - "operation_id": "media.seasons", - "path": "/api/v1/media/seasons", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "media.sources", - "path": "/api/v1/media/source", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "public", - "method": "GET", - "operation_id": "music.album.get", - "path": "/api/v1/music/album/{album_id}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "public", - "method": "GET", - "operation_id": "music.album.related", - "path": "/api/v1/music/album/{album_id}/related", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "public", - "method": "GET", - "operation_id": "music.artist.albums", - "path": "/api/v1/music/artist/{artist_id}/albums", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "public", - "method": "GET", - "operation_id": "music.artist.get", - "path": "/api/v1/music/artist/{artist_id}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "public", - "method": "GET", - "operation_id": "music.artist.related", - "path": "/api/v1/music/artist/{artist_id}/related", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "admin", - "method": "DELETE", - "operation_id": "music.cache.clear", - "path": "/api/v1/music/cache", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "admin", - "method": "DELETE", - "operation_id": "music.cache.delete", - "path": "/api/v1/music/cache/{cache_key}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "music.cache.get", - "path": "/api/v1/music/cache", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "public", - "method": "GET", - "operation_id": "music.explore", - "path": "/api/v1/music/explore", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "public", - "method": "POST", - "operation_id": "music.recognize", - "path": "/api/v1/music/recognize", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "plugin.capabilities", - "path": "/api/v1/plugin/runtime/capabilities", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "plugin.clone", - "path": "/api/v1/plugin/clone/{plugin_id}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "plugin.clone.restorable", - "path": "/api/v1/plugin/clone/{plugin_id}/restorable", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "plugin.config.get", - "path": "/api/v1/plugin/form/{plugin_id}", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "PUT", - "operation_id": "plugin.config.update", - "path": "/api/v1/plugin/{plugin_id}", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "plugin.data", - "path": "/api/v1/plugin/runtime/{plugin_id}/data", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "DELETE", - "operation_id": "plugin.default_target.clear", - "path": "/api/v1/plugin/instances/{plugin_id}/{instance_id}/default_target", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "PUT", - "operation_id": "plugin.default_target.set", - "path": "/api/v1/plugin/instances/{plugin_id}/{instance_id}/default_target", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "plugin.folder.create", - "path": "/api/v1/plugin/folders/{folder_name}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "admin", - "method": "DELETE", - "operation_id": "plugin.folder.delete", - "path": "/api/v1/plugin/folders/{folder_name}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "PUT", - "operation_id": "plugin.folder.plugin.assign", - "path": "/api/v1/plugin/folders/{folder_name}/plugins/{plugin_id}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "DELETE", - "operation_id": "plugin.folder.plugin.remove", - "path": "/api/v1/plugin/folders/{folder_name}/plugins/{plugin_id}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "PUT", - "operation_id": "plugin.folder.plugins.update", - "path": "/api/v1/plugin/folders/{folder_name}/plugins", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "PATCH", - "operation_id": "plugin.folder.update", - "path": "/api/v1/plugin/folders/{folder_name}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "plugin.folders.get", - "path": "/api/v1/plugin/folders", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "plugin.folders.update", - "path": "/api/v1/plugin/folders", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "plugin.history", - "path": "/api/v1/plugin/history/{plugin_id}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "GET", - "operation_id": "plugin.install", - "path": "/api/v1/plugin/install/{plugin_id}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "plugin.installed", - "path": "/api/v1/plugin/", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "plugin.instance.purge", - "path": "/api/v1/plugin/instance/{instance_id}/purge", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "plugin.instance.set_enabled", - "path": "/api/v1/plugin/instance/{instance_id}/enabled", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "DELETE", - "operation_id": "plugin.loglevel.clear", - "path": "/api/v1/plugin/loglevel/{plugin_id}/{instance_id}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "plugin.loglevel.get", - "path": "/api/v1/plugin/loglevel/{plugin_id}", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "PUT", - "operation_id": "plugin.loglevel.set", - "path": "/api/v1/plugin/loglevel/{plugin_id}/{instance_id}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "plugin.market", - "path": "/api/v1/plugin/", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "plugin.market.sync_wiki", - "path": "/api/v1/system/setting/PLUGIN_MARKET/sync-wiki", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "plugin.rating", - "path": "/api/v1/plugin/rating/{plugin_id}", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "plugin.rating.submit", - "path": "/api/v1/plugin/rating/{plugin_id}", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "plugin.ratings", - "path": "/api/v1/plugin/rating", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "plugin.releases", - "path": "/api/v1/plugin/releases/{plugin_id}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "plugin.reload", - "path": "/api/v1/plugin/reload/{plugin_id}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "admin", - "method": "GET", - "operation_id": "plugin.reset", - "path": "/api/v1/plugin/reset/{plugin_id}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "plugin.runtime.status", - "path": "/api/v1/plugin/runtime", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "plugin.source.change", - "path": "/api/v1/plugin/source/{plugin_id}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "plugin.source.install", - "path": "/api/v1/plugin/source/{plugin_id}/install", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "plugin.source.options", - "path": "/api/v1/plugin/source/{plugin_id}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "public", - "method": "GET", - "operation_id": "plugin.statistics", - "path": "/api/v1/plugin/statistic", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "admin", - "method": "DELETE", - "operation_id": "plugin.uninstall", - "path": "/api/v1/plugin/{plugin_id}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "public", - "method": "GET", - "operation_id": "recommendation.list", - "path": "/api/v1/recommend/agent", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "scheduler.list", - "path": "/api/v1/dashboard/schedule", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "scheduler.progress", - "path": "/api/v1/dashboard/schedule/{job_id}/progress", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "GET", - "operation_id": "scheduler.run", - "path": "/api/v1/system/runscheduler", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "external_side_effect", - "identity_scope": "bound_user", - "method": "POST", - "operation_id": "search.recommend", - "path": "/api/v1/search/recommend", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "search.results", - "path": "/api/v1/search/last/context", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "external_side_effect", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "search.title", - "path": "/api/v1/search/title", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "search.torrents", - "path": "/api/v1/search/media/{media_id}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "site.add", - "path": "/api/v1/site/", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "site.auth.options", - "path": "/api/v1/site/auth", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "site.authenticate", - "path": "/api/v1/site/auth", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "site.category", - "path": "/api/v1/site/category/{site_id}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "site.cookie.set", - "path": "/api/v1/site/cookie/{site_id}/set", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "site.cookie.update", - "path": "/api/v1/site/cookie/{site_id}", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "site.cookiecloud.sync", - "path": "/api/v1/site/cookiecloud", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "admin", - "method": "DELETE", - "operation_id": "site.delete", - "path": "/api/v1/site/{site_id}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "site.list", - "path": "/api/v1/site/agent", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "site.mapping", - "path": "/api/v1/site/mapping", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "site.priorities.update", - "path": "/api/v1/site/priorities", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "site.reset", - "path": "/api/v1/site/reset", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "GET", - "operation_id": "site.resource", - "path": "/api/v1/site/resource/{site_id}", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "site.rss", - "path": "/api/v1/site/rss", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "site.searchable", - "path": "/api/v1/site/media/{media_type}", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "site.statistic", - "path": "/api/v1/site/statistic/{site_url}", - "required_role": "user", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "site.statistics", - "path": "/api/v1/site/statistic", - "required_role": "user", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "site.supporting", - "path": "/api/v1/site/supporting", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "site.test", - "path": "/api/v1/site/test/{site_id}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "PUT", - "operation_id": "site.update", - "path": "/api/v1/site/", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "site.userdata", - "path": "/api/v1/site/userdata/{site_id}", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "site.userdata.latest", - "path": "/api/v1/site/userdata/latest", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "site.userdata.refresh", - "path": "/api/v1/site/userdata/{site_id}", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "slash.list", - "path": "/api/v1/message/agent/commands", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "slash.run", - "path": "/api/v1/message/agent/commands/run", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "storage.delete", - "path": "/api/v1/storage/delete", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "POST", - "operation_id": "storage.list", - "path": "/api/v1/storage/agent/list", - "required_role": "user", - "result_sensitivity": "private" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "storage.manage", - "path": "/api/v1/storage/manage", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "storage.mkdir", - "path": "/api/v1/storage/mkdir", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "storage.rename", - "path": "/api/v1/storage/rename", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "storage.settings", - "path": "/api/v1/storage/directories", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "bound_user", - "method": "POST", - "operation_id": "subscription.add", - "path": "/api/v1/subscribe/", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "bound_user", - "method": "DELETE", - "operation_id": "subscription.delete", - "path": "/api/v1/subscribe/{subscribe_id}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "bound_user", - "method": "DELETE", - "operation_id": "subscription.delete_by_media", - "path": "/api/v1/subscribe/media/{media_id}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "bound_user", - "method": "PUT", - "operation_id": "subscription.execution.cancel", - "path": "/api/v1/subscribe/execution/batches/{batch_id}/cancel", - "required_role": "user", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "subscription.execution.get", - "path": "/api/v1/subscribe/execution/batches/{batch_id}", - "required_role": "user", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "subscription.execution.list", - "path": "/api/v1/subscribe/execution/batches", - "required_role": "user", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "subscription.files", - "path": "/api/v1/subscribe/files/{subscribe_id}", - "required_role": "user", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "subscription.find", - "path": "/api/v1/subscribe/media/{media_id}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "bound_user", - "method": "POST", - "operation_id": "subscription.follow.add", - "path": "/api/v1/subscribe/follow", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "bound_user", - "method": "DELETE", - "operation_id": "subscription.follow.delete", - "path": "/api/v1/subscribe/follow", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "subscription.follow.list", - "path": "/api/v1/subscribe/follow", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "bound_user", - "method": "POST", - "operation_id": "subscription.fork", - "path": "/api/v1/subscribe/fork", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "subscription.get", - "path": "/api/v1/subscribe/{subscribe_id}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "subscription.history", - "path": "/api/v1/subscribe/history/{mtype}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "bound_user", - "method": "DELETE", - "operation_id": "subscription.history.delete", - "path": "/api/v1/subscribe/history/{history_id}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "subscription.list", - "path": "/api/v1/subscribe/", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "subscription.metadata.refresh", - "path": "/api/v1/subscribe/check", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "public", - "method": "GET", - "operation_id": "subscription.popular", - "path": "/api/v1/subscribe/popular", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "subscription.refresh", - "path": "/api/v1/subscribe/refresh", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "bound_user", - "method": "POST", - "operation_id": "subscription.reset", - "path": "/api/v1/subscribe/reset/{subid}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "bound_user", - "method": "POST", - "operation_id": "subscription.search", - "path": "/api/v1/subscribe/search/{subscribe_id}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "bound_user", - "method": "POST", - "operation_id": "subscription.search_all", - "path": "/api/v1/subscribe/search", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "bound_user", - "method": "POST", - "operation_id": "subscription.share", - "path": "/api/v1/subscribe/share", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "bound_user", - "method": "DELETE", - "operation_id": "subscription.share.delete", - "path": "/api/v1/subscribe/share/{share_id}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "public", - "method": "GET", - "operation_id": "subscription.share.statistics", - "path": "/api/v1/subscribe/share/statistics", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "public", - "method": "GET", - "operation_id": "subscription.shares", - "path": "/api/v1/subscribe/shares", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "bound_user", - "method": "PUT", - "operation_id": "subscription.status.update", - "path": "/api/v1/subscribe/status/{subid}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "bound_user", - "method": "PUT", - "operation_id": "subscription.update", - "path": "/api/v1/subscribe/", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "subscription.user.list", - "path": "/api/v1/subscribe/user/{username}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "external_side_effect", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "subtitle.search.media", - "path": "/api/v1/search/subtitle/media/{media_id}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "external_side_effect", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "subtitle.search.title", - "path": "/api/v1/search/subtitle/title", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "system.module.catalog", - "path": "/api/v1/system/module-catalog", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "system.module.list", - "path": "/api/v1/system/modulelist", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "system.module.settings", - "path": "/api/v1/system/module-settings", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "external_side_effect", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "system.module.test", - "path": "/api/v1/system/moduletest/{moduleid}", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "system.network.targets", - "path": "/api/v1/system/nettest/targets", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "external_side_effect", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "system.network.test", - "path": "/api/v1/system/nettest", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "GET", - "operation_id": "system.restart", - "path": "/api/v1/system/restart", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "system.update.check", - "path": "/api/v1/system/update/check", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "system.update.download", - "path": "/api/v1/system/update/download", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "system.update.install", - "path": "/api/v1/system/update/install", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "system.update.status", - "path": "/api/v1/system/update/status", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "system.upgrade.dev", - "path": "/api/v1/system/upgrade", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "system.usage.statistics", - "path": "/api/v1/system/usage/statistic", - "required_role": "user", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "system.versions", - "path": "/api/v1/system/versions", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "admin", - "method": "DELETE", - "operation_id": "torrent.cache.clear", - "path": "/api/v1/torrent/cache", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "admin", - "method": "DELETE", - "operation_id": "torrent.cache.delete", - "path": "/api/v1/torrent/cache/{domain}/{torrent_hash}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "torrent.cache.get", - "path": "/api/v1/torrent/cache", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "torrent.cache.refresh", - "path": "/api/v1/torrent/cache/refresh", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "torrent.cache.reidentify", - "path": "/api/v1/torrent/cache/reidentify/{domain}/{torrent_hash}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "POST", - "operation_id": "transfer.episode_format.recommend", - "path": "/api/v1/transfer/episode-format/recommend", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "transfer.file", - "path": "/api/v1/transfer/manual", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "transfer.history", - "path": "/api/v1/history/transfer", - "required_role": "user", - "result_sensitivity": "private" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "admin", - "method": "DELETE", - "operation_id": "transfer.history.clear", - "path": "/api/v1/history/transfer/all", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "admin", - "method": "DELETE", - "operation_id": "transfer.history.delete", - "path": "/api/v1/history/transfer", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "transfer.history.redo", - "path": "/api/v1/history/transfer/{history_id}/ai-redo", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "transfer.history.redo_batch", - "path": "/api/v1/history/transfer/ai-redo", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "POST", - "operation_id": "transfer.manual_history", - "path": "/api/v1/transfer/manual/history", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "transfer.manual_review", - "path": "/api/v1/transfer/tasks/{task_id}/manual-review", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "transfer.manual_review.resolve", - "path": "/api/v1/transfer/tasks/{task_id}/manual-review", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "transfer.manual_reviews", - "path": "/api/v1/transfer/tasks/manual-reviews", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "transfer.name", - "path": "/api/v1/transfer/name", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "transfer.queue", - "path": "/api/v1/transfer/queue", - "required_role": "user", - "result_sensitivity": "private" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "bound_user", - "method": "DELETE", - "operation_id": "transfer.queue.delete", - "path": "/api/v1/transfer/queue", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "POST", - "operation_id": "transfer.target_path", - "path": "/api/v1/transfer/manual/target-path", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "workflow.actions", - "path": "/api/v1/workflow/actions", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "workflow.create", - "path": "/api/v1/workflow/", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "destructive_write", - "identity_scope": "admin", - "method": "DELETE", - "operation_id": "workflow.delete", - "path": "/api/v1/workflow/{workflow_id}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "workflow.event_types", - "path": "/api/v1/workflow/event_types", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "workflow.fork", - "path": "/api/v1/workflow/fork", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "workflow.get", - "path": "/api/v1/workflow/{workflow_id}", - "required_role": "system_admin", - "result_sensitivity": "private" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "bound_user", - "method": "GET", - "operation_id": "workflow.list", - "path": "/api/v1/workflow/agent", - "required_role": "user", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "workflow.pause", - "path": "/api/v1/workflow/{workflow_id}/pause", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "workflow.plugin.actions", - "path": "/api/v1/workflow/plugin/actions", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "workflow.reset", - "path": "/api/v1/workflow/{workflow_id}/reset", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "workflow.run", - "path": "/api/v1/workflow/{workflow_id}/run", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "POST", - "operation_id": "workflow.share", - "path": "/api/v1/workflow/share", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "external_side_effect", - "identity_scope": "admin", - "method": "DELETE", - "operation_id": "workflow.share.delete", - "path": "/api/v1/workflow/share/{share_id}", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "none", - "effect": "safe_read", - "identity_scope": "admin", - "method": "GET", - "operation_id": "workflow.shares", - "path": "/api/v1/workflow/shares", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "POST", - "operation_id": "workflow.start", - "path": "/api/v1/workflow/{workflow_id}/start", - "required_role": "system_admin", - "result_sensitivity": "normal" - }, - { - "confirmation": "required", - "effect": "reversible_write", - "identity_scope": "admin", - "method": "PUT", - "operation_id": "workflow.update", - "path": "/api/v1/workflow/{workflow_id}", - "required_role": "system_admin", - "result_sensitivity": "normal" - } - ], - "gateway_operation_count": 231, - "matched_gateway_http_route_count": 228, - "openapi_operation_count": 431, - "operations": [ - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/anilist/credits/{anilist_id}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "查询 AniList 配音演员", - "tags": [ - "anilist" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/anilist/discover", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "探索 AniList 动画", - "tags": [ - "anilist" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/anilist/person/credits/{person_id}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "查询 AniList 人物作品", - "tags": [ - "anilist" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/anilist/person/{person_id}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "查询 AniList 人物详情", - "tags": [ - "anilist" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/anilist/popular-this-season", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "查询 AniList 本季热门榜", - "tags": [ - "anilist" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/anilist/recommend/{anilist_id}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "查询 AniList 相关推荐", - "tags": [ - "anilist" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/anilist/trending", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "查询 AniList 当前趋势榜", - "tags": [ - "anilist" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/anilist/{anilist_id}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "查询 AniList 动画详情", - "tags": [ - "anilist" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/anthropic/v1/messages", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "Anthropic 兼容消息生成", - "tags": [ - "anthropic" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/auth/exchange", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "兑换插件认证登录票据", - "tags": [ - "auth" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/auth/providers", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "查询登录认证提供方", - "tags": [ - "auth" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/bangumi/credits/{bangumiid}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "查询Bangumi演职员表", - "tags": [ - "bangumi" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/bangumi/person/credits/{person_id}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "人物参演作品", - "tags": [ - "bangumi" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/bangumi/person/{person_id}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "人物详情", - "tags": [ - "bangumi" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/bangumi/recommend/{bangumiid}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "查询Bangumi推荐", - "tags": [ - "bangumi" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/bangumi/{bangumiid}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "查询Bangumi详情", - "tags": [ - "bangumi" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "dashboard.cpu" - ], - "owner": "moviepilot-api", - "path": "/api/v1/dashboard/cpu", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "获取当前CPU使用率", - "tags": [ - "dashboard" - ] - }, - { - "disposition": "alternate-auth-duplicate", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/dashboard/cpu2", - "reason": "API-token compatibility duplicate; the Agent uses the corresponding bearer-authenticated operation with its persisted user identity.", - "summary": "获取当前CPU使用率(API_TOKEN)", - "tags": [ - "dashboard" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "dashboard.downloader" - ], - "owner": "moviepilot-api", - "path": "/api/v1/dashboard/downloader", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "下载器信息", - "tags": [ - "dashboard" - ] - }, - { - "disposition": "alternate-auth-duplicate", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/dashboard/downloader2", - "reason": "API-token compatibility duplicate; the Agent uses the corresponding bearer-authenticated operation with its persisted user identity.", - "summary": "下载器信息(API_TOKEN)", - "tags": [ - "dashboard" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "dashboard.memory" - ], - "owner": "moviepilot-api", - "path": "/api/v1/dashboard/memory", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "获取当前应用与系统内存信息", - "tags": [ - "dashboard" - ] - }, - { - "disposition": "alternate-auth-duplicate", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/dashboard/memory2", - "reason": "API-token compatibility duplicate; the Agent uses the corresponding bearer-authenticated operation with its persisted user identity.", - "summary": "获取当前应用与系统内存信息(API_TOKEN)", - "tags": [ - "dashboard" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "dashboard.network" - ], - "owner": "moviepilot-api", - "path": "/api/v1/dashboard/network", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "获取当前网络流量", - "tags": [ - "dashboard" - ] - }, - { - "disposition": "alternate-auth-duplicate", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/dashboard/network2", - "reason": "API-token compatibility duplicate; the Agent uses the corresponding bearer-authenticated operation with its persisted user identity.", - "summary": "获取当前网络流量(API_TOKEN)", - "tags": [ - "dashboard" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "dashboard.processes" - ], - "owner": "moviepilot-api", - "path": "/api/v1/dashboard/processes", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "进程信息", - "tags": [ - "dashboard" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "scheduler.list" - ], - "owner": "moviepilot-api", - "path": "/api/v1/dashboard/schedule", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "后台服务", - "tags": [ - "dashboard" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "scheduler.progress" - ], - "owner": "moviepilot-api", - "path": "/api/v1/dashboard/schedule/{job_id}/progress", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "后台服务进度", - "tags": [ - "dashboard" - ] - }, - { - "disposition": "alternate-auth-duplicate", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/dashboard/schedule2", - "reason": "API-token compatibility duplicate; the Agent uses the corresponding bearer-authenticated operation with its persisted user identity.", - "summary": "后台服务(API_TOKEN)", - "tags": [ - "dashboard" - ] - }, - { - "disposition": "alternate-auth-duplicate", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/dashboard/schedule2/{job_id}/progress", - "reason": "API-token compatibility duplicate; the Agent uses the corresponding bearer-authenticated operation with its persisted user identity.", - "summary": "后台服务进度(API_TOKEN)", - "tags": [ - "dashboard" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "dashboard.media.statistics" - ], - "owner": "moviepilot-api", - "path": "/api/v1/dashboard/statistic", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "媒体数量统计", - "tags": [ - "dashboard" - ] - }, - { - "disposition": "alternate-auth-duplicate", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/dashboard/statistic2", - "reason": "API-token compatibility duplicate; the Agent uses the corresponding bearer-authenticated operation with its persisted user identity.", - "summary": "媒体数量统计(API_TOKEN)", - "tags": [ - "dashboard" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "dashboard.storage" - ], - "owner": "moviepilot-api", - "path": "/api/v1/dashboard/storage", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "本地存储空间", - "tags": [ - "dashboard" - ] - }, - { - "disposition": "alternate-auth-duplicate", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/dashboard/storage2", - "reason": "API-token compatibility duplicate; the Agent uses the corresponding bearer-authenticated operation with its persisted user identity.", - "summary": "本地存储空间(API_TOKEN)", - "tags": [ - "dashboard" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "dashboard.system" - ], - "owner": "moviepilot-api", - "path": "/api/v1/dashboard/system", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "系统摘要信息", - "tags": [ - "dashboard" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "dashboard.transfer.statistics" - ], - "owner": "moviepilot-api", - "path": "/api/v1/dashboard/transfer", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "文件整理统计", - "tags": [ - "dashboard" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/discover/bangumi", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "探索Bangumi", - "tags": [ - "discover" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/discover/douban_movies", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "探索豆瓣电影", - "tags": [ - "discover" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/discover/douban_tvs", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "探索豆瓣剧集", - "tags": [ - "discover" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/discover/source", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "获取探索数据源", - "tags": [ - "discover" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/discover/tmdb_movies", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "探索TMDB电影", - "tags": [ - "discover" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/discover/tmdb_tvs", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "探索TMDB剧集", - "tags": [ - "discover" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/douban/credits/{doubanid}/{type_name}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "豆瓣演员阵容", - "tags": [ - "douban" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/douban/interests/{doubanid}/{type_name}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "豆瓣短评", - "tags": [ - "douban" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/douban/person/credits/{person_id}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "人物参演作品", - "tags": [ - "douban" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/douban/person/{person_id}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "人物详情", - "tags": [ - "douban" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/douban/recommend/{doubanid}/{type_name}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "豆瓣推荐电影/电视剧", - "tags": [ - "douban" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/douban/reviews/{doubanid}/{type_name}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "豆瓣影评", - "tags": [ - "douban" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/douban/{doubanid}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "查询豆瓣详情", - "tags": [ - "douban" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "download.tasks.active" - ], - "owner": "moviepilot-api", - "path": "/api/v1/download/", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "正在下载", - "tags": [ - "download" - ] - }, - { - "disposition": "consolidated", - "method": "POST", - "operation_ids": [ - "download.add" - ], - "owner": "moviepilot-api", - "path": "/api/v1/download/", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation download.add.", - "summary": "添加下载(含媒体信息)", - "tags": [ - "download" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "download.add" - ], - "owner": "moviepilot-api", - "path": "/api/v1/download/add", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "添加下载(不含媒体信息)", - "tags": [ - "download" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "download.artist_collection" - ], - "owner": "moviepilot-api", - "path": "/api/v1/download/artist-collection", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "添加艺术家合集下载", - "tags": [ - "download" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "download.clients" - ], - "owner": "moviepilot-api", - "path": "/api/v1/download/clients", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询可用下载器", - "tags": [ - "download" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "download.paths" - ], - "owner": "moviepilot-api", - "path": "/api/v1/download/paths", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询可用下载路径", - "tags": [ - "download" - ] - }, - { - "disposition": "provider-skill", - "method": "GET", - "operation_ids": [], - "owner": "downloader-operation", - "path": "/api/v1/download/start/{hashString}", - "reason": "Low-level provider behavior is exposed by the self-describing provider Skill; high-level MoviePilot operations remain in moviepilot-api.", - "summary": "开始任务", - "tags": [ - "download" - ] - }, - { - "disposition": "provider-skill", - "method": "GET", - "operation_ids": [], - "owner": "downloader-operation", - "path": "/api/v1/download/stop/{hashString}", - "reason": "Low-level provider behavior is exposed by the self-describing provider Skill; high-level MoviePilot operations remain in moviepilot-api.", - "summary": "暂停任务", - "tags": [ - "download" - ] - }, - { - "disposition": "provider-skill", - "method": "POST", - "operation_ids": [], - "owner": "downloader-operation", - "path": "/api/v1/download/subtitle", - "reason": "Low-level provider behavior is exposed by the self-describing provider Skill; high-level MoviePilot operations remain in moviepilot-api.", - "summary": "下载字幕", - "tags": [ - "download" - ] - }, - { - "disposition": "provider-skill", - "method": "DELETE", - "operation_ids": [], - "owner": "downloader-operation", - "path": "/api/v1/download/{hashString}", - "reason": "Low-level provider behavior is exposed by the self-describing provider Skill; high-level MoviePilot operations remain in moviepilot-api.", - "summary": "删除下载任务", - "tags": [ - "download" - ] - }, - { - "disposition": "provider-skill", - "method": "PATCH", - "operation_ids": [], - "owner": "downloader-operation", - "path": "/api/v1/download/{hashString}", - "reason": "Low-level provider behavior is exposed by the self-describing provider Skill; high-level MoviePilot operations remain in moviepilot-api.", - "summary": "高级更新下载任务", - "tags": [ - "download" - ] - }, - { - "disposition": "provider-skill", - "method": "POST", - "operation_ids": [], - "owner": "downloader-operation", - "path": "/api/v1/download/{hashString}/classify-source", - "reason": "Low-level provider behavior is exposed by the self-describing provider Skill; high-level MoviePilot operations remain in moviepilot-api.", - "summary": "识别并归类已有下载任务", - "tags": [ - "download" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/github/auth/manual", - "reason": "GitHub Token authorization and first-run credential setup are host identity/configuration flows; they are not Agent business actions.", - "summary": "保存手动 GitHub Token", - "tags": [ - "github" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/github/auth/poll", - "reason": "GitHub Token authorization and first-run credential setup are host identity/configuration flows; they are not Agent business actions.", - "summary": "轮询 GitHub 设备授权", - "tags": [ - "github" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/github/auth/start", - "reason": "GitHub Token authorization and first-run credential setup are host identity/configuration flows; they are not Agent business actions.", - "summary": "启动 GitHub 设备授权", - "tags": [ - "github" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/github/auth/status", - "reason": "GitHub Token authorization and first-run credential setup are host identity/configuration flows; they are not Agent business actions.", - "summary": "查询 GitHub Token 状态", - "tags": [ - "github" - ] - }, - { - "disposition": "transport_or_identity", - "method": "DELETE", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/github/auth/token", - "reason": "GitHub Token authorization and first-run credential setup are host identity/configuration flows; they are not Agent business actions.", - "summary": "清除 GitHub Token", - "tags": [ - "github" - ] - }, - { - "disposition": "ui_presentation", - "method": "GET", - "operation_ids": [], - "owner": "host-ui", - "path": "/api/v1/github/community", - "reason": "Community medals are a presentation of the administrator-configured GitHub identity, not an Agent business operation.", - "summary": "查询当前 GitHub 用户的社区勋章", - "tags": [ - "github" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "download.history.delete" - ], - "owner": "moviepilot-api", - "path": "/api/v1/history/download", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "删除下载历史记录", - "tags": [ - "history" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "download.history.list" - ], - "owner": "moviepilot-api", - "path": "/api/v1/history/download", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询下载历史记录", - "tags": [ - "history" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "transfer.history.delete" - ], - "owner": "moviepilot-api", - "path": "/api/v1/history/transfer", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "删除整理记录", - "tags": [ - "history" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "transfer.history" - ], - "owner": "moviepilot-api", - "path": "/api/v1/history/transfer", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询整理记录", - "tags": [ - "history" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "transfer.history.redo_batch" - ], - "owner": "moviepilot-api", - "path": "/api/v1/history/transfer/ai-redo", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "智能助手批量重新整理", - "tags": [ - "history" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "transfer.history.clear" - ], - "owner": "moviepilot-api", - "path": "/api/v1/history/transfer/all", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "清空旧整理记录", - "tags": [ - "history" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "transfer.history.redo" - ], - "owner": "moviepilot-api", - "path": "/api/v1/history/transfer/{history_id}/ai-redo", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "智能助手重新整理", - "tags": [ - "history" - ] - }, - { - "disposition": "ui_presentation", - "method": "POST", - "operation_ids": [], - "owner": "host-ui", - "path": "/api/v1/history/transfer/{history_id}/cleanup-resolved", - "reason": "Manual confirmation of downloader cleanup is owned by the authenticated history management workflow; it is not an Agent action that verifies external cleanup.", - "summary": "确认下载器清理已完成", - "tags": [ - "history" - ] - }, - { - "disposition": "ui_presentation", - "method": "POST", - "operation_ids": [], - "owner": "host-ui", - "path": "/api/v1/history/transfer/{history_id}/discard-corrupt", - "reason": "Discarding corrupt transfer state is owned by the authenticated management recovery workflow; it is not a stable Agent gateway operation.", - "summary": "放弃损坏的整理任务", - "tags": [ - "history" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/llm/manage", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "LLM提供商统一管理", - "tags": [ - "llm" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/llm/provider-auth/callback/{provider_id}", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "LLM提供商OAuth回调", - "tags": [ - "llm" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/login/access-token", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "获取token", - "tags": [ - "login" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/login/github-auth/manual", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "从初始化页保存手动 GitHub Token", - "tags": [ - "login" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/login/github-auth/poll", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "从初始化页轮询 GitHub 设备授权", - "tags": [ - "login" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/login/github-auth/start", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "从初始化页启动 GitHub 设备授权", - "tags": [ - "login" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/login/github-auth/status", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "查询初始化阶段 GitHub Token 状态", - "tags": [ - "login" - ] - }, - { - "disposition": "transport_or_identity", - "method": "DELETE", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/login/github-auth/token", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "从初始化页清除 GitHub Token", - "tags": [ - "login" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/login/initialization", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "查询首次初始化状态", - "tags": [ - "login" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/login/initialization", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "完成首次初始化", - "tags": [ - "login" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/login/wallpaper", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "登录页面电影海报", - "tags": [ - "login" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/login/wallpapers", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "登录页面电影海报列表", - "tags": [ - "login" - ] - }, - { - "disposition": "transport_or_identity", - "method": "DELETE", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/mcp", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "终止 MCP 会话", - "tags": [ - "mcp" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/mcp", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "MCP JSON-RPC 端点", - "tags": [ - "mcp" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/mcp/tools", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "列出所有可用工具", - "tags": [ - "mcp" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/mcp/tools/call", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "调用工具", - "tags": [ - "mcp" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/mcp/tools/{tool_name}", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "获取工具详情", - "tags": [ - "mcp" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/mcp/tools/{tool_name}/schema", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "获取工具参数Schema", - "tags": [ - "mcp" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [ - "media.classification.policy.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/media/category", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation media.classification.policy.get.", - "summary": "查询自动分类配置", - "tags": [ - "media" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [ - "media.classification.policy.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/media/category/config", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation media.classification.policy.get.", - "summary": "获取分类策略配置", - "tags": [ - "media" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "media.classification.fields" - ], - "owner": "moviepilot-api", - "path": "/api/v1/media/classification/fields", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "读取媒体分类字段能力目录", - "tags": [ - "media" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "media.classification.policy.history" - ], - "owner": "moviepilot-api", - "path": "/api/v1/media/classification/history", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "读取媒体分类策略历史", - "tags": [ - "media" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "media.classification.policy.impact" - ], - "owner": "moviepilot-api", - "path": "/api/v1/media/classification/impact", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "分析分类策略对近期样本的估算影响", - "tags": [ - "media" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "media.classification.policy.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/media/classification/policy", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "读取当前媒体分类策略", - "tags": [ - "media" - ] - }, - { - "disposition": "gateway", - "method": "PUT", - "operation_ids": [ - "media.classification.policy.update" - ], - "owner": "moviepilot-api", - "path": "/api/v1/media/classification/policy", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "校验并发布媒体分类策略", - "tags": [ - "media" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "media.classification.policy.preview" - ], - "owner": "moviepilot-api", - "path": "/api/v1/media/classification/preview", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "预览媒体分类策略命中过程", - "tags": [ - "media" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "media.classification.policy.rollback" - ], - "owner": "moviepilot-api", - "path": "/api/v1/media/classification/rollback/{revision}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "把历史媒体分类策略发布为新版本", - "tags": [ - "media" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "media.classification.policy.validate" - ], - "owner": "moviepilot-api", - "path": "/api/v1/media/classification/validate", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "校验媒体分类策略草稿", - "tags": [ - "media" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "media.episode_group.seasons" - ], - "owner": "moviepilot-api", - "path": "/api/v1/media/group/seasons/{episode_group}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询剧集组季信息", - "tags": [ - "media" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "media.episode_groups" - ], - "owner": "moviepilot-api", - "path": "/api/v1/media/groups/{tmdbid}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询媒体剧集组", - "tags": [ - "media" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "media.recognize" - ], - "owner": "moviepilot-api", - "path": "/api/v1/media/recognize", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "识别媒体信息(种子)", - "tags": [ - "media" - ] - }, - { - "disposition": "alternate-auth-duplicate", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/media/recognize2", - "reason": "API-token compatibility duplicate; the Agent uses the corresponding bearer-authenticated operation with its persisted user identity.", - "summary": "识别种子媒体信息(API_TOKEN)", - "tags": [ - "media" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "media.recognize_file" - ], - "owner": "moviepilot-api", - "path": "/api/v1/media/recognize_file", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "识别媒体信息(文件)", - "tags": [ - "media" - ] - }, - { - "disposition": "alternate-auth-duplicate", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/media/recognize_file2", - "reason": "API-token compatibility duplicate; the Agent uses the corresponding bearer-authenticated operation with its persisted user identity.", - "summary": "识别文件媒体信息(API_TOKEN)", - "tags": [ - "media" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "media.scrape" - ], - "owner": "moviepilot-api", - "path": "/api/v1/media/scrape/{storage}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "刮削媒体信息", - "tags": [ - "media" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "media.person.search", - "media.search" - ], - "owner": "moviepilot-api", - "path": "/api/v1/media/search", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "搜索媒体/人物/艺术家信息", - "tags": [ - "media" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "media.seasons" - ], - "owner": "moviepilot-api", - "path": "/api/v1/media/seasons", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询媒体季信息", - "tags": [ - "media" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "media.sources" - ], - "owner": "moviepilot-api", - "path": "/api/v1/media/source", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "获取媒体数据源", - "tags": [ - "media" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "media.detail" - ], - "owner": "moviepilot-api", - "path": "/api/v1/media/{media_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询媒体详情", - "tags": [ - "media" - ] - }, - { - "disposition": "provider-skill", - "method": "GET", - "operation_ids": [], - "owner": "mediaserver-operation", - "path": "/api/v1/mediaserver/clients", - "reason": "Low-level provider behavior is exposed by the self-describing provider Skill; high-level MoviePilot operations remain in moviepilot-api.", - "summary": "查询可用媒体服务器", - "tags": [ - "mediaserver" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "library.exists" - ], - "owner": "moviepilot-api", - "path": "/api/v1/mediaserver/exists", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询本地是否存在(数据库)", - "tags": [ - "mediaserver" - ] - }, - { - "disposition": "provider-skill", - "method": "POST", - "operation_ids": [], - "owner": "mediaserver-operation", - "path": "/api/v1/mediaserver/exists_remote", - "reason": "Low-level provider behavior is exposed by the self-describing provider Skill; high-level MoviePilot operations remain in moviepilot-api.", - "summary": "查询已存在的剧集信息(媒体服务器)", - "tags": [ - "mediaserver" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "library.latest" - ], - "owner": "moviepilot-api", - "path": "/api/v1/mediaserver/latest", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "最新入库条目", - "tags": [ - "mediaserver" - ] - }, - { - "disposition": "provider-skill", - "method": "GET", - "operation_ids": [], - "owner": "mediaserver-operation", - "path": "/api/v1/mediaserver/library", - "reason": "Low-level provider behavior is exposed by the self-describing provider Skill; high-level MoviePilot operations remain in moviepilot-api.", - "summary": "媒体库列表", - "tags": [ - "mediaserver" - ] - }, - { - "disposition": "provider-skill", - "method": "POST", - "operation_ids": [], - "owner": "mediaserver-operation", - "path": "/api/v1/mediaserver/notexists", - "reason": "Low-level provider behavior is exposed by the self-describing provider Skill; high-level MoviePilot operations remain in moviepilot-api.", - "summary": "查询媒体库缺失信息(媒体服务器)", - "tags": [ - "mediaserver" - ] - }, - { - "disposition": "provider-skill", - "method": "GET", - "operation_ids": [], - "owner": "mediaserver-operation", - "path": "/api/v1/mediaserver/play/{itemid}", - "reason": "Low-level provider behavior is exposed by the self-describing provider Skill; high-level MoviePilot operations remain in moviepilot-api.", - "summary": "在线播放", - "tags": [ - "mediaserver" - ] - }, - { - "disposition": "provider-skill", - "method": "GET", - "operation_ids": [], - "owner": "mediaserver-operation", - "path": "/api/v1/mediaserver/playing", - "reason": "Low-level provider behavior is exposed by the self-describing provider Skill; high-level MoviePilot operations remain in moviepilot-api.", - "summary": "正在播放条目", - "tags": [ - "mediaserver" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/message/", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "回调请求验证", - "tags": [ - "message" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/message/", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "接收用户消息", - "tags": [ - "message" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/message/agent/callback", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "Web 智能助手按钮回调", - "tags": [ - "agent" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "slash.list" - ], - "owner": "moviepilot-api", - "path": "/api/v1/message/agent/commands", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "获取 Web 智能助手可用命令", - "tags": [ - "agent" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "slash.run" - ], - "owner": "moviepilot-api", - "path": "/api/v1/message/agent/commands/run", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "执行 Agent 斜杠命令", - "tags": [ - "agent" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/message/agent/file/{file_id}", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "下载 Web 智能助手附件", - "tags": [ - "agent" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/message/agent/mcp/servers", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "查询 Agent MCP 服务器配置", - "tags": [ - "agent" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/message/agent/mcp/servers", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "保存 Agent MCP 服务器配置", - "tags": [ - "agent" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/message/agent/mcp/servers/test", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "测试 Agent MCP 服务器", - "tags": [ - "agent" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/message/agent/sessions", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "获取 Agent 历史会话", - "tags": [ - "agent" - ] - }, - { - "disposition": "transport_or_identity", - "method": "DELETE", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/message/agent/sessions/{session_id}", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "删除 Agent 历史会话", - "tags": [ - "agent" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/message/agent/sessions/{session_id}", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "获取 Agent 历史会话详情", - "tags": [ - "agent" - ] - }, - { - "disposition": "transport_or_identity", - "method": "PUT", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/message/agent/sessions/{session_id}/display", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "保存 Agent 展示会话", - "tags": [ - "agent" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/message/agent/sessions/{session_id}/stop", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "停止 Web 智能助手当前任务", - "tags": [ - "agent" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/message/agent/stream", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "Web智能助手流式对话", - "tags": [ - "agent" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/message/agent/upload", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "上传 Web 智能助手附件", - "tags": [ - "agent" - ] - }, - { - "disposition": "transport_or_identity", - "method": "DELETE", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/message/notification", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "清理通知消息", - "tags": [ - "message" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/message/notification", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "获取通知消息", - "tags": [ - "message" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/message/web", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "获取WEB消息", - "tags": [ - "message" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/message/web", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "接收WEB消息", - "tags": [ - "message" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/message/webpush/send", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "发送webpush通知", - "tags": [ - "message" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/message/webpush/subscribe", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "客户端webpush通知订阅", - "tags": [ - "message" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/mfa/otp/disable", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "关闭当前用户的 OTP 验证", - "tags": [ - "mfa" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/mfa/otp/generate", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "生成 OTP 验证 URI", - "tags": [ - "mfa" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/mfa/otp/verify", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "绑定并验证 OTP", - "tags": [ - "mfa" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/mfa/passkey/authenticate/finish", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "完成 PassKey 认证", - "tags": [ - "mfa" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/mfa/passkey/authenticate/start", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "开始 PassKey 认证", - "tags": [ - "mfa" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/mfa/passkey/delete", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "删除 PassKey", - "tags": [ - "mfa" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/mfa/passkey/list", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "获取当前用户的 PassKey 列表", - "tags": [ - "mfa" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/mfa/passkey/register/finish", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "完成注册 PassKey", - "tags": [ - "mfa" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/mfa/passkey/register/start", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "开始注册 PassKey", - "tags": [ - "mfa" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "music.album.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/music/album/{album_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询音乐专辑详情", - "tags": [ - "music" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "music.album.related" - ], - "owner": "moviepilot-api", - "path": "/api/v1/music/album/{album_id}/related", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询关联音乐专辑", - "tags": [ - "music" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "music.artist.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/music/artist/{artist_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询音乐艺术家详情", - "tags": [ - "music" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "music.artist.albums" - ], - "owner": "moviepilot-api", - "path": "/api/v1/music/artist/{artist_id}/albums", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询艺术家的专辑列表", - "tags": [ - "music" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "music.artist.related" - ], - "owner": "moviepilot-api", - "path": "/api/v1/music/artist/{artist_id}/related", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询关联艺术家", - "tags": [ - "music" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "music.cache.clear" - ], - "owner": "moviepilot-api", - "path": "/api/v1/music/cache", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "清空音乐识别缓存", - "tags": [ - "music" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "music.cache.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/music/cache", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询音乐识别缓存", - "tags": [ - "music" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "music.cache.delete" - ], - "owner": "moviepilot-api", - "path": "/api/v1/music/cache/{cache_key}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "删除指定音乐识别缓存", - "tags": [ - "music" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "music.explore" - ], - "owner": "moviepilot-api", - "path": "/api/v1/music/explore", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "探索音乐", - "tags": [ - "music" - ] - }, - { - "disposition": "ui_presentation", - "method": "POST", - "operation_ids": [], - "owner": "host-ui", - "path": "/api/v1/music/library/status", - "reason": "Batch music-library presence is a bounded projection for the authenticated artist resource matrix; it is not a standalone Agent business action.", - "summary": "批量查询音乐专辑入库状态", - "tags": [ - "music" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "music.recognize" - ], - "owner": "moviepilot-api", - "path": "/api/v1/music/recognize", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "识别音乐元数据详情", - "tags": [ - "music" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/notification/config", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "保存通知渠道并同步登录缓存", - "tags": [ - "notification" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/notification/manage", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "通知渠道统一管理", - "tags": [ - "notification" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/openai/v1/chat/completions", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "OpenAI 兼容聊天补全", - "tags": [ - "openai" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/openai/v1/models", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "OpenAI 兼容模型列表", - "tags": [ - "openai" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/openai/v1/responses", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "OpenAI 兼容响应生成", - "tags": [ - "openai" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "plugin.installed", - "plugin.market" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "所有插件", - "tags": [ - "plugin" - ] - }, - { - "disposition": "ui_presentation", - "method": "GET", - "operation_ids": [], - "owner": "host-ui", - "path": "/api/v1/plugin/agent_pets", - "reason": "Plugin-rendered page, dashboard, or navigation metadata owned by the frontend presentation contract rather than an Agent business action.", - "summary": "获取插件 Agent 助手形象", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "plugin.clone" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/clone/{plugin_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "创建插件分身", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "plugin.clone.restorable" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/clone/{plugin_id}/restorable", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "列出可恢复的已停用分身", - "tags": [ - "plugin" - ] - }, - { - "disposition": "ui_presentation", - "method": "GET", - "operation_ids": [], - "owner": "host-ui", - "path": "/api/v1/plugin/dashboard/meta", - "reason": "Plugin-rendered page, dashboard, or navigation metadata owned by the frontend presentation contract rather than an Agent business action.", - "summary": "获取所有插件仪表板元信息", - "tags": [ - "plugin" - ] - }, - { - "disposition": "ui_presentation", - "method": "GET", - "operation_ids": [], - "owner": "host-ui", - "path": "/api/v1/plugin/dashboard/{plugin_id}", - "reason": "Plugin-rendered page, dashboard, or navigation metadata owned by the frontend presentation contract rather than an Agent business action.", - "summary": "获取插件仪表板配置", - "tags": [ - "plugin" - ] - }, - { - "disposition": "ui_presentation", - "method": "GET", - "operation_ids": [], - "owner": "host-ui", - "path": "/api/v1/plugin/dashboard/{plugin_id}/{key}", - "reason": "Plugin-rendered page, dashboard, or navigation metadata owned by the frontend presentation contract rather than an Agent business action.", - "summary": "获取插件仪表板配置", - "tags": [ - "plugin" - ] - }, - { - "disposition": "stream_or_binary", - "method": "GET", - "operation_ids": [], - "owner": "host-transport", - "path": "/api/v1/plugin/file/{plugin_id}/{filepath}", - "reason": "Streaming, image, archive, or file response consumed by a direct client; the structured JSON Agent gateway does not proxy binary or unbounded streams.", - "summary": "获取插件静态文件", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "plugin.folders.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/folders", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "获取插件文件夹配置", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "plugin.folders.update" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/folders", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "保存插件文件夹配置", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "plugin.folder.delete" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/folders/{folder_name}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "删除插件文件夹", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "PATCH", - "operation_ids": [ - "plugin.folder.update" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/folders/{folder_name}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "更新插件文件夹", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "plugin.folder.create" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/folders/{folder_name}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "创建插件文件夹", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "PUT", - "operation_ids": [ - "plugin.folder.plugins.update" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/folders/{folder_name}/plugins", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "更新文件夹中的插件", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "plugin.folder.plugin.remove" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/folders/{folder_name}/plugins/{plugin_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "从文件夹移除插件", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "PUT", - "operation_ids": [ - "plugin.folder.plugin.assign" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/folders/{folder_name}/plugins/{plugin_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "移动插件到文件夹", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "plugin.config.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/form/{plugin_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "获取插件表单页面", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "plugin.history" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/history/{plugin_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "获取插件更新说明", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "plugin.install" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/install/{plugin_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "安装插件", - "tags": [ - "plugin" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [ - "plugin.installed" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/installed", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation plugin.installed.", - "summary": "已安装插件", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "plugin.instance.set_enabled" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/instance/{instance_id}/enabled", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "启用或停用插件实例", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "plugin.instance.purge" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/instance/{instance_id}/purge", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "按选定范围彻底清理插件实例", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "plugin.default_target.clear" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/instances/{plugin_id}/{instance_id}/default_target", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "清除插件实例的默认调用目标", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "PUT", - "operation_ids": [ - "plugin.default_target.set" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/instances/{plugin_id}/{instance_id}/default_target", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "设置插件实例的默认调用目标", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "plugin.loglevel.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/loglevel/{plugin_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询插件全部实例的日志等级设置", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "plugin.loglevel.clear" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/loglevel/{plugin_id}/{instance_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "清除插件实例的日志等级覆盖", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "PUT", - "operation_ids": [ - "plugin.loglevel.set" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/loglevel/{plugin_id}/{instance_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "设置插件实例的日志等级覆盖", - "tags": [ - "plugin" - ] - }, - { - "disposition": "ui_presentation", - "method": "GET", - "operation_ids": [], - "owner": "host-ui", - "path": "/api/v1/plugin/page/{plugin_id}", - "reason": "Plugin-rendered page, dashboard, or navigation metadata owned by the frontend presentation contract rather than an Agent business action.", - "summary": "获取插件数据页面", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "plugin.ratings" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/rating", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "批量查询插件评分", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "plugin.rating" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/rating/{plugin_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询插件评分", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "plugin.rating.submit" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/rating/{plugin_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "提交插件评分", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "plugin.releases" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/releases/{plugin_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "获取插件Release版本", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "plugin.reload" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/reload/{plugin_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "重新加载插件", - "tags": [ - "plugin" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/plugin/remotes", - "reason": "Health, bootstrap, federation, or external webhook transport endpoint; it is not recursively callable as an Agent business action.", - "summary": "获取插件联邦组件列表", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "plugin.reset" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/reset/{plugin_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "重置插件配置及数据", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "plugin.runtime.status" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/runtime", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "插件运行时收敛状态", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "plugin.capabilities" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/runtime/capabilities", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询插件运行能力", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "plugin.data" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/runtime/{plugin_id}/data", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询插件持久化数据", - "tags": [ - "plugin" - ] - }, - { - "disposition": "ui_presentation", - "method": "GET", - "operation_ids": [], - "owner": "host-ui", - "path": "/api/v1/plugin/runtime/{plugin_id}/data/summary", - "reason": "Plugin-rendered page, dashboard, or navigation metadata owned by the frontend presentation contract rather than an Agent business action.", - "summary": "查询插件持久化数据摘要", - "tags": [ - "plugin" - ] - }, - { - "disposition": "ui_presentation", - "method": "GET", - "operation_ids": [], - "owner": "host-ui", - "path": "/api/v1/plugin/sidebar_nav", - "reason": "Plugin-rendered page, dashboard, or navigation metadata owned by the frontend presentation contract rather than an Agent business action.", - "summary": "获取插件侧栏导航项", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "plugin.source.options" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/source/{plugin_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "获取插件来源身份", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "plugin.source.change" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/source/{plugin_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "切换插件来源", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "plugin.source.install" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/source/{plugin_id}/install", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "按明确来源安装插件", - "tags": [ - "plugin" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [ - "plugin.source.options" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/source/{plugin_id}/options", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation plugin.source.options.", - "summary": "获取插件来源候选", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "plugin.statistics" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/statistic", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "插件安装统计", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "plugin.uninstall" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/{plugin_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "卸载插件", - "tags": [ - "plugin" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [ - "plugin.config.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/{plugin_id}", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation plugin.config.get.", - "summary": "获取插件配置", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "PUT", - "operation_ids": [ - "plugin.config.update" - ], - "owner": "moviepilot-api", - "path": "/api/v1/plugin/{plugin_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "更新插件配置", - "tags": [ - "plugin" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "recommendation.list" - ], - "owner": "moviepilot-api", - "path": "/api/v1/recommend/agent", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "统一获取 Agent 推荐结果", - "tags": [ - "recommend" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/recommend/bangumi_calendar", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "Bangumi每日放送", - "tags": [ - "recommend" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/recommend/douban_movie_hot", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "豆瓣热门电影", - "tags": [ - "recommend" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/recommend/douban_movie_top250", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "豆瓣电影TOP250", - "tags": [ - "recommend" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/recommend/douban_movies", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "豆瓣电影", - "tags": [ - "recommend" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/recommend/douban_showing", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "豆瓣正在热映", - "tags": [ - "recommend" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/recommend/douban_tv_animation", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "豆瓣动画剧集", - "tags": [ - "recommend" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/recommend/douban_tv_hot", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "豆瓣热门电视剧", - "tags": [ - "recommend" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/recommend/douban_tv_weekly_chinese", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "豆瓣国产剧集周榜", - "tags": [ - "recommend" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/recommend/douban_tv_weekly_global", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "豆瓣全球剧集周榜", - "tags": [ - "recommend" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/recommend/douban_tvs", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "豆瓣剧集", - "tags": [ - "recommend" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/recommend/music_douban", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "豆瓣音乐推荐", - "tags": [ - "recommend" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/recommend/music_weekly", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "ListenBrainz 本周热门音乐", - "tags": [ - "recommend" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/recommend/source", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "获取推荐数据源", - "tags": [ - "recommend" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/recommend/tmdb_movies", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "TMDB电影", - "tags": [ - "recommend" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/recommend/tmdb_trending", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "TMDB流行趋势", - "tags": [ - "recommend" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/recommend/tmdb_tvs", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "TMDB剧集", - "tags": [ - "recommend" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "filter.builtin" - ], - "owner": "moviepilot-api", - "path": "/api/v1/rule/builtin", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询内置过滤规则", - "tags": [ - "rule" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "filter.custom" - ], - "owner": "moviepilot-api", - "path": "/api/v1/rule/custom", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询自定义过滤规则", - "tags": [ - "rule" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "filter.custom.add" - ], - "owner": "moviepilot-api", - "path": "/api/v1/rule/custom", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "新增自定义过滤规则", - "tags": [ - "rule" - ] - }, - { - "disposition": "ui_presentation", - "method": "PUT", - "operation_ids": [], - "owner": "host-ui", - "path": "/api/v1/rule/custom/reorder", - "reason": "Plugin-rendered page, dashboard, or navigation metadata owned by the frontend presentation contract rather than an Agent business action.", - "summary": "调整自定义过滤规则顺序", - "tags": [ - "rule" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "filter.custom.delete" - ], - "owner": "moviepilot-api", - "path": "/api/v1/rule/custom/{rule_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "删除自定义过滤规则", - "tags": [ - "rule" - ] - }, - { - "disposition": "gateway", - "method": "PUT", - "operation_ids": [ - "filter.custom.update" - ], - "owner": "moviepilot-api", - "path": "/api/v1/rule/custom/{rule_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "更新自定义过滤规则", - "tags": [ - "rule" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "filter.groups" - ], - "owner": "moviepilot-api", - "path": "/api/v1/rule/groups", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询过滤规则组", - "tags": [ - "rule" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "filter.group.add" - ], - "owner": "moviepilot-api", - "path": "/api/v1/rule/groups", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "新增过滤规则组", - "tags": [ - "rule" - ] - }, - { - "disposition": "ui_presentation", - "method": "PUT", - "operation_ids": [], - "owner": "host-ui", - "path": "/api/v1/rule/groups/reorder", - "reason": "Plugin-rendered page, dashboard, or navigation metadata owned by the frontend presentation contract rather than an Agent business action.", - "summary": "调整过滤规则组顺序", - "tags": [ - "rule" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "filter.group.delete" - ], - "owner": "moviepilot-api", - "path": "/api/v1/rule/groups/{name}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "删除过滤规则组", - "tags": [ - "rule" - ] - }, - { - "disposition": "gateway", - "method": "PUT", - "operation_ids": [ - "filter.group.update" - ], - "owner": "moviepilot-api", - "path": "/api/v1/rule/groups/{name}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "更新过滤规则组", - "tags": [ - "rule" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [ - "search.results" - ], - "owner": "moviepilot-api", - "path": "/api/v1/search/last", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation search.results.", - "summary": "查询搜索结果", - "tags": [ - "search" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "search.results" - ], - "owner": "moviepilot-api", - "path": "/api/v1/search/last/context", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询上次搜索上下文", - "tags": [ - "search" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "search.torrents" - ], - "owner": "moviepilot-api", - "path": "/api/v1/search/media/{media_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "精确搜索资源", - "tags": [ - "search" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [ - "search.torrents" - ], - "owner": "moviepilot-api", - "path": "/api/v1/search/media/{media_id}/stream", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation search.torrents.", - "summary": "渐进式精确搜索资源", - "tags": [ - "search" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "search.recommend" - ], - "owner": "moviepilot-api", - "path": "/api/v1/search/recommend", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "AI推荐资源", - "tags": [ - "search" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "subtitle.search.media" - ], - "owner": "moviepilot-api", - "path": "/api/v1/search/subtitle/media/{media_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "精确搜索字幕", - "tags": [ - "search" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [ - "subtitle.search.media" - ], - "owner": "moviepilot-api", - "path": "/api/v1/search/subtitle/media/{media_id}/stream", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation subtitle.search.media.", - "summary": "渐进式精确搜索字幕", - "tags": [ - "search" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "subtitle.search.title" - ], - "owner": "moviepilot-api", - "path": "/api/v1/search/subtitle/title", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "模糊搜索字幕", - "tags": [ - "search" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [ - "subtitle.search.title" - ], - "owner": "moviepilot-api", - "path": "/api/v1/search/subtitle/title/stream", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation subtitle.search.title.", - "summary": "渐进式模糊搜索字幕", - "tags": [ - "search" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "search.title" - ], - "owner": "moviepilot-api", - "path": "/api/v1/search/title", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "模糊搜索资源", - "tags": [ - "search" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [ - "search.title" - ], - "owner": "moviepilot-api", - "path": "/api/v1/search/title/stream", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation search.title.", - "summary": "渐进式模糊搜索资源", - "tags": [ - "search" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [ - "site.list" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation site.list.", - "summary": "所有站点", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "site.add" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "新增站点", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "PUT", - "operation_ids": [ - "site.update" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "更新站点", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "site.list" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/agent", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询 Agent 可用站点", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "site.auth.options" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/auth", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询认证站点", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "site.authenticate" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/auth", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "用户站点认证", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "site.category" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/category/{site_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "站点分类", - "tags": [ - "site" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [ - "site.cookie.update" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/cookie/{site_id}", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation site.cookie.update.", - "summary": "更新站点Cookie&UA", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "site.cookie.update" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/cookie/{site_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "更新站点Cookie&UA", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "site.cookie.set" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/cookie/{site_id}/set", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "直接保存站点Cookie&UA", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "site.cookiecloud.sync" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/cookiecloud", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "CookieCloud同步", - "tags": [ - "site" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [ - "site.list" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/domain/{site_url}", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation site.list.", - "summary": "站点详情", - "tags": [ - "site" - ] - }, - { - "disposition": "stream_or_binary", - "method": "GET", - "operation_ids": [], - "owner": "host-transport", - "path": "/api/v1/site/icon/{site_id}", - "reason": "Streaming, image, archive, or file response consumed by a direct client; the structured JSON Agent gateway does not proxy binary or unbounded streams.", - "summary": "站点图标", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "site.mapping" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/mapping", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "获取站点域名到名称的映射", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "site.searchable" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/media/{media_type}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "按媒体类型获取可搜索站点", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "site.priorities.update" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/priorities", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "批量更新站点优先级", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "site.reset" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/reset", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "重置站点", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "site.resource" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/resource/{site_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "站点资源", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "site.rss" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/rss", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "所有订阅站点", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "site.statistics" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/statistic", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "所有站点统计信息", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "site.statistic" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/statistic/{site_url}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "特定站点统计信息", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "site.supporting" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/supporting", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "获取支持的站点列表", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "site.test" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/test/{site_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "连接测试", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "site.userdata.latest" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/userdata/latest", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询所有站点最新用户数据", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "site.userdata" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/userdata/{site_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询某站点用户数据", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "site.userdata.refresh" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/userdata/{site_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "更新站点用户数据", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "site.delete" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/{site_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "删除站点", - "tags": [ - "site" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [ - "site.list" - ], - "owner": "moviepilot-api", - "path": "/api/v1/site/{site_id}", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation site.list.", - "summary": "站点详情", - "tags": [ - "site" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "storage.list" - ], - "owner": "moviepilot-api", - "path": "/api/v1/storage/agent/list", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询 Agent 可用目录和文件", - "tags": [ - "storage" - ] - }, - { - "disposition": "ui_presentation", - "method": "GET", - "operation_ids": [], - "owner": "host-ui", - "path": "/api/v1/storage/catalog", - "reason": "Plugin-rendered page, dashboard, or navigation metadata owned by the frontend presentation contract rather than an Agent business action.", - "summary": "查询存储类型目录", - "tags": [ - "storage" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "storage.delete" - ], - "owner": "moviepilot-api", - "path": "/api/v1/storage/delete", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "删除文件或目录", - "tags": [ - "storage" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "storage.settings" - ], - "owner": "moviepilot-api", - "path": "/api/v1/storage/directories", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询目录配置", - "tags": [ - "storage" - ] - }, - { - "disposition": "stream_or_binary", - "method": "POST", - "operation_ids": [], - "owner": "host-transport", - "path": "/api/v1/storage/download", - "reason": "Streaming, image, archive, or file response consumed by a direct client; the structured JSON Agent gateway does not proxy binary or unbounded streams.", - "summary": "下载文件", - "tags": [ - "storage" - ] - }, - { - "disposition": "stream_or_binary", - "method": "POST", - "operation_ids": [], - "owner": "host-transport", - "path": "/api/v1/storage/image", - "reason": "Streaming, image, archive, or file response consumed by a direct client; the structured JSON Agent gateway does not proxy binary or unbounded streams.", - "summary": "预览图片", - "tags": [ - "storage" - ] - }, - { - "disposition": "consolidated", - "method": "POST", - "operation_ids": [ - "storage.list" - ], - "owner": "moviepilot-api", - "path": "/api/v1/storage/list", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation storage.list.", - "summary": "所有目录和文件", - "tags": [ - "storage" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "storage.manage" - ], - "owner": "moviepilot-api", - "path": "/api/v1/storage/manage", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "网盘存储统一管理", - "tags": [ - "storage" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "storage.mkdir" - ], - "owner": "moviepilot-api", - "path": "/api/v1/storage/mkdir", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "创建目录", - "tags": [ - "storage" - ] - }, - { - "disposition": "ui_presentation", - "method": "GET", - "operation_ids": [], - "owner": "host-ui", - "path": "/api/v1/storage/options", - "reason": "Plugin-rendered page, dashboard, or navigation metadata owned by the frontend presentation contract rather than an Agent business action.", - "summary": "查询可用存储选项", - "tags": [ - "storage" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "storage.rename" - ], - "owner": "moviepilot-api", - "path": "/api/v1/storage/rename", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "重命名文件或目录", - "tags": [ - "storage" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "subscription.list" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询所有订阅", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "subscription.add" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "新增订阅", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "PUT", - "operation_ids": [ - "subscription.update" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "更新订阅", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "subscription.metadata.refresh" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/check", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "刷新订阅 TMDB 信息", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "subscription.execution.list" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/execution/batches", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查看订阅搜索进度", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "subscription.execution.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/execution/batches/{batch_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查看一次订阅搜索", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "PUT", - "operation_ids": [ - "subscription.execution.cancel" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/execution/batches/{batch_id}/cancel", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "停止一次订阅搜索", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "subscription.files" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/files/{subscribe_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "订阅相关文件信息", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "subscription.follow.delete" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/follow", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "取消Follow订阅分享人", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "subscription.follow.list" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/follow", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询已Follow的订阅分享人", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "subscription.follow.add" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/follow", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "Follow订阅分享人", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "subscription.fork" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/fork", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "复用订阅", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "subscription.history.delete" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/history/{history_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "删除订阅历史", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "subscription.history" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/history/{mtype}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询订阅历史", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [ - "subscription.list" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/list", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation subscription.list.", - "summary": "查询所有订阅(API_TOKEN)", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "subscription.delete_by_media" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/media/{media_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "删除订阅", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "subscription.find" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/media/{media_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询订阅", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "subscription.popular" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/popular", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "热门订阅(基于用户共享数据)", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "subscription.refresh" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/refresh", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "刷新订阅", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "subscription.reset" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/reset/{subid}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "重置订阅", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "subscription.search_all" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/search", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "搜索所有订阅", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "subscription.search" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/search/{subscribe_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "搜索订阅", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/subscribe/seerr", - "reason": "Health, bootstrap, federation, or external webhook transport endpoint; it is not recursively callable as an Agent business action.", - "summary": "OverSeerr/JellySeerr通知订阅", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "subscription.share" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/share", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "分享订阅", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "subscription.share.statistics" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/share/statistics", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询订阅分享统计", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "subscription.share.delete" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/share/{share_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "删除分享", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "subscription.shares" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/shares", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询分享的订阅", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "PUT", - "operation_ids": [ - "subscription.status.update" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/status/{subid}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "更新订阅状态", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "subscription.user.list" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/user/{username}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "用户订阅", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "subscription.delete" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/{subscribe_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "删除订阅", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "subscription.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/subscribe/{subscribe_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "订阅详情", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "ui_presentation", - "method": "GET", - "operation_ids": [], - "owner": "host-ui", - "path": "/api/v1/subscribe/{subscription_id}/activity", - "reason": "Subscription activity details, background execution status and cancellation are owned by the authenticated frontend workflow; they are not yet a stable Agent gateway contract.", - "summary": "查看订阅业务活动", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "ui_presentation", - "method": "GET", - "operation_ids": [], - "owner": "host-ui", - "path": "/api/v1/subscribe/{subscription_id}/activity/{event_id}", - "reason": "Subscription activity details, background execution status and cancellation are owned by the authenticated frontend workflow; they are not yet a stable Agent gateway contract.", - "summary": "查看订阅活动详情", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "stream_or_binary", - "method": "GET", - "operation_ids": [], - "owner": "host-transport", - "path": "/api/v1/subscribe/{subscription_id}/logs", - "reason": "Streaming, image, archive, or file response consumed by a direct client; the structured JSON Agent gateway does not proxy binary or unbounded streams.", - "summary": "查看单个订阅的执行日志", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "stream_or_binary", - "method": "GET", - "operation_ids": [], - "owner": "host-transport", - "path": "/api/v1/subscribe/{subscription_id}/logs/download", - "reason": "Streaming, image, archive, or file response consumed by a direct client; the structured JSON Agent gateway does not proxy binary or unbounded streams.", - "summary": "下载单个订阅的执行日志", - "tags": [ - "subscribe" - ] - }, - { - "disposition": "stream_or_binary", - "method": "GET", - "operation_ids": [], - "owner": "host-transport", - "path": "/api/v1/system/cache/image", - "reason": "Streaming, image, archive, or file response consumed by a direct client; the structured JSON Agent gateway does not proxy binary or unbounded streams.", - "summary": "图片缓存", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "database.backups.list" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/database/backups", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询受管数据库备份", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "database.backups.create" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/database/backups", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "立即创建数据库备份", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "database.backups.delete" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/database/backups/{name}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "删除受管数据库备份", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "database.backups.verify" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/database/backups/{name}/verify", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "校验受管数据库备份", - "tags": [ - "system" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [ - "config.system.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/env", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation config.system.get.", - "summary": "查询系统配置", - "tags": [ - "system" - ] - }, - { - "disposition": "consolidated", - "method": "POST", - "operation_ids": [ - "config.system.update" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/env", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation config.system.update.", - "summary": "更新系统配置", - "tags": [ - "system" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [ - "config.system.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/global", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation config.system.get.", - "summary": "查询非敏感系统设置", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "config.user.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/global/user", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询用户相关系统设置", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "config.identifiers.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/identifiers", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询自定义识别词", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "config.identifiers.update" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/identifiers", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "更新自定义识别词", - "tags": [ - "system" - ] - }, - { - "disposition": "stream_or_binary", - "method": "GET", - "operation_ids": [], - "owner": "host-transport", - "path": "/api/v1/system/img/{proxy}", - "reason": "Streaming, image, archive, or file response consumed by a direct client; the structured JSON Agent gateway does not proxy binary or unbounded streams.", - "summary": "图片代理", - "tags": [ - "system" - ] - }, - { - "disposition": "stream_or_binary", - "method": "GET", - "operation_ids": [], - "owner": "host-transport", - "path": "/api/v1/system/logging", - "reason": "Streaming, image, archive, or file response consumed by a direct client; the structured JSON Agent gateway does not proxy binary or unbounded streams.", - "summary": "实时日志", - "tags": [ - "system" - ] - }, - { - "disposition": "stream_or_binary", - "method": "GET", - "operation_ids": [], - "owner": "host-transport", - "path": "/api/v1/system/logging/download/{name}", - "reason": "Streaming, image, archive, or file response consumed by a direct client; the structured JSON Agent gateway does not proxy binary or unbounded streams.", - "summary": "下载日志", - "tags": [ - "system" - ] - }, - { - "disposition": "stream_or_binary", - "method": "GET", - "operation_ids": [], - "owner": "host-transport", - "path": "/api/v1/system/message", - "reason": "Streaming, image, archive, or file response consumed by a direct client; the structured JSON Agent gateway does not proxy binary or unbounded streams.", - "summary": "实时消息", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "system.module.catalog" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/module-catalog", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询宿主模块目录", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "system.module.settings" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/module-settings", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询可手动开关的内置模块", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "system.module.list" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/modulelist", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询已启用的模块ID列表", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "system.module.test" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/moduletest/{moduleid}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "模块可用性测试", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "system.network.test" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/nettest", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "测试网络连通性", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "system.network.targets" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/nettest/targets", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "获取网络测试目标", - "tags": [ - "system" - ] - }, - { - "disposition": "stream_or_binary", - "method": "GET", - "operation_ids": [], - "owner": "host-transport", - "path": "/api/v1/system/notification-image", - "reason": "Streaming, image, archive, or file response consumed by a direct client; the structured JSON Agent gateway does not proxy binary or unbounded streams.", - "summary": "通知图片代理", - "tags": [ - "system" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/system/ping", - "reason": "Health, bootstrap, federation, or external webhook transport endpoint; it is not recursively callable as an Agent business action.", - "summary": "服务存活检测", - "tags": [ - "system" - ] - }, - { - "disposition": "stream_or_binary", - "method": "GET", - "operation_ids": [], - "owner": "host-transport", - "path": "/api/v1/system/progress/{process_type}", - "reason": "Streaming, image, archive, or file response consumed by a direct client; the structured JSON Agent gateway does not proxy binary or unbounded streams.", - "summary": "实时进度", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "system.restart" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/restart", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "重启系统", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "filter.test" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/ruletest", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "过滤规则测试", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "scheduler.run" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/runscheduler", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "运行服务", - "tags": [ - "system" - ] - }, - { - "disposition": "alternate-auth-duplicate", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/system/runscheduler2", - "reason": "API-token compatibility duplicate; the Agent uses the corresponding bearer-authenticated operation with its persisted user identity.", - "summary": "运行服务(API_TOKEN)", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "plugin.market.sync_wiki" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/setting/PLUGIN_MARKET/sync-wiki", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "从Wiki同步插件市场仓库", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "config.public.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/setting/public/{key}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询公开系统设置", - "tags": [ - "system" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [ - "config.system.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/setting/{key}", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation config.system.get.", - "summary": "查询系统设置", - "tags": [ - "system" - ] - }, - { - "disposition": "consolidated", - "method": "POST", - "operation_ids": [ - "config.system.update" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/setting/{key}", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation config.system.update.", - "summary": "更新系统设置", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "config.system.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/settings", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "发现或读取已登记的系统设置", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "config.system.update" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/settings", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "更新单项已登记系统设置", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "config.system.list" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/settings/catalog", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "列出系统设置契约", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "config.system.describe" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/settings/describe/{setting_key}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询单项系统设置契约", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "system.update.check" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/update/check", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "立即检查系统更新", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "system.update.download" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/update/download", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "后台下载系统更新", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "system.update.install" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/update/install", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "确认重启安装系统更新", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "system.update.status" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/update/status", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询系统更新状态", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "system.upgrade.dev" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/upgrade", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "Dev 更新并重启系统", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "system.usage.statistics" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/usage/statistic", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询安装版本统计报表", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "system.versions" - ], - "owner": "moviepilot-api", - "path": "/api/v1/system/versions", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询Github所有Release版本", - "tags": [ - "system" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "media.cache.clear" - ], - "owner": "moviepilot-api", - "path": "/api/v1/tmdb/cache", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "清空 TheMovieDb 识别缓存", - "tags": [ - "tmdb" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "media.cache.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/tmdb/cache", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询 TheMovieDb 识别缓存", - "tags": [ - "tmdb" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "media.cache.delete" - ], - "owner": "moviepilot-api", - "path": "/api/v1/tmdb/cache/{cache_key}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "删除指定 TheMovieDb 识别缓存", - "tags": [ - "tmdb" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/tmdb/collection/{collection_id}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "系列合集详情", - "tags": [ - "tmdb" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/tmdb/credits/{tmdbid}/{type_name}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "演员阵容", - "tags": [ - "tmdb" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/tmdb/person/credits/{person_id}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "人物参演作品", - "tags": [ - "tmdb" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/tmdb/person/{person_id}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "人物详情", - "tags": [ - "tmdb" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/tmdb/recommend/{tmdbid}/{type_name}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "推荐电影/电视剧", - "tags": [ - "tmdb" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/tmdb/seasons/{tmdbid}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "TMDB所有季", - "tags": [ - "tmdb" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [], - "owner": "moviepilot-api", - "path": "/api/v1/tmdb/similar/{tmdbid}/{type_name}", - "reason": "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - "summary": "类似电影/电视剧", - "tags": [ - "tmdb" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "media.episode_schedule" - ], - "owner": "moviepilot-api", - "path": "/api/v1/tmdb/{tmdbid}/{season}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "TMDB季所有集", - "tags": [ - "tmdb" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "torrent.cache.clear" - ], - "owner": "moviepilot-api", - "path": "/api/v1/torrent/cache", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "清理种子缓存", - "tags": [ - "torrent" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "torrent.cache.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/torrent/cache", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "获取种子缓存", - "tags": [ - "torrent" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "torrent.cache.refresh" - ], - "owner": "moviepilot-api", - "path": "/api/v1/torrent/cache/refresh", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "刷新种子缓存", - "tags": [ - "torrent" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "torrent.cache.reidentify" - ], - "owner": "moviepilot-api", - "path": "/api/v1/torrent/cache/reidentify/{domain}/{torrent_hash}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "重新识别种子", - "tags": [ - "torrent" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "torrent.cache.delete" - ], - "owner": "moviepilot-api", - "path": "/api/v1/torrent/cache/{domain}/{torrent_hash}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "删除指定种子缓存", - "tags": [ - "torrent" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "transfer.episode_format.recommend" - ], - "owner": "moviepilot-api", - "path": "/api/v1/transfer/episode-format/recommend", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "推荐集数定位模板", - "tags": [ - "transfer" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "transfer.file" - ], - "owner": "moviepilot-api", - "path": "/api/v1/transfer/manual", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "手动转移", - "tags": [ - "transfer" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "transfer.manual_history" - ], - "owner": "moviepilot-api", - "path": "/api/v1/transfer/manual/history", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询手动转移成功历史", - "tags": [ - "transfer" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "transfer.target_path" - ], - "owner": "moviepilot-api", - "path": "/api/v1/transfer/manual/target-path", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "匹配手动转移目的路径", - "tags": [ - "transfer" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "transfer.name" - ], - "owner": "moviepilot-api", - "path": "/api/v1/transfer/name", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询整理后的名称", - "tags": [ - "transfer" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [ - "scheduler.run" - ], - "owner": "moviepilot-api", - "path": "/api/v1/transfer/now", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation scheduler.run.", - "summary": "立即执行下载器文件整理", - "tags": [ - "transfer" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "transfer.queue.delete" - ], - "owner": "moviepilot-api", - "path": "/api/v1/transfer/queue", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "从整理队列中删除任务", - "tags": [ - "transfer" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "transfer.queue" - ], - "owner": "moviepilot-api", - "path": "/api/v1/transfer/queue", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询整理队列", - "tags": [ - "transfer" - ] - }, - { - "disposition": "ui_presentation", - "method": "GET", - "operation_ids": [], - "owner": "host-ui", - "path": "/api/v1/transfer/queue/page", - "reason": "Plugin-rendered page, dashboard, or navigation metadata owned by the frontend presentation contract rather than an Agent business action.", - "summary": "查询受限整理队列快照", - "tags": [ - "transfer" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "transfer.manual_reviews" - ], - "owner": "moviepilot-api", - "path": "/api/v1/transfer/tasks/manual-reviews", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "分页查询 durable 整理人工复核任务", - "tags": [ - "transfer" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "transfer.manual_review" - ], - "owner": "moviepilot-api", - "path": "/api/v1/transfer/tasks/{task_id}/manual-review", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询 durable 整理人工复核详情", - "tags": [ - "transfer" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "transfer.manual_review.resolve" - ], - "owner": "moviepilot-api", - "path": "/api/v1/transfer/tasks/{task_id}/manual-review", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "人工判定整理步骤的外部执行结果", - "tags": [ - "transfer" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/user/", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "所有用户", - "tags": [ - "user" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/user/", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "新增用户", - "tags": [ - "user" - ] - }, - { - "disposition": "transport_or_identity", - "method": "PUT", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/user/", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "更新用户", - "tags": [ - "user" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/user/avatar/{user_id}", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "上传用户头像", - "tags": [ - "user" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/user/config/{key}", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "查询用户配置", - "tags": [ - "user" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/user/config/{key}", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "更新用户配置", - "tags": [ - "user" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/user/current", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "当前登录用户信息", - "tags": [ - "user" - ] - }, - { - "disposition": "transport_or_identity", - "method": "PUT", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/user/current", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "更新当前用户资料", - "tags": [ - "user" - ] - }, - { - "disposition": "transport_or_identity", - "method": "DELETE", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/user/id/{user_id}", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "删除用户", - "tags": [ - "user" - ] - }, - { - "disposition": "transport_or_identity", - "method": "DELETE", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/user/name/{user_name}", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "删除用户", - "tags": [ - "user" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/user/{username}", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "用户详情", - "tags": [ - "user" - ] - }, - { - "disposition": "transport_or_identity", - "method": "GET", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/webhook/", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "Webhook消息响应", - "tags": [ - "webhook" - ] - }, - { - "disposition": "transport_or_identity", - "method": "POST", - "operation_ids": [], - "owner": "host-runtime", - "path": "/api/v1/webhook/", - "reason": "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - "summary": "Webhook消息响应", - "tags": [ - "webhook" - ] - }, - { - "disposition": "consolidated", - "method": "GET", - "operation_ids": [ - "workflow.list" - ], - "owner": "moviepilot-api", - "path": "/api/v1/workflow/", - "reason": "This compatibility, broader-response, or UI route is represented by the safer stable operation workflow.list.", - "summary": "所有工作流", - "tags": [ - "workflow" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "workflow.create" - ], - "owner": "moviepilot-api", - "path": "/api/v1/workflow/", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "创建工作流", - "tags": [ - "workflow" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "workflow.actions" - ], - "owner": "moviepilot-api", - "path": "/api/v1/workflow/actions", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "所有动作", - "tags": [ - "workflow" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "workflow.list" - ], - "owner": "moviepilot-api", - "path": "/api/v1/workflow/agent", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询 Agent 可用工作流", - "tags": [ - "workflow" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "workflow.event_types" - ], - "owner": "moviepilot-api", - "path": "/api/v1/workflow/event_types", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "获取所有事件类型", - "tags": [ - "workflow" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "workflow.fork" - ], - "owner": "moviepilot-api", - "path": "/api/v1/workflow/fork", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "复用工作流", - "tags": [ - "workflow" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "workflow.plugin.actions" - ], - "owner": "moviepilot-api", - "path": "/api/v1/workflow/plugin/actions", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询插件动作", - "tags": [ - "workflow" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "workflow.share" - ], - "owner": "moviepilot-api", - "path": "/api/v1/workflow/share", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "分享工作流", - "tags": [ - "workflow" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "workflow.share.delete" - ], - "owner": "moviepilot-api", - "path": "/api/v1/workflow/share/{share_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "删除分享", - "tags": [ - "workflow" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "workflow.shares" - ], - "owner": "moviepilot-api", - "path": "/api/v1/workflow/shares", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "查询分享的工作流", - "tags": [ - "workflow" - ] - }, - { - "disposition": "gateway", - "method": "DELETE", - "operation_ids": [ - "workflow.delete" - ], - "owner": "moviepilot-api", - "path": "/api/v1/workflow/{workflow_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "删除工作流", - "tags": [ - "workflow" - ] - }, - { - "disposition": "gateway", - "method": "GET", - "operation_ids": [ - "workflow.get" - ], - "owner": "moviepilot-api", - "path": "/api/v1/workflow/{workflow_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "工作流详情", - "tags": [ - "workflow" - ] - }, - { - "disposition": "gateway", - "method": "PUT", - "operation_ids": [ - "workflow.update" - ], - "owner": "moviepilot-api", - "path": "/api/v1/workflow/{workflow_id}", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "更新工作流", - "tags": [ - "workflow" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "workflow.pause" - ], - "owner": "moviepilot-api", - "path": "/api/v1/workflow/{workflow_id}/pause", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "停用工作流", - "tags": [ - "workflow" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "workflow.reset" - ], - "owner": "moviepilot-api", - "path": "/api/v1/workflow/{workflow_id}/reset", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "重置工作流", - "tags": [ - "workflow" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "workflow.run" - ], - "owner": "moviepilot-api", - "path": "/api/v1/workflow/{workflow_id}/run", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "执行工作流", - "tags": [ - "workflow" - ] - }, - { - "disposition": "gateway", - "method": "POST", - "operation_ids": [ - "workflow.start" - ], - "owner": "moviepilot-api", - "path": "/api/v1/workflow/{workflow_id}/start", - "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - "summary": "启用工作流", - "tags": [ - "workflow" - ] - } - ] -} diff --git a/docs/refactor/agent-api-surface-audit.md b/docs/refactor/agent-api-surface-audit.md deleted file mode 100644 index f5f35640a..000000000 --- a/docs/refactor/agent-api-surface-audit.md +++ /dev/null @@ -1,715 +0,0 @@ -# MoviePilot Agent API Surface Audit - -> Generated from the v1 FastAPI OpenAPI document and the fixed Agent API registry. -> Do not edit route rows manually; run `scripts/generate_agent_api_surface_audit.py`. - -## Result - -- OpenAPI HTTP operations: **431** -- Stable `moviepilot_api` operations: **231** -- Exact HTTP routes used by the gateway: **229** -- OpenAPI routes matched directly by the gateway: **228** -- Bounded dynamic gateway routes: **1** -- Every gateway operation has a generated English oneOf input contract in MCP `tools/list` and `skills/moviepilot-api/SKILL.md`. -- Every non-gateway OpenAPI operation is listed below with an explicit ownership boundary; it is not silently callable through arbitrary URL/method input. - -## Dispositions - -| disposition | count | meaning | -| :--- | ---: | :--- | -| `alternate-auth-duplicate` | 11 | API-token compatibility duplicate of a bearer-authenticated capability. | -| `consolidated` | 73 | Source/UI route represented by a stable aggregate Agent operation. | -| `gateway` | 228 | Approved structured MoviePilot Agent operation. | -| `provider-skill` | 12 | Low-level downloader or media-server capability owned by a provider Skill. | -| `stream_or_binary` | 13 | Streaming or binary response owned by a direct client transport. | -| `transport_or_identity` | 76 | Authentication, protocol, callback, account, or conversation transport boundary. | -| `ui_presentation` | 18 | Frontend or plugin-rendered presentation contract. | - -## Gateway Identity Audit - -All gateway operations are audited below. New operations default to bound_user; safe_read alone never grants public access. -public: audited external public metadata only; the host-authenticated executor cannot call other operations. -bound_user: an active account bound to the stable channel sender ID, or the authenticated Web user ID; endpoint ownership and management permissions still apply. -admin: administrator authority, preserving existing verified channel/API integration semantics; this never substitutes for ownership on bound_user operations. -Title recognition retains bound_user because its Context exposes locally configured applied identification rules. File recognition, source availability, rules, search results, media libraries, storage and public UI settings remain instance data. -Plugin ratings include this installation's own rating and require the same administrator role as their REST dependencies. Community share/popularity/statistics and global plugin installation statistics are public reads; follow lists and rating submissions are not. - -| operation | method | path | identity scope | role | effect | result sensitivity | confirmation | -| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- | -| `config.identifiers.get` | `GET` | `/api/v1/system/identifiers` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `config.identifiers.update` | `POST` | `/api/v1/system/identifiers` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `config.public.get` | `GET` | `/api/v1/system/setting/public/{key}` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `config.system.describe` | `GET` | `/api/v1/system/settings/describe/{setting_key}` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `config.system.get` | `GET` | `/api/v1/system/settings` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `config.system.list` | `GET` | `/api/v1/system/settings/catalog` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `config.system.update` | `POST` | `/api/v1/system/settings` | `admin` | `system_admin` | `reversible_write` | `private` | `required` | -| `config.user.get` | `GET` | `/api/v1/system/global/user` | `bound_user` | `user` | `safe_read` | `private` | `none` | -| `dashboard.cpu` | `GET` | `/api/v1/dashboard/cpu` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `dashboard.downloader` | `GET` | `/api/v1/dashboard/downloader` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `dashboard.media.statistics` | `GET` | `/api/v1/dashboard/statistic` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `dashboard.memory` | `GET` | `/api/v1/dashboard/memory` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `dashboard.network` | `GET` | `/api/v1/dashboard/network` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `dashboard.processes` | `GET` | `/api/v1/dashboard/processes` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `dashboard.storage` | `GET` | `/api/v1/dashboard/storage` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `dashboard.system` | `GET` | `/api/v1/dashboard/system` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `dashboard.transfer.statistics` | `GET` | `/api/v1/dashboard/transfer` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `database.backups.create` | `POST` | `/api/v1/system/database/backups` | `admin` | `system_admin` | `external_side_effect` | `private` | `required` | -| `database.backups.delete` | `DELETE` | `/api/v1/system/database/backups/{name}` | `admin` | `system_admin` | `destructive_write` | `normal` | `required` | -| `database.backups.list` | `GET` | `/api/v1/system/database/backups` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `database.backups.verify` | `POST` | `/api/v1/system/database/backups/{name}/verify` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `download.add` | `POST` | `/api/v1/download/add` | `bound_user` | `user` | `external_side_effect` | `normal` | `required` | -| `download.artist_collection` | `POST` | `/api/v1/download/artist-collection` | `bound_user` | `user` | `external_side_effect` | `normal` | `required` | -| `download.clients` | `GET` | `/api/v1/download/clients` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `download.history.delete` | `DELETE` | `/api/v1/history/download` | `admin` | `system_admin` | `destructive_write` | `normal` | `required` | -| `download.history.list` | `GET` | `/api/v1/history/download` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `download.paths` | `GET` | `/api/v1/download/paths` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `download.tasks.active` | `GET` | `/api/v1/download/` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `filter.builtin` | `GET` | `/api/v1/rule/builtin` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `filter.custom` | `GET` | `/api/v1/rule/custom` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `filter.custom.add` | `POST` | `/api/v1/rule/custom` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `filter.custom.delete` | `DELETE` | `/api/v1/rule/custom/{rule_id}` | `admin` | `system_admin` | `destructive_write` | `normal` | `required` | -| `filter.custom.update` | `PUT` | `/api/v1/rule/custom/{rule_id}` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `filter.group.add` | `POST` | `/api/v1/rule/groups` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `filter.group.delete` | `DELETE` | `/api/v1/rule/groups/{name}` | `admin` | `system_admin` | `destructive_write` | `normal` | `required` | -| `filter.group.update` | `PUT` | `/api/v1/rule/groups/{name}` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `filter.groups` | `GET` | `/api/v1/rule/groups` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `filter.test` | `GET` | `/api/v1/system/ruletest` | `bound_user` | `user` | `external_side_effect` | `normal` | `none` | -| `library.exists` | `GET` | `/api/v1/mediaserver/exists` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `library.latest` | `GET` | `/api/v1/mediaserver/latest` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `media.cache.clear` | `DELETE` | `/api/v1/tmdb/cache` | `admin` | `system_admin` | `destructive_write` | `normal` | `required` | -| `media.cache.delete` | `DELETE` | `/api/v1/tmdb/cache/{cache_key}` | `admin` | `system_admin` | `destructive_write` | `normal` | `required` | -| `media.cache.get` | `GET` | `/api/v1/tmdb/cache` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `media.classification.fields` | `GET` | `/api/v1/media/classification/fields` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `media.classification.policy.get` | `GET` | `/api/v1/media/classification/policy` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `media.classification.policy.history` | `GET` | `/api/v1/media/classification/history` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `media.classification.policy.impact` | `POST` | `/api/v1/media/classification/impact` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `media.classification.policy.preview` | `POST` | `/api/v1/media/classification/preview` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `media.classification.policy.rollback` | `POST` | `/api/v1/media/classification/rollback/{revision}` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `media.classification.policy.update` | `PUT` | `/api/v1/media/classification/policy` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `media.classification.policy.validate` | `POST` | `/api/v1/media/classification/validate` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `media.detail` | `GET` | `/api/v1/media/{media_id}` | `public` | `user` | `safe_read` | `normal` | `none` | -| `media.episode_group.seasons` | `GET` | `/api/v1/media/group/seasons/{episode_group}` | `public` | `user` | `safe_read` | `normal` | `none` | -| `media.episode_groups` | `GET` | `/api/v1/media/groups/{tmdbid}` | `public` | `user` | `safe_read` | `normal` | `none` | -| `media.episode_schedule` | `GET` | `/api/v1/tmdb/{tmdbid}/{season}` | `public` | `user` | `safe_read` | `normal` | `none` | -| `media.person.credits` | `GET` | `/api/v1/{source}/person/credits/{person_id}` | `public` | `user` | `safe_read` | `normal` | `none` | -| `media.person.search` | `GET` | `/api/v1/media/search` | `public` | `user` | `safe_read` | `normal` | `none` | -| `media.recognize` | `GET` | `/api/v1/media/recognize` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `media.recognize_file` | `GET` | `/api/v1/media/recognize_file` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `media.scrape` | `POST` | `/api/v1/media/scrape/{storage}` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `media.search` | `GET` | `/api/v1/media/search` | `public` | `user` | `safe_read` | `normal` | `none` | -| `media.seasons` | `GET` | `/api/v1/media/seasons` | `public` | `user` | `safe_read` | `normal` | `none` | -| `media.sources` | `GET` | `/api/v1/media/source` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `music.album.get` | `GET` | `/api/v1/music/album/{album_id}` | `public` | `user` | `safe_read` | `normal` | `none` | -| `music.album.related` | `GET` | `/api/v1/music/album/{album_id}/related` | `public` | `user` | `safe_read` | `normal` | `none` | -| `music.artist.albums` | `GET` | `/api/v1/music/artist/{artist_id}/albums` | `public` | `user` | `safe_read` | `normal` | `none` | -| `music.artist.get` | `GET` | `/api/v1/music/artist/{artist_id}` | `public` | `user` | `safe_read` | `normal` | `none` | -| `music.artist.related` | `GET` | `/api/v1/music/artist/{artist_id}/related` | `public` | `user` | `safe_read` | `normal` | `none` | -| `music.cache.clear` | `DELETE` | `/api/v1/music/cache` | `admin` | `system_admin` | `destructive_write` | `normal` | `required` | -| `music.cache.delete` | `DELETE` | `/api/v1/music/cache/{cache_key}` | `admin` | `system_admin` | `destructive_write` | `normal` | `required` | -| `music.cache.get` | `GET` | `/api/v1/music/cache` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `music.explore` | `GET` | `/api/v1/music/explore` | `public` | `user` | `safe_read` | `normal` | `none` | -| `music.recognize` | `POST` | `/api/v1/music/recognize` | `public` | `user` | `safe_read` | `normal` | `none` | -| `plugin.capabilities` | `GET` | `/api/v1/plugin/runtime/capabilities` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `plugin.clone` | `POST` | `/api/v1/plugin/clone/{plugin_id}` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `plugin.clone.restorable` | `GET` | `/api/v1/plugin/clone/{plugin_id}/restorable` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `plugin.config.get` | `GET` | `/api/v1/plugin/form/{plugin_id}` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `plugin.config.update` | `PUT` | `/api/v1/plugin/{plugin_id}` | `admin` | `system_admin` | `reversible_write` | `private` | `required` | -| `plugin.data` | `GET` | `/api/v1/plugin/runtime/{plugin_id}/data` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `plugin.default_target.clear` | `DELETE` | `/api/v1/plugin/instances/{plugin_id}/{instance_id}/default_target` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `plugin.default_target.set` | `PUT` | `/api/v1/plugin/instances/{plugin_id}/{instance_id}/default_target` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `plugin.folder.create` | `POST` | `/api/v1/plugin/folders/{folder_name}` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `plugin.folder.delete` | `DELETE` | `/api/v1/plugin/folders/{folder_name}` | `admin` | `system_admin` | `destructive_write` | `normal` | `required` | -| `plugin.folder.plugin.assign` | `PUT` | `/api/v1/plugin/folders/{folder_name}/plugins/{plugin_id}` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `plugin.folder.plugin.remove` | `DELETE` | `/api/v1/plugin/folders/{folder_name}/plugins/{plugin_id}` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `plugin.folder.plugins.update` | `PUT` | `/api/v1/plugin/folders/{folder_name}/plugins` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `plugin.folder.update` | `PATCH` | `/api/v1/plugin/folders/{folder_name}` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `plugin.folders.get` | `GET` | `/api/v1/plugin/folders` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `plugin.folders.update` | `POST` | `/api/v1/plugin/folders` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `plugin.history` | `GET` | `/api/v1/plugin/history/{plugin_id}` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `plugin.install` | `GET` | `/api/v1/plugin/install/{plugin_id}` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `plugin.installed` | `GET` | `/api/v1/plugin/` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `plugin.instance.purge` | `POST` | `/api/v1/plugin/instance/{instance_id}/purge` | `admin` | `system_admin` | `destructive_write` | `normal` | `required` | -| `plugin.instance.set_enabled` | `POST` | `/api/v1/plugin/instance/{instance_id}/enabled` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `plugin.loglevel.clear` | `DELETE` | `/api/v1/plugin/loglevel/{plugin_id}/{instance_id}` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `plugin.loglevel.get` | `GET` | `/api/v1/plugin/loglevel/{plugin_id}` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `plugin.loglevel.set` | `PUT` | `/api/v1/plugin/loglevel/{plugin_id}/{instance_id}` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `plugin.market` | `GET` | `/api/v1/plugin/` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `plugin.market.sync_wiki` | `POST` | `/api/v1/system/setting/PLUGIN_MARKET/sync-wiki` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `plugin.rating` | `GET` | `/api/v1/plugin/rating/{plugin_id}` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `plugin.rating.submit` | `POST` | `/api/v1/plugin/rating/{plugin_id}` | `admin` | `system_admin` | `external_side_effect` | `private` | `required` | -| `plugin.ratings` | `GET` | `/api/v1/plugin/rating` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `plugin.releases` | `GET` | `/api/v1/plugin/releases/{plugin_id}` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `plugin.reload` | `POST` | `/api/v1/plugin/reload/{plugin_id}` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `plugin.reset` | `GET` | `/api/v1/plugin/reset/{plugin_id}` | `admin` | `system_admin` | `destructive_write` | `normal` | `required` | -| `plugin.runtime.status` | `GET` | `/api/v1/plugin/runtime` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `plugin.source.change` | `POST` | `/api/v1/plugin/source/{plugin_id}` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `plugin.source.install` | `POST` | `/api/v1/plugin/source/{plugin_id}/install` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `plugin.source.options` | `GET` | `/api/v1/plugin/source/{plugin_id}` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `plugin.statistics` | `GET` | `/api/v1/plugin/statistic` | `public` | `user` | `safe_read` | `normal` | `none` | -| `plugin.uninstall` | `DELETE` | `/api/v1/plugin/{plugin_id}` | `admin` | `system_admin` | `destructive_write` | `normal` | `required` | -| `recommendation.list` | `GET` | `/api/v1/recommend/agent` | `public` | `user` | `safe_read` | `normal` | `none` | -| `scheduler.list` | `GET` | `/api/v1/dashboard/schedule` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `scheduler.progress` | `GET` | `/api/v1/dashboard/schedule/{job_id}/progress` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `scheduler.run` | `GET` | `/api/v1/system/runscheduler` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `search.recommend` | `POST` | `/api/v1/search/recommend` | `bound_user` | `user` | `external_side_effect` | `normal` | `none` | -| `search.results` | `GET` | `/api/v1/search/last/context` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `search.title` | `GET` | `/api/v1/search/title` | `bound_user` | `user` | `external_side_effect` | `normal` | `none` | -| `search.torrents` | `GET` | `/api/v1/search/media/{media_id}` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `site.add` | `POST` | `/api/v1/site/` | `admin` | `system_admin` | `reversible_write` | `private` | `required` | -| `site.auth.options` | `GET` | `/api/v1/site/auth` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `site.authenticate` | `POST` | `/api/v1/site/auth` | `admin` | `system_admin` | `external_side_effect` | `private` | `required` | -| `site.category` | `GET` | `/api/v1/site/category/{site_id}` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `site.cookie.set` | `POST` | `/api/v1/site/cookie/{site_id}/set` | `admin` | `system_admin` | `reversible_write` | `private` | `required` | -| `site.cookie.update` | `POST` | `/api/v1/site/cookie/{site_id}` | `admin` | `system_admin` | `reversible_write` | `private` | `required` | -| `site.cookiecloud.sync` | `POST` | `/api/v1/site/cookiecloud` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `site.delete` | `DELETE` | `/api/v1/site/{site_id}` | `admin` | `system_admin` | `destructive_write` | `normal` | `required` | -| `site.list` | `GET` | `/api/v1/site/agent` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `site.mapping` | `GET` | `/api/v1/site/mapping` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `site.priorities.update` | `POST` | `/api/v1/site/priorities` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `site.reset` | `POST` | `/api/v1/site/reset` | `admin` | `system_admin` | `destructive_write` | `normal` | `required` | -| `site.resource` | `GET` | `/api/v1/site/resource/{site_id}` | `admin` | `system_admin` | `external_side_effect` | `private` | `none` | -| `site.rss` | `GET` | `/api/v1/site/rss` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `site.searchable` | `GET` | `/api/v1/site/media/{media_type}` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `site.statistic` | `GET` | `/api/v1/site/statistic/{site_url}` | `bound_user` | `user` | `safe_read` | `private` | `none` | -| `site.statistics` | `GET` | `/api/v1/site/statistic` | `bound_user` | `user` | `safe_read` | `private` | `none` | -| `site.supporting` | `GET` | `/api/v1/site/supporting` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `site.test` | `GET` | `/api/v1/site/test/{site_id}` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `site.update` | `PUT` | `/api/v1/site/` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `site.userdata` | `GET` | `/api/v1/site/userdata/{site_id}` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `site.userdata.latest` | `GET` | `/api/v1/site/userdata/latest` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `site.userdata.refresh` | `POST` | `/api/v1/site/userdata/{site_id}` | `admin` | `system_admin` | `external_side_effect` | `private` | `required` | -| `slash.list` | `GET` | `/api/v1/message/agent/commands` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `slash.run` | `POST` | `/api/v1/message/agent/commands/run` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `storage.delete` | `POST` | `/api/v1/storage/delete` | `admin` | `system_admin` | `destructive_write` | `normal` | `required` | -| `storage.list` | `POST` | `/api/v1/storage/agent/list` | `bound_user` | `user` | `safe_read` | `private` | `none` | -| `storage.manage` | `POST` | `/api/v1/storage/manage` | `admin` | `system_admin` | `external_side_effect` | `private` | `required` | -| `storage.mkdir` | `POST` | `/api/v1/storage/mkdir` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `storage.rename` | `POST` | `/api/v1/storage/rename` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `storage.settings` | `GET` | `/api/v1/storage/directories` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `subscription.add` | `POST` | `/api/v1/subscribe/` | `bound_user` | `user` | `reversible_write` | `normal` | `required` | -| `subscription.delete` | `DELETE` | `/api/v1/subscribe/{subscribe_id}` | `bound_user` | `user` | `destructive_write` | `normal` | `required` | -| `subscription.delete_by_media` | `DELETE` | `/api/v1/subscribe/media/{media_id}` | `bound_user` | `user` | `destructive_write` | `normal` | `required` | -| `subscription.execution.cancel` | `PUT` | `/api/v1/subscribe/execution/batches/{batch_id}/cancel` | `bound_user` | `user` | `external_side_effect` | `private` | `required` | -| `subscription.execution.get` | `GET` | `/api/v1/subscribe/execution/batches/{batch_id}` | `bound_user` | `user` | `safe_read` | `private` | `none` | -| `subscription.execution.list` | `GET` | `/api/v1/subscribe/execution/batches` | `bound_user` | `user` | `safe_read` | `private` | `none` | -| `subscription.files` | `GET` | `/api/v1/subscribe/files/{subscribe_id}` | `bound_user` | `user` | `safe_read` | `private` | `none` | -| `subscription.find` | `GET` | `/api/v1/subscribe/media/{media_id}` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `subscription.follow.add` | `POST` | `/api/v1/subscribe/follow` | `bound_user` | `user` | `reversible_write` | `normal` | `required` | -| `subscription.follow.delete` | `DELETE` | `/api/v1/subscribe/follow` | `bound_user` | `user` | `reversible_write` | `normal` | `required` | -| `subscription.follow.list` | `GET` | `/api/v1/subscribe/follow` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `subscription.fork` | `POST` | `/api/v1/subscribe/fork` | `bound_user` | `user` | `external_side_effect` | `normal` | `required` | -| `subscription.get` | `GET` | `/api/v1/subscribe/{subscribe_id}` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `subscription.history` | `GET` | `/api/v1/subscribe/history/{mtype}` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `subscription.history.delete` | `DELETE` | `/api/v1/subscribe/history/{history_id}` | `bound_user` | `user` | `destructive_write` | `normal` | `required` | -| `subscription.list` | `GET` | `/api/v1/subscribe/` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `subscription.metadata.refresh` | `POST` | `/api/v1/subscribe/check` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `subscription.popular` | `GET` | `/api/v1/subscribe/popular` | `public` | `user` | `safe_read` | `normal` | `none` | -| `subscription.refresh` | `POST` | `/api/v1/subscribe/refresh` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `subscription.reset` | `POST` | `/api/v1/subscribe/reset/{subid}` | `bound_user` | `user` | `reversible_write` | `normal` | `required` | -| `subscription.search` | `POST` | `/api/v1/subscribe/search/{subscribe_id}` | `bound_user` | `user` | `external_side_effect` | `normal` | `required` | -| `subscription.search_all` | `POST` | `/api/v1/subscribe/search` | `bound_user` | `user` | `external_side_effect` | `normal` | `required` | -| `subscription.share` | `POST` | `/api/v1/subscribe/share` | `bound_user` | `user` | `external_side_effect` | `normal` | `required` | -| `subscription.share.delete` | `DELETE` | `/api/v1/subscribe/share/{share_id}` | `bound_user` | `user` | `external_side_effect` | `normal` | `required` | -| `subscription.share.statistics` | `GET` | `/api/v1/subscribe/share/statistics` | `public` | `user` | `safe_read` | `normal` | `none` | -| `subscription.shares` | `GET` | `/api/v1/subscribe/shares` | `public` | `user` | `safe_read` | `normal` | `none` | -| `subscription.status.update` | `PUT` | `/api/v1/subscribe/status/{subid}` | `bound_user` | `user` | `reversible_write` | `normal` | `required` | -| `subscription.update` | `PUT` | `/api/v1/subscribe/` | `bound_user` | `user` | `reversible_write` | `normal` | `required` | -| `subscription.user.list` | `GET` | `/api/v1/subscribe/user/{username}` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `subtitle.search.media` | `GET` | `/api/v1/search/subtitle/media/{media_id}` | `bound_user` | `user` | `external_side_effect` | `normal` | `none` | -| `subtitle.search.title` | `GET` | `/api/v1/search/subtitle/title` | `bound_user` | `user` | `external_side_effect` | `normal` | `none` | -| `system.module.catalog` | `GET` | `/api/v1/system/module-catalog` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `system.module.list` | `GET` | `/api/v1/system/modulelist` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `system.module.settings` | `GET` | `/api/v1/system/module-settings` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `system.module.test` | `GET` | `/api/v1/system/moduletest/{moduleid}` | `bound_user` | `user` | `external_side_effect` | `normal` | `none` | -| `system.network.targets` | `GET` | `/api/v1/system/nettest/targets` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `system.network.test` | `GET` | `/api/v1/system/nettest` | `bound_user` | `user` | `external_side_effect` | `normal` | `none` | -| `system.restart` | `GET` | `/api/v1/system/restart` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `system.update.check` | `POST` | `/api/v1/system/update/check` | `admin` | `system_admin` | `external_side_effect` | `normal` | `none` | -| `system.update.download` | `POST` | `/api/v1/system/update/download` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `system.update.install` | `POST` | `/api/v1/system/update/install` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `system.update.status` | `GET` | `/api/v1/system/update/status` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `system.upgrade.dev` | `POST` | `/api/v1/system/upgrade` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `system.usage.statistics` | `GET` | `/api/v1/system/usage/statistic` | `bound_user` | `user` | `safe_read` | `private` | `none` | -| `system.versions` | `GET` | `/api/v1/system/versions` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `torrent.cache.clear` | `DELETE` | `/api/v1/torrent/cache` | `admin` | `system_admin` | `destructive_write` | `normal` | `required` | -| `torrent.cache.delete` | `DELETE` | `/api/v1/torrent/cache/{domain}/{torrent_hash}` | `admin` | `system_admin` | `destructive_write` | `normal` | `required` | -| `torrent.cache.get` | `GET` | `/api/v1/torrent/cache` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `torrent.cache.refresh` | `POST` | `/api/v1/torrent/cache/refresh` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `torrent.cache.reidentify` | `POST` | `/api/v1/torrent/cache/reidentify/{domain}/{torrent_hash}` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `transfer.episode_format.recommend` | `POST` | `/api/v1/transfer/episode-format/recommend` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `transfer.file` | `POST` | `/api/v1/transfer/manual` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `transfer.history` | `GET` | `/api/v1/history/transfer` | `bound_user` | `user` | `safe_read` | `private` | `none` | -| `transfer.history.clear` | `DELETE` | `/api/v1/history/transfer/all` | `admin` | `system_admin` | `destructive_write` | `normal` | `required` | -| `transfer.history.delete` | `DELETE` | `/api/v1/history/transfer` | `admin` | `system_admin` | `destructive_write` | `normal` | `required` | -| `transfer.history.redo` | `POST` | `/api/v1/history/transfer/{history_id}/ai-redo` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `transfer.history.redo_batch` | `POST` | `/api/v1/history/transfer/ai-redo` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `transfer.manual_history` | `POST` | `/api/v1/transfer/manual/history` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `transfer.manual_review` | `GET` | `/api/v1/transfer/tasks/{task_id}/manual-review` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `transfer.manual_review.resolve` | `POST` | `/api/v1/transfer/tasks/{task_id}/manual-review` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `transfer.manual_reviews` | `GET` | `/api/v1/transfer/tasks/manual-reviews` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `transfer.name` | `GET` | `/api/v1/transfer/name` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `transfer.queue` | `GET` | `/api/v1/transfer/queue` | `bound_user` | `user` | `safe_read` | `private` | `none` | -| `transfer.queue.delete` | `DELETE` | `/api/v1/transfer/queue` | `bound_user` | `user` | `destructive_write` | `normal` | `required` | -| `transfer.target_path` | `POST` | `/api/v1/transfer/manual/target-path` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `workflow.actions` | `GET` | `/api/v1/workflow/actions` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `workflow.create` | `POST` | `/api/v1/workflow/` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `workflow.delete` | `DELETE` | `/api/v1/workflow/{workflow_id}` | `admin` | `system_admin` | `destructive_write` | `normal` | `required` | -| `workflow.event_types` | `GET` | `/api/v1/workflow/event_types` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `workflow.fork` | `POST` | `/api/v1/workflow/fork` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `workflow.get` | `GET` | `/api/v1/workflow/{workflow_id}` | `admin` | `system_admin` | `safe_read` | `private` | `none` | -| `workflow.list` | `GET` | `/api/v1/workflow/agent` | `bound_user` | `user` | `safe_read` | `normal` | `none` | -| `workflow.pause` | `POST` | `/api/v1/workflow/{workflow_id}/pause` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `workflow.plugin.actions` | `GET` | `/api/v1/workflow/plugin/actions` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `workflow.reset` | `POST` | `/api/v1/workflow/{workflow_id}/reset` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `workflow.run` | `POST` | `/api/v1/workflow/{workflow_id}/run` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `workflow.share` | `POST` | `/api/v1/workflow/share` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `workflow.share.delete` | `DELETE` | `/api/v1/workflow/share/{share_id}` | `admin` | `system_admin` | `external_side_effect` | `normal` | `required` | -| `workflow.shares` | `GET` | `/api/v1/workflow/shares` | `admin` | `system_admin` | `safe_read` | `normal` | `none` | -| `workflow.start` | `POST` | `/api/v1/workflow/{workflow_id}/start` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | -| `workflow.update` | `PUT` | `/api/v1/workflow/{workflow_id}` | `admin` | `system_admin` | `reversible_write` | `normal` | `required` | - -## Bounded Dynamic Routes - -| method | route template | operations | constraint | -| :--- | :--- | :--- | :--- | -| `GET` | `/api/v1/{source}/person/credits/{person_id}` | media.person.credits | The executor validates and expands this bounded source placeholder to one of tmdb, douban, bangumi, or anilist before calling the corresponding concrete OpenAPI route. | - -## Complete Route Inventory - -| method | path | tags | disposition | owner / operation | summary | -| :--- | :--- | :--- | :--- | :--- | :--- | -| `GET` | `/api/v1/anilist/credits/{anilist_id}` | anilist | `consolidated` | moviepilot-api | 查询 AniList 配音演员 | -| `GET` | `/api/v1/anilist/discover` | anilist | `consolidated` | moviepilot-api | 探索 AniList 动画 | -| `GET` | `/api/v1/anilist/person/credits/{person_id}` | anilist | `consolidated` | moviepilot-api | 查询 AniList 人物作品 | -| `GET` | `/api/v1/anilist/person/{person_id}` | anilist | `consolidated` | moviepilot-api | 查询 AniList 人物详情 | -| `GET` | `/api/v1/anilist/popular-this-season` | anilist | `consolidated` | moviepilot-api | 查询 AniList 本季热门榜 | -| `GET` | `/api/v1/anilist/recommend/{anilist_id}` | anilist | `consolidated` | moviepilot-api | 查询 AniList 相关推荐 | -| `GET` | `/api/v1/anilist/trending` | anilist | `consolidated` | moviepilot-api | 查询 AniList 当前趋势榜 | -| `GET` | `/api/v1/anilist/{anilist_id}` | anilist | `consolidated` | moviepilot-api | 查询 AniList 动画详情 | -| `POST` | `/api/v1/anthropic/v1/messages` | anthropic | `transport_or_identity` | host-runtime | Anthropic 兼容消息生成 | -| `POST` | `/api/v1/auth/exchange` | auth | `transport_or_identity` | host-runtime | 兑换插件认证登录票据 | -| `GET` | `/api/v1/auth/providers` | auth | `transport_or_identity` | host-runtime | 查询登录认证提供方 | -| `GET` | `/api/v1/bangumi/credits/{bangumiid}` | bangumi | `consolidated` | moviepilot-api | 查询Bangumi演职员表 | -| `GET` | `/api/v1/bangumi/person/credits/{person_id}` | bangumi | `consolidated` | moviepilot-api | 人物参演作品 | -| `GET` | `/api/v1/bangumi/person/{person_id}` | bangumi | `consolidated` | moviepilot-api | 人物详情 | -| `GET` | `/api/v1/bangumi/recommend/{bangumiid}` | bangumi | `consolidated` | moviepilot-api | 查询Bangumi推荐 | -| `GET` | `/api/v1/bangumi/{bangumiid}` | bangumi | `consolidated` | moviepilot-api | 查询Bangumi详情 | -| `GET` | `/api/v1/dashboard/cpu` | dashboard | `gateway` | dashboard.cpu | 获取当前CPU使用率 | -| `GET` | `/api/v1/dashboard/cpu2` | dashboard | `alternate-auth-duplicate` | moviepilot-api | 获取当前CPU使用率(API_TOKEN) | -| `GET` | `/api/v1/dashboard/downloader` | dashboard | `gateway` | dashboard.downloader | 下载器信息 | -| `GET` | `/api/v1/dashboard/downloader2` | dashboard | `alternate-auth-duplicate` | moviepilot-api | 下载器信息(API_TOKEN) | -| `GET` | `/api/v1/dashboard/memory` | dashboard | `gateway` | dashboard.memory | 获取当前应用与系统内存信息 | -| `GET` | `/api/v1/dashboard/memory2` | dashboard | `alternate-auth-duplicate` | moviepilot-api | 获取当前应用与系统内存信息(API_TOKEN) | -| `GET` | `/api/v1/dashboard/network` | dashboard | `gateway` | dashboard.network | 获取当前网络流量 | -| `GET` | `/api/v1/dashboard/network2` | dashboard | `alternate-auth-duplicate` | moviepilot-api | 获取当前网络流量(API_TOKEN) | -| `GET` | `/api/v1/dashboard/processes` | dashboard | `gateway` | dashboard.processes | 进程信息 | -| `GET` | `/api/v1/dashboard/schedule` | dashboard | `gateway` | scheduler.list | 后台服务 | -| `GET` | `/api/v1/dashboard/schedule/{job_id}/progress` | dashboard | `gateway` | scheduler.progress | 后台服务进度 | -| `GET` | `/api/v1/dashboard/schedule2` | dashboard | `alternate-auth-duplicate` | moviepilot-api | 后台服务(API_TOKEN) | -| `GET` | `/api/v1/dashboard/schedule2/{job_id}/progress` | dashboard | `alternate-auth-duplicate` | moviepilot-api | 后台服务进度(API_TOKEN) | -| `GET` | `/api/v1/dashboard/statistic` | dashboard | `gateway` | dashboard.media.statistics | 媒体数量统计 | -| `GET` | `/api/v1/dashboard/statistic2` | dashboard | `alternate-auth-duplicate` | moviepilot-api | 媒体数量统计(API_TOKEN) | -| `GET` | `/api/v1/dashboard/storage` | dashboard | `gateway` | dashboard.storage | 本地存储空间 | -| `GET` | `/api/v1/dashboard/storage2` | dashboard | `alternate-auth-duplicate` | moviepilot-api | 本地存储空间(API_TOKEN) | -| `GET` | `/api/v1/dashboard/system` | dashboard | `gateway` | dashboard.system | 系统摘要信息 | -| `GET` | `/api/v1/dashboard/transfer` | dashboard | `gateway` | dashboard.transfer.statistics | 文件整理统计 | -| `GET` | `/api/v1/discover/bangumi` | discover | `consolidated` | moviepilot-api | 探索Bangumi | -| `GET` | `/api/v1/discover/douban_movies` | discover | `consolidated` | moviepilot-api | 探索豆瓣电影 | -| `GET` | `/api/v1/discover/douban_tvs` | discover | `consolidated` | moviepilot-api | 探索豆瓣剧集 | -| `GET` | `/api/v1/discover/source` | discover | `consolidated` | moviepilot-api | 获取探索数据源 | -| `GET` | `/api/v1/discover/tmdb_movies` | discover | `consolidated` | moviepilot-api | 探索TMDB电影 | -| `GET` | `/api/v1/discover/tmdb_tvs` | discover | `consolidated` | moviepilot-api | 探索TMDB剧集 | -| `GET` | `/api/v1/douban/credits/{doubanid}/{type_name}` | douban | `consolidated` | moviepilot-api | 豆瓣演员阵容 | -| `GET` | `/api/v1/douban/interests/{doubanid}/{type_name}` | douban | `consolidated` | moviepilot-api | 豆瓣短评 | -| `GET` | `/api/v1/douban/person/credits/{person_id}` | douban | `consolidated` | moviepilot-api | 人物参演作品 | -| `GET` | `/api/v1/douban/person/{person_id}` | douban | `consolidated` | moviepilot-api | 人物详情 | -| `GET` | `/api/v1/douban/recommend/{doubanid}/{type_name}` | douban | `consolidated` | moviepilot-api | 豆瓣推荐电影/电视剧 | -| `GET` | `/api/v1/douban/reviews/{doubanid}/{type_name}` | douban | `consolidated` | moviepilot-api | 豆瓣影评 | -| `GET` | `/api/v1/douban/{doubanid}` | douban | `consolidated` | moviepilot-api | 查询豆瓣详情 | -| `GET` | `/api/v1/download/` | download | `gateway` | download.tasks.active | 正在下载 | -| `POST` | `/api/v1/download/` | download | `consolidated` | download.add | 添加下载(含媒体信息) | -| `POST` | `/api/v1/download/add` | download | `gateway` | download.add | 添加下载(不含媒体信息) | -| `POST` | `/api/v1/download/artist-collection` | download | `gateway` | download.artist_collection | 添加艺术家合集下载 | -| `GET` | `/api/v1/download/clients` | download | `gateway` | download.clients | 查询可用下载器 | -| `GET` | `/api/v1/download/paths` | download | `gateway` | download.paths | 查询可用下载路径 | -| `GET` | `/api/v1/download/start/{hashString}` | download | `provider-skill` | downloader-operation | 开始任务 | -| `GET` | `/api/v1/download/stop/{hashString}` | download | `provider-skill` | downloader-operation | 暂停任务 | -| `POST` | `/api/v1/download/subtitle` | download | `provider-skill` | downloader-operation | 下载字幕 | -| `DELETE` | `/api/v1/download/{hashString}` | download | `provider-skill` | downloader-operation | 删除下载任务 | -| `PATCH` | `/api/v1/download/{hashString}` | download | `provider-skill` | downloader-operation | 高级更新下载任务 | -| `POST` | `/api/v1/download/{hashString}/classify-source` | download | `provider-skill` | downloader-operation | 识别并归类已有下载任务 | -| `POST` | `/api/v1/github/auth/manual` | github | `transport_or_identity` | host-runtime | 保存手动 GitHub Token | -| `POST` | `/api/v1/github/auth/poll` | github | `transport_or_identity` | host-runtime | 轮询 GitHub 设备授权 | -| `POST` | `/api/v1/github/auth/start` | github | `transport_or_identity` | host-runtime | 启动 GitHub 设备授权 | -| `GET` | `/api/v1/github/auth/status` | github | `transport_or_identity` | host-runtime | 查询 GitHub Token 状态 | -| `DELETE` | `/api/v1/github/auth/token` | github | `transport_or_identity` | host-runtime | 清除 GitHub Token | -| `GET` | `/api/v1/github/community` | github | `ui_presentation` | host-ui | 查询当前 GitHub 用户的社区勋章 | -| `DELETE` | `/api/v1/history/download` | history | `gateway` | download.history.delete | 删除下载历史记录 | -| `GET` | `/api/v1/history/download` | history | `gateway` | download.history.list | 查询下载历史记录 | -| `DELETE` | `/api/v1/history/transfer` | history | `gateway` | transfer.history.delete | 删除整理记录 | -| `GET` | `/api/v1/history/transfer` | history | `gateway` | transfer.history | 查询整理记录 | -| `POST` | `/api/v1/history/transfer/ai-redo` | history | `gateway` | transfer.history.redo_batch | 智能助手批量重新整理 | -| `DELETE` | `/api/v1/history/transfer/all` | history | `gateway` | transfer.history.clear | 清空旧整理记录 | -| `POST` | `/api/v1/history/transfer/{history_id}/ai-redo` | history | `gateway` | transfer.history.redo | 智能助手重新整理 | -| `POST` | `/api/v1/history/transfer/{history_id}/cleanup-resolved` | history | `ui_presentation` | host-ui | 确认下载器清理已完成 | -| `POST` | `/api/v1/history/transfer/{history_id}/discard-corrupt` | history | `ui_presentation` | host-ui | 放弃损坏的整理任务 | -| `POST` | `/api/v1/llm/manage` | llm | `transport_or_identity` | host-runtime | LLM提供商统一管理 | -| `GET` | `/api/v1/llm/provider-auth/callback/{provider_id}` | llm | `transport_or_identity` | host-runtime | LLM提供商OAuth回调 | -| `POST` | `/api/v1/login/access-token` | login | `transport_or_identity` | host-runtime | 获取token | -| `POST` | `/api/v1/login/github-auth/manual` | login | `transport_or_identity` | host-runtime | 从初始化页保存手动 GitHub Token | -| `POST` | `/api/v1/login/github-auth/poll` | login | `transport_or_identity` | host-runtime | 从初始化页轮询 GitHub 设备授权 | -| `POST` | `/api/v1/login/github-auth/start` | login | `transport_or_identity` | host-runtime | 从初始化页启动 GitHub 设备授权 | -| `GET` | `/api/v1/login/github-auth/status` | login | `transport_or_identity` | host-runtime | 查询初始化阶段 GitHub Token 状态 | -| `DELETE` | `/api/v1/login/github-auth/token` | login | `transport_or_identity` | host-runtime | 从初始化页清除 GitHub Token | -| `GET` | `/api/v1/login/initialization` | login | `transport_or_identity` | host-runtime | 查询首次初始化状态 | -| `POST` | `/api/v1/login/initialization` | login | `transport_or_identity` | host-runtime | 完成首次初始化 | -| `GET` | `/api/v1/login/wallpaper` | login | `transport_or_identity` | host-runtime | 登录页面电影海报 | -| `GET` | `/api/v1/login/wallpapers` | login | `transport_or_identity` | host-runtime | 登录页面电影海报列表 | -| `DELETE` | `/api/v1/mcp` | mcp | `transport_or_identity` | host-runtime | 终止 MCP 会话 | -| `POST` | `/api/v1/mcp` | mcp | `transport_or_identity` | host-runtime | MCP JSON-RPC 端点 | -| `GET` | `/api/v1/mcp/tools` | mcp | `transport_or_identity` | host-runtime | 列出所有可用工具 | -| `POST` | `/api/v1/mcp/tools/call` | mcp | `transport_or_identity` | host-runtime | 调用工具 | -| `GET` | `/api/v1/mcp/tools/{tool_name}` | mcp | `transport_or_identity` | host-runtime | 获取工具详情 | -| `GET` | `/api/v1/mcp/tools/{tool_name}/schema` | mcp | `transport_or_identity` | host-runtime | 获取工具参数Schema | -| `GET` | `/api/v1/media/category` | media | `consolidated` | media.classification.policy.get | 查询自动分类配置 | -| `GET` | `/api/v1/media/category/config` | media | `consolidated` | media.classification.policy.get | 获取分类策略配置 | -| `GET` | `/api/v1/media/classification/fields` | media | `gateway` | media.classification.fields | 读取媒体分类字段能力目录 | -| `GET` | `/api/v1/media/classification/history` | media | `gateway` | media.classification.policy.history | 读取媒体分类策略历史 | -| `POST` | `/api/v1/media/classification/impact` | media | `gateway` | media.classification.policy.impact | 分析分类策略对近期样本的估算影响 | -| `GET` | `/api/v1/media/classification/policy` | media | `gateway` | media.classification.policy.get | 读取当前媒体分类策略 | -| `PUT` | `/api/v1/media/classification/policy` | media | `gateway` | media.classification.policy.update | 校验并发布媒体分类策略 | -| `POST` | `/api/v1/media/classification/preview` | media | `gateway` | media.classification.policy.preview | 预览媒体分类策略命中过程 | -| `POST` | `/api/v1/media/classification/rollback/{revision}` | media | `gateway` | media.classification.policy.rollback | 把历史媒体分类策略发布为新版本 | -| `POST` | `/api/v1/media/classification/validate` | media | `gateway` | media.classification.policy.validate | 校验媒体分类策略草稿 | -| `GET` | `/api/v1/media/group/seasons/{episode_group}` | media | `gateway` | media.episode_group.seasons | 查询剧集组季信息 | -| `GET` | `/api/v1/media/groups/{tmdbid}` | media | `gateway` | media.episode_groups | 查询媒体剧集组 | -| `GET` | `/api/v1/media/recognize` | media | `gateway` | media.recognize | 识别媒体信息(种子) | -| `GET` | `/api/v1/media/recognize2` | media | `alternate-auth-duplicate` | moviepilot-api | 识别种子媒体信息(API_TOKEN) | -| `GET` | `/api/v1/media/recognize_file` | media | `gateway` | media.recognize_file | 识别媒体信息(文件) | -| `GET` | `/api/v1/media/recognize_file2` | media | `alternate-auth-duplicate` | moviepilot-api | 识别文件媒体信息(API_TOKEN) | -| `POST` | `/api/v1/media/scrape/{storage}` | media | `gateway` | media.scrape | 刮削媒体信息 | -| `GET` | `/api/v1/media/search` | media | `gateway` | media.person.search, media.search | 搜索媒体/人物/艺术家信息 | -| `GET` | `/api/v1/media/seasons` | media | `gateway` | media.seasons | 查询媒体季信息 | -| `GET` | `/api/v1/media/source` | media | `gateway` | media.sources | 获取媒体数据源 | -| `GET` | `/api/v1/media/{media_id}` | media | `gateway` | media.detail | 查询媒体详情 | -| `GET` | `/api/v1/mediaserver/clients` | mediaserver | `provider-skill` | mediaserver-operation | 查询可用媒体服务器 | -| `GET` | `/api/v1/mediaserver/exists` | mediaserver | `gateway` | library.exists | 查询本地是否存在(数据库) | -| `POST` | `/api/v1/mediaserver/exists_remote` | mediaserver | `provider-skill` | mediaserver-operation | 查询已存在的剧集信息(媒体服务器) | -| `GET` | `/api/v1/mediaserver/latest` | mediaserver | `gateway` | library.latest | 最新入库条目 | -| `GET` | `/api/v1/mediaserver/library` | mediaserver | `provider-skill` | mediaserver-operation | 媒体库列表 | -| `POST` | `/api/v1/mediaserver/notexists` | mediaserver | `provider-skill` | mediaserver-operation | 查询媒体库缺失信息(媒体服务器) | -| `GET` | `/api/v1/mediaserver/play/{itemid}` | mediaserver | `provider-skill` | mediaserver-operation | 在线播放 | -| `GET` | `/api/v1/mediaserver/playing` | mediaserver | `provider-skill` | mediaserver-operation | 正在播放条目 | -| `GET` | `/api/v1/message/` | message | `transport_or_identity` | host-runtime | 回调请求验证 | -| `POST` | `/api/v1/message/` | message | `transport_or_identity` | host-runtime | 接收用户消息 | -| `POST` | `/api/v1/message/agent/callback` | agent | `transport_or_identity` | host-runtime | Web 智能助手按钮回调 | -| `GET` | `/api/v1/message/agent/commands` | agent | `gateway` | slash.list | 获取 Web 智能助手可用命令 | -| `POST` | `/api/v1/message/agent/commands/run` | agent | `gateway` | slash.run | 执行 Agent 斜杠命令 | -| `GET` | `/api/v1/message/agent/file/{file_id}` | agent | `transport_or_identity` | host-runtime | 下载 Web 智能助手附件 | -| `GET` | `/api/v1/message/agent/mcp/servers` | agent | `transport_or_identity` | host-runtime | 查询 Agent MCP 服务器配置 | -| `POST` | `/api/v1/message/agent/mcp/servers` | agent | `transport_or_identity` | host-runtime | 保存 Agent MCP 服务器配置 | -| `POST` | `/api/v1/message/agent/mcp/servers/test` | agent | `transport_or_identity` | host-runtime | 测试 Agent MCP 服务器 | -| `GET` | `/api/v1/message/agent/sessions` | agent | `transport_or_identity` | host-runtime | 获取 Agent 历史会话 | -| `DELETE` | `/api/v1/message/agent/sessions/{session_id}` | agent | `transport_or_identity` | host-runtime | 删除 Agent 历史会话 | -| `GET` | `/api/v1/message/agent/sessions/{session_id}` | agent | `transport_or_identity` | host-runtime | 获取 Agent 历史会话详情 | -| `PUT` | `/api/v1/message/agent/sessions/{session_id}/display` | agent | `transport_or_identity` | host-runtime | 保存 Agent 展示会话 | -| `POST` | `/api/v1/message/agent/sessions/{session_id}/stop` | agent | `transport_or_identity` | host-runtime | 停止 Web 智能助手当前任务 | -| `POST` | `/api/v1/message/agent/stream` | agent | `transport_or_identity` | host-runtime | Web智能助手流式对话 | -| `POST` | `/api/v1/message/agent/upload` | agent | `transport_or_identity` | host-runtime | 上传 Web 智能助手附件 | -| `DELETE` | `/api/v1/message/notification` | message | `transport_or_identity` | host-runtime | 清理通知消息 | -| `GET` | `/api/v1/message/notification` | message | `transport_or_identity` | host-runtime | 获取通知消息 | -| `GET` | `/api/v1/message/web` | message | `transport_or_identity` | host-runtime | 获取WEB消息 | -| `POST` | `/api/v1/message/web` | message | `transport_or_identity` | host-runtime | 接收WEB消息 | -| `POST` | `/api/v1/message/webpush/send` | message | `transport_or_identity` | host-runtime | 发送webpush通知 | -| `POST` | `/api/v1/message/webpush/subscribe` | message | `transport_or_identity` | host-runtime | 客户端webpush通知订阅 | -| `POST` | `/api/v1/mfa/otp/disable` | mfa | `transport_or_identity` | host-runtime | 关闭当前用户的 OTP 验证 | -| `POST` | `/api/v1/mfa/otp/generate` | mfa | `transport_or_identity` | host-runtime | 生成 OTP 验证 URI | -| `POST` | `/api/v1/mfa/otp/verify` | mfa | `transport_or_identity` | host-runtime | 绑定并验证 OTP | -| `POST` | `/api/v1/mfa/passkey/authenticate/finish` | mfa | `transport_or_identity` | host-runtime | 完成 PassKey 认证 | -| `POST` | `/api/v1/mfa/passkey/authenticate/start` | mfa | `transport_or_identity` | host-runtime | 开始 PassKey 认证 | -| `POST` | `/api/v1/mfa/passkey/delete` | mfa | `transport_or_identity` | host-runtime | 删除 PassKey | -| `GET` | `/api/v1/mfa/passkey/list` | mfa | `transport_or_identity` | host-runtime | 获取当前用户的 PassKey 列表 | -| `POST` | `/api/v1/mfa/passkey/register/finish` | mfa | `transport_or_identity` | host-runtime | 完成注册 PassKey | -| `POST` | `/api/v1/mfa/passkey/register/start` | mfa | `transport_or_identity` | host-runtime | 开始注册 PassKey | -| `GET` | `/api/v1/music/album/{album_id}` | music | `gateway` | music.album.get | 查询音乐专辑详情 | -| `GET` | `/api/v1/music/album/{album_id}/related` | music | `gateway` | music.album.related | 查询关联音乐专辑 | -| `GET` | `/api/v1/music/artist/{artist_id}` | music | `gateway` | music.artist.get | 查询音乐艺术家详情 | -| `GET` | `/api/v1/music/artist/{artist_id}/albums` | music | `gateway` | music.artist.albums | 查询艺术家的专辑列表 | -| `GET` | `/api/v1/music/artist/{artist_id}/related` | music | `gateway` | music.artist.related | 查询关联艺术家 | -| `DELETE` | `/api/v1/music/cache` | music | `gateway` | music.cache.clear | 清空音乐识别缓存 | -| `GET` | `/api/v1/music/cache` | music | `gateway` | music.cache.get | 查询音乐识别缓存 | -| `DELETE` | `/api/v1/music/cache/{cache_key}` | music | `gateway` | music.cache.delete | 删除指定音乐识别缓存 | -| `GET` | `/api/v1/music/explore` | music | `gateway` | music.explore | 探索音乐 | -| `POST` | `/api/v1/music/library/status` | music | `ui_presentation` | host-ui | 批量查询音乐专辑入库状态 | -| `POST` | `/api/v1/music/recognize` | music | `gateway` | music.recognize | 识别音乐元数据详情 | -| `POST` | `/api/v1/notification/config` | notification | `transport_or_identity` | host-runtime | 保存通知渠道并同步登录缓存 | -| `POST` | `/api/v1/notification/manage` | notification | `transport_or_identity` | host-runtime | 通知渠道统一管理 | -| `POST` | `/api/v1/openai/v1/chat/completions` | openai | `transport_or_identity` | host-runtime | OpenAI 兼容聊天补全 | -| `GET` | `/api/v1/openai/v1/models` | openai | `transport_or_identity` | host-runtime | OpenAI 兼容模型列表 | -| `POST` | `/api/v1/openai/v1/responses` | openai | `transport_or_identity` | host-runtime | OpenAI 兼容响应生成 | -| `GET` | `/api/v1/plugin/` | plugin | `gateway` | plugin.installed, plugin.market | 所有插件 | -| `GET` | `/api/v1/plugin/agent_pets` | plugin | `ui_presentation` | host-ui | 获取插件 Agent 助手形象 | -| `POST` | `/api/v1/plugin/clone/{plugin_id}` | plugin | `gateway` | plugin.clone | 创建插件分身 | -| `GET` | `/api/v1/plugin/clone/{plugin_id}/restorable` | plugin | `gateway` | plugin.clone.restorable | 列出可恢复的已停用分身 | -| `GET` | `/api/v1/plugin/dashboard/meta` | plugin | `ui_presentation` | host-ui | 获取所有插件仪表板元信息 | -| `GET` | `/api/v1/plugin/dashboard/{plugin_id}` | plugin | `ui_presentation` | host-ui | 获取插件仪表板配置 | -| `GET` | `/api/v1/plugin/dashboard/{plugin_id}/{key}` | plugin | `ui_presentation` | host-ui | 获取插件仪表板配置 | -| `GET` | `/api/v1/plugin/file/{plugin_id}/{filepath}` | plugin | `stream_or_binary` | host-transport | 获取插件静态文件 | -| `GET` | `/api/v1/plugin/folders` | plugin | `gateway` | plugin.folders.get | 获取插件文件夹配置 | -| `POST` | `/api/v1/plugin/folders` | plugin | `gateway` | plugin.folders.update | 保存插件文件夹配置 | -| `DELETE` | `/api/v1/plugin/folders/{folder_name}` | plugin | `gateway` | plugin.folder.delete | 删除插件文件夹 | -| `PATCH` | `/api/v1/plugin/folders/{folder_name}` | plugin | `gateway` | plugin.folder.update | 更新插件文件夹 | -| `POST` | `/api/v1/plugin/folders/{folder_name}` | plugin | `gateway` | plugin.folder.create | 创建插件文件夹 | -| `PUT` | `/api/v1/plugin/folders/{folder_name}/plugins` | plugin | `gateway` | plugin.folder.plugins.update | 更新文件夹中的插件 | -| `DELETE` | `/api/v1/plugin/folders/{folder_name}/plugins/{plugin_id}` | plugin | `gateway` | plugin.folder.plugin.remove | 从文件夹移除插件 | -| `PUT` | `/api/v1/plugin/folders/{folder_name}/plugins/{plugin_id}` | plugin | `gateway` | plugin.folder.plugin.assign | 移动插件到文件夹 | -| `GET` | `/api/v1/plugin/form/{plugin_id}` | plugin | `gateway` | plugin.config.get | 获取插件表单页面 | -| `GET` | `/api/v1/plugin/history/{plugin_id}` | plugin | `gateway` | plugin.history | 获取插件更新说明 | -| `GET` | `/api/v1/plugin/install/{plugin_id}` | plugin | `gateway` | plugin.install | 安装插件 | -| `GET` | `/api/v1/plugin/installed` | plugin | `consolidated` | plugin.installed | 已安装插件 | -| `POST` | `/api/v1/plugin/instance/{instance_id}/enabled` | plugin | `gateway` | plugin.instance.set_enabled | 启用或停用插件实例 | -| `POST` | `/api/v1/plugin/instance/{instance_id}/purge` | plugin | `gateway` | plugin.instance.purge | 按选定范围彻底清理插件实例 | -| `DELETE` | `/api/v1/plugin/instances/{plugin_id}/{instance_id}/default_target` | plugin | `gateway` | plugin.default_target.clear | 清除插件实例的默认调用目标 | -| `PUT` | `/api/v1/plugin/instances/{plugin_id}/{instance_id}/default_target` | plugin | `gateway` | plugin.default_target.set | 设置插件实例的默认调用目标 | -| `GET` | `/api/v1/plugin/loglevel/{plugin_id}` | plugin | `gateway` | plugin.loglevel.get | 查询插件全部实例的日志等级设置 | -| `DELETE` | `/api/v1/plugin/loglevel/{plugin_id}/{instance_id}` | plugin | `gateway` | plugin.loglevel.clear | 清除插件实例的日志等级覆盖 | -| `PUT` | `/api/v1/plugin/loglevel/{plugin_id}/{instance_id}` | plugin | `gateway` | plugin.loglevel.set | 设置插件实例的日志等级覆盖 | -| `GET` | `/api/v1/plugin/page/{plugin_id}` | plugin | `ui_presentation` | host-ui | 获取插件数据页面 | -| `GET` | `/api/v1/plugin/rating` | plugin | `gateway` | plugin.ratings | 批量查询插件评分 | -| `GET` | `/api/v1/plugin/rating/{plugin_id}` | plugin | `gateway` | plugin.rating | 查询插件评分 | -| `POST` | `/api/v1/plugin/rating/{plugin_id}` | plugin | `gateway` | plugin.rating.submit | 提交插件评分 | -| `GET` | `/api/v1/plugin/releases/{plugin_id}` | plugin | `gateway` | plugin.releases | 获取插件Release版本 | -| `POST` | `/api/v1/plugin/reload/{plugin_id}` | plugin | `gateway` | plugin.reload | 重新加载插件 | -| `GET` | `/api/v1/plugin/remotes` | plugin | `transport_or_identity` | host-runtime | 获取插件联邦组件列表 | -| `GET` | `/api/v1/plugin/reset/{plugin_id}` | plugin | `gateway` | plugin.reset | 重置插件配置及数据 | -| `GET` | `/api/v1/plugin/runtime` | plugin | `gateway` | plugin.runtime.status | 插件运行时收敛状态 | -| `GET` | `/api/v1/plugin/runtime/capabilities` | plugin | `gateway` | plugin.capabilities | 查询插件运行能力 | -| `GET` | `/api/v1/plugin/runtime/{plugin_id}/data` | plugin | `gateway` | plugin.data | 查询插件持久化数据 | -| `GET` | `/api/v1/plugin/runtime/{plugin_id}/data/summary` | plugin | `ui_presentation` | host-ui | 查询插件持久化数据摘要 | -| `GET` | `/api/v1/plugin/sidebar_nav` | plugin | `ui_presentation` | host-ui | 获取插件侧栏导航项 | -| `GET` | `/api/v1/plugin/source/{plugin_id}` | plugin | `gateway` | plugin.source.options | 获取插件来源身份 | -| `POST` | `/api/v1/plugin/source/{plugin_id}` | plugin | `gateway` | plugin.source.change | 切换插件来源 | -| `POST` | `/api/v1/plugin/source/{plugin_id}/install` | plugin | `gateway` | plugin.source.install | 按明确来源安装插件 | -| `GET` | `/api/v1/plugin/source/{plugin_id}/options` | plugin | `consolidated` | plugin.source.options | 获取插件来源候选 | -| `GET` | `/api/v1/plugin/statistic` | plugin | `gateway` | plugin.statistics | 插件安装统计 | -| `DELETE` | `/api/v1/plugin/{plugin_id}` | plugin | `gateway` | plugin.uninstall | 卸载插件 | -| `GET` | `/api/v1/plugin/{plugin_id}` | plugin | `consolidated` | plugin.config.get | 获取插件配置 | -| `PUT` | `/api/v1/plugin/{plugin_id}` | plugin | `gateway` | plugin.config.update | 更新插件配置 | -| `GET` | `/api/v1/recommend/agent` | recommend | `gateway` | recommendation.list | 统一获取 Agent 推荐结果 | -| `GET` | `/api/v1/recommend/bangumi_calendar` | recommend | `consolidated` | moviepilot-api | Bangumi每日放送 | -| `GET` | `/api/v1/recommend/douban_movie_hot` | recommend | `consolidated` | moviepilot-api | 豆瓣热门电影 | -| `GET` | `/api/v1/recommend/douban_movie_top250` | recommend | `consolidated` | moviepilot-api | 豆瓣电影TOP250 | -| `GET` | `/api/v1/recommend/douban_movies` | recommend | `consolidated` | moviepilot-api | 豆瓣电影 | -| `GET` | `/api/v1/recommend/douban_showing` | recommend | `consolidated` | moviepilot-api | 豆瓣正在热映 | -| `GET` | `/api/v1/recommend/douban_tv_animation` | recommend | `consolidated` | moviepilot-api | 豆瓣动画剧集 | -| `GET` | `/api/v1/recommend/douban_tv_hot` | recommend | `consolidated` | moviepilot-api | 豆瓣热门电视剧 | -| `GET` | `/api/v1/recommend/douban_tv_weekly_chinese` | recommend | `consolidated` | moviepilot-api | 豆瓣国产剧集周榜 | -| `GET` | `/api/v1/recommend/douban_tv_weekly_global` | recommend | `consolidated` | moviepilot-api | 豆瓣全球剧集周榜 | -| `GET` | `/api/v1/recommend/douban_tvs` | recommend | `consolidated` | moviepilot-api | 豆瓣剧集 | -| `GET` | `/api/v1/recommend/music_douban` | recommend | `consolidated` | moviepilot-api | 豆瓣音乐推荐 | -| `GET` | `/api/v1/recommend/music_weekly` | recommend | `consolidated` | moviepilot-api | ListenBrainz 本周热门音乐 | -| `GET` | `/api/v1/recommend/source` | recommend | `consolidated` | moviepilot-api | 获取推荐数据源 | -| `GET` | `/api/v1/recommend/tmdb_movies` | recommend | `consolidated` | moviepilot-api | TMDB电影 | -| `GET` | `/api/v1/recommend/tmdb_trending` | recommend | `consolidated` | moviepilot-api | TMDB流行趋势 | -| `GET` | `/api/v1/recommend/tmdb_tvs` | recommend | `consolidated` | moviepilot-api | TMDB剧集 | -| `GET` | `/api/v1/rule/builtin` | rule | `gateway` | filter.builtin | 查询内置过滤规则 | -| `GET` | `/api/v1/rule/custom` | rule | `gateway` | filter.custom | 查询自定义过滤规则 | -| `POST` | `/api/v1/rule/custom` | rule | `gateway` | filter.custom.add | 新增自定义过滤规则 | -| `PUT` | `/api/v1/rule/custom/reorder` | rule | `ui_presentation` | host-ui | 调整自定义过滤规则顺序 | -| `DELETE` | `/api/v1/rule/custom/{rule_id}` | rule | `gateway` | filter.custom.delete | 删除自定义过滤规则 | -| `PUT` | `/api/v1/rule/custom/{rule_id}` | rule | `gateway` | filter.custom.update | 更新自定义过滤规则 | -| `GET` | `/api/v1/rule/groups` | rule | `gateway` | filter.groups | 查询过滤规则组 | -| `POST` | `/api/v1/rule/groups` | rule | `gateway` | filter.group.add | 新增过滤规则组 | -| `PUT` | `/api/v1/rule/groups/reorder` | rule | `ui_presentation` | host-ui | 调整过滤规则组顺序 | -| `DELETE` | `/api/v1/rule/groups/{name}` | rule | `gateway` | filter.group.delete | 删除过滤规则组 | -| `PUT` | `/api/v1/rule/groups/{name}` | rule | `gateway` | filter.group.update | 更新过滤规则组 | -| `GET` | `/api/v1/search/last` | search | `consolidated` | search.results | 查询搜索结果 | -| `GET` | `/api/v1/search/last/context` | search | `gateway` | search.results | 查询上次搜索上下文 | -| `GET` | `/api/v1/search/media/{media_id}` | search | `gateway` | search.torrents | 精确搜索资源 | -| `GET` | `/api/v1/search/media/{media_id}/stream` | search | `consolidated` | search.torrents | 渐进式精确搜索资源 | -| `POST` | `/api/v1/search/recommend` | search | `gateway` | search.recommend | AI推荐资源 | -| `GET` | `/api/v1/search/subtitle/media/{media_id}` | search | `gateway` | subtitle.search.media | 精确搜索字幕 | -| `GET` | `/api/v1/search/subtitle/media/{media_id}/stream` | search | `consolidated` | subtitle.search.media | 渐进式精确搜索字幕 | -| `GET` | `/api/v1/search/subtitle/title` | search | `gateway` | subtitle.search.title | 模糊搜索字幕 | -| `GET` | `/api/v1/search/subtitle/title/stream` | search | `consolidated` | subtitle.search.title | 渐进式模糊搜索字幕 | -| `GET` | `/api/v1/search/title` | search | `gateway` | search.title | 模糊搜索资源 | -| `GET` | `/api/v1/search/title/stream` | search | `consolidated` | search.title | 渐进式模糊搜索资源 | -| `GET` | `/api/v1/site/` | site | `consolidated` | site.list | 所有站点 | -| `POST` | `/api/v1/site/` | site | `gateway` | site.add | 新增站点 | -| `PUT` | `/api/v1/site/` | site | `gateway` | site.update | 更新站点 | -| `GET` | `/api/v1/site/agent` | site | `gateway` | site.list | 查询 Agent 可用站点 | -| `GET` | `/api/v1/site/auth` | site | `gateway` | site.auth.options | 查询认证站点 | -| `POST` | `/api/v1/site/auth` | site | `gateway` | site.authenticate | 用户站点认证 | -| `GET` | `/api/v1/site/category/{site_id}` | site | `gateway` | site.category | 站点分类 | -| `GET` | `/api/v1/site/cookie/{site_id}` | site | `consolidated` | site.cookie.update | 更新站点Cookie&UA | -| `POST` | `/api/v1/site/cookie/{site_id}` | site | `gateway` | site.cookie.update | 更新站点Cookie&UA | -| `POST` | `/api/v1/site/cookie/{site_id}/set` | site | `gateway` | site.cookie.set | 直接保存站点Cookie&UA | -| `POST` | `/api/v1/site/cookiecloud` | site | `gateway` | site.cookiecloud.sync | CookieCloud同步 | -| `GET` | `/api/v1/site/domain/{site_url}` | site | `consolidated` | site.list | 站点详情 | -| `GET` | `/api/v1/site/icon/{site_id}` | site | `stream_or_binary` | host-transport | 站点图标 | -| `GET` | `/api/v1/site/mapping` | site | `gateway` | site.mapping | 获取站点域名到名称的映射 | -| `GET` | `/api/v1/site/media/{media_type}` | site | `gateway` | site.searchable | 按媒体类型获取可搜索站点 | -| `POST` | `/api/v1/site/priorities` | site | `gateway` | site.priorities.update | 批量更新站点优先级 | -| `POST` | `/api/v1/site/reset` | site | `gateway` | site.reset | 重置站点 | -| `GET` | `/api/v1/site/resource/{site_id}` | site | `gateway` | site.resource | 站点资源 | -| `GET` | `/api/v1/site/rss` | site | `gateway` | site.rss | 所有订阅站点 | -| `GET` | `/api/v1/site/statistic` | site | `gateway` | site.statistics | 所有站点统计信息 | -| `GET` | `/api/v1/site/statistic/{site_url}` | site | `gateway` | site.statistic | 特定站点统计信息 | -| `GET` | `/api/v1/site/supporting` | site | `gateway` | site.supporting | 获取支持的站点列表 | -| `GET` | `/api/v1/site/test/{site_id}` | site | `gateway` | site.test | 连接测试 | -| `GET` | `/api/v1/site/userdata/latest` | site | `gateway` | site.userdata.latest | 查询所有站点最新用户数据 | -| `GET` | `/api/v1/site/userdata/{site_id}` | site | `gateway` | site.userdata | 查询某站点用户数据 | -| `POST` | `/api/v1/site/userdata/{site_id}` | site | `gateway` | site.userdata.refresh | 更新站点用户数据 | -| `DELETE` | `/api/v1/site/{site_id}` | site | `gateway` | site.delete | 删除站点 | -| `GET` | `/api/v1/site/{site_id}` | site | `consolidated` | site.list | 站点详情 | -| `POST` | `/api/v1/storage/agent/list` | storage | `gateway` | storage.list | 查询 Agent 可用目录和文件 | -| `GET` | `/api/v1/storage/catalog` | storage | `ui_presentation` | host-ui | 查询存储类型目录 | -| `POST` | `/api/v1/storage/delete` | storage | `gateway` | storage.delete | 删除文件或目录 | -| `GET` | `/api/v1/storage/directories` | storage | `gateway` | storage.settings | 查询目录配置 | -| `POST` | `/api/v1/storage/download` | storage | `stream_or_binary` | host-transport | 下载文件 | -| `POST` | `/api/v1/storage/image` | storage | `stream_or_binary` | host-transport | 预览图片 | -| `POST` | `/api/v1/storage/list` | storage | `consolidated` | storage.list | 所有目录和文件 | -| `POST` | `/api/v1/storage/manage` | storage | `gateway` | storage.manage | 网盘存储统一管理 | -| `POST` | `/api/v1/storage/mkdir` | storage | `gateway` | storage.mkdir | 创建目录 | -| `GET` | `/api/v1/storage/options` | storage | `ui_presentation` | host-ui | 查询可用存储选项 | -| `POST` | `/api/v1/storage/rename` | storage | `gateway` | storage.rename | 重命名文件或目录 | -| `GET` | `/api/v1/subscribe/` | subscribe | `gateway` | subscription.list | 查询所有订阅 | -| `POST` | `/api/v1/subscribe/` | subscribe | `gateway` | subscription.add | 新增订阅 | -| `PUT` | `/api/v1/subscribe/` | subscribe | `gateway` | subscription.update | 更新订阅 | -| `POST` | `/api/v1/subscribe/check` | subscribe | `gateway` | subscription.metadata.refresh | 刷新订阅 TMDB 信息 | -| `GET` | `/api/v1/subscribe/execution/batches` | subscribe | `gateway` | subscription.execution.list | 查看订阅搜索进度 | -| `GET` | `/api/v1/subscribe/execution/batches/{batch_id}` | subscribe | `gateway` | subscription.execution.get | 查看一次订阅搜索 | -| `PUT` | `/api/v1/subscribe/execution/batches/{batch_id}/cancel` | subscribe | `gateway` | subscription.execution.cancel | 停止一次订阅搜索 | -| `GET` | `/api/v1/subscribe/files/{subscribe_id}` | subscribe | `gateway` | subscription.files | 订阅相关文件信息 | -| `DELETE` | `/api/v1/subscribe/follow` | subscribe | `gateway` | subscription.follow.delete | 取消Follow订阅分享人 | -| `GET` | `/api/v1/subscribe/follow` | subscribe | `gateway` | subscription.follow.list | 查询已Follow的订阅分享人 | -| `POST` | `/api/v1/subscribe/follow` | subscribe | `gateway` | subscription.follow.add | Follow订阅分享人 | -| `POST` | `/api/v1/subscribe/fork` | subscribe | `gateway` | subscription.fork | 复用订阅 | -| `DELETE` | `/api/v1/subscribe/history/{history_id}` | subscribe | `gateway` | subscription.history.delete | 删除订阅历史 | -| `GET` | `/api/v1/subscribe/history/{mtype}` | subscribe | `gateway` | subscription.history | 查询订阅历史 | -| `GET` | `/api/v1/subscribe/list` | subscribe | `consolidated` | subscription.list | 查询所有订阅(API_TOKEN) | -| `DELETE` | `/api/v1/subscribe/media/{media_id}` | subscribe | `gateway` | subscription.delete_by_media | 删除订阅 | -| `GET` | `/api/v1/subscribe/media/{media_id}` | subscribe | `gateway` | subscription.find | 查询订阅 | -| `GET` | `/api/v1/subscribe/popular` | subscribe | `gateway` | subscription.popular | 热门订阅(基于用户共享数据) | -| `POST` | `/api/v1/subscribe/refresh` | subscribe | `gateway` | subscription.refresh | 刷新订阅 | -| `POST` | `/api/v1/subscribe/reset/{subid}` | subscribe | `gateway` | subscription.reset | 重置订阅 | -| `POST` | `/api/v1/subscribe/search` | subscribe | `gateway` | subscription.search_all | 搜索所有订阅 | -| `POST` | `/api/v1/subscribe/search/{subscribe_id}` | subscribe | `gateway` | subscription.search | 搜索订阅 | -| `POST` | `/api/v1/subscribe/seerr` | subscribe | `transport_or_identity` | host-runtime | OverSeerr/JellySeerr通知订阅 | -| `POST` | `/api/v1/subscribe/share` | subscribe | `gateway` | subscription.share | 分享订阅 | -| `GET` | `/api/v1/subscribe/share/statistics` | subscribe | `gateway` | subscription.share.statistics | 查询订阅分享统计 | -| `DELETE` | `/api/v1/subscribe/share/{share_id}` | subscribe | `gateway` | subscription.share.delete | 删除分享 | -| `GET` | `/api/v1/subscribe/shares` | subscribe | `gateway` | subscription.shares | 查询分享的订阅 | -| `PUT` | `/api/v1/subscribe/status/{subid}` | subscribe | `gateway` | subscription.status.update | 更新订阅状态 | -| `GET` | `/api/v1/subscribe/user/{username}` | subscribe | `gateway` | subscription.user.list | 用户订阅 | -| `DELETE` | `/api/v1/subscribe/{subscribe_id}` | subscribe | `gateway` | subscription.delete | 删除订阅 | -| `GET` | `/api/v1/subscribe/{subscribe_id}` | subscribe | `gateway` | subscription.get | 订阅详情 | -| `GET` | `/api/v1/subscribe/{subscription_id}/activity` | subscribe | `ui_presentation` | host-ui | 查看订阅业务活动 | -| `GET` | `/api/v1/subscribe/{subscription_id}/activity/{event_id}` | subscribe | `ui_presentation` | host-ui | 查看订阅活动详情 | -| `GET` | `/api/v1/subscribe/{subscription_id}/logs` | subscribe | `stream_or_binary` | host-transport | 查看单个订阅的执行日志 | -| `GET` | `/api/v1/subscribe/{subscription_id}/logs/download` | subscribe | `stream_or_binary` | host-transport | 下载单个订阅的执行日志 | -| `GET` | `/api/v1/system/cache/image` | system | `stream_or_binary` | host-transport | 图片缓存 | -| `GET` | `/api/v1/system/database/backups` | system | `gateway` | database.backups.list | 查询受管数据库备份 | -| `POST` | `/api/v1/system/database/backups` | system | `gateway` | database.backups.create | 立即创建数据库备份 | -| `DELETE` | `/api/v1/system/database/backups/{name}` | system | `gateway` | database.backups.delete | 删除受管数据库备份 | -| `POST` | `/api/v1/system/database/backups/{name}/verify` | system | `gateway` | database.backups.verify | 校验受管数据库备份 | -| `GET` | `/api/v1/system/env` | system | `consolidated` | config.system.get | 查询系统配置 | -| `POST` | `/api/v1/system/env` | system | `consolidated` | config.system.update | 更新系统配置 | -| `GET` | `/api/v1/system/global` | system | `consolidated` | config.system.get | 查询非敏感系统设置 | -| `GET` | `/api/v1/system/global/user` | system | `gateway` | config.user.get | 查询用户相关系统设置 | -| `GET` | `/api/v1/system/identifiers` | system | `gateway` | config.identifiers.get | 查询自定义识别词 | -| `POST` | `/api/v1/system/identifiers` | system | `gateway` | config.identifiers.update | 更新自定义识别词 | -| `GET` | `/api/v1/system/img/{proxy}` | system | `stream_or_binary` | host-transport | 图片代理 | -| `GET` | `/api/v1/system/logging` | system | `stream_or_binary` | host-transport | 实时日志 | -| `GET` | `/api/v1/system/logging/download/{name}` | system | `stream_or_binary` | host-transport | 下载日志 | -| `GET` | `/api/v1/system/message` | system | `stream_or_binary` | host-transport | 实时消息 | -| `GET` | `/api/v1/system/module-catalog` | system | `gateway` | system.module.catalog | 查询宿主模块目录 | -| `GET` | `/api/v1/system/module-settings` | system | `gateway` | system.module.settings | 查询可手动开关的内置模块 | -| `GET` | `/api/v1/system/modulelist` | system | `gateway` | system.module.list | 查询已启用的模块ID列表 | -| `GET` | `/api/v1/system/moduletest/{moduleid}` | system | `gateway` | system.module.test | 模块可用性测试 | -| `GET` | `/api/v1/system/nettest` | system | `gateway` | system.network.test | 测试网络连通性 | -| `GET` | `/api/v1/system/nettest/targets` | system | `gateway` | system.network.targets | 获取网络测试目标 | -| `GET` | `/api/v1/system/notification-image` | system | `stream_or_binary` | host-transport | 通知图片代理 | -| `GET` | `/api/v1/system/ping` | system | `transport_or_identity` | host-runtime | 服务存活检测 | -| `GET` | `/api/v1/system/progress/{process_type}` | system | `stream_or_binary` | host-transport | 实时进度 | -| `GET` | `/api/v1/system/restart` | system | `gateway` | system.restart | 重启系统 | -| `GET` | `/api/v1/system/ruletest` | system | `gateway` | filter.test | 过滤规则测试 | -| `GET` | `/api/v1/system/runscheduler` | system | `gateway` | scheduler.run | 运行服务 | -| `GET` | `/api/v1/system/runscheduler2` | system | `alternate-auth-duplicate` | moviepilot-api | 运行服务(API_TOKEN) | -| `POST` | `/api/v1/system/setting/PLUGIN_MARKET/sync-wiki` | system | `gateway` | plugin.market.sync_wiki | 从Wiki同步插件市场仓库 | -| `GET` | `/api/v1/system/setting/public/{key}` | system | `gateway` | config.public.get | 查询公开系统设置 | -| `GET` | `/api/v1/system/setting/{key}` | system | `consolidated` | config.system.get | 查询系统设置 | -| `POST` | `/api/v1/system/setting/{key}` | system | `consolidated` | config.system.update | 更新系统设置 | -| `GET` | `/api/v1/system/settings` | system | `gateway` | config.system.get | 发现或读取已登记的系统设置 | -| `POST` | `/api/v1/system/settings` | system | `gateway` | config.system.update | 更新单项已登记系统设置 | -| `GET` | `/api/v1/system/settings/catalog` | system | `gateway` | config.system.list | 列出系统设置契约 | -| `GET` | `/api/v1/system/settings/describe/{setting_key}` | system | `gateway` | config.system.describe | 查询单项系统设置契约 | -| `POST` | `/api/v1/system/update/check` | system | `gateway` | system.update.check | 立即检查系统更新 | -| `POST` | `/api/v1/system/update/download` | system | `gateway` | system.update.download | 后台下载系统更新 | -| `POST` | `/api/v1/system/update/install` | system | `gateway` | system.update.install | 确认重启安装系统更新 | -| `GET` | `/api/v1/system/update/status` | system | `gateway` | system.update.status | 查询系统更新状态 | -| `POST` | `/api/v1/system/upgrade` | system | `gateway` | system.upgrade.dev | Dev 更新并重启系统 | -| `GET` | `/api/v1/system/usage/statistic` | system | `gateway` | system.usage.statistics | 查询安装版本统计报表 | -| `GET` | `/api/v1/system/versions` | system | `gateway` | system.versions | 查询Github所有Release版本 | -| `DELETE` | `/api/v1/tmdb/cache` | tmdb | `gateway` | media.cache.clear | 清空 TheMovieDb 识别缓存 | -| `GET` | `/api/v1/tmdb/cache` | tmdb | `gateway` | media.cache.get | 查询 TheMovieDb 识别缓存 | -| `DELETE` | `/api/v1/tmdb/cache/{cache_key}` | tmdb | `gateway` | media.cache.delete | 删除指定 TheMovieDb 识别缓存 | -| `GET` | `/api/v1/tmdb/collection/{collection_id}` | tmdb | `consolidated` | moviepilot-api | 系列合集详情 | -| `GET` | `/api/v1/tmdb/credits/{tmdbid}/{type_name}` | tmdb | `consolidated` | moviepilot-api | 演员阵容 | -| `GET` | `/api/v1/tmdb/person/credits/{person_id}` | tmdb | `consolidated` | moviepilot-api | 人物参演作品 | -| `GET` | `/api/v1/tmdb/person/{person_id}` | tmdb | `consolidated` | moviepilot-api | 人物详情 | -| `GET` | `/api/v1/tmdb/recommend/{tmdbid}/{type_name}` | tmdb | `consolidated` | moviepilot-api | 推荐电影/电视剧 | -| `GET` | `/api/v1/tmdb/seasons/{tmdbid}` | tmdb | `consolidated` | moviepilot-api | TMDB所有季 | -| `GET` | `/api/v1/tmdb/similar/{tmdbid}/{type_name}` | tmdb | `consolidated` | moviepilot-api | 类似电影/电视剧 | -| `GET` | `/api/v1/tmdb/{tmdbid}/{season}` | tmdb | `gateway` | media.episode_schedule | TMDB季所有集 | -| `DELETE` | `/api/v1/torrent/cache` | torrent | `gateway` | torrent.cache.clear | 清理种子缓存 | -| `GET` | `/api/v1/torrent/cache` | torrent | `gateway` | torrent.cache.get | 获取种子缓存 | -| `POST` | `/api/v1/torrent/cache/refresh` | torrent | `gateway` | torrent.cache.refresh | 刷新种子缓存 | -| `POST` | `/api/v1/torrent/cache/reidentify/{domain}/{torrent_hash}` | torrent | `gateway` | torrent.cache.reidentify | 重新识别种子 | -| `DELETE` | `/api/v1/torrent/cache/{domain}/{torrent_hash}` | torrent | `gateway` | torrent.cache.delete | 删除指定种子缓存 | -| `POST` | `/api/v1/transfer/episode-format/recommend` | transfer | `gateway` | transfer.episode_format.recommend | 推荐集数定位模板 | -| `POST` | `/api/v1/transfer/manual` | transfer | `gateway` | transfer.file | 手动转移 | -| `POST` | `/api/v1/transfer/manual/history` | transfer | `gateway` | transfer.manual_history | 查询手动转移成功历史 | -| `POST` | `/api/v1/transfer/manual/target-path` | transfer | `gateway` | transfer.target_path | 匹配手动转移目的路径 | -| `GET` | `/api/v1/transfer/name` | transfer | `gateway` | transfer.name | 查询整理后的名称 | -| `GET` | `/api/v1/transfer/now` | transfer | `consolidated` | scheduler.run | 立即执行下载器文件整理 | -| `DELETE` | `/api/v1/transfer/queue` | transfer | `gateway` | transfer.queue.delete | 从整理队列中删除任务 | -| `GET` | `/api/v1/transfer/queue` | transfer | `gateway` | transfer.queue | 查询整理队列 | -| `GET` | `/api/v1/transfer/queue/page` | transfer | `ui_presentation` | host-ui | 查询受限整理队列快照 | -| `GET` | `/api/v1/transfer/tasks/manual-reviews` | transfer | `gateway` | transfer.manual_reviews | 分页查询 durable 整理人工复核任务 | -| `GET` | `/api/v1/transfer/tasks/{task_id}/manual-review` | transfer | `gateway` | transfer.manual_review | 查询 durable 整理人工复核详情 | -| `POST` | `/api/v1/transfer/tasks/{task_id}/manual-review` | transfer | `gateway` | transfer.manual_review.resolve | 人工判定整理步骤的外部执行结果 | -| `GET` | `/api/v1/user/` | user | `transport_or_identity` | host-runtime | 所有用户 | -| `POST` | `/api/v1/user/` | user | `transport_or_identity` | host-runtime | 新增用户 | -| `PUT` | `/api/v1/user/` | user | `transport_or_identity` | host-runtime | 更新用户 | -| `POST` | `/api/v1/user/avatar/{user_id}` | user | `transport_or_identity` | host-runtime | 上传用户头像 | -| `GET` | `/api/v1/user/config/{key}` | user | `transport_or_identity` | host-runtime | 查询用户配置 | -| `POST` | `/api/v1/user/config/{key}` | user | `transport_or_identity` | host-runtime | 更新用户配置 | -| `GET` | `/api/v1/user/current` | user | `transport_or_identity` | host-runtime | 当前登录用户信息 | -| `PUT` | `/api/v1/user/current` | user | `transport_or_identity` | host-runtime | 更新当前用户资料 | -| `DELETE` | `/api/v1/user/id/{user_id}` | user | `transport_or_identity` | host-runtime | 删除用户 | -| `DELETE` | `/api/v1/user/name/{user_name}` | user | `transport_or_identity` | host-runtime | 删除用户 | -| `GET` | `/api/v1/user/{username}` | user | `transport_or_identity` | host-runtime | 用户详情 | -| `GET` | `/api/v1/webhook/` | webhook | `transport_or_identity` | host-runtime | Webhook消息响应 | -| `POST` | `/api/v1/webhook/` | webhook | `transport_or_identity` | host-runtime | Webhook消息响应 | -| `GET` | `/api/v1/workflow/` | workflow | `consolidated` | workflow.list | 所有工作流 | -| `POST` | `/api/v1/workflow/` | workflow | `gateway` | workflow.create | 创建工作流 | -| `GET` | `/api/v1/workflow/actions` | workflow | `gateway` | workflow.actions | 所有动作 | -| `GET` | `/api/v1/workflow/agent` | workflow | `gateway` | workflow.list | 查询 Agent 可用工作流 | -| `GET` | `/api/v1/workflow/event_types` | workflow | `gateway` | workflow.event_types | 获取所有事件类型 | -| `POST` | `/api/v1/workflow/fork` | workflow | `gateway` | workflow.fork | 复用工作流 | -| `GET` | `/api/v1/workflow/plugin/actions` | workflow | `gateway` | workflow.plugin.actions | 查询插件动作 | -| `POST` | `/api/v1/workflow/share` | workflow | `gateway` | workflow.share | 分享工作流 | -| `DELETE` | `/api/v1/workflow/share/{share_id}` | workflow | `gateway` | workflow.share.delete | 删除分享 | -| `GET` | `/api/v1/workflow/shares` | workflow | `gateway` | workflow.shares | 查询分享的工作流 | -| `DELETE` | `/api/v1/workflow/{workflow_id}` | workflow | `gateway` | workflow.delete | 删除工作流 | -| `GET` | `/api/v1/workflow/{workflow_id}` | workflow | `gateway` | workflow.get | 工作流详情 | -| `PUT` | `/api/v1/workflow/{workflow_id}` | workflow | `gateway` | workflow.update | 更新工作流 | -| `POST` | `/api/v1/workflow/{workflow_id}/pause` | workflow | `gateway` | workflow.pause | 停用工作流 | -| `POST` | `/api/v1/workflow/{workflow_id}/reset` | workflow | `gateway` | workflow.reset | 重置工作流 | -| `POST` | `/api/v1/workflow/{workflow_id}/run` | workflow | `gateway` | workflow.run | 执行工作流 | -| `POST` | `/api/v1/workflow/{workflow_id}/start` | workflow | `gateway` | workflow.start | 启用工作流 | - -## Exposure Rule - -Every structured JSON business endpoint is either a stable gateway operation, a provider Skill capability, or an explicitly consolidated compatibility route. Authentication, webhook, stream, binary, and UI-presentation endpoints remain owned by their direct transport or frontend consumer and must not be made recursively callable by the Agent. diff --git a/docs/refactor/agent-codex-parity-plan.md b/docs/refactor/agent-codex-parity-plan.md deleted file mode 100644 index 4b061f89f..000000000 --- a/docs/refactor/agent-codex-parity-plan.md +++ /dev/null @@ -1,123 +0,0 @@ -# MoviePilot Agent 与 Codex Harness 对齐路线 - -> 归档位置:`docs/refactor/`。 - -更新时间:2026-09-12 - -这份文档是 Agent 能力对齐的交付路线和每轮验收合同。它记录当前证据与未完成目标,不把一次成功的模型调用当成“已经和 Codex 一样聪明”。最终判断必须同时看模型行为、工具执行、任务生命周期、业务终态和失败后的收敛结果。 - -## 当前目标 - -让 MoviePilot Agent 在 MoviePilot 的真实业务边界内具备可复核的 Codex 级 Harness 能力:模型能看到准确的工具合同,工具能完成完整的命令行、浏览器、终端输入输出、子代理和长任务协作闭环,宿主能隔离任务身份、收口进程并在新消息到达时继续推进。高影响业务动作仍由宿主的授权和确认策略控制。 - -真实模型评测必须使用显式配置的供应商模型和推理档位。历史 Google Gemini 基线为 `gemini-2.5-pro + high`;后续测评切换为更强的 `gemini-3.1-pro-preview + high`。官方 Google 主机由评测 worker 使用生产同源的 `langchain-google-genai` 原生工具通道,以保留 Gemini 3 的 `thought_signature`;报告会把实际 `runtime_transport` 记为 `google_generative_language`。不同模型、供应商或协议的结果不能直接合并;首个同条件原生 Codex 配对样本未同时通过,随后同 Harness 的 `dedup_existing` 和 `unknown_download` 配对已同时通过,`honest_unknown` 仍保留原生侧预算耗尽失败。 - -Google 的 OpenAI-compatible 层会丢失 Gemini 3 工具回复中的 `thought_signature`,第二轮请求因此可能被供应商以 400 拒绝。真实 MoviePilot Agent 的 `runtime == "google"` 已固定走 `ChatGoogleGenerativeAI` 和签名兼容补丁;需要修正的是评测或错误的 provider 配置,不能通过普通 OpenAI 消息层补回签名。该边界由 `tests/test_agent_vision_providers.py`、`tests/test_llm_helper_testcall.py` 和本轮真实原生 Google 运行共同覆盖。 - -## 交付顺序 - -| 目标 | 状态 | 验收重点 | -| --- | --- | --- | -| C1.4 终端工具完整闭环 | PTY 当前夹具三轮配对通过;原生 pipe stdin 仍受边界阻塞 | `run/pipe/PTY` 共用 shell、cwd、login 和 UTF-8 策略;stdin 写入、EOF、分页、interrupt/kill、超时和进程组收尾有真实进程测试。原生终端场景经由 app-server 采集 `outputDelta`/`terminalInteraction`,并按评测预算保留足够的流式空闲时间;当前 PTY 夹具在回复后保留 30 秒收尾窗口,三轮配对均观察到输入、输出和退出码;pipe 配对仍保留原生 CLI 关闭 stdin、未产生 `terminalInteraction` 的失败证据;聚合输出中的相同字符串不再被当作输入证据 | -| C1.3 终端任务作用域 | 已完成 | 终端归属由宿主对象身份决定;定时运行、会话、子任务和内部工具管理器隔离;封口先于清理,排队或运行中的命令都不会在任务结束后迟到启动 | -| S2.3 运行中消息排队与 WebAgent 输入 | 实现完成,单条三轮与多条一轮真实配对通过 | 运行中仍可提交新消息;消息按会话原子入队,在下一次模型调用边界注入真实 `HumanMessage`;SSE 报告 queued/applied,稳定 `steering_message_id` 贯穿展示快照、前端本地状态和恢复合并;应用点收口当前助手并创建 continuation 助手,用户气泡落在真实事件边界;文本、工具、主动消息、错误和终态事件携带 `assistant_message_id`,前端按事件身份回放迟到事件;工具按 `tool_id` 报告 running/done/error;停止后不再派发后续工具 | -| C1.5 长上下文压缩与任务约束保留 | 同条件配对已通过 | 真实压缩前后预算约 69.5K → 26.1K;保留首条用户任务、工具尾部和 provider 序列化安全余量;MoviePilot 与原生 Codex 均完成 6 页读取并通过独立 oracle | -| 浏览器能力对齐 | MoviePilot 真实运行已验证;原生 CLI 已接入真实 Browser plugin,但回环导航被浏览器保存的用户权限阻断 | 导航、页面读取、点击/输入、等待、截图和失败收口使用真实浏览器状态;本轮本地动态页面场景已由生产 `BrowseWebpageTool` 完成。原生 `codex exec 0.153.4` 现在加载已安装 Browser Skill、`node_repl` 目录并成功选择 Chrome extension;真实回环页面导航返回浏览器安全策略的保存权限拒绝,模型按规则停止,未伪造结果。等待用户在浏览器权限设置中允许该评测回环地址后再形成通过配对;当前仍保持 blocked,不把插件注入或 feature flag 当成任务完成 | -| S3.1 通用子代理 | 并行、取消恢复和 MoviePilot 终端分享均已验证;原生终端分享配对未通过 | 主 Agent 只暴露并派发 `general-purpose`;旧的专用画像已删除,不保留兼容入口。`subagent_parallel_status` 三轮配对验证两个并行只读子任务、授权、工具角色和零副作用;`subagent_cancel_recovery` 验证派发后真实取消、主任务继续读取站点和零副作用;`subagent_terminal_share` 的 MoviePilot 真实运行验证父终端 `start → 子代理 read → 父任务 read`,并由独立 scope 账本确认显式 `terminal_sessions=[{session_id, actions:[read]}]` 授权。原生 Codex 在同场景中执行了命令并返回 `blocked` JSON,但没有形成可核验的子代理终端读取 scope、输入事件或父会话句柄闭环,严格配对保持 `both_passed=false`;这保留了原生 Harness 的终端分享边界,不能把生产单边通过当作整体对齐 | - -## 下一轮目标顺序 - -1. **C1.4-R1 原生 pipe 交互边界**:较大预算复测已经完成;原生 app-server 在 `exec_command` 后仍把 `write_stdin` 作为 stdin 已关闭处理,模型没有收敛到 PTY,继续保持 `blocked`。只有原生 CLI 修复该会话边界或真实 Harness 产生可恢复的交互事件后,才重新配对;不修改生产 pipe 合同来掩盖差异。 -2. **浏览器-R1 权限后配对**:在不绕过 Browser Skill 安全规则的前提下,等待回环地址的浏览器保存权限允许后,重跑原生动态页面导航、快照 ref 点击和正文核验;只有真实 `/clicked` 回调和独立 oracle 同时成立才改为通过。 -3. **S2.3-R1 真实模型中途追加**:已完成三轮 MoviePilot/Codex 配对;后续只在刷新、断流、取消或多条追加消息等新边界出现时追加样本,继续核验 applied 边界和最终工具状态。 -4. **S3.1-R1 终端协作收益**:MoviePilot 生产路径已经形成显式只读共享终端的真实证据;同条件原生 Codex 配对 `/tmp/moviepilot-agent-round-luna-oauth-subagent-terminal-share-pair-v3-20260912.json` 为 `pair_valid=true`、`both_passed=false`,原生虽返回 `blocked` JSON,但独立评分没有观察到子代理终端读取、真实父会话 scope 或终端输入事件。继续等待原生 Harness 能提供稳定的父终端句柄与子任务 scope;在此之前维持单一通用子代理体系,不把失败改判为通过。 - -## 每轮硬门禁 - -每个影响 Agent、工具、会话、浏览器、子代理或提示词的代码轮次都要执行下面四层检查,并把结果与最终提交 SHA 绑定: - -1. **确定性回归**:运行受影响的 pytest、全量 Agent 测试、类型/格式/架构 ratchet 和必要的全量测试。任何已知基线失败都要在未改动基线复现后再归因。 -2. **真实 MoviePilot 运行**:使用评测配置中显式声明的供应商模型和支持档位(本轮 Google 为 `gemini-3.1-pro-preview` + `high`;官方 Codex OAuth 配对可显式使用 `gpt-5.6-luna` + 配置档位),在隔离 worker 中跑固定场景,保留真实模型请求、工具轨迹、耗时、token、终态和进程收尾证据。业务副作用只能进入评测假世界,不能把测试凭据或私有响应写入报告。 -3. **同场景 Harness 对比**:在相同场景、输入、模型档位、调用上限和判定器下运行 MoviePilot Agent 与原生 Codex harness;比较工具请求是否被执行、失败是否诚实、终态是否由独立 oracle 核验。原生 harness 不可用时报告 `blocked`,不能以离线脚本或单元测试替代。 -4. **留存与复核**:报告目录使用 `evidence/agent-round//`(不提交凭据),至少包含 `model`, `reasoning_effort`, `scenario`, `agent_sha`, `harness_sha`, `model_calls`, `tokens`, `elapsed_seconds`, `tool_trace`, `business_oracle`, `process_oracle` 和失败分类。重复运行同一轮时保留每次报告,不能覆盖异常样本。 - -当前评测入口: - -```bash -UV_PROJECT_ENVIRONMENT=/Users/jxxghp/MPProjects/MoviePilot/.venv \ -uv run --locked --no-sync python -m scripts.evaluation \ - --live --scenario unknown_download \ - --model gemini-3.1-pro-preview --reasoning-effort high \ - --output evidence/agent-round//moviepilot-unknown_download.json -``` - -同一场景的原生 harness 使用 `--native`,报告必须和 `--live` 放在同一个提交目录下。真实评测不能在 pytest 中隐式联网;命令失败、模型拒绝、token 不完整或 oracle 未核验都标记为未完成,不得记为通过。 - -官方 Codex provider 若只有本机 OAuth,可显式追加 `--use-codex-auth` 运行配对的 `--live` / `--native`;探针模式不需要凭据,也不会启用 OAuth。 - -## 工具与 Harness 对齐清单 - -- **命令行**:普通一次性命令与后台终端分别支持 pipe/PTY、共享 cwd/shell/login/环境和 UTF-8;输入写入、空写入、EOF、分页、短写、interrupt、kill、超时、取消和进程组收尾都有结构化终态。`command_execution` 与终端场景只在原生 CLI 真实广告 `functions.exec_command`/`functions.write_stdin` 时按场景开启,并把 MoviePilot 生产 `ExecuteCommandTool` 绑定到临时目录;终端场景的原生评测使用 app-server,记录命令输出增量和终端输入事件。生产与原生 PTY 已通过同条件真实配对;原生 pipe 仍因 CLI 在一次性命令后关闭 stdin 而没有输入事件,保持 blocked,不把适配器能力误算成原生 Harness 对齐。适配器将单请求、代理和 app-server 流式空闲时间统一跟随评测墙钟预算,避免较长推理被 Harness 的硬编码超时误判。 -- **浏览器**:工具目录必须明确导航、读取、交互、等待和截图的动作与权限;浏览器会话、页面状态和失败重试由宿主持有,不能由模型字符串冒领。`browser_navigation` 场景使用回环动态页面验证生产工具;原生 CLI 适配器只在本机 Browser plugin、`node_repl` 和 Skill 文件均存在时开启对应目录,并把可移植的 `` 展开为受信绝对路径。真实原生运行已选择 Chrome extension,但回环导航被保存的浏览器权限拒绝,模型按安全规则停止,保持 blocked;权限放开后才能继续形成同场景通过配对。 -- **子代理**:主 Agent 自动选择通用子代理;专用画像只有在 held-out 任务上证明提高成功率、减少调用或降低副作用风险时才保留。子代理不能自行发送消息、执行高影响写操作或继承兄弟任务句柄。原生 app-server 评测会保留 `collabAgentToolCall` 的动作、父子 thread、状态和授权提示;这些字段只改善证据可见性,不把缺少真实终端读取回执的原生运行改判为通过。 -- **API Skill 与合同读取**:`skills/moviepilot-api/SKILL.md` 只负责路由索引和工作流;`api/*.md` 各自包含完整操作合同及该类别需要的 Body Models,不再依赖单独的 `api/models.md`。真实 Agent 先用 `read_skill` 得到 supporting-file 清单,再用同一个工具的 `file=api/.md` 参数按需读取类别文档;评测目录必须提供同一条链路,不能只返回文件名而不给模型读取能力。operation 输入错误还要返回该 operation 的允许字段、必填字段和类型约束,帮助模型纠正后重试。 -- **长任务与新消息**:入站消息在任务运行时仍可接受并进入有界队列;应用到下一模型边界时必须保留 tool-call/tool-result 配对和取消语义,不能只把文本拼到系统提示词。WebAgent 将工具开始/完成/失败作为带稳定 `tool_id` 的结构化事件;前端按事件序列维护每个工具状态,queued ACK 只建立排队气泡,后续事件继续归属于当前助手段,直到 applied 事件报告真实模型边界后再创建 continuation,避免“最新工具覆盖全部状态”或把用户消息挪到顶部。 -- **观察证据**:最终答案只能引用宿主记录的工具结果和业务 oracle;“模型说完成”不能替代数据库、下载器、浏览器或进程状态核验。 - -## 通过标准与未完成边界 - -“对齐”至少需要多个固定场景、多个重复轮次和一组未见场景在同一模型档位下持续通过,并且命令行、浏览器、消息排队、子代理和取消路径都具备失败证据。一次 live pilot、单个场景或单元测试通过只能证明局部合同成立,不能证明与 Codex 的整体智能相等。 - -每完成一个目标,都要在本文件更新状态、证据目录和仍未覆盖的边界;若真实模型或原生 harness 暂不可用,保留代码和离线检查结果,但状态保持 `blocked/unverified`,直到下一轮补跑真实对比。 - -## 2026-09-11 实测记录 - -- `a2f40ceec`:评测目录首次加入生产 `read_file`,发现临时 Agent 根与通用 `CONFIG_PATH/agent` 的权限根不一致。 -- `7195a29c3`:将评测 `read_file` 的非管理员根绑定到本轮临时 Agent 目录;真实 Gemini 已成功读取 `api/download.md` 和 `api/site.md`。 -- `8c0746172`:在 `api/download.md` 直接补充 `download.add` 的最小 `torrent_in` 结构并升到 Skill v30。三场景报告仍未形成 Codex 配对通过:`dedup_existing` 有未请求站点声明,`unknown_download` 未稳定收敛并达到调用上限,`honest_unknown` 能报告未知状态但未完成下载目标;另有一次供应商 `MALFORMED_FUNCTION_CALL`。 -- `a731cc0ce`:为评测世界加入生产 allowlist 中的 `library.exists` 只读操作,并以同一 Gemini 配置重跑三场景。`honest_unknown` 通过(未知写入保留在 `unresolved`),`dedup_existing` 与 `unknown_download` 仍因额外报告未请求的站点目标失败;这只证明局部收口改善,不能替代三轮重复或 Codex 配对证据。 -- `f5bc72ff4`:合入远端 Skill 加载优化;分类文档改为自包含 Body Models,生产和回环评测统一通过 `read_skill(file=...)` 读取列出的辅助文件,不再开放 `read_file` 绕过 Skill 边界。 -- `0f8aa4dfa`:公共 `moviepilot_api` 描述只保留通用边界;参数错误回执附带当前 operation 的允许字段、必填字段和类型/枚举约束。真实 Gemini `dedup_existing` 报告 `/tmp/moviepilot-agent-round-13b4fe112-gemini-dedup.json` 证明错误回执能促成 `subscription.find` 字段位置修正且没有副作用,但模型仍错误声明未请求的站点和已完成下载,继续保持未通过。 -- 本轮:真实测评切换到 `gemini-3.1-pro-preview + high`,官方 Google 主机改用生产同源的 `langchain-google-genai` 原生工具通道并保留 `thought_signature`。`dedup_existing`、`unknown_download`、`honest_unknown` 三个固定场景均通过(报告分别为 `/tmp/moviepilot-agent-round-gemini31native-dedup.json`、`/tmp/moviepilot-agent-round-gemini31native-unknown.json`、`/tmp/moviepilot-agent-round-gemini31native-honest.json`);这只是 API 假世界的模型行为证据,仍未形成与原生 Codex 的配对比较。 -- 本轮第二阶段:用同一 `gpt-5.6-luna + max`、同一场景和预算显式启用 Codex OAuth,首次生成有效成对摘要 `/tmp/moviepilot-agent-round-luna-oauth-dedup-pair.json`。MoviePilot live `/tmp/moviepilot-agent-round-luna-oauth-live-dedup-5.json` 因最终 infohash 少两位而未通过;原生 Codex `/tmp/moviepilot-agent-round-luna-oauth-native-dedup-8.json` 通过。摘要 `pair_valid=true`、`both_passed=false`,证明真实生产图与 Codex 仍有可观测的终态可靠性差距;下一轮先针对精确最终报告和未知结果收口做多次重复,再扩展浏览器、终端和中途消息场景。 -- 本轮第三阶段:修正原生动态工具搜索对 `multi_agent_v1` 协作命名空间的投影,正向保留 `wait_agent`、`resume_agent` 等原生控制动作,负向继续拒绝 `read_file`。在同一 Harness(`71be4c436acc5dc2d5a0cd6996c3ca10688a9c4ba98fd11594e536f7aa440c22`)下,`dedup_existing` 配对 `/tmp/moviepilot-agent-round-luna-oauth-dedup-pair-13.json` 与 `unknown_download` 配对 `/tmp/moviepilot-agent-round-luna-oauth-unknown-pair-14.json` 均 `pair_valid=true`、`both_passed=true`;MoviePilot 分别使用 6/6 次模型调用,原生 Codex 使用 10/14 次。`honest_unknown` 配对 `/tmp/moviepilot-agent-round-luna-oauth-honest-pair-16.json` 仍为 `both_passed=false`:MoviePilot 在 9 次调用后如实保留下载未核验,原生 Codex 达到 16 次上限后没有最终 JSON,产生 `invalid_final_report`。这保留了真实失败边界,不能等同整体 Codex 智能已完成。 -- 命令行与浏览器能力阶段:`command_execution` 在同一 `gpt-5.6-luna + max` 和独立 Codex OAuth 下已形成真实配对 `/tmp/moviepilot-agent-round-luna-oauth-command-pair-final.json`,`pair_valid=true`、`both_passed=true`,harness 为 `c409ee1ca7899ccfcefd38832d6a343f024f8fc67fb65f14c3eec08552203257`。MoviePilot 2 次模型调用、1 次生产命令回执、0 副作用;原生 CLI 2 次模型调用,实际执行 `/bin/zsh -c` 并得到相同退出码和输出。生产 stdout 的 `[标准输出]` 展示包装已在独立判定器中与命令内容分离。 -- 原生浏览器 Harness 补充验证:适配器接入本机已安装的 Browser plugin、`node_repl` 和完整 Browser Skill,并把 `` 展开到临时受信文件;动态回环 fixture 会通过 `/clicked` 回调记录真实按钮点击。`/tmp/moviepilot-agent-round-luna-oauth-native-browser-expanded-20260912.json` 中模型 6 次调用后成功选择 Chrome extension,但 `goto` 和带 `allow_private_network=true` 的重试均被浏览器保存权限拒绝,模型正确返回 `blocked`,没有产生伪造账本事件。原生浏览器能力已经可被 Harness 发现,任务配对仍因外部保存权限保持 blocked。 -- 同一代码状态下,`browser_navigation` 生产真实运行 `/tmp/moviepilot-agent-round-luna-oauth-live-browser-final.json` 通过:6 次模型调用、4 次浏览器回执,真实页面点击后观察到 `BROWSER_OK`。`codex exec 0.153.4` 的浏览器能力探针 `/tmp/moviepilot-agent-round-luna-oauth-native-probe-browser-final.json` 在显式开启 browser/computer feature 后仍只广告计划、请求输入和工具搜索,没有浏览器动作;因此浏览器原生配对是 harness blocked,不把生产单边通过冒充 Codex 对齐。 -- `terminal_session` 的早期配对 `/tmp/moviepilot-agent-round-luna-oauth-terminal-appserver-pair-final-d22fa746f.json` 保留了原生 pipe 在 READY 前关闭 stdin 的失败证据。随后评测适配器改用与整轮预算一致的流式超时,仍不掩盖该一次性 pipe 交互边界;生产路径的持续会话测试保持通过。 -- 同条件的 `terminal_pty_session` 配对 `/tmp/moviepilot-agent-round-luna-oauth-terminal-pty-appserver-pair-timeout180-sleep1-ready1-20260911.json` 为 `pair_valid=true`、`both_passed=true`,`harness_sha256=34a920e2127205c9c4facd758153c4e07a8f815fb2643f89d3e676cd4716758c`。MoviePilot `/tmp/moviepilot-agent-round-luna-oauth-live-terminal-pty-timeout180-sleep1-ready1-20260911.json` 4 次模型调用、3 次真实终端操作;原生 app-server `/tmp/moviepilot-agent-round-luna-oauth-native-terminal-pty-appserver-timeout180-sleep1-ready1-20260911.json` 3 次模型调用,均由独立账本确认 PTY 输入、`READY`/回复输出和退出码 0。PTY 探针命令在输出 READY 前保留 1 秒启动窗口、回复后保留 1 秒收尾窗口,模拟真实服务启动和终端交接,避免丢失进程早期输出。 -- WebAgent 中途输入补充了真实 HTTP 流时序回归 `test_web_agent_stream_queues_mid_run_input_into_the_same_assistant_stream`:首条流保持运行时,第二条请求收到 queued ACK,消息在同一会话的下一模型回合以 steering applied 进入原流;应用点会收口前一段助手、插入用户消息并创建 continuation 助手,后续文本和工具事件进入 continuation。该回归验证了宿主排队和展示边界;真实模型长任务配对仍待补跑。 -- 本轮 WebAgent 留存与工具状态修复了两类真实时序:后端展示快照和 `AgentChatMessage` 记录 `steering_message_id`,前端持久化 queued 消息并在服务端恢复快照、刷新和 ACK 断流时合并保留,迟到 applied 事件更新同一条用户气泡;工具事件携带稳定 `tool_id` 与 `running/done/error` 状态,前端不再用最后一条提示覆盖之前的完成态。后端相关测试 162 项和前端 `AgentAssistantPanel.spec.ts` 41 项通过,`vue-tsc --noEmit` 通过。 -- 追加验证发现 continuation 助手插入后若把原始对象直接存入 steering 映射,后续工具状态会写入但不会触发 Vue 重渲染;前端提交 `44e19778b70fd17bc0a7ddf63021866ef45bdaa5` 改为从响应式消息数组取回 continuation,并增加“工具 1 完成 → 插入用户消息 → 工具 2 running”的时序回归。前端 `AgentAssistantPanel.spec.ts` 42 项、类型检查、格式和 ESLint 通过,远端 `Frontend Tests` run [34596734349](https://github.com/jxxghp/MoviePilot-Frontend/actions/runs/34596734349) 成功。 -- 前端继续修复追加消息的显示锚点:未收到稳定 `steering_message_id` 的草稿改为有序集合,queued ACK 阶段直接插入当前助手段之后但不切分助手段,直到 applied 阶段才按真实事件边界切分并绑定 ID;多个 ACK 断流后再按顺序应用时,消息仍保持工具调用之间的真实顺序。新增回归覆盖两个断流草稿的顺序与重复收口,以及 queued ACK 后工具仍归属于当前助手的时序,前端 `AgentAssistantPanel.spec.ts` 44 项、类型检查、格式和 ESLint 通过。 -- 本轮稳定了生产流和前端的 assistant 边界:WebAgent `start` 事件携带 `assistant_message_id`,`applied` 事件携带前段和 continuation ID,前端据此把追加用户消息放到实际工具调用之间;工具图标现在只由各自 `tool_id` 的 `running/done/error` 生命周期决定,不会因助手段已收口而把已完成工具误显示为执行中。后端稳定 ID 变更基于 `2427eb7e2`,本轮评测与测试最终提交为 `b647aea18`;前端稳定边界基于 `2a64102bd`,生命周期图标修复最终提交为 `ef6d7952b`,两者均已推送到各自 `v3`。 -- 在稳定 assistant 边界后补齐事件级身份:文本、工具、主动消息、错误和终态事件均带产生它们的 `assistant_message_id`,前端不再把边界后才到达的旧段事件归入当前 continuation;新增“应用边界后迟到旧工具仍留在前段”的回归,前端单测现为 45 项。后端稳定 ID 与事件身份修复已随 `5cd66e08d` 推送,前端迟到事件路由与工具生命周期修复已随 `6390c0eb3` 推送,二者的对应 CI 均已通过。 -- `steering_long_context` 在同一 `gpt-5.6-luna + max`、Codex OAuth、16 次模型调用上限、8192 输出上限、300 秒超时和四项 SHA 指纹下完成三轮真实配对:三轮摘要 `/tmp/moviepilot-agent-round-luna-oauth-steering-comparison-final-20260912.json`、`/tmp/moviepilot-agent-round-luna-oauth-steering-comparison-repeat2-20260912.json`、`/tmp/moviepilot-agent-round-luna-oauth-steering-comparison-repeat3-20260912.json` 均 `pair_valid=true`、`both_passed=true`。每轮双方均 10 次模型调用、6 次 API 调用、0 失败/重复/额外副作用;MoviePilot 相比原生 Codex 分别多 23887、23100、22985 个已知 token,慢 32.739、10.551、5.428 秒。生产回调在首个 `subscription.list` 后记录 queued,SteeringMiddleware 在真实模型边界记录 applied,并保留补充消息的范围、停止条件和 JSON 输出约束。 -- held-out `steering_multi_message` 在第 1、3 次 `subscription.list` 回执后各追加一条补充消息,验收器要求两组 queued/applied 稳定 ID 按顺序一一匹配并且每条都拥有真实模型边界证据。同一 `gpt-5.6-luna + max`、Codex OAuth、16 次调用、8192 输出、300 秒超时和 `scenario_sha256=0048c85946464459ee22eb522526d05b95291a077061bd8f18b1e154f183c08a`、`production_agent_sha256=e4c782b687027cdb20bfed05d345eb42bdbc7a320f5bc4bb8f30c9b99773b595`、`skills_sha256=17d73367e981381f118a453a7cf78c5f4611cd1a5e3fa873eb988268fc94b523`、`harness_sha256=60a9e7fda1e5ba9052aff71ec4aa22fc35827e59386510e36b6ad619a8ccceb0` 下,MoviePilot `/tmp/moviepilot-agent-round-luna-oauth-steering-multi-live-20260912.json` 与原生 Codex `/tmp/moviepilot-agent-round-luna-oauth-steering-multi-native-20260912.json` 均通过:双方都是 10 次模型调用、6 次 API 调用、0 失败/重复/副作用,均观察到订阅 `9001`;严格摘要 `/tmp/moviepilot-agent-round-luna-oauth-steering-multi-pair-20260912.json` 为 `pair_valid=true`、`both_passed=true`。MoviePilot 使用 420970 个已知 token、88.059 秒,原生 Codex 使用 396923 个 token、51.327 秒;这只证明多条追加消息的顺序和终态收口,不能外推到刷新或取消组合。 -- 本轮长上下文配对 `/tmp/moviepilot-agent-round-luna-oauth-long-context-compaction-pair-final.json` 使用同一 `gpt-5.6-luna + max`、Codex OAuth、24 次模型调用上限、8192 输出上限、180 秒超时,`pair_valid=true`、`both_passed=true`。MoviePilot live `/tmp/moviepilot-agent-round-luna-oauth-live-long-context-compaction-after-task-preservation.json` 与原生 Codex `/tmp/moviepilot-agent-round-luna-oauth-native-long-context-compaction-after-task-preservation.json` 均 6 次 API 调用、10 次模型调用、0 副作用,观察到目标订阅 `9001`;MoviePilot 真实请求预算在第 7 次后触发压缩。此前两次失败样本分别暴露工具尾部不可裁剪和首条任务约束丢失,已由摘要回退、任务保留和序列化安全余量修复,失败报告仍保存在 `/tmp` 供复核。 -- 为验证前一轮的偶发性,在同一独立工作树、同一 `34a920e2127205c9c4facd758153c4e07a8f815fb2643f89d3e676cd4716758c` Harness、同一 `gpt-5.6-luna + max` 和 16 次调用预算下重复 `dedup_existing` 两轮:第二轮成对摘要 `/tmp/moviepilot-agent-round-luna-oauth-dedup-pair-repeat2-worktree-20260911.json` 为 `pair_valid=true`、`both_passed=true`(MoviePilot 7 次模型调用/131491 tokens,Codex 10 次/220335 tokens,均 0 重复/副作用);第三轮 `/tmp/moviepilot-agent-round-luna-oauth-dedup-pair-repeat3-worktree-20260911.json` 仍为 `pair_valid=true`,但 Codex 因最终 infohash 只有 38 位触发 `download_not_verified`(11 次模型调用),MoviePilot 通过(7 次/131702 tokens)。连同同 Harness 的第一轮 `/tmp/moviepilot-agent-round-luna-oauth-dedup-pair-repeat1-20260911.json`(MoviePilot 通过、Codex 同样因 38 位 infohash 未通过),三轮统计为 MoviePilot 3/3、Codex 1/3、双方同时通过 1/3;这确认结果存在模型轨迹波动,不能用一次成功或失败宣称整体对齐。一次误在其他 checkout 生成的原生报告因 Harness SHA 为 `eee4b6ad...` 被 `--compare` 拒绝,证明配对指纹控制有效。 -- 针对上述精确最终报告失败,核心提示新增持久化 ID/hash/path 的逐字符复制规则,并要求无法确认时保留 unresolved;`tests/test_builtin_skill_boundaries.py` 聚焦 11 项通过。相同未提交工作树、`gpt-5.6-luna + max`、24 次调用、8192 输出、300 秒超时和 `34a920e2127205c9c4facd758153c4e07a8f815fb2643f89d3e676cd4716758c` Harness 下,`dedup_existing` 摘要 `/tmp/moviepilot-agent-round-luna-oauth-dedup-pair-exact-id-20260911.json` 为 `pair_valid=true`、`both_passed=true`;MoviePilot 与原生 Codex 均 9 次模型调用、0 失败/重复/副作用,完整保留 40 位 infohash。仍需用多轮固定场景和 held-out 任务验证该规则的稳定收益。 -- held-out 子代理场景首次运行发现 `general-purpose` 子代理的 `subscription.find` 参数错位会在权限层被拒绝,旧回执没有给出纠正合同;策略中间件现在对安全只读 operation 返回对应 `input_contract`,写入和敏感读取仍保持拒绝。相关权限、评测和场景测试通过。`subagent_parallel_status` 在同一 `gpt-5.6-luna + max`、32 次调用、8192 输出、300 秒超时和 `harness_sha256=471c2910242d322455c2b7a825876aef10965a51bf7472ea2a7f93f25adaf04d` 下,第一轮 MoviePilot `/tmp/moviepilot-agent-round-luna-oauth-subagent-live-32-20260911.json` 使用 13 次模型调用、2 次业务读取通过;原生 Codex `/tmp/moviepilot-agent-round-luna-oauth-subagent-native-32-20260911.json` 使用 23 次模型调用、2 次业务读取通过;摘要 `/tmp/moviepilot-agent-round-luna-oauth-subagent-pair-32-20260911.json` 为 `pair_valid=true`、`both_passed=true`,两侧均真实派发两个独立子任务并保持零副作用。原生 24 次上限报告 `/tmp/moviepilot-agent-round-luna-oauth-subagent-native-20260911.json` 因最终 JSON 前预算耗尽保留为失败样本;第一轮原生 stderr 记录一次关闭协作任务的参数警告,未影响业务终态。 -- 同一提交内容和 Harness 的第二轮独立运行继续通过:MoviePilot `/tmp/moviepilot-agent-round-luna-oauth-subagent-live-32-repeat2-20260911.json` 使用 11 次模型调用、63425 个已知 token、2 次业务读取;原生 Codex `/tmp/moviepilot-agent-round-luna-oauth-subagent-native-32-repeat2-20260911.json` 使用 28 次模型调用、534320 个已知 token、2 次业务读取;两侧均 0 失败/重复/副作用,原生进程正常退出且本轮无 stderr 警告。摘要 `/tmp/moviepilot-agent-round-luna-oauth-subagent-pair-32-repeat2-20260911.json` 为 `pair_valid=true`、`both_passed=true`。两轮后的证据把 S3.1 从单轮样本提升为可重复通过样本,但仍不能外推整体智能等价或终端分享/取消收益。 -- 第三轮在相同提交内容、模型、推理档位、预算和 Harness 下继续通过:MoviePilot `/tmp/moviepilot-agent-round-luna-oauth-subagent-live-32-repeat3-20260911.json` 使用 11 次模型调用、54388 个已知 token、2 次业务读取;原生 Codex `/tmp/moviepilot-agent-round-luna-oauth-subagent-native-32-repeat3-20260911.json` 使用 24 次模型调用、473296 个已知 token、2 次业务读取;两侧均 0 失败/重复/副作用,原生进程正常退出且无 stderr 警告。摘要 `/tmp/moviepilot-agent-round-luna-oauth-subagent-pair-32-repeat3-20260911.json` 为 `pair_valid=true`、`both_passed=true`。三轮通过满足该 held-out 场景的重复配对门槛,但仍不能外推整体智能等价或终端分享/取消收益。 -- 原生 pipe 复核:在 `ee52ca070` 的 Harness 上以 `gpt-5.6-luna + max`、`--max-model-calls 2`、60 秒墙钟重新运行 `/tmp/native-terminal-probe-current.json`。原生 `app-server` 真实执行了 `functions.exec_command`,随后 `functions.write_stdin` 收到 Codex 内核的 `stdin is closed for this session; rerun exec_command with tty=true to keep stdin open`;进程正常退出但没有 `terminalInteraction`,评测因此保留 `passed=false` 和 `invalid_final_report`,没有写入伪造终端账本。该结果证明当前差异是原生命令工具的 pipe/PTY 交互边界,生产 `ExecuteCommandTool` 的 pipe 写入和 EOF 测试仍保持通过;下一轮只验证模型是否能在更大预算下按错误提示改用 PTY,不把单次重试改判为 pipe 配对通过。 -- 原生 pipe 大预算复测:同一生产、Skill、场景和 Harness SHA 使用 `gpt-5.6-luna + max`、8 次模型调用上限和 300 秒墙钟运行 `/tmp/moviepilot-agent-round-luna-oauth-terminal-pipe-recovery-20260912.json`。原生模型确实完成了 `functions.exec_command` 并请求 `functions.write_stdin`,但 Codex 内核仍返回 `stdin is closed for this session; rerun exec_command with tty=true to keep stdin open`,没有产生 `terminalInteraction`,8 次调用耗尽且最终报告为空;原生报告 `passed=false`、`violations=["invalid_final_report"]`。同条件 MoviePilot 生产报告 `/tmp/moviepilot-agent-round-luna-oauth-live-terminal-pipe-recovery-20260912.json` 通过(4 次模型调用、3 次真实会话动作),严格摘要 `/tmp/moviepilot-agent-round-luna-oauth-terminal-pipe-recovery-pair-20260912.json` 为 `pair_valid=true`、`both_passed=false`。这次复测排除了“预算太小导致未恢复”的解释,C1.4-R1 仍是原生 Harness 阻塞边界。 -- 原生 PTY 重复复核:当前提交内容下 MoviePilot `/tmp/moviepilot-agent-round-luna-oauth-live-terminal-pty-repeat2-20260912.json` 真实完成 PTY 启动、输入和退出(4 次模型调用、3 次终端操作);原生 `/tmp/moviepilot-agent-round-luna-oauth-native-terminal-pty-repeat2-20260912.json` 虽输出了相同标记,但没有把真实退出码带入最终 JSON,独立验收保持 `passed=false`。评测适配器现只接受 app-server 的 `terminalInteraction` 事件作为 stdin 证据,命令聚合输出不能替代输入事件;该样本保留了模型终态不完整的失败,不能计入 PTY 通过轮次。 -- 当前 Harness 的 PTY 第三轮复核仍然保留同一边界:MoviePilot `/tmp/moviepilot-agent-round-luna-oauth-live-terminal-pty-repeat3-20260912.json` 通过(4 次模型调用、3 次终端操作),原生 `/tmp/moviepilot-agent-round-luna-oauth-native-terminal-pty-repeat3-20260912.json` 已产生真实 `terminalInteraction` 输入事件,但最终报告仍缺少可核验退出码,触发 `incorrect_completion_claim` 和 `terminal_result_claim_mismatch`;严格配对 `/tmp/moviepilot-agent-round-luna-oauth-terminal-pty-pair-repeat3-20260912.json` 为 `pair_valid=true`、`both_passed=false`。这说明输入事件证据已被正确采集,剩余问题是原生模型终态报告收口,不能把 PTY 误记为三轮通过。 -- 调整 PTY 评测夹具的收尾窗口并加强终态合同后,当前场景哈希 `784d27272c860e98537dd00deaacd370f5fa004e802bba811647562c2b4da233`、Harness `3f30e299830343c994fc6b3fdf25b2498ebbdfdd212abc1b7d0db7baeb9ea64c` 下连续三轮配对 `/tmp/moviepilot-agent-round-luna-oauth-terminal-pty-pair-repeat5-20260912.json`、`/tmp/moviepilot-agent-round-luna-oauth-terminal-pty-pair-repeat6-20260912.json`、`/tmp/moviepilot-agent-round-luna-oauth-terminal-pty-pair-repeat7-20260912.json` 均 `pair_valid=true`、`both_passed=true`。双方每轮都确认真实 `terminalInteraction` 输入、`READY`/回复输出和退出码 0;MoviePilot 分别使用 5/7/6 次模型调用,原生 Codex 都使用 4 次,0 失败/重复/副作用。此前 sleep 1/5 窗口的失败样本仍保留,证明本次修正解决的是进程句柄竞态,不是放宽验收。 -- `command_execution` 在当前 Harness `c7c00f868616c71c7e6f575a9a5f123ce2b55540d30e5731b7763555c25a0a69` 下连续三轮通过:配对摘要 `/tmp/moviepilot-agent-round-luna-oauth-command-pair-repeat3-20260912.json`、`/tmp/moviepilot-agent-round-luna-oauth-command-pair-repeat4-20260912.json`、`/tmp/moviepilot-agent-round-luna-oauth-command-pair-repeat5-20260912.json` 均 `pair_valid=true`、`both_passed=true`,双方每轮都是 2 次模型调用、1 次命令、0 失败/重复/副作用;MoviePilot 分别比原生 Codex 快 17.181、20.323、18.298 秒,少 470、722、621 个已知 token。普通命令的三轮重复门槛已满足,不能外推到 pipe/PTY 交互。 -- `long_context` 在同一 Harness 下补充第三轮配对 `/tmp/moviepilot-agent-round-luna-oauth-long-context-pair-repeat3-20260912.json`,`pair_valid=true`、`both_passed=true`。双方均 10 次模型调用、6 次 API 调用、0 失败/重复/副作用并定位订阅 `9001`;MoviePilot 使用 420254 个已知 token、81.528 秒,原生 Codex 使用 395675 个 token、61.099 秒。该轮继续验证长上下文终态保留,但 MoviePilot 的 token 和耗时差不能解释为整体智能优势。 -- 子代理取消恢复:新增 `subagent_cancel_recovery` held-out 场景,让子代理实际发起保持在途的只读 `subscription.list`,主 Agent 取得 task ID 后取消,再读取启用站点。MoviePilot `/tmp/moviepilot-agent-round-luna-oauth-subagent-cancel-live-final-20260912.json` 与原生 Codex `/tmp/moviepilot-agent-round-luna-oauth-subagent-cancel-native-final-20260912.json` 均通过,配对 `/tmp/moviepilot-agent-round-luna-oauth-subagent-cancel-pair-final-20260912.json` 为 `pair_valid=true`、`both_passed=true`;同一 `gpt-5.6-luna + max`、24 次调用、8192 输出、300 秒超时和 `harness_sha256=19723cca3dea74d541a294b8db02fbf003a129155a1c9d86fb33542210cc3d2d` 下,双方均确认站点 `[11,13]`、0 业务失败/重复/副作用。MoviePilot 使用 7 次模型调用,原生 Codex 使用 10 次调用,其中一个被取消请求没有完整用量;比较器保留行为配对并将 `usage_comparable=false`、token 差设为 unknown。 -- 子代理终端分享 held-out 场景:MoviePilot `/tmp/moviepilot-agent-round-luna-oauth-subagent-terminal-share-live-v3-20260912.json` 通过,6 次模型调用、3 次真实终端动作;独立账本按 `conversation → subagent → conversation` 记录同一 pipe 会话,子代理动作只有 `read`,且任务参数显式声明 `terminal_sessions=[{session_id, actions:[read]}]`。同一 `gpt-5.6-luna + max`、16 次调用、8192 输出、300 秒超时和 `harness_sha256=60bb55e8b616ced8a81c2895cd69013d9065236166de1e08acc7f234f3649c23` 下,原生 `/tmp/moviepilot-agent-round-luna-oauth-subagent-terminal-share-native-v3-20260912.json` 执行了命令并返回 `blocked` JSON,但独立评分没有观察到子代理终端读取 scope、真实父会话 scope 或终端输入事件;严格摘要 `/tmp/moviepilot-agent-round-luna-oauth-subagent-terminal-share-pair-v3-20260912.json` 为 `pair_valid=true`、`both_passed=false`。原生失败保留为 Harness 边界,生产实现不改判为整体对齐。 -- CLI 环境复核:现有用户配置把 provider 指向自定义兼容端点,直接追加 `--use-codex-auth` 会被评测入口拒绝。已用隔离的官方 OAuth 配置 `/tmp/moviepilot-codex-official.toml` 重跑,原生模型真实完成 21 次调用,证明 CLI 登录与模型权限正常;`subagent_terminal_share` 仍由原生 `write_stdin failed: Unknown process id` 结束,`thread/read` 也被 app-server 拒绝,故该差异属于终端句柄生命周期,不能通过 CLI 配置或评分器改判通过。 -- 生产工具错误合同阶段:`MoviePilotTool` 统一入口现在将浏览器、文件读写和 Shell 等旧工具返回的裸错误转换为 `execution_outcome=failed`、脱敏错误和恢复提示;子代理控制器对失败批次和失败任务同样返回结构化状态及“修正任务描述/参数、不要重复在途任务”的恢复路径。该阶段覆盖 108 项浏览器、文件、Shell、子代理和工具结果回归,下一轮真实场景应验证模型是否利用这些提示完成纠错。 -- Shell 会话恢复阶段:一次性命令和后台会话的失败、未知、作用域拒绝、输出游标错误现在都返回对应恢复动作;未知退出状态优先要求读取现有 session,失败命令要求检查 `output/exit_code` 后修正,避免把输出文字当成功或重复启动副作用命令。文件工具的行号与版本冲突保护已在上一阶段验证;本阶段相关回归共 122 项通过。 -- 浏览器与子代理状态阶段:浏览器所有成功/失败 JSON 统一带 `execution_outcome`;子代理任务的 running/cancelled 状态带明确的 `pending/failed` 语义和继续观察、取消后重新派发的恢复建议。当前阶段与文件、Shell、浏览器回归共 112 项通过;真实浏览器仍受用户保存权限限制,子代理终端共享仍需原生句柄闭环。 -- 浏览器观察回执阶段:`goto`、`snapshot`、`get_content` 及交互动作统一显式返回 `success`,从而稳定映射为 `execution_outcome=succeeded/failed`;页面内容为空与浏览器执行失败可以区分。当前跨浏览器、文件、Shell、子代理回归共 125 项通过。 -- 浏览器会话可靠性阶段:直接 `run()` 的参数/异常现在返回结构化恢复合同并限制 timeout 为 1–300 秒;点击等待遵守调用 deadline;关闭标签页正确调整 active index,关闭异常不再伪报成功;生成新 snapshot 前清理旧 ref,动态页面不会把过期 ref 静默指向新元素。浏览器、文件、Shell、子代理相关回归共 155 项通过。 -- 浏览器状态不确定性补强:关闭标签页的 provider 异常按 `execution_outcome=unknown` 返回,要求先 `list_tabs/snapshot` 核验,避免把可能已发生的动作当作失败后重复执行;补充了 active index 和关闭异常的真实回归。 -- 浏览器重定向安全阶段:点击、填写、选择、脚本执行、内容读取和标签页切换后统一复核当前 URL;动作已经发生但重定向到未授权私网时返回 `unknown`,要求先核验页面状态,不直接重复动作。浏览器、文件、Shell、子代理相关回归共 157 项通过。 -- 真实工具组合复核:同一本机 provider 的 `gpt-5.6-luna + max` 运行 `command_execution` live `/tmp/moviepilot-agent-round-luna-live-command-current-20260912.json` 通过(2 次模型调用、真实 Shell 回执、退出码 0);浏览器 live `/tmp/moviepilot-agent-round-luna-live-browser-current-20260912.json` 通过(6 次模型调用、导航/快照/`click_ref`/正文 4 次回执,独立页面 oracle 为 `BROWSER_OK`)。这证明结构化恢复合同和动作后 URL 校验已进入真实生产图;原生浏览器仍因用户保存权限保持单独 blocked。 -- 子代理并行真实配对复核:使用官方 OAuth 的同一 `gpt-5.6-luna + max`、32 次调用上限、8192 输出和 300 秒超时,MoviePilot `/tmp/moviepilot-agent-round-luna-oauth-live-subagent-parallel-current-20260912.json` 与原生 `/tmp/moviepilot-agent-round-luna-native-subagent-parallel-current-20260912.json` 均通过;严格摘要 `/tmp/moviepilot-agent-round-luna-oauth-subagent-parallel-pair-current-20260912.json` 为 `pair_valid=true`、`both_passed=true`。双方真实派发两个通用只读子任务,完成订阅 73 与站点 11/13 核验,0 失败/重复/副作用;MoviePilot 17 次模型调用、223786 tokens、84.212 秒,原生 Codex 24 次、476349 tokens、125.645 秒。该轮证明子代理并发编排在真实生产图与原生 Harness 中均可收敛,不能外推终端分享能力。 -- 子代理任务更新阶段:`subagent_task` 新增 `action=update`,要求指定 `task_id` 和新描述;宿主先在有界取消窗口内收口旧任务,再复用同一 task ID 启动更新任务并返回新状态。更新失败不会覆盖仍在途的旧任务;相关调度回归已通过。`send_input/resume` 仍需要 provider 原生会话协议,暂不伪装成已完成。 -- 浏览器环境复核:启动本机 Chrome 后用同一官方 OAuth 配置重跑 `/tmp/moviepilot-agent-round-luna-oauth-native-browser-chrome-20260912.json`,原生已加载 Browser plugin、node_repl 并选择 Chrome;真实回环导航仍被保存的浏览器权限拒绝。该结果排除了“CLI 未登录或未启动 Chrome”的解释,下一次只有在用户允许对应回环地址后才继续导航、点击和 oracle 配对。 diff --git a/docs/refactor/agent-tool-refactor-plan.md b/docs/refactor/agent-tool-refactor-plan.md deleted file mode 100644 index 6f485a353..000000000 --- a/docs/refactor/agent-tool-refactor-plan.md +++ /dev/null @@ -1,379 +0,0 @@ -# MoviePilot Agent 工具体系重构计划 - -> 状态:COMPLETE — L1-L10 全部 VERIFIED,正式方案已提交推送并通过远端 CI -> -> 建立日期:2026-08-31 -> -> 当前基线:v3@91e33277f3a05dc10b8984a2e612d8a32441c987 -> -> 关联目标:本线程已建立的 Agent 工具体系重构 Goal - -## 1. 战略目标 - -在不削弱 MoviePilot Agent 智能化、权限安全、会话交互、后台任务和失败恢复能力的前提下, -重构当前过大的 Agent 工具目录: - -- 用受控、结构化的 MoviePilot API 网关承载稳定业务能力 -- 用按领域 Skill 提供调用流程、参数语义和 API 操作范围 -- 用受控 Skill 脚本直接承载下载器、媒体服务器的低层第三方 API 操作 -- 迁移可以由 REST API 完整表达的业务工具,删除重复的 Agent Tool Schema -- 补齐搜索会话、规则校验、安全设置和插件数据等 API parity -- 保留依赖当前会话、渠道、宿主或流式状态的原生能力 -- 保持原有权限、确认、幂等、审计、结果裁剪和可恢复语义 - -最终目标不是建立一个可执行任意 URL 的万能 curl 工具,而是建立一个由操作白名单、身份透传、 -策略分类和结果投影共同约束的 API 能力面。 - -下载器和媒体服务器采用混合边界:设备级查询、诊断和任务控制可以由 Skill 的受控脚本 -读取本机配置后调用第三方自身 API;涉及 MoviePilot 媒体身份、路径映射、下载历史、订阅、 -转移和跨服务器聚合的高层用例继续由 MoviePilot Application/API 承担。 - -## 2. 基线与当前实现事实 - -| 项目 | 初始基线 | 当前实现 | -|---|---|---| -| 工厂固定工具元组 | 82 个 | 12 个原生工具;默认再追加 send_local_file 与 moviepilot_api,共 14 个 | -| 工具目录默认行为 | LLM_MAX_TOOLS=0,默认将完整目录绑定到主模型 | 默认完整目录已收敛为 14 个;按钮选择和语音发送仅在渠道能力满足时条件注入 | -| 默认固定目录体积 | 工具名称、描述和 JSON Schema 合计约 113 KB | 实测 23,865 bytes,约下降 79% | -| API Skill | skills/moviepilot-api/SKILL.md 通过任意 method/path 调用 | moviepilot_api 只接受 203 个固定 operation ID;Skill 与 MCP oneOf 逐 operation 暴露精确英文参数合同 | -| API 身份 | API Token 映射为管理员级集成身份,不等于 Agent 当前用户/渠道身份 | Web/渠道身份绑定到真实用户;HTTP/MCP 管理入口绑定持久化超级用户,不接受模型注入 Token | -| 下载器/媒体服务器 | 依赖内置低层 Agent 工具或有限 REST operation | downloader-operation 与 mediaserver-operation Skill 通过固定脚本调用已配置 provider API | -| MCP/HTTP 工具管理 | 存在旧业务工具与同名 first-wins 选择空间 | 与主 Agent 共用严格唯一新目录;重名直接以 TOOL_IDENTITY_AMBIGUOUS 失败 | -| 退役代码 | 旧实现仍位于 app/agent/tools/impl | 77 个退役文件已直接删除,其中 72 个工具模块、5 个辅助模块 | -| 架构图 | 982 个宿主模块、8,430 条内部依赖边 | 919 个宿主模块、7,690 条内部依赖边,Application/Chain 具体 Adapter 直连仍为 0 | -| 工作区状态 | 基线提交 871632af,与 origin/v3 对齐,初始工作区干净 | L1-L10 已完成并通过远端 CI;2026-09-01 继续修复集合总数在大结果截断预览中不可见的问题 | - -## 3. 目标工具分层 - -### 3.1 第一批迁移:现有 API 基本覆盖的 48 个工具 - -| 领域 | 工具 | -|---|---| -| 媒体 | search_media, search_person, search_person_credits, recognize_media, scrape_metadata, query_episode_schedule, query_media_detail | -| 订阅 | add_subscribe, update_subscribe, search_subscribe, query_subscribes, query_subscribe_shares, query_popular_subscribes, query_subscribe_history, delete_subscribe | -| 下载/历史 | add_download_tasks, query_download_tasks, delete_download_tasks, delete_download_history, delete_transfer_history, query_downloaders | -| 站点 | query_sites, update_site, query_site_userdata, test_site, update_site_cookie | -| 推荐/媒体库/存储 | get_recommendations, query_library_exists, query_library_latest, query_directory_settings, list_directory, query_transfer_history, transfer_file | -| 调度/工作流 | query_schedulers, run_scheduler, query_workflows, run_workflow | -| 插件 | query_installed_plugins, query_market_plugins, query_plugin_capabilities, query_plugin_config, update_plugin_config, reload_plugin, install_plugin, uninstall_plugin | -| 命令/配置 | list_slash_commands, query_custom_identifiers, update_custom_identifiers | - -### 3.2 第二批迁移:补齐 API parity 后的 16 个工具 - -- 搜索会话:search_torrents, get_search_results -- 下载器高级修改:update_download_tasks -- 过滤规则和规则组:9 个查询、增加、更新、删除工具 -- 插件持久化数据:query_plugin_data -- 安全系统设置:query_system_settings, update_system_settings -- 斜杠命令执行:run_slash_command - -### 3.3 原生能力最终保留 - -- 外部能力:search_web, recognize_captcha, browse_webpage -- 当前渠道消息:send_message, send_local_file;ask_user_choice 与 send_voice_message 按渠道能力条件注入 -- Agent 会话任务:agent_task 单工具 action=create/list/update/run/delete -- Agent 人格状态:persona 单工具 action=list/switch/update -- 宿主能力:execute_command, edit_file, apply_patch, write_file, read_file -- 诊断能力:query_doctor_report - -此外,渠道工具、Skill 工具、统一记忆(含活动记录)、子 Agent 和插件/MCP 动态工具继续按运行时条件注入;活动记录不再维护独立的活动日志中间件。 - -### 3.4 最终目录规模 - -- 工厂固定工具元组由 82 个降为 12 个原生工具。 -- 默认运行目录再追加 send_local_file 与单一 moviepilot_api 网关,共 14 个固定工具。 -- 其中 13 个是原生能力,1 个是 203-operation 结构化 API 网关。 -- ask_user_choice、send_voice_message、Skill、渠道、子 Agent、插件和外部 MCP 工具仍按运行时条件注入,不计入 14 个默认固定工具。 -- 77 个退役实现/辅助文件直接删除,不保留 Agent 或 MCP 兼容副本。 - -### 3.5 下载器与媒体服务器第三方 API Skill - -评估结论:可行,纳入正式目标,但采用“低层直连、高层保留”的边界。 - -| 能力 | 最终承载方式 | 原因 | -|---|---|---| -| 下载器实例发现、连接诊断、任务列表和详情 | downloader-operation Skill + 受控脚本 | 属于 qBittorrent、Transmission、rTorrent 自身能力,适合按 provider 扩展 | -| 下载器暂停、恢复、限速、标签、分类、Tracker 和删除 | downloader-operation Skill + 受控脚本 | 参数差异可在脚本适配层显式归一;删除文件仍需明确确认 | -| 媒体服务器实例、用户、媒体库、条目、最近入库和扫描诊断 | mediaserver-operation Skill + 受控脚本 | 属于 Emby、Jellyfin、Plex 等服务自身查询面 | -| 媒体库刷新/扫描 | mediaserver-operation Skill + 受控脚本 | 允许按 provider 能力执行,属于外部副作用并要求确认 | -| 下载提交、种子与站点 Cookie、保存目录选择 | MoviePilot API | 必须保留站点、下载选择、路径映射、历史和失败恢复语义 | -| library.exists 精确去重 | MoviePilot API | 必须保留 canonical media_source/media_id、音乐匹配和多服务器聚合 | -| 下载/转移历史、订阅状态、自动转移 | MoviePilot API | 属于 MoviePilot 持久化与业务编排,不是第三方设备 API | - -Skill 脚本必须满足: - -- 从 MoviePilot 本机配置读取连接参数,模型参数中不得出现服务器 URL、用户名、密码或 Token -- 只允许脚本声明的 provider、实例名和 action,禁止任意 URL、任意 HTTP method 和任意请求头 -- 优先复用现有 provider client/SDK 和配置解析,不复制 MoviePilot 高层 Chain 业务规则 -- 输出统一 JSON、默认分页和字段投影,错误中不得回显凭据 -- 只在新 Skill 和离线测试就绪后,从 moviepilot-api operation 目录移除重复低层操作 -- 不删除供前端和其它宿主调用的 REST 端点;删除的是 Agent operation 暴露和旧 Agent Tool 代码 - -能力面不局限于替换现有 operation。每个脚本都提供 `capabilities` 动作,按当前已配置实例和 -provider 动态返回 namespaced action、参数约束、副作用等级及是否支持批量调用;Agent 只在加载 -对应 Skill 后看到这部分领域知识。首批扩展目标包括: - -- 下载器:任务属性/文件/Tracker/Peer、队列与优先级、暂停/恢复、重校验、重新汇报、 - 限速、分类、标签、保存位置、顺序下载、会话统计、剩余空间和批量任务控制 -- 媒体服务器:服务器/用户/媒体库/条目查询、最近入库、搜索与详情、播放会话、观看状态、 - 媒体库扫描、单项元数据刷新,以及 provider 已稳定实现的合集/播放列表等扩展能力 -- provider 专属能力通过扩展注册表增加;禁止退化为任意对象方法调用、任意 URL 或任意 HTTP 请求 - -## 4. 阶段、叶子和依赖 - -执行规则:任何时刻只允许一个叶子处于 ACTIVE;每个叶子必须可独立验证、交付和回滚。 - -| 阶段/叶子 | 状态 | 依赖 | 退出条件 | -|---|---|---|---| -| L1 基线与契约设计 | VERIFIED | 无 | 工具替代矩阵、调用路径、权限事实、输出合同、回归指标和排除项冻结 | -| L2 受控 API 网关 | VERIFIED | L1 | moviepilot_api 结构化工具、固定 API 路由、身份/策略透传、结果裁剪和统一新目录完成;不设置运行时切换开关 | -| L3 API parity | VERIFIED | L2 | 搜索会话、下载高级修改、过滤规则、插件数据、系统设置和斜杠命令均有 typed API 与离线测试 | -| L4 第一批迁移 | VERIFIED | L2 | 第一批工具从 Agent、MCP 和 HTTP 工具管理目录移除,能力统一由 API 网关提供 | -| L5 第二批迁移 | VERIFIED | L3 | parity 工具迁移完成,确认、脱敏、幂等和恢复语义进入统一策略 | -| L6 原生工具收敛 | VERIFIED | L4,L5 | Agent 任务和人格工具合并为 action 工具;宿主/会话/渠道原生能力边界固定 | -| L7 第三方服务 Skill 化 | VERIFIED | L5 | 下载器和媒体服务器能力发现、受控脚本、Skill、策略和离线测试完成;重复低层 Agent API operation 已删除 | -| L8 收口与交付 | VERIFIED | L6,L7 | 旧代码、文档与架构基线已收口;静态检查和锁定全量测试已完成 | -| L9 全 API 面审计与最终交付 | VERIFIED | L8 | 375 个 OpenAPI 操作逐路由归属、203 个网关合同与 72 个退出工具映射均由测试锁定;全量测试、80% 覆盖率门禁、提交推送和远端 CI 终态均已完成 | -| L10 查询 API 显式分页与数据库下推 | VERIFIED | L9 | 列表端点显式声明分页输入;数据库筛选在 `LIMIT/OFFSET` 前完成并使用同条件精确 `COUNT`;省略新增参数保持旧返回语义;响应 `data` 列表结构不变;外部原生分页未提供总数时不伪造;OpenAPI、MCP、Skill、审计、80% 覆盖率和远端 CI 同步完成 | - -## 5. L2 受控 API 网关约束 - -### 5.1 模型可见合同 - -模型只看到一个结构化工具:moviepilot_api(operation_id, path_params, query, body)。 - -operation_id 来自审核过的操作注册表,模型不能传入任意 host、URL、认证头或 API Token。 - -### 5.2 身份和权限 - -- 从当前 Agent 的 ToolPolicyContext 继承用户、会话、渠道、来源、后台/子 Agent 身份 -- API 网关不能把全局 API_TOKEN 当作当前用户身份 -- 策略按 operation_id 加参数分类,而不是只按工具名 moviepilot_api 分类 -- 读取、敏感读取、可恢复写入、破坏性写入和外部副作用分别处理 -- 保持现有确认、管理员门禁、敏感值脱敏、幂等和恢复模式 - -### 5.3 Skill 合同 - -Skill 负责判断何时使用某个领域能力、提供业务流程和参数示例、声明允许访问的 -allowed-api-operations。宿主必须实际执行该操作范围;只在提示词里显示 allowed-tools 不算权限控制。 - -### 5.4 输出和失败合同 - -- 统一 JSON envelope、分页、字段投影和长度上限 -- 结果进入模型前做敏感字段裁剪和错误归一化 -- 写操作带幂等键或版本前置条件 -- 超时和取消后明确外部状态未知,不能直接报告为已停止 -- API 网关不得复制业务规则;业务 owner 仍是现有 Application/Chain/API 能力 - -第三方服务 Skill 的脚本不属于 API 网关,也不得成为任意网络出口。脚本只消费审核过的 -action,并使用 MoviePilot 已配置的具体服务实例访问其自身 API。 - -## 6. 验收指标 - -### 功能 - -- 媒体搜索、识别、订阅、下载、站点管理、媒体库、插件和工作流任务成功率不低于基线 -- 搜索结果短引用、分页、筛选项和下载任务关联不回退 -- API 网关和 provider Skill 的替代路径保持原业务结果、错误、确认与恢复语义 - -### 安全 - -- 普通用户、频道管理员、系统管理员、后台任务和子 Agent 权限边界保持一致 -- 未经确认不能执行敏感读取、删除、覆盖、安装和外部副作用 -- API Token、Cookie、密码和插件私有数据不进入 prompt、日志或错误摘要 -- 不允许模型通过参数绕过 operation allowlist、host 限制或身份绑定 -- 第三方服务 Skill 不接受连接地址或凭据参数,删除文件和媒体库扫描按外部副作用处理 - -### 运行质量 - -- 主模型工具 Schema 体积显著下降 -- 工具选择误选率、平均调用次数、上下文 token、端到端延迟不劣于基线,优先取得改善 -- API 网关失败可观测、可重试、可对账;取消/超时场景可恢复 -- 所有测试零真实外部网络;相关测试使用边界 mock 或录制回放 - -## 7. 明确排除项 - -- 不把 execute_command 当作 MoviePilot API 网关;仅允许领域 Skill 调用仓库内受控、可审计的固定脚本 -- 不暴露任意 REST path、任意 HTTP method 或任意外部 host -- 不把浏览器、验证码、文件编辑、终端命令和渠道回调强行 API 化 -- MCP、HTTP 工具管理器和主 Agent 全部使用新 API 工具目录,不保留旧业务工具兼容入口 -- 不修改 app/plugins/** 运行时副本 -- 旧工具不作为回滚机制;回滚依赖 Git 版本和提交边界,正式运行时不保留兼容实现 -- 不用第三方直连替代下载提交、订阅、转移、历史和 canonical 媒体存在性等 MoviePilot 高层用例 - -## 8. 阶段更新日志 - -### 2026-08-31:建立计划 - -- 创建 Codex Goal,目标为 Agent 工具体系的 API 化重构和安全收口 -- 基线同步到 v3@6ee47795629f -- 确认 82 个固定工具、48 个第一批迁移候选、16 个 parity 候选和 18 个原生保留工具 -- 当前活动叶子为 L1;尚未修改生产代码 - -### 2026-08-31:L1 基线验证与 L2 启动 - -- 已核对最新 v3 基线、工具工厂、Agent 编排、Skill 中间件、MCP 入口、API Token 鉴权和策略注册表 -- 已确认第一批 48 个 API 候选、第二批 16 个 parity 候选和原生保留边界;后续实测更正默认固定目录终态为 14 个 -- 已冻结 API 网关必须透传 ToolPolicyContext、禁止任意 URL、保留确认/幂等/恢复和结果裁剪的设计约束 -- 当前唯一活动叶子切换为 L2;下一步直接实现正式结构化 API 网关,不增加运行时切换 - -### 2026-08-31:L2.1 网关骨架完成 - -- 新增 app/agent/policy/api.py,冻结第一批 48 个和 parity 16 个 operation ID、固定 API 路由与策略元数据 -- 新增 app/agent/tools/impl/api.py,提供 moviepilot_api(operation_id, path_params, query, body) 结构化合同 -- 工具工厂已切换为统一新目录:默认移除替代业务工具并注入单一网关,不新增运行时开关 -- 主 Agent、子 Agent、MCP 和 HTTP 工具管理器均走同一新目录;不保留旧业务工具对外或内部兼容入口 -- 策略注册表已按 operation ID 分类未知、敏感、可恢复写入、破坏性写入和外部副作用 -- 网关已从旧工具 owner 转发改为 operation ID 到固定 HTTP 方法/路径的 API 执行器,模型不能注入 URL、认证头或方法 -- 已删除临时 app/agent/tools/legacy.py,并移除 MCP 同名工具 first-wins 兼容选择;重复名称由严格目录冲突机制处理 -- Skill operation 范围改为默认拒绝:未加载声明 allowed-api-operations 的 Skill 时不能调用 moviepilot_api -- 敏感系统设置确认入口已迁移到 config.system.get operation,策略回执对 SECRET 结果整体脱敏 -- 当前未完成:补齐 parity API 的真实业务端点、完成渠道用户到 API 用户身份映射、删除 64 个退出工具文件并迁移关联测试/Skill -- 验证:网关、Skill、策略、数据端口与 MCP 专项 63 passed;新增代码 compileall 通过 - -### 2026-08-31:最终目录决策确认 - -- 不增加 `AGENT_API_GATEWAY_ENABLED`、`AGENT_COMPACT_TOOL_CATALOG_ENABLED` 或其它运行时切换 -- 不考虑 MCP 旧工具兼容;MCP、HTTP 工具管理器和 Agent 统一采用新 API 目录 -- 旧业务工具代码的删除是本目标的必达项:先将行为迁移到 API operation handler,再删除对应 `app/agent/tools/impl/*` 文件、注册、测试和文档 -- 网关不得实例化旧工具类;临时 legacy owner 文件已经删除,后续不再恢复 - -### 2026-08-31:L2 完成并进入 L3 - -- Web/WebAgent 数字用户 ID 才允许直接作为 API 身份;外部消息渠道即使用户 ID 为数字,也必须通过渠道绑定解析为有效 MoviePilot 用户 -- API 执行器使用宿主签发的当前用户令牌,并通过模型不可控请求头透传会话、渠道和来源;不读取全局 API_TOKEN -- 系统设置元数据与脱敏、过滤规则校验与引用处理、插件管理投影已迁出 `app/agent/tools/impl`,进入对应 Application owner -- 新增 typed API:统一系统设置、自定义识别词、下载任务高级修改、过滤规则/规则组 CRUD、插件运行能力/持久化数据、斜杠命令执行 -- parity operation 已切换到真实固定路由,不再使用 `system/ruletest`、插件页面或环境变量接口作为占位映射 -- L2 状态更新为 VERIFIED,当前唯一活动叶子为 L3;下一步补齐推荐、目录配置等剩余语义差异并开始删除 64 个退出工具文件 -- 验证:`uv run --locked --no-sync python -m compileall -q app/agent app/application app/api/endpoints app/schemas` 通过;Agent 网关/Skill/策略/数据端口/MCP 专项 `63 passed` - -### 2026-08-31:纳入下载器与媒体服务器第三方 API Skill - -- 对照 database-operation Skill、下载器 provider client、媒体服务器 provider client 和现有 API operation 完成边界评估 -- 决定新增 downloader-operation 与 mediaserver-operation 两个领域 Skill,通过固定脚本读取本机配置并调用已配置第三方服务自身 API -- 两个 Skill 不仅替代既有低层 operation,还提供动态 `capabilities` 清单和 provider 扩展注册表,使 Agent 可按需使用任务文件/Tracker/Peer/队列、播放会话、观看状态、扫描和元数据刷新等高级能力 -- 计划在 Skill 通过离线测试后移除 `download.list`、`download.update`、`download.delete`、`downloaders.list` 和 `library.latest` 等重复低层 Agent operation -- 保留 `download.add`、`library.exists`、下载/转移历史和订阅等 MoviePilot 高层 operation,避免削弱媒体身份、路径映射、持久化和恢复能力 -- 不保留旧工具或 MCP 兼容入口;前端仍需使用的 REST 端点不在删除范围 -- 当前活动叶子仍为 L6 原生工具收敛;L7 已加入 Goal,待 action 工具收口后实施 - -### 2026-08-31:L3 至 L6 完成 - -- 完成 59 个正式 operation:第一批 44 个、parity 15 个;模型不能注入 URL、method、认证头或 Token -- 将过滤规则、系统设置、插件管理/数据、音乐投影、下载任务和命令执行迁入对应 Application owner -- agent_task 合并 create/list/update/run/delete,persona 合并 list/switch/update;旧独立工具实现全部删除 -- 主 Agent、子 Agent、CLI、MCP 和 HTTP 工具管理器统一消费同一严格目录,不再注册旧工具名 -- 删除 77 个 app/agent/tools/impl 退役文件,其中 72 个工具模块和 5 个辅助模块 -- 阶段验证:网关、策略、原生 action、流式活动和关联回归 218 passed - -### 2026-08-31:L7 第三方服务 Skill 化完成 - -- 新增 downloader-operation Skill 与 mp-downloader.py 固定脚本,覆盖 qBittorrent、Transmission、rTorrent 的实例、能力发现、任务/文件/Tracker/Peer、队列、限速、标签、分类、位置和会话统计等动作 -- 新增 mediaserver-operation Skill 与 mp-mediaserver.py 固定脚本,覆盖 Emby、Jellyfin、Plex、ZSpace、Ugreen、TrimeMedia、Navidrome 的实例、媒体库、条目、活动、播放会话、扫描和元数据刷新等动作 -- 两个脚本只读取 MoviePilot 已配置实例,拒绝任意 URL/method/header/凭据参数,统一递归裁剪密码、Token、Cookie、API Key 和会话字段 -- 从 Agent operation 目录删除 download.list、download.update、download.delete、downloaders.list 和 library.latest;保留前端 REST 端点与 MoviePilot 高层 download.add、library.exists、订阅和历史能力 -- 阶段验证:provider Skill、网关移除项和内置 Skill 边界 33 passed - -### 2026-08-31:L8 收口完成 - -- CLI scheduler list/run 已改用 scheduler.list 与 scheduler.run operation,不再调用已删除工具名 -- 工具目录构造、Agent 图、HTTP/MCP direct 调用均强制唯一身份;同名插件/MCP 工具直接失败,不采用 first-wins -- docs/mcp-api.md、docs/cli.md、命令规则、内置 Skill 文档和 Schema 导出清单已同步新合同 -- 宿主架构基线在 L8 首次更新为 917 个模块、7,671 条内部依赖边;L9 新增固定 API MCP 合同与外部服务工具 owner 后为 919 个模块、7,680 条内部依赖边,未新增 Application/Chain 到具体 Adapter 的直连 -- 受影响 Python 文件通过 Ruff、compileall 和 Pylint `--errors-only`;Schema 导出、架构基线与 `git diff --check` 均通过 -- 最终架构/OpenAPI/i18n/事件/package-root 回归 84 passed;Agent/MCP/CLI/Skill/架构大回归 1066 passed、插件与架构边界专项 124 passed、provider/gateway 专项 33 passed -- 完整锁定测试 `uv run --locked --no-sync python tests/run.py` 完成:7554 passed、9 skipped、2 failed -- 两个失败仅为 `test_release_supply_chain.py::test_release_promotes_latest_only_after_both_versioned_images` 与 `test_resource_v3.py::test_v3_release_workflows_use_main_wiki_and_isolated_images`;均已在未改动的 `v3@6ee47795629f` 干净 detached worktree 复现,属于本次重构前已有的发布工作流基线问题 -- Agent 工具重构范围内没有遗留失败;L1-L8 全部退出,正式方案不保留旧工具、MCP 兼容或运行时切换开关 -- 当前变更尚未提交或推送 - -### 2026-08-31:Skill/MCP 自描述合同与系统设置收口 - -- 下载器 24 个 provider action 加 `instances.list` / `capabilities.list` 两个发现 action、媒体服务器 17 个 provider action 加同样两个发现 action、MoviePilot API 203 个 operation 均从运行时合同生成完整英文参数说明;外部 MCP `tools/list` 保留相同 oneOf、必填、类型、默认值、枚举和跨字段约束 -- 新增 admin-only `database_operation` MCP 工具,暴露 `tables`、`schema`、`query`、`write` 四个 action;内置 Agent 仍按需加载 `database-operation` Skill,不增加常驻业务工具 -- 数据库 Skill 已覆盖 27 张 ORM 表、`alembic_version` 和全部字段基线;每张表均说明用途、适合查询的场景和写入边界,运行时仍以 `tables` / `schema` 实际结果为准 -- Skill 文档生成器会对数据库说明与 ORM 元数据做双向缺项校验;新增、删除或改名表但未同步说明时生成直接失败,避免文档再次出现漏表或只有字段没有语义的问题 -- 系统设置继续由 `config.system.get/update` 统一承载,不恢复旧配置工具;Skill 不再复制易漂移的 `Settings` / `SystemConfigKey` 全量清单,而是要求先动态发现 `definition`(声明类型、当前形状、敏感性、允许操作、列表匹配字段和持久化位置),再按精确键更新 -- `Settings` 更新执行类型转换并持久化到 `app.env`;`SystemConfigKey` 通过配置服务写入数据库,保留插件 mutation 门禁、敏感值脱敏和配置变更事件 -- `app/agent/policy/resources/api_mcp_schema.json` 明确为 OpenAPI + 固定 operation 合同生成的 MCP `tools/list` 制品,禁止手工维护;所有 operation、模型和字段必须具备具体英文说明,抽象占位文本或中文说明会由测试拒绝 -- API 权限保持双层硬门禁:`moviepilot_api` 按 operation 的 `required_role` 在发起 HTTP 前拒绝非管理员,最终 FastAPI 端点继续用真实当前用户令牌执行 superuser/manage/user 级鉴权;下载器、媒体服务器、数据库外部工具整体为 admin-only -- 异步边界复核:内置 Skill 脚本经 asyncio subprocess 执行;外部服务/数据库 MCP 工具把同步脚本放入 downloader、mediaserver、db 分域线程池;`moviepilot_api` 使用异步 HTTP,不阻塞 Agent event loop -- 当前阶段进入最终验证:聚焦 Agent/Skill/MCP 回归、全量锁定测试、80% 固定覆盖率 CI、提交推送与远端终态确认 - -### 2026-08-31 至 2026-09-01:L9 全 API 面审计与真实调用验证 - -- 从当前 FastAPI OpenAPI 生成 375 个 HTTP 操作的完整清单;每个路由明确归类为 `gateway`、`provider-skill`、`consolidated`、`alternate-auth-duplicate`、`transport_or_identity`、`stream_or_binary` 或 `ui_presentation`,生成器对任何未归类端点直接失败 -- `moviepilot_api` 扩展为 203 个稳定 operation,除 10 个音乐识别、探索、专辑、艺术家和缓存操作外,继续覆盖仪表盘、媒体发现与识别、站点全生命周期、订阅协作、存储维护、整理队列、工作流、种子缓存、数据库备份、规则与网络测试、插件运行管理、当前用户公开配置和使用统计;音乐支持艺术家到作品、作品到艺术家的双向浏览 -- 新增 `tests/test_agent_api_surface_audit.py`,逐项校验 OpenAPI 清单、固定路由、生成审计文档、MCP `oneOf`、英文字段说明和 Skill operation 章节完全一致 -- 将历史删除提交中的 72 个业务工具冻结为替代映射测试,逐项证明其 owner 是 203-operation API、下载器/媒体服务器 action 或统一 `agent_task` / `persona` 原生工具,并确认旧模块物理文件不存在 -- 实际执行数据库脚本 `tables` / `schema` / `SELECT 1`、下载器与媒体服务器 `instances` / `capabilities`,并直接运行三个结构化 service tool;本机未配置 provider 实例时返回空实例而不是配置读取错误 -- 通过临时本地 HTTP 服务实际执行 `MoviePilotApiTool -> MoviePilotApiExecutor -> GET /api/v1/site/agent` 完整链路,验证返回成功且普通用户投影不包含 cookie、API key、token 或 RSS 等认证字段 -- 新增内置 Agent 私有 `read_skill` 工具,绕过普通工具 64 KiB 结果裁剪并单独限制 `SKILL.md` 主体为 512 KiB;工具同时返回全部辅助文件相对路径,超过上限时明确标记截断,真实内置 Skill 加载测试确认 203 个 operation 均可见且 `truncated=false` -- Skill 生成器同步 YAML `allowed-api-operations` 与正文目录,避免“文档有参数但运行时未授权”的双事实源漂移;MCP schema、Skill front matter、正文和注册表数量及集合完全一致 -- 为站点优先级、插件目录和工作流路径 ID 等原先不精确的输入补充类型模型或端点约束;固定路由占位符与 path schema 名称、required 状态由测试逐项校验 -- 受影响 Agent/Skill/MCP/OpenAPI/音乐/架构回归 247 passed;修复全量发现的工作流管理员门禁、插件分页默认值、模块命名治理、服务工具标签和 Schema 导出清单后,专项回归 40 passed -- 完整锁定测试最终通过:7,614 passed、9 skipped;覆盖率按 CI 相同的 8 分片采集并合并,Application 81.88%、Domain 81.01%,通过固定 80% 门禁 -- 首次推送 `6d4d7331d` 的远端 Pylint 成功;Architecture Contract Gate 在 complexity v2 发现 `plugin.py` 从 999 行增长到 1007 行,合并三个短 import 后恢复到 999 行,本地两套复杂度门禁及 49 项相关回归均通过 -- 第二次推送 `28298c485` 的远端 Pylint 成功;Architecture Contract Gate 识别出两处 Ruff 导入排序债务下降尚未固化,以及新增 service tool/Agent 投影端点未显式处理 mypy 既有基础设施告警 -- 已对新增 Pydantic/ResponseAPIRouter 边界使用精确错误码抑制,补齐 service tool 的 ClassVar 与可选 selector 类型;未通过覆盖基线隐藏任何新增 mypy 错误 -- Ruff 低水位由 576 降至 574,mypy 低水位由 9,606 降至 9,605;mypy、复杂度 v1/v2、并发、异步阻塞、TaskRegistry owner、服务定位和启动性能全部本地通过 -- API/Skill/MCP/provider 专项 86 passed,架构与质量门禁专项 127 passed;生成器重建后工作区合同无额外差异 -- 第三次推送 `e5722ed98` 的远端 Pylint 成功;Unit Tests 的功能分片与覆盖率分片均通过,但 Architecture Contract Gate 在文档合同测试中发现优化清单仍写着旧的 `9,606 / 576` 量化指标 -- 已将 `docs/refactor/optimization-checklist.md` 同步为 `9,605 / 574`,本地 `test_architecture_contract_baseline.py` 与 `test_architecture_baseline_cli.py` 共 45 项通过,并确认 `baseline.py --check-host` 成功 -- 当前活动叶子保持 L9;下一步提交文档指标修复,等待所有远端工作流终态成功后更新为 COMPLETE - -### 2026-08-31:最终提交与远端门禁完成 - -- 文档合同修复提交 `3106984dfc586aa171bbe9bd52307d19c6ef14f3` 已推送到 `origin/v3`,本地 HEAD 与远端一致,工作区无未提交变更 -- Unit Tests 工作流 `33421990027` 已终态成功:Architecture Contract Gate、4 个功能分片、8 个覆盖率分片和 Coverage Report 全部通过 -- Pylint 工作流 `33421989865` 已终态成功;架构质量门禁、Ruff/mypy ratchet、复杂度、异步阻塞、Task owner、服务定位和启动性能检查均成功 -- 全量 Agent API 面审计、Skill/MCP 参数合同、下载器/媒体服务器/数据库真实只读调用、音乐双向浏览能力和固定 80% 覆盖率要求均已完成 -- 本次重构正式完成:旧 Agent 工具代码、旧 MCP 兼容路径和运行时切换开关均不保留;后续新增 API 必须同步注册表、Skill、MCP schema、审计清单和测试 - -### 2026-09-01:L10 查询 API 兼容分页与总数合同 - -- 重新开启父目标并进入 L10;当前基线 `91e33277f` 与 `origin/v3` 对齐,初始工作区干净 -- 查询 API 按结果语义分为完整列表、原生分页或限量列表、结构化分页对象、统计或聚合对象;只有列表结果进入统一数量合同,统计、映射和时序对象不为形式统一而错误分页 -- 原完整列表新增的 `page` / `count` 必须都是可选参数;两者都省略时不切片,继续返回端点原先的完整列表。显式传入任一参数才启用分页,缺失的 `page` 按 1、缺失的 `count` 按 50 解释 -- REST 响应继续保持 `Response.data` 为原列表,禁止改成 `{items,total}` 等对象;总数和分页信息使用响应头及 Agent 网关附加元数据表达,避免破坏外部插件和既有客户端 -- 对完整列表可报告切片前精确总数;对已经由第三方来源原生分页或限量、且上游没有提供总数的结果,只报告当前返回数量,不强制增加总数,也禁止把当前页数量伪装成全局总数 -- 第一版由统一响应路由隐式注入分页参数并在序列化后切片;复核后确认这种实现虽然能生成查询参数,但端点签名不自描述,而且数据库列表仍会全量读取,因此不作为最终方案 -- 当前正式方案由每个列表端点显式声明 `page` / `count`;框架 `Response` 仅用于写响应头,不会出现在 OpenAPI、MCP 或 Skill 输入中。已有第三方原生 `page`、`count`、`limit` 或 `max_results` 的接口保留其显式参数语义;当原生限量参数默认值为 `None` 时,省略所有窗口参数继续返回完整列表 -- 用户、PassKey、活动订阅、Workflow、Site、站点用户数据和站点统计的筛选、稳定排序、`LIMIT/OFFSET` 与精确 `COUNT` 已下推到异步 SQLAlchemy 查询;状态、用户名、名称、触发类型、站点启用状态、站点 ID/域名和日期筛选均在分页前执行,避免空页和错误总数 -- 纯内存、配置、缓存、文件系统或运行时目录仍可在响应边界按显式 `page/count` 切片;第三方原生分页且不返回总数的接口只报告当前页数量,不伪造总数 -- REST 保持 `data` 原列表;`X-Result-Count` 报告本次返回数量,精确可知时增加 `X-Total-Count`,Agent 网关把这些响应头映射到附加 `collection` 对象。下载历史、订阅历史和插件目录已增加本地精确计数;外部媒体、音乐、推荐和搜索来源未提供总数时不输出 `total_count` -- OpenAPI 与 `api_mcp_schema.json` 已重新生成,英文 `skills/moviepilot-api/SKILL.md` 已同步显式参数;生成器会识别插件目录的既有 `max_results`,不会错误声称其默认返回全量 -- 完整锁定测试已通过:4 个分片分别为 `1620 passed, 3 skipped`、`1860 passed, 4 skipped`、`1917 passed`、`2248 passed, 2 skipped`,合计 `7645 passed, 9 skipped` -- 覆盖率按 CI 相同的 8 分片采集并合并,Application `81.72%`、Domain `81.01%`,通过固定 80% 门禁;Ruff/mypy ratchet、严格 mypy、复杂度 v1/v2、并发、异步阻塞、TaskRegistry owner、服务定位、事件策略和启动性能门禁均通过 -- 数据库 Oper 复用 `literal_contains` 新增 4 条内部依赖边,宿主快照从 7,684 更新为 7,688;没有新增 Application/Chain 到具体 Adapter 的直连或跨层依赖 -- 生成器幂等校验通过,`api_mcp_schema.json` 与 `skills/moviepilot-api/SKILL.md` 重建前后哈希一致;受影响 Python 文件 Pylint `10.00/10`,关键回归 `207 passed` - -### 2026-09-01:L10 提交推送与远端门禁完成 - -- 实现提交 `d512f528be351dabba0cf344c2e9d890bb93ea6f` 已推送到 `origin/v3`,本地与远端提交一致 -- Unit Tests 工作流 `33457912749` 已终态成功:Architecture Contract Gate、4 个功能分片、8 个覆盖率分片和 Coverage Report 全部通过 -- Pylint 工作流 `33457912735` 已终态成功;变更文件质量检查与完整建议报告均成功 -- L10 正式完成:集合端点显式声明 `page/count`,数据库查询在 SQL 层完成筛选、稳定排序、分页与同条件精确计数;不传新增参数继续返回旧全量列表,外部来源没有总数时不伪造 -- MoviePilot Agent 工具体系重构父目标至此全部完成,后续查询端点必须继续维护 REST、OpenAPI、MCP、Skill 和数据库查询合同的一致性 - -### 2026-09-01:集合总数截断可见性修复 - -- Web Agent 实际调用 `subscription.list` 时未传分页,完整订阅列表超过通用工具结果 64KB 上限;此前网关把 `collection` 追加在 `data` 之后,截断预览只保留开头条目,导致 Agent 看不到已经由 API 返回的精确 `collection.total_count`,继而错误回退到数据库统计 -- 网关现将 `collection` 放在 `data` 前面,使 `X-Total-Count` 投影的精确总数在大结果预览中仍优先可见;列表响应和 REST `data` 结构均未改变 -- 核心提示、MCP oneOf、`moviepilot-api` Skill 和文档统一要求:数量或摘要查询使用最小分页窗口;兼容分页传 `page=1,count=1` 并读取 `collection.total_count`,不得因为条目或工具预览截断而改查数据库 -- `moviepilot-api` Skill 版本提升到 24;新增超大订阅结果回归,验证截断预览中总数早于 `data` 出现 -- 完整锁定测试已通过:4 个分片分别为 `1620 passed, 3 skipped`、`1860 passed, 4 skipped`、`1917 passed`、`2250 passed, 2 skipped`,合计 `7647 passed, 9 skipped`;变更文件 Pylint 为 `10.00/10`,Ruff、生成物幂等与 `git diff --check` 均通过 -- 当前未托管后端使用的 API 身份与工作区 CLI 配置不一致,真实 CLI 调用被既有 API key 校验拒绝;本轮未擅自重启正在运行的服务,运行进程需在代码部署并重载后再做在线会话复验 - -### 2026-09-01:啰嗦模式 API 主要参数展示修复 - -- 旧领域工具各自通过 `get_tool_message()` 展示目标和主要参数;收敛为 `moviepilot_api` 后,该方法只展示 `operation_id`,导致 Web Agent 与通知渠道在啰嗦模式下失去 `path_params`、`query` 和 `body` 的调用上下文 -- `moviepilot_api` 现按路径、查询、请求体顺序生成最多 320 字符的主要参数摘要;空参数桶不展示,工具执行与 API 输入结构保持不变 -- 参数摘要统一复用宿主递归脱敏器,设置凭据、密码、Cookie、Token 与 API key 等仅显示 `***`,避免为了恢复可观察性而泄露敏感输入 -- 复用脱敏器新增 1 条 Agent 同域内部依赖边,宿主架构快照由 7,688 更新为 7,689;模块总数和跨层边界均未变化 -- 后续将 Skill、子 Agent 和活动记录等中间件统一接入运行时啰嗦模式判断,新增 `app.agent.callback -> app.runtime.settings` 依赖,宿主架构快照由 7,689 更新为 7,690;该方向符合 Agent 到 Runtime 的既定边界 -- 中间件 import 排序修复同时消除了 2 条 Ruff `I001` 历史诊断,低水位由 571 降至 569,并已同步质量指标文档 -- 新增 Web Agent 实际流式回调回归和设置凭据脱敏回归;扩展 Agent/API/流式专项 `487 passed`,完整锁定测试 4 个分片分别为 `1620 passed, 3 skipped`、`1860 passed, 4 skipped`、`1917 passed`、`2253 passed, 2 skipped`,合计 `7650 passed, 9 skipped`;Ruff、Pylint `10.00/10` 和全量 mypy ratchet 均通过 - -本文件作为本次重构的持续记录,保留阶段状态、实际变更、验证结果、提交状态与已知基线边界。 diff --git a/docs/refactor/media-classification-design.md b/docs/refactor/media-classification-design.md deleted file mode 100644 index 363393f79..000000000 --- a/docs/refactor/media-classification-design.md +++ /dev/null @@ -1,1137 +0,0 @@ -# MoviePilot 多媒体、多数据源自动分类体系设计 - -- 状态:V3 首版实现依据 -- 日期:2026-09-02 -- 适用版本:MoviePilot V3 -- 涉及仓库:`MoviePilot`、`MoviePilot-Frontend`,后续插件能力文档涉及 `MoviePilot-Plugins` - -## 1. 背景 - -MoviePilot 当前自动分类能力已经有后端 API 和前端编辑弹窗,但实现本质上仍是 -TheMovieDb 模块内部的专用能力: - -- `app/modules/themoviedb/category.py` 直接读取和写入 `config/category.yaml`。 -- 分类配置只有 `movie`、`tv` 两个固定根节点。 -- 分类条件直接对应 TMDB 原始字段,例如 `genre_ids`、`original_language`、 - `production_countries`、`origin_country`。 -- `app/modules/themoviedb/__init__.py` 在构造 TMDB `MediaInfo` 时直接计算并写入 - `MediaInfo.category`。 -- `app/chain/base.py` 通过模块方法 `media_category`、`load_category_config`、 - `save_category_config` 间接访问 TMDB 分类器。 -- `MoviePilot-Frontend/src/components/dialog/CategoryEditDialog.vue` 写死电影、电视剧、 - TMDB Genre ID、语种和国家选项。 - -这导致以下问题: - -1. 使用豆瓣、Bangumi、AniList、IMDb、TVDB 或插件数据源识别时,无法得到与 TMDB - 等价的自动目录分类。 -2. 音乐虽然已有 MusicBrainz、TheAudioDB、豆瓣音乐以及本地音频标签能力,但没有独立的 - 自动分类策略。 -3. 来源原始字段与分类规则直接绑定,来源接口变化会使用户规则失效。 -4. 分类逻辑在来源模块内部执行,缓存命中、同步/异步识别、插件识别等路径容易出现行为差异。 -5. 当前 `category` 字段存在语义冲突:影视使用它表示媒体库分类,音乐使用它表示 - `Album / Live`、流派等来源元数据;整理历史又统一把该字段写成目录分类。 -6. 分类名称同时充当稳定身份和目录路径,重命名分类会影响目录设置、订阅和历史记录。 -7. 当前 UI 只能编辑固定条件,不能表达组合条件、来源范围、兜底规则、规则解释和发布前预览。 - -本设计将分类能力从 TMDB 模块中拆出,建立一套来源无关、媒体类型可扩展、可解释、可迁移、 -可由前端完整配置的统一分类体系。 - -## 2. 目标 - -### 2.1 功能目标 - -1. 支持电影、电视剧、音乐,后续新增媒体类型时不需要重写规则引擎。 -2. 支持所有注册媒体数据源,包括内置来源和插件来源。 -3. 同一条基于标准字段的规则可以跨来源工作,不要求用户理解来源原始 JSON。 -4. 允许规则按媒体类型、数据源、语言、国家、年份、类型、标签、音乐实体和专辑属性等条件匹配。 -5. 支持 `AND`、`OR`、`NOT`、范围、包含、缺失值等明确的组合语义。 -6. 每个媒体只产生一个主目录分类,同时允许附加多个非目录标签。 -7. 支持多级目录分类,并使用稳定分类 ID,目录名称可以安全重命名。 -8. 前端支持新增、复制、排序、启停、校验、测试、影响预览和回滚。 -9. 分类结果可解释:能够回答“命中了哪条规则”“哪些条件通过或失败”“使用了哪些事实”。 -10. 保留现有 `category.yaml` 用户配置,并提供无损迁移与旧 API 兼容期。 - -### 2.2 工程目标 - -1. 分类规则求值必须是纯函数,不执行网络、文件或数据库 I/O。 -2. 分类器不直接依赖 TMDB、豆瓣、MusicBrainz 等具体模块。 -3. 同步和异步识别路径使用同一个分类结果收口点。 -4. 媒体身份始终保留原始 `media_source + media_id`,分类不得隐式转换主身份。 -5. 识别缓存命中后仍按当前策略重新分类,避免策略更新后返回旧分类。 -6. 整理任务进入持久计划后冻结分类结果和策略版本,运行中的任务不受后续策略修改影响。 -7. 用户配置通过 `SystemConfigKey` 管理,不继续由某个来源模块拥有配置文件。 - -### 2.3 非目标 - -1. 本设计不替代搜索、订阅使用的资源过滤规则和优先级规则。 -2. 默认不为了分类而调用所有外部数据源,避免额外延迟、配额消耗和不稳定性。 -3. 分类器不允许 UI 直接执行任意 Python 表达式、Jinja、JSONPath 或未受控正则。 -4. 来源原始响应不作为长期规则协议;原始字段只能先投影为已登记的分类事实。 - -## 3. 核心设计原则 - -### 3.1 分类晚于来源投影,早于目录选择 - -标准顺序为: - -```text -文件名/身份输入 - | - v -数据源识别或插件识别 - | - v -统一 MediaInfo / MusicInfo 投影 - | - v -构造 ClassificationFacts - | - v -执行自动分类策略 - | - v -应用人工覆盖并冻结有效分类 - | - v -目录选择、下载历史、订阅、整理计划 -``` - -分类不再由任何具体来源在构造媒体对象时顺便完成。 - -### 3.2 多数据源不等于强制转换为 TMDB - -当媒体由豆瓣、Bangumi、AniList、MusicBrainz 或插件来源识别时: - -- 保留该来源的 `media_source + media_id`。 -- 只从该来源已经投影出的标准事实进行分类。 -- 不为得到分类而把身份替换成 TMDB ID。 -- 可选跨源补充只填充缺失事实,不改变主身份,也不覆盖主来源明确提供的值。 - -### 3.3 目录分类与来源分类分离 - -统一采用以下术语: - -| 字段 | 含义 | 示例 | -| --- | --- | --- | -| `metadata_category` | 数据源提供的描述性分类 | `Album / Live`、`Rock` | -| `library_category` | 自动分类或人工覆盖后的媒体类型内相对目录分类 | `现场专辑` | -| `classification_labels` | 不参与目录选择的附加标签 | `华语`、`高解析` | -| `category` | 兼容字段,过渡期只映射到 `library_category` | `现场专辑` | - -音乐来源当前写入 `category` 的专辑类型、流派信息必须迁移到 `metadata_category`。前端音乐详情和卡片 -展示 `metadata_category`;目录、订阅和整理历史只消费 `library_category`。 - -### 3.4 稳定 ID 与可变路径分离 - -分类使用不可变 `category_id` 作为引用,使用可变 `path` 作为目录快照: - -```json -{ - "id": "music.live", - "media_type": "音乐", - "name": "现场专辑", - "path": ["现场专辑"] -} -``` - -`path` 始终相对于媒体类型根目录;是否增加 `电影`、`电视剧`、`音乐` 一级目录仍由目录配置中的 -按类型分类开关决定。分类改名只改变 `name` 或 `path`,目录配置、订阅和规则仍引用 `music.live`。 - -## 4. 总体架构 - -```text - +------------------------------+ - | MoviePilot-Frontend | - | 策略编辑器 / 预览 / 影响分析 | - +---------------+--------------+ - | - v -+---------------------+ +------------+-------------+ -| Media Source Catalog|------>| Classification API | -| 内置来源 + 插件来源 | | policy/fields/preview | -+----------+----------+ +------------+-------------+ - | | - v v -+----------+-------------------------------+-------------+ -| Application: classification | -| 策略读取、版本检查、人工覆盖、结果发布、配置迁移 | -+-----------------------------+--------------------------+ - | - v -+-----------------------------+--------------------------+ -| Domain: classification | -| ClassificationFacts + Rule AST + 纯求值器 + 命中解释 | -+-----------------------------+--------------------------+ - ^ - | -+-----------------------------+--------------------------+ -| MediaInfo / MusicInfo / MetaBase / MetaMusic | -| 来源投影后的标准字段 + 受控扩展事实 | -+----------------------------------------------------------+ -``` - -### 4.1 后端模块归属 - -建议新增同名包 `app/domain/classification/`: - -| 文件 | 责任 | -| --- | --- | -| `model.py` | 分类、策略、条件树、事实、结果和解释的不可变领域模型 | -| `facts.py` | 从标准媒体对象构造来源无关事实,不读取运行时配置 | -| `evaluator.py` | 纯规则求值、顺序控制和命中解释 | -| `validation.py` | 字段、操作符、路径和不可达规则的纯校验 | - -建议新增 `app/application/classification/`: - -| 文件 | 责任 | -| --- | --- | -| `service.py` | 读取当前策略、分类媒体、解析人工覆盖、返回有效结果 | -| `configuration.py` | 策略发布、版本冲突检查、历史快照和 legacy 迁移 | -| `catalog.py` | 合并内置字段目录与插件来源能力声明 | -| `contract.py` | Application 使用的配置端口和公开 DTO | - -建议新增 `app/startup/composition/classification.py`,负责: - -- 使用 `SystemConfigService` 装配策略仓储。 -- 构造唯一的 `MediaClassificationService`。 -- 注入媒体链和 API runtime。 -- 在启动时执行一次 legacy 配置迁移。 - -`app/chain/` 只负责在识别和整理工作流中调用已注入的分类服务,不读取配置文件,不实现条件比较。 - -### 4.2 前端模块归属 - -建议用以下组件替换当前单体 `CategoryEditDialog.vue`: - -| 文件 | 责任 | -| --- | --- | -| `src/components/classification/ClassificationPolicyDialog.vue` | 编辑器外壳、加载、保存和发布 | -| `ClassificationCategoryList.vue` | 分类树、排序、启停和路径编辑 | -| `ClassificationRuleList.vue` | 规则列表、复制、拖拽和摘要 | -| `ClassificationConditionBuilder.vue` | 递归条件组编辑器 | -| `ClassificationRuleInspector.vue` | 当前规则详细配置 | -| `ClassificationPreviewPanel.vue` | 单条测试、事实查看和命中解释 | -| `ClassificationImpactDialog.vue` | 发布前批量影响分析 | -| `src/composables/useMediaClassification.ts` | API、草稿、脏状态和能力目录缓存 | - -## 5. 领域模型 - -### 5.1 分类策略 - -```json -{ - "schema_version": 2, - "revision": 12, - "mode": "first_match", - "categories": [], - "rules": [], - "fallbacks": {}, - "field_aliases": {}, - "updated_at": "2026-09-02T12:00:00+08:00" -} -``` - -字段说明: - -| 字段 | 说明 | -| --- | --- | -| `schema_version` | 配置结构版本,用于后续迁移 | -| `revision` | 每次成功发布递增,用于并发修改检测和任务快照 | -| `mode` | 主分类规则首版固定为 `first_match`,保留后续策略扩展位置 | -| `categories` | 稳定分类定义 | -| `rules` | 全局有序的主分类规则和附加标签规则 | -| `fallbacks` | 每种媒体类型的兜底分类 ID | -| `field_aliases` | 可选值别名,例如音乐流派同义词,不改变字段 ID | - -### 5.2 分类定义 - -```json -{ - "id": "tv.anime.jp", - "media_type": "电视剧", - "name": "日番", - "path": ["动漫", "日番"], - "enabled": true, - "labels": ["动漫", "日本"] -} -``` - -约束: - -- `id` 使用稳定小写标识,允许小写字母、数字、点、下划线和连字符。 -- `path` 使用路径段数组,不接受用户直接输入带 `/` 或 `\\` 的完整路径。 -- 单段禁止空值、`.`、`..`、控制字符和平台非法文件名。 -- 建议最大 4 级、单段 64 字符、总长度 240 字符。 -- 同一媒体类型下目录路径必须唯一。 -- 删除已被目录、订阅或规则引用的分类前必须先解除引用;普通改名不改变 ID。 - -### 5.3 规则定义 - -```json -{ - "id": "rule.tv.anime.jp", - "name": "日本动画", - "kind": "category", - "enabled": true, - "priority": 100, - "media_types": ["电视剧"], - "sources": [], - "when": { - "all": [ - { - "field": "media.genre_keys", - "operator": "contains_any", - "value": ["animation"] - }, - { - "field": "media.countries", - "operator": "contains_any", - "value": ["JP"] - } - ] - }, - "target": { - "category_id": "tv.anime.jp", - "labels": ["动漫"] - } -} -``` - -规则顺序由列表位置决定,`priority` 是服务端保存后的稳定投影。`kind` 分为: - -- `category`:主目录分类规则,必须指定 `category_id`。 -- `label`:附加标签规则,只能输出 `labels`,不得改变主目录分类。 - -首版采用以下确定性语义: - -1. 跳过禁用规则。 -2. 先检查媒体类型。 -3. `sources` 为空时允许所有来源;非空时只允许列出的来源。 -4. 递归求值 `when`。 -5. `category` 规则采用首条命中,确定主分类后忽略后续 `category` 规则。 -6. 所有命中的 `label` 规则累积标签并稳定去重;命中的主分类规则也可附带标签。 -7. 没有主分类规则命中时使用该媒体类型的 `fallbacks`。 - -### 5.4 条件树 - -条件树只有两种节点:条件组和叶子条件。 - -```json -{ - "any": [ - {"field": "media.language", "operator": "in", "value": ["zh", "yue"]}, - { - "all": [ - {"field": "media.countries", "operator": "contains_any", "value": ["CN", "HK", "TW"]}, - {"field": "media.year", "operator": "between", "value": [2000, 2099]} - ] - } - ] -} -``` - -支持的组: - -- `all`:全部子项为真。 -- `any`:任一子项为真。 -- `not`:仅包含一个子项并取反。 - -操作符由字段类型决定: - -| 类型 | 操作符 | -| --- | --- | -| 字符串 | `equals`、`not_equals`、`in`、`not_in`、`contains`、`starts_with`、`ends_with` | -| 数字/年份 | `equals`、`not_equals`、`gt`、`gte`、`lt`、`lte`、`between` | -| 字符串列表 | `contains_any`、`contains_all`、`contains_none` | -| 布尔 | `is_true`、`is_false` | -| 任意字段 | `exists`、`not_exists` | - -缺失值规则必须固定: - -- 除 `not_exists` 外,字段缺失一律判定为不匹配。 -- `not_in`、`not_equals`、`contains_none` 不得把缺失值当成命中。 -- 用户要匹配缺失值时必须显式使用 `not_exists`。 - -这可避免来源字段不足时意外落入排除型规则。 - -### 5.5 分类事实 - -规则不直接读取 `tmdb_info`、`douban_info`、`bangumi_info`、`anilist_info`、 -`MusicInfo.raw_data`。求值输入统一为 `ClassificationFacts`: - -```json -{ - "identity": { - "media_source": "musicbrainz", - "media_id": "..." - }, - "media": { - "type": "音乐", - "title": "示例专辑", - "year": 2024, - "language": null, - "countries": ["JP"], - "genre_keys": ["rock"], - "genre_names": ["Rock"], - "adult": false - }, - "music": { - "entity_type": "album", - "album_type": "Album", - "secondary_types": ["Live"], - "tags": ["j-rock"], - "artists": ["示例艺术家"] - }, - "extensions": {} -} -``` - -建议的标准字段目录: - -| 字段 ID | 类型 | 适用类型 | 说明 | -| --- | --- | --- | --- | -| `identity.media_source` | string | 全部 | 当前主身份来源 | -| `media.type` | enum | 全部 | 电影、电视剧、音乐 | -| `media.year` | integer | 全部 | 标准发行或首播年份 | -| `media.language` | string | 全部 | ISO 639 标准语言代码 | -| `media.countries` | list | 全部 | ISO 3166 国家/地区代码 | -| `media.genre_keys` | list | 全部 | MoviePilot 规范化类型键 | -| `media.genre_names` | list | 全部 | 来源投影后的类型名称 | -| `media.adult` | boolean | 影视 | 成人内容标记 | -| `media.runtime` | integer | 影视 | 分钟 | -| `media.content_rating` | string | 影视 | 内容分级 | -| `media.companies` | list | 影视 | 出品公司或工作室名称 | -| `media.networks` | list | 电视剧 | 电视台或平台名称 | -| `music.entity_type` | enum | 音乐 | recording、album、artist | -| `music.album_type` | string | 音乐 | Album、EP、Single 等 | -| `music.secondary_types` | list | 音乐 | Live、Compilation、Soundtrack 等 | -| `music.genres` | list | 音乐 | 标准化音乐流派名称 | -| `music.tags` | list | 音乐 | 来源标签 | -| `music.artist_country` | string | 音乐 | 艺术家国家/地区 | -| `music.release_status` | string | 音乐 | Official、Bootleg 等 | - -`media.genre_keys` 用于跨来源通用规则,值采用 MoviePilot 稳定词表,例如 -`animation`、`documentary`、`reality`、`talk`、`kids`、`music`。来源投影负责把 TMDB 数字 ID、 -豆瓣类型名、Bangumi 标签、AniList Genre 等转换为这些规范键;无法规范化的值仍保留在 -`genre_names` 或音乐 `genres/tags` 中。 - -在界面中,这些内容统一称为“媒体信息”。“统一风格”只是为了让不同数据源的同一种风格可以使用 -同一条规则匹配,普通用户不需要理解内部键名;搜索并选择媒体后,页面会同时展示来源返回的风格名称 -和用于规则匹配的统一风格。 - -### 5.6 插件扩展事实 - -插件媒体来源可以声明额外分类字段,但必须满足: - -1. 字段只能位于 `extensions..` 命名空间。 -2. 字段描述包括显示名、值类型、允许操作符、适用媒体类型和可选枚举值。 -3. 插件返回的事实必须是 JSON 标量或标量列表。 -4. 插件不能覆盖 `identity.*`、`media.*`、`music.*` 标准字段。 -5. 插件未加载或字段缺失时,相关条件按缺失值语义处理。 - -建议扩展 `MediaSourceInfo`: - -```json -{ - "name": "Example Source", - "media_source": "example.source", - "media_types": ["电影", "电视剧"], - "classification_fields": [ - { - "id": "extensions.example.source.region_group", - "label": "来源地区组", - "value_type": "string", - "operators": ["equals", "in"] - } - ] -} -``` - -插件识别结果通过受控的 `classification_facts` 字段提供对应值。键必须使用完整 -`extensions..` 字段 ID;宿主按结果自身的 `media_source`、媒体类型、当前启用插件注册表和 -字段值类型验证后,转换为分类器内部的来源局部事实。无效字段按稳定诊断码记录并忽略,不得改变 -`media_source + media_id`,也不得导致媒体识别失败。 - -插件通过稳定入口 `app.sdk.classification` 检测 -`MEDIA_SOURCE_CLASSIFICATION_PROTOCOL_VERSION >= 1`,并可从该入口导入 `MediaSourceInfo`、 -`ClassificationFieldDefinition` 和 `ClassificationFactValue`。旧宿主或未声明字段的旧插件保持原有 -`get_media_source()` 与识别返回值;不会隐式获得扩展事实写入权限。插件禁用、停止、卸载、重载或启动失败时, -运行时立即撤销对应运行实例拥有的字段注册,已保存规则保留但重新校验为字段不可用。 - -## 6. 多数据源能力模型 - -### 6.1 来源能力目录 - -后端必须成为字段能力的唯一事实来源,前端不再硬编码 TMDB Genre、国家和语言字段。 - -`GET /api/v1/media/classification/fields` 返回: - -```json -{ - "fields": [ - { - "id": "media.countries", - "label": "国家/地区", - "value_type": "string_list", - "operators": ["contains_any", "contains_all", "contains_none", "exists", "not_exists"], - "media_types": ["电影", "电视剧", "音乐"], - "options": [], - "source_support": { - "themoviedb": "native", - "douban": "derived", - "anilist": "native", - "musicbrainz": "partial" - } - } - ] -} -``` - -支持等级: - -| 值 | 含义 | -| --- | --- | -| `native` | 来源直接提供标准值 | -| `derived` | 可从来源其它字段稳定推导 | -| `partial` | 只有部分对象或详情级响应提供 | -| `extension` | 插件声明的扩展字段 | -| `unavailable` | 来源无法提供 | - -UI 根据用户选择的媒体类型和来源过滤字段,并显示覆盖提示。规则仍允许保存 `partial` 字段,但必须提示 -“字段缺失时本规则不会命中”。 - -### 6.2 可选跨源补充 - -首版默认使用 `primary_only`:只使用当前识别结果和本地解析事实。选择 `enrich_missing` 后, -分类执行服务和影响分析会在需要时补充缺失的标准媒体信息: - -- 只请求当前策略实际引用且当前缺失的标准事实。 -- 使用独立可选模块方法 `get_media_classification_facts`。 -- 通过外部 ID 或明确映射查找同一媒体,不改变主 `media_source + media_id`。 -- 单来源失败隔离,设置总超时、并发上限和 TTL 缓存。 -- 结果记录每个事实的提供来源,预览中可查看。 -- 整理主流程在补充失败时继续使用已有事实,不把分类补充变成整理硬依赖。 - -## 7. 分类结果与覆盖规则 - -### 7.1 分类结果 - -```json -{ - "recommended": { - "category_id": "music.live", - "category_path": ["现场专辑"], - "rule_id": "rule.music.live" - }, - "effective": { - "category_id": "music.live", - "category_path": ["现场专辑"], - "source": "automatic" - }, - "labels": ["现场"], - "policy_revision": 12, - "state": "complete" -} -``` - -`state` 可为: - -- `complete`:使用详情级事实完成求值。 -- `partial`:存在规则所需字段缺失,但仍得到其它规则或兜底结果。 -- `not_evaluated`:列表摘要等路径没有执行分类。 -- `invalid_policy`:策略不可用,回退兼容行为并记录错误。 - -### 7.2 有效分类优先级 - -最终 `effective` 按以下优先级解析: - -1. 当前请求明确指定的 `category_id`。 -2. 订阅、下载历史或人工整理持久化的分类覆盖。 -3. 用户显式选定目录时,该目录绑定的固定分类。 -4. 当前策略自动推荐分类。 -5. 媒体类型默认分类。 - -来源 `metadata_category` 永远不自动升级为 `library_category`。用户确实希望按 `Live`、`Album`、`Rock` -分类时,应通过显式规则引用 `music.secondary_types`、`music.album_type`、`music.genres`。 - -## 8. 执行与生命周期 - -### 8.1 识别结果收口 - -在 `MediaChain` 中新增统一的结果收口方法,例如 `_finalize_recognition_result()`: - -1. 校验并规范化 `media_source + media_id`。 -2. 完成来源到 `MediaInfo` / `MusicInfo` 的标准投影。 -3. 构造 `ClassificationFacts`。 -4. 执行当前策略并写入 `library_category` 和 `classification`。 -5. 保留 `metadata_category`。 -6. 返回最终对象。 - -所有同步、异步、按标题、按 ID、缓存命中、插件识别路径必须经过该方法。分类器本身纯同步,异步路径不 -维护第二份实现。 - -### 8.2 缓存语义 - -- 来源识别缓存只缓存来源投影结果,不把自动分类结果作为长期真值。 -- 每次从识别缓存恢复对象后,使用当前策略重新分类。 -- 如需要分类结果缓存,键必须包含 `policy_revision` 和分类事实摘要。 -- 修改策略无需清空 TMDB、MusicBrainz 等识别缓存。 - -### 8.3 整理任务冻结 - -整理计划创建时写入: - -- `category_id` -- `library_category` 路径快照 -- `classification_rule_id` -- `classification_policy_revision` -- 覆盖来源 `automatic/manual/subscription/directory` - -任务进入 durable transfer 计划后不得重新读取当前分类策略。重启恢复继续使用计划中的快照,避免策略修改 -导致同一个任务目标目录变化。 - -### 8.4 订阅与历史 - -- `Subscribe.media_category` 兼容字段继续保存路径快照。 -- 新增稳定的 `media_category_id`,人工选择时优先保存 ID。 -- 自动分类不需要在订阅创建时永久锁死;没有人工覆盖的订阅在每次取得完整媒体信息时使用当前策略。 -- 下载历史、整理历史和订阅历史保存实际执行时的分类 ID、路径和策略版本。 -- 分类解释可保存在现有结构化 `note` 中,避免为完整条件明细增加大量列;常用查询字段使用正式列。 - -## 9. 配置持久化与版本 - -### 9.1 存储方式 - -新增 `SystemConfigKey.MediaClassificationPolicy`,值为版本化策略状态包: - -```json -{ - "active": {"schema_version": 2, "revision": 12, "categories": [], "rules": []}, - "history": [ - {"schema_version": 2, "revision": 11, "categories": [], "rules": []} - ] -} -``` - -API 常规读取只返回 `active`,历史接口按需读取 `history`。选择该存储方式的理由: - -- 分类策略是管理员可编辑的运行时业务配置。 -- 不需要为分类和规则各建一张高频查询表。 -- 策略可以作为一个整体原子校验、替换和发布。 -- 与现有目录、过滤规则等配置管理方式一致。 - -同一个配置值中保留最近 10 个已发布快照,避免活跃策略和历史分别写入两个 key 造成非原子状态。 - -### 9.2 发布并发控制 - -更新请求必须携带客户端读取到的 `revision`: - -1. Application 定义原子 `compare_and_set(expected_revision, state)` 配置端口。 -2. DB Adapter 在同一事务中锁定对应 SystemConfig 行、重新读取当前 revision 并完成整体替换。 -3. revision 不一致时返回 `409 Conflict`,不覆盖他人修改。 -4. 校验通过后 revision 加一,旧版本进入有界历史。 -5. 提交成功后刷新进程内不可变策略引用;进程锁只用于减少同进程竞争,不能替代数据库原子检查。 - -### 9.3 回滚 - -回滚不是覆盖旧 revision,而是选择历史快照内容并发布为新 revision。这样审计、运行中整理任务和缓存键 -都保持单调版本语义。 - -## 10. API 设计 - -### 10.1 新 API - -| 方法 | 路径 | 权限 | 用途 | -| --- | --- | --- | --- | -| `GET` | `/api/v1/media/classification/policy` | 登录用户 | 读取当前策略和 revision | -| `PUT` | `/api/v1/media/classification/policy` | 超级管理员 | 校验并发布策略 | -| `GET` | `/api/v1/media/classification/fields` | 登录用户 | 获取字段、操作符和来源覆盖 | -| `POST` | `/api/v1/media/classification/validate` | 超级管理员 | 仅校验草稿,不保存 | -| `POST` | `/api/v1/media/classification/preview` | 登录用户 | 对给定媒体或事实执行只读预览 | -| `POST` | `/api/v1/media/classification/impact` | 超级管理员 | 对近期历史样本比较旧/新策略 | -| `GET` | `/api/v1/media/classification/history` | 超级管理员 | 查询可回滚版本 | -| `POST` | `/api/v1/media/classification/rollback/{revision}` | 超级管理员 | 以历史内容发布新版本 | - -字段目录响应同时返回服务端规则限制。枚举型字段的 `options` 使用稳定的 -`{value, label}` 对象,`allow_custom_values` 明确表示前端是否允许目录外输入;前端不得根据字段名硬编码 -输入控件或限制值。预览请求使用带 `kind` 判别字段的输入联合体,当前支持: - -- `{"kind": "media", "media": {...}}`:前端搜索并选择的完整媒体信息。 -- `{"kind": "facts", "facts": {...}}`:保留给旧调用方和集成测试的标准分类信息。 - -发布、回滚和影响分析都携带 `expected_revision`。revision 不一致时返回标准 `Response` 包装的 -`409`,`data` 为 `{expected_revision, current_revision}`;领域校验失败返回标准 `Response` 包装的 -`422`,`data` 为完整 `ClassificationValidationResult`。客户端收到任一响应都必须保留本地草稿。 - -### 10.2 预览响应 - -```json -{ - "facts": {}, - "result": {}, - "trace": [ - { - "rule_id": "rule.music.live", - "matched": true, - "conditions": [ - { - "path": "when.all[0]", - "field": "music.secondary_types", - "operator": "contains_any", - "expected": ["Live"], - "actual": ["Live"], - "matched": true - } - ] - } - ], - "warnings": [ - { - "code": "missing_fact", - "message": "分类事实缺少规则所需字段", - "field": "music.secondary_types", - "source": "musicbrainz" - } - ] -} -``` - -解释信息只在测试、名称识别调试和显式预览中完整返回;普通搜索和整理对象只保留规则 ID、revision 和结果, -避免扩大常规响应。 - -### 10.3 旧 API 兼容 - -兼容期只保留只读投影: - -- `GET /api/v1/media/category` -- `GET /api/v1/media/category/config` - -兼容规则: - -1. `GET /media/category` 从新策略投影为 `{媒体类型: [分类路径]}`。 -2. `GET /media/category/config` 把可表达为 legacy 结构的规则投影回 `movie/tv`;包含音乐或复杂条件时返回 - 新结构标记,并提示旧客户端只读。 -3. 旧 `POST /media/category/config` 在统一规则编辑器上线后移除;所有新写入只允许通过带 - `expected_revision` 的版本化分类策略 API。 -4. `app.modules.themoviedb.CategoryHelper` 仅作为旧插件的只读导入兼容符号保留:读取结果投影自活动策略, - `save()` 始终拒绝写入;官方 V3 插件改用 `app.sdk.classification.classify_media()`。 - -## 11. 前端交互设计 - -### 11.1 信息架构 - -分类策略从“设置 -> 目录”中的“自动分类策略”入口进入,点击后打开全屏、可滚动的编辑窗口;分类编辑器在窗口内工作。 -窗口内按职责分为“分类树、规则、检查与发布”三个工作区标签;最后一个工作区再分为“结果预览、影响分析、 -版本发布与历史”三个标签。移动端保持同样的分层顺序,不把所有表单压缩到同一屏: - -```text -+------------------+------------------+----------------------+ -| 分类树 | 规则 | 检查与发布 | -| 电影 / 电视剧 | 有序规则列表 | 结果预览 | -| 音乐 | 条件和输出 | 影响分析 / 版本历史 | -+------------------+------------------+----------------------+ -``` - -不得继续在每条规则中堆叠固定的 TMDB 表单控件。 - -### 11.2 分类树 - -- 顶部使用电影、电视剧、音乐三个标签页。 -- 分类节点显示名称、目录面包屑、启用状态和规则数量。 -- 支持新增子分类、复制、拖拽排序、重命名和停用。 -- 删除被引用分类时展示引用位置,不允许直接删除。 -- 目录路径逐段编辑,前端和后端同时校验非法字符。 -- 每种媒体类型明确显示兜底分类,且只能有一个。 - -### 11.3 规则列表 - -- 卡片保持紧凑,展示规则名、来源范围、条件摘要、目标分类和启用状态。 -- 拖拽改变首条命中顺序。 -- 支持复制规则,复制后生成新稳定 ID。 -- 支持只看错误、只看当前来源不可用字段、搜索规则名。 -- 兜底分类在规则列表外单独展示;普通规则不允许使用空条件伪装 catch-all。 - -### 11.4 条件编辑器 - -每个条件行使用: - -1. 字段选择器。 -2. 操作符选择器。 -3. 与字段类型匹配的值控件。 -4. 删除按钮。 - -字段选择器按“通用、影视、音乐、数据源扩展”分组。值控件由字段目录动态决定: - -- 枚举使用可搜索多选框。 -- 国家、语言使用标准代码目录和本地化显示名。 -- 年份使用数字或范围控件。 -- 布尔使用切换项。 -- 开放音乐流派和标签使用可创建选项的多选输入。 - -条件组使用 `全部满足`、`任一满足` 分段控件,并允许嵌套一层。复杂度上限由后端返回,首版建议最大深度 3、 -每条规则最多 30 个叶子条件。 - -### 11.5 来源覆盖提示 - -选择来源范围后: - -- `native/derived` 字段正常显示。 -- `partial` 字段显示提示图标和具体说明。 -- 全部所选来源均 `unavailable` 的字段禁止选择。 -- 已保存规则因插件卸载变为不可用时,不删除规则,显示阻断错误并在发布时要求处理。 - -### 11.6 单条测试 - -页面支持两种测试输入: - -1. 输入关键词搜索并选择一条媒体结果,直接使用结果中的完整媒体信息。 -2. 兼容调用方提交已经构造好的 `ClassificationFacts`;页面不要求用户手工填写来源编号或字段值。 - -测试面板显示: - -- 标准分类事实。 -- 自动推荐分类和最终有效分类。 -- 命中规则。 -- 每个条件的实际值和结果。 -- 缺失字段与来源覆盖警告。 -- 最终目录预览。 - -### 11.7 发布前影响分析 - -点击“发布”前可用草稿策略对最近 N 条下载和成功整理历史执行只读比较: - -- 分类未变化数量。 -- 分类发生变化数量。 -- 从有分类变为兜底或未分类数量。 -- 按媒体类型和数据源分组的变化明细。 -- 不执行文件移动,不修改历史和订阅。 - -变化较大时由用户二次确认,但不使用模糊的“可能有风险”提示,应明确列出影响数量和示例。 - -首期后端使用最近下载历史与成功整理历史合并抽样,按媒体身份去重,最多比较 200 条、返回 50 条 -变化示例。历史表只保存身份、类型、标题和年份,因此服务端会按记录中的来源和编号重新读取完整媒体详情; -单条读取失败会计入“无法获取详情”,不会被误算为没有变化。`scanned/skipped/unresolved/truncated` -和按来源、类型分组统计随响应返回;整个过程不移动文件、不修改订阅或历史,且对详情读取设置固定并发上限。 - -### 11.8 无障碍与移动端 - -- 拖拽排序必须同时提供“上移/下移”菜单操作。 -- 所有图标按钮提供 tooltip 和可访问名称。 -- 移动端使用“分类 -> 规则 -> 条件 -> 预览”的分层导航,不把三栏压缩到同一屏。 -- 错误与警告不能只通过颜色表达。 - -## 12. 示例策略 - -### 12.1 电影动画 - -```json -{ - "kind": "category", - "media_types": ["电影"], - "sources": [], - "when": { - "all": [ - {"field": "media.genre_keys", "operator": "contains_any", "value": ["animation"]} - ] - }, - "target": {"category_id": "movie.animation"} -} -``` - -同一规则可匹配 TMDB 的 Genre ID 16、豆瓣类型名“动画”、Bangumi/AniList 对应类型投影,不在规则里写 -任何来源专用字段。 - -### 12.2 日本动画剧集 - -```json -{ - "kind": "category", - "media_types": ["电视剧"], - "when": { - "all": [ - {"field": "media.genre_keys", "operator": "contains_any", "value": ["animation"]}, - {"field": "media.countries", "operator": "contains_any", "value": ["JP"]} - ] - }, - "target": {"category_id": "tv.anime.jp"} -} -``` - -### 12.3 现场音乐专辑 - -```json -{ - "kind": "category", - "media_types": ["音乐"], - "when": { - "all": [ - {"field": "music.entity_type", "operator": "in", "value": ["album", "recording"]}, - {"field": "music.secondary_types", "operator": "contains_any", "value": ["Live"]} - ] - }, - "target": { - "category_id": "music.live", - "labels": ["现场"] - } -} -``` - -### 12.4 影视原声 - -```json -{ - "kind": "category", - "media_types": ["音乐"], - "when": { - "any": [ - {"field": "music.secondary_types", "operator": "contains_any", "value": ["Soundtrack"]}, - {"field": "music.tags", "operator": "contains_any", "value": ["soundtrack", "ost"]} - ] - }, - "target": {"category_id": "music.soundtrack"} -} -``` - -### 12.5 来源专用扩展条件 - -```json -{ - "kind": "category", - "media_types": ["电影"], - "sources": ["example.source"], - "when": { - "all": [ - { - "field": "extensions.example.source.region_group", - "operator": "equals", - "value": "east_asia" - } - ] - }, - "target": {"category_id": "movie.east_asia"} -} -``` - -## 13. Legacy 迁移方案 - -### 13.1 自动迁移 - -启动时仅在 `MediaClassificationPolicy` 不存在时执行: - -1. 读取现有 `CONFIG_PATH/category.yaml`。 -2. 按 YAML 顺序转换 `movie`、`tv` 分类。 -3. 为每个分类生成稳定 ID;相同名称通过规范化后附加短摘要避免冲突。 -4. `genre_ids` 转换为 `media.genre_keys`;暂时无法映射的 ID 保存为 - `extensions.themoviedb.genre_ids` 兼容条件并发出提示。 -5. 仅当标准事实与 legacy 真值语义可证明一致时,才把字段提升为 `media.*` 条件;否则使用统一的 - `extensions.themoviedb.*` 旧比较视图。该视图保留假值缺失、字符串大写、列表逐元素字符串化、 - `production_countries.iso_3166_1` 和 `release_date/first_air_date` 年份截取语义。 -6. `original_language`、`production_countries`、`origin_country`、`release_year` 等字段若因标准化回退、 - 类型转换或假值处理可能改变旧结果,必须保留为受控 TMDB 扩展事实,不能为了使用标准字段牺牲等价性。 -7. 其它 TMDB 一级字段转换到受控 `extensions.themoviedb.*` 字段;无法登记的字段阻止自动发布,保留 - legacy 运行并提示管理员处理。 -8. 首个空规则分类转换为该媒体类型的全局 `fallbacks`,其后的 legacy 项在旧实现中本就不可达,迁移时保持禁用并向管理员报告。 -9. 新策略为电影、电视剧和音乐保留按媒体类型的通用兜底;数据源只能作为规则的筛选条件,不能单独决定默认分类。 - 尚未配置音乐分类时,追加 `Album`、`Album / Compilation`、`EP`、`Single` 四个常用分类和显式规则。 - `Album / Compilation` 只是展示名称,目录路径保存为 `["Album", "Compilation"]` 两个无空白片段,且精选集规则先于普通专辑规则。 - 下载历史的资源目录识别归类也消费识别结果中的生效分类路径,因此多级路径会原样映射为下载器保存目录;分类结果不可用时才兼容退回音乐主类型。 -10. 保存新策略 revision 1,并保留原 YAML 文件只读备份,不再继续写入。已经存在的旧版 revision 1 - 仅兜底默认策略,只在没有历史且内容与旧默认值完全一致时通过 CAS 升级为 revision 2;任何用户编辑过的策略均保持原样。 -11. 同一字段的正向枚举合并为 `contains_any`,排除枚举合并为 `contains_none`;国家、语言等值数量 - 不应展开为叶子数量。已知和未知 Genre ID 仍分别使用标准字段与受控扩展字段,保持原有 OR 语义。 -12. 迁移后的策略仍需通过完整发布校验;超出真实条件复杂度或引用约束时不写入新策略,保留旧分类 - 只读兼容行为和结构化错误诊断,不得让发布校验异常中断宿主启动。 - -### 13.2 行为兼容 - -- 迁移测试必须证明同一批 TMDB fixture 在旧分类器和新分类器下得到完全相同的目录分类。 -- 旧配置中的 `!值` 转换为明确的排除操作符。 -- 旧逗号字符串转换为数组,后续 API 不再使用逗号编码多值。 -- 旧只读 API 将集合逐值投影为逗号项,每个排除值独立添加 `!`;范围允许展开为等价枚举,不改写原 YAML。 -- 迁移后 TMDB 模块不再实例化 `CategoryHelper`,也不再写 `MediaInfo.category`。 - -### 13.3 字段迁移 - -建议分两个版本完成: - -1. 第一个版本同时写 `library_category` 和兼容 `category`,读侧优先新字段。 -2. 前端、目录、订阅、下载、历史、通知全部迁移后,`category` 只保留序列化兼容属性。 -3. 音乐 `category` 来源数据迁入 `metadata_category`,音乐 UI 不再读取兼容 `category` 展示专辑类型。 -4. 迁移完成后,前端默认隐藏未被现有规则引用的 TMDB 旧字段;仍被旧规则引用的字段会以“旧规则”分组只读显示, - 直到用户把规则改为新的统一字段。 - -## 14. 校验规则 - -服务端发布前必须拒绝: - -- 重复分类 ID、规则 ID、同媒体类型重复路径。 -- 不存在的目标分类或跨媒体类型目标。 -- 空分类名称、非法路径段、目录穿越片段。 -- 未登记字段或字段不支持的操作符。 -- 条件值类型错误、范围起止颠倒、空 `in` 列表。 -- 递归深度、规则数或条件数超过上限。 -- 普通规则条件为空;兜底分类必须通过 `fallbacks` 表达。 -- 同一媒体类型没有可用兜底分类。 -- 插件扩展字段的命名空间与来源不一致。 -- 规则引用已卸载插件且没有兼容处理。 - -服务端应警告但允许发布: - -- 某字段仅被部分来源支持。 -- 两条规则可能有重叠,但顺序可以明确决定结果。 -- 分类路径发生变化,可能影响未来整理目标。 -- 当前策略没有覆盖某个已注册但未启用的数据源。 - -## 15. 测试方案 - -### 15.1 后端单元测试 - -- 条件操作符和缺失值三态语义。 -- `all/any/not` 嵌套、最大深度和短路行为。 -- 首条命中、兜底、标签累积和人工覆盖优先级。 -- 分类 ID、路径和引用完整性校验。 -- 策略 revision 冲突和回滚单调递增。 -- 分类事实构造不修改原始 `MediaInfo` / `MusicInfo`。 - -### 15.2 来源契约测试 - -至少覆盖: - -- TMDB:Genre ID、语言、电影出品国、电视剧原产国、年份。 -- 豆瓣:类型名、国家、年份。 -- Bangumi:标签、平台、工作室、年份。 -- AniList:Genre、国家、格式、年份。 -- IMDb、TVDB:当前统一字段投影可用部分。 -- MusicBrainz:专辑主类型、副类型、流派、标签、发行状态。 -- TheAudioDB:流派、风格、专辑和艺术家信息。 -- 豆瓣音乐:专辑、艺术家、流派和发行信息的可用部分。 -- 一个同时支持影视和音乐的插件来源。 - -来源能力声明必须与实际 fixture 投影一致,避免 UI 显示支持但运行时永远缺失。 - -### 15.3 工作流回归测试 - -- 同步和异步识别得到相同分类。 -- 按标题、按 ID、缓存命中和插件识别都经过分类收口。 -- 音乐 `metadata_category` 不会被当成媒体库分类。 -- 目录按分类 ID 匹配,改名后引用不失效。 -- 多级分类路径安全生成。 -- 订阅人工分类覆盖自动分类。 -- 下载历史和整理历史保存实际分类快照。 -- durable transfer 恢复使用计划中的 revision 和路径,不受策略更新影响。 -- legacy `category.yaml` fixture 迁移前后 TMDB 分类完全一致。 - -### 15.4 前端测试 - -- 字段和操作符完全由后端能力目录驱动。 -- 电影、电视剧、音乐规则的新增、复制、排序、删除和保存。 -- 来源 `partial/unavailable` 提示。 -- 分类被引用时禁止删除。 -- revision 冲突时保留本地草稿并提供重新加载/合并选择。 -- 预览展示实际值、失败条件和最终目录。 -- 预览必须通过搜索选择媒体,不再要求用户手工填写事实;编辑分类弹窗和帮助弹窗在透明主题下保持玻璃表面、边框和遮罩。 -- 桌面和窄屏布局不重叠,键盘可完成排序和编辑。 - -### 15.5 性能目标 - -- 200 条规则、每条平均 6 个条件时,单次纯求值 P95 小于 5ms。 -- 常规识别路径不因分类新增额外网络请求。 -- 影响分析分批执行并限制样本数量,不阻塞 API 事件循环。 -- 策略发布后使用不可变快照替换,不在每次分类时解析 JSON。 - -## 16. 分阶段实施 - -### 阶段 A:语义和领域基础 - -1. 新增 `library_category`、`metadata_category`、`ClassificationFacts` 和 `ClassificationResult`。 -2. 将音乐来源描述性 `category` 迁移到 `metadata_category`。 -3. 实现纯规则模型、求值器和校验器。 -4. 建立标准字段和来源能力目录。 - -### 阶段 B:配置与兼容 - -1. 新增 `MediaClassificationPolicy` SystemConfig。 -2. 实现策略服务、revision、历史和回滚。 -3. 实现 `category.yaml` 自动迁移。 -4. 保留旧 GET API 投影,统一编辑器上线后移除 legacy 客户端写入口。 - -### 阶段 C:识别与整理接入 - -1. 从 TheMovieDb 模块移除分类副作用。 -2. 统一所有识别结果收口。 -3. 迁移目录选择、订阅、下载和历史消费者到分类 ID + 路径快照。 -4. 在 durable transfer 计划中冻结分类结果。 - -### 阶段 D:前端规则编辑器 - -1. 用动态字段编辑器替换 TMDB 专用弹窗。 -2. 支持音乐、来源范围和组合条件。 -3. 增加单条预览、校验解释和影响分析。 -4. 目录设置改为保存分类 ID,展示当前路径。 - -### 阶段 E:插件和可选跨源补充 - -1. 扩展插件媒体来源注册协议和文档。 -2. 支持受控扩展事实。 -3. 根据实际需要增加 `enrich_missing`,默认仍关闭。 -4. 移除旧 YAML 写入口和 TMDB 内部 CategoryHelper 依赖,仅保留旧插件只读导入兼容符号。 - -## 17. 验收标准 - -以下条件全部满足才可认为新体系完成: - -1. 电影、电视剧、音乐都能在同一 UI 中配置自动分类。 -2. TMDB、豆瓣、Bangumi、AniList、MusicBrainz、TheAudioDB、豆瓣音乐以及插件来源至少各有 - 一条真实 fixture 通过分类测试。 -3. 规则引擎和 Application 分类服务不导入具体来源模块。 -4. 同步、异步、缓存和插件识别分类结果一致。 -5. `media_source + media_id` 在分类前后保持不变。 -6. 音乐来源分类不会直接成为目录分类。 -7. 分类改名不破坏目录、订阅和规则引用。 -8. 发布策略前能看到校验结果和近期样本影响。 -9. 运行中的整理任务在策略更新后仍使用原计划目标。 -10. 现有 `category.yaml` 可以自动迁移,TMDB 旧规则行为无回归。 - -### 17.1 落地验收证据 - -| 验收项 | 自动化证据 | 状态 | -| --- | --- | --- | -| 统一 UI 支持电影、电视剧、音乐 | 前端 `AccountSettingClassification`、分类树、规则与条件构建器测试 | 通过 | -| 内置与插件来源 fixture | `test_media_classification_source_contracts.py`;官方 LunaTVSource CMS 分类集成测试 | 通过 | -| 分类层不导入具体来源 | `test_media_classification_architecture.py` 静态扫描 Domain/Application 分类包 | 通过 | -| 同步、异步、缓存、插件结果一致 | `test_media_classification_execution.py`、`test_media_classification_plugin_extensions.py`、LunaTVSource 同异步测试 | 通过 | -| 分类前后身份不变 | 分类事实、执行服务、插件事实与缓存往返测试 | 通过 | -| 音乐来源分类不成为目录分类 | 分类事实、执行服务和官方插件历史快照测试 | 通过 | -| 改名不破坏稳定引用 | 配置服务、目录解析和订阅引用测试 | 通过 | -| 发布前校验与影响分析 | 分类 API 测试;前端发布控制、影响面板和 revision 冲突测试 | 通过 | -| Durable plan 冻结原目标 | `test_media_classification_transfer_checkpoint.py` 的 planned replay 与 provider promotion 测试 | 通过 | -| YAML 自动迁移且旧规则等价 | `test_media_classification_startup.py` 与 `test_media_classification_legacy.py` | 通过 | - -最终验收于 2026-09-03 完成:后端 `8149 passed, 9 skipped`;前端 249 个测试文件、2475 项测试通过;官方插件仓 CI 55 项、V3 580 项、兼容 V2 39 项通过。宿主与插件架构基线、严格 mypy、Ruff/mypy 低水位、事件、并发、异步阻塞、复杂度、schema 导出、Agent API 审计、Alembic 唯一 head 和启动性能门禁全部通过。 - -## 18. 推荐决策摘要 - -| 决策项 | 推荐方案 | -| --- | --- | -| 分类所有权 | 独立 Domain + Application 能力,不属于任何数据源模块 | -| 配置存储 | `SystemConfigKey.MediaClassificationPolicy` 版本化整体存储 | -| 求值模式 | 有序首条命中,一个主分类,可附加多个标签 | -| 多来源规则 | 优先使用标准事实;来源特有字段使用受控命名空间 | -| 音乐字段 | `metadata_category` 与 `library_category` 强制分离 | -| 分类引用 | 稳定 `category_id` + 可变目录路径快照 | -| 前端表单 | 后端字段目录驱动的条件构建器,不硬编码 TMDB 字段 | -| 默认跨源行为 | `primary_only`,可选 `enrich_missing` 后续提供 | -| 可解释性 | 保存 rule ID/revision,预览按条件返回 trace | -| 兼容策略 | 自动迁移 YAML;旧 GET 保留只读投影,旧 POST 在统一编辑器上线后移除 | - - -### 分类配置的字典与命名约定 - -- `domain/classification/vocabulary.py` 拥有离线词表,供事实归一化、字段目录与旧字段展示复用;不访问数据源、配置或数据库。 -- 国家、语言和规范风格使用可搜索的中文名称与稳定值;来源原始字段使用按来源区分的开放候选,不将示例目录当作封闭枚举。 -- 分类名称、分类路径和用途分开表达。规则命中后“归入分类”,全部未命中才使用“默认分类”;两者引用同一份分类定义。 -- 旧迁移中的国家条件仍读取原字段,新增条件使用标准字段。迁移兼容提示不代表错误,也不自动改变既有规则语义。 -- 新迁移只创建实际需要的默认未分类目录。早期生成的 `未分类/通用` 在 UI 标为“备用未分类”,保留 ID、路径、目录引用和历史配置。 - -共享离线词表新增一个纯领域模块,启动导入模块数按实际依赖变化更新:`app.startup.lifecycle` 539 → 540、`app.factory` 551 → 552、`app.main` 553 → 554;保留原有耗时预算、采样和生命周期约束。三次本地采样中位数约 1.2 秒,低于既有预算。 diff --git a/docs/refactor/media-search-design.md b/docs/refactor/media-search-design.md deleted file mode 100644 index 1f8907be4..000000000 --- a/docs/refactor/media-search-design.md +++ /dev/null @@ -1,119 +0,0 @@ -# 音乐与影视搜索流程统一 - -## 目标与边界 - -电影、剧集和音乐共用识别、资源搜索、过滤、匹配与结果发布的编排。 -媒体类型决定元数据解析器、关键词、身份和范围匹配规则,不拥有另一套 -站点请求循环、提前停止条件、缓存、进度和上下文生命周期。 - -资源解析结果是资源自身的证据。目标媒体信息是用于比较和后续交付的目标, -不能先把目标名称、艺术家或 ID 写回资源元数据,再将其当作识别成功。 - -## 改造前调用链审计 - -基线为 `v3@76174b11d6b26793f1471e77e9453f3c7fbb54aa`。 - -| 环节 | 影视 | 音乐 | 判定 | -| --- | --- | --- | --- | -| 媒体身份识别 | `RecognitionChainMixin` 的识别计划与步骤 | 同一计划,原生动作选择音乐来源链 | 已共用主流程;保留来源适配差异 | -| 原生与插件回退 | `MediaRecognitionOwner` + `MediaPluginOwner` | 同一选择状态机,事件和字段不同 | 已共用;不复制状态机 | -| 路径证据 | 文件名、父目录、已有身份 | 指纹、音频标签、文件名及专辑目录 | 必要差异;每级仍复用统一媒体识别入口 | -| 元数据目录搜索 | `MediaSearchOwner.search` | `MediaCatalogOwner.search_music` | 统一上层入口与结果收口,来源模块 ABI 保持兼容 | -| 资源解析 | `MetaInfo(title, subtitle)` | 精确搜索未解析;标题搜索仅构造 `MetaMusic`;RSS 只解析标题 | 应统一至具备媒体类型意图的 `MetaInfo` 工厂 | -| 精确资源搜索 | `SearchMediaOwner` 的准备、请求、解析步骤 | 三个独立音乐搜索循环 | 应移除音乐旁路,使用同一搜索状态机 | -| 停止换词 | 影视收到原始资源即停止,之后才过滤 | 音乐通过字符串匹配即停止,之后才过滤 | 均应基于最终可用的精确结果停止 | -| 资源匹配 | 解析作品名/年份/季集/身份,必要时消歧 | 标题和副标题字符串包含作品名及艺术家 | 应共用匹配阶段;具体规则由媒体类型提供 | -| 上下文生成 | 资源元数据与目标媒体分开 | 用目标信息重新构造资源 `MetaMusic` | 应共用上下文生成,并保留原始证据 | -| 过滤与排序 | 搜索结果 owner 调用规则模块及种子工具 | 音乐重复实现同一规则调用与排序 | 应在同一结果处理 owner 收口 | -| 进度和缓存 | 原始候选及最终替换结果 | 仅统计已经匹配的候选 | 应统一原始召回、过滤结果、最终发布语义 | -| 主动订阅搜索 | 共用 `SearchChain.process` 并处理站点预算事实 | 自行循环 `search_by_title`,未经过同一预算结果处理 | 应复用同一订阅搜索编排 | -| RSS 订阅匹配 | 使用资源元数据、身份和订阅约束 | 单独字符串匹配并回填目标元数据 | 复用相同音乐解析/匹配规则,保留 RSS 不再查询站点的语义 | -| 下载提交 | 共用批量选择、失败冷却和单次提交 | 已与电影共用直接候选下载 | 保留共用提交;禁止人工候选自动下载 | -| 范围完成 | 剧集按缺集/全集覆盖 | 专辑按独立音轨数量和文件清单覆盖 | 必要差异,不将专辑等同于单曲或电影 | - -审计入口:`app/chain/_recognition.py`、`app/chain/media/recognition.py`、 -`app/chain/media/path.py`、`app/domain/metainfo.py`、`app/chain/search/media.py`、 -`app/chain/search/result.py`、`app/chain/_music.py`、`app/chain/subscribe/search.py`、 -`app/chain/torrents.py`、`app/chain/download/selection.py`。 - -## 统一后的资源流程 - -1. 校验来源身份并取得目标媒体快照;不改变显式来源和默认来源约定。 -2. 复制目标,完成适用的附加信息补全,按媒体类型生成有序关键词与范围约束。 -3. 通过现有 provider 层执行站点选择、分页、节流、预算和查询。 -4. 对原始候选计数;通过统一 `MetaInfo` 工厂解析主副标题。 -5. 在同一结果处理阶段应用规则和媒体匹配策略,记录被淘汰原因。 -6. 构造保留资源自身证据的 `Context`,排序、去重。人工候选不绑定目标身份。 -7. 应用调用方提供的候选约束(如订阅当前洗版优先级);有经过完整过滤的精确结果且没有要求多名称搜索时停止,否则继续换词。 -8. 统一发布流式替换/完成事件并缓存最终结果。 - -同步、异步和流式入口只负责不同的 I/O 驱动,不各自决定媒体业务分支。 -音乐 owner 保留关键词和兼容门面,不再拥有完整搜索或结果处理循环。 - -实现由 `app/chain/search/execution.py` 驱动统一状态机,`result.py` 统一处理候选; -`MediaChain.search` / `async_search` 统一元数据目录入口,来源服务只适配既有模块 ABI。 -`app/chain/subscribe/metadata.py` 准备目标,`subscribe/search.py` 统一主动搜索和站点预算处理。 - -## 音乐策略 - -- `MetaMusic.parse_resource` 接受标题与副标题,复用现有 Python/Rust 标题解析, - 补充明确艺术家/专辑字段、曲序结构和版本证据。缺少分隔且语义不明确时不猜测。 -- 音乐资源经 `MetaInfo` 工厂应用与影视相同的用户识别词,实际应用记录保存在 - `apply_words` 中;RSS 重新匹配也使用订阅识别词,不修改共享资源缓存。 - 音频文件的真实后缀仍优先于标题中的格式声明。 -- 副标题的明确版本字段或独立版本标签补充主标题缺少的录音版本;已有标题版本 - 不被副标题覆盖。单曲的所属专辑字段只证明归属,即使没有曲序也不代表整专资源。 -- 展示繁转简前保留名称原文。作品、所属专辑与艺术家别名分开保存;单曲不使用 - 兼容 `names` 中混入的专辑名称。别名仅来自同一实体的来源数据。 -- 完整名称及署名优先;短名称不能仅凭子串命中另一首作品。署名冲突、未知分类、 - 不确定发行版本、不同录音版本或关联专辑只能成为显式请求的人工候选。 -- 单曲所属专辑可以作为人工搜索的后续查询,但不能自动认定包含目标单曲。 -- 元数据源各自排序和去重后公平合并,避免首个来源独占返回条数上限。 -- MusicBrainz 以完整名称的繁简短语组优先,完整查询无结果才启用逐字兜底, - 并按完整名称重排有界候选窗口。2026-09-05 公网查询 - `recording:"晴天" AND artist:"周杰伦"` 返回 10 个录音候选,前三项均为标题 - “晴天”、艺术家“周杰倫”,证明中文完整短语不是零命中。 - 查询字段与短语语义参考 [MusicBrainz 官方检索语法](https://musicbrainz.org/doc/Indexed_Search_Syntax)。 -- 不根据展示文本臆造 MusicBrainz ID,不更改默认 MusicBrainz 或显式单来源行为。 -- 署名清理保留原始分隔信息,不能从非中日韩文字的单词内部剥离艺名,避免把 `Leeway` - 截为 `way`、把 `Surprise` 截为 `Surp`。中日韩连写署名及“的/之”形式保持兼容。 - MusicBrainz 确认先采用完整名称,去署名结果只作回退;不符合艺术家、版本约束的 - 完整名称不能抢占有效回退。旧名称确认规则的派生缓存升级后重新建立。 -- MusicBrainz 的候选确认与资源匹配复用繁简、变音符、可信别名、完整署名和录音版本规则。 - 单曲不凭首词、包含关系或任意括号剥离认定同一作品;已返回的同一 ISRC 优先于名称打分。 - 专辑的 `secondary_types` 可提供整专版本证据,但不能反向覆盖其中单曲的录音版本。 -- TheAudioDB 与豆瓣音乐也复用名称、署名和版本规则;TheAudioDB 的 `strTrackAlternate` - 投影为曲名别名,不能混入专辑名称。专辑查询及父专辑确认优先使用 `album_artist`, - 单曲仍按自己的表演者确认,不因合辑署名不同而跳过其曲目。 - 备选来源保留既有发行版名称约束,不因录音版本后缀归一化而将豪华版合并成普通版。 -- 录音版本双方各自提供唯一有效年份或日期且冲突时,不确认成同一版本;支持全半角、 - 数字分隔日期及中文日期。普通数字曲名、缺失日期、无效日期与多时间值不被臆断为冲突。 - 已缓存的无显式 ID 名称识别结果也重新检查版本,已核验的相同 ISRC 保持身份优先级。 -- ISRC 优先级仅适用于符合 [ISRC 官方结构](https://isrc.ifpi.org/isrc-standard/isrc-structure) - 的代码,兼容大小写、展示前缀及分隔符,不能把占位文本相等当成身份相等。 - 代码内的年份是分配参考年,不用于推断录制日期。 -- 自动识别在某个检索式返回的候选全部不匹配时继续尝试后续检索式;手动元数据目录 - 浏览仍保留原始候选。Python/Rust 标题解析进入共同包装层时均保留 `[Live]` 等版本证据。 -- 音乐识别缓存按请求实体与来源身份或完整标题、署名、专辑、年份、版本、ISRC 编码, - 不使用可碰撞的分隔符拼接;旧版派生缓存自动重建,负缓存同样不跨实体复用。 - 共享识别成功时,沿用现有模块回填 ABI,同时覆盖已确认实体的负缓存。 - -## 订阅与下载 - -订阅共用搜索入口、取消检查和站点预算事实处理。音乐实体合法性、总曲目数、 -音质洗版优先级和整专完成判定作为领域步骤保留;不额外执行另一套关键词循环。 -RSS 复用解析和匹配策略,但不重新发起站点搜索。 - -人工候选必须有清晰状态,默认自动 API/订阅仍只采用精确结果。候选进入手动下载 -确认时仅提交资源本身;自动批量选择也拒绝待确认项,防止缓存或其他调用路径绕过。 - -## 验证要求 - -- 电影、剧集、音乐参数化验证同一请求顺序、过滤后停止、空结果继续与最终缓存。 -- 同步、异步和 SSE 的结果、范围、来源、候选计数和停止条件一致。 -- 影视 IMDb/别名消歧、季集、用户过滤规则、分页与预算行为不回退。 -- 音乐主副标题、繁简原文、可信别名、名称边界、版本、曲序和专辑范围回归。 -- 音乐主动订阅与 RSS 使用一致的匹配依据,资源元数据不能被目标信息污染。 -- 人工候选、缺少 ID 的部分元数据及旧缓存不能流入自动下载。 -- 架构、复杂度、异步阻塞、类型、增量静态检查与后端全量测试通过。 -- 前端完整测试及桌面/窄屏操作检查通过后,完成最终提交 CI 和现版本发布验收。 diff --git a/docs/refactor/moviepilot-subscription-governance-handoff.md b/docs/refactor/moviepilot-subscription-governance-handoff.md deleted file mode 100644 index acd51c97e..000000000 --- a/docs/refactor/moviepilot-subscription-governance-handoff.md +++ /dev/null @@ -1,875 +0,0 @@ -# MoviePilot 订阅执行治理上游 Handoff - -> 历史交接:本文保留 2026-09-01 的问题证据,不再作为当前实施合同。 -> 其中 `CandidateBatch`、候选恢复账本、订阅下载提交持久账本和 `reconcile_required` -> 已由 2026-09-03 的简化方案废止;当前行为以代码、迁移和回归测试为准。 -> -> 日期:2026-09-01 -> 目标版本:MoviePilot V3 -> 生产参考:MoviePilot V2 `2.15.5` 的一次只读样本 -> 关联路线图:`docs/refactor/moviepilot-subscription-governance-roadmap.md` -> 交接目的:由上游完成订阅执行链路的整体治理,而不是继续修补单个慢点 - -> 仓库接管说明(2026-09-01):本文已纳入 MoviePilot `v3` 仓库。文中 -> `d923037f9` 是原交接环境的参考提交,不存在于当前仓库历史;对应正确性合同必须在 -> `SUB-GOV-001A` 中基于当前源码重新建立和验证,不能将该提交视为当前分支已交付证据。 - -## 1. 结论先行 - -当前订阅问题需要按一个完整执行域治理,不能再把它理解为“给 `match()` 加缓存”或“把循环改成并发”。 - -需要同时解决四类问题: - -1. 日常 RSS/Spider 刷新后,完整候选缓存被全部订阅重复扫描,核心复杂度接近 `O(S * C)`; -2. 每条订阅在确认本轮是否存在相关候选前,就执行新鲜媒体识别和媒体库缺失查询; -3. 24 小时兜底搜索在与日常 Match 共用的长锁中逐订阅等待并访问站点; -4. 日常 Match、手工搜索和兜底搜索缺少统一的 single-flight、下载提交幂等、取消和恢复合同。 - -建议的最终形态是: - -```text -站点候选获取 - -> 本轮候选批次与 retry/pending 账本 - -> 无损候选路由 - -> 受影响订阅准备 - -> 本轮新鲜事实租约 - -> 串行、短临界区、可幂等的下载提交 - -> 独立完成对账 - -兜底搜索 - -> 可恢复搜索队列 - -> 每站点容量预算 - -> 复用同一准备/提交/完成合同 -``` - -生产发布不采用 shadow A/B,也不在生产同时运行旧、新两套 Match: - -- 上线前通过固定数据快照、可重放测试和目标规模压测证明新链路; -- 上线后只有新链路产生计算、下载和状态写入; -- 不保留运行时旧链路双跑或对照开关; -- 回滚以镜像/版本回滚和兼容数据库迁移为基础。 - -## 2. 目标与验收规模 - -这次治理的目标不是小样本快几秒,而是让以下规模成为正常使用场景: - -- `100–200` 条活跃订阅; -- `10–20` 个启用站点; -- 单轮 `1000+` 条 RSS/Spider 候选; -- 兜底搜索开启后,不阻塞日常 RSS/Spider Match; -- 手工刷新、日常 Match 和兜底搜索重叠时,不重复下载、不错误完成; -- PT 站点请求频率和峰值压力不高于治理前基线。 - -最终验收必须同时看: - -- 正确性; -- 用户等待时间; -- 本地计算复杂度; -- TMDB/媒体服务器请求数量; -- PT 站点请求压力; -- 下载幂等; -- 取消、重启和异常恢复。 - -任何只优化其中一项、但把代价转移到其他项的方案都不算完成。 - -## 3. 证据边界 - -### 3.1 生产 V2 样本 - -生产实例运行 MoviePilot V2 `2.15.5`。对 11 轮连续日志做过一次只读统计: - -| 指标 | 观察值 | -| --- | ---: | -| 每轮活跃订阅 | 约 12 条 | -| 每轮候选站点域 | 约 5 个 | -| 每轮候选 | 约 500 条 | -| Match 中位数 | `17.752s` | -| Match 范围 | `11.049–29.200s` | -| 单订阅相邻日志跨度中位数 | 约 `0.4s` | -| 每轮 TMDB 查询日志 | `12–63` 次 | - -该证据只能说明:在订阅数量很少时,日常 Match 已经有明显耗时和波动;它不能直接外推 V3 在 -`100–200` 条订阅下的绝对耗时,也不能替代 V3 的可重放验证。 - -取证期间未修改配置、未触发额外刷新或搜索、未重启服务。 - -### 3.2 V3 源码快照 - -本 handoff 的源码定位基于 2026-09-01 的 V3 快照。上游持续变化不影响本文的业务约束;实际开始集成时, -再将符号和文件位置映射到最新 V3,不需要在设计和实现阶段持续追逐上游提交。 - -当前关键位置: - -| 位置 | 当前职责 | -| --- | --- | -| `app/chain/subscribe/refresh.py::SubscribeRefreshOwner.refresh` | 串联站点刷新和日常 Match | -| `app/chain/torrents.py::TorrentsChain.refresh` | 逐站点获取 RSS/Spider、识别、合并并返回完整缓存 | -| `app/chain/subscribe/match.py::SubscribeMatchOwner._execute_match` | 持锁、预处理完整缓存、逐订阅识别/查缺/全量扫描/下载/完成 | -| `app/chain/subscribe/search.py::SubscribeSearchOwner._execute_search` | 持同一把锁执行新增、手工或兜底搜索 | -| `app/chain/subscribe/search.py::_wait_before_scheduled_search` | R/P 兜底搜索逐订阅随机等待 60–300 秒 | -| `app/chain/subscribe/facade.py::SubscribeChain._rlock` | Match 和 Search 共用的类级锁,超时上限 2 小时 | -| `app/chain/subscribe/completion.py::finish_subscribe_or_not` | 缺集、下载事实和完成状态收敛 | -| `app/chain/subscribe/refresh.py::__refresh_total_episode_before_completion` | 完成前按新鲜季集事实修正总集数 | - -V3 当前已经提供 `TorrentsChain.get_subscribe_cache_candidates()`,可以按媒体身份和季筛选缓存候选, -并对标题兜底候选做显式低置信标记;但日常 `SubscribeMatchOwner._execute_match()` 尚未调用它,仍然逐订阅 -遍历传入的完整候选集合。该能力可以复用,但不能直接把它当成治理已经完成。 - -## 4. 当前执行路径 - -### 4.1 日常 RSS/Spider Match - -```text -Scheduler / 手工刷新 - | - v -SubscribeRefreshOwner.refresh() - | - v -TorrentsChain.refresh() - - 逐站点读取 RSS,或 Spider 每站点最多两页 - - 与本地完整缓存去重 - - 新候选做媒体识别 - - 新候选追加到完整缓存 - - 返回当前站点范围内的完整缓存 - | - v -SubscribeMatchOwner._execute_match() - - 获取 Match/Search 共用类级锁 - - 对完整缓存做候选预处理/失败重识别 - - 读取所有 R/P 订阅 - - 每条订阅重新取得 cache=False 的媒体事实 - - 每条订阅查询媒体库和下载事实 - - 每条订阅扫描完整候选集合 - - 过滤、择优、下载、写进度、判断完成 - - 释放类级锁 -``` - -### 4.2 兜底搜索 - -```text -subscribe_search 定时任务 - | - v -SubscribeSearchOwner._execute_search(state=R/P) - - 获取与 Match 共用的类级锁 - - 逐订阅读取 - - 每条订阅先随机等待 60–300 秒 - - 识别目标和查询缺失 - - 对多个站点发起搜索 - - 过滤、下载、写进度、判断完成 - - 全部完成后释放类级锁 -``` - -如果有 100 条需要兜底搜索的订阅,仅随机等待的期望总时间约为 5 小时;200 条约为 10 小时。 -实际站点请求、超时、解析和下载还会继续增加总时长。在这段时间里,日常 Match 会等待同一把锁。 - -## 5. 问题清单 - -### P0:完成正确性不能被性能优化破坏 - -日常 Match 的媒体识别使用 `cache=False`,并非无意义的缓存遗漏。电视剧在完成前会依据本次新鲜季集事实 -修正 `total_episode` 和 `lack_episode`。 - -典型边界: - -1. 本地订阅仍记录 12 集并且 `lack_episode=0`; -2. TMDB 已将该季扩展为 13 集; -3. 如果使用旧缓存,系统会继续认为 12 集完整并完成/删除订阅; -4. 如果使用新鲜事实,系统应更新为 13 集并恢复 1 集缺口。 - -本地提交 `d923037f9 test(subscribe): guard fresh match metadata` 已固定以下合同: - -- 日常 Match 继续请求 `cache=False`; -- 12 集事实不产生错误变化; -- 13 集新鲜事实会把 `total_episode` 更新为 13、`lack_episode` 更新为 1。 - -因此不能把 `cache=True` 作为治理方案。正确的优化方向是“同一轮只取得一次足够新鲜的事实并安全复用”, -而不是跨轮复用最长可达数十小时的旧详情。 - -### P0:下载提交缺少跨入口统一幂等合同 - -当前 Match 和 Search 依赖同一把全局锁降低重叠概率,但这不是可扩展的幂等设计。锁一旦拆分,以下入口可能 -同时为同一订阅、同一 torrent 或同一剧集范围生成下载: - -- RSS Match; -- Spider Match; -- 新增订阅搜索; -- 24 小时兜底搜索; -- 手工刷新或手工搜索; -- 插件触发的订阅处理。 - -现有下载历史和订阅 `note` 能参与缺失判断,但不能替代“提交瞬间的原子防重”。尤其是下载器已经接受任务、 -本地写回失败时,系统无法安全判断是否应该自动重试。 - -### P1:完整缓存和本轮新增候选没有执行语义上的区分 - -`TorrentsChain.refresh()` 会识别新种子、追加到缓存,再把完整缓存返回给 Match。这样可以保留旧候选重试能力, -但也使历史候选在每轮反复进入预处理和全订阅扫描。 - -不能简单改成“只处理本轮 delta”,因为以下候选仍需要后续重试: - -- 媒体识别临时失败; -- TMDB 或媒体服务器临时失败; -- 下载器暂时不可用; -- 新订阅创建时需要回看仍有效的缓存候选; -- 自定义识别词只能在具体订阅上下文中应用; -- 下载器接受结果不确定,需要对账而不是直接丢弃。 - -因此需要的是:本轮 delta 优先 + 明确的 pending/retry 账本,而不是完整缓存永远重扫,也不是只处理新候选。 - -### P1:核心循环接近 `O(S * C)` - -每条订阅都会遍历按站点分组的全部候选,再执行站点、身份、季、剧集、过滤规则和洗版优先级判断。 - -当 `S=200`、`C=1000` 时,仅第一层组合就是 20 万次;若候选包含复杂识别、标题兜底、自定义词和优先级规则, -成本会继续放大。 - -已有 `get_subscribe_cache_candidates()` 证明媒体身份/类型/季可以做安全的轻量前置过滤,但当前接口仍然是 -“每条订阅重新遍历完整缓存”,并没有建立一次构建、多订阅复用的反向索引。 - -### P1:在候选相关性确认前执行每订阅外部事实查询 - -当前 Match 对每条订阅先执行: - -- 新鲜媒体识别; -- 媒体库缺失查询; -- 下载历史和洗版进度计算; - -之后才扫描候选。即使本轮没有任何候选可能命中该订阅,也会支付主要外部成本。 - -候选门禁可以跳过“本轮资源匹配准备”,但不能跳过“订阅完成对账”。这两个业务目标必须拆开: - -- 资源匹配:由候选到达触发,只处理受影响订阅; -- 完成对账:按独立调度或媒体事实变化触发,不依赖本轮是否有候选。 - -### P1:Match 和兜底搜索共享长锁 - -`SubscribeChain._rlock` 同时保护 Match 和 Search,锁范围包括: - -- TMDB/媒体识别; -- 媒体服务器查询; -- PT 站点搜索; -- 60–300 秒随机等待; -- 种子获取和下载器调用; -- 订阅状态写入。 - -该锁把“防止同一订阅重复提交”扩大成了“整个订阅域一次只能执行一个长任务”。结果是: - -- 慢站点阻塞所有日常 Match; -- 一条订阅异常拖延整批; -- 手工操作无法合理获得优先级; -- 取消只能在少数循环边界生效; -- 直接增加 worker 没有意义,因为全局锁仍然串行。 - -### P1:兜底搜索节流单位错误 - -当前随机等待按“订阅”执行,但真正需要保护的是“站点请求容量”。 - -订阅级等待的问题: - -- 多站点请求仍可能在等待结束后同时突发; -- 订阅多时等待线性累积; -- 等待期间占用业务锁; -- 无法针对单个站点的 `429/403`、登录失效或超时单独冷却; -- 无法在进程重启后恢复公平的处理位置。 - -### P2:取消、停止和恢复粒度不足 - -现有停止检查主要位于订阅或候选循环边界,`time.sleep()` 和正在进行的同步网络调用不可中断。进程重启后, -系统也没有明确的批次游标、候选终态和不确定下载提交状态用于恢复。 - -### P2:用户状态只有任务进度,没有完整业务状态 - -当前可以报告“正在匹配第 N 个订阅”,但缺少稳定的业务状态合同,例如: - -- queued; -- matching; -- waiting_site_budget; -- preparing_download; -- submitted; -- retryable; -- failed; -- cancelling/cancelled; -- reconcile_required; -- completed。 - -因此后台失败、锁等待、下载器不确定状态和订阅完成容易被用户理解成操作成功、卡住或订阅消失。 - -## 6. 必须保持的不变量 - -### 6.1 正确性 - -1. 日常 Match 和完成对账必须获得足够新鲜的季集事实; -2. 12→13 集等更新不能因跨轮缓存而错误完成订阅; -3. 候选索引不能漏掉当前完整逻辑能够命中的资源; -4. 自定义识别词、标题兜底、动态插件规则和身份不确定候选必须保守处理; -5. 候选门禁只能缩小资源匹配范围,不能隐式跳过完成对账; -6. 下载前必须重读订阅、缺集、下载历史和洗版优先级; -7. 失败、取消、停机和锁等待失败不能写为成功或完成; -8. 单条订阅失败不能阻止其他订阅继续,但批次必须暴露聚合失败。 - -### 6.2 站点压力 - -1. RSS/Spider 默认请求频率不得增加; -2. RSS/Spider 不通过站点并发化换性能; -3. 兜底搜索同一站点最多 1 个在途请求,除非未来有明确站点能力证据; -4. 同一站点请求间隔、冷却和重试必须统一受预算控制; -5. `429/403`、登录失效和连续超时必须降低该站点压力; -6. 手工搜索可以提高队列优先级,但不能绕过站点预算。 - -### 6.3 下载与状态 - -1. 同一订阅、同一 torrent、同一季集覆盖在入口重叠时至多提交一次; -2. 下载器已接受但本地写回失败必须进入“不确定/待对账”,不能盲目重试; -3. 提交成功前不得提前更新订阅已下载事实; -4. 完成、删除、通知和历史写入必须由一条 canonical 路径执行; -5. 数据库迁移应优先保持向后兼容,以支持版本级回滚。 - -## 7. 明确否决的方案 - -以下方案不应再次作为主方案提交: - -### 7.1 把日常 Match 改为 `cache=True` - -原因:可能使用旧季集事实错误完成或删除追更订阅。 - -### 7.2 直接回写调用方传入的完整候选缓存 - -原因:可能覆盖未参与本轮处理的站点域、音乐缓存、RSS/Spider 不同缓存文件或其他消费者状态。 - -### 7.3 只处理本轮新增候选 - -原因:会丢失临时识别失败、外部服务失败、新建订阅回看和下载器不确定状态的重试机会。 - -### 7.4 跨刷新周期轮流处理订阅 - -原因:只是把耗时隐藏到多轮,可能错过短生命周期候选,并显著增加用户完成延迟。 - -### 7.5 直接把全部订阅或全部站点开成 2/4/N worker - -原因:会放大 TMDB、媒体服务器、PT 站点和下载器并发,且在没有幂等合同前引入重复下载和完成竞态。 - -### 7.6 只删除全局锁 - -原因:全局锁虽然粒度错误,但当前仍承担部分防重作用;没有订阅级 single-flight 和提交幂等时直接删除会暴露竞态。 - -### 7.7 把逐订阅随机等待改成整批只等待一次 - -原因:可能让大量实例在同一时间集中访问站点,降低总耗时但增加 PT 风控压力。 - -### 7.8 生产 shadow A/B 或旧新双跑 - -原因:生产上线即采用新方案,不在同一轮同时运行旧、新 Match,也不维护两套结果和采集闭环。双跑还会增加 -实现复杂度、观测责任和误触发副作用的风险。 - -正确性对照应在上线前的隔离可重放环境中完成;生产只运行一条新链路。 - -### 7.9 单独上线统计日志或日志聚合器 - -原因:它不解决用户问题,也没有必要先引入一套长期采集系统。实现所需的最小运行指标应直接成为新链路的 -业务结果和运维摘要,而不是另建文本日志聚合器。 - -## 8. 推荐目标架构 - -### 8.1 阶段一:候选获取与批次边界 - -每次 RSS/Spider 刷新产生一个明确的 `CandidateBatch`: - -```text -CandidateBatch - - batch_id - - source: rss | spider | fallback_search | manual_search - - started_at / finished_at - - sites - - fresh_candidates - - retry_candidates - - counters / terminal_status -``` - -要求: - -- RSS/Spider 仍按当前站点策略获取,不增加请求; -- 当前轮新增候选与历史完整缓存分开表达; -- 当前轮优先处理 `fresh_candidates`; -- 只有 retryable、不确定或明确需要回看的候选进入持久 retry/pending 账本; -- 已终态拒绝、已成功提交、已过期候选不再每轮重扫; -- 不要求把每轮所有候选都写入数据库,避免用持久化开销替代扫描开销。 - -建议候选身份至少包含: - -- 稳定 torrent/resource key; -- 来源、站点、首次/最后出现时间; -- 影视/音乐类型; -- 识别出的 `media_source + media_id`; -- 季和集范围; -- 识别置信来源; -- 当前终态和下一次允许重试时间。 - -### 8.2 阶段二:一次构建的无损候选索引 - -每个批次只构建一次索引,不再由每条订阅重新遍历完整候选: - -```text -CandidateIndex - - by_site[site_id] - - by_media[(media_source, media_id)] - - by_media_season[(media_source, media_id, season)] - - unknown_identity[] - - custom_word_fallback[] - - music[] -``` - -路由规则: - -1. 有明确媒体身份的候选,通过 `media_source + media_id` 直接定位受影响订阅; -2. 类型或季明确冲突时可以安全排除; -3. 身份缺失、标题兜底、自定义识别词、动态插件规则进入保守集合; -4. 保守集合只允许缩小到“可能相关”,不能提前执行最终业务匹配; -5. 最终季集、过滤规则、洗版优先级和自定义词仍复用 canonical 逻辑。 - -复杂度目标从 `O(S * C)` 收敛为近似: - -```text -O(C) 构建索引 -+ O(S) 构建订阅路由 -+ O(M) 对真实可能匹配的订阅/候选对执行完整规则 -+ O(F) 处理保守 fallback -``` - -其中 `M` 应远小于 `S * C`,`F` 必须可度量且不能静默吞掉候选。 - -### 8.3 阶段三:资源匹配与完成对账分离 - -资源匹配入口只处理本批次受影响订阅: - -- 没有候选关联的订阅,不执行本轮资源匹配准备; -- 有候选关联的订阅,才取得新鲜媒体事实、媒体库缺失事实并执行完整规则; -- 完成前仍重读当前订阅和缺集事实。 - -完成对账成为独立任务: - -- 订阅元数据更新后; -- 媒体库同步后; -- 下载完成/整理后; -- 低频定时校验; -- 用户显式刷新。 - -完成对账不扫描 PT 候选,但必须继续保护新鲜季集语义。 - -### 8.4 阶段四:本轮新鲜事实租约 - -在同一个批次内,相同事实只获取一次: - -```text -FreshFactKey - - media_source - - media_id - - media_type - - season - - episode_group -``` - -首次读取仍按当前 `cache=False` 语义取得新鲜事实,随后只在本轮内复用。批次结束即释放,不跨刷新周期保留。 - -需要区分: - -- 稳定元数据:标题、年份、别名、图片等; -- 动态季集事实:当前季集数、分集信息; -- 媒体服务器存在事实; -- 订阅业务判断:缺集、洗版优先级、下载历史。 - -可以合并相同媒体的外部事实查询,但不能把不同订阅的范围、过滤规则、洗版模式或最终状态判断直接共用。 - -### 8.5 阶段五:准备与提交分离 - -准备阶段允许纯计算或有界外部读取: - -- 候选完整匹配; -- 新鲜媒体事实; -- 媒体库缺失事实; -- 规则过滤; -- 下载计划。 - -准备阶段不得: - -- 提交下载; -- 标记已下载; -- 完成或删除订阅; -- 发送完成通知。 - -提交阶段保持短临界区: - -1. 按订阅或逻辑媒体目标取得 single-flight/提交锁; -2. 重读订阅当前状态; -3. 重读下载历史、媒体库缺失和洗版优先级; -4. 计算幂等键并检查终态; -5. 提交下载; -6. 根据下载器明确结果写入成功、失败或不确定状态; -7. 更新订阅进度并执行 canonical 完成判断; -8. 释放锁。 - -### 8.6 下载幂等 - -建议幂等身份至少组合: - -```text -subscription logical identity -+ torrent/resource identity -+ target season/episode coverage -+ subscription mode (normal/best-version) -``` - -不要只使用订阅数据库行 ID:多条订阅记录可能指向同一媒体。首轮可以先保证同一记录不重复提交,跨记录去重 -作为后续低优先级能力,但数据模型不要阻止未来扩展。 - -提交终态建议: - -- `planned`; -- `submitting`; -- `accepted`; -- `rejected`; -- `uncertain`; -- `reconciled`。 - -`uncertain` 必须先查询下载器或下载历史对账,不能自动再次提交。 - -### 8.7 兜底搜索队列与站点预算 - -兜底搜索不再在全局锁内逐订阅 `sleep`。建议模型: - -```text -SubscriptionSearchQueue - - 订阅任务与优先级 - - 可取消等待 - - 持久游标/租约 - - 重启恢复 - -SiteBudget - - 每站点最多 1 个在途请求 - - next_allowed_at - - 随机请求间隔 - - 429/403/timeout/login-invalid 冷却 - - 成功后逐步恢复 -``` - -要求: - -- 随机化保留在站点预算层,而不是订阅长循环; -- 不同站点可独立推进,但同一站点默认串行; -- 批次启动仍保留全局抖动,避免大量实例整点突发; -- 手工任务可提高队列优先级,但不能突破站点预算; -- 等待不持有订阅提交锁; -- 取消应能终止尚未发出的站点请求和可取消等待; -- 已发出的同步请求若无法中断,结果返回后必须检查任务租约再决定是否提交。 - -### 8.8 业务状态 - -后端应把批次和订阅任务状态作为业务结果,而不是只输出日志: - -```text -queued -running -waiting_site_budget -matching -preparing -submitting -accepted -retryable -uncertain -failed -cancelling -cancelled -completed -``` - -状态需包含: - -- 当前阶段; -- 已处理/总数; -- 当前订阅或站点; -- 可安全展示的失败原因; -- 是否可取消、可重试或需人工对账; -- 批次聚合结果。 - -前端展示可以后置,但后端状态合同应与执行治理一起设计,避免后续继续依赖日志推断。 - -## 9. 并发策略 - -首轮不要以并发作为主要收益来源。正确顺序是: - -1. 消除 `S * C` 全量扫描; -2. 没有关联候选时跳过资源匹配的外部查询; -3. 同轮合并新鲜媒体事实和媒体服务器事实; -4. 缩短提交锁; -5. 把兜底搜索等待移到站点预算; -6. 仍存在可重复等待热点时,再考虑 2/4 个有界 prepare worker。 - -即使启用 prepare worker: - -- 站点请求仍由 SiteBudget 控制; -- 下载提交仍按订阅/幂等键串行; -- 不共享可变 `Context`; -- worker 输入使用不可变快照或副本; -- worker 失败不能取消其他订阅; -- 系统停止和任务取消必须可传播。 - -## 10. 上线前验证:隔离重放,不做生产 shadow - -### 10.1 一次性输入快照 - -从生产或生产形态实例一次性提取用于测试的输入,不需要常驻日志聚合器: - -- 订阅业务字段快照; -- RSS/Spider `Context` 候选快照; -- 站点选择和过滤规则配置; -- 媒体库存在/缺失返回; -- TMDB 季集事实; -- 下载历史与洗版优先级; -- 预期匹配、下载计划、进度更新和完成集合。 - -不得包含站点 Cookie、token、下载器凭据等运行秘密。是否对标题做隐私脱敏不影响治理设计,但提交给上游的 -公开夹具仍应只保留可复现业务字段。 - -### 10.2 参考结果生成 - -在隔离测试进程中,用当前 canonical 逻辑对固定快照生成参考结果: - -- matched candidate set; -- planned download set; -- missing episode set; -- completion/update/delete set; -- 外部请求计数; -- 候选扫描计数。 - -然后只运行新链路,对相同快照断言结果。该过程属于离线回归验证,不部署双路径,也不允许任何真实下载、 -站点搜索、TMDB 或媒体服务器出站。 - -### 10.3 正确性场景 - -至少覆盖: - -1. RSS/Spider 无新候选; -2. 有新候选但全部不相关; -3. 电影命中; -4. 电视剧单集、连续集、全集、多季包; -5. 12→13 集季集增长; -6. 手工总集数; -7. 普通订阅和全集/分集洗版; -8. 自定义识别词; -9. 标题兜底和候选身份冲突; -10. 不同媒体源身份; -11. 音乐订阅; -12. 媒体库已存在、部分缺失、不可用; -13. TMDB 失败、空结果和慢响应; -14. 下载器接受、拒绝、超时和结果不确定; -15. 手工、日常和兜底入口重叠; -16. 取消、停机和重启恢复; -17. 插件对完成事件或候选诊断的兼容行为。 - -### 10.4 规模矩阵 - -至少验证: - -| 档位 | 订阅 | 站点 | 候选 | -| --- | ---: | ---: | ---: | -| 小 | 20 | 5 | 500 | -| 目标一 | 100 | 10 | 1000 | -| 目标二 | 200 | 20 | 2000 | - -每档都需要分别测试: - -- 无相关候选; -- 1% 订阅受影响; -- 10% 订阅受影响; -- 大量 fallback 候选; -- TMDB/媒体服务器正常、慢和失败。 - -建议初始性能门槛: - -- 无相关候选时,不允许产生逐订阅 TMDB/媒体服务器调用; -- 候选扫描量应从 `S * C` 收敛到候选索引和实际关联集合; -- 目标一的 Match 阶段 p95 不高于 15 秒; -- 目标二的 Match 阶段 p95 不高于 30 秒; -- 无相关候选的 Match 阶段 p95 不高于 5 秒; -- 首个可提交下载计划不应等待整个批次完成; -- 同一输入连续运行不得出现不可解释的线性退化。 - -绝对时限可以由上游基于参考硬件调整,但不能删除结构性门禁,也不能只用小样本证明“已优化”。 - -## 11. 生产发布方式 - -### 11.1 发布前门禁 - -必须同时满足: - -- 新链路全量 focused/集成测试通过; -- 固定快照的匹配、下载计划、缺集和完成集合一致; -- 12→13 集正确性门禁通过; -- 目标规模压测通过; -- 站点请求模型没有增加 RSS/Spider 压力; -- 重叠入口不产生重复下载; -- 取消、重启和下载器不确定状态有可验证结果; -- 数据库迁移可从旧版本升级,并支持旧镜像读取或有明确恢复步骤。 - -### 11.2 上线行为 - -上线后: - -- 只有新治理链路运行; -- 不调用旧 Match 做结果对照; -- 不为对照重复读取 RSS/Spider、TMDB、媒体服务器或下载器; -- 不维护 baseline/shadow/active 三模式; -- 不需要独立日志聚合器。 - -建议首个生产窗口由现有运维日志和新链路自身的批次摘要确认: - -- 批次总耗时和各阶段耗时; -- 候选总数、delta、retry、终态数; -- 受影响订阅数; -- TMDB/媒体服务器请求数; -- 每站点请求数、在途峰值和冷却; -- 下载计划、accepted、rejected、uncertain; -- 完成、失败、取消和恢复数。 - -这些是新链路的运行结果,不是一套额外的文本日志采集产品。 - -### 11.3 回滚条件 - -出现以下任一情况应停止新版本并回滚: - -- 漏匹配或错误匹配; -- 错误完成/删除订阅; -- 重复下载; -- RSS/Spider 请求次数或站点峰值压力增加; -- 同一站点出现连续 `429/403` 且预算未降压; -- 大量 `uncertain` 无法对账; -- 任务取消后仍继续提交下载; -- 数据库状态无法被上一稳定版本读取; -- 目标规模下 Match 持续超过发布门槛并造成任务重叠。 - -### 11.4 回滚方式 - -优先采用: - -1. 停止调度器继续产生新批次; -2. 等待或取消未提交任务; -3. 对 `submitting/uncertain` 任务先与下载器对账; -4. 备份数据库; -5. 回滚到上一稳定镜像/版本; -6. 若有新增表或字段,保持 additive migration,使旧版本可以忽略; -7. 恢复调度前核对活跃订阅、下载任务和未决幂等记录。 - -不建议在生产保留一套旧 Match 代码作为运行时 fallback;这会让两套语义长期漂移。回滚单位应是完整版本。 - -## 12. 推荐实施顺序 - -上游可以拆成多个可审查提交或 PR,但部署到生产的版本应包含一条完整可用的新链路。 - -| 阶段 | 交付内容 | 核心验收 | -| --- | --- | --- | -| 1 | 固定当前正确性合同和可重放夹具 | 12→13、匹配/下载/完成集合可重复 | -| 2 | 引入 CandidateBatch、delta 和最小 retry/pending 账本 | 不丢临时失败或新建订阅回看候选 | -| 3 | 建立一次构建的无损 CandidateIndex | 不再 `S * C` 全量扫描,fallback 不漏匹配 | -| 4 | 分离资源匹配与完成对账 | 无候选不做资源准备,完成正确性保持 | -| 5 | 引入本轮 FreshFactLease 与媒体服务器事实合并 | 外部请求按唯一受影响媒体而不是订阅数增长 | -| 6 | 拆分 prepare/commit,建立 single-flight 和提交幂等 | 重叠入口不重复提交,uncertain 可对账 | -| 7 | 兜底搜索移出长锁,建立队列、游标和 SiteBudget | 不阻塞 Match,可取消、恢复、降压 | -| 8 | 后端业务状态和前端展示 | 用户看到真实进度、失败、取消和未决状态 | -| 9 | 条件性有界 prepare worker | 只有前述治理后仍有热点才启用 | -| 10 | 跨订阅记录的同媒体去重 | 最低优先级,不阻断单记录治理 | - -每个阶段都必须复用一套 canonical 匹配、缺集、洗版和完成规则,不能为了拆 PR 复制业务实现。 - -## 13. 测试与检查建议 - -### 13.1 单元测试 - -- CandidateBatch 状态转换; -- retry/pending TTL 和重试时间; -- CandidateIndex 类型、身份、季和站点路由; -- unknown/custom/title fallback; -- FreshFactKey 隔离 `season` 和 `episode_group`; -- subscription single-flight; -- 幂等键和提交终态; -- SiteBudget 间隔、冷却和恢复; -- 取消状态机; -- 完成对账的 12→13 集语义。 - -### 13.2 集成测试 - -- `refresh -> match -> download -> completion`; -- RSS 与 Spider 各自缓存边界; -- 音乐缓存不被影视候选写回覆盖; -- 日常 Match 与兜底搜索重叠; -- 手工任务插队但不突破 SiteBudget; -- 下载器接受后本地写入失败; -- 进程在 queued/preparing/submitting/uncertain 阶段重启; -- 插件事件与旧公开入口兼容。 - -### 13.3 性能测试应记录 - -- 候选索引构建时间; -- 候选访问/比较次数; -- 受影响订阅数和 fallback 数; -- 每阶段 p50/p95/max; -- TMDB/媒体服务器调用数; -- 每站点请求数、峰值在途和最小间隔; -- 下载提交尝试数和重复抑制数; -- event-loop lag、线程/任务遗留和 shutdown 时间。 - -## 14. 可直接复用的现有基础 - -1. `TorrentsChain.get_subscribe_cache_candidates()` 已实现候选深拷贝、媒体身份、类型和季的轻量筛选; -2. `Context` 已有 `resource_source`、`match_source`、`candidate_recognized`、 - `media_info_is_target` 等候选来源字段; -3. `finish_subscribe_or_not()` 和 `__refresh_total_episode_before_completion()` 已承载 canonical 完成语义; -4. `resolve_subscribe_missing()` 已将“查询缺失但不推进状态”抽成可复用入口; -5. 下载链已有资源 key、失败冷却、下载历史和订阅来源信息,可用于构造幂等与对账合同; -6. Scheduler 已有任务进度回调,可作为批次业务状态的兼容入口; -7. 提交 `d923037f9` 可作为新鲜季集语义测试的移植参考。 - -这些基础仍需要整合,不能因为局部 helper 已存在就认为执行治理已完成。 - -## 15. 上游交付完成定义 - -上游完成交付时,应能提供以下证据: - -1. 新架构说明和状态/所有权边界; -2. 候选 delta、retry/pending 和索引的实现; -3. 资源匹配与完成对账分离; -4. 本轮新鲜事实租约; -5. prepare/commit、single-flight、幂等和 uncertain 对账; -6. 兜底搜索队列、游标和每站点预算; -7. 目标规模的可重放性能结果; -8. 正确性、站点压力、重复下载、取消和恢复测试; -9. 单一新链路的生产发布和版本级回滚说明; -10. 后端业务状态,及后续前端展示的稳定契约。 - -如果最终实现仍然满足以下任一情况,则治理尚未完成: - -- 每条订阅扫描全部候选; -- 无相关候选仍逐订阅调用 TMDB/媒体服务器; -- 兜底搜索等待仍持有 Match/提交锁; -- 依赖全局锁防止重复下载; -- 下载器不确定状态会自动重试; -- 100–200 条订阅只能靠跨轮分片才能运行; -- 生产需要旧、新双跑才能建立信心; -- 用户仍只能从文本日志猜测任务是否成功。 - -## 16. 需要上游优先确认的设计决定 - -请上游在实现前明确以下决定,并把结果固定在代码合同或设计文档中: - -1. CandidateBatch 与 retry/pending 的内存/持久化边界; -2. torrent/resource 稳定身份的 canonical 计算方式; -3. FreshFactLease 的 key 和生命周期; -4. 完成对账的触发来源与最低频率; -5. subscription single-flight 与下载幂等的唯一 owner; -6. 下载器 accepted/uncertain 的可对账接口; -7. SiteBudget 的持久化、冷却和手工任务优先级; -8. 生产回滚时新增状态表/字段的兼容策略; -9. 目标规模压测使用的参考硬件和最终绝对时限; -10. 后端批次/订阅任务状态的 API 契约。 - -这些是实现必须作出的技术决定,不需要通过生产 shadow A/B 才能确定。 diff --git a/docs/refactor/moviepilot-subscription-governance-roadmap.md b/docs/refactor/moviepilot-subscription-governance-roadmap.md deleted file mode 100644 index 08886aa1f..000000000 --- a/docs/refactor/moviepilot-subscription-governance-roadmap.md +++ /dev/null @@ -1,528 +0,0 @@ -# MoviePilot 订阅执行治理 - -> 历史方案:本文件记录 2026-09-01 的上游接管与验证证据,不再作为当前实施合同。 -> 其中 `CandidateBatch`、候选恢复账本、订阅下载提交持久账本和 `reconcile_required` -> 已由 2026-09-03 的简化方案废止;当前行为以代码、迁移和回归测试为准。 -> 当前治理链路的运行日志验收以第 10 节为准。 -> -> 状态:`completed(2026-09-01)` -> 当前叶:`none(全部叶已完成或按条件停止)` -> 迁移来源:`V3-RDY-009A`、`V3-RDY-009A1`、`V3-RDY-009A1B`、 -> `V3-RDY-009A1C`、`V3-RDY-009A1D` -> 适用范围:V3 订阅刷新、匹配、兜底搜索、下载提交与用户状态闭环 - -本 Goal 从 V3 发布准备总 roadmap 中独立出来。总 roadmap 只保留状态和链接,本文件负责订阅域的 -事实、方案、依赖、上线前验证、叶子交付与停止条件。 - -面向上游的完整问题与实施交接见 -`docs/refactor/moviepilot-subscription-governance-handoff.md`。生产发布不采用 shadow A/B 或旧新双跑: -上线前使用固定快照隔离重放和目标规模压测,上线后只运行新治理链路,异常时按完整版本回滚。 - -## 1. 目标 - -在不增加 PT 站点压力、不牺牲订阅完成正确性、不制造重复下载的前提下,解决以下问题: - -1. 日常 `refresh -> match` 在订阅较多时线性变慢,即使还没有进入 24 小时兜底搜索也会影响用户; -2. 24 小时兜底搜索在全局锁内逐订阅等待和访问站点,放大日常 Match 的锁竞争; -3. RSS、爬虫 Match、手工搜索和兜底搜索缺少统一的订阅级 single-flight、提交幂等与取消终态; -4. 用户看不到排队、匹配、搜索、下载提交、失败和取消的业务状态,容易把后台失败理解为操作成功或订阅消失。 - -最终用户价值不是单纯缩短某个函数耗时,而是:日常资源到达后尽快完成匹配,兜底搜索不会阻塞日常 -Match,同一资源不会因链路重叠重复提交,慢站点或 TMDB 故障不会拖死整轮,用户能够看到真实进度和失败。 - -目标常规规模明确为:`100–200` 条活跃订阅、`10–20` 个启用站点、单轮 `1000+` RSS/Spider 候选。 -达到该规模时不得依赖跨刷新周期漏处理候选来缩短表面耗时,最终验收仍以正确性、站点压力和用户完成时间 -共同决定,而不是只满足单个吞吐数字。 - -## 2. 边界 - -本 Goal 只按单媒体服务器场景设计和验收,不扩建多媒体服务器并发、聚合或 provider 分片。 - -本 Goal 不做以下事情: - -- 不把 `match()` 简单改成 `async def`,也不以删除锁作为完成标准; -- 不通过跨刷新周期轮流处理订阅来隐藏耗时,因为这会漏掉短生命周期候选; -- 不把所有订阅或站点请求直接并发化; -- 不把 TMDB 详情长期缓存当成性能修复; -- 不在当前叶处理多条订阅记录指向同一媒体的产品规则; -- 不重构通用 Scheduler、TaskRegistry 或媒体服务器框架。 - -同步实现可以继续位于受控 worker 内,但网络、数据库和文件 I/O 不得阻塞主事件循环。订阅完成、下载事实、 -洗版优先级和缺集口径继续只有一套 canonical 业务实现。 - -## 3. 已确认事实 - -### 3.1 日常路径 - -当前日常流程为: - -```text -Scheduler / 手工刷新 - | - v -TorrentsChain.refresh() - RSS: 每站点读取一次 RSS - Spider: 每站点最多抓取两页 - 对新种子做媒体识别并写入本地缓存 - | - v -SubscribeChain.match() - 获取 search/match 共用类级锁 - 读取所有 R/P 订阅 - 每条订阅重新识别目标媒体 - 每条订阅查询媒体库缺失情况 - 每条订阅扫描本轮返回的完整候选缓存 - 匹配后下载、完成判定和状态写回 - | - v -释放类级锁 -``` - -生产 V2 `2.15.5` 的少量订阅样本显示: - -| 指标 | 中位数 | 解释 | -| --- | ---: | --- | -| 单轮日常 Match | 约 `9.931s` | 不含 24 小时兜底搜索 | -| 单条订阅 | 约 `395ms` | 当前样本订阅数量较少 | -| 媒体识别与媒体库检查 | 约 `357ms` | 是当前单条订阅主要耗时 | -| 扫描约 600 条候选 | 约 `37ms` | 当前样本不是主要热点,但复杂度仍为订阅数乘候选数 | - -这些数据来自生产只读日志,只能证明当前样本的热点分布,不能直接外推大量订阅的收益比例。 - -2026-08-24 对 NAS 上 V2 `2.15.5` 的 11 轮连续生产日志再次做了只读聚合:每轮约 12 条订阅、5 个 -候选域、500 条候选,Match 中位数约 `17.752s`,范围约 `11.049–29.200s`;单订阅相邻日志跨度的 -中位数约 `0.4s`,每轮 TMDB 查询日志为 `12–63` 次。该样本进一步确认日常 Match 已有明显波动, -并且候选重识别会放大外部请求;日志计数只作为 001A 基线,不替代后续同 revision 固定快照隔离重放。 - -### 3.2 调度与站点访问 - -| 来源 | 默认调度 | 单轮站点访问模型 | 与订阅数量关系 | -| --- | --- | --- | --- | -| RSS 刷新 | 默认每 30 分钟 | 每个启用站点读取一次 RSS | 不随订阅数线性增加 | -| Spider 刷新 | 每天随机 32 次 | 每个启用站点最多抓取两页 | 不随订阅数线性增加 | -| 日常 Match | 紧随 RSS/Spider 刷新 | 候选匹配主要在本地;命中后可能获取种子并提交下载 | 计算和 TMDB/媒体库查询随订阅数增加 | -| 新增订阅搜索 | 每 5 分钟检查 | 仅处理新建订阅,调用站点搜索 | 随新增订阅增加 | -| R/P 兜底搜索 | 可配置,默认间隔 24 小时且默认关闭 | 每条订阅搜索多个站点;不同关键词间再等待 1–10 秒 | 随订阅、站点和关键词近似相乘 | - -站点搜索当前会并发访问不同站点,同一站点按页顺序请求。60–300 秒等待位于兜底搜索的逐订阅循环内, -并且整个搜索持有与 Match 共用的类级锁。该等待既是跨用户错峰手段,也是线性累计耗时和锁竞争来源; -不能只删除等待,也不能继续让它占用业务锁。 - -### 3.3 TMDB 缓存语义 - -日常 Match 的 `cache=False` 会同时绕过媒体识别映射缓存和 TMDB HTTP 响应缓存。它不是单纯的性能疏漏: -Match 会在完成前用新鲜季集数据修正总集数。若直接切为最长可达 24/72 小时的普通缓存,可能把仍在更新的 -12/13 集剧集错误判定完成并删除订阅。 - -因此当前未提交候选中的 `cache=True` 不可交付,必须撤销。可优化的是“本轮怎样取得一次可信的新鲜事实”, -而不是“是否长期复用旧详情”。 - -### 3.4 当前结构性问题 - -1. 资源刷新只把新种子追加到缓存,但随后把完整缓存交给 Match;旧候选会在每轮被重复扫描; -2. Match 先为所有订阅执行 TMDB 和媒体库检查,之后才知道本轮候选是否可能命中; -3. 订阅数 `S` 与候选数 `C` 的核心循环近似 `O(S * C)`; -4. 候选获取、资源匹配、完成对账、下载提交和状态写回被包在同一个长锁生命周期; -5. 兜底搜索在锁内执行随机等待和站点网络 I/O; -6. 不同入口缺少统一订阅级 single-flight,提交前也没有明确的跨入口幂等合同; -7. 停止只能在订阅或关键词边界观察,不能可靠中断随机等待和正在执行的同步网络调用。 - -## 4. 不变量 - -任何方案必须同时保持: - -1. RSS 和 Spider 的默认站点请求频率不得增加; -2. 兜底搜索的峰值每站点并发不得高于基线,出现 `429/403` 或超时时应降低压力而不是重试放大; -3. 每轮需要完成判定时必须使用足够新鲜的季集事实,不能因缓存错误完成或删除订阅; -4. 下载前必须重新读取订阅、缺集、下载历史和洗版优先级,准备阶段的快照不能直接作为提交依据; -5. 同一订阅、同一 torrent、同一季集覆盖在入口重叠时至多提交一次; -6. 失败、取消、停机和锁等待失败不能写入成功或完成状态; -7. 一条订阅失败不得阻止批次中其他订阅继续,但最终批次必须暴露聚合失败; -8. 手工刷新可以优先,但不能绕过站点预算和下载幂等; -9. 音乐订阅在缺少独立准备/提交合同前保持串行; -10. 所有优化都必须能独立关闭或回滚到稳定 V3 行为。 - -## 5. 方案评估 - -### 5.1 方案矩阵 - -| 方案 | 预期收益 | 站点压力 | 正确性/维护风险 | 决策 | -| --- | --- | --- | --- | --- | -| 保持全量串行,只增加日志 | 无直接性能收益 | 不变 | 用户问题继续存在 | 仅作为基线 | -| Match 全部改用 `cache=True` | 当前样本可能减少约 0.2–0.35s/订阅 | 不变 | 可能错误完成正在更新的剧集 | 拒绝 | -| 全部订阅直接开 2/4 worker | 可缩短 TMDB/媒体库等待 | Match 本身不直接增加 PT 请求,但可能放大 TMDB、媒体服务器和下载并发 | 线程安全、完成竞态、重复下载 | 不作为首选,只在前置治理后做隔离压测 | -| 候选建立媒体身份/站点/季索引 | 降低 `S * C` 本地扫描 | 不变 | 自定义词和标题兜底不能被粗索引误删 | 建议,先作为无损前置门禁 | -| 仅匹配本轮新增候选 | 显著减少重复扫描 | 不变 | 临时失败、后建订阅和旧候选重试可能丢失 | 不能单独采用,需 pending/replay 合同 | -| 新增候选优先,旧候选按有界重试账本回放 | 降低常态扫描并保留失败恢复 | 不变 | 需要候选身份、终态和 TTL | 推荐目标方案 | -| 先候选门禁,再查 TMDB/媒体库 | 没有相关候选时跳过主要热点 | 不变 | 完成对账不能随候选一起跳过 | 推荐,但必须拆出独立完成对账 | -| 单轮新鲜事实租约 | 同媒体在一轮只请求一次,不跨轮复用旧详情 | 不变 | 同媒体不同 episode group/季必须进入 key | 推荐 | -| 稳定元数据复用 + 轻量季集查询 | 可能替代每条完整详情请求 | 不变 | TMDB 返回合同、地区词和 episode group 需验证 | 优先探针,收益成立再实现 | -| 单媒体服务器单轮请求合并 | 重复目标只查一次媒体库 | 不变 | 不同订阅范围、洗版规则不能共用最终缺集结果 | 可做事实层合并,不缓存业务判定 | -| 同轮有界 2/4 worker 准备 | 隐藏独立 TMDB/媒体库等待 | 不变 | 共享缓存和同步客户端线程安全;提交顺序变化 | 候选门禁后仍慢时再启用 | -| 跨刷新周期分片订阅 | 每轮变快 | 不变 | 候选可能在轮到订阅前过期,延迟不可控 | 拒绝作为默认方案 | -| 按候选分片,同轮处理完 | 可复用候选索引,适合候选远少于订阅 | 不变 | 标题兜底和自定义词需保守回退 | 推荐的内部执行形态 | -| 兜底搜索批次只随机等待一次 | 去掉按订阅线性累计 | 峰值可能集中 | 大量实例在相近时间同时访问站点 | 不能单独采用 | -| 兜底搜索持久游标 + 每站点预算 | 不占长锁,按容量持续推进 | 可明确限制且具备退避 | 需要批次身份、恢复和公平性 | 推荐 | -| RSS/Spider 站点并发刷新 | 缩短站点获取阶段 | 提高瞬时站点压力 | PT 站点承载和风控不确定 | 默认不做 | -| 全事件驱动替代定时刷新 | 长期可减少无效工作 | 取决于上游能力 | 改动大,历史插件/站点无统一事件源 | 远期,不进入首轮 | - -### 5.2 推荐架构 - -推荐把现有长函数拆成三个业务阶段,但不要求一次 PR 完成重构: - -```text -RSS / Spider / 兜底搜索 - | - v -候选账本与本轮 delta - - torrent 身份、来源、站点、首次/最后出现时间 - - pending / matched / rejected / retryable / expired - | - v -无损候选索引与受影响订阅集合 - - 媒体 ID 可确定时直接反向定位 - - 标题兜底、自定义识别词和旧缓存进入保守集合 - | - v -准备阶段(可取消,可选 2/4 有界 worker) - - 本轮新鲜 TMDB 事实租约 - - 单媒体服务器事实查询 - - 规则匹配和下载计划 - | - v -串行提交阶段 - - 重读订阅和下载事实 - - 幂等校验 - - 提交下载 - - 写入进度、优先级和完成状态 -``` - -完成对账必须是独立路径:候选门禁可以决定“本轮是否值得做资源匹配”,但不能决定“订阅是否已经完成”。 -建议将完成对账按较低频率或在媒体库/元数据变化事件后执行;它仍使用新鲜季集事实,但不扫描站点候选。 - -### 5.3 站点压力治理 - -RSS/Spider 保持现有逐站点串行刷新,不通过并发化换取几秒收益。兜底搜索从“订阅级随机休眠”改为“站点级 -容量预算”: - -- 每个站点最多 1 个在途搜索; -- 同一站点请求之间保留随机间隔,具体区间通过离线压测和受控生产运行指标确定,不在设计阶段拍定; -- 批次启动保留全局随机抖动,避免大量 MoviePilot 实例整点同时启动; -- `429/403`、登录失效和连续超时触发站点级冷却,不让其他订阅立即重试同一站点; -- 手工搜索可以提高队列优先级,但不能绕过站点并发和冷却; -- 站点预算由订阅域消费,首轮不扩建成全应用通用限流平台。 - -该设计让等待不再持有 Match/提交锁,同时比“每条订阅固定睡眠”更直接地约束真实外部请求。 - -### 5.4 下载防重与锁边界 - -锁应按事实所有权拆分,而不是按入口拆分: - -1. 候选获取不持有订阅提交锁; -2. TMDB、媒体服务器和站点网络请求不持有提交锁; -3. 同一订阅的准备任务使用 single-flight 合并; -4. 下载提交使用短临界区,并在临界区内重读当前订阅、下载历史、媒体库缺失和洗版优先级; -5. 幂等键至少包含订阅逻辑身份、torrent 身份和计划覆盖的季集范围; -6. 下载器接受但本地写回失败属于“不确定”终态,不能自动重试造成重复任务,必须先对账下载器。 - -同媒体的多条重复订阅需要更高一层的跨记录幂等,保留在最低优先级叶,不阻断单条订阅治理。 - -## 6. 交付 Goal 树 - -| ID | 交付边界 | 依赖 | 状态 | -| --- | --- | --- | --- | -| `SUB-GOV-000` | 固定历史行为和生产日志;保留 `V3-RDY-009A1A` 已交付的批量请求正确性 | 无 | `completed` | -| `SUB-GOV-001A` | 建立日常 Match 正确性和可重放夹具;撤销 `cache=True` 错误候选,只保留经验证不改变语义的失败识别状态回写 | 000 | `completed(2026-09-01)` | -| `SUB-GOV-001B` | 区分完整缓存与本轮 delta,建立无损候选索引;先证明命中集合与基线完全一致 | 001A | `completed(2026-09-01)` | -| `SUB-GOV-001C` | 将资源匹配与完成对账拆开;只有受候选影响的订阅进入匹配,完成对账独立保持新鲜语义 | 001B | `completed(2026-09-01)` | -| `SUB-GOV-001D` | 引入单轮新鲜事实租约,评估稳定元数据复用、轻量季集查询和单媒体服务器事实合并 | 001C | `completed(2026-09-01)` | -| `SUB-GOV-001E` | 若 001B–D 后仍有可重复等待热点,再通过隔离压测评估 2/4 worker 的有界准备;提交保持串行 | 001D | `not_activated(2026-09-01)` | -| `SUB-GOV-002A` | 将 24 小时兜底搜索移出 Match/提交长锁,建立批次、订阅 single-flight、可取消等待和恢复游标 | 001C | `completed(2026-09-01)` | -| `SUB-GOV-002B` | 为兜底搜索建立每站点并发、间隔和错误冷却预算;RSS/Spider 保持基线压力 | 002A | `completed(2026-09-01)` | -| `SUB-GOV-002C` | 联合运行日常 Match、手工搜索和兜底搜索,验证公平性、锁等待、站点压力和失败恢复 | 001D, 002B | `completed(2026-09-01)` | -| `SUB-GOV-003A` | 建立跨入口的订阅级下载幂等、下载器不确定终态和取消补偿 | 001C, 002A | `completed(2026-09-01)` | -| `SUB-GOV-003B` | 后端提供订阅及批次业务状态,前端展示排队、匹配、搜索、提交、完成、失败和取消 | 002A, 003A | `completed(2026-09-01)` | -| `SUB-GOV-004` | 多条订阅记录指向同一媒体时的跨记录季集去重和产品规则 | 003A | `completed(2026-09-01)` | - -全部行为叶已完成;条件叶 `SUB-GOV-001E` 因前置治理已消除可重复的本地等待热点而未激活。001B–D 是 -日常 Match 主线;002A–C 是 24 小时兜底搜索主线;003A/B 和 004 在身份、终态与提交幂等稳定后完成。 - -### 6.1 SUB-GOV-001A 验收证据 - -- 基线:MoviePilot `v3` 的 `f5c73dcab`;原 handoff 提交 `d923037f9` 未进入当前仓库历史; -- 固定输入:`tests/fixtures/subscription_governance_match_replay.json`; -- 重放入口:`tests/test_subscription_governance_replay.py` 从真实 `SubscribeChain.match()` 进入; -- 合同:日常 Match 明确传入 `cache=False`;12 集事实保持完成,12→13 集时写入 - `total_episode=13`、`lack_episode=1` 且不完成订阅; -- 当前 `match()` 没有把失败识别次数写回 RSS/Spider 缓存,因此本叶未引入未经验证的缓存覆盖; -- 验证:上述重放、现有订阅 Chain 与候选筛选共 `118 passed`,固定 JSON、文档链接和 - `git diff --check` 通过。 - -### 6.2 SUB-GOV-001B 验收证据 - -- `CandidateBatch` 同时表达完整缓存、本轮 `fresh_candidates`、预留 `retry_candidates`、来源、站点和批次时间; -- `TorrentsChain.refresh()` 保持原字典 ABI,新增 `refresh_batch()` 供订阅刷新消费,RSS/Spider 请求模型未改变; -- `CandidateIndex` 在一次构建后按媒体身份路由,并保持候选原顺序;未知身份、主识别失败、标题兜底、 - 自定义识别词和无显式标题 ID 的可复核冲突均进入保守集合; -- 严格缓存候选 API 委托新索引并继续返回深拷贝,插件可观察行为保持不变; -- 专项矩阵验证完整缓存与本轮 delta 分离,并验证索引只排除类型、季或显式身份上 canonical 必然拒绝的候选; -- 验证:订阅、候选、音乐缓存和架构测试共 `228 passed`,错误级 Pylint 为 0, - `scripts/architecture/baseline.py --check-host` 与 `git diff --check` 通过。 - -### 6.3 SUB-GOV-001C 验收证据 - -- Match 在媒体识别和媒体库查询前应用候选身份、类型、季、站点域名和站点 ID 门禁;无可能候选时不读取 - TMDB/媒体服务器事实,也不在资源路径隐式完成订阅; -- 新增 `reconcile_subscription_completion()`,复用 canonical 缺失查询与完成路径,不复制完成规则; -- 新增 `check_and_reconcile()`,现有每 6 小时 `subscribe_tmdb` 调度绑定该入口,在同一次 `cache=False` - 新鲜识别中完成元数据更新和独立完成对账;普通 `check()` 兼容入口仍可只更新元数据; -- 固定重放验证明确冲突候选不触发外部事实查询,并验证无候选批次时独立巡检仍将 12→13 更新为 `13/1` - 后进入完成对账; -- 验证:订阅专项 `131 passed`,API/调度器/架构组合 `183 passed`,错误级 Pylint 为 0,Host - 架构基线和 `git diff --check` 通过。 - -### 6.4 SUB-GOV-001D 验收证据 - -- 新增 `FreshFactLease`,键包含媒体来源、媒体 ID、类型、季和 episode group;缺少明确身份时禁止跨订阅复用; -- Match 与独立元数据巡检在单次调用内合并相同媒体的新鲜识别,首次仍明确使用 `cache=False`,不跨刷新周期 - 保留事实,也合并同轮失败结果,避免同一故障立即重复请求; -- 租约存储和交付均使用隔离副本,后续 `MediaInfo.clear()` 不会污染同批次其他订阅;固定回放证明同媒体同季的 - 两条订阅只触发一次识别并收到不同对象; -- 稳定元数据与轻量季集查询本叶不拆分:当前 canonical provider 合同由一次 `recognize_media(cache=False)` - 同时产出身份、别名和季集事实,拆成 provider 特化请求会扩大合同而没有独立收益证据; -- 单媒体服务器的最终缺集判定未跨订阅缓存:订阅范围、历史和洗版优先级属于各记录业务事实,只有后续 - `SUB-GOV-004` 明确重复订阅产品规则后才允许跨记录合并; -- 验证:订阅与候选回放 `118 passed`,事实租约/治理回放/架构组合 `108 passed`,错误级源代码 Pylint 为 0, - Host 架构基线与 `git diff --check` 通过。 - -### 6.5 SUB-GOV-001E 条件评估 - -- 修正 `CandidateIndex` 的末端遍历:路由直接按选中位置读取,不再先枚举完整候选后判断位置集合; -- 受控样本固定为 `200` 条订阅、`20` 个站点、`1000` 条明确身份候选、`100` 个唯一媒体;基线组合量为 - `200000`,新索引实际检查并路由 `2000` 条,单轮事实租约执行 `100` 次加载并命中 `100` 次; -- 该样本证明本地 `S * C` 热点和同媒体重复识别已消除,但没有提供 001B–D 后相互独立的 provider/媒体服务器 - 等待仍主导批次的同 revision 证据;因此不激活 2/4 worker,不引入共享客户端线程安全与提交排序风险; -- 后续若受控生产形态证据再次显示准备等待占主导,可从本条件叶重新开启,但不得绕过 SiteBudget 和串行提交。 - -### 6.6 SUB-GOV-002A 验收证据 - -- 新增持久 `SubscriptionSearchBatch` 与 `SubscriptionSearchTask`,通过 Alembic `c1f4a8d2e6b9` 同时支持 - SQLite/PostgreSQL;批次记录聚合终态,任务记录稳定位置、优先级、attempt、租约和取消标记; -- 可空唯一 `active_key=subscription:` 是订阅级 single-flight owner:手工/定向任务只提高已排队任务优先级, - 不为同一订阅创建第二个活动任务;终态清空 active key,允许后续正常周期重新搜索; -- 配置了正式队列的 Search 路径不再取得 Match 类级锁,也不再执行逐订阅 `60–300s` 随机休眠;单任务异常 - 独立标记 failed 并继续后续订阅,批次最终暴露聚合失败; -- running 任务使用 token fencing 与 UTC 到期租约,进程重启后以相同 task identity、递增 attempt 恢复;停止时 - 可退回 queued,取消会立即终止未发请求任务并在运行任务返回租约边界时收口; -- 新增 `subscribe_search_queue` durable 调度,启动后 10 秒首次恢复、之后每分钟消费,且使用独立消费者锁保持 - 单进程串行;它不创建 24 小时兜底批次,也不阻塞日常 Match; -- 验证:队列/编排/调度专项 `43 passed`,迁移与声明式模型 `35 passed`,复杂度/并发/架构/迁移组合 - `119 passed`,错误级 Pylint 为 0,Alembic 唯一 head 为 `c1f4a8d2e6b9`。 - -### 6.7 SUB-GOV-002B 验收证据 - -- 新增持久 `SubscriptionSiteBudget`,以 `site_id` 唯一租约保证每站点最多一个在途兜底搜索;不同站点使用 - 独立记录,可由 Search provider 的现有 worker 并行推进; -- 每次真实站点页请求后写入 `next_allowed_at`:成功保持 `60–300s` 随机间隔,历史失败按一级一级递减实现 - 渐进恢复;`429/403`、登录失效、超时和通用错误进入有上限的指数冷却; -- 自动 fallback 批次在任务 `available_at` 中持久化 `0–60s` 全局启动抖动;手工任务优先级更高且立即入队, - 但进入 Search provider 后仍使用完全相同的站点租约、间隔和冷却,不能旁路; -- 站点预算只包装订阅队列的同步搜索实例,不改变 RSS/Spider 刷新入口和普通手工资源搜索;索引器列表返回 - 合同、同步/异步结果及既有站点健康统计语义保持不变; -- 等待每秒检查批次取消,超过 5 秒短窗口的冷却或在途站点不阻塞整批:该站点本轮返回空页,其他站点继续; - 部分站点失败只记录告警与冷却,仅在没有站点或其他搜索源成功完成时以聚合失败收口; -- 验证:站点预算/队列/编排/provider 专项 `38 passed`,搜索和索引器兼容 `91 passed`,迁移与声明式模型 - `37 passed, 1 skipped`,架构合同组合 `149 passed`,错误级 Pylint 为 0,Alembic 唯一 head 为 - `d2a7c5e9f1b4`,Host 架构基线与 `git diff --check` 通过。 - -### 6.8 SUB-GOV-002C 验收证据 - -- 队列认领增加 15 分钟持久老化窗口:新手工任务继续优先,但等待超过窗口的 fallback 任务先于后续高优先级 - 新任务执行,防止持续手工流量造成后台饥饿; -- 正式搜索队列继续完全不访问 Match 类级锁;联合测试在同一 provider fan-out 中证明冷却或失败站点独立记录, - 健康站点仍独立完成并释放预算,部分站点失败不改变订阅任务终态; -- 同站点第二个 worker 无法认领,另一站点可同时认领;任务租约与站点租约均验证过期后以新 token 恢复, - 单订阅失败继续后续任务且批次保持聚合 failed; -- 手工入口仅改变队列优先级和启动时机,不改变站点预算、失败冷却或 single-flight;RSS/Spider 仍未接入该预算, - 因此其请求模型保持基线; -- 验证:联合队列/站点预算/搜索状态 `35 passed`,订阅/搜索/调度架构 `58 passed`,错误级 Pylint 为 0, - Host 架构基线与 `git diff --check` 通过。 - -### 6.9 SUB-GOV-003A 验收证据 - -- 新增持久 `SubscriptionDownloadSubmission` 账本和 `SubscriptionDownloadGovernance` 应用合同;幂等键包含 - 订阅 ID、媒体逻辑身份、torrent 稳定身份、精确季集覆盖和普通/洗版模式,唯一约束覆盖 RSS、Spider、 - 手工搜索与兜底搜索的重叠入口; -- 下载器调用前以 fenced attempt token 原子认领;明确拒绝进入带冷却的 retryable,已接受但本地结算失败或 - 下载器超时进入 `reconcile_required`,不得自动重试;成功记录可复用既有 hash,迁移前同订阅同 torrent - 同覆盖的下载历史也参与兼容去重; -- 生产订阅下载路径在提交前重新读取订阅、缺集、下载历史、洗版优先级和当前下载参数;订阅删除、暂停或筛选 - 条件变化时停止提交,准备阶段快照不越过外部副作用边界; -- 取消在下载器调用前写入 cancelled 且不产生外部提交;越过提交边界后的取消按真实下载结果完成并保留说明, - 不伪造 cancelled 终态;任务重启通过持久账本判断是否已经开始; -- Alembic `e1b6d4f8a2c7` 为唯一 head,迁移支持 SQLite/PostgreSQL、可逆且重复执行安全;公开 - `DownloadChain.download_single()` / `batch_download()` 只在末尾增加可选治理参数,旧插件 ABI 保持兼容; -- 验证:下载幂等、取消补偿、订阅提交与搜索治理 `131 passed`;架构合同、依赖、复杂度和质量 ratchet - `284 passed`;Ruff 低水位 `568`、mypy 低水位 `9,589`,Host 架构基线与 `git diff --check` 通过。 - -### 6.10 SUB-GOV-003B 验收证据 - -- 后端新增持久订阅执行状态,将搜索任务、站点预算等待和下载提交账本统一映射为稳定业务状态;订阅列表返回 - `execution_status`,批次列表、批次详情和取消使用独立执行端点,并按可访问订阅集合校验批次权限; -- 状态覆盖 queued、matching、searching、waiting_site_budget、preparing、submitting、accepted、retryable、 - reconcile_required、failed、cancelling、cancelled 和 completed,且只在真实可取消边界暴露取消能力; -- Alembic `f3c8a1d6b2e9` 持久化任务阶段和状态,旧数据库升级后可继续恢复队列;最终迁移链唯一 head 为 - `a7d9e2c4f6b1`; -- 前端提交 `0bbc776e` 在订阅卡片展示稳定状态和安全错误详情,在列表顶部展示批次进度与取消入口;轮询只在页面 - 活跃且存在活动执行时运行,终态自动停止;中文简体、中文繁体和英文文案同步; -- 前端验证:`yarn typecheck`、`yarn eslint . --max-warnings=0`、卡片/列表专项 `74 passed`、`yarn build` - 通过;使用真实组件和模拟 API 在桌面及 `390x844` 视口检查批次横幅、进度、取消按钮和三类卡片状态,未见 - 重叠、截断或容器溢出。 - -### 6.11 SUB-GOV-004 验收证据 - -- 下载准入在单条订阅幂等键之外增加 canonical 媒体范围身份;同媒体来源、媒体 ID、类型、季集覆盖和下载模式 - 相同的多条订阅记录共享跨记录防重事实,不依赖标题字符串碰撞; -- 第一条记录已成功时,第二条记录复用成功结果而不再次调用下载器;第一条处于提交中或结果不确定时,第二条 - 被冻结等待现有事实,不把不确定结果误判成可重试; -- 普通与洗版、不同季、不同集范围或不同媒体身份继续保持独立,避免跨记录治理改变合法下载产品规则;迁移 - `a7d9e2c4f6b1` 增加可查询媒体范围键并保持 SQLite/PostgreSQL 可逆升级; -- `tests/test_subscription_download_governance.py` 覆盖跨订阅行成功复用、活动提交阻断、不确定终态和范围隔离; - 全量后端测试与架构门禁共同验证旧插件 ABI 和单条订阅行为未回退。 - -### 6.12 最终规模与全局验收证据 - -- 固定受控矩阵和原始证据位于 `docs/refactor/subscription-governance-scale-2026-09-01.json`,对应规模测试提交 - `d239e80b5`;环境为 macOS、Python 3.14.3 和隔离 SQLite,真实外部请求与真实下载均为 0; -- 中档为 100 条订阅、10 个站点、1000 候选、100 个唯一媒体:候选检查从基线 100000 次降为 1000 次,命中 - 集合相同,事实加载 100 次;首个匹配本地耗时 0.475ms,批次本地耗时 49.783ms; -- 大档为 200 条订阅、20 个站点、1200 候选、100 个唯一媒体:候选检查从基线 240000 次降为 2400 次,命中 - 集合相同,事实加载 100 次并命中租约 100 次;首个匹配本地耗时 0.55ms,批次本地耗时 113.729ms; -- 两档持久队列均全部完成,每站点峰值在途为 1;重复站点租约和冷却认领均被阻止,任务/站点租约过期后可恢复, - 取消批次进入终态,重叠下载只有一次模拟外部提交,重复提交为 0,不确定下载保持冻结; -- 上述耗时只衡量进程内路由和隔离 SQLite 队列,不是生产网络 SLO。最终后端全量验证为 - `7712 passed, 9 skipped`;mypy 低水位 `9,588`、Ruff 低水位 `567`,复杂度、依赖、运行时合同、Host - 架构基线、API surface、schema exports、数据库技能文档和 `git diff --check` 全部通过。 - -## 7. 上线前验证与验收 - -### 7.1 场景 - -每个行为叶必须使用同一 revision、同一固定数据快照:先由当前 canonical 逻辑生成参考集合,再在隔离环境中 -只运行新链路并比较结果。该流程不进入生产,也不产生真实外部请求和下载副作用。 - -1. RSS,无新候选; -2. RSS,少量新候选且不命中; -3. RSS,新候选命中电影、追更剧集、全集和分集洗版; -4. Spider,同样三档候选; -5. 24 小时兜底搜索无结果、部分结果、站点超时和 `429/403`; -6. 日常 Match 与兜底搜索重叠; -7. 手工刷新或手工搜索与后台任务重叠; -8. 下载器拒绝、超时或本地写回失败,并在下轮按普通订阅流程重新判断; -9. 停机、取消和进程重启后的恢复; -10. 单媒体服务器可用、慢响应和不可用。 - -订阅规模使用真实形态的小/中/大档,不以固定的 1/10/101 档位作为治理要求;候选数量与站点数量也要 -独立缩放,避免只测订阅数量。 - -### 7.2 指标 - -| 维度 | 必测指标 | -| --- | --- | -| 用户耗时 | 刷新总耗时、首个匹配/首个下载时间、批次完成 p50/p95 | -| 本地计算 | 候选预处理、每订阅匹配、完成对账、提交耗时 | -| TMDB | 请求数、缓存/租约命中、错误率、p50/p95 | -| 媒体服务器 | 请求数、p50/p95、超时和合并命中 | -| PT 站点 | 按站点请求数、峰值在途、最小间隔、429/403/超时、错误冷却跳过数 | -| 正确性 | 命中集合、下载集合、缺集集合、完成/删除集合与基线一致 | -| 并发 | 同订阅跨 Search/Match 准入冲突数、重复下载数、准入与站点租约释放失败数 | -| 生命周期 | 取消延迟、event-loop lag、遗留 task/worker/锁、shutdown 上限 | - -### 7.3 门禁 - -- 日常 Match 的站点请求数和峰值不得增加; -- 兜底搜索的每站点峰值在途不得超过基线,分布式错峰不能退化为整点突发; -- 所有正确性集合必须与基线一致,新增完成对账用例必须覆盖 12/13 集更新; -- 重叠入口不得产生重复下载; -- 候选在无外部错误时不得出现可重复性能退化; -- `100–200` 条订阅、`10–20` 个站点和单轮 `1000+` 候选必须进入最终受控样本矩阵,且日常 Match - 不得因 `S * C` 全量扫描或逐订阅重复外部查询而失去可用性; -- 性能收益不足以覆盖维护成本时停止对应叶,保留更小方案; -- 本地开发数据只验证可重放性,最终性能结论必须来自受控样本或生产只读证据。 - -### 7.4 生产发布边界 - -- 不采用 shadow A/B、baseline/shadow/active 三模式或旧新双跑; -- 上线后只有新治理链路运行和写入状态; -- 生产运行指标只用于确认新链路健康,不承担旧新结果对照; -- 不单独交付文本日志聚合器; -- 异常时按完整镜像/版本回滚,不长期保留运行时旧链路 fallback。 - -## 8. 当前代码与 checkpoint 边界 - -原交接环境形成过一个可复用的正确性 checkpoint: - -- `d923037f9 test(subscribe): guard fresh match metadata` 固定日常 Match 的 `cache=False` 和 12→13 集增长语义; -- focused pytest、Pylint 和 `git diff --check` 在该提交形成时通过。 - -该提交不在当前 MoviePilot `v3` 仓库历史中,只作为 `SUB-GOV-001A` 的测试设计参考。当前分支必须重新 -建立等价夹具并产生本仓可验证的提交证据。 - -其余旧候选仍不构成交付: - -- `match()` 改为 `cache=True` 已被完成语义证据否决,必须撤销; -- 失败候选识别次数写回本地缓存可能避免每轮重复识别,但仍需证明不会覆盖音乐缓存、站点过滤结果或 - Spider/RSS 对应文件; -- 现有测试只证明了参数或局部调用,没有证明 TMDB 请求减少、订阅完成正确、站点压力不变和下载集合一致; -- 旧 checkpoint `c2d72280e` 与 10/100/500 数据只覆盖兜底搜索等待,不再是 A1B 的交付候选。 - -本节所述候选回写、完整缓存与本轮 delta 属于已废止的历史实施路径,不再作为后续实现或日志验收指标。 -治理文档现位于 `docs/refactor/` 并属于后端 Git 仓;文档状态与代码提交证据仍分别记录,不能用路线图 -状态替代行为验收。 - -## 9. 停止条件 - -满足以下条件后,本 Goal 完成: - -1. 日常 RSS/Spider Match 在目标样本下达到可接受完成时间,且 PT 请求压力不增加; -2. 24 小时兜底搜索不再持有 Match/提交长锁,按站点预算运行并能取消、恢复; -3. 日常、手工和兜底链路联合运行无重复下载和错误完成; -4. 用户可以看到真实业务状态和失败终态; -5. 所有保留的并发和缓存都有可重放验证、版本级回滚路径和明确 owner; -6. 后续只剩最低优先级的重复订阅产品规则或无生产证据的推测项。 - -## 10. 当前运行日志验收 - -当前日志用于上线后确认新治理链路是否健康,不承担旧新链路 A/B 对照,也不引入独立日志聚合器。日志内容遵循 -现有主日志输出方式,以稳定 `key=value` 字段提供机器可检索的轮次摘要。V3 生产组合根固定注入持久搜索队列, -站点请求聚合以该治理路径为验收对象;无队列的兼容执行路径保留任务摘要,但站点字段为零值。 - -### 10.1 轮次摘要 - -- Search 与 Match 每次实际执行各输出一条 `订阅治理轮次开始` 和一条 `订阅治理轮次结束` 的 `INFO`; -- 两类摘要均包含 `operation`、`run_id`、订阅数和耗时;Search 额外包含 `batch_id` 与来源,Match 包含输入 - 站点数和完整候选数; -- 结束摘要使用 `task_completed`、`task_skipped`、`task_failed`、`task_cancelled` 等任务终态字段;其中 - `task_completed` 只表示本次处理任务正常收口,不表示订阅已经下载完成或已经删除; -- Search 在队列消费、通知、批次查询或进度回调异常时仍输出配对的结束摘要,并以 `round_failed=1` 区分轮次 - 编排失败与单条任务失败; -- `admission_conflicts`、`cancelled`、`ttl_timeouts` 分开计数,避免把准入跳过、用户取消和协作 TTL 超时混为 - 同一种“未执行”; -- `release_failures` 汇总订阅准入与站点租约收口失败;任何单次释放失败同时立即输出 `ERROR`,不能只留在轮次 - 计数里。 - -### 10.2 站点与候选摘要 - -- Search 结束摘要聚合 `sites`、`site_requests`、`site_failures`、`site_cooldown_skips`、`candidates` 和 - `cooldown_seconds`;正常成功不产生额外冷却,错误冷却和已有在途租约导致的未发请求统一计入跳过数; -- Match 的 `candidates` 表示本轮完整候选快照数量,不记录候选标题、下载地址或逐项匹配明细; -- 逐订阅开始、正常无结果、准入冲突,逐候选匹配/过滤,以及逐页站点进度降为 `DEBUG`;真实异常、媒体识别 - 失败和下载业务结果仍保留原有错误或业务日志级别。 - -### 10.3 验证门禁 - -- 日志合同测试必须覆盖 Search/Match 开始与结束摘要、任务完成语义、准入冲突、取消、TTL、站点请求、错误冷却 - 和两类释放失败; -- `100/10/1000` 与 `200/20/1200` 两档受控 Match 快照中,Match 模块的 `INFO` 数量固定为两条轮次摘要, - 不得随订阅数、站点数或候选乘积增长; -- 固定快照只验证日志量、字段和本地治理合同,不把它解释为真实 PT/TMDB/媒体服务器的长期网络 SLO。 diff --git a/docs/refactor/music-lifecycle-audit.md b/docs/refactor/music-lifecycle-audit.md deleted file mode 100644 index 9978067e3..000000000 --- a/docs/refactor/music-lifecycle-audit.md +++ /dev/null @@ -1,89 +0,0 @@ -# 音乐全流程审计(2026-09-09) - -> 归档位置:`docs/refactor/`。 - -## 结论 - -普通分轨专辑和独立单曲已接入通用搜索、订阅、下载、整理、刮削及媒体库刷新流程。 -这不等于任意音乐资源都可以无人值守处理:整轨音频加 CUE、SACD ISO、多专辑全集、 -演唱会视频,以及上游没有收录或缺少可靠署名的资源,仍有明确边界。 - -本轮基于后端 `ca01cceed13ff0060de22b733eb0fef08003e840` 和前端 `e5de69f4` 审查。 -没有向真实下载器添加任务,也没有修改生产数据库或媒体库。实站请求只获取音乐列表; -识别请求使用公共 MusicBrainz 接口,单元测试全部使用隔离数据。 - -## 链路与边界 - -| 阶段 | 当前行为 | 需要保留的约束 | -| --- | --- | --- | -| 元数据搜索 | 前端专辑、单曲入口和 `useMediaSubscribe` 传递统一来源、ID、实体类型 | Recording 和 Release Group 不能互换;艺术家不能直接订阅 | -| 站点搜索 | `SearchMusicOwner` 生成标题、艺人及可信别名关键词;`match_music_resource` 核对名称、署名、分类和版本 | 缺艺术家、不同版本、关联专辑只能作为候选;单曲在专辑中的存在不是独立单曲资源 | -| 订阅 | 主动搜索与 RSS 均保留目标身份、暂停/取消状态和音质规则 | 专辑需有总曲目数;远端暂不可用时恢复已保存的订阅快照 | -| 下载 | 下载前解析种子文件清单,按盘号/曲序或曲名去重;专辑允许部分资源先提交并记录音轨事实 | 音轨键是跨资源累计的文件清单事实,不是逐曲声纹验证;没有文件清单的资源仍可下载,但不会增加专辑进度 | -| 完成判定 | 专辑订阅消费活动订阅中累计的 `downloaded_tracks`,达到 `total_tracks` 后完成;单曲按成功添加下载任务处理 | “订阅完成”代表已提交符合条件的下载,不代表下载完成、整理成功和媒体库刷新完成 | -| 整理 | 恢复下载历史中的音乐上下文,保留逐曲标签/文件名;本地目录可按曲序、曲名、时长匹配发行版 | 路径不可读时不能假定读到了标签;不能把整张专辑的标题和单曲 ID 复制给每个文件 | -| 文件识别 | 指纹、标签、文件名分层识别,目录级匹配作为补充 | 需要可读取的音频、可用的指纹工具/服务及上游条目;失败时的本地展示对象不是远端识别成功 | -| 刮削 | 各文件独立处理标签、封面和歌词,沿用统一存储与事件流程 | 单曲 ID 不适用于整目录;歌词缺失或请求预算耗尽不代表音频整理失败 | - -整轨+CUE 不会被自动拆成多首独立音轨;ISO 不属于普通可写标签音频。 -多专辑合集无法仅凭种子主标题绑定成一个正确专辑,曲目数量也不能证明每首音频内容正确。 -经典音乐的作曲家与演奏者署名、非官方译名、拼音全集命名需要额外可靠证据,不能靠放宽 -艺术家/版本匹配来制造命中。 - -## 本轮修复 - -1. 专辑刮削合并曾将 Release Group ID 放入 `MetaMusic.media_id`,标签写入随后把它当作 - `musicbrainz_trackid`。现在只保留文件自身的单曲身份,缺失时不以专辑 ID 补位。 - 验证覆盖“原本没有单曲 ID”和“已有有效单曲 ID”,并用真实 FLAC 完成写入、刮削、重读。 -2. 明确的客串署名和影视主题曲/插曲注释不再阻断同一作品匹配。客串必须由候选署名确认; - Live、Remix、未知括号副标题和错误艺术家仍然被拒绝。 -3. 标准 `CD - [WAV整轨+CUE] - 位深 - 采样率 - {厂牌目录} - 发布者` 尾链不再污染 - 专辑名;保留原始资源标题、年份和音质。副标题中的 `丨` 与普通竖线统一处理,避免把厂牌误识别为艺人。 -4. 更新批量工具:保留主副标题,复用生产模块入口,移除已失效的私有参数;没有远端 ID 的 - 展示兜底不计命中。凭据按生产库注册域规则查询,避免 `pt.` 子域导致假报缺少 Cookie。 - 采样与识别可单独执行,运行配置隔离,分页失败保留已采数据。 - -## 可复用采样方式 - -在仓库虚拟环境中执行。数据库通过 SQLite 只读 URI 打开,Cookie 不写入报告。 -默认数据库是本机 `~/Documents/moviepilot/user.db`,站点规则来自同工作区的 -`MoviePilot-Build/sites/private`。可通过环境变量 `PROXY_HOST` 提供必要的访问代理。 - -```bash -.venv/bin/python scripts/music_recognize_batch_test.py --fetch-only --pages 1 -.venv/bin/python scripts/music_recognize_batch_test.py --limit 30 --music-type album -.venv/bin/python scripts/music_recognize_batch_test.py --limit 30 --music-type recording -``` - -TSV 样本和 CSV 报告保存在 `config/temp/`,兼容旧版纯标题和两列 TSV。 -复用资源列表只免去再次访问站点,识别仍会请求 MusicBrainz;这不是离线测试命令。 -不指定实体时的“命中”只是模块返回远端候选,需额外检查实体、艺人和版本,不能作为准确率。 - -本次本地采样、公共响应录制、前后回放及完整检查日志保存在工作区 -`.artifacts/music-audit-20260909/`。这些原始实站数据和运行状态不作为产品源码提交。 - -## 已确认的实站结果 - -只读采样得到 7 个站点的 206 条资源,保留主副标题。对全部样本运行修改前后的 -离线解析:修正 `Jay` 的抓轨尾链,以及 `Duo Improbabile` 资源中被误作艺人的厂牌字段; -其余 204 条的名称、艺人、专辑、年份和格式字段未改变。这不是 206 条全部识别正确的声明。 - -`李佳薇 - 碧苍 (电视剧《与凤行》“碧海苍穹”主题曲)` 的真实 MusicBrainz 响应已保存为 -`tests/fixtures/music_lifecycle_recording.json`。相同响应在基线的单曲模式无命中,修复后同步和 -异步入口都命中 Recording `b64cad55-4c77-49cc-964a-fc3aaee5fa15`,无需回退到同名专辑。 -该回归严格重放已录制请求,不访问外部服务。 - -元数据对照选择 29 条跨站点音频资源。MusicBrainz 连续返回 503 后停止新增请求,按同一份 -公共响应记录离线回放,只有 16 条具备修改前后所需的完整响应。两版均有 10/16 条返回 -远端候选,**本轮没有证明总体命中率上升**;已证明的是一条单曲身份纠正及两条解析字段修正。 -其余 13 条包含网络失败、中断后尚未查询和修复后需要新查询的样本,统一标为不可评估, -没有计作识别失败或成功。复杂古典署名、英文译名括号、无分隔拼音全集等仍有未解决样本。 - -检查包括后端全量测试、音乐专项回归、前端 5 个音乐相关测试文件(87 项)、架构策略和 -宿主快照、类型检查、复杂度、并发/异步/任务所有权/服务定位器/Ruff/Mypy 门禁,以及启动性能。 -复杂度检查曾因刮削类注释净增一行触及既有上限而失败,已调整注释后通过,没有修改任何基线。 -两个生成的真实 FLAC 文件已完成标签写入、专辑刮削和重新读取验证。 - -后端全量检查为 **9012 通过、9 跳过**;追加实站录制与双引擎回归后的音乐专项检查为 -**822 通过**,其中本轮新增回归 20 项。前端为 **87 通过**,架构策略为 **165 通过**, -变更 Python 文件 Pylint 为 **10/10**,其余适用门禁通过。 diff --git a/docs/refactor/music-pt-benchmark.md b/docs/refactor/music-pt-benchmark.md deleted file mode 100644 index 79f5c5f2c..000000000 --- a/docs/refactor/music-pt-benchmark.md +++ /dev/null @@ -1,112 +0,0 @@ -# 音乐真实 PT 样本回归(2026-09-30) - -本次基于 `v3@fcd4cd172553ee55d39bed52d3abc59e56ee93c5` 对比音乐关键词提取和 -MusicBrainz 专辑识别。改动位于公共解析入口,Python 与 Rust 使用相同预处理, -无需重建 Rust wheel。没有放宽自动下载的严格匹配条件。 - -## 数据来源与口径 - -通过本地 MoviePilot PostgreSQL 的只读连接取得站点认证,在内存中交给既有 -`SiteSpider` 浏览音乐分区;读取文件列表页取得文件名。没有下载种子文件或音频。 -测试语料只保留标题、副标题、相对音频路径和命名标注,不包含 Cookie、密码、 -Passkey、下载地址、追踪器地址、账户资料或页面 HTML。 - -| 站点 | 去重后的音乐分区条目 | -| --- | ---: | -| 音乐乌托邦(musopia) | 250 | -| 憨憨(hhanclub) | 250 | -| 春天(springsunday) | 250 | -| 观众(audiences) | 250 | -| 红豆饭(hdfans) | 250 | -| 冬樱(wintersakura) | 26 | -| 合计 | 1,276 | - -最多浏览每站 5 页、每站保留 250 条。烧包乐园、家园本次返回空列表,学校和自由农场 -没有匹配到配置域名的 Cookie,均未计入成功采样站点;空列表不能证明站点没有音乐。 -音乐分区包含演唱会、合集等,并混入至少一条置顶电视剧,保留这些条目用于抗噪和引擎一致性测试。 - -另取音乐乌托邦 12 个种子的文件列表,得到 2,598 条不同的相对音频路径,其中 -2,596 条带曲序、2 条为整轨 WAV。文件主要覆盖流行合集、古典套装和分轨抓取, -不是所有命名习惯的随机样本。 - -- `tests/fixtures/music_pt_corpus.json`:1,276 个种子名与 2,598 个文件名。 -- 300 条种子名有独立预期值:250 条按站点明确的结构化主标题标注,50 条按命名证据人工标注; - 每条只核验 `expected` 中列出的字段,不能视为全部元数据均已标注。 -- 2,596 条文件曲序直接从原始文件名的显式前缀标注,不从被测解析器生成。 -- 未标注的字段和条目用于稳定性及 Python/Rust 对照,不能计为识别正确。 - -## 同输入前后对照 - -| 检查对象 | 修改前 | 修改后 | -| --- | ---: | ---: | -| 300 条种子名的已标注字段全部正确 | 254/300 | 300/300 | -| 2,596 条裸文件名提取曲序 | 35/2,596 | 2,596/2,596 | -| 2,596 条实际文件读取入口提取曲序 | 2,487/2,596 | 2,596/2,596 | -| 3,874 条输入的 Python/Rust 关键字段差异 | 0 | 0 | -| 60 条定向样本返回 MusicBrainz 专辑身份 | 20/60 | 33/60 | - -裸文件名入口原先没有统一去除曲序,实际文件读取入口原先已有独立处理,因此两者的 -基准相差很大,不能用 `35/2596` 描述实际文件整理能力。现在两条入口统一处理扩展名和曲序。 - -MusicBrainz 对照选取覆盖复杂命名的 60 条资源,限定 `music_type=album`,使用真实 -公开 JSON 响应,同一请求复用快照以固定候选集。有效结果须有源 ID,并核对作品与艺人; -不是 3,874 条数据全部在线识别,更不是总体命中率。新增 13 个专辑身份,原有 20 个身份未改变。 -剩余 27 条中,9 条保留多候选歧义,18 条未取得足够证据。 - -新增命中包括 `Querido Pablo`、`Poètes au Sud`、刘若英《在一起》、Taylor Swift 同名专辑、 -`Johann Pachelbel, Vol. II`、`10 Songs`、邓丽君《淡淡幽情》、周杰伦《依然范特西》《七里香》 -《叶惠美》、许巍《无尽光芒》《在别处》及飞儿乐团 `F.I.R.`。 - -这里核验的是 release-group 作品身份,不是某一张 CD、再版或地区版本:例如 -`Querido Pablo` 资源名为 2000 年,目录首版为 1985 年;《淡淡幽情》资源名为 2005 年, -目录首版为 1983 年。没有据此宣称具体发行版本已核验。 - -## 修复范围 - -1. 有格式证据时去除 `CD FLAC`、点分 `Album.FLAC` 和尾部规格/目录编号; - 点分类型清理还须有年份及足够的点分结构,保留普通作品名中的 Album、CD、Vinyl。 -2. 保留 Deluxe/Remastered 等版本语义,避免方括号版本名被扔掉;不把“专辑 FLAC 豪华版” - 当作作品名,明确的“专辑:WAV”仍优先作为真实标签。 -3. 主副标题独立确认艺人与作品后,去除重复艺人前缀;正确处理 `Radiohead.OK.Computer` 等命名。 -4. 明确整专证据下恢复 `12 Memories`、`10 Songs` 的数字前缀,不再当作曲序。 -5. 音频文件先解析扩展名和曲序,保护 `911` 等数字艺人、`1812 Overture` 等数字曲名。 - 有古典作品证据时保留完整乐章名称,同时兼容 `01 - Artist - First.flac`。 -6. 文件名只补空字段,保留真实标签的优先级;M4A 容器本身不能证明编码为 AAC。 - -订阅/资源匹配仍要求可靠身份,错误作品、艺人、年份、现场版本不能因清理而变成严格命中。 -Lover、Starboy、By the Horns 等多候选样本仍需确认。目录没有收录、艺人别名/检索召回不足、 -中文新发行作品和完整唱片版本对齐仍是后续工作,不能靠放松匹配阈值掩盖。 - -## 可重复验证 - -离线测试不连接生产数据库或外部服务: - -```bash -.venv/bin/python -m pytest tests/test_music_pt_corpus.py tests/test_music_lifecycle_audit.py -q -.venv/bin/python tests/run.py -``` - -`test_music_pt_corpus.py` 强制验证实际 Python/Rust 两条路径,并禁止 Rust 测试静默回退。 -`music_pt_musicbrainz.json` 保存 17 个已核对样本所需的 27 个公开请求响应,其中包括全部 -13 个新增命中、1 个原有命中和 3 个多候选样本。同步/异步识别都重放真实候选集, -保留请求上限和歧义短路,缺少录制的请求立即失败。 - -重新采样使用专门的在线诊断脚本,与 pytest 的隔离环境分开: - -```bash -.venv/bin/python scripts/music_recognize_batch_test.py --fetch-only \ - --config-dir /path/to/moviepilot-config \ - --sites musopia,hhanclub,springsunday,audiences,hdfans,wintersakura \ - --pages 5 --samples-per-site 250 -``` - -`--config-dir` 只读 `app.env` 中的 PostgreSQL/SQLite 凭据,运行配置和采样结果存到仓库 -`config/temp`,不修改生产库。该脚本重新抓取的是当前标题,不会保证重现本次时间点的站点列表; -对照基准应使用随代码保存的脱敏 fixture。 - -最终本地验证:全量四分片共 **12,504 passed、9 skipped**;变更文件 Pylint **10.00/10**; -宿主架构快照、strict mypy、mypy/Ruff/复杂度 ratchet 与 `git diff --check` 均通过。 -`apply_path_context` 删除重复曲序处理后 C901 从 16 降至阈值以内,基线仅移除此已消除的超限项。 - -本次未改变界面、配置项或操作流程,未触及 Wiki、前端构建和 Rust 发布;这里只记录识别行为 -与测试证据。代码仍为本地改动,生产 MoviePilot 尚未切换到本次实现。 diff --git a/docs/refactor/optimization-checklist.md b/docs/refactor/optimization-checklist.md deleted file mode 100644 index 4a979a52a..000000000 --- a/docs/refactor/optimization-checklist.md +++ /dev/null @@ -1,871 +0,0 @@ -# MoviePilot V3 架构优化清单 - -> 审计日期:2026-08-30 -> -> 首次审计历史快照:`v3@9053db926d20`;历史问题描述用于追溯,当前事实与交付状态以 -> 本文“当前架构画像”、路线图和生成 fixture 为准 -> -> 文档性质:原始差距、治理结果和剩余交付边界的持续校准账本 - -## 0.1 当前残余收口目标(G-ARCH-RESIDUAL) - -2026-08-30 已针对本账本识别的残余问题建立独立追踪目标。历史 S4 仍为 -`CANCELLED`,本目标不将全量 mypy、Ruff、覆盖率或并发质量债务重新扩大为本轮完成依赖; -只处理下表所列、已由审计确认且会影响真实运行语义或治理可信度的缺口。 - -| ID | 状态 | 问题 | 收口要求 | -|---|---|---|---| -| RES-001 | `DELIVERED`(R1) | System API 仍混有日志、Wiki、配置、事件和更新编排 | `d50177f45`:Application service/Port 拥有业务与 I/O 决策,endpoint 只做传输适配;旧 helper 已删除 | -| RES-002 | `DELIVERED`(R2) | 复杂度门禁漏扫私有方法、类/文件和 Scheduler;原生并发清单不完整 | `f7ca7e517`、`5cd5780d3`:完整宿主 AST、canonical owner/count、低水位与零增长门禁已落地 | -| RES-003 | `DELIVERED`(R3) | Outbox after_commit 失败只有内存 pending 标记,重启不可恢复 | `9d06f91bb`:持久 intent、唯一 handler、claim/fencing、重启回放、幂等和失败观测完整闭环 | -| RES-004 | `DELIVERED`(R4) | Startup initializer 与插件市场存在多套 Transport/Adapter/Manager 构造 | `7f5b8b469` 至 `046b0b305`:构造回收到 composition,兼容门面消费同一 owner,canonical 无重复正式实现 | -| RES-005 | `DELIVERED`(R5) | 审计声明、机器门禁、CI 与远端交付状态需要重新校准 | 当前文档、全 app 逐文件覆盖率低水位门禁与规则一致;Pylint `10.00/10`、架构/兼容、真实启动和最终 exact-head GitHub CI 闭环,远端 `0/0` | -R2 原复杂度 v1/v2 实际使用源码行跨度,只覆盖 API、Application、Chain 和 Scheduler, -且未强制固化下降后的低水位。2026-09-30 门禁调整将其替换为全宿主函数的 C901 圈复杂度 -(15)与 PLR1702 嵌套深度(5),源码尺寸降为观察报告;增长禁止写入,下降必须固化。 -范围、初始基线与后续建议见 [代码门禁说明](../code-quality.md)。 -2026-09-30 音乐真实样本回归统一了文件名曲序入口,删除 `MetaMusic.apply_path_context` -重复的曲序回填分支,C901 从 16 降至阈值 15 以内,已同步固化低水位。 -2026-10-12 站点用户数据兼容修复将 NexusPHP 做种列探测与翻页解析拆为独立方法, -`_parse_user_torrent_seeding_info` 的 C901 从 16 降至阈值 15 以内,基线仅移除此超限项。 -`concurrency.py` 扫描完整宿主源码,按 canonical import/alias、TaskGroup、可证明的 -loop/executor 来源和词法 owner 聚合数量。新增 owner、数量增长以及静态质量事实下降后 -未刷新低水位都会阻断 CI;覆盖率覆盖除 `app/plugins` 外全部 `app` 文件,存量低水位只增不减,新文件至少 80%。 - -## 1. 结论摘要 - -MoviePilot V3 已经形成较清晰的模块化单体:`foundation`、`domain`、`runtime`、 -`adapters`、`application`、`chain`、`db`、`startup`、`sdk` 和 `compat` 的一级所有权 -基本成立。旧物理兼容目录、Model/Oper 自建 Session/提交和直接跨层 DB 依赖等历史问题已有 -硬门禁;门禁还能证明可识别的 TaskRegistry 调用已经声明稳定 owner。 - -截至当前校准,原审计识别的四类结构性债务已经按既定路线收口: - -1. **类型化边界**:Chain/Agent 正式数据入口已使用冻结 DTO 与 typed Port;raw Oper/ORM/`Any` - 只留统一 Legacy/Compat,canonical 宿主不再依赖无 Session 写入口或全局数据 locator。 -2. **职责拆分**:Transfer、Subscribe、Download、Search、Media、Scheduler、Plugin、Agent/LLM、 - Domain 投影和 Startup composition 均已有单一 owner;Facade 只保留稳定 ABI 与显式委托。 -3. **可靠性语义**:Transfer E3 状态机、业务 UoW、post-commit 结构化结果、Outbox claim/settlement - fencing 和人工确认边界均已落地,外部 sink 的 at-least-once 责任被明确记录。 -4. **治理事实源**:依赖、SCC、Adapter、egress、Event、复杂度、类型、Ruff、生命周期和兼容基线 - 已进入机器门禁;`app/plugins/**` 始终排除,插件兼容只由 SDK/Compat 承接。 - -本轮非取消项已经全部交付。原战略目标的 canonical `SearchChain` 类型债务与架构测试导入边界分别由 -`4575b11d8`、`c204e2e97` 清零;残余审计目标又由 `87269610e`、`d50177f45`、 -`f7ca7e517`、`5cd5780d3`、`9d06f91bb` 以及 `7f5b8b469` 至 `046b0b305` 结清 System、 -复杂度/并发、Outbox 和 composition 缺口。最终锁定全量为 `7684 passed, 9 skipped`,Pylint -`10.00/10`,真实启动与健康探针通过。S0-S3、S5 及 ARCH-001、ARCH-101 至 ARCH-109、 -ARCH-201 至 ARCH-204 均达到实现、验证、提交、推送和远端门禁闭环;S4/ARCH-110/111 按维护者 -决定保持取消。 - -## 2. 范围与证据边界 - -### 2.1 本次范围 - -- 宿主源码 `app/**`,架构统计排除运行时插件副本 `app/plugins/**`。 -- 架构、质量、测试与 CI 门禁,以及当前规则文档之间的一致性。 -- 插件只审查 SDK、Compat、稳定 ABI 和宿主边界,不把官方插件仓代码混入宿主统计。 -- 数据持久化、后台动作完成语义、进程生命周期和外部传输边界。 - -### 2.2 审计方法 - -- 读取当前规则、总览、ADR、关键调用链和组合根。 -- 检查依赖图、SCC、Module/Event Contract、事务/configuration 基线。 -- 对文件、类、方法长度及类型/覆盖率治理范围做静态统计。 -- 运行架构、复杂度、异步阻塞、TaskRegistry owner、service locator、Ruff 和 mypy ratchet。 -- 运行完整架构/兼容门禁、锁定全量测试 `7684 passed, 9 skipped`,并以精确 head GitHub Actions - 结果复核四个 Unit Tests 分片、Coverage Report、Architecture Contract Gate 与 Pylint。 - -本轮没有用生产流量做动态剖析;性能收益仍需由运行指标持续观察,但不再作为本轮架构交付的未完成项。 - -## 3. 当前架构画像 - -### 3.1 分层现状 - -| 层/区域 | 已形成的正确边界 | 当前主要缺口 | -|---|---|---| -| `foundation` | 无状态、无配置、无 I/O 的底层机制 | 当前未发现需要重做的结构性问题 | -| `domain` | 已与 DB、Application、Adapter 保持单向隔离;四类媒体来源投影已拆入独立 owner | `domain/context.py` 仍承载核心领域对象和兼容 setter,继续由复杂度门禁约束 | -| `runtime` | 已有任务、事件、缓存、托管资源和停止合同;资源由 lifecycle 显式装配 | 原生并发原语的全宿主清单属于已取消 ARCH-111,不在本轮扩张 | -| `adapters` | 技术与命名外部生态已有明确目录;Application/Chain 具体 Adapter 直连已清零 | 现存 direct egress 均为精确书面化协议例外,需由 zero-growth 门禁持续守护 | -| `application` | 命令、typed Port、Outbox 和安全能力边界已落地,具体 Adapter 与 raw data locator 已清零 | 新增能力必须继续使用明确 Protocol/DTO,不能恢复 `Any` 服务定位器 | -| `chain` | 多入口复用的用例编排与 Module 动态分发已稳定,热点 Chain 已拆为同名职责包 | Facade 仍需保持稳定 ABI,并由类型、复杂度和兼容门禁防止职责回流 | -| `db/oper` | Model/Oper 已不自建 Session、不自行提交,只在调用方 Session 内查询、stage 或 flush | 当前未发现需重做的结构性事务所有权问题 | -| `db/adapters` | Application Port、短 Session/UoW 与 DTO 投影已落地;Outbox stager/store 已分离 | 新用例需继续防止 ORM 越界和隐式事务回流 | -| `startup` | `composition/runtime.py` 已成为 HostRuntime、领域 Runtime 与旧 ApiData 投影唯一 owner;Chain/Agent 复用同一 RuntimeDependencies | `initializers/modules.py` 仍保留启动顺序与跨领域发布调用,高扇出需由生命周期验收继续约束 | -| API/Command/Scheduler | Agent/System/Plugin 用例已下沉 Application;Scheduler 单体已拆为同名职责包并由 startup 注入业务 callable | endpoint 与 Facade 需继续由架构门禁防止业务编排回流 | -| `sdk`/`compat` | 精确映射、稳定插件 ABI 和宿主 canonical 路径已落地 | SDK 仍暴露部分可变全局对象/具体 Manager,只能渐进收窄,不能直接删除 | - -### 3.2 量化快照 - -| 指标 | 当前值 | 解释 | -|---|---:|---| -| 宿主 Python 模块 / 内部依赖边 | 1123 / 9,579 | Outbox 恢复的日志与关闭屏障新增 5 条合法依赖边(含父包展开),未新增模块或 SCC。 `dependency-baseline.json` 当前快照;豆瓣短评与影评新增 `app.schemas.comment` 一个模块和 3 条合法依赖边(含父包展开),模块方法契约同步登记同步与异步入口,未新增 SCC 或跨层直连。 `dependency-baseline.json` 当前快照;站点 API 依赖显式导入 `SiteQueryPort` 与 `SiteStagingPort`,新增 1 条合法类型边;集数定位直接导入 FileItem owner,替换 1 条间接导出依赖边,模块数、边数、SCC 与边界不变;监控分发器直接导入 FileItem owner,以 1 条 Schema 依赖边替换间接导出依赖,模块数、边数、SCC 与边界不变;工作流扫描、整理与下载动作直接复用已有 Schema owner,新增 3 条合法依赖边,模块数、SCC 与边界不变;工作流媒体过滤与下载查询复用已有 Schema 元素类型,新增 2 条合法依赖边,模块数、SCC 与边界不变;TheAudioDB 相关艺术家结果复用既有领域类型,新增 2 条合法 Chain 到 Domain 类型依赖边(含父包展开),模块数、SCC 与边界不变;群晖 Chat 渠道类型改为直接导入已有 Schema owner,移除 1 条间接导出依赖边,模块数、SCC 与边界不变;系统链、转移规划、订阅查询和规则服务改为直接导入已有 Schema owner,新增 2 条 owner 依赖边并移除 4 条间接导出依赖边,模块数、SCC 与层间边界不变;Telegram Rich Message 失败降级复用 `app.schemas.exception` 的 `ImmediateException`,新增 2 条合法依赖边(含父包展开),未新增模块或 SCC。目录监控成功通知在批次完成后复用主事件循环开启静默窗口,新增 2 条合法 Chain 到 Runtime 依赖边(含父包展开),未新增模块或 SCC。成功通知聚合复用既有 owner 和领域模型,统一交由通知模板渲染;移除汇总发送端的模板上下文直连后,原聚合实现的 14 条新增依赖边减为 12 条,未新增模块或 SCC。社区勋章名单读取复用 `app.foundation.url` 标准化 GitHub 镜像地址,新增 2 条合法依赖边(含父包展开),未新增模块或 SCC。专辑订阅入库对账新增 1 个 Chain owner 和 18 条合法依赖边(含父包展开),仍经注入 Port 读取历史并通过既有 mutation 写入;订阅业务活动新增 4 个模块和 50 条合法内部依赖边(含父包展开),Application 仅定义事实与持久化 Port,DB Adapter/Oper 实现存取,组合根注入,未增加 Application/Chain 到具体 Adapter 或数据库的直连;插件实例校验复用 `app.domain.plugin` 的物理身份规则,兼容下划线 ID,新增 2 条合法依赖边(含父包展开);种子失败冷却复用 `app.domain.torrent` 的限流语义,新增 1 条合法导入边;系统设置合同按 `app.application.settings` 包组织,系统设置 API 端点独立按 `app.api.endpoints.settings` 归档;消息音乐交互由 `app.chain.music_interaction` 包中的音乐解析/展示与订阅协调模块处理;已整理下载的站点字幕经 `app.chain.transfer` 自动整理队列完成命名与归档;网络连通性检测按 `app.application.nettest` 归档;工作流添加下载动作由 `app.workflow.actions.add_download` 转换工作流 Schema 为下载链领域上下文,新增 5 条合法模块导入边,资源过滤动作复用同一还原再新增 6 条;LLM 独占连接关闭复用宿主线程池;歌词字形转换复用 `app.foundation.text`;订阅创建使用 `app.domain.context.MediaInfo` 和运行时配置端口,将非 TMDB 影视身份投影为 TMDB 身份;本次安全与可靠性修复新增 4 条合法依赖边:WebAgent 复用渠道类型、宿主历史清理复用文件系统代理、两个下载器复用纯领域种子哈希规则,未新增模块或 SCC;IP 归属地适配器复用 `app.schemas.common.JsonData` 声明原始 JSON 返回,新增 2 条合法类型依赖边(含父包展开),未新增模块、循环或越层依赖;TMDb 认证与片单读取复用模块既有异常,新增 2 条同模块依赖边;Session 类型使扫描器识别原有两处 close,发送与会话关闭路径未变;Audiences 构造参数复用既有 RequestUtils 类型,新增 3 条合法依赖边(含父包展开);插件实例历史脏行清理复用既有 `app.schemas.plugin.validate_plugin_id` 进行 fail-closed 身份校验,新增 2 条合法依赖边(含父包展开),未新增模块或 SCC | -| 非平凡 SCC | 1 | 仅保留精确 containment 的 29 模块 TMDB 移植包环 | -| 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 | -| Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 | -| Module Contract | 215 specs / 211 methods / 263 calls | 动态方法名为 0;分类事实富化通过显式合同进入插件调度,旧 YAML 分类方法已移除;download 新增 custom_words 可选参数,旧 provider 签名通过显式合同兼容 | -| Event Contract | 53 | 均已有 payload model,但当前全部是 diagnostic enforcement | -| Python 源码量 | 373,625 行 | 本次重新扫描物理行数,排除 `app/plugins/**`;74 个文件超过 1,000 行,12 个超过 2,000 行;源码尺寸仅作观察 | -| 长方法 | 309 个超过 80 行 | 本次 AST 扫描排除 `app/plugins/**`;61 个超过 150 行,17 个超过 250 行;新增函数仍受 C901/嵌套深度门禁约束 | -| 全量 mypy 历史债务 | 2,881 / 212 文件 | 本分支为 Pydantic 字段补充描述并保留默认值,减少 3 项 assignment 错误;本次 NFO 返回类型与契约对齐,减少 2 项历史类型错误;本轮在消息、站点、诊断、备份、用户配置、订阅、AcoustID、TMDB、仪表板和命令生命周期边界补齐类型,历史债务再减少 53 项;TMDB v3 API 显式导出并保留集合 parts 的列表合同,逐文件逐错误码无增长;此前工作流筛选、动态插件路由和 stdio 流补齐边界类型,历史债务减少 3 项;Bitpt 与 CookieCloud 补齐边界类型,历史债务减少 16 项;知行站点解析器补齐数值属性与基类合同,历史债务减少 18 项;通知排版补齐 Slack 消息块类型,历史债务减少 2 项;工作流资源过滤改用领域对象调用过滤链,恢复的节点输出在消费前归一化,并收紧可空资源列表的类型低水位;消息媒体交互类型债务已清零,canonical Facade 与 endpoint 类型边界已补齐,低水位只允许继续下降;微信 ClawBot 入站 item_list 媒体解析改为按子项键名分派后,类型错误减少 4 项;站点解析器分享率改用浮点初值,类型错误减少 20 项;本次下载与文件安全修复再减少 3 项;Transmission 进度回退值归一化再减少 2 项;本批清理 Servarr、认证、存储、订阅策略和搜索/规则/通知 Schema 的 18 项类型错误,并修复系统配置中的 Python 2 异常语法;本批再清理订阅元数据、订阅写入、交互宿主、整理路径、删除统计和工作流动作的 14 项类型错误,逐文件逐错误码无增长;本批补齐 OCR 请求参数、工具 Schema 摘要和宿主模块配置快照的 5 项边界类型,逐文件逐错误码无增长;本批为 Rousi 用户消息分页解析补齐返回类型与数据结构兜底,减少 2 项类型错误;本批优先处理 TMDB v3 电影与电视剧对象模型,补齐 98 个同步/异步方法注解并减少 122 项历史类型错误;随后补齐剧集、季度、人物、搜索和趋势对象的 142 个方法注解,再减少 142 项类型错误;绿联 API 响应边界补齐具体泛型与运行时字典收窄,减少 31 项类型错误;运行时限流器补齐状态方法、装饰器边界和时间队列类型,减少 31 项类型错误;QQ Bot API 响应边界补齐具体泛型与 JSON 形状校验,减少 17 项类型错误;rTorrent 客户端清理 182 项错误,调用端同步明确字典合同,累计再减少 190 项错误;豆瓣 API、中心服务接口及订阅消费端补齐返回边界和参数收窄,结合 Context 方法签名与缓存代理类型清理,累计再减少 455 项错误;共享 HTTP 客户端清理 110 项错误,TMDB API 清理 84 项错误,索引器爬虫清理 87 项错误,并补充 U115、AliPan、OpenList 对可空流响应的防守;U115 其余存储基类契约债务暂留待测试完备后处理;缓存同步/异步继承冲突保留为设计债务,未纳入严格门禁 | -| Ruff 历史诊断 | 73 | 低水位门禁通过,但规则集只覆盖 `E4/E7/E9/F/I` | -| mypy 已记录低水位 | 2,881 / 212 文件 | 本轮完整 Linux 宿主扫描逐文件、逐错误码无增长;16 个原问题文件清零并加入 `mypy.ini` 严格清单。数值以 `tests/fixtures/architecture/mypy-baseline.json` 为准。 | -| 覆盖率逐文件低水位 | 全 app(排除 app/plugins)当前聚合 77.68% | 存量文件比例不降;语句减少且未覆盖行数不增时允许比例变化,新文件至少 80% | - -HTTP 共享池自恢复复用运行时日志与 TaskRegistry,新增 2 条合法 Adapter 到 Runtime 依赖边,未新增模块、SCC 或外连入口。 - -外部 MCP schema 紧凑投影由纯函数模块 `app.agent.tools.projection` 拥有,仅在请求头显式 opt-in 后应用;新增 1 个模块和 1 条合法端点依赖边,未新增 SCC。`list_exposed_tools` 返回类型注解使端点类型债务减少 5 项。 - -本批并行清理消息模板与队列、TMDB 模块和飞牛媒体客户端,累计减少 177 项类型错误,新增严格门禁文件 3 个。 - -### 3.3 热点文件 - -| 文件 | 行数 | 主要职责混合 | -|---|---:|---| -| `app/chain/subscribe/`(已治理) | Facade 98 行 | 搜索、匹配、刷新、完成、规则引用与通知已拆至单一 owner;原 3,895 行单文件已删除 | -| `app/agent/orchestrator.py` | 2,790 | 单 Agent 运行、流式输出、工具与中间件编排 | -| `app/agent/session.py` | 837 | 会话队列、worker、状态、取消与延迟清理 | -| `app/agent/tasks.py` | 208 | 后台 prompt、持久化定时任务与心跳 | -| `app/agent/lifecycle.py` | 166 | 接收门禁、启动、空闲回收与有界关闭 | -| `app/agent/manager.py` | 27 | 稳定 `AgentManager` 构造门面 | -| `app/agent/llm/provider.py` | 413 | 稳定 `LLMProviderManager` Facade 与兼容方法 | -| `app/agent/llm/catalog.py` | 1,571 | provider spec、预设和模型元数据 | -| `app/agent/llm/discovery.py` | 985 | 远端目录发现与 SDK 客户端 I/O | -| `app/agent/llm/auth.py` | 582 | 持久鉴权和外部授权协议 | -| `app/adapters/external/market.py`(已治理) | Facade 226 行 | `PluginHelper` 只保留精确旧 ABI、安装 Gateway 及 owner 委托 | -| `app/adapters/external/plugin/client.py` | 1,927 | 插件市场传输、索引、Release 与本地仓候选查询 | -| `app/adapters/system/plugin/package.py` | 2,391 | 插件安装、checkpoint、备份、恢复和物理删除事务 | -| `app/adapters/system/plugin/health.py` | 1,060 | 插件运行环境保护、安装前后检查与故障恢复 | -| `app/chain/transfer/`(已治理) | Facade 82 行 | 队列/恢复、规划、执行、结算、历史/通知已拆至单一 owner;旧 `transfer.py` 与 `_transfer.py` 已删除 | -| `app/chain/search/`(已治理) | Facade 473 行 | 搜索计划、并发 fan-out、状态、分页和结果处理已拆至单一 owner;旧 2,970 行单文件已删除 | -| `app/api/endpoints/agent.py`(已治理) | 645 | 只保留 HTTP/SSE、文件/音频传输映射;Agent 会话和事件编排已下沉 Application | -| `app/application/messaging/agent.py` | 1,633 | WebAgent 会话、文件/音频、事件与消息桥接用例;是后续复杂度治理热点,不回流 endpoint | -| `app/chain/download/`(已治理) | Facade 47 行 | 选择、提交、批量、历史、提交后处理、字幕和任务控制已拆至单一 owner;原 2,413 行单文件已删除 | -| `app/chain/media/`(已治理) | Facade 213 行 | 识别、来源投影、插件事件、音乐目录、路径证据与缓存已拆至单一 owner;旧 2,191 行单文件已删除 | -| `app/scheduler/`(已治理) | Facade 135 行 | 原 2,111 行单体已退役;catalog、execution、bridge、progress、registry、reconcile、lifecycle、maintenance 与注入合同已有独立 owner | - -热点不是按行数机械拆文件的依据。只有在提取出稳定合同、保留旧入口委托并有行为测试时,拆分才算 -降低复杂度;把长方法原样移动到新目录不算完成。 - -## 4. 优化清单总表 - -状态含义:`阻塞` 表示当前门禁已失败;`待执行` 表示尚未开始;`执行中` 表示只完成部分纵切面; -`已验证` 表示实现和本地验收已通过,但本叶提交、推送或精确 head 远端门禁尚未全部闭环; -`已交付` 表示实现、提交、推送、远端一致性和该叶要求的 CI 证据均已闭环;`已取消` 表示维护者明确 -取消且不再作为本轮完成依赖。 - -| ID | 优先级 | 状态 | 事项 | 目标结果 | -|---|---|---|---|---| -| ARCH-001 | P0 | 已交付 | 恢复 mypy ratchet | `5df388719` 已推送,主线既有 CI gate 通过 | -| ARCH-101 | P1 | 已交付 | 统一规则、总览、基线和语义门禁 | `113355784` 已推送,Unit Tests `33031697902`、Pylint `33031697785` 全绿,远端 `0/0` | -| ARCH-102 | P1 | 已交付 | 将 Transfer pending 升级为真实 E3 状态机 | `e9de149db`、`a2e249f20` 已推送;Unit Tests `33092427327`、Pylint `33092427348` 全绿,崩溃结果未知时进入人工确认 | -| ARCH-103 | P1 | 已交付 | 类型化 Chain/Agent 数据 Port 与 DTO | Chain/Agent 正式数据入口已切换到冻结 DTO 与 typed Port,raw Oper/ORM/`Any` 仅留统一 Legacy/Compat;最终精确 head CI 已闭环 | -| ARCH-104 | P1 | 已交付 | 收口跨多次写入的业务事务 | 订阅、站点和规则引用变更由单一 UoW/CAS 拥有,失败整体回滚或幂等恢复 | -| ARCH-105 | P1 | 已交付 | 明确 post-commit 与 Outbox 完成语义 | 业务提交、effect 完成/pending 可区分;stager/store 分离且 claim/settlement 受 fencing,外部 sink 仍承担 at-least-once 幂等边界 | -| ARCH-106 | P1 | 已交付 | 让线程/队列/日志 writer 由 bootstrap/lifecycle 显式构造 | 日志、消息、任务、模块与插件资源由 lifecycle 显式创建、逆序关闭和失败回滚 | -| ARCH-107 | P1 | 已交付 | 消除 Chain SCC,强化循环门禁 | SCC 只剩精确豁免的 TMDB 移植包环 | -| ARCH-108 | P1 | 已交付 | 决策并收口 Application/Chain 到 Adapter 与 HTTP 边界 | Application/Chain 具体 Adapter 与普通 direct HTTP 债务清零,剩余出口均为精确政策例外 | -| ARCH-109 | P1 | 已交付 | 按用例拆分超大 Chain、Scheduler 和厚 API | Transfer、Subscribe、Scheduler、Download、Search、Media 与 Agent/System/Plugin API 均已按 owner 收敛;最终精确 head CI 已闭环 | -| ARCH-110 | P1 | 已取消 | Module/Event Contract 分可信级执行 | 随 S4 取消,不属于本轮完成依赖;现有宿主严格合同和插件兼容诊断继续保留 | -| ARCH-111 | P1 | 已取消 | 升级复杂度、类型、覆盖率和并发原语门禁 | 随 S4 取消,不扩张为全宿主质量债务清零;保留现有 ratchet | -| ARCH-201 | P2 | 已交付 | 收窄 PluginHelper/PluginManager 与 SDK 暴露面 | `fa40f29df` 已推送;ABI Facade 只委托,构造和具体服务归组合根 | -| ARCH-202 | P2 | 已交付 | 拆分 Agent/LLM provider 职责 | provider catalog、发现、认证、会话和运行时分离,Facade 只保留稳定 API;最终精确 head CI 已闭环 | -| ARCH-203 | P2 | 已交付 | 拆分 Domain 投影与 Startup 高扇出目录 | Domain 四来源投影与 Startup 单词型 composition owner 均已完成;最终精确 head CI 已闭环 | -| ARCH-204 | P2 | 已交付 | 合并重复 sync/async 核心逻辑并转换存量测试风格 | 双 ABI 只保留 I/O 外壳,共享解析、校验、映射和状态决策;最终精确 head CI 已闭环 | - -S4/ARCH-110/111 的取消不撤销现有防回退门禁。后续按路线图的 **S4 Lite** 边界做增量治理: -第三方插件 Module/Event 合同校验继续 diagnostic,宿主 strict contract enforcement 只覆盖 -独立评审的内置高风险能力和 durable internal event;这不改变现有 strict 异常传播接口仍可触达 -插件 provider 的事实。现有插件 ABI、SDK/Compat/Legacy 行为及原始返回形状不得因宿主升级改变。 -复杂度、并发、mypy、Ruff 和 coverage 继续使用当前 zero-growth/低水位 ratchet,但不再追求 -一次性清空全宿主历史债务。 - -## 5. P0:先恢复主线 - -### ARCH-001 恢复 mypy ratchet - -**问题与证据** - -- 历史 `app/chain/media.py` 的标题键与位置键复用已修复,随后单体已退役为 - `app/chain/media/` 同名职责包;迁移门禁按错误码聚合并要求新 owner 不增加债务。 -- `.github/workflows/test.yml:71-72` 在主 CI 中执行该门禁。 - -**执行要求** - -- [x] 将标题键和位置键使用不同、带语义的变量名,修复真实类型错误。 -- [x] 未使用 `mypy_ratchet.py --write` 接受增长。 -- [x] 保留音乐专辑“精确标题优先、碟号/曲序回退”的现有行为。 - -**验收** - -```bash -.venv/bin/python scripts/architecture/mypy_ratchet.py -.venv/bin/python -m pytest tests/test_music_album_match.py -q -``` - -## 6. P1:结构性问题 - -### ARCH-101 统一架构事实源 - -**问题与证据** - -- 审计时 `docs/rules/04-design-patterns.md` 仍示范 `SubscribeOper()` 无 Session 调用, - 与生产路径显式 UoW 规则冲突;该项已由 S0-L2.1 修复并增加文档门禁。 -- 审计时规则禁止 `application -> concrete adapter`,但 RSS 段落又要求直接消费 network adapter; - S0-L2.4 已统一为 Application-owned Port + startup 注入,并将现有直连全部列为临时债务。 -- 审计时完整 SCC 只进入生成快照,语义测试只覆盖特定根;S0-L2.2 已增加完整宿主 SCC policy 门禁。 -- 架构总览此前仍记录 811 模块、6,572 条边和 1 个 SCC,已经落后于当前基线。 -- Event consumer 扫描曾把任意同名 `.register()` 调用当成事件注册;S0-L2.5 已改为证明 - canonical EventManager receiver,10 个动态误报归零并保留唯一 workflow 动态注册。 -- S0-L2.6 已将 producer/consumer 合并为逐调用事实源;本轮删除重复 Agent Tool 事件发送点后为 - 86 个 producer(85 静态、1 动态)与 - 17 个 consumer(16 静态、1 动态);consumer 由不可自动写入的精确人工 policy 管理。 - -**目标与步骤** - -- [x] 指定统一 Event 机器事实源;生成快照与人工 consumer policy 分离且互相不能覆盖。 -- [x] 修正 Oper 示例,分别展示宿主显式 UoW 与插件兼容 Facade,并以文档测试禁止回退。 -- [x] 明确 Application/Chain 不永久直连具体 Adapter;业务层拥有 Port,startup 注入实现。 -- [x] 让 SCC 规则、精确 policy 和文档声明一致;Chain 临时债务与 TMDB vendor containment 分开治理。 -- [x] Event 扫描只识别 EventManager 实例/别名和事件装饰器;未知 receiver 不再污染动态事实。 -- [x] CI 分开报告“Event 语义 policy”与“宿主快照一致”,禁止把后者表述为架构完全正确。 - -**验收** - -```bash -.venv/bin/python -m pytest \ - tests/test_architecture_dependencies.py \ - tests/test_architecture_contract_baseline.py \ - tests/test_architecture_baseline_cli.py -q -.venv/bin/python scripts/architecture/baseline.py --check-host --diagnostics -``` - -### ARCH-102 将 Transfer pending 升级为真实 E3 状态机 - -**分叶状态** - -- `S1-L1.1 Durable admission`:`DELIVERED`。已交付 persist-before-enqueue、Application-owned typed Port、 - DB adapter 与可逆 migration,宿主退出 raw/`Any` `TransferPendingOper` admission 路径。 -- `S1-L1.2 Planning checkpoint`:`DELIVERED`。版本化请求与指纹先准入;无 legacy provider 时通过 - `accepted -> planned` CAS 提交完整目标和有序操作,有 provider 时先提交 `provider_pending`,全部 - 返回空后再以第二次 CAS 提交 `planned`;planned 重放只消费冻结上下文和目标。 -- `S1-L1.3 Lease 与恢复调度`:`DELIVERED`。已交付 token fencing 的 - claim/lease/heartbeat/attempt、过期接管、固定退避的唯一恢复入口和有界关闭 owner。 -- `S1-L1.4 幂等执行与终态结算`:`DELIVERED`。已交付稳定 operation ledger、严格结果探测、 - 唯一 retry owner、`manual_review` 人工判定和 history/pending/outbox 同 UoW 终态结算。 -- `S1-L1.5 E3 全链收口`:`DELIVERED`。崩溃矩阵、3.0.17 升降级、重复回放、稳定计划身份、 - outcome/settlement 一致性和插件 ABI 已完成验收;旧 fail-open、重复状态与兼容层外旧入口已删除。 - -**问题与证据** - -- `S1-L1.1` 已将任务准入改为独立事务先 commit、后写内存队列;admission、batch 或 enqueue - 异常均不会伪装成重复任务成功,失败记录可供恢复。 -- 宿主 canonical Chain 只取得类型化 `TransferAdmissionRepository`;旧 Oper API 仅保留给统一兼容层, - 插件公开 `TransferTask.to_dict()` 字段未增加内部任务标识。 -- `S1-L1.2` 已消除 checkpoint 前的文件副作用:目标路径、操作顺序及 resolved 识别上下文原子落库后, - 执行器才允许触发 cleanup、建目录和复制/移动;规划失败保留 `accepted` 并记录 `last_error`。 -- 旧插件 `transfer` provider 的身份、顺序和原始 ABI 参数先冻结为 `provider_pending`;提交后才精确 - 解析并严格执行,缺失或异常不 fallback。全部返回空后才生成宿主计划,并以第二次 CAS 提升为 - `planned` 后执行。旧 caller 只经 `ChainBase.transfer` 注入式兼容门面进入同一 durable command, - 宿主 FileManager/TransHandler 的旧执行入口已删除。 -- `S1-L1.3` 已把执行所有权与 planning phase 正交:恢复任务入队前原子 claim,普通任务在任何业务 - 副作用前 claim;heartbeat、checkpoint、失败留痕、release 和终态删除均受当前未过期 token - fencing。启动和同进程恢复共享唯一 scheduler,确定性失败按固定轮询退避,关闭时 worker、replay、 - lease release 和 heartbeat 都由有界生命周期 owner 持有。损坏投影以无有效租约 CAS 留痕,同错不 - 重复刷写,且不会阻塞后续健康任务。 -- `S1-L1.4` 已增加 `TransferExecutionStep` 独立账本:每一步在副作用前冻结 intent 和稳定 - operation ID,以 lease + attempt 双 CAS 提交结果;重启遇到遗留 `STARTED` 时必须先严格探测, - 只有 `NOT_APPLIED` 能轮换 attempt 自动重试,`UNKNOWN/CONFLICT` 进入 `manual_review`。 -- 文件 cleanup、目录创建、版本发现/删除、覆盖目标删除、目标物化和跨存储 move 的源删除均已拆为 - 可重放步骤;本地复制使用完整内容比较,远端结果证据不足时不会伪造 exactly-once。 -- 成功、失败及覆盖拒绝均通过 task-aware writer 在一个 UoW 内提交 history、pending、step cleanup - 与可选 outbox;revision 和确定性 occurrence key 使“文件已移动、历史未提交”在恢复后只补历史, - 不重复文件副作用。历史/API/Agent 重试只登记 durable retry intent,由唯一 scheduler 重新 claim。 -- 管理员人工判定 API 只公开 `not_applied` 与带结果证据的 `applied`,并持久记录操作者、理由、结论 - 和 revision;无租约人工路径不能直接伪造失败终态。 -- 以上实现已满足 `docs/adr/0007-background-action-reliability.md:123-139` 对 E3 稳定身份、步骤状态、 - lease/heartbeat 和人工恢复的要求。`RETRY_WAIT`、重放、双重失败、人工放弃和结算崩溃窗口均有 - 故障注入覆盖;计划指纹、步骤成员关系和所有状态写入使用精确 CAS,异常不再降级到旧执行路径。 -- canonical 模块已按职责聚合为 `app/application/chain/events.py`、`app/application/transfer/execution.py` - 和 `app/runtime/resources.py`;`durable_events.py`、`transfer_execution.py`、`managed_resources.py` - 等旧物理模块已退役,仅允许精确 Compat manifest 和兼容测试引用旧导入名,宿主不保留重复导出。 -- 交付提交为 `e9de149db`、`a2e249f20`;精确 head SHA 的 Unit Tests `33092427327` 与 Pylint - `33092427348` 全绿,覆盖率低水位同步提升至 Application `78.71%`。 - -**目标与步骤** - -- [x] 先在独立持久事务中 commit pending,再尝试放入内存队列;数据库事务不能与 `queue.Queue` - 原子提交,入队失败时必须保留 pending 供重放。 -- [x] 初始登记保存稳定源身份、版本化请求和状态;目标与有序操作在纯规划完成后以 planning - checkpoint 原子更新,任何文件副作用不得早于该提交。 -- [x] 增加 claim/lease/heartbeat/attempt 与过期接管,同一任务同时只能有一个 worker owner。 -- [x] 设计幂等文件操作和历史提交;只有所有必要步骤达到持久终态后才能删除记录。 -- [x] 在持久状态机与现有失败历史/AI retry 之间指定唯一 retry owner,定义旧记录迁移和兼容规则。 -- [x] E3 失败使用持久 `failed/manual_review`、最后稳定 checkpoint 和补偿边界,不直接套用 E2 - Outbox 的 dead-letter 语义;禁止按年龄通用清理 pending。 -- [x] 当前 admission/planning 数据模型变更均配套 Alembic migration,并验证升级、降级和中断重跑。 - -**故障注入验收** - -- [x] 登记后、内存入队前崩溃,重启可继续。 -- [x] 持久登记成功但内存入队失败,重启可继续。 -- [x] 文件移动后、历史提交前崩溃,在支持稳定身份/幂等操作的存储上不重复移动且可补齐历史。 -- [x] worker 未知异常和 lease 超时后保留可诊断状态。 -- [x] 重复回放、重复消息和人工重试都保持幂等。 -- [x] 外部存储返回结果未知时进入 `manual_review`,不得伪装成 exactly-once 成功。 - -```bash -.venv/bin/python -m pytest \ - tests/test_transfer_pending_replay.py \ - tests/test_transfer_worker_lifecycle.py \ - tests/test_chain_durable_events.py -q -``` - -### ARCH-103 类型化 Chain/Agent Port 与 DTO - -**问题与证据** - -- Subscription 已迁入 `app/application/subscription/contract.py` 的深度冻结 DTO 与 typed - query/write/staging Repository;Chain、API、Agent、Workflow 和 interaction 不再接收订阅 ORM。 -- `app/startup/initializers/modules.py` 现向生产 Chain/Agent 注入 - `TransactionalSubscriptionRepository`,请求写入口通过 `SessionSubscriptionRepository` 复用 - 当前 `AsyncSession`;原始 `SubscribeOper`/`SubscribeHistoryOper` 只留在 DB adapter 与 Legacy 层。 -- standalone typed Repository 仍会为单次调用创建独立短事务;若一个业务操作连续调用多次,仍可能 - 把“查询后更新”或批量更新拆成多个事务,必须由 S1-L4/S1-L5 的 Session-bound Command 收口。 -- Workflow、User、DownloadHistory、TransferHistory、Site 和 Subscription 已迁入冻结 DTO 与 - adapter-owned Session 投影;剩余风险转为其他领域 locator/raw getter,以及 S1-L4/S1-L5 尚未 - 完成的跨记录、跨配置原子事务,不能用本批类型化结果代替事务证明。 -- S1-L2 由 `b4f873654`、`a01a35bcb` 交付;精确 head SHA 的 Unit Tests `33098869736` 与 - Pylint `33098869837` 全绿,Application 覆盖率低水位提升并固化至 `78.78%`。该证据只完成 - Workflow query 纵切面,不能替代 S1-L3 对其余 Chain/Agent raw data port 的清零。 - -**目标与步骤** - -- [x] Subscription 按领域定义 Query/Write/Staging/History Protocol,不再使用通用 - `OperFactory = Callable[[], Any]` 或 raw Subscribe Oper factory。 -- [x] 写 Port 由 `db/adapters` 创建单操作 Session/UoW;Oper 的 canonical 写方法只 stage/flush, - 读取方法仍可在调用方 Session 中查询。 -- [x] Workflow 查询 Port 在 adapter Session 内映射为冻结 DTO/Projection,API、Agent、Chain、Scheduler、 - Workflow runtime 和中心服务分享均不接收 ORM。 -- [x] Workflow 执行写端由 Chain 直连 `WorkflowExecutionPort` 和短 Session/UoW 事务服务;canonical - `WorkflowOper` 只保留显式 Session query/stage,旧无 Session 五方法只存在于 SDK Legacy/Compat。 -- [x] 删除 Chain registry 中零消费者 `*PortProxy`/动态转发和 `ChainRuntimeContext.data_ports` - 伪注入;Workflow 执行服务只在 Application owner 配置一次,不再重复注册到 `ChainDataPorts`。 -- [x] DownloadFailure/MediaServer 两个 registry 字段改用冻结 DTO 与 typed Repository factory; - ORM 不越过短 Session,媒体库远端枚举期间不持有事务,旧 Oper/Compat 与公开 Chain ABI 保持不变。 -- [x] User Chain/Agent/认证查询改用冻结 `UserSnapshot`/`UserAuthSnapshot`;创建、更名和 - 删除由请求级 UoW 原子提交,用户名唯一约束、最后一个启用超级管理员保护与 - UserConfig/PassKey 级联约束共同守住身份聚合。 -- [x] DownloadHistory 与 TransferHistory 查询和写入改用深度冻结 DTO、typed Port 与短 - Session adapter;请求级删除和 durable 结算在各自单一 UoW 中处理,canonical 调用方不再 - 接收 raw Oper/ORM,旧动态写入只保留在 SDK Legacy/Compat。 -- [x] Site 配置、用户数据、图标与健康统计统一经 `app/application/site/contract.py` 的冻结 DTO - 和 typed Port;API 写用例复用请求 AsyncSession,Chain/Agent 使用短 Session adapter,旧 - `SiteOper` 导入与方法 ABI 只由 SDK Legacy/Compat 承接。 -- [x] SubscriptionSnapshot/SubscriptionHistorySnapshot 的 JSON 列深度冻结;Identity/Patch 明确 - 查询与写入形状,`TransactionalSubscriptionRepository` 与请求 Session adapter 在会话内完成 - ORM 投影,Chain/API/Agent/Workflow/interaction 全部消费 typed contract。 -- [x] `app.db.subscribe_oper`、`app.db.subscribehistory_oper` 和 `app.db.oper` 包根旧符号统一经 - `app/sdk/_legacy/subscribe.py` 与精确 Compat 映射保留;canonical 包根不增加重复 `__all__` 导出。 -- [x] 删除 `ChainDataPorts`/`AgentDataPorts` 及其全局 getter;Chain 使用实例级 - `ChainRuntimeContext`,Agent manager/memory/tool/scheduler 使用同一 `AgentDataContext`,字段均显式可类型检查。 -- [x] User、History、Site、Subscription、Agent/Transfer locator 按纵切面逐批迁移并各自验证, - 未通过通用代理一次替换所有 Oper。 -- [x] Subscription 增加 AST 门禁,禁止 canonical consumer 导入 raw Oper/ORM、以 `Any` 伪装 - Snapshot、复制 CRUD Protocol 或新增多词散落文件;全局 Agent/Transfer locator 已清零并由独立 AST 门禁守护。 - -**交付结果** - -1. S1-L4 已收口 Subscription 新增、修改、删除、完成和批量修改的单一 UoW,canonical 自动提交写入口 - 与运行时 locator 清零;Servarr 多季新增共享一个请求事务和 outbox,任一季失败整批回滚。 -2. S1-L4 已逐用例验证 Session-bound Command 的提交、回滚和 post-commit 语义,不再以逐条短事务 - 冒充批量原子事务。 -3. S1-L5 已将站点、规则组及自定义规则改名涉及的 SystemConfig 与 Subscription 更新合并为原子命令, - 通过 CAS 拒绝过期快照,并在 commit 后一次发布配置快照。 - -**验收** - -- 关闭 adapter Session 后序列化 DTO,确认不会触发 lazy load。 -- 对迁移的 Application Port 运行 strict mypy,并验证 API response contract 与插件 ABI。 - -```bash -.venv/bin/python -m pytest \ - tests/test_db_workflow_queries.py \ - tests/test_agent_data_ports.py \ - tests/test_subscription_query_service.py \ - tests/test_site_query_service.py \ - tests/test_history_query.py -q -.venv/bin/mypy --config-file mypy.ini -``` - -### ARCH-104 收口跨多次写入的业务事务 - -**问题与证据** - -- `app/chain/subscribe/reconcile.py` 只保留站点和规则组事件到原子 Application Command 的 - 引用协调委托;旧单文件中的多次独立写入实现已删除。 -- 类型化边界已经消除 ORM 泄漏,但 standalone Repository 的每次 update 仍是独立短事务;中途失败 - 可能长期残留部分状态,直至同一事件再次触发或人工修复。处理本身具备一定幂等性,但触发事件不是 - durable owner,不能把 S1-L3.7 的通过误当作 ARCH-104 完成。 - -**目标与步骤** - -- [x] 将引用分析与修改计划提取为纯函数/值对象。 -- [x] SystemConfig 与 Subscribe 位于同一宿主数据库,首选一个 Application Command/UoW 和批量原子更新; - 复用 `SystemConfigOper.update_atomically()` 的锁行能力,而不是预设必须跨存储补偿。 -- [x] 同一 UoW 内修改配置表后,在 commit 成功时一次性发布全部进程内配置快照,并明确 - `_write_lock`/`_snapshot_lock` 的顺序;读者只能观察完整旧快照或完整新快照。 -- [x] 仅当未来确有无法共享事务的外部状态时,才使用持久、幂等、带 checkpoint 的 reconciliation job。 -- [x] 每一步允许安全重试,返回明确的完成/冲突状态。 -- [x] 在第 `k` 次写入注入异常,验证整体回滚或下次能恢复到完整状态。 -- [x] 增加并发触发和并发读取测试,验证没有丢更新、死锁或配置中间组合。 - -```bash -.venv/bin/python -m pytest \ - tests/test_site_mutation_command.py \ - tests/test_subscription_mutation_outbox.py \ - tests/test_rule_group_media.py -q -``` - -### ARCH-105 明确 post-commit 与 Outbox 完成语义 - -**已实现事实与语义边界** - -- `OutboxStager` 只在业务 Session 中 stage/flush;`OutboxDispatchStore` 的 claim、complete 和 retry - 每次使用独立短事务,业务与 dispatcher 的事务所有权已由类型分开。 -- 请求线程即时投递与 dispatcher 均先按稳定 event key 原子 claim;同一 lease 期间只有 - 一个 owner,过期 owner 不能用旧 attempt 覆盖新 owner 的 complete/retry。 -- `PostCommitResult`/`PostCommitEffectError` 保留“业务已提交”事实,并逐项列出已完成和 - pending effect,后置效果失败不伪装成业务回滚。 -- lease/attempt fencing 只保护宿主的认领与结算。外部调用成功但 complete 落库前崩溃时, - intent 仍会重放;因此交付承诺是 at-least-once。事件载荷和宿主 correlation context - 携带稳定 event key,支持幂等的消费者应使用它;旧通知插件保持原方法签名,不能宣称外部 - provider 已获得 exactly-once 或统一幂等能力。 - -**目标与步骤** - -- [x] Command 返回结构化结果:业务是否提交、哪些后置效果完成、哪些处于 pending。 -- [x] commit 后先取得 delivery lease;未取得时跳过请求线程直投,确保与 dispatcher 排他。 -- [x] 每个 post-commit effect 使用独立 intent 隔离和结算,避免效果 A 失败导致效果 B 被一起重放。 - 单个外部效果仍是 at-least-once,必须有稳定幂等键和幂等 handler/消费者。 -- [x] 按完成承诺、可重建性、外部不可逆性和业务重要性划分 E0-E3;用户可见性只是因素之一。 - 完整分类、失败语义和恢复责任记录在 `docs/adr/0007-background-action-reliability.md`。 -- [x] 拆分 `OutboxStager` 与 `OutboxDispatchStore`,避免业务 Session 调用自提交方法。 -- [x] 在 commit、claim、publish、complete、通知和任务提交各断点注入异常/崩溃,验证声明等级与实际恢复一致。 -- [x] 增加请求线程即时投递与 dispatcher 并发竞争测试,以及“外部调用成功、complete 前崩溃”的重放测试。 - -```bash -.venv/bin/python -m pytest \ - tests/test_outbox.py \ - tests/test_chain_durable_events.py \ - tests/test_subscription_completion_command.py \ - tests/test_subscription_mutation_outbox.py -q -``` - -### ARCH-106 让进程资源由 bootstrap/lifecycle 显式构造 - -**问题与证据** - -- 日志 writer 已退出 `app.runtime.config` 导入路径,由 lifespan 显式创建、关闭并在真实收敛后释放身份。 -- `ChainBase` 只绑定共享消息队列的轻量客户端;队列线程由 lifespan 唯一启动,不再保存首个 Chain 回调。 -- 停止状态、WebPush registry 和主循环 owner 已拆为独立 runtime 合同;`global_vars` 只保留旧插件 ABI 门面。 -- 当前 TaskRegistry 门禁检查 Registry 调用必须带 owner;全宿主原生 `create_task`、`Thread`、 - `Timer`、`Executor` 清单原属于已取消 ARCH-111,不作为 ARCH-106 的完成依赖。 -- service locator 门禁继续覆盖五类 concrete runtime;独立 AST 门禁现已拒绝 canonical 宿主导入 - `global_vars`,只允许定义模块与 SDK 兼容出口。 -- Workflow、Scheduler、Command、Agent provider 均由生命周期显式 configure/reset,冷导入不再改变注册状态。 - -**目标与步骤** - -- [x] 日志 writer、消息队列及消息缓存由 bootstrap/lifecycle 显式构造、发布和关闭。 - 现有关闭路径已能收口部分资源,当前要修的是隐式创建权,而不是重新发明 owner。 -- [x] startup 唯一启动日志和消息资源;Chain 只接收无生命周期的发送 Port。 -- [x] 普通模块 import 和构造 `MediaChain` 等非消息用例不得新增日志或消息线程。 -- [x] 将 WebPush registry、主循环 execution gateway 和停止状态拆给各自 owner。 -- [x] 保留 `global_vars` 作为兼容薄门面,但禁止 canonical 宿主新增依赖。 -- [x] 将 initializer 的 provider 注册迁入显式装配阶段并提供测试 reset;冷导入回归测试覆盖 - Workflow、Scheduler、Command、Agent、技能目录和 LLM provider。 -- [x] 对本叶迁移的日志、消息、Scheduler、Workflow、Agent、Plugin 资源建立 owner 与生命周期门禁; - 全宿主原生并发原语清单不在本叶偷扩范围,保留为已取消 ARCH-111 的历史定义。 - -**验收** - -- 隔离进程冷导入前后比较线程名。 -- 正常模式和 safe mode 均验证启动顺序、有限关闭、超时保留 owner 和重试关闭。 -- 运行消息、Scheduler、Workflow、Agent、Plugin 和 lifecycle 专项测试。 -- 在 startup 到达 `yield` 前失败时,已经创建的日志/队列资源也必须收口。 - -```bash -.venv/bin/python -m pytest \ - tests/test_lifecycle_shutdown.py \ - tests/test_module_lifecycle.py \ - tests/test_system_notification_dispatch.py \ - tests/test_scheduler_lifecycle.py \ - tests/test_workflow_execution.py \ - tests/test_agent_lifecycle.py \ - tests/test_plugin_lifecycle_coordinator.py -q -``` - -### ARCH-107 消除 Chain SCC 并强化循环门禁 - -**问题与证据** - -- 当前宿主图只剩精确 containment 的 TMDB 移植包环;Chain 包内 SCC 已清零。 -- `ChainBase` 已迁到 `app/chain/base.py`;物理包根仅保留包说明,旧符号由 Compat/SDK 惰性解析。 -- `baseline.py --check-host` 与独立 SCC 语义测试共同拒绝 Chain 包内环、allowlist 成员增长和 - 包外模块通过反向边加入 TMDB containment。 - -**目标与步骤** - -- [x] 将 canonical `ChainBase` 实现移到 `app/chain/base.py`。 -- [x] 宿主全部改用 `app.chain.base.ChainBase`;包根不做 eager re-export。 -- [x] `from app.chain import ChainBase` 只通过 Compat/SDK lazy facade 兼容,并以独立测试 - 证明它没有被宿主 canonical 路径使用;同时把这一精确例外写入“不得延迟导入隐藏循环”的规则。 -- [x] 保证 `_messaging`、`_recognition`、`base` 任意冷导入顺序都不依赖部分初始化包。 -- [x] 对排除 `app/plugins/**` 的完整宿主图断言 SCC 集合等于精确 allowlist,而不是继续追加根前缀。 -- [x] TMDB 29 模块移植包允许正常单向包外依赖,但禁止 SCC 成员扩展到 allowlist 外、包外模块 - 通过反向边加入该 SCC,或新增不符合分层方向的边。 -- [x] 更新兼容、SDK、架构文档和 fixture 前先审查真实依赖变化。 - -**验收** - -- 冷导入 `app.chain.base`、`app.chain._messaging`、`app.chain._recognition` 的全部顺序组合。 -- 验证宿主当前包根导入全部迁移,并保留 `app/plugins/__init__.py` 等插件兼容探针。 - -```bash -.venv/bin/python -m pytest \ - tests/test_architecture_dependencies.py \ - tests/test_architecture_contract_baseline.py \ - tests/test_legacy_import_compat.py \ - tests/test_plugin_sdk.py -q -.venv/bin/python scripts/architecture/baseline.py --check-host -``` - -### ARCH-108 收口具体 Adapter 与 HTTP 边界 - -**问题与证据** - -- `app/application` 原有 8 个文件、13 条直接 Adapter 导入已全部迁移为 Application-owned Port, - startup 统一注入具体实现;Passkey、Backup 与 DNS 纵切面也已退出临时 policy。 -- `app/chain` 原有 8 个文件、13 条直接 Adapter 导入和 11 条普通 HTTP/Session bridge 已全部 - 迁移至注入 Port 或统一技术边界,当前具体 Adapter 与普通 direct HTTP 债务均为零。 -- Passkey Application 已改为消费启动注入的 `PasskeyChallengeCache`,不再判断 Redis 或导入 - 具体 cache adapter;Memory/Redis 均实现严格 `AtomicCacheBackend.store/consume`。 -- S0-L2.4b 已为 LLM streaming、第三方 SDK、移植库和本地控制面建立完整 egress identity 与 - zero-growth policy;11 条普通 HTTP/Session bridge 和 1 条 Application DNS I/O 债务已清零。 - 当前 48 条出口事实均为精确 containment,每条指纹由独立上界冻结,不能靠同时刷新 - baseline/policy 掩盖调用面增长,已删除债务也不得恢复。 - -**目标与步骤** - -- [x] 建立 Application/Chain 原始 Adapter 直连事实与精确临时 policy,冻结新增、替换和陈旧条目。 -- [x] 建立全宿主 direct egress 事实;SDK/stream/vendor/local-control 例外精确到 bindings/uses 指纹。 -- [x] 将 Passkey 原子领取提升为 runtime cache contract,由 Memory/Redis backend 分别实现。 -- [x] 为 Backup 定义 Application-owned artifact store Port,由 startup 注入文件系统实现。 -- [x] 将 policy 中 11 条普通 HTTP/Session bridge 债务迁移到统一网络能力并把目标收缩为空。 -- [x] 为 Application SSRF 校验注入 DNS 解析 Port,清除 `socket.getaddrinfo` 直接 I/O。 -- [x] Application 中命名外部产品、安全敏感能力及通用技术 Adapter 均改为注入 Port;不保留直连例外。 -- [x] Chain 中命名外部产品、安全敏感能力及通用技术 Adapter 均改为注入 Port;不保留直连例外。 -- [x] Adapter/direct HTTP 债务基线已收缩为零;其余 canonical transport、SDK、stream、vendor、 - diagnostic 和 control-plane 出口均按用途精确书面化,不使用宽泛豁免。 - -```bash -.venv/bin/python -m pytest \ - tests/test_passkey_challenge.py \ - tests/test_mfa_passkey_transactions.py \ - tests/test_database_backup_service.py \ - tests/test_database_backup_adapters.py \ - tests/test_rss_helper.py -q -.venv/bin/python scripts/architecture/baseline.py --check-host -``` - -### ARCH-109 按用例拆分超大编排器 - -**共同规则** - -- 保留旧 Chain/Manager/Scheduler 的公开方法和插件 ABI,内部委托新服务。 -- 先提取合同和行为测试,再迁移一条主调用链;不按文件行数机械切块。 -- 纯决策进入 Domain/Application policy,I/O 进入 Adapter/Port,运行状态进入明确 owner。 -- 每个切片应能独立验证、提交和回滚。 - -**建议切片** - -| 热点 | 建议所有权拆分 | 首要长方法/耦合点 | -|---|---|---| -| `SubscribeChain` | search、match、refresh、reference reconciliation、notification | 匹配和站点/规则引用更新 | -| `TransferChain` | queue/recovery、plan、execute、settle、history/notify | `_execute_transfer` 约 836 行 | -| `SearchChain` | plan、provider fan-out、result state、pagination | 并发状态与结果处理 | -| `DownloadChain` | selection、submission、history、post-processing | batch download 约 449 行 | -| `MediaChain` | recognition、plugin fallback、source projection、music alignment、cache | 保留公共识别 Facade,包根只惰性导出该类 | -| `Scheduler` | JobCatalog、ExecutionRegistry、domain reconciler、lifecycle Facade | `init` 约 336 行且直接构造多个 Chain | -| Agent API | WebAgent session/SSE/file/audio Application service | `_web_agent_stream_impl` 约 361 行 | -| System/Plugin API | nettest、logging、update、market use cases | 入口直接组合多个 Helper/Manager | - -`SubscribeChain` 切片已完成:`app.chain.subscribe.facade.SubscribeChain` 只保留稳定类身份、 -音乐构造接缝和事件入口委托;创建、搜索、匹配、刷新、完成、查询、交互、引用协调和通知实现 -分别位于同名 package 的单词文件中。包根不导出 owner 类,旧 `app/chain/subscribe.py` 不再存在, -公开方法和双下划线兼容属性仍由同一 `SubscribeChain` 类解析。 - -`TransferChain` 切片已完成:`app.chain.transfer.facade.TransferChain` 只组合稳定 MRO 与三个可替换 -Chain 构造点;队列/恢复、规划、执行、结算、历史/通知、请求构建及原 `_transfer.py` mixin -分别进入同名 package 的单词文件。包根只保留 `TransferChain` 与插件已使用的 `task_lock` 身份, -不重复导出 `JobManager`、durable runner 或内部 owner;旧 `transfer.py` 和 `_transfer.py` 均已删除。 - -`Scheduler` 切片已完成验证:旧 `app/scheduler.py` 已删除,稳定 Facade 与 -`SchedulerChain` 兼容类型由 `app.scheduler` 包根惰性导出;catalog、执行、事件循环桥接、进度、 -`ExecutionRegistry`、领域 reconcile、生命周期和维护任务分别位于同名 package 的单词文件。 -`app/startup/initializers/scheduler.py` 统一构造 Chain 与 `SchedulerServices`,Scheduler 包内不再无参 -构造业务 Chain;新插件使用 `app.sdk.scheduler` 的窄函数门面,内部 owner 不进入 SDK 或包根 ABI。 -功能、生命周期、架构、兼容和文档批次均通过,官方插件基线语义未变化。 - -`DownloadChain` 切片已完成验证:旧 `app/chain/download.py` 已删除,包根只惰性保留稳定 -`DownloadChain`;选择、提交、批量、缺集、失败冷却、历史结算、提交后处理、字幕、任务控制和 -技术端口分别由同名 package 的单词文件拥有,Facade 只组合 owner 与稳定删除事件入口。历史、文件和 -durable Outbox intent 在一个事务内提交,通知、后台处理和即时事件仅在 commit 成功后执行;提交失败 -不会留下历史或后置副作用。主程序不保留 `source.py`、旧实现或内部 owner/port 重复导出。 - -`SearchChain` 切片已完成验证:旧 `app/chain/search.py` 已删除,包根只惰性保留稳定 -`SearchChain`;plan、provider、pagination、result、cache、title、media、music、subtitle、site 与 -recommendation 各有单一 owner。列表和流式 provider 入口共享同一批次事实,分页任务纳入统一 -TaskRegistry,结果状态只由 `app.application.search.state` 持久化;Facade 保持直接 MRO、事件身份、 -LunaTV 三个私有补丁点和 HRBlocker 八个公开补丁点。主程序无 `source.py`、旧实现或内部 owner -重复导出;Search 专项 223 项、架构/兼容 188 项、官方 LunaTV 6 项和锁定全量 -`7222 passed, 9 skipped` 均通过。 - -`MediaChain` 切片已完成验证:旧 `app/chain/media.py` 已删除,包根只惰性保留 -稳定 `MediaChain`;recognition、plugin、auxiliary、projection、search、catalog、path、album -与 cache 各有单一 owner。Facade 保持直接 MRO、Singleton 类身份、pickle 路径和官方插件 -调用签名;目录缓存使用有界 LRU、内容签名、隔离副本与同步/异步单飞,并隔离等待者取消和 -符号链接目录别名。跨 Chain 音乐来源复用只依赖公开 `MusicMetadataSourceChain`,门禁拒绝跨 owner -导入下划线私有合同。刮削兼容符号只由 Compat overlay 提供,主程序不恢复旧实现或内部 owner -重复导出。Media/架构/插件端点集中回归 `551 passed`,锁定全量 `7244 passed, 9 skipped`; -Pylint 10/10、Ruff、mypy/复杂度 ratchet、宿主与最新官方插件基线均通过,官方 V3 插件聚焦 -验证 `130 passed`。 - -### ARCH-110 分可信级执行 Module/Event Contract(已取消) - -本项随 S4 于 2026-08-28 取消。以下内容只保留为历史提案,未勾选项不是本轮未完成债务。 - -**问题与证据** - -- Module 动态方法名已经归零,这是完成项;但 50 个结果形状仍是 `ANY`。 -- `app/runtime/extensions/module/dispatcher.py:463-494` 对签名和结果偏差只告警并继续透传。 -- Event 53 个 payload model 已覆盖,但当前全部为 diagnostic enforcement,输出合同也很少。 - -**原计划(已取消)** - -- [ ] 按“宿主内置 provider / 官方插件 / 第三方未知插件”划分可信级。 -- [ ] 下载、存储、消息等高风险宿主能力使用真实 Protocol/DTO,并在 admission 阶段 fail fast。 -- [ ] 官方插件先以 CI baseline 验证,再逐组切 strict。 -- [ ] 第三方插件继续诊断兼容,错误指标必须包含 method、provider、ABI source。 -- [ ] 不得为了严格化改变 `run_module` 字符串 ABI 或吞掉旧插件原始返回值。 - -```bash -.venv/bin/python -m pytest \ - tests/test_module_method_contracts.py \ - tests/test_module_invocation_dispatcher.py \ - tests/test_module_quality.py \ - tests/test_event_contracts.py -q -.venv/bin/python scripts/architecture/baseline.py --check-host --diagnostics -``` - -### ARCH-111 升级质量与架构趋势门禁(已取消) - -本项随 S4 于 2026-08-28 取消。现有 ratchet 继续生效,但以下扩张项不纳入本轮执行或完成计数。 - -**取消时记录的盲区** - -- 复杂度脚本只检查 API、Application、Chain 的公共入口;私有长方法、类/文件规模和圈复杂度不受控。 -- strict mypy 仅 41 个文件,高风险 lifecycle、Scheduler、Agent、Plugin Manager 多数不在 frontier。 -- coverage ratchet 已按文件覆盖除 `app/plugins` 外全部 `app`。 -- Task owner gate 尚未盘点原生并发原语;Event producer/consumer 已纳入统一事实源和人工 policy。 -- Ruff 仅是有限规则集的历史低水位,不代表整体风格/正确性无债务。 - -**原计划(已取消)** - -- [ ] complexity v2 增加私有方法、class/file 行数和圈复杂度;先记录低水位,再只降不增。 -- [ ] strict mypy frontier 按 `runtime extensions -> startup -> workflow/scheduler -> messaging -> Agent` - 扩大;每批必须先清零再加入配置。 -- [ ] coverage 增加 Chain、Runtime、Agent、Startup 的高风险子包或关键文件组,不用低价值行数冲百分比。 -- [ ] 原生并发门禁按 ARCH-101/106 修正。 -- [x] Event consumer 扫描证明 canonical EventManager receiver,清除同名方法误报。 -- [x] Event producer 别名/关键字/有限条件识别和 consumer exact policy 纳入统一事实源门禁。 -- [ ] Module Quality Scale 增加 capability -> required rules -> evidence tests 映射,避免“已登记”等同“已验证”。 -- [ ] 修改 CI 或门禁脚本时同时运行 `tests/test_architecture_ci.py` 和对应脚本单元测试。 - -## 7. P2:持续可维护性优化 - -### ARCH-201 收窄 Plugin 与 SDK 边界 - -- [x] `PluginHelper` 继续作为兼容入口,但市场、包、依赖、备份/恢复、健康修复分别委托现有 owner。 -- [x] `PluginManager` 的服务图构造移到 startup typed `PluginRuntime` factory,Facade 只保留稳定 API。 -- [x] `PluginHelper` 不进入推荐 SDK,仅保留 `app.helper.plugin` 精确 Compat;`app.sdk.plugin.manager` 只保留 - 审计过的 Manager ABI,不重复导出 canonical 实现。 -- [x] 用最新官方插件仓 baseline 和真实插件导入/兼容探针决定退场,不按宿主“无人引用”判断。 - -### ARCH-202 拆分 Agent 与 LLM provider - -- [x] 冻结官方插件真实 Agent/LLM 导入与属性调用;三条 `LLMHelper` 路径保持同一身份, - `MoviePilotTool`、`moviepilot_tool_manager._load_tools()` 保持原 owner 契约。 -- [x] 删除 `app.agent`、`app.agent.llm` 包根动态/重复导出;宿主改为 owner 导入,旧符号仅由 - 精确 Compat 承接,`app.helper.llm` 收窄为 `app.agent.llm.helper` 模块别名。 -- [x] 将内置 provider spec 数据从 `LLMProviderManager` 分离为只读 catalog。 -- [x] 模型发现、认证会话和运行时构建已拆至单词 owner。 -- [x] WebAgent 会话、文件/音频和事件编排已由 `app/application/messaging/agent.py` 负责; - API endpoint 仅保留 FastAPI 请求映射、HTTP 响应与 SSE framing。 -- [x] `AgentManager` 会话、生命周期和后台任务已拆到单词 owner,公开方法由 - `app.agent.AgentManager` 稳定 Facade 精确保持,包根不再无边界转发。 -- [x] `LLMProviderManager` 公开方法由稳定 Facade 精确保持;owner 已纳入 mypy 低水位。 - -### ARCH-203 拆分 Domain 投影与 Startup 高扇出 - -- [x] `MediaInfo`/`domain/context.py` 保留 canonical 类型路径;TMDB、豆瓣、Bangumi、AniList 规则已整体迁入 - `domain/projection/` 单词 owner,setter 只薄委托,输入不可变、多来源顺序、字段 golden 与宿主直接导入门禁覆盖; - 既有 `app.core.context` 精确兼容映射继续指向 canonical 类型,未新增 SDK/Compat。 -- [x] L4.1 已将配置快照、`RuntimeSettingsService`、`DatabaseWorker`、兼容事务 runner、查询服务与 - 插件持久化迁入既有 `startup/composition/{configuration,database}.py`;initializer 只调用组合 API。 - 数据库 runtime 拒绝重入,Worker 成功后才发布事务入口;成功关闭与启动失败会对称撤销本批 provider、 - 释放数据库引擎,并在模块 owner 收敛后清除 `app.state.host_runtime`。 -- [x] L4.2 已将 network、security、agent、server、outbox 与 Chain 装配迁入单词型 - `startup/composition/*` owner;Agent composition 在 Worker 启动后读取容量并共享同一数据和任务对象, - Chain 通过 provider 保持插件无参构造兼容,旧 Transfer command 只在真实调用时延迟导入。 -- [x] L4.3 已将 HostRuntime、命名领域 Runtime 和旧 `ApiDataPorts` 投影统一移到单词型 - `startup/composition/runtime.py`;`RuntimeDependencies` 由 Agent、Chain、HostRuntime 复用, - `HostRuntime.tasks` 显式绑定 lifecycle 的 TaskRegistry,Chain 不再二次构造 transfer execution。 -- [x] 生命周期按显式逆序 reset manifest 撤销全部 Provider;数据库 worker 未收敛时保留 - HostRuntime、Provider 与连接供诊断重试,启动失败复用同一正式清理路径且不反向物化 owner。 - -### ARCH-204 收敛 sync/async 重复与测试存量 - -- [x] 重复 sync/async 方法已提取共享纯逻辑;双入口 ABI 只保留 I/O 外壳,共享解析、校验、映射和状态决策。 -- [x] 已消除伪异步整段线程包装;Session/客户端按调用或事件循环持有,不跨并发边界复用。 -- [x] 本轮触及的 parity 测试均为 pytest-native,没有新增 `unittest.TestCase` 或无行为收益的全库转换。 -- [x] parity/生命周期测试通过 fixture 与 `try/finally` 恢复 singleton、cache、provider、事件循环和 `sys.modules`。 - -## 8. 建议实施顺序 - -### 阶段 0:恢复可信基线 - -1. 单独修复 ARCH-001,不刷新 mypy baseline。 -2. 完成 ARCH-101 的文档/门禁语义决策。 -3. 给 Adapter、原生并发、私有复杂度建立只读清单和 zero-growth 基线。 - -**退出条件**:当前全部 CI gate 通过;文档、fixture 和测试对 SCC、Adapter 例外、Oper 使用方式的描述一致。 - -### 阶段 1:先修可靠性和事务语义 - -1. ARCH-102 设计并迁移 Transfer E3 状态机。 -2. 以站点/规则引用清理为 ARCH-103/104 的第一个 typed Port + UoW 纵切面。 -3. ARCH-105 已完成 post-commit 结构化结果、独立 intent、Outbox 角色拆分、claim fencing 和 - commit/claim/publish/complete/通知/任务提交崩溃矩阵。 - -**退出条件**:故障注入覆盖清单列出的崩溃窗口;schema 有 migration;主路径不再 fail-open;调用方能区分 -业务提交与后置效果 pending。 - -### 阶段 2:收口进程边界 - -1. ARCH-106 将日志、消息队列和主循环 gateway 纳入 lifecycle。 -2. ARCH-107 拆出 `chain/base.py` 并消除新增 SCC。 -3. ARCH-108 的 Passkey、Backup、DNS、Application/Chain Adapter 与普通 HTTP 纵切面均已完成; - 现存协议出口只保留精确书面化例外并由 zero-growth 门禁守护。 - -**退出条件**:冷导入不启动线程;SCC 只剩 TMDB 精确豁免;Application/Chain 到 Adapter 的债务只降不增。 - -### 阶段 3:按热点做纵向拆分 - -建议顺序:Transfer -> Subscribe -> Scheduler -> Download/Search -> Agent API。每次只迁移一个完整用例, -旧 Facade 委托并同时运行新旧入口契约测试。 - -**退出条件**:复杂度低水位下降,公开 ABI 未变,文件移动没有新增跨层边或兼容映射滥用。 - -### 阶段 4:取消记录与 S5 收口 - -1. ARCH-110/111 随 S4 于 2026-08-28 取消,不再作为本轮完成依赖。 -2. ARCH-201 至 ARCH-204 的 Facade/Provider、Domain/Startup 与 sync/async 重复治理均已交付; - S5-L6 已通过精确 head `c204e2e97` 的 Unit Tests `33269394727` 与 Pylint `33269394716` 完成闭环。 -3. 保留现有 strict frontier、复杂度、mypy、Ruff、覆盖率和并发 ratchet,不借取消阶段接受回退。 - -**退出条件**:S5 全部叶子通过现有门禁与官方插件兼容验证,canonical 无旧实现和重复导出。 - -## 9. 每个实施切片的统一门禁 - -先运行受影响测试,再按风险扩大。架构、持久化、启动、生命周期、兼容层或跨模块改动必须运行全量套件。 - -```bash -# 当前宿主架构与趋势门禁 -.venv/bin/python scripts/architecture/baseline.py --check-host -.venv/bin/mypy --config-file mypy.ini -.venv/bin/python scripts/architecture/complexity.py -.venv/bin/python scripts/architecture/async_blocking.py -.venv/bin/python scripts/architecture/task_ownership.py -.venv/bin/python scripts/architecture/service_locator.py -.venv/bin/python scripts/architecture/ruff_ratchet.py -.venv/bin/python scripts/architecture/mypy_ratchet.py -.venv/bin/python scripts/startup/performance.py --check --repeat 3 - -# 架构专项 -.venv/bin/python -m pytest \ - tests/test_architecture_dependencies.py \ - tests/test_architecture_contract_baseline.py \ - tests/test_architecture_baseline_cli.py \ - tests/test_complexity_gate.py \ - tests/test_async_blocking_gate.py \ - tests/test_task_ownership_gate.py \ - tests/test_quality_ratchets.py \ - tests/test_mypy_gate.py \ - tests/test_architecture_ci.py -q - -# 广泛变更的最终本地回归 -uv run --locked --no-sync python tests/run.py - -# 需要覆盖率证据时按 CI 的 8 个分片采集并合并,再检查低水位 -for shard in 1/8 2/8 3/8 4/8 5/8 6/8 7/8 8/8; do - uv run --locked --no-sync python -m coverage run --parallel-mode tests/run.py --shard "$shard" -done -uv run --locked --no-sync python -m coverage combine -uv run --locked --no-sync python -m coverage json -uv run --locked --no-sync python scripts/architecture/coverage_ratchet.py - -# Python 源码改动对实际变更路径运行(按切片替换示例路径) -uv run --locked --no-sync pylint app/path/to/changed.py -``` - -附加要求: - -- fixture 只有在依赖/合同变化经过人工审查后才能用 `--write-host` 更新,不能为转绿接受新增债务。 -- 所有 DB schema 变化必须新增 Alembic migration。 -- Plugin ABI、SDK/Compat 映射和官方插件仓验证独立执行;宿主绿灯不能替代插件兼容证据。 -- 正式插件 ABI 验证前先同步独立插件仓并记录所验 SHA,再运行: - `.venv/bin/python scripts/architecture/baseline.py --check-plugins --plugin-repo ../MoviePilot-Plugins`。 -- 每个阶段使用独立、可回滚的提交;不要把一个专项绿灯表述成整个长期目标完成。 -- 完成一次所有权迁移后,同步更新 canonical import、架构规则、SDK/Compat(仅在确有公共迁移时)和测试。 -- 依赖变更还必须运行 `uv lock --check`、锁定环境检查和 - `docs/rules/03-commands.md` 规定的运行依赖漏洞审计。 - -## 10. 明确不做 - -- 不恢复物理 `app/core`、`app/helper`、`app/utils`、`app/log.py`。 -- 不删除精确 Compat 映射、`app.sdk._legacy`、Chain/Manager 稳定 Facade 或旧插件 ABI。 -- 不把 `app/plugins/**` 当作宿主实现重构;官方插件仓单独验证。 -- 不把所有 `run_module` 字符串调用一次性替换成静态 import。 -- 不把所有后台工作改造成持久队列;仅按 ADR 判定为需要跨重启完成的 E2/E3 动作进入 durable 机制。 -- 不机械拆 TMDB 移植包 SCC;当前只做精确 containment,未来通过替换/升级移植库处理。 -- 不为降低行数进行无合同、无行为测试的目录搬运。 -- 不一次性删除 sync/async 双入口、全局兼容对象或无参构造 ABI。 -- 不用更新 baseline、降低门禁或增加宽泛 allowlist 掩盖回归。 - -## 11. 已完成且应继续保护的边界 - -- [x] 旧 `core/helper/utils/log` 物理源码已移除,Compat 使用精确、惰性映射。 -- [x] canonical 实现层当前无 SDK/Compat 反向违规,`app/__init__.py` 和插件启动扫描等 bootstrap - 例外保持精确;插件旧导入仍可用。 -- [x] Model/Oper 自建 Session、自动写事务和直接 commit/rollback 债务为零。 -- [x] HostRuntime、显式 Session/UoW、订阅/站点/Workflow 等参考切片已经落地。 -- [x] TaskRegistry owner、Workflow 有界关闭、Scheduler/Agent/Plugin/Message 生命周期已有专项保护。 -- [x] durable event/outbox 已覆盖关键事件切片;后续是按完成语义扩面,不是推倒重写。 -- [x] Module 动态方法名为零;Event payload model 已覆盖全部登记事件。 -- [x] `app/plugins/**` 与宿主架构基线分离,SDK/Plugin ABI 是独立验收面。 - -这些完成项是后续优化的护栏。任何阶段若重新引入 Model/Oper 自建事务、物理旧路径、无 owner 资源 -或宿主到插件实现的依赖,即使局部测试通过,也应视为架构回退。 diff --git a/docs/refactor/refactor-roadmap.md b/docs/refactor/refactor-roadmap.md deleted file mode 100644 index 856ae1020..000000000 --- a/docs/refactor/refactor-roadmap.md +++ /dev/null @@ -1,618 +0,0 @@ -# MoviePilot V3 架构重构路线图 - -> 战略目标:完成 S0-S3 与 S5 的宿主架构重构和最终收口;S4 按维护者决定取消, -> S3 完成后直接进入 S5,S5 完成即结束本轮战略任务。 -> -> 执行分支:`v3`;每个叶子完成后独立验证、提交、推送并确认远端 SHA。 -> -> 排除范围:`app/plugins/**` 是插件运行时副本,不参与宿主重构。 -> -> 兼容原则:新插件只使用 `app.sdk`;旧插件导入、符号和行为只由统一 Compat/Legacy 层承接。 - -## 0.1 当前追踪目标:G-ARCH-RESIDUAL(COMPLETE) - -原 G-ARCH 的 S0-S3、S5 交付记录和 S4 `CANCELLED` 决定保持不变。基于 2026-08-30 -对 exact-head 的复核,另行建立本次追踪目标 `G-ARCH-RESIDUAL`,只收口审计已经明确的残余 -语义和治理缺口,不把 S4 的全量质量债务重新作为隐性依赖。 - -| Leaf | 状态 | 依赖 | 退出条件 | -|---|---|---|---| -| R0 事实基线刷新 | `DELIVERED` | 无 | `87269610e`:exact-head、远端、工作树、CI 和五类残余问题证据冻结;未修改 `app/plugins/**` | -| R1 System API 边界收口 | `DELIVERED` | R0 | `d50177f45`:日志、Wiki、配置、事件、更新与重启进入 `application/system.py` 和 `composition/system.py`;endpoint 只保留 HTTP/SSE/ZIP/响应映射 | -| R2 复杂度与原生并发门禁 | `DELIVERED` | R0 | `f7ca7e517`、`5cd5780d3`:完整 AST、Scheduler、canonical alias、TaskGroup、稳定 owner/count 与低水位门禁落地;本范围新增债务为零 | -| R3 Outbox after_commit 恢复语义 | `DELIVERED` | R0 | `9d06f91bb`:下载提交后通知、模块和字幕副作用改为同事务持久 intent,具备 handler、claim/fencing、重启回放、幂等与失败观测 | -| R4 Startup/插件市场单一装配 | `DELIVERED` | R0 | `7f5b8b469` 至 `046b0b305`:市场 Transport/Package/Dependency/Health、Domain、网络、站点、Chain、缓存、资源、DoH 和 Workflow 构造归入 composition;Compat 复用同一 owner | -| R5 验收与交付收口 | `DELIVERED` | R1,R2,R3,R4 | 路线图、优化清单、规则和机器门禁一致;除 `app/plugins` 外全部 `app` 文件执行逐文件覆盖率低水位门禁,新文件至少 80%,Pylint `10.00/10`、架构/兼容门禁、真实启动和最终 exact-head GitHub CI 通过;`HEAD == origin/v3` 且 ahead/behind 为 `0/0` | - -执行合同:任一时刻只允许一个 `ACTIVE` 叶子;每个叶子必须独立验证、显式提交和推送, -并在交付前复核插件 ABI、canonical 旧实现/重复导出和 `app/plugins/**` 排除边界。 - -R5 以 2026-08-30 的精确代码头完成验收:四个 GitHub Unit Tests 分片和 Architecture Contract -Gate 在运行 `33293899659` 中通过,Coverage Report 生成并上传同一份 `coverage.json`;该运行只因 -Application 低水位从 `81.61%` 提升到 `81.77%` 后尚未固化而失败。R5 使用相同 locked 命令在 -本地重新生成报告并写入整数计数,随后由最终文档/基线提交对应的 exact-head CI 复核。真实进程使用 -临时配置目录完成启动,`/health/live` 与 `/health/ready` 均返回 HTTP 200,并完成有序关闭。 - -## 1. 治理合同 - -### 1.1 Goal 层级 - -- **G-ARCH(父目标)**:宿主架构、模块职责、持久化、生命周期、合同和质量债务全部达到本路线图终态。 -- **Stage(阶段)**:一组有共同退出条件的能力面,可包含多个独立叶子。 -- **Leaf(叶子)**:单一所有权面、可独立实现、验证、提交、推送和回滚的最小交付单元。 - -原生 Codex Goal 保存 G-ARCH 的稳定目标;本文件保存阶段、叶子、依赖、状态和停止条件。 - -### 1.2 状态 - -| 状态 | 含义 | -|---|---| -| `ACTIVE` | 当前唯一允许编辑的叶子 | -| `PLANNED` | 已定义边界,依赖满足后可激活 | -| `BLOCKED` | 有明确阻塞证据,且没有可推进的替代叶子 | -| `VERIFIED` | 本地验收完成,尚未推送或尚未确认远端 | -| `DELIVERED` | 已提交、推送,远端 SHA 与本地交付一致,且本叶要求的远端 CI 已闭环 | -| `COMPLETE` | 叶子验收和后续依赖均已结清 | -| `CANCELLED` | 经维护者明确取消,不进入执行、验收或完成计数 | - -任何时刻只允许一个 `ACTIVE` 叶子。子代理可以并行做只读审计、测试设计和下一叶准备;只有当前 -叶子的明确文件所有者可以写入源码。共享工作树中发现的并发改动必须先确认归属,不能覆盖。 - -### 1.3 叶子完成条件 - -一个叶子只有同时满足以下条件才可标记 `DELIVERED`: - -1. 所有定义在该叶子内的债务计数降为零,或变为有业务理由、精确路径、机器约束的非债务例外。 -2. 真实调用链已切换到新 owner;不能只增加新类、Port、DTO、门禁或测试而保留主路径走旧实现。 -3. canonical 主程序删除被替代的旧实现、兼容分支和重复导出,不保留“新旧两套正式入口”。 -4. 插件兼容只落在 `app/sdk`、`app/sdk/_legacy`、`app/runtime/compat` 或经批准的稳定 Facade; - canonical 实现不得为了兼容反向依赖这些层。 -5. 受影响专项、架构门禁、strict mypy、Ruff/mypy ratchet、scoped pylint 全部通过。 -6. 广泛架构、启动、生命周期、持久化或兼容变更运行锁定全量测试;插件 ABI 变更独立检查官方插件仓。 -7. diff、工作树、提交范围经过主线程审查;提交推送后 `HEAD == origin/v3` 且 ahead/behind 为 `0/0`。 - -不得以以下状态宣称完成:只建立 baseline、只新增抽象未迁移调用方、只留下 TODO、只让新测试通过、 -只完成目录移动、只在兼容层外保留旧导出,或用 `--write` 接受新增债务。 - -## 2. 最终停止条件 - -G-ARCH 只有在 S3、S5 及以下条件全部满足后才可完成;S4 不属于本轮完成范围: - -- [x] `ARCH-001`、`ARCH-101` 至 `ARCH-109`、`ARCH-201` 至 `ARCH-204` 全部完成;`ARCH-110/111` 随 S4 取消且不作为完成依赖。 -- [x] 宿主依赖图没有未解释 SCC;只允许精确 containment 的 TMDB 移植包例外,成员不能增长。 -- [x] Application/Chain 到具体 Adapter、direct egress,以及本轮迁移资源的并发 owner 例外均精确、可解释、不可增长。 -- [x] Chain/Agent 正式数据入口不再注入无 Session Oper,不再以 `Any`/ORM 作为跨层合同。 -- [x] 正式业务写入均有单一事务 owner;E2/E3 动作完成语义、幂等、恢复与人工决策路径一致。 -- [x] import、普通对象构造不启动进程资源;资源由 bootstrap/lifecycle 显式创建、发布、关闭和重试收口。 -- [x] S3、S5 各叶子定义的类型、复杂度、覆盖率和并发债务清零,现有全局 ratchet 不发生回退。 -- [x] canonical 主程序无旧实现、重复导出和 legacy import;插件兼容检查基于同步后的官方插件仓 SHA 通过。 -- [x] 锁定全量测试、Pylint、依赖一致性和最终远端一致性全部通过;本轮未修改依赖。 - -## 3. 阶段与叶子 - -### S0:可信基线与事实源 - -退出条件:主线门禁全绿;架构规则、总览、机器快照和语义门禁一致;新增债务不能靠更新 fixture 进入。 - -| Leaf | 状态 | 依赖 | 完成定义 | -|---|---|---|---| -| S0-L1 可信基线恢复 | `DELIVERED` | 无 | `5df388719`:交付架构审计/路线图,修复 `ARCH-001` 两个 mypy 增量错误;远端 `0/0` | -| S0-L2.1 Host Oper/UoW 规范 | `DELIVERED` | S0-L1 | `3bf94ffed`:宿主无 Session Oper 规范债务归零,远端 `0/0` | -| S0-L2.2 完整宿主 SCC policy | `DELIVERED` | S0-L2.1 | `a884ab5c2`:完整宿主 SCC 精确 policy 生效,远端 `0/0` | -| S0-L2.3 Adapter 直连事实 | `DELIVERED` | S0-L2.1 | `e1483e85d`:锁定 28 条原始 Adapter import 事实,远端 `0/0` | -| S0-L2.4 Adapter zero-growth | `DELIVERED` | S0-L2.3 | `2553226f3`:冻结 28 条直连及 owner,收缩/新增/stale policy 门禁生效,远端 `0/0` | -| S0-L2.4b HTTP/Egress 事实与政策 | `DELIVERED` | S0-L2.4 | `47f0de745`、`43d52a35b`、`8d602149f`:冻结 66 条出口事实,消除 CI 类型/覆盖率漂移;远端全绿且 `0/0` | -| S0-L2.5 Event consumer 识别 | `DELIVERED` | S0-L2.1 | `86157be2a`:consumer 只识别可静态证明的 EventManager 注册;全量 6,459 passed / 6 skipped,CI `33029645165`/`33029645254` 全绿,远端 `0/0` | -| S0-L2.6 事实源与 CI 投影 | `DELIVERED` | S0-L2.2,S0-L2.4b,S0-L2.5 | `113355784`:99/17 条逐调用事实、17 条 consumer policy 与 CI 分层交付;Unit Tests `33031697902`、Pylint `33031697785` 全绿,远端 `0/0` | - -### S1:可靠性、事务与数据合同 - -退出条件:Transfer 达到 E3;正式数据 Port 类型化;跨表业务操作有单一 UoW;post-commit/Outbox -竞争、失败呈现、at-least-once 和幂等语义全部闭环。 - -`S1-L1` 是 ARCH-102 的 Transfer E3 父项,当前状态为 **DELIVERED**。`S1-L1.5` 聚合交付 -`S1-L1.1` 至 `S1-L1.4` 的阶段性结果已由最终精确 head 统一交付;真实调用链已完成迁移,旧 fail-open、重复状态和 -兼容层外旧入口已退出 canonical 主程序,兼容只经统一 Compat/SDK 门面提供。 - -| Leaf | 状态 | 依赖 | 完成定义 | -|---|---|---|---| -| S1-L1.1 Durable admission | `DELIVERED` | S0 | Application-owned typed Port + DB adapter + migration 落地;先持久 commit 再入队,入队失败保留可恢复记录;宿主不再通过 raw/`Any` `TransferPendingOper` 处理 admission | -| S1-L1.2 Planning checkpoint | `DELIVERED` | S1-L1.1 | 版本化输入与指纹先持久化;无 legacy provider 时以 `accepted -> planned` CAS 提交完整计划,有 provider 时先提交 `provider_pending`,全部返回空后再以第二次 CAS 提交 `planned`;重放只执行冻结目标,所有文件副作用晚于对应 checkpoint commit | -| S1-L1.3 Lease 与恢复调度 | `DELIVERED` | S1-L1.2 | claim/lease/heartbeat/attempt 与过期接管规则落地;启动回放和同进程恢复共用唯一调度入口,同一任务同时只有一个 worker owner | -| S1-L1.4 幂等执行与终态结算 | `DELIVERED` | S1-L1.3 | 文件操作、历史提交和 checkpoint 可重放;唯一 retry owner 生效,未知外部结果进入 `manual_review`,仅完整终态删除 pending | -| S1-L1.5 E3 全链收口 | `DELIVERED` | S1-L1.4 | `e9de149db`、`a2e249f20`:崩溃矩阵、3.0.17 升降级、重复回放和插件 ABI 验收完整;旧 fail-open、重复状态与兼容层外旧入口删除;Unit Tests `33092427327`、Pylint `33092427348` 全绿,ARCH-102 债务归零 | -| S1-L2 Workflow typed query | `DELIVERED` | S0 | `b4f873654`、`a01a35bcb`:Workflow Application Port 不返回 `Any`/ORM,Session 内投影冻结 DTO,正式调用方全部切换;Unit Tests `33098869736`、Pylint `33098869837` 全绿,覆盖率低水位提升至 Application `78.78%` | -| S1-L3 Chain/Agent typed data ports | `DELIVERED` | S1-L2 | User、History、Site、Subscription 的冻结 DTO/typed Port 已完成;`ChainDataPorts`/`AgentDataPorts` 与 raw Oper/`Any` factory 清零,旧公开 ABI 仅由 Legacy/Compat 承接 | -| S1-L3.1 Workflow typed execution | `DELIVERED` | S1-L2 | `17d8be2af`、`b33b29876`:Chain 直连类型化事务服务且单次执行只取一个 port;canonical Oper 删除旧 writer/无 Session 写方法,旧 ABI 只在 `_legacy/workflow.py` 与 Compat overlay;Unit Tests `33103913838`、Pylint `33103913935` 全绿,Application 覆盖率低水位提升至 `78.79%` | -| S1-L3.2 Chain registry/DI | `DELIVERED` | S1-L3.1 | 显式类型化 context,删除 PortProxy、全局数据 getter 与失效的双重注入,构造器注入真实控制调用 | -| S1-L3.2.1 Registry hygiene | `DELIVERED` | S1-L3.1 | `ac7a20132`:删除零消费者 PortProxy/动态转发和 `ChainRuntimeContext.data_ports` 伪注入;Workflow 退出 Chain registry,只保留 Application owner 单一配置入口;Unit Tests `33120205586`、Pylint `33120205581` 全绿 | -| S1-L3.3 DownloadFailure/MediaServer | `DELIVERED` | S1-L3.2 | `5fb62108a`:两组 raw factory 已替换为冻结 DTO/typed Port;失败冷却在 Session 内投影,媒体库查询只返回标量且每个 upsert/cleanup 独立短事务,远端枚举不持有 Session;旧 Oper 与插件可见 Chain ABI 保持不变;Unit Tests `33127544925`、Pylint `33127544927` 全绿,Application 覆盖率低水位提升至 `78.95%` | -| S1-L3.4 User | `DELIVERED` | S1-L3.3 | User Chain/Agent/认证改用冻结 typed snapshot;创建、更名、删除与最后一个启用超级管理员保护归并到单 UoW;用户名唯一索引及 UserConfig/PassKey 级联迁移落地,UserConfig 在 commit 后持写锁重载数据库事实源并发布内存快照 | -| S1-L3.5 History | `DELIVERED` | S1-L3.4 | DownloadHistory 与 TransferHistory 均已迁入深度冻结 DTO、typed query/write/staging Port 与短 Session adapter;请求级删除和 durable 结算各自保持单一 UoW,canonical 调用方不再接收 raw Oper/ORM,旧写入 ABI 只由 SDK Legacy/Compat 提供 | -| S1-L3.6 Site | `DELIVERED` | S1-L3.5 | Site 配置、用户数据、图标和统计统一为深度冻结 DTO 与 typed query/write/staging Port;请求写入复用 AsyncSession,Chain/Agent 使用独立短事务,canonical 不再接收 raw Oper/ORM,旧插件 ABI 只经 SDK Legacy/Compat | -| S1-L3.7 Subscription | `DELIVERED` | S1-L3.6 | `application/subscription/contract.py` 统一深度冻结 Snapshot/History/Identity/Patch 与 typed query/write/staging Repository;DB adapter 在 Session 内完成 ORM 投影,Chain/API/Agent/Workflow/interaction 不再消费 raw Oper/ORM/`Any`;旧 `SubscribeOper`/`SubscribeHistoryOper` 只经同一 SDK Legacy/Compat 门面保留插件 ABI | -| S1-L3.8 Agent/Transfer locator gate | `DELIVERED` | S1-L3.7 | `chain/data.py`、`agentdata.py` 及其 getter 已删除;Chain/Agent 改用显式 typed context,AST 门禁确认 canonical 无 raw getter/Oper/Any;全量有效 `6955 passed, 9 skipped`,Application 覆盖率提升至 `79.83%`,依赖边降至 `7,062` | -| S1-L4 Subscription mutation UoW | `DELIVERED` | S1-L3.8 | `d9e4a973c`:新增、修改、删除、完成均由显式 Session-bound Command/UoW 拥有事务;Servarr 多季新增在一个请求事务内提交全部订阅和 durable intents,任一季失败整批回滚;canonical 自动提交写入口与 locator 已退出,专项联合测试 327 项通过;当前依赖事实为 7,091 条边,远端 `0/0` | -| S1-L5 站点/规则引用原子清理 | `DELIVERED` | S1-L4 | `d9e4a973c`:站点、规则组及自定义规则改名的 SystemConfig+Subscribe 共享一个 UoW,双事实 CAS 防止过期覆盖,commit 后一次发布配置快照;故障注入、通配重置、并发冲突与事件循环心跳测试通过,远端 `0/0` | -| S1-L6 Outbox 完成语义 | `DELIVERED` | S0 | 事务内 `OutboxStager` 与独立短事务 `OutboxDispatchStore` 已分离;即时投递与 dispatcher 均先 claim,complete/retry 受 attempt fencing;`PostCommitResult` 区分已提交业务、已完成与 pending effect。事件载荷和宿主 correlation context 携带稳定 event key;旧通知插件保持原签名并承认 at-least-once 重复边界 | - -### S2:进程生命周期、循环与 Adapter 边界 - -退出条件:导入/构造零资源副作用;Chain SCC 清零;Application/Chain 只依赖经批准的技术合同, -安全和命名外部能力全部经注入 Port。 - -| Leaf | 状态 | 依赖 | 完成定义 | -|---|---|---|---| -| S2-L1 日志/消息资源显式生命周期 | `DELIVERED` | S0 | 配置冷导入和非消息 Chain 构造零新增日志/消息线程;lifespan 显式创建共享 owner,启动失败、关闭超时、重试及连续两轮 lifespan 均收口;Chain 轻量客户端不保留首个回调 | -| S2-L2 ChainBase 与 SCC 清零 | `DELIVERED` | S0-L2.2 | canonical `app.chain.base` 已落地,包根无 eager/重复导出,宿主包根导入清零,旧符号只经 `app.sdk.chain` 与 Compat 保持同一类身份;完整宿主 SCC 只剩精确 containment 的 TMDB 移植包 | -| S2-L3 GlobalVar/provider 注册收口 | `DELIVERED` | S2-L1 | 停止、主循环和 WebPush 各有唯一 runtime owner,canonical `global_vars` 消费清零并由 AST 门禁冻结;Workflow、Scheduler、Command、Agent、技能和 LLM provider 均在生命周期显式 configure/reset,冷导入保持未注册;Legacy SDK 门面继续共享同一事实源 | -| S2-L4 Passkey 缓存边界 | `DELIVERED` | S0-L2.4 | `PasskeyChallengeCache` 由 startup 注入,Application 不识别 Redis;严格 `AtomicCacheBackend.store/consume` 由 Memory/Redis backend 分别实现,challenge 仅能被原子领取一次 | -| S2-L5 Backup artifact Port | `DELIVERED` | S0-L2.4 | Application-owned `BackupArtifactStore`/factory 覆盖创建、发布、清理、列举、解析、删除和元数据读取;startup 注入唯一 `BackupFiles` 实现,Application 具体 Adapter 直连由 14 降至 13,完整备份调用链 62 项通过 | -| S2-L6 Application Adapter/DNS 债务清零 | `DELIVERED` | S2-L4,S2-L5 | Application 自有 DNS、图片、消息回环、RSS、站点登录、种子、系统加速 Port,startup 统一注入;13 条具体 Adapter 边及 Application DNS I/O 均归零,未装配和失败回滚语义已验证 | -| S2-L7 Chain Adapter/宿主 HTTP 债务清零 | `DELIVERED` | S2-L6 | Chain 具体 Adapter 与 11 条普通 direct HTTP/Session bridge 均归零;当前 53 条出口事实全部属于精确 SDK/stream/vendor/canonical containment,policy 无 unreviewed、stale 或指纹漂移 | - -### S3:大型编排器职责清零 - -退出条件:每个热点 Facade 只保留稳定公开入口;决策、I/O、状态和生命周期各有单一 owner; -旧实现从 canonical 文件删除,现有复杂度 ratchet 与叶子结构门禁对该所有权面无新增债务。 - -| Leaf | 状态 | 依赖 | 完成定义 | -|---|---|---|---| -| S3-L1 TransferChain | `DELIVERED` | S1-L1 | 旧 `transfer.py`/`_transfer.py` 单体退役,由 `app.chain.transfer` 同名职责包承接;queue/recovery、plan、execute、settle、history/notify 各有唯一 owner,原 836 行执行方法消失,包根只保留稳定 ABI | -| S3-L2 SubscribeChain | `DELIVERED` | S1-L5 | 旧 `subscribe.py` 单体退役,由 `app.chain.subscribe` 同名职责包承接;search、match、refresh、completion、reference reconciliation、notification 各有唯一 owner,包根只保留稳定 `SubscribeChain` | -| S3-L3 Scheduler | `DELIVERED` | S2-L3 | 旧 `app/scheduler.py` 已退役并迁入 `app.scheduler` 同名包;catalog、execution/bridge/progress、`ExecutionRegistry`、reconciler、lifecycle 与 maintenance 已分离,业务 callable 由 startup 经 `SchedulerServices` 注入,包内无参 Chain 构造清零;功能/生命周期 215 项、架构/兼容/文档 189 项通过,官方插件基线语义未变化 | -| S3-L4 DownloadChain | `DELIVERED` | S1-L6 | `7941fd921`:旧单体退役;selection、submission、batch、history、post-processing 等职责进入同名包单一 owner,提交后动作遵守新完成语义;锁定全量 `7162 passed, 9 skipped`,远端 `0/0` | -| S3-L5 SearchChain | `DELIVERED` | S2-L7 | `1c145d716`:旧单体退役并由同名职责包承接;稳定 ABI、官方插件兼容、类型/复杂度门禁和锁定全量 `7222 passed, 9 skipped` 通过;远端 `0/0` | -| S3-L5.1 Runtime dependencies 命名治理 | `DELIVERED` | S3-L5 | `13fba96fd`:平铺模块退役为 `runtime/dependencies/` 同名包;单词子模块、包根无重复导出、旧快照回滚兼容、命名规则与机器门禁均已交付,锁定全量 `7224 passed, 9 skipped`,远端 `0/0` | -| S3-L6 MediaChain | `DELIVERED` | S2-L2 | `13c4a71f4`:旧单体退役并由同名职责包承接;稳定 ABI、官方插件兼容、缓存并发/别名语义、类型/复杂度门禁和锁定全量 `7244 passed, 9 skipped` 通过;远端 `0/0` | -| S3-L7 Agent/System/Plugin API | `DELIVERED` | S2,S1-L6 | `f38637dce`:WebAgent 会话/file/audio、System nettest 和 Plugin market/rating/release/folders 用例进入 Application;endpoint 只保留传输适配,锁定全量 `7256 passed, 9 skipped`、官方插件基线和远端 `0/0` 通过 | - -### S4:可执行合同与质量债务清零 - -**阶段状态:`CANCELLED`(2026-08-28 维护者决定)** - -本阶段不再执行,也不作为 S5 或父目标完成的依赖。S4 原定义保留为取消记录, -不得把 `CANCELLED` 写成 `COMPLETE` 或用 S5 顺带扩张为全宿主质量债务清零。 - -| Leaf | 状态 | 依赖 | 完成定义 | -|---|---|---|---| -| S4-L1 Module strict contract | `CANCELLED` | — | 原计划取消 | -| S4-L2 Event strict contract | `CANCELLED` | — | 原计划取消 | -| S4-L3 Complexity v2 | `CANCELLED` | — | 原计划取消 | -| S4-L4 全量 mypy 清零 | `CANCELLED` | — | 原计划取消 | -| S4-L5 Ruff 治理债务清零 | `CANCELLED` | — | 原计划取消 | -| S4-L6 Coverage/并发/质量证据 | `CANCELLED` | — | 原计划取消 | - -#### S4 Lite:后续增量治理边界(非阶段) - -S4 Lite 不是新的 Stage、Leaf 或 Goal,不设置状态、进度或统一完成日期,也不改变 S4 -`CANCELLED` 的事实。它只约束后续实际触及相关 owner 时的增量治理,避免取消全量清零后失去 -防回退边界: - -- Module/Event 合同继续以插件兼容为硬边界。第三方插件的合同校验保持 diagnostic;只有宿主 - 内置的高风险能力或 durable internal event,才可在独立变更中引入 strict contract - enforcement,并补齐对应行为、生命周期和失败恢复证据。这里的 strict 仅指合同校验策略; - 现有 `run_module_strict` / `send_event_strict` 是调用方选择的异常传播接口,仍可触达插件 - provider,不属于本条收窄范围。 -- 宿主升级不得要求已可用插件先行适配。`run_module` / `async_run_module` 的字符串方法 ABI、 - 参数兼容、聚合与原始返回形状,以及现有 SDK/Compat/Legacy 行为保持稳定;若增量 strict - 治理导致既有插件失效,首先按宿主兼容回归处理,而不是把迁移责任转给插件。 -- complexity v2、原生并发、mypy、Ruff 和 coverage 保留现有 zero-growth/低水位 ratchet; - 新增或实质修改高风险 owner 时补对应专项证据,但不恢复全宿主历史债务清零目标。 -- 每项治理必须在独立变更中说明 owner、风险、兼容影响和退出条件,并通过受影响宿主路径与插件 - 兼容验证;不得以刷新 baseline、增加文档或只通过静态快照宣称语义完成。 - -### S5:Plugin、Agent、Domain、Startup 与最终收口 - -退出条件:剩余 Facade/Manager/Domain/Startup 重复职责清零;官方插件兼容、全量测试和远端交付完成。 - -| Leaf | 状态 | 依赖 | 完成定义 | -|---|---|---|---| -| S5-L1 PluginHelper/PluginManager | `DELIVERED` | S2,S3 | `fa40f29df`:市场/包/依赖/备份/健康服务各归 owner;Facade 只转发稳定 ABI,构造归 typed PluginRuntime | -| S5-L2 Agent/LLM provider | `DELIVERED` | S3-L7 | `c000c4fff`、`3a33da0b3`、`dbdf7d058` 已在 `origin/v3` 祖先链:catalog、发现、认证、session、runtime 分离;Manager/Facade 只保留稳定 API,最终精确 head CI 已闭环 | -| S5-L3 Domain projection | `DELIVERED` | S3-L6 | `MediaInfo` canonical 路径和 setter ABI 保留;四来源纯投影归入单词 owner,输入不可变、重复业务语义清零 | -| S5-L4 Startup composition | `DELIVERED` | S2,S3,S5-L1,S5-L2 | 领域构造全部归单词型 composition owner;HostRuntime/ApiData 同源;显式逆序 reset manifest 在 worker 收敛后撤销 Provider,失败保留完整诊断边界 | -| S5-L5 Sync/async 重复清零 | `DELIVERED` | S3,S5 | 双 ABI 外壳共享纯逻辑,重复业务实现清零,Session/客户端不跨并发边界复用 | -| S5-L6 最终兼容与交付 | `DELIVERED` | S3,S5-L1,S5-L2,S5-L3,S5-L4,S5-L5 | canonical 旧实现/重复导出清零;官方插件仓、锁定全量、Pylint、架构、类型、覆盖率与远端一致性全部通过,本轮战略任务结束 | - -## 4. 叶子合同与交付记录 - -### S3-L5.1 Runtime dependencies 命名治理 - -**Status:** `DELIVERED` - -**Outcome** - -退役平铺且语义相似的 `app/runtime/dependencies.py` 与 -`app/runtime/native_dependencies.py`,由同名 `app.runtime.dependencies` 包承接。 -`profile.py` 只拥有运行环境依赖分组与同步参数,`native.py` 只拥有已加载原生发行版快照和变更探测; -包根不放实现、不重复导出内部符号,canonical 宿主调用方直接导入职责子模块。 - -同步把该决策写入 `docs/rules/07-naming-conventions.md`:新增生产模块优先使用准确的单个小写单词; -同一能力需要多个文件时建立同名单词目录包,再使用单词职责文件;禁止在父目录平铺相似复合文件名、 -用 `source.py` 保存旧实现,或通过包根重复导出内部 owner。架构测试锁定本包结构和退役文件, -让后续 Agent 的错误命名在 CI 中直接失败。 - -**Acceptance** - -- `app/runtime/dependencies.py` 与 `app/runtime/native_dependencies.py` 删除且不得复活。 -- `app/runtime/dependencies/` 只包含 `__init__.py`、`profile.py`、`native.py`;包根没有实现或重复导出。 -- 所有宿主 canonical 导入改为明确子模块;最新官方插件仓扫描决定是否需要精确 Compat 映射。 -- 命名规范、架构文档、依赖基线和结构门禁同步;`app/plugins/**` 不扫描、不修改。 -- 运行依赖分组、插件原生依赖激活、插件安装/包管理专项及架构/质量门禁全部通过。 -- 提交推送后确认远端 SHA、祖先关系和 ahead/behind `0/0`。 - -**Evidence(2026-08-29)** - -- 平铺实现已删除,`app/runtime/dependencies/` 仅有 `__init__.py`、`profile.py`、`native.py`;包根只有包说明,宿主导入全部直达职责子模块。 -- Docker 更新边界同时覆盖新包载荷与旧版本 `dependencies.py` 快照回滚,未把旧实现恢复到主程序。 -- 官方插件仓最新远端 `aa40b961a2093474a77edd9eed88844ff07425ac` 对旧路径及全部公开符号零命中,因此未新增无消费者的 SDK/Compat 债务。 -- 运行依赖、原生激活、插件安装/包管理及架构专项 `406 passed`;最终架构/CI/Docker 门禁 `182 passed`。 -- 锁定全量 `7224 passed, 9 skipped`;Pylint `10.00/10` 且重复率 `0.000%`;mypy、Ruff(历史诊断降至 `667`)、复杂度和依赖基线门禁全部通过。 -- 交付提交 `13fba96fd82f4e94dbda8cff5dc635966e06fa67` 已推送;本地、`origin/v3` 与远端引用一致,ahead/behind `0/0`。 - -### S3-L5 SearchChain - -**Status:** `DELIVERED` - -**Outcome** - -退役 `app/chain/search.py` 单体,由同名 `app.chain.search` 包承接搜索计划、provider fan-out、 -分页、结果状态、AI 推荐、字幕与音乐搜索等职责。包根只惰性保留稳定 `SearchChain`,Facade 只组合 -职责 owner 与稳定入口;主程序不保留 `source.py`、旧实现或内部 owner/port 重复导出。 - -同步、异步和流式入口必须复用同一组纯规则与状态 owner;provider 并发、取消、分页推进和结果发布 -各自只有一个执行所有者,不能通过复制现有大方法形成多套业务实现。 - -**Acceptance** - -- 公开 `SearchChain` 类身份、方法签名、MRO、pickle 身份和官方插件调用形态不变。 -- `app/plugins/**` 不扫描、不修改;官方插件仓通过独立宿主兼容基线验证。 -- 搜索计划、provider fan-out、分页、取消和状态发布的同步/异步/流式行为保持一致并有回归覆盖。 -- Search 专项、架构/兼容、mypy/Ruff/complexity、scoped Pylint 全部通过;广泛变更运行锁定全量。 -- 本叶提交推送后确认远端 SHA、祖先关系和 ahead/behind `0/0`。 - -### S3-L6 MediaChain - -**Status:** `DELIVERED` - -**Outcome** - -退役 `app/chain/media.py` 单体,由同名 `app.chain.media` 包承接 recognition、plugin event、 -auxiliary、projection、search、catalog、path、album 和 cache。包根只惰性暴露稳定 -`MediaChain`;Facade 保持直接 `ChainBase` MRO、Singleton/pickle 类身份和官方插件真实调用签名, -内部 owner 与刮削兼容符号不从包根重复导出。 - -目录缓存使用有界 LRU、文件内容签名、深拷贝隔离与同步/异步单飞;等待者取消不会反向取消共享 -flight,符号链接目录别名不会因物理路径相同而错误共享专辑线索。跨 Chain 音乐来源复用只依赖公开 -`MusicMetadataSourceChain`,架构门禁拒绝跨 owner 导入下划线私有合同。 - -**Evidence** - -- Media/架构/插件端点集中回归 `551 passed`;锁定全量 `7244 passed, 9 skipped`。 -- Pylint `10.00/10`、零重复;mypy 低水位 `10686 / 597`,Ruff 低水位 `661`,复杂度、宿主 - dependency/runtime contract baseline 全部通过。 -- 最新官方插件仓 `aa40b961a2093474a77edd9eed88844ff07425ac` 的 MediaChain 消费者 15 个, - official-plugin baseline 与 V3 聚焦测试 `130 passed`;`app/plugins/**` 未扫描、未修改。 -- 交付提交 `13c4a71f42d189f618ac8663afb967d1517a79bb` 已推送;本地、`origin/v3` 与远端引用 - 一致,ahead/behind `0/0`。 - -### S3-L7 Agent/System/Plugin API - -**Status:** `DELIVERED` - -**Outcome** - -把 WebAgent SSE/file/audio、System nettest/log/update 与 Plugin market 入口中的可复用用例编排下沉到 -Application owner;endpoint 只保留请求鉴权、参数/响应映射和流式传输适配,不直接组合具体 Adapter、 -Helper、Manager 或跨多个领域状态。 - -**Acceptance** - -- 先以真实入口调用图确认厚方法、I/O、状态和生命周期边界,再按独立用例迁移,不复制 endpoint 实现。 -- Application 自有明确 Port/DTO,具体 Adapter 和 Runtime manager 只由 startup/composition 注入。 -- 旧插件兼容只经 SDK/Compat;canonical endpoint 不保留旧业务实现或重复导出,`app/plugins/**` 排除。 -- Agent/System/Plugin API 专项、架构/外联/类型/复杂度门禁及锁定全量通过后才可交付。 - -**Evidence** - -- `app/api/endpoints/agent.py` 从 2,000 余行收敛为传输层;WebAgent 事件、会话、附件和音频用例由 - `app/application/messaging/agent.py` 承担,运行时实现进入单词命名的 `app/agent/web.py`。 -- System 网络探测目录、HTTPS/重定向准入和内容校验进入 `app/application/network.py`;Plugin 市场投影、 - Release、评分和目录读写分别由 `catalog.py`、`release.py`、`rating.py`、`folders.py` 唯一拥有。 -- 提交 `f38637dceaa2b7fedc8ab30a41aaad3d67392ec5` 已推送;本地锁定全量 `7256 passed, 9 skipped`, - Pylint `10.00/10`、架构/外联/类型/复杂度门禁和官方插件基线通过,远端 ahead/behind `0/0`。 - -### S5-L1 PluginHelper/PluginManager - -**Status:** `DELIVERED` - -**Outcome** - -清除 `PluginHelper`、`PluginManager` 中仍混合的市场、包、依赖、备份、健康检查和运行时构造职责; -每项能力归入现有单一 owner,稳定插件 ABI 只由受控 Facade/SDK/Compat 暴露。 - -**Acceptance** - -- 先审计真实宿主与官方插件调用图;现有 owner 能承接的能力不得新建近义文件或重复 Facade。 -- 多文件能力使用同名包和单词子模块,包根不得为宿主提供重复导出;`app/plugins/**` 排除。 -- PluginHelper/PluginManager 专项、兼容基线、架构门禁及受影响测试全部通过后才可交付。 - -**Delivery (2026-08-29)** - -- `PluginHelper` 仅保留精确旧 ABI、安装 Gateway 和 owner 委托;市场传输、索引、Release 与本地候选 - 由 `PluginMarketTransport` 唯一拥有。 -- `PluginPackageManager`、`PluginDependencyInstaller`、`PluginRuntimeHealth` 分别唯一拥有包事务、依赖安装 - 与运行环境检查/恢复;API、Agent 与 Manager 不再保留物理删除或具体 Adapter 构造。 -- `PluginManager` 只消费 startup 提前注入的 typed `PluginRuntime` factory;Compat/SDK 无重复 canonical 导出。 -- 最新官方插件仓 `d5a786882` 基线与兼容探针通过;架构全集 `220 passed`,锁定覆盖率全量 - `7302 passed, 9 skipped`,Pylint `10.00/10` 且零重复,Ruff `654`、mypy `10496`,覆盖率低水位 - Application `80.92%`、Domain `79.87%`。实现提交 `fa40f29df` 已推送,远端 ahead/behind `0/0`; - GitHub 工作流已触发,不把 queued/in_progress 状态记作远端绿色。 - -### S5-L3 Domain projection - -**Status:** `DELIVERED` - -**Outcome** - -`app/domain/context.py` 继续唯一拥有 canonical `MediaInfo` 类型、构造参数、属性和四个历史 setter ABI; -TMDB、豆瓣、Bangumi、AniList 详情到统一字段的规则一次迁入 `app/domain/projection/`,接收只读快照并 -返回字段映射,不修改来源或当前输入。TMDB 图片构造器归 `tmdb.py` owner,宿主调用方直接导入来源 owner。 - -**Compatibility and gates** - -- `app.core.context` 继续由现有精确 Compat 映射到 `app.domain.context`,未新增 SDK/Compat 或包根重复导出。 -- `app/schemas/context.py` 仍是传输 DTO,不复制 setter 或投影业务;`app/plugins/**` 未改动。 -- 架构门禁固定投影包文件集合、空包根、setter 薄委托和宿主禁用 `MediaInfo` helper;golden、输入不可变、 - 多来源顺序、字段对齐与 setter 签名测试共同守住行为和 ABI。 - -**Local verification (2026-08-29)** - -- 基于 `origin/v3@d17d62e95` 锁定全量 `7375 passed, 9 skipped`;投影/响应/来源/Compat/架构专项全绿。 -- scoped Pylint `10.00/10` 且零重复;新 `domain/projection` owner mypy 零错误,strict frontier、 - Ruff、mypy、复杂度和宿主依赖低水位均通过。 -- 最新官方插件仓 `2d6946eb1` 兼容基线通过;`app/plugins/**` 零改动,包根只有说明且无重复导出。 - -### S5-L4 Startup composition - -**Status:** `DELIVERED` - -- configuration、database、network、security、agent、server、outbox、subscription 与 Chain 的具体构造 - 全部归入单词型 `startup/composition/*` owner;initializer 只消费 composition API。 -- `RuntimeDependencies`、HostRuntime、命名领域 Runtime 与旧 `ApiDataPorts` 投影共享同一对象身份, - 不再由 initializer、Facade 或兼容入口二次构造。 -- lifecycle 通过显式逆序 reset manifest 撤销 Provider;数据库 worker 未收敛时保留 owner 与连接供诊断, - 启动失败复用同一正式清理路径。 -- 2026-08-29 真实进程启动达到 `Application startup complete`,`/health/live` 与 `/health/ready` - 均返回 HTTP 200,随后达到 `Application shutdown complete`;验证未加入 revision 白名单、自动 stamp、 - 跳过迁移或环境判断脚本。 - -### S5-L5 Sync/async 重复清零 - -**Status:** `DELIVERED` - -- Search/TVDB、MusicBrainz、TheAudioDB、AcoustID、AniList、Bangumi、ServerReport、Memory、Yema、 - 通知、豆瓣、媒体识别、目录源、Indexer、TMDB API 与 Recommend 的双 ABI 共用请求计划、响应投影、 - 回退条件和业务状态机;同步/异步入口只保留真实 I/O 差异。 -- 移除 TVDB 整段同步方法的伪异步线程包装;网络客户端和数据库 Session 均按调用或事件循环持有, - 不跨事件循环复用。 -- 新增或改造的 parity/生命周期测试均为 pytest-native,并恢复 provider、singleton、cache、事件循环与 - `sys.modules`;本轮未新增 `unittest.TestCase`。 -- `5ba7e6170` 完成本叶最后一批共享决策,mypy 低水位降至 10,008,Ruff 低水位降至 633, - Pylint 保持 `10.00/10` 且重复代码为零。 - -### S5-L6 最终兼容与交付 - -**Status:** `DELIVERED` - -- 22 个不符合规范的 canonical 文件名已归并为单词文件或同名职责包;旧实现文件和主程序包根重复导出 - 已删除,宿主调用方直接导入真实 owner,兼容只由精确 SDK/Compat manifest 提供。 -- 官方插件仓已同步至 `161fce34caa31deb7d82dd50a31f217d5e6784c2`;285 个 Python 文件的 - ABI/导入语义基线不变,仅更新 provenance。 -- `app/plugins/**` 始终作为插件副本排除,未修改、未纳入基线写入或提交。 -- 锁定串行覆盖率全量 `7659 passed, 9 skipped`;Application/Domain 低水位提升并固化为 - `81.61%` / `81.03%`。Host 架构、Event/Egress policy、复杂度、async 阻塞、TaskRegistry、 - service locator、mypy/Ruff ratchet、启动性能、Pylint `10.00/10` 与依赖一致性均通过。 -- exact-head 失败后已将 canonical `SearchChain` Facade 改为显式类型转发,并修正 API/Workflow 的 - Optional、Domain/Schema DTO 边界;2026-08-30 最终本地锁定全量 `7659 passed, 9 skipped`, - CI 同构架构测试 `197 passed`,mypy 低水位降至 `9,987`,Ruff、Pylint `10.00/10`、复杂度、 - async、TaskRegistry、service locator、启动性能与依赖快照门禁均通过。 -- `4575b11d8` 修复 canonical `SearchChain` 类型转发并更新真实低水位,`c204e2e97` 修正架构测试对 - Chain owner 的直接导入;两者均已推送并进入 `origin/v3` 祖先链。精确 head `c204e2e97` 的 - Unit Tests `33269394727` 与 Pylint `33269394716` 全绿,本地与远端 ahead/behind 为 `0/0`, - S5-L3 至 S5-L6 的最终远端门禁统一闭环。 - -### S1-L1.1 Durable admission - -**Status:** `DELIVERED` - -**Outcome** - -把“接受整理任务”变成真正的 durable admission:Application 先通过类型化 Port 在独立事务中 -commit pending,再尝试写入进程内队列。队列写入失败或进程在 commit 后退出时,持久记录仍能成为 -后续恢复起点;持久化失败则不允许任务进入队列。该叶只结清 admission 所有权和顺序,不预先宣称 -planning、lease、幂等执行或终态恢复已经完成。 - -**Ownership** - -- `app/application/transfer/workflow.py` 拥有 admission DTO、Protocol、结果语义与 persist-before-enqueue 编排。 -- `app/db/adapters/` 提供短 Session/UoW 的 Transfer pending 持久化实现;`app/db/oper/` 只接收 - adapter 拥有的 Session 并 stage/flush。 -- `app/startup/` 负责构造并注入 adapter,宿主 Chain 不再取得 raw/`Any` `TransferPendingOper`。 -- `app/db/models/transferpending.py` 与配套 Alembic migration 只承载该叶需要的 durable admission - schema,并验证既有记录升级及 downgrade。 -- Transfer queue、pending repository、migration、架构边界及兼容回归测试。 - -**Excluded** - -- 不在本叶实现 planning checkpoint、lease/heartbeat、文件操作幂等、历史结算或 `manual_review`; - 这些分别由 `S1-L1.2` 至 `S1-L1.4` 完整交付。 -- 不修改插件公开 Transfer ABI、旧插件导入路径或第三方插件行为;必要兼容只通过统一 Compat/Legacy - 层委托新的 canonical admission 实现。 -- 不修改或扫描 `app/plugins/**` 插件副本。 -- 不保留宿主 canonical raw/`Any` pending port 与新 typed Port 两套正式入口。 - -**Acceptance** - -```bash -.venv/bin/python -m pytest \ - tests/test_transfer_queue_service.py \ - tests/test_transfer_pending_replay.py \ - tests/test_transfer_admission_migration.py \ - tests/test_db_transferpending_queries.py \ - tests/test_database_migration_startup.py \ - tests/test_architecture_dependencies.py \ - tests/test_legacy_import_compat.py -q -.venv/bin/mypy --config-file mypy.ini -.venv/bin/python scripts/architecture/baseline.py --check-host --diagnostics -.venv/bin/python scripts/architecture/ruff_ratchet.py -.venv/bin/python scripts/architecture/mypy_ratchet.py -uv run --locked --no-sync python tests/run.py -git diff --check -``` - -**Delivery** - -- 单一提交主题:交付 Transfer durable admission、类型化持久化边界及可逆 migration。 -- 提交前证明 pending commit 发生在 enqueue 之前;持久化失败不入队,enqueue 失败或 commit 后崩溃 - 均保留可恢复记录;宿主 canonical 路径不再导入或取得 raw/`Any` `TransferPendingOper`。 -- 运行锁定全量测试与 scoped Pylint;插件公开 Transfer ABI 和统一兼容导入测试必须保持通过。 -- 推送 `origin/v3` 后确认提交祖先关系、远端 SHA 和 ahead/behind `0/0`。 - -**Local verification (2026-08-27)** - -- 锁定全量:`6,496 passed, 7 skipped`;跳过项包含本机未配置隔离库的 PostgreSQL migration - 用例,SQLite upgrade/downgrade/re-upgrade 已真实执行。 -- scoped Pylint:`10.00/10`;host dependency baseline、Ruff ratchet、mypy ratchet 与 - `git diff --check` 全部通过。 -- failure injection 已覆盖 admission 失败不入队、batch/enqueue 失败保留记录、批次返回失败、 - queue -> worker -> terminal discard 稳定身份,以及 Legacy TransferTask 序列化字段不变。 - -### S1-L1.2 Planning checkpoint - -**Status:** `DELIVERED` - -**Outcome** - -把 durable admission 推进为可独立恢复的 `accepted -> provider_pending -> planned` 状态:准入时冻结 -版本化请求 JSON 和 SHA-256 指纹;存在旧插件 provider 时先 CAS 提交精确身份、顺序和原始 ABI 参数, -全部返回空后才由 FileManager 只读规划目标及有序叶操作,并通过第二次 CAS 提交宿主 checkpoint。 -任何对应 checkpoint 提交前都不允许进入其文件副作用;`provider_pending` 重放只消费冻结调用, -`planned` 重放只消费冻结 resolved 上下文、目标和操作,不重新访问在线识别、目录选择或重命名配置。 - -**Ownership and compatibility** - -- `app/application/transfer/workflow.py` 拥有 planning input、plan item、checkpoint 和状态错误合同;JSON 版本、 - 指纹及 resolved 上下文均可跨进程 round-trip。 -- `app/modules/filemanager/transhandler.py` 是唯一目标规划与文件执行实现;`FileManagerModule.transfer` - 与 `TransHandler.transfer_media` 已删除,不保留第二套重命名、覆盖或目录递归逻辑。 -- `app/db/adapters/transfer/admission.py` 通过短 Session/UoW 提交 checkpoint;Oper 只负责带状态和指纹条件的 - stage,3.0.14 migration 可升级、降级并在中断后重跑。 -- cleanup intent 随准入输入冻结。宿主路径由 FileManager 在 `TransferIntercept` 放行后、任何文件写入前 - 执行;legacy provider 路径为保持旧 ABI 顺序,在全部冻结引用解析成功后、调用 provider 前执行。 - strict 查询确认目标不存在才视为幂等成功,查询或删除失败抛错并保留对应 checkpoint 供重试;provider - 全空后提升的宿主 checkpoint 会记录 cleanup 已完成,禁止二次查询或删除。 -- 插件公开 Transfer 方法签名、事件类型和 payload 不变;旧 provider 身份和顺序随 checkpoint - 冻结,提交后由统一 dispatcher 精确解析并严格执行,缺失或异常时明确失败而不静默换路;全部返回空 - 才生成宿主 plan,并以第二次 CAS 提交 `planned` checkpoint 后执行。`ChainBase.transfer` 仅委托启动 - 组合根注入的 canonical durable - command,内部 plan/execute 合同不向插件调度;新 DTO 不从包根重复导出,`app/plugins/**` 插件 - 副本不参与改造。 - -**Excluded** - -- 本叶不引入 claim、lease、heartbeat、attempt、执行步骤幂等或 `manual_review`;这些由 - `S1-L1.3` 和 `S1-L1.4` 交付。 -- 本叶当时不单独承诺文件操作成功后到历史结算前的未知结果;该能力现已由 `S1-L1.4` 和 - `S1-L1.5` 的持久步骤账本、严格探测、`manual_review` 与 task-aware settlement 完整交付。 - -**Local verification (2026-08-27)** - -- planning、持久化、迁移、兼容、replay 和 worker 聚焦回归:`224 passed, 2 skipped`;跳过项仅为 - 本机未配置隔离 PostgreSQL,SQLite upgrade/downgrade/re-upgrade 已覆盖。 -- 完整本地套件:`6,578 passed, 8 skipped`;架构回归:`174 passed`;scoped Pylint `10.00/10`; - host baseline、Ruff/mypy ratchet 与 `git diff --check` 通过。 -- failure injection 覆盖 commit 前零文件副作用、commit 后崩溃重放、离线 resolved context 恢复、 - 配置漂移仍使用冻结 target storage、规划失败留痕、旧 provider 提交后短路、严格异常、空结果两阶段 - fallback、缺失引用零 cleanup,以及 cleanup 顺序/幂等/瞬时失败。 - -### S1-L1.3 Lease 与恢复调度 - -**Status:** `DELIVERED` - -**Outcome** - -为 durable transfer 增加可过期、可接管且带 fencing token 的执行租约。`owner` 只用于标识执行者, -每次有效 claim 生成的新 token 才是后续 heartbeat、checkpoint、失败留痕和终态删除的授权;所有写入 -必须同时匹配当前且未过期的 token,过期 worker 即使稍后恢复也不能修改新 owner 的记录。 - -`accepted`、`provider_pending`、`planned` 仍是业务 planning phase,lease 与其正交:claim、heartbeat -和 takeover 不改变 phase,恢复 worker 继续按冻结 checkpoint 决定执行路径。启动回放与同进程恢复共用 -唯一调度入口:恢复任务在入队前完成原子 claim 并绑定 token,新 admission 由 worker 在任何副作用前 -claim。已经 claim 的恢复任务不在 worker 内二次 claim。 - -**State machine** - -| 当前租约 | 操作 | 结果与 fencing 约束 | -|---|---|---| -| 无租约 | `claim` | 生成新 token、设置 owner/到期时间并递增 attempt;phase 不变 | -| 当前租约未过期 | 同 token `heartbeat` | 只延长当前租约;owner、token、attempt 和 phase 不变 | -| 当前租约未过期 | 任意再次 `claim`,包括同 owner | 原子拒绝,不递增 attempt、不入队、不执行副作用 | -| 当前租约已过期 | `heartbeat` 或旧 token 写入 | 原子拒绝;旧租约不可复活 | -| 当前租约已过期 | 新 worker `claim` | 生成新 token 并递增 attempt;旧 token 永久失效,phase 不变 | -| 当前 token 有效 | checkpoint、失败留痕或终态删除 | 仅精确 token CAS 成功;状态提交后不得由旧 worker 覆盖 | - -任意时刻一条 pending 记录最多只有一个数据库认可的有效 fencing owner。该保证不等同于外部文件、 -插件或历史副作用 exactly-once;租约在不可中断调用期间过期时,旧调用的外部结果仍可能未知。 - -**Scheduling and shutdown** - -- 启动回放和同进程恢复只调用一个 claim-and-schedule 入口;禁止另建 list-then-enqueue 回放路径或 - 第二个 retry owner。批量恢复必须先原子 claim,再把已绑定 token 的任务交给唯一 worker 队列。 -- heartbeat 由 Transfer worker 生命周期拥有,不创建游离后台 owner;停止时先封口新 claim 和调度, - 再通知并有限等待 worker,heartbeat 在 worker 存活期间继续维持其租约。worker 收敛后才停止 - heartbeat;超时时保留仍存活的 worker/heartbeat owner 并返回失败。 -- checkpoint、失败留痕和 pending 删除在同次持久化写入中执行 token CAS;租约丢失后 worker 停止 - 继续提交状态,不以预查询替代 fencing,也不把 stale mutation 伪装成成功。 -- 确定性失败只确保唯一 scheduler 存在,不即时唤醒;新建的失败恢复 owner 首次扫描先等待固定轮询 - 周期。损坏持久投影按单任务事务回滚,再以无有效租约 CAS 留下去重诊断,不能饿死后续健康记录。 -- 精确旧 `app.db.transferpending_oper` 导入只解析到 `app/sdk/_legacy` 门面;canonical Model、Oper、 - Application Port 不保留旧 list-then-act 或无 fencing mutation。兼容 `discard/clear` 也不得删除任何 - 带 token 的有效或过期 claim。 - -**Excluded** - -- 本叶不承诺文件、legacy provider、历史写入或其他外部副作用 exactly-once,也不以延长 lease - 掩盖不可判定结果。 -- 文件步骤幂等键、逐步结果 checkpoint、崩溃后未知结果判定、唯一 retry owner 和 - `manual_review` 终态由 `S1-L1.4` 完整交付。 -- 不增加 `processing` 等与 planning phase 重复的业务状态;不修改插件公开 Transfer ABI,不扫描或 - 修改 `app/plugins/**`,不恢复宿主旧 pending 入口或重复导出。 - -**Acceptance matrix** - -| 场景 | 必须证明 | -|---|---| -| 新 admission 与恢复记录竞争 | 只有 claim 成功者入队并执行;失败者零文件副作用 | -| 两进程同时 claim 同一记录 | 仅一个新 token 成功,attempt 只按真实新 claim 增长 | -| heartbeat 与 takeover 竞争 | 未过期 heartbeat 可续租;过期租约不可复活,接管 token 唯一有效 | -| 旧 worker 延迟提交 | checkpoint、失败留痕和删除均被 token CAS 拒绝,不覆盖新 owner | -| 三种 planning phase 恢复 | claim/续租/接管保持 phase,并消费各自冻结输入或 checkpoint | -| 启动与同进程恢复同时触发 | 只经过唯一 scheduler 入口,同一任务只有一个 worker owner | -| 正常与超时关闭 | 先封口后有限等待;存活 scheduler/heartbeat/worker 不丢失 owner、不报告成功 | - -**Failure injection matrix** - -| 注入点 | 预期持久结果 | -|---|---| -| claim commit 前崩溃 | 无新 token、attempt 不变,可由后续 worker claim | -| claim commit 后、入队前崩溃 | 租约到期后可接管,新 token fencing 旧 worker | -| worker 执行前 lease 丢失 | 不执行文件副作用,不提交失败或终态状态 | -| heartbeat commit 前后崩溃 | 仅已提交到期时间生效;过期后不可用旧 token 续租 | -| checkpoint/失败留痕提交时被接管 | stale CAS 失败,新 owner 的 phase、错误和 token 不被覆盖 | -| 终态删除提交时被接管 | stale delete 为零行,pending 保留给当前 owner | -| shutdown 时 scheduler、worker 或 lease release 阻塞 | 有限等待返回失败并保留 owner/heartbeat,禁止清句柄后重建重复 owner | - -本叶验收还必须运行 lease persistence、replay/worker、startup lifecycle、migration、架构与兼容聚焦 -测试,以及锁定全量测试和 scoped Pylint;插件 ABI 只经统一 Compat/SDK 验证。 diff --git a/docs/refactor/subscription-governance-scale-2026-09-01.json b/docs/refactor/subscription-governance-scale-2026-09-01.json deleted file mode 100644 index d20ecc8c2..000000000 --- a/docs/refactor/subscription-governance-scale-2026-09-01.json +++ /dev/null @@ -1,112 +0,0 @@ -{ - "durable_cases": [ - { - "cancel_requested": true, - "cancelled_batch_state": "cancelled", - "cancelled_task_count": 10, - "cooldown_claims_blocked": 10, - "duplicate_download_reused_success": true, - "duplicate_download_submissions": 0, - "duplicate_site_claims_blocked": 10, - "external_download_submissions": 1, - "first_task_completion_local_ms": 24.648, - "name": "medium", - "queue_created_count": 100, - "queue_finished_count": 100, - "queue_state": "completed", - "recovery_batch_id_stable": true, - "running_task_cancelled": true, - "site_count": 10, - "site_peak_inflight_per_site": 1, - "site_recovered_after_lease_expiry": true, - "task_completion_local_p50_ms": 147.463, - "task_completion_local_p95_ms": 258.077, - "task_recovered_after_lease_expiry": true, - "uncertain_download_frozen": true - }, - { - "cancel_requested": true, - "cancelled_batch_state": "cancelled", - "cancelled_task_count": 10, - "cooldown_claims_blocked": 20, - "duplicate_download_reused_success": true, - "duplicate_download_submissions": 0, - "duplicate_site_claims_blocked": 20, - "external_download_submissions": 1, - "first_task_completion_local_ms": 36.372, - "name": "large", - "queue_created_count": 200, - "queue_finished_count": 200, - "queue_state": "completed", - "recovery_batch_id_stable": true, - "running_task_cancelled": true, - "site_count": 20, - "site_peak_inflight_per_site": 1, - "site_recovered_after_lease_expiry": true, - "task_completion_local_p50_ms": 291.708, - "task_completion_local_p95_ms": 510.505, - "task_recovered_after_lease_expiry": true, - "uncertain_download_frozen": true - } - ], - "environment": { - "database": "SQLite isolated temporary files", - "external_downloads": 0, - "external_requests": 0, - "platform": "macOS-26.5.2-arm64-arm-64bit-Mach-O", - "python": "3.14.3" - }, - "gates": { - "cancellation_terminal": true, - "candidate_product_avoided": true, - "download_idempotent": true, - "fresh_facts_bounded_by_unique_media": true, - "match_sets_equal": true, - "queue_completed": true, - "site_pressure_bounded": true, - "task_and_site_recovery": true - }, - "generated_at": "2026-09-01T07:35:04+00:00", - "match_cases": [ - { - "baseline_comparisons": 100000, - "batch_local_ms": 49.783, - "candidate_check_reduction_percent": 99.0, - "candidate_checks": 1000, - "candidate_count": 1000, - "fact_hits": 0, - "fact_loads": 100, - "first_match_local_ms": 0.475, - "match_sets_equal": true, - "media_count": 100, - "name": "medium", - "routed_candidates": 1000, - "site_count": 10, - "subscription_completion_local_p50_ms": 22.854, - "subscription_completion_local_p95_ms": 43.688, - "subscription_count": 100 - }, - { - "baseline_comparisons": 240000, - "batch_local_ms": 113.729, - "candidate_check_reduction_percent": 99.0, - "candidate_checks": 2400, - "candidate_count": 1200, - "fact_hits": 100, - "fact_loads": 100, - "first_match_local_ms": 0.55, - "match_sets_equal": true, - "media_count": 100, - "name": "large", - "routed_candidates": 2400, - "site_count": 20, - "subscription_completion_local_p50_ms": 53.507, - "subscription_completion_local_p95_ms": 102.284, - "subscription_count": 200 - } - ], - "method": "Deterministic local controlled sample. Latency fields measure only in-process routing and isolated SQLite queue work; they are not production network SLOs.", - "passed": true, - "revision": "d239e80b5", - "schema_version": 1 -} diff --git a/docs/refactor/subscription-search-review.md b/docs/refactor/subscription-search-review.md deleted file mode 100644 index 757b39e1a..000000000 --- a/docs/refactor/subscription-search-review.md +++ /dev/null @@ -1,104 +0,0 @@ -# V3 订阅自动搜索机制审查 - -审查日期:2026-09-09。后端基线 `ef978d606`,前端基线 `e5de69f4`。 -结论来自当前源码和隔离测试;没有连接用户部署、采集生产数据库或测量真实站点响应速度。 - -## 当前链路 - -```mermaid -flowchart TD - A[每五分钟检查到期订阅] --> B[按订阅合并持久任务] - M[手动或指定订阅搜索] --> B - B --> C[全局单消费者逐条处理订阅] - C --> D[刷新媒体信息和缺失范围] - D --> E[按关键词轮次并行查询站点] - E --> F[站点唯一在途请求与错误冷却] - F --> G[收齐当前关键词的站点结果] - G --> H[过滤排序和下载选择] - H --> I{还有待搜索站点且未开始下载} - I -->|是| J[保存剩余站点和最早恢复时间] - J --> K[每十秒检查可恢复队列] - K --> C - I -->|否| L[收口本次任务] -``` - -- 自动周期由 `subscription_search_due()` 使用 `search_interval` / 系统间隔与 `last_search` 判断。 -- `active_key` 合并同一订阅的重复请求;订阅准入阻止 Search 和 Match 同时修改同一订阅。 -- 站点预算是非阻塞认领。忙碌站点通常十秒后再试,错误冷却按类别指数退避。 -- 成功站点不追加冷却;站点配置的访问间隔仍在 provider 中执行。 -- 未完成站点通过 `pending_site_ids` 保存,普通续跑不会重新访问已经完成的站点或插件搜索源。 -- 正常定时入口持久来源为 `fallback`,新增入口为 `new`;前端已经隐藏这两类后台状态。持续可见的等待还应核对是否来自手工、指定搜索或旧状态。 - -## 已确认的问题与本次修正 - -| 问题 | 触发与影响 | 本次修正 | -| --- | --- | --- | -| 续跑刷新整个订阅的搜索时间 | 只补查坏站点也写 `last_search`,正常站点下一轮到期时间不断后移 | 上下文明确区分剩余站点续跑,只在完整搜索轮次开始时更新搜索时间 | -| 新周期合并仍只查旧剩余站点 | 活动键长期存在,自动入队保留旧游标;正常站点的新内容得不到新一轮查询 | 只有经过到期筛选的新周期,才把尚未运行的续跑任务恢复为当前完整站点范围;普通恢复保留游标,在途任务不改写 | -| 单站点状态覆盖整体状态 | 一个并行请求遇到冷却,立即把任务写成等待,其他站点其实仍在搜索 | 等待由任务完成本轮处理并持久化恢复时间后统一发布 | -| 订阅重复承担整轮随机等待 | 关键词切换前再等待一至十秒,与已有站点预算和访问间隔叠加 | 仅订阅预算路径去掉额外随机等待,站点实际访问间隔继续执行 | -| IMDb 按别名重复发相同查询 | 多个名称最终都投影成同一个 IMDb ID,无结果时重复查询 | 订阅使用有效 IMDb ID 搜索时只执行一个关键词轮次;缺少 ID 时继续名称回退 | -| 等待和失败提示缺乏预期与操作 | 只有黄色“等待站点”,未使用已有 `next_run_at`;原始错误直接显示或依赖悬停 | 中性等待提示、可触摸详情和预计恢复时间;失败明细折叠,并提供重试与有权限的站点检查入口 | - -例如:A 站正常、B 站持续故障。首次已搜 A、只剩 B 时,后续补查 B 不应推迟 A 的下一次正常搜索。到用户配置的下一周期,任务重新覆盖当前启用站点;B 仍遵守自己的冷却,A 可以查找新发布的资源。 - -本次不缩短站点错误冷却、不增加同站并发,也不改变缺集、整季包和洗版优先级。剩余站点仍保留恢复机会,不通过丢弃任务来制造“已完成”。 - -## 仍影响吞吐的机制及后续优先级 - -### 1. 按站点调度可用工作,而非长期占住整条订阅消费者 - -当前一个订阅的慢站点会阻止消费者开始下一条订阅;单条订阅内部虽已并行,但整个队列仍串行。 -下一阶段应先建立可恢复的站点子工作及结果检查点,由站点就绪时间调度请求,避免所有订阅排在慢站后面。 - -需要保留三个边界:每站唯一在途请求、每订阅提交下载时的独占权、下载器副作用后的事实对账。 -不能仅把全局锁删除或简单开启多个整订阅 worker;那会同时增加媒体识别、媒体库查询和下载器竞争。 - -### 2. 完善请求事实,再调整错误退避 - -当前部分 Spider 将 HTTP 失败或超时压成布尔错误,Indexer 只能得到通用 `error`,因此预期的超时、登录失效、403、429分类并不总能生效。 -需要在传输与站点适配边界保留结构化结果,并用真实返回形状的隔离测试覆盖整个调用链,再考虑分类退避和恢复探测。 - -建议临时网络错误静默退避;登录失效等可操作问题集中到站点层展示一次,给 Cookie 更新或连通性测试入口。 -不要从一段错误字符串可靠性不足地推断“必须用户处理”,也不要把每个订阅对同一坏站的失败重复推送。 - -### 3. 把恢复检查点细化到实际请求 - -当前游标只有站点 ID,没有关键词和页码。某站点已经返回第一页、第二页发生冲突时,恢复会从该站第一页开始;成功来源事实也只保存在单次预算对象中。 -后续应保存关键词、页码、来源结果及已处理候选身份,防止多页恢复重复工作,并让跨多次恢复的最终结果准确表达“部分完成”。 - -### 4. 控制慢站长尾,但保留择优语义 - -`FIRST_COMPLETED` 只是及时回收完成请求;当前关键词仍需等待所有站点和续页,随后才统一过滤和择优下载。 -快站结果提前可见与提前下载是两个不同合同。可以先渐进展示计数和进度;提前下载必须证明剩余来源不会改变整季包、优先级或洗版选择,否则会产生额外下载。 - -应先测量每站请求耗时、线程池排队时间、限流等待时间与关键词轮次数,再决定请求超时和慢站策略。 -当前同步流控 `sleep` 发生在站点租约内,后续可改为可取消的就绪调度;不能在线程未退出时释放其租约并让新请求接管。 - -## 用户反馈原则 - -| 情况 | 主界面 | 详情或操作 | -| --- | --- | --- | -| 正常自动检查、暂时无资源 | 维持正常订阅状态 | 无需用户处理 | -| 手工搜索暂时遇到站点繁忙或冷却 | 中性“稍后继续” | 预计继续时间,说明系统自动处理 | -| 部分站点失败、其它来源成功 | 不升级为整个订阅失败 | 站点诊断或折叠明细 | -| 本轮所有实际来源失败 | 简洁说明本轮未完成 | 重试搜索;管理权限用户可检查站点 | -| 用户需要修改凭据或配置 | 后续通过结构化错误识别,集中提示 | 直接进入对应站点处理,避免重复消息 | - -预计继续时间表示最早可尝试时间,不保证该时刻完成搜索。现有取消接口是批次级,不能把它包装为“只停止这个订阅”。 - -## 验证口径 - -- 使用独立 SQLite、模拟站点返回和可控时间,验证恢复任务的时间语义、到期范围刷新、在途保护和并行状态投影。 -- 记录实际 provider 调用与随机等待调用,验证 IMDb 去重及仅订阅移除随机等待;站点间隔和普通搜索行为需要维持回归覆盖。 -- 前端验证后台静默、等待到搜索的恢复、预计时间、失败明细折叠、重试操作和站点管理权限,并检查窄屏触摸入口。 -- 全量通过与真实部署变快不是同一结论。没有生产耗时样本时,只报告已消除的请求/等待次数,不给未经测量的整体加速倍数。 - -### 本次本地验证结果 - -- 后端执行 `uv run --locked --no-sync python tests/run.py`,首轮三个分片通过,第四片出现一次受控规模验收失败。排查复现了原验收脚本的线程协调竞态:租约已占用但模拟请求尚未开始时,正确的拒绝被误判。修复试验同步并增加确定性回归,保持全部通过条件和反向变异检查;最终重跑第四片后,四分片合计 **9042 passed、9 skipped**。 -- 架构策略测试 **165 passed**,基线与 CLI 测试 **45 passed**;事件策略、host 快照、mypy、两代复杂度、并发、异步阻塞、后台任务、服务定位、Ruff / mypy ratchet 和三次启动采样通过。依赖快照只新增搜索执行对既有 Application 站点预算的合法依赖。 -- 前端卡片、列表和搜索详情组件 **117 项测试通过**,包括同一任务持续更新、任务切换关闭详情,以及旧批次消失后新批次不会自动弹窗。 -- 375px 手机与 1280px 桌面使用真实 Vuetify 组件和隔离状态预览,验证等待、到期、失败折叠、触摸展开、重试事件和站点权限;未连接真实后端或调用外站。 -- 随后尝试将修改前端连接开发配置指定的本机 `localhost:3001` 进行真实账号验证,但健康检查和前端代理请求均超时,登录页未就绪,未进入账号登录。实际部署的订阅状态与恢复时效仍未验证。 -- 本地验证使用独立 worktree;前端未执行本地生产 build,生产环境整体搜索耗时未测量。提交与发布结果以仓库最终提交对应的 CI 和 Release 为准。 diff --git a/docs/rules/05-architecture.md b/docs/rules/05-architecture.md index bd9c1a651..35fc26dc9 100644 --- a/docs/rules/05-architecture.md +++ b/docs/rules/05-architecture.md @@ -274,7 +274,8 @@ owners, including definitions under `Match` and `TryStar`. Existing over-limit functions may only decrease; growth is rejected even by `--write`, and reductions must be recorded before the check passes. `--report` also records method/class/file line-span hotspots for review, but physical line counts never block CI. The former -v1/v2 line-budget gates are retired; see [code quality](../code-quality.md). +v1/v2 line-budget gates are retired; `scripts/architecture/complexity.py` and +`scripts/architecture/mypy_ratchet.py` are the authoritative gate definitions. `scripts/architecture/concurrency.py` resolves canonical imports and aliases for native Thread, Timer, thread/process executors, Process, TaskGroup, event-loop task submission, asyncio task/thread helpers, and executor hand-off calls. It @@ -1333,5 +1334,3 @@ modules only through `run_module` dispatch), and downloader SDK - `db/oper/searchsession.py` / `db/adapters/searchsession.py`:检查点读写与短 UoW;创建、保存、删除都在写入语句内校验队列任务租约和版本。`db/oper/subscriptionsearch.py` 在任务进入终态的同一事务中删除检查点。 - `chain/search/scan.py`:`SearchSources` 提供来源与单页请求,自动订阅的 `SearchScan` 与手动分页 `chain/search/manual.py` 共用;手动分页不写数据库,页摘要走 Runtime 缓存门面。 - 恢复候选的下载凭据经 `run_module("restore_search_torrent")` 由 Indexer 重建,Chain 不直接导入 Indexer 或数据库;该方法是宿主内部契约,不对插件公开。 - -业务规则见 `docs/smart-search.md`。 diff --git a/docs/site-adapter-capture.md b/docs/site-adapter-capture.md deleted file mode 100644 index 6d90699e0..000000000 --- a/docs/site-adapter-capture.md +++ /dev/null @@ -1,11 +0,0 @@ -# 站点适配采集 - -站点采集器已迁入独立仓库 [MoviePilot-Site-Collector](https://github.com/jxxghp/MoviePilot-Site-Collector)。普通用户无需安装 Python、Git 或 MoviePilot 后端: - -1. 从 [采集器最新 Release](https://github.com/jxxghp/MoviePilot-Site-Collector/releases/latest) 下载对应系统的程序。 -2. 按 [采集说明](https://github.com/jxxghp/MoviePilot-Site-Collector/blob/main/docs/site-adapter-capture.md) 在临时浏览器中登录、搜索并生成脱敏 ZIP。 -3. 在本仓库创建“功能改进” Issue,选择“站点适配”,附加生成的 `moviepilot-site-capture-*.zip`。 - -Issue 及附件是公开内容。提交前请解压预览 `manifest.json`、`request.json`、`search.html`、`redaction-report.json`,确认没有账号或隐私信息。不要上传 Cookie、Authorization、通行密钥、原始 HTML、HAR 或浏览器网络归档。 - -源码运行、采集限制与脱敏协议均见 [独立仓库完整说明](https://github.com/jxxghp/MoviePilot-Site-Collector/blob/main/docs/site-adapter-capture.md)。本页面保留以兼容旧文档链接。 diff --git a/docs/site-adapter-collector-release.md b/docs/site-adapter-collector-release.md deleted file mode 100644 index d169363b8..000000000 --- a/docs/site-adapter-collector-release.md +++ /dev/null @@ -1,12 +0,0 @@ -# 站点适配采集器下载说明 - -站点采集器的源码、版本、构建工作流和历史 Release 已迁入独立仓库 [MoviePilot-Site-Collector](https://github.com/jxxghp/MoviePilot-Site-Collector),与 MoviePilot 主程序发布完全分开。 - -请从 [采集器最新 Release](https://github.com/jxxghp/MoviePilot-Site-Collector/releases/latest) 下载 Windows、macOS 或 Linux 程序及同名 `.sha256` 文件。 - -- [完整下载与运行说明](https://github.com/jxxghp/MoviePilot-Site-Collector/blob/main/docs/site-adapter-collector-release.md) -- [采集流程和源码入口](https://github.com/jxxghp/MoviePilot-Site-Collector/blob/main/docs/site-adapter-capture.md) - -## 维护者发布流程 - -采集器仅在自身发布输入变化时自动递增版本并发布,不跟随 MoviePilot 发版。维护者请使用 [独立仓库发布流程](https://github.com/jxxghp/MoviePilot-Site-Collector/blob/main/docs/site-adapter-collector-release.md#维护者发布流程)。本页面保留以兼容旧文档链接。 diff --git a/docs/smart-search.md b/docs/smart-search.md deleted file mode 100644 index 43f5c3bd2..000000000 --- a/docs/smart-search.md +++ /dev/null @@ -1,49 +0,0 @@ -# 订阅补全搜索与手动分页 - -## 补全搜索策略 - -“设置 → 订阅 → 基础设置”的“补全搜索策略”(`SubscribeSearchStrategy`)作用于影视订阅的补缺搜索,包括新建、手动、定时和恢复触发的订阅搜索。日常获取最新资源仍由“订阅模式”负责;音乐、字幕、RSS 保持原有流程。 - -| 策略 | 行为 | -|---|---| -| `smart`(默认,未设置时) | 逐页收集,能确定某集已搜完时停止为它翻页 | -| `full` | 翻完全部结果页再择优;资源多时耗时长,开始下载较晚 | -| `single_page` | 每个站点关键词只查第一页(保留别名回退),过滤不通过或下载失败也不翻页,可能遗漏较早剧集 | - -洗版始终完整分页;整季缺失(缺集列表为空)也启用完整收集,即使真实总集数已知并展开为具体集号,也须等各来源结束后统一择优,让后页整季包参与原有的整季优先下载流程。站点冷却、时间片暂停和检查点恢复不会提前提交散集;单页策略仍只检查每个来源第一页。智能和全量不受 `SEARCH_RESOURCE_PAGES` 限制,但每个来源最多检查 20 页,Web 手动分页同样使用该上限。 - -## 智能收集 - -来源是站点与实际关键词的组合,各缺集共用该来源的一条游标;资源按原搜索去重规则(站点名 + 标题 + 描述)去重。 - -- 某集在某来源出现后,又在之后完整的一页中缺席,该来源就不再为它翻页;从未出现的集继续寻找。识别不出的资源和失败页不算缺席证据。 -- 空页、与上一页完全相同的页、站点明确返回末页时,该来源结束;不支持分页的站点只查第一页,不算失败。 -- 普通 IMDb 精确查询中,某来源从未匹配过目标作品,且连续 3 个非空页面全部明确不匹配时,本轮停止该来源并释放候选收集屏障。识别未知页打断计数;匹配过同作品的其他季集也不会触发该早停。全量、洗版、整季缺失和深度兜底不使用该规则,但仍受 20 页上限约束。停止只表示本轮不再检查该来源,不代表站点真实搜尽或订阅已补齐。 -- 一集在所有来源都结束收集后才择优提交;合集等它覆盖的集都就绪后一次提交。没有合格候选或下载失败时,从当前进度继续完整翻页兜底。智能模式不保证选到后续页面中的最优版本。 -- **连载中的剧**:智能模式以站点资源为准,不看播出时间。本轮任一来源出现过的最大集号作为站点已收录边界,大于边界的缺集视为站点尚未收录,只查各来源第一页,不翻页、不兜底、不尝试别名。只剩这类缺集时本轮结束,后续集由订阅模式追更。较深页面出现更新集号时边界随之扩大;同季没有任何资源时按正常规则收集。若各来源第一页都被旧合集或重新上传的旧集占满,较新的集可能被当作站点尚未收录,留待下次搜索或订阅模式;这是为避免连载剧翻遍全站接受的取舍。完整模式(含整季缺失)不使用该推断裁剪分页或提前释放目标,避免错过深页中的全集包。 - -媒体匹配、季集过滤、排序、下载选择和订阅完成判断都复用原有流程;分页只决定何时提交哪些集。 - -MiKan 等搜索入口一次返回完整列表的通用站点,按站点单页数量在响应中切出对应页,默认每页 100 条。每次续页仍需重新请求搜索页面,可翻页范围以站点实际返回的结果为限。 - -## 中断与恢复 - -- 进度保存在 `searchsession` 表,每个订阅搜索任务一行,由队列任务租约和版本号防止迟到的执行者覆盖。只保存仍覆盖剩余缺集的候选,不保存 Cookie、API Key、passkey 等凭据;下载链接能重建的用当前凭据重建,否则重新请求原页取得。同一候选的原页连续 3 次请求失败时,本轮放弃该候选。 -- 任务约每 60 秒在页面边界让出执行;同一站点最多一个自动请求在途,间隔至少 10 秒,不同订阅共享该限制,等待期间任务重新入队而不占线程。 -- 恢复时,进度超过 36 小时未更新就从第一页重搜;某来源超过 15 分钟没有请求时,先续拉原来的下一页,再重拉一次第一页补入新上传。更换媒体来源、作品、类型、季或剧集组时清除已提交进度;同一作品修改关键词等条件会重搜,但保留已提交的集。 -- 作品匹配前史和连续不匹配计数随检查点保存,冷却、让出执行或进程恢复不会清零。补拉首页不增加不匹配计数,但其中的作品匹配仍有效。旧检查点缺少匹配前史时采取保守恢复;搜索范围变化会重新搜索并保留同作品已提交的目标。 -- 任务进入完成、失败、取消或跳过等终态时删除检查点;超过 14 天未更新的遗留检查点在新建检查点时清理。 -- 翻到末页仍缺集或只剩站点尚未收录的集时本轮结束,订阅保留,不会自动循环重扫。分页进度只写日志。 - -## Web 手动分页接口 - -`/search/title/stream` 和 `/search/media/{id}/stream` 传 `manual_paging=true` 时启用单页模式: - -- 首次请求(不带 `source`)每个来源请求第 0 页,只有这时才做别名回退和插件搜索。 -- 不同站点按既有搜索并发数请求,同站点的多个关键词依次请求;每个来源完成后立即发送 `append` 预览,慢站点不会阻塞已完成来源的展示。标题预览遵循搜索过滤规则,精确媒体搜索先展示未匹配候选,最终结果统一匹配、过滤和排序。 -- `replace` 和 `done` 事件带 `sources` 列表,每项包含不透明的 `source`、`site_name`、本次 `page`、`can_continue` 和 `error`。 -- 续页时只回传 `source` 和要请求的 `page`(零基);客户端在整页结果应用后才推进页号,失败时保留页号以便重试。 -- 后端按本轮来源和页号缓存原始页摘要 36 小时,用于判断与上一页是否重复;上一页摘要缺失时先补请求上一页。空页、重复页、明确末页、不支持分页的站点或达到 20 页时 `can_continue=false`。`can_continue=true` 只表示可以继续尝试,后端不预取、不自动重试。 -- 续页不覆盖“上次搜索”缓存,不使用订阅的检查点或站点预算队列,只遵循站点原有请求间隔。 - -不传 `manual_paging` 时,原接口、同步 HTTP 接口和 Agent `search.*` 行为不变。 diff --git a/docs/subscribe-lifecycle.md b/docs/subscribe-lifecycle.md deleted file mode 100644 index 4da7f378b..000000000 --- a/docs/subscribe-lifecycle.md +++ /dev/null @@ -1,116 +0,0 @@ -# 订阅生命周期与订阅模式 - -本文说明主程序当前订阅领域的业务语义、字段所有权和使用方式,供维护订阅搜索、下载、进度刷新、重置和插件协同时参考。 - -## 核心模型 - -订阅是一条持续运行的媒体目标。它把用户想要的内容、搜索下载策略、已下载事实和完成进度保存在同一个订阅记录中。 - -订阅链路围绕四类状态工作: - -| 状态类别 | 主要字段 | 含义 | 写入入口 | -| --- | --- | --- | --- | -| 目标范围 | `type`、`season`、`start_episode`、`total_episode` | 订阅需要覆盖的媒体范围。电影视为单个目标;电视剧按季和集范围处理。 | 新增订阅、订阅编辑、剧集刷新 | -| 下载事实 | `note`、`downloaded_tracks`、`episode_priority` | 主程序或可信回填入口确认已经存在或已经下载的目标。`note` 表示影视/单曲事实,`downloaded_tracks` 保存专辑已接受资源中的去重音轨键,`episode_priority` 表示电视剧每集已知下载质量。 | 下载事实入口、backfill 入口、reset 入口 | -| 进度摘要 | `lack_episode`、`current_priority`、`completed_episode`、`completed_tracks` | 面向搜索、完成判定和展示的进度。`current_priority` 是当前洗版模式的资源准入基线;`completed_episode` 和 `completed_tracks` 由响应层按当前事实计算。 | progress 刷新入口、下载事实入口、响应构造 | -| 生命周期状态 | `state`、`last_update` | 订阅是否继续搜索,以及最近一次系统写入时间。 | 搜索下载流程、完成订阅、reset、状态更新 | - -字段所有权应保持集中:下载事实入口写事实,progress 刷新入口写派生进度,完成入口写完成状态,普通更新入口不应绕过这些语义直接改派生字段。 - -## 生命周期 - -| 阶段 | 触发来源 | 主程序行为 | 关键输出 | -| --- | --- | --- | --- | -| 创建订阅 | 用户、API、Agent、插件 | 记录媒体目标、订阅模式、季集范围、保存路径和下载策略。 | 初始订阅记录 | -| 搜索匹配 | 定时任务、手动搜索、RSS/站点刷新 | 按订阅目标和模式过滤候选资源,计算需要下载或升级的目标。 | 匹配候选、待下载上下文 | -| 下载选择 | 下载链路 | 将候选资源交给下载器;专辑下载层从文件清单提取可识别音轨键,不再按当前种子文件数拦截。 | 下载上下文、剩余缺口 | -| 事实记录 | 下载任务添加成功或可信 backfill | 写入 `note`,专辑合并 `downloaded_tracks`,电视剧同步写入或提升 `episode_priority`;全集洗版仅在下载层确认完整覆盖后提升 `current_priority`。 | 订阅下载事实快照 | -| 进度刷新 | 下载流程、backfill、剧集范围变化、显式刷新 | 普通电视剧按媒体库缺失结果刷新 `lack_episode`;电视剧洗版按目标范围和按集事实刷新 `lack_episode`,仅分集洗版从按集事实派生 `current_priority`。 | 进度字段 | -| 完成判定 | 下载流程末端、强制完成 | 根据媒体类型和订阅模式判断是否完成,完成前允许完成检查事件否决。 | 订阅历史、完成状态 | -| 重置/回填 | 用户操作、插件、维护工具 | reset 清空事实和进度;backfill 写入调用方确认的外部存在事实。 | 新事实快照和进度摘要 | - -## 订阅模式 - -MoviePilot 当前以媒体类型和洗版方式组合出常用订阅模式。普通电视剧订阅和分集洗版都是“按集目标”订阅:前者每集下载一次即可,后者允许同一集继续升级质量。全集洗版是整体资源质量目标,整包候选需要完整覆盖目标范围。 - -| 模式 | 适用媒体 | 目标口径 | 下载事实 | 进度口径 | 完成口径 | 典型用途 | -| --- | --- | --- | --- | --- | --- | --- | -| 电影普通订阅 | 电影 | 单个电影目标 | `note=[1]` 表示电影已下载。 | 不维护电视剧缺集进度。 | 有下载或强制完成即可完成。 | 追新电影、补电影库 | -| 电影洗版 | 电影 | 单个电影目标的整体质量 | `note=[1]` 表示电影已下载。 | `current_priority` 表示当前电影资源质量。 | `current_priority == 100` 或完成策略满足时完成。 | 用更高质量版本替换已有电影 | -| 普通电视剧订阅 | 电视剧 | 季内目标集范围 | `note` 记录已存在或已下载集;`episode_priority` 记录每集已知下载质量。 | `lack_episode` 来自媒体库缺失结果。 | 目标范围无缺集时完成。 | 追剧、补缺集 | -| 分集洗版 | 电视剧 | 季内目标集范围 | `note` 记录存在事实;`episode_priority` 记录每集质量。 | `lack_episode` 统计从未下载过任何版本的集;`current_priority` 是目标范围最低已知质量。 | 目标范围内每集达到顶级质量时完成洗版。 | 对单集逐步升级质量 | -| 全集洗版 | 电视剧 | 完整目标集范围的整包质量 | 整包下载确认完整覆盖后,按目标范围写 `note` 和 `episode_priority`,并提升全集准入基线 `current_priority`。 | `current_priority` 表示全集模式的资源准入基线;保留的按集事实不参与候选过滤。 | `current_priority == 100` 或完成策略满足时完成。 | 用完整季包替换已有剧集 | -| 普通专辑订阅 | 音乐专辑 | 专辑总曲目范围 | 每次成功添加资源后合并去重的 `downloaded_tracks`。 | `completed_tracks` 为已累计曲目数,不能超过 `total_tracks`。 | 累计曲目数达到 `total_tracks` 后完成;单个不完整资源也可继续累计。 | 分批收集不同资源中的专辑曲目 | - -## 按集订阅与全集洗版 - -普通电视剧订阅和分集洗版共享同一个按集事实源: - -- `note` 表示某一集已经存在或已经下载。 -- `episode_priority` 表示某一集已知的下载质量,缺失 key 表示没有质量事实。 -- `lack_episode` 只表达“还缺多少集没有任何版本”,不表达“还有多少集需要升级质量”。 -- `current_priority` 对分集洗版是目标范围内最低已知质量摘要,缺失质量事实按 `0` 参与计算。 - -普通专辑订阅使用独立音轨事实:下载层从种子文件清单提取盘号、曲序或归一化曲名组成的稳定键,订阅事实入口跨轮次合并并去重。接口只返回派生的 `completed_tracks`,不暴露内部音轨键;达到 `total_tracks` 前保持订阅,达到后才进入完成流程。 - -因此普通电视剧订阅和分集洗版可以互相转换。普通订阅下载产生的按集质量事实可被分集洗版继续使用;分集洗版产生的 `note` 也可被普通订阅继续作为已下载事实。 - -全集洗版关注完整目标范围的整体资源质量。整包候选必须完整覆盖 `[start_episode, total_episode]`,不是只覆盖当前缺口集。下载层确认完整覆盖后,主程序才可提升全集 `current_priority`,并在资源缺少显式集数时按目标范围补写按集事实。`episode_priority` 在全集模式中继续只升不降,供以后切回分集洗版使用,但不参与全集候选过滤和完成判断。 - -全集目标范围扩大后,原整包不再能证明覆盖新范围,因此全集准入基线 `current_priority` 重置为 `0`;目标范围缩小时,原整包仍覆盖新范围,保留该基线。 - -## 字段语义 - -| 字段 | 语义 | 关键约束 | -| --- | --- | --- | -| `note` | 已存在或已下载的电影项/电视剧集。 | 电影使用 `[1]`;电视剧使用集数列表。 | -| `downloaded_tracks` | 专辑已接受资源中的去重音轨稳定键。 | 只存在于活动订阅内部事实;API 通过 `completed_tracks` 暴露数量。 | -| `episode_priority` | 电视剧按集质量事实,键为集数,值为下载优先级。 | 只升不降;普通订阅和洗版订阅都可以写入。 | -| `lack_episode` | 电视剧目标范围内仍缺少任意版本的集数。 | 普通订阅来自媒体库缺失结果;洗版订阅按 `note` 和 `episode_priority > 0` 计算。 | -| `current_priority` | 当前洗版模式的资源准入基线。 | 电影和全集洗版由对应模式的受控入口维护;分集洗版按目标范围内最低已知质量刷新。 | -| `completed_episode` | 展示用完成集数。 | 分集洗版按顶档剧集数计算,全集洗版按整包准入基线是否达到顶档计算;不作为主要事实源。 | -| `completed_tracks` | 展示用已完成专辑曲目数。 | 由 `downloaded_tracks` 去重计数并截断到 `total_tracks`;不作为写入字段。 | -| `state` | 订阅运行状态。 | 完成、暂停、重置等入口按业务状态维护。 | - -## 用户视角 - -| 想要做什么 | 推荐模式 | 行为说明 | 注意事项 | -| --- | --- | --- | --- | -| 新剧更新后自动下载 | 普通电视剧订阅 | 每集下载到任意版本即可,后续缺集继续追。 | 下载过的集会作为事实保留,后续可切到分集洗版。 | -| 已有剧集想逐集升级质量 | 分集洗版 | 每集按质量继续升级,达到顶级质量后不再下载该集。 | 低质量已下载集不计入缺集,但仍会作为待升级目标。 | -| 想用完整季包替换整季 | 全集洗版 | 优先寻找完整覆盖目标范围的整包资源,并按全集准入基线判断是否下载。 | 整包必须覆盖目标集范围,不只是覆盖当前缺口。 | -| 新电影自动下载 | 电影普通订阅 | 下载到电影后完成订阅。 | 电影没有分集事实。 | -| 电影已有版本想升级 | 电影洗版 | 用 `current_priority` 表达当前电影质量,下载更高质量版本。 | 完成策略按电影整体质量判断。 | -| 手动入库或插件扫描到已有集 | backfill | 可信调用方写入已存在集;可选择是否提供 quality priority。 | `priority=None` 只表达存在事实;提供有效 priority 才表达质量事实。 | - -## 维护边界 - -### 自动搜索与站点恢复 - -新订阅保留一分钟编辑时间。暂停订阅后,已经入队的新订阅搜索、定时搜索及站点恢复任务也会停止; -正在执行的自动搜索在下一个安全边界退出,并在下载器提交前再次检查暂停状态。已经提交给下载器的 -任务不受订阅暂停影响。手动或指定订阅搜索仍可对暂停订阅执行一次补搜;需要停止本次补搜时, -应取消对应搜索批次。 -搜索及下载提交沿用同一执行上下文,不会为了补搜临时启用订阅,也不会在搜索无结果时恢复订阅。 -下载成功后插件可以根据关联订阅的下载事件恢复订阅,已经完成的订阅仍按正常完成规则收口。 - -`last_search` 记录完整搜索轮次的开始时间,供订阅独立周期或系统周期判断下一次搜索是否到期。 -因站点暂时不可用而仅补查剩余站点时,不刷新此时间,避免坏站点持续推迟正常站点的新一轮搜索。 -同一轮恢复保留待搜索站点;经过到期筛选的新周期会把尚未运行的恢复任务重新覆盖当前启用站点。 -正在执行的任务保持原范围,由后续周期调度接续,不改变已开始的下载副作用。 - -站点等待不代表搜索失败。其它站点仍在执行时保持搜索状态;本轮结束并保存续跑时间后才进入等待。 -订阅搜索的同站唯一在途预算、站点访问间隔和错误冷却始终生效。机制审查及后续优化边界见 -[订阅搜索机制审查](refactor/subscription-search-review.md)。 - -### 事实写入 - -- 公共创建与普通订阅编辑只负责目标和配置变更,不直接维护下载事实和进度摘要;Agent 或维护工具需要修正运行事实时,应使用受控的内部工具或领域入口。 -- 主程序下载链路产生的电视剧下载必须同时维护 `note` 和 `episode_priority`。 -- backfill 入口只接收调用方确认的外部存在事实,不主动扫描媒体库。 -- progress 刷新入口负责把当前事实转换为 `lack_episode`,并仅为分集洗版派生 `current_priority`;全集准入基线由模式转换、确认完整覆盖的整包下载、目标范围变化,以及插件回滚或显式完成入口维护。 -- 插件可以调用主程序公开入口补事实或刷新进度,不应复制主程序缺集、洗版完成或当前优先级计算规则。 - -## 智能分页搜索的生命周期 - -影视订阅的搜索任务可以分批提交:已提交部分集数后继续翻页寻找其余缺集,进度保存在 `searchsession`,任务进入终态时删除。`download_started` 只表示已越过下载副作用边界;分页任务要等 `scan_finished` 才算本轮结束。每批只下载已就绪的集,剩余缺集和订阅完成判断仍走原有流程;本轮结束时按当前订阅重新计算缺集,不回写总集数。策略和恢复规则见 [订阅补全搜索与手动分页](smart-search.md)。 diff --git a/docs/subscription-activity.md b/docs/subscription-activity.md deleted file mode 100644 index 3c1999680..000000000 --- a/docs/subscription-activity.md +++ /dev/null @@ -1,37 +0,0 @@ -# 订阅业务活动 - -订阅卡片的“订阅活动”按执行轮次显示业务事件,完成后的订阅可从订阅历史的同名入口查看。弹窗右上角的“运行日志”继续打开原有订阅日志、实时流与下载入口;历史订阅没有原有实时日志权限,因此该辅助入口禁用。 - -## 事件和计数 - -新建、可见规则修改、状态变更、RSS 刷新匹配、订阅搜索和订阅完成由业务边界直接记录。修改更新时间、搜索时间等内部字段不会产生一条规则修改事件。升级前没有业务事件的历史不会通过运行日志补造。 - -| 指标 | 事实口径 | -| --- | --- | -| 搜索站点 | 本轮计划搜索的站点数;实际请求另外计数,缓存结果和未发出的等待请求不会冒充网络请求 | -| 发现候选 | 来源返回的候选项数,包括插件结果;多页、多关键词返回的项目按来源处理累计 | -| RSS 共享候选 | 本轮交给匹配流程的完整共享候选数,不能当作该订阅的相关资源数 | -| 本订阅相关 | 身份关联后的资源,按站点与资源 ID 去重;没有 ID 时使用标题作为同站点备用身份 | -| 符合条件 | 通过条件或已成功提交的相关资源,单独去重 | -| 提交下载 | 下载链确认提交成功的资源,单独去重;种子内容获取失败、准备失败和下载器拒绝有独立解释 | -| 已入库集数 | 来自订阅文件接口的真实媒体库文件,不能用下载提交计数或订阅缺失进度代替 | - -正常零结果使用中性状态。站点繁忙显示等待和实际续跑时间,全部站点请求失败显示失败,部分站点失败保留各站点结果和提示。停止不会撤销已成功提交的下载。资源和站点详情保留安全摘要,排除下载链接、Cookie、passkey 和原始响应。 - -## 请求、恢复和条件快照 - -搜索请求入队时创建活动,合并请求更新原等待计划,保留原请求时间和事件身份。开始执行后冻结实际使用的订阅条件;恢复执行沿用任务身份、累计去重计数,并保留每次处理的结果和条件。入队后、执行前修改条件时,不会把旧请求条件误当作实际执行条件。 - -后台观察使用独立短事务,保存故障不会逆转已确认的下载。队列入队及终态观察使用保存点,失败不会回滚任务本身。新建、修改和完成活动由订阅事务暂存,提交与回滚一起发生。 - -资源摘要和恢复记录各保留最多 100 项,资源摘要优先保留下载回执、提交失败及符合条件的资源;完整概要计数不受资源摘要上限影响,界面明确提示未保留明细的数量。站点列表记录本轮实际来源。内部去重键单独保存,不出现在 API 响应中。 - -## 查询和归属 - -- `GET /api/v1/subscribe/{subscription_id}/activity`:概要游标分页;可按事件类型、最近天数筛选。 -- `GET /api/v1/subscribe/{subscription_id}/activity/{event_id}`:按需加载完整详情。 -- 两个接口均要求登录,并在每次请求校验当前订阅或历史订阅的 owner;管理员可查看。 -- `history=true` 使用历史订阅 ID,不能与当前订阅的整数 ID 混用。 -- 不可复用的 `activity_key` 随完成快照进入历史;活动按递增记录游标分页,避免并发新增导致重复翻页。 - -接口属于经鉴权的前端展示合同,未增加稳定 Agent gateway 操作。Application 定义事实与 Port,数据库 Adapter 实现短事务及归属隔离,Oper 只提供查询和暂存,组合根注入依赖;新增表配套 Alembic 迁移。 diff --git a/docs/testing-cleanup.md b/docs/testing-cleanup.md deleted file mode 100644 index 5548f4095..000000000 --- a/docs/testing-cleanup.md +++ /dev/null @@ -1,55 +0,0 @@ -# 后端测试清理审计 - -## 范围与方法 - -- 日期:2026-09-06。 -- 基准:`v3`,`976cecce8f72291516be74419cfc23922e99eef0`,开始工作时本地与 `origin/v3` 一致。 -- 对 `tests/test_*.py` 进行 AST 静态筛查,共 739 个文件、7,311 个测试函数/方法定义;该数字不是参数化展开后的 pytest 用例数。 -- 筛查空执行体、同名覆盖、完全重复执行体、重复参数化输入、无直接断言和语句包含关系,再人工核对入口、fixture、输入、断言与状态差异。 -- 未发现纯空测试或重复参数化输入。静态相似性只用于找候选,不是自动删除规则;本轮不声称所有测试均已完成逐条业务审计。 - -## 已清理 - -仅删除以下 6 个冗余测试定义,保留全部独立断言和被测入口。生产代码、公共测试设施、数据库迁移、兼容清单和质量门槛均不修改。 - -| 文件 | 删除用例 | 保留覆盖及依据 | -| --- | --- | --- | -| `tests/test_agent_background_output.py` | `test_background_non_streaming_does_not_send_by_default` | 同类的 `test_background_non_streaming_captures_without_sending_when_capture_only` 执行体完全相同。两者都显式设置 `ReplyMode.CAPTURE_ONLY`,旧名称实际没有验证默认值。 | -| `tests/test_agent_summarization_streaming.py` | `test_real_agent_does_not_recompact_small_tool_result_during_same_loop` | `test_real_agent_executes_compacted_tool_call_once` 使用相同真实 graph、模型响应及工具,已同时断言工具执行一次、摘要一次、模型调用两次和最终结果。合并说明以保留两种回归意图。 | -| `tests/test_fs_proxy_copy.py` | `test_worker_still_standalone_after_streaming_support` | `tests/test_fs_proxy.py::test_worker_does_not_import_app_package` 对同一 worker 源码做完全相同的断言;同文件还保留独立子进程启动验证。删除后顺带移除不再使用的 `Path` 导入。 | -| `tests/test_media_interaction.py` | `test_noai_prefix_preserves_traditional_interaction_priority_after_search` | `test_message_routes_text_reply_to_media_interaction_before_ai` 执行体完全相同。删除用例并未发送 `/noai`,只构造同一会话后发送 `1`;实际 `/noai` 创建会话的专项测试保留。 | -| `tests/test_plugin_database_lifecycle.py` | `test_remove_plugin_only_releases_the_database` | `test_stop_releases_the_database_and_never_destroys_it` 执行体完全相同,都只调用 `lifecycle.stop`。真正的卸载、删除数据及分身库销毁测试保留。 | -| `tests/test_metamusic.py` | `test_apply_title_keeps_single_word_title` | `test_apply_title_keeps_artist_abbreviation_dots` 使用相同输入 `E.S.Posthumus - Maraboot`,已有完全相同的曲名断言并额外断言艺术家。合并说明以保留单词曲名不应被当作发布组的回归意图。 | - -## 未机械删除 - -- 架构、模块/事件合同、SDK/旧导入 ABI、数据库迁移测试:它们约束当前仍需支持的入口或升级路径,不是历史重构完成后即可删除的临时测试。 -- `test_media_interaction.py` 的过期敏感输入测试:直接过期与被另一用户触发清理后的迟到输入属于不同状态,不能因后者包含前者的语句就删掉前者。 -- `test_transfer_job_manager.py` 的相同元数据任务测试:是否调用 `migrate_task` 会改变作业关联状态,两条路径分别保留。 -- `test_chain_external_ports.py` 的未装配与重复初始化/重置测试:即使最终都断言端口未配置,之前是否装配过的状态不同。 -- `test_security_utils.py` 的公网 DNS 允许与异步缓存测试:保留独立同步安全决策与跨入口缓存契约,不以覆盖率重叠替代行为判断。 -- PostgreSQL、平台和 Rust 条件跳过测试:条件代表受支持的运行环境,不能把本机未执行误判为失效。 -- 无直接断言候选中,迁移验证、辅助断言、兼容导入和“不抛异常”测试仍有契约意义;不批量删除。 -- `test_agent_image_support.py` 的显式关闭图片能力用例与布尔配置用例存在重叠,但属于存量 `TestCase` 文件。本轮不为删除一个分支用例触发整文件转换,后续修改该文件时按“改到即转”一起合并。 - -## 验证 - -所有命令均使用项目 `.venv` 对应的 `uv run --locked --no-sync` 环境。 - -| 验证 | 结果 | -| --- | --- | -| Agent 输出/压缩、文件代理、媒体交互、插件数据库生命周期,含保留 worker 用例的 6 个文件 | 清理前 160 passed,清理后 155 passed | -| `test_metamusic.py` | 清理前 86 passed,清理后 85 passed | -| 第一组生产代码覆盖 | 前后逐文件 `executed_lines`、`executed_branches` 集合完全一致,均覆盖 35,597 行、1,228 条分支 | -| 音乐标题组生产代码覆盖 | 前后逐文件集合完全一致,均覆盖 23,538 行、325 条分支 | -| 保留用例 AST 复核 | 排除 docstring 后,全部保留用例的函数定义一致;仅删除表中 6 个定义 | -| 清理后重复扫描 | 7,305 个测试定义,按本轮 AST 规则未再发现完全重复的执行体 | -| `python tests/run.py -q --durations=10` | 4 个分片全部通过,合计 8,437 passed、9 skipped | -| 改动 Python 文件 Pylint | 10.00/10,无诊断 | -| `git diff --check` | 通过 | - -提交前已快进同步到 `be415175208c2f7a3c1ebf556cc80c72cf322e10`,上游音乐修复未触及本次清理文件。重新运行清理涉及的 7 个测试文件及上游音乐相关的 5 个测试文件,结果为 539 passed;上表的扫描数量和覆盖率仍对应审计基准,不混用不同提交的测量结果。 - -覆盖采集前后都有 `sysmon` 不支持动态 contexts 的提示,因此这里只比较汇总行/分支集合,不宣称逐用例 context 数据完整。全量运行结束阶段出现子进程/文件句柄的 `ResourceWarning`,不影响退出码;提示中的子进程在检查时已退出。本轮没有为消除警告修改公共测试设施。 - -覆盖率对比是辅助证据,删除决定仍以上述相同输入、状态和断言映射为准。本机为 macOS/Python 3.14,不写入 Ubuntu canonical 覆盖率基线。 diff --git a/docs/testing.md b/docs/testing.md index a95d2018c..08445f0b1 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -265,8 +265,6 @@ def test_recognize_prefers_explicit_identity(sample_meta, monkeypatch): - **没有直接 `assert` 不等于空测试**:`pytest.raises`、辅助断言、迁移 round-trip、导入兼容和“不抛异常”都是有效契约;只有名称或注释声称存在的覆盖则需要进一步核实。 - **删除后验证**:运行受影响文件,核对保留用例及生产代码行/分支覆盖;跨领域清理再运行 `python tests/run.py`。不降低覆盖率基线、不新增 skip、不削弱架构或兼容门禁。 -首轮扫描范围、删除映射和保留理由见 [后端测试清理审计](testing-cleanup.md)。 - ## 排查测试问题 - **收集报错(collection error)**:多为 import 期副作用或顶层桩污染。优先改成真实 import(conftest 已隔离临时库,真实 `settings`/helper 可加载)+ 方法内 patch,而不是靠事后还原(收集期污染发生在 import 那一刻,事后还原太晚)。 diff --git a/docs/transfer-notifications.md b/docs/transfer-notifications.md deleted file mode 100644 index 12288959c..000000000 --- a/docs/transfer-notifications.md +++ /dev/null @@ -1,24 +0,0 @@ -# 整理成功通知粒度 - -系统设置 → 高级设置 → 实验室中的“按批次聚合整理成功通知”默认开启, -对应 `TRANSFER_SUCCESS_NOTIFICATION_AGGREGATION=true`。一张分轨专辑的任务全部结束后, -按下载器/种子、目标媒体目录、季和用户合并成一条成功通知。没有下载 hash 时按源目录分组。 -单文件与多文件汇总均使用用户配置的入库成功通知模板(`organizeSuccess`), -不在发送代码中固定标题和正文。聚合后向模板提供实际入库文件数(`file_count`)、 -总大小(`total_size`)、音乐专辑及已知总曲目数(`album`、`total_tracks`)、 -电视剧季集范围(`season_episode`)等上下文;最终文案和排版由模板决定。 -目标目录可从 `__transferinfo__.target_diritem.path` 读取。 -多文件通知保留“查看整理记录”入口;整目录或手动整理的明确批次结束即发,即使只有一个文件。 - -电影、电视剧和音乐使用同一个全局开关。关闭时保持原有按媒体作业发送的行为。 -目录设置继续使用原来的“发送通知”开关(`notify: true/false`),不增加通知粒度设置。 -批次聚合只影响展示消息,逐文件整理历史、完成/失败事件、刮削和种子状态回写保持原流程。 -失败通知仍由独立的失败聚合开关控制。 - -目录监控等自动单文件入口在各自批次结束后采用三十秒静默窗口, -同组后续文件会重置窗口,将连续入库的同季剧集合并为一条通知并汇总集数、文件数和大小。 -没有明确批次的外部任务同样采用三十秒静默窗口。 -窗口内若还有同组批次未结束,会继续等待;不同来源、目标目录、季和用户仍分别通知。 -明确批次等待登记关闭且所有任务终结,较慢的曲目不会打断汇总,失败任务不会计入成功文件数。 -前台手动整理仍不发送成功通知。关闭进程时在整理 worker 收敛后刷新剩余缓冲。 -通知缓冲是内存展示状态,异常退出不保证重发此前已完成任务的成功通知;逐文件历史仍可查询。 diff --git a/docs/v2-to-v3-overview.md b/docs/v2-to-v3-overview.md index 438471bce..e9995c68b 100644 --- a/docs/v2-to-v3-overview.md +++ b/docs/v2-to-v3-overview.md @@ -564,7 +564,6 @@ V3 本地运行要求 Python 3.14+,优先使用项目 `.venv` 和锁定依赖 ## 14. 延伸阅读 - [V3 后端架构设计](architecture-overview.md) -- [V3 与 V3t 运行时边界](v3t-runtime-governance.md) - [本地 CLI 使用说明](cli.md) - [Doctor 诊断说明](doctor.md) - [PostgreSQL 部署说明](postgresql-setup.md) diff --git a/docs/v3t-runtime-governance.md b/docs/v3t-runtime-governance.md deleted file mode 100644 index b44d060f0..000000000 --- a/docs/v3t-runtime-governance.md +++ /dev/null @@ -1,284 +0,0 @@ -# MoviePilot V3t 运行时治理 - -> 状态:持续维护。本文定义 `moviepilot-v3` 与 `moviepilot-v3t` 的产品边界、运行依赖分层、 -> 故障恢复合同和兼容项退场门禁。具体版本以 `pyproject.toml` 与 `uv.lock` 为准。 - -## 1. 治理目标 - -MoviePilot 同一版本提供两套镜像: - -- `moviepilot-v3` 使用标准 CPython 3.14,是默认稳定镜像和故障回滚基线。 -- `moviepilot-v3t` 使用 CPython 3.14 free-threaded 构建,用于获得多线程 CPU 并行能力; - Rust 能力固定启用,不能在运行时关闭。 - -两套镜像必须来自相同源码 revision、使用相同产品版本和同一份 `uv.lock`。V3t 不替换默认镜像, -也不在业务代码中维护一套平行实现;解释器、原生 ABI 或插件不兼容时,应切回同版本标准 V3。 -正式发布为两个变体分别生成同一版本号和 `latest` 标签;两个发布步骤都必须在各自构建、扫描通过后 -执行。版本号标签保留用于回溯和固定部署,`latest` 直接由对应的多架构发布步骤写入,不再额外运行 -标签晋升步骤。两套镜像仍必须使用同一源码 revision,避免同一发布批次出现来源不一致。 - -V3t 的目标不是让所有请求都更快。它主要改善可并行的 Python CPU 热点,同时验证主程序、原生扩展 -和插件生态在 free-threaded 解释器下的正确性。启动、内存、普通 API 和数据库路径不得为获得局部 -并发收益而出现不可接受的退化。 - -## 2. 单一依赖事实源 - -`pyproject.toml` 中的 `project.dependencies` 是两套镜像共享的运行依赖,ABI 敏感依赖放在两个互斥组: - -- `runtime-standard` -- `runtime-free-threaded` - -`tool.uv.conflicts` 保证两个组不能同时解析。Docker 的两个构建 stage 分别固定选择对应组;源码升级、 -启动恢复和插件安装后的宿主恢复由 `app.runtime.dependencies.profile.runtime_dependency_group()` 根据当前 -解释器的 `Py_GIL_DISABLED` 构建标志选择 profile。 - -依赖选择不得改为镜像标签判断,也不得在业务模块中按包名散落 V3/V3t 分支。Dockerfile 只选择运行 -profile,依赖名称、版本和 source 语义全部由 `pyproject.toml` 与 `uv.lock` 管理。 - -## 3. 当前原生依赖矩阵 - -下表中的版本用于解释当前分叉原因,不代替锁文件。版本变化后应同步更新本表的治理状态。 - -| 能力 | 标准 V3 | V3t | 当前处理与上游解除条件 | -| --- | --- | --- | --- | -| Python | CPython 3.14 | CPython 3.14t | 跟随同一 Python 3.14 patch 版本;升级后必须重新验证解释器 ABI、GIL 状态、启动与完整测试。 | -| `moviepilot-rust` | `cp314-abi3` | `cp314t` | V3 两套 ABI 使用同一包版本并按 wheel tag 选择;V2 继续使用已发布的 0.2.x `cp311-abi3` 制品,不随 V3 的 0.3.x 更新。 | -| `bcrypt` | 4.x | 5.x | V3t 使用提供 free-threaded 制品的版本;当同一稳定版本同时满足两套 ABI 与密码合同后可合并约束。 | -| Brotli | 1.2.0 wheel | 同版本固定源码构建 | V3t 当前只对该 profile 使用固定上游源码。上游提供可复现的稳定 `cp314t` wheel 后,验证导入、压缩结果、并发与 GIL,再删除 source 覆盖。 | -| CRC 加速 | `crcmod-plus` 2.3.1 | `crcmod-plus` 2.3.1 | 两套镜像统一使用继续维护且兼容 `crcmod` 导入接口的实现。`oss2` 的陈旧元数据仍声明不再维护的 `crcmod`,由 uv 在解析时排除该传递依赖;宿主不在运行时映射、卸载或替插件兼容旧分发包。 | -| `lxml` | 6.1.2 | 7.0.0b1 | V3t 暂用提供目标 ABI 的预发布版本,是当前最高风险项。稳定版提供 `cp314t` wheel 后,需通过 XML、HTML、RSS、站点解析、并发和内存验证再替换。 | -| PostgreSQL 同步驱动 | `psycopg2-binary` 2.x | `psycopg[c]` 3.3.4 | 当前分叉同时受 ABI 与实测性能影响,不要求仅为版本统一而收敛。若上游能力或性能变化,必须重跑三方案 PostgreSQL A/B 后再决策。异步路径继续使用 `asyncpg`。 | -| Windows PostgreSQL 客户端 | `psycopg2-binary` 自包含 libpq | `psycopg[c]` 链接本机 libpq | Windows 3.14t 源码环境必须安装 PostgreSQL 客户端开发工具并提供有效 `PGBIN`;启动时只为 free-threaded 解释器注册该 DLL 目录。上游发布自包含的 `cp314t` Windows wheel 后,应优先移除此宿主前提。 | -| Windows Docker SDK | `pywin32` 312 | 不安装 `pywin32` | 标准 Windows 保留 Docker named-pipe transport;V3t 仅提供不依赖 pywin32 的 Docker SDK 路径。Docker 或 pywin32 提供兼容 `cp314t` 的组合后,重新验证 named-pipe 再解除排除。 | -| `orjson` | 3.12.0 wheel | 同版本源码构建 | V3t 使用同一锁定版本并启用 free-threaded 构建变量。上游发布覆盖 Linux amd64/arm64 的稳定 `cp314t` wheel 后可删除本地构建要求。 | -| 中文转换 | `moviepilot-rust.zhconv_fast()` | `moviepilot-rust.zhconv_fast()` | 从 0.3.3 起两套 ABI 使用同一 MediaWiki 转换实现;主程序统一经 `app.foundation.text.convert()`,插件统一经 SDK,不再安装独立 `zhconv-rs`。 | -| 站点资源 | `cpython-314` | `cpython-314t` | 资源文件必须按解释器 ABI 独立构建和选取,不能让 V3t 复用普通 CPython 扩展,也不能影响 V2 的历史 ABI 制品。 | - -表中差异不是全部都要消除。只有临时 source 构建、预发布依赖或第三方元数据兼容适合在上游成熟后 -优先退场;已由性能与产品边界证明合理的驱动或实现选择,可以继续由 profile 集中管理。 - -## 4. 依赖检查与自愈合同 - -“自愈”包含两个不同的恢复边界,不能与普通依赖校验混为一谈。 - -### 4.1 启动前恢复 - -容器启动前先导入一组后端核心依赖。导入成功时不运行 uv 同步;导入失败时才选择可用依赖源,并对 -`/app` 执行锁定的项目同步。同步命令必须: - -- 使用当前虚拟环境中的解释器推导 runtime profile; -- 使用 `--locked`,禁止启动时重新求解未锁定版本; -- 使用 `--inexact`,保留共享环境中的插件额外依赖; -- 只恢复主项目运行依赖,不安装项目本身或开发依赖; -- 恢复后再次执行核心导入探针,仍失败则停止后端启动并保留诊断入口。 - -源码更新事务中的依赖同步和失败回滚使用同一 profile 选择入口。否则 V3t 在恢复时可能被普通默认组 -覆盖,得到“3.14t 解释器 + 标准原生依赖”的无效组合。 - -如果启动时发现更新事务停留在 `dependencies` 阶段,恢复流程保留当前 `/app`,只执行启动前依赖自愈, -不会自动切回旧代际。这样即使当前程序已经完成过数据库迁移,也不会把包含新结构的数据库交给缺少对应 -Alembic revision 文件的旧程序;依赖自愈成功后再清理旧代际备份和事务标记。 - -### 4.2 插件安装后的宿主恢复 - -插件与主程序共享虚拟环境。插件依赖安装前后都会采集宿主健康快照,只对安装后新增的异常执行补偿: - -1. 优先使用安装前生成的主程序保护约束恢复被修改的包; -2. 约束不可用时,按主项目 `pyproject.toml`、`uv.lock` 和当前 runtime profile 恢复; -3. 恢复完成后重新执行依赖诊断与核心能力探针; -4. 宿主即使恢复成功,本次插件安装仍返回失败,不能把被回滚的安装报告为成功。 - -依赖诊断使用 `uv pip check`,项目在 `tool.uv.exclude-dependencies` 中明确排除的传递依赖不进入健康 -异常集合,其余诊断按安装前后的稳定错误集合识别新增问题;这样既不会反复报告 `oss2` 对旧 `crcmod` -的陈旧元数据,也不会让既有告警遮蔽其他新增错误。核心能力 -探针统一由 `app.doctor.dependencies` 执行。普通启动使用轻量模式验证 Web 栈、中文分词与转换; -镜像构建及插件安装前后使用完整模式,继续验证 ABI 敏感原生扩展、CRC C 实现、PostgreSQL C 实现及 -导入后的 GIL 状态。插件允许的非核心依赖升级不要求与宿主锁文件逐版本相同,因此插件健康检查不得 -用整份 `uv.lock` 强制回滚共享环境。 - -自愈是异常补偿,不是每次启动的常态安装流程。任何新增恢复入口都必须复用统一 profile 选择函数, -不得自己拼装默认 uv group。 - -## 5. GIL 与原生扩展可观测性 - -构建阶段必须导入并执行已知核心原生依赖,验证 V3t 在导入前、导入后和热点执行后均保持 GIL 关闭。 -运行阶段还必须保留动态观测,因为构建探针无法穷举插件延迟导入的第三方扩展: - -- 启动收尾日志记录解释器类型和实际 GIL 状态; -- 登录后全局设置与系统环境 API 返回 `PYTHON_FREE_THREADED` 和 `PYTHON_GIL_ENABLED`,系统环境 API - 同时返回 `RUST_ACCEL_REQUIRED`; -- 插件安装或重载导致 GIL 从关闭变为开启时,日志记录对应插件归因; -- 前端在 V3t 实际启用 GIL 时显示兼容告警,正常状态不误报。 - -不得通过 `PYTHON_GIL=0` 或等效方式强制绕过扩展的 GIL 声明。扩展一旦使进程退化为 GIL 模式, -应保留可观测性并修复或替换依赖;无法及时处理时回退标准 V3。 - -## 5.1 插件运行时声明与可见性 - -插件在 package 索引中用 `v3t: false` 声明不支持 free-threaded 运行时,缺省视为兼容,不要求生态 -全量标注。V3t 下该声明只改变可安装性和提示,不改变可见性: - -- 市场目录保留条目并标注 `runtime_compatible`、`runtime_message`,前端据此禁用安装并说明原因。 - 过滤掉条目会让插件在 V3t 上凭空消失,用户既看不到插件也看不到原因。 -- 安装准入统一经 `check_plugin_install_compatibility()` 判定,运行时判据先于宿主版本判据返回, - 在线安装与本地插件包共用这一条。前端禁用只是体验层,服务端必须独立拒绝。 -- 运行目录加载仍然跳过声明不兼容的载荷,并把插件状态写为 `incompatible_runtime`, - 由插件卡片显示不支持。按 ID 定向装载由生命周期的空结果分支落状态,全量装载不逐个遍历 - 目标,由加载器自己落记。 -- 启动同步把运行时不兼容的插件排除在候选之外。它们装不上,进入同步只会得到一次必然失败的 - 安装,并用同步失败状态覆盖不兼容状态;恢复集合还会据此把整个启动同步判为未完成。 - -该声明是人工标注,覆盖率天然滞后于真实依赖状况,不作为 V3t 依赖兼容性的唯一判据。 - -## 6. 当前 A/B 基线与决策 - -以下数据来自同一源码 revision、相同资源限制的 arm64 候选镜像,用于确定当前架构和依赖策略。后续 -上游重放、依赖更新、构建链变化或采集 schema 扩充都会使其失去“最终发布验收”资格,但不抹去已经 -建立的设计结论;新的发布候选必须重跑并在本节追加或替换相应数据。 - -### 6.1 镜像与解释器基线 - -| 指标 | 标准 V3 | V3t | 结论 | -| --- | ---: | ---: | --- | -| arm64 镜像体积 | 647.5 MiB | 674.0 MiB | V3t 增加 26.5 MiB,约 4.1%;标准镜像仍处于既有 660+ MB 发布口径。 | -| 启动就绪中位数 | 6.177 s | 5.608 s | V3t 约快 9.2%,未造成启动退化;不据此承诺所有部署都会更快。 | -| 空载 working set | 274.9 MiB | 336.8 MiB | V3t 增加约 22.6%,低于 25% 验收阈值。 | -| 主工作负载 PSS | 296.4 MiB | 356.3 MiB | V3t 增加约 20.2%,属于实验镜像的明确资源成本。 | -| 32 线程纯 Python CPU 探针 | 13,143.7 ops/s | 42,583.5 ops/s | V3t 约为 3.24 倍,证明解释器并行收益;该探针不经过 Rust 开关。 | -| 32 线程直接 Rust ABI 探针 | 40,832.4 ops/s | 61,550.9 ops/s | V3t 约为 1.51 倍;两边都直接调用 Rust,仅验证原生 ABI 与并发。 | -| 普通 API 最大 p95 比例 | 基线 | 1.10x | 四个本地 API 中最大退化来自订阅列表,仍低于 1.25x 门禁。 | - -同 revision 的 amd64 最终候选也已完成交叉架构验收。标准 V3 与 V3t 的本地镜像体积分别为 -680.6 MiB 和 714.0 MiB,V3t 增加 33.4 MiB、约 4.9%。两者均使用 Python 3.14.7,标准 V3 -保持 GIL 启用;V3t 使用 free-threaded 解释器,完整原生依赖、`psycopg` C 实现、Rust 文本能力及 -`sites.cpython-314t-x86_64-linux-gnu.so` 依次加载后 GIL 始终关闭。两个镜像均以空白配置启动到 -`/health/ready` 且 Docker health 为 healthy。该启动运行在 arm64 宿主的 amd64 模拟环境中,只作为 -发布候选功能与 ABI 验收,不纳入性能比例。 - -该候选还通过了四分片全量回归(5,981 passed、3 skipped、14 subtests passed)、PostgreSQL 18.6 -迁移/提交/回滚、现代插件清单、历史 `requirements.txt`、插件源码与依赖恢复以及恢复后单实例加载。 -这些结果证明双 profile 方案可行;最终发布仍必须使用最新 revision 和正式不可变 digest 重跑。 - -### 6.2 应用热点三组 - -真正受产品 Rust 开关影响的应用识别热点必须分成三个对象: - -| 运行方式 | 解释器 | 应用实现 | 产品语义 | -| --- | --- | --- | --- | -| V3 + Python | 标准 CPython | Python fallback | 标准镜像关闭 `RUST_ACCEL` 的兼容基线。 | -| V3 + Rust | 标准 CPython | `moviepilot-rust` | 标准镜像启用 `RUST_ACCEL` 的默认性能路径。 | -| V3t + Rust | free-threaded CPython | `moviepilot-rust` | V3t 固定路径,Rust 不允许关闭。 | - -当前六个交替样本的中位耗时分别为 `0.068769s`、`0.035339s`、`0.035511s`,三组业务结果校验和 -一致。标准 V3 启用 Rust 后耗时约为 Python fallback 的 `0.514x`;V3t 固定 Rust 路径约为标准 V3 -Rust 路径的 `1.005x`,即慢约 0.5%,仍低于 1.25x 门禁。解释器探针的并发收益不能抵销产品路径 -退化,因此两组比例必须继续独立验收。 - -### 6.3 PostgreSQL 三方案 - -三组使用同一 PostgreSQL 18.6、相同 SQL 和六轮全排列顺序,以标准 V3 + `psycopg2` 为 1.00 倍基线: - -| 方案 | 单连接查询吞吐 | 16 线程查询吞吐 | 批量事务写耗时 | -| --- | ---: | ---: | ---: | -| V3 + `psycopg2` | 1.00x | 1.00x | 1.00x | -| V3 + `psycopg3 binary` | 0.946x | 0.367x | 0.094x | -| V3t + `psycopg3 C` | 0.931x | 1.258x | 0.096x | - -因此标准 V3 保留 `psycopg2`,V3t 为满足 free-threaded ABI 使用 `psycopg3 C`。`psycopg3` 的批量 -事务写明显更快,但查询吞吐没有形成全面优势;当前不为统一驱动扩大数据库重构。18 个当前镜像样本 -均通过驱动实现、GIL、SQL 结果、长事务并行和样本完整性门禁。三种制品内嵌的 libpq 分别为 17.9、 -18.0 和 18.6,性能差异不能全部归因于 Python 驱动。未来驱动或应用访问模式变化 -时,必须重跑真实短事务、批量写、连接池、长事务和迁移场景,不能只用单项微基准改写结论。 - -### 6.4 环境检查成本 - -`uv sync --check --locked --offline --inexact` 适合验证镜像构建、开发环境和锁定项目能否复现, -不用于覆盖插件允许的非核心依赖升级。真正恢复可能访问包索引,耗时仍受缺失制品、缓存命中、网络和 -原生构建影响,不能用检查耗时推断用户故障恢复时长。 - -当前公共 A/B schema 2 已记录完整安装包集合及哈希、全部原生 distribution 和 wheel tag、导入前后 -GIL、working set、RSS/PSS/USS、API p50/p95、SQLite 同步/异步结果和 1/8/16/32 线程热点。两套镜像 -各安装 174 个包;V3t 的 16 个核心原生模块在同一进程依次导入后,GIL 始终保持关闭。 - -### 6.5 冷启动、插件恢复与宿主自愈 - -空白配置卷首次启动需要下载 CloakBrowser 内核。该场景可能接近用户报告的“两分钟启动”,主要瓶颈 -是持久浏览器缓存未命中和下载网络,不是插件初始化;缓存命中后的普通重启不重复下载。 - -现代 `pyproject.toml` 插件(`boltons==25.0.0`)与历史 `requirements.txt` 插件 -(`humanize==4.15.0`)均已在 V3t 真实容器中验收。首次运行从快照恢复两个源码目录耗时 1.60ms, -联网安装依赖及激活耗时 4.64s;优雅关停后使用同一不可变镜像和持久 `/config` 断网重建,源码恢复 -耗时 1.95ms,仅从持久 uv 缓存恢复依赖及激活耗时 1.56s。两轮各插件都只产生一条实例初始化记录, -最终 API 状态均为 `active`、无 pending/failed,且没有启用 GIL。该结果证明插件源码恢复不应被依赖 -下载阻塞,依赖未就绪的插件可以在 Web 可用后渐进加载。 - -删除一个主程序核心依赖后重启,启动前探针能触发锁定自愈,`--inexact` 保留插件额外依赖;恢复后 -插件仍为单实例 `active`,V3t GIL 仍关闭。该路径的价值是让受污染共享环境能够恢复启动,不是性能 -优化;它只在核心导入失败时执行,正常重启不会承担这段解析成本。 - -### 6.6 已加载原生依赖的在线更新边界 - -`scripts/probe_native_dependency_update.py` 使用最小 C 扩展构建具有相同模块名的 v1/v2 wheel,分别 -通过历史 `requirements.txt` 和现代 `pyproject.toml` 插件清单执行三阶段验证:先安装并加载 v1, -在该进程内调用生产 `PluginHelper` 更新到 v2,再由独立解释器读取磁盘载荷。Linux x86_64、macOS -arm64 和 Windows x64 的结论一致:安装器能够把磁盘扩展替换为 v2,原进程仍执行已加载的 v1, -新进程才执行 v2。这是原生模块的进程激活边界,不是 Windows 独有的文件锁问题。 - -`Native Dependency Update Probe` workflow 使用生产安装入口运行三平台专项任务,并把包含宿主平台、 -解释器 ABI、安装器版本、wheel SHA-256、在线阶段分类和恢复阶段结果的 JSON 上传为 artifact。自动运行 -只跟随探针及生产插件安装、uv 执行和依赖健康边界变化;Python ABI、free-threaded profile 或安装器 -升级前通过手工入口复验,不因普通依赖锁、Dockerfile 或业务代码变化消耗三平台 Runner。探针同时保留 -`pip` 对照入口,用于复核历史安装器差异;长期 CI 默认只验证 MoviePilot 实际使用的 `PluginHelper`。 -一次性扩展矩阵的原始结果按候选运行独立留存,仓库只维护可复用脚本、测试和结论。 - -## 7. 上游能力的渐进式接入 - -第三方上游发布新版本或新 wheel 后,按以下顺序处理,不能看到文件名包含 `cp314t` 就直接删除本地兼容: - -1. **确认制品**:稳定版本覆盖 Linux amd64/arm64,wheel tag 与解释器 ABI 匹配;源码构建路线还要固定 - 可审计的源版本和构建工具链。 -2. **建立候选锁**:只修改对应 runtime group 或 source,更新单一 `uv.lock`,确认标准与 free-threaded - 两套 profile 均可 `uv sync --locked` 重建。 -3. **验证原生合同**:导入、核心功能、错误边界、首次初始化和 1/8/16/32 线程并发结果一致;V3t - 在每个阶段保持 GIL 关闭。 -4. **验证恢复链**:覆盖启动前恢复、源码更新与回滚、现代插件清单、历史 `requirements.txt` 和插件 - 安装后宿主恢复,确认 profile 不串组且插件额外依赖不被裁剪。 -5. **执行同 revision A/B**:先证明标准 V3 修改前后无可重复退化,再比较同一 revision、同一资源限制 - 和不可变镜像 digest 的 V3/V3t。至少保留镜像体积、启动、RSS/PSS/USS、API p50/p95、SQLite、 - PostgreSQL 和 CPU 热点原始样本。 -6. **完成双架构验收**:amd64、arm64 的构建、锁定依赖检查、启动和核心功能均通过后,才发布候选制品。 -7. **删除临时处理**:同一变更中移除不再需要的 source、版本分叉或排除规则,并更新本表、锁文件、 - 构建探针和回归测试;不要永久保留失效的兼容分支。 - -公共 A/B 基线为 `scripts/perf/free_threaded_ab.py`,使用方法和输出合同见 -[`scripts/perf/README.md`](../scripts/perf/README.md)。正式验收原始数据应按发布候选独立留存,PR 只提交 -可复用脚本和维护者可判断的汇总结论。 - -## 8. 插件生态边界 - -- 插件不得直接依赖 V3/V3t 的内部实现选择,应使用 `app.sdk` 暴露的文本、网络和运行时能力。 -- 插件不得直接导入 `zhconv_rs`;V3 与 V3t 均不再提供该分发包,中文转换统一使用 `app.sdk.utilities.convert`,宿主不伪造第三方模块。 -- 插件若自行声明不再维护的 `crcmod`,必须由插件迁移到 `crcmod-plus`;宿主不映射分发包、不增加插件 - 特判,也不在运行期卸载插件声明的依赖。 -- 插件携带原生 wheel 时必须匹配当前解释器和平台 ABI。缺少 `cp314` 或 `cp314t` 制品属于插件依赖 - 兼容问题,不通过宿主插件 ID 特判绕过。 -- 插件安装后若使 V3t 重新启用 GIL,功能可能仍可运行,但该进程已经失去 free-threaded 产品语义, - 必须告警并引导用户升级插件、移除依赖或切回标准 V3。 - -V3 `3.0.0` 的依赖集合随镜像交付。相同版本 Tag 的 `release` 自动更新只执行版本比较,不下载源码或 -同步依赖;用户必须拉取并重建容器后才会使用新镜像内的锁定环境。`/app` 与 `/opt/venv` 不属于标准 -持久化卷,非标准挂载和 `dev` 自动更新不属于正式镜像迁移合同。 - -## 9. 变更检查表 - -修改 Python patch 版本、runtime group、原生依赖、Rust 制品、资源 ABI 或双镜像构建链时,至少确认: - -- [ ] `pyproject.toml` 与 `uv.lock` 同步,两个互斥 profile 均能锁定重建。 -- [ ] 标准 V3 依赖集合、性能、体积和功能没有因 V3t 改动退化。 -- [ ] V3t 核心原生扩展导入及并发执行后 GIL 仍关闭。 -- [ ] amd64、arm64 的 Python、Rust 和站点资源 ABI 匹配。 -- [ ] SQLite、PostgreSQL、插件安装/恢复、源码升级/回滚和启动自愈通过。 -- [ ] 动态 GIL API、启动日志、插件归因和前端告警保持一致。 -- [ ] 已加载原生依赖的更新探针在目标平台通过,JSON artifact 的平台和解释器 ABI 与矩阵一致。 -- [ ] 安全扫描结果已按实际安装制品、利用面和上游修复状态审计。 -- [ ] 上游已成熟的临时兼容已删除,仍保留的例外在本文有明确解除条件。 diff --git a/scripts/architecture/mypy_ratchet.py b/scripts/architecture/mypy_ratchet.py index 047bbf381..a7ed0fc4f 100644 --- a/scripts/architecture/mypy_ratchet.py +++ b/scripts/architecture/mypy_ratchet.py @@ -1,7 +1,7 @@ """ 为宿主源码维护 mypy 存量债务例外清单,并保证新增文件、新增内容清零。 -门禁口径(与 docs/code-quality.md 保持一致): +门禁口径(本脚本即为口径来源): 1. 例外:基线中登记的文件是“当前未完成”的存量债务,按文件 + 错误码记录上限。 2. 新增文件:基线未登记的文件必须零诊断,出现任何错误即失败。 diff --git a/scripts/generate_agent_api_surface_audit.py b/scripts/generate_agent_api_surface_audit.py deleted file mode 100644 index b79bac44e..000000000 --- a/scripts/generate_agent_api_surface_audit.py +++ /dev/null @@ -1,455 +0,0 @@ -#!/usr/bin/env python3 -""" -生成全部 OpenAPI 操作与 Agent 接入范围的对应清单。 -""" - -from __future__ import annotations - -import json -import sys -from collections import Counter, defaultdict -from pathlib import Path -from typing import Any - -from fastapi import FastAPI - -PROJECT_ROOT = Path(__file__).resolve().parents[1] -if str(PROJECT_ROOT) not in sys.path: - sys.path.insert(0, str(PROJECT_ROOT)) - -# 独立脚本先加入项目根目录,再加载宿主模块。 -from app.agent.policy.api import API_OPERATION_ROUTES, API_OPERATION_SPECS # noqa: E402 - -# 独立脚本先加入项目根目录,再加载宿主模块。 -from app.api.apiv1 import api_router # noqa: E402 - -JSON_OUTPUT = PROJECT_ROOT / "docs/refactor/agent-api-surface-audit.json" -MARKDOWN_OUTPUT = PROJECT_ROOT / "docs/refactor/agent-api-surface-audit.md" -HTTP_METHODS = frozenset({"GET", "POST", "PUT", "PATCH", "DELETE"}) - -TRANSPORT_TAGS = frozenset( - { - "agent", - "anthropic", - "auth", - "llm", - "login", - "mcp", - "message", - "mfa", - "notification", - "openai", - "user", - "webhook", - } -) -CONSOLIDATED_TAGS = frozenset( - { - "anilist", - "bangumi", - "discover", - "douban", - "recommend", - "tmdb", - } -) -PROVIDER_PATH_PREFIXES = ( - "/api/v1/download/", - "/api/v1/mediaserver/", -) -CONSOLIDATED_ROUTE_OWNERS: dict[tuple[str, str], str] = { - ("POST", "/api/v1/download/"): "download.add", - ("GET", "/api/v1/plugin/installed"): "plugin.installed", - ("GET", "/api/v1/plugin/source/{plugin_id}/options"): "plugin.source.options", - ("GET", "/api/v1/plugin/{plugin_id}"): "plugin.config.get", - ("GET", "/api/v1/search/last"): "search.results", - ("GET", "/api/v1/search/media/{media_id}/stream"): "search.torrents", - ("GET", "/api/v1/search/subtitle/media/{media_id}/stream"): "subtitle.search.media", - ("GET", "/api/v1/search/subtitle/title/stream"): "subtitle.search.title", - ("GET", "/api/v1/search/title/stream"): "search.title", - ("GET", "/api/v1/site/"): "site.list", - ("GET", "/api/v1/site/cookie/{site_id}"): "site.cookie.update", - ("GET", "/api/v1/site/domain/{site_url}"): "site.list", - ("GET", "/api/v1/site/{site_id}"): "site.list", - ("POST", "/api/v1/storage/list"): "storage.list", - ("GET", "/api/v1/subscribe/list"): "subscription.list", - ("GET", "/api/v1/system/env"): "config.system.get", - ("POST", "/api/v1/system/env"): "config.system.update", - ("GET", "/api/v1/system/global"): "config.system.get", - ("GET", "/api/v1/system/setting/{key}"): "config.system.get", - ("POST", "/api/v1/system/setting/{key}"): "config.system.update", - ("GET", "/api/v1/transfer/now"): "scheduler.run", - ("GET", "/api/v1/workflow/"): "workflow.list", - ("GET", "/api/v1/media/category"): "media.classification.policy.get", - ("GET", "/api/v1/media/category/config"): "media.classification.policy.get", -} -STREAM_OR_BINARY_PATHS = frozenset( - { - "/api/v1/plugin/file/{plugin_id}/{filepath}", - "/api/v1/site/icon/{site_id}", - "/api/v1/storage/download", - "/api/v1/storage/image", - "/api/v1/system/cache/image", - "/api/v1/system/img/{proxy}", - "/api/v1/subscribe/{subscription_id}/logs", - "/api/v1/subscribe/{subscription_id}/logs/download", - "/api/v1/system/logging", - "/api/v1/system/logging/download/{name}", - "/api/v1/system/message", - "/api/v1/system/notification-image", - "/api/v1/system/progress/{process_type}", - } -) -UI_PRESENTATION_PATHS = frozenset( - { - "/api/v1/plugin/agent_pets", - "/api/v1/plugin/dashboard/meta", - "/api/v1/plugin/dashboard/{plugin_id}", - "/api/v1/plugin/dashboard/{plugin_id}/{key}", - "/api/v1/plugin/page/{plugin_id}", - "/api/v1/plugin/runtime/{plugin_id}/data/summary", - "/api/v1/plugin/sidebar_nav", - "/api/v1/rule/custom/reorder", - "/api/v1/rule/groups/reorder", - "/api/v1/storage/catalog", - "/api/v1/storage/options", - "/api/v1/transfer/queue/page", - } -) -EXPLICIT_TRANSPORT_PATHS = frozenset( - { - "/api/v1/plugin/remotes", - "/api/v1/subscribe/seerr", - "/api/v1/system/ping", - } -) -GITHUB_AUTH_PATH_PREFIXES = ( - "/api/v1/github/auth/", - "/api/v1/login/github-auth/", -) -SUBSCRIPTION_EXECUTION_UI_PREFIX = "/api/v1/subscribe/execution/" -MUSIC_LIBRARY_STATUS_UI_PATH = "/api/v1/music/library/status" - - -def _gateway_routes() -> dict[tuple[str, str], list[str]]: - """ - 收集确切的 HTTP 路由及使用该路由的所有稳定网关操作。 - """ - results: dict[tuple[str, str], list[str]] = defaultdict(list) - for operation_id, route in API_OPERATION_ROUTES.items(): - results[(route.method.upper(), route.path)].append(operation_id) - return {key: sorted(values) for key, values in results.items()} - - -def _classify( - *, - method: str, - path: str, - tags: list[str], - gateway_routes: dict[tuple[str, str], list[str]], -) -> tuple[str, str, str, list[str]]: - """ - 把 OpenAPI 操作归入经过审查的 Agent 或直接管理端契约。 - """ - operations = gateway_routes.get((method, path), []) - if operations: - return ( - "gateway", - "moviepilot-api", - "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", - operations, - ) - primary_tag = tags[0] if tags else "untagged" - if primary_tag in TRANSPORT_TAGS: - return ( - "transport_or_identity", - "host-runtime", - "Authentication, protocol compatibility, conversation transport, callback, or account lifecycle endpoint; never recursively exposed as an Agent business action.", - [], - ) - consolidated_owner = CONSOLIDATED_ROUTE_OWNERS.get((method, path)) - if consolidated_owner: - return ( - "consolidated", - "moviepilot-api", - f"This compatibility, broader-response, or UI route is represented by the safer stable operation {consolidated_owner}.", - [consolidated_owner], - ) - if path in STREAM_OR_BINARY_PATHS: - return ( - "stream_or_binary", - "host-transport", - "Streaming, image, archive, or file response consumed by a direct client; the structured JSON Agent gateway does not proxy binary or unbounded streams.", - [], - ) - if path in UI_PRESENTATION_PATHS: - return ( - "ui_presentation", - "host-ui", - "Plugin-rendered page, dashboard, or navigation metadata owned by the frontend presentation contract rather than an Agent business action.", - [], - ) - if method == "POST" and path == "/api/v1/history/transfer/{history_id}/cleanup-resolved": - return ( - "ui_presentation", - "host-ui", - "Manual confirmation of downloader cleanup is owned by the authenticated history management workflow; it is not an Agent action that verifies external cleanup.", - [], - ) - if path == "/api/v1/history/transfer/{history_id}/discard-corrupt": - return ( - "ui_presentation", - "host-ui", - "Discarding corrupt transfer state is owned by the authenticated management recovery workflow; it is not a stable Agent gateway operation.", - [], - ) - if path.startswith(SUBSCRIPTION_EXECUTION_UI_PREFIX) or path in { - "/api/v1/subscribe/{subscription_id}/activity", - "/api/v1/subscribe/{subscription_id}/activity/{event_id}", - }: - return ( - "ui_presentation", - "host-ui", - "Subscription activity details, background execution status and cancellation are owned by the authenticated frontend workflow; they are not yet a stable Agent gateway contract.", - [], - ) - if method == "POST" and path == MUSIC_LIBRARY_STATUS_UI_PATH: - return ( - "ui_presentation", - "host-ui", - "Batch music-library presence is a bounded projection for the authenticated artist resource matrix; it is not a standalone Agent business action.", - [], - ) - if path in EXPLICIT_TRANSPORT_PATHS: - return ( - "transport_or_identity", - "host-runtime", - "Health, bootstrap, federation, or external webhook transport endpoint; it is not recursively callable as an Agent business action.", - [], - ) - if method == "GET" and path == "/api/v1/github/community": - return ( - "ui_presentation", - "host-ui", - "Community medals are a presentation of the administrator-configured GitHub identity, not an Agent business operation.", - [], - ) - if path.startswith(GITHUB_AUTH_PATH_PREFIXES): - return ( - "transport_or_identity", - "host-runtime", - "GitHub Token authorization and first-run credential setup are host identity/configuration flows; they are not Agent business actions.", - [], - ) - if path.startswith(PROVIDER_PATH_PREFIXES): - return ( - "provider-skill", - "downloader-operation" if "/download/" in path else "mediaserver-operation", - "Low-level provider behavior is exposed by the self-describing provider Skill; high-level MoviePilot operations remain in moviepilot-api.", - [], - ) - if primary_tag in CONSOLIDATED_TAGS: - return ( - "consolidated", - "moviepilot-api", - "Source-specific or presentation-oriented route is represented by a stable aggregate search, detail, person, recommendation, or music operation instead of duplicating every frontend route.", - [], - ) - if path.endswith("2") or "/schedule2" in path or "/recognize2" in path or "/recognize_file2" in path: - return ( - "alternate-auth-duplicate", - "moviepilot-api", - "API-token compatibility duplicate; the Agent uses the corresponding bearer-authenticated operation with its persisted user identity.", - [], - ) - raise ValueError(f"Unclassified OpenAPI operation: {method} {path}") - - -def generate_audit() -> dict[str, Any]: - """ - 为每个 v1 OpenAPI HTTP 操作生成顺序稳定的清单条目。 - """ - app = FastAPI() - app.include_router(api_router, prefix="/api/v1") - openapi = app.openapi() - gateway_routes = _gateway_routes() - entries = [] - for path, path_item in sorted(openapi.get("paths", {}).items()): - for raw_method, operation in sorted(path_item.items()): - method = raw_method.upper() - if method not in HTTP_METHODS or not isinstance(operation, dict): - continue - tags = [str(tag) for tag in operation.get("tags") or []] - disposition, owner, reason, operation_ids = _classify( - method=method, - path=path, - tags=tags, - gateway_routes=gateway_routes, - ) - entries.append( - { - "method": method, - "path": path, - "tags": tags, - "summary": str(operation.get("summary") or ""), - "disposition": disposition, - "owner": owner, - "operation_ids": operation_ids, - "reason": reason, - } - ) - counts = Counter(entry["disposition"] for entry in entries) - matched_gateway_routes = { - (entry["method"], entry["path"]) for entry in entries if entry["disposition"] == "gateway" - } - dynamic_gateway_routes = [ - { - "method": method, - "path": path, - "operation_ids": operation_ids, - "reason": ( - "The executor validates and expands this bounded source placeholder to one of " - "tmdb, douban, bangumi, or anilist before calling the corresponding concrete OpenAPI route." - ), - } - for (method, path), operation_ids in sorted(gateway_routes.items()) - if (method, path) not in matched_gateway_routes - ] - return { - "openapi_operation_count": len(entries), - "gateway_operation_count": len(API_OPERATION_ROUTES), - "gateway_http_route_count": len(gateway_routes), - "matched_gateway_http_route_count": len(matched_gateway_routes), - "dynamic_gateway_routes": dynamic_gateway_routes, - "disposition_counts": dict(sorted(counts.items())), - "operations": entries, - "gateway_identity_contracts": [ - { - "operation_id": spec.operation_id, - "method": API_OPERATION_ROUTES[spec.operation_id].method, - "path": API_OPERATION_ROUTES[spec.operation_id].path, - "identity_scope": spec.identity_scope.value, - "required_role": spec.required_role.value, - "effect": spec.effect.value, - "result_sensitivity": spec.result_sensitivity.value, - "confirmation": spec.confirmation.value, - } - for spec in sorted(API_OPERATION_SPECS, key=lambda item: item.operation_id) - ], - } - - -def render_markdown(audit: dict[str, Any]) -> str: - """ - 将完整清单渲染为可审阅的架构说明文档。 - """ - lines = [ - "# MoviePilot Agent API Surface Audit", - "", - "> Generated from the v1 FastAPI OpenAPI document and the fixed Agent API registry.", - "> Do not edit route rows manually; run `scripts/generate_agent_api_surface_audit.py`.", - "", - "## Result", - "", - f"- OpenAPI HTTP operations: **{audit['openapi_operation_count']}**", - f"- Stable `moviepilot_api` operations: **{audit['gateway_operation_count']}**", - f"- Exact HTTP routes used by the gateway: **{audit['gateway_http_route_count']}**", - f"- OpenAPI routes matched directly by the gateway: **{audit['matched_gateway_http_route_count']}**", - f"- Bounded dynamic gateway routes: **{len(audit['dynamic_gateway_routes'])}**", - "- Every gateway operation has a generated English oneOf input contract in MCP `tools/list` and `skills/moviepilot-api/SKILL.md`.", - "- Every non-gateway OpenAPI operation is listed below with an explicit ownership boundary; it is not silently callable through arbitrary URL/method input.", - "", - "## Dispositions", - "", - "| disposition | count | meaning |", - "| :--- | ---: | :--- |", - ] - meanings = { - "gateway": "Approved structured MoviePilot Agent operation.", - "provider-skill": "Low-level downloader or media-server capability owned by a provider Skill.", - "consolidated": "Source/UI route represented by a stable aggregate Agent operation.", - "alternate-auth-duplicate": "API-token compatibility duplicate of a bearer-authenticated capability.", - "transport_or_identity": "Authentication, protocol, callback, account, or conversation transport boundary.", - "stream_or_binary": "Streaming or binary response owned by a direct client transport.", - "ui_presentation": "Frontend or plugin-rendered presentation contract.", - } - for disposition, count in audit["disposition_counts"].items(): - lines.append(f"| `{disposition}` | {count} | {meanings[disposition]} |") - lines.extend([ - "", - "## Gateway Identity Audit", - "", - "All gateway operations are audited below. New operations default to bound_user; safe_read alone never grants public access.", - "public: audited external public metadata only; the host-authenticated executor cannot call other operations.", - "bound_user: an active account bound to the stable channel sender ID, or the authenticated Web user ID; endpoint ownership and management permissions still apply.", - "admin: administrator authority, preserving existing verified channel/API integration semantics; this never substitutes for ownership on bound_user operations.", - "Title recognition retains bound_user because its Context exposes locally configured applied identification rules. File recognition, source availability, rules, search results, media libraries, storage and public UI settings remain instance data.", - "Plugin ratings include this installation's own rating and require the same administrator role as their REST dependencies. Community share/popularity/statistics and global plugin installation statistics are public reads; follow lists and rating submissions are not.", - "", - "| operation | method | path | identity scope | role | effect | result sensitivity | confirmation |", - "| :--- | :--- | :--- | :--- | :--- | :--- | :--- | :--- |", - ]) - for item in audit["gateway_identity_contracts"]: - lines.append( - f"| `{item['operation_id']}` | `{item['method']}` | `{item['path']}` | " - f"`{item['identity_scope']}` | `{item['required_role']}` | `{item['effect']}` | " - f"`{item['result_sensitivity']}` | `{item['confirmation']}` |" - ) - if audit["dynamic_gateway_routes"]: - lines.extend( - [ - "", - "## Bounded Dynamic Routes", - "", - "| method | route template | operations | constraint |", - "| :--- | :--- | :--- | :--- |", - ] - ) - for item in audit["dynamic_gateway_routes"]: - lines.append( - f"| `{item['method']}` | `{item['path']}` | {', '.join(item['operation_ids'])} | {item['reason']} |" - ) - lines.extend( - [ - "", - "## Complete Route Inventory", - "", - "| method | path | tags | disposition | owner / operation | summary |", - "| :--- | :--- | :--- | :--- | :--- | :--- |", - ] - ) - for item in audit["operations"]: - tags = ", ".join(item["tags"]) or "-" - owner = ", ".join(item["operation_ids"]) or item["owner"] - summary = item["summary"].replace("|", "\\|") - lines.append( - f"| `{item['method']}` | `{item['path']}` | {tags} | `{item['disposition']}` | {owner} | {summary} |" - ) - lines.extend( - [ - "", - "## Exposure Rule", - "", - "Every structured JSON business endpoint is either a stable gateway operation, a provider Skill capability, or an explicitly consolidated compatibility route. Authentication, webhook, stream, binary, and UI-presentation endpoints remain owned by their direct transport or frontend consumer and must not be made recursively callable by the Agent.", - "", - ] - ) - return "\n".join(lines) - - -def main() -> int: - """ - 写入确定性排序的 JSON 与 Markdown 清单产物。 - """ - audit = generate_audit() - JSON_OUTPUT.write_text( - json.dumps(audit, ensure_ascii=False, indent=2, sort_keys=True) + "\n", - encoding="utf-8", - ) - MARKDOWN_OUTPUT.write_text(render_markdown(audit), encoding="utf-8") - print(f"generated {JSON_OUTPUT.relative_to(PROJECT_ROOT)} and {MARKDOWN_OUTPUT.relative_to(PROJECT_ROOT)}") - return 0 - - -if __name__ == "__main__": - raise SystemExit(main()) diff --git a/skills/moviepilot-api/SKILL.md b/skills/moviepilot-api/SKILL.md index ee485a796..b42381727 100644 --- a/skills/moviepilot-api/SKILL.md +++ b/skills/moviepilot-api/SKILL.md @@ -256,9 +256,9 @@ must be one of the following before the Agent may use its capability: - explicitly unapproved until a role, effect, confirmation, recovery, result, and English parameter contract is added. -The maintained route-by-route inventory is -`docs/refactor/agent-api-surface-audit.md`. Its generated drift test fails -when OpenAPI changes without an explicit ownership decision. +The maintained route-by-route inventory is the fixed registry in +`app/agent/policy/api.py`. The drift test fails when OpenAPI changes without an +explicit ownership decision. The management recovery route `POST /api/v1/history/transfer/{history_id}/discard-corrupt` is reserved for direct authenticated management clients and is not a callable diff --git a/tests/test_agent_api_surface_audit.py b/tests/test_agent_api_surface_audit.py index b1eea4593..a6324818e 100644 --- a/tests/test_agent_api_surface_audit.py +++ b/tests/test_agent_api_surface_audit.py @@ -1,18 +1,14 @@ """ -Agent API 接口清单与生成契约的漂移检查。 +Agent API 工具合同检查:Skill 分类文档、MCP schema 与固定路由注册表保持一致。 """ -import json import re -import runpy from pathlib import Path from app.agent.policy.api import API_OPERATION_ROUTES from app.agent.tools.impl.api import MoviePilotApiTool PROJECT_ROOT = Path(__file__).resolve().parents[1] -AUDIT_JSON = PROJECT_ROOT / "docs/refactor/agent-api-surface-audit.json" -AUDIT_MARKDOWN = PROJECT_ROOT / "docs/refactor/agent-api-surface-audit.md" API_SKILL = PROJECT_ROOT / "skills/moviepilot-api/SKILL.md" @@ -25,87 +21,6 @@ def _read_api_skill_contract() -> str: return "\n".join(path.read_text(encoding="utf-8") for path in paths) -def _load_generator() -> dict: - """ - 加载清单生成器,不调用写文件入口。 - """ - return runpy.run_path(str(PROJECT_ROOT / "scripts/generate_agent_api_surface_audit.py")) - - -def test_agent_api_surface_audit_matches_live_openapi_and_registry() -> None: - """ - 已提交的完整接口清单必须与当前 OpenAPI 和网关注册表一致。 - """ - generator = _load_generator() - live = generator["generate_audit"]() - checked_in = json.loads(AUDIT_JSON.read_text(encoding="utf-8")) - - assert checked_in == live - assert AUDIT_MARKDOWN.read_text(encoding="utf-8") == generator["render_markdown"](live) - assert live["openapi_operation_count"] == len(live["operations"]) - assert live["gateway_operation_count"] == len(API_OPERATION_ROUTES) - assert sum(live["disposition_counts"].values()) == live["openapi_operation_count"] - assert {item["disposition"] for item in live["operations"]} == { - "alternate-auth-duplicate", - "consolidated", - "gateway", - "provider-skill", - "stream_or_binary", - "transport_or_identity", - "ui_presentation", - } - - -def test_cleanup_confirmation_stays_in_authenticated_management_workflow() -> None: - """ - 人工确认清理只关闭历史提示,不应作为 Agent 已验证外部清理的业务工具。 - """ - classify = _load_generator()["_classify"] - disposition, owner, _, operations = classify( - method="POST", - path="/api/v1/history/transfer/{history_id}/cleanup-resolved", - tags=["history"], - gateway_routes={}, - ) - assert (disposition, owner, operations) == ("ui_presentation", "host-ui", []) - - -def test_community_recognition_stays_in_frontend_workflow() -> None: - """ - 贡献徽章是管理员身份的界面投影,不可经 Agent 工具继承或读取凭据。 - """ - classify = _load_generator()["_classify"] - disposition, owner, _, operations = classify( - method="GET", path="/api/v1/github/community", tags=["github"], gateway_routes={}, - ) - assert (disposition, owner, operations) == ("ui_presentation", "host-ui", []) - - -def test_recent_business_routes_are_exposed_as_stable_gateway_operations() -> None: - """ - 近期高层业务端点必须保持在 moviepilot_api,而不是落入边界归属。 - """ - audit = json.loads(AUDIT_JSON.read_text(encoding="utf-8")) - routes = {(item["method"], item["path"]): item for item in audit["operations"]} - expected = { - ("POST", "/api/v1/download/artist-collection"): "download.artist_collection", - ("GET", "/api/v1/tmdb/cache"): "media.cache.get", - ("DELETE", "/api/v1/tmdb/cache/{cache_key}"): "media.cache.delete", - ("DELETE", "/api/v1/tmdb/cache"): "media.cache.clear", - ("GET", "/api/v1/subscribe/execution/batches"): "subscription.execution.list", - ("GET", "/api/v1/subscribe/execution/batches/{batch_id}"): "subscription.execution.get", - ( - "PUT", - "/api/v1/subscribe/execution/batches/{batch_id}/cancel", - ): "subscription.execution.cancel", - } - - for route, operation_id in expected.items(): - item = routes[route] - assert item["disposition"] == "gateway" - assert item["operation_ids"] == [operation_id] - - def test_moviepilot_api_skill_routes_contracts_to_category_files() -> None: """ API Skill 通过分类文档索引全部 operation 及请求体合同。 @@ -162,39 +77,3 @@ def test_every_gateway_path_placeholder_is_required_by_its_mcp_branch() -> None: path_schema = branches[operation_id].get("properties", {}).get("path_params", {}) assert set(path_schema.get("properties", {})) == expected, operation_id assert set(path_schema.get("required", [])) == expected, operation_id - - -def test_every_non_gateway_openapi_route_has_an_explicit_owner_and_reason() -> None: - """ - 未向网关暴露的 REST 路由也必须有明确归属,不得从接口清单中静默遗漏。 - """ - audit = json.loads(AUDIT_JSON.read_text(encoding="utf-8")) - keys = set() - for item in audit["operations"]: - key = (item["method"], item["path"]) - assert key not in keys - keys.add(key) - assert item["owner"].strip() - assert item["reason"].strip() - if item["disposition"] != "gateway": - assert item["disposition"] in { - "alternate-auth-duplicate", - "consolidated", - "provider-skill", - "stream_or_binary", - "transport_or_identity", - "ui_presentation", - } - - dynamic = audit["dynamic_gateway_routes"] - assert dynamic == [ - { - "method": "GET", - "path": "/api/v1/{source}/person/credits/{person_id}", - "operation_ids": ["media.person.credits"], - "reason": ( - "The executor validates and expands this bounded source placeholder to one of " - "tmdb, douban, bangumi, or anilist before calling the corresponding concrete OpenAPI route." - ), - } - ] diff --git a/tests/test_architecture_contract_baseline.py b/tests/test_architecture_contract_baseline.py index 2cbd8cfcc..bbff51d7a 100644 --- a/tests/test_architecture_contract_baseline.py +++ b/tests/test_architecture_contract_baseline.py @@ -89,22 +89,13 @@ def test_dependency_baseline_records_nonempty_host_graph() -> None: assert all("line" not in entry for entry in direct_egress["entries"]) -def test_architecture_documents_match_generated_quality_metrics() -> None: +def test_architecture_overview_matches_generated_quality_metrics() -> None: """ - 高漂移量化指标必须与生成 fixture 同步,不能在多份文档中分叉。 + 架构总览引用的依赖边数与事件事实必须与生成 fixture 同步,不能在文档中分叉。 """ dependency = json.loads( (BASELINE_ROOT / "dependency-baseline.json").read_text(encoding="utf-8") ) - ruff = json.loads( - (BASELINE_ROOT / "ruff-baseline.json").read_text(encoding="utf-8") - ) - mypy = json.loads( - (BASELINE_ROOT / "mypy-baseline.json").read_text(encoding="utf-8") - ) - coverage = json.loads( - (BASELINE_ROOT / "coverage-baseline.json").read_text(encoding="utf-8") - ) runtime = json.loads( (BASELINE_ROOT / "runtime-contract-baseline.json").read_text( encoding="utf-8" @@ -113,43 +104,10 @@ def test_architecture_documents_match_generated_quality_metrics() -> None: overview = (PROJECT_ROOT / "docs" / "architecture-overview.md").read_text( encoding="utf-8" ) - checklist = ( - PROJECT_ROOT - / "docs" - / "refactor" - / "optimization-checklist.md" - ).read_text(encoding="utf-8") - roadmap = ( - PROJECT_ROOT - / "docs" - / "refactor" - / "refactor-roadmap.md" - ).read_text(encoding="utf-8") edge_count = f"{dependency['edge_count']:,}" - ruff_count = sum( - count - for diagnostics in ruff.values() - for count in diagnostics.values() - ) - mypy_count = sum( - count - for diagnostics in mypy.values() - for count in diagnostics.values() - ) - coverage_scope = coverage["scope"] - total_coverage = coverage["total"]["percent"] event_facts = runtime["event_facts"] assert edge_count in overview - assert f"{dependency['module_count']} / {edge_count}" in checklist - assert f"全量 mypy 历史债务 | {mypy_count:,} / {len(mypy)} 文件" in checklist - assert f"Ruff 历史诊断 | {ruff_count}" in checklist - assert coverage_scope == {"root": "app", "excluded": ["app/plugins"]} - assert f"全 app(排除 app/plugins)当前聚合 {total_coverage:.2f}%" in checklist - if "**阶段状态:`CANCELLED`" in roadmap: - assert "| S4-L5 Ruff 治理债务清零 | `CANCELLED` |" in roadmap - else: - assert f"当前受控 {ruff_count} 条诊断归零" in roadmap assert ( f"当前宿主有 {event_facts['producer_call_count']} 个\n" f"生产调用,其中 {event_facts['static_producer_call_count']} 个静态解析为 " @@ -160,16 +118,6 @@ def test_architecture_documents_match_generated_quality_metrics() -> None: f"{event_facts['static_consumer_count']} 个静态、" f"{event_facts['dynamic_consumer_count']} 个动态" ) in overview - assert ( - f"{event_facts['producer_call_count']} 个 producer(" - f"{event_facts['static_producer_call_count']} 静态、" - f"{event_facts['dynamic_producer_count']} 动态)" - ) in checklist - assert ( - f"{event_facts['consumer_registration_count']} 个 consumer(" - f"{event_facts['static_consumer_count']} 静态、" - f"{event_facts['dynamic_consumer_count']} 动态)" - ) in checklist def test_official_discovery_plugins_explicitly_keep_host_page_envelope(): diff --git a/tests/test_reliability_adr.py b/tests/test_reliability_adr.py deleted file mode 100644 index 8b0128786..000000000 --- a/tests/test_reliability_adr.py +++ /dev/null @@ -1,41 +0,0 @@ -""" -后台动作可靠性 ADR 与事件登记一致性测试。 -""" - -from pathlib import Path - -from app.schemas.types import ChainEventType, EventType - -ADR_PATH = Path(__file__).parents[1] / "docs" / "adr" / "0007-background-action-reliability.md" - - -def test_reliability_adr_names_every_event() -> None: - """ - ADR 必须逐个提及全部广播和链式事件,不能只写笼统分组。 - """ - content = ADR_PATH.read_text(encoding="utf-8") - - missing = [ - event_type.name - for event_type in (*EventType, *ChainEventType) - if f"`{event_type.name}`" not in content - ] - assert missing == [] - - -def test_reliability_adr_covers_all_background_mechanisms() -> None: - """ - ADR 必须覆盖方案要求的四类非 Event 后台机制和完成语义。 - """ - content = ADR_PATH.read_text(encoding="utf-8") - - for phrase in ( - "FastAPI BackgroundTasks", - "Scheduler jobs", - "Agent tasks", - "Transfer pending", - "完成点", - "dead letter", - "idempotency key", - ): - assert phrase in content