diff --git a/app/agent/policy/api.py b/app/agent/policy/api.py index 05b62b5fc..95262fc72 100644 --- a/app/agent/policy/api.py +++ b/app/agent/policy/api.py @@ -119,6 +119,12 @@ API_FIRST_BATCH_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = ( _spec("subscription.history"), _write("subscription.delete", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=_DELETE_RECOVERABLE), _spec("download.add", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, confirmation=_CONFIRM, recovery=_IDEMPOTENT), + _spec( + "download.artist_collection", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + confirmation=_CONFIRM, + recovery=_IDEMPOTENT, + ), _spec("download.tasks.active"), _spec("download.clients"), _spec("download.paths"), @@ -298,6 +304,9 @@ API_EXTENDED_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = ( _admin_read("dashboard.network"), _spec("media.sources"), _spec("media.recognize_file"), + _admin_read("media.cache.get", sensitivity=ResultSensitivity.PRIVATE), + _write("media.cache.delete", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=RecoveryMode.NONE), + _write("media.cache.clear", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=RecoveryMode.NONE), _spec("media.classification.fields"), _spec("media.classification.policy.get"), _admin_read("media.classification.policy.validate"), @@ -395,6 +404,14 @@ API_EXTENDED_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = ( ), _spec("subscription.user.list"), _spec("subscription.files", result_sensitivity=ResultSensitivity.PRIVATE), + _spec("subscription.execution.list", result_sensitivity=ResultSensitivity.PRIVATE), + _spec("subscription.execution.get", result_sensitivity=ResultSensitivity.PRIVATE), + _user_write( + "subscription.execution.cancel", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + recovery=RecoveryMode.RECONCILE, + sensitivity=ResultSensitivity.PRIVATE, + ), _user_write("subscription.share", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=RecoveryMode.RECONCILE), _user_write( "subscription.share.delete", @@ -588,6 +605,7 @@ API_OPERATION_ROUTES: dict[str, ApiOperationRoute] = { "subscription.history": ApiOperationRoute("GET", "/api/v1/subscribe/history/{mtype}"), "subscription.delete": ApiOperationRoute("DELETE", "/api/v1/subscribe/{subscribe_id}"), "download.add": ApiOperationRoute("POST", "/api/v1/download/add"), + "download.artist_collection": ApiOperationRoute("POST", "/api/v1/download/artist-collection"), "download.tasks.active": ApiOperationRoute("GET", "/api/v1/download/"), "download.clients": ApiOperationRoute("GET", "/api/v1/download/clients"), "download.paths": ApiOperationRoute("GET", "/api/v1/download/paths"), @@ -658,6 +676,9 @@ API_OPERATION_ROUTES: dict[str, ApiOperationRoute] = { "dashboard.network": ApiOperationRoute("GET", "/api/v1/dashboard/network"), "media.sources": ApiOperationRoute("GET", "/api/v1/media/source"), "media.recognize_file": ApiOperationRoute("GET", "/api/v1/media/recognize_file"), + "media.cache.get": ApiOperationRoute("GET", "/api/v1/tmdb/cache"), + "media.cache.delete": ApiOperationRoute("DELETE", "/api/v1/tmdb/cache/{cache_key}"), + "media.cache.clear": ApiOperationRoute("DELETE", "/api/v1/tmdb/cache"), "media.classification.fields": ApiOperationRoute("GET", "/api/v1/media/classification/fields"), "media.classification.policy.get": ApiOperationRoute("GET", "/api/v1/media/classification/policy"), "media.classification.policy.update": ApiOperationRoute("PUT", "/api/v1/media/classification/policy"), @@ -703,6 +724,13 @@ API_OPERATION_ROUTES: dict[str, ApiOperationRoute] = { "subscription.history.delete": ApiOperationRoute("DELETE", "/api/v1/subscribe/history/{history_id}"), "subscription.user.list": ApiOperationRoute("GET", "/api/v1/subscribe/user/{username}"), "subscription.files": ApiOperationRoute("GET", "/api/v1/subscribe/files/{subscribe_id}"), + "subscription.execution.list": ApiOperationRoute("GET", "/api/v1/subscribe/execution/batches"), + "subscription.execution.get": ApiOperationRoute( + "GET", "/api/v1/subscribe/execution/batches/{batch_id}" + ), + "subscription.execution.cancel": ApiOperationRoute( + "PUT", "/api/v1/subscribe/execution/batches/{batch_id}/cancel" + ), "subscription.share": ApiOperationRoute("POST", "/api/v1/subscribe/share"), "subscription.share.delete": ApiOperationRoute("DELETE", "/api/v1/subscribe/share/{share_id}"), "subscription.fork": ApiOperationRoute("POST", "/api/v1/subscribe/fork"), diff --git a/app/agent/policy/mcp.py b/app/agent/policy/mcp.py index aab70f215..2a72c00f2 100644 --- a/app/agent/policy/mcp.py +++ b/app/agent/policy/mcp.py @@ -12,6 +12,7 @@ OPERATION_DESCRIPTIONS = { "config.system.get": "Discover registered system settings or read one exact setting.", "config.system.update": "Update one exact registered system setting.", "download.add": "Submit one torrent to MoviePilot's normal download workflow.", + "download.artist_collection": "Submit one artist-wide music torrent to MoviePilot's collection download workflow.", "download.clients": "List enabled downloader instance names and provider types without credentials.", "download.history.list": "Page MoviePilot download-history records in reverse chronological order.", "download.history.delete": "Delete one MoviePilot download-history record.", @@ -29,6 +30,9 @@ OPERATION_DESCRIPTIONS = { "library.exists": "Check configured media servers for one canonical media identity.", "library.latest": "List recently added items from one configured media-server instance for the current user.", "media.detail": "Read canonical media details from one selected metadata source.", + "media.cache.get": "Inspect the administrator-only TMDB recognition cache and summary counts.", + "media.cache.delete": "Delete one administrator-only TMDB recognition-cache entry by exact key.", + "media.cache.clear": "Clear the complete administrator-only TMDB recognition cache.", "media.episode_schedule": "Read TMDB episode release information for one season.", "media.person.credits": "Read one person's credits from the selected metadata source.", "media.person.search": "Search people and music artists across selected metadata sources.", @@ -154,6 +158,9 @@ OPERATION_DESCRIPTIONS = { "storage.rename": "Rename one exact storage item, optionally applying media-aware recursive renaming.", "subscription.delete_by_media": "Delete accessible subscriptions matching one canonical media identity.", "subscription.files": "Read local library and transfer-file coverage for one accessible subscription.", + "subscription.execution.list": "List recent durable subscription-search batches visible to the current user.", + "subscription.execution.get": "Read one durable subscription-search batch visible to the current user.", + "subscription.execution.cancel": "Request cancellation of one durable subscription-search batch before its download side-effect boundary.", "subscription.find": "Find one accessible subscription by canonical media identity and optional season.", "subscription.follow.add": "Follow one subscription-sharing user by exact share user ID.", "subscription.follow.delete": "Stop following one subscription-sharing user by exact share user ID.", @@ -230,6 +237,7 @@ FIELD_DESCRIPTIONS = { "apikey": "Site API key used by sites that support API-key authentication.", "area": "Optional region filter applied by the torrent search workflow.", "artist_id": "Source-native artist ID returned by music search or an album detail response.", + "artist_name": "Artist name preserved as the identity label for an artist-wide music collection.", "audio_format": "Requested or recorded audio container or codec, such as FLAC or MP3.", "audio_lossless": "Whether the recorded audio result is lossless.", "audio_quality": "Subscription audio-quality rule, such as hires, lossless, or lossy.", @@ -246,7 +254,7 @@ FIELD_DESCRIPTIONS = { "scalar and object request bodies are not interchangeable." ), "category": "MoviePilot media category or filter-group category, depending on the operation.", - "cache_key": "Exact recognition-cache key returned by music.cache.get.", + "cache_key": "Exact recognition-cache key returned by the corresponding recognition-cache get operation.", "channel": "Message channel that originally submitted the download.", "children": "Child storage items nested below this item.", "code": "Two-factor verification code or site-specific authentication secret.", @@ -331,6 +339,7 @@ FIELD_DESCRIPTIONS = { "limit_count": "Maximum number of site requests allowed in one rate-limit interval.", "limit_interval": "Number of requests in the site's rate-limit window.", "limit_seconds": "Site rate-limit window length in seconds.", + "limit": "Maximum number of recent subscription-search batches to return.", "logid": "One download-history or transfer-log identifier used by manual transfer.", "logids": "Multiple download-history or transfer-log identifiers included in manual transfer.", "match_field": "Object field used to match one list item during an upsert or removal.", @@ -624,6 +633,9 @@ FIELD_DESCRIPTIONS.update( MODEL_DESCRIPTIONS = { "AgentCommandRunRequest": "Slash-command execution request.", "Body_add_api_v1_download_add_post": "MoviePilot download submission request.", + "Body_download_artist_collection_api_v1_download_artist_collection_post": ( + "Artist-wide music collection download submission request." + ), "CustomFilterRuleCreateRequest": "Custom filter-rule creation request.", "CustomFilterRuleUpdateRequest": "Custom filter-rule update request.", "CustomIdentifiersUpdateRequest": "Complete custom recognition-identifier replacement request.", diff --git a/app/agent/policy/resources/api_mcp_schema.json b/app/agent/policy/resources/api_mcp_schema.json index 5f816c43a..0a23e399b 100644 --- a/app/agent/policy/resources/api_mcp_schema.json +++ b/app/agent/policy/resources/api_mcp_schema.json @@ -449,6 +449,63 @@ "title": "Body_add_api_v1_download_add_post", "type": "object" }, + "Body_download_artist_collection_api_v1_download_artist_collection_post": { + "description": "Artist-wide music collection download submission request.", + "properties": { + "artist_id": { + "description": "Source-native artist ID returned by music search or an album detail response.", + "minLength": 1, + "title": "Artist Id", + "type": "string" + }, + "artist_name": { + "description": "Artist name preserved as the identity label for an artist-wide music collection.", + "minLength": 1, + "title": "Artist Name", + "type": "string" + }, + "downloader": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Configured downloader instance name.", + "title": "Downloader" + }, + "media_source": { + "$ref": "#/$defs/MediaSource", + "description": "Metadata source identifier. Preserve the exact value returned with media_id." + }, + "save_path": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Configured downloader-side save path for the download or subscription.", + "title": "Save Path" + }, + "torrent_in": { + "$ref": "#/$defs/TorrentInfo", + "description": "Complete torrent candidate returned by search.results or search.torrents." + } + }, + "required": [ + "artist_name", + "artist_id", + "media_source", + "torrent_in" + ], + "title": "Body_download_artist_collection_api_v1_download_artist_collection_post", + "type": "object" + }, "Body_recommend_search_results_api_v1_search_recommend_post": { "description": "Torrent search results and recommendation controls supplied to the configured model.", "properties": { @@ -7020,6 +7077,27 @@ "title": "download.add", "type": "object" }, + { + "additionalProperties": false, + "description": "Submit one artist-wide music torrent to MoviePilot's collection download workflow. Method: POST. Path: /api/v1/download/artist-collection. Effect: external_side_effect.", + "properties": { + "body": { + "$ref": "#/$defs/Body_download_artist_collection_api_v1_download_artist_collection_post", + "description": "Request value for download.artist_collection. Submit one artist-wide music torrent to MoviePilot's collection download workflow. Use the exact type and fields below." + }, + "operation_id": { + "const": "download.artist_collection", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id", + "body" + ], + "title": "download.artist_collection", + "type": "object" + }, { "additionalProperties": false, "description": "List enabled downloader instance names and provider types without credentials. Method: GET. Path: /api/v1/download/clients. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", @@ -7778,6 +7856,70 @@ "total_count_field": null } }, + { + "additionalProperties": false, + "description": "Clear the complete administrator-only TMDB recognition cache. Method: DELETE. Path: /api/v1/tmdb/cache. Effect: destructive_write.", + "properties": { + "operation_id": { + "const": "media.cache.clear", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "media.cache.clear", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Delete one administrator-only TMDB recognition-cache entry by exact key. Method: DELETE. Path: /api/v1/tmdb/cache/{cache_key}. Effect: destructive_write.", + "properties": { + "operation_id": { + "const": "media.cache.delete", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for media.cache.delete. Delete one administrator-only TMDB recognition-cache entry by exact key. Use only the named fields below.", + "properties": { + "cache_key": { + "description": "Exact recognition-cache key returned by the corresponding recognition-cache get operation.", + "title": "Cache Key", + "type": "string" + } + }, + "required": [ + "cache_key" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "media.cache.delete", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Inspect the administrator-only TMDB recognition cache and summary counts. Method: GET. Path: /api/v1/tmdb/cache. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "media.cache.get", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "media.cache.get", + "type": "object" + }, { "additionalProperties": false, "description": "Read the media-classification field catalog and source capabilities. Method: GET. Path: /api/v1/media/classification/fields. Effect: safe_read.", @@ -9149,7 +9291,7 @@ "description": "Resource identity placeholders for music.cache.delete. Delete one administrator-only MusicBrainz recognition-cache entry by exact key. Use only the named fields below.", "properties": { "cache_key": { - "description": "Exact recognition-cache key returned by music.cache.get.", + "description": "Exact recognition-cache key returned by the corresponding recognition-cache get operation.", "title": "Cache Key", "type": "string" } @@ -12428,6 +12570,105 @@ "title": "subscription.delete_by_media", "type": "object" }, + { + "additionalProperties": false, + "description": "Request cancellation of one durable subscription-search batch before its download side-effect boundary. Method: PUT. Path: /api/v1/subscribe/execution/batches/{batch_id}/cancel. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "subscription.execution.cancel", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for subscription.execution.cancel. Request cancellation of one durable subscription-search batch before its download side-effect boundary. Use only the named fields below.", + "properties": { + "batch_id": { + "description": "Stable subscription search batch identifier.", + "title": "Batch Id", + "type": "string" + } + }, + "required": [ + "batch_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "subscription.execution.cancel", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read one durable subscription-search batch visible to the current user. Method: GET. Path: /api/v1/subscribe/execution/batches/{batch_id}. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "subscription.execution.get", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for subscription.execution.get. Read one durable subscription-search batch visible to the current user. Use only the named fields below.", + "properties": { + "batch_id": { + "description": "Stable subscription search batch identifier.", + "title": "Batch Id", + "type": "string" + } + }, + "required": [ + "batch_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "subscription.execution.get", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List recent durable subscription-search batches visible to the current user. Method: GET. Path: /api/v1/subscribe/execution/batches. Effect: safe_read. Collection response: data remains a list and successful gateway output adds collection.result_count. collection.total_count is omitted when the endpoint or its upstream source does not expose a total.", + "properties": { + "operation_id": { + "const": "subscription.execution.list", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for subscription.execution.list. List recent durable subscription-search batches visible to the current user. Use only the named fields below.", + "properties": { + "limit": { + "default": 10, + "description": "Maximum number of recent subscription-search batches to return.", + "title": "Limit", + "type": "integer" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id" + ], + "title": "subscription.execution.list", + "type": "object", + "x-moviepilot-collection": { + "body_shape": "list", + "default_pagination": "endpoint-defined", + "result_count_field": "collection.result_count", + "total_count_field": null + } + }, { "additionalProperties": false, "description": "Read local library and transfer-file coverage for one accessible subscription. Method: GET. Path: /api/v1/subscribe/files/{subscribe_id}. Effect: safe_read.", @@ -15458,6 +15699,7 @@ "database.backups.list", "database.backups.verify", "download.add", + "download.artist_collection", "download.clients", "download.history.delete", "download.history.list", @@ -15475,6 +15717,9 @@ "filter.test", "library.exists", "library.latest", + "media.cache.clear", + "media.cache.delete", + "media.cache.get", "media.classification.fields", "media.classification.policy.get", "media.classification.policy.history", @@ -15576,6 +15821,9 @@ "subscription.add", "subscription.delete", "subscription.delete_by_media", + "subscription.execution.cancel", + "subscription.execution.get", + "subscription.execution.list", "subscription.files", "subscription.find", "subscription.follow.add", diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index 4f108f513..beeb0cd90 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -802,7 +802,7 @@ flowchart LR 保留多余导出。 当前架构问题、优先级、分阶段实施步骤与验收门禁见 -[`docs/architecture/optimization-checklist.md`](architecture/optimization-checklist.md)。 +[`docs/refactor/optimization-checklist.md`](refactor/optimization-checklist.md)。 --- @@ -818,7 +818,7 @@ flowchart LR | [`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/architecture/optimization-checklist.md`](architecture/optimization-checklist.md) | 当前架构差距、优先级与可执行优化清单 | -| [`docs/architecture/refactor-roadmap.md`](architecture/refactor-roadmap.md) | 多级 Goal、叶子依赖、清零条件与交付状态 | +| [`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 可靠性分级与完成语义决策 | diff --git a/docs/mcp-api.md b/docs/mcp-api.md index ec82ef4d2..c8ff7485b 100644 --- a/docs/mcp-api.md +++ b/docs/mcp-api.md @@ -56,27 +56,27 @@ MCP 当前不会主动发送工具列表变更通知(`listChanged=false`)。 `app/agent/policy/resources/api_mcp_schema.json` 是 `moviepilot_api` 的生成制品,不是设置项或 API 参数的手工事实源。`scripts/generate_agent_api_mcp_schema.py` 从当前 FastAPI OpenAPI、固定 operation 路由和 Agent 专用英文参数说明生成该文件;运行时直接读取它响应外部 MCP `tools/list`,测试会校验生成结果没有漂移。修改 API、请求模型或 operation 后应重新生成并提交该文件,不应直接编辑 JSON。 -当前完整 FastAPI OpenAPI 包含 397 个 HTTP 操作,其中 211 个稳定业务操作进入 -`moviepilot_api`,使用 209 个固定路由模板:208 条 OpenAPI 路由直接匹配,另有 1 条只允许 +当前完整 FastAPI OpenAPI 包含 397 个 HTTP 操作,其中 218 个稳定业务操作进入 +`moviepilot_api`,使用 216 个固定路由模板:215 条 OpenAPI 路由直接匹配,另有 1 条只允许 `tmdb`、`douban`、`bangumi`、`anilist` 四个来源的受限人物作品动态路由。每个 operation 均同时具备固定 method/path、角色权限、副作用等级、确认与恢复策略、结果敏感性、英文用途说明, 以及可直接提交的 path/query/body JSON Schema;Skill front matter、正文 operation 章节、运行时 -注册表和 MCP `tools/list` 的 211 个 `oneOf` 分支必须完全一致。 +注册表和 MCP `tools/list` 的 218 个 `oneOf` 分支必须完全一致。 数量不相等是明确的安全与语义边界,而不是漏生成。当前 397 条路由均被审计并锁定为以下一种 归属,审计生成器不再提供“未归类”兜底: | 归属 | 数量 | Agent 使用方式 | | :--- | ---: | :--- | -| `gateway` | 208 | 通过 `moviepilot_api` 的稳定 operation 和精确参数合同调用 | -| `consolidated` | 74 | 通过同领域聚合 operation 调用,不复制数据源或前端专用路由 | -| `provider-skill` | 13 | 通过下载器或媒体服务器 Skill 调用第三方 provider API | +| `gateway` | 215 | 通过 `moviepilot_api` 的稳定 operation 和精确参数合同调用 | +| `consolidated` | 71 | 通过同领域聚合 operation 调用,不复制数据源或前端专用路由 | +| `provider-skill` | 12 | 通过下载器或媒体服务器 Skill 调用第三方 provider API | | `alternate-auth-duplicate` | 11 | 使用对应 bearer-authenticated gateway operation,不暴露 API_TOKEN 兼容副本 | | `transport_or_identity` | 66 | 由登录、令牌、MCP、会话、回调、健康检查等宿主传输/身份边界拥有 | | `stream_or_binary` | 10 | 由直接客户端处理流式日志、消息、文件、图片等非结构化响应 | -| `ui_presentation` | 15 | 由前端或插件渲染面拥有,不作为业务 Agent operation | +| `ui_presentation` | 12 | 由前端或插件渲染面拥有,不作为业务 Agent operation | -逐路由归属见 `docs/architecture/agent-api-surface-audit.md`,并由 +逐路由归属见 `docs/refactor/agent-api-surface-audit.md`,并由 `tests/test_agent_api_surface_audit.py` 对当前 OpenAPI、固定注册表、MCP schema、英文 Skill 合同及全部非网关归属做漂移检查。任何新增端点必须先明确归属;对 Agent 开放时还必须补齐 operation ID、权限、副作用、确认、恢复、结果敏感性及精确参数说明。 @@ -454,12 +454,12 @@ AMLL 使用无需鉴权的原生搜索与获取接口,先尝试 ISRC,再核 | 方法 | 路径 | 说明 | | :--- | :--- | :--- | -| GET | `/api/v1/tmdb/cache` | 查询 TheMovieDb 识别缓存统计、共享识别累计成功命中次数及开关状态 | -| DELETE | `/api/v1/tmdb/cache/{cache_key}` | 按缓存键删除单条 TheMovieDb 识别缓存,缓存键需要进行 URL 编码 | -| DELETE | `/api/v1/tmdb/cache` | 清空全部 TheMovieDb 识别缓存 | -| GET | `/api/v1/music/cache` | 查询 MusicBrainz 音乐识别缓存统计及条目列表 | -| DELETE | `/api/v1/music/cache/{cache_key}` | 按缓存键删除单条音乐识别缓存,缓存键需要进行 URL 编码 | -| DELETE | `/api/v1/music/cache` | 清空全部音乐识别缓存 | +| GET | `/api/v1/tmdb/cache` | `media.cache.get`:查询 TheMovieDb 识别缓存统计、共享识别累计成功命中次数及开关状态 | +| DELETE | `/api/v1/tmdb/cache/{cache_key}` | `media.cache.delete`:按缓存键删除单条 TheMovieDb 识别缓存,缓存键需要进行 URL 编码 | +| DELETE | `/api/v1/tmdb/cache` | `media.cache.clear`:清空全部 TheMovieDb 识别缓存 | +| GET | `/api/v1/music/cache` | `music.cache.get`:查询 MusicBrainz 音乐识别缓存统计及条目列表 | +| DELETE | `/api/v1/music/cache/{cache_key}` | `music.cache.delete`:按缓存键删除单条音乐识别缓存,缓存键需要进行 URL 编码 | +| DELETE | `/api/v1/music/cache` | `music.cache.clear`:清空全部音乐识别缓存 | TMDB 缓存查询响应的 `data` 包含 `count`、`recognized`、`unrecognized`、`data`,以及共享识别统计字段 `shared_recognized` 和开关字段 `shared_recognize_enabled`。共享命中次数仅在共享结果驱动的二次媒体识别成功后累计。 @@ -470,6 +470,18 @@ TMDB 缓存查询响应的 `data` 包含 `count`、`recognized`、`unrecognized` 旧版未包含这些证据的派生缓存在升级后重新建立,不影响下载历史或订阅数据。 名称确认规则更新时同样重建旧派生缓存,避免艺术家前后缀误截断的旧结果继续命中。 +### 订阅搜索执行批次 + +订阅搜索会持久化为可恢复的执行批次,Agent 可以使用以下 `moviepilot_api` operation 查询或停止当前用户可见的批次: + +| Operation | 方法 | 路径 | 说明 | +| :--- | :--- | :--- | :--- | +| `subscription.execution.list` | GET | `/api/v1/subscribe/execution/batches` | 查询最近的订阅搜索批次,`limit` 默认 10 | +| `subscription.execution.get` | GET | `/api/v1/subscribe/execution/batches/{batch_id}` | 查询一个批次的状态、进度、恢复信息和最终结果 | +| `subscription.execution.cancel` | PUT | `/api/v1/subscribe/execution/batches/{batch_id}/cancel` | 在下载副作用边界前请求取消一个批次 | + +取消是幂等的状态请求,不会撤销已经提交到下载器的任务;批次详情中的状态和任务结果才是最终事实。 + ### 单条订阅搜索周期 `POST /api/v1/subscribe/` 和 `PUT /api/v1/subscribe/` 支持 `search_interval`: @@ -600,8 +612,9 @@ Web Agent 直接调用 `moviepilot_api` 时,宿主会自动加载 `moviepilot- | :--- | :--- | | 媒体/搜索 | `media.search`、`media.person.search`、`media.person.credits`、`media.recognize`、`media.scrape`、`media.episode_schedule`、`media.detail`、`search.torrents`、`search.results`、`recommendation.list` | | 媒体自动分类 | `media.classification.fields`、`media.classification.policy.get`、`media.classification.policy.validate`、`media.classification.policy.preview`、`media.classification.policy.impact`、`media.classification.policy.history`、`media.classification.policy.update`、`media.classification.policy.rollback` | -| 订阅 | `subscription.add`、`subscription.update`、`subscription.search`、`subscription.list`、`subscription.shares`、`subscription.popular`、`subscription.history`、`subscription.delete` | -| 下载/历史 | `download.add`、`download.history.delete`、`transfer.history.delete` | +| 订阅 | `subscription.add`、`subscription.update`、`subscription.search`、`subscription.list`、`subscription.shares`、`subscription.popular`、`subscription.history`、`subscription.delete`、`subscription.execution.list`、`subscription.execution.get`、`subscription.execution.cancel` | +| 下载/历史 | `download.add`、`download.artist_collection`、`download.history.delete`、`transfer.history.delete` | +| 媒体缓存 | `media.cache.get`、`media.cache.delete`、`media.cache.clear`、`music.cache.get`、`music.cache.delete`、`music.cache.clear` | | 媒体库/存储/转移 | `library.exists`、`storage.settings`、`storage.list`、`transfer.history`、`transfer.file` | | 站点 | `site.list`、`site.update`、`site.userdata`、`site.test`、`site.cookie.update` | | 调度/工作流 | `scheduler.list`、`scheduler.run`、`workflow.list`、`workflow.run` | diff --git a/docs/architecture/agent-api-surface-audit.json b/docs/refactor/agent-api-surface-audit.json similarity index 98% rename from docs/architecture/agent-api-surface-audit.json rename to docs/refactor/agent-api-surface-audit.json index abacfae93..bc1710e7a 100644 --- a/docs/architecture/agent-api-surface-audit.json +++ b/docs/refactor/agent-api-surface-audit.json @@ -1,12 +1,12 @@ { "disposition_counts": { "alternate-auth-duplicate": 11, - "consolidated": 74, - "gateway": 208, - "provider-skill": 13, + "consolidated": 71, + "gateway": 215, + "provider-skill": 12, "stream_or_binary": 10, "transport_or_identity": 66, - "ui_presentation": 15 + "ui_presentation": 12 }, "dynamic_gateway_routes": [ { @@ -18,9 +18,9 @@ "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": 209, - "gateway_operation_count": 211, - "matched_gateway_http_route_count": 208, + "gateway_http_route_count": 216, + "gateway_operation_count": 218, + "matched_gateway_http_route_count": 215, "openapi_operation_count": 397, "operations": [ { @@ -640,12 +640,14 @@ ] }, { - "disposition": "provider-skill", + "disposition": "gateway", "method": "POST", - "operation_ids": [], - "owner": "downloader-operation", + "operation_ids": [ + "download.artist_collection" + ], + "owner": "moviepilot-api", "path": "/api/v1/download/artist-collection", - "reason": "Low-level provider behavior is exposed by the self-describing provider Skill; high-level MoviePilot operations remain in moviepilot-api.", + "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", "summary": "添加艺术家合集下载", "tags": [ "download" @@ -3616,36 +3618,42 @@ ] }, { - "disposition": "ui_presentation", + "disposition": "gateway", "method": "GET", - "operation_ids": [], - "owner": "host-ui", + "operation_ids": [ + "subscription.execution.list" + ], + "owner": "moviepilot-api", "path": "/api/v1/subscribe/execution/batches", - "reason": "Background subscription execution status and cancellation are owned by the authenticated frontend workflow; they are not yet a stable Agent gateway contract.", + "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", "summary": "查看订阅搜索进度", "tags": [ "subscribe" ] }, { - "disposition": "ui_presentation", + "disposition": "gateway", "method": "GET", - "operation_ids": [], - "owner": "host-ui", + "operation_ids": [ + "subscription.execution.get" + ], + "owner": "moviepilot-api", "path": "/api/v1/subscribe/execution/batches/{batch_id}", - "reason": "Background subscription execution status and cancellation are owned by the authenticated frontend workflow; they are not yet a stable Agent gateway contract.", + "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", "summary": "查看一次订阅搜索", "tags": [ "subscribe" ] }, { - "disposition": "ui_presentation", + "disposition": "gateway", "method": "PUT", - "operation_ids": [], - "owner": "host-ui", + "operation_ids": [ + "subscription.execution.cancel" + ], + "owner": "moviepilot-api", "path": "/api/v1/subscribe/execution/batches/{batch_id}/cancel", - "reason": "Background subscription execution status and cancellation are owned by the authenticated frontend workflow; they are not yet a stable Agent gateway contract.", + "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", "summary": "停止一次订阅搜索", "tags": [ "subscribe" @@ -4502,36 +4510,42 @@ ] }, { - "disposition": "consolidated", + "disposition": "gateway", "method": "DELETE", - "operation_ids": [], + "operation_ids": [ + "media.cache.clear" + ], "owner": "moviepilot-api", "path": "/api/v1/tmdb/cache", - "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.", + "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", "summary": "清空 TheMovieDb 识别缓存", "tags": [ "tmdb" ] }, { - "disposition": "consolidated", + "disposition": "gateway", "method": "GET", - "operation_ids": [], + "operation_ids": [ + "media.cache.get" + ], "owner": "moviepilot-api", "path": "/api/v1/tmdb/cache", - "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.", + "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", "summary": "查询 TheMovieDb 识别缓存", "tags": [ "tmdb" ] }, { - "disposition": "consolidated", + "disposition": "gateway", "method": "DELETE", - "operation_ids": [], + "operation_ids": [ + "media.cache.delete" + ], "owner": "moviepilot-api", "path": "/api/v1/tmdb/cache/{cache_key}", - "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.", + "reason": "Executable through moviepilot_api; exact inputs are generated into MCP tools/list and SKILL.md.", "summary": "删除指定 TheMovieDb 识别缓存", "tags": [ "tmdb" diff --git a/docs/architecture/agent-api-surface-audit.md b/docs/refactor/agent-api-surface-audit.md similarity index 97% rename from docs/architecture/agent-api-surface-audit.md rename to docs/refactor/agent-api-surface-audit.md index 85801b671..ef681f05f 100644 --- a/docs/architecture/agent-api-surface-audit.md +++ b/docs/refactor/agent-api-surface-audit.md @@ -6,9 +6,9 @@ ## Result - OpenAPI HTTP operations: **397** -- Stable `moviepilot_api` operations: **211** -- Exact HTTP routes used by the gateway: **209** -- OpenAPI routes matched directly by the gateway: **208** +- Stable `moviepilot_api` operations: **218** +- Exact HTTP routes used by the gateway: **216** +- OpenAPI routes matched directly by the gateway: **215** - 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. @@ -18,12 +18,12 @@ | disposition | count | meaning | | :--- | ---: | :--- | | `alternate-auth-duplicate` | 11 | API-token compatibility duplicate of a bearer-authenticated capability. | -| `consolidated` | 74 | Source/UI route represented by a stable aggregate Agent operation. | -| `gateway` | 208 | Approved structured MoviePilot Agent operation. | -| `provider-skill` | 13 | Low-level downloader or media-server capability owned by a provider Skill. | +| `consolidated` | 71 | Source/UI route represented by a stable aggregate Agent operation. | +| `gateway` | 215 | Approved structured MoviePilot Agent operation. | +| `provider-skill` | 12 | Low-level downloader or media-server capability owned by a provider Skill. | | `stream_or_binary` | 10 | Streaming or binary response owned by a direct client transport. | | `transport_or_identity` | 66 | Authentication, protocol, callback, account, or conversation transport boundary. | -| `ui_presentation` | 15 | Frontend or plugin-rendered presentation contract. | +| `ui_presentation` | 12 | Frontend or plugin-rendered presentation contract. | ## Bounded Dynamic Routes @@ -84,7 +84,7 @@ | `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 | `provider-skill` | downloader-operation | 添加艺术家合集下载 | +| `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 | 开始任务 | @@ -310,9 +310,9 @@ | `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 | `ui_presentation` | host-ui | 查看订阅搜索进度 | -| `GET` | `/api/v1/subscribe/execution/batches/{batch_id}` | subscribe | `ui_presentation` | host-ui | 查看一次订阅搜索 | -| `PUT` | `/api/v1/subscribe/execution/batches/{batch_id}/cancel` | subscribe | `ui_presentation` | host-ui | 停止一次订阅搜索 | +| `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的订阅分享人 | @@ -375,9 +375,9 @@ | `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 | `consolidated` | moviepilot-api | 清空 TheMovieDb 识别缓存 | -| `GET` | `/api/v1/tmdb/cache` | tmdb | `consolidated` | moviepilot-api | 查询 TheMovieDb 识别缓存 | -| `DELETE` | `/api/v1/tmdb/cache/{cache_key}` | tmdb | `consolidated` | moviepilot-api | 删除指定 TheMovieDb 识别缓存 | +| `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 | 人物参演作品 | diff --git a/docs/architecture/agent-tool-refactor-plan.md b/docs/refactor/agent-tool-refactor-plan.md similarity index 99% rename from docs/architecture/agent-tool-refactor-plan.md rename to docs/refactor/agent-tool-refactor-plan.md index 02f9bbbc8..abb77953c 100644 --- a/docs/architecture/agent-tool-refactor-plan.md +++ b/docs/refactor/agent-tool-refactor-plan.md @@ -320,7 +320,7 @@ action,并使用 MoviePilot 已配置的具体服务实例访问其自身 API - 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/architecture/optimization-checklist.md` 同步为 `9,605 / 574`,本地 `test_architecture_contract_baseline.py` 与 `test_architecture_baseline_cli.py` 共 45 项通过,并确认 `baseline.py --check-host` 成功 +- 已将 `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:最终提交与远端门禁完成 diff --git a/docs/architecture/media-classification-design.md b/docs/refactor/media-classification-design.md similarity index 100% rename from docs/architecture/media-classification-design.md rename to docs/refactor/media-classification-design.md diff --git a/docs/architecture/media-search-design.md b/docs/refactor/media-search-design.md similarity index 100% rename from docs/architecture/media-search-design.md rename to docs/refactor/media-search-design.md diff --git a/docs/architecture/optimization-checklist.md b/docs/refactor/optimization-checklist.md similarity index 100% rename from docs/architecture/optimization-checklist.md rename to docs/refactor/optimization-checklist.md diff --git a/docs/architecture/refactor-roadmap.md b/docs/refactor/refactor-roadmap.md similarity index 100% rename from docs/architecture/refactor-roadmap.md rename to docs/refactor/refactor-roadmap.md diff --git a/docs/architecture/subscription-search-review.md b/docs/refactor/subscription-search-review.md similarity index 100% rename from docs/architecture/subscription-search-review.md rename to docs/refactor/subscription-search-review.md diff --git a/docs/subscribe-lifecycle.md b/docs/subscribe-lifecycle.md index 90b508479..11936b12f 100644 --- a/docs/subscribe-lifecycle.md +++ b/docs/subscribe-lifecycle.md @@ -89,7 +89,7 @@ MoviePilot 当前以媒体类型和洗版方式组合出常用订阅模式。普 站点等待不代表搜索失败。其它站点仍在执行时保持搜索状态;本轮结束并保存续跑时间后才进入等待。 订阅搜索的同站唯一在途预算、站点访问间隔和错误冷却始终生效。机制审查及后续优化边界见 -[订阅搜索机制审查](architecture/subscription-search-review.md)。 +[订阅搜索机制审查](refactor/subscription-search-review.md)。 ### 事实写入 diff --git a/scripts/generate_agent_api_surface_audit.py b/scripts/generate_agent_api_surface_audit.py index dbd2536b1..1f3253655 100644 --- a/scripts/generate_agent_api_surface_audit.py +++ b/scripts/generate_agent_api_surface_audit.py @@ -18,8 +18,8 @@ if str(PROJECT_ROOT) not in sys.path: from app.agent.policy.api import API_OPERATION_ROUTES # noqa: E402 from app.api.apiv1 import api_router # noqa: E402 -JSON_OUTPUT = PROJECT_ROOT / "docs/architecture/agent-api-surface-audit.json" -MARKDOWN_OUTPUT = PROJECT_ROOT / "docs/architecture/agent-api-surface-audit.md" +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( diff --git a/skills/moviepilot-api/SKILL.md b/skills/moviepilot-api/SKILL.md index 30b6bf008..520821744 100644 --- a/skills/moviepilot-api/SKILL.md +++ b/skills/moviepilot-api/SKILL.md @@ -13,23 +13,24 @@ allowed-api-operations: >- media.search media.person.search media.person.credits media.recognize media.scrape media.episode_schedule media.detail subscription.add subscription.update subscription.search subscription.list subscription.shares subscription.popular subscription.history - subscription.delete download.add download.tasks.active download.clients download.paths - download.history.list download.history.delete transfer.history.delete site.update site.list - site.userdata site.test site.cookie.update recommendation.list library.exists library.latest - storage.settings storage.list transfer.history transfer.file scheduler.list scheduler.run - workflow.list workflow.run plugin.installed plugin.market plugin.capabilities plugin.config.get - plugin.config.update plugin.source.options plugin.source.install plugin.source.change - plugin.reload plugin.install plugin.uninstall slash.list config.identifiers.get - config.identifiers.update search.torrents search.results filter.builtin filter.custom - filter.groups filter.custom.add filter.custom.update filter.custom.delete filter.group.add - filter.group.update filter.group.delete plugin.data config.system.get config.system.update - slash.run music.recognize music.explore music.album.get music.album.related music.artist.get - music.artist.albums music.artist.related music.cache.get music.cache.delete music.cache.clear - system.versions system.update.status system.update.check system.update.download system.restart - system.update.install system.upgrade.dev dashboard.media.statistics dashboard.storage - dashboard.processes dashboard.system dashboard.downloader scheduler.progress - dashboard.transfer.statistics dashboard.cpu dashboard.memory dashboard.network media.sources - media.recognize_file media.classification.fields media.classification.policy.get + subscription.delete download.add download.artist_collection download.tasks.active + download.clients download.paths download.history.list download.history.delete + transfer.history.delete site.update site.list site.userdata site.test site.cookie.update + recommendation.list library.exists library.latest storage.settings storage.list transfer.history + transfer.file scheduler.list scheduler.run workflow.list workflow.run plugin.installed + plugin.market plugin.capabilities plugin.config.get plugin.config.update plugin.source.options + plugin.source.install plugin.source.change plugin.reload plugin.install plugin.uninstall + slash.list config.identifiers.get config.identifiers.update search.torrents search.results + filter.builtin filter.custom filter.groups filter.custom.add filter.custom.update + filter.custom.delete filter.group.add filter.group.update filter.group.delete plugin.data + config.system.get config.system.update slash.run music.recognize music.explore music.album.get + music.album.related music.artist.get music.artist.albums music.artist.related music.cache.get + music.cache.delete music.cache.clear system.versions system.update.status system.update.check + system.update.download system.restart system.update.install system.upgrade.dev + dashboard.media.statistics dashboard.storage dashboard.processes dashboard.system + dashboard.downloader scheduler.progress dashboard.transfer.statistics dashboard.cpu + dashboard.memory dashboard.network media.sources media.recognize_file media.cache.get + media.cache.delete media.cache.clear media.classification.fields media.classification.policy.get media.classification.policy.validate media.classification.policy.preview media.classification.policy.impact media.classification.policy.history media.classification.policy.update media.classification.policy.rollback media.episode_groups @@ -40,10 +41,11 @@ allowed-api-operations: >- site.statistic site.mapping site.supporting subscription.get subscription.find subscription.delete_by_media subscription.status.update subscription.reset subscription.search_all subscription.refresh subscription.metadata.refresh - subscription.history.delete subscription.user.list subscription.files subscription.share - subscription.share.delete subscription.fork subscription.follow.list subscription.follow.add - subscription.follow.delete subscription.share.statistics storage.manage storage.mkdir - storage.rename storage.delete transfer.queue transfer.queue.delete transfer.name + subscription.history.delete subscription.user.list subscription.files + subscription.execution.list subscription.execution.get subscription.execution.cancel + subscription.share subscription.share.delete subscription.fork subscription.follow.list + subscription.follow.add subscription.follow.delete subscription.share.statistics storage.manage + storage.mkdir storage.rename storage.delete transfer.queue transfer.queue.delete transfer.name transfer.target_path transfer.manual_history transfer.episode_format.recommend transfer.manual_reviews transfer.manual_review transfer.manual_review.resolve transfer.history.redo transfer.history.redo_batch transfer.history.clear workflow.create @@ -150,7 +152,7 @@ must be one of the following before the Agent may use its capability: and English parameter contract is added. The maintained route-by-route inventory is -`docs/architecture/agent-api-surface-audit.md`. Its generated drift test fails +`docs/refactor/agent-api-surface-audit.md`. Its generated drift test fails when OpenAPI changes without an explicit ownership decision. The management recovery route `POST /api/v1/history/transfer/{history_id}/discard-corrupt` diff --git a/skills/moviepilot-api/api/download.md b/skills/moviepilot-api/api/download.md index 1b3da7a9f..72d77d682 100644 --- a/skills/moviepilot-api/api/download.md +++ b/skills/moviepilot-api/api/download.md @@ -26,6 +26,13 @@ For `download.add`, `torrent_in` is an object whose minimum usable fields are: object. Load `api/models.md` when the full optional `TorrentInfo` fields are needed. +### `download.artist_collection` +`POST /api/v1/download/artist-collection`; policy effect: `external_side_effect`. +Purpose: Submit one artist-wide music torrent to MoviePilot's collection download workflow. +- `path_params`: none +- `query`: none +- `body`: `artist_id*` (string; minimum length `1`): Source-native artist ID returned by music search or an album detail response.; `artist_name*` (string; minimum length `1`): Artist name preserved as the identity label for an artist-wide music collection.; `downloader` (string|null): Configured downloader instance name.; `media_source*` (MediaSource): Metadata source identifier. Preserve the exact value returned with media_id.; `save_path` (string|null): Configured downloader-side save path for the download or subscription.; `torrent_in*` (TorrentInfo): Complete torrent candidate returned by search.results or search.torrents. + ### `download.clients` `GET /api/v1/download/clients`; policy effect: `safe_read`. Purpose: List enabled downloader instance names and provider types without credentials. diff --git a/skills/moviepilot-api/api/media.md b/skills/moviepilot-api/api/media.md index cf8cf9458..e54bb000e 100644 --- a/skills/moviepilot-api/api/media.md +++ b/skills/moviepilot-api/api/media.md @@ -30,6 +30,27 @@ other clients. ## Operations +### `media.cache.clear` +`DELETE /api/v1/tmdb/cache`; policy effect: `destructive_write`. +Purpose: Clear the complete administrator-only TMDB recognition cache. +- `path_params`: none +- `query`: none +- `body`: none + +### `media.cache.delete` +`DELETE /api/v1/tmdb/cache/{cache_key}`; policy effect: `destructive_write`. +Purpose: Delete one administrator-only TMDB recognition-cache entry by exact key. +- `path_params`: `cache_key*` (string): Exact recognition-cache key returned by the corresponding recognition-cache get operation. +- `query`: none +- `body`: none + +### `media.cache.get` +`GET /api/v1/tmdb/cache`; policy effect: `safe_read`. +Purpose: Inspect the administrator-only TMDB recognition cache and summary counts. +- `path_params`: none +- `query`: none +- `body`: none + ### `media.classification.fields` `GET /api/v1/media/classification/fields`; policy effect: `safe_read`. Purpose: Read the media-classification field catalog and source capabilities. diff --git a/skills/moviepilot-api/api/music.md b/skills/moviepilot-api/api/music.md index 63fe4ad1e..ced0dd42b 100644 --- a/skills/moviepilot-api/api/music.md +++ b/skills/moviepilot-api/api/music.md @@ -83,7 +83,7 @@ Purpose: Clear the complete administrator-only MusicBrainz recognition cache. ### `music.cache.delete` `DELETE /api/v1/music/cache/{cache_key}`; policy effect: `destructive_write`. Purpose: Delete one administrator-only MusicBrainz recognition-cache entry by exact key. -- `path_params`: `cache_key*` (string): Exact recognition-cache key returned by music.cache.get. +- `path_params`: `cache_key*` (string): Exact recognition-cache key returned by the corresponding recognition-cache get operation. - `query`: none - `body`: none diff --git a/skills/moviepilot-api/api/subscription.md b/skills/moviepilot-api/api/subscription.md index ba98c1c93..9fead9a0d 100644 --- a/skills/moviepilot-api/api/subscription.md +++ b/skills/moviepilot-api/api/subscription.md @@ -25,6 +25,28 @@ Purpose: Delete accessible subscriptions matching one canonical media identity. - `query`: `media_source*` (MediaSource): Metadata source identifier. Preserve the exact value returned with media_id.; `music_type` (string|null): Music identity level: recording, album, or artist where supported.; `season` (integer|null): Season number used by the media, search, subscription, or transfer operation. - `body`: none +### `subscription.execution.cancel` +`PUT /api/v1/subscribe/execution/batches/{batch_id}/cancel`; policy effect: `external_side_effect`. +Purpose: Request cancellation of one durable subscription-search batch before its download side-effect boundary. +- `path_params`: `batch_id*` (string): Stable subscription search batch identifier. +- `query`: none +- `body`: none + +### `subscription.execution.get` +`GET /api/v1/subscribe/execution/batches/{batch_id}`; policy effect: `safe_read`. +Purpose: Read one durable subscription-search batch visible to the current user. +- `path_params`: `batch_id*` (string): Stable subscription search batch identifier. +- `query`: none +- `body`: none + +### `subscription.execution.list` +`GET /api/v1/subscribe/execution/batches`; policy effect: `safe_read`. +Purpose: List recent durable subscription-search batches visible to the current user. +- `response`: `data` remains a list and `collection.result_count` reports the returned items. `collection.total_count` is omitted because this endpoint or its upstream source does not expose a total. +- `path_params`: none +- `query`: `limit` (integer; default `10`): Maximum number of recent subscription-search batches to return. +- `body`: none + ### `subscription.files` `GET /api/v1/subscribe/files/{subscribe_id}`; policy effect: `safe_read`. Purpose: Read local library and transfer-file coverage for one accessible subscription. diff --git a/tests/test_agent_api_gateway.py b/tests/test_agent_api_gateway.py index 2775af81b..588ad819a 100644 --- a/tests/test_agent_api_gateway.py +++ b/tests/test_agent_api_gateway.py @@ -30,12 +30,12 @@ from app.agent.tools.manager import MoviePilotToolsManager def test_api_operation_registry_matches_migration_batches() -> None: """API 操作注册表必须覆盖各迁移批次且每项具有固定路由。""" - assert len(API_FIRST_BATCH_OPERATION_SPECS) == 52 + assert len(API_FIRST_BATCH_OPERATION_SPECS) == 53 assert len(API_PARITY_OPERATION_SPECS) == 15 assert len(API_MUSIC_OPERATION_SPECS) == 10 assert len(API_SYSTEM_OPERATION_SPECS) == 7 - assert len(API_EXTENDED_OPERATION_SPECS) == 127 - assert len(API_OPERATION_SPECS) == 211 + assert len(API_EXTENDED_OPERATION_SPECS) == 133 + assert len(API_OPERATION_SPECS) == 218 assert {spec.operation_id for spec in API_OPERATION_SPECS} == set(API_OPERATION_ROUTES) assert { "download.list", diff --git a/tests/test_agent_api_surface_audit.py b/tests/test_agent_api_surface_audit.py index 3845dec41..aca1afaa3 100644 --- a/tests/test_agent_api_surface_audit.py +++ b/tests/test_agent_api_surface_audit.py @@ -9,8 +9,8 @@ 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/architecture/agent-api-surface-audit.json" -AUDIT_MARKDOWN = PROJECT_ROOT / "docs/architecture/agent-api-surface-audit.md" +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" @@ -60,6 +60,29 @@ def test_cleanup_confirmation_stays_in_authenticated_management_workflow() -> No 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: """The API Skill entrypoint stays concise and every operation has a namespace file.""" entrypoint = API_SKILL.read_text(encoding="utf-8") diff --git a/tests/test_agent_skills_middleware.py b/tests/test_agent_skills_middleware.py index a737c7307..ae7c2f05b 100644 --- a/tests/test_agent_skills_middleware.py +++ b/tests/test_agent_skills_middleware.py @@ -138,7 +138,7 @@ async def test_bundled_moviepilot_api_skill_loads_complete_contract() -> None: assert payload["content_limit_bytes"] == MAX_SKILL_CONTENT_BYTES assert payload["truncated"] is False assert payload["truncation_message"] is None - assert len(payload["skill"]["allowed_api_operations"]) == 211 + assert len(payload["skill"]["allowed_api_operations"]) == 218 assert "## API Category Index" in payload["content"] assert "### `workflow.update`" not in payload["content"] assert "api/workflow.md" in payload["supporting_files"] diff --git a/tests/test_architecture_contract_baseline.py b/tests/test_architecture_contract_baseline.py index bee66b277..6b443bbdb 100644 --- a/tests/test_architecture_contract_baseline.py +++ b/tests/test_architecture_contract_baseline.py @@ -107,13 +107,13 @@ def test_architecture_documents_match_generated_quality_metrics() -> None: checklist = ( PROJECT_ROOT / "docs" - / "architecture" + / "refactor" / "optimization-checklist.md" ).read_text(encoding="utf-8") roadmap = ( PROJECT_ROOT / "docs" - / "architecture" + / "refactor" / "refactor-roadmap.md" ).read_text(encoding="utf-8") edge_count = f"{dependency['edge_count']:,}"