From 6d4d7331d1f474210a6237c4e8880155279b206c Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 1 Sep 2026 01:19:45 +0800 Subject: [PATCH] refactor: complete agent skill API contracts --- app/agent/api/executor.py | 12 +- app/agent/middleware/skills.py | 5 +- app/agent/policy/api.py | 537 +- app/agent/policy/api_mcp_contract.py | 295 - app/agent/policy/api_mcp_schema.json | 7605 ++++++++++++++++- app/agent/policy/mcp.py | 995 +++ app/agent/policy/registry.py | 1 + app/agent/prompt/System Core Prompt.txt | 2 +- app/agent/tools/factory.py | 7 +- app/agent/tools/impl/api.py | 103 +- .../impl/{service_operation.py => service.py} | 245 +- app/api/endpoints/plugin.py | 20 +- app/api/endpoints/rule.py | 45 +- app/api/endpoints/site.py | 71 +- app/api/endpoints/storage.py | 28 +- app/api/endpoints/system.py | 44 +- app/api/endpoints/workflow.py | 51 +- app/application/settings.py | 109 +- app/schemas/exports.py | 1 + app/schemas/plugin.py | 21 +- app/schemas/site.py | 12 +- app/schemas/system.py | 42 +- docs/architecture-overview.md | 4 +- .../architecture/agent-api-surface-audit.json | 4973 +++++++++++ docs/architecture/agent-api-surface-audit.md | 416 + docs/architecture/agent-tool-refactor-plan.md | 43 +- docs/architecture/optimization-checklist.md | 2 +- docs/mcp-api.md | 112 + docs/rules/05-architecture.md | 6 + scripts/generate_agent_api_mcp_schema.py | 2 +- scripts/generate_agent_api_surface_audit.py | 349 + scripts/generate_agent_skill_docs.py | 663 ++ skills/database-operation/SKILL.md | 217 +- skills/database-operation/scripts/mp-db.py | 120 +- skills/downloader-operation/SKILL.md | 230 +- .../scripts/mp-downloader.py | 112 +- skills/mediaserver-operation/SKILL.md | 168 +- .../scripts/mp-mediaserver.py | 80 +- skills/moviepilot-api/SKILL.md | 1717 +++- skills/moviepilot-api/scripts/mp-api.py | 371 - skills/moviepilot-update/SKILL.md | 59 +- skills/moviepilot-update/scripts/mp-update.py | 77 - .../architecture/dependency-baseline.json | 17 +- tests/test_agent_api_gateway.py | 275 +- tests/test_agent_api_projection_endpoints.py | 162 + tests/test_agent_api_surface_audit.py | 109 + tests/test_agent_application_services.py | 15 + tests/test_agent_prompt_secrets.py | 2 + tests/test_agent_skills_middleware.py | 18 + tests/test_builtin_skill_boundaries.py | 184 +- tests/test_service_operation_mcp_tools.py | 111 +- tests/test_service_operation_skills.py | 12 +- tests/test_skill_scripts_security.py | 32 - 53 files changed, 19368 insertions(+), 1541 deletions(-) delete mode 100644 app/agent/policy/api_mcp_contract.py create mode 100644 app/agent/policy/mcp.py rename app/agent/tools/impl/{service_operation.py => service.py} (56%) create mode 100644 docs/architecture/agent-api-surface-audit.json create mode 100644 docs/architecture/agent-api-surface-audit.md create mode 100644 scripts/generate_agent_api_surface_audit.py create mode 100644 scripts/generate_agent_skill_docs.py delete mode 100644 skills/moviepilot-api/scripts/mp-api.py delete mode 100644 skills/moviepilot-update/scripts/mp-update.py create mode 100644 tests/test_agent_api_projection_endpoints.py create mode 100644 tests/test_agent_api_surface_audit.py diff --git a/app/agent/api/executor.py b/app/agent/api/executor.py index 3a06f1a04..9b4b5a6df 100644 --- a/app/agent/api/executor.py +++ b/app/agent/api/executor.py @@ -116,7 +116,7 @@ class MoviePilotApiExecutor: *, path_params: Mapping[str, Any] | None = None, query: Mapping[str, Any] | None = None, - body: Mapping[str, Any] | None = None, + body: Any = None, ) -> str: """执行白名单 operation,并把响应转换为稳定 JSON 文本。""" route = resolve_api_route(operation_id) @@ -125,10 +125,12 @@ class MoviePilotApiExecutor: path = self._render_path(route, path_params or {}) url = f"{self._resolve_base_url()}{path}" query_data = dict(query or {}) - body_data = dict(body or {}) - if route.method == "GET" and body_data: + body_data = dict(body) if isinstance(body, Mapping) else body + if route.method == "GET" and body_data is not None: + if not isinstance(body_data, Mapping): + raise ApiExecutionError("GET operation 的 body 必须是 JSON 对象") query_data.update(body_data) - body_data = {} + body_data = None request = self._request_factory( headers=self._build_headers(), timeout=30, @@ -140,7 +142,7 @@ class MoviePilotApiExecutor: method=route.method, url=url, params=query_data or None, - json=body_data or None, + json=body_data, raise_exception=True, ) if response is None: diff --git a/app/agent/middleware/skills.py b/app/agent/middleware/skills.py index df4e582b2..6c0dac904 100644 --- a/app/agent/middleware/skills.py +++ b/app/agent/middleware/skills.py @@ -33,8 +33,9 @@ from app.agent.skills.metadata import ( from app.agent.tools.tags import ToolTag from app.runtime.log import logger -# 模型返回上限独立于领域层的磁盘读取上限,避免异常内容撑爆上下文。 -MAX_SKILL_RESULT_CHARS = 64 * 1024 +# 模型返回上限独立于领域层的磁盘读取上限;需要容纳完整的内置 API +# 合同,同时继续阻止接近 1 MiB 磁盘上限的异常 Skill 撑满上下文。 +MAX_SKILL_RESULT_CHARS = 256 * 1024 SKILL_CONTENT_TRUNCATION_SUFFIX = "\n...(Skill 内容已截断)" diff --git a/app/agent/policy/api.py b/app/agent/policy/api.py index 4f8b0df80..ff702ad85 100644 --- a/app/agent/policy/api.py +++ b/app/agent/policy/api.py @@ -85,6 +85,23 @@ def _admin_read( return _spec(operation_id, required_role=_ADMIN, result_sensitivity=sensitivity) +def _user_write( + operation_id: str, + *, + effect: ActionEffect = ActionEffect.REVERSIBLE_WRITE, + recovery: RecoveryMode = _IDEMPOTENT, + sensitivity: ResultSensitivity = ResultSensitivity.NORMAL, +) -> ApiOperationSpec: + """构造保留普通用户 API 权限的确认写操作。""" + return _spec( + operation_id, + effect=effect, + confirmation=_CONFIRM, + recovery=recovery, + result_sensitivity=sensitivity, + ) + + API_FIRST_BATCH_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = ( _spec("media.search"), _spec("media.person.search"), @@ -102,18 +119,23 @@ 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.tasks.active"), + _spec("download.clients"), + _spec("download.paths"), + _spec("download.history.list"), _write("download.history.delete", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=_DELETE_RECOVERABLE), _write("transfer.history.delete", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=_DELETE_RECOVERABLE), _write("site.update"), - _admin_read("site.list"), + _spec("site.list"), _admin_read("site.userdata", sensitivity=ResultSensitivity.PRIVATE), - _spec("site.test", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, required_role=_ADMIN, confirmation=_CONFIRM), + _spec("site.test"), _write("site.cookie.update", sensitivity=ResultSensitivity.PRIVATE), _spec("recommendation.list"), - _admin_read("library.exists"), + _spec("library.exists"), + _spec("library.latest"), _admin_read("storage.settings"), - _admin_read("storage.list", sensitivity=ResultSensitivity.PRIVATE), - _admin_read("transfer.history", sensitivity=ResultSensitivity.PRIVATE), + _spec("storage.list", result_sensitivity=ResultSensitivity.PRIVATE), + _spec("transfer.history", result_sensitivity=ResultSensitivity.PRIVATE), _spec( "transfer.file", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, @@ -129,7 +151,7 @@ API_FIRST_BATCH_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = ( confirmation=_CONFIRM, recovery=RecoveryMode.RECONCILE, ), - _admin_read("workflow.list"), + _spec("workflow.list"), _spec( "workflow.run", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, @@ -142,6 +164,21 @@ API_FIRST_BATCH_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = ( _admin_read("plugin.capabilities"), _admin_read("plugin.config.get", sensitivity=ResultSensitivity.PRIVATE), _write("plugin.config.update", sensitivity=ResultSensitivity.PRIVATE), + _admin_read("plugin.source.options"), + _spec( + "plugin.source.install", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + confirmation=_CONFIRM, + recovery=RecoveryMode.RECONCILE, + ), + _spec( + "plugin.source.change", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + confirmation=_CONFIRM, + recovery=RecoveryMode.RECONCILE, + ), _spec( "plugin.reload", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, @@ -166,9 +203,9 @@ API_FIRST_BATCH_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = ( API_PARITY_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = ( _spec("search.torrents"), _spec("search.results"), - _admin_read("filter.builtin"), - _admin_read("filter.custom"), - _admin_read("filter.groups"), + _spec("filter.builtin"), + _spec("filter.custom"), + _spec("filter.groups"), _write("filter.custom.add", recovery=RecoveryMode.TRANSACTION), _write("filter.custom.update", recovery=RecoveryMode.TRANSACTION), _write("filter.custom.delete", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=RecoveryMode.TRANSACTION), @@ -187,8 +224,331 @@ API_PARITY_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = ( ), ) +API_MUSIC_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = ( + _spec("music.recognize"), + _spec("music.explore"), + _spec("music.album.get"), + _spec("music.album.related"), + _spec("music.artist.get"), + _spec("music.artist.albums"), + _spec("music.artist.related"), + _admin_read("music.cache.get", sensitivity=ResultSensitivity.PRIVATE), + _write( + "music.cache.delete", + effect=ActionEffect.DESTRUCTIVE_WRITE, + recovery=RecoveryMode.NONE, + ), + _write( + "music.cache.clear", + effect=ActionEffect.DESTRUCTIVE_WRITE, + recovery=RecoveryMode.NONE, + ), +) -API_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = (*API_FIRST_BATCH_OPERATION_SPECS, *API_PARITY_OPERATION_SPECS) +API_SYSTEM_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = ( + _admin_read("system.versions"), + _admin_read("system.update.status"), + _spec( + "system.update.check", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + recovery=_IDEMPOTENT, + ), + _spec( + "system.update.download", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + confirmation=_CONFIRM, + recovery=RecoveryMode.RECONCILE, + ), + _spec( + "system.restart", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + confirmation=_CONFIRM, + recovery=RecoveryMode.RECONCILE, + ), + _spec( + "system.update.install", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + confirmation=_CONFIRM, + recovery=RecoveryMode.RECONCILE, + ), + _spec( + "system.upgrade.dev", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + confirmation=_CONFIRM, + recovery=RecoveryMode.RECONCILE, + ), +) + + +API_EXTENDED_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = ( + _admin_read("dashboard.media.statistics", sensitivity=ResultSensitivity.PRIVATE), + _admin_read("dashboard.storage", sensitivity=ResultSensitivity.PRIVATE), + _admin_read("dashboard.processes", sensitivity=ResultSensitivity.PRIVATE), + _admin_read("dashboard.system", sensitivity=ResultSensitivity.PRIVATE), + _admin_read("dashboard.downloader", sensitivity=ResultSensitivity.PRIVATE), + _admin_read("scheduler.progress"), + _admin_read("dashboard.transfer.statistics"), + _admin_read("dashboard.cpu"), + _admin_read("dashboard.memory"), + _admin_read("dashboard.network"), + _spec("media.sources"), + _spec("media.recognize_file"), + _spec("media.category.config.get"), + _write("media.category.config.update", recovery=RecoveryMode.TRANSACTION), + _spec("media.categories"), + _spec("media.episode_groups"), + _spec("media.episode_group.seasons"), + _spec("media.seasons"), + _spec("search.title", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=_IDEMPOTENT), + _spec("search.recommend", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=_IDEMPOTENT), + _spec("subtitle.search.title", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=_IDEMPOTENT), + _spec("subtitle.search.media", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=_IDEMPOTENT), + _write("site.add", recovery=RecoveryMode.TRANSACTION, sensitivity=ResultSensitivity.PRIVATE), + _write( + "site.delete", + effect=ActionEffect.DESTRUCTIVE_WRITE, + recovery=_DELETE_RECOVERABLE, + ), + _spec("site.auth.options"), + _spec( + "site.authenticate", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + confirmation=_CONFIRM, + recovery=RecoveryMode.RECONCILE, + result_sensitivity=ResultSensitivity.PRIVATE, + ), + _spec( + "site.cookiecloud.sync", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + confirmation=_CONFIRM, + recovery=RecoveryMode.RECONCILE, + ), + _write( + "site.reset", + effect=ActionEffect.DESTRUCTIVE_WRITE, + recovery=RecoveryMode.RECONCILE, + ), + _write("site.priorities.update", recovery=RecoveryMode.TRANSACTION), + _spec( + "site.userdata.refresh", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + confirmation=_CONFIRM, + recovery=_IDEMPOTENT, + result_sensitivity=ResultSensitivity.PRIVATE, + ), + _admin_read("site.userdata.latest", sensitivity=ResultSensitivity.PRIVATE), + _spec("site.category"), + _spec( + "site.resource", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + recovery=_IDEMPOTENT, + result_sensitivity=ResultSensitivity.PRIVATE, + ), + _admin_read("site.searchable", sensitivity=ResultSensitivity.PRIVATE), + _admin_read("site.rss", sensitivity=ResultSensitivity.PRIVATE), + _spec("site.statistics", result_sensitivity=ResultSensitivity.PRIVATE), + _spec("site.statistic", result_sensitivity=ResultSensitivity.PRIVATE), + _admin_read("site.mapping", sensitivity=ResultSensitivity.PRIVATE), + _admin_read("site.supporting"), + _spec("subscription.get"), + _spec("subscription.find"), + _user_write( + "subscription.delete_by_media", + effect=ActionEffect.DESTRUCTIVE_WRITE, + recovery=_DELETE_RECOVERABLE, + ), + _user_write("subscription.status.update"), + _user_write("subscription.reset"), + _spec("subscription.search_all", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=_IDEMPOTENT), + _spec( + "subscription.refresh", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + confirmation=_CONFIRM, + recovery=RecoveryMode.RECONCILE, + ), + _spec( + "subscription.metadata.refresh", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + confirmation=_CONFIRM, + recovery=RecoveryMode.RECONCILE, + ), + _user_write( + "subscription.history.delete", + effect=ActionEffect.DESTRUCTIVE_WRITE, + recovery=_DELETE_RECOVERABLE, + ), + _spec("subscription.user.list"), + _spec("subscription.files", result_sensitivity=ResultSensitivity.PRIVATE), + _user_write("subscription.share", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=RecoveryMode.RECONCILE), + _user_write( + "subscription.share.delete", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + recovery=RecoveryMode.RECONCILE, + ), + _user_write("subscription.fork", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=RecoveryMode.RECONCILE), + _spec("subscription.follow.list"), + _user_write("subscription.follow.add"), + _user_write("subscription.follow.delete"), + _spec("subscription.share.statistics"), + _spec( + "storage.manage", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + confirmation=_CONFIRM, + recovery=RecoveryMode.RECONCILE, + result_sensitivity=ResultSensitivity.PRIVATE, + ), + _write("storage.mkdir", recovery=RecoveryMode.TRANSACTION), + _write("storage.rename", recovery=RecoveryMode.RECONCILE), + _write( + "storage.delete", + effect=ActionEffect.DESTRUCTIVE_WRITE, + recovery=RecoveryMode.MANUAL_ONLY, + ), + _spec("transfer.queue", result_sensitivity=ResultSensitivity.PRIVATE), + _user_write( + "transfer.queue.delete", + effect=ActionEffect.DESTRUCTIVE_WRITE, + recovery=RecoveryMode.RECONCILE, + ), + _admin_read("transfer.name"), + _admin_read("transfer.target_path", sensitivity=ResultSensitivity.PRIVATE), + _admin_read("transfer.manual_history", sensitivity=ResultSensitivity.PRIVATE), + _admin_read("transfer.episode_format.recommend", sensitivity=ResultSensitivity.PRIVATE), + _admin_read("transfer.manual_reviews", sensitivity=ResultSensitivity.PRIVATE), + _admin_read("transfer.manual_review", sensitivity=ResultSensitivity.PRIVATE), + _write("transfer.manual_review.resolve", recovery=RecoveryMode.RECONCILE), + _spec( + "transfer.history.redo", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + confirmation=_CONFIRM, + recovery=RecoveryMode.RECONCILE, + ), + _spec( + "transfer.history.redo_batch", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + confirmation=_CONFIRM, + recovery=RecoveryMode.RECONCILE, + ), + _write( + "transfer.history.clear", + effect=ActionEffect.DESTRUCTIVE_WRITE, + recovery=RecoveryMode.MANUAL_ONLY, + ), + _write("workflow.create", recovery=RecoveryMode.TRANSACTION), + _admin_read("workflow.get", sensitivity=ResultSensitivity.PRIVATE), + _write("workflow.update", recovery=RecoveryMode.TRANSACTION), + _write("workflow.delete", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=_DELETE_RECOVERABLE), + _admin_read("workflow.actions"), + _admin_read("workflow.event_types"), + _admin_read("workflow.plugin.actions"), + _write("workflow.start"), + _write("workflow.pause"), + _write("workflow.reset", recovery=RecoveryMode.TRANSACTION), + _spec("workflow.shares", required_role=_ADMIN), + _spec( + "workflow.share", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + confirmation=_CONFIRM, + recovery=RecoveryMode.RECONCILE, + ), + _spec( + "workflow.share.delete", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + confirmation=_CONFIRM, + recovery=RecoveryMode.RECONCILE, + ), + _spec( + "workflow.fork", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + confirmation=_CONFIRM, + recovery=RecoveryMode.RECONCILE, + ), + _admin_read("torrent.cache.get", sensitivity=ResultSensitivity.PRIVATE), + _write("torrent.cache.delete", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=RecoveryMode.NONE), + _write("torrent.cache.clear", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=RecoveryMode.NONE), + _spec( + "torrent.cache.refresh", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + confirmation=_CONFIRM, + recovery=RecoveryMode.RECONCILE, + ), + _write("torrent.cache.reidentify", recovery=RecoveryMode.RECONCILE), + _admin_read("database.backups.list", sensitivity=ResultSensitivity.PRIVATE), + _spec( + "database.backups.create", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + confirmation=_CONFIRM, + recovery=RecoveryMode.RECONCILE, + result_sensitivity=ResultSensitivity.PRIVATE, + ), + _admin_read("database.backups.verify", sensitivity=ResultSensitivity.PRIVATE), + _write( + "database.backups.delete", + effect=ActionEffect.DESTRUCTIVE_WRITE, + recovery=RecoveryMode.MANUAL_ONLY, + ), + _spec("filter.test", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=_IDEMPOTENT), + _spec("system.network.targets"), + _spec("system.network.test", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=_IDEMPOTENT), + _spec("system.module.list"), + _spec("system.module.test", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=_IDEMPOTENT), + _spec( + "plugin.market.sync_wiki", + effect=ActionEffect.EXTERNAL_SIDE_EFFECT, + required_role=_ADMIN, + confirmation=_CONFIRM, + recovery=RecoveryMode.RECONCILE, + ), + _admin_read("plugin.runtime.status", sensitivity=ResultSensitivity.PRIVATE), + _admin_read("plugin.history"), + _admin_read("plugin.releases"), + _spec("plugin.ratings"), + _spec("plugin.rating"), + _user_write("plugin.rating.submit", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=_IDEMPOTENT), + _spec("plugin.statistics"), + _write( + "plugin.reset", + effect=ActionEffect.DESTRUCTIVE_WRITE, + recovery=RecoveryMode.MANUAL_ONLY, + ), + _write("plugin.clone", effect=ActionEffect.EXTERNAL_SIDE_EFFECT, recovery=RecoveryMode.RECONCILE), + _spec("config.user.get", result_sensitivity=ResultSensitivity.PRIVATE), + _spec("config.public.get"), + _spec("system.usage.statistics", result_sensitivity=ResultSensitivity.PRIVATE), + _admin_read("plugin.folders.get", sensitivity=ResultSensitivity.PRIVATE), + _write("plugin.folders.update", recovery=RecoveryMode.TRANSACTION), + _write("plugin.folder.create", recovery=RecoveryMode.TRANSACTION), + _write("plugin.folder.delete", effect=ActionEffect.DESTRUCTIVE_WRITE, recovery=_DELETE_RECOVERABLE), + _write("plugin.folder.plugins.update", recovery=RecoveryMode.TRANSACTION), +) + + +API_OPERATION_SPECS: tuple[ApiOperationSpec, ...] = ( + *API_FIRST_BATCH_OPERATION_SPECS, + *API_PARITY_OPERATION_SPECS, + *API_MUSIC_OPERATION_SPECS, + *API_SYSTEM_OPERATION_SPECS, + *API_EXTENDED_OPERATION_SPECS, +) API_OPERATION_BY_ID = {spec.operation_id: spec for spec in API_OPERATION_SPECS} @@ -201,6 +561,16 @@ API_OPERATION_ROUTES: dict[str, ApiOperationRoute] = { "media.scrape": ApiOperationRoute("POST", "/api/v1/media/scrape/{storage}"), "media.episode_schedule": ApiOperationRoute("GET", "/api/v1/tmdb/{tmdbid}/{season}"), "media.detail": ApiOperationRoute("GET", "/api/v1/media/{media_id}"), + "music.recognize": ApiOperationRoute("POST", "/api/v1/music/recognize"), + "music.explore": ApiOperationRoute("GET", "/api/v1/music/explore"), + "music.album.get": ApiOperationRoute("GET", "/api/v1/music/album/{album_id}"), + "music.album.related": ApiOperationRoute("GET", "/api/v1/music/album/{album_id}/related"), + "music.artist.get": ApiOperationRoute("GET", "/api/v1/music/artist/{artist_id}"), + "music.artist.albums": ApiOperationRoute("GET", "/api/v1/music/artist/{artist_id}/albums"), + "music.artist.related": ApiOperationRoute("GET", "/api/v1/music/artist/{artist_id}/related"), + "music.cache.get": ApiOperationRoute("GET", "/api/v1/music/cache"), + "music.cache.delete": ApiOperationRoute("DELETE", "/api/v1/music/cache/{cache_key}"), + "music.cache.clear": ApiOperationRoute("DELETE", "/api/v1/music/cache"), "subscription.add": ApiOperationRoute("POST", "/api/v1/subscribe/"), "subscription.update": ApiOperationRoute("PUT", "/api/v1/subscribe/"), "subscription.search": ApiOperationRoute("GET", "/api/v1/subscribe/search/{subscribe_id}"), @@ -210,28 +580,36 @@ 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.tasks.active": ApiOperationRoute("GET", "/api/v1/download/"), + "download.clients": ApiOperationRoute("GET", "/api/v1/download/clients"), + "download.paths": ApiOperationRoute("GET", "/api/v1/download/paths"), + "download.history.list": ApiOperationRoute("GET", "/api/v1/history/download"), "download.history.delete": ApiOperationRoute("DELETE", "/api/v1/history/download"), "transfer.history.delete": ApiOperationRoute("DELETE", "/api/v1/history/transfer"), - "site.list": ApiOperationRoute("GET", "/api/v1/site/"), + "site.list": ApiOperationRoute("GET", "/api/v1/site/agent"), "site.update": ApiOperationRoute("PUT", "/api/v1/site/"), "site.userdata": ApiOperationRoute("GET", "/api/v1/site/userdata/{site_id}"), "site.test": ApiOperationRoute("GET", "/api/v1/site/test/{site_id}"), "site.cookie.update": ApiOperationRoute("POST", "/api/v1/site/cookie/{site_id}"), "recommendation.list": ApiOperationRoute("GET", "/api/v1/recommend/agent"), "library.exists": ApiOperationRoute("GET", "/api/v1/mediaserver/exists"), + "library.latest": ApiOperationRoute("GET", "/api/v1/mediaserver/latest"), "storage.settings": ApiOperationRoute("GET", "/api/v1/storage/directories"), - "storage.list": ApiOperationRoute("POST", "/api/v1/storage/list"), + "storage.list": ApiOperationRoute("POST", "/api/v1/storage/agent/list"), "transfer.history": ApiOperationRoute("GET", "/api/v1/history/transfer"), "transfer.file": ApiOperationRoute("POST", "/api/v1/transfer/manual"), "scheduler.list": ApiOperationRoute("GET", "/api/v1/dashboard/schedule"), "scheduler.run": ApiOperationRoute("GET", "/api/v1/system/runscheduler"), - "workflow.list": ApiOperationRoute("GET", "/api/v1/workflow/"), + "workflow.list": ApiOperationRoute("GET", "/api/v1/workflow/agent"), "workflow.run": ApiOperationRoute("POST", "/api/v1/workflow/{workflow_id}/run"), - "plugin.installed": ApiOperationRoute("GET", "/api/v1/plugin/installed"), + "plugin.installed": ApiOperationRoute("GET", "/api/v1/plugin/"), "plugin.market": ApiOperationRoute("GET", "/api/v1/plugin/"), "plugin.capabilities": ApiOperationRoute("GET", "/api/v1/plugin/runtime/capabilities"), - "plugin.config.get": ApiOperationRoute("GET", "/api/v1/plugin/{plugin_id}"), + "plugin.config.get": ApiOperationRoute("GET", "/api/v1/plugin/form/{plugin_id}"), "plugin.config.update": ApiOperationRoute("PUT", "/api/v1/plugin/{plugin_id}"), + "plugin.source.options": ApiOperationRoute("GET", "/api/v1/plugin/source/{plugin_id}"), + "plugin.source.install": ApiOperationRoute("POST", "/api/v1/plugin/source/{plugin_id}/install"), + "plugin.source.change": ApiOperationRoute("POST", "/api/v1/plugin/source/{plugin_id}"), "plugin.reload": ApiOperationRoute("GET", "/api/v1/plugin/reload/{plugin_id}"), "plugin.install": ApiOperationRoute("GET", "/api/v1/plugin/install/{plugin_id}"), "plugin.uninstall": ApiOperationRoute("DELETE", "/api/v1/plugin/{plugin_id}"), @@ -253,6 +631,132 @@ API_OPERATION_ROUTES: dict[str, ApiOperationRoute] = { "config.system.get": ApiOperationRoute("GET", "/api/v1/system/settings"), "config.system.update": ApiOperationRoute("POST", "/api/v1/system/settings"), "slash.run": ApiOperationRoute("POST", "/api/v1/message/agent/commands/run"), + "system.versions": ApiOperationRoute("GET", "/api/v1/system/versions"), + "system.restart": ApiOperationRoute("GET", "/api/v1/system/restart"), + "system.update.status": ApiOperationRoute("GET", "/api/v1/system/update/status"), + "system.update.check": ApiOperationRoute("POST", "/api/v1/system/update/check"), + "system.update.download": ApiOperationRoute("POST", "/api/v1/system/update/download"), + "system.update.install": ApiOperationRoute("POST", "/api/v1/system/update/install"), + "system.upgrade.dev": ApiOperationRoute("POST", "/api/v1/system/upgrade"), + "dashboard.media.statistics": ApiOperationRoute("GET", "/api/v1/dashboard/statistic"), + "dashboard.storage": ApiOperationRoute("GET", "/api/v1/dashboard/storage"), + "dashboard.processes": ApiOperationRoute("GET", "/api/v1/dashboard/processes"), + "dashboard.system": ApiOperationRoute("GET", "/api/v1/dashboard/system"), + "dashboard.downloader": ApiOperationRoute("GET", "/api/v1/dashboard/downloader"), + "scheduler.progress": ApiOperationRoute("GET", "/api/v1/dashboard/schedule/{job_id}/progress"), + "dashboard.transfer.statistics": ApiOperationRoute("GET", "/api/v1/dashboard/transfer"), + "dashboard.cpu": ApiOperationRoute("GET", "/api/v1/dashboard/cpu"), + "dashboard.memory": ApiOperationRoute("GET", "/api/v1/dashboard/memory"), + "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.category.config.get": ApiOperationRoute("GET", "/api/v1/media/category/config"), + "media.category.config.update": ApiOperationRoute("POST", "/api/v1/media/category/config"), + "media.categories": ApiOperationRoute("GET", "/api/v1/media/category"), + "media.episode_groups": ApiOperationRoute("GET", "/api/v1/media/groups/{tmdbid}"), + "media.episode_group.seasons": ApiOperationRoute("GET", "/api/v1/media/group/seasons/{episode_group}"), + "media.seasons": ApiOperationRoute("GET", "/api/v1/media/seasons"), + "search.title": ApiOperationRoute("GET", "/api/v1/search/title"), + "search.recommend": ApiOperationRoute("POST", "/api/v1/search/recommend"), + "subtitle.search.title": ApiOperationRoute("GET", "/api/v1/search/subtitle/title"), + "subtitle.search.media": ApiOperationRoute("GET", "/api/v1/search/subtitle/media/{media_id}"), + "site.add": ApiOperationRoute("POST", "/api/v1/site/"), + "site.delete": ApiOperationRoute("DELETE", "/api/v1/site/{site_id}"), + "site.auth.options": ApiOperationRoute("GET", "/api/v1/site/auth"), + "site.authenticate": ApiOperationRoute("POST", "/api/v1/site/auth"), + "site.cookiecloud.sync": ApiOperationRoute("GET", "/api/v1/site/cookiecloud"), + "site.reset": ApiOperationRoute("GET", "/api/v1/site/reset"), + "site.priorities.update": ApiOperationRoute("POST", "/api/v1/site/priorities"), + "site.userdata.refresh": ApiOperationRoute("POST", "/api/v1/site/userdata/{site_id}"), + "site.userdata.latest": ApiOperationRoute("GET", "/api/v1/site/userdata/latest"), + "site.category": ApiOperationRoute("GET", "/api/v1/site/category/{site_id}"), + "site.resource": ApiOperationRoute("GET", "/api/v1/site/resource/{site_id}"), + "site.searchable": ApiOperationRoute("GET", "/api/v1/site/media/{media_type}"), + "site.rss": ApiOperationRoute("GET", "/api/v1/site/rss"), + "site.statistics": ApiOperationRoute("GET", "/api/v1/site/statistic"), + "site.statistic": ApiOperationRoute("GET", "/api/v1/site/statistic/{site_url}"), + "site.mapping": ApiOperationRoute("GET", "/api/v1/site/mapping"), + "site.supporting": ApiOperationRoute("GET", "/api/v1/site/supporting"), + "subscription.get": ApiOperationRoute("GET", "/api/v1/subscribe/{subscribe_id}"), + "subscription.find": ApiOperationRoute("GET", "/api/v1/subscribe/media/{media_id}"), + "subscription.delete_by_media": ApiOperationRoute("DELETE", "/api/v1/subscribe/media/{media_id}"), + "subscription.status.update": ApiOperationRoute("PUT", "/api/v1/subscribe/status/{subid}"), + "subscription.reset": ApiOperationRoute("GET", "/api/v1/subscribe/reset/{subid}"), + "subscription.search_all": ApiOperationRoute("GET", "/api/v1/subscribe/search"), + "subscription.refresh": ApiOperationRoute("GET", "/api/v1/subscribe/refresh"), + "subscription.metadata.refresh": ApiOperationRoute("GET", "/api/v1/subscribe/check"), + "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.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"), + "subscription.follow.list": ApiOperationRoute("GET", "/api/v1/subscribe/follow"), + "subscription.follow.add": ApiOperationRoute("POST", "/api/v1/subscribe/follow"), + "subscription.follow.delete": ApiOperationRoute("DELETE", "/api/v1/subscribe/follow"), + "subscription.share.statistics": ApiOperationRoute("GET", "/api/v1/subscribe/share/statistics"), + "storage.manage": ApiOperationRoute("POST", "/api/v1/storage/manage"), + "storage.mkdir": ApiOperationRoute("POST", "/api/v1/storage/mkdir"), + "storage.rename": ApiOperationRoute("POST", "/api/v1/storage/rename"), + "storage.delete": ApiOperationRoute("POST", "/api/v1/storage/delete"), + "transfer.queue": ApiOperationRoute("GET", "/api/v1/transfer/queue"), + "transfer.queue.delete": ApiOperationRoute("DELETE", "/api/v1/transfer/queue"), + "transfer.name": ApiOperationRoute("GET", "/api/v1/transfer/name"), + "transfer.target_path": ApiOperationRoute("POST", "/api/v1/transfer/manual/target-path"), + "transfer.manual_history": ApiOperationRoute("POST", "/api/v1/transfer/manual/history"), + "transfer.episode_format.recommend": ApiOperationRoute("POST", "/api/v1/transfer/episode-format/recommend"), + "transfer.manual_reviews": ApiOperationRoute("GET", "/api/v1/transfer/tasks/manual-reviews"), + "transfer.manual_review": ApiOperationRoute("GET", "/api/v1/transfer/tasks/{task_id}/manual-review"), + "transfer.manual_review.resolve": ApiOperationRoute("POST", "/api/v1/transfer/tasks/{task_id}/manual-review"), + "transfer.history.redo": ApiOperationRoute("POST", "/api/v1/history/transfer/{history_id}/ai-redo"), + "transfer.history.redo_batch": ApiOperationRoute("POST", "/api/v1/history/transfer/ai-redo"), + "transfer.history.clear": ApiOperationRoute("GET", "/api/v1/history/empty/transfer"), + "workflow.create": ApiOperationRoute("POST", "/api/v1/workflow/"), + "workflow.get": ApiOperationRoute("GET", "/api/v1/workflow/{workflow_id}"), + "workflow.update": ApiOperationRoute("PUT", "/api/v1/workflow/{workflow_id}"), + "workflow.delete": ApiOperationRoute("DELETE", "/api/v1/workflow/{workflow_id}"), + "workflow.actions": ApiOperationRoute("GET", "/api/v1/workflow/actions"), + "workflow.event_types": ApiOperationRoute("GET", "/api/v1/workflow/event_types"), + "workflow.plugin.actions": ApiOperationRoute("GET", "/api/v1/workflow/plugin/actions"), + "workflow.start": ApiOperationRoute("POST", "/api/v1/workflow/{workflow_id}/start"), + "workflow.pause": ApiOperationRoute("POST", "/api/v1/workflow/{workflow_id}/pause"), + "workflow.reset": ApiOperationRoute("POST", "/api/v1/workflow/{workflow_id}/reset"), + "workflow.shares": ApiOperationRoute("GET", "/api/v1/workflow/shares"), + "workflow.share": ApiOperationRoute("POST", "/api/v1/workflow/share"), + "workflow.share.delete": ApiOperationRoute("DELETE", "/api/v1/workflow/share/{share_id}"), + "workflow.fork": ApiOperationRoute("POST", "/api/v1/workflow/fork"), + "torrent.cache.get": ApiOperationRoute("GET", "/api/v1/torrent/cache"), + "torrent.cache.delete": ApiOperationRoute("DELETE", "/api/v1/torrent/cache/{domain}/{torrent_hash}"), + "torrent.cache.clear": ApiOperationRoute("DELETE", "/api/v1/torrent/cache"), + "torrent.cache.refresh": ApiOperationRoute("POST", "/api/v1/torrent/cache/refresh"), + "torrent.cache.reidentify": ApiOperationRoute("POST", "/api/v1/torrent/cache/reidentify/{domain}/{torrent_hash}"), + "database.backups.list": ApiOperationRoute("GET", "/api/v1/system/database/backups"), + "database.backups.create": ApiOperationRoute("POST", "/api/v1/system/database/backups"), + "database.backups.verify": ApiOperationRoute("POST", "/api/v1/system/database/backups/{name}/verify"), + "database.backups.delete": ApiOperationRoute("DELETE", "/api/v1/system/database/backups/{name}"), + "filter.test": ApiOperationRoute("GET", "/api/v1/system/ruletest"), + "system.network.targets": ApiOperationRoute("GET", "/api/v1/system/nettest/targets"), + "system.network.test": ApiOperationRoute("GET", "/api/v1/system/nettest"), + "system.module.list": ApiOperationRoute("GET", "/api/v1/system/modulelist"), + "system.module.test": ApiOperationRoute("GET", "/api/v1/system/moduletest/{moduleid}"), + "plugin.market.sync_wiki": ApiOperationRoute("POST", "/api/v1/system/setting/PLUGIN_MARKET/sync-wiki"), + "plugin.runtime.status": ApiOperationRoute("GET", "/api/v1/plugin/runtime"), + "plugin.history": ApiOperationRoute("GET", "/api/v1/plugin/history/{plugin_id}"), + "plugin.releases": ApiOperationRoute("GET", "/api/v1/plugin/releases/{plugin_id}"), + "plugin.ratings": ApiOperationRoute("GET", "/api/v1/plugin/rating"), + "plugin.rating": ApiOperationRoute("GET", "/api/v1/plugin/rating/{plugin_id}"), + "plugin.rating.submit": ApiOperationRoute("POST", "/api/v1/plugin/rating/{plugin_id}"), + "plugin.statistics": ApiOperationRoute("GET", "/api/v1/plugin/statistic"), + "plugin.reset": ApiOperationRoute("GET", "/api/v1/plugin/reset/{plugin_id}"), + "plugin.clone": ApiOperationRoute("POST", "/api/v1/plugin/clone/{plugin_id}"), + "config.user.get": ApiOperationRoute("GET", "/api/v1/system/global/user"), + "config.public.get": ApiOperationRoute("GET", "/api/v1/system/setting/public/{key}"), + "system.usage.statistics": ApiOperationRoute("GET", "/api/v1/system/usage/statistic"), + "plugin.folders.get": ApiOperationRoute("GET", "/api/v1/plugin/folders"), + "plugin.folders.update": ApiOperationRoute("POST", "/api/v1/plugin/folders"), + "plugin.folder.create": ApiOperationRoute("POST", "/api/v1/plugin/folders/{folder_name}"), + "plugin.folder.delete": ApiOperationRoute("DELETE", "/api/v1/plugin/folders/{folder_name}"), + "plugin.folder.plugins.update": ApiOperationRoute("PUT", "/api/v1/plugin/folders/{folder_name}/plugins"), } @@ -273,10 +777,13 @@ def list_api_operation_ids() -> tuple[str, ...]: __all__ = [ "API_FIRST_BATCH_OPERATION_SPECS", + "API_EXTENDED_OPERATION_SPECS", + "API_MUSIC_OPERATION_SPECS", "API_OPERATION_BY_ID", "API_OPERATION_ROUTES", "API_OPERATION_SPECS", "API_PARITY_OPERATION_SPECS", + "API_SYSTEM_OPERATION_SPECS", "ApiOperationRoute", "ApiOperationSpec", "list_api_operation_ids", diff --git a/app/agent/policy/api_mcp_contract.py b/app/agent/policy/api_mcp_contract.py deleted file mode 100644 index f12a77aa1..000000000 --- a/app/agent/policy/api_mcp_contract.py +++ /dev/null @@ -1,295 +0,0 @@ -"""从业务 OpenAPI 构建 moviepilot_api 的外部 MCP 输入合同。""" - -from __future__ import annotations - -from copy import deepcopy -from typing import Any, Mapping, Sequence - - -def _rewrite_schema_refs( - schema: Mapping[str, Any], - *, - components: Mapping[str, Any], - definitions: dict[str, Any], -) -> dict[str, Any]: - """把 OpenAPI components 引用改写为独立 MCP schema 的 $defs 引用。""" - reference = schema.get("$ref") - if isinstance(reference, str) and reference.startswith("#/components/schemas/"): - name = reference.rsplit("/", 1)[-1] - if name not in definitions: - definitions[name] = {} - source = components.get(name) - if not isinstance(source, Mapping): - raise ValueError(f"OpenAPI 缺少请求模型: {name}") - definitions[name] = _rewrite_schema_refs( - source, - components=components, - definitions=definitions, - ) - return {"$ref": f"#/$defs/{name}"} - - rewritten: dict[str, Any] = {} - for key, value in schema.items(): - if isinstance(value, Mapping): - rewritten[key] = _rewrite_schema_refs( - value, - components=components, - definitions=definitions, - ) - elif isinstance(value, list): - rewritten[key] = [ - _rewrite_schema_refs( - item, - components=components, - definitions=definitions, - ) - if isinstance(item, Mapping) - else deepcopy(item) - for item in value - ] - else: - rewritten[key] = deepcopy(value) - return rewritten - - -def _parameter_object_schema( - parameters: Sequence[Mapping[str, Any]], - *, - location: str, - components: Mapping[str, Any], - definitions: dict[str, Any], -) -> dict[str, Any] | None: - """把 OpenAPI path/query 参数投影为网关结构化对象。""" - selected = [parameter for parameter in parameters if parameter.get("in") == location] - if not selected: - return None - properties: dict[str, Any] = {} - required: list[str] = [] - for parameter in selected: - name = str(parameter["name"]) - raw_schema = parameter.get("schema") - if not isinstance(raw_schema, Mapping): - raw_schema = {} - field_schema = _rewrite_schema_refs( - raw_schema, - components=components, - definitions=definitions, - ) - if parameter.get("description") and "description" not in field_schema: - field_schema["description"] = parameter["description"] - properties[name] = field_schema - if parameter.get("required"): - required.append(name) - result: dict[str, Any] = { - "type": "object", - "properties": properties, - "additionalProperties": False, - } - if required: - result["required"] = required - return result - - -def _request_body_schema( - operation: Mapping[str, Any], - *, - components: Mapping[str, Any], - definitions: dict[str, Any], -) -> tuple[dict[str, Any] | None, bool]: - """读取一个 OpenAPI operation 的 JSON 请求体及必填性。""" - request_body = operation.get("requestBody") - if not isinstance(request_body, Mapping): - return None, False - content = request_body.get("content") - if not isinstance(content, Mapping): - return None, bool(request_body.get("required")) - media = content.get("application/json") - if not isinstance(media, Mapping): - media = next((item for item in content.values() if isinstance(item, Mapping)), None) - raw_schema = media.get("schema") if isinstance(media, Mapping) else None - if not isinstance(raw_schema, Mapping): - return None, bool(request_body.get("required")) - return ( - _rewrite_schema_refs( - raw_schema, - components=components, - definitions=definitions, - ), - bool(request_body.get("required")), - ) - - -def _person_credits_operation(openapi: Mapping[str, Any]) -> dict[str, Any]: - """合并四个来源端点为稳定 media.person.credits 网关合同。""" - paths = openapi.get("paths", {}) - source_paths = { - "douban": "/api/v1/douban/person/credits/{person_id}", - "tmdb": "/api/v1/tmdb/person/credits/{person_id}", - "bangumi": "/api/v1/bangumi/person/credits/{person_id}", - "anilist": "/api/v1/anilist/person/credits/{person_id}", - } - for source_path in source_paths.values(): - if source_path not in paths: - raise ValueError(f"OpenAPI 缺少人物作品端点: {source_path}") - return { - "summary": "读取人物作品", - "parameters": [ - { - "name": "source", - "in": "path", - "required": True, - "schema": {"type": "string", "enum": list(source_paths)}, - "description": "人物数据来源。", - }, - { - "name": "person_id", - "in": "path", - "required": True, - "schema": {"type": "integer"}, - "description": "来源原生人物 ID。", - }, - { - "name": "page", - "in": "query", - "required": False, - "schema": {"type": "integer", "minimum": 1, "default": 1}, - }, - { - "name": "count", - "in": "query", - "required": False, - "schema": {"type": "integer", "minimum": 1, "maximum": 50, "default": 20}, - "description": "Bangumi 与 AniList 支持的每页条数;其他来源忽略。", - }, - ], - } - - -def _resolve_openapi_operation( - operation_id: str, - route: Any, - openapi: Mapping[str, Any], -) -> Mapping[str, Any]: - """按固定路由读取 OpenAPI operation,并处理多来源合成路由。""" - if operation_id == "media.person.credits": - return _person_credits_operation(openapi) - path_item = openapi.get("paths", {}).get(route.path) - if not isinstance(path_item, Mapping): - raise ValueError(f"OpenAPI 缺少 operation 路径: {operation_id} -> {route.path}") - operation = path_item.get(str(route.method).lower()) - if not isinstance(operation, Mapping): - raise ValueError(f"OpenAPI 缺少 operation 方法: {operation_id} -> {route.method} {route.path}") - return operation - - -def _apply_operation_overrides( - operation_id: str, - query_schema: dict[str, Any] | None, -) -> None: - """补充同一路由多 operation 时无法由 FastAPI 自动表达的语义约束。""" - if operation_id != "media.person.search" or query_schema is None: - return - query_schema["properties"]["type"] = { - "type": "string", - "const": "person", - "description": "人物搜索固定传 person。", - } - required = query_schema.setdefault("required", []) - if "type" not in required: - required.append("type") - - -def build_api_mcp_input_schema( - *, - openapi: Mapping[str, Any], - routes: Mapping[str, Any], - specs: Sequence[Any], -) -> dict[str, Any]: - """构建 59 个白名单 operation 的完整 MCP oneOf 输入合同。""" - components = openapi.get("components", {}).get("schemas", {}) - if not isinstance(components, Mapping): - components = {} - definitions: dict[str, Any] = {} - spec_by_id = {spec.operation_id: spec for spec in specs} - if set(spec_by_id) != set(routes): - raise ValueError("API operation 策略与路由注册表不一致") - - branches: list[dict[str, Any]] = [] - for operation_id in sorted(routes): - route = routes[operation_id] - operation = _resolve_openapi_operation(operation_id, route, openapi) - parameters = operation.get("parameters") - if not isinstance(parameters, list): - parameters = [] - path_schema = _parameter_object_schema( - parameters, - location="path", - components=components, - definitions=definitions, - ) - query_schema = _parameter_object_schema( - parameters, - location="query", - components=components, - definitions=definitions, - ) - _apply_operation_overrides(operation_id, query_schema) - body_schema, body_required = _request_body_schema( - operation, - components=components, - definitions=definitions, - ) - - properties: dict[str, Any] = { - "operation_id": {"type": "string", "const": operation_id}, - } - required = ["operation_id"] - if path_schema is not None: - properties["path_params"] = path_schema - if path_schema.get("required"): - required.append("path_params") - if query_schema is not None: - properties["query"] = query_schema - if query_schema.get("required"): - required.append("query") - if body_schema is not None: - properties["body"] = body_schema - if body_required: - required.append("body") - - summary = str(operation.get("summary") or operation.get("description") or operation_id) - spec = spec_by_id[operation_id] - branches.append( - { - "type": "object", - "title": operation_id, - "description": ( - f"{summary} Method: {route.method}. Path: {route.path}. " - f"Effect: {spec.effect.value}." - ), - "properties": properties, - "required": required, - "additionalProperties": False, - } - ) - - schema: dict[str, Any] = { - "$schema": "https://json-schema.org/draft/2020-12/schema", - "title": "moviepilot_api", - "type": "object", - "description": "Select the oneOf branch matching operation_id and send exactly its documented fields.", - "properties": { - "operation_id": {"type": "string", "enum": sorted(routes)}, - "path_params": {"type": "object"}, - "query": {"type": "object"}, - "body": {"type": "object"}, - }, - "required": ["operation_id"], - "oneOf": branches, - } - if definitions: - schema["$defs"] = definitions - return schema - - -__all__ = ["build_api_mcp_input_schema"] diff --git a/app/agent/policy/api_mcp_schema.json b/app/agent/policy/api_mcp_schema.json index 183d1a59b..f6e4bf575 100644 --- a/app/agent/policy/api_mcp_schema.json +++ b/app/agent/policy/api_mcp_schema.json @@ -1,10 +1,346 @@ { "$defs": { + "Action-Input": { + "description": "One executable action node in a workflow definition.", + "properties": { + "branch_policy": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Workflow branch policy controlling selected downstream paths.", + "title": "Branch Policy" + }, + "concurrency_key": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Workflow expression used to serialize actions sharing the same runtime key.", + "title": "Concurrency Key" + }, + "data": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/$defs/JsonData-Input" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "description": "Serialized workflow action configuration or runtime payload.", + "title": "Data" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Human-readable media, torrent, or subscription description.", + "title": "Description" + }, + "fail_policy": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Workflow failure policy controlling stop, continue, or branch behavior.", + "title": "Fail Policy" + }, + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Persistent database identifier of the supplied record.", + "title": "Id" + }, + "inputs": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Named input bindings consumed by this workflow action.", + "title": "Inputs" + }, + "join_policy": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Workflow fan-in policy controlling when downstream execution may continue.", + "title": "Join Policy" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Human-readable name of the site, storage item, subscription, or rule group.", + "title": "Name" + }, + "outputs": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/$defs/JsonData-Input" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "description": "Named output mappings produced by this workflow action.", + "title": "Outputs" + }, + "position": { + "anyOf": [ + { + "$ref": "#/$defs/ActionPosition" + }, + { + "type": "null" + } + ], + "description": "Workflow editor coordinates for one action node." + }, + "retry": { + "anyOf": [ + { + "$ref": "#/$defs/ActionRetry" + }, + { + "type": "null" + } + ], + "description": "Workflow retry-policy definition for this action." + }, + "timeout": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Per-request site timeout in seconds.", + "title": "Timeout" + }, + "type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "MoviePilot media or storage item type required by the selected operation.", + "title": "Type" + } + }, + "title": "Action", + "type": "object" + }, + "ActionFlow-Input": { + "description": "One directed connection between workflow action nodes.", + "properties": { + "animated": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": true, + "description": "Whether the workflow connection is rendered as animated in the editor.", + "title": "Animated" + }, + "branch_policy": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Workflow branch policy controlling selected downstream paths.", + "title": "Branch Policy" + }, + "condition": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Workflow branch or flow condition expression evaluated at runtime.", + "title": "Condition" + }, + "data": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/$defs/JsonData-Input" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "description": "Serialized workflow action configuration or runtime payload.", + "title": "Data" + }, + "id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Persistent database identifier of the supplied record.", + "title": "Id" + }, + "join_policy": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Workflow fan-in policy controlling when downstream execution may continue.", + "title": "Join Policy" + }, + "source": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Exact metadata or recommendation source selected by the operation.", + "title": "Source" + }, + "target": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Exact configured storage target name accepted by storage.manage.", + "title": "Target" + } + }, + "title": "ActionFlow", + "type": "object" + }, + "ActionPosition": { + "description": "Editor coordinates for one workflow action node.", + "properties": { + "x": { + "default": 0, + "description": "Horizontal workflow editor coordinate.", + "title": "X", + "type": "number" + }, + "y": { + "default": 0, + "description": "Vertical workflow editor coordinate.", + "title": "Y", + "type": "number" + } + }, + "title": "ActionPosition", + "type": "object" + }, + "ActionRetry": { + "description": "Retry limits and timing for one workflow action.", + "properties": { + "backoff": { + "default": 1, + "description": "Retry backoff multiplier applied after each failed workflow action attempt.", + "minimum": 1.0, + "title": "Backoff", + "type": "number" + }, + "interval": { + "default": 0, + "description": "Retry delay in seconds before the next workflow action attempt.", + "minimum": 0.0, + "title": "Interval", + "type": "number" + }, + "max_attempts": { + "default": 1, + "description": "Maximum number of attempts allowed by the workflow retry policy.", + "minimum": 1.0, + "title": "Max Attempts", + "type": "integer" + } + }, + "title": "ActionRetry", + "type": "object" + }, "AgentCommandRunRequest": { - "description": "通过 Agent API 触发斜杠命令的请求。", + "description": "Slash-command execution request.", "properties": { "command": { - "description": "要执行的完整斜杠命令", + "description": "Complete slash command, including the leading slash and all arguments.", "title": "Command", "type": "string" } @@ -15,10 +351,27 @@ "title": "AgentCommandRunRequest", "type": "object" }, + "BatchTransferHistoryRedoRequest": { + "description": "Explicit transfer-history IDs for AI-assisted batch reorganization.", + "properties": { + "history_ids": { + "description": "Explicit persistent transfer-history IDs included in one batch redo request.", + "items": { + "type": "integer" + }, + "title": "History Ids", + "type": "array" + } + }, + "title": "BatchTransferHistoryRedoRequest", + "type": "object" + }, "Body_add_api_v1_download_add_post": { + "description": "MoviePilot download submission request.", "properties": { "allow_unrecognized": { "default": false, + "description": "Allow a download when MoviePilot cannot resolve a canonical media identity.", "title": "Allow Unrecognized", "type": "boolean" }, @@ -31,6 +384,7 @@ "type": "null" } ], + "description": "Configured downloader instance name.", "title": "Downloader" }, "media_id": { @@ -42,6 +396,7 @@ "type": "null" } ], + "description": "Source-native media ID. Always pair it with the exact media_source returned by search.", "title": "Media Id" }, "media_source": { @@ -52,7 +407,8 @@ { "type": "null" } - ] + ], + "description": "Metadata source identifier. Preserve the exact value returned with media_id." }, "music_type": { "anyOf": [ @@ -67,6 +423,7 @@ "type": "null" } ], + "description": "Music identity level: recording, album, or artist where supported.", "title": "Music Type" }, "save_path": { @@ -78,10 +435,12 @@ "type": "null" } ], + "description": "Configured downloader-side save path for the download or subscription.", "title": "Save Path" }, "torrent_in": { - "$ref": "#/$defs/TorrentInfo" + "$ref": "#/$defs/TorrentInfo", + "description": "Complete torrent candidate returned by search.results or search.torrents." } }, "required": [ @@ -90,8 +449,163 @@ "title": "Body_add_api_v1_download_add_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": { + "check_only": { + "default": false, + "description": "Validate or preview the recommendation without applying search-result filtering.", + "title": "Check Only", + "type": "boolean" + }, + "filtered_indices": { + "anyOf": [ + { + "items": { + "type": "integer" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Zero-based search-result indices selected by the recommendation model.", + "title": "Filtered Indices" + }, + "force": { + "default": false, + "description": "Force a marketplace refresh or plugin installation when true.", + "title": "Force", + "type": "boolean" + } + }, + "title": "Body_recommend_search_results_api_v1_search_recommend_post", + "type": "object" + }, + "CategoryConfig": { + "description": "Complete automatic media-category strategy configuration.", + "properties": { + "movie": { + "anyOf": [ + { + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/CategoryRule" + }, + { + "type": "null" + } + ] + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": {}, + "description": "Automatic movie-category rules evaluated in order.", + "title": "Movie" + }, + "tv": { + "anyOf": [ + { + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/$defs/CategoryRule" + }, + { + "type": "null" + } + ] + }, + "type": "object" + }, + { + "type": "null" + } + ], + "default": {}, + "description": "Automatic TV-category rules evaluated in order.", + "title": "Tv" + } + }, + "title": "CategoryConfig", + "type": "object" + }, + "CategoryRule": { + "additionalProperties": true, + "description": "One ordered automatic media-category matching rule.", + "properties": { + "genre_ids": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Genre identifiers accepted by the automatic category rule.", + "title": "Genre Ids" + }, + "origin_country": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Production-country code matched by an automatic category rule.", + "title": "Origin Country" + }, + "original_language": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Original-language code matched by an automatic category rule.", + "title": "Original Language" + }, + "production_countries": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Production-country codes matched by an automatic category rule.", + "title": "Production Countries" + }, + "release_year": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Release year matched by an automatic category rule.", + "title": "Release Year" + } + }, + "title": "CategoryRule", + "type": "object" + }, "CustomFilterRuleCreateRequest": { - "description": "新增自定义过滤规则请求。", + "description": "Custom filter-rule creation request.", "properties": { "exclude": { "anyOf": [ @@ -102,6 +616,7 @@ "type": "null" } ], + "description": "Regular expression or filter expression that rejects matching releases.", "title": "Exclude" }, "include": { @@ -113,9 +628,11 @@ "type": "null" } ], + "description": "Regular expression or filter expression that a release must match.", "title": "Include" }, "name": { + "description": "Human-readable name of the site, storage item, subscription, or rule group.", "title": "Name", "type": "string" }, @@ -128,9 +645,11 @@ "type": "null" } ], + "description": "Release-age filter expression for a custom filter rule.", "title": "Publish Time" }, "rule_id": { + "description": "Stable custom filter-rule ID.", "title": "Rule Id", "type": "string" }, @@ -143,6 +662,7 @@ "type": "null" } ], + "description": "Minimum seeder expression for a filter rule, or the torrent's seeder count.", "title": "Seeders" }, "size_range": { @@ -154,6 +674,7 @@ "type": "null" } ], + "description": "Accepted torrent size range expression for a custom filter rule.", "title": "Size Range" } }, @@ -165,7 +686,7 @@ "type": "object" }, "CustomFilterRuleUpdateRequest": { - "description": "更新自定义过滤规则请求。", + "description": "Custom filter-rule update request.", "properties": { "exclude": { "anyOf": [ @@ -176,6 +697,7 @@ "type": "null" } ], + "description": "Regular expression or filter expression that rejects matching releases.", "title": "Exclude" }, "include": { @@ -187,6 +709,7 @@ "type": "null" } ], + "description": "Regular expression or filter expression that a release must match.", "title": "Include" }, "name": { @@ -198,6 +721,7 @@ "type": "null" } ], + "description": "Human-readable name of the site, storage item, subscription, or rule group.", "title": "Name" }, "new_rule_id": { @@ -209,6 +733,7 @@ "type": "null" } ], + "description": "Replacement stable ID for the existing custom filter rule.", "title": "New Rule Id" }, "publish_time": { @@ -220,6 +745,7 @@ "type": "null" } ], + "description": "Release-age filter expression for a custom filter rule.", "title": "Publish Time" }, "seeders": { @@ -231,6 +757,7 @@ "type": "null" } ], + "description": "Minimum seeder expression for a filter rule, or the torrent's seeder count.", "title": "Seeders" }, "size_range": { @@ -242,6 +769,7 @@ "type": "null" } ], + "description": "Accepted torrent size range expression for a custom filter rule.", "title": "Size Range" } }, @@ -249,9 +777,10 @@ "type": "object" }, "CustomIdentifiersUpdateRequest": { - "description": "完整替换自定义识别词的请求。", + "description": "Complete custom recognition-identifier replacement request.", "properties": { "identifiers": { + "description": "Complete ordered list of custom recognition identifier rules.", "items": { "type": "string" }, @@ -263,7 +792,7 @@ "type": "object" }, "DownloadHistory-Input": { - "description": "下载历史记录", + "description": "One MoviePilot download-history record.", "properties": { "channel": { "anyOf": [ @@ -274,6 +803,7 @@ "type": "null" } ], + "description": "Message channel that originally submitted the download.", "title": "Channel" }, "date": { @@ -285,6 +815,7 @@ "type": "null" } ], + "description": "Record creation or completion timestamp used by the history item.", "title": "Date" }, "download_hash": { @@ -296,6 +827,7 @@ "type": "null" } ], + "description": "Provider-native torrent hash associated with the record.", "title": "Download Hash" }, "episode_group": { @@ -307,6 +839,7 @@ "type": "null" } ], + "description": "TMDB episode-group identifier used for alternate episode ordering.", "title": "Episode Group" }, "episodes": { @@ -318,9 +851,11 @@ "type": "null" } ], + "description": "Episode-number expression recorded in history, such as E01-E03.", "title": "Episodes" }, "id": { + "description": "Persistent database identifier of the supplied record.", "title": "Id", "type": "integer" }, @@ -333,6 +868,7 @@ "type": "null" } ], + "description": "Image URL stored with the history record.", "title": "Image" }, "media_category": { @@ -344,6 +880,7 @@ "type": "null" } ], + "description": "MoviePilot library category assigned to the media.", "title": "Media Category" }, "media_id": { @@ -355,6 +892,7 @@ "type": "null" } ], + "description": "Source-native media ID. Always pair it with the exact media_source returned by search.", "title": "Media Id" }, "media_source": { @@ -365,7 +903,8 @@ { "type": "null" } - ] + ], + "description": "Metadata source identifier. Preserve the exact value returned with media_id." }, "music_type": { "anyOf": [ @@ -376,6 +915,7 @@ "type": "null" } ], + "description": "Music identity level: recording, album, or artist where supported.", "title": "Music Type" }, "note": { @@ -386,7 +926,8 @@ { "type": "null" } - ] + ], + "description": "Structured auxiliary metadata stored with the record." }, "path": { "anyOf": [ @@ -397,6 +938,7 @@ "type": "null" } ], + "description": "Storage or history path represented by this record.", "title": "Path" }, "poster": { @@ -408,6 +950,7 @@ "type": "null" } ], + "description": "Poster image URL stored with the media or subscription.", "title": "Poster" }, "seasons": { @@ -419,6 +962,7 @@ "type": "null" } ], + "description": "Season-number expression recorded in history.", "title": "Seasons" }, "title": { @@ -430,6 +974,7 @@ "type": "null" } ], + "description": "Media, torrent, subscription, or history title used by the operation.", "title": "Title" }, "torrent_description": { @@ -441,6 +986,7 @@ "type": "null" } ], + "description": "Torrent release description recorded in download history.", "title": "Torrent Description" }, "torrent_name": { @@ -452,6 +998,7 @@ "type": "null" } ], + "description": "Torrent release name recorded in download history.", "title": "Torrent Name" }, "torrent_site": { @@ -463,6 +1010,7 @@ "type": "null" } ], + "description": "Source site name recorded in download history.", "title": "Torrent Site" }, "type": { @@ -474,6 +1022,7 @@ "type": "null" } ], + "description": "MoviePilot media or storage item type required by the selected operation.", "title": "Type" }, "userid": { @@ -485,6 +1034,7 @@ "type": "null" } ], + "description": "Message-channel user ID recorded with download history.", "title": "Userid" }, "username": { @@ -496,6 +1046,7 @@ "type": "null" } ], + "description": "MoviePilot or site username required by the selected operation.", "title": "Username" }, "year": { @@ -507,6 +1058,7 @@ "type": "null" } ], + "description": "Release or premiere year used to disambiguate the media title.", "title": "Year" } }, @@ -516,8 +1068,41 @@ "title": "DownloadHistory", "type": "object" }, + "EpisodeFormatRecommendItem": { + "description": "File samples used to infer an episode-number extraction template.", + "properties": { + "fileitem": { + "anyOf": [ + { + "$ref": "#/$defs/FileItem-Input" + }, + { + "type": "null" + } + ], + "description": "One complete source storage item returned by storage.list." + }, + "fileitems": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/FileItem-Input" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Additional source storage items included in the same manual transfer.", + "title": "Fileitems" + } + }, + "title": "EpisodeFormatRecommendItem", + "type": "object" + }, "FileItem-Input": { - "description": "文件或目录条目,目录可递归包含子条目。", + "description": "One file or directory returned by a configured storage provider.", "properties": { "basename": { "anyOf": [ @@ -528,6 +1113,7 @@ "type": "null" } ], + "description": "Base filename without its parent path.", "title": "Basename" }, "children": { @@ -542,6 +1128,7 @@ "type": "null" } ], + "description": "Child storage items nested below this item.", "title": "Children" }, "drive_id": { @@ -553,6 +1140,7 @@ "type": "null" } ], + "description": "Provider-native storage drive identifier.", "title": "Drive Id" }, "extension": { @@ -564,6 +1152,7 @@ "type": "null" } ], + "description": "Filename extension, including or excluding the leading dot as returned by storage.", "title": "Extension" }, "fileid": { @@ -575,6 +1164,7 @@ "type": "null" } ], + "description": "Provider-native storage item identifier.", "title": "Fileid" }, "modify_time": { @@ -586,6 +1176,7 @@ "type": "null" } ], + "description": "Storage item modification timestamp.", "title": "Modify Time" }, "name": { @@ -597,6 +1188,7 @@ "type": "null" } ], + "description": "Human-readable name of the site, storage item, subscription, or rule group.", "title": "Name" }, "parent_fileid": { @@ -608,6 +1200,7 @@ "type": "null" } ], + "description": "Provider-native identifier of the parent storage directory.", "title": "Parent Fileid" }, "path": { @@ -620,6 +1213,7 @@ } ], "default": "/", + "description": "Storage or history path represented by this record.", "title": "Path" }, "pickcode": { @@ -631,6 +1225,7 @@ "type": "null" } ], + "description": "115 storage pickcode associated with the item.", "title": "Pickcode" }, "size": { @@ -642,6 +1237,7 @@ "type": "null" } ], + "description": "File or torrent size in bytes.", "title": "Size" }, "storage": { @@ -654,6 +1250,7 @@ } ], "default": "local", + "description": "Configured storage name or storage type used by the operation.", "title": "Storage" }, "thumbnail": { @@ -665,6 +1262,7 @@ "type": "null" } ], + "description": "Thumbnail URL returned by the storage provider.", "title": "Thumbnail" }, "type": { @@ -676,6 +1274,7 @@ "type": "null" } ], + "description": "MoviePilot media or storage item type required by the selected operation.", "title": "Type" }, "url": { @@ -687,6 +1286,7 @@ "type": "null" } ], + "description": "Site, storage, or torrent URL represented by this field.", "title": "Url" } }, @@ -694,7 +1294,7 @@ "type": "object" }, "FilterRuleGroupCreateRequest": { - "description": "新增过滤规则组请求。", + "description": "Filter-rule group creation request.", "properties": { "category": { "anyOf": [ @@ -705,6 +1305,7 @@ "type": "null" } ], + "description": "MoviePilot media category or filter-group category, depending on the operation.", "title": "Category" }, "media_type": { @@ -716,13 +1317,16 @@ "type": "null" } ], + "description": "MoviePilot media type used to filter recommendations or rule groups.", "title": "Media Type" }, "name": { + "description": "Human-readable name of the site, storage item, subscription, or rule group.", "title": "Name", "type": "string" }, "rule_string": { + "description": "Ordered filter-rule expression stored in the group.", "title": "Rule String", "type": "string" } @@ -735,7 +1339,7 @@ "type": "object" }, "FilterRuleGroupUpdateRequest": { - "description": "更新过滤规则组请求。", + "description": "Filter-rule group update request.", "properties": { "category": { "anyOf": [ @@ -746,6 +1350,7 @@ "type": "null" } ], + "description": "MoviePilot media category or filter-group category, depending on the operation.", "title": "Category" }, "media_type": { @@ -757,6 +1362,7 @@ "type": "null" } ], + "description": "MoviePilot media type used to filter recommendations or rule groups.", "title": "Media Type" }, "new_name": { @@ -768,6 +1374,7 @@ "type": "null" } ], + "description": "Replacement name for the existing filter-rule group.", "title": "New Name" }, "rule_string": { @@ -779,6 +1386,7 @@ "type": "null" } ], + "description": "Ordered filter-rule expression stored in the group.", "title": "Rule String" } }, @@ -814,10 +1422,38 @@ { "type": "null" } - ] + ], + "description": "Arbitrary JSON-compatible auxiliary data." + }, + "ManageRequest": { + "description": "Configured storage target, provider-defined action, and action parameters.", + "properties": { + "action": { + "description": "Exact provider or workflow action identifier required by the selected operation.", + "title": "Action", + "type": "string" + }, + "params": { + "additionalProperties": true, + "description": "Provider-defined JSON parameters for the selected authentication or storage action.", + "title": "Params", + "type": "object" + }, + "target": { + "description": "Exact configured storage target name accepted by storage.manage.", + "title": "Target", + "type": "string" + } + }, + "required": [ + "target", + "action" + ], + "title": "ManageRequest", + "type": "object" }, "ManualTransferItem": { - "description": "手动整理请求,媒体身份接受内置或插件来源与原生 ID。", + "description": "Manual file-transfer and organization request.", "properties": { "episode_detail": { "anyOf": [ @@ -828,6 +1464,7 @@ "type": "null" } ], + "description": "Episode mapping details used by manual transfer.", "title": "Episode Detail" }, "episode_format": { @@ -839,6 +1476,7 @@ "type": "null" } ], + "description": "Episode-number formatting rule used by manual transfer.", "title": "Episode Format" }, "episode_group": { @@ -850,6 +1488,7 @@ "type": "null" } ], + "description": "TMDB episode-group identifier used for alternate episode ordering.", "title": "Episode Group" }, "episode_offset": { @@ -861,6 +1500,7 @@ "type": "null" } ], + "description": "Integer offset added to detected episode numbers.", "title": "Episode Offset" }, "episode_part": { @@ -872,10 +1512,12 @@ "type": "null" } ], + "description": "Episode part number used when one episode is split across files.", "title": "Episode Part" }, "fileitem": { - "$ref": "#/$defs/FileItem-Input" + "$ref": "#/$defs/FileItem-Input", + "description": "One complete source storage item returned by storage.list." }, "fileitems": { "anyOf": [ @@ -889,6 +1531,7 @@ "type": "null" } ], + "description": "Additional source storage items included in the same manual transfer.", "title": "Fileitems" }, "from_history": { @@ -901,6 +1544,7 @@ } ], "default": false, + "description": "Treat the transfer input as originating from an existing history record.", "title": "From History" }, "library_category_folder": { @@ -912,6 +1556,7 @@ "type": "null" } ], + "description": "Create or use a category-level folder in the target library.", "title": "Library Category Folder" }, "library_type_folder": { @@ -923,6 +1568,7 @@ "type": "null" } ], + "description": "Create or use a media-type folder in the target library.", "title": "Library Type Folder" }, "logid": { @@ -934,6 +1580,7 @@ "type": "null" } ], + "description": "One download-history or transfer-log identifier used by manual transfer.", "title": "Logid" }, "logids": { @@ -948,6 +1595,7 @@ "type": "null" } ], + "description": "Multiple download-history or transfer-log identifiers included in manual transfer.", "title": "Logids" }, "media_id": { @@ -959,6 +1607,7 @@ "type": "null" } ], + "description": "Source-native media ID. Always pair it with the exact media_source returned by search.", "title": "Media Id" }, "media_source": { @@ -969,7 +1618,8 @@ { "type": "null" } - ] + ], + "description": "Metadata source identifier. Preserve the exact value returned with media_id." }, "min_filesize": { "anyOf": [ @@ -981,6 +1631,7 @@ } ], "default": 0, + "description": "Minimum source file size accepted by manual transfer, in bytes.", "title": "Min Filesize" }, "music_type": { @@ -996,6 +1647,7 @@ "type": "null" } ], + "description": "Music identity level: recording, album, or artist where supported.", "title": "Music Type" }, "preview": { @@ -1008,6 +1660,7 @@ } ], "default": false, + "description": "Validate and preview manual-transfer output without committing file changes.", "title": "Preview" }, "reorganize": { @@ -1020,6 +1673,7 @@ } ], "default": false, + "description": "Allow manual transfer to organize an item that was already processed.", "title": "Reorganize" }, "scrape": { @@ -1032,6 +1686,7 @@ } ], "default": false, + "description": "Generate metadata and images after manual transfer.", "title": "Scrape" }, "season": { @@ -1043,6 +1698,7 @@ "type": "null" } ], + "description": "Season number used by the media, search, subscription, or transfer operation.", "title": "Season" }, "target_path": { @@ -1054,6 +1710,7 @@ "type": "null" } ], + "description": "Destination path used by manual transfer.", "title": "Target Path" }, "target_storage": { @@ -1065,6 +1722,7 @@ "type": "null" } ], + "description": "Configured storage name receiving the manual transfer.", "title": "Target Storage" }, "transfer_type": { @@ -1076,6 +1734,7 @@ "type": "null" } ], + "description": "Manual-transfer mode, such as move, copy, link, or softlink.", "title": "Transfer Type" }, "type_name": { @@ -1087,6 +1746,7 @@ "type": "null" } ], + "description": "Explicit media type name used when source IDs alone are ambiguous.", "title": "Type Name" } }, @@ -1094,7 +1754,7 @@ "type": "object" }, "MediaSource": { - "description": "媒体主身份的数据来源,内置来源为常量,插件来源为动态扩展成员。", + "description": "Canonical metadata source identifier paired with a source-native media ID.", "examples": [ "themoviedb", "douban", @@ -1116,6 +1776,7 @@ "type": "string" }, "MediaType": { + "description": "MoviePilot media type.", "enum": [ "电影", "电视剧", @@ -1126,8 +1787,310 @@ "title": "MediaType", "type": "string" }, + "MusicRecognizeRequest": { + "description": "Exact source-native recording or album identity to resolve into canonical music metadata.", + "properties": { + "media_id": { + "description": "Source-native media ID. Always pair it with the exact media_source returned by search.", + "title": "Media Id", + "type": "string" + }, + "media_source": { + "$ref": "#/$defs/MediaSource", + "description": "Metadata source identifier. Preserve the exact value returned with media_id." + }, + "music_type": { + "anyOf": [ + { + "enum": [ + "recording", + "album" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Music identity level: recording, album, or artist where supported.", + "title": "Music Type" + } + }, + "required": [ + "media_source", + "media_id" + ], + "title": "MusicRecognizeRequest", + "type": "object" + }, + "PluginCloneRequest": { + "description": "Plugin clone identifier and optional display-name request.", + "properties": { + "description": { + "default": "", + "description": "Human-readable media, torrent, or subscription description.", + "title": "Description", + "type": "string" + }, + "icon": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Icon name or URL used by a workflow, network target, plugin, or category.", + "title": "Icon" + }, + "name": { + "default": "", + "description": "Human-readable name of the site, storage item, subscription, or rule group.", + "title": "Name", + "type": "string" + }, + "suffix": { + "description": "File suffix or extension matched by an automatic category rule.", + "maxLength": 20, + "minLength": 1, + "pattern": "^[A-Za-z0-9]+$", + "title": "Suffix", + "type": "string" + }, + "version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Plugin release or schema version selected by the operation.", + "title": "Version" + } + }, + "required": [ + "suffix" + ], + "title": "PluginCloneRequest", + "type": "object" + }, + "PluginFolderConfigData": { + "description": "One plugin folder's ordered members and optional presentation settings.", + "properties": { + "background": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional folder background color or style.", + "title": "Background" + }, + "color": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional folder foreground color.", + "title": "Color" + }, + "gradient": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional folder gradient definition.", + "title": "Gradient" + }, + "icon": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional folder icon name.", + "title": "Icon" + }, + "order": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Folder display-order value.", + "title": "Order" + }, + "plugins": { + "description": "Ordered installed plugin IDs assigned to this folder.", + "items": { + "type": "string" + }, + "title": "Plugins", + "type": "array" + }, + "showIcon": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "description": "Whether the frontend should display the folder icon.", + "title": "Showicon" + } + }, + "title": "PluginFolderConfigData", + "type": "object" + }, + "PluginFoldersData": { + "additionalProperties": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "$ref": "#/$defs/PluginFolderConfigData" + } + ] + }, + "description": "Complete mapping from plugin folder names to ordered plugin IDs or display configuration.", + "title": "PluginFoldersData", + "type": "object" + }, + "PluginMarketSyncRequest": { + "description": "Approved Wiki source request for plugin-market synchronization.", + "properties": { + "wiki_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "https://raw.githubusercontent.com/jxxghp/MoviePilot-Wiki/main/plugin.md", + "description": "Approved MoviePilot Wiki URL used as the plugin-market synchronization source.", + "title": "Wiki Url" + } + }, + "title": "PluginMarketSyncRequest", + "type": "object" + }, + "PluginRatingRequest": { + "description": "Current user's numeric plugin-rating submission.", + "properties": { + "rating": { + "description": "Numeric plugin rating accepted by the endpoint's declared bounds.", + "maximum": 5.0, + "minimum": 0.1, + "multipleOf": 0.1, + "title": "Rating", + "type": "number" + } + }, + "required": [ + "rating" + ], + "title": "PluginRatingRequest", + "type": "object" + }, + "PluginSourceChangeRequest": { + "description": "Explicit online-source change request guarded by the current identity revision.", + "properties": { + "expected_revision": { + "description": "Exact current plugin source-identity revision returned by plugin.source.options.", + "minimum": 1.0, + "title": "Expected Revision", + "type": "integer" + }, + "release_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Exact plugin release version to install when one is required.", + "title": "Release Version" + }, + "repo_url": { + "description": "Approved plugin repository URL used to resolve the installation source.", + "minLength": 1, + "title": "Repo Url", + "type": "string" + } + }, + "required": [ + "repo_url", + "expected_revision" + ], + "title": "PluginSourceChangeRequest", + "type": "object" + }, + "PluginSourceInstallRequest": { + "description": "Explicit online-source installation request for an unbound plugin.", + "properties": { + "force": { + "default": false, + "description": "Force a marketplace refresh or plugin installation when true.", + "title": "Force", + "type": "boolean" + }, + "release_version": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Exact plugin release version to install when one is required.", + "title": "Release Version" + }, + "repo_url": { + "description": "Approved plugin repository URL used to resolve the installation source.", + "minLength": 1, + "title": "Repo Url", + "type": "string" + } + }, + "required": [ + "repo_url" + ], + "title": "PluginSourceInstallRequest", + "type": "object" + }, "Site-Input": { - "description": "站点配置及运行状态。", + "description": "Complete site configuration and runtime state.", "properties": { "apikey": { "anyOf": [ @@ -1138,6 +2101,7 @@ "type": "null" } ], + "description": "Site API key used by sites that support API-key authentication.", "title": "Apikey" }, "cookie": { @@ -1149,6 +2113,7 @@ "type": "null" } ], + "description": "Site authentication cookie. Treat this value as a secret.", "title": "Cookie" }, "domain": { @@ -1160,6 +2125,7 @@ "type": "null" } ], + "description": "Site hostname or domain used for matching and requests.", "title": "Domain" }, "downloader": { @@ -1171,6 +2137,7 @@ "type": "null" } ], + "description": "Configured downloader instance name.", "title": "Downloader" }, "filter": { @@ -1182,6 +2149,7 @@ "type": "null" } ], + "description": "Named filter rule or rule expression applied to this site or subscription.", "title": "Filter" }, "id": { @@ -1193,6 +2161,7 @@ "type": "null" } ], + "description": "Persistent database identifier of the supplied record.", "title": "Id" }, "is_active": { @@ -1205,6 +2174,7 @@ } ], "default": true, + "description": "Whether the configured site is enabled.", "title": "Is Active" }, "limit_count": { @@ -1216,6 +2186,7 @@ "type": "null" } ], + "description": "Maximum number of site requests allowed in one rate-limit interval.", "title": "Limit Count" }, "limit_interval": { @@ -1227,6 +2198,7 @@ "type": "null" } ], + "description": "Number of requests in the site's rate-limit window.", "title": "Limit Interval" }, "limit_seconds": { @@ -1238,6 +2210,7 @@ "type": "null" } ], + "description": "Site rate-limit window length in seconds.", "title": "Limit Seconds" }, "name": { @@ -1249,6 +2222,7 @@ "type": "null" } ], + "description": "Human-readable name of the site, storage item, subscription, or rule group.", "title": "Name" }, "note": { @@ -1259,7 +2233,8 @@ { "type": "null" } - ] + ], + "description": "Structured auxiliary metadata stored with the record." }, "pri": { "anyOf": [ @@ -1271,6 +2246,7 @@ } ], "default": 0, + "description": "Site search priority; lower or higher ordering follows the existing site API convention.", "title": "Pri" }, "proxy": { @@ -1283,6 +2259,7 @@ } ], "default": 0, + "description": "Whether the site uses MoviePilot's configured proxy.", "title": "Proxy" }, "public": { @@ -1295,6 +2272,7 @@ } ], "default": 0, + "description": "Whether the site is treated as a public indexer.", "title": "Public" }, "render": { @@ -1307,6 +2285,7 @@ } ], "default": 0, + "description": "Whether site requests require browser rendering.", "title": "Render" }, "rss": { @@ -1318,6 +2297,7 @@ "type": "null" } ], + "description": "Site RSS feed URL.", "title": "Rss" }, "timeout": { @@ -1330,6 +2310,7 @@ } ], "default": 15, + "description": "Per-request site timeout in seconds.", "title": "Timeout" }, "token": { @@ -1341,6 +2322,7 @@ "type": "null" } ], + "description": "Site authentication token. Treat this value as a secret.", "title": "Token" }, "ua": { @@ -1352,6 +2334,7 @@ "type": "null" } ], + "description": "Site User-Agent string used for authenticated requests.", "title": "Ua" }, "url": { @@ -1363,14 +2346,56 @@ "type": "null" } ], + "description": "Site, storage, or torrent URL represented by this field.", "title": "Url" } }, "title": "Site", "type": "object" }, + "SiteAuth": { + "description": "Supported site-account authentication provider and its exact parameter values.", + "properties": { + "params": { + "anyOf": [ + { + "additionalProperties": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "string" + } + ] + }, + "type": "object" + }, + { + "type": "null" + } + ], + "description": "Provider-defined JSON parameters for the selected authentication or storage action.", + "title": "Params" + }, + "site": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Source site identifier associated with the torrent result.", + "title": "Site" + } + }, + "title": "SiteAuth", + "type": "object" + }, "SiteCookieUpdate": { - "description": "站点 Cookie 与 UA 更新请求。", + "description": "Site login request used to refresh the stored cookie and User-Agent.", "properties": { "code": { "anyOf": [ @@ -1381,16 +2406,16 @@ "type": "null" } ], - "description": "二步验证码或密钥", + "description": "Two-factor verification code or site-specific authentication secret.", "title": "Code" }, "password": { - "description": "站点登录密码", + "description": "Site login password. Treat this value as a secret.", "title": "Password", "type": "string" }, "username": { - "description": "站点登录用户名", + "description": "MoviePilot or site username required by the selected operation.", "title": "Username", "type": "string" } @@ -1402,8 +2427,29 @@ "title": "SiteCookieUpdate", "type": "object" }, + "SitePriorityUpdate": { + "description": "One configured site ID and its replacement search priority.", + "properties": { + "id": { + "description": "Persistent site ID returned by site.list.", + "title": "Id", + "type": "integer" + }, + "pri": { + "description": "Replacement site search priority value.", + "title": "Pri", + "type": "integer" + } + }, + "required": [ + "id", + "pri" + ], + "title": "SitePriorityUpdate", + "type": "object" + }, "Subscribe": { - "description": "订阅输入与响应模型,媒体身份必须为空对或完整有效对。", + "description": "Movie, TV, or music subscription input model.", "properties": { "audio_format": { "anyOf": [ @@ -1414,6 +2460,7 @@ "type": "null" } ], + "description": "Requested or recorded audio container or codec, such as FLAC or MP3.", "title": "Audio Format" }, "audio_quality": { @@ -1425,6 +2472,7 @@ "type": "null" } ], + "description": "Subscription audio-quality rule, such as hires, lossless, or lossy.", "title": "Audio Quality" }, "backdrop": { @@ -1436,6 +2484,7 @@ "type": "null" } ], + "description": "Backdrop image URL stored with the media or subscription.", "title": "Backdrop" }, "best_version": { @@ -1447,6 +2496,7 @@ "type": "null" } ], + "description": "Enable normal best-version upgrading when set to 1.", "title": "Best Version" }, "best_version_full": { @@ -1458,6 +2508,7 @@ "type": "null" } ], + "description": "Enable full best-version upgrading when set to 1.", "title": "Best Version Full" }, "completed_episode": { @@ -1469,6 +2520,7 @@ "type": "null" } ], + "description": "Highest episode number already completed for the subscription.", "title": "Completed Episode" }, "current_audio_format": { @@ -1480,6 +2532,7 @@ "type": "null" } ], + "description": "Audio format of the best version currently held.", "title": "Current Audio Format" }, "current_bit_depth": { @@ -1491,6 +2544,7 @@ "type": "null" } ], + "description": "Bit depth of the best version currently held.", "title": "Current Bit Depth" }, "current_bitrate": { @@ -1502,6 +2556,7 @@ "type": "null" } ], + "description": "Bitrate of the best version currently held.", "title": "Current Bitrate" }, "current_priority": { @@ -1513,6 +2568,7 @@ "type": "null" } ], + "description": "Calculated priority of the best version currently held.", "title": "Current Priority" }, "current_sample_rate": { @@ -1524,6 +2580,7 @@ "type": "null" } ], + "description": "Sample rate of the best version currently held.", "title": "Current Sample Rate" }, "custom_words": { @@ -1535,6 +2592,7 @@ "type": "null" } ], + "description": "Custom recognition or rename words applied to this media workflow.", "title": "Custom Words" }, "date": { @@ -1546,6 +2604,7 @@ "type": "null" } ], + "description": "Record creation or completion timestamp used by the history item.", "title": "Date" }, "description": { @@ -1557,6 +2616,7 @@ "type": "null" } ], + "description": "Human-readable media, torrent, or subscription description.", "title": "Description" }, "downloader": { @@ -1568,6 +2628,7 @@ "type": "null" } ], + "description": "Configured downloader instance name.", "title": "Downloader" }, "effect": { @@ -1579,6 +2640,7 @@ "type": "null" } ], + "description": "Video or release-effect filter expression used by the subscription.", "title": "Effect" }, "episode_group": { @@ -1590,6 +2652,7 @@ "type": "null" } ], + "description": "TMDB episode-group identifier used for alternate episode ordering.", "title": "Episode Group" }, "episode_priority": { @@ -1604,6 +2667,7 @@ "type": "null" } ], + "description": "Per-episode best-version priority state.", "title": "Episode Priority" }, "exclude": { @@ -1615,6 +2679,7 @@ "type": "null" } ], + "description": "Regular expression or filter expression that rejects matching releases.", "title": "Exclude" }, "filter": { @@ -1626,6 +2691,7 @@ "type": "null" } ], + "description": "Named filter rule or rule expression applied to this site or subscription.", "title": "Filter" }, "filter_groups": { @@ -1640,6 +2706,7 @@ "type": "null" } ], + "description": "Ordered filter-rule group names applied to the subscription.", "title": "Filter Groups" }, "id": { @@ -1651,6 +2718,7 @@ "type": "null" } ], + "description": "Persistent database identifier of the supplied record.", "title": "Id" }, "include": { @@ -1662,6 +2730,7 @@ "type": "null" } ], + "description": "Regular expression or filter expression that a release must match.", "title": "Include" }, "keyword": { @@ -1673,6 +2742,7 @@ "type": "null" } ], + "description": "Case-insensitive substring used to discover settings or filter storage entries.", "title": "Keyword" }, "lack_episode": { @@ -1685,6 +2755,7 @@ } ], "default": 0, + "description": "Number of episodes still missing from the subscription.", "title": "Lack Episode" }, "last_update": { @@ -1696,6 +2767,7 @@ "type": "null" } ], + "description": "Timestamp of the subscription's most recent update.", "title": "Last Update" }, "media_category": { @@ -1707,6 +2779,7 @@ "type": "null" } ], + "description": "MoviePilot library category assigned to the media.", "title": "Media Category" }, "media_id": { @@ -1718,6 +2791,7 @@ "type": "null" } ], + "description": "Source-native media ID. Always pair it with the exact media_source returned by search.", "title": "Media Id" }, "media_source": { @@ -1728,7 +2802,8 @@ { "type": "null" } - ] + ], + "description": "Metadata source identifier. Preserve the exact value returned with media_id." }, "min_bit_depth": { "anyOf": [ @@ -1739,6 +2814,7 @@ "type": "null" } ], + "description": "Minimum acceptable audio bit depth in bits.", "title": "Min Bit Depth" }, "min_bitrate": { @@ -1750,6 +2826,7 @@ "type": "null" } ], + "description": "Minimum acceptable audio bitrate in bits per second.", "title": "Min Bitrate" }, "min_sample_rate": { @@ -1761,6 +2838,7 @@ "type": "null" } ], + "description": "Minimum acceptable audio sample rate in hertz.", "title": "Min Sample Rate" }, "music_type": { @@ -1772,6 +2850,7 @@ "type": "null" } ], + "description": "Music identity level: recording, album, or artist where supported.", "title": "Music Type" }, "name": { @@ -1783,6 +2862,7 @@ "type": "null" } ], + "description": "Human-readable name of the site, storage item, subscription, or rule group.", "title": "Name" }, "note": { @@ -1797,6 +2877,7 @@ "type": "null" } ], + "description": "Structured auxiliary metadata stored with the record.", "title": "Note" }, "poster": { @@ -1808,6 +2889,7 @@ "type": "null" } ], + "description": "Poster image URL stored with the media or subscription.", "title": "Poster" }, "quality": { @@ -1819,6 +2901,7 @@ "type": "null" } ], + "description": "Video or release quality filter expression.", "title": "Quality" }, "resolution": { @@ -1830,6 +2913,7 @@ "type": "null" } ], + "description": "Video resolution filter expression, such as 1080p or 2160p.", "title": "Resolution" }, "save_path": { @@ -1841,6 +2925,7 @@ "type": "null" } ], + "description": "Configured downloader-side save path for the download or subscription.", "title": "Save Path" }, "search_imdbid": { @@ -1853,6 +2938,7 @@ } ], "default": 0, + "description": "Use IMDb identity during subscription search when set to 1.", "title": "Search Imdbid" }, "season": { @@ -1864,6 +2950,7 @@ "type": "null" } ], + "description": "Season number used by the media, search, subscription, or transfer operation.", "title": "Season" }, "sites": { @@ -1878,6 +2965,7 @@ "type": "null" } ], + "description": "Exact site IDs included in the search or subscription scope.", "title": "Sites" }, "start_episode": { @@ -1890,6 +2978,7 @@ } ], "default": 0, + "description": "First episode number requested by the subscription.", "title": "Start Episode" }, "state": { @@ -1901,6 +2990,7 @@ "type": "null" } ], + "description": "Current site, subscription, marketplace, or transfer state filter.", "title": "State" }, "total_episode": { @@ -1913,6 +3003,7 @@ } ], "default": 0, + "description": "Expected total episode count for the subscription.", "title": "Total Episode" }, "total_tracks": { @@ -1924,6 +3015,7 @@ "type": "null" } ], + "description": "Expected or recorded track count for a music item.", "title": "Total Tracks" }, "type": { @@ -1935,6 +3027,7 @@ "type": "null" } ], + "description": "MoviePilot media or storage item type required by the selected operation.", "title": "Type" }, "username": { @@ -1946,6 +3039,7 @@ "type": "null" } ], + "description": "MoviePilot or site username required by the selected operation.", "title": "Username" }, "vote": { @@ -1958,6 +3052,7 @@ } ], "default": 0.0, + "description": "Media vote average stored with the subscription.", "title": "Vote" }, "year": { @@ -1969,14 +3064,444 @@ "type": "null" } ], + "description": "Release or premiere year used to disambiguate the media title.", "title": "Year" } }, "title": "Subscribe", "type": "object" }, + "SubscribeShare": { + "description": "Shared subscription definition or publication metadata.", + "properties": { + "audio_format": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Requested or recorded audio container or codec, such as FLAC or MP3.", + "title": "Audio Format" + }, + "audio_quality": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Subscription audio-quality rule, such as hires, lossless, or lossy.", + "title": "Audio Quality" + }, + "backdrop": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Backdrop image URL stored with the media or subscription.", + "title": "Backdrop" + }, + "count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 0, + "description": "Maximum number of records to return on the requested page.", + "title": "Count" + }, + "custom_words": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Custom recognition or rename words applied to this media workflow.", + "title": "Custom Words" + }, + "date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Record creation or completion timestamp used by the history item.", + "title": "Date" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Human-readable media, torrent, or subscription description.", + "title": "Description" + }, + "effect": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Video or release-effect filter expression used by the subscription.", + "title": "Effect" + }, + "episode_group": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "TMDB episode-group identifier used for alternate episode ordering.", + "title": "Episode Group" + }, + "exclude": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Regular expression or filter expression that rejects matching releases.", + "title": "Exclude" + }, + "id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Persistent database identifier of the supplied record.", + "title": "Id" + }, + "include": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Regular expression or filter expression that a release must match.", + "title": "Include" + }, + "keyword": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Case-insensitive substring used to discover settings or filter storage entries.", + "title": "Keyword" + }, + "media_category": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "MoviePilot library category assigned to the media.", + "title": "Media Category" + }, + "media_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Source-native media ID. Always pair it with the exact media_source returned by search.", + "title": "Media Id" + }, + "media_source": { + "anyOf": [ + { + "$ref": "#/$defs/MediaSource" + }, + { + "type": "null" + } + ], + "description": "Metadata source identifier. Preserve the exact value returned with media_id." + }, + "min_bit_depth": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Minimum acceptable audio bit depth in bits.", + "title": "Min Bit Depth" + }, + "min_bitrate": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Minimum acceptable audio bitrate in bits per second.", + "title": "Min Bitrate" + }, + "min_sample_rate": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Minimum acceptable audio sample rate in hertz.", + "title": "Min Sample Rate" + }, + "music_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Music identity level: recording, album, or artist where supported.", + "title": "Music Type" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Human-readable name of the site, storage item, subscription, or rule group.", + "title": "Name" + }, + "poster": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Poster image URL stored with the media or subscription.", + "title": "Poster" + }, + "quality": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Video or release quality filter expression.", + "title": "Quality" + }, + "resolution": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Video resolution filter expression, such as 1080p or 2160p.", + "title": "Resolution" + }, + "season": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Season number used by the media, search, subscription, or transfer operation.", + "title": "Season" + }, + "share_comment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional explanatory comment published with a shared item.", + "title": "Share Comment" + }, + "share_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Public title used when publishing a subscription or workflow.", + "title": "Share Title" + }, + "share_uid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Exact MoviePilot Server sharing-user ID to follow or unfollow.", + "title": "Share Uid" + }, + "share_user": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Public contributor name used when publishing a subscription or workflow.", + "title": "Share User" + }, + "subscribe_id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Persistent subscription ID returned by subscription.list.", + "title": "Subscribe Id" + }, + "total_episode": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 0, + "description": "Expected total episode count for the subscription.", + "title": "Total Episode" + }, + "total_tracks": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Expected or recorded track count for a music item.", + "title": "Total Tracks" + }, + "type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "MoviePilot media or storage item type required by the selected operation.", + "title": "Type" + }, + "vote": { + "anyOf": [ + { + "type": "number" + }, + { + "type": "null" + } + ], + "default": 0.0, + "description": "Media vote average stored with the subscription.", + "title": "Vote" + }, + "year": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Release or premiere year used to disambiguate the media title.", + "title": "Year" + } + }, + "title": "SubscribeShare", + "type": "object" + }, "SystemSettingsUpdateRequest": { - "description": "统一系统设置更新请求。", + "description": "One registered system-setting update request.", "properties": { "match_field": { "anyOf": [ @@ -1987,13 +3512,16 @@ "type": "null" } ], + "description": "Object field used to match a list item. Downloaders, MediaServers, Notifications, Directories, and Storages default to name; NotificationSwitchs defaults to type. Supply it for other object lists.", "title": "Match Field" }, "match_value": { + "description": "Value compared against match_field. If omitted, use value[match_field]; scalar lists use value directly.", "title": "Match Value" }, "operation": { "default": "replace", + "description": "replace overwrites the complete value; merge_dict shallow-merges an object; upsert_list_item inserts or replaces one matched list item; remove_list_item removes one matched list item.", "enum": [ "replace", "merge_dict", @@ -2004,6 +3532,7 @@ "type": "string" }, "remove_keys": { + "description": "Object keys to remove after merge_dict applies the supplied value.", "items": { "type": "string" }, @@ -2011,10 +3540,12 @@ "type": "array" }, "setting_key": { + "description": "Exact setting key. Accepts a Settings field name, a SystemConfigKey value or enum name, or an alias that resolves to one unique setting. Call config.system.get with group or keyword first when the key is unknown.", "title": "Setting Key", "type": "string" }, "value": { + "description": "New value or list item. For replace, send the complete value. For merge_dict, send the object fragment to merge. For upsert_list_item or remove_list_item, send one object or scalar item.", "title": "Value" } }, @@ -2025,7 +3556,7 @@ "type": "object" }, "TorrentInfo": { - "description": "搜索种子信息", + "description": "One torrent candidate returned by MoviePilot search.", "properties": { "category": { "anyOf": [ @@ -2036,6 +3567,7 @@ "type": "null" } ], + "description": "MoviePilot media category or filter-group category, depending on the operation.", "title": "Category" }, "date_elapsed": { @@ -2047,6 +3579,7 @@ "type": "null" } ], + "description": "Human-readable age of the torrent publication date.", "title": "Date Elapsed" }, "description": { @@ -2058,6 +3591,7 @@ "type": "null" } ], + "description": "Human-readable media, torrent, or subscription description.", "title": "Description" }, "downloadvolumefactor": { @@ -2069,6 +3603,7 @@ "type": "null" } ], + "description": "Torrent download-volume multiplier reported by the site.", "title": "Downloadvolumefactor" }, "enclosure": { @@ -2080,6 +3615,7 @@ "type": "null" } ], + "description": "Torrent download URL or enclosure supplied by the indexer result.", "title": "Enclosure" }, "freedate": { @@ -2091,6 +3627,7 @@ "type": "null" } ], + "description": "Torrent freeleech expiration timestamp reported by the site.", "title": "Freedate" }, "freedate_diff": { @@ -2102,6 +3639,7 @@ "type": "null" } ], + "description": "Seconds remaining until the torrent freeleech period ends.", "title": "Freedate Diff" }, "grabs": { @@ -2114,6 +3652,7 @@ } ], "default": 0, + "description": "Number of completed downloads reported for the torrent.", "title": "Grabs" }, "hit_and_run": { @@ -2126,6 +3665,7 @@ } ], "default": false, + "description": "Whether the torrent is subject to hit-and-run requirements.", "title": "Hit And Run" }, "labels": { @@ -2140,6 +3680,7 @@ "type": "null" } ], + "description": "Torrent labels supplied by the site result.", "title": "Labels" }, "media_id": { @@ -2151,6 +3692,7 @@ "type": "null" } ], + "description": "Source-native media ID. Always pair it with the exact media_source returned by search.", "title": "Media Id" }, "media_source": { @@ -2161,7 +3703,8 @@ { "type": "null" } - ] + ], + "description": "Metadata source identifier. Preserve the exact value returned with media_id." }, "page_url": { "anyOf": [ @@ -2172,6 +3715,7 @@ "type": "null" } ], + "description": "Public details page for the torrent result.", "title": "Page Url" }, "peers": { @@ -2184,6 +3728,7 @@ } ], "default": 0, + "description": "Number of downloading peers reported for the torrent.", "title": "Peers" }, "pri_order": { @@ -2196,6 +3741,7 @@ } ], "default": 0, + "description": "Indexer priority order assigned to the torrent result.", "title": "Pri Order" }, "pubdate": { @@ -2207,6 +3753,7 @@ "type": "null" } ], + "description": "Torrent publication timestamp.", "title": "Pubdate" }, "seeders": { @@ -2219,6 +3766,7 @@ } ], "default": 0, + "description": "Minimum seeder expression for a filter rule, or the torrent's seeder count.", "title": "Seeders" }, "site": { @@ -2230,6 +3778,7 @@ "type": "null" } ], + "description": "Source site identifier associated with the torrent result.", "title": "Site" }, "site_cookie": { @@ -2241,6 +3790,7 @@ "type": "null" } ], + "description": "Site cookie bundled with the torrent result. Treat this value as a secret.", "title": "Site Cookie" }, "site_downloader": { @@ -2252,6 +3802,7 @@ "type": "null" } ], + "description": "Downloader instance selected by the source site.", "title": "Site Downloader" }, "site_name": { @@ -2263,6 +3814,7 @@ "type": "null" } ], + "description": "Human-readable source site name.", "title": "Site Name" }, "site_order": { @@ -2275,6 +3827,7 @@ } ], "default": 0, + "description": "Source site's configured search order.", "title": "Site Order" }, "site_proxy": { @@ -2287,6 +3840,7 @@ } ], "default": false, + "description": "Whether the torrent's source site uses the configured proxy.", "title": "Site Proxy" }, "site_ua": { @@ -2298,6 +3852,7 @@ "type": "null" } ], + "description": "User-Agent associated with the source site.", "title": "Site Ua" }, "size": { @@ -2310,6 +3865,7 @@ } ], "default": 0.0, + "description": "File or torrent size in bytes.", "title": "Size" }, "title": { @@ -2321,6 +3877,7 @@ "type": "null" } ], + "description": "Media, torrent, subscription, or history title used by the operation.", "title": "Title" }, "uploadvolumefactor": { @@ -2332,6 +3889,7 @@ "type": "null" } ], + "description": "Torrent upload-volume multiplier reported by the site.", "title": "Uploadvolumefactor" }, "volume_factor": { @@ -2343,6 +3901,7 @@ "type": "null" } ], + "description": "Combined upload/download volume-factor label shown for the torrent.", "title": "Volume Factor" } }, @@ -2350,7 +3909,7 @@ "type": "object" }, "TransferHistory-Input": { - "description": "文件整理历史记录", + "description": "One MoviePilot file-transfer history record.", "properties": { "audio_format": { "anyOf": [ @@ -2361,6 +3920,7 @@ "type": "null" } ], + "description": "Requested or recorded audio container or codec, such as FLAC or MP3.", "title": "Audio Format" }, "audio_lossless": { @@ -2372,6 +3932,7 @@ "type": "null" } ], + "description": "Whether the recorded audio result is lossless.", "title": "Audio Lossless" }, "bit_depth": { @@ -2383,6 +3944,7 @@ "type": "null" } ], + "description": "Recorded audio bit depth in bits.", "title": "Bit Depth" }, "bitrate": { @@ -2394,6 +3956,7 @@ "type": "null" } ], + "description": "Recorded audio bitrate in bits per second.", "title": "Bitrate" }, "category": { @@ -2405,6 +3968,7 @@ "type": "null" } ], + "description": "MoviePilot media category or filter-group category, depending on the operation.", "title": "Category" }, "date": { @@ -2416,6 +3980,7 @@ "type": "null" } ], + "description": "Record creation or completion timestamp used by the history item.", "title": "Date" }, "dest": { @@ -2427,6 +3992,7 @@ "type": "null" } ], + "description": "Organized destination path recorded in transfer history.", "title": "Dest" }, "dest_fileitem": { @@ -2437,7 +4003,8 @@ { "type": "null" } - ] + ], + "description": "Serialized destination storage item recorded by the transfer." }, "dest_storage": { "anyOf": [ @@ -2448,6 +4015,7 @@ "type": "null" } ], + "description": "Configured storage name containing the organized destination.", "title": "Dest Storage" }, "download_hash": { @@ -2459,6 +4027,7 @@ "type": "null" } ], + "description": "Provider-native torrent hash associated with the record.", "title": "Download Hash" }, "episode_group": { @@ -2470,6 +4039,7 @@ "type": "null" } ], + "description": "TMDB episode-group identifier used for alternate episode ordering.", "title": "Episode Group" }, "episodes": { @@ -2481,6 +4051,7 @@ "type": "null" } ], + "description": "Episode-number expression recorded in history, such as E01-E03.", "title": "Episodes" }, "errmsg": { @@ -2492,6 +4063,7 @@ "type": "null" } ], + "description": "Error message recorded for a failed transfer.", "title": "Errmsg" }, "files": { @@ -2502,9 +4074,11 @@ { "type": "null" } - ] + ], + "description": "Serialized list of files recorded by the history item." }, "id": { + "description": "Persistent database identifier of the supplied record.", "title": "Id", "type": "integer" }, @@ -2517,6 +4091,7 @@ "type": "null" } ], + "description": "Image URL stored with the history record.", "title": "Image" }, "media_id": { @@ -2528,6 +4103,7 @@ "type": "null" } ], + "description": "Source-native media ID. Always pair it with the exact media_source returned by search.", "title": "Media Id" }, "media_source": { @@ -2538,7 +4114,8 @@ { "type": "null" } - ] + ], + "description": "Metadata source identifier. Preserve the exact value returned with media_id." }, "mode": { "anyOf": [ @@ -2549,6 +4126,7 @@ "type": "null" } ], + "description": "Operation mode; music.explore accepts chart or fresh, while transfer history records move, copy, link, or softlink.", "title": "Mode" }, "music_type": { @@ -2560,6 +4138,7 @@ "type": "null" } ], + "description": "Music identity level: recording, album, or artist where supported.", "title": "Music Type" }, "sample_rate": { @@ -2571,6 +4150,7 @@ "type": "null" } ], + "description": "Recorded audio sample rate in hertz.", "title": "Sample Rate" }, "seasons": { @@ -2582,6 +4162,7 @@ "type": "null" } ], + "description": "Season-number expression recorded in history.", "title": "Seasons" }, "src": { @@ -2593,6 +4174,7 @@ "type": "null" } ], + "description": "Source path recorded in transfer history.", "title": "Src" }, "src_fileitem": { @@ -2603,7 +4185,8 @@ { "type": "null" } - ] + ], + "description": "Serialized source storage item recorded by the transfer." }, "src_storage": { "anyOf": [ @@ -2614,10 +4197,12 @@ "type": "null" } ], + "description": "Configured storage name containing the transfer source.", "title": "Src Storage" }, "status": { "default": true, + "description": "Transfer success status used to filter history or describe a record.", "title": "Status", "type": "boolean" }, @@ -2630,6 +4215,7 @@ "type": "null" } ], + "description": "Media, torrent, subscription, or history title used by the operation.", "title": "Title" }, "total_tracks": { @@ -2641,6 +4227,7 @@ "type": "null" } ], + "description": "Expected or recorded track count for a music item.", "title": "Total Tracks" }, "transfer_task_id": { @@ -2652,6 +4239,7 @@ "type": "null" } ], + "description": "Stable durable transfer-task ID associated with the history record.", "title": "Transfer Task Id" }, "type": { @@ -2663,6 +4251,7 @@ "type": "null" } ], + "description": "MoviePilot media or storage item type required by the selected operation.", "title": "Type" }, "year": { @@ -2674,6 +4263,7 @@ "type": "null" } ], + "description": "Release or premiere year used to disambiguate the media title.", "title": "Year" } }, @@ -2682,6 +4272,647 @@ ], "title": "TransferHistory", "type": "object" + }, + "TransferManualReviewRequest": { + "additionalProperties": false, + "description": "Authorized decision and optional result for one durable transfer operation.", + "properties": { + "decision": { + "description": "Manual-review decision selected from the endpoint's declared enum.", + "enum": [ + "not_applied", + "applied" + ], + "title": "Decision", + "type": "string" + }, + "operation_id": { + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "minLength": 1, + "title": "Operation Id", + "type": "string" + }, + "reason": { + "description": "Human-readable justification recorded with a manual-review decision.", + "maxLength": 2000, + "minLength": 1, + "title": "Reason", + "type": "string" + }, + "result_payload": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/$defs/JsonData-Input" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "description": "Structured external-operation result recorded with manual review.", + "title": "Result Payload" + } + }, + "required": [ + "operation_id", + "decision", + "reason" + ], + "title": "TransferManualReviewRequest", + "type": "object" + }, + "Workflow-Input": { + "description": "Complete workflow definition accepted by create and update operations.", + "properties": { + "actions": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/Action-Input" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Ordered workflow action definitions executed by this workflow or flow.", + "title": "Actions" + }, + "add_time": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Timestamp when the workflow definition was created.", + "title": "Add Time" + }, + "current_action": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Identifier of the workflow action currently selected or executing.", + "title": "Current Action" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Human-readable media, torrent, or subscription description.", + "title": "Description" + }, + "event_conditions": { + "anyOf": [ + { + "additionalProperties": { + "$ref": "#/$defs/JsonData-Input" + }, + "type": "object" + }, + { + "type": "null" + } + ], + "description": "Additional workflow event-filter conditions.", + "title": "Event Conditions" + }, + "event_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Exact event type returned by workflow.event_types.", + "title": "Event Type" + }, + "execution_config": { + "anyOf": [ + { + "$ref": "#/$defs/WorkflowExecutionConfig" + }, + { + "type": "null" + } + ], + "description": "Workflow runtime limits, concurrency, and failure-policy configuration." + }, + "execution_state": { + "anyOf": [ + { + "$ref": "#/$defs/WorkflowExecutionState-Input" + }, + { + "type": "null" + } + ], + "description": "Persisted resumable workflow execution state." + }, + "flows": { + "anyOf": [ + { + "items": { + "$ref": "#/$defs/ActionFlow-Input" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Workflow connection definitions linking action nodes.", + "title": "Flows" + }, + "id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Persistent database identifier of the supplied record.", + "title": "Id" + }, + "last_time": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Timestamp of the workflow's most recent execution.", + "title": "Last Time" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Human-readable name of the site, storage item, subscription, or rule group.", + "title": "Name" + }, + "result": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Persisted workflow action result value.", + "title": "Result" + }, + "run_count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 0, + "description": "Number of times the workflow has been executed.", + "title": "Run Count" + }, + "state": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Current site, subscription, marketplace, or transfer state filter.", + "title": "State" + }, + "timer": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Workflow timer or cron expression used for scheduled execution.", + "title": "Timer" + }, + "trigger_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "timer", + "description": "Workflow trigger filter: timer, event, manual, or all.", + "title": "Trigger Type" + } + }, + "title": "Workflow", + "type": "object" + }, + "WorkflowExecutionConfig": { + "description": "Workflow concurrency, join, branch, and failure policies.", + "properties": { + "max_workers": { + "anyOf": [ + { + "minimum": 1.0, + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Maximum concurrent workflow actions allowed by the execution configuration.", + "title": "Max Workers" + } + }, + "title": "WorkflowExecutionConfig", + "type": "object" + }, + "WorkflowExecutionState-Input": { + "description": "Persisted resumable workflow execution state.", + "properties": { + "errors": { + "additionalProperties": { + "type": "string" + }, + "description": "Workflow execution errors keyed or ordered by action identity.", + "title": "Errors", + "type": "object" + }, + "nodes": { + "additionalProperties": { + "$ref": "#/$defs/WorkflowNodeState" + }, + "description": "Persisted workflow node runtime states keyed by action identity.", + "title": "Nodes", + "type": "object" + }, + "outputs": { + "additionalProperties": { + "$ref": "#/$defs/JsonData-Input" + }, + "description": "Named output mappings produced by this workflow action.", + "title": "Outputs", + "type": "object" + }, + "runtime": { + "$ref": "#/$defs/WorkflowRuntimeState", + "description": "Persisted workflow runtime metadata used for safe resume." + }, + "version": { + "default": 1, + "description": "Plugin release or schema version selected by the operation.", + "title": "Version", + "type": "integer" + } + }, + "title": "WorkflowExecutionState", + "type": "object" + }, + "WorkflowNodeState": { + "description": "Persisted runtime state for one workflow action node.", + "properties": { + "attempt": { + "default": 0, + "description": "Current execution-attempt number for this workflow node.", + "title": "Attempt", + "type": "integer" + }, + "finished_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Timestamp when the workflow node or execution finished.", + "title": "Finished At" + }, + "message": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Human-readable workflow runtime or provider result message.", + "title": "Message" + }, + "started_at": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Timestamp when the workflow node or execution started.", + "title": "Started At" + }, + "state": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Current site, subscription, marketplace, or transfer state filter.", + "title": "State" + } + }, + "title": "WorkflowNodeState", + "type": "object" + }, + "WorkflowRuntimeState": { + "description": "Complete persisted workflow runtime and progress state.", + "properties": { + "attempts": { + "additionalProperties": { + "type": "integer" + }, + "description": "Attempt counters keyed by workflow node or operation identity.", + "title": "Attempts", + "type": "object" + }, + "errors": { + "additionalProperties": { + "type": "string" + }, + "description": "Workflow execution errors keyed or ordered by action identity.", + "title": "Errors", + "type": "object" + }, + "finished_actions": { + "default": 0, + "description": "Workflow action IDs already completed in the persisted execution state.", + "title": "Finished Actions", + "type": "integer" + }, + "node_states": { + "additionalProperties": { + "type": "string" + }, + "description": "Persisted runtime states keyed by workflow node identity.", + "title": "Node States", + "type": "object" + }, + "progress": { + "default": 0, + "description": "Current numeric or structured workflow execution progress.", + "title": "Progress", + "type": "integer" + }, + "running_tasks": { + "default": 0, + "description": "Workflow task IDs currently executing.", + "title": "Running Tasks", + "type": "integer" + } + }, + "title": "WorkflowRuntimeState", + "type": "object" + }, + "WorkflowShare": { + "description": "Shared workflow definition or publication metadata.", + "properties": { + "actions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Ordered workflow action definitions executed by this workflow or flow.", + "title": "Actions" + }, + "context": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Persisted workflow execution context available to later actions.", + "title": "Context" + }, + "count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 0, + "description": "Maximum number of records to return on the requested page.", + "title": "Count" + }, + "date": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Record creation or completion timestamp used by the history item.", + "title": "Date" + }, + "description": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Human-readable media, torrent, or subscription description.", + "title": "Description" + }, + "event_conditions": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Additional workflow event-filter conditions.", + "title": "Event Conditions" + }, + "event_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Exact event type returned by workflow.event_types.", + "title": "Event Type" + }, + "flows": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Workflow connection definitions linking action nodes.", + "title": "Flows" + }, + "id": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Persistent database identifier of the supplied record.", + "title": "Id" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Human-readable name of the site, storage item, subscription, or rule group.", + "title": "Name" + }, + "share_comment": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional explanatory comment published with a shared item.", + "title": "Share Comment" + }, + "share_title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Public title used when publishing a subscription or workflow.", + "title": "Share Title" + }, + "share_uid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Exact MoviePilot Server sharing-user ID to follow or unfollow.", + "title": "Share Uid" + }, + "share_user": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Public contributor name used when publishing a subscription or workflow.", + "title": "Share User" + }, + "timer": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Workflow timer or cron expression used for scheduled execution.", + "title": "Timer" + }, + "trigger_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Workflow trigger filter: timer, event, manual, or all.", + "title": "Trigger Type" + } + }, + "title": "WorkflowShare", + "type": "object" } }, "$schema": "https://json-schema.org/draft/2020-12/schema", @@ -2689,10 +4920,11 @@ "oneOf": [ { "additionalProperties": false, - "description": "查询自定义识别词 Method: GET. Path: /api/v1/system/identifiers. Effect: safe_read.", + "description": "Read the complete custom media-recognition identifier list. Method: GET. Path: /api/v1/system/identifiers. Effect: safe_read.", "properties": { "operation_id": { "const": "config.identifiers.get", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" } }, @@ -2704,13 +4936,15 @@ }, { "additionalProperties": false, - "description": "更新自定义识别词 Method: POST. Path: /api/v1/system/identifiers. Effect: reversible_write.", + "description": "Replace the complete custom media-recognition identifier list. Method: POST. Path: /api/v1/system/identifiers. Effect: reversible_write.", "properties": { "body": { - "$ref": "#/$defs/CustomIdentifiersUpdateRequest" + "$ref": "#/$defs/CustomIdentifiersUpdateRequest", + "description": "Request value for config.identifiers.update. Replace the complete custom media-recognition identifier list. Use the exact type and fields below." }, "operation_id": { "const": "config.identifiers.update", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" } }, @@ -2723,14 +4957,48 @@ }, { "additionalProperties": false, - "description": "统一查询系统设置 Method: GET. Path: /api/v1/system/settings. Effect: safe_read.", + "description": "Read one explicitly public system setting by exact key. Method: GET. Path: /api/v1/system/setting/public/{key}. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "config.public.get", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for config.public.get. Read one explicitly public system setting by exact key. Use only the named fields below.", + "properties": { + "key": { + "description": "Optional exact plugin data key used to narrow the returned preview.", + "title": "Key", + "type": "string" + } + }, + "required": [ + "key" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "config.public.get", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Discover registered system settings or read one exact setting. Method: GET. Path: /api/v1/system/settings. Effect: safe_read.", "properties": { "operation_id": { "const": "config.system.get", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "query": { "additionalProperties": false, + "description": "Filters and control values for config.system.get. Discover registered system settings or read one exact setting. Use only the named fields below.", "properties": { "group": { "anyOf": [ @@ -2742,6 +5010,7 @@ } ], "default": "all", + "description": "Discovery group used when setting_key is omitted. Supported groups are all, settings, systemconfig, downloaders, media_servers, notifications, notification_switches, storages, directories, search_sites, subscribe_sites, site_auth, ai_agent, filter_rules, subscribe_defaults, plugins, customization, transfer, scraping, and misc.", "title": "Group" }, "include_values": { @@ -2753,6 +5022,7 @@ "type": "null" } ], + "description": "Return full values. Defaults to true for one exact key and false for discovery results.", "title": "Include Values" }, "keyword": { @@ -2764,6 +5034,7 @@ "type": "null" } ], + "description": "Case-insensitive substring used to discover matching keys, groups, or labels.", "title": "Keyword" }, "setting_key": { @@ -2775,10 +5046,12 @@ "type": "null" } ], + "description": "Exact setting key. Accepts Settings field names such as APP_DOMAIN or LLM_MODEL, SystemConfigKey values or enum names such as Downloaders or MediaServers, and aliases that resolve to one unique setting. Omit it to discover settings.", "title": "Setting Key" }, "show_secrets": { "default": false, + "description": "Return unredacted secret values. Defaults to false and remains confirmation-protected.", "title": "Show Secrets", "type": "boolean" } @@ -2794,13 +5067,15 @@ }, { "additionalProperties": false, - "description": "统一更新系统设置 Method: POST. Path: /api/v1/system/settings. Effect: reversible_write.", + "description": "Update one exact registered system setting. Method: POST. Path: /api/v1/system/settings. Effect: reversible_write.", "properties": { "body": { - "$ref": "#/$defs/SystemSettingsUpdateRequest" + "$ref": "#/$defs/SystemSettingsUpdateRequest", + "description": "Request value for config.system.update. Update one exact registered system setting. Use the exact type and fields below." }, "operation_id": { "const": "config.system.update", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" } }, @@ -2813,13 +5088,329 @@ }, { "additionalProperties": false, - "description": "添加下载(不含媒体信息) Method: POST. Path: /api/v1/download/add. Effect: external_side_effect.", + "description": "Read current-user feature flags, runtime capabilities, and effective permissions. Method: GET. Path: /api/v1/system/global/user. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "config.user.get", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "config.user.get", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read the current host CPU utilization percentage. Method: GET. Path: /api/v1/dashboard/cpu. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "dashboard.cpu", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "dashboard.cpu", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read aggregate downloader task counts, speeds, and free-space information. Method: GET. Path: /api/v1/dashboard/downloader. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "dashboard.downloader", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for dashboard.downloader. Read aggregate downloader task counts, speeds, and free-space information. Use only the named fields below.", + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Human-readable name of the site, storage item, subscription, or rule group.", + "title": "Name" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id" + ], + "title": "dashboard.downloader", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read aggregate movie, TV, episode, and music library counts. Method: GET. Path: /api/v1/dashboard/statistic. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "dashboard.media.statistics", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for dashboard.media.statistics. Read aggregate movie, TV, episode, and music library counts. Use only the named fields below.", + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Human-readable name of the site, storage item, subscription, or rule group.", + "title": "Name" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id" + ], + "title": "dashboard.media.statistics", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read current MoviePilot process and host memory utilization. Method: GET. Path: /api/v1/dashboard/memory. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "dashboard.memory", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "dashboard.memory", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read the current host network receive and transmit counters. Method: GET. Path: /api/v1/dashboard/network. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "dashboard.network", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "dashboard.network", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List host processes visible to the MoviePilot runtime. Method: GET. Path: /api/v1/dashboard/processes. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "dashboard.processes", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "dashboard.processes", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read local filesystem capacity and free-space information. Method: GET. Path: /api/v1/dashboard/storage. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "dashboard.storage", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "dashboard.storage", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read MoviePilot host, runtime, platform, and uptime summary information. Method: GET. Path: /api/v1/dashboard/system. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "dashboard.system", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "dashboard.system", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read aggregate file-transfer counts grouped by time period. Method: GET. Path: /api/v1/dashboard/transfer. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "dashboard.transfer.statistics", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for dashboard.transfer.statistics. Read aggregate file-transfer counts grouped by time period. Use only the named fields below.", + "properties": { + "days": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 7, + "description": "Recommendation time window in days.", + "title": "Days" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id" + ], + "title": "dashboard.transfer.statistics", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Create, verify, and atomically publish a managed database backup. Method: POST. Path: /api/v1/system/database/backups. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "database.backups.create", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "database.backups.create", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Delete one exact managed database backup artifact. Method: DELETE. Path: /api/v1/system/database/backups/{name}. Effect: destructive_write.", + "properties": { + "operation_id": { + "const": "database.backups.delete", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for database.backups.delete. Delete one exact managed database backup artifact. Use only the named fields below.", + "properties": { + "name": { + "description": "Human-readable name of the site, storage item, subscription, or rule group.", + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "database.backups.delete", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List managed database backup artifacts without exposing host paths. Method: GET. Path: /api/v1/system/database/backups. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "database.backups.list", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "database.backups.list", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Verify the integrity of one exact managed database backup artifact. Method: POST. Path: /api/v1/system/database/backups/{name}/verify. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "database.backups.verify", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for database.backups.verify. Verify the integrity of one exact managed database backup artifact. Use only the named fields below.", + "properties": { + "name": { + "description": "Human-readable name of the site, storage item, subscription, or rule group.", + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "database.backups.verify", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Submit one torrent to MoviePilot's normal download workflow. Method: POST. Path: /api/v1/download/add. Effect: external_side_effect.", "properties": { "body": { - "$ref": "#/$defs/Body_add_api_v1_download_add_post" + "$ref": "#/$defs/Body_add_api_v1_download_add_post", + "description": "Request value for download.add. Submit one torrent to MoviePilot's normal download workflow. Use the exact type and fields below." }, "operation_id": { "const": "download.add", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" } }, @@ -2832,13 +5423,31 @@ }, { "additionalProperties": false, - "description": "删除下载历史记录 Method: DELETE. Path: /api/v1/history/download. Effect: destructive_write.", + "description": "List enabled downloader instance names and provider types without credentials. Method: GET. Path: /api/v1/download/clients. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "download.clients", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "download.clients", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Delete one MoviePilot download-history record. Method: DELETE. Path: /api/v1/history/download. Effect: destructive_write.", "properties": { "body": { - "$ref": "#/$defs/DownloadHistory-Input" + "$ref": "#/$defs/DownloadHistory-Input", + "description": "Request value for download.history.delete. Delete one MoviePilot download-history record. Use the exact type and fields below." }, "operation_id": { "const": "download.history.delete", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" } }, @@ -2851,25 +5460,134 @@ }, { "additionalProperties": false, - "description": "查询内置过滤规则 Method: GET. Path: /api/v1/rule/builtin. Effect: safe_read.", + "description": "Page MoviePilot download-history records in reverse chronological order. Method: GET. Path: /api/v1/history/download. Effect: safe_read.", "properties": { - "body": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Rule Ids" + "operation_id": { + "const": "download.history.list", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for download.history.list. Page MoviePilot download-history records in reverse chronological order. Use only the named fields below.", + "properties": { + "count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 30, + "description": "Maximum number of records to return on the requested page.", + "title": "Count" + }, + "page": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 1, + "description": "One-based result page number.", + "title": "Page" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id" + ], + "title": "download.history.list", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List configured downloader save-path URIs that may be passed to download.add. Method: GET. Path: /api/v1/download/paths. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "download.paths", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "download.paths", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List currently downloading MoviePilot tasks with their canonical media context. Method: GET. Path: /api/v1/download/. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "download.tasks.active", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for download.tasks.active. List currently downloading MoviePilot tasks with their canonical media context. Use only the named fields below.", + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Human-readable name of the site, storage item, subscription, or rule group.", + "title": "Name" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id" + ], + "title": "download.tasks.active", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List built-in torrent filter rules. Method: GET. Path: /api/v1/rule/builtin. Effect: safe_read.", + "properties": { "operation_id": { "const": "filter.builtin", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for filter.builtin. List built-in torrent filter rules. Use only the named fields below.", + "properties": { + "rule_ids": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Exact built-in rule IDs to return. Repeat rule_ids in the query string; omit it to list every built-in rule.", + "title": "Rule Ids" + } + }, + "type": "object" } }, "required": [ @@ -2880,33 +5598,37 @@ }, { "additionalProperties": false, - "description": "查询自定义过滤规则 Method: GET. Path: /api/v1/rule/custom. Effect: safe_read.", + "description": "List user-defined torrent filter rules. Method: GET. Path: /api/v1/rule/custom. Effect: safe_read.", "properties": { - "body": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Rule Ids" - }, "operation_id": { "const": "filter.custom", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "query": { "additionalProperties": false, + "description": "Filters and control values for filter.custom. List user-defined torrent filter rules. Use only the named fields below.", "properties": { "include_group_refs": { "default": true, + "description": "Include custom rules referenced only through rule groups.", "title": "Include Group Refs", "type": "boolean" + }, + "rule_ids": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Exact custom rule IDs to return. Repeat rule_ids in the query string; omit it to list every custom rule.", + "title": "Rule Ids" } }, "type": "object" @@ -2920,13 +5642,15 @@ }, { "additionalProperties": false, - "description": "新增自定义过滤规则 Method: POST. Path: /api/v1/rule/custom. Effect: reversible_write.", + "description": "Create one user-defined torrent filter rule. Method: POST. Path: /api/v1/rule/custom. Effect: reversible_write.", "properties": { "body": { - "$ref": "#/$defs/CustomFilterRuleCreateRequest" + "$ref": "#/$defs/CustomFilterRuleCreateRequest", + "description": "Request value for filter.custom.add. Create one user-defined torrent filter rule. Use the exact type and fields below." }, "operation_id": { "const": "filter.custom.add", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" } }, @@ -2939,16 +5663,19 @@ }, { "additionalProperties": false, - "description": "删除自定义过滤规则 Method: DELETE. Path: /api/v1/rule/custom/{rule_id}. Effect: destructive_write.", + "description": "Delete one user-defined torrent filter rule. Method: DELETE. Path: /api/v1/rule/custom/{rule_id}. Effect: destructive_write.", "properties": { "operation_id": { "const": "filter.custom.delete", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for filter.custom.delete. Delete one user-defined torrent filter rule. Use only the named fields below.", "properties": { "rule_id": { + "description": "Stable custom filter-rule ID.", "title": "Rule Id", "type": "string" } @@ -2968,19 +5695,23 @@ }, { "additionalProperties": false, - "description": "更新自定义过滤规则 Method: PUT. Path: /api/v1/rule/custom/{rule_id}. Effect: reversible_write.", + "description": "Update one user-defined torrent filter rule. Method: PUT. Path: /api/v1/rule/custom/{rule_id}. Effect: reversible_write.", "properties": { "body": { - "$ref": "#/$defs/CustomFilterRuleUpdateRequest" + "$ref": "#/$defs/CustomFilterRuleUpdateRequest", + "description": "Request value for filter.custom.update. Update one user-defined torrent filter rule. Use the exact type and fields below." }, "operation_id": { "const": "filter.custom.update", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for filter.custom.update. Update one user-defined torrent filter rule. Use only the named fields below.", "properties": { "rule_id": { + "description": "Stable custom filter-rule ID.", "title": "Rule Id", "type": "string" } @@ -3001,13 +5732,15 @@ }, { "additionalProperties": false, - "description": "新增过滤规则组 Method: POST. Path: /api/v1/rule/groups. Effect: reversible_write.", + "description": "Create one named filter-rule group. Method: POST. Path: /api/v1/rule/groups. Effect: reversible_write.", "properties": { "body": { - "$ref": "#/$defs/FilterRuleGroupCreateRequest" + "$ref": "#/$defs/FilterRuleGroupCreateRequest", + "description": "Request value for filter.group.add. Create one named filter-rule group. Use the exact type and fields below." }, "operation_id": { "const": "filter.group.add", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" } }, @@ -3020,16 +5753,19 @@ }, { "additionalProperties": false, - "description": "删除过滤规则组 Method: DELETE. Path: /api/v1/rule/groups/{name}. Effect: destructive_write.", + "description": "Delete one named filter-rule group. Method: DELETE. Path: /api/v1/rule/groups/{name}. Effect: destructive_write.", "properties": { "operation_id": { "const": "filter.group.delete", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for filter.group.delete. Delete one named filter-rule group. Use only the named fields below.", "properties": { "name": { + "description": "Human-readable name of the site, storage item, subscription, or rule group.", "title": "Name", "type": "string" } @@ -3049,19 +5785,23 @@ }, { "additionalProperties": false, - "description": "更新过滤规则组 Method: PUT. Path: /api/v1/rule/groups/{name}. Effect: reversible_write.", + "description": "Update or rename one named filter-rule group. Method: PUT. Path: /api/v1/rule/groups/{name}. Effect: reversible_write.", "properties": { "body": { - "$ref": "#/$defs/FilterRuleGroupUpdateRequest" + "$ref": "#/$defs/FilterRuleGroupUpdateRequest", + "description": "Request value for filter.group.update. Update or rename one named filter-rule group. Use the exact type and fields below." }, "operation_id": { "const": "filter.group.update", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for filter.group.update. Update or rename one named filter-rule group. Use only the named fields below.", "properties": { "name": { + "description": "Human-readable name of the site, storage item, subscription, or rule group.", "title": "Name", "type": "string" } @@ -3082,31 +5822,35 @@ }, { "additionalProperties": false, - "description": "查询过滤规则组 Method: GET. Path: /api/v1/rule/groups. Effect: safe_read.", + "description": "List named filter-rule groups. Method: GET. Path: /api/v1/rule/groups. Effect: safe_read.", "properties": { - "body": { - "anyOf": [ - { - "items": { - "type": "string" - }, - "type": "array" - }, - { - "type": "null" - } - ], - "title": "Group Names" - }, "operation_id": { "const": "filter.groups", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "query": { "additionalProperties": false, + "description": "Filters and control values for filter.groups. List named filter-rule groups. Use only the named fields below.", "properties": { + "group_names": { + "anyOf": [ + { + "items": { + "type": "string" + }, + "type": "array" + }, + { + "type": "null" + } + ], + "description": "Exact rule-group names to return. Repeat group_names in the query string; omit it to list every group.", + "title": "Group Names" + }, "include_usage": { "default": true, + "description": "Include the subscriptions or defaults that reference each rule group.", "title": "Include Usage", "type": "boolean" } @@ -3122,14 +5866,66 @@ }, { "additionalProperties": false, - "description": "查询本地是否存在(数据库) Method: GET. Path: /api/v1/mediaserver/exists. Effect: safe_read.", + "description": "Test one title and optional subtitle against an exact named filter-rule group. Method: GET. Path: /api/v1/system/ruletest. Effect: external_side_effect.", "properties": { "operation_id": { - "const": "library.exists", + "const": "filter.test", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "query": { "additionalProperties": false, + "description": "Filters and control values for filter.test. Test one title and optional subtitle against an exact named filter-rule group. Use only the named fields below.", + "properties": { + "rulegroup_name": { + "description": "Exact filter-rule group name returned by filter.groups.", + "title": "Rulegroup Name", + "type": "string" + }, + "subtitle": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional subtitle text used together with title during media recognition.", + "title": "Subtitle" + }, + "title": { + "description": "Media, torrent, subscription, or history title used by the operation.", + "title": "Title", + "type": "string" + } + }, + "required": [ + "title", + "rulegroup_name" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "query" + ], + "title": "filter.test", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Check configured media servers for one canonical media identity. Method: GET. Path: /api/v1/mediaserver/exists. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "library.exists", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for library.exists. Check configured media servers for one canonical media identity. Use only the named fields below.", "properties": { "media_id": { "anyOf": [ @@ -3140,6 +5936,7 @@ "type": "null" } ], + "description": "Source-native media ID. Always pair it with the exact media_source returned by search.", "title": "Media Id" }, "media_source": { @@ -3151,6 +5948,7 @@ "type": "null" } ], + "description": "Metadata source identifier. Preserve the exact value returned with media_id.", "title": "Media Source" }, "mtype": { @@ -3162,6 +5960,7 @@ "type": "null" } ], + "description": "MoviePilot media type or subscription-history category required by the operation.", "title": "Mtype" }, "season": { @@ -3173,6 +5972,7 @@ "type": "null" } ], + "description": "Season number used by the media, search, subscription, or transfer operation.", "title": "Season" }, "title": { @@ -3184,6 +5984,7 @@ "type": "null" } ], + "description": "Media, torrent, subscription, or history title used by the operation.", "title": "Title" }, "year": { @@ -3195,6 +5996,7 @@ "type": "null" } ], + "description": "Release or premiere year used to disambiguate the media title.", "title": "Year" } }, @@ -3209,16 +6011,117 @@ }, { "additionalProperties": false, - "description": "查询媒体详情 Method: GET. Path: /api/v1/media/{media_id}. Effect: safe_read.", + "description": "List recently added items from one configured media-server instance for the current user. Method: GET. Path: /api/v1/mediaserver/latest. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "library.latest", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for library.latest. List recently added items from one configured media-server instance for the current user. Use only the named fields below.", + "properties": { + "count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 20, + "description": "Maximum number of records to return on the requested page.", + "title": "Count" + }, + "server": { + "description": "Exact configured media-server instance name returned by the media-server instance list.", + "title": "Server", + "type": "string" + } + }, + "required": [ + "server" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "query" + ], + "title": "library.latest", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read the resolved automatic media-category mapping. Method: GET. Path: /api/v1/media/category. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "media.categories", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "media.categories", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read the complete automatic media-category strategy configuration. Method: GET. Path: /api/v1/media/category/config. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "media.category.config.get", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "media.category.config.get", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Replace the complete automatic media-category strategy configuration. Method: POST. Path: /api/v1/media/category/config. Effect: reversible_write.", + "properties": { + "body": { + "$ref": "#/$defs/CategoryConfig", + "description": "Request value for media.category.config.update. Replace the complete automatic media-category strategy configuration. Use the exact type and fields below." + }, + "operation_id": { + "const": "media.category.config.update", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id", + "body" + ], + "title": "media.category.config.update", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read canonical media details from one selected metadata source. Method: GET. Path: /api/v1/media/{media_id}. Effect: safe_read.", "properties": { "operation_id": { "const": "media.detail", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for media.detail. Read canonical media details from one selected metadata source. Use only the named fields below.", "properties": { "media_id": { + "description": "Source-native media ID. Always pair it with the exact media_source returned by search.", "title": "Media Id", "type": "string" } @@ -3230,11 +6133,14 @@ }, "query": { "additionalProperties": false, + "description": "Filters and control values for media.detail. Read canonical media details from one selected metadata source. Use only the named fields below.", "properties": { "media_source": { - "$ref": "#/$defs/MediaSource" + "$ref": "#/$defs/MediaSource", + "description": "Metadata source identifier. Preserve the exact value returned with media_id." }, "type_name": { + "description": "Explicit media type name used when source IDs alone are ambiguous.", "title": "Type Name", "type": "string" } @@ -3256,20 +6162,88 @@ }, { "additionalProperties": false, - "description": "TMDB季所有集 Method: GET. Path: /api/v1/tmdb/{tmdbid}/{season}. Effect: safe_read.", + "description": "List seasons defined by one exact TMDB episode-group identity. Method: GET. Path: /api/v1/media/group/seasons/{episode_group}. Effect: safe_read.", "properties": { "operation_id": { - "const": "media.episode_schedule", + "const": "media.episode_group.seasons", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for media.episode_group.seasons. List seasons defined by one exact TMDB episode-group identity. Use only the named fields below.", + "properties": { + "episode_group": { + "description": "TMDB episode-group identifier used for alternate episode ordering.", + "title": "Episode Group", + "type": "string" + } + }, + "required": [ + "episode_group" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "media.episode_group.seasons", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List alternate TMDB episode groups available for one TV media identity. Method: GET. Path: /api/v1/media/groups/{tmdbid}. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "media.episode_groups", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for media.episode_groups. List alternate TMDB episode groups available for one TV media identity. Use only the named fields below.", + "properties": { + "tmdbid": { + "description": "TMDB media ID returned by media search or detail.", + "title": "Tmdbid", + "type": "integer" + } + }, + "required": [ + "tmdbid" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "media.episode_groups", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read TMDB episode release information for one season. Method: GET. Path: /api/v1/tmdb/{tmdbid}/{season}. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "media.episode_schedule", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for media.episode_schedule. Read TMDB episode release information for one season. Use only the named fields below.", "properties": { "season": { + "description": "Season number used by the media, search, subscription, or transfer operation.", "title": "Season", "type": "integer" }, "tmdbid": { + "description": "TMDB media ID returned by media search or detail.", "title": "Tmdbid", "type": "integer" } @@ -3282,6 +6256,7 @@ }, "query": { "additionalProperties": false, + "description": "Filters and control values for media.episode_schedule. Read TMDB episode release information for one season. Use only the named fields below.", "properties": { "episode_group": { "anyOf": [ @@ -3292,6 +6267,7 @@ "type": "null" } ], + "description": "TMDB episode-group identifier used for alternate episode ordering.", "title": "Episode Group" } }, @@ -3307,21 +6283,23 @@ }, { "additionalProperties": false, - "description": "读取人物作品 Method: GET. Path: /api/v1/{source}/person/credits/{person_id}. Effect: safe_read.", + "description": "Read one person's credits from the selected metadata source. Method: GET. Path: /api/v1/{source}/person/credits/{person_id}. Effect: safe_read.", "properties": { "operation_id": { "const": "media.person.credits", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for media.person.credits. Read one person's credits from the selected metadata source. Use only the named fields below.", "properties": { "person_id": { - "description": "来源原生人物 ID。", + "description": "Source-native person ID.", "type": "integer" }, "source": { - "description": "人物数据来源。", + "description": "Metadata source that owns the person ID.", "enum": [ "douban", "tmdb", @@ -3339,16 +6317,18 @@ }, "query": { "additionalProperties": false, + "description": "Filters and control values for media.person.credits. Read one person's credits from the selected metadata source. Use only the named fields below.", "properties": { "count": { "default": 20, - "description": "Bangumi 与 AniList 支持的每页条数;其他来源忽略。", + "description": "Page size used by Bangumi and AniList; other sources ignore it.", "maximum": 50, "minimum": 1, "type": "integer" }, "page": { "default": 1, + "description": "One-based result page number.", "minimum": 1, "type": "integer" } @@ -3365,22 +6345,26 @@ }, { "additionalProperties": false, - "description": "搜索媒体/人物信息 Method: GET. Path: /api/v1/media/search. Effect: safe_read.", + "description": "Search people across selected metadata sources. Method: GET. Path: /api/v1/media/search. Effect: safe_read.", "properties": { "operation_id": { "const": "media.person.search", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "query": { "additionalProperties": false, + "description": "Filters and control values for media.person.search. Search people across selected metadata sources. Use only the named fields below.", "properties": { "count": { "default": 8, + "description": "Maximum number of records to return on the requested page.", "title": "Count", "type": "integer" }, "media_source": { "default": [], + "description": "Metadata source identifier. Preserve the exact value returned with media_id.", "items": { "$ref": "#/$defs/MediaSource" }, @@ -3389,16 +6373,18 @@ }, "page": { "default": 1, + "description": "One-based result page number.", "title": "Page", "type": "integer" }, "title": { + "description": "Media, torrent, subscription, or history title used by the operation.", "title": "Title", "type": "string" }, "type": { "const": "person", - "description": "人物搜索固定传 person。", + "description": "Literal person, selecting person search instead of media search.", "type": "string" } }, @@ -3418,14 +6404,16 @@ }, { "additionalProperties": false, - "description": "识别媒体信息(种子) Method: GET. Path: /api/v1/media/recognize. Effect: safe_read.", + "description": "Recognize media identity from a title, subtitle, or custom rule context. Method: GET. Path: /api/v1/media/recognize. Effect: safe_read.", "properties": { "operation_id": { "const": "media.recognize", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "query": { "additionalProperties": false, + "description": "Filters and control values for media.recognize. Recognize media identity from a title, subtitle, or custom rule context. Use only the named fields below.", "properties": { "custom_words": { "anyOf": [ @@ -3436,6 +6424,7 @@ "type": "null" } ], + "description": "Custom recognition or rename words applied to this media workflow.", "title": "Custom Words" }, "media_source": { @@ -3447,6 +6436,7 @@ "type": "null" } ], + "description": "Metadata source identifier. Preserve the exact value returned with media_id.", "title": "Media Source" }, "subtitle": { @@ -3458,9 +6448,11 @@ "type": "null" } ], + "description": "Optional subtitle text used together with title during media recognition.", "title": "Subtitle" }, "title": { + "description": "Media, torrent, subscription, or history title used by the operation.", "title": "Title", "type": "string" } @@ -3480,17 +6472,64 @@ }, { "additionalProperties": false, - "description": "刮削媒体信息 Method: POST. Path: /api/v1/media/scrape/{storage}. Effect: external_side_effect.", + "description": "Recognize canonical media identity from one exact filename and optional path context. Method: GET. Path: /api/v1/media/recognize_file. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "media.recognize_file", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for media.recognize_file. Recognize canonical media identity from one exact filename and optional path context. Use only the named fields below.", + "properties": { + "media_source": { + "anyOf": [ + { + "$ref": "#/$defs/MediaSource" + }, + { + "type": "null" + } + ], + "description": "Metadata source identifier. Preserve the exact value returned with media_id.", + "title": "Media Source" + }, + "path": { + "description": "Storage or history path represented by this record.", + "title": "Path", + "type": "string" + } + }, + "required": [ + "path" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "query" + ], + "title": "media.recognize_file", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Generate or refresh metadata for one storage item. Method: POST. Path: /api/v1/media/scrape/{storage}. Effect: external_side_effect.", "properties": { "body": { - "$ref": "#/$defs/FileItem-Input" + "$ref": "#/$defs/FileItem-Input", + "description": "Request value for media.scrape. Generate or refresh metadata for one storage item. Use the exact type and fields below." }, "operation_id": { "const": "media.scrape", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for media.scrape. Generate or refresh metadata for one storage item. Use only the named fields below.", "properties": { "storage": { "anyOf": [ @@ -3501,6 +6540,7 @@ "type": "null" } ], + "description": "Configured storage name or storage type used by the operation.", "title": "Storage" } }, @@ -3511,6 +6551,7 @@ }, "query": { "additionalProperties": false, + "description": "Filters and control values for media.scrape. Generate or refresh metadata for one storage item. Use only the named fields below.", "properties": { "media_id": { "anyOf": [ @@ -3521,6 +6562,7 @@ "type": "null" } ], + "description": "Source-native media ID. Always pair it with the exact media_source returned by search.", "title": "Media Id" }, "media_source": { @@ -3532,6 +6574,7 @@ "type": "null" } ], + "description": "Metadata source identifier. Preserve the exact value returned with media_id.", "title": "Media Source" }, "music_type": { @@ -3543,6 +6586,7 @@ "type": "null" } ], + "description": "Music identity level: recording, album, or artist where supported.", "title": "Music Type" }, "type_name": { @@ -3554,6 +6598,7 @@ "type": "null" } ], + "description": "Explicit media type name used when source IDs alone are ambiguous.", "title": "Type Name" } }, @@ -3570,22 +6615,26 @@ }, { "additionalProperties": false, - "description": "搜索媒体/人物信息 Method: GET. Path: /api/v1/media/search. Effect: safe_read.", + "description": "Search canonical media across selected metadata sources. Method: GET. Path: /api/v1/media/search. Effect: safe_read.", "properties": { "operation_id": { "const": "media.search", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "query": { "additionalProperties": false, + "description": "Filters and control values for media.search. Search canonical media across selected metadata sources. Use only the named fields below.", "properties": { "count": { "default": 8, + "description": "Maximum number of records to return on the requested page.", "title": "Count", "type": "integer" }, "media_source": { "default": [], + "description": "Metadata source identifier. Preserve the exact value returned with media_id.", "items": { "$ref": "#/$defs/MediaSource" }, @@ -3594,10 +6643,12 @@ }, "page": { "default": 1, + "description": "One-based result page number.", "title": "Page", "type": "integer" }, "title": { + "description": "Media, torrent, subscription, or history title used by the operation.", "title": "Title", "type": "string" }, @@ -3611,6 +6662,7 @@ } ], "default": "media", + "description": "MoviePilot media or storage item type required by the selected operation.", "title": "Type" } }, @@ -3629,14 +6681,568 @@ }, { "additionalProperties": false, - "description": "查询插件运行能力 Method: GET. Path: /api/v1/plugin/runtime/capabilities. Effect: safe_read.", + "description": "List seasons for one exact media identity or a title-and-year fallback. Method: GET. Path: /api/v1/media/seasons. Effect: safe_read.", "properties": { "operation_id": { - "const": "plugin.capabilities", + "const": "media.seasons", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "query": { "additionalProperties": false, + "description": "Filters and control values for media.seasons. List seasons for one exact media identity or a title-and-year fallback. Use only the named fields below.", + "properties": { + "media_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Source-native media ID. Always pair it with the exact media_source returned by search.", + "title": "Media Id" + }, + "media_source": { + "anyOf": [ + { + "$ref": "#/$defs/MediaSource" + }, + { + "type": "null" + } + ], + "description": "Metadata source identifier. Preserve the exact value returned with media_id.", + "title": "Media Source" + }, + "season": { + "description": "Season number used by the media, search, subscription, or transfer operation.", + "title": "Season", + "type": "integer" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Media, torrent, subscription, or history title used by the operation.", + "title": "Title" + }, + "year": { + "description": "Release or premiere year used to disambiguate the media title.", + "title": "Year", + "type": "string" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id" + ], + "title": "media.seasons", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List metadata sources currently registered for MoviePilot media operations. Method: GET. Path: /api/v1/media/source. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "media.sources", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "media.sources", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read one album's details, tracks, releases, and aligned artist names and IDs. Method: GET. Path: /api/v1/music/album/{album_id}. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "music.album.get", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for music.album.get. Read one album's details, tracks, releases, and aligned artist names and IDs. Use only the named fields below.", + "properties": { + "album_id": { + "description": "Source-native album ID returned by music search, exploration, or artist-album browsing.", + "title": "Album Id", + "type": "string" + } + }, + "required": [ + "album_id" + ], + "type": "object" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for music.album.get. Read one album's details, tracks, releases, and aligned artist names and IDs. Use only the named fields below.", + "properties": { + "media_source": { + "$ref": "#/$defs/MediaSource", + "description": "Metadata source identifier. Preserve the exact value returned with media_id." + } + }, + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "music.album.get", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Browse albums related to one source-native album identity. Method: GET. Path: /api/v1/music/album/{album_id}/related. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "music.album.related", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for music.album.related. Browse albums related to one source-native album identity. Use only the named fields below.", + "properties": { + "album_id": { + "description": "Source-native album ID returned by music search, exploration, or artist-album browsing.", + "title": "Album Id", + "type": "string" + } + }, + "required": [ + "album_id" + ], + "type": "object" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for music.album.related. Browse albums related to one source-native album identity. Use only the named fields below.", + "properties": { + "count": { + "default": 24, + "description": "Maximum number of records to return on the requested page.", + "maximum": 100, + "minimum": 1, + "title": "Count", + "type": "integer" + }, + "media_source": { + "$ref": "#/$defs/MediaSource", + "description": "Metadata source identifier. Preserve the exact value returned with media_id." + } + }, + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "music.album.related", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Browse one artist's albums, singles, EPs, or another exact release-group type. Method: GET. Path: /api/v1/music/artist/{artist_id}/albums. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "music.artist.albums", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for music.artist.albums. Browse one artist's albums, singles, EPs, or another exact release-group type. Use only the named fields below.", + "properties": { + "artist_id": { + "description": "Source-native artist ID returned by music search or an album detail response.", + "title": "Artist Id", + "type": "string" + } + }, + "required": [ + "artist_id" + ], + "type": "object" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for music.artist.albums. Browse one artist's albums, singles, EPs, or another exact release-group type. Use only the named fields below.", + "properties": { + "album_type": { + "anyOf": [ + { + "pattern": "^(album|single|ep|broadcast|other|compilation|soundtrack|live|remix)$", + "type": "string" + }, + { + "type": "null" + } + ], + "description": "MusicBrainz release-group type filter: album, single, ep, broadcast, other, compilation, soundtrack, live, or remix.", + "title": "Album Type" + }, + "count": { + "default": 30, + "description": "Maximum number of records to return on the requested page.", + "maximum": 100, + "minimum": 1, + "title": "Count", + "type": "integer" + }, + "media_source": { + "$ref": "#/$defs/MediaSource", + "description": "Metadata source identifier. Preserve the exact value returned with media_id." + }, + "page": { + "default": 1, + "description": "One-based result page number.", + "minimum": 1, + "title": "Page", + "type": "integer" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "music.artist.albums", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read one artist's canonical details from the selected music metadata source. Method: GET. Path: /api/v1/music/artist/{artist_id}. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "music.artist.get", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for music.artist.get. Read one artist's canonical details from the selected music metadata source. Use only the named fields below.", + "properties": { + "artist_id": { + "description": "Source-native artist ID returned by music search or an album detail response.", + "title": "Artist Id", + "type": "string" + } + }, + "required": [ + "artist_id" + ], + "type": "object" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for music.artist.get. Read one artist's canonical details from the selected music metadata source. Use only the named fields below.", + "properties": { + "media_source": { + "$ref": "#/$defs/MediaSource", + "description": "Metadata source identifier. Preserve the exact value returned with media_id." + } + }, + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "music.artist.get", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Browse artists related to one source-native artist identity. Method: GET. Path: /api/v1/music/artist/{artist_id}/related. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "music.artist.related", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for music.artist.related. Browse artists related to one source-native artist identity. Use only the named fields below.", + "properties": { + "artist_id": { + "description": "Source-native artist ID returned by music search or an album detail response.", + "title": "Artist Id", + "type": "string" + } + }, + "required": [ + "artist_id" + ], + "type": "object" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for music.artist.related. Browse artists related to one source-native artist identity. Use only the named fields below.", + "properties": { + "count": { + "default": 24, + "description": "Maximum number of records to return on the requested page.", + "maximum": 100, + "minimum": 1, + "title": "Count", + "type": "integer" + }, + "media_source": { + "$ref": "#/$defs/MediaSource", + "description": "Metadata source identifier. Preserve the exact value returned with media_id." + } + }, + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "music.artist.related", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Clear the complete administrator-only MusicBrainz recognition cache. Method: DELETE. Path: /api/v1/music/cache. Effect: destructive_write.", + "properties": { + "operation_id": { + "const": "music.cache.clear", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "music.cache.clear", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Delete one administrator-only MusicBrainz recognition-cache entry by exact key. Method: DELETE. Path: /api/v1/music/cache/{cache_key}. Effect: destructive_write.", + "properties": { + "operation_id": { + "const": "music.cache.delete", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "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.", + "title": "Cache Key", + "type": "string" + } + }, + "required": [ + "cache_key" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "music.cache.delete", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Inspect the administrator-only MusicBrainz recognition cache and summary counts. Method: GET. Path: /api/v1/music/cache. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "music.cache.get", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "music.cache.get", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Browse MusicBrainz charts or fresh releases, or Douban Music tag categories. Method: GET. Path: /api/v1/music/explore. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "music.explore", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for music.explore. Browse MusicBrainz charts or fresh releases, or Douban Music tag categories. Use only the named fields below.", + "properties": { + "count": { + "default": 30, + "description": "Maximum number of records to return on the requested page.", + "maximum": 100, + "minimum": 1, + "title": "Count", + "type": "integer" + }, + "days": { + "default": 14, + "description": "Fresh-release lookback/lookahead window, from 1 through the endpoint maximum.", + "maximum": 90, + "minimum": 1, + "title": "Days", + "type": "integer" + }, + "douban_sort": { + "default": "U", + "description": "Douban Music order: U comprehensive, S rating, R newest, or O hottest.", + "pattern": "^(U|S|R|O)$", + "title": "Douban Sort", + "type": "string" + }, + "entity": { + "default": "recording", + "description": "Chart entity: recording for tracks or album for release groups. Fresh results are albums.", + "pattern": "^(recording|album)$", + "title": "Entity", + "type": "string" + }, + "future": { + "default": true, + "description": "Include releases after today in fresh mode.", + "title": "Future", + "type": "boolean" + }, + "media_source": { + "$ref": "#/$defs/MediaSource", + "description": "Music exploration source. Use musicbrainz for chart/fresh modes or doubanmusic for tag browsing." + }, + "min_listen_count": { + "default": 0, + "description": "Minimum ListenBrainz listen count in chart mode.", + "minimum": 0, + "title": "Min Listen Count", + "type": "integer" + }, + "mode": { + "default": "chart", + "description": "MusicBrainz mode: chart reads listening charts; fresh reads new album releases.", + "pattern": "^(chart|fresh)$", + "title": "Mode", + "type": "string" + }, + "page": { + "default": 1, + "description": "One-based result page number.", + "minimum": 1, + "title": "Page", + "type": "integer" + }, + "past": { + "default": true, + "description": "Include releases before today in fresh mode.", + "title": "Past", + "type": "boolean" + }, + "range_name": { + "default": "this_month", + "description": "ListenBrainz chart range: this_week, this_month, this_year, week, month, or year.", + "pattern": "^(this_week|this_month|this_year|week|month|quarter|half_yearly|year|all_time)$", + "title": "Range Name", + "type": "string" + }, + "sort": { + "default": "release_date", + "description": "Fresh-release order accepted by the current ListenBrainz implementation.", + "pattern": "^(release_date|artist_credit_name|release_name)$", + "title": "Sort", + "type": "string" + }, + "sort_by": { + "default": "listen_count.desc", + "description": "ListenBrainz chart order: listen_count.desc or listen_count.asc.", + "pattern": "^listen_count\\.(desc|asc)$", + "title": "Sort By", + "type": "string" + }, + "tags": { + "default": "", + "description": "Comma-separated Douban Music tags used only when media_source is doubanmusic.", + "title": "Tags", + "type": "string" + }, + "with_cover": { + "default": false, + "description": "Keep only results with cover artwork when true.", + "title": "With Cover", + "type": "boolean" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id" + ], + "title": "music.explore", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Resolve one recording or album from an exact music source and source-native ID. Method: POST. Path: /api/v1/music/recognize. Effect: safe_read.", + "properties": { + "body": { + "$ref": "#/$defs/MusicRecognizeRequest", + "description": "Request value for music.recognize. Resolve one recording or album from an exact music source and source-native ID. Use the exact type and fields below." + }, + "operation_id": { + "const": "music.recognize", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id", + "body" + ], + "title": "music.recognize", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Inspect the runtime capabilities exposed by installed plugins. Method: GET. Path: /api/v1/plugin/runtime/capabilities. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "plugin.capabilities", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for plugin.capabilities. Inspect the runtime capabilities exposed by installed plugins. Use only the named fields below.", "properties": { "plugin_id": { "anyOf": [ @@ -3647,6 +7253,7 @@ "type": "null" } ], + "description": "Exact installed or marketplace plugin ID.", "title": "Plugin Id" } }, @@ -3661,16 +7268,56 @@ }, { "additionalProperties": false, - "description": "获取插件配置 Method: GET. Path: /api/v1/plugin/{plugin_id}. Effect: safe_read.", + "description": "Create a configurable clone of one installed plugin. Method: POST. Path: /api/v1/plugin/clone/{plugin_id}. Effect: external_side_effect.", "properties": { + "body": { + "$ref": "#/$defs/PluginCloneRequest", + "description": "Request value for plugin.clone. Create a configurable clone of one installed plugin. Use the exact type and fields below." + }, "operation_id": { - "const": "plugin.config.get", + "const": "plugin.clone", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for plugin.clone. Create a configurable clone of one installed plugin. Use only the named fields below.", "properties": { "plugin_id": { + "description": "Exact installed or marketplace plugin ID.", + "title": "Plugin Id", + "type": "string" + } + }, + "required": [ + "plugin_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params", + "body" + ], + "title": "plugin.clone", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read one loaded plugin's configuration form and its defaults merged with saved values. Method: GET. Path: /api/v1/plugin/form/{plugin_id}. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "plugin.config.get", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for plugin.config.get. Read one loaded plugin's configuration form and its defaults merged with saved values. Use only the named fields below.", + "properties": { + "plugin_id": { + "description": "Exact installed or marketplace plugin ID.", "title": "Plugin Id", "type": "string" } @@ -3690,21 +7337,26 @@ }, { "additionalProperties": false, - "description": "更新插件配置 Method: PUT. Path: /api/v1/plugin/{plugin_id}. Effect: reversible_write.", + "description": "Replace one installed plugin's complete configuration and apply it immediately. Method: PUT. Path: /api/v1/plugin/{plugin_id}. Effect: reversible_write.", "properties": { "body": { "additionalProperties": true, + "description": "Complete plugin configuration object. First call plugin.config.get, copy its returned model, change only the intended keys, and submit the full resulting object. Omit a key only when it must be removed. A successful update reinitializes the plugin and refreshes commands, jobs, and routes.", + "minProperties": 1, "title": "Conf", "type": "object" }, "operation_id": { "const": "plugin.config.update", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for plugin.config.update. Replace one installed plugin's complete configuration and apply it immediately. Use only the named fields below.", "properties": { "plugin_id": { + "description": "Exact installed or marketplace plugin ID.", "title": "Plugin Id", "type": "string" } @@ -3725,16 +7377,19 @@ }, { "additionalProperties": false, - "description": "查询插件持久化数据 Method: GET. Path: /api/v1/plugin/runtime/{plugin_id}/data. Effect: safe_read.", + "description": "Read a bounded preview of one plugin's persisted data. Method: GET. Path: /api/v1/plugin/runtime/{plugin_id}/data. Effect: safe_read.", "properties": { "operation_id": { "const": "plugin.data", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for plugin.data. Read a bounded preview of one plugin's persisted data. Use only the named fields below.", "properties": { "plugin_id": { + "description": "Exact installed or marketplace plugin ID.", "title": "Plugin Id", "type": "string" } @@ -3746,6 +7401,7 @@ }, "query": { "additionalProperties": false, + "description": "Filters and control values for plugin.data. Read a bounded preview of one plugin's persisted data. Use only the named fields below.", "properties": { "key": { "anyOf": [ @@ -3756,6 +7412,7 @@ "type": "null" } ], + "description": "Optional exact plugin data key used to narrow the returned preview.", "title": "Key" }, "max_chars": { @@ -3767,6 +7424,7 @@ "type": "null" } ], + "description": "Maximum number of serialized plugin-data characters to return.", "title": "Max Chars" } }, @@ -3782,16 +7440,161 @@ }, { "additionalProperties": false, - "description": "安装插件 Method: GET. Path: /api/v1/plugin/install/{plugin_id}. Effect: external_side_effect.", + "description": "Create one named plugin folder. Method: POST. Path: /api/v1/plugin/folders/{folder_name}. Effect: reversible_write.", "properties": { "operation_id": { - "const": "plugin.install", + "const": "plugin.folder.create", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for plugin.folder.create. Create one named plugin folder. Use only the named fields below.", + "properties": { + "folder_name": { + "description": "Exact plugin folder name returned by plugin.folders.get.", + "title": "Folder Name", + "type": "string" + } + }, + "required": [ + "folder_name" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "plugin.folder.create", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Delete one named plugin folder without uninstalling its plugins. Method: DELETE. Path: /api/v1/plugin/folders/{folder_name}. Effect: destructive_write.", + "properties": { + "operation_id": { + "const": "plugin.folder.delete", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for plugin.folder.delete. Delete one named plugin folder without uninstalling its plugins. Use only the named fields below.", + "properties": { + "folder_name": { + "description": "Exact plugin folder name returned by plugin.folders.get.", + "title": "Folder Name", + "type": "string" + } + }, + "required": [ + "folder_name" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "plugin.folder.delete", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Replace the ordered plugin IDs assigned to one named plugin folder. Method: PUT. Path: /api/v1/plugin/folders/{folder_name}/plugins. Effect: reversible_write.", + "properties": { + "body": { + "description": "Request value for plugin.folder.plugins.update. Replace the ordered plugin IDs assigned to one named plugin folder. Use the exact type and fields below.", + "items": { + "type": "string" + }, + "title": "Plugin Ids", + "type": "array" + }, + "operation_id": { + "const": "plugin.folder.plugins.update", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for plugin.folder.plugins.update. Replace the ordered plugin IDs assigned to one named plugin folder. Use only the named fields below.", + "properties": { + "folder_name": { + "description": "Exact plugin folder name returned by plugin.folders.get.", + "title": "Folder Name", + "type": "string" + } + }, + "required": [ + "folder_name" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params", + "body" + ], + "title": "plugin.folder.plugins.update", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read the complete administrator plugin-folder grouping configuration. Method: GET. Path: /api/v1/plugin/folders. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "plugin.folders.get", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "plugin.folders.get", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Replace the complete administrator plugin-folder grouping configuration. Method: POST. Path: /api/v1/plugin/folders. Effect: reversible_write.", + "properties": { + "body": { + "$ref": "#/$defs/PluginFoldersData", + "description": "Request value for plugin.folders.update. Replace the complete administrator plugin-folder grouping configuration. Use the exact type and fields below." + }, + "operation_id": { + "const": "plugin.folders.update", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id", + "body" + ], + "title": "plugin.folders.update", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read marketplace update notes and history for one plugin. Method: GET. Path: /api/v1/plugin/history/{plugin_id}. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "plugin.history", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for plugin.history. Read marketplace update notes and history for one plugin. Use only the named fields below.", "properties": { "plugin_id": { + "description": "Exact installed or marketplace plugin ID.", "title": "Plugin Id", "type": "string" } @@ -3803,6 +7606,52 @@ }, "query": { "additionalProperties": false, + "description": "Filters and control values for plugin.history. Read marketplace update notes and history for one plugin. Use only the named fields below.", + "properties": { + "force": { + "default": true, + "description": "Force a marketplace refresh or plugin installation when true.", + "title": "Force", + "type": "boolean" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "plugin.history", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Install or update one plugin from an approved source. Method: GET. Path: /api/v1/plugin/install/{plugin_id}. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "plugin.install", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for plugin.install. Install or update one plugin from an approved source. Use only the named fields below.", + "properties": { + "plugin_id": { + "description": "Exact installed or marketplace plugin ID.", + "title": "Plugin Id", + "type": "string" + } + }, + "required": [ + "plugin_id" + ], + "type": "object" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for plugin.install. Install or update one plugin from an approved source. Use only the named fields below.", "properties": { "force": { "anyOf": [ @@ -3814,6 +7663,7 @@ } ], "default": false, + "description": "Force a marketplace refresh or plugin installation when true.", "title": "Force" }, "release_version": { @@ -3825,6 +7675,7 @@ "type": "null" } ], + "description": "Exact plugin release version to install when one is required.", "title": "Release Version" }, "repo_url": { @@ -3837,6 +7688,7 @@ } ], "default": "", + "description": "Approved plugin repository URL used to resolve the installation source.", "title": "Repo Url" } }, @@ -3852,36 +7704,32 @@ }, { "additionalProperties": false, - "description": "已安装插件 Method: GET. Path: /api/v1/plugin/installed. Effect: safe_read.", + "description": "List installed plugins and their runtime status. Method: GET. Path: /api/v1/plugin/. Effect: safe_read.", "properties": { "operation_id": { "const": "plugin.installed", - "type": "string" - } - }, - "required": [ - "operation_id" - ], - "title": "plugin.installed", - "type": "object" - }, - { - "additionalProperties": false, - "description": "所有插件 Method: GET. Path: /api/v1/plugin/. Effect: safe_read.", - "properties": { - "operation_id": { - "const": "plugin.market", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "query": { "additionalProperties": false, + "description": "Filters and control values for plugin.installed. List installed plugins and their runtime status. Use only the named fields below.", "properties": { "force": { "default": false, + "description": "Force a marketplace refresh or plugin installation when true.", "title": "Force", "type": "boolean" }, - "state": { + "max_results": { + "default": 50, + "description": "Maximum number of plugin catalog results to return, from 1 to 200.", + "maximum": 200, + "minimum": 1, + "title": "Max Results", + "type": "integer" + }, + "query": { "anyOf": [ { "type": "string" @@ -3890,8 +7738,207 @@ "type": "null" } ], - "default": "all", - "title": "State" + "description": "Optional case-insensitive keyword matched against plugin ID, name, description, and author.", + "title": "Query" + }, + "state": { + "const": "installed", + "description": "Literal installed, selecting only installed plugin catalog entries.", + "type": "string" + } + }, + "required": [ + "state" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "query" + ], + "title": "plugin.installed", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List plugins available from configured marketplaces. Method: GET. Path: /api/v1/plugin/. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "plugin.market", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for plugin.market. List plugins available from configured marketplaces. Use only the named fields below.", + "properties": { + "force": { + "default": false, + "description": "Force a marketplace refresh or plugin installation when true.", + "title": "Force", + "type": "boolean" + }, + "max_results": { + "default": 50, + "description": "Maximum number of plugin catalog results to return, from 1 to 200.", + "maximum": 200, + "minimum": 1, + "title": "Max Results", + "type": "integer" + }, + "query": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Optional case-insensitive keyword matched against plugin ID, name, description, and author.", + "title": "Query" + }, + "state": { + "const": "market", + "description": "Literal market, selecting only market plugin catalog entries.", + "type": "string" + } + }, + "required": [ + "state" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "query" + ], + "title": "plugin.market", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Refresh the configured plugin marketplace repositories from the MoviePilot Wiki. Method: POST. Path: /api/v1/system/setting/PLUGIN_MARKET/sync-wiki. Effect: external_side_effect.", + "properties": { + "body": { + "anyOf": [ + { + "$ref": "#/$defs/PluginMarketSyncRequest" + }, + { + "type": "null" + } + ], + "description": "Request value for plugin.market.sync_wiki. Refresh the configured plugin marketplace repositories from the MoviePilot Wiki. Use the exact type and fields below.", + "title": "Request" + }, + "operation_id": { + "const": "plugin.market.sync_wiki", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "plugin.market.sync_wiki", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read the current aggregate rating for one plugin. Method: GET. Path: /api/v1/plugin/rating/{plugin_id}. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "plugin.rating", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for plugin.rating. Read the current aggregate rating for one plugin. Use only the named fields below.", + "properties": { + "plugin_id": { + "description": "Exact installed or marketplace plugin ID.", + "title": "Plugin Id", + "type": "string" + } + }, + "required": [ + "plugin_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "plugin.rating", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Submit or replace the current user's rating for one plugin. Method: POST. Path: /api/v1/plugin/rating/{plugin_id}. Effect: external_side_effect.", + "properties": { + "body": { + "$ref": "#/$defs/PluginRatingRequest", + "description": "Request value for plugin.rating.submit. Submit or replace the current user's rating for one plugin. Use the exact type and fields below." + }, + "operation_id": { + "const": "plugin.rating.submit", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for plugin.rating.submit. Submit or replace the current user's rating for one plugin. Use only the named fields below.", + "properties": { + "plugin_id": { + "description": "Exact installed or marketplace plugin ID.", + "title": "Plugin Id", + "type": "string" + } + }, + "required": [ + "plugin_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params", + "body" + ], + "title": "plugin.rating.submit", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read aggregate ratings for a requested plugin set. Method: GET. Path: /api/v1/plugin/rating. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "plugin.ratings", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for plugin.ratings. Read aggregate ratings for a requested plugin set. Use only the named fields below.", + "properties": { + "plugin_ids": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Exact plugin IDs whose aggregate ratings should be returned.", + "title": "Plugin Ids" } }, "type": "object" @@ -3900,21 +7947,82 @@ "required": [ "operation_id" ], - "title": "plugin.market", + "title": "plugin.ratings", "type": "object" }, { "additionalProperties": false, - "description": "重新加载插件 Method: GET. Path: /api/v1/plugin/reload/{plugin_id}. Effect: external_side_effect.", + "description": "List available release versions for one plugin source. Method: GET. Path: /api/v1/plugin/releases/{plugin_id}. Effect: safe_read.", "properties": { "operation_id": { - "const": "plugin.reload", + "const": "plugin.releases", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for plugin.releases. List available release versions for one plugin source. Use only the named fields below.", "properties": { "plugin_id": { + "description": "Exact installed or marketplace plugin ID.", + "title": "Plugin Id", + "type": "string" + } + }, + "required": [ + "plugin_id" + ], + "type": "object" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for plugin.releases. List available release versions for one plugin source. Use only the named fields below.", + "properties": { + "force": { + "default": false, + "description": "Force a marketplace refresh or plugin installation when true.", + "title": "Force", + "type": "boolean" + }, + "repo_url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "default": "", + "description": "Approved plugin repository URL used to resolve the installation source.", + "title": "Repo Url" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "plugin.releases", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Reload one installed plugin into the running process. Method: GET. Path: /api/v1/plugin/reload/{plugin_id}. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "plugin.reload", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for plugin.reload. Reload one installed plugin into the running process. Use only the named fields below.", + "properties": { + "plugin_id": { + "description": "Exact installed or marketplace plugin ID.", "title": "Plugin Id", "type": "string" } @@ -3934,16 +8042,189 @@ }, { "additionalProperties": false, - "description": "卸载插件 Method: DELETE. Path: /api/v1/plugin/{plugin_id}. Effect: destructive_write.", + "description": "Delete one plugin's saved configuration and data, then restore its default runtime state. Method: GET. Path: /api/v1/plugin/reset/{plugin_id}. Effect: destructive_write.", "properties": { "operation_id": { - "const": "plugin.uninstall", + "const": "plugin.reset", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for plugin.reset. Delete one plugin's saved configuration and data, then restore its default runtime state. Use only the named fields below.", "properties": { "plugin_id": { + "description": "Exact installed or marketplace plugin ID.", + "title": "Plugin Id", + "type": "string" + } + }, + "required": [ + "plugin_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "plugin.reset", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read plugin runtime convergence, loading, and failure state. Method: GET. Path: /api/v1/plugin/runtime. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "plugin.runtime.status", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "plugin.runtime.status", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Switch an installed plugin to one explicitly selected online source revision. Method: POST. Path: /api/v1/plugin/source/{plugin_id}. Effect: external_side_effect.", + "properties": { + "body": { + "$ref": "#/$defs/PluginSourceChangeRequest", + "description": "Request value for plugin.source.change. Switch an installed plugin to one explicitly selected online source revision. Use the exact type and fields below." + }, + "operation_id": { + "const": "plugin.source.change", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for plugin.source.change. Switch an installed plugin to one explicitly selected online source revision. Use only the named fields below.", + "properties": { + "plugin_id": { + "description": "Exact installed or marketplace plugin ID.", + "title": "Plugin Id", + "type": "string" + } + }, + "required": [ + "plugin_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params", + "body" + ], + "title": "plugin.source.change", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Install an unbound plugin from one explicitly selected online source. Method: POST. Path: /api/v1/plugin/source/{plugin_id}/install. Effect: external_side_effect.", + "properties": { + "body": { + "$ref": "#/$defs/PluginSourceInstallRequest", + "description": "Request value for plugin.source.install. Install an unbound plugin from one explicitly selected online source. Use the exact type and fields below." + }, + "operation_id": { + "const": "plugin.source.install", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for plugin.source.install. Install an unbound plugin from one explicitly selected online source. Use only the named fields below.", + "properties": { + "plugin_id": { + "description": "Exact installed or marketplace plugin ID.", + "title": "Plugin Id", + "type": "string" + } + }, + "required": [ + "plugin_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params", + "body" + ], + "title": "plugin.source.install", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Inspect source candidates and the current immutable source identity before installation or source change. Method: GET. Path: /api/v1/plugin/source/{plugin_id}. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "plugin.source.options", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for plugin.source.options. Inspect source candidates and the current immutable source identity before installation or source change. Use only the named fields below.", + "properties": { + "plugin_id": { + "description": "Exact installed or marketplace plugin ID.", + "title": "Plugin Id", + "type": "string" + } + }, + "required": [ + "plugin_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "plugin.source.options", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read public installation statistics for plugins. Method: GET. Path: /api/v1/plugin/statistic. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "plugin.statistics", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "plugin.statistics", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Uninstall one plugin and remove it from the installed set. Method: DELETE. Path: /api/v1/plugin/{plugin_id}. Effect: destructive_write.", + "properties": { + "operation_id": { + "const": "plugin.uninstall", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for plugin.uninstall. Uninstall one plugin and remove it from the installed set. Use only the named fields below.", + "properties": { + "plugin_id": { + "description": "Exact installed or marketplace plugin ID.", "title": "Plugin Id", "type": "string" } @@ -3963,37 +8244,44 @@ }, { "additionalProperties": false, - "description": "统一获取 Agent 推荐结果 Method: GET. Path: /api/v1/recommend/agent. Effect: safe_read.", + "description": "Read personalized media or music recommendations. Method: GET. Path: /api/v1/recommend/agent. Effect: safe_read.", "properties": { "operation_id": { "const": "recommendation.list", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "query": { "additionalProperties": false, + "description": "Filters and control values for recommendation.list. Read personalized media or music recommendations. Use only the named fields below.", "properties": { "days": { "default": 14, + "description": "Recommendation time window in days.", "title": "Days", "type": "integer" }, "fresh_sort": { "default": "release_date", + "description": "Freshness ordering used by the recommendation source.", "title": "Fresh Sort", "type": "string" }, "future": { "default": true, + "description": "Include future recommendation periods when supported.", "title": "Future", "type": "boolean" }, "media_type": { "default": "all", + "description": "MoviePilot media type used to filter recommendations or rule groups.", "title": "Media Type", "type": "string" }, "min_listen_count": { "default": 0, + "description": "Minimum listen count required for a music recommendation.", "title": "Min Listen Count", "type": "integer" }, @@ -4006,35 +8294,42 @@ "type": "null" } ], + "description": "Music identity level: recording, album, or artist where supported.", "title": "Music Type" }, "page": { "default": 1, + "description": "One-based result page number.", "title": "Page", "type": "integer" }, "past": { "default": true, + "description": "Include past recommendation periods when supported.", "title": "Past", "type": "boolean" }, "range_name": { "default": "this_month", + "description": "Named recommendation time range.", "title": "Range Name", "type": "string" }, "sort_by": { "default": "listen_count.desc", + "description": "Recommendation field used for ordering results.", "title": "Sort By", "type": "string" }, "source": { "default": "tmdb_trending", + "description": "Exact metadata or recommendation source selected by the operation.", "title": "Source", "type": "string" }, "with_cover": { "default": false, + "description": "Require recommendation results to include cover artwork.", "title": "With Cover", "type": "boolean" } @@ -4050,10 +8345,11 @@ }, { "additionalProperties": false, - "description": "后台服务 Method: GET. Path: /api/v1/dashboard/schedule. Effect: safe_read.", + "description": "List registered scheduler jobs and their current state. Method: GET. Path: /api/v1/dashboard/schedule. Effect: safe_read.", "properties": { "operation_id": { "const": "scheduler.list", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" } }, @@ -4065,16 +8361,51 @@ }, { "additionalProperties": false, - "description": "运行服务 Method: GET. Path: /api/v1/system/runscheduler. Effect: external_side_effect.", + "description": "Read current progress for one exact scheduler job. Method: GET. Path: /api/v1/dashboard/schedule/{job_id}/progress. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "scheduler.progress", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for scheduler.progress. Read current progress for one exact scheduler job. Use only the named fields below.", + "properties": { + "job_id": { + "description": "Exact scheduler job ID returned by scheduler.list.", + "title": "Job Id", + "type": "string" + } + }, + "required": [ + "job_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "scheduler.progress", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Run one registered scheduler job immediately. Method: GET. Path: /api/v1/system/runscheduler. Effect: external_side_effect.", "properties": { "operation_id": { "const": "scheduler.run", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "query": { "additionalProperties": false, + "description": "Filters and control values for scheduler.run. Run one registered scheduler job immediately. Use only the named fields below.", "properties": { "jobid": { + "description": "Exact scheduler job ID returned by scheduler.list.", "title": "Jobid", "type": "string" } @@ -4094,10 +8425,31 @@ }, { "additionalProperties": false, - "description": "查询上次搜索上下文 Method: GET. Path: /api/v1/search/last/context. Effect: safe_read.", + "description": "Use the configured recommendation model to rank or recommend torrent search results. Method: POST. Path: /api/v1/search/recommend. Effect: external_side_effect.", + "properties": { + "body": { + "$ref": "#/$defs/Body_recommend_search_results_api_v1_search_recommend_post", + "description": "Request value for search.recommend. Use the configured recommendation model to rank or recommend torrent search results. Use the exact type and fields below." + }, + "operation_id": { + "const": "search.recommend", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "search.recommend", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read the most recent torrent-search context and result set. Method: GET. Path: /api/v1/search/last/context. Effect: safe_read.", "properties": { "operation_id": { "const": "search.results", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" } }, @@ -4109,16 +8461,91 @@ }, { "additionalProperties": false, - "description": "精确搜索资源 Method: GET. Path: /api/v1/search/media/{media_id}. Effect: safe_read.", + "description": "Search torrent sites directly from a free-form title and optional media filters. Method: GET. Path: /api/v1/search/title. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "search.title", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for search.title. Search torrent sites directly from a free-form title and optional media filters. Use only the named fields below.", + "properties": { + "keyword": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Case-insensitive substring used to discover settings or filter storage entries.", + "title": "Keyword" + }, + "mtype": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "MoviePilot media type or subscription-history category required by the operation.", + "title": "Mtype" + }, + "page": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 0, + "description": "One-based result page number.", + "title": "Page" + }, + "sites": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Exact site IDs included in the search or subscription scope.", + "title": "Sites" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id" + ], + "title": "search.title", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Search torrent sites for one canonical media identity. Method: GET. Path: /api/v1/search/media/{media_id}. Effect: safe_read.", "properties": { "operation_id": { "const": "search.torrents", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for search.torrents. Search torrent sites for one canonical media identity. Use only the named fields below.", "properties": { "media_id": { + "description": "Source-native media ID. Always pair it with the exact media_source returned by search.", "title": "Media Id", "type": "string" } @@ -4130,6 +8557,7 @@ }, "query": { "additionalProperties": false, + "description": "Filters and control values for search.torrents. Search torrent sites for one canonical media identity. Use only the named fields below.", "properties": { "area": { "anyOf": [ @@ -4141,10 +8569,12 @@ } ], "default": "title", + "description": "Optional region filter applied by the torrent search workflow.", "title": "Area" }, "media_source": { - "$ref": "#/$defs/MediaSource" + "$ref": "#/$defs/MediaSource", + "description": "Metadata source identifier. Preserve the exact value returned with media_id." }, "mtype": { "anyOf": [ @@ -4155,6 +8585,7 @@ "type": "null" } ], + "description": "MoviePilot media type or subscription-history category required by the operation.", "title": "Mtype" }, "music_type": { @@ -4166,6 +8597,7 @@ "type": "null" } ], + "description": "Music identity level: recording, album, or artist where supported.", "title": "Music Type" }, "season": { @@ -4177,6 +8609,7 @@ "type": "null" } ], + "description": "Season number used by the media, search, subscription, or transfer operation.", "title": "Season" }, "sites": { @@ -4188,6 +8621,7 @@ "type": "null" } ], + "description": "Exact site IDs included in the search or subscription scope.", "title": "Sites" } }, @@ -4207,19 +8641,113 @@ }, { "additionalProperties": false, - "description": "更新站点Cookie&UA Method: POST. Path: /api/v1/site/cookie/{site_id}. Effect: reversible_write.", + "description": "Create one configured site with its complete authentication and search settings. Method: POST. Path: /api/v1/site/. Effect: reversible_write.", "properties": { "body": { - "$ref": "#/$defs/SiteCookieUpdate" + "$ref": "#/$defs/Site-Input", + "description": "Request value for site.add. Create one configured site with its complete authentication and search settings. Use the exact type and fields below." }, "operation_id": { - "const": "site.cookie.update", + "const": "site.add", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id", + "body" + ], + "title": "site.add", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List site-account authentication providers and their required input definitions. Method: GET. Path: /api/v1/site/auth. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "site.auth.options", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "site.auth.options", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Authenticate a supported site account and persist the resulting site authorization state. Method: POST. Path: /api/v1/site/auth. Effect: external_side_effect.", + "properties": { + "body": { + "$ref": "#/$defs/SiteAuth", + "description": "Request value for site.authenticate. Authenticate a supported site account and persist the resulting site authorization state. Use the exact type and fields below." + }, + "operation_id": { + "const": "site.authenticate", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id", + "body" + ], + "title": "site.authenticate", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List torrent categories supported by one configured site. Method: GET. Path: /api/v1/site/category/{site_id}. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "site.category", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for site.category. List torrent categories supported by one configured site. Use only the named fields below.", "properties": { "site_id": { + "description": "Persistent site ID returned by site.list.", + "title": "Site Id", + "type": "integer" + } + }, + "required": [ + "site_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "site.category", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Log in to one site and refresh its stored authentication cookie. Method: POST. Path: /api/v1/site/cookie/{site_id}. Effect: reversible_write.", + "properties": { + "body": { + "$ref": "#/$defs/SiteCookieUpdate", + "description": "Request value for site.cookie.update. Log in to one site and refresh its stored authentication cookie. Use the exact type and fields below." + }, + "operation_id": { + "const": "site.cookie.update", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for site.cookie.update. Log in to one site and refresh its stored authentication cookie. Use only the named fields below.", + "properties": { + "site_id": { + "description": "Persistent site ID returned by site.list.", "title": "Site Id", "type": "integer" } @@ -4240,11 +8768,90 @@ }, { "additionalProperties": false, - "description": "所有站点 Method: GET. Path: /api/v1/site/. Effect: safe_read.", + "description": "Start a CookieCloud synchronization of configured sites. Method: GET. Path: /api/v1/site/cookiecloud. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "site.cookiecloud.sync", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "site.cookiecloud.sync", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Delete one configured site by persistent site ID. Method: DELETE. Path: /api/v1/site/{site_id}. Effect: destructive_write.", + "properties": { + "operation_id": { + "const": "site.delete", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for site.delete. Delete one configured site by persistent site ID. Use only the named fields below.", + "properties": { + "site_id": { + "description": "Persistent site ID returned by site.list.", + "title": "Site Id", + "type": "integer" + } + }, + "required": [ + "site_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "site.delete", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List configured sites with status/name filters; authentication fields are returned only to a superuser. Method: GET. Path: /api/v1/site/agent. Effect: safe_read.", "properties": { "operation_id": { "const": "site.list", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for site.list. List configured sites with status/name filters; authentication fields are returned only to a superuser. Use only the named fields below.", + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Human-readable name of the site, storage item, subscription, or rule group.", + "title": "Name" + }, + "status": { + "default": "all", + "description": "Transfer success status used to filter history or describe a record.", + "enum": [ + "active", + "inactive", + "all" + ], + "title": "Status", + "type": "string" + } + }, + "type": "object" } }, "required": [ @@ -4255,16 +8862,276 @@ }, { "additionalProperties": false, - "description": "连接测试 Method: GET. Path: /api/v1/site/test/{site_id}. Effect: external_side_effect.", + "description": "Read the configured site-domain to site-name mapping. Method: GET. Path: /api/v1/site/mapping. Effect: safe_read.", "properties": { "operation_id": { - "const": "site.test", + "const": "site.mapping", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "site.mapping", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Replace priorities for the supplied configured site IDs. Method: POST. Path: /api/v1/site/priorities. Effect: reversible_write.", + "properties": { + "body": { + "description": "Request value for site.priorities.update. Replace priorities for the supplied configured site IDs. Use the exact type and fields below.", + "items": { + "$ref": "#/$defs/SitePriorityUpdate" + }, + "title": "Priorities", + "type": "array" + }, + "operation_id": { + "const": "site.priorities.update", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id", + "body" + ], + "title": "site.priorities.update", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Delete all configured sites and start a fresh CookieCloud synchronization. Method: GET. Path: /api/v1/site/reset. Effect: destructive_write.", + "properties": { + "operation_id": { + "const": "site.reset", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "site.reset", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Browse torrent resources from one configured site with category and keyword filters. Method: GET. Path: /api/v1/site/resource/{site_id}. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "site.resource", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for site.resource. Browse torrent resources from one configured site with category and keyword filters. Use only the named fields below.", "properties": { "site_id": { + "description": "Persistent site ID returned by site.list.", + "title": "Site Id", + "type": "integer" + } + }, + "required": [ + "site_id" + ], + "type": "object" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for site.resource. Browse torrent resources from one configured site with category and keyword filters. Use only the named fields below.", + "properties": { + "cat": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Exact site category identifier returned by site.category.", + "title": "Cat" + }, + "keyword": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Case-insensitive substring used to discover settings or filter storage entries.", + "title": "Keyword" + }, + "mtype": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "MoviePilot media type or subscription-history category required by the operation.", + "title": "Mtype" + }, + "page": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 0, + "description": "One-based result page number.", + "title": "Page" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "site.resource", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List configured sites selected for RSS subscription processing. Method: GET. Path: /api/v1/site/rss. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "site.rss", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "site.rss", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List active configured sites supporting one exact media type. Method: GET. Path: /api/v1/site/media/{media_type}. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "site.searchable", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for site.searchable. List active configured sites supporting one exact media type. Use only the named fields below.", + "properties": { + "media_type": { + "description": "MoviePilot media type used to filter recommendations or rule groups.", + "title": "Media Type", + "type": "string" + } + }, + "required": [ + "media_type" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "site.searchable", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read account and traffic statistics for one exact configured site domain. Method: GET. Path: /api/v1/site/statistic/{site_url}. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "site.statistic", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for site.statistic. Read account and traffic statistics for one exact configured site domain. Use only the named fields below.", + "properties": { + "site_url": { + "description": "Configured site URL or hostname used to select one site's statistics.", + "title": "Site Url", + "type": "string" + } + }, + "required": [ + "site_url" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "site.statistic", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read the latest account and traffic statistics for all configured sites. Method: GET. Path: /api/v1/site/statistic. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "site.statistics", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "site.statistics", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List indexer definitions supported by the installed MoviePilot resources. Method: GET. Path: /api/v1/site/supporting. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "site.supporting", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "site.supporting", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Test connectivity and authentication for one configured site. Method: GET. Path: /api/v1/site/test/{site_id}. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "site.test", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for site.test. Test connectivity and authentication for one configured site. Use only the named fields below.", + "properties": { + "site_id": { + "description": "Persistent site ID returned by site.list.", "title": "Site Id", "type": "integer" } @@ -4284,13 +9151,15 @@ }, { "additionalProperties": false, - "description": "更新站点 Method: PUT. Path: /api/v1/site/. Effect: reversible_write.", + "description": "Update one configured site's complete settings. Method: PUT. Path: /api/v1/site/. Effect: reversible_write.", "properties": { "body": { - "$ref": "#/$defs/Site-Input" + "$ref": "#/$defs/Site-Input", + "description": "Request value for site.update. Update one configured site's complete settings. Use the exact type and fields below." }, "operation_id": { "const": "site.update", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" } }, @@ -4303,16 +9172,19 @@ }, { "additionalProperties": false, - "description": "查询某站点用户数据 Method: GET. Path: /api/v1/site/userdata/{site_id}. Effect: safe_read.", + "description": "Read the latest account statistics collected from one site. Method: GET. Path: /api/v1/site/userdata/{site_id}. Effect: safe_read.", "properties": { "operation_id": { "const": "site.userdata", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for site.userdata. Read the latest account statistics collected from one site. Use only the named fields below.", "properties": { "site_id": { + "description": "Persistent site ID returned by site.list.", "title": "Site Id", "type": "integer" } @@ -4324,6 +9196,7 @@ }, "query": { "additionalProperties": false, + "description": "Filters and control values for site.userdata. Read the latest account statistics collected from one site. Use only the named fields below.", "properties": { "workdate": { "anyOf": [ @@ -4334,6 +9207,7 @@ "type": "null" } ], + "description": "Date used when retrieving one site's historical user statistics.", "title": "Workdate" } }, @@ -4349,10 +9223,59 @@ }, { "additionalProperties": false, - "description": "获取 Web 智能助手可用命令 Method: GET. Path: /api/v1/message/agent/commands. Effect: safe_read.", + "description": "Read the latest collected account statistics for every configured site. Method: GET. Path: /api/v1/site/userdata/latest. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "site.userdata.latest", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "site.userdata.latest", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Refresh and return account statistics for one configured site. Method: POST. Path: /api/v1/site/userdata/{site_id}. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "site.userdata.refresh", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for site.userdata.refresh. Refresh and return account statistics for one configured site. Use only the named fields below.", + "properties": { + "site_id": { + "description": "Persistent site ID returned by site.list.", + "title": "Site Id", + "type": "integer" + } + }, + "required": [ + "site_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "site.userdata.refresh", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List slash commands that the Agent may dispatch. Method: GET. Path: /api/v1/message/agent/commands. Effect: safe_read.", "properties": { "operation_id": { "const": "slash.list", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" } }, @@ -4364,13 +9287,15 @@ }, { "additionalProperties": false, - "description": "执行 Agent 斜杠命令 Method: POST. Path: /api/v1/message/agent/commands/run. Effect: external_side_effect.", + "description": "Execute one complete slash command through MoviePilot messaging. Method: POST. Path: /api/v1/message/agent/commands/run. Effect: external_side_effect.", "properties": { "body": { - "$ref": "#/$defs/AgentCommandRunRequest" + "$ref": "#/$defs/AgentCommandRunRequest", + "description": "Request value for slash.run. Execute one complete slash command through MoviePilot messaging. Use the exact type and fields below." }, "operation_id": { "const": "slash.run", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" } }, @@ -4383,17 +9308,41 @@ }, { "additionalProperties": false, - "description": "所有目录和文件 Method: POST. Path: /api/v1/storage/list. Effect: safe_read.", + "description": "Delete one exact file or directory from a configured storage provider. Method: POST. Path: /api/v1/storage/delete. Effect: destructive_write.", "properties": { "body": { - "$ref": "#/$defs/FileItem-Input" + "$ref": "#/$defs/FileItem-Input", + "description": "Request value for storage.delete. Delete one exact file or directory from a configured storage provider. Use the exact type and fields below." + }, + "operation_id": { + "const": "storage.delete", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id", + "body" + ], + "title": "storage.delete", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List files or directories from one configured storage location. Method: POST. Path: /api/v1/storage/agent/list. Effect: safe_read.", + "properties": { + "body": { + "$ref": "#/$defs/FileItem-Input", + "description": "Request value for storage.list. List files or directories from one configured storage location. Use the exact type and fields below." }, "operation_id": { "const": "storage.list", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "query": { "additionalProperties": false, + "description": "Filters and control values for storage.list. List files or directories from one configured storage location. Use only the named fields below.", "properties": { "keyword": { "anyOf": [ @@ -4404,6 +9353,7 @@ "type": "null" } ], + "description": "Case-insensitive substring used to discover settings or filter storage entries.", "title": "Keyword" }, "sort": { @@ -4416,6 +9366,7 @@ } ], "default": "updated_at", + "description": "Storage-list sort field or ordering expression.", "title": "Sort" } }, @@ -4431,17 +9382,128 @@ }, { "additionalProperties": false, - "description": "查询目录配置 Method: GET. Path: /api/v1/storage/directories. Effect: safe_read.", + "description": "Run one provider-defined management action against an exact configured storage target. Method: POST. Path: /api/v1/storage/manage. Effect: external_side_effect.", "properties": { + "body": { + "$ref": "#/$defs/ManageRequest", + "description": "Request value for storage.manage. Run one provider-defined management action against an exact configured storage target. Use the exact type and fields below." + }, "operation_id": { - "const": "storage.settings", + "const": "storage.manage", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id", + "body" + ], + "title": "storage.manage", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Create a named child directory below one exact storage directory item. Method: POST. Path: /api/v1/storage/mkdir. Effect: reversible_write.", + "properties": { + "body": { + "$ref": "#/$defs/FileItem-Input", + "description": "Request value for storage.mkdir. Create a named child directory below one exact storage directory item. Use the exact type and fields below." + }, + "operation_id": { + "const": "storage.mkdir", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "query": { "additionalProperties": false, + "description": "Filters and control values for storage.mkdir. Create a named child directory below one exact storage directory item. Use only the named fields below.", + "properties": { + "name": { + "description": "Human-readable name of the site, storage item, subscription, or rule group.", + "title": "Name", + "type": "string" + } + }, + "required": [ + "name" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "query", + "body" + ], + "title": "storage.mkdir", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Rename one exact storage item, optionally applying media-aware recursive renaming. Method: POST. Path: /api/v1/storage/rename. Effect: reversible_write.", + "properties": { + "body": { + "$ref": "#/$defs/FileItem-Input", + "description": "Request value for storage.rename. Rename one exact storage item, optionally applying media-aware recursive renaming. Use the exact type and fields below." + }, + "operation_id": { + "const": "storage.rename", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for storage.rename. Rename one exact storage item, optionally applying media-aware recursive renaming. Use only the named fields below.", + "properties": { + "new_name": { + "description": "Replacement name for the existing filter-rule group.", + "title": "New Name", + "type": "string" + }, + "recursive": { + "anyOf": [ + { + "type": "boolean" + }, + { + "type": "null" + } + ], + "default": false, + "description": "Apply media-aware renaming recursively to child files when true.", + "title": "Recursive" + } + }, + "required": [ + "new_name" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "query", + "body" + ], + "title": "storage.rename", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read configured directory or storage settings. Method: GET. Path: /api/v1/storage/directories. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "storage.settings", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for storage.settings. Read configured directory or storage settings. Use only the named fields below.", "properties": { "directory_type": { "default": "all", + "description": "Directory configuration subtype to return.", "title": "Directory Type", "type": "string" }, @@ -4454,10 +9516,12 @@ "type": "null" } ], + "description": "Human-readable name of the site, storage item, subscription, or rule group.", "title": "Name" }, "storage_type": { "default": "all", + "description": "Configured storage provider type to return.", "title": "Storage Type", "type": "string" } @@ -4473,13 +9537,15 @@ }, { "additionalProperties": false, - "description": "新增订阅 Method: POST. Path: /api/v1/subscribe/. Effect: reversible_write.", + "description": "Create one movie, TV, or music subscription. Method: POST. Path: /api/v1/subscribe/. Effect: reversible_write.", "properties": { "body": { - "$ref": "#/$defs/Subscribe" + "$ref": "#/$defs/Subscribe", + "description": "Request value for subscription.add. Create one movie, TV, or music subscription. Use the exact type and fields below." }, "operation_id": { "const": "subscription.add", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" } }, @@ -4492,16 +9558,19 @@ }, { "additionalProperties": false, - "description": "删除订阅 Method: DELETE. Path: /api/v1/subscribe/{subscribe_id}. Effect: destructive_write.", + "description": "Delete one active subscription. Method: DELETE. Path: /api/v1/subscribe/{subscribe_id}. Effect: destructive_write.", "properties": { "operation_id": { "const": "subscription.delete", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for subscription.delete. Delete one active subscription. Use only the named fields below.", "properties": { "subscribe_id": { + "description": "Persistent subscription ID returned by subscription.list.", "title": "Subscribe Id", "type": "integer" } @@ -4521,16 +9590,344 @@ }, { "additionalProperties": false, - "description": "查询订阅历史 Method: GET. Path: /api/v1/subscribe/history/{mtype}. Effect: safe_read.", + "description": "Delete accessible subscriptions matching one canonical media identity. Method: DELETE. Path: /api/v1/subscribe/media/{media_id}. Effect: destructive_write.", "properties": { "operation_id": { - "const": "subscription.history", + "const": "subscription.delete_by_media", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for subscription.delete_by_media. Delete accessible subscriptions matching one canonical media identity. Use only the named fields below.", + "properties": { + "media_id": { + "description": "Source-native media ID. Always pair it with the exact media_source returned by search.", + "title": "Media Id", + "type": "string" + } + }, + "required": [ + "media_id" + ], + "type": "object" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for subscription.delete_by_media. Delete accessible subscriptions matching one canonical media identity. Use only the named fields below.", + "properties": { + "media_source": { + "$ref": "#/$defs/MediaSource", + "description": "Metadata source identifier. Preserve the exact value returned with media_id." + }, + "music_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Music identity level: recording, album, or artist where supported.", + "title": "Music Type" + }, + "season": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Season number used by the media, search, subscription, or transfer operation.", + "title": "Season" + } + }, + "required": [ + "media_source" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params", + "query" + ], + "title": "subscription.delete_by_media", + "type": "object" + }, + { + "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.", + "properties": { + "operation_id": { + "const": "subscription.files", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for subscription.files. Read local library and transfer-file coverage for one accessible subscription. Use only the named fields below.", + "properties": { + "subscribe_id": { + "description": "Persistent subscription ID returned by subscription.list.", + "title": "Subscribe Id", + "type": "integer" + } + }, + "required": [ + "subscribe_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "subscription.files", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Find one accessible subscription by canonical media identity and optional season. Method: GET. Path: /api/v1/subscribe/media/{media_id}. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "subscription.find", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for subscription.find. Find one accessible subscription by canonical media identity and optional season. Use only the named fields below.", + "properties": { + "media_id": { + "description": "Source-native media ID. Always pair it with the exact media_source returned by search.", + "title": "Media Id", + "type": "string" + } + }, + "required": [ + "media_id" + ], + "type": "object" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for subscription.find. Find one accessible subscription by canonical media identity and optional season. Use only the named fields below.", + "properties": { + "media_source": { + "$ref": "#/$defs/MediaSource", + "description": "Metadata source identifier. Preserve the exact value returned with media_id." + }, + "music_type": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Music identity level: recording, album, or artist where supported.", + "title": "Music Type" + }, + "season": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "description": "Season number used by the media, search, subscription, or transfer operation.", + "title": "Season" + }, + "title": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Media, torrent, subscription, or history title used by the operation.", + "title": "Title" + } + }, + "required": [ + "media_source" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params", + "query" + ], + "title": "subscription.find", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Follow one subscription-sharing user by exact share user ID. Method: POST. Path: /api/v1/subscribe/follow. Effect: reversible_write.", + "properties": { + "operation_id": { + "const": "subscription.follow.add", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for subscription.follow.add. Follow one subscription-sharing user by exact share user ID. Use only the named fields below.", + "properties": { + "share_uid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Exact MoviePilot Server sharing-user ID to follow or unfollow.", + "title": "Share Uid" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id" + ], + "title": "subscription.follow.add", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Stop following one subscription-sharing user by exact share user ID. Method: DELETE. Path: /api/v1/subscribe/follow. Effect: reversible_write.", + "properties": { + "operation_id": { + "const": "subscription.follow.delete", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for subscription.follow.delete. Stop following one subscription-sharing user by exact share user ID. Use only the named fields below.", + "properties": { + "share_uid": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Exact MoviePilot Server sharing-user ID to follow or unfollow.", + "title": "Share Uid" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id" + ], + "title": "subscription.follow.delete", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List subscription-sharing user IDs followed by the current user. Method: GET. Path: /api/v1/subscribe/follow. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "subscription.follow.list", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "subscription.follow.list", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Create a local subscription from one shared subscription definition. Method: POST. Path: /api/v1/subscribe/fork. Effect: external_side_effect.", + "properties": { + "body": { + "$ref": "#/$defs/SubscribeShare", + "description": "Request value for subscription.fork. Create a local subscription from one shared subscription definition. Use the exact type and fields below." + }, + "operation_id": { + "const": "subscription.fork", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id", + "body" + ], + "title": "subscription.fork", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read one accessible subscription by persistent subscription ID. Method: GET. Path: /api/v1/subscribe/{subscribe_id}. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "subscription.get", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for subscription.get. Read one accessible subscription by persistent subscription ID. Use only the named fields below.", + "properties": { + "subscribe_id": { + "description": "Persistent subscription ID returned by subscription.list.", + "title": "Subscribe Id", + "type": "integer" + } + }, + "required": [ + "subscribe_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "subscription.get", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List completed or archived subscription records. Method: GET. Path: /api/v1/subscribe/history/{mtype}. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "subscription.history", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for subscription.history. List completed or archived subscription records. Use only the named fields below.", "properties": { "mtype": { + "description": "MoviePilot media type or subscription-history category required by the operation.", "title": "Mtype", "type": "string" } @@ -4542,6 +9939,7 @@ }, "query": { "additionalProperties": false, + "description": "Filters and control values for subscription.history. List completed or archived subscription records. Use only the named fields below.", "properties": { "count": { "anyOf": [ @@ -4553,6 +9951,7 @@ } ], "default": 30, + "description": "Maximum number of records to return on the requested page.", "title": "Count" }, "page": { @@ -4565,6 +9964,7 @@ } ], "default": 1, + "description": "One-based result page number.", "title": "Page" } }, @@ -4580,10 +9980,43 @@ }, { "additionalProperties": false, - "description": "查询所有订阅 Method: GET. Path: /api/v1/subscribe/. Effect: safe_read.", + "description": "Delete one accessible subscription-history record. Method: DELETE. Path: /api/v1/subscribe/history/{history_id}. Effect: destructive_write.", + "properties": { + "operation_id": { + "const": "subscription.history.delete", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for subscription.history.delete. Delete one accessible subscription-history record. Use only the named fields below.", + "properties": { + "history_id": { + "description": "Persistent transfer- or subscription-history ID returned by a history operation.", + "title": "History Id", + "type": "integer" + } + }, + "required": [ + "history_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "subscription.history.delete", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List active subscriptions. Method: GET. Path: /api/v1/subscribe/. Effect: safe_read.", "properties": { "operation_id": { "const": "subscription.list", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" } }, @@ -4595,14 +10028,32 @@ }, { "additionalProperties": false, - "description": "热门订阅(基于用户共享数据) Method: GET. Path: /api/v1/subscribe/popular. Effect: safe_read.", + "description": "Start a system-wide refresh of subscription TMDB metadata. Method: GET. Path: /api/v1/subscribe/check. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "subscription.metadata.refresh", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "subscription.metadata.refresh", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List globally popular subscriptions with filters and pagination. Method: GET. Path: /api/v1/subscribe/popular. Effect: safe_read.", "properties": { "operation_id": { "const": "subscription.popular", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "query": { "additionalProperties": false, + "description": "Filters and control values for subscription.popular. List globally popular subscriptions with filters and pagination. Use only the named fields below.", "properties": { "count": { "anyOf": [ @@ -4614,6 +10065,7 @@ } ], "default": 30, + "description": "Maximum number of records to return on the requested page.", "title": "Count" }, "genre_id": { @@ -4625,6 +10077,7 @@ "type": "null" } ], + "description": "Genre identifier used to filter shared or popular subscriptions.", "title": "Genre Id" }, "max_rating": { @@ -4636,6 +10089,7 @@ "type": "null" } ], + "description": "Maximum rating used to filter shared or popular subscriptions.", "title": "Max Rating" }, "min_rating": { @@ -4647,6 +10101,7 @@ "type": "null" } ], + "description": "Minimum rating used to filter shared or popular subscriptions.", "title": "Min Rating" }, "min_sub": { @@ -4658,6 +10113,7 @@ "type": "null" } ], + "description": "Minimum subscriber count used to filter popular subscriptions.", "title": "Min Sub" }, "page": { @@ -4670,6 +10126,7 @@ } ], "default": 1, + "description": "One-based result page number.", "title": "Page" }, "sort_type": { @@ -4681,9 +10138,11 @@ "type": "null" } ], + "description": "Ascending or descending order used by shared or popular subscriptions.", "title": "Sort Type" }, "stype": { + "description": "Popular-subscription category requested by the endpoint.", "title": "Stype", "type": "string" } @@ -4703,16 +10162,67 @@ }, { "additionalProperties": false, - "description": "搜索订阅 Method: GET. Path: /api/v1/subscribe/search/{subscribe_id}. Effect: safe_read.", + "description": "Start the configured system-wide subscription refresh job. Method: GET. Path: /api/v1/subscribe/refresh. Effect: external_side_effect.", "properties": { "operation_id": { - "const": "subscription.search", + "const": "subscription.refresh", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "subscription.refresh", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Reset one accessible subscription so it can be processed again. Method: GET. Path: /api/v1/subscribe/reset/{subid}. Effect: reversible_write.", + "properties": { + "operation_id": { + "const": "subscription.reset", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for subscription.reset. Reset one accessible subscription so it can be processed again. Use only the named fields below.", + "properties": { + "subid": { + "description": "Persistent subscription ID whose status or processing state will change.", + "title": "Subid", + "type": "integer" + } + }, + "required": [ + "subid" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "subscription.reset", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Run an immediate search for one existing subscription. Method: GET. Path: /api/v1/subscribe/search/{subscribe_id}. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "subscription.search", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for subscription.search. Run an immediate search for one existing subscription. Use only the named fields below.", "properties": { "subscribe_id": { + "description": "Persistent subscription ID returned by subscription.list.", "title": "Subscribe Id", "type": "integer" } @@ -4732,14 +10242,101 @@ }, { "additionalProperties": false, - "description": "查询分享的订阅 Method: GET. Path: /api/v1/subscribe/shares. Effect: safe_read.", + "description": "Start immediate searches for all subscriptions accessible to the current user. Method: GET. Path: /api/v1/subscribe/search. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "subscription.search_all", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "subscription.search_all", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Publish one accessible subscription to the MoviePilot sharing service. Method: POST. Path: /api/v1/subscribe/share. Effect: external_side_effect.", + "properties": { + "body": { + "$ref": "#/$defs/SubscribeShare", + "description": "Request value for subscription.share. Publish one accessible subscription to the MoviePilot sharing service. Use the exact type and fields below." + }, + "operation_id": { + "const": "subscription.share", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id", + "body" + ], + "title": "subscription.share", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Delete one shared-subscription publication by share ID. Method: DELETE. Path: /api/v1/subscribe/share/{share_id}. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "subscription.share.delete", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for subscription.share.delete. Delete one shared-subscription publication by share ID. Use only the named fields below.", + "properties": { + "share_id": { + "description": "Persistent MoviePilot Server share ID returned by a share-list operation.", + "title": "Share Id", + "type": "integer" + } + }, + "required": [ + "share_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "subscription.share.delete", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read aggregate contribution and reuse counts for subscription sharers. Method: GET. Path: /api/v1/subscribe/share/statistics. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "subscription.share.statistics", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "subscription.share.statistics", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List shared subscriptions with filters and pagination. Method: GET. Path: /api/v1/subscribe/shares. Effect: safe_read.", "properties": { "operation_id": { "const": "subscription.shares", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "query": { "additionalProperties": false, + "description": "Filters and control values for subscription.shares. List shared subscriptions with filters and pagination. Use only the named fields below.", "properties": { "count": { "anyOf": [ @@ -4751,6 +10348,7 @@ } ], "default": 30, + "description": "Maximum number of records to return on the requested page.", "title": "Count" }, "genre_id": { @@ -4762,6 +10360,7 @@ "type": "null" } ], + "description": "Genre identifier used to filter shared or popular subscriptions.", "title": "Genre Id" }, "max_rating": { @@ -4773,6 +10372,7 @@ "type": "null" } ], + "description": "Maximum rating used to filter shared or popular subscriptions.", "title": "Max Rating" }, "min_rating": { @@ -4784,6 +10384,7 @@ "type": "null" } ], + "description": "Minimum rating used to filter shared or popular subscriptions.", "title": "Min Rating" }, "name": { @@ -4795,6 +10396,7 @@ "type": "null" } ], + "description": "Human-readable name of the site, storage item, subscription, or rule group.", "title": "Name" }, "page": { @@ -4807,6 +10409,7 @@ } ], "default": 1, + "description": "One-based result page number.", "title": "Page" }, "sort_type": { @@ -4818,6 +10421,7 @@ "type": "null" } ], + "description": "Ascending or descending order used by shared or popular subscriptions.", "title": "Sort Type" } }, @@ -4832,13 +10436,63 @@ }, { "additionalProperties": false, - "description": "更新订阅 Method: PUT. Path: /api/v1/subscribe/. Effect: reversible_write.", + "description": "Set one accessible subscription to running, paused, or stopped state. Method: PUT. Path: /api/v1/subscribe/status/{subid}. Effect: reversible_write.", + "properties": { + "operation_id": { + "const": "subscription.status.update", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for subscription.status.update. Set one accessible subscription to running, paused, or stopped state. Use only the named fields below.", + "properties": { + "subid": { + "description": "Persistent subscription ID whose status or processing state will change.", + "title": "Subid", + "type": "integer" + } + }, + "required": [ + "subid" + ], + "type": "object" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for subscription.status.update. Set one accessible subscription to running, paused, or stopped state. Use only the named fields below.", + "properties": { + "state": { + "description": "Current site, subscription, marketplace, or transfer state filter.", + "title": "State", + "type": "string" + } + }, + "required": [ + "state" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params", + "query" + ], + "title": "subscription.status.update", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Update one existing movie, TV, or music subscription. Method: PUT. Path: /api/v1/subscribe/. Effect: reversible_write.", "properties": { "body": { - "$ref": "#/$defs/Subscribe" + "$ref": "#/$defs/Subscribe", + "description": "Request value for subscription.update. Update one existing movie, TV, or music subscription. Use the exact type and fields below." }, "operation_id": { "const": "subscription.update", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" } }, @@ -4851,17 +10505,656 @@ }, { "additionalProperties": false, - "description": "手动转移 Method: POST. Path: /api/v1/transfer/manual. Effect: external_side_effect.", + "description": "List public subscriptions owned by one accessible MoviePilot username. Method: GET. Path: /api/v1/subscribe/user/{username}. Effect: safe_read.", "properties": { - "body": { - "$ref": "#/$defs/ManualTransferItem" - }, "operation_id": { - "const": "transfer.file", + "const": "subscription.user.list", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for subscription.user.list. List public subscriptions owned by one accessible MoviePilot username. Use only the named fields below.", + "properties": { + "username": { + "description": "MoviePilot or site username required by the selected operation.", + "title": "Username", + "type": "string" + } + }, + "required": [ + "username" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "subscription.user.list", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Search subtitle providers for one canonical media identity and optional season or episode. Method: GET. Path: /api/v1/search/subtitle/media/{media_id}. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "subtitle.search.media", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for subtitle.search.media. Search subtitle providers for one canonical media identity and optional season or episode. Use only the named fields below.", + "properties": { + "media_id": { + "description": "Source-native media ID. Always pair it with the exact media_source returned by search.", + "title": "Media Id", + "type": "string" + } + }, + "required": [ + "media_id" + ], + "type": "object" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for subtitle.search.media. Search subtitle providers for one canonical media identity and optional season or episode. Use only the named fields below.", + "properties": { + "episode": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Episode number used to narrow a subtitle or media search.", + "title": "Episode" + }, + "media_source": { + "$ref": "#/$defs/MediaSource", + "description": "Metadata source identifier. Preserve the exact value returned with media_id." + }, + "mtype": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "MoviePilot media type or subscription-history category required by the operation.", + "title": "Mtype" + }, + "season": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Season number used by the media, search, subscription, or transfer operation.", + "title": "Season" + }, + "sites": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Exact site IDs included in the search or subscription scope.", + "title": "Sites" + } + }, + "required": [ + "media_source" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params", + "query" + ], + "title": "subtitle.search.media", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Search subtitle providers from a free-form title and optional media filters. Method: GET. Path: /api/v1/search/subtitle/title. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "subtitle.search.title", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "query": { "additionalProperties": false, + "description": "Filters and control values for subtitle.search.title. Search subtitle providers from a free-form title and optional media filters. Use only the named fields below.", + "properties": { + "keyword": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Case-insensitive substring used to discover settings or filter storage entries.", + "title": "Keyword" + }, + "page": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 0, + "description": "One-based result page number.", + "title": "Page" + }, + "sites": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Exact site IDs included in the search or subscription scope.", + "title": "Sites" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id" + ], + "title": "subtitle.search.title", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List loaded MoviePilot module IDs and localized names. Method: GET. Path: /api/v1/system/modulelist. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "system.module.list", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "system.module.list", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Run the built-in availability test for one loaded MoviePilot module. Method: GET. Path: /api/v1/system/moduletest/{moduleid}. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "system.module.test", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for system.module.test. Run the built-in availability test for one loaded MoviePilot module. Use only the named fields below.", + "properties": { + "moduleid": { + "description": "Exact loaded module ID returned by system.module.list.", + "title": "Moduleid", + "type": "string" + } + }, + "required": [ + "moduleid" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "system.module.test", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List approved built-in network-test targets without exposing their request URLs. Method: GET. Path: /api/v1/system/nettest/targets. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "system.network.targets", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "system.network.targets", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Test connectivity to one approved target or the legacy constrained URL input. Method: GET. Path: /api/v1/system/nettest. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "system.network.test", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for system.network.test. Test connectivity to one approved target or the legacy constrained URL input. Use only the named fields below.", + "properties": { + "include": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Regular expression or filter expression that a release must match.", + "title": "Include" + }, + "target_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Approved built-in network-test target ID returned by system.network.targets.", + "title": "Target Id" + }, + "url": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Site, storage, or torrent URL represented by this field.", + "title": "Url" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id" + ], + "title": "system.network.test", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Restart the running MoviePilot process. Method: GET. Path: /api/v1/system/restart. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "system.restart", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "system.restart", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Check GitHub for the latest stable MoviePilot v3 release. Method: POST. Path: /api/v1/system/update/check. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "system.update.check", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "system.update.check", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Start downloading and verifying the available stable release in the background. Method: POST. Path: /api/v1/system/update/download. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "system.update.download", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "system.update.download", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Install the already downloaded and verified stable release, then restart MoviePilot. Method: POST. Path: /api/v1/system/update/install. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "system.update.install", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "system.update.install", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read the current stable-release check, download, verification, or install state. Method: GET. Path: /api/v1/system/update/status. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "system.update.status", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "system.update.status", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Update to the current v3 development branch and restart MoviePilot. Method: POST. Path: /api/v1/system/upgrade. Effect: external_side_effect.", + "properties": { + "body": { + "const": "dev", + "description": "Literal dev. Release updates must use the separate check, download, and install operations.", + "type": "string" + }, + "operation_id": { + "const": "system.upgrade.dev", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id", + "body" + ], + "title": "system.upgrade.dev", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read the installation version and runtime usage report available to the current user. Method: GET. Path: /api/v1/system/usage/statistic. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "system.usage.statistics", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "system.usage.statistics", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List available MoviePilot GitHub releases. Method: GET. Path: /api/v1/system/versions. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "system.versions", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "system.versions", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Delete every cached torrent context. Method: DELETE. Path: /api/v1/torrent/cache. Effect: destructive_write.", + "properties": { + "operation_id": { + "const": "torrent.cache.clear", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "torrent.cache.clear", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Delete one cached torrent context by site domain and cache hash. Method: DELETE. Path: /api/v1/torrent/cache/{domain}/{torrent_hash}. Effect: destructive_write.", + "properties": { + "operation_id": { + "const": "torrent.cache.delete", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for torrent.cache.delete. Delete one cached torrent context by site domain and cache hash. Use only the named fields below.", + "properties": { + "domain": { + "description": "Site hostname or domain used for matching and requests.", + "title": "Domain", + "type": "string" + }, + "torrent_hash": { + "description": "Cache hash returned by torrent.cache.get for one exact site-domain entry.", + "title": "Torrent Hash", + "type": "string" + } + }, + "required": [ + "domain", + "torrent_hash" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "torrent.cache.delete", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Inspect cached torrent contexts and their recognized media identities. Method: GET. Path: /api/v1/torrent/cache. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "torrent.cache.get", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "torrent.cache.get", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Refresh torrent caches from configured RSS or spider sources. Method: POST. Path: /api/v1/torrent/cache/refresh. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "torrent.cache.refresh", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "torrent.cache.refresh", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Replace or recompute the media identity for one cached torrent context. Method: POST. Path: /api/v1/torrent/cache/reidentify/{domain}/{torrent_hash}. Effect: reversible_write.", + "properties": { + "operation_id": { + "const": "torrent.cache.reidentify", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for torrent.cache.reidentify. Replace or recompute the media identity for one cached torrent context. Use only the named fields below.", + "properties": { + "domain": { + "description": "Site hostname or domain used for matching and requests.", + "title": "Domain", + "type": "string" + }, + "torrent_hash": { + "description": "Cache hash returned by torrent.cache.get for one exact site-domain entry.", + "title": "Torrent Hash", + "type": "string" + } + }, + "required": [ + "domain", + "torrent_hash" + ], + "type": "object" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for torrent.cache.reidentify. Replace or recompute the media identity for one cached torrent context. Use only the named fields below.", + "properties": { + "media_id": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Source-native media ID. Always pair it with the exact media_source returned by search.", + "title": "Media Id" + }, + "media_source": { + "anyOf": [ + { + "$ref": "#/$defs/MediaSource" + }, + { + "type": "null" + } + ], + "description": "Metadata source identifier. Preserve the exact value returned with media_id.", + "title": "Media Source" + }, + "music_type": { + "anyOf": [ + { + "enum": [ + "recording", + "album" + ], + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Music identity level: recording, album, or artist where supported.", + "title": "Music Type" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "torrent.cache.reidentify", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Recommend an episode-number extraction template from supplied file samples. Method: POST. Path: /api/v1/transfer/episode-format/recommend. Effect: safe_read.", + "properties": { + "body": { + "$ref": "#/$defs/EpisodeFormatRecommendItem", + "description": "Request value for transfer.episode_format.recommend. Recommend an episode-number extraction template from supplied file samples. Use the exact type and fields below." + }, + "operation_id": { + "const": "transfer.episode_format.recommend", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id", + "body" + ], + "title": "transfer.episode_format.recommend", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Run MoviePilot's manual file-transfer and organization workflow. Method: POST. Path: /api/v1/transfer/manual. Effect: external_side_effect.", + "properties": { + "body": { + "$ref": "#/$defs/ManualTransferItem", + "description": "Request value for transfer.file. Run MoviePilot's manual file-transfer and organization workflow. Use the exact type and fields below." + }, + "operation_id": { + "const": "transfer.file", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for transfer.file. Run MoviePilot's manual file-transfer and organization workflow. Use only the named fields below.", "properties": { "background": { "anyOf": [ @@ -4873,6 +11166,7 @@ } ], "default": false, + "description": "Run the transfer asynchronously and return before completion.", "title": "Background" } }, @@ -4888,14 +11182,16 @@ }, { "additionalProperties": false, - "description": "查询整理记录 Method: GET. Path: /api/v1/history/transfer. Effect: safe_read.", + "description": "List file-transfer history with filters and pagination. Method: GET. Path: /api/v1/history/transfer. Effect: safe_read.", "properties": { "operation_id": { "const": "transfer.history", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "query": { "additionalProperties": false, + "description": "Filters and control values for transfer.history. List file-transfer history with filters and pagination. Use only the named fields below.", "properties": { "count": { "anyOf": [ @@ -4907,6 +11203,7 @@ } ], "default": 30, + "description": "Maximum number of records to return on the requested page.", "title": "Count" }, "page": { @@ -4919,6 +11216,7 @@ } ], "default": 1, + "description": "One-based result page number.", "title": "Page" }, "status": { @@ -4930,6 +11228,7 @@ "type": "null" } ], + "description": "Transfer success status used to filter history or describe a record.", "title": "Status" }, "title": { @@ -4941,6 +11240,7 @@ "type": "null" } ], + "description": "Media, torrent, subscription, or history title used by the operation.", "title": "Title" } }, @@ -4955,17 +11255,36 @@ }, { "additionalProperties": false, - "description": "删除整理记录 Method: DELETE. Path: /api/v1/history/transfer. Effect: destructive_write.", + "description": "Delete every transfer-history record while leaving transferred files untouched. Method: GET. Path: /api/v1/history/empty/transfer. Effect: destructive_write.", + "properties": { + "operation_id": { + "const": "transfer.history.clear", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "transfer.history.clear", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Delete one transfer-history record and optionally remove files. Method: DELETE. Path: /api/v1/history/transfer. Effect: destructive_write.", "properties": { "body": { - "$ref": "#/$defs/TransferHistory-Input" + "$ref": "#/$defs/TransferHistory-Input", + "description": "Request value for transfer.history.delete. Delete one transfer-history record and optionally remove files. Use the exact type and fields below." }, "operation_id": { "const": "transfer.history.delete", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "query": { "additionalProperties": false, + "description": "Filters and control values for transfer.history.delete. Delete one transfer-history record and optionally remove files. Use only the named fields below.", "properties": { "deletedest": { "anyOf": [ @@ -4977,6 +11296,7 @@ } ], "default": false, + "description": "Also delete the organized destination files when deleting transfer history.", "title": "Deletedest" }, "deletesrc": { @@ -4989,6 +11309,7 @@ } ], "default": false, + "description": "Also delete the recorded source files when deleting transfer history.", "title": "Deletesrc" } }, @@ -5004,11 +11325,482 @@ }, { "additionalProperties": false, - "description": "所有工作流 Method: GET. Path: /api/v1/workflow/. Effect: safe_read.", + "description": "Start AI-assisted reorganization for one transfer-history record. Method: POST. Path: /api/v1/history/transfer/{history_id}/ai-redo. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "transfer.history.redo", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for transfer.history.redo. Start AI-assisted reorganization for one transfer-history record. Use only the named fields below.", + "properties": { + "history_id": { + "description": "Persistent transfer- or subscription-history ID returned by a history operation.", + "title": "History Id", + "type": "integer" + } + }, + "required": [ + "history_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "transfer.history.redo", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Start AI-assisted reorganization for an explicit list of transfer-history records. Method: POST. Path: /api/v1/history/transfer/ai-redo. Effect: external_side_effect.", + "properties": { + "body": { + "$ref": "#/$defs/BatchTransferHistoryRedoRequest", + "description": "Request value for transfer.history.redo_batch. Start AI-assisted reorganization for an explicit list of transfer-history records. Use the exact type and fields below." + }, + "operation_id": { + "const": "transfer.history.redo_batch", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id", + "body" + ], + "title": "transfer.history.redo_batch", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Check whether supplied storage items already have successful transfer history. Method: POST. Path: /api/v1/transfer/manual/history. Effect: safe_read.", + "properties": { + "body": { + "$ref": "#/$defs/ManualTransferItem", + "description": "Request value for transfer.manual_history. Check whether supplied storage items already have successful transfer history. Use the exact type and fields below." + }, + "operation_id": { + "const": "transfer.manual_history", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id", + "body" + ], + "title": "transfer.manual_history", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read one durable transfer task awaiting manual review. Method: GET. Path: /api/v1/transfer/tasks/{task_id}/manual-review. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "transfer.manual_review", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for transfer.manual_review. Read one durable transfer task awaiting manual review. Use only the named fields below.", + "properties": { + "task_id": { + "description": "Stable durable transfer task ID returned by transfer.manual_reviews.", + "title": "Task Id", + "type": "string" + } + }, + "required": [ + "task_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "transfer.manual_review", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Record the authorized decision for one durable transfer manual-review operation. Method: POST. Path: /api/v1/transfer/tasks/{task_id}/manual-review. Effect: reversible_write.", + "properties": { + "body": { + "$ref": "#/$defs/TransferManualReviewRequest", + "description": "Request value for transfer.manual_review.resolve. Record the authorized decision for one durable transfer manual-review operation. Use the exact type and fields below." + }, + "operation_id": { + "const": "transfer.manual_review.resolve", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for transfer.manual_review.resolve. Record the authorized decision for one durable transfer manual-review operation. Use only the named fields below.", + "properties": { + "task_id": { + "description": "Stable durable transfer task ID returned by transfer.manual_reviews.", + "title": "Task Id", + "type": "string" + } + }, + "required": [ + "task_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params", + "body" + ], + "title": "transfer.manual_review.resolve", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Page durable transfer tasks awaiting manual review or retry recovery. Method: GET. Path: /api/v1/transfer/tasks/manual-reviews. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "transfer.manual_reviews", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for transfer.manual_reviews. Page durable transfer tasks awaiting manual review or retry recovery. Use only the named fields below.", + "properties": { + "page": { + "default": 1, + "description": "One-based result page number.", + "minimum": 1, + "title": "Page", + "type": "integer" + }, + "page_size": { + "default": 30, + "description": "Maximum records returned on one page.", + "maximum": 100, + "minimum": 1, + "title": "Page Size", + "type": "integer" + }, + "state": { + "default": "manual_review", + "description": "Current site, subscription, marketplace, or transfer state filter.", + "enum": [ + "manual_review", + "retry_wait" + ], + "title": "State", + "type": "string" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id" + ], + "title": "transfer.manual_reviews", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Preview the organized destination name for one source path and media identity. Method: GET. Path: /api/v1/transfer/name. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "transfer.name", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for transfer.name. Preview the organized destination name for one source path and media identity. Use only the named fields below.", + "properties": { + "filetype": { + "description": "Media file type used to preview the organized destination name.", + "title": "Filetype", + "type": "string" + }, + "path": { + "description": "Storage or history path represented by this record.", + "title": "Path", + "type": "string" + } + }, + "required": [ + "path", + "filetype" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "query" + ], + "title": "transfer.name", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List items waiting in the file-transfer queue. Method: GET. Path: /api/v1/transfer/queue. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "transfer.queue", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "transfer.queue", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Remove one exact storage item from the file-transfer queue and stop its transfer. Method: DELETE. Path: /api/v1/transfer/queue. Effect: destructive_write.", + "properties": { + "body": { + "$ref": "#/$defs/FileItem-Input", + "description": "Request value for transfer.queue.delete. Remove one exact storage item from the file-transfer queue and stop its transfer. Use the exact type and fields below." + }, + "operation_id": { + "const": "transfer.queue.delete", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id", + "body" + ], + "title": "transfer.queue.delete", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Resolve the configured transfer destination for supplied source storage items. Method: POST. Path: /api/v1/transfer/manual/target-path. Effect: safe_read.", + "properties": { + "body": { + "$ref": "#/$defs/ManualTransferItem", + "description": "Request value for transfer.target_path. Resolve the configured transfer destination for supplied source storage items. Use the exact type and fields below." + }, + "operation_id": { + "const": "transfer.target_path", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id", + "body" + ], + "title": "transfer.target_path", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List built-in workflow action definitions and their parameter contracts. Method: GET. Path: /api/v1/workflow/actions. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "workflow.actions", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "workflow.actions", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Create one workflow from a complete workflow definition. Method: POST. Path: /api/v1/workflow/. Effect: reversible_write.", + "properties": { + "body": { + "$ref": "#/$defs/Workflow-Input", + "description": "Request value for workflow.create. Create one workflow from a complete workflow definition. Use the exact type and fields below." + }, + "operation_id": { + "const": "workflow.create", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id", + "body" + ], + "title": "workflow.create", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Delete one configured workflow by persistent workflow ID. Method: DELETE. Path: /api/v1/workflow/{workflow_id}. Effect: destructive_write.", + "properties": { + "operation_id": { + "const": "workflow.delete", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for workflow.delete. Delete one configured workflow by persistent workflow ID. Use only the named fields below.", + "properties": { + "workflow_id": { + "description": "Persistent workflow ID returned by workflow.list.", + "title": "Workflow Id", + "type": "integer" + } + }, + "required": [ + "workflow_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "workflow.delete", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List event types that can trigger workflows. Method: GET. Path: /api/v1/workflow/event_types. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "workflow.event_types", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id" + ], + "title": "workflow.event_types", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Create a local workflow from one shared workflow definition. Method: POST. Path: /api/v1/workflow/fork. Effect: external_side_effect.", + "properties": { + "body": { + "$ref": "#/$defs/WorkflowShare", + "description": "Request value for workflow.fork. Create a local workflow from one shared workflow definition. Use the exact type and fields below." + }, + "operation_id": { + "const": "workflow.fork", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id", + "body" + ], + "title": "workflow.fork", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Read one complete configured workflow definition. Method: GET. Path: /api/v1/workflow/{workflow_id}. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "workflow.get", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for workflow.get. Read one complete configured workflow definition. Use only the named fields below.", + "properties": { + "workflow_id": { + "description": "Persistent workflow ID returned by workflow.list.", + "title": "Workflow Id", + "type": "integer" + } + }, + "required": [ + "workflow_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "workflow.get", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List configured workflows and their execution state. Method: GET. Path: /api/v1/workflow/agent. Effect: safe_read.", "properties": { "operation_id": { "const": "workflow.list", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for workflow.list. List configured workflows and their execution state. Use only the named fields below.", + "properties": { + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Human-readable name of the site, storage item, subscription, or rule group.", + "title": "Name" + }, + "state": { + "default": "all", + "description": "Current site, subscription, marketplace, or transfer state filter.", + "enum": [ + "W", + "R", + "P", + "S", + "F", + "all" + ], + "title": "State", + "type": "string" + }, + "trigger_type": { + "default": "all", + "description": "Workflow trigger filter: timer, event, manual, or all.", + "enum": [ + "timer", + "event", + "manual", + "all" + ], + "title": "Trigger Type", + "type": "string" + } + }, + "type": "object" } }, "required": [ @@ -5019,16 +11811,111 @@ }, { "additionalProperties": false, - "description": "执行工作流 Method: POST. Path: /api/v1/workflow/{workflow_id}/run. Effect: external_side_effect.", + "description": "Disable automatic execution of one configured workflow. Method: POST. Path: /api/v1/workflow/{workflow_id}/pause. Effect: reversible_write.", "properties": { "operation_id": { - "const": "workflow.run", + "const": "workflow.pause", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "type": "string" }, "path_params": { "additionalProperties": false, + "description": "Resource identity placeholders for workflow.pause. Disable automatic execution of one configured workflow. Use only the named fields below.", "properties": { "workflow_id": { + "description": "Persistent workflow ID returned by workflow.list.", + "title": "Workflow Id", + "type": "integer" + } + }, + "required": [ + "workflow_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "workflow.pause", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List workflow actions contributed by installed plugins, optionally filtered by plugin ID. Method: GET. Path: /api/v1/workflow/plugin/actions. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "workflow.plugin.actions", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for workflow.plugin.actions. List workflow actions contributed by installed plugins, optionally filtered by plugin ID. Use only the named fields below.", + "properties": { + "plugin_id": { + "description": "Exact installed or marketplace plugin ID.", + "title": "Plugin Id", + "type": "string" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id" + ], + "title": "workflow.plugin.actions", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Reset one configured workflow definition and execution state. Method: POST. Path: /api/v1/workflow/{workflow_id}/reset. Effect: reversible_write.", + "properties": { + "operation_id": { + "const": "workflow.reset", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for workflow.reset. Reset one configured workflow definition and execution state. Use only the named fields below.", + "properties": { + "workflow_id": { + "description": "Persistent workflow ID returned by workflow.list.", + "title": "Workflow Id", + "type": "integer" + } + }, + "required": [ + "workflow_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "workflow.reset", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Run one configured workflow from the beginning or resume point. Method: POST. Path: /api/v1/workflow/{workflow_id}/run. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "workflow.run", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for workflow.run. Run one configured workflow from the beginning or resume point. Use only the named fields below.", + "properties": { + "workflow_id": { + "description": "Persistent workflow ID returned by workflow.list.", "title": "Workflow Id", "type": "integer" } @@ -5040,6 +11927,7 @@ }, "query": { "additionalProperties": false, + "description": "Filters and control values for workflow.run. Run one configured workflow from the beginning or resume point. Use only the named fields below.", "properties": { "from_begin": { "anyOf": [ @@ -5051,6 +11939,7 @@ } ], "default": true, + "description": "Restart the workflow from its first action instead of resuming progress.", "title": "From Begin" } }, @@ -5063,20 +11952,223 @@ ], "title": "workflow.run", "type": "object" + }, + { + "additionalProperties": false, + "description": "Publish one configured workflow to the MoviePilot sharing service. Method: POST. Path: /api/v1/workflow/share. Effect: external_side_effect.", + "properties": { + "body": { + "$ref": "#/$defs/WorkflowShare", + "description": "Request value for workflow.share. Publish one configured workflow to the MoviePilot sharing service. Use the exact type and fields below." + }, + "operation_id": { + "const": "workflow.share", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + } + }, + "required": [ + "operation_id", + "body" + ], + "title": "workflow.share", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Delete one shared-workflow publication by share ID. Method: DELETE. Path: /api/v1/workflow/share/{share_id}. Effect: external_side_effect.", + "properties": { + "operation_id": { + "const": "workflow.share.delete", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for workflow.share.delete. Delete one shared-workflow publication by share ID. Use only the named fields below.", + "properties": { + "share_id": { + "description": "Persistent MoviePilot Server share ID returned by a share-list operation.", + "title": "Share Id", + "type": "integer" + } + }, + "required": [ + "share_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "workflow.share.delete", + "type": "object" + }, + { + "additionalProperties": false, + "description": "List shared workflows with name and pagination filters. Method: GET. Path: /api/v1/workflow/shares. Effect: safe_read.", + "properties": { + "operation_id": { + "const": "workflow.shares", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "query": { + "additionalProperties": false, + "description": "Filters and control values for workflow.shares. List shared workflows with name and pagination filters. Use only the named fields below.", + "properties": { + "count": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 30, + "description": "Maximum number of records to return on the requested page.", + "title": "Count" + }, + "name": { + "anyOf": [ + { + "type": "string" + }, + { + "type": "null" + } + ], + "description": "Human-readable name of the site, storage item, subscription, or rule group.", + "title": "Name" + }, + "page": { + "anyOf": [ + { + "type": "integer" + }, + { + "type": "null" + } + ], + "default": 1, + "description": "One-based result page number.", + "title": "Page" + } + }, + "type": "object" + } + }, + "required": [ + "operation_id" + ], + "title": "workflow.shares", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Enable automatic execution of one configured workflow. Method: POST. Path: /api/v1/workflow/{workflow_id}/start. Effect: reversible_write.", + "properties": { + "operation_id": { + "const": "workflow.start", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for workflow.start. Enable automatic execution of one configured workflow. Use only the named fields below.", + "properties": { + "workflow_id": { + "description": "Persistent workflow ID returned by workflow.list.", + "title": "Workflow Id", + "type": "integer" + } + }, + "required": [ + "workflow_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params" + ], + "title": "workflow.start", + "type": "object" + }, + { + "additionalProperties": false, + "description": "Replace one configured workflow definition. Method: PUT. Path: /api/v1/workflow/{workflow_id}. Effect: reversible_write.", + "properties": { + "body": { + "$ref": "#/$defs/Workflow-Input", + "description": "Request value for workflow.update. Replace one configured workflow definition. Use the exact type and fields below." + }, + "operation_id": { + "const": "workflow.update", + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "type": "string" + }, + "path_params": { + "additionalProperties": false, + "description": "Resource identity placeholders for workflow.update. Replace one configured workflow definition. Use only the named fields below.", + "properties": { + "workflow_id": { + "description": "Persistent workflow ID returned by workflow.list.", + "title": "Workflow Id", + "type": "integer" + } + }, + "required": [ + "workflow_id" + ], + "type": "object" + } + }, + "required": [ + "operation_id", + "path_params", + "body" + ], + "title": "workflow.update", + "type": "object" } ], "properties": { "body": { - "type": "object" + "description": "Request value for the selected operation. Match the exact operation oneOf branch; scalar and object request bodies are not interchangeable." }, "operation_id": { + "description": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", "enum": [ "config.identifiers.get", "config.identifiers.update", + "config.public.get", "config.system.get", "config.system.update", + "config.user.get", + "dashboard.cpu", + "dashboard.downloader", + "dashboard.media.statistics", + "dashboard.memory", + "dashboard.network", + "dashboard.processes", + "dashboard.storage", + "dashboard.system", + "dashboard.transfer.statistics", + "database.backups.create", + "database.backups.delete", + "database.backups.list", + "database.backups.verify", "download.add", + "download.clients", "download.history.delete", + "download.history.list", + "download.paths", + "download.tasks.active", "filter.builtin", "filter.custom", "filter.custom.add", @@ -5086,57 +12178,184 @@ "filter.group.delete", "filter.group.update", "filter.groups", + "filter.test", "library.exists", + "library.latest", + "media.categories", + "media.category.config.get", + "media.category.config.update", "media.detail", + "media.episode_group.seasons", + "media.episode_groups", "media.episode_schedule", "media.person.credits", "media.person.search", "media.recognize", + "media.recognize_file", "media.scrape", "media.search", + "media.seasons", + "media.sources", + "music.album.get", + "music.album.related", + "music.artist.albums", + "music.artist.get", + "music.artist.related", + "music.cache.clear", + "music.cache.delete", + "music.cache.get", + "music.explore", + "music.recognize", "plugin.capabilities", + "plugin.clone", "plugin.config.get", "plugin.config.update", "plugin.data", + "plugin.folder.create", + "plugin.folder.delete", + "plugin.folder.plugins.update", + "plugin.folders.get", + "plugin.folders.update", + "plugin.history", "plugin.install", "plugin.installed", "plugin.market", + "plugin.market.sync_wiki", + "plugin.rating", + "plugin.rating.submit", + "plugin.ratings", + "plugin.releases", "plugin.reload", + "plugin.reset", + "plugin.runtime.status", + "plugin.source.change", + "plugin.source.install", + "plugin.source.options", + "plugin.statistics", "plugin.uninstall", "recommendation.list", "scheduler.list", + "scheduler.progress", "scheduler.run", + "search.recommend", "search.results", + "search.title", "search.torrents", + "site.add", + "site.auth.options", + "site.authenticate", + "site.category", "site.cookie.update", + "site.cookiecloud.sync", + "site.delete", "site.list", + "site.mapping", + "site.priorities.update", + "site.reset", + "site.resource", + "site.rss", + "site.searchable", + "site.statistic", + "site.statistics", + "site.supporting", "site.test", "site.update", "site.userdata", + "site.userdata.latest", + "site.userdata.refresh", "slash.list", "slash.run", + "storage.delete", "storage.list", + "storage.manage", + "storage.mkdir", + "storage.rename", "storage.settings", "subscription.add", "subscription.delete", + "subscription.delete_by_media", + "subscription.files", + "subscription.find", + "subscription.follow.add", + "subscription.follow.delete", + "subscription.follow.list", + "subscription.fork", + "subscription.get", "subscription.history", + "subscription.history.delete", "subscription.list", + "subscription.metadata.refresh", "subscription.popular", + "subscription.refresh", + "subscription.reset", "subscription.search", + "subscription.search_all", + "subscription.share", + "subscription.share.delete", + "subscription.share.statistics", "subscription.shares", + "subscription.status.update", "subscription.update", + "subscription.user.list", + "subtitle.search.media", + "subtitle.search.title", + "system.module.list", + "system.module.test", + "system.network.targets", + "system.network.test", + "system.restart", + "system.update.check", + "system.update.download", + "system.update.install", + "system.update.status", + "system.upgrade.dev", + "system.usage.statistics", + "system.versions", + "torrent.cache.clear", + "torrent.cache.delete", + "torrent.cache.get", + "torrent.cache.refresh", + "torrent.cache.reidentify", + "transfer.episode_format.recommend", "transfer.file", "transfer.history", + "transfer.history.clear", "transfer.history.delete", + "transfer.history.redo", + "transfer.history.redo_batch", + "transfer.manual_history", + "transfer.manual_review", + "transfer.manual_review.resolve", + "transfer.manual_reviews", + "transfer.name", + "transfer.queue", + "transfer.queue.delete", + "transfer.target_path", + "workflow.actions", + "workflow.create", + "workflow.delete", + "workflow.event_types", + "workflow.fork", + "workflow.get", "workflow.list", - "workflow.run" + "workflow.pause", + "workflow.plugin.actions", + "workflow.reset", + "workflow.run", + "workflow.share", + "workflow.share.delete", + "workflow.shares", + "workflow.start", + "workflow.update" ], "type": "string" }, "path_params": { + "description": "Resource identities inserted only into the selected operation's fixed route placeholders. Use the exact names and types in its oneOf branch.", "type": "object" }, "query": { + "description": "Filters and control values sent in the query string. Use the exact names, types, defaults, and enums in the selected operation's oneOf branch.", "type": "object" } }, diff --git a/app/agent/policy/mcp.py b/app/agent/policy/mcp.py new file mode 100644 index 000000000..b72a30cc1 --- /dev/null +++ b/app/agent/policy/mcp.py @@ -0,0 +1,995 @@ +"""从业务 OpenAPI 构建 moviepilot_api 的外部 MCP 输入合同。""" + +from __future__ import annotations + +import re +from copy import deepcopy +from typing import Any, Mapping, Sequence + +OPERATION_DESCRIPTIONS = { + "config.identifiers.get": "Read the complete custom media-recognition identifier list.", + "config.identifiers.update": "Replace the complete custom media-recognition identifier list.", + "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.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.", + "download.paths": "List configured downloader save-path URIs that may be passed to download.add.", + "download.tasks.active": "List currently downloading MoviePilot tasks with their canonical media context.", + "filter.builtin": "List built-in torrent filter rules.", + "filter.custom": "List user-defined torrent filter rules.", + "filter.custom.add": "Create one user-defined torrent filter rule.", + "filter.custom.delete": "Delete one user-defined torrent filter rule.", + "filter.custom.update": "Update one user-defined torrent filter rule.", + "filter.group.add": "Create one named filter-rule group.", + "filter.group.delete": "Delete one named filter-rule group.", + "filter.group.update": "Update or rename one named filter-rule group.", + "filter.groups": "List named filter-rule groups.", + "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.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 across selected metadata sources.", + "media.recognize": "Recognize media identity from a title, subtitle, or custom rule context.", + "media.scrape": "Generate or refresh metadata for one storage item.", + "media.search": "Search canonical media across selected metadata sources.", + "music.album.get": "Read one album's details, tracks, releases, and aligned artist names and IDs.", + "music.album.related": "Browse albums related to one source-native album identity.", + "music.artist.albums": "Browse one artist's albums, singles, EPs, or another exact release-group type.", + "music.artist.get": "Read one artist's canonical details from the selected music metadata source.", + "music.artist.related": "Browse artists related to one source-native artist identity.", + "music.cache.clear": "Clear the complete administrator-only MusicBrainz recognition cache.", + "music.cache.delete": "Delete one administrator-only MusicBrainz recognition-cache entry by exact key.", + "music.cache.get": "Inspect the administrator-only MusicBrainz recognition cache and summary counts.", + "music.explore": "Browse MusicBrainz charts or fresh releases, or Douban Music tag categories.", + "music.recognize": "Resolve one recording or album from an exact music source and source-native ID.", + "plugin.capabilities": "Inspect the runtime capabilities exposed by installed plugins.", + "plugin.config.get": "Read one loaded plugin's configuration form and its defaults merged with saved values.", + "plugin.config.update": "Replace one installed plugin's complete configuration and apply it immediately.", + "plugin.data": "Read a bounded preview of one plugin's persisted data.", + "plugin.install": "Install or update one plugin from an approved source.", + "plugin.installed": "List installed plugins and their runtime status.", + "plugin.market": "List plugins available from configured marketplaces.", + "plugin.reload": "Reload one installed plugin into the running process.", + "plugin.source.change": "Switch an installed plugin to one explicitly selected online source revision.", + "plugin.source.install": "Install an unbound plugin from one explicitly selected online source.", + "plugin.source.options": "Inspect source candidates and the current immutable source identity before installation or source change.", + "plugin.uninstall": "Uninstall one plugin and remove it from the installed set.", + "recommendation.list": "Read personalized media or music recommendations.", + "scheduler.list": "List registered scheduler jobs and their current state.", + "scheduler.run": "Run one registered scheduler job immediately.", + "search.results": "Read the most recent torrent-search context and result set.", + "search.torrents": "Search torrent sites for one canonical media identity.", + "site.cookie.update": "Log in to one site and refresh its stored authentication cookie.", + "site.list": "List configured sites with status/name filters; authentication fields are returned only to a superuser.", + "site.test": "Test connectivity and authentication for one configured site.", + "site.update": "Update one configured site's complete settings.", + "site.userdata": "Read the latest account statistics collected from one site.", + "slash.list": "List slash commands that the Agent may dispatch.", + "slash.run": "Execute one complete slash command through MoviePilot messaging.", + "storage.list": "List files or directories from one configured storage location.", + "storage.settings": "Read configured directory or storage settings.", + "subscription.add": "Create one movie, TV, or music subscription.", + "subscription.delete": "Delete one active subscription.", + "subscription.history": "List completed or archived subscription records.", + "subscription.list": "List active subscriptions.", + "subscription.popular": "List globally popular subscriptions with filters and pagination.", + "subscription.search": "Run an immediate search for one existing subscription.", + "subscription.shares": "List shared subscriptions with filters and pagination.", + "subscription.update": "Update one existing movie, TV, or music subscription.", + "system.restart": "Restart the running MoviePilot process.", + "system.update.check": "Check GitHub for the latest stable MoviePilot v3 release.", + "system.update.download": "Start downloading and verifying the available stable release in the background.", + "system.update.install": "Install the already downloaded and verified stable release, then restart MoviePilot.", + "system.update.status": "Read the current stable-release check, download, verification, or install state.", + "system.upgrade.dev": "Update to the current v3 development branch and restart MoviePilot.", + "system.versions": "List available MoviePilot GitHub releases.", + "transfer.file": "Run MoviePilot's manual file-transfer and organization workflow.", + "transfer.history": "List file-transfer history with filters and pagination.", + "transfer.history.delete": "Delete one transfer-history record and optionally remove files.", + "workflow.list": "List configured workflows and their execution state.", + "workflow.run": "Run one configured workflow from the beginning or resume point.", + "dashboard.cpu": "Read the current host CPU utilization percentage.", + "dashboard.downloader": "Read aggregate downloader task counts, speeds, and free-space information.", + "dashboard.media.statistics": "Read aggregate movie, TV, episode, and music library counts.", + "dashboard.memory": "Read current MoviePilot process and host memory utilization.", + "dashboard.network": "Read the current host network receive and transmit counters.", + "dashboard.processes": "List host processes visible to the MoviePilot runtime.", + "dashboard.storage": "Read local filesystem capacity and free-space information.", + "dashboard.system": "Read MoviePilot host, runtime, platform, and uptime summary information.", + "dashboard.transfer.statistics": "Read aggregate file-transfer counts grouped by time period.", + "database.backups.create": "Create, verify, and atomically publish a managed database backup.", + "database.backups.delete": "Delete one exact managed database backup artifact.", + "database.backups.list": "List managed database backup artifacts without exposing host paths.", + "database.backups.verify": "Verify the integrity of one exact managed database backup artifact.", + "filter.test": "Test one title and optional subtitle against an exact named filter-rule group.", + "media.categories": "Read the resolved automatic media-category mapping.", + "media.category.config.get": "Read the complete automatic media-category strategy configuration.", + "media.category.config.update": "Replace the complete automatic media-category strategy configuration.", + "media.episode_group.seasons": "List seasons defined by one exact TMDB episode-group identity.", + "media.episode_groups": "List alternate TMDB episode groups available for one TV media identity.", + "media.recognize_file": "Recognize canonical media identity from one exact filename and optional path context.", + "media.seasons": "List seasons for one exact media identity or a title-and-year fallback.", + "media.sources": "List metadata sources currently registered for MoviePilot media operations.", + "plugin.clone": "Create a configurable clone of one installed plugin.", + "plugin.history": "Read marketplace update notes and history for one plugin.", + "plugin.market.sync_wiki": "Refresh the configured plugin marketplace repositories from the MoviePilot Wiki.", + "plugin.rating": "Read the current aggregate rating for one plugin.", + "plugin.rating.submit": "Submit or replace the current user's rating for one plugin.", + "plugin.ratings": "Read aggregate ratings for a requested plugin set.", + "plugin.releases": "List available release versions for one plugin source.", + "plugin.reset": "Delete one plugin's saved configuration and data, then restore its default runtime state.", + "plugin.runtime.status": "Read plugin runtime convergence, loading, and failure state.", + "plugin.statistics": "Read public installation statistics for plugins.", + "scheduler.progress": "Read current progress for one exact scheduler job.", + "search.recommend": "Use the configured recommendation model to rank or recommend torrent search results.", + "search.title": "Search torrent sites directly from a free-form title and optional media filters.", + "site.add": "Create one configured site with its complete authentication and search settings.", + "site.auth.options": "List site-account authentication providers and their required input definitions.", + "site.authenticate": "Authenticate a supported site account and persist the resulting site authorization state.", + "site.category": "List torrent categories supported by one configured site.", + "site.cookiecloud.sync": "Start a CookieCloud synchronization of configured sites.", + "site.delete": "Delete one configured site by persistent site ID.", + "site.mapping": "Read the configured site-domain to site-name mapping.", + "site.priorities.update": "Replace priorities for the supplied configured site IDs.", + "site.reset": "Delete all configured sites and start a fresh CookieCloud synchronization.", + "site.resource": "Browse torrent resources from one configured site with category and keyword filters.", + "site.rss": "List configured sites selected for RSS subscription processing.", + "site.searchable": "List active configured sites supporting one exact media type.", + "site.statistic": "Read account and traffic statistics for one exact configured site domain.", + "site.statistics": "Read the latest account and traffic statistics for all configured sites.", + "site.supporting": "List indexer definitions supported by the installed MoviePilot resources.", + "site.userdata.latest": "Read the latest collected account statistics for every configured site.", + "site.userdata.refresh": "Refresh and return account statistics for one configured site.", + "storage.delete": "Delete one exact file or directory from a configured storage provider.", + "storage.manage": "Run one provider-defined management action against an exact configured storage target.", + "storage.mkdir": "Create a named child directory below one exact storage directory item.", + "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.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.", + "subscription.follow.list": "List subscription-sharing user IDs followed by the current user.", + "subscription.fork": "Create a local subscription from one shared subscription definition.", + "subscription.get": "Read one accessible subscription by persistent subscription ID.", + "subscription.history.delete": "Delete one accessible subscription-history record.", + "subscription.metadata.refresh": "Start a system-wide refresh of subscription TMDB metadata.", + "subscription.refresh": "Start the configured system-wide subscription refresh job.", + "subscription.reset": "Reset one accessible subscription so it can be processed again.", + "subscription.search_all": "Start immediate searches for all subscriptions accessible to the current user.", + "subscription.share": "Publish one accessible subscription to the MoviePilot sharing service.", + "subscription.share.delete": "Delete one shared-subscription publication by share ID.", + "subscription.share.statistics": "Read aggregate contribution and reuse counts for subscription sharers.", + "subscription.status.update": "Set one accessible subscription to running, paused, or stopped state.", + "subscription.user.list": "List public subscriptions owned by one accessible MoviePilot username.", + "subtitle.search.media": "Search subtitle providers for one canonical media identity and optional season or episode.", + "subtitle.search.title": "Search subtitle providers from a free-form title and optional media filters.", + "system.module.list": "List loaded MoviePilot module IDs and localized names.", + "system.module.test": "Run the built-in availability test for one loaded MoviePilot module.", + "system.network.targets": "List approved built-in network-test targets without exposing their request URLs.", + "system.network.test": "Test connectivity to one approved target or the legacy constrained URL input.", + "torrent.cache.clear": "Delete every cached torrent context.", + "torrent.cache.delete": "Delete one cached torrent context by site domain and cache hash.", + "torrent.cache.get": "Inspect cached torrent contexts and their recognized media identities.", + "torrent.cache.refresh": "Refresh torrent caches from configured RSS or spider sources.", + "torrent.cache.reidentify": "Replace or recompute the media identity for one cached torrent context.", + "transfer.episode_format.recommend": "Recommend an episode-number extraction template from supplied file samples.", + "transfer.history.clear": "Delete every transfer-history record while leaving transferred files untouched.", + "transfer.history.redo": "Start AI-assisted reorganization for one transfer-history record.", + "transfer.history.redo_batch": "Start AI-assisted reorganization for an explicit list of transfer-history records.", + "transfer.manual_history": "Check whether supplied storage items already have successful transfer history.", + "transfer.manual_review": "Read one durable transfer task awaiting manual review.", + "transfer.manual_review.resolve": "Record the authorized decision for one durable transfer manual-review operation.", + "transfer.manual_reviews": "Page durable transfer tasks awaiting manual review or retry recovery.", + "transfer.name": "Preview the organized destination name for one source path and media identity.", + "transfer.queue": "List items waiting in the file-transfer queue.", + "transfer.queue.delete": "Remove one exact storage item from the file-transfer queue and stop its transfer.", + "transfer.target_path": "Resolve the configured transfer destination for supplied source storage items.", + "workflow.actions": "List built-in workflow action definitions and their parameter contracts.", + "workflow.create": "Create one workflow from a complete workflow definition.", + "workflow.delete": "Delete one configured workflow by persistent workflow ID.", + "workflow.event_types": "List event types that can trigger workflows.", + "workflow.fork": "Create a local workflow from one shared workflow definition.", + "workflow.get": "Read one complete configured workflow definition.", + "workflow.pause": "Disable automatic execution of one configured workflow.", + "workflow.plugin.actions": "List workflow actions contributed by installed plugins, optionally filtered by plugin ID.", + "workflow.reset": "Reset one configured workflow definition and execution state.", + "workflow.share": "Publish one configured workflow to the MoviePilot sharing service.", + "workflow.share.delete": "Delete one shared-workflow publication by share ID.", + "workflow.shares": "List shared workflows with name and pagination filters.", + "workflow.start": "Enable automatic execution of one configured workflow.", + "workflow.update": "Replace one configured workflow definition.", + "config.user.get": "Read current-user feature flags, runtime capabilities, and effective permissions.", + "config.public.get": "Read one explicitly public system setting by exact key.", + "system.usage.statistics": "Read the installation version and runtime usage report available to the current user.", + "plugin.folders.get": "Read the complete administrator plugin-folder grouping configuration.", + "plugin.folders.update": "Replace the complete administrator plugin-folder grouping configuration.", + "plugin.folder.create": "Create one named plugin folder.", + "plugin.folder.delete": "Delete one named plugin folder without uninstalling its plugins.", + "plugin.folder.plugins.update": "Replace the ordered plugin IDs assigned to one named plugin folder.", +} + + +FIELD_DESCRIPTIONS = { + "action_name": "Exact action name whose capability contract should be returned.", + "allow_unrecognized": "Allow a download when MoviePilot cannot resolve a canonical media identity.", + "album": "Album title associated with a music recording or subscription.", + "album_id": "Source-native album ID returned by music search, exploration, or artist-album browsing.", + "album_type": "MusicBrainz release-group type filter: album, single, ep, broadcast, other, compilation, soundtrack, live, or remix.", + "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.", + "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.", + "backdrop": "Backdrop image URL stored with the media or subscription.", + "background": "Run the transfer asynchronously and return before completion.", + "basename": "Base filename without its parent path.", + "best_version": "Enable normal best-version upgrading when set to 1.", + "best_version_full": "Enable full best-version upgrading when set to 1.", + "bit_depth": "Recorded audio bit depth in bits.", + "bitrate": "Recorded audio bitrate in bits per second.", + "body": ( + "Request value for the selected operation. Match the exact operation oneOf branch; " + "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.", + "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.", + "command": "Complete slash command, including the leading slash and all arguments.", + "completed_episode": "Highest episode number already completed for the subscription.", + "cookie": "Site authentication cookie. Treat this value as a secret.", + "count": "Maximum number of records to return on the requested page.", + "current_audio_format": "Audio format of the best version currently held.", + "current_bit_depth": "Bit depth of the best version currently held.", + "current_bitrate": "Bitrate of the best version currently held.", + "current_priority": "Calculated priority of the best version currently held.", + "current_sample_rate": "Sample rate of the best version currently held.", + "custom_words": "Custom recognition or rename words applied to this media workflow.", + "date": "Record creation or completion timestamp used by the history item.", + "date_elapsed": "Human-readable age of the torrent publication date.", + "days": "Recommendation time window in days.", + "deletedest": "Also delete the organized destination files when deleting transfer history.", + "deletesrc": "Also delete the recorded source files when deleting transfer history.", + "description": "Human-readable media, torrent, or subscription description.", + "dest": "Organized destination path recorded in transfer history.", + "dest_fileitem": "Serialized destination storage item recorded by the transfer.", + "dest_storage": "Configured storage name containing the organized destination.", + "directory_type": "Directory configuration subtype to return.", + "domain": "Site hostname or domain used for matching and requests.", + "download_hash": "Provider-native torrent hash associated with the record.", + "downloader": "Configured downloader instance name.", + "downloadvolumefactor": "Torrent download-volume multiplier reported by the site.", + "douban_sort": "Douban Music category order: U for comprehensive, S for rating, R for newest, or O for hottest.", + "drive_id": "Provider-native storage drive identifier.", + "effect": "Video or release-effect filter expression used by the subscription.", + "entity": "Music exploration entity: recording for tracks or album for release groups.", + "enclosure": "Torrent download URL or enclosure supplied by the indexer result.", + "episode_detail": "Episode mapping details used by manual transfer.", + "episode_format": "Episode-number formatting rule used by manual transfer.", + "episode_group": "TMDB episode-group identifier used for alternate episode ordering.", + "episode_offset": "Integer offset added to detected episode numbers.", + "episode_part": "Episode part number used when one episode is split across files.", + "episode_priority": "Per-episode best-version priority state.", + "episodes": "Episode-number expression recorded in history, such as E01-E03.", + "errmsg": "Error message recorded for a failed transfer.", + "expected_revision": "Exact current plugin source-identity revision returned by plugin.source.options.", + "exclude": "Regular expression or filter expression that rejects matching releases.", + "extension": "Filename extension, including or excluding the leading dot as returned by storage.", + "fileid": "Provider-native storage item identifier.", + "fileitem": "One complete source storage item returned by storage.list.", + "fileitems": "Additional source storage items included in the same manual transfer.", + "files": "Serialized list of files recorded by the history item.", + "filter": "Named filter rule or rule expression applied to this site or subscription.", + "filter_groups": "Ordered filter-rule group names applied to the subscription.", + "force": "Force a marketplace refresh or plugin installation when true.", + "freedate": "Torrent freeleech expiration timestamp reported by the site.", + "freedate_diff": "Seconds remaining until the torrent freeleech period ends.", + "fresh_sort": "Freshness ordering used by the recommendation source.", + "from_begin": "Restart the workflow from its first action instead of resuming progress.", + "from_history": "Treat the transfer input as originating from an existing history record.", + "future": "Include future recommendation periods when supported.", + "genre_id": "Genre identifier used to filter shared or popular subscriptions.", + "grabs": "Number of completed downloads reported for the torrent.", + "group": "Registered setting group used for dynamic setting discovery.", + "hit_and_run": "Whether the torrent is subject to hit-and-run requirements.", + "id": "Persistent database identifier of the supplied record.", + "identifiers": "Complete ordered list of custom recognition identifier rules.", + "image": "Image URL stored with the history record.", + "include": "Regular expression or filter expression that a release must match.", + "include_group_refs": "Include custom rules referenced only through rule groups.", + "include_usage": "Include the subscriptions or defaults that reference each rule group.", + "include_values": "Return complete setting values instead of discovery summaries.", + "is_active": "Whether the configured site is enabled.", + "jobid": "Exact scheduler job ID returned by scheduler.list.", + "key": "Optional exact plugin data key used to narrow the returned preview.", + "keyword": "Case-insensitive substring used to discover settings or filter storage entries.", + "labels": "Torrent labels supplied by the site result.", + "lack_episode": "Number of episodes still missing from the subscription.", + "last_update": "Timestamp of the subscription's most recent update.", + "library_category_folder": "Create or use a category-level folder in the target library.", + "library_type_folder": "Create or use a media-type folder in the target library.", + "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.", + "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.", + "match_value": "Exact value compared against match_field during a list-item update.", + "max_chars": "Maximum number of serialized plugin-data characters to return.", + "max_results": "Maximum number of plugin catalog results to return, from 1 to 200.", + "max_rating": "Maximum rating used to filter shared or popular subscriptions.", + "media_category": "MoviePilot library category assigned to the media.", + "media_id": "Source-native media ID. Always pair it with the exact media_source returned by search.", + "media_source": "Metadata source identifier. Preserve the exact value returned with media_id.", + "media_type": "MoviePilot media type used to filter recommendations or rule groups.", + "min_bit_depth": "Minimum acceptable audio bit depth in bits.", + "min_bitrate": "Minimum acceptable audio bitrate in bits per second.", + "min_filesize": "Minimum source file size accepted by manual transfer, in bytes.", + "min_listen_count": "Minimum listen count required for a music recommendation.", + "min_rating": "Minimum rating used to filter shared or popular subscriptions.", + "min_sample_rate": "Minimum acceptable audio sample rate in hertz.", + "min_sub": "Minimum subscriber count used to filter popular subscriptions.", + "mode": "Operation mode; music.explore accepts chart or fresh, while transfer history records move, copy, link, or softlink.", + "modify_time": "Storage item modification timestamp.", + "mtype": "MoviePilot media type or subscription-history category required by the operation.", + "music_type": "Music identity level: recording, album, or artist where supported.", + "name": "Human-readable name of the site, storage item, subscription, or rule group.", + "new_name": "Replacement name for the existing filter-rule group.", + "new_rule_id": "Replacement stable ID for the existing custom filter rule.", + "note": "Structured auxiliary metadata stored with the record.", + "operation": "Setting update mode: replace, merge_dict, upsert_list_item, or remove_list_item.", + "operation_id": "Exact allowlisted MoviePilot operation ID selecting this oneOf branch.", + "page": "One-based result page number.", + "page_url": "Public details page for the torrent result.", + "parent_fileid": "Provider-native identifier of the parent storage directory.", + "password": "Site login password. Treat this value as a secret.", + "past": "Include past recommendation periods when supported.", + "path": "Storage or history path represented by this record.", + "path_params": ( + "Resource identities inserted only into the selected operation's fixed route placeholders. " + "Use the exact names and types in its oneOf branch." + ), + "peers": "Number of downloading peers reported for the torrent.", + "person_id": "Source-native person ID returned by person search.", + "pickcode": "115 storage pickcode associated with the item.", + "plugin_id": "Exact installed or marketplace plugin ID.", + "poster": "Poster image URL stored with the media or subscription.", + "preview": "Validate and preview manual-transfer output without committing file changes.", + "pri": "Site search priority; lower or higher ordering follows the existing site API convention.", + "pri_order": "Indexer priority order assigned to the torrent result.", + "proxy": "Whether the site uses MoviePilot's configured proxy.", + "public": "Whether the site is treated as a public indexer.", + "pubdate": "Torrent publication timestamp.", + "publish_time": "Release-age filter expression for a custom filter rule.", + "quality": "Video or release quality filter expression.", + "query": ( + "Filters and control values sent in the query string. Use the exact names, types, " + "defaults, and enums in the selected operation's oneOf branch." + ), + "range_name": "Named recommendation time range.", + "release_version": "Exact plugin release version to install when one is required.", + "remove_keys": "Object keys removed after merge_dict applies its supplied value.", + "render": "Whether site requests require browser rendering.", + "reorganize": "Allow manual transfer to organize an item that was already processed.", + "repo_url": "Approved plugin repository URL used to resolve the installation source.", + "resolution": "Video resolution filter expression, such as 1080p or 2160p.", + "rss": "Site RSS feed URL.", + "rule_id": "Stable custom filter-rule ID.", + "rule_string": "Ordered filter-rule expression stored in the group.", + "sample_rate": "Recorded audio sample rate in hertz.", + "save_path": "Configured downloader-side save path for the download or subscription.", + "scrape": "Generate metadata and images after manual transfer.", + "search_imdbid": "Use IMDb identity during subscription search when set to 1.", + "season": "Season number used by the media, search, subscription, or transfer operation.", + "seasons": "Season-number expression recorded in history.", + "seeders": "Minimum seeder expression for a filter rule, or the torrent's seeder count.", + "server": "Exact configured media-server instance name returned by the media-server instance list.", + "setting_key": "Exact registered setting key returned by config.system.get discovery.", + "show_secrets": "Return unredacted secret values; use only with explicit authorization.", + "site": "Source site identifier associated with the torrent result.", + "site_cookie": "Site cookie bundled with the torrent result. Treat this value as a secret.", + "site_downloader": "Downloader instance selected by the source site.", + "site_id": "Persistent site ID returned by site.list.", + "site_name": "Human-readable source site name.", + "site_order": "Source site's configured search order.", + "site_proxy": "Whether the torrent's source site uses the configured proxy.", + "site_ua": "User-Agent associated with the source site.", + "sites": "Exact site IDs included in the search or subscription scope.", + "size": "File or torrent size in bytes.", + "size_range": "Accepted torrent size range expression for a custom filter rule.", + "sort": "Storage-list sort field or ordering expression.", + "sort_by": "Recommendation field used for ordering results.", + "sort_type": "Ascending or descending order used by shared or popular subscriptions.", + "source": "Exact metadata or recommendation source selected by the operation.", + "src": "Source path recorded in transfer history.", + "src_fileitem": "Serialized source storage item recorded by the transfer.", + "src_storage": "Configured storage name containing the transfer source.", + "start_episode": "First episode number requested by the subscription.", + "state": "Current site, subscription, marketplace, or transfer state filter.", + "status": "Transfer success status used to filter history or describe a record.", + "storage": "Configured storage name or storage type used by the operation.", + "storage_type": "Configured storage provider type to return.", + "stype": "Popular-subscription category requested by the endpoint.", + "subscribe_id": "Persistent subscription ID returned by subscription.list.", + "subtitle": "Optional subtitle text used together with title during media recognition.", + "target_path": "Destination path used by manual transfer.", + "target_storage": "Configured storage name receiving the manual transfer.", + "tags": "Comma-separated Douban Music category tags; use only with a Douban Music exploration source.", + "thumbnail": "Thumbnail URL returned by the storage provider.", + "timeout": "Per-request site timeout in seconds.", + "title": "Media, torrent, subscription, or history title used by the operation.", + "tmdbid": "TMDB media ID returned by media search or detail.", + "token": "Site authentication token. Treat this value as a secret.", + "torrent_description": "Torrent release description recorded in download history.", + "torrent_in": "Complete torrent candidate returned by search.results or search.torrents.", + "torrent_name": "Torrent release name recorded in download history.", + "torrent_site": "Source site name recorded in download history.", + "total_episode": "Expected total episode count for the subscription.", + "total_tracks": "Expected or recorded track count for a music item.", + "transfer_task_id": "Stable durable transfer-task ID associated with the history record.", + "transfer_type": "Manual-transfer mode, such as move, copy, link, or softlink.", + "trigger_type": "Workflow trigger filter: timer, event, manual, or all.", + "type": "MoviePilot media or storage item type required by the selected operation.", + "type_name": "Explicit media type name used when source IDs alone are ambiguous.", + "ua": "Site User-Agent string used for authenticated requests.", + "uploadvolumefactor": "Torrent upload-volume multiplier reported by the site.", + "url": "Site, storage, or torrent URL represented by this field.", + "userid": "Message-channel user ID recorded with download history.", + "username": "MoviePilot or site username required by the selected operation.", + "value": "Complete replacement value, object fragment, or one list item for the selected update mode.", + "volume_factor": "Combined upload/download volume-factor label shown for the torrent.", + "vote": "Media vote average stored with the subscription.", + "with_cover": "Require recommendation results to include cover artwork.", + "workdate": "Date used when retrieving one site's historical user statistics.", + "workflow_id": "Persistent workflow ID returned by workflow.list.", + "year": "Release or premiere year used to disambiguate the media title.", +} + +FIELD_DESCRIPTIONS.update( + { + "action": "Exact provider or workflow action identifier required by the selected operation.", + "actions": "Ordered workflow action definitions executed by this workflow or flow.", + "add_time": "Timestamp when the workflow definition was created.", + "animated": "Whether the workflow connection is rendered as animated in the editor.", + "attempt": "Current execution-attempt number for this workflow node.", + "attempts": "Attempt counters keyed by workflow node or operation identity.", + "backoff": "Retry backoff multiplier applied after each failed workflow action attempt.", + "branch_policy": "Workflow branch policy controlling selected downstream paths.", + "cat": "Exact site category identifier returned by site.category.", + "check_only": "Validate or preview the recommendation without applying search-result filtering.", + "concurrency_key": "Workflow expression used to serialize actions sharing the same runtime key.", + "condition": "Workflow branch or flow condition expression evaluated at runtime.", + "context": "Persisted workflow execution context available to later actions.", + "current_action": "Identifier of the workflow action currently selected or executing.", + "data": "Serialized workflow action configuration or runtime payload.", + "decision": "Manual-review decision selected from the endpoint's declared enum.", + "episode": "Episode number used to narrow a subtitle or media search.", + "errors": "Workflow execution errors keyed or ordered by action identity.", + "event_conditions": "Additional workflow event-filter conditions.", + "event_type": "Exact event type returned by workflow.event_types.", + "execution_config": "Workflow runtime limits, concurrency, and failure-policy configuration.", + "execution_state": "Persisted resumable workflow execution state.", + "fail_policy": "Workflow failure policy controlling stop, continue, or branch behavior.", + "filetype": "Media file type used to preview the organized destination name.", + "filtered_indices": "Zero-based search-result indices selected by the recommendation model.", + "finished_actions": "Workflow action IDs already completed in the persisted execution state.", + "finished_at": "Timestamp when the workflow node or execution finished.", + "flows": "Workflow connection definitions linking action nodes.", + "folder_name": "Exact plugin folder name returned by plugin.folders.get.", + "genre_ids": "Genre identifiers accepted by the automatic category rule.", + "history_id": "Persistent transfer- or subscription-history ID returned by a history operation.", + "history_ids": "Explicit persistent transfer-history IDs included in one batch redo request.", + "icon": "Icon name or URL used by a workflow, network target, plugin, or category.", + "inputs": "Named input bindings consumed by this workflow action.", + "interval": "Retry delay in seconds before the next workflow action attempt.", + "job_id": "Exact scheduler job ID returned by scheduler.list.", + "join_policy": "Workflow fan-in policy controlling when downstream execution may continue.", + "last_time": "Timestamp of the workflow's most recent execution.", + "max_attempts": "Maximum number of attempts allowed by the workflow retry policy.", + "max_workers": "Maximum concurrent workflow actions allowed by the execution configuration.", + "message": "Human-readable workflow runtime or provider result message.", + "moduleid": "Exact loaded module ID returned by system.module.list.", + "movie": "Automatic movie-category rules evaluated in order.", + "node_states": "Persisted runtime states keyed by workflow node identity.", + "nodes": "Persisted workflow node runtime states keyed by action identity.", + "origin_country": "Production-country code matched by an automatic category rule.", + "original_language": "Original-language code matched by an automatic category rule.", + "outputs": "Named output mappings produced by this workflow action.", + "page_size": "Maximum records returned on one page.", + "params": "Provider-defined JSON parameters for the selected authentication or storage action.", + "plugin_ids": "Exact plugin IDs whose aggregate ratings should be returned.", + "position": "Workflow editor coordinates for one action node.", + "production_countries": "Production-country codes matched by an automatic category rule.", + "progress": "Current numeric or structured workflow execution progress.", + "rating": "Numeric plugin rating accepted by the endpoint's declared bounds.", + "reason": "Human-readable justification recorded with a manual-review decision.", + "recursive": "Apply media-aware renaming recursively to child files when true.", + "release_year": "Release year matched by an automatic category rule.", + "result": "Persisted workflow action result value.", + "result_payload": "Structured external-operation result recorded with manual review.", + "retry": "Workflow retry-policy definition for this action.", + "rulegroup_name": "Exact filter-rule group name returned by filter.groups.", + "run_count": "Number of times the workflow has been executed.", + "running_tasks": "Workflow task IDs currently executing.", + "runtime": "Persisted workflow runtime metadata used for safe resume.", + "share_comment": "Optional explanatory comment published with a shared item.", + "share_id": "Persistent MoviePilot Server share ID returned by a share-list operation.", + "share_title": "Public title used when publishing a subscription or workflow.", + "share_uid": "Exact MoviePilot Server sharing-user ID to follow or unfollow.", + "share_user": "Public contributor name used when publishing a subscription or workflow.", + "site_url": "Configured site URL or hostname used to select one site's statistics.", + "started_at": "Timestamp when the workflow node or execution started.", + "subid": "Persistent subscription ID whose status or processing state will change.", + "suffix": "File suffix or extension matched by an automatic category rule.", + "target": "Exact configured storage target name accepted by storage.manage.", + "target_id": "Approved built-in network-test target ID returned by system.network.targets.", + "task_id": "Stable durable transfer task ID returned by transfer.manual_reviews.", + "timer": "Workflow timer or cron expression used for scheduled execution.", + "torrent_hash": "Cache hash returned by torrent.cache.get for one exact site-domain entry.", + "tv": "Automatic TV-category rules evaluated in order.", + "version": "Plugin release or schema version selected by the operation.", + "wiki_url": "Approved MoviePilot Wiki URL used as the plugin-market synchronization source.", + "x": "Horizontal workflow editor coordinate.", + "y": "Vertical workflow editor coordinate.", + } +) + + +MODEL_DESCRIPTIONS = { + "AgentCommandRunRequest": "Slash-command execution request.", + "Body_add_api_v1_download_add_post": "MoviePilot download submission request.", + "CategoryConfig": "Complete automatic media-category strategy configuration.", + "CustomFilterRuleCreateRequest": "Custom filter-rule creation request.", + "CustomFilterRuleUpdateRequest": "Custom filter-rule update request.", + "CustomIdentifiersUpdateRequest": "Complete custom recognition-identifier replacement request.", + "DownloadHistory-Input": "One MoviePilot download-history record.", + "FileItem-Input": "One file or directory returned by a configured storage provider.", + "FilterRuleGroupCreateRequest": "Filter-rule group creation request.", + "FilterRuleGroupUpdateRequest": "Filter-rule group update request.", + "JsonData-Input": "Arbitrary JSON-compatible auxiliary data.", + "ManualTransferItem": "Manual file-transfer and organization request.", + "MediaSource": "Canonical metadata source identifier paired with a source-native media ID.", + "MediaType": "MoviePilot media type.", + "MusicRecognizeRequest": "Exact source-native recording or album identity to resolve into canonical music metadata.", + "PluginSourceChangeRequest": "Explicit online-source change request guarded by the current identity revision.", + "PluginSourceInstallRequest": "Explicit online-source installation request for an unbound plugin.", + "Site-Input": "Complete site configuration and runtime state.", + "SiteCookieUpdate": "Site login request used to refresh the stored cookie and User-Agent.", + "Subscribe": "Movie, TV, or music subscription input model.", + "SystemSettingsUpdateRequest": "One registered system-setting update request.", + "TorrentInfo": "One torrent candidate returned by MoviePilot search.", + "TransferHistory-Input": "One MoviePilot file-transfer history record.", +} + +MODEL_DESCRIPTIONS.update( + { + "CategoryRule": "One ordered automatic media-category matching rule.", + "PluginCloneRequest": "Plugin clone identifier and optional display-name request.", + "PluginMarketSyncRequest": "Approved Wiki source request for plugin-market synchronization.", + "PluginRatingRequest": "Current user's numeric plugin-rating submission.", + "PluginFoldersData": "Complete mapping from plugin folder names to ordered plugin IDs or display configuration.", + "PluginFolderConfigData": "One plugin folder's ordered members and optional presentation settings.", + "Body_recommend_search_results_api_v1_search_recommend_post": "Torrent search results and recommendation controls supplied to the configured model.", + "SiteAuth": "Supported site-account authentication provider and its exact parameter values.", + "SitePriorityUpdate": "One configured site ID and its replacement search priority.", + "ManageRequest": "Configured storage target, provider-defined action, and action parameters.", + "SubscribeShare": "Shared subscription definition or publication metadata.", + "EpisodeFormatRecommendItem": "File samples used to infer an episode-number extraction template.", + "BatchTransferHistoryRedoRequest": "Explicit transfer-history IDs for AI-assisted batch reorganization.", + "TransferManualReviewRequest": "Authorized decision and optional result for one durable transfer operation.", + "Workflow-Input": "Complete workflow definition accepted by create and update operations.", + "Action-Input": "One executable action node in a workflow definition.", + "ActionPosition": "Editor coordinates for one workflow action node.", + "ActionRetry": "Retry limits and timing for one workflow action.", + "ActionFlow-Input": "One directed connection between workflow action nodes.", + "WorkflowExecutionConfig": "Workflow concurrency, join, branch, and failure policies.", + "WorkflowExecutionState-Input": "Persisted resumable workflow execution state.", + "WorkflowNodeState": "Persisted runtime state for one workflow action node.", + "WorkflowRuntimeState": "Complete persisted workflow runtime and progress state.", + "WorkflowShare": "Shared workflow definition or publication metadata.", + } +) + + +def _apply_field_descriptions(schema: Mapping[str, Any]) -> dict[str, Any]: + """Attach concrete English guidance without replacing richer endpoint text.""" + rewritten: dict[str, Any] = {} + for key, value in schema.items(): + if isinstance(value, Mapping): + rewritten[key] = _apply_field_descriptions(value) + elif isinstance(value, list): + rewritten[key] = [ + _apply_field_descriptions(item) if isinstance(item, Mapping) else deepcopy(item) + for item in value + ] + else: + rewritten[key] = deepcopy(value) + properties = rewritten.get("properties") + if isinstance(properties, dict): + for field_name, field_schema in properties.items(): + if not isinstance(field_schema, dict): + continue + existing = field_schema.get("description") + if ( + isinstance(existing, str) + and existing.strip() + and not re.search(r"[\u3400-\u9fff]", existing) + ): + continue + description = FIELD_DESCRIPTIONS.get(str(field_name)) + if not description: + raise ValueError(f"MCP field guidance is missing: {field_name}") + field_schema["description"] = description + return rewritten + + +def _rewrite_schema_refs( + schema: Mapping[str, Any], + *, + components: Mapping[str, Any], + definitions: dict[str, Any], +) -> dict[str, Any]: + """把 OpenAPI components 引用改写为独立 MCP schema 的 $defs 引用。""" + reference = schema.get("$ref") + if isinstance(reference, str) and reference.startswith("#/components/schemas/"): + name = reference.rsplit("/", 1)[-1] + if name not in definitions: + definitions[name] = {} + source = components.get(name) + if not isinstance(source, Mapping): + raise ValueError(f"OpenAPI 缺少请求模型: {name}") + definitions[name] = _rewrite_schema_refs( + source, + components=components, + definitions=definitions, + ) + return {"$ref": f"#/$defs/{name}"} + + rewritten: dict[str, Any] = {} + for key, value in schema.items(): + if isinstance(value, Mapping): + rewritten[key] = _rewrite_schema_refs( + value, + components=components, + definitions=definitions, + ) + elif isinstance(value, list): + rewritten[key] = [ + _rewrite_schema_refs( + item, + components=components, + definitions=definitions, + ) + if isinstance(item, Mapping) + else deepcopy(item) + for item in value + ] + else: + rewritten[key] = deepcopy(value) + return rewritten + + +def _parameter_object_schema( + parameters: Sequence[Mapping[str, Any]], + *, + location: str, + components: Mapping[str, Any], + definitions: dict[str, Any], +) -> dict[str, Any] | None: + """把 OpenAPI path/query 参数投影为网关结构化对象。""" + selected = [parameter for parameter in parameters if parameter.get("in") == location] + if not selected: + return None + properties: dict[str, Any] = {} + required: list[str] = [] + for parameter in selected: + name = str(parameter["name"]) + raw_schema = parameter.get("schema") + if not isinstance(raw_schema, Mapping): + raw_schema = {} + field_schema = _rewrite_schema_refs( + raw_schema, + components=components, + definitions=definitions, + ) + if parameter.get("description") and "description" not in field_schema: + field_schema["description"] = parameter["description"] + properties[name] = field_schema + if parameter.get("required"): + required.append(name) + result: dict[str, Any] = { + "type": "object", + "properties": properties, + "additionalProperties": False, + } + if required: + result["required"] = required + return result + + +def _request_body_schema( + operation: Mapping[str, Any], + *, + components: Mapping[str, Any], + definitions: dict[str, Any], +) -> tuple[dict[str, Any] | None, bool]: + """读取一个 OpenAPI operation 的 JSON 请求体及必填性。""" + request_body = operation.get("requestBody") + if not isinstance(request_body, Mapping): + return None, False + content = request_body.get("content") + if not isinstance(content, Mapping): + return None, bool(request_body.get("required")) + media = content.get("application/json") + if not isinstance(media, Mapping): + media = next((item for item in content.values() if isinstance(item, Mapping)), None) + raw_schema = media.get("schema") if isinstance(media, Mapping) else None + if not isinstance(raw_schema, Mapping): + return None, bool(request_body.get("required")) + return ( + _rewrite_schema_refs( + raw_schema, + components=components, + definitions=definitions, + ), + bool(request_body.get("required")), + ) + + +def _person_credits_operation(openapi: Mapping[str, Any]) -> dict[str, Any]: + """合并四个来源端点为稳定 media.person.credits 网关合同。""" + paths = openapi.get("paths", {}) + source_paths = { + "douban": "/api/v1/douban/person/credits/{person_id}", + "tmdb": "/api/v1/tmdb/person/credits/{person_id}", + "bangumi": "/api/v1/bangumi/person/credits/{person_id}", + "anilist": "/api/v1/anilist/person/credits/{person_id}", + } + for source_path in source_paths.values(): + if source_path not in paths: + raise ValueError(f"OpenAPI 缺少人物作品端点: {source_path}") + return { + "summary": "Read person credits", + "parameters": [ + { + "name": "source", + "in": "path", + "required": True, + "schema": {"type": "string", "enum": list(source_paths)}, + "description": "Metadata source that owns the person ID.", + }, + { + "name": "person_id", + "in": "path", + "required": True, + "schema": {"type": "integer"}, + "description": "Source-native person ID.", + }, + { + "name": "page", + "in": "query", + "required": False, + "schema": {"type": "integer", "minimum": 1, "default": 1}, + }, + { + "name": "count", + "in": "query", + "required": False, + "schema": {"type": "integer", "minimum": 1, "maximum": 50, "default": 20}, + "description": "Page size used by Bangumi and AniList; other sources ignore it.", + }, + ], + } + + +def _resolve_openapi_operation( + operation_id: str, + route: Any, + openapi: Mapping[str, Any], +) -> Mapping[str, Any]: + """按固定路由读取 OpenAPI operation,并处理多来源合成路由。""" + if operation_id == "media.person.credits": + return _person_credits_operation(openapi) + path_item = openapi.get("paths", {}).get(route.path) + if not isinstance(path_item, Mapping): + raise ValueError(f"OpenAPI 缺少 operation 路径: {operation_id} -> {route.path}") + operation = path_item.get(str(route.method).lower()) + if not isinstance(operation, Mapping): + raise ValueError(f"OpenAPI 缺少 operation 方法: {operation_id} -> {route.method} {route.path}") + return operation + + +def _apply_operation_overrides( + operation_id: str, + query_schema: dict[str, Any] | None, + body_schema: dict[str, Any] | None, +) -> tuple[dict[str, Any] | None, bool | None]: + """补充同一路由多 operation 时无法由 FastAPI 自动表达的语义约束。""" + if operation_id == "media.person.search" and query_schema is not None: + query_schema["properties"]["type"] = { + "type": "string", + "const": "person", + "description": "Literal person, selecting person search instead of media search.", + } + required = query_schema.setdefault("required", []) + if "type" not in required: + required.append("type") + if operation_id in {"plugin.installed", "plugin.market"} and query_schema is not None: + state = "installed" if operation_id == "plugin.installed" else "market" + query_schema["properties"]["query"]["description"] = ( + "Optional case-insensitive keyword matched against plugin ID, name, description, and author." + ) + query_schema["properties"]["state"] = { + "type": "string", + "const": state, + "description": f"Literal {state}, selecting only {state} plugin catalog entries.", + } + required = query_schema.setdefault("required", []) + if "state" not in required: + required.append("state") + if operation_id == "music.explore" and query_schema is not None: + descriptions = { + "media_source": ( + "Music exploration source. Use musicbrainz for chart/fresh modes or doubanmusic for tag browsing." + ), + "mode": "MusicBrainz mode: chart reads listening charts; fresh reads new album releases.", + "entity": "Chart entity: recording for tracks or album for release groups. Fresh results are albums.", + "range_name": "ListenBrainz chart range: this_week, this_month, this_year, week, month, or year.", + "sort_by": "ListenBrainz chart order: listen_count.desc or listen_count.asc.", + "sort": "Fresh-release order accepted by the current ListenBrainz implementation.", + "days": "Fresh-release lookback/lookahead window, from 1 through the endpoint maximum.", + "past": "Include releases before today in fresh mode.", + "future": "Include releases after today in fresh mode.", + "min_listen_count": "Minimum ListenBrainz listen count in chart mode.", + "with_cover": "Keep only results with cover artwork when true.", + "tags": "Comma-separated Douban Music tags used only when media_source is doubanmusic.", + "douban_sort": "Douban Music order: U comprehensive, S rating, R newest, or O hottest.", + } + for field_name, description in descriptions.items(): + field_schema = query_schema["properties"].get(field_name) + if isinstance(field_schema, dict): + field_schema["description"] = description + if operation_id == "plugin.config.update" and body_schema is not None: + body_schema["minProperties"] = 1 + body_schema["description"] = ( + "Complete plugin configuration object. First call plugin.config.get, copy its returned model, " + "change only the intended keys, and submit the full resulting object. Omit a key only when it " + "must be removed. A successful update reinitializes the plugin and refreshes commands, jobs, and routes." + ) + if operation_id == "system.upgrade.dev": + return ( + { + "type": "string", + "const": "dev", + "description": "Literal dev. Release updates must use the separate check, download, and install operations.", + }, + True, + ) + return body_schema, None + + +def build_api_mcp_input_schema( + *, + openapi: Mapping[str, Any], + routes: Mapping[str, Any], + specs: Sequence[Any], +) -> dict[str, Any]: + """构建全部白名单 operation 的完整 MCP oneOf 输入合同。""" + components = openapi.get("components", {}).get("schemas", {}) + if not isinstance(components, Mapping): + components = {} + definitions: dict[str, Any] = {} + spec_by_id = {spec.operation_id: spec for spec in specs} + if set(spec_by_id) != set(routes): + raise ValueError("API operation 策略与路由注册表不一致") + + branches: list[dict[str, Any]] = [] + for operation_id in sorted(routes): + route = routes[operation_id] + operation = _resolve_openapi_operation(operation_id, route, openapi) + summary = OPERATION_DESCRIPTIONS.get(operation_id) + if not summary: + raise ValueError(f"MCP operation guidance is missing: {operation_id}") + parameters = operation.get("parameters") + if not isinstance(parameters, list): + parameters = [] + path_schema = _parameter_object_schema( + parameters, + location="path", + components=components, + definitions=definitions, + ) + query_schema = _parameter_object_schema( + parameters, + location="query", + components=components, + definitions=definitions, + ) + body_schema, body_required = _request_body_schema( + operation, + components=components, + definitions=definitions, + ) + body_schema, required_override = _apply_operation_overrides( + operation_id, + query_schema, + body_schema, + ) + if required_override is not None: + body_required = required_override + + properties: dict[str, Any] = { + "operation_id": {"type": "string", "const": operation_id}, + } + required = ["operation_id"] + if path_schema is not None: + path_schema["description"] = ( + f"Resource identity placeholders for {operation_id}. {summary} " + "Use only the named fields below." + ) + properties["path_params"] = path_schema + if path_schema.get("required"): + required.append("path_params") + if query_schema is not None: + query_schema["description"] = ( + f"Filters and control values for {operation_id}. {summary} " + "Use only the named fields below." + ) + properties["query"] = query_schema + if query_schema.get("required"): + required.append("query") + if body_schema is not None: + body_schema.setdefault( + "description", + f"Request value for {operation_id}. {summary} Use the exact type and fields below.", + ) + properties["body"] = body_schema + if body_required: + required.append("body") + + spec = spec_by_id[operation_id] + branches.append( + { + "type": "object", + "title": operation_id, + "description": ( + f"{summary} Method: {route.method}. Path: {route.path}. " + f"Effect: {spec.effect.value}." + ), + "properties": properties, + "required": required, + "additionalProperties": False, + } + ) + + schema: dict[str, Any] = { + "$schema": "https://json-schema.org/draft/2020-12/schema", + "title": "moviepilot_api", + "type": "object", + "description": "Select the oneOf branch matching operation_id and send exactly its documented fields.", + "properties": { + "operation_id": {"type": "string", "enum": sorted(routes)}, + "path_params": {"type": "object"}, + "query": {"type": "object"}, + "body": {}, + }, + "required": ["operation_id"], + "oneOf": branches, + } + if definitions: + for model_name, definition in definitions.items(): + description = MODEL_DESCRIPTIONS.get(model_name) + if not description: + raise ValueError(f"MCP model guidance is missing: {model_name}") + definition["description"] = description + schema["$defs"] = definitions + return _apply_field_descriptions(schema) + + +__all__ = ["build_api_mcp_input_schema"] diff --git a/app/agent/policy/registry.py b/app/agent/policy/registry.py index 81f038939..1007612c3 100644 --- a/app/agent/policy/registry.py +++ b/app/agent/policy/registry.py @@ -49,6 +49,7 @@ BUILTIN_LEGACY_SHADOW_INVENTORY = frozenset( "moviepilot_api", "downloader_operation", "mediaserver_operation", + "database_operation", } ) diff --git a/app/agent/prompt/System Core Prompt.txt b/app/agent/prompt/System Core Prompt.txt index e52427847..51d75e44c 100644 --- a/app/agent/prompt/System Core Prompt.txt +++ b/app/agent/prompt/System Core Prompt.txt @@ -21,7 +21,7 @@ You act as a proactive agent. Your goal is to fully resolve the user's media-rel - Do not stop for approval on read-only operations. -- Raw secret reads are protected operations rather than ordinary read-only queries. When a user explicitly asks for a raw credential or another unredacted sensitive setting, load the relevant configuration Skill and call `moviepilot_api` with `operation_id=config.system.get` and `body.show_secrets=true`; do not refuse solely because the value is sensitive. The host verifies administrator authority, obtains confirmation, and delivers the result through a protected channel. Never expose or repeat the secret in an ordinary assistant response, tool narration, or follow-up model context. +- Raw secret reads are protected operations rather than ordinary read-only queries. When a user explicitly asks for a raw credential or another unredacted sensitive setting, load the relevant configuration Skill and call `moviepilot_api` with `operation_id=config.system.get` and `query.show_secrets=true`; do not refuse solely because the value is sensitive. The host verifies administrator authority, obtains confirmation, and delivers the result through a protected channel. Never expose or repeat the secret in an ordinary assistant response, tool narration, or follow-up model context. - If the user has not explicitly requested an operation that changes system behavior, ask for confirmation before proceeding. This includes modifying system settings, updating plugin configuration, reloading plugins, running restart/stop/start commands, or triggering slash commands such as `/restart`. - Always get explicit consent before destructive or high-impact actions such as starting downloads, deleting subscriptions, deleting download tasks or files, removing history, installing/uninstalling plugins, changing site authentication, changing scheduler or workflow execution state, restarting services, or stopping services. - When the user explicitly asks for delayed, recurring, reminder, or monitoring work, use `agent_task` with `action=create` instead of promising to remember it or writing a JOB.md file. Use a `date` trigger with `delay_minutes` for requests such as "in 30 minutes", an exact `date` trigger for other single future runs, and a five-field `cron` trigger for recurring work. Manage existing autonomous tasks through the same tool with `action=list|update|run|delete`; these actions use integer `task_id` values. Use `moviepilot_api` operations `scheduler.list` and `scheduler.run` only for MoviePilot system, plugin, or workflow runtime services, whose string `job_id` values must never be passed to `agent_task`. diff --git a/app/agent/tools/factory.py b/app/agent/tools/factory.py index 380096426..586e50272 100644 --- a/app/agent/tools/factory.py +++ b/app/agent/tools/factory.py @@ -17,7 +17,11 @@ from app.agent.tools.impl.search_web import SearchWebTool from app.agent.tools.impl.send_local_file import SendLocalFileTool from app.agent.tools.impl.send_message import SendMessageTool from app.agent.tools.impl.send_voice_message import SendVoiceMessageTool -from app.agent.tools.impl.service_operation import DownloaderOperationTool, MediaServerOperationTool +from app.agent.tools.impl.service import ( + DatabaseOperationTool, + DownloaderOperationTool, + MediaServerOperationTool, +) from app.agent.tools.impl.write_file import WriteFileTool from app.application.agent import AgentDataContext from app.application.plugin.runtime import get_plugin_manager @@ -65,6 +69,7 @@ class MoviePilotToolFactory: EXTERNAL_SERVICE_TOOL_CLASSES: tuple[Type[MoviePilotTool], ...] = ( DownloaderOperationTool, MediaServerOperationTool, + DatabaseOperationTool, ) # 这些通用工具需要始终保留,避免大工具集裁剪后让 Agent 丢失基础的 diff --git a/app/agent/tools/impl/api.py b/app/agent/tools/impl/api.py index 46297b3f9..b10821eb7 100644 --- a/app/agent/tools/impl/api.py +++ b/app/agent/tools/impl/api.py @@ -10,6 +10,7 @@ from pydantic import BaseModel, Field, PrivateAttr from app.agent.api.executor import ApiExecutionContext, ApiExecutionError, MoviePilotApiExecutor from app.agent.policy.api import resolve_api_operation +from app.agent.policy.contracts import PrincipalRole from app.agent.tools.base import MoviePilotTool from app.agent.tools.tags import ToolTag from app.schemas.types import NotificationChannel @@ -31,20 +32,24 @@ class MoviePilotApiInput(BaseModel): # type: ignore[misc] operation_id: str = Field( ..., description=( - "稳定的 MoviePilot API operation ID。先根据领域 Skill 选择操作,不要传入 URL、认证头或 API Token。" + "Exact allowlisted MoviePilot operation ID selected from the loaded domain Skill. " + "Never supply a URL, authentication header, or API token." ), ) path_params: Dict[str, Any] = Field( default_factory=dict, - description="路径参数;仅填当前 operation 声明的参数。", + description="Route placeholder values declared by the selected operation.", ) query: Dict[str, Any] = Field( default_factory=dict, - description="查询参数;仅填当前 operation 声明的参数。", + description="Query-string fields declared by the selected operation.", ) - body: Dict[str, Any] = Field( - default_factory=dict, - description="JSON 请求体;字段由当前 operation 的 Skill 合同定义。", + body: Any = Field( + default=None, + description=( + "JSON request value declared by the selected operation and its loaded Skill contract. " + "Most operations use an object; a oneOf branch may require an exact scalar." + ), ) @@ -66,9 +71,9 @@ class MoviePilotApiTool(MoviePilotTool): ToolTag.Plugin, ] description: str = ( - "调用经过白名单审核的 MoviePilot 业务 API。使用领域 Skill 获取 operation_id、" - "参数和失败处理;外部 MCP tools/list 会为每个 operation 提供完整 oneOf 参数合同;" - "不能调用任意 URL、命令或认证接口。" + "Call allowlisted MoviePilot business APIs. Use the domain Skill to select operation_id, " + "parameters, and failure handling. External MCP tools/list exposes one complete oneOf " + "branch per operation. Arbitrary URLs, commands, and authentication endpoints are forbidden." ) require_admin: bool = False args_schema: Type[BaseModel] = MoviePilotApiInput @@ -96,24 +101,42 @@ class MoviePilotApiTool(MoviePilotTool): """返回包含全部白名单 operation 精确参数的 MCP JSON Schema。""" return deepcopy(_load_api_mcp_input_schema()) - async def _resolve_api_identity(self) -> tuple[str, Optional[str], bool]: - """把 Web 或渠道身份解析为真实 MoviePilot 用户身份。""" + async def _resolve_superuser_integration_identity( + self, + ) -> tuple[str, Optional[str], bool]: + """为已验证的管理员集成解析一个真实持久化超级管理员身份。""" + from app.application.security.auth import build_superuser_token_payload + + payload = await self.run_blocking( + "db", + build_superuser_token_payload, + ) + if payload.sub is None: + raise ApiExecutionError("管理员集成身份没有持久化用户 ID") + return str(payload.sub), payload.username, bool(payload.super_user) + + async def _resolve_api_identity( + self, + *, + require_system_admin: bool = False, + ) -> tuple[str, Optional[str], bool]: + """把 Web、渠道或集成身份解析为真实 MoviePilot API 用户身份。""" raw_user_id = str(self._user_id or "") if self._source == "api" and bool(self._agent_context.get("is_admin")): - from app.application.security.auth import build_superuser_token_payload - - payload = await self.run_blocking( - "db", - build_superuser_token_payload, - ) - if payload.sub is None: - raise ApiExecutionError("管理员集成身份没有持久化用户 ID") - return str(payload.sub), payload.username, bool(payload.super_user) + return await self._resolve_superuser_integration_identity() direct_user_channels = { NotificationChannel.Web.value, NotificationChannel.WebAgent.value, } is_direct_user_channel = self._channel in direct_user_channels + if ( + require_system_admin + and bool(self._agent_context.get("is_admin")) + and not is_direct_user_channel + ): + # 通知渠道管理员延续旧管理员工具语义,但只在管理员 operation + # 上借用系统管理员集成身份;普通 operation 仍解析其绑定用户。 + return await self._resolve_superuser_integration_identity() if self._data is None: if is_direct_user_channel and raw_user_id.isdigit(): return raw_user_id, self._username, bool(self._agent_context.get("is_admin")) @@ -154,11 +177,19 @@ class MoviePilotApiTool(MoviePilotTool): raise ApiExecutionError("当前 Agent 身份未绑定有效的 MoviePilot 用户") return str(user.id), user.name, bool(user.is_superuser) - async def _get_executor(self) -> MoviePilotApiExecutor: - """返回注入执行器,未注入时按当前可信 Agent 身份创建。""" - if self._executor is None: - user_id, username, is_admin = await self._resolve_api_identity() - self._executor = MoviePilotApiExecutor( + async def _get_executor( + self, + *, + require_system_admin: bool = False, + ) -> tuple[MoviePilotApiExecutor, bool]: + """返回当前 operation 的执行器及其管理员身份事实。""" + if self._executor is not None: + return self._executor, await self.is_admin_user() + user_id, username, is_admin = await self._resolve_api_identity( + require_system_admin=require_system_admin, + ) + return ( + MoviePilotApiExecutor( context=ApiExecutionContext( user_id=user_id, username=username, @@ -167,15 +198,16 @@ class MoviePilotApiTool(MoviePilotTool): channel=self._channel, source=self._source, ) - ) - return self._executor + ), + is_admin, + ) async def run( # type: ignore[override] self, operation_id: str, path_params: Optional[Dict[str, Any]] = None, query: Optional[Dict[str, Any]] = None, - body: Optional[Dict[str, Any]] = None, + body: Any = None, **kwargs: Any, ) -> str: """ @@ -198,9 +230,20 @@ class MoviePilotApiTool(MoviePilotTool): }, ensure_ascii=False, ) - try: - executor = await self._get_executor() + requires_system_admin = spec.required_role is PrincipalRole.SYSTEM_ADMIN + executor, is_admin = await self._get_executor( + require_system_admin=requires_system_admin, + ) + if requires_system_admin and not is_admin: + return json.dumps( + { + "success": False, + "error": "permission_denied", + "message": "This MoviePilot API operation requires a system administrator.", + }, + ensure_ascii=False, + ) return await executor.execute( operation_id, path_params=path_params, diff --git a/app/agent/tools/impl/service_operation.py b/app/agent/tools/impl/service.py similarity index 56% rename from app/agent/tools/impl/service_operation.py rename to app/agent/tools/impl/service.py index 9f2a79e57..f505fed16 100644 --- a/app/agent/tools/impl/service_operation.py +++ b/app/agent/tools/impl/service.py @@ -20,9 +20,17 @@ from app.runtime.settings import get_runtime_setting class DownloaderOperationInput(BaseModel): """下载器操作工具的运行时输入模型。""" - client: Optional[str] = Field(default=None, description="下载器实例名;省略时使用默认或唯一实例。") - action: str = Field(description="固定下载器 action;MCP inputSchema 会按 action 展示精确枚举和参数。") - arguments: Dict[str, Any] = Field(default_factory=dict, description="当前 action 的结构化参数对象。") + client: Optional[str] = Field( + default=None, + description="Configured downloader instance name; omit it to use the default or only enabled instance.", + ) + action: str = Field( + description="Exact downloader action. Select one action branch from the MCP input schema.", + ) + arguments: Dict[str, Any] = Field( + default_factory=dict, + description="Structured arguments declared by the selected downloader action.", + ) model_config = ConfigDict(extra="forbid") @@ -30,14 +38,36 @@ class DownloaderOperationInput(BaseModel): class MediaServerOperationInput(BaseModel): """媒体服务器操作工具的运行时输入模型。""" - server: Optional[str] = Field(default=None, description="媒体服务器实例名;省略时使用唯一启用实例。") - action: str = Field(description="固定媒体服务器 action;MCP inputSchema 会按 action 展示精确枚举和参数。") - arguments: Dict[str, Any] = Field(default_factory=dict, description="当前 action 的结构化参数对象。") + server: Optional[str] = Field( + default=None, + description="Configured media-server instance name; omit it to use the only enabled instance.", + ) + action: str = Field( + description="Exact media-server action. Select one action branch from the MCP input schema.", + ) + arguments: Dict[str, Any] = Field( + default_factory=dict, + description="Structured arguments declared by the selected media-server action.", + ) model_config = ConfigDict(extra="forbid") -@lru_cache(maxsize=2) +class DatabaseOperationInput(BaseModel): + """数据库操作工具的运行时输入模型。""" + + action: str = Field( + description="Exact database action. Select one action branch from the MCP input schema.", + ) + arguments: Dict[str, Any] = Field( + default_factory=dict, + description="Structured arguments declared by the selected database action.", + ) + + model_config = ConfigDict(extra="forbid") + + +@lru_cache(maxsize=3) def _load_action_contracts(script_path: str) -> dict[str, Any]: """从固定 Skill 脚本加载无配置副作用的 action 注册表。""" namespace = runpy.run_path(script_path) @@ -105,18 +135,23 @@ def _metadata_refresh_items_schema() -> dict[str, Any]: "items": { "type": "object", "properties": { - "title": {"type": "string", "description": "媒体标题。"}, + "title": {"type": "string", "description": "Media title used for recognition."}, "year": { "anyOf": [{"type": "string"}, {"type": "integer"}], - "description": "媒体年份。", + "description": "Release or premiere year used to disambiguate the title.", }, "type": { "type": "string", "enum": ["电影", "电视剧", "音乐"], - "description": "MoviePilot 媒体类型。", + "description": ( + "Exact MoviePilot media-type literal: 电影 (movie), 电视剧 (TV), or 音乐 (music)." + ), + }, + "category": {"type": "string", "description": "Optional MoviePilot library category."}, + "target_path": { + "type": "string", + "description": "Media file or directory path whose metadata should be refreshed.", }, - "category": {"type": "string", "description": "媒体分类。"}, - "target_path": {"type": "string", "description": "媒体文件或目录路径。"}, }, "additionalProperties": False, }, @@ -163,6 +198,11 @@ def _add_action_argument_rules(action: str, schema: dict[str, Any]) -> None: ] if action == "items.season_episodes": schema["anyOf"] = [{"required": ["item_id"]}, {"required": ["title"]}] + if action in {"query", "write"}: + schema["oneOf"] = [ + {"required": ["sql"], "not": {"required": ["file"]}}, + {"required": ["file"], "not": {"required": ["sql"]}}, + ] def _build_arguments_schema(action: str, spec: Any) -> dict[str, Any]: @@ -177,7 +217,7 @@ def _build_arguments_schema(action: str, spec: Any) -> dict[str, Any]: properties["items"]["description"] = contract["arguments"][0]["description"] schema: dict[str, Any] = { "type": "object", - "description": ";".join(contract.get("argument_rules") or []), + "description": " ".join(contract.get("argument_rules") or []), "properties": properties, "required": list(contract.get("required_arguments") or []), "additionalProperties": False, @@ -189,8 +229,8 @@ def _build_arguments_schema(action: str, spec: Any) -> dict[str, Any]: def _build_mcp_input_schema( *, actions: dict[str, Any], - selector_name: str, - selector_description: str, + selector_name: Optional[str], + selector_description: Optional[str], title: str, ) -> dict[str, Any]: """构建按 action 分支且可由外部 MCP Client 直接发现的输入合同。""" @@ -198,6 +238,15 @@ def _build_mcp_input_schema( branches = [] for action in action_names: contract = actions[action].to_dict(action) + properties = { + "action": {"type": "string", "const": action}, + "arguments": _build_arguments_schema(action, actions[action]), + } + if selector_name: + properties[selector_name] = { + "type": "string", + "description": selector_description or "", + } branches.append( { "type": "object", @@ -206,27 +255,28 @@ def _build_mcp_input_schema( f"{contract['description']} Effect: {contract['effect']}. " f"Providers: {', '.join(contract['providers'])}." ), - "properties": { - selector_name: {"type": "string", "description": selector_description}, - "action": {"type": "string", "const": action}, - "arguments": _build_arguments_schema(action, actions[action]), - }, + "properties": properties, "required": ["action", "arguments"], "additionalProperties": False, } ) + properties = { + "action": {"type": "string", "enum": action_names}, + "arguments": { + "type": "object", + "description": "Arguments must match the oneOf branch selected by action.", + }, + } + if selector_name: + properties[selector_name] = { + "type": "string", + "description": selector_description or "", + } return { "$schema": "https://json-schema.org/draft/2020-12/schema", "title": title, "type": "object", - "properties": { - selector_name: {"type": "string", "description": selector_description}, - "action": {"type": "string", "enum": action_names}, - "arguments": { - "type": "object", - "description": "参数必须匹配所选 action 的 oneOf 分支。", - }, - }, + "properties": properties, "required": ["action", "arguments"], "oneOf": branches, } @@ -283,18 +333,21 @@ class _ServiceOperationTool(MoviePilotTool): """固定 Skill 脚本的 MCP-only 安全包装基类。""" require_admin: bool = True + tags: list[str] = [ToolTag.Admin] _relative_script: ClassVar[str] - _selector_name: ClassVar[str] - _selector_flag: ClassVar[str] + _selector_name: ClassVar[Optional[str]] + _selector_flag: ClassVar[Optional[str]] _blocking_bucket: ClassVar[str] def get_mcp_input_schema(self) -> dict[str, Any]: """返回保留 action 条件分支的完整 MCP JSON Schema。""" root_path = Path(get_runtime_setting("ROOT_PATH")) actions = _load_action_contracts(str(root_path / self._relative_script)) - selector_description = self.args_schema.model_json_schema()["properties"][self._selector_name][ - "description" - ] + selector_description = None + if self._selector_name: + selector_description = self.args_schema.model_json_schema()["properties"][self._selector_name][ + "description" + ] return _build_mcp_input_schema( actions=actions, selector_name=self._selector_name, @@ -383,9 +436,135 @@ class MediaServerOperationTool(_ServiceOperationTool): ) +def _run_database_script(arguments: Dict[str, Any], *, root_path: Path) -> dict[str, Any]: + """不经 shell 调用数据库 Skill 脚本,并返回结构化结果。""" + script_path = root_path / "skills/database-operation/scripts/mp-db.py" + action = str(arguments.get("action") or "") + action_arguments = arguments.get("arguments") or {} + if not isinstance(action_arguments, dict): + raise ValueError("数据库 action arguments 必须是对象") + + contracts = _load_action_contracts(str(script_path)) + contract = contracts.get(action) + if contract is None: + raise ValueError(f"未知数据库 action: {action}") + contract_data = contract.to_dict(action) + declared_arguments = {item["name"]: item for item in contract_data["arguments"]} + unknown_arguments = sorted(set(action_arguments) - set(declared_arguments)) + if unknown_arguments: + raise ValueError(f"数据库 action 存在未知参数: {', '.join(unknown_arguments)}") + missing_arguments = [ + name + for name in contract_data["required_arguments"] + if name not in action_arguments + ] + if missing_arguments: + raise ValueError(f"数据库 action 缺少必填参数: {', '.join(missing_arguments)}") + if action in {"query", "write"}: + has_sql = bool(action_arguments.get("sql")) + has_file = bool(action_arguments.get("file")) + if has_sql == has_file: + raise ValueError("数据库 query/write 必须在 sql 与 file 中二选一") + for field_name in ("sql", "file"): + if field_name in action_arguments and ( + not isinstance(action_arguments[field_name], str) + or not action_arguments[field_name].strip() + ): + raise ValueError(f"数据库参数 {field_name} 必须是非空 string") + if action == "schema" and ( + not isinstance(action_arguments.get("table_name"), str) + or not action_arguments["table_name"].strip() + ): + raise ValueError("数据库参数 table_name 必须是非空 string") + if action == "query": + limit = action_arguments.get("limit", 100) + if isinstance(limit, bool) or not isinstance(limit, int) or not 1 <= limit <= 200: + raise ValueError("数据库参数 limit 必须是 1 到 200 的 integer") + write_flag = action_arguments.get("write", False) + if not isinstance(write_flag, bool): + raise ValueError("数据库参数 write 必须是 boolean") + + command = [sys.executable, str(script_path), action] + if action == "schema": + command.append(str(action_arguments.get("table_name") or "")) + elif action in {"query", "write"}: + sql = action_arguments.get("sql") + sql_file = action_arguments.get("file") + if sql: + command.append(str(sql)) + if sql_file: + command.extend(["--file", str(sql_file)]) + if action == "query": + command.extend(["--limit", str(action_arguments.get("limit", 100))]) + if action_arguments.get("write") is True: + command.append("--write") + + completed = subprocess.run( + command, + cwd=root_path, + capture_output=True, + text=True, + check=False, + ) + if completed.returncode != 0: + error_text = completed.stderr.strip() or completed.stdout.strip() or "数据库脚本执行失败" + return {"success": False, "error": "database_operation_failed", "message": error_text} + return _parse_script_payload(completed.stdout) + + +class DatabaseOperationTool(_ServiceOperationTool): + """外部 HTTP/MCP 调用 MoviePilot 数据库 Skill 的结构化工具。""" + + name: str = "database_operation" + description: str = ( + "Inspect or explicitly modify the configured MoviePilot database. The input schema " + "contains exact branches for tables, schema, query, and write, including field types, " + "defaults, mutually exclusive SQL sources, and safety rules." + ) + tags: list[str] = [ToolTag.Admin] + args_schema: Type[BaseModel] = DatabaseOperationInput + _relative_script = "skills/database-operation/scripts/mp-db.py" + _selector_name = None + _selector_flag = None + _blocking_bucket = "db" + + def get_mcp_input_schema(self) -> dict[str, Any]: + """返回数据库 action 的完整 MCP JSON Schema。""" + root_path = Path(get_runtime_setting("ROOT_PATH")) + actions = _load_action_contracts(str(root_path / self._relative_script)) + return _build_mcp_input_schema( + actions=actions, + selector_name=None, + selector_description=None, + title=self.name, + ) + + async def run( + self, + action: str, + arguments: Optional[Dict[str, Any]] = None, + ) -> str: + """ + 执行一次数据库 Skill 操作。 + + :param action: tables、schema、query 或 write + :param arguments: 当前 action 的结构化参数 + :return: 结构化数据库结果 + """ + payload = await self.run_blocking( + self._blocking_bucket, + _run_database_script, + root_path=Path(get_runtime_setting("ROOT_PATH")), + arguments={"action": action, "arguments": arguments or {}}, + ) + return json.dumps(payload, ensure_ascii=False, indent=2) + + __all__ = [ "DownloaderOperationInput", "DownloaderOperationTool", + "DatabaseOperationInput", + "DatabaseOperationTool", "MediaServerOperationInput", "MediaServerOperationTool", ] diff --git a/app/api/endpoints/plugin.py b/app/api/endpoints/plugin.py index 8117891e3..c9ab9f0c7 100644 --- a/app/api/endpoints/plugin.py +++ b/app/api/endpoints/plugin.py @@ -4,7 +4,7 @@ from typing import Annotated, Any, Dict, List, Optional import aiofiles from anyio import Path as AsyncPath -from fastapi import Depends, Header, HTTPException, Security +from fastapi import Depends, Header, HTTPException, Query, Security from starlette import status from starlette.responses import StreamingResponse @@ -38,7 +38,7 @@ from app.application.plugin.folders import ( remove_plugin_from_folders, ) from app.application.plugin.gateway import get_plugin_install_service -from app.application.plugin.management import get_plugin_snapshot +from app.application.plugin.management import get_plugin_snapshot, search_plugin_candidates from app.application.plugin.rating import ( PluginNotInstalledError, get_plugin_rating_service, @@ -183,14 +183,19 @@ async def all_plugins( _: ApiPrincipal = Depends(get_current_active_superuser_async), state: Optional[str] = "all", force: bool = False, + query: Optional[str] = None, + max_results: Annotated[int, Query(ge=1, le=200)] = 50, ) -> List[_SchemaPlugin]: """ - 查询所有插件清单,包括本地插件和在线插件,插件状态:installed, market, all + 查询插件清单,并支持 Agent 使用关键字和有界结果完成精确选择。 """ - return await get_plugin_catalog_query().query( + plugins = await get_plugin_catalog_query().query( state=state or "all", force=force, ) + if query: + plugins = [item["plugin"] for item in search_plugin_candidates(query, plugins)] + return plugins[:max_results] @router.get("/installed", summary="已安装插件", response_model=List[str]) @@ -775,11 +780,14 @@ async def get_plugin_folders( @router.post("/folders", summary="保存插件文件夹配置", response_model=_SchemaResponse[None]) -async def save_plugin_folders(folders: dict, _: ApiPrincipal = Depends(get_current_active_superuser_async)) -> Any: +async def save_plugin_folders( + folders: _SchemaPluginFoldersData, + _: ApiPrincipal = Depends(get_current_active_superuser_async), +) -> Any: """ 保存插件文件夹分组配置 """ - result = await get_plugin_folder_service().save(folders) + result = await get_plugin_folder_service().save(folders.root) return _SchemaResponse(success=result.success, message=result.message) diff --git a/app/api/endpoints/rule.py b/app/api/endpoints/rule.py index 526fcf7ce..ed4549c6a 100644 --- a/app/api/endpoints/rule.py +++ b/app/api/endpoints/rule.py @@ -1,11 +1,14 @@ """过滤规则和规则组管理 API。""" -from typing import Any, Optional +from typing import Annotated, Any, Optional -from fastapi import Depends +from fastapi import Depends, Query from app.api.context import get_host_runtime -from app.api.dependencies.auth import get_current_active_superuser_async +from app.api.dependencies.auth import ( + get_current_active_superuser_async, + get_current_active_user_async, +) from app.api.principal import ApiPrincipal from app.api.response import ResponseAPIRouter from app.application.filtering import FilterRuleService @@ -43,8 +46,16 @@ def _service(runtime: HostRuntime) -> FilterRuleService: response_model=_SchemaResponse[_SchemaJsonObject], ) async def query_builtin_rules( - rule_ids: Optional[list[str]] = None, - _: ApiPrincipal = Depends(get_current_active_superuser_async), + rule_ids: Annotated[ + Optional[list[str]], + Query( + description=( + "Exact built-in rule IDs to return. Repeat rule_ids in the query string; " + "omit it to list every built-in rule." + ) + ), + ] = None, + _: ApiPrincipal = Depends(get_current_active_user_async), ) -> _SchemaResponse[Any]: """返回内置规则及规则串语法。""" return _SchemaResponse( @@ -59,9 +70,17 @@ async def query_builtin_rules( response_model=_SchemaResponse[_SchemaJsonObject], ) async def query_custom_rules( - rule_ids: Optional[list[str]] = None, + rule_ids: Annotated[ + Optional[list[str]], + Query( + description=( + "Exact custom rule IDs to return. Repeat rule_ids in the query string; " + "omit it to list every custom rule." + ) + ), + ] = None, include_group_refs: bool = True, - _: ApiPrincipal = Depends(get_current_active_superuser_async), + _: ApiPrincipal = Depends(get_current_active_user_async), ) -> _SchemaResponse[Any]: """返回自定义规则和可选规则组引用。""" return _SchemaResponse( @@ -79,9 +98,17 @@ async def query_custom_rules( response_model=_SchemaResponse[_SchemaJsonObject], ) async def query_rule_groups( - group_names: Optional[list[str]] = None, + group_names: Annotated[ + Optional[list[str]], + Query( + description=( + "Exact rule-group names to return. Repeat group_names in the query string; " + "omit it to list every group." + ) + ), + ] = None, include_usage: bool = True, - _: ApiPrincipal = Depends(get_current_active_superuser_async), + _: ApiPrincipal = Depends(get_current_active_user_async), runtime: HostRuntime = Depends(get_host_runtime), ) -> _SchemaResponse[Any]: """返回规则组、解析层级和可选引用位置。""" diff --git a/app/api/endpoints/site.py b/app/api/endpoints/site.py index f6f5abd3d..412d420d6 100644 --- a/app/api/endpoints/site.py +++ b/app/api/endpoints/site.py @@ -1,4 +1,4 @@ -from typing import Annotated, Any, Dict, List, Optional +from typing import Annotated, Any, Dict, List, Literal, Optional from fastapi import Depends, HTTPException @@ -9,6 +9,7 @@ from app.api.dependencies.auth import ( get_current_active_manage_user_async, get_current_active_superuser, get_current_active_superuser_async, + get_current_active_user_async, ) from app.api.dependencies.site import ( get_site_mutation_command, @@ -38,6 +39,7 @@ from app.schemas.site import SiteCategory as _SchemaSiteCategory from app.schemas.site import SiteCookieUpdate as _SchemaSiteCookieUpdate from app.schemas.site import SiteIconData as _SchemaSiteIconData from app.schemas.site import SiteMappingData as _SchemaSiteMappingData +from app.schemas.site import SitePriorityUpdate as _SchemaSitePriorityUpdate from app.schemas.site import SiteStatistic as _SchemaSiteStatistic from app.schemas.site import SiteUserData as _SchemaSiteUserData from app.schemas.system import TorrentInfo as _SchemaTorrentInfo @@ -48,6 +50,39 @@ from app.schemas.workflow import Site as _SchemaSite router = ResponseAPIRouter() +def _project_agent_site(site: Any, *, include_secrets: bool) -> dict[str, JsonData]: + """构造旧 Agent 查询语义的站点安全投影,普通用户不返回认证凭据。""" + projected: dict[str, JsonData] = { + "id": site.id, + "name": site.name, + "domain": site.domain, + "url": site.url, + "pri": site.pri, + "is_active": site.is_active, + "downloader": site.downloader, + "ua": site.ua, + "proxy": site.proxy, + "filter": site.filter, + "render": site.render, + "public": site.public, + "note": site.note, + "limit_interval": site.limit_interval, + "limit_count": site.limit_count, + "limit_seconds": site.limit_seconds, + "timeout": site.timeout, + } + if include_secrets: + projected.update( + { + "rss": site.rss, + "cookie": site.cookie, + "apikey": site.apikey, + "token": site.token, + } + ) + return projected + + def _indexer_supports_media_type(indexer: dict, media_type: MediaType) -> bool: """ 判断站点索引器是否支持指定媒体类型。 @@ -95,6 +130,36 @@ async def read_sites( return await query.list_ordered() +@router.get( + "/agent", + summary="查询 Agent 可用站点", + response_model=List[_SchemaJsonObject], +) +async def read_agent_sites( + status: Literal["active", "inactive", "all"] = "all", + name: Optional[str] = None, + query: SiteQueryService = Depends(get_site_query_service), + current_user: Any = Depends(get_current_active_user_async), +) -> List[dict[str, JsonData]]: + """按旧 Agent 过滤语义返回站点,非超级管理员自动剔除认证字段。""" + sites = await query.list_ordered() + results = [] + for site in sites: + if status == "active" and not site.is_active: + continue + if status == "inactive" and site.is_active: + continue + if name and name.lower() not in (site.name or "").lower(): + continue + results.append( + _project_agent_site( + site, + include_secrets=bool(current_user.is_superuser), + ) + ) + return results + + @router.get( "/media/{media_type}", summary="按媒体类型获取可搜索站点", @@ -208,14 +273,14 @@ async def reset( "/priorities", summary="批量更新站点优先级", response_model=_SchemaResponse[None] ) async def update_sites_priority( - priorities: List[Dict[str, JsonData]], + priorities: List[_SchemaSitePriorityUpdate], command: SiteMutationCommand = Depends(get_site_mutation_command), _: ApiPrincipal = Depends(get_current_active_manage_user_async), ) -> Any: """ 批量更新站点优先级 """ - result = await command.update_priorities(priorities) + result = await command.update_priorities([priority.model_dump() for priority in priorities]) return _SchemaResponse(success=result.success, message=result.message) diff --git a/app/api/endpoints/storage.py b/app/api/endpoints/storage.py index a812deef0..da8627631 100644 --- a/app/api/endpoints/storage.py +++ b/app/api/endpoints/storage.py @@ -10,6 +10,7 @@ from starlette.responses import FileResponse, Response from app.api.dependencies.auth import ( get_current_active_manage_user, get_current_active_superuser, + get_current_active_user, ) from app.api.principal import ApiPrincipal from app.api.response import ResponseAPIRouter @@ -123,6 +124,16 @@ def list_files( :param _: token :return: 所有目录和文件 """ + return _list_files(fileitem=fileitem, sort=sort, keyword=keyword) + + +def _list_files( + *, + fileitem: _SchemaFileItem, + sort: Optional[str], + keyword: Optional[str], +) -> List[_SchemaFileItem]: + """执行目录查询、通配符过滤和稳定排序,供管理端与 Agent 安全入口复用。""" file_list = StorageChain().list_files(fileitem) if file_list: if keyword: @@ -132,7 +143,22 @@ def list_files( file_list.sort(key=lambda x: text_tools.natural_sort_key(x.name or "")) else: file_list.sort(key=lambda x: x.modify_time or -math.inf, reverse=True) - return file_list + return file_list or [] + + +@router.post( + "/agent/list", + summary="查询 Agent 可用目录和文件", + response_model=List[_SchemaFileItem], +) +def list_agent_files( + fileitem: _SchemaFileItem, + sort: Optional[str] = "updated_at", + keyword: Optional[str] = None, + _: Any = Depends(get_current_active_user), +) -> List[_SchemaFileItem]: + """保留旧 Agent 普通用户目录读取能力,不开放创建、改名或删除入口。""" + return _list_files(fileitem=fileitem, sort=sort, keyword=keyword) @router.post("/mkdir", summary="创建目录", response_model=_SchemaResponse[None]) diff --git a/app/api/endpoints/system.py b/app/api/endpoints/system.py index e8b53d652..455878a1e 100644 --- a/app/api/endpoints/system.py +++ b/app/api/endpoints/system.py @@ -7,7 +7,7 @@ from typing import Annotated, Any, Optional, Union import anyio import pillow_avif # noqa: F401 # pylint: disable=unused-import # AVIF 注册副作用 -from fastapi import Body, Depends, Header, HTTPException, Request, Response +from fastapi import Body, Depends, Header, HTTPException, Query, Request, Response from fastapi.responses import StreamingResponse from app.adapters.web.security.access import verify_apitoken, verify_resource_token, verify_token @@ -620,15 +620,43 @@ async def set_setting( @router.get( # type: ignore[misc] "/settings", - summary="统一查询系统设置", + summary="Discover or read registered system settings", response_model=_SchemaResponse[_SchemaJsonObject], ) async def query_settings( - setting_key: Optional[str] = None, - group: Optional[str] = "all", - keyword: Optional[str] = None, - include_values: Optional[bool] = None, - show_secrets: bool = False, + setting_key: Annotated[ + Optional[str], + Query( + description=( + "Exact setting key. Accepts Settings field names such as APP_DOMAIN or LLM_MODEL, " + "SystemConfigKey values or enum names such as Downloaders or MediaServers, and " + "aliases that resolve to one unique setting. Omit it to discover settings." + ) + ), + ] = None, + group: Annotated[ + Optional[str], + Query( + description=( + "Discovery group used when setting_key is omitted. Supported groups are all, settings, systemconfig, downloaders, " + "media_servers, notifications, notification_switches, storages, directories, " + "search_sites, subscribe_sites, site_auth, ai_agent, filter_rules, " + "subscribe_defaults, plugins, customization, transfer, scraping, and misc." + ) + ), + ] = "all", + keyword: Annotated[ + Optional[str], + Query(description="Case-insensitive substring used to discover matching keys, groups, or labels."), + ] = None, + include_values: Annotated[ + Optional[bool], + Query(description="Return full values. Defaults to true for one exact key and false for discovery results."), + ] = None, + show_secrets: Annotated[ + bool, + Query(description="Return unredacted secret values. Defaults to false and remains confirmation-protected."), + ] = False, _: ApiPrincipal = Depends(get_current_active_superuser_async), runtime: HostRuntime = Depends(get_host_runtime), ) -> _SchemaResponse[Any]: @@ -652,7 +680,7 @@ async def query_settings( @router.post( # type: ignore[misc] "/settings", - summary="统一更新系统设置", + summary="Update one registered system setting", response_model=_SchemaResponse[_SchemaJsonObject], ) async def update_settings( diff --git a/app/api/endpoints/workflow.py b/app/api/endpoints/workflow.py index 1ba0bb4f7..9b5827e72 100644 --- a/app/api/endpoints/workflow.py +++ b/app/api/endpoints/workflow.py @@ -1,4 +1,4 @@ -from typing import Any, List, Optional +from typing import Any, List, Literal, Optional from fastapi import Depends @@ -21,6 +21,7 @@ from app.application.workflow import ( get_workflow_manager, ) from app.chain.workflow import WorkflowChain +from app.schemas.common import JsonObject as _SchemaJsonObject from app.schemas.response import Response as _SchemaResponse from app.schemas.types import EVENT_TYPE_NAMES, EventType from app.schemas.workflow import NameValueOption as _SchemaNameValueOption @@ -42,6 +43,47 @@ async def list_workflows( return await query.list() +@router.get( + "/agent", + summary="查询 Agent 可用工作流", + response_model=List[_SchemaJsonObject], +) +async def list_agent_workflows( + state: Literal["W", "R", "P", "S", "F", "all"] = "all", + name: Optional[str] = None, + trigger_type: Literal["timer", "event", "manual", "all"] = "all", + query: WorkflowQueryService = Depends(get_workflow_query_service), + _: Any = Depends(get_current_active_manage_user_async), +) -> List[dict[str, Any]]: + """按旧 Agent 过滤与字段投影返回工作流列表,避免输出完整动作上下文。""" + workflows = await query.list() + results = [] + for workflow in workflows: + if state != "all" and workflow.state != state: + continue + normalized_trigger = workflow.trigger_type or "timer" + if trigger_type != "all" and normalized_trigger != trigger_type: + continue + if name and name.lower() not in (workflow.name or "").lower(): + continue + results.append( + { + "id": workflow.id, + "name": workflow.name, + "description": workflow.description, + "trigger_type": normalized_trigger, + "state": workflow.state, + "run_count": workflow.run_count, + "timer": workflow.timer, + "event_type": workflow.event_type, + "add_time": workflow.add_time, + "last_time": workflow.last_time, + "current_action": workflow.current_action, + } + ) + return results + + @router.post("/", summary="创建工作流", response_model=_SchemaResponse[None]) async def create_workflow( workflow: _SchemaWorkflow, @@ -235,14 +277,17 @@ async def get_workflow( @router.put("/{workflow_id}", summary="更新工作流", response_model=_SchemaResponse[None]) def update_workflow( + workflow_id: int, workflow: _SchemaWorkflow, command: WorkflowMutationCommand = Depends(get_workflow_mutation_command), _: Any = Depends(get_current_active_manage_user), ) -> Any: """ - 更新工作流 + 更新工作流,路径 ID 是本次更新的唯一目标。 """ - result = command.update(workflow.model_dump()) + workflow_data = workflow.model_dump() + workflow_data["id"] = workflow_id + result = command.update(workflow_data) return _SchemaResponse(success=result.success, message=result.message) diff --git a/app/application/settings.py b/app/application/settings.py index 0e461cb49..32c984f05 100644 --- a/app/application/settings.py +++ b/app/application/settings.py @@ -25,141 +25,142 @@ class SettingSpec: source: str group: str label: str + declared_type: str = "unknown" systemconfig_key: Optional[SystemConfigKey] = None SYSTEMCONFIG_SETTING_METADATA = { SystemConfigKey.Downloaders.value: { "group": "downloaders", - "label": "下载器配置", + "label": "Downloader configurations", }, SystemConfigKey.MediaServers.value: { "group": "media_servers", - "label": "媒体服务器配置", + "label": "Media-server configurations", }, SystemConfigKey.Notifications.value: { "group": "notifications", - "label": "消息通知配置", + "label": "Notification-channel configurations", }, SystemConfigKey.NotificationSwitchs.value: { "group": "notification_switches", - "label": "通知场景开关", + "label": "Notification-scenario switches", }, SystemConfigKey.Directories.value: { "group": "directories", - "label": "目录配置", + "label": "Directory configurations", }, SystemConfigKey.Storages.value: { "group": "storages", - "label": "存储配置", + "label": "Storage configurations", }, SystemConfigKey.IndexerSites.value: { "group": "search_sites", - "label": "搜索站点范围", + "label": "Search-site scope", }, SystemConfigKey.RssSites.value: { "group": "subscribe_sites", - "label": "订阅站点范围", + "label": "Subscription-site scope", }, SystemConfigKey.UserSiteAuthParams.value: { "group": "site_auth", - "label": "站点认证参数", + "label": "Site authentication parameters", }, SystemConfigKey.AIAgentConfig.value: { "group": "ai_agent", - "label": "AI 智能体配置", + "label": "AI Agent configuration", }, SystemConfigKey.AIAgentMcpServers.value: { "group": "ai_agent", - "label": "AI 智能体外部 MCP 服务器", + "label": "AI Agent external MCP servers", }, SystemConfigKey.CustomIdentifiers.value: { "group": "custom_identifiers", - "label": "自定义识别词", + "label": "Custom recognition identifiers", }, SystemConfigKey.EpisodeFormatRuleTable.value: { "group": "transfer", - "label": "集数定位规则词表", + "label": "Episode-position rule table", }, SystemConfigKey.CustomReleaseGroups.value: { "group": "customization", - "label": "自定义制作组/字幕组", + "label": "Custom release and subtitle groups", }, SystemConfigKey.Customization.value: { "group": "customization", - "label": "自定义占位符", + "label": "Custom placeholders", }, SystemConfigKey.TransferExcludeWords.value: { "group": "transfer", - "label": "整理屏蔽词", + "label": "Transfer exclusion words", }, SystemConfigKey.TorrentsPriority.value: { "group": "filter_rules", - "label": "种子优先级规则", + "label": "Torrent-priority rules", }, SystemConfigKey.CustomFilterRules.value: { "group": "filter_rules", - "label": "用户自定义规则", + "label": "User-defined filter rules", }, SystemConfigKey.UserFilterRuleGroups.value: { "group": "filter_rules", - "label": "用户规则组", + "label": "User filter-rule groups", }, SystemConfigKey.SearchFilterRuleGroups.value: { "group": "filter_rules", - "label": "搜索默认过滤规则组", + "label": "Default search filter-rule groups", }, SystemConfigKey.SubscribeFilterRuleGroups.value: { "group": "filter_rules", - "label": "订阅默认过滤规则组", + "label": "Default subscription filter-rule groups", }, SystemConfigKey.BestVersionFilterRuleGroups.value: { "group": "filter_rules", - "label": "洗版默认过滤规则组", + "label": "Default upgrade filter-rule groups", }, SystemConfigKey.SubscribeDefaultParams.value: { "group": "subscribe_defaults", - "label": "订阅默认参数", + "label": "Default subscription parameters", }, SystemConfigKey.DefaultMovieSubscribeConfig.value: { "group": "subscribe_defaults", - "label": "默认电影订阅规则", + "label": "Default movie subscription rules", }, SystemConfigKey.DefaultTvSubscribeConfig.value: { "group": "subscribe_defaults", - "label": "默认电视剧订阅规则", + "label": "Default TV subscription rules", }, SystemConfigKey.DefaultMusicSubscribeConfig.value: { "group": "subscribe_defaults", - "label": "默认音乐订阅规则", + "label": "Default music subscription rules", }, SystemConfigKey.UserInstalledPlugins.value: { "group": "plugins", - "label": "已安装插件列表", + "label": "Installed plugin list", }, SystemConfigKey.PluginFolders.value: { "group": "plugins", - "label": "插件文件夹分组配置", + "label": "Plugin folder grouping", }, SystemConfigKey.PluginInstallReport.value: { "group": "plugins", - "label": "插件安装统计", + "label": "Plugin installation report", }, SystemConfigKey.NotificationSendTime.value: { "group": "notifications", - "label": "通知发送时间", + "label": "Notification delivery time", }, SystemConfigKey.NotificationTemplates.value: { "group": "notifications", - "label": "通知模板", + "label": "Notification templates", }, SystemConfigKey.ScrapingSwitchs.value: { "group": "scraping", - "label": "刮削开关设置", + "label": "Metadata-scraping switches", }, SystemConfigKey.FollowSubscribers.value: { "group": "subscribe_sites", - "label": "Follow 订阅分享者", + "label": "Followed subscription publishers", }, } @@ -258,6 +259,12 @@ def _resolve_core_setting_group(key: str) -> str: return "settings" +def _format_declared_type(annotation: Any) -> str: + """把 Pydantic 字段注解转换为稳定且便于 Agent 阅读的类型文本。""" + rendered = str(annotation).replace("typing.", "") + return rendered.replace("", "") + + def _build_specs() -> tuple[dict[str, SettingSpec], dict[str, SettingSpec]]: core_specs = { key: SettingSpec( @@ -265,8 +272,9 @@ def _build_specs() -> tuple[dict[str, SettingSpec], dict[str, SettingSpec]]: source="settings", group=_resolve_core_setting_group(key), label=key, + declared_type=_format_declared_type(field.annotation), ) - for key in Settings.model_fields.keys() + for key, field in Settings.model_fields.items() } system_specs = {} for item in SystemConfigKey: @@ -276,6 +284,11 @@ def _build_specs() -> tuple[dict[str, SettingSpec], dict[str, SettingSpec]]: source="systemconfig", group=metadata.get("group", "misc"), label=metadata.get("label", item.value), + declared_type=( + "list[object]" + if item.value in LIST_ITEM_MATCH_FIELD_DEFAULTS + else "JSON-compatible value" + ), systemconfig_key=item, ) return core_specs, system_specs @@ -429,6 +442,29 @@ class SystemSettingsService: summary["value_preview"] = value return summary + @staticmethod + def _definition(spec: SettingSpec, value: Any, *, sensitive: bool) -> dict[str, Any]: + """返回当前设置值形状、持久化位置和允许的更新操作。""" + default_match_field = get_default_list_match_field(spec.key) + if isinstance(value, list) or (value is None and default_match_field): + update_operations = ["replace", "upsert_list_item", "remove_list_item"] + value_shape = "list" + elif isinstance(value, dict): + update_operations = ["replace", "merge_dict"] + value_shape = "object" + else: + update_operations = ["replace"] + value_shape = type(value).__name__ if value is not None else "unknown" + return { + "declared_type": spec.declared_type, + "value_shape": value_shape, + "nullable": value is None, + "sensitive": sensitive, + "update_operations": update_operations, + "default_match_field": default_match_field, + "persistence": "app.env" if spec.source == "settings" else "database:systemconfig", + } + def query( self, *, @@ -466,6 +502,11 @@ class SystemSettingsService: "source": spec.source, "group": spec.group, "label": spec.label, + "definition": self._definition( + spec, + value, + sensitive=should_redact_setting(spec, value), + ), **self._summarize(response_value, redacted=redacted), } if should_include_values: diff --git a/app/schemas/exports.py b/app/schemas/exports.py index e33695e5f..7216e543c 100644 --- a/app/schemas/exports.py +++ b/app/schemas/exports.py @@ -359,6 +359,7 @@ SCHEMA_EXPORTS = { 'SiteEventData': ('app.schemas.event', 'SiteEventData'), 'SiteIconData': ('app.schemas.site', 'SiteIconData'), 'SiteMappingData': ('app.schemas.site', 'SiteMappingData'), + 'SitePriorityUpdate': ('app.schemas.site', 'SitePriorityUpdate'), 'SiteStatistic': ('app.schemas.site', 'SiteStatistic'), 'SiteUnreadMessage': ('app.schemas.site', 'SiteUnreadMessage'), 'SiteUserData': ('app.schemas.site', 'SiteUserData'), diff --git a/app/schemas/plugin.py b/app/schemas/plugin.py index 33ce73d8f..62a5c9285 100644 --- a/app/schemas/plugin.py +++ b/app/schemas/plugin.py @@ -405,19 +405,26 @@ class PluginFolderConfigData(BaseModel): """新版插件文件夹配置(对象格式,含展示配置与插件列表)。""" # 文件夹内插件 ID 列表 - plugins: List[str] = Field(default_factory=list) + plugins: List[str] = Field( + default_factory=list, + description="Ordered installed plugin IDs assigned to this folder.", + ) # 文件夹排序值 - order: Optional[int] = None + order: Optional[int] = Field(default=None, description="Folder display-order value.") # 文件夹图标 - icon: Optional[str] = None + icon: Optional[str] = Field(default=None, description="Optional folder icon name.") # 文件夹颜色 - color: Optional[str] = None + color: Optional[str] = Field(default=None, description="Optional folder foreground color.") # 文件夹渐变 - gradient: Optional[str] = None + gradient: Optional[str] = Field(default=None, description="Optional folder gradient definition.") # 文件夹背景 - background: Optional[str] = None + background: Optional[str] = Field(default=None, description="Optional folder background color or style.") # 是否显示图标(前端字段为驼峰命名) - show_icon: Optional[bool] = Field(default=None, alias="showIcon") + show_icon: Optional[bool] = Field( + default=None, + alias="showIcon", + description="Whether the frontend should display the folder icon.", + ) class PluginFoldersData(RootModel[Dict[str, Union[List[str], PluginFolderConfigData]]]): diff --git a/app/schemas/site.py b/app/schemas/site.py index cca6229bd..e53a36543 100644 --- a/app/schemas/site.py +++ b/app/schemas/site.py @@ -1,10 +1,9 @@ -from typing import Optional, Union, Dict +from typing import Dict, Optional, Union -from pydantic import BaseModel, Field, ConfigDict, RootModel +from pydantic import BaseModel, ConfigDict, Field, RootModel from app.schemas.common import JsonData - SiteUnreadMessage = Union[ tuple[Optional[str], Optional[str], Optional[str]], tuple[Optional[str], Optional[str], Optional[str], Optional[str]], @@ -61,6 +60,13 @@ class Site(BaseModel): model_config = ConfigDict(from_attributes=True) +class SitePriorityUpdate(BaseModel): + """站点批量优先级更新项。""" + + id: int = Field(..., description="Persistent site ID returned by site.list.") + pri: int = Field(..., description="Replacement site search priority value.") + + class SiteStatistic(BaseModel): """单个站点的访问成功率与耗时统计。""" diff --git a/app/schemas/system.py b/app/schemas/system.py index f74c16f5e..df22e9221 100644 --- a/app/schemas/system.py +++ b/app/schemas/system.py @@ -149,17 +149,47 @@ class SystemEnvironmentUpdateData(BaseModel): class SystemSettingsUpdateRequest(BaseModel): # type: ignore[misc] """统一系统设置更新请求。""" - setting_key: str - value: Any = None + setting_key: str = Field( + description=( + "Exact setting key. Accepts a Settings field name, a SystemConfigKey value or enum name, " + "or an alias that resolves to one unique setting. Call config.system.get with group or " + "keyword first when the key is unknown." + ) + ) + value: Any = Field( + default=None, + description=( + "New value or list item. For replace, send the complete value. For merge_dict, send the " + "object fragment to merge. For upsert_list_item or remove_list_item, send one object or scalar item." + ), + ) operation: Literal[ "replace", "merge_dict", "upsert_list_item", "remove_list_item", - ] = "replace" - remove_keys: list[str] = Field(default_factory=list) - match_field: Optional[str] = None - match_value: Any = None + ] = Field( + default="replace", + description=( + "replace overwrites the complete value; merge_dict shallow-merges an object; " + "upsert_list_item inserts or replaces one matched list item; remove_list_item removes one matched list item." + ), + ) + remove_keys: list[str] = Field( + default_factory=list, + description="Object keys to remove after merge_dict applies the supplied value.", + ) + match_field: Optional[str] = Field( + default=None, + description=( + "Object field used to match a list item. Downloaders, MediaServers, Notifications, Directories, " + "and Storages default to name; NotificationSwitchs defaults to type. Supply it for other object lists." + ), + ) + match_value: Any = Field( + default=None, + description="Value compared against match_field. If omitted, use value[match_field]; scalar lists use value directly.", + ) class CustomIdentifiersUpdateRequest(BaseModel): # type: ignore[misc] diff --git a/docs/architecture-overview.md b/docs/architecture-overview.md index fc56ce482..62efb5efe 100644 --- a/docs/architecture-overview.md +++ b/docs/architecture-overview.md @@ -754,8 +754,8 @@ flowchart LR | 指标 | 当前值 | |---|---:| -| Python 模块 | 917 | -| 内部导入边 | 7,671 | +| Python 模块 | 919 | +| 内部导入边 | 7,680 | | 非平凡 SCC | 1(精确 containment 的 TMDB 移植包环) | | Application / Chain 具体 Adapter 直连 | 0 / 0 | | Direct egress | 53(债务已清零,53 条精确 containment) | diff --git a/docs/architecture/agent-api-surface-audit.json b/docs/architecture/agent-api-surface-audit.json new file mode 100644 index 000000000..d83a65e60 --- /dev/null +++ b/docs/architecture/agent-api-surface-audit.json @@ -0,0 +1,4973 @@ +{ + "disposition_counts": { + "alternate-auth-duplicate": 11, + "consolidated": 72, + "gateway": 200, + "provider-skill": 11, + "stream_or_binary": 10, + "transport_or_identity": 66, + "ui_presentation": 5 + }, + "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": 201, + "gateway_operation_count": 203, + "matched_gateway_http_route_count": 200, + "openapi_operation_count": 375, + "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 compatible messages", + "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/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/{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": "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": "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": "GET", + "operation_ids": [ + "transfer.history.clear" + ], + "owner": "moviepilot-api", + "path": "/api/v1/history/empty/transfer", + "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": "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": "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": "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": "gateway", + "method": "GET", + "operation_ids": [ + "media.categories" + ], + "owner": "moviepilot-api", + "path": "/api/v1/media/category", + "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.category.config.get" + ], + "owner": "moviepilot-api", + "path": "/api/v1/media/category/config", + "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.category.config.update" + ], + "owner": "moviepilot-api", + "path": "/api/v1/media/category/config", + "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": "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 compatible chat completions", + "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 compatible models", + "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 compatible responses", + "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": "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": "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": "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": "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": "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": "GET", + "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/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": "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": "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": "GET", + "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": "GET", + "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": "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": "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": "GET", + "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.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": "GET", + "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": "GET", + "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": "GET", + "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": "GET", + "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": "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.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": "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": "Discover or read registered system settings", + "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": "Update one registered system setting", + "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": "consolidated", + "method": "DELETE", + "operation_ids": [], + "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.", + "summary": "清空 TheMovieDb 识别缓存", + "tags": [ + "tmdb" + ] + }, + { + "disposition": "consolidated", + "method": "GET", + "operation_ids": [], + "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.", + "summary": "查询 TheMovieDb 识别缓存", + "tags": [ + "tmdb" + ] + }, + { + "disposition": "consolidated", + "method": "DELETE", + "operation_ids": [], + "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.", + "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": "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/architecture/agent-api-surface-audit.md b/docs/architecture/agent-api-surface-audit.md new file mode 100644 index 000000000..7c2affa13 --- /dev/null +++ b/docs/architecture/agent-api-surface-audit.md @@ -0,0 +1,416 @@ +# 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: **375** +- Stable `moviepilot_api` operations: **203** +- Exact HTTP routes used by the gateway: **201** +- OpenAPI routes matched directly by the gateway: **200** +- 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` | 72 | Source/UI route represented by a stable aggregate Agent operation. | +| `gateway` | 200 | Approved structured MoviePilot Agent operation. | +| `provider-skill` | 11 | 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` | 5 | Frontend or plugin-rendered presentation contract. | + +## 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 compatible messages | +| `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/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/{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 | 添加下载(不含媒体信息) | +| `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 | 高级更新下载任务 | +| `DELETE` | `/api/v1/history/download` | history | `gateway` | download.history.delete | 删除下载历史记录 | +| `GET` | `/api/v1/history/download` | history | `gateway` | download.history.list | 查询下载历史记录 | +| `GET` | `/api/v1/history/empty/transfer` | history | `gateway` | transfer.history.clear | 清空整理记录 | +| `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 | 智能助手批量重新整理 | +| `POST` | `/api/v1/history/transfer/{history_id}/ai-redo` | history | `gateway` | transfer.history.redo | 智能助手重新整理 | +| `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 | +| `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 | `gateway` | media.categories | 查询自动分类配置 | +| `GET` | `/api/v1/media/category/config` | media | `gateway` | media.category.config.get | 获取分类策略配置 | +| `POST` | `/api/v1/media/category/config` | media | `gateway` | media.category.config.update | 保存分类策略配置 | +| `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/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 compatible chat completions | +| `GET` | `/api/v1/openai/v1/models` | openai | `transport_or_identity` | host-runtime | OpenAI compatible models | +| `POST` | `/api/v1/openai/v1/responses` | openai | `transport_or_identity` | host-runtime | OpenAI compatible responses | +| `GET` | `/api/v1/plugin/` | plugin | `gateway` | plugin.installed, plugin.market | 所有插件 | +| `POST` | `/api/v1/plugin/clone/{plugin_id}` | plugin | `gateway` | plugin.clone | 创建插件分身 | +| `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 | 删除插件文件夹 | +| `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 | 更新文件夹中的插件 | +| `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 | 已安装插件 | +| `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版本 | +| `GET` | `/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/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 | 新增自定义过滤规则 | +| `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 | 新增过滤规则组 | +| `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 | +| `GET` | `/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 | 批量更新站点优先级 | +| `GET` | `/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 可用目录和文件 | +| `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 | 创建目录 | +| `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 | 更新订阅 | +| `GET` | `/api/v1/subscribe/check` | subscribe | `gateway` | subscription.metadata.refresh | 刷新订阅 TMDB 信息 | +| `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 | 热门订阅(基于用户共享数据) | +| `GET` | `/api/v1/subscribe/refresh` | subscribe | `gateway` | subscription.refresh | 刷新订阅 | +| `GET` | `/api/v1/subscribe/reset/{subid}` | subscribe | `gateway` | subscription.reset | 重置订阅 | +| `GET` | `/api/v1/subscribe/search` | subscribe | `gateway` | subscription.search_all | 搜索所有订阅 | +| `GET` | `/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/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/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/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 | Discover or read registered system settings | +| `POST` | `/api/v1/system/settings` | system | `gateway` | config.system.update | Update one registered system setting | +| `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 | `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 识别缓存 | +| `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/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/architecture/agent-tool-refactor-plan.md b/docs/architecture/agent-tool-refactor-plan.md index 89c312c7a..4a69d806a 100644 --- a/docs/architecture/agent-tool-refactor-plan.md +++ b/docs/architecture/agent-tool-refactor-plan.md @@ -1,10 +1,10 @@ # MoviePilot Agent 工具体系重构计划 -> 状态:COMPLETE +> 状态:IN PROGRESS — 本地验收已通过,提交推送与远端 CI 收口 > > 建立日期:2026-08-31 > -> 当前基线:v3@6ee47795629f +> 当前基线:v3@871632af257a663abc159c516acc027a81ab2314 > > 关联目标:本线程已建立的 Agent 工具体系重构 Goal @@ -35,13 +35,13 @@ | 工厂固定工具元组 | 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 只接受 59 个固定 operation ID;Skill 以 allowed-api-operations 强制收敛 | +| 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 条内部依赖边 | 917 个宿主模块、7,671 条内部依赖边,Application/Chain 具体 Adapter 直连仍为 0 | -| 工作区状态 | 基线提交 6ee47795629f,与 origin/v3 对齐,初始工作区干净 | 重构与验证已完成,尚未提交或推送 | +| 架构图 | 982 个宿主模块、8,430 条内部依赖边 | 919 个宿主模块、7,680 条内部依赖边,Application/Chain 具体 Adapter 直连仍为 0 | +| 工作区状态 | 基线提交 871632af,与 origin/v3 对齐,初始工作区干净 | 生产改动、生成合同、静态检查、全量测试和固定 80% 覆盖率门禁已完成;提交、推送和远端 CI 尚待完成 | ## 3. 目标工具分层 @@ -82,7 +82,7 @@ - 工厂固定工具元组由 82 个降为 12 个原生工具。 - 默认运行目录再追加 send_local_file 与单一 moviepilot_api 网关,共 14 个固定工具。 -- 其中 13 个是原生能力,1 个是 59-operation 结构化 API 网关。 +- 其中 13 个是原生能力,1 个是 203-operation 结构化 API 网关。 - ask_user_choice、send_voice_message、Skill、渠道、子 Agent、插件和外部 MCP 工具仍按运行时条件注入,不计入 14 个默认固定工具。 - 77 个退役实现/辅助文件直接删除,不保留 Agent 或 MCP 兼容副本。 @@ -133,6 +133,7 @@ provider 动态返回 namespaced action、参数约束、副作用等级及是 | L6 原生工具收敛 | VERIFIED | L4,L5 | Agent 任务和人格工具合并为 action 工具;宿主/会话/渠道原生能力边界固定 | | L7 第三方服务 Skill 化 | VERIFIED | L5 | 下载器和媒体服务器能力发现、受控脚本、Skill、策略和离线测试完成;重复低层 Agent API operation 已删除 | | L8 收口与交付 | VERIFIED | L6,L7 | 旧代码、文档与架构基线已收口;静态检查和锁定全量测试已完成 | +| L9 全 API 面审计与最终交付 | ACTIVE | L8 | 375 个 OpenAPI 操作逐路由归属、203 个网关合同与 72 个退出工具映射均由测试锁定;完成全量测试、80% 覆盖率门禁、提交推送和远端 CI 终态 | ## 5. L2 受控 API 网关约束 @@ -278,7 +279,7 @@ action,并使用 MoviePilot 已配置的具体服务实例访问其自身 API - 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 导出清单已同步新合同 -- 宿主架构基线已更新:模块 982 降至 917,内部依赖边 8,430 降至 7,671,未新增 Application/Chain 到具体 Adapter 的直连 +- 宿主架构基线在 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 @@ -286,4 +287,32 @@ action,并使用 MoviePilot 已配置的具体服务实例访问其自身 API - 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/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 等认证字段 +- 修复完整 API Skill 超过原 64 KiB 运行时返回上限而被截断的问题:结果上限调整为 256 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% 门禁 +- 当前活动叶子保持 L9;下一步完成提交前差异审查、提交推送并检查远端 CI 终态 + 本文件作为本次重构的持续记录,保留阶段状态、实际变更、验证结果、提交状态与已知基线边界。 diff --git a/docs/architecture/optimization-checklist.md b/docs/architecture/optimization-checklist.md index 13a506c6a..63a98e32f 100644 --- a/docs/architecture/optimization-checklist.md +++ b/docs/architecture/optimization-checklist.md @@ -94,7 +94,7 @@ ARCH-201 至 ARCH-204 均达到实现、验证、提交、推送和远端门禁 | 指标 | 当前值 | 解释 | |---|---:|---| -| 宿主 Python 模块 / 内部依赖边 | 917 / 7,671 | `dependency-baseline.json` 当前快照 | +| 宿主 Python 模块 / 内部依赖边 | 919 / 7,680 | `dependency-baseline.json` 当前快照 | | 非平凡 SCC | 1 | 仅保留精确 containment 的 29 模块 TMDB 移植包环 | | 跨层 DB 边界债务 | 0 | Application、Chain、API、Agent、Runtime、Workflow 到 DB 的受控债务均为零 | | Model/Oper 事务债务 | 0 | 自建 Session、自动事务装饰器、直接 commit/rollback 等基线均为零 | diff --git a/docs/mcp-api.md b/docs/mcp-api.md index 0ac722741..c07ec1962 100644 --- a/docs/mcp-api.md +++ b/docs/mcp-api.md @@ -37,6 +37,118 @@ MCP 使用系统配置中的 `API_TOKEN` 作为认证密钥,文档中的 API K MCP 当前不会主动发送工具列表变更通知(`listChanged=false`)。如果客户端缓存了工具列表,插件状态变化后需要让客户端重新请求 `tools/list`;无法手动刷新的客户端应重新连接 MCP 服务或新建会话。 +## 3.1 结构化 Agent 工具与完整参数合同 + +`tools/list` 会为以下四个正式入口返回可直接校验的 JSON Schema。每个入口都按 operation/action 生成 `oneOf` 分支,分支中包含必填字段、类型、默认值、枚举、嵌套对象和互斥/至少一项等跨字段规则;外部 MCP 客户端可以在一次 `tools/call` 中完成参数构造,不需要猜测 URL、HTTP 方法或第三方 SDK 参数。 + +| MCP 工具 | 用途 | 参数合同来源 | +| :--- | :--- | :--- | +| `moviepilot_api` | MoviePilot 产品业务 API:媒体、搜索、订阅、下载、整理、站点、存储、调度、工作流、插件、过滤规则和系统配置 | `skills/moviepilot-api/SKILL.md`;运行时 schema 为 `app/agent/policy/api_mcp_schema.json` | +| `downloader_operation` | qBittorrent、Transmission、rTorrent 原生任务、队列、文件、限速、标签和会话操作 | `skills/downloader-operation/SKILL.md` 与 `skills/downloader-operation/scripts/mp-downloader.py` 的 `ACTIONS` | +| `mediaserver_operation` | Emby、Jellyfin、Plex、ZSpace、UGREEN、TrimeMedia、Navidrome 原生媒体库、搜索、播放、扫描和刷新操作 | `skills/mediaserver-operation/SKILL.md` 与 `skills/mediaserver-operation/scripts/mp-mediaserver.py` 的 `ACTIONS` | +| `database_operation` | MoviePilot 配置数据库表清单、实时 schema、只读 SQL 和明确授权写入 | `skills/database-operation/SKILL.md` 与 `skills/database-operation/scripts/mp-db.py` 的 `ACTIONS` | + +这四个工具都要求管理员级 MCP 集成身份;`tools/list` 的可见性不等于绕过业务权限或写操作确认。下载器和媒体服务器工具会在一次调用内自动选择默认/唯一实例;实例不明确时,错误结果会列出可复用的精确实例名。数据库工具不接受任意连接串或凭据,脚本从 MoviePilot 运行时配置读取数据库连接。 + +`app/agent/policy/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 包含 375 个 HTTP 操作,其中 203 个稳定业务操作进入 +`moviepilot_api`,使用 201 个固定路由模板:200 条 OpenAPI 路由直接匹配,另有 1 条只允许 +`tmdb`、`douban`、`bangumi`、`anilist` 四个来源的受限人物作品动态路由。每个 operation +均同时具备固定 method/path、角色权限、副作用等级、确认与恢复策略、结果敏感性、英文用途说明, +以及可直接提交的 path/query/body JSON Schema;Skill front matter、正文 operation 章节、运行时 +注册表和 MCP `tools/list` 的 203 个 `oneOf` 分支必须完全一致。 + +数量不相等是明确的安全与语义边界,而不是漏生成。当前 375 条路由均被审计并锁定为以下一种 +归属,审计生成器不再提供“未归类”兜底: + +| 归属 | 数量 | Agent 使用方式 | +| :--- | ---: | :--- | +| `gateway` | 200 | 通过 `moviepilot_api` 的稳定 operation 和精确参数合同调用 | +| `consolidated` | 72 | 通过同领域聚合 operation 调用,不复制数据源或前端专用路由 | +| `provider-skill` | 11 | 通过下载器或媒体服务器 Skill 调用第三方 provider API | +| `alternate-auth-duplicate` | 11 | 使用对应 bearer-authenticated gateway operation,不暴露 API_TOKEN 兼容副本 | +| `transport_or_identity` | 66 | 由登录、令牌、MCP、会话、回调、健康检查等宿主传输/身份边界拥有 | +| `stream_or_binary` | 10 | 由直接客户端处理流式日志、消息、文件、图片等非结构化响应 | +| `ui_presentation` | 5 | 由前端或插件渲染面拥有,不作为业务 Agent operation | + +逐路由归属见 `docs/architecture/agent-api-surface-audit.md`,并由 +`tests/test_agent_api_surface_audit.py` 对当前 OpenAPI、固定注册表、MCP schema、英文 Skill +合同及全部非网关归属做漂移检查。任何新增端点必须先明确归属;对 Agent 开放时还必须补齐 +operation ID、权限、副作用、确认、恢复、结果敏感性及精确参数说明。 + +### `moviepilot_api` 调用形状 + +```json +{ + "name": "moviepilot_api", + "arguments": { + "operation_id": "subscription.list", + "path_params": {}, + "query": {"page": 1, "count": 20}, + "body": {} + } +} +``` + +只允许传 `tools/list` 对应 operation 分支中声明的 `path_params`、`query` 和 `body` 字段。不得传 URL、认证头、API Token 或任意 HTTP 方法。 + +### `downloader_operation` / `mediaserver_operation` 调用形状 + +```json +{ + "name": "downloader_operation", + "arguments": { + "client": "main-qb", + "action": "tasks.list", + "arguments": { + "status": "downloading", + "offset": 0, + "limit": 20 + } + } +} +``` + +媒体服务器将顶层实例字段改为 `server`,其余结构相同。`client`/`server` 可省略;具体 action 的 `arguments` 必须严格匹配对应 `oneOf` 分支。 + +### `database_operation` 调用形状 + +```json +{ + "name": "database_operation", + "arguments": { + "action": "query", + "arguments": { + "sql": "SELECT title, year FROM downloadhistory ORDER BY id DESC", + "limit": 20, + "write": false + } + } +} +``` + +数据库 action 参数为: + +- `tables`:无参数,列出当前数据库实际表。 +- `schema`:必填 `table_name`,必须使用 `tables` 返回的精确名称。 +- `query`:`sql` 与 `file` 二选一;可选 `limit`(默认 100)和 `write`(默认 false)。默认只允许 `SELECT`、`WITH`、`EXPLAIN`。 +- `write`:`sql` 与 `file` 二选一,只允许单条写入或结构变更语句;必须已有明确授权。 + +数据库 ORM 当前维护的完整表清单与字段基线见 `skills/database-operation/SKILL.md` 的 `Core Tables`;运行时仍应先调用 `schema`,因为实际部署可能存在迁移差异或插件表。 + +### 系统设置、配置变量与数据库配置 + +系统设置统一使用 `moviepilot_api`,不需要恢复旧的 `query_system_settings` / `update_system_settings` 工具: + +- `config.system.get` 同时查询 `Settings` 运行配置变量和 `SystemConfigKey` 数据库配置。可用 `setting_key` 精确读取,或用 `group` + `keyword` 发现键;单项默认返回完整值,多项默认只返回摘要。 +- 每个发现结果都返回动态 `definition`:声明类型、当前值形状、是否可空/敏感、允许的更新操作、列表默认匹配字段和持久化位置。Agent 应先发现定义,再按返回的精确键和形状调用更新。 +- `config.system.update` 支持 `replace`、`merge_dict`、`upsert_list_item`、`remove_list_item`。`Settings` 字段会执行类型转换并持久化到 `app.env`;`SystemConfigKey` 会经配置服务写入数据库并发布配置变更事件。 +- 敏感值默认脱敏;只有管理员明确要求时才传 `query.show_secrets=true`,并继续受宿主确认和保护输出策略约束。 +- `database_operation` 直接修改 `systemconfig` 只用于受控数据修复。普通配置修改不得绕过键注册、类型转换、插件 mutation 门禁和事件通知。 + +`skills/moviepilot-api/SKILL.md` 只维护稳定的发现与更新流程,不复制当前版本全部 `Settings` / `SystemConfigKey` 清单。真实键、类型和值形状由 `config.system.get` 运行时发现;MCP `tools/list` 的 `config.system.get/update` 分支负责说明发现参数和更新请求结构。 + --- ## 4. 客户端配置示例 diff --git a/docs/rules/05-architecture.md b/docs/rules/05-architecture.md index b4d4dd5f9..d2bab91ce 100644 --- a/docs/rules/05-architecture.md +++ b/docs/rules/05-architecture.md @@ -86,6 +86,9 @@ to make the directory tree look symmetrical. | `app/agent/lifecycle.py` | Runtime generation admission, initialization, idle-session collection and bounded manager shutdown | | `app/agent/tasks.py` | Background prompts, durable scheduled-task execution and heartbeat wakeups | | `app/agent/orchestrator.py` | One `MoviePilotAgent` execution instance: prompt/tool/middleware assembly, model invocation, streaming and per-agent state | +| `app/agent/policy/api.py` | Fixed `moviepilot_api` operation registry, HTTP route templates and per-operation authorization/effect policy; no arbitrary URL or method input | +| `app/agent/policy/mcp.py` | Generated external MCP input-contract builder for the fixed API registry; owns exact English oneOf parameter projection, not runtime authorization | +| `app/agent/tools/impl/service.py` | Admin-only external MCP wrappers for downloader, media-server and database Skill scripts; synchronous scripts run only through the Agent blocking executor | `app.agent.AgentManager` remains the stable plugin-facing import and resolves lazily to `app.agent.manager.AgentManager`. Historical symbols formerly imported from @@ -94,6 +97,9 @@ orchestrator must not re-export manager/session implementations. The package roo uses a reviewed symbol whitelist and must not restore an unbounded `__getattr__` forwarder. +The policy and service modules above are host-internal owners. They are not SDK or +Compat surfaces, and retired multiword module paths must not be restored as aliases. + Application services may use domain rules and runtime contracts. They own the persistence Protocol needed by a use case, but must not import `app.db`, SQLAlchemy, Session, Oper classes or concrete adapters. `app/db/adapters/` diff --git a/scripts/generate_agent_api_mcp_schema.py b/scripts/generate_agent_api_mcp_schema.py index 1beee79f2..9c12af105 100644 --- a/scripts/generate_agent_api_mcp_schema.py +++ b/scripts/generate_agent_api_mcp_schema.py @@ -14,7 +14,7 @@ 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.agent.policy.api_mcp_contract import build_api_mcp_input_schema # noqa: E402 +from app.agent.policy.mcp import build_api_mcp_input_schema # noqa: E402 from app.api.apiv1 import api_router # noqa: E402 OUTPUT_PATH = PROJECT_ROOT / "app/agent/policy/api_mcp_schema.json" diff --git a/scripts/generate_agent_api_surface_audit.py b/scripts/generate_agent_api_surface_audit.py new file mode 100644 index 000000000..1ddba8850 --- /dev/null +++ b/scripts/generate_agent_api_surface_audit.py @@ -0,0 +1,349 @@ +#!/usr/bin/env python3 +"""Generate the complete OpenAPI-to-Agent surface audit.""" + +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 # 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" +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", +} +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/system/logging", + "/api/v1/system/logging/download/{name}", + "/api/v1/system/message", + "/api/v1/system/progress/{process_type}", + } +) +UI_PRESENTATION_PATHS = frozenset( + { + "/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/sidebar_nav", + } +) +EXPLICIT_TRANSPORT_PATHS = frozenset( + { + "/api/v1/plugin/remotes", + "/api/v1/subscribe/seerr", + "/api/v1/system/ping", + } +) + + +def _gateway_routes() -> dict[tuple[str, str], list[str]]: + """Return exact HTTP routes and every stable gateway operation using them.""" + 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]]: + """Classify one OpenAPI operation into one reviewed Agent ownership boundary.""" + 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 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 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]: + """Build a deterministic entry for every v1 OpenAPI HTTP operation.""" + 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, + } + + +def render_markdown(audit: dict[str, Any]) -> str: + """Render the complete audit as a reviewable architecture document.""" + 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]} |") + 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: + """Write deterministic JSON and Markdown audit artifacts.""" + 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( + "generated " + f"{JSON_OUTPUT.relative_to(PROJECT_ROOT)} and {MARKDOWN_OUTPUT.relative_to(PROJECT_ROOT)}" + ) + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/generate_agent_skill_docs.py b/scripts/generate_agent_skill_docs.py new file mode 100644 index 000000000..57971cee3 --- /dev/null +++ b/scripts/generate_agent_skill_docs.py @@ -0,0 +1,663 @@ +#!/usr/bin/env python3 +"""Generate English Agent Skill contracts from runtime metadata.""" + +from __future__ import annotations + +import json +import runpy +import sys +import textwrap +from pathlib import Path +from typing import Any, Mapping + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +if str(PROJECT_ROOT) not in sys.path: + sys.path.insert(0, str(PROJECT_ROOT)) + + +DATABASE_TABLE_GUIDES: dict[str, tuple[str, str, str]] = { + "alembic_version": ( + "Records the Alembic migration revision currently applied to the database.", + "Diagnosing startup migration failures or a database/code revision mismatch.", + "Never edit it directly; advance or roll back revisions only through Alembic.", + ), + "agentchat": ( + "Stores Web Agent and messaging-channel session indexes, titles, previews, and message snapshots.", + "Tracing Agent history or context restoration by user, session, or update time.", + "Owned by the Agent conversation service; do not rewrite message JSON, counters, or ownership.", + ), + "agenttask": ( + "Stores one-shot or recurring Agent task definitions, triggers, and the latest execution summary.", + "Inspecting task ownership, enablement, cron/run_at settings, and the latest result.", + "Create, update, enable, disable, or delete tasks through the Agent task API.", + ), + "agenttaskrun": ( + "Stores the input snapshot, status, timestamps, and result of each Agent task execution.", + "Auditing one run or correlating a failure with task_id, run_id, and trigger source.", + "Execution evidence owned by the task runner; never fabricate rows or edit run status.", + ), + "downloadfailure": ( + "Stores stable fingerprints, media/torrent context, errors, and retry scheduling for failed downloads.", + "Analyzing failure causes, retry counts, next retry time, and affected media or sites.", + "Owned by download-failure compensation; retry or clean records through its business API.", + ), + "downloadfiles": ( + "Maps downloader task hashes to full paths, save directories, relative files, and active state.", + "Finding task files by downloader/download_hash or diagnosing savepath associations.", + "Maintained by download and transfer flows; do not manually change state or path mappings.", + ), + "downloadhistory": ( + "Stores media identity, torrent, downloader, user, and recognition context for submitted downloads.", + "Reviewing download history or tracing a media identity or hash back to its source.", + "Written by the download use case; delete or correct records through the download-history API.", + ), + "mediaserveritem": ( + "Stores the local index and canonical media identity projected from media-server libraries.", + "Checking library presence, server/library/path placement, and season information.", + "This is a rebuildable projection; writes and cleanup belong to media-server synchronization.", + ), + "message": ( + "Stores inbound and outbound messages, channels, content, attachments, users, and timestamps.", + "Paging notification history, distinguishing direction, or tracing duplicates by source.", + "Written by messaging and notification services; clean it through the message API or retention job.", + ), + "outboxmessage": ( + "Stores externally visible side-effect intents committed atomically with business transactions.", + "Diagnosing pending/processing/failed state, leases, attempts, and the last error.", + "Owned by the Outbox Dispatcher state machine; never mark completion or delete undelivered events manually.", + ), + "passkey": ( + "Stores WebAuthn/PassKey credentials, public keys, signature counters, and activation state.", + "Authorized authentication diagnostics such as ownership, activation, and last use.", + "Security-sensitive; manage it only through the PassKey API and never disclose credential material.", + ), + "plugindata": ( + "Stores plugin-owned JSON values isolated by plugin_id and key.", + "Diagnosing persistence or migration issues for one explicitly identified plugin and key.", + "The plugin owns these values; prefer plugin capabilities or the plugin-data API.", + ), + "pluginidentity": ( + "Stores trusted source, payload source, version, receipt, and CAS revision for a physical plugin package.", + "Auditing source binding, package generation, payload application, or identity conflicts.", + "Plugin supply-chain state owned exclusively by installation and update transactions.", + ), + "plugininstallation": ( + "Stores plugin installation phase, membership target, identity revisions, and backup state.", + "Diagnosing interrupted installations, rollback conditions, and package or backup presence.", + "Owned by the plugin installation state machine; never advance phase or overwrite evidence manually.", + ), + "site": ( + "Stores private-tracker URLs, RSS, credentials, rate limits, proxy state, and downloader binding.", + "Inspecting enablement, domain, rate limits, or downloader binding with minimal credential exposure.", + "Contains cookies, API keys, and tokens; manage it through the site API.", + ), + "siteicon": ( + "Caches site names, domains, icon URLs, and Base64 icon content.", + "Diagnosing missing icons, incorrect domain mapping, or cache generation.", + "Rebuildable cache owned by site-icon synchronization; direct writes are not recommended.", + ), + "sitestatistic": ( + "Aggregates site request successes, failures, durations, latest state, and diagnostic notes.", + "Comparing site availability, failure rate, and the most recent access state.", + "Accumulated by site access statistics; never edit counters to conceal runtime behavior.", + ), + "siteuserdata": ( + "Stores tracker account level, traffic, ratio, seeding, and unread-message data.", + "Inspecting account state, traffic trends, seeding volume, and the latest collection error.", + "A site-scraping projection refreshed by synchronization; do not edit it directly.", + ), + "subscribe": ( + "Stores active movie, TV, or music subscriptions, filters, progress, and download targets.", + "Inspecting state, missing episodes/tracks, quality rules, site scope, and match progress.", + "Create, update, search, or delete through the subscription API to preserve state-machine consistency.", + ), + "subscribehistory": ( + "Stores snapshots of completed or archived subscriptions and their final filter state.", + "Auditing historical subscriptions, media identity, completion criteria, and filter configuration.", + "Generated by subscription completion and archival; restore or delete through its business API.", + ), + "systemconfig": ( + "Stores JSON business configuration values keyed by SystemConfigKey.", + "Verifying the physical value only when the managed settings API behaves unexpectedly.", + "Use config.system.get/update first; direct writes bypass validation, events, and plugin admission.", + ), + "transferexecutionstep": ( + "Stores intent, attempt identity, state, and result evidence for each durable transfer operation.", + "Diagnosing stuck, failed, or repeated steps by task_id or operation_id.", + "Owned by the transfer execution state machine and lease CAS; never force state transitions manually.", + ), + "transferhistory": ( + "Stores transfer source, destination, mode, media identity, download linkage, and outcome.", + "Reviewing success/failure history, destination paths, media classification, and download linkage.", + "Written by transfer settlement; delete or retry through transfer-history business APIs.", + ), + "transferpending": ( + "Durably stores pending transfer input, plans, checkpoints, leases, retries, and manual review state.", + "Diagnosing restart recovery, expired leases, retry_wait, terminal failures, or manual review.", + "Core durable state machine advanced only by planning, execution, retry, and review services.", + ), + "transfersettlementreceipt": ( + "Stores immutable terminal settlement receipts with contiguous revisions per transfer task.", + "Verifying that history, pending deletion, and execution fingerprints were settled reliably.", + "Idempotency and audit evidence; append revisions only and never overwrite or delete old receipts.", + ), + "user": ( + "Stores user accounts, password hashes, administrator state, OTP, permissions, and preferences.", + "Authorized diagnostics of account state, permissions, or authentication configuration.", + "Security-sensitive; manage through user, permission, password, and two-factor APIs.", + ), + "userconfig": ( + "Stores per-user JSON configuration isolated by username and key.", + "Inspecting UI preferences, message clear cursors, or other personalized state.", + "Modify through the owning user or messaging API to preserve key semantics.", + ), + "workflow": ( + "Stores workflow definitions, triggers, action graphs, execution context, and runtime state.", + "Inspecting scheduled/event workflows, pause state, current action, run count, and failures.", + "Create, modify, run, pause, or reset through the workflow API.", + ), +} + + +def _load_json_schema() -> dict[str, Any]: + """Load the generated moviepilot_api MCP schema.""" + path = PROJECT_ROOT / "app/agent/policy/api_mcp_schema.json" + payload = json.loads(path.read_text(encoding="utf-8")) + if not isinstance(payload, dict): + raise ValueError("The API MCP schema must be a JSON object") + return payload + + +def _schema_type(schema: Mapping[str, Any], definitions: Mapping[str, Any]) -> str: + """Compress JSON Schema into a readable Skill type expression.""" + reference = schema.get("$ref") + if isinstance(reference, str): + return reference.rsplit("/", 1)[-1] + if isinstance(schema.get("const"), (str, int, float, bool)): + return f"{schema.get('type', 'value')}={schema['const']}" + enum = schema.get("enum") + if isinstance(enum, list): + return f"{schema.get('type', 'value')}({','.join(map(str, enum))})" + schema_type = schema.get("type") + if schema_type == "array": + items = schema.get("items") + item_type = _schema_type(items, definitions) if isinstance(items, Mapping) else "value" + return f"array<{item_type}>" + if schema_type: + return str(schema_type) + any_of = schema.get("anyOf") + if isinstance(any_of, list): + return "|".join( + _schema_type(item, definitions) + for item in any_of + if isinstance(item, Mapping) + ) + if "properties" in schema or schema.get("additionalProperties") is not None: + return "object" + return "value" + + +def _field_suffix(schema: Mapping[str, Any]) -> str: + """Format defaults, ranges, and collection constraints.""" + suffix: list[str] = [] + if "default" in schema: + suffix.append(f"default `{schema['default']}`") + if "minimum" in schema: + suffix.append(f"minimum `{schema['minimum']}`") + if "maximum" in schema: + suffix.append(f"maximum `{schema['maximum']}`") + if "minLength" in schema: + suffix.append(f"minimum length `{schema['minLength']}`") + if "minItems" in schema: + suffix.append(f"minimum items `{schema['minItems']}`") + return f"; {'; '.join(suffix)}" if suffix else "" + + +def _schema_fields( + schema: Mapping[str, Any], + definitions: Mapping[str, Any], +) -> list[tuple[str, str, str, bool]]: + """Extract fields, types, descriptions, and required markers.""" + reference = schema.get("$ref") + if isinstance(reference, str): + schema = definitions.get(reference.rsplit("/", 1)[-1], {}) + properties = schema.get("properties") + if not isinstance(properties, Mapping): + return [] + required = set(schema.get("required") or []) + fields = [] + for name, raw_schema in properties.items(): + if not isinstance(raw_schema, Mapping): + continue + fields.append( + ( + str(name), + _schema_type(raw_schema, definitions), + str(raw_schema.get("description") or ""), + str(name) in required, + ) + ) + return fields + + +def _resolve_schema( + schema: Mapping[str, Any], + definitions: Mapping[str, Any], +) -> Mapping[str, Any]: + """Resolve a local JSON Schema reference into an enumerable object.""" + reference = schema.get("$ref") + if isinstance(reference, str): + resolved = definitions.get(reference.rsplit("/", 1)[-1]) + if isinstance(resolved, Mapping): + return resolved + return schema + + +def _render_api_docs() -> str: + """Render the complete MoviePilot API operation and field contract.""" + schema = _load_json_schema() + definitions = schema.get("$defs") if isinstance(schema.get("$defs"), Mapping) else {} + from app.agent.policy.api import API_OPERATION_ROUTES, API_OPERATION_SPECS + + specs = {spec.operation_id: spec for spec in API_OPERATION_SPECS} + lines = [ + "## Operation Catalog", + "", + "The operations, HTTP methods, routes, and path/query/body fields below exactly match external MCP `tools/list`.", + "A field name ending in `*` is required. Omit an empty bucket or send `{}`. Referenced body models are expanded below.", + "", + ] + for operation_id in sorted(API_OPERATION_ROUTES): + route = API_OPERATION_ROUTES[operation_id] + branch = next( + item + for item in schema["oneOf"] + if item["properties"]["operation_id"].get("const") == operation_id + ) + description = str(branch.get("description") or "") + effect = specs[operation_id].effect.value + lines.extend( + [ + f"### `{operation_id}`", + f"`{route.method} {route.path}`; policy effect: `{effect}`.", + f"Purpose: {description.split(' Method:', 1)[0].strip()}", + ] + ) + for bucket in ("path_params", "query", "body"): + bucket_schema = branch["properties"].get(bucket) + if not isinstance(bucket_schema, Mapping): + lines.append(f"- `{bucket}`: none") + continue + resolved_bucket = _resolve_schema(bucket_schema, definitions) + fields = _schema_fields(resolved_bucket, definitions) + if not fields: + if resolved_bucket.get("type") != "object": + required_mark = "*" if bucket in set(branch.get("required") or []) else "" + description_text = str(resolved_bucket.get("description") or "") + if not description_text: + raise ValueError( + f"API Skill scalar guidance is missing: {operation_id}.{bucket}" + ) + lines.append( + f"- `{bucket}{required_mark}` ({_schema_type(resolved_bucket, definitions)}): " + f"{description_text}" + ) + continue + dynamic_description = str(resolved_bucket.get("description") or "") + if resolved_bucket.get("additionalProperties") is True and dynamic_description: + required_mark = "*" if bucket in set(branch.get("required") or []) else "" + lines.append(f"- `{bucket}{required_mark}` (object): {dynamic_description}") + continue + body_ref = bucket_schema.get("$ref") + label = ( + body_ref.rsplit("/", 1)[-1] + if isinstance(body_ref, str) + else "empty object" + ) + lines.append(f"- `{bucket}`: `{label}` with no direct fields") + continue + rendered = [] + for name, field_type, description_text, required in fields: + required_mark = "*" if required else "" + if not description_text: + raise ValueError( + f"API Skill field guidance is missing: {operation_id}.{bucket}.{name}" + ) + raw_schema = resolved_bucket["properties"][name] + rendered.append( + f"`{name}{required_mark}` ({field_type}{_field_suffix(raw_schema)}): {description_text}" + ) + lines.append(f"- `{bucket}`: " + "; ".join(rendered)) + lines.append("") + + lines.extend(["### Referenced Body Models", ""]) + referenced: set[str] = set() + for branch in schema["oneOf"]: + for bucket in ("path_params", "query", "body"): + bucket_schema = branch["properties"].get(bucket) + if not isinstance(bucket_schema, Mapping): + continue + resolved_bucket = _resolve_schema(bucket_schema, definitions) + for raw_schema in (resolved_bucket.get("properties") or {}).values(): + if not isinstance(raw_schema, Mapping): + continue + refs = [raw_schema.get("$ref")] + refs.extend( + item.get("$ref") + for item in raw_schema.get("anyOf", []) + if isinstance(item, Mapping) + ) + for reference in refs: + if isinstance(reference, str) and reference.startswith("#/$defs/"): + referenced.add(reference.rsplit("/", 1)[-1]) + pending = list(referenced) + while pending: + name = pending.pop() + model = definitions.get(name) + if not isinstance(model, Mapping): + continue + for raw_schema in (model.get("properties") or {}).values(): + if not isinstance(raw_schema, Mapping): + continue + references = [raw_schema.get("$ref")] + references.extend( + item.get("$ref") + for item in raw_schema.get("anyOf", []) + if isinstance(item, Mapping) + ) + for reference in references: + if isinstance(reference, str) and reference.startswith("#/$defs/"): + child = reference.rsplit("/", 1)[-1] + if child not in referenced: + referenced.add(child) + pending.append(child) + + for name in sorted(referenced): + model = definitions.get(name) + if not isinstance(model, Mapping): + continue + lines.append(f"#### `{name}`") + model_description = str(model.get("description") or "") + if not model_description: + raise ValueError(f"API Skill model guidance is missing: {name}") + lines.append(model_description) + fields = _schema_fields(model, definitions) + if not fields: + lines.append("This runtime model has no directly writable fields.") + else: + for field_name, field_type, description_text, required in fields: + required_mark = "*" if required else "" + raw_schema = model["properties"][field_name] + if not description_text: + raise ValueError(f"API Skill model field guidance is missing: {name}.{field_name}") + lines.append( + f"- `{field_name}{required_mark}` ({field_type}{_field_suffix(raw_schema)}): {description_text}" + ) + lines.append("") + + lines.append(_render_system_settings_docs().rstrip()) + lines.append("") + lines.extend( + [ + "## Operation Order And Failure Handling", + "", + "1. Select the operation first, then place each value in its documented bucket. Never move query fields into path_params or send undeclared fields.", + "2. Reuse the exact `media_source` + `media_id` pair returned by search. For music, also preserve `music_type`.", + "3. Downloads, transfers, configuration/rule/plugin writes, scheduler/workflow runs, and deletions have side effects; obtain confirmation and inspect the result.", + "4. `success=false`, HTTP errors, validation errors, and empty results are real outcomes. Never report them as success.", + "5. Use `database-operation`, `downloader-operation`, or `mediaserver-operation` for their native capabilities. Never bypass the gateway with an arbitrary URL.", + "", + ] + ) + return "\n".join(lines).rstrip() + "\n" + + +def _render_service_docs(script_path: Path, title: str) -> str: + """Render one downloader or media-server action contract.""" + namespace = runpy.run_path(str(script_path)) + actions = namespace.get("ACTIONS") + if not isinstance(actions, Mapping): + raise ValueError(f"{script_path} does not expose ACTIONS") + lines = [ + "## Complete Action Contract", + "", + f"This is the complete {title} action contract. It comes directly from the script `ACTIONS` registry and matches the external MCP `tools/list` oneOf branches.", + "A field name ending in `*` is required. Put every action parameter in the `arguments` object.", + "", + "| action | Purpose and argument summary |", + "| :--- | :--- |", + ] + for name, spec in sorted(actions.items()): + contract = spec.to_dict(name) + argument_names = [ + f"`{argument['name']}{'*' if argument['required'] else ''}`" + for argument in contract["arguments"] + ] + summary = contract["description"] + if argument_names: + summary += "; arguments: " + ", ".join(argument_names) + else: + summary += "; no arguments" + lines.append(f"| `{name}` | {summary} |") + lines.append("") + for name, spec in sorted(actions.items()): + contract = spec.to_dict(name) + lines.extend( + [ + f"### `{name}`", + f"{contract['description']} Effect: `{contract['effect']}`. Providers: `{', '.join(contract['providers'])}`.", + ] + ) + if not contract["arguments"]: + lines.append("- `arguments`: `{}`") + else: + for argument in contract["arguments"]: + required_mark = "*" if argument["required"] else "" + extras = [] + if "default" in argument: + extras.append(f"default `{argument['default']}`") + if argument.get("enum"): + extras.append(f"allowed values `{','.join(map(str, argument['enum']))}`") + suffix = f"; {'; '.join(extras)}" if extras else "" + lines.append( + f"- `{argument['name']}{required_mark}` ({argument['type']}{suffix}): {argument['description']}" + ) + for rule in contract.get("argument_rules") or []: + lines.append(f"- Rule: {rule}") + lines.append("") + return "\n".join(lines).rstrip() + "\n" + + +def _render_database_tables() -> str: + """Render all ORM tables, purposes, and access boundaries.""" + from app.db.base import Base + from app.db.models import load_all_models + + load_all_models() + table_names = {"alembic_version", *Base.metadata.tables} + missing_guides = table_names - DATABASE_TABLE_GUIDES.keys() + stale_guides = DATABASE_TABLE_GUIDES.keys() - table_names + if missing_guides or stale_guides: + raise ValueError( + "Database table guidance does not match ORM metadata: " + f"missing={sorted(missing_guides)}, stale={sorted(stale_guides)}" + ) + lines = [ + "## Core Tables", + "", + "`tables` returns the tables that exist in the current instance. The catalog below covers every MoviePilot ORM table plus Alembic metadata. Always treat the live `schema ` result as authoritative.", + "", + ] + for table_name in sorted(table_names): + purpose, query_usage, write_boundary = DATABASE_TABLE_GUIDES[table_name] + if table_name == "alembic_version": + columns = "`version_num`" + else: + table = Base.metadata.tables[table_name] + columns = ", ".join(f"`{column.name}`" for column in table.columns) + lines.extend( + [ + f"### `{table_name}`", + f"- Purpose: {purpose}", + f"- Useful queries: {query_usage}", + f"- Write boundary: {write_boundary}", + f"- Columns: {columns}", + "", + ] + ) + lines.extend( + [ + "## Database Action Contract", + "", + "- `tables`: `arguments={}` lists current database tables.", + "- `schema`: `arguments={\"table_name\":\"downloadhistory\"}`; table_name must come from `tables`.", + "- `query`: `arguments={\"sql\":\"SELECT ...\",\"limit\":100,\"write\":false}`; provide exactly one of sql and file. SELECT/WITH/EXPLAIN are allowed by default.", + "- `write`: `arguments={\"sql\":\"UPDATE ... WHERE ...\"}`; provide exactly one of sql and file and only one statement.", + "- `file` is a local SQL path readable by the MoviePilot process. MCP clients normally send `sql` directly.", + "", + "Use the live `schema` result instead of guessing columns from older documentation. Treat `media_source` and `media_id` as one atomic identity pair.", + "", + ] + ) + return "\n".join(lines).rstrip() + "\n" + + +def _render_system_settings_docs() -> str: + """Render dynamic system-setting discovery and update guidance.""" + return "\n".join([ + "## System Settings Contract", + "", + "Do not enumerate setting keys in this Skill. Settings change as MoviePilot evolves, so use `config.system.get` as the runtime discovery operation before updating an unfamiliar key.", + "", + "| `source` | Contents | Persistence |", + "| :--- | :--- | :--- |", + "| `settings` | Runtime `Settings` fields such as APP_DOMAIN or LLM_MODEL | Type-converted and persisted to `app.env`, then applied to the current process |", + "| `systemconfig` | Database-backed business configuration such as downloaders, media servers, directories, and notifications | Written through the configuration service with plugin admission and change events |", + "", + "The `systemconfig` database table is only the physical store for the second source. Use `config.system.get/update` for normal reads and writes. Direct SQL is reserved for an explicitly authorized repair when the managed API cannot complete the operation.", + "", + "### Discover definitions", + "", + "1. Call `config.system.get` with `query={\"group\":\"settings\",\"keyword\":\"LLM\"}` or another group/keyword. Discovery defaults to summaries instead of full values.", + "2. Each returned setting includes `setting_key`, `source`, `group`, `label`, and a `definition` object with `declared_type`, current `value_shape`, `nullable`, `sensitive`, allowed `update_operations`, `default_match_field`, and `persistence`.", + "3. Read one exact value with `query={\"setting_key\":\"LLM_MODEL\"}`. Exact-key reads include the value by default.", + "4. Use `show_secrets=true` only when an administrator explicitly requests the plaintext value; secret reads remain confirmation-protected.", + "", + "### Update settings", + "", + "Choose an operation listed in the discovered setting definition, then send it in `body`:", + "", + "| operation | Fields | Meaning |", + "| :--- | :--- | :--- |", + "| `replace` | `setting_key*`, `value` | Replace the complete scalar, list, or object value |", + "| `merge_dict` | `setting_key*`, `value`; optional `remove_keys` | Shallow-merge an object and optionally remove keys |", + "| `upsert_list_item` | `setting_key*`, `value`; optional `match_field`, `match_value` | Replace a matched list item or append it when absent |", + "| `remove_list_item` | `setting_key*`, `value` or `match_value`; optional `match_field` | Remove one matched list item without replacing the list |", + "", + "After every update, call `config.system.get` again with the exact setting_key and verify the saved value. Do not guess a key, value shape, list match field, or update operation when discovery can return it.", + "", + ]).rstrip() + "\n" + + +def _sync_api_frontmatter(text: str) -> str: + """Synchronize the API Skill authorization list with the fixed operation registry.""" + from app.agent.policy.api import list_api_operation_ids + + operation_text = " ".join(list_api_operation_ids()) + wrapped = textwrap.wrap( + operation_text, + width=96, + break_long_words=False, + break_on_hyphens=False, + ) + replacement = "allowed-api-operations: >-\n" + "\n".join(f" {line}" for line in wrapped) + "\n" + lines = text.splitlines(keepends=True) + frontmatter_end = next( + (index for index, line in enumerate(lines[1:], start=1) if line.strip() == "---"), + None, + ) + if frontmatter_end is None: + raise ValueError("moviepilot-api Skill frontmatter closing marker is missing") + field_index = next( + ( + index + for index, line in enumerate(lines[:frontmatter_end]) + if line.startswith("allowed-api-operations:") + ), + None, + ) + if field_index is None: + raise ValueError("moviepilot-api Skill frontmatter operation list is missing") + field_end = field_index + 1 + while field_end < frontmatter_end and lines[field_end].startswith((" ", "\t")): + field_end += 1 + lines[field_index:field_end] = [replacement] + return "".join(lines) + + +def _replace_section(text: str, heading: str, replacement: str, next_heading: str | None = None) -> str: + """Replace one Markdown section from a heading to the next heading.""" + start = text.index(heading) + if next_heading is None: + return text[:start].rstrip() + "\n\n" + replacement + end = text.index(next_heading, start) + return text[:start].rstrip() + "\n\n" + replacement + "\n" + text[end:] + + +def main() -> int: + """Generate complete contracts for four built-in Skills.""" + api_path = PROJECT_ROOT / "skills/moviepilot-api/SKILL.md" + api_text = api_path.read_text(encoding="utf-8") + api_text = _sync_api_frontmatter(api_text) + api_path.write_text( + _replace_section(api_text, "## Operation Catalog", _render_api_docs()), + encoding="utf-8", + ) + + downloader_path = PROJECT_ROOT / "skills/downloader-operation/SKILL.md" + downloader_text = downloader_path.read_text(encoding="utf-8") + downloader_path.write_text( + _replace_section( + downloader_text, + "## Complete Action Contract", + _render_service_docs( + PROJECT_ROOT / "skills/downloader-operation/scripts/mp-downloader.py", + "Downloader Operation", + ), + "## Verification", + ), + encoding="utf-8", + ) + + mediaserver_path = PROJECT_ROOT / "skills/mediaserver-operation/SKILL.md" + mediaserver_text = mediaserver_path.read_text(encoding="utf-8") + mediaserver_path.write_text( + _replace_section( + mediaserver_text, + "## Complete Action Contract", + _render_service_docs( + PROJECT_ROOT / "skills/mediaserver-operation/scripts/mp-mediaserver.py", + "Media Server Operation", + ), + "## Safety And Verification", + ), + encoding="utf-8", + ) + + database_path = PROJECT_ROOT / "skills/database-operation/SKILL.md" + database_text = database_path.read_text(encoding="utf-8") + database_path.write_text( + _replace_section( + database_text, + "## Core Tables", + _render_database_tables(), + "## Common Queries", + ), + encoding="utf-8", + ) + print("generated four Agent Skill contracts") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/skills/database-operation/SKILL.md b/skills/database-operation/SKILL.md index 126524b3e..7e0ad6c59 100644 --- a/skills/database-operation/SKILL.md +++ b/skills/database-operation/SKILL.md @@ -1,6 +1,6 @@ --- name: database-operation -version: 5 +version: 6 description: >- Use this skill when you need to inspect, query, maintain, or carefully modify the MoviePilot database. This skill uses the bundled scripts/mp-db.py helper, @@ -9,6 +9,7 @@ description: >- include data statistics, counts, aggregations, inspecting or fixing records, cleanup requests, and questions like "how many downloads", "show site stats", "delete old records", or "why is this subscription stuck". +allowed-tools: execute_command --- # Database Operation @@ -38,6 +39,20 @@ Use this skill as the final fallback for data access or mutation. It may run the bundled script, but broad or destructive writes still require explicit user authorization. +System settings have two managed sources and should not normally be edited here: + +- Runtime `Settings` variables are queried and updated by `moviepilot-api` + operations `config.system.get` / `config.system.update`; updates perform type + conversion and persist to `app.env`. +- `SystemConfigKey` values are stored in the database `systemconfig` table, but + the same API operations must be preferred because they enforce registered + keys, plugin mutation admission, value normalization, secret redaction, and + configuration-change events. + +Use direct SQL against `systemconfig` only for an explicitly authorized repair +when the managed API cannot complete the operation. Inspect the exact row first, +avoid broad writes, and verify the managed API can read the repaired value. + ## Commands List tables: @@ -99,71 +114,185 @@ python scripts/mp-db.py write "UPDATE subscribe SET state = 'S' WHERE id = 123" ## Core Tables -### downloadhistory -Key columns: `id`, `path`, `type`, `title`, `year`, `media_source`, `media_id`, `music_type`, `seasons`, `episodes`, `downloader`, `download_hash`, `torrent_name`, `torrent_site`, `userid`, `username`, `date`, `media_category` +`tables` returns the tables that exist in the current instance. The catalog below covers every MoviePilot ORM table plus Alembic metadata. Always treat the live `schema
` result as authoritative. -### downloadfiles -Key columns: `id`, `downloader`, `download_hash`, `fullpath`, `savepath`, `filepath`, `torrentname`, `state` +### `agentchat` +- Purpose: Stores Web Agent and messaging-channel session indexes, titles, previews, and message snapshots. +- Useful queries: Tracing Agent history or context restoration by user, session, or update time. +- Write boundary: Owned by the Agent conversation service; do not rewrite message JSON, counters, or ownership. +- Columns: `id`, `session_id`, `client_session_id`, `user_id`, `username`, `channel`, `source`, `original_chat_id`, `title`, `preview`, `agent_messages`, `display_messages`, `message_count`, `created_at`, `updated_at` -### transferhistory +### `agenttask` +- Purpose: Stores one-shot or recurring Agent task definitions, triggers, and the latest execution summary. +- Useful queries: Inspecting task ownership, enablement, cron/run_at settings, and the latest result. +- Write boundary: Create, update, enable, disable, or delete tasks through the Agent task API. +- Columns: `id`, `name`, `content`, `trigger_type`, `cron_expression`, `run_at`, `enabled`, `user_id`, `username`, `session_id`, `channel`, `source`, `original_chat_id`, `last_status`, `last_run_at`, `last_result`, `last_run_id`, `run_count`, `created_at`, `updated_at` -Music rows persist actual `audio_format`, `audio_lossless`, `bit_depth`, `sample_rate`, and `bitrate` values read during organization. Bitrate uses bps and sample rate uses Hz. -Key columns: `id`, `src`, `dest`, `mode`, `type`, `category`, `title`, `year`, `media_source`, `media_id`, `music_type`, `seasons`, `episodes`, `download_hash`, `status`, `errmsg`, `date` +### `agenttaskrun` +- Purpose: Stores the input snapshot, status, timestamps, and result of each Agent task execution. +- Useful queries: Auditing one run or correlating a failure with task_id, run_id, and trigger source. +- Write boundary: Execution evidence owned by the task runner; never fabricate rows or edit run status. +- Columns: `id`, `run_id`, `task_id`, `trigger_source`, `name`, `content`, `trigger_type`, `cron_expression`, `run_at`, `user_id`, `username`, `session_id`, `channel`, `message_source`, `original_chat_id`, `status`, `started_at`, `finished_at`, `result` -### downloadfailure +### `alembic_version` +- Purpose: Records the Alembic migration revision currently applied to the database. +- Useful queries: Diagnosing startup migration failures or a database/code revision mismatch. +- Write boundary: Never edit it directly; advance or roll back revisions only through Alembic. +- Columns: `version_num` -Key columns: `id`, `fingerprint`, `type`, `title`, `year`, `media_source`, `media_id`, `seasons`, `episodes`, `site`, `torrent_id`, `downloader`, `error_message`, `retry_count`, `next_retry_at` +### `downloadfailure` +- Purpose: Stores stable fingerprints, media/torrent context, errors, and retry scheduling for failed downloads. +- Useful queries: Analyzing failure causes, retry counts, next retry time, and affected media or sites. +- Write boundary: Owned by download-failure compensation; retry or clean records through its business API. +- Columns: `id`, `fingerprint`, `type`, `title`, `year`, `media_source`, `media_id`, `seasons`, `episodes`, `site`, `site_name`, `torrent_id`, `torrent_name`, `torrent_size`, `downloader`, `source`, `error_message`, `retry_count`, `first_failed_at`, `last_failed_at`, `next_retry_at` -### subscribe +### `downloadfiles` +- Purpose: Maps downloader task hashes to full paths, save directories, relative files, and active state. +- Useful queries: Finding task files by downloader/download_hash or diagnosing savepath associations. +- Write boundary: Maintained by download and transfer flows; do not manually change state or path mappings. +- Columns: `id`, `downloader`, `download_hash`, `fullpath`, `savepath`, `filepath`, `torrentname`, `state` -Music filters use `audio_quality`, `audio_format`, `min_bitrate`, `min_bit_depth`, and `min_sample_rate`. Quality upgrades reuse `current_priority` and persist the current exact values in `current_audio_format`, `current_bitrate`, `current_bit_depth`, and `current_sample_rate`. -Key columns: `id`, `name`, `year`, `type`, `media_source`, `media_id`, `music_type`, `season`, `total_episode`, `start_episode`, `lack_episode`, `state`, `filter`, `include`, `exclude`, `quality`, `resolution`, `sites`, `best_version`, `best_version_full`, `date`, `username` +### `downloadhistory` +- Purpose: Stores media identity, torrent, downloader, user, and recognition context for submitted downloads. +- Useful queries: Reviewing download history or tracing a media identity or hash back to its source. +- Write boundary: Written by the download use case; delete or correct records through the download-history API. +- Columns: `id`, `path`, `type`, `title`, `year`, `media_source`, `media_id`, `music_type`, `seasons`, `episodes`, `image`, `poster`, `downloader`, `download_hash`, `torrent_name`, `torrent_description`, `torrent_site`, `userid`, `username`, `channel`, `date`, `note`, `media_category`, `episode_group`, `custom_words` -### subscribehistory +### `mediaserveritem` +- Purpose: Stores the local index and canonical media identity projected from media-server libraries. +- Useful queries: Checking library presence, server/library/path placement, and season information. +- Write boundary: This is a rebuildable projection; writes and cleanup belong to media-server synchronization. +- Columns: `id`, `server`, `library`, `item_id`, `item_type`, `title`, `original_title`, `year`, `media_source`, `media_id`, `path`, `seasoninfo`, `note`, `lst_mod_date` -Completed music subscriptions retain both audio filters and the final current-quality snapshot for auditing. -Key columns: `id`, `name`, `year`, `type`, `media_source`, `media_id`, `music_type`, `season`, `total_episode`, `start_episode`, `date`, `username` +### `message` +- Purpose: Stores inbound and outbound messages, channels, content, attachments, users, and timestamps. +- Useful queries: Paging notification history, distinguishing direction, or tracing duplicates by source. +- Write boundary: Written by messaging and notification services; clean it through the message API or retention job. +- Columns: `id`, `channel`, `source`, `mtype`, `title`, `text`, `image`, `link`, `userid`, `reg_time`, `action`, `note` -### user -Key columns: `id`, `name`, `email`, `is_active`, `is_superuser`, `permissions`, `settings` +### `outboxmessage` +- Purpose: Stores externally visible side-effect intents committed atomically with business transactions. +- Useful queries: Diagnosing pending/processing/failed state, leases, attempts, and the last error. +- Write boundary: Owned by the Outbox Dispatcher state machine; never mark completion or delete undelivered events manually. +- Columns: `id`, `event_key`, `topic`, `payload_version`, `payload`, `status`, `attempt`, `next_retry_at`, `lease_until`, `last_error`, `created_at`, `completed_at` -### site -Key columns: `id`, `name`, `domain`, `url`, `pri`, `cookie`, `proxy`, `is_active`, `downloader`, `limit_interval`, `limit_count` +### `passkey` +- Purpose: Stores WebAuthn/PassKey credentials, public keys, signature counters, and activation state. +- Useful queries: Authorized authentication diagnostics such as ownership, activation, and last use. +- Write boundary: Security-sensitive; manage it only through the PassKey API and never disclose credential material. +- Columns: `id`, `user_id`, `credential_id`, `public_key`, `sign_count`, `name`, `aaguid`, `created_at`, `last_used_at`, `is_active`, `transports` -### siteuserdata -Key columns: `id`, `domain`, `name`, `username`, `user_level`, `bonus`, `upload`, `download`, `ratio`, `seeding`, `leeching`, `seeding_size`, `updated_day` +### `plugindata` +- Purpose: Stores plugin-owned JSON values isolated by plugin_id and key. +- Useful queries: Diagnosing persistence or migration issues for one explicitly identified plugin and key. +- Write boundary: The plugin owns these values; prefer plugin capabilities or the plugin-data API. +- Columns: `id`, `plugin_id`, `key`, `value` -### sitestatistic -Key columns: `id`, `domain`, `success`, `fail`, `seconds`, `lst_state`, `lst_mod_date` +### `pluginidentity` +- Purpose: Stores trusted source, payload source, version, receipt, and CAS revision for a physical plugin package. +- Useful queries: Auditing source binding, package generation, payload application, or identity conflicts. +- Write boundary: Plugin supply-chain state owned exclusively by installation and update transactions. +- Columns: `id`, `plugin_id`, `normalized_plugin_id`, `trusted_source_type`, `trusted_source_key`, `binding_basis`, `payload_source_type`, `payload_source_key`, `declared_version`, `package_generation`, `declared_metadata`, `payload_receipt`, `revision`, `created_at`, `updated_at`, `bound_at`, `payload_applied_at` -### mediaserveritem -Key columns: `id`, `server`, `library`, `item_id`, `item_type`, `title`, `original_title`, `year`, `media_source`, `media_id`, `path` +### `plugininstallation` +- Purpose: Stores plugin installation phase, membership target, identity revisions, and backup state. +- Useful queries: Diagnosing interrupted installations, rollback conditions, and package or backup presence. +- Write boundary: Owned by the plugin installation state machine; never advance phase or overwrite evidence manually. +- Columns: `id`, `transaction_id`, `plugin_id`, `phase`, `membership_before`, `membership_target`, `identity_before_revision`, `identity_target_revision`, `package_existed`, `persistent_backup_existed`, `created_at`, `updated_at`, `schema_version` -The media-bearing tables above store one primary identity only. Treat -`media_source` and `media_id` as an atomic pair: both are null for an unknown -identity, or both contain a valid source enum value and its native ID. Do not -write source-specific identity columns back into these tables. +### `site` +- Purpose: Stores private-tracker URLs, RSS, credentials, rate limits, proxy state, and downloader binding. +- Useful queries: Inspecting enablement, domain, rate limits, or downloader binding with minimal credential exposure. +- Write boundary: Contains cookies, API keys, and tokens; manage it through the site API. +- Columns: `id`, `name`, `domain`, `url`, `pri`, `rss`, `cookie`, `ua`, `apikey`, `token`, `proxy`, `filter`, `render`, `public`, `note`, `limit_interval`, `limit_count`, `limit_seconds`, `timeout`, `is_active`, `lst_mod_date`, `downloader` -### systemconfig -Key columns: `id`, `key`, `value` +### `siteicon` +- Purpose: Caches site names, domains, icon URLs, and Base64 icon content. +- Useful queries: Diagnosing missing icons, incorrect domain mapping, or cache generation. +- Write boundary: Rebuildable cache owned by site-icon synchronization; direct writes are not recommended. +- Columns: `id`, `name`, `domain`, `url`, `base64` -### userconfig -Key columns: `id`, `username`, `key`, `value` +### `sitestatistic` +- Purpose: Aggregates site request successes, failures, durations, latest state, and diagnostic notes. +- Useful queries: Comparing site availability, failure rate, and the most recent access state. +- Write boundary: Accumulated by site access statistics; never edit counters to conceal runtime behavior. +- Columns: `id`, `domain`, `success`, `fail`, `seconds`, `lst_state`, `lst_mod_date`, `note` -### plugindata -Key columns: `id`, `plugin_id`, `key`, `value` +### `siteuserdata` +- Purpose: Stores tracker account level, traffic, ratio, seeding, and unread-message data. +- Useful queries: Inspecting account state, traffic trends, seeding volume, and the latest collection error. +- Write boundary: A site-scraping projection refreshed by synchronization; do not edit it directly. +- Columns: `id`, `domain`, `name`, `username`, `userid`, `user_level`, `join_at`, `bonus`, `upload`, `download`, `ratio`, `seeding`, `leeching`, `seeding_size`, `leeching_size`, `seeding_info`, `message_unread`, `message_unread_contents`, `err_msg`, `updated_day`, `updated_time` -### message -Key columns: `id`, `channel`, `source`, `mtype`, `title`, `text`, `image`, `link`, `userid`, `reg_time` +### `subscribe` +- Purpose: Stores active movie, TV, or music subscriptions, filters, progress, and download targets. +- Useful queries: Inspecting state, missing episodes/tracks, quality rules, site scope, and match progress. +- Write boundary: Create, update, search, or delete through the subscription API to preserve state-machine consistency. +- Columns: `id`, `name`, `year`, `type`, `keyword`, `media_source`, `media_id`, `music_type`, `total_tracks`, `season`, `poster`, `backdrop`, `vote`, `description`, `filter`, `include`, `exclude`, `quality`, `resolution`, `effect`, `audio_quality`, `audio_format`, `min_bitrate`, `min_bit_depth`, `min_sample_rate`, `total_episode`, `start_episode`, `lack_episode`, `note`, `state`, `last_update`, `date`, `username`, `sites`, `downloader`, `best_version`, `best_version_full`, `current_priority`, `current_audio_format`, `current_bitrate`, `current_bit_depth`, `current_sample_rate`, `episode_priority`, `save_path`, `search_imdbid`, `manual_total_episode`, `custom_words`, `media_category`, `filter_groups`, `episode_group` -### workflow -Key columns: `id`, `name`, `description`, `timer`, `trigger_type`, `event_type`, `state`, `run_count`, `actions`, `flows`, `last_time` +### `subscribehistory` +- Purpose: Stores snapshots of completed or archived subscriptions and their final filter state. +- Useful queries: Auditing historical subscriptions, media identity, completion criteria, and filter configuration. +- Write boundary: Generated by subscription completion and archival; restore or delete through its business API. +- Columns: `id`, `name`, `year`, `type`, `keyword`, `media_source`, `media_id`, `music_type`, `total_tracks`, `season`, `poster`, `backdrop`, `vote`, `description`, `filter`, `include`, `exclude`, `quality`, `resolution`, `effect`, `audio_quality`, `audio_format`, `min_bitrate`, `min_bit_depth`, `min_sample_rate`, `total_episode`, `start_episode`, `date`, `username`, `sites`, `best_version`, `best_version_full`, `current_priority`, `current_audio_format`, `current_bitrate`, `current_bit_depth`, `current_sample_rate`, `episode_priority`, `save_path`, `search_imdbid`, `custom_words`, `media_category`, `filter_groups`, `episode_group` -### passkey -Key columns: `id`, `user_id`, `credential_id`, `public_key`, `name`, `created_at`, `last_used_at`, `is_active` +### `systemconfig` +- Purpose: Stores JSON business configuration values keyed by SystemConfigKey. +- Useful queries: Verifying the physical value only when the managed settings API behaves unexpectedly. +- Write boundary: Use config.system.get/update first; direct writes bypass validation, events, and plugin admission. +- Columns: `id`, `key`, `value` -### siteicon -Key columns: `id`, `name`, `domain`, `url`, `base64` +### `transferexecutionstep` +- Purpose: Stores intent, attempt identity, state, and result evidence for each durable transfer operation. +- Useful queries: Diagnosing stuck, failed, or repeated steps by task_id or operation_id. +- Write boundary: Owned by the transfer execution state machine and lease CAS; never force state transitions manually. +- Columns: `id`, `task_id`, `operation_id`, `checkpoint_fingerprint`, `ordinal`, `phase`, `kind`, `state`, `attempt_token`, `attempt_count`, `intent_version`, `intent_payload`, `result_version`, `result_payload`, `last_error`, `prepared_at`, `started_at`, `completed_at`, `updated_at` + +### `transferhistory` +- Purpose: Stores transfer source, destination, mode, media identity, download linkage, and outcome. +- Useful queries: Reviewing success/failure history, destination paths, media classification, and download linkage. +- Write boundary: Written by transfer settlement; delete or retry through transfer-history business APIs. +- Columns: `id`, `transfer_task_id`, `transfer_settlement_revision`, `src`, `src_storage`, `src_fileitem`, `dest`, `dest_storage`, `dest_fileitem`, `mode`, `type`, `category`, `title`, `year`, `media_source`, `media_id`, `music_type`, `total_tracks`, `audio_format`, `audio_lossless`, `bit_depth`, `sample_rate`, `bitrate`, `seasons`, `episodes`, `image`, `downloader`, `download_hash`, `status`, `errmsg`, `date`, `files`, `episode_group` + +### `transferpending` +- Purpose: Durably stores pending transfer input, plans, checkpoints, leases, retries, and manual review state. +- Useful queries: Diagnosing restart recovery, expired leases, retry_wait, terminal failures, or manual review. +- Write boundary: Core durable state machine advanced only by planning, execution, retry, and review services. +- Columns: `id`, `task_id`, `storage`, `src_path`, `created_at`, `state`, `updated_at`, `last_error`, `input_version`, `planning_input`, `input_fingerprint`, `checkpoint_version`, `checkpoint_payload`, `planned_at`, `lease_owner`, `lease_token`, `lease_expires_at`, `heartbeat_at`, `attempt_count`, `execution_state`, `execution_version`, `execution_payload`, `execution_fingerprint`, `retry_generation`, `retry_count`, `retry_due_at`, `retry_requested_by`, `retry_reason`, `settlement_revision`, `terminal_history_id`, `manual_review_revision`, `reviewed_at`, `reviewed_by`, `review_reason`, `review_decision` + +### `transfersettlementreceipt` +- Purpose: Stores immutable terminal settlement receipts with contiguous revisions per transfer task. +- Useful queries: Verifying that history, pending deletion, and execution fingerprints were settled reliably. +- Write boundary: Idempotency and audit evidence; append revisions only and never overwrite or delete old receipts. +- Columns: `id`, `task_id`, `history_id`, `settlement_revision`, `outcome`, `execution_fingerprint`, `lease_token`, `history_status`, `src`, `src_storage`, `pending_deleted`, `error`, `created_at`, `updated_at` + +### `user` +- Purpose: Stores user accounts, password hashes, administrator state, OTP, permissions, and preferences. +- Useful queries: Authorized diagnostics of account state, permissions, or authentication configuration. +- Write boundary: Security-sensitive; manage through user, permission, password, and two-factor APIs. +- Columns: `id`, `name`, `email`, `hashed_password`, `is_active`, `is_superuser`, `avatar`, `is_otp`, `otp_secret`, `permissions`, `settings` + +### `userconfig` +- Purpose: Stores per-user JSON configuration isolated by username and key. +- Useful queries: Inspecting UI preferences, message clear cursors, or other personalized state. +- Write boundary: Modify through the owning user or messaging API to preserve key semantics. +- Columns: `id`, `username`, `key`, `value` + +### `workflow` +- Purpose: Stores workflow definitions, triggers, action graphs, execution context, and runtime state. +- Useful queries: Inspecting scheduled/event workflows, pause state, current action, run count, and failures. +- Write boundary: Create, modify, run, pause, or reset through the workflow API. +- Columns: `id`, `name`, `description`, `timer`, `trigger_type`, `event_type`, `event_conditions`, `state`, `current_action`, `result`, `run_count`, `actions`, `flows`, `context`, `execution_config`, `execution_state`, `add_time`, `last_time` + +## Database Action Contract + +- `tables`: `arguments={}` lists current database tables. +- `schema`: `arguments={"table_name":"downloadhistory"}`; table_name must come from `tables`. +- `query`: `arguments={"sql":"SELECT ...","limit":100,"write":false}`; provide exactly one of sql and file. SELECT/WITH/EXPLAIN are allowed by default. +- `write`: `arguments={"sql":"UPDATE ... WHERE ..."}`; provide exactly one of sql and file and only one statement. +- `file` is a local SQL path readable by the MoviePilot process. MCP clients normally send `sql` directly. + +Use the live `schema` result instead of guessing columns from older documentation. Treat `media_source` and `media_id` as one atomic identity pair. ## Common Queries diff --git a/skills/database-operation/scripts/mp-db.py b/skills/database-operation/scripts/mp-db.py index cb71aeb1d..0fafee17a 100644 --- a/skills/database-operation/scripts/mp-db.py +++ b/skills/database-operation/scripts/mp-db.py @@ -1,17 +1,11 @@ #!/usr/bin/env python3 -""" -MoviePilot 数据库操作脚本。 - -脚本从项目配置读取数据库连接参数,不要求 Agent 在提示词中接触数据库密码。 -默认只允许查询语句;写操作必须显式传入 --write。 -""" - -from __future__ import annotations +"""Controlled MoviePilot database helper used by the database-operation Skill.""" import argparse import json import re import sys +from dataclasses import dataclass from pathlib import Path from typing import Any, Optional @@ -19,7 +13,6 @@ from sqlalchemy import create_engine, inspect, text from sqlalchemy.engine import Engine from sqlalchemy.exc import SQLAlchemyError - SCRIPT_PATH = Path(__file__).resolve() PROJECT_ROOT = SCRIPT_PATH.parents[3] WRITE_STATEMENT_RE = re.compile( @@ -33,6 +26,115 @@ WRITE_KEYWORD_RE = re.compile( SELECT_STATEMENT_RE = re.compile(r"^\s*(select|with|explain)\b", re.IGNORECASE) +@dataclass(frozen=True, slots=True) +class ArgumentSpec: + """Describe one public database action argument.""" + + name: str + type: str + description: str + required: bool = False + default: Any = None + has_default: bool = False + + def to_dict(self) -> dict[str, Any]: + """Return the public argument contract used by the MCP schema generator.""" + result = { + "name": self.name, + "type": self.type, + "description": self.description, + "required": self.required, + } + if self.has_default: + result["default"] = self.default + return result + + +@dataclass(frozen=True, slots=True) +class ActionSpec: + """Describe one stable database helper action.""" + + description: str + effect: str + arguments: tuple[ArgumentSpec, ...] = () + argument_rules: tuple[str, ...] = () + + @property + def required(self) -> tuple[str, ...]: + """Return required argument names for this action.""" + return tuple(argument.name for argument in self.arguments if argument.required) + + def to_dict(self, name: str) -> dict[str, Any]: + """Return the implementation-independent public action contract.""" + return { + "action": name, + "description": self.description, + "effect": self.effect, + "providers": ["sqlite", "postgresql"], + "required_arguments": list(self.required), + "arguments": [argument.to_dict() for argument in self.arguments], + "argument_rules": list(self.argument_rules), + } + + +ACTIONS: dict[str, ActionSpec] = { + "tables": ActionSpec( + "List all tables visible to the configured MoviePilot database.", + "safe_read", + ), + "schema": ActionSpec( + "Show columns and nullability for one database table.", + "safe_read", + arguments=( + ArgumentSpec( + "table_name", + "string", + "Exact database table name returned by the tables action.", + required=True, + ), + ), + ), + "query": ActionSpec( + "Run one bounded SQL query, optionally enabling the explicit write mode.", + "safe_read", + arguments=( + ArgumentSpec("sql", "string", "One SQL statement; mutually exclusive with file."), + ArgumentSpec("file", "string", "Local SQL file path; mutually exclusive with sql."), + ArgumentSpec( + "limit", + "integer", + "Maximum row count appended to a plain SELECT that has no LIMIT.", + default=100, + has_default=True, + ), + ArgumentSpec( + "write", + "boolean", + "Allow query to execute a write statement; use true only with explicit authorization.", + default=False, + has_default=True, + ), + ), + argument_rules=( + "Provide exactly one of sql and file.", + "Only SELECT, WITH, and EXPLAIN are allowed unless write=true.", + ), + ), + "write": ActionSpec( + "Run one explicitly authorized SQL write or schema statement.", + "destructive_write", + arguments=( + ArgumentSpec("sql", "string", "One data-write or schema-change statement; mutually exclusive with file."), + ArgumentSpec("file", "string", "Local SQL file path; mutually exclusive with sql."), + ), + argument_rules=( + "Provide exactly one of sql and file.", + "Multiple SQL statements in one call are rejected.", + ), + ), +} + + def _ensure_project_import() -> None: """确保脚本可以从任意工作目录导入 MoviePilot 项目模块。""" project_path = str(PROJECT_ROOT) diff --git a/skills/downloader-operation/SKILL.md b/skills/downloader-operation/SKILL.md index 63a15d8b5..fd0e3e78f 100644 --- a/skills/downloader-operation/SKILL.md +++ b/skills/downloader-operation/SKILL.md @@ -1,6 +1,6 @@ --- name: downloader-operation -version: 2 +version: 3 description: >- Use this skill when the user asks to inspect, diagnose, or directly control a configured qBittorrent, Transmission, or rTorrent instance. It exposes @@ -122,82 +122,184 @@ instances remain ambiguous, the result lists the valid client names. ## Complete Action Contract -In the tables below, `*` means required. Every listed field belongs inside the -single `--arguments` JSON object. Do not send fields that are not listed. +This is the complete Downloader Operation action contract. It comes directly from the script `ACTIONS` registry and matches the external MCP `tools/list` oneOf branches. +A field name ending in `*` is required. Put every action parameter in the `arguments` object. -Shared rules: +| action | Purpose and argument summary | +| :--- | :--- | +| `capabilities.list` | List supported downloader actions and their complete argument contracts.; arguments: `action_name` | +| `instances.list` | List configured downloader instances without connection secrets.; no arguments | +| `session.content_layout` | Read qBittorrent's default torrent content layout.; no arguments | +| `session.details` | Read Transmission session configuration and capacity details.; no arguments | +| `session.speed_limits.get` | Read global speed limits.; no arguments | +| `session.speed_limits.set` | Set global speed limits in KB/s.; arguments: `download_limit`, `upload_limit` | +| `session.stats` | Read provider transfer/session statistics.; no arguments | +| `tasks.add.direct` | Submit a magnet, URL, or local torrent file directly to the provider.; arguments: `content*`, `torrent_file`, `paused`, `download_dir`, `tags`, `category` | +| `tasks.category.set` | Set qBittorrent category.; arguments: `task_id*`, `category*` | +| `tasks.delete` | Delete tasks and optionally their data.; arguments: `task_id`, `task_ids`, `delete_files` | +| `tasks.files` | List files and priorities for one task.; arguments: `task_id*`, `offset`, `limit` | +| `tasks.files.selection.set` | Select wanted and unwanted files within one task.; arguments: `task_id*`, `wanted_file_ids`, `unwanted_file_ids` | +| `tasks.force_start.set` | Enable or disable qBittorrent force-start for tasks.; arguments: `task_id`, `task_ids`, `enabled*` | +| `tasks.list` | List and filter downloader tasks.; arguments: `task_id`, `task_ids`, `status`, `tags`, `offset`, `limit` | +| `tasks.location.set` | Move or retarget one task to a provider-side path.; arguments: `task_id*`, `location*` | +| `tasks.peers` | Read qBittorrent peer synchronization data.; arguments: `task_id*` | +| `tasks.properties.set` | Set task speed, ratio, or seeding-time limits.; arguments: `task_id*`, `upload_limit`, `download_limit`, `ratio_limit`, `seeding_time_limit` | +| `tasks.queue.move` | Move tasks to top, up, down, or bottom of the queue.; arguments: `task_id`, `task_ids`, `position*` | +| `tasks.reannounce` | Force tracker reannounce.; arguments: `task_id`, `task_ids` | +| `tasks.recheck` | Force data verification for tasks.; arguments: `task_id`, `task_ids` | +| `tasks.start` | Start or resume one or more tasks.; arguments: `task_id`, `task_ids` | +| `tasks.stop` | Pause one or more tasks.; arguments: `task_id`, `task_ids` | +| `tasks.tags.get` | Read task tags or labels.; arguments: `task_id*` | +| `tasks.tags.set` | Set or add task tags/labels.; arguments: `task_id`, `task_ids`, `tags*` | +| `tasks.trackers` | List trackers for one task.; arguments: `task_id*` | +| `tasks.trackers.update` | Add or replace task trackers.; arguments: `task_id*`, `trackers*` | -- Task batch actions require exactly one of `task_id:string` or - `task_ids:string[]`. -- Paged reads accept `offset:integer=0` and `limit:integer=50`; `offset` must be - non-negative and `limit` is clamped to `1..200`. -- Speed values are numbers in `KB/s`. A value of `0` means unlimited. -- Task IDs, file indexes, tags, tracker URLs, and provider paths must come from - the selected downloader or the user's explicit input; never invent them. +### `capabilities.list` +List supported downloader actions and their complete argument contracts. Effect: `safe_read`. Providers: `qbittorrent, transmission, rtorrent`. +- `action_name` (string): Optional exact action name used to return one capability contract. -### Task reads +### `instances.list` +List configured downloader instances without connection secrets. Effect: `safe_read`. Providers: `qbittorrent, transmission, rtorrent`. +- `arguments`: `{}` -| Action | Function and providers | `--arguments` fields | -|---|---|---| -| `tasks.list` | List/filter tasks; all | `task_id:string` or `task_ids:string[]`; `status:string`; `tags:string\|string[]`; `offset:integer=0`; `limit:integer=50` | -| `tasks.files` | List files and priorities for one task; all | `task_id*:string`; `offset:integer=0`; `limit:integer=50` | -| `tasks.trackers` | List tracker URLs; qBittorrent, Transmission | `task_id*:string` | -| `tasks.tags.get` | Read tags/labels for one task; all | `task_id*:string` | -| `tasks.peers` | Read peer synchronization data; qBittorrent | `task_id*:string` | +### `session.content_layout` +Read qBittorrent's default torrent content layout. Effect: `safe_read`. Providers: `qbittorrent`. +- `arguments`: `{}` -### Task control +### `session.details` +Read Transmission session configuration and capacity details. Effect: `safe_read`. Providers: `transmission`. +- `arguments`: `{}` -| Action | Function and effect | `--arguments` fields | -|---|---|---| -| `tasks.start` | Start/resume tasks; reversible write | exactly one of `task_id:string`, `task_ids:string[]` | -| `tasks.stop` | Pause tasks; reversible write | exactly one of `task_id:string`, `task_ids:string[]` | -| `tasks.recheck` | Force data verification; external side effect | exactly one of `task_id:string`, `task_ids:string[]` | -| `tasks.reannounce` | Force tracker reannounce; qBittorrent/Transmission, external side effect | exactly one of `task_id:string`, `task_ids:string[]` | -| `tasks.queue.move` | Move queue position; qBittorrent/Transmission, reversible write | exactly one of `task_id:string`, `task_ids:string[]`; `position*:string` = `top\|up\|down\|bottom` | -| `tasks.force_start.set` | Toggle force-start; qBittorrent, reversible write | exactly one of `task_id:string`, `task_ids:string[]`; `enabled*:boolean` | -| `tasks.files.selection.set` | Select files within one task; reversible write | `task_id*:string`; `wanted_file_ids:integer[]`; `unwanted_file_ids:integer[]`; at least one list, with no overlapping index | -| `tasks.properties.set` | Set per-task limits; reversible write | `task_id*:string`; at least one of `upload_limit:number`, `download_limit:number`, `ratio_limit:number`, `seeding_time_limit:integer` minutes. rTorrent supports only speed fields | -| `tasks.location.set` | Move/retarget data to a downloader-side path; external side effect | `task_id*:string`; `location*:string` | -| `tasks.category.set` | Set a non-empty category; qBittorrent, reversible write | `task_id*:string`; `category*:string` | -| `tasks.tags.set` | Set/add tags or labels; reversible write | exactly one of `task_id:string`, `task_ids:string[]`; `tags*:string[]` | -| `tasks.trackers.update` | Add/replace trackers; qBittorrent/Transmission, reversible write | `task_id*:string`; `trackers*:string[]` of URLs | -| `tasks.delete` | Delete tasks and optionally data; destructive write | exactly one of `task_id:string`, `task_ids:string[]`; `delete_files:boolean=false` | -| `tasks.add.direct` | Submit directly to provider, bypassing MoviePilot orchestration; external side effect | `content*:string` magnet/URL/path; `torrent_file:boolean=false`; `paused:boolean=false`; `download_dir:string`; `tags:string[]`; `category:string` (qBittorrent only) | +### `session.speed_limits.get` +Read global speed limits. Effect: `safe_read`. Providers: `qbittorrent, transmission`. +- `arguments`: `{}` -### Session operations +### `session.speed_limits.set` +Set global speed limits in KB/s. Effect: `reversible_write`. Providers: `qbittorrent, transmission`. +- `download_limit` (number): Global download limit in KB/s; 0 or omission means unlimited. +- `upload_limit` (number): Global upload limit in KB/s; 0 or omission means unlimited. -| Action | Function and providers | `--arguments` fields | -|---|---|---| -| `session.stats` | Read transfer/session statistics; all | none (`{}`) | -| `session.speed_limits.get` | Read global download/upload limits; qBittorrent, Transmission | none (`{}`) | -| `session.speed_limits.set` | Set global limits; qBittorrent, Transmission | at least one of `download_limit:number`, `upload_limit:number`; use explicit `0` to clear a limit | -| `session.details` | Read Transmission session configuration/capacity; Transmission | none (`{}`) | -| `session.content_layout` | Read default torrent content layout; qBittorrent | none (`{}`) | +### `session.stats` +Read provider transfer/session statistics. Effect: `safe_read`. Providers: `qbittorrent, transmission, rtorrent`. +- `arguments`: `{}` -Examples: +### `tasks.add.direct` +Submit a magnet, URL, or local torrent file directly to the provider. Effect: `external_side_effect`. Providers: `qbittorrent, transmission, rtorrent`. +- `content*` (string): Magnet URI, torrent URL, or a local torrent path when torrent_file=true. +- `torrent_file` (boolean; default `False`): Interpret content as a local torrent-file path. +- `paused` (boolean; default `False`): Add the task in a paused state. +- `download_dir` (string): Provider-side save path. +- `tags` (string[]): Tags to assign to the new task. +- `category` (string): qBittorrent category; ignored by other providers. -```bash -# Read one task's files. -python skills/downloader-operation/scripts/mp-downloader.py call \ - --client "main-qb" \ - --action tasks.files \ - --arguments '{"task_id":"exact-provider-hash","offset":0,"limit":50}' +### `tasks.category.set` +Set qBittorrent category. Effect: `reversible_write`. Providers: `qbittorrent`. +- `task_id*` (string): One provider-native task hash or ID. +- `category*` (string): Non-empty qBittorrent category name. -# Limit one task to 2048 KB/s download and 512 KB/s upload. -python skills/downloader-operation/scripts/mp-downloader.py call \ - --client "main-qb" \ - --action tasks.properties.set \ - --arguments '{"task_id":"exact-provider-hash","download_limit":2048,"upload_limit":512}' -``` +### `tasks.delete` +Delete tasks and optionally their data. Effect: `destructive_write`. Providers: `qbittorrent, transmission, rtorrent`. +- `task_id` (string): One provider-native task hash or ID. +- `task_ids` (string[]): Multiple provider-native task hashes or IDs; mutually exclusive with task_id. +- `delete_files` (boolean; default `False`): Also permanently delete the task data files. +- Rule: Provide exactly one of task_id and task_ids. -Before deleting data, confirm the exact client, tasks, and `delete_files=true`. -Before a direct add, confirm the exact magnet/URL or local torrent file, client, -paused state, provider path, tags, and category. +### `tasks.files` +List files and priorities for one task. Effect: `safe_read`. Providers: `qbittorrent, transmission, rtorrent`. +- `task_id*` (string): One provider-native task hash or ID. +- `offset` (integer; default `0`): Zero-based list offset. +- `limit` (integer; default `50`): Number of items to return, from 1 to 200. -For `tasks.files.selection.set`, pass provider file indexes from `tasks.files` -through `wanted_file_ids` and/or `unwanted_file_ids`; never infer indexes from -filenames alone. `session.details` is Transmission-only and -`session.content_layout` is qBittorrent-only. +### `tasks.files.selection.set` +Select wanted and unwanted files within one task. Effect: `reversible_write`. Providers: `qbittorrent, transmission, rtorrent`. +- `task_id*` (string): One provider-native task hash or ID. +- `wanted_file_ids` (integer[]): Provider file indexes to download; provide this or unwanted_file_ids. +- `unwanted_file_ids` (integer[]): Provider file indexes to skip; provide this or wanted_file_ids. +- Rule: Provide wanted_file_ids or unwanted_file_ids, and never place one index in both lists. + +### `tasks.force_start.set` +Enable or disable qBittorrent force-start for tasks. Effect: `reversible_write`. Providers: `qbittorrent`. +- `task_id` (string): One provider-native task hash or ID. +- `task_ids` (string[]): Multiple provider-native task hashes or IDs; mutually exclusive with task_id. +- `enabled*` (boolean): Whether force-start is enabled. +- Rule: Provide exactly one of task_id and task_ids. + +### `tasks.list` +List and filter downloader tasks. Effect: `safe_read`. Providers: `qbittorrent, transmission, rtorrent`. +- `task_id` (string): One provider-native task hash or ID. +- `task_ids` (string[]): Multiple provider-native task hashes or IDs; mutually exclusive with task_id. +- `status` (string): Filter by the provider-native task status. +- `tags` (string|string[]): Return only tasks that contain all specified tags. +- `offset` (integer; default `0`): Zero-based list offset. +- `limit` (integer; default `50`): Number of items to return, from 1 to 200. + +### `tasks.location.set` +Move or retarget one task to a provider-side path. Effect: `external_side_effect`. Providers: `qbittorrent, transmission, rtorrent`. +- `task_id*` (string): One provider-native task hash or ID. +- `location*` (string): New provider-side save path. + +### `tasks.peers` +Read qBittorrent peer synchronization data. Effect: `safe_read`. Providers: `qbittorrent`. +- `task_id*` (string): One provider-native task hash or ID. + +### `tasks.properties.set` +Set task speed, ratio, or seeding-time limits. Effect: `reversible_write`. Providers: `qbittorrent, transmission, rtorrent`. +- `task_id*` (string): One provider-native task hash or ID. +- `upload_limit` (number): Upload limit in KB/s; 0 means unlimited. +- `download_limit` (number): Download limit in KB/s; 0 means unlimited. +- `ratio_limit` (number): Share-ratio limit; unsupported by rTorrent. +- `seeding_time_limit` (integer): Seeding-time limit in minutes; unsupported by rTorrent. + +### `tasks.queue.move` +Move tasks to top, up, down, or bottom of the queue. Effect: `reversible_write`. Providers: `qbittorrent, transmission`. +- `task_id` (string): One provider-native task hash or ID. +- `task_ids` (string[]): Multiple provider-native task hashes or IDs; mutually exclusive with task_id. +- `position*` (string; allowed values `top,up,down,bottom`): Target queue position. +- Rule: Provide exactly one of task_id and task_ids. + +### `tasks.reannounce` +Force tracker reannounce. Effect: `external_side_effect`. Providers: `qbittorrent, transmission`. +- `task_id` (string): One provider-native task hash or ID. +- `task_ids` (string[]): Multiple provider-native task hashes or IDs; mutually exclusive with task_id. +- Rule: Provide exactly one of task_id and task_ids. + +### `tasks.recheck` +Force data verification for tasks. Effect: `external_side_effect`. Providers: `qbittorrent, transmission, rtorrent`. +- `task_id` (string): One provider-native task hash or ID. +- `task_ids` (string[]): Multiple provider-native task hashes or IDs; mutually exclusive with task_id. +- Rule: Provide exactly one of task_id and task_ids. + +### `tasks.start` +Start or resume one or more tasks. Effect: `reversible_write`. Providers: `qbittorrent, transmission, rtorrent`. +- `task_id` (string): One provider-native task hash or ID. +- `task_ids` (string[]): Multiple provider-native task hashes or IDs; mutually exclusive with task_id. +- Rule: Provide exactly one of task_id and task_ids. + +### `tasks.stop` +Pause one or more tasks. Effect: `reversible_write`. Providers: `qbittorrent, transmission, rtorrent`. +- `task_id` (string): One provider-native task hash or ID. +- `task_ids` (string[]): Multiple provider-native task hashes or IDs; mutually exclusive with task_id. +- Rule: Provide exactly one of task_id and task_ids. + +### `tasks.tags.get` +Read task tags or labels. Effect: `safe_read`. Providers: `qbittorrent, transmission, rtorrent`. +- `task_id*` (string): One provider-native task hash or ID. + +### `tasks.tags.set` +Set or add task tags/labels. Effect: `reversible_write`. Providers: `qbittorrent, transmission, rtorrent`. +- `task_id` (string): One provider-native task hash or ID. +- `task_ids` (string[]): Multiple provider-native task hashes or IDs; mutually exclusive with task_id. +- `tags*` (string[]): Tags or labels to set or add. +- Rule: Provide exactly one of task_id and task_ids. + +### `tasks.trackers` +List trackers for one task. Effect: `safe_read`. Providers: `qbittorrent, transmission`. +- `task_id*` (string): One provider-native task hash or ID. + +### `tasks.trackers.update` +Add or replace task trackers. Effect: `reversible_write`. Providers: `qbittorrent, transmission`. +- `task_id*` (string): One provider-native task hash or ID. +- `trackers*` (string[]): Tracker URL list. ## Verification diff --git a/skills/downloader-operation/scripts/mp-downloader.py b/skills/downloader-operation/scripts/mp-downloader.py index 3add916a7..8d3a87f51 100644 --- a/skills/downloader-operation/scripts/mp-downloader.py +++ b/skills/downloader-operation/scripts/mp-downloader.py @@ -83,21 +83,40 @@ class ActionSpec: } -TASK_ID = ArgumentSpec("task_id", "string", "单个任务的 provider 原生 hash 或 ID。") -TASK_IDS = ArgumentSpec("task_ids", "string[]", "多个任务的 provider 原生 hash 或 ID;与 task_id 二选一。") -OFFSET = ArgumentSpec("offset", "integer", "列表起始偏移,必须大于等于 0。", default=0) -LIMIT = ArgumentSpec("limit", "integer", "返回条数,范围 1..200。", default=DEFAULT_LIMIT) +TASK_ID = ArgumentSpec("task_id", "string", "One provider-native task hash or ID.") +TASK_IDS = ArgumentSpec( + "task_ids", + "string[]", + "Multiple provider-native task hashes or IDs; mutually exclusive with task_id.", +) +OFFSET = ArgumentSpec("offset", "integer", "Zero-based list offset.", default=0) +LIMIT = ArgumentSpec("limit", "integer", "Number of items to return, from 1 to 200.", default=DEFAULT_LIMIT) ACTIONS: dict[str, ActionSpec] = { + "instances.list": ActionSpec( + "List configured downloader instances without connection secrets.", + "safe_read", + ), + "capabilities.list": ActionSpec( + "List supported downloader actions and their complete argument contracts.", + "safe_read", + arguments=( + ArgumentSpec( + "action_name", + "string", + "Optional exact action name used to return one capability contract.", + ), + ), + ), "tasks.list": ActionSpec( "List and filter downloader tasks.", "safe_read", arguments=( TASK_ID, TASK_IDS, - ArgumentSpec("status", "string", "按 provider 原生任务状态过滤。"), - ArgumentSpec("tags", "string|string[]", "只返回同时包含这些标签的任务。"), + ArgumentSpec("status", "string", "Filter by the provider-native task status."), + ArgumentSpec("tags", "string|string[]", "Return only tasks that contain all specified tags."), OFFSET, LIMIT, ), @@ -112,10 +131,12 @@ ACTIONS: dict[str, ActionSpec] = { "reversible_write", arguments=( ArgumentSpec("task_id", "string", TASK_ID.description, required=True), - ArgumentSpec("wanted_file_ids", "integer[]", "要下载的 provider 文件索引;与 unwanted_file_ids 至少提供一项。"), - ArgumentSpec("unwanted_file_ids", "integer[]", "跳过的 provider 文件索引;与 wanted_file_ids 至少提供一项。"), + ArgumentSpec("wanted_file_ids", "integer[]", "Provider file indexes to download; provide this or unwanted_file_ids."), + ArgumentSpec("unwanted_file_ids", "integer[]", "Provider file indexes to skip; provide this or wanted_file_ids."), + ), + argument_rules=( + "Provide wanted_file_ids or unwanted_file_ids, and never place one index in both lists.", ), - argument_rules=("wanted_file_ids 与 unwanted_file_ids 至少提供一项,且同一索引不能同时出现。",), ), "tasks.trackers": ActionSpec( "List trackers for one task.", @@ -138,13 +159,13 @@ ACTIONS: dict[str, ActionSpec] = { "Start or resume one or more tasks.", "reversible_write", arguments=(TASK_ID, TASK_IDS), - argument_rules=("task_id 与 task_ids 必须提供且只能选择一种。",), + argument_rules=("Provide exactly one of task_id and task_ids.",), ), "tasks.stop": ActionSpec( "Pause one or more tasks.", "reversible_write", arguments=(TASK_ID, TASK_IDS), - argument_rules=("task_id 与 task_ids 必须提供且只能选择一种。",), + argument_rules=("Provide exactly one of task_id and task_ids.",), ), "tasks.delete": ActionSpec( "Delete tasks and optionally their data.", @@ -152,22 +173,22 @@ ACTIONS: dict[str, ActionSpec] = { arguments=( TASK_ID, TASK_IDS, - ArgumentSpec("delete_files", "boolean", "同时永久删除任务数据文件。", default=False), + ArgumentSpec("delete_files", "boolean", "Also permanently delete the task data files.", default=False), ), - argument_rules=("task_id 与 task_ids 必须提供且只能选择一种。",), + argument_rules=("Provide exactly one of task_id and task_ids.",), ), "tasks.recheck": ActionSpec( "Force data verification for tasks.", "external_side_effect", arguments=(TASK_ID, TASK_IDS), - argument_rules=("task_id 与 task_ids 必须提供且只能选择一种。",), + argument_rules=("Provide exactly one of task_id and task_ids.",), ), "tasks.reannounce": ActionSpec( "Force tracker reannounce.", "external_side_effect", ("qbittorrent", "transmission"), (TASK_ID, TASK_IDS), - ("task_id 与 task_ids 必须提供且只能选择一种。",), + ("Provide exactly one of task_id and task_ids.",), ), "tasks.queue.move": ActionSpec( "Move tasks to top, up, down, or bottom of the queue.", @@ -179,12 +200,12 @@ ACTIONS: dict[str, ActionSpec] = { ArgumentSpec( "position", "string", - "目标队列位置。", + "Target queue position.", required=True, enum=("top", "up", "down", "bottom"), ), ), - ("task_id 与 task_ids 必须提供且只能选择一种。",), + ("Provide exactly one of task_id and task_ids.",), ), "tasks.force_start.set": ActionSpec( "Enable or disable qBittorrent force-start for tasks.", @@ -193,19 +214,19 @@ ACTIONS: dict[str, ActionSpec] = { ( TASK_ID, TASK_IDS, - ArgumentSpec("enabled", "boolean", "是否启用强制开始。", required=True), + ArgumentSpec("enabled", "boolean", "Whether force-start is enabled.", required=True), ), - ("task_id 与 task_ids 必须提供且只能选择一种。",), + ("Provide exactly one of task_id and task_ids.",), ), "tasks.properties.set": ActionSpec( "Set task speed, ratio, or seeding-time limits.", "reversible_write", arguments=( ArgumentSpec("task_id", "string", TASK_ID.description, required=True), - ArgumentSpec("upload_limit", "number", "上传限速,单位 KB/s;0 表示不限速。"), - ArgumentSpec("download_limit", "number", "下载限速,单位 KB/s;0 表示不限速。"), - ArgumentSpec("ratio_limit", "number", "分享率上限;rTorrent 不支持。"), - ArgumentSpec("seeding_time_limit", "integer", "做种时间上限,单位分钟;rTorrent 不支持。"), + ArgumentSpec("upload_limit", "number", "Upload limit in KB/s; 0 means unlimited."), + ArgumentSpec("download_limit", "number", "Download limit in KB/s; 0 means unlimited."), + ArgumentSpec("ratio_limit", "number", "Share-ratio limit; unsupported by rTorrent."), + ArgumentSpec("seeding_time_limit", "integer", "Seeding-time limit in minutes; unsupported by rTorrent."), ), ), "tasks.location.set": ActionSpec( @@ -213,7 +234,7 @@ ACTIONS: dict[str, ActionSpec] = { "external_side_effect", arguments=( ArgumentSpec("task_id", "string", TASK_ID.description, required=True), - ArgumentSpec("location", "string", "下载器侧的新保存路径。", required=True), + ArgumentSpec("location", "string", "New provider-side save path.", required=True), ), ), "tasks.category.set": ActionSpec( @@ -222,7 +243,7 @@ ACTIONS: dict[str, ActionSpec] = { ("qbittorrent",), ( ArgumentSpec("task_id", "string", TASK_ID.description, required=True), - ArgumentSpec("category", "string", "非空分类名称。", required=True), + ArgumentSpec("category", "string", "Non-empty qBittorrent category name.", required=True), ), ), "tasks.tags.set": ActionSpec( @@ -231,9 +252,9 @@ ACTIONS: dict[str, ActionSpec] = { arguments=( TASK_ID, TASK_IDS, - ArgumentSpec("tags", "string[]", "要设置或添加的标签列表。", required=True), + ArgumentSpec("tags", "string[]", "Tags or labels to set or add.", required=True), ), - argument_rules=("task_id 与 task_ids 必须提供且只能选择一种。",), + argument_rules=("Provide exactly one of task_id and task_ids.",), ), "tasks.trackers.update": ActionSpec( "Add or replace task trackers.", @@ -241,19 +262,19 @@ ACTIONS: dict[str, ActionSpec] = { ("qbittorrent", "transmission"), ( ArgumentSpec("task_id", "string", TASK_ID.description, required=True), - ArgumentSpec("trackers", "string[]", "Tracker URL 列表。", required=True), + ArgumentSpec("trackers", "string[]", "Tracker URL list.", required=True), ), ), "tasks.add.direct": ActionSpec( "Submit a magnet, URL, or local torrent file directly to the provider.", "external_side_effect", arguments=( - ArgumentSpec("content", "string", "Magnet、torrent URL,或 torrent_file=true 时的本地种子文件路径。", required=True), - ArgumentSpec("torrent_file", "boolean", "将 content 解释为本地种子文件路径。", default=False), - ArgumentSpec("paused", "boolean", "以暂停状态添加任务。", default=False), - ArgumentSpec("download_dir", "string", "下载器侧保存路径。"), - ArgumentSpec("tags", "string[]", "添加到任务的标签。"), - ArgumentSpec("category", "string", "qBittorrent 分类;其他 provider 忽略。"), + ArgumentSpec("content", "string", "Magnet URI, torrent URL, or a local torrent path when torrent_file=true.", required=True), + ArgumentSpec("torrent_file", "boolean", "Interpret content as a local torrent-file path.", default=False), + ArgumentSpec("paused", "boolean", "Add the task in a paused state.", default=False), + ArgumentSpec("download_dir", "string", "Provider-side save path."), + ArgumentSpec("tags", "string[]", "Tags to assign to the new task."), + ArgumentSpec("category", "string", "qBittorrent category; ignored by other providers."), ), ), "session.stats": ActionSpec("Read provider transfer/session statistics.", "safe_read"), @@ -263,8 +284,8 @@ ACTIONS: dict[str, ActionSpec] = { "reversible_write", ("qbittorrent", "transmission"), ( - ArgumentSpec("download_limit", "number", "全局下载限速,单位 KB/s;0 或省略表示不限速。"), - ArgumentSpec("upload_limit", "number", "全局上传限速,单位 KB/s;0 或省略表示不限速。"), + ArgumentSpec("download_limit", "number", "Global download limit in KB/s; 0 or omission means unlimited."), + ArgumentSpec("upload_limit", "number", "Global upload limit in KB/s; 0 or omission means unlimited."), ), ), "session.details": ActionSpec( @@ -758,6 +779,25 @@ def call_action(client_name: Optional[str], action: str, arguments: Mapping[str, if spec is None: raise ValueError(f"未知 downloader action: {action}") _validate_action_arguments(action, spec, arguments) + if action == "instances.list": + return { + "success": True, + "client": None, + "provider": None, + "action": action, + "effect": spec.effect, + "data": list_instances()["instances"], + } + if action == "capabilities.list": + capabilities = list_capabilities(client_name, arguments.get("action_name")) + return { + "success": True, + "client": capabilities["client"], + "provider": capabilities["provider"], + "action": action, + "effect": spec.effect, + "data": capabilities["actions"], + } config = _select_config(client_name) provider = str(config.type or "").lower() if provider not in spec.providers: diff --git a/skills/mediaserver-operation/SKILL.md b/skills/mediaserver-operation/SKILL.md index 64f0a5fa1..89ae5f0ba 100644 --- a/skills/mediaserver-operation/SKILL.md +++ b/skills/mediaserver-operation/SKILL.md @@ -1,6 +1,6 @@ --- name: mediaserver-operation -version: 2 +version: 3 description: >- Use this skill when the user asks to inspect, diagnose, or directly operate a configured Emby, Jellyfin, Plex, ZSpace, UGREEN, TrimeMedia, or Navidrome @@ -113,84 +113,122 @@ instances remain ambiguous, the result lists the valid server names. ## Complete Action Contract -In the tables below, `*` means required. Every listed field belongs inside the -single `--arguments` JSON object. Do not send fields that are not listed. +This is the complete Media Server Operation action contract. It comes directly from the script `ACTIONS` registry and matches the external MCP `tools/list` oneOf branches. +A field name ending in `*` is required. Put every action parameter in the `arguments` object. -Shared rules: +| action | Purpose and argument summary | +| :--- | :--- | +| `activity.backdrops` | Read recent provider backdrop images.; arguments: `limit`, `remote` | +| `activity.latest` | Read recently added provider items.; arguments: `limit`, `username` | +| `activity.resume` | Read in-progress/resumable provider items.; arguments: `limit`, `username` | +| `capabilities.list` | List supported media-server actions and their complete argument contracts.; arguments: `action_name` | +| `instances.list` | List configured media-server instances without connection secrets.; no arguments | +| `items.count` | Count items below one library or parent.; arguments: `parent` | +| `items.detail` | Read one provider item by native ID.; arguments: `item_id*` | +| `items.list` | Page items below one library or parent.; arguments: `parent`, `offset`, `limit` | +| `items.movies.search` | Search provider-native movie items by title and optional year.; arguments: `title*`, `year` | +| `items.music.search` | Search provider-native music by title, artist, or album.; arguments: `title`, `artist`, `album` | +| `items.season_episodes` | Read native episode coverage for one series and optional season.; arguments: `item_id`, `title`, `year`, `season` | +| `libraries.list` | List visible provider libraries.; arguments: `hidden`, `username` | +| `library.scan` | Trigger a provider library scan.; arguments: `scan_mode` | +| `metadata.refresh` | Refresh provider metadata for mapped items.; arguments: `items*` | +| `playback.sessions` | Read active playback sessions.; no arguments | +| `playback.url` | Build the provider play URL for one item.; arguments: `item_id*` | +| `server.statistics` | Read media counts and provider statistics.; no arguments | +| `server.user.library_folders` | Read the current user's visible library folders.; no arguments | +| `server.users.count` | Read provider user count.; no arguments | -- Paged reads accept `offset:integer=0` where documented and - `limit:integer=50`; `offset` must be non-negative and `limit` is clamped to - `1..200`. -- `parent`, `item_id`, library IDs, and usernames are native to the selected - server. Obtain them from that server's earlier response; never reuse IDs from - another instance. -- All actions support only the providers shown by `capabilities`. The provider - list below lets the Agent choose without inspecting source; query the selected - instance only when provider support must be confirmed. +### `activity.backdrops` +Read recent provider backdrop images. Effect: `safe_read`. Providers: `ugreen, trimemedia`. +- `limit` (integer; default `50`): Number of items to return, from 1 to 200. +- `remote` (boolean; default `False`): Return provider URLs that are remotely accessible. -Provider abbreviations used below: all = Emby, Jellyfin, Plex, ZSpace, UGREEN, -TrimeMedia, and Navidrome. +### `activity.latest` +Read recently added provider items. Effect: `safe_read`. Providers: `emby, jellyfin, plex, zspace, ugreen, trimemedia, navidrome`. +- `limit` (integer; default `50`): Number of items to return, from 1 to 200. +- `username` (string): Read for this username; supported by Emby, Jellyfin, and ZSpace. -### Server and library reads +### `activity.resume` +Read in-progress/resumable provider items. Effect: `safe_read`. Providers: `emby, jellyfin, plex, zspace, ugreen, trimemedia, navidrome`. +- `limit` (integer; default `50`): Number of items to return, from 1 to 200. +- `username` (string): Read for this username; supported by Emby, Jellyfin, and ZSpace. -| Action | Function and providers | `--arguments` fields | -|---|---|---| -| `server.statistics` | Read media counts/provider statistics; all | none (`{}`) | -| `server.users.count` | Read provider user count; Emby, Jellyfin, ZSpace, UGREEN, TrimeMedia, Navidrome | none (`{}`) | -| `server.user.library_folders` | Read current user's visible folders; Emby, Jellyfin, ZSpace | none (`{}`) | -| `libraries.list` | List visible libraries; all | `hidden:boolean=false` (true = configured sync scope only); `username:string` only for Emby/Jellyfin/ZSpace | -| `items.list` | Page items below a library/parent; all | `parent:string\|integer` required except Navidrome; `offset:integer=0`; `limit:integer=50` | -| `items.count` | Count items below a library/parent; all | `parent:string\|integer` required except Navidrome; omitted on Navidrome uses `music` | -| `items.detail` | Read one provider item; all | `item_id*:string` | +### `capabilities.list` +List supported media-server actions and their complete argument contracts. Effect: `safe_read`. Providers: `emby, jellyfin, plex, zspace, ugreen, trimemedia, navidrome`. +- `action_name` (string): Optional exact action name used to return one capability contract. -### Native search and activity +### `instances.list` +List configured media-server instances without connection secrets. Effect: `safe_read`. Providers: `emby, jellyfin, plex, zspace, ugreen, trimemedia, navidrome`. +- `arguments`: `{}` -| Action | Function and providers | `--arguments` fields | -|---|---|---| -| `items.movies.search` | Search movies; Emby, Jellyfin, Plex, ZSpace, UGREEN, TrimeMedia | `title*:string`; `year:string\|integer` | -| `items.music.search` | Search music; Emby, Jellyfin, Plex, ZSpace, UGREEN, Navidrome | `title:string`; `artist:string`; `album:string`; at least one is required | -| `items.season_episodes` | Read existing episode coverage for a series; Emby, Jellyfin, Plex, ZSpace, UGREEN, TrimeMedia | `item_id:string`; `title:string`; at least one is required; optional `year:string\|integer`; `season:integer` | -| `activity.latest` | Read recently added items; all | `limit:integer=50`; `username:string` only for Emby/Jellyfin/ZSpace | -| `activity.resume` | Read in-progress/resumable items; all | `limit:integer=50`; `username:string` only for Emby/Jellyfin/ZSpace | -| `activity.backdrops` | Read recent backdrop URLs; UGREEN, TrimeMedia | `limit:integer=50`; `remote:boolean=false` | +### `items.count` +Count items below one library or parent. Effect: `safe_read`. Providers: `emby, jellyfin, plex, zspace, ugreen, trimemedia, navidrome`. +- `parent` (string|integer): Library or parent item ID; Navidrome may omit it and use music. +- Rule: parent is required except for Navidrome, which defaults to music. -### Playback and writes +### `items.detail` +Read one provider item by native ID. Effect: `safe_read`. Providers: `emby, jellyfin, plex, zspace, ugreen, trimemedia, navidrome`. +- `item_id*` (string): Provider-native item ID returned by the selected media server. -| Action | Function, providers, and effect | `--arguments` fields | -|---|---|---| -| `playback.sessions` | Read active sessions; Emby, Jellyfin, Plex; safe read | none (`{}`) | -| `playback.url` | Build provider play URL; all; safe read | `item_id*:string` | -| `library.scan` | Trigger provider root-library scan; all; external side effect | `scan_mode:string\|integer` only for UGREEN; otherwise omit | -| `metadata.refresh` | Refresh metadata for mapped items; Emby, Plex, ZSpace, UGREEN, TrimeMedia; external side effect | `items*:object[]`; each object supports `title:string`, `year:string\|integer`, `type:string` (`电影\|电视剧\|音乐`), `category:string`, `target_path:string` | +### `items.list` +Page items below one library or parent. Effect: `safe_read`. Providers: `emby, jellyfin, plex, zspace, ugreen, trimemedia, navidrome`. +- `parent` (string|integer): Library or parent item ID; Navidrome may omit it and use music. +- `offset` (integer; default `0`): Zero-based list offset. +- `limit` (integer; default `50`): Number of items to return, from 1 to 200. +- Rule: parent is required except for Navidrome, which ignores it. -Examples: +### `items.movies.search` +Search provider-native movie items by title and optional year. Effect: `safe_read`. Providers: `emby, jellyfin, plex, zspace, ugreen, trimemedia`. +- `title*` (string): Movie title. +- `year` (string|integer): Optional release year. -```bash -# List the first page below an exact library ID. -python skills/mediaserver-operation/scripts/mp-mediaserver.py call \ - --server "living-room" \ - --action items.list \ - --arguments '{"parent":"exact-library-id","offset":0,"limit":50}' +### `items.music.search` +Search provider-native music by title, artist, or album. Effect: `safe_read`. Providers: `emby, jellyfin, plex, zspace, ugreen, navidrome`. +- `title` (string): Track, album, or music-item title. +- `artist` (string): Artist name. +- `album` (string): Album name; provide title, artist, or album. +- Rule: Provide at least one of title, artist, and album. -# Read season 2 coverage using an exact provider series ID. -python skills/mediaserver-operation/scripts/mp-mediaserver.py call \ - --server "living-room" \ - --action items.season_episodes \ - --arguments '{"item_id":"exact-series-id","season":2}' -``` +### `items.season_episodes` +Read native episode coverage for one series and optional season. Effect: `safe_read`. Providers: `emby, jellyfin, plex, zspace, ugreen, trimemedia`. +- `item_id` (string): Provider-native item ID returned by the selected media server. +- `title` (string): Series title; provide it or item_id. +- `year` (string|integer): Optional premiere year. +- `season` (integer): Optional season number. +- Rule: Provide at least one of item_id and title. -Use the exact `server` and item/library IDs returned by earlier calls. Do not -invent IDs or reuse IDs across different server instances. `items.list` expects -`parent` for all video providers; Navidrome uses its single music library and -does not require a parent. `metadata.refresh` accepts an `items` array matching -MoviePilot's refresh item contract (`title`, `year`, `type`, `category`, -`target_path`). +### `libraries.list` +List visible provider libraries. Effect: `safe_read`. Providers: `emby, jellyfin, plex, zspace, ugreen, trimemedia, navidrome`. +- `hidden` (boolean; default `False`): Return only libraries configured for synchronization. +- `username` (string): Read libraries visible to this username; supported by Emby, Jellyfin, and ZSpace. -Use `items.movies.search` for provider-native movie lookup, -`items.music.search` for a title/artist/album lookup, and -`items.season_episodes` when a direct server series ID or exact title is known. -These results describe one server only; use `library.exists` when the task needs -MoviePilot's canonical cross-server duplicate decision. +### `library.scan` +Trigger a provider library scan. Effect: `external_side_effect`. Providers: `emby, jellyfin, plex, zspace, ugreen, trimemedia, navidrome`. +- `scan_mode` (string|integer): UGREEN-native scan mode; omit it for every other provider. + +### `metadata.refresh` +Refresh provider metadata for mapped items. Effect: `external_side_effect`. Providers: `emby, plex, zspace, ugreen, trimemedia`. +- `items*` (object[]): Items to refresh. Each item supports title:string, year:string|integer, type using the exact MoviePilot media-type value, category:string, and target_path:string. + +### `playback.sessions` +Read active playback sessions. Effect: `safe_read`. Providers: `emby, jellyfin, plex`. +- `arguments`: `{}` + +### `playback.url` +Build the provider play URL for one item. Effect: `safe_read`. Providers: `emby, jellyfin, plex, zspace, ugreen, trimemedia, navidrome`. +- `item_id*` (string): Provider-native item ID returned by the selected media server. + +### `server.statistics` +Read media counts and provider statistics. Effect: `safe_read`. Providers: `emby, jellyfin, plex, zspace, ugreen, trimemedia, navidrome`. +- `arguments`: `{}` + +### `server.user.library_folders` +Read the current user's visible library folders. Effect: `safe_read`. Providers: `emby, jellyfin, zspace`. +- `arguments`: `{}` + +### `server.users.count` +Read provider user count. Effect: `safe_read`. Providers: `emby, jellyfin, zspace, ugreen, trimemedia, navidrome`. +- `arguments`: `{}` ## Safety And Verification diff --git a/skills/mediaserver-operation/scripts/mp-mediaserver.py b/skills/mediaserver-operation/scripts/mp-mediaserver.py index b98e45e2c..3848ac095 100644 --- a/skills/mediaserver-operation/scripts/mp-mediaserver.py +++ b/skills/mediaserver-operation/scripts/mp-mediaserver.py @@ -96,13 +96,28 @@ class ActionSpec: } -ITEM_ID = ArgumentSpec("item_id", "string", "当前媒体服务器返回的 provider 原生条目 ID。") -PARENT = ArgumentSpec("parent", "string|integer", "媒体库或父条目 ID;Navidrome 可省略并使用 music。") -OFFSET = ArgumentSpec("offset", "integer", "列表起始偏移,必须大于等于 0。", default=0) -LIMIT = ArgumentSpec("limit", "integer", "返回条数,范围 1..200。", default=DEFAULT_LIMIT) +ITEM_ID = ArgumentSpec("item_id", "string", "Provider-native item ID returned by the selected media server.") +PARENT = ArgumentSpec("parent", "string|integer", "Library or parent item ID; Navidrome may omit it and use music.") +OFFSET = ArgumentSpec("offset", "integer", "Zero-based list offset.", default=0) +LIMIT = ArgumentSpec("limit", "integer", "Number of items to return, from 1 to 200.", default=DEFAULT_LIMIT) ACTIONS: dict[str, ActionSpec] = { + "instances.list": ActionSpec( + "List configured media-server instances without connection secrets.", + "safe_read", + ), + "capabilities.list": ActionSpec( + "List supported media-server actions and their complete argument contracts.", + "safe_read", + arguments=( + ArgumentSpec( + "action_name", + "string", + "Optional exact action name used to return one capability contract.", + ), + ), + ), "server.statistics": ActionSpec("Read media counts and provider statistics.", "safe_read"), "server.users.count": ActionSpec( "Read provider user count.", @@ -118,21 +133,21 @@ ACTIONS: dict[str, ActionSpec] = { "List visible provider libraries.", "safe_read", arguments=( - ArgumentSpec("hidden", "boolean", "仅返回配置为同步范围的媒体库。", default=False), - ArgumentSpec("username", "string", "按用户名读取可见媒体库;仅 Emby、Jellyfin、ZSpace 支持。"), + ArgumentSpec("hidden", "boolean", "Return only libraries configured for synchronization.", default=False), + ArgumentSpec("username", "string", "Read libraries visible to this username; supported by Emby, Jellyfin, and ZSpace."), ), ), "items.list": ActionSpec( "Page items below one library or parent.", "safe_read", arguments=(PARENT, OFFSET, LIMIT), - argument_rules=("除 Navidrome 外必须提供 parent;Navidrome 忽略 parent。",), + argument_rules=("parent is required except for Navidrome, which ignores it.",), ), "items.count": ActionSpec( "Count items below one library or parent.", "safe_read", arguments=(PARENT,), - argument_rules=("除 Navidrome 外必须提供 parent;Navidrome 省略时使用 music。",), + argument_rules=("parent is required except for Navidrome, which defaults to music.",), ), "items.detail": ActionSpec( "Read one provider item by native ID.", @@ -144,8 +159,8 @@ ACTIONS: dict[str, ActionSpec] = { "safe_read", ("emby", "jellyfin", "plex", "zspace", "ugreen", "trimemedia"), ( - ArgumentSpec("title", "string", "电影标题。", required=True), - ArgumentSpec("year", "string|integer", "可选发行年份。"), + ArgumentSpec("title", "string", "Movie title.", required=True), + ArgumentSpec("year", "string|integer", "Optional release year."), ), ), "items.music.search": ActionSpec( @@ -153,11 +168,11 @@ ACTIONS: dict[str, ActionSpec] = { "safe_read", ("emby", "jellyfin", "plex", "zspace", "ugreen", "navidrome"), ( - ArgumentSpec("title", "string", "歌曲、专辑或音乐条目标题。"), - ArgumentSpec("artist", "string", "艺人名称。"), - ArgumentSpec("album", "string", "专辑名称;title、artist、album 至少提供一项。"), + ArgumentSpec("title", "string", "Track, album, or music-item title."), + ArgumentSpec("artist", "string", "Artist name."), + ArgumentSpec("album", "string", "Album name; provide title, artist, or album."), ), - ("title、artist、album 至少提供一项。",), + ("Provide at least one of title, artist, and album.",), ), "items.season_episodes": ActionSpec( "Read native episode coverage for one series and optional season.", @@ -165,21 +180,21 @@ ACTIONS: dict[str, ActionSpec] = { ("emby", "jellyfin", "plex", "zspace", "ugreen", "trimemedia"), ( ITEM_ID, - ArgumentSpec("title", "string", "剧集标题;与 item_id 至少提供一项。"), - ArgumentSpec("year", "string|integer", "可选首播年份。"), - ArgumentSpec("season", "integer", "可选季号。"), + ArgumentSpec("title", "string", "Series title; provide it or item_id."), + ArgumentSpec("year", "string|integer", "Optional premiere year."), + ArgumentSpec("season", "integer", "Optional season number."), ), - ("item_id 与 title 至少提供一项。",), + ("Provide at least one of item_id and title.",), ), "activity.latest": ActionSpec( "Read recently added provider items.", "safe_read", - arguments=(LIMIT, ArgumentSpec("username", "string", "按用户名读取;仅 Emby、Jellyfin、ZSpace 支持。")), + arguments=(LIMIT, ArgumentSpec("username", "string", "Read for this username; supported by Emby, Jellyfin, and ZSpace.")), ), "activity.resume": ActionSpec( "Read in-progress/resumable provider items.", "safe_read", - arguments=(LIMIT, ArgumentSpec("username", "string", "按用户名读取;仅 Emby、Jellyfin、ZSpace 支持。")), + arguments=(LIMIT, ArgumentSpec("username", "string", "Read for this username; supported by Emby, Jellyfin, and ZSpace.")), ), "activity.backdrops": ActionSpec( "Read recent provider backdrop images.", @@ -187,7 +202,7 @@ ACTIONS: dict[str, ActionSpec] = { ("ugreen", "trimemedia"), ( LIMIT, - ArgumentSpec("remote", "boolean", "返回 provider 可远程访问的图片地址。", default=False), + ArgumentSpec("remote", "boolean", "Return provider URLs that are remotely accessible.", default=False), ), ), "playback.sessions": ActionSpec("Read active playback sessions.", "safe_read", ("emby", "jellyfin", "plex")), @@ -200,7 +215,7 @@ ACTIONS: dict[str, ActionSpec] = { "Trigger a provider library scan.", "external_side_effect", arguments=( - ArgumentSpec("scan_mode", "string|integer", "UGREEN 原生扫描模式;其他 provider 必须省略。"), + ArgumentSpec("scan_mode", "string|integer", "UGREEN-native scan mode; omit it for every other provider."), ), ), "metadata.refresh": ActionSpec( @@ -211,7 +226,7 @@ ACTIONS: dict[str, ActionSpec] = { ArgumentSpec( "items", "object[]", - "刷新条目;每项支持 title:string、year:string|integer、type:电影|电视剧|音乐、category:string、target_path:string。", + "Items to refresh. Each item supports title:string, year:string|integer, type using the exact MoviePilot media-type value, category:string, and target_path:string.", required=True, ), ), @@ -615,6 +630,25 @@ def call_action(server_name: Optional[str], action: str, arguments: Mapping[str, if spec is None: raise ValueError(f"未知 media server action: {action}") _validate_action_arguments(action, spec, arguments) + if action == "instances.list": + return { + "success": True, + "server": None, + "provider": None, + "action": action, + "effect": spec.effect, + "data": list_instances()["instances"], + } + if action == "capabilities.list": + capabilities = list_capabilities(server_name, arguments.get("action_name")) + return { + "success": True, + "server": capabilities["server"], + "provider": capabilities["provider"], + "action": action, + "effect": spec.effect, + "data": capabilities["actions"], + } config = _select_config(server_name) provider = str(config.type or "").lower() if provider not in spec.providers: diff --git a/skills/moviepilot-api/SKILL.md b/skills/moviepilot-api/SKILL.md index b1161cf26..1dded2808 100644 --- a/skills/moviepilot-api/SKILL.md +++ b/skills/moviepilot-api/SKILL.md @@ -1,6 +1,6 @@ --- name: moviepilot-api -version: 16 +version: 23 description: >- Use this skill for MoviePilot product operations such as media search, torrent search, downloads, subscriptions, library checks, sites, storage, workflows, @@ -11,18 +11,49 @@ description: >- allowed-tools: moviepilot_api 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.history.delete - transfer.history.delete site.list site.update site.userdata site.test site.cookie.update - recommendation.list library.exists + 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.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 + 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.category.config.get media.category.config.update media.categories + media.episode_groups media.episode_group.seasons media.seasons search.title search.recommend + subtitle.search.title subtitle.search.media site.add site.delete site.auth.options + site.authenticate site.cookiecloud.sync site.reset site.priorities.update site.userdata.refresh + site.userdata.latest site.category site.resource site.searchable site.rss site.statistics + 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 + 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 + workflow.get workflow.update workflow.delete workflow.actions workflow.event_types + workflow.plugin.actions workflow.start workflow.pause workflow.reset workflow.shares + workflow.share workflow.share.delete workflow.fork torrent.cache.get torrent.cache.delete + torrent.cache.clear torrent.cache.refresh torrent.cache.reidentify database.backups.list + database.backups.create database.backups.verify database.backups.delete filter.test + system.network.targets system.network.test system.module.list system.module.test + plugin.market.sync_wiki plugin.runtime.status plugin.history plugin.releases plugin.ratings + plugin.rating plugin.rating.submit plugin.statistics plugin.reset plugin.clone config.user.get + config.public.get system.usage.statistics plugin.folders.get plugin.folders.update + plugin.folder.create plugin.folder.delete plugin.folder.plugins.update --- # MoviePilot API @@ -44,6 +75,31 @@ Use `downloader-operation` for downloader instances, task inspection and native task control. Use `mediaserver-operation` for libraries, items, playback sessions, scans, refreshes and other native media-server capabilities. +## API Surface Scope + +This Skill is the complete callable MoviePilot business API surface for the +Agent. Every operation in `allowed-api-operations` has one exact parameter +contract below and one matching MCP `tools/list` branch. There is no hidden +fallback to an arbitrary REST route. + +MoviePilot's underlying OpenAPI document is larger because it also serves the +web UI, authentication, account lifecycle, binary and streaming responses, +callbacks, compatibility endpoints, and source-specific presentation routes. +Those routes are deliberately not copied into this Skill. A non-listed route +must be one of the following before the Agent may use its capability: + +- represented by one stable aggregate operation in this Skill; +- owned by `downloader-operation`, `mediaserver-operation`, or another domain + Skill with its own exact action contract; +- reserved for host transport, identity, UI, streaming, binary, or diagnostic + behavior and therefore unavailable as an Agent business action; or +- explicitly unapproved until a role, effect, confirmation, recovery, result, + 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 +when OpenAPI changes without an explicit ownership decision. + ## Calling Contract Call the gateway with this shape: @@ -52,7 +108,7 @@ Call the gateway with this shape: { "operation_id": "media.search", "path_params": {}, - "query": {"title": "流浪地球", "type": "media"}, + "query": {"title": "The Wandering Earth", "type": "media"}, "body": {} } ``` @@ -69,106 +125,1571 @@ Call the gateway with this shape: - Treat `success=false`, HTTP error data, empty results, and validation errors as real outcomes. Do not claim success without checking the response. +## Music Navigation + +- Search titles, albums, or artists with `media.search` using `type=music`. Preserve + every returned `media_source`, `media_id`, `music_type`, `album_id`, and + `artist_ids` value instead of matching by display name. +- Use `music.artist.albums` to browse an artist's works. Its `album_type` filter + distinguishes albums, singles, EPs, compilations, soundtracks, live releases, + remixes, and the other documented MusicBrainz release-group types. +- Use `music.album.get` to browse from a work back to its artists. The response + includes aligned `artists` and `artist_ids`, plus tracks and releases; pass one + returned artist ID to `music.artist.get`, `music.artist.albums`, or + `music.artist.related` with the same `media_source`. +- Use `music.album.related` for related works and `music.artist.related` for + related artists. Use `music.explore` for MusicBrainz charts/fresh releases or + Douban Music tag browsing. +- `music.recognize` resolves only a recording or album. Artist identities are + browse-only. Music recognition-cache operations are administrator-only; call + `music.cache.get` before deleting one exact key, and clear all entries only + after explicit confirmation. + ## Operation Catalog -### Media and search +The operations, HTTP methods, routes, and path/query/body fields below exactly match external MCP `tools/list`. +A field name ending in `*` is required. Omit an empty bucket or send `{}`. Referenced body models are expanded below. -| Operation | Parameters | Purpose | -|---|---|---| -| `media.search` | query: `title`, `type`, `page`, `count`, `media_source` | Search video, music, collection, or media entities | -| `media.person.search` | query: `title`, `type=person`, paging/source | Search people | -| `media.person.credits` | path: `source`, `person_id`; query: paging | Read TMDB or Douban credits | -| `media.recognize` | query: `title`, optional `subtitle`, `custom_words`, `media_source` | Recognize a title or file path | -| `media.detail` | path: `media_id`; query: `media_source`, `type_name`, music fields when applicable | Read exact media detail | -| `media.episode_schedule` | path: `tmdbid`, `season`; query: optional episode group | Read TMDB season episodes | -| `recommendation.list` | query: source/category/paging fields | Read the unified recommendation feed | -| `search.torrents` | path: `media_id`; query: `media_source`, `mtype`, `season`, `sites`, `music_type` | Search site resources | -| `search.results` | query: result filters | Read and filter the latest search context | -| `media.scrape` | path: `storage`; query: identity/type; body: file item | Scrape metadata, artwork, and configured music lyrics | +### `config.identifiers.get` +`GET /api/v1/system/identifiers`; policy effect: `safe_read`. +Purpose: Read the complete custom media-recognition identifier list. +- `path_params`: none +- `query`: none +- `body`: none -After `search.torrents`, present the returned filter choices before narrowing -results. Reuse `search.results` instead of repeating the same search. Obtain -explicit consent before `download.add` or another external side effect. +### `config.identifiers.update` +`POST /api/v1/system/identifiers`; policy effect: `reversible_write`. +Purpose: Replace the complete custom media-recognition identifier list. +- `path_params`: none +- `query`: none +- `body`: `identifiers` (array): Complete ordered list of custom recognition identifier rules. -### Subscriptions and downloads +### `config.public.get` +`GET /api/v1/system/setting/public/{key}`; policy effect: `safe_read`. +Purpose: Read one explicitly public system setting by exact key. +- `path_params`: `key*` (string): Optional exact plugin data key used to narrow the returned preview. +- `query`: none +- `body`: none -| Operation | Parameters | Purpose | -|---|---|---| -| `subscription.list` | query filters | List subscriptions | -| `subscription.add` | body: subscribe model | Create a subscription | -| `subscription.update` | body: updated subscribe model | Update a subscription | -| `subscription.search` | path: `subscribe_id` | Trigger/search one subscription | -| `subscription.delete` | path: `subscribe_id` | Permanently remove a subscription | -| `subscription.history` | path: `mtype`; query paging | Read subscription history | -| `subscription.shares` | query paging | Read shared subscriptions | -| `subscription.popular` | query paging/type | Read popular subscriptions | -| `download.add` | body: torrent input and optional media identity/client/path | Add a download | -| `download.history.delete` | query/body accepted by endpoint | Delete download history | +### `config.system.get` +`GET /api/v1/system/settings`; policy effect: `safe_read`. +Purpose: Discover registered system settings or read one exact setting. +- `path_params`: none +- `query`: `group` (string|null; default `all`): Discovery group used when setting_key is omitted. Supported groups are all, settings, systemconfig, downloaders, media_servers, notifications, notification_switches, storages, directories, search_sites, subscribe_sites, site_auth, ai_agent, filter_rules, subscribe_defaults, plugins, customization, transfer, scraping, and misc.; `include_values` (boolean|null): Return full values. Defaults to true for one exact key and false for discovery results.; `keyword` (string|null): Case-insensitive substring used to discover matching keys, groups, or labels.; `setting_key` (string|null): Exact setting key. Accepts Settings field names such as APP_DOMAIN or LLM_MODEL, SystemConfigKey values or enum names such as Downloaders or MediaServers, and aliases that resolve to one unique setting. Omit it to discover settings.; `show_secrets` (boolean; default `False`): Return unredacted secret values. Defaults to false and remains confirmation-protected. +- `body`: none -Before adding a download or subscription, check `library.exists` and -`subscription.list` when duplicate risk exists. Deletions and file removal need -explicit confirmation. +### `config.system.update` +`POST /api/v1/system/settings`; policy effect: `reversible_write`. +Purpose: Update one exact registered system setting. +- `path_params`: none +- `query`: none +- `body`: `match_field` (string|null): Object field used to match a list item. Downloaders, MediaServers, Notifications, Directories, and Storages default to name; NotificationSwitchs defaults to type. Supply it for other object lists.; `match_value` (value): Value compared against match_field. If omitted, use value[match_field]; scalar lists use value directly.; `operation` (string(replace,merge_dict,upsert_list_item,remove_list_item); default `replace`): replace overwrites the complete value; merge_dict shallow-merges an object; upsert_list_item inserts or replaces one matched list item; remove_list_item removes one matched list item.; `remove_keys` (array): Object keys to remove after merge_dict applies the supplied value.; `setting_key*` (string): Exact setting key. Accepts a Settings field name, a SystemConfigKey value or enum name, or an alias that resolves to one unique setting. Call config.system.get with group or keyword first when the key is unknown.; `value` (value): New value or list item. For replace, send the complete value. For merge_dict, send the object fragment to merge. For upsert_list_item or remove_list_item, send one object or scalar item. -### Library, storage, and transfer +### `config.user.get` +`GET /api/v1/system/global/user`; policy effect: `safe_read`. +Purpose: Read current-user feature flags, runtime capabilities, and effective permissions. +- `path_params`: none +- `query`: none +- `body`: none -| Operation | Parameters | Purpose | -|---|---|---| -| `library.exists` | query: exact media identity and type | Check library presence | -| `storage.settings` | none | Read configured download/library storage roots | -| `storage.list` | body: storage/path/paging/sort fields | List a local or remote storage directory | -| `transfer.history` | query filters and paging | Read transfer history | -| `transfer.file` | body: manual-transfer model | Organize a file or directory | -| `transfer.history.delete` | query/body accepted by endpoint | Submit durable retry or delete legacy history | +### `dashboard.cpu` +`GET /api/v1/dashboard/cpu`; policy effect: `safe_read`. +Purpose: Read the current host CPU utilization percentage. +- `path_params`: none +- `query`: none +- `body`: none -For transfer retries, preserve durable scheduler evidence. If history deletion -returns a durable retry decision, stop and report it; only an actually deleted -legacy record may be followed by `transfer.file`. +### `dashboard.downloader` +`GET /api/v1/dashboard/downloader`; policy effect: `safe_read`. +Purpose: Read aggregate downloader task counts, speeds, and free-space information. +- `path_params`: none +- `query`: `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group. +- `body`: none -Downloader task state and media-server library browsing deliberately do not pass -through this gateway. Discover the configured instance and its live capability -set with the matching provider-operation skill before calling the fixed helper. +### `dashboard.media.statistics` +`GET /api/v1/dashboard/statistic`; policy effect: `safe_read`. +Purpose: Read aggregate movie, TV, episode, and music library counts. +- `path_params`: none +- `query`: `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group. +- `body`: none -### Sites, workflows, and schedulers +### `dashboard.memory` +`GET /api/v1/dashboard/memory`; policy effect: `safe_read`. +Purpose: Read current MoviePilot process and host memory utilization. +- `path_params`: none +- `query`: none +- `body`: none -| Operation | Parameters | Purpose | -|---|---|---| -| `site.list` | query filters | List configured sites | -| `site.userdata` | path: `site_id` | Read site account data | -| `site.test` | path: `site_id` | Test site connectivity/login | -| `site.update` | body: site model | Update site configuration | -| `site.cookie.update` | path: `site_id`; body: credentials/2FA fields | Refresh site authentication | -| `scheduler.list` | none | List system/plugin/workflow schedules | -| `scheduler.run` | query: `job_id` | Run one scheduler job | -| `workflow.list` | query filters | List workflows | -| `workflow.run` | path: `workflow_id`; body/query endpoint fields | Run one workflow | +### `dashboard.network` +`GET /api/v1/dashboard/network`; policy effect: `safe_read`. +Purpose: Read the current host network receive and transmit counters. +- `path_params`: none +- `query`: none +- `body`: none -Scheduler `job_id` values are strings and are unrelated to autonomous Agent -task IDs. Test and credential updates are external side effects and require the -appropriate authorization/confirmation. +### `dashboard.processes` +`GET /api/v1/dashboard/processes`; policy effect: `safe_read`. +Purpose: List host processes visible to the MoviePilot runtime. +- `path_params`: none +- `query`: none +- `body`: none -### Plugins, rules, and configuration +### `dashboard.storage` +`GET /api/v1/dashboard/storage`; policy effect: `safe_read`. +Purpose: Read local filesystem capacity and free-space information. +- `path_params`: none +- `query`: none +- `body`: none -| Operation | Parameters | Purpose | -|---|---|---| -| `plugin.installed` / `plugin.market` | query filters | List installed or market plugins | -| `plugin.capabilities` | query: optional plugin ID | Read commands, actions, services, and Agent capabilities | -| `plugin.config.get` / `plugin.config.update` | path: `plugin_id`; update body | Read or update plugin configuration | -| `plugin.data` | path: `plugin_id`; query: key/limit/offset | Read bounded plugin data previews | -| `plugin.install` / `plugin.reload` / `plugin.uninstall` | path: `plugin_id` | Manage plugin lifecycle | -| `filter.builtin` / `filter.custom` / `filter.groups` | none or query filters | Read filter definitions | -| `filter.custom.add` / `filter.custom.update` / `filter.custom.delete` | update/delete path: `rule_id`; body for writes | Manage custom filter rules | -| `filter.group.add` / `filter.group.update` / `filter.group.delete` | update/delete path: `name`; body for writes | Manage filter groups | -| `config.identifiers.get` / `config.identifiers.update` | update body | Read or replace custom identifiers | -| `config.system.get` / `config.system.update` | query/body for read; update body | Read or update system settings | -| `slash.list` / `slash.run` | run body: `command` | Discover or dispatch system/plugin slash commands | +### `dashboard.system` +`GET /api/v1/dashboard/system`; policy effect: `safe_read`. +Purpose: Read MoviePilot host, runtime, platform, and uptime summary information. +- `path_params`: none +- `query`: none +- `body`: none -For a raw credential explicitly requested by an administrator, call -`config.system.get` with `body.show_secrets=true` and the narrowest -`body.setting_key` or `body.group`. The host pauses the turn for confirmation and -delivers the value through a protected channel. Never repeat the secret in a -normal response. +### `dashboard.transfer.statistics` +`GET /api/v1/dashboard/transfer`; policy effect: `safe_read`. +Purpose: Read aggregate file-transfer counts grouped by time period. +- `path_params`: none +- `query`: `days` (integer|null; default `7`): Recommendation time window in days. +- `body`: none -Plugin install, reload, uninstall, configuration writes, rule writes, system -setting writes, identifier writes, scheduler/workflow runs, and slash commands -are state changes. Inspect current state first and confirm unless the user's -request already explicitly authorizes the exact action. +### `database.backups.create` +`POST /api/v1/system/database/backups`; policy effect: `external_side_effect`. +Purpose: Create, verify, and atomically publish a managed database backup. +- `path_params`: none +- `query`: none +- `body`: none + +### `database.backups.delete` +`DELETE /api/v1/system/database/backups/{name}`; policy effect: `destructive_write`. +Purpose: Delete one exact managed database backup artifact. +- `path_params`: `name*` (string): Human-readable name of the site, storage item, subscription, or rule group. +- `query`: none +- `body`: none + +### `database.backups.list` +`GET /api/v1/system/database/backups`; policy effect: `safe_read`. +Purpose: List managed database backup artifacts without exposing host paths. +- `path_params`: none +- `query`: none +- `body`: none + +### `database.backups.verify` +`POST /api/v1/system/database/backups/{name}/verify`; policy effect: `safe_read`. +Purpose: Verify the integrity of one exact managed database backup artifact. +- `path_params`: `name*` (string): Human-readable name of the site, storage item, subscription, or rule group. +- `query`: none +- `body`: none + +### `download.add` +`POST /api/v1/download/add`; policy effect: `external_side_effect`. +Purpose: Submit one torrent to MoviePilot's normal download workflow. +- `path_params`: none +- `query`: none +- `body`: `allow_unrecognized` (boolean; default `False`): Allow a download when MoviePilot cannot resolve a canonical media identity.; `downloader` (string|null): Configured downloader instance name.; `media_id` (string|null): Source-native media ID. Always pair it with the exact media_source returned by search.; `media_source` (MediaSource|null): Metadata source identifier. Preserve the exact value returned with media_id.; `music_type` (string(recording,album)|null): Music identity level: recording, album, or artist where supported.; `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. +- `path_params`: none +- `query`: none +- `body`: none + +### `download.history.delete` +`DELETE /api/v1/history/download`; policy effect: `destructive_write`. +Purpose: Delete one MoviePilot download-history record. +- `path_params`: none +- `query`: none +- `body`: `channel` (string|null): Message channel that originally submitted the download.; `date` (string|null): Record creation or completion timestamp used by the history item.; `download_hash` (string|null): Provider-native torrent hash associated with the record.; `episode_group` (string|null): TMDB episode-group identifier used for alternate episode ordering.; `episodes` (string|null): Episode-number expression recorded in history, such as E01-E03.; `id*` (integer): Persistent database identifier of the supplied record.; `image` (string|null): Image URL stored with the history record.; `media_category` (string|null): MoviePilot library category assigned to the media.; `media_id` (string|null): Source-native media ID. Always pair it with the exact media_source returned by search.; `media_source` (MediaSource|null): Metadata source identifier. Preserve the exact value returned with media_id.; `music_type` (string|null): Music identity level: recording, album, or artist where supported.; `note` (JsonData-Input|null): Structured auxiliary metadata stored with the record.; `path` (string|null): Storage or history path represented by this record.; `poster` (string|null): Poster image URL stored with the media or subscription.; `seasons` (string|null): Season-number expression recorded in history.; `title` (string|null): Media, torrent, subscription, or history title used by the operation.; `torrent_description` (string|null): Torrent release description recorded in download history.; `torrent_name` (string|null): Torrent release name recorded in download history.; `torrent_site` (string|null): Source site name recorded in download history.; `type` (string|null): MoviePilot media or storage item type required by the selected operation.; `userid` (string|null): Message-channel user ID recorded with download history.; `username` (string|null): MoviePilot or site username required by the selected operation.; `year` (string|null): Release or premiere year used to disambiguate the media title. + +### `download.history.list` +`GET /api/v1/history/download`; policy effect: `safe_read`. +Purpose: Page MoviePilot download-history records in reverse chronological order. +- `path_params`: none +- `query`: `count` (integer|null; default `30`): Maximum number of records to return on the requested page.; `page` (integer|null; default `1`): One-based result page number. +- `body`: none + +### `download.paths` +`GET /api/v1/download/paths`; policy effect: `safe_read`. +Purpose: List configured downloader save-path URIs that may be passed to download.add. +- `path_params`: none +- `query`: none +- `body`: none + +### `download.tasks.active` +`GET /api/v1/download/`; policy effect: `safe_read`. +Purpose: List currently downloading MoviePilot tasks with their canonical media context. +- `path_params`: none +- `query`: `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group. +- `body`: none + +### `filter.builtin` +`GET /api/v1/rule/builtin`; policy effect: `safe_read`. +Purpose: List built-in torrent filter rules. +- `path_params`: none +- `query`: `rule_ids` (array|null): Exact built-in rule IDs to return. Repeat rule_ids in the query string; omit it to list every built-in rule. +- `body`: none + +### `filter.custom` +`GET /api/v1/rule/custom`; policy effect: `safe_read`. +Purpose: List user-defined torrent filter rules. +- `path_params`: none +- `query`: `include_group_refs` (boolean; default `True`): Include custom rules referenced only through rule groups.; `rule_ids` (array|null): Exact custom rule IDs to return. Repeat rule_ids in the query string; omit it to list every custom rule. +- `body`: none + +### `filter.custom.add` +`POST /api/v1/rule/custom`; policy effect: `reversible_write`. +Purpose: Create one user-defined torrent filter rule. +- `path_params`: none +- `query`: none +- `body`: `exclude` (string|null): Regular expression or filter expression that rejects matching releases.; `include` (string|null): Regular expression or filter expression that a release must match.; `name*` (string): Human-readable name of the site, storage item, subscription, or rule group.; `publish_time` (string|null): Release-age filter expression for a custom filter rule.; `rule_id*` (string): Stable custom filter-rule ID.; `seeders` (string|null): Minimum seeder expression for a filter rule, or the torrent's seeder count.; `size_range` (string|null): Accepted torrent size range expression for a custom filter rule. + +### `filter.custom.delete` +`DELETE /api/v1/rule/custom/{rule_id}`; policy effect: `destructive_write`. +Purpose: Delete one user-defined torrent filter rule. +- `path_params`: `rule_id*` (string): Stable custom filter-rule ID. +- `query`: none +- `body`: none + +### `filter.custom.update` +`PUT /api/v1/rule/custom/{rule_id}`; policy effect: `reversible_write`. +Purpose: Update one user-defined torrent filter rule. +- `path_params`: `rule_id*` (string): Stable custom filter-rule ID. +- `query`: none +- `body`: `exclude` (string|null): Regular expression or filter expression that rejects matching releases.; `include` (string|null): Regular expression or filter expression that a release must match.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `new_rule_id` (string|null): Replacement stable ID for the existing custom filter rule.; `publish_time` (string|null): Release-age filter expression for a custom filter rule.; `seeders` (string|null): Minimum seeder expression for a filter rule, or the torrent's seeder count.; `size_range` (string|null): Accepted torrent size range expression for a custom filter rule. + +### `filter.group.add` +`POST /api/v1/rule/groups`; policy effect: `reversible_write`. +Purpose: Create one named filter-rule group. +- `path_params`: none +- `query`: none +- `body`: `category` (string|null): MoviePilot media category or filter-group category, depending on the operation.; `media_type` (string|null): MoviePilot media type used to filter recommendations or rule groups.; `name*` (string): Human-readable name of the site, storage item, subscription, or rule group.; `rule_string*` (string): Ordered filter-rule expression stored in the group. + +### `filter.group.delete` +`DELETE /api/v1/rule/groups/{name}`; policy effect: `destructive_write`. +Purpose: Delete one named filter-rule group. +- `path_params`: `name*` (string): Human-readable name of the site, storage item, subscription, or rule group. +- `query`: none +- `body`: none + +### `filter.group.update` +`PUT /api/v1/rule/groups/{name}`; policy effect: `reversible_write`. +Purpose: Update or rename one named filter-rule group. +- `path_params`: `name*` (string): Human-readable name of the site, storage item, subscription, or rule group. +- `query`: none +- `body`: `category` (string|null): MoviePilot media category or filter-group category, depending on the operation.; `media_type` (string|null): MoviePilot media type used to filter recommendations or rule groups.; `new_name` (string|null): Replacement name for the existing filter-rule group.; `rule_string` (string|null): Ordered filter-rule expression stored in the group. + +### `filter.groups` +`GET /api/v1/rule/groups`; policy effect: `safe_read`. +Purpose: List named filter-rule groups. +- `path_params`: none +- `query`: `group_names` (array|null): Exact rule-group names to return. Repeat group_names in the query string; omit it to list every group.; `include_usage` (boolean; default `True`): Include the subscriptions or defaults that reference each rule group. +- `body`: none + +### `filter.test` +`GET /api/v1/system/ruletest`; policy effect: `external_side_effect`. +Purpose: Test one title and optional subtitle against an exact named filter-rule group. +- `path_params`: none +- `query`: `rulegroup_name*` (string): Exact filter-rule group name returned by filter.groups.; `subtitle` (string|null): Optional subtitle text used together with title during media recognition.; `title*` (string): Media, torrent, subscription, or history title used by the operation. +- `body`: none + +### `library.exists` +`GET /api/v1/mediaserver/exists`; policy effect: `safe_read`. +Purpose: Check configured media servers for one canonical media identity. +- `path_params`: none +- `query`: `media_id` (string|null): Source-native media ID. Always pair it with the exact media_source returned by search.; `media_source` (MediaSource|null): Metadata source identifier. Preserve the exact value returned with media_id.; `mtype` (string|null): MoviePilot media type or subscription-history category required by the operation.; `season` (integer|null): Season number used by the media, search, subscription, or transfer operation.; `title` (string|null): Media, torrent, subscription, or history title used by the operation.; `year` (string|null): Release or premiere year used to disambiguate the media title. +- `body`: none + +### `library.latest` +`GET /api/v1/mediaserver/latest`; policy effect: `safe_read`. +Purpose: List recently added items from one configured media-server instance for the current user. +- `path_params`: none +- `query`: `count` (integer|null; default `20`): Maximum number of records to return on the requested page.; `server*` (string): Exact configured media-server instance name returned by the media-server instance list. +- `body`: none + +### `media.categories` +`GET /api/v1/media/category`; policy effect: `safe_read`. +Purpose: Read the resolved automatic media-category mapping. +- `path_params`: none +- `query`: none +- `body`: none + +### `media.category.config.get` +`GET /api/v1/media/category/config`; policy effect: `safe_read`. +Purpose: Read the complete automatic media-category strategy configuration. +- `path_params`: none +- `query`: none +- `body`: none + +### `media.category.config.update` +`POST /api/v1/media/category/config`; policy effect: `reversible_write`. +Purpose: Replace the complete automatic media-category strategy configuration. +- `path_params`: none +- `query`: none +- `body`: `movie` (object|null; default `{}`): Automatic movie-category rules evaluated in order.; `tv` (object|null; default `{}`): Automatic TV-category rules evaluated in order. + +### `media.detail` +`GET /api/v1/media/{media_id}`; policy effect: `safe_read`. +Purpose: Read canonical media details from one selected metadata source. +- `path_params`: `media_id*` (string): Source-native media ID. Always pair it with the exact media_source returned by search. +- `query`: `media_source*` (MediaSource): Metadata source identifier. Preserve the exact value returned with media_id.; `type_name*` (string): Explicit media type name used when source IDs alone are ambiguous. +- `body`: none + +### `media.episode_group.seasons` +`GET /api/v1/media/group/seasons/{episode_group}`; policy effect: `safe_read`. +Purpose: List seasons defined by one exact TMDB episode-group identity. +- `path_params`: `episode_group*` (string): TMDB episode-group identifier used for alternate episode ordering. +- `query`: none +- `body`: none + +### `media.episode_groups` +`GET /api/v1/media/groups/{tmdbid}`; policy effect: `safe_read`. +Purpose: List alternate TMDB episode groups available for one TV media identity. +- `path_params`: `tmdbid*` (integer): TMDB media ID returned by media search or detail. +- `query`: none +- `body`: none + +### `media.episode_schedule` +`GET /api/v1/tmdb/{tmdbid}/{season}`; policy effect: `safe_read`. +Purpose: Read TMDB episode release information for one season. +- `path_params`: `season*` (integer): Season number used by the media, search, subscription, or transfer operation.; `tmdbid*` (integer): TMDB media ID returned by media search or detail. +- `query`: `episode_group` (string|null): TMDB episode-group identifier used for alternate episode ordering. +- `body`: none + +### `media.person.credits` +`GET /api/v1/{source}/person/credits/{person_id}`; policy effect: `safe_read`. +Purpose: Read one person's credits from the selected metadata source. +- `path_params`: `person_id*` (integer): Source-native person ID.; `source*` (string(douban,tmdb,bangumi,anilist)): Metadata source that owns the person ID. +- `query`: `count` (integer; default `20`; minimum `1`; maximum `50`): Page size used by Bangumi and AniList; other sources ignore it.; `page` (integer; default `1`; minimum `1`): One-based result page number. +- `body`: none + +### `media.person.search` +`GET /api/v1/media/search`; policy effect: `safe_read`. +Purpose: Search people across selected metadata sources. +- `path_params`: none +- `query`: `count` (integer; default `8`): Maximum number of records to return on the requested page.; `media_source` (array; default `[]`): Metadata source identifier. Preserve the exact value returned with media_id.; `page` (integer; default `1`): One-based result page number.; `title*` (string): Media, torrent, subscription, or history title used by the operation.; `type*` (string=person): Literal person, selecting person search instead of media search. +- `body`: none + +### `media.recognize` +`GET /api/v1/media/recognize`; policy effect: `safe_read`. +Purpose: Recognize media identity from a title, subtitle, or custom rule context. +- `path_params`: none +- `query`: `custom_words` (string|null): Custom recognition or rename words applied to this media workflow.; `media_source` (MediaSource|null): Metadata source identifier. Preserve the exact value returned with media_id.; `subtitle` (string|null): Optional subtitle text used together with title during media recognition.; `title*` (string): Media, torrent, subscription, or history title used by the operation. +- `body`: none + +### `media.recognize_file` +`GET /api/v1/media/recognize_file`; policy effect: `safe_read`. +Purpose: Recognize canonical media identity from one exact filename and optional path context. +- `path_params`: none +- `query`: `media_source` (MediaSource|null): Metadata source identifier. Preserve the exact value returned with media_id.; `path*` (string): Storage or history path represented by this record. +- `body`: none + +### `media.scrape` +`POST /api/v1/media/scrape/{storage}`; policy effect: `external_side_effect`. +Purpose: Generate or refresh metadata for one storage item. +- `path_params`: `storage*` (string|null): Configured storage name or storage type used by the operation. +- `query`: `media_id` (string|null): Source-native media ID. Always pair it with the exact media_source returned by search.; `media_source` (MediaSource|null): Metadata source identifier. Preserve the exact value returned with media_id.; `music_type` (string|null): Music identity level: recording, album, or artist where supported.; `type_name` (MediaType|null): Explicit media type name used when source IDs alone are ambiguous. +- `body`: `basename` (string|null): Base filename without its parent path.; `children` (array|null): Child storage items nested below this item.; `drive_id` (string|null): Provider-native storage drive identifier.; `extension` (string|null): Filename extension, including or excluding the leading dot as returned by storage.; `fileid` (string|null): Provider-native storage item identifier.; `modify_time` (number|null): Storage item modification timestamp.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `parent_fileid` (string|null): Provider-native identifier of the parent storage directory.; `path` (string|null; default `/`): Storage or history path represented by this record.; `pickcode` (string|null): 115 storage pickcode associated with the item.; `size` (integer|null): File or torrent size in bytes.; `storage` (string|null; default `local`): Configured storage name or storage type used by the operation.; `thumbnail` (string|null): Thumbnail URL returned by the storage provider.; `type` (string|null): MoviePilot media or storage item type required by the selected operation.; `url` (string|null): Site, storage, or torrent URL represented by this field. + +### `media.search` +`GET /api/v1/media/search`; policy effect: `safe_read`. +Purpose: Search canonical media across selected metadata sources. +- `path_params`: none +- `query`: `count` (integer; default `8`): Maximum number of records to return on the requested page.; `media_source` (array; default `[]`): Metadata source identifier. Preserve the exact value returned with media_id.; `page` (integer; default `1`): One-based result page number.; `title*` (string): Media, torrent, subscription, or history title used by the operation.; `type` (string|null; default `media`): MoviePilot media or storage item type required by the selected operation. +- `body`: none + +### `media.seasons` +`GET /api/v1/media/seasons`; policy effect: `safe_read`. +Purpose: List seasons for one exact media identity or a title-and-year fallback. +- `path_params`: none +- `query`: `media_id` (string|null): Source-native media ID. Always pair it with the exact media_source returned by search.; `media_source` (MediaSource|null): Metadata source identifier. Preserve the exact value returned with media_id.; `season` (integer): Season number used by the media, search, subscription, or transfer operation.; `title` (string|null): Media, torrent, subscription, or history title used by the operation.; `year` (string): Release or premiere year used to disambiguate the media title. +- `body`: none + +### `media.sources` +`GET /api/v1/media/source`; policy effect: `safe_read`. +Purpose: List metadata sources currently registered for MoviePilot media operations. +- `path_params`: none +- `query`: none +- `body`: none + +### `music.album.get` +`GET /api/v1/music/album/{album_id}`; policy effect: `safe_read`. +Purpose: Read one album's details, tracks, releases, and aligned artist names and IDs. +- `path_params`: `album_id*` (string): Source-native album ID returned by music search, exploration, or artist-album browsing. +- `query`: `media_source` (MediaSource): Metadata source identifier. Preserve the exact value returned with media_id. +- `body`: none + +### `music.album.related` +`GET /api/v1/music/album/{album_id}/related`; policy effect: `safe_read`. +Purpose: Browse albums related to one source-native album identity. +- `path_params`: `album_id*` (string): Source-native album ID returned by music search, exploration, or artist-album browsing. +- `query`: `count` (integer; default `24`; minimum `1`; maximum `100`): Maximum number of records to return on the requested page.; `media_source` (MediaSource): Metadata source identifier. Preserve the exact value returned with media_id. +- `body`: none + +### `music.artist.albums` +`GET /api/v1/music/artist/{artist_id}/albums`; policy effect: `safe_read`. +Purpose: Browse one artist's albums, singles, EPs, or another exact release-group type. +- `path_params`: `artist_id*` (string): Source-native artist ID returned by music search or an album detail response. +- `query`: `album_type` (string|null): MusicBrainz release-group type filter: album, single, ep, broadcast, other, compilation, soundtrack, live, or remix.; `count` (integer; default `30`; minimum `1`; maximum `100`): Maximum number of records to return on the requested page.; `media_source` (MediaSource): Metadata source identifier. Preserve the exact value returned with media_id.; `page` (integer; default `1`; minimum `1`): One-based result page number. +- `body`: none + +### `music.artist.get` +`GET /api/v1/music/artist/{artist_id}`; policy effect: `safe_read`. +Purpose: Read one artist's canonical details from the selected music metadata source. +- `path_params`: `artist_id*` (string): Source-native artist ID returned by music search or an album detail response. +- `query`: `media_source` (MediaSource): Metadata source identifier. Preserve the exact value returned with media_id. +- `body`: none + +### `music.artist.related` +`GET /api/v1/music/artist/{artist_id}/related`; policy effect: `safe_read`. +Purpose: Browse artists related to one source-native artist identity. +- `path_params`: `artist_id*` (string): Source-native artist ID returned by music search or an album detail response. +- `query`: `count` (integer; default `24`; minimum `1`; maximum `100`): Maximum number of records to return on the requested page.; `media_source` (MediaSource): Metadata source identifier. Preserve the exact value returned with media_id. +- `body`: none + +### `music.cache.clear` +`DELETE /api/v1/music/cache`; policy effect: `destructive_write`. +Purpose: Clear the complete administrator-only MusicBrainz recognition cache. +- `path_params`: none +- `query`: none +- `body`: none + +### `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. +- `query`: none +- `body`: none + +### `music.cache.get` +`GET /api/v1/music/cache`; policy effect: `safe_read`. +Purpose: Inspect the administrator-only MusicBrainz recognition cache and summary counts. +- `path_params`: none +- `query`: none +- `body`: none + +### `music.explore` +`GET /api/v1/music/explore`; policy effect: `safe_read`. +Purpose: Browse MusicBrainz charts or fresh releases, or Douban Music tag categories. +- `path_params`: none +- `query`: `count` (integer; default `30`; minimum `1`; maximum `100`): Maximum number of records to return on the requested page.; `days` (integer; default `14`; minimum `1`; maximum `90`): Fresh-release lookback/lookahead window, from 1 through the endpoint maximum.; `douban_sort` (string; default `U`): Douban Music order: U comprehensive, S rating, R newest, or O hottest.; `entity` (string; default `recording`): Chart entity: recording for tracks or album for release groups. Fresh results are albums.; `future` (boolean; default `True`): Include releases after today in fresh mode.; `media_source` (MediaSource): Music exploration source. Use musicbrainz for chart/fresh modes or doubanmusic for tag browsing.; `min_listen_count` (integer; default `0`; minimum `0`): Minimum ListenBrainz listen count in chart mode.; `mode` (string; default `chart`): MusicBrainz mode: chart reads listening charts; fresh reads new album releases.; `page` (integer; default `1`; minimum `1`): One-based result page number.; `past` (boolean; default `True`): Include releases before today in fresh mode.; `range_name` (string; default `this_month`): ListenBrainz chart range: this_week, this_month, this_year, week, month, or year.; `sort` (string; default `release_date`): Fresh-release order accepted by the current ListenBrainz implementation.; `sort_by` (string; default `listen_count.desc`): ListenBrainz chart order: listen_count.desc or listen_count.asc.; `tags` (string; default ``): Comma-separated Douban Music tags used only when media_source is doubanmusic.; `with_cover` (boolean; default `False`): Keep only results with cover artwork when true. +- `body`: none + +### `music.recognize` +`POST /api/v1/music/recognize`; policy effect: `safe_read`. +Purpose: Resolve one recording or album from an exact music source and source-native ID. +- `path_params`: none +- `query`: none +- `body`: `media_id*` (string): Source-native media ID. Always pair it with the exact media_source returned by search.; `media_source*` (MediaSource): Metadata source identifier. Preserve the exact value returned with media_id.; `music_type` (string(recording,album)|null): Music identity level: recording, album, or artist where supported. + +### `plugin.capabilities` +`GET /api/v1/plugin/runtime/capabilities`; policy effect: `safe_read`. +Purpose: Inspect the runtime capabilities exposed by installed plugins. +- `path_params`: none +- `query`: `plugin_id` (string|null): Exact installed or marketplace plugin ID. +- `body`: none + +### `plugin.clone` +`POST /api/v1/plugin/clone/{plugin_id}`; policy effect: `external_side_effect`. +Purpose: Create a configurable clone of one installed plugin. +- `path_params`: `plugin_id*` (string): Exact installed or marketplace plugin ID. +- `query`: none +- `body`: `description` (string; default ``): Human-readable media, torrent, or subscription description.; `icon` (string|null): Icon name or URL used by a workflow, network target, plugin, or category.; `name` (string; default ``): Human-readable name of the site, storage item, subscription, or rule group.; `suffix*` (string; minimum length `1`): File suffix or extension matched by an automatic category rule.; `version` (string|null): Plugin release or schema version selected by the operation. + +### `plugin.config.get` +`GET /api/v1/plugin/form/{plugin_id}`; policy effect: `safe_read`. +Purpose: Read one loaded plugin's configuration form and its defaults merged with saved values. +- `path_params`: `plugin_id*` (string): Exact installed or marketplace plugin ID. +- `query`: none +- `body`: none + +### `plugin.config.update` +`PUT /api/v1/plugin/{plugin_id}`; policy effect: `reversible_write`. +Purpose: Replace one installed plugin's complete configuration and apply it immediately. +- `path_params`: `plugin_id*` (string): Exact installed or marketplace plugin ID. +- `query`: none +- `body*` (object): Complete plugin configuration object. First call plugin.config.get, copy its returned model, change only the intended keys, and submit the full resulting object. Omit a key only when it must be removed. A successful update reinitializes the plugin and refreshes commands, jobs, and routes. + +### `plugin.data` +`GET /api/v1/plugin/runtime/{plugin_id}/data`; policy effect: `safe_read`. +Purpose: Read a bounded preview of one plugin's persisted data. +- `path_params`: `plugin_id*` (string): Exact installed or marketplace plugin ID. +- `query`: `key` (string|null): Optional exact plugin data key used to narrow the returned preview.; `max_chars` (integer|null): Maximum number of serialized plugin-data characters to return. +- `body`: none + +### `plugin.folder.create` +`POST /api/v1/plugin/folders/{folder_name}`; policy effect: `reversible_write`. +Purpose: Create one named plugin folder. +- `path_params`: `folder_name*` (string): Exact plugin folder name returned by plugin.folders.get. +- `query`: none +- `body`: none + +### `plugin.folder.delete` +`DELETE /api/v1/plugin/folders/{folder_name}`; policy effect: `destructive_write`. +Purpose: Delete one named plugin folder without uninstalling its plugins. +- `path_params`: `folder_name*` (string): Exact plugin folder name returned by plugin.folders.get. +- `query`: none +- `body`: none + +### `plugin.folder.plugins.update` +`PUT /api/v1/plugin/folders/{folder_name}/plugins`; policy effect: `reversible_write`. +Purpose: Replace the ordered plugin IDs assigned to one named plugin folder. +- `path_params`: `folder_name*` (string): Exact plugin folder name returned by plugin.folders.get. +- `query`: none +- `body*` (array): Request value for plugin.folder.plugins.update. Replace the ordered plugin IDs assigned to one named plugin folder. Use the exact type and fields below. + +### `plugin.folders.get` +`GET /api/v1/plugin/folders`; policy effect: `safe_read`. +Purpose: Read the complete administrator plugin-folder grouping configuration. +- `path_params`: none +- `query`: none +- `body`: none + +### `plugin.folders.update` +`POST /api/v1/plugin/folders`; policy effect: `reversible_write`. +Purpose: Replace the complete administrator plugin-folder grouping configuration. +- `path_params`: none +- `query`: none +- `body`: `PluginFoldersData` with no direct fields + +### `plugin.history` +`GET /api/v1/plugin/history/{plugin_id}`; policy effect: `safe_read`. +Purpose: Read marketplace update notes and history for one plugin. +- `path_params`: `plugin_id*` (string): Exact installed or marketplace plugin ID. +- `query`: `force` (boolean; default `True`): Force a marketplace refresh or plugin installation when true. +- `body`: none + +### `plugin.install` +`GET /api/v1/plugin/install/{plugin_id}`; policy effect: `external_side_effect`. +Purpose: Install or update one plugin from an approved source. +- `path_params`: `plugin_id*` (string): Exact installed or marketplace plugin ID. +- `query`: `force` (boolean|null; default `False`): Force a marketplace refresh or plugin installation when true.; `release_version` (string|null): Exact plugin release version to install when one is required.; `repo_url` (string|null; default ``): Approved plugin repository URL used to resolve the installation source. +- `body`: none + +### `plugin.installed` +`GET /api/v1/plugin/`; policy effect: `safe_read`. +Purpose: List installed plugins and their runtime status. +- `path_params`: none +- `query`: `force` (boolean; default `False`): Force a marketplace refresh or plugin installation when true.; `max_results` (integer; default `50`; minimum `1`; maximum `200`): Maximum number of plugin catalog results to return, from 1 to 200.; `query` (string|null): Optional case-insensitive keyword matched against plugin ID, name, description, and author.; `state*` (string=installed): Literal installed, selecting only installed plugin catalog entries. +- `body`: none + +### `plugin.market` +`GET /api/v1/plugin/`; policy effect: `safe_read`. +Purpose: List plugins available from configured marketplaces. +- `path_params`: none +- `query`: `force` (boolean; default `False`): Force a marketplace refresh or plugin installation when true.; `max_results` (integer; default `50`; minimum `1`; maximum `200`): Maximum number of plugin catalog results to return, from 1 to 200.; `query` (string|null): Optional case-insensitive keyword matched against plugin ID, name, description, and author.; `state*` (string=market): Literal market, selecting only market plugin catalog entries. +- `body`: none + +### `plugin.market.sync_wiki` +`POST /api/v1/system/setting/PLUGIN_MARKET/sync-wiki`; policy effect: `external_side_effect`. +Purpose: Refresh the configured plugin marketplace repositories from the MoviePilot Wiki. +- `path_params`: none +- `query`: none +- `body` (PluginMarketSyncRequest|null): Request value for plugin.market.sync_wiki. Refresh the configured plugin marketplace repositories from the MoviePilot Wiki. Use the exact type and fields below. + +### `plugin.rating` +`GET /api/v1/plugin/rating/{plugin_id}`; policy effect: `safe_read`. +Purpose: Read the current aggregate rating for one plugin. +- `path_params`: `plugin_id*` (string): Exact installed or marketplace plugin ID. +- `query`: none +- `body`: none + +### `plugin.rating.submit` +`POST /api/v1/plugin/rating/{plugin_id}`; policy effect: `external_side_effect`. +Purpose: Submit or replace the current user's rating for one plugin. +- `path_params`: `plugin_id*` (string): Exact installed or marketplace plugin ID. +- `query`: none +- `body`: `rating*` (number; minimum `0.1`; maximum `5.0`): Numeric plugin rating accepted by the endpoint's declared bounds. + +### `plugin.ratings` +`GET /api/v1/plugin/rating`; policy effect: `safe_read`. +Purpose: Read aggregate ratings for a requested plugin set. +- `path_params`: none +- `query`: `plugin_ids` (string|null): Exact plugin IDs whose aggregate ratings should be returned. +- `body`: none + +### `plugin.releases` +`GET /api/v1/plugin/releases/{plugin_id}`; policy effect: `safe_read`. +Purpose: List available release versions for one plugin source. +- `path_params`: `plugin_id*` (string): Exact installed or marketplace plugin ID. +- `query`: `force` (boolean; default `False`): Force a marketplace refresh or plugin installation when true.; `repo_url` (string|null; default ``): Approved plugin repository URL used to resolve the installation source. +- `body`: none + +### `plugin.reload` +`GET /api/v1/plugin/reload/{plugin_id}`; policy effect: `external_side_effect`. +Purpose: Reload one installed plugin into the running process. +- `path_params`: `plugin_id*` (string): Exact installed or marketplace plugin ID. +- `query`: none +- `body`: none + +### `plugin.reset` +`GET /api/v1/plugin/reset/{plugin_id}`; policy effect: `destructive_write`. +Purpose: Delete one plugin's saved configuration and data, then restore its default runtime state. +- `path_params`: `plugin_id*` (string): Exact installed or marketplace plugin ID. +- `query`: none +- `body`: none + +### `plugin.runtime.status` +`GET /api/v1/plugin/runtime`; policy effect: `safe_read`. +Purpose: Read plugin runtime convergence, loading, and failure state. +- `path_params`: none +- `query`: none +- `body`: none + +### `plugin.source.change` +`POST /api/v1/plugin/source/{plugin_id}`; policy effect: `external_side_effect`. +Purpose: Switch an installed plugin to one explicitly selected online source revision. +- `path_params`: `plugin_id*` (string): Exact installed or marketplace plugin ID. +- `query`: none +- `body`: `expected_revision*` (integer; minimum `1.0`): Exact current plugin source-identity revision returned by plugin.source.options.; `release_version` (string|null): Exact plugin release version to install when one is required.; `repo_url*` (string; minimum length `1`): Approved plugin repository URL used to resolve the installation source. + +### `plugin.source.install` +`POST /api/v1/plugin/source/{plugin_id}/install`; policy effect: `external_side_effect`. +Purpose: Install an unbound plugin from one explicitly selected online source. +- `path_params`: `plugin_id*` (string): Exact installed or marketplace plugin ID. +- `query`: none +- `body`: `force` (boolean; default `False`): Force a marketplace refresh or plugin installation when true.; `release_version` (string|null): Exact plugin release version to install when one is required.; `repo_url*` (string; minimum length `1`): Approved plugin repository URL used to resolve the installation source. + +### `plugin.source.options` +`GET /api/v1/plugin/source/{plugin_id}`; policy effect: `safe_read`. +Purpose: Inspect source candidates and the current immutable source identity before installation or source change. +- `path_params`: `plugin_id*` (string): Exact installed or marketplace plugin ID. +- `query`: none +- `body`: none + +### `plugin.statistics` +`GET /api/v1/plugin/statistic`; policy effect: `safe_read`. +Purpose: Read public installation statistics for plugins. +- `path_params`: none +- `query`: none +- `body`: none + +### `plugin.uninstall` +`DELETE /api/v1/plugin/{plugin_id}`; policy effect: `destructive_write`. +Purpose: Uninstall one plugin and remove it from the installed set. +- `path_params`: `plugin_id*` (string): Exact installed or marketplace plugin ID. +- `query`: none +- `body`: none + +### `recommendation.list` +`GET /api/v1/recommend/agent`; policy effect: `safe_read`. +Purpose: Read personalized media or music recommendations. +- `path_params`: none +- `query`: `days` (integer; default `14`): Recommendation time window in days.; `fresh_sort` (string; default `release_date`): Freshness ordering used by the recommendation source.; `future` (boolean; default `True`): Include future recommendation periods when supported.; `media_type` (string; default `all`): MoviePilot media type used to filter recommendations or rule groups.; `min_listen_count` (integer; default `0`): Minimum listen count required for a music recommendation.; `music_type` (string|null): Music identity level: recording, album, or artist where supported.; `page` (integer; default `1`): One-based result page number.; `past` (boolean; default `True`): Include past recommendation periods when supported.; `range_name` (string; default `this_month`): Named recommendation time range.; `sort_by` (string; default `listen_count.desc`): Recommendation field used for ordering results.; `source` (string; default `tmdb_trending`): Exact metadata or recommendation source selected by the operation.; `with_cover` (boolean; default `False`): Require recommendation results to include cover artwork. +- `body`: none + +### `scheduler.list` +`GET /api/v1/dashboard/schedule`; policy effect: `safe_read`. +Purpose: List registered scheduler jobs and their current state. +- `path_params`: none +- `query`: none +- `body`: none + +### `scheduler.progress` +`GET /api/v1/dashboard/schedule/{job_id}/progress`; policy effect: `safe_read`. +Purpose: Read current progress for one exact scheduler job. +- `path_params`: `job_id*` (string): Exact scheduler job ID returned by scheduler.list. +- `query`: none +- `body`: none + +### `scheduler.run` +`GET /api/v1/system/runscheduler`; policy effect: `external_side_effect`. +Purpose: Run one registered scheduler job immediately. +- `path_params`: none +- `query`: `jobid*` (string): Exact scheduler job ID returned by scheduler.list. +- `body`: none + +### `search.recommend` +`POST /api/v1/search/recommend`; policy effect: `external_side_effect`. +Purpose: Use the configured recommendation model to rank or recommend torrent search results. +- `path_params`: none +- `query`: none +- `body`: `check_only` (boolean; default `False`): Validate or preview the recommendation without applying search-result filtering.; `filtered_indices` (array|null): Zero-based search-result indices selected by the recommendation model.; `force` (boolean; default `False`): Force a marketplace refresh or plugin installation when true. + +### `search.results` +`GET /api/v1/search/last/context`; policy effect: `safe_read`. +Purpose: Read the most recent torrent-search context and result set. +- `path_params`: none +- `query`: none +- `body`: none + +### `search.title` +`GET /api/v1/search/title`; policy effect: `external_side_effect`. +Purpose: Search torrent sites directly from a free-form title and optional media filters. +- `path_params`: none +- `query`: `keyword` (string|null): Case-insensitive substring used to discover settings or filter storage entries.; `mtype` (string|null): MoviePilot media type or subscription-history category required by the operation.; `page` (integer|null; default `0`): One-based result page number.; `sites` (string|null): Exact site IDs included in the search or subscription scope. +- `body`: none + +### `search.torrents` +`GET /api/v1/search/media/{media_id}`; policy effect: `safe_read`. +Purpose: Search torrent sites for one canonical media identity. +- `path_params`: `media_id*` (string): Source-native media ID. Always pair it with the exact media_source returned by search. +- `query`: `area` (string|null; default `title`): Optional region filter applied by the torrent search workflow.; `media_source*` (MediaSource): Metadata source identifier. Preserve the exact value returned with media_id.; `mtype` (string|null): MoviePilot media type or subscription-history category required by the operation.; `music_type` (string|null): Music identity level: recording, album, or artist where supported.; `season` (string|null): Season number used by the media, search, subscription, or transfer operation.; `sites` (string|null): Exact site IDs included in the search or subscription scope. +- `body`: none + +### `site.add` +`POST /api/v1/site/`; policy effect: `reversible_write`. +Purpose: Create one configured site with its complete authentication and search settings. +- `path_params`: none +- `query`: none +- `body`: `apikey` (string|null): Site API key used by sites that support API-key authentication.; `cookie` (string|null): Site authentication cookie. Treat this value as a secret.; `domain` (string|null): Site hostname or domain used for matching and requests.; `downloader` (string|null): Configured downloader instance name.; `filter` (string|null): Named filter rule or rule expression applied to this site or subscription.; `id` (integer|null): Persistent database identifier of the supplied record.; `is_active` (boolean|null; default `True`): Whether the configured site is enabled.; `limit_count` (integer|null): Maximum number of site requests allowed in one rate-limit interval.; `limit_interval` (integer|null): Number of requests in the site's rate-limit window.; `limit_seconds` (integer|null): Site rate-limit window length in seconds.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `note` (JsonData-Input|null): Structured auxiliary metadata stored with the record.; `pri` (integer|null; default `0`): Site search priority; lower or higher ordering follows the existing site API convention.; `proxy` (integer|null; default `0`): Whether the site uses MoviePilot's configured proxy.; `public` (integer|null; default `0`): Whether the site is treated as a public indexer.; `render` (integer|null; default `0`): Whether site requests require browser rendering.; `rss` (string|null): Site RSS feed URL.; `timeout` (integer|null; default `15`): Per-request site timeout in seconds.; `token` (string|null): Site authentication token. Treat this value as a secret.; `ua` (string|null): Site User-Agent string used for authenticated requests.; `url` (string|null): Site, storage, or torrent URL represented by this field. + +### `site.auth.options` +`GET /api/v1/site/auth`; policy effect: `safe_read`. +Purpose: List site-account authentication providers and their required input definitions. +- `path_params`: none +- `query`: none +- `body`: none + +### `site.authenticate` +`POST /api/v1/site/auth`; policy effect: `external_side_effect`. +Purpose: Authenticate a supported site account and persist the resulting site authorization state. +- `path_params`: none +- `query`: none +- `body`: `params` (object|null): Provider-defined JSON parameters for the selected authentication or storage action.; `site` (string|null): Source site identifier associated with the torrent result. + +### `site.category` +`GET /api/v1/site/category/{site_id}`; policy effect: `safe_read`. +Purpose: List torrent categories supported by one configured site. +- `path_params`: `site_id*` (integer): Persistent site ID returned by site.list. +- `query`: none +- `body`: none + +### `site.cookie.update` +`POST /api/v1/site/cookie/{site_id}`; policy effect: `reversible_write`. +Purpose: Log in to one site and refresh its stored authentication cookie. +- `path_params`: `site_id*` (integer): Persistent site ID returned by site.list. +- `query`: none +- `body`: `code` (string|null): Two-factor verification code or site-specific authentication secret.; `password*` (string): Site login password. Treat this value as a secret.; `username*` (string): MoviePilot or site username required by the selected operation. + +### `site.cookiecloud.sync` +`GET /api/v1/site/cookiecloud`; policy effect: `external_side_effect`. +Purpose: Start a CookieCloud synchronization of configured sites. +- `path_params`: none +- `query`: none +- `body`: none + +### `site.delete` +`DELETE /api/v1/site/{site_id}`; policy effect: `destructive_write`. +Purpose: Delete one configured site by persistent site ID. +- `path_params`: `site_id*` (integer): Persistent site ID returned by site.list. +- `query`: none +- `body`: none + +### `site.list` +`GET /api/v1/site/agent`; policy effect: `safe_read`. +Purpose: List configured sites with status/name filters; authentication fields are returned only to a superuser. +- `path_params`: none +- `query`: `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `status` (string(active,inactive,all); default `all`): Transfer success status used to filter history or describe a record. +- `body`: none + +### `site.mapping` +`GET /api/v1/site/mapping`; policy effect: `safe_read`. +Purpose: Read the configured site-domain to site-name mapping. +- `path_params`: none +- `query`: none +- `body`: none + +### `site.priorities.update` +`POST /api/v1/site/priorities`; policy effect: `reversible_write`. +Purpose: Replace priorities for the supplied configured site IDs. +- `path_params`: none +- `query`: none +- `body*` (array): Request value for site.priorities.update. Replace priorities for the supplied configured site IDs. Use the exact type and fields below. + +### `site.reset` +`GET /api/v1/site/reset`; policy effect: `destructive_write`. +Purpose: Delete all configured sites and start a fresh CookieCloud synchronization. +- `path_params`: none +- `query`: none +- `body`: none + +### `site.resource` +`GET /api/v1/site/resource/{site_id}`; policy effect: `external_side_effect`. +Purpose: Browse torrent resources from one configured site with category and keyword filters. +- `path_params`: `site_id*` (integer): Persistent site ID returned by site.list. +- `query`: `cat` (string|null): Exact site category identifier returned by site.category.; `keyword` (string|null): Case-insensitive substring used to discover settings or filter storage entries.; `mtype` (string|null): MoviePilot media type or subscription-history category required by the operation.; `page` (integer|null; default `0`): One-based result page number. +- `body`: none + +### `site.rss` +`GET /api/v1/site/rss`; policy effect: `safe_read`. +Purpose: List configured sites selected for RSS subscription processing. +- `path_params`: none +- `query`: none +- `body`: none + +### `site.searchable` +`GET /api/v1/site/media/{media_type}`; policy effect: `safe_read`. +Purpose: List active configured sites supporting one exact media type. +- `path_params`: `media_type*` (string): MoviePilot media type used to filter recommendations or rule groups. +- `query`: none +- `body`: none + +### `site.statistic` +`GET /api/v1/site/statistic/{site_url}`; policy effect: `safe_read`. +Purpose: Read account and traffic statistics for one exact configured site domain. +- `path_params`: `site_url*` (string): Configured site URL or hostname used to select one site's statistics. +- `query`: none +- `body`: none + +### `site.statistics` +`GET /api/v1/site/statistic`; policy effect: `safe_read`. +Purpose: Read the latest account and traffic statistics for all configured sites. +- `path_params`: none +- `query`: none +- `body`: none + +### `site.supporting` +`GET /api/v1/site/supporting`; policy effect: `safe_read`. +Purpose: List indexer definitions supported by the installed MoviePilot resources. +- `path_params`: none +- `query`: none +- `body`: none + +### `site.test` +`GET /api/v1/site/test/{site_id}`; policy effect: `safe_read`. +Purpose: Test connectivity and authentication for one configured site. +- `path_params`: `site_id*` (integer): Persistent site ID returned by site.list. +- `query`: none +- `body`: none + +### `site.update` +`PUT /api/v1/site/`; policy effect: `reversible_write`. +Purpose: Update one configured site's complete settings. +- `path_params`: none +- `query`: none +- `body`: `apikey` (string|null): Site API key used by sites that support API-key authentication.; `cookie` (string|null): Site authentication cookie. Treat this value as a secret.; `domain` (string|null): Site hostname or domain used for matching and requests.; `downloader` (string|null): Configured downloader instance name.; `filter` (string|null): Named filter rule or rule expression applied to this site or subscription.; `id` (integer|null): Persistent database identifier of the supplied record.; `is_active` (boolean|null; default `True`): Whether the configured site is enabled.; `limit_count` (integer|null): Maximum number of site requests allowed in one rate-limit interval.; `limit_interval` (integer|null): Number of requests in the site's rate-limit window.; `limit_seconds` (integer|null): Site rate-limit window length in seconds.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `note` (JsonData-Input|null): Structured auxiliary metadata stored with the record.; `pri` (integer|null; default `0`): Site search priority; lower or higher ordering follows the existing site API convention.; `proxy` (integer|null; default `0`): Whether the site uses MoviePilot's configured proxy.; `public` (integer|null; default `0`): Whether the site is treated as a public indexer.; `render` (integer|null; default `0`): Whether site requests require browser rendering.; `rss` (string|null): Site RSS feed URL.; `timeout` (integer|null; default `15`): Per-request site timeout in seconds.; `token` (string|null): Site authentication token. Treat this value as a secret.; `ua` (string|null): Site User-Agent string used for authenticated requests.; `url` (string|null): Site, storage, or torrent URL represented by this field. + +### `site.userdata` +`GET /api/v1/site/userdata/{site_id}`; policy effect: `safe_read`. +Purpose: Read the latest account statistics collected from one site. +- `path_params`: `site_id*` (integer): Persistent site ID returned by site.list. +- `query`: `workdate` (string|null): Date used when retrieving one site's historical user statistics. +- `body`: none + +### `site.userdata.latest` +`GET /api/v1/site/userdata/latest`; policy effect: `safe_read`. +Purpose: Read the latest collected account statistics for every configured site. +- `path_params`: none +- `query`: none +- `body`: none + +### `site.userdata.refresh` +`POST /api/v1/site/userdata/{site_id}`; policy effect: `external_side_effect`. +Purpose: Refresh and return account statistics for one configured site. +- `path_params`: `site_id*` (integer): Persistent site ID returned by site.list. +- `query`: none +- `body`: none + +### `slash.list` +`GET /api/v1/message/agent/commands`; policy effect: `safe_read`. +Purpose: List slash commands that the Agent may dispatch. +- `path_params`: none +- `query`: none +- `body`: none + +### `slash.run` +`POST /api/v1/message/agent/commands/run`; policy effect: `external_side_effect`. +Purpose: Execute one complete slash command through MoviePilot messaging. +- `path_params`: none +- `query`: none +- `body`: `command*` (string): Complete slash command, including the leading slash and all arguments. + +### `storage.delete` +`POST /api/v1/storage/delete`; policy effect: `destructive_write`. +Purpose: Delete one exact file or directory from a configured storage provider. +- `path_params`: none +- `query`: none +- `body`: `basename` (string|null): Base filename without its parent path.; `children` (array|null): Child storage items nested below this item.; `drive_id` (string|null): Provider-native storage drive identifier.; `extension` (string|null): Filename extension, including or excluding the leading dot as returned by storage.; `fileid` (string|null): Provider-native storage item identifier.; `modify_time` (number|null): Storage item modification timestamp.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `parent_fileid` (string|null): Provider-native identifier of the parent storage directory.; `path` (string|null; default `/`): Storage or history path represented by this record.; `pickcode` (string|null): 115 storage pickcode associated with the item.; `size` (integer|null): File or torrent size in bytes.; `storage` (string|null; default `local`): Configured storage name or storage type used by the operation.; `thumbnail` (string|null): Thumbnail URL returned by the storage provider.; `type` (string|null): MoviePilot media or storage item type required by the selected operation.; `url` (string|null): Site, storage, or torrent URL represented by this field. + +### `storage.list` +`POST /api/v1/storage/agent/list`; policy effect: `safe_read`. +Purpose: List files or directories from one configured storage location. +- `path_params`: none +- `query`: `keyword` (string|null): Case-insensitive substring used to discover settings or filter storage entries.; `sort` (string|null; default `updated_at`): Storage-list sort field or ordering expression. +- `body`: `basename` (string|null): Base filename without its parent path.; `children` (array|null): Child storage items nested below this item.; `drive_id` (string|null): Provider-native storage drive identifier.; `extension` (string|null): Filename extension, including or excluding the leading dot as returned by storage.; `fileid` (string|null): Provider-native storage item identifier.; `modify_time` (number|null): Storage item modification timestamp.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `parent_fileid` (string|null): Provider-native identifier of the parent storage directory.; `path` (string|null; default `/`): Storage or history path represented by this record.; `pickcode` (string|null): 115 storage pickcode associated with the item.; `size` (integer|null): File or torrent size in bytes.; `storage` (string|null; default `local`): Configured storage name or storage type used by the operation.; `thumbnail` (string|null): Thumbnail URL returned by the storage provider.; `type` (string|null): MoviePilot media or storage item type required by the selected operation.; `url` (string|null): Site, storage, or torrent URL represented by this field. + +### `storage.manage` +`POST /api/v1/storage/manage`; policy effect: `external_side_effect`. +Purpose: Run one provider-defined management action against an exact configured storage target. +- `path_params`: none +- `query`: none +- `body`: `action*` (string): Exact provider or workflow action identifier required by the selected operation.; `params` (object): Provider-defined JSON parameters for the selected authentication or storage action.; `target*` (string): Exact configured storage target name accepted by storage.manage. + +### `storage.mkdir` +`POST /api/v1/storage/mkdir`; policy effect: `reversible_write`. +Purpose: Create a named child directory below one exact storage directory item. +- `path_params`: none +- `query`: `name*` (string): Human-readable name of the site, storage item, subscription, or rule group. +- `body`: `basename` (string|null): Base filename without its parent path.; `children` (array|null): Child storage items nested below this item.; `drive_id` (string|null): Provider-native storage drive identifier.; `extension` (string|null): Filename extension, including or excluding the leading dot as returned by storage.; `fileid` (string|null): Provider-native storage item identifier.; `modify_time` (number|null): Storage item modification timestamp.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `parent_fileid` (string|null): Provider-native identifier of the parent storage directory.; `path` (string|null; default `/`): Storage or history path represented by this record.; `pickcode` (string|null): 115 storage pickcode associated with the item.; `size` (integer|null): File or torrent size in bytes.; `storage` (string|null; default `local`): Configured storage name or storage type used by the operation.; `thumbnail` (string|null): Thumbnail URL returned by the storage provider.; `type` (string|null): MoviePilot media or storage item type required by the selected operation.; `url` (string|null): Site, storage, or torrent URL represented by this field. + +### `storage.rename` +`POST /api/v1/storage/rename`; policy effect: `reversible_write`. +Purpose: Rename one exact storage item, optionally applying media-aware recursive renaming. +- `path_params`: none +- `query`: `new_name*` (string): Replacement name for the existing filter-rule group.; `recursive` (boolean|null; default `False`): Apply media-aware renaming recursively to child files when true. +- `body`: `basename` (string|null): Base filename without its parent path.; `children` (array|null): Child storage items nested below this item.; `drive_id` (string|null): Provider-native storage drive identifier.; `extension` (string|null): Filename extension, including or excluding the leading dot as returned by storage.; `fileid` (string|null): Provider-native storage item identifier.; `modify_time` (number|null): Storage item modification timestamp.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `parent_fileid` (string|null): Provider-native identifier of the parent storage directory.; `path` (string|null; default `/`): Storage or history path represented by this record.; `pickcode` (string|null): 115 storage pickcode associated with the item.; `size` (integer|null): File or torrent size in bytes.; `storage` (string|null; default `local`): Configured storage name or storage type used by the operation.; `thumbnail` (string|null): Thumbnail URL returned by the storage provider.; `type` (string|null): MoviePilot media or storage item type required by the selected operation.; `url` (string|null): Site, storage, or torrent URL represented by this field. + +### `storage.settings` +`GET /api/v1/storage/directories`; policy effect: `safe_read`. +Purpose: Read configured directory or storage settings. +- `path_params`: none +- `query`: `directory_type` (string; default `all`): Directory configuration subtype to return.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `storage_type` (string; default `all`): Configured storage provider type to return. +- `body`: none + +### `subscription.add` +`POST /api/v1/subscribe/`; policy effect: `reversible_write`. +Purpose: Create one movie, TV, or music subscription. +- `path_params`: none +- `query`: none +- `body`: `audio_format` (string|null): Requested or recorded audio container or codec, such as FLAC or MP3.; `audio_quality` (string|null): Subscription audio-quality rule, such as hires, lossless, or lossy.; `backdrop` (string|null): Backdrop image URL stored with the media or subscription.; `best_version` (integer|null): Enable normal best-version upgrading when set to 1.; `best_version_full` (integer|null): Enable full best-version upgrading when set to 1.; `completed_episode` (integer|null): Highest episode number already completed for the subscription.; `current_audio_format` (string|null): Audio format of the best version currently held.; `current_bit_depth` (integer|null): Bit depth of the best version currently held.; `current_bitrate` (integer|null): Bitrate of the best version currently held.; `current_priority` (integer|null): Calculated priority of the best version currently held.; `current_sample_rate` (integer|null): Sample rate of the best version currently held.; `custom_words` (string|null): Custom recognition or rename words applied to this media workflow.; `date` (string|null): Record creation or completion timestamp used by the history item.; `description` (string|null): Human-readable media, torrent, or subscription description.; `downloader` (string|null): Configured downloader instance name.; `effect` (string|null): Video or release-effect filter expression used by the subscription.; `episode_group` (string|null): TMDB episode-group identifier used for alternate episode ordering.; `episode_priority` (object|null): Per-episode best-version priority state.; `exclude` (string|null): Regular expression or filter expression that rejects matching releases.; `filter` (string|null): Named filter rule or rule expression applied to this site or subscription.; `filter_groups` (array|null): Ordered filter-rule group names applied to the subscription.; `id` (integer|null): Persistent database identifier of the supplied record.; `include` (string|null): Regular expression or filter expression that a release must match.; `keyword` (string|null): Case-insensitive substring used to discover settings or filter storage entries.; `lack_episode` (integer|null; default `0`): Number of episodes still missing from the subscription.; `last_update` (string|null): Timestamp of the subscription's most recent update.; `media_category` (string|null): MoviePilot library category assigned to the media.; `media_id` (string|null): Source-native media ID. Always pair it with the exact media_source returned by search.; `media_source` (MediaSource|null): Metadata source identifier. Preserve the exact value returned with media_id.; `min_bit_depth` (integer|null): Minimum acceptable audio bit depth in bits.; `min_bitrate` (integer|null): Minimum acceptable audio bitrate in bits per second.; `min_sample_rate` (integer|null): Minimum acceptable audio sample rate in hertz.; `music_type` (string|null): Music identity level: recording, album, or artist where supported.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `note` (array|null): Structured auxiliary metadata stored with the record.; `poster` (string|null): Poster image URL stored with the media or subscription.; `quality` (string|null): Video or release quality filter expression.; `resolution` (string|null): Video resolution filter expression, such as 1080p or 2160p.; `save_path` (string|null): Configured downloader-side save path for the download or subscription.; `search_imdbid` (integer|null; default `0`): Use IMDb identity during subscription search when set to 1.; `season` (integer|null): Season number used by the media, search, subscription, or transfer operation.; `sites` (array|null): Exact site IDs included in the search or subscription scope.; `start_episode` (integer|null; default `0`): First episode number requested by the subscription.; `state` (string|null): Current site, subscription, marketplace, or transfer state filter.; `total_episode` (integer|null; default `0`): Expected total episode count for the subscription.; `total_tracks` (integer|null): Expected or recorded track count for a music item.; `type` (string|null): MoviePilot media or storage item type required by the selected operation.; `username` (string|null): MoviePilot or site username required by the selected operation.; `vote` (number|null; default `0.0`): Media vote average stored with the subscription.; `year` (string|null): Release or premiere year used to disambiguate the media title. + +### `subscription.delete` +`DELETE /api/v1/subscribe/{subscribe_id}`; policy effect: `destructive_write`. +Purpose: Delete one active subscription. +- `path_params`: `subscribe_id*` (integer): Persistent subscription ID returned by subscription.list. +- `query`: none +- `body`: none + +### `subscription.delete_by_media` +`DELETE /api/v1/subscribe/media/{media_id}`; policy effect: `destructive_write`. +Purpose: Delete accessible subscriptions matching one canonical media identity. +- `path_params`: `media_id*` (string): Source-native media ID. Always pair it with the exact media_source returned by search. +- `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.files` +`GET /api/v1/subscribe/files/{subscribe_id}`; policy effect: `safe_read`. +Purpose: Read local library and transfer-file coverage for one accessible subscription. +- `path_params`: `subscribe_id*` (integer): Persistent subscription ID returned by subscription.list. +- `query`: none +- `body`: none + +### `subscription.find` +`GET /api/v1/subscribe/media/{media_id}`; policy effect: `safe_read`. +Purpose: Find one accessible subscription by canonical media identity and optional season. +- `path_params`: `media_id*` (string): Source-native media ID. Always pair it with the exact media_source returned by search. +- `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.; `title` (string|null): Media, torrent, subscription, or history title used by the operation. +- `body`: none + +### `subscription.follow.add` +`POST /api/v1/subscribe/follow`; policy effect: `reversible_write`. +Purpose: Follow one subscription-sharing user by exact share user ID. +- `path_params`: none +- `query`: `share_uid` (string|null): Exact MoviePilot Server sharing-user ID to follow or unfollow. +- `body`: none + +### `subscription.follow.delete` +`DELETE /api/v1/subscribe/follow`; policy effect: `reversible_write`. +Purpose: Stop following one subscription-sharing user by exact share user ID. +- `path_params`: none +- `query`: `share_uid` (string|null): Exact MoviePilot Server sharing-user ID to follow or unfollow. +- `body`: none + +### `subscription.follow.list` +`GET /api/v1/subscribe/follow`; policy effect: `safe_read`. +Purpose: List subscription-sharing user IDs followed by the current user. +- `path_params`: none +- `query`: none +- `body`: none + +### `subscription.fork` +`POST /api/v1/subscribe/fork`; policy effect: `external_side_effect`. +Purpose: Create a local subscription from one shared subscription definition. +- `path_params`: none +- `query`: none +- `body`: `audio_format` (string|null): Requested or recorded audio container or codec, such as FLAC or MP3.; `audio_quality` (string|null): Subscription audio-quality rule, such as hires, lossless, or lossy.; `backdrop` (string|null): Backdrop image URL stored with the media or subscription.; `count` (integer|null; default `0`): Maximum number of records to return on the requested page.; `custom_words` (string|null): Custom recognition or rename words applied to this media workflow.; `date` (string|null): Record creation or completion timestamp used by the history item.; `description` (string|null): Human-readable media, torrent, or subscription description.; `effect` (string|null): Video or release-effect filter expression used by the subscription.; `episode_group` (string|null): TMDB episode-group identifier used for alternate episode ordering.; `exclude` (string|null): Regular expression or filter expression that rejects matching releases.; `id` (integer|null): Persistent database identifier of the supplied record.; `include` (string|null): Regular expression or filter expression that a release must match.; `keyword` (string|null): Case-insensitive substring used to discover settings or filter storage entries.; `media_category` (string|null): MoviePilot library category assigned to the media.; `media_id` (string|null): Source-native media ID. Always pair it with the exact media_source returned by search.; `media_source` (MediaSource|null): Metadata source identifier. Preserve the exact value returned with media_id.; `min_bit_depth` (integer|null): Minimum acceptable audio bit depth in bits.; `min_bitrate` (integer|null): Minimum acceptable audio bitrate in bits per second.; `min_sample_rate` (integer|null): Minimum acceptable audio sample rate in hertz.; `music_type` (string|null): Music identity level: recording, album, or artist where supported.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `poster` (string|null): Poster image URL stored with the media or subscription.; `quality` (string|null): Video or release quality filter expression.; `resolution` (string|null): Video resolution filter expression, such as 1080p or 2160p.; `season` (integer|null): Season number used by the media, search, subscription, or transfer operation.; `share_comment` (string|null): Optional explanatory comment published with a shared item.; `share_title` (string|null): Public title used when publishing a subscription or workflow.; `share_uid` (string|null): Exact MoviePilot Server sharing-user ID to follow or unfollow.; `share_user` (string|null): Public contributor name used when publishing a subscription or workflow.; `subscribe_id` (integer|null): Persistent subscription ID returned by subscription.list.; `total_episode` (integer|null; default `0`): Expected total episode count for the subscription.; `total_tracks` (integer|null): Expected or recorded track count for a music item.; `type` (string|null): MoviePilot media or storage item type required by the selected operation.; `vote` (number|null; default `0.0`): Media vote average stored with the subscription.; `year` (string|null): Release or premiere year used to disambiguate the media title. + +### `subscription.get` +`GET /api/v1/subscribe/{subscribe_id}`; policy effect: `safe_read`. +Purpose: Read one accessible subscription by persistent subscription ID. +- `path_params`: `subscribe_id*` (integer): Persistent subscription ID returned by subscription.list. +- `query`: none +- `body`: none + +### `subscription.history` +`GET /api/v1/subscribe/history/{mtype}`; policy effect: `safe_read`. +Purpose: List completed or archived subscription records. +- `path_params`: `mtype*` (string): MoviePilot media type or subscription-history category required by the operation. +- `query`: `count` (integer|null; default `30`): Maximum number of records to return on the requested page.; `page` (integer|null; default `1`): One-based result page number. +- `body`: none + +### `subscription.history.delete` +`DELETE /api/v1/subscribe/history/{history_id}`; policy effect: `destructive_write`. +Purpose: Delete one accessible subscription-history record. +- `path_params`: `history_id*` (integer): Persistent transfer- or subscription-history ID returned by a history operation. +- `query`: none +- `body`: none + +### `subscription.list` +`GET /api/v1/subscribe/`; policy effect: `safe_read`. +Purpose: List active subscriptions. +- `path_params`: none +- `query`: none +- `body`: none + +### `subscription.metadata.refresh` +`GET /api/v1/subscribe/check`; policy effect: `external_side_effect`. +Purpose: Start a system-wide refresh of subscription TMDB metadata. +- `path_params`: none +- `query`: none +- `body`: none + +### `subscription.popular` +`GET /api/v1/subscribe/popular`; policy effect: `safe_read`. +Purpose: List globally popular subscriptions with filters and pagination. +- `path_params`: none +- `query`: `count` (integer|null; default `30`): Maximum number of records to return on the requested page.; `genre_id` (integer|null): Genre identifier used to filter shared or popular subscriptions.; `max_rating` (number|null): Maximum rating used to filter shared or popular subscriptions.; `min_rating` (number|null): Minimum rating used to filter shared or popular subscriptions.; `min_sub` (integer|null): Minimum subscriber count used to filter popular subscriptions.; `page` (integer|null; default `1`): One-based result page number.; `sort_type` (string|null): Ascending or descending order used by shared or popular subscriptions.; `stype*` (string): Popular-subscription category requested by the endpoint. +- `body`: none + +### `subscription.refresh` +`GET /api/v1/subscribe/refresh`; policy effect: `external_side_effect`. +Purpose: Start the configured system-wide subscription refresh job. +- `path_params`: none +- `query`: none +- `body`: none + +### `subscription.reset` +`GET /api/v1/subscribe/reset/{subid}`; policy effect: `reversible_write`. +Purpose: Reset one accessible subscription so it can be processed again. +- `path_params`: `subid*` (integer): Persistent subscription ID whose status or processing state will change. +- `query`: none +- `body`: none + +### `subscription.search` +`GET /api/v1/subscribe/search/{subscribe_id}`; policy effect: `safe_read`. +Purpose: Run an immediate search for one existing subscription. +- `path_params`: `subscribe_id*` (integer): Persistent subscription ID returned by subscription.list. +- `query`: none +- `body`: none + +### `subscription.search_all` +`GET /api/v1/subscribe/search`; policy effect: `external_side_effect`. +Purpose: Start immediate searches for all subscriptions accessible to the current user. +- `path_params`: none +- `query`: none +- `body`: none + +### `subscription.share` +`POST /api/v1/subscribe/share`; policy effect: `external_side_effect`. +Purpose: Publish one accessible subscription to the MoviePilot sharing service. +- `path_params`: none +- `query`: none +- `body`: `audio_format` (string|null): Requested or recorded audio container or codec, such as FLAC or MP3.; `audio_quality` (string|null): Subscription audio-quality rule, such as hires, lossless, or lossy.; `backdrop` (string|null): Backdrop image URL stored with the media or subscription.; `count` (integer|null; default `0`): Maximum number of records to return on the requested page.; `custom_words` (string|null): Custom recognition or rename words applied to this media workflow.; `date` (string|null): Record creation or completion timestamp used by the history item.; `description` (string|null): Human-readable media, torrent, or subscription description.; `effect` (string|null): Video or release-effect filter expression used by the subscription.; `episode_group` (string|null): TMDB episode-group identifier used for alternate episode ordering.; `exclude` (string|null): Regular expression or filter expression that rejects matching releases.; `id` (integer|null): Persistent database identifier of the supplied record.; `include` (string|null): Regular expression or filter expression that a release must match.; `keyword` (string|null): Case-insensitive substring used to discover settings or filter storage entries.; `media_category` (string|null): MoviePilot library category assigned to the media.; `media_id` (string|null): Source-native media ID. Always pair it with the exact media_source returned by search.; `media_source` (MediaSource|null): Metadata source identifier. Preserve the exact value returned with media_id.; `min_bit_depth` (integer|null): Minimum acceptable audio bit depth in bits.; `min_bitrate` (integer|null): Minimum acceptable audio bitrate in bits per second.; `min_sample_rate` (integer|null): Minimum acceptable audio sample rate in hertz.; `music_type` (string|null): Music identity level: recording, album, or artist where supported.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `poster` (string|null): Poster image URL stored with the media or subscription.; `quality` (string|null): Video or release quality filter expression.; `resolution` (string|null): Video resolution filter expression, such as 1080p or 2160p.; `season` (integer|null): Season number used by the media, search, subscription, or transfer operation.; `share_comment` (string|null): Optional explanatory comment published with a shared item.; `share_title` (string|null): Public title used when publishing a subscription or workflow.; `share_uid` (string|null): Exact MoviePilot Server sharing-user ID to follow or unfollow.; `share_user` (string|null): Public contributor name used when publishing a subscription or workflow.; `subscribe_id` (integer|null): Persistent subscription ID returned by subscription.list.; `total_episode` (integer|null; default `0`): Expected total episode count for the subscription.; `total_tracks` (integer|null): Expected or recorded track count for a music item.; `type` (string|null): MoviePilot media or storage item type required by the selected operation.; `vote` (number|null; default `0.0`): Media vote average stored with the subscription.; `year` (string|null): Release or premiere year used to disambiguate the media title. + +### `subscription.share.delete` +`DELETE /api/v1/subscribe/share/{share_id}`; policy effect: `external_side_effect`. +Purpose: Delete one shared-subscription publication by share ID. +- `path_params`: `share_id*` (integer): Persistent MoviePilot Server share ID returned by a share-list operation. +- `query`: none +- `body`: none + +### `subscription.share.statistics` +`GET /api/v1/subscribe/share/statistics`; policy effect: `safe_read`. +Purpose: Read aggregate contribution and reuse counts for subscription sharers. +- `path_params`: none +- `query`: none +- `body`: none + +### `subscription.shares` +`GET /api/v1/subscribe/shares`; policy effect: `safe_read`. +Purpose: List shared subscriptions with filters and pagination. +- `path_params`: none +- `query`: `count` (integer|null; default `30`): Maximum number of records to return on the requested page.; `genre_id` (integer|null): Genre identifier used to filter shared or popular subscriptions.; `max_rating` (number|null): Maximum rating used to filter shared or popular subscriptions.; `min_rating` (number|null): Minimum rating used to filter shared or popular subscriptions.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `page` (integer|null; default `1`): One-based result page number.; `sort_type` (string|null): Ascending or descending order used by shared or popular subscriptions. +- `body`: none + +### `subscription.status.update` +`PUT /api/v1/subscribe/status/{subid}`; policy effect: `reversible_write`. +Purpose: Set one accessible subscription to running, paused, or stopped state. +- `path_params`: `subid*` (integer): Persistent subscription ID whose status or processing state will change. +- `query`: `state*` (string): Current site, subscription, marketplace, or transfer state filter. +- `body`: none + +### `subscription.update` +`PUT /api/v1/subscribe/`; policy effect: `reversible_write`. +Purpose: Update one existing movie, TV, or music subscription. +- `path_params`: none +- `query`: none +- `body`: `audio_format` (string|null): Requested or recorded audio container or codec, such as FLAC or MP3.; `audio_quality` (string|null): Subscription audio-quality rule, such as hires, lossless, or lossy.; `backdrop` (string|null): Backdrop image URL stored with the media or subscription.; `best_version` (integer|null): Enable normal best-version upgrading when set to 1.; `best_version_full` (integer|null): Enable full best-version upgrading when set to 1.; `completed_episode` (integer|null): Highest episode number already completed for the subscription.; `current_audio_format` (string|null): Audio format of the best version currently held.; `current_bit_depth` (integer|null): Bit depth of the best version currently held.; `current_bitrate` (integer|null): Bitrate of the best version currently held.; `current_priority` (integer|null): Calculated priority of the best version currently held.; `current_sample_rate` (integer|null): Sample rate of the best version currently held.; `custom_words` (string|null): Custom recognition or rename words applied to this media workflow.; `date` (string|null): Record creation or completion timestamp used by the history item.; `description` (string|null): Human-readable media, torrent, or subscription description.; `downloader` (string|null): Configured downloader instance name.; `effect` (string|null): Video or release-effect filter expression used by the subscription.; `episode_group` (string|null): TMDB episode-group identifier used for alternate episode ordering.; `episode_priority` (object|null): Per-episode best-version priority state.; `exclude` (string|null): Regular expression or filter expression that rejects matching releases.; `filter` (string|null): Named filter rule or rule expression applied to this site or subscription.; `filter_groups` (array|null): Ordered filter-rule group names applied to the subscription.; `id` (integer|null): Persistent database identifier of the supplied record.; `include` (string|null): Regular expression or filter expression that a release must match.; `keyword` (string|null): Case-insensitive substring used to discover settings or filter storage entries.; `lack_episode` (integer|null; default `0`): Number of episodes still missing from the subscription.; `last_update` (string|null): Timestamp of the subscription's most recent update.; `media_category` (string|null): MoviePilot library category assigned to the media.; `media_id` (string|null): Source-native media ID. Always pair it with the exact media_source returned by search.; `media_source` (MediaSource|null): Metadata source identifier. Preserve the exact value returned with media_id.; `min_bit_depth` (integer|null): Minimum acceptable audio bit depth in bits.; `min_bitrate` (integer|null): Minimum acceptable audio bitrate in bits per second.; `min_sample_rate` (integer|null): Minimum acceptable audio sample rate in hertz.; `music_type` (string|null): Music identity level: recording, album, or artist where supported.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `note` (array|null): Structured auxiliary metadata stored with the record.; `poster` (string|null): Poster image URL stored with the media or subscription.; `quality` (string|null): Video or release quality filter expression.; `resolution` (string|null): Video resolution filter expression, such as 1080p or 2160p.; `save_path` (string|null): Configured downloader-side save path for the download or subscription.; `search_imdbid` (integer|null; default `0`): Use IMDb identity during subscription search when set to 1.; `season` (integer|null): Season number used by the media, search, subscription, or transfer operation.; `sites` (array|null): Exact site IDs included in the search or subscription scope.; `start_episode` (integer|null; default `0`): First episode number requested by the subscription.; `state` (string|null): Current site, subscription, marketplace, or transfer state filter.; `total_episode` (integer|null; default `0`): Expected total episode count for the subscription.; `total_tracks` (integer|null): Expected or recorded track count for a music item.; `type` (string|null): MoviePilot media or storage item type required by the selected operation.; `username` (string|null): MoviePilot or site username required by the selected operation.; `vote` (number|null; default `0.0`): Media vote average stored with the subscription.; `year` (string|null): Release or premiere year used to disambiguate the media title. + +### `subscription.user.list` +`GET /api/v1/subscribe/user/{username}`; policy effect: `safe_read`. +Purpose: List public subscriptions owned by one accessible MoviePilot username. +- `path_params`: `username*` (string): MoviePilot or site username required by the selected operation. +- `query`: none +- `body`: none + +### `subtitle.search.media` +`GET /api/v1/search/subtitle/media/{media_id}`; policy effect: `external_side_effect`. +Purpose: Search subtitle providers for one canonical media identity and optional season or episode. +- `path_params`: `media_id*` (string): Source-native media ID. Always pair it with the exact media_source returned by search. +- `query`: `episode` (string|null): Episode number used to narrow a subtitle or media search.; `media_source*` (MediaSource): Metadata source identifier. Preserve the exact value returned with media_id.; `mtype` (string|null): MoviePilot media type or subscription-history category required by the operation.; `season` (string|null): Season number used by the media, search, subscription, or transfer operation.; `sites` (string|null): Exact site IDs included in the search or subscription scope. +- `body`: none + +### `subtitle.search.title` +`GET /api/v1/search/subtitle/title`; policy effect: `external_side_effect`. +Purpose: Search subtitle providers from a free-form title and optional media filters. +- `path_params`: none +- `query`: `keyword` (string|null): Case-insensitive substring used to discover settings or filter storage entries.; `page` (integer|null; default `0`): One-based result page number.; `sites` (string|null): Exact site IDs included in the search or subscription scope. +- `body`: none + +### `system.module.list` +`GET /api/v1/system/modulelist`; policy effect: `safe_read`. +Purpose: List loaded MoviePilot module IDs and localized names. +- `path_params`: none +- `query`: none +- `body`: none + +### `system.module.test` +`GET /api/v1/system/moduletest/{moduleid}`; policy effect: `external_side_effect`. +Purpose: Run the built-in availability test for one loaded MoviePilot module. +- `path_params`: `moduleid*` (string): Exact loaded module ID returned by system.module.list. +- `query`: none +- `body`: none + +### `system.network.targets` +`GET /api/v1/system/nettest/targets`; policy effect: `safe_read`. +Purpose: List approved built-in network-test targets without exposing their request URLs. +- `path_params`: none +- `query`: none +- `body`: none + +### `system.network.test` +`GET /api/v1/system/nettest`; policy effect: `external_side_effect`. +Purpose: Test connectivity to one approved target or the legacy constrained URL input. +- `path_params`: none +- `query`: `include` (string|null): Regular expression or filter expression that a release must match.; `target_id` (string|null): Approved built-in network-test target ID returned by system.network.targets.; `url` (string|null): Site, storage, or torrent URL represented by this field. +- `body`: none + +### `system.restart` +`GET /api/v1/system/restart`; policy effect: `external_side_effect`. +Purpose: Restart the running MoviePilot process. +- `path_params`: none +- `query`: none +- `body`: none + +### `system.update.check` +`POST /api/v1/system/update/check`; policy effect: `external_side_effect`. +Purpose: Check GitHub for the latest stable MoviePilot v3 release. +- `path_params`: none +- `query`: none +- `body`: none + +### `system.update.download` +`POST /api/v1/system/update/download`; policy effect: `external_side_effect`. +Purpose: Start downloading and verifying the available stable release in the background. +- `path_params`: none +- `query`: none +- `body`: none + +### `system.update.install` +`POST /api/v1/system/update/install`; policy effect: `external_side_effect`. +Purpose: Install the already downloaded and verified stable release, then restart MoviePilot. +- `path_params`: none +- `query`: none +- `body`: none + +### `system.update.status` +`GET /api/v1/system/update/status`; policy effect: `safe_read`. +Purpose: Read the current stable-release check, download, verification, or install state. +- `path_params`: none +- `query`: none +- `body`: none + +### `system.upgrade.dev` +`POST /api/v1/system/upgrade`; policy effect: `external_side_effect`. +Purpose: Update to the current v3 development branch and restart MoviePilot. +- `path_params`: none +- `query`: none +- `body*` (string=dev): Literal dev. Release updates must use the separate check, download, and install operations. + +### `system.usage.statistics` +`GET /api/v1/system/usage/statistic`; policy effect: `safe_read`. +Purpose: Read the installation version and runtime usage report available to the current user. +- `path_params`: none +- `query`: none +- `body`: none + +### `system.versions` +`GET /api/v1/system/versions`; policy effect: `safe_read`. +Purpose: List available MoviePilot GitHub releases. +- `path_params`: none +- `query`: none +- `body`: none + +### `torrent.cache.clear` +`DELETE /api/v1/torrent/cache`; policy effect: `destructive_write`. +Purpose: Delete every cached torrent context. +- `path_params`: none +- `query`: none +- `body`: none + +### `torrent.cache.delete` +`DELETE /api/v1/torrent/cache/{domain}/{torrent_hash}`; policy effect: `destructive_write`. +Purpose: Delete one cached torrent context by site domain and cache hash. +- `path_params`: `domain*` (string): Site hostname or domain used for matching and requests.; `torrent_hash*` (string): Cache hash returned by torrent.cache.get for one exact site-domain entry. +- `query`: none +- `body`: none + +### `torrent.cache.get` +`GET /api/v1/torrent/cache`; policy effect: `safe_read`. +Purpose: Inspect cached torrent contexts and their recognized media identities. +- `path_params`: none +- `query`: none +- `body`: none + +### `torrent.cache.refresh` +`POST /api/v1/torrent/cache/refresh`; policy effect: `external_side_effect`. +Purpose: Refresh torrent caches from configured RSS or spider sources. +- `path_params`: none +- `query`: none +- `body`: none + +### `torrent.cache.reidentify` +`POST /api/v1/torrent/cache/reidentify/{domain}/{torrent_hash}`; policy effect: `reversible_write`. +Purpose: Replace or recompute the media identity for one cached torrent context. +- `path_params`: `domain*` (string): Site hostname or domain used for matching and requests.; `torrent_hash*` (string): Cache hash returned by torrent.cache.get for one exact site-domain entry. +- `query`: `media_id` (string|null): Source-native media ID. Always pair it with the exact media_source returned by search.; `media_source` (MediaSource|null): Metadata source identifier. Preserve the exact value returned with media_id.; `music_type` (string(recording,album)|null): Music identity level: recording, album, or artist where supported. +- `body`: none + +### `transfer.episode_format.recommend` +`POST /api/v1/transfer/episode-format/recommend`; policy effect: `safe_read`. +Purpose: Recommend an episode-number extraction template from supplied file samples. +- `path_params`: none +- `query`: none +- `body`: `fileitem` (FileItem-Input|null): One complete source storage item returned by storage.list.; `fileitems` (array|null): Additional source storage items included in the same manual transfer. + +### `transfer.file` +`POST /api/v1/transfer/manual`; policy effect: `external_side_effect`. +Purpose: Run MoviePilot's manual file-transfer and organization workflow. +- `path_params`: none +- `query`: `background` (boolean|null; default `False`): Run the transfer asynchronously and return before completion. +- `body`: `episode_detail` (string|null): Episode mapping details used by manual transfer.; `episode_format` (string|null): Episode-number formatting rule used by manual transfer.; `episode_group` (string|null): TMDB episode-group identifier used for alternate episode ordering.; `episode_offset` (string|null): Integer offset added to detected episode numbers.; `episode_part` (string|null): Episode part number used when one episode is split across files.; `fileitem` (FileItem-Input): One complete source storage item returned by storage.list.; `fileitems` (array|null): Additional source storage items included in the same manual transfer.; `from_history` (boolean|null; default `False`): Treat the transfer input as originating from an existing history record.; `library_category_folder` (boolean|null): Create or use a category-level folder in the target library.; `library_type_folder` (boolean|null): Create or use a media-type folder in the target library.; `logid` (integer|null): One download-history or transfer-log identifier used by manual transfer.; `logids` (array|null): Multiple download-history or transfer-log identifiers included in manual transfer.; `media_id` (string|null): Source-native media ID. Always pair it with the exact media_source returned by search.; `media_source` (MediaSource|null): Metadata source identifier. Preserve the exact value returned with media_id.; `min_filesize` (integer|null; default `0`): Minimum source file size accepted by manual transfer, in bytes.; `music_type` (string(recording,album)|null): Music identity level: recording, album, or artist where supported.; `preview` (boolean|null; default `False`): Validate and preview manual-transfer output without committing file changes.; `reorganize` (boolean|null; default `False`): Allow manual transfer to organize an item that was already processed.; `scrape` (boolean|null; default `False`): Generate metadata and images after manual transfer.; `season` (integer|null): Season number used by the media, search, subscription, or transfer operation.; `target_path` (string|null): Destination path used by manual transfer.; `target_storage` (string|null): Configured storage name receiving the manual transfer.; `transfer_type` (string|null): Manual-transfer mode, such as move, copy, link, or softlink.; `type_name` (string|null): Explicit media type name used when source IDs alone are ambiguous. + +### `transfer.history` +`GET /api/v1/history/transfer`; policy effect: `safe_read`. +Purpose: List file-transfer history with filters and pagination. +- `path_params`: none +- `query`: `count` (integer|null; default `30`): Maximum number of records to return on the requested page.; `page` (integer|null; default `1`): One-based result page number.; `status` (boolean|null): Transfer success status used to filter history or describe a record.; `title` (string|null): Media, torrent, subscription, or history title used by the operation. +- `body`: none + +### `transfer.history.clear` +`GET /api/v1/history/empty/transfer`; policy effect: `destructive_write`. +Purpose: Delete every transfer-history record while leaving transferred files untouched. +- `path_params`: none +- `query`: none +- `body`: none + +### `transfer.history.delete` +`DELETE /api/v1/history/transfer`; policy effect: `destructive_write`. +Purpose: Delete one transfer-history record and optionally remove files. +- `path_params`: none +- `query`: `deletedest` (boolean|null; default `False`): Also delete the organized destination files when deleting transfer history.; `deletesrc` (boolean|null; default `False`): Also delete the recorded source files when deleting transfer history. +- `body`: `audio_format` (string|null): Requested or recorded audio container or codec, such as FLAC or MP3.; `audio_lossless` (boolean|null): Whether the recorded audio result is lossless.; `bit_depth` (integer|null): Recorded audio bit depth in bits.; `bitrate` (integer|null): Recorded audio bitrate in bits per second.; `category` (string|null): MoviePilot media category or filter-group category, depending on the operation.; `date` (string|null): Record creation or completion timestamp used by the history item.; `dest` (string|null): Organized destination path recorded in transfer history.; `dest_fileitem` (JsonData-Input|null): Serialized destination storage item recorded by the transfer.; `dest_storage` (string|null): Configured storage name containing the organized destination.; `download_hash` (string|null): Provider-native torrent hash associated with the record.; `episode_group` (string|null): TMDB episode-group identifier used for alternate episode ordering.; `episodes` (string|null): Episode-number expression recorded in history, such as E01-E03.; `errmsg` (string|null): Error message recorded for a failed transfer.; `files` (JsonData-Input|null): Serialized list of files recorded by the history item.; `id*` (integer): Persistent database identifier of the supplied record.; `image` (string|null): Image URL stored with the history record.; `media_id` (string|null): Source-native media ID. Always pair it with the exact media_source returned by search.; `media_source` (MediaSource|null): Metadata source identifier. Preserve the exact value returned with media_id.; `mode` (string|null): Operation mode; music.explore accepts chart or fresh, while transfer history records move, copy, link, or softlink.; `music_type` (string|null): Music identity level: recording, album, or artist where supported.; `sample_rate` (integer|null): Recorded audio sample rate in hertz.; `seasons` (string|null): Season-number expression recorded in history.; `src` (string|null): Source path recorded in transfer history.; `src_fileitem` (JsonData-Input|null): Serialized source storage item recorded by the transfer.; `src_storage` (string|null): Configured storage name containing the transfer source.; `status` (boolean; default `True`): Transfer success status used to filter history or describe a record.; `title` (string|null): Media, torrent, subscription, or history title used by the operation.; `total_tracks` (integer|null): Expected or recorded track count for a music item.; `transfer_task_id` (string|null): Stable durable transfer-task ID associated with the history record.; `type` (string|null): MoviePilot media or storage item type required by the selected operation.; `year` (string|null): Release or premiere year used to disambiguate the media title. + +### `transfer.history.redo` +`POST /api/v1/history/transfer/{history_id}/ai-redo`; policy effect: `external_side_effect`. +Purpose: Start AI-assisted reorganization for one transfer-history record. +- `path_params`: `history_id*` (integer): Persistent transfer- or subscription-history ID returned by a history operation. +- `query`: none +- `body`: none + +### `transfer.history.redo_batch` +`POST /api/v1/history/transfer/ai-redo`; policy effect: `external_side_effect`. +Purpose: Start AI-assisted reorganization for an explicit list of transfer-history records. +- `path_params`: none +- `query`: none +- `body`: `history_ids` (array): Explicit persistent transfer-history IDs included in one batch redo request. + +### `transfer.manual_history` +`POST /api/v1/transfer/manual/history`; policy effect: `safe_read`. +Purpose: Check whether supplied storage items already have successful transfer history. +- `path_params`: none +- `query`: none +- `body`: `episode_detail` (string|null): Episode mapping details used by manual transfer.; `episode_format` (string|null): Episode-number formatting rule used by manual transfer.; `episode_group` (string|null): TMDB episode-group identifier used for alternate episode ordering.; `episode_offset` (string|null): Integer offset added to detected episode numbers.; `episode_part` (string|null): Episode part number used when one episode is split across files.; `fileitem` (FileItem-Input): One complete source storage item returned by storage.list.; `fileitems` (array|null): Additional source storage items included in the same manual transfer.; `from_history` (boolean|null; default `False`): Treat the transfer input as originating from an existing history record.; `library_category_folder` (boolean|null): Create or use a category-level folder in the target library.; `library_type_folder` (boolean|null): Create or use a media-type folder in the target library.; `logid` (integer|null): One download-history or transfer-log identifier used by manual transfer.; `logids` (array|null): Multiple download-history or transfer-log identifiers included in manual transfer.; `media_id` (string|null): Source-native media ID. Always pair it with the exact media_source returned by search.; `media_source` (MediaSource|null): Metadata source identifier. Preserve the exact value returned with media_id.; `min_filesize` (integer|null; default `0`): Minimum source file size accepted by manual transfer, in bytes.; `music_type` (string(recording,album)|null): Music identity level: recording, album, or artist where supported.; `preview` (boolean|null; default `False`): Validate and preview manual-transfer output without committing file changes.; `reorganize` (boolean|null; default `False`): Allow manual transfer to organize an item that was already processed.; `scrape` (boolean|null; default `False`): Generate metadata and images after manual transfer.; `season` (integer|null): Season number used by the media, search, subscription, or transfer operation.; `target_path` (string|null): Destination path used by manual transfer.; `target_storage` (string|null): Configured storage name receiving the manual transfer.; `transfer_type` (string|null): Manual-transfer mode, such as move, copy, link, or softlink.; `type_name` (string|null): Explicit media type name used when source IDs alone are ambiguous. + +### `transfer.manual_review` +`GET /api/v1/transfer/tasks/{task_id}/manual-review`; policy effect: `safe_read`. +Purpose: Read one durable transfer task awaiting manual review. +- `path_params`: `task_id*` (string): Stable durable transfer task ID returned by transfer.manual_reviews. +- `query`: none +- `body`: none + +### `transfer.manual_review.resolve` +`POST /api/v1/transfer/tasks/{task_id}/manual-review`; policy effect: `reversible_write`. +Purpose: Record the authorized decision for one durable transfer manual-review operation. +- `path_params`: `task_id*` (string): Stable durable transfer task ID returned by transfer.manual_reviews. +- `query`: none +- `body`: `decision*` (string(not_applied,applied)): Manual-review decision selected from the endpoint's declared enum.; `operation_id*` (string; minimum length `1`): Exact allowlisted MoviePilot operation ID selecting this oneOf branch.; `reason*` (string; minimum length `1`): Human-readable justification recorded with a manual-review decision.; `result_payload` (object|null): Structured external-operation result recorded with manual review. + +### `transfer.manual_reviews` +`GET /api/v1/transfer/tasks/manual-reviews`; policy effect: `safe_read`. +Purpose: Page durable transfer tasks awaiting manual review or retry recovery. +- `path_params`: none +- `query`: `page` (integer; default `1`; minimum `1`): One-based result page number.; `page_size` (integer; default `30`; minimum `1`; maximum `100`): Maximum records returned on one page.; `state` (string(manual_review,retry_wait); default `manual_review`): Current site, subscription, marketplace, or transfer state filter. +- `body`: none + +### `transfer.name` +`GET /api/v1/transfer/name`; policy effect: `safe_read`. +Purpose: Preview the organized destination name for one source path and media identity. +- `path_params`: none +- `query`: `filetype*` (string): Media file type used to preview the organized destination name.; `path*` (string): Storage or history path represented by this record. +- `body`: none + +### `transfer.queue` +`GET /api/v1/transfer/queue`; policy effect: `safe_read`. +Purpose: List items waiting in the file-transfer queue. +- `path_params`: none +- `query`: none +- `body`: none + +### `transfer.queue.delete` +`DELETE /api/v1/transfer/queue`; policy effect: `destructive_write`. +Purpose: Remove one exact storage item from the file-transfer queue and stop its transfer. +- `path_params`: none +- `query`: none +- `body`: `basename` (string|null): Base filename without its parent path.; `children` (array|null): Child storage items nested below this item.; `drive_id` (string|null): Provider-native storage drive identifier.; `extension` (string|null): Filename extension, including or excluding the leading dot as returned by storage.; `fileid` (string|null): Provider-native storage item identifier.; `modify_time` (number|null): Storage item modification timestamp.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `parent_fileid` (string|null): Provider-native identifier of the parent storage directory.; `path` (string|null; default `/`): Storage or history path represented by this record.; `pickcode` (string|null): 115 storage pickcode associated with the item.; `size` (integer|null): File or torrent size in bytes.; `storage` (string|null; default `local`): Configured storage name or storage type used by the operation.; `thumbnail` (string|null): Thumbnail URL returned by the storage provider.; `type` (string|null): MoviePilot media or storage item type required by the selected operation.; `url` (string|null): Site, storage, or torrent URL represented by this field. + +### `transfer.target_path` +`POST /api/v1/transfer/manual/target-path`; policy effect: `safe_read`. +Purpose: Resolve the configured transfer destination for supplied source storage items. +- `path_params`: none +- `query`: none +- `body`: `episode_detail` (string|null): Episode mapping details used by manual transfer.; `episode_format` (string|null): Episode-number formatting rule used by manual transfer.; `episode_group` (string|null): TMDB episode-group identifier used for alternate episode ordering.; `episode_offset` (string|null): Integer offset added to detected episode numbers.; `episode_part` (string|null): Episode part number used when one episode is split across files.; `fileitem` (FileItem-Input): One complete source storage item returned by storage.list.; `fileitems` (array|null): Additional source storage items included in the same manual transfer.; `from_history` (boolean|null; default `False`): Treat the transfer input as originating from an existing history record.; `library_category_folder` (boolean|null): Create or use a category-level folder in the target library.; `library_type_folder` (boolean|null): Create or use a media-type folder in the target library.; `logid` (integer|null): One download-history or transfer-log identifier used by manual transfer.; `logids` (array|null): Multiple download-history or transfer-log identifiers included in manual transfer.; `media_id` (string|null): Source-native media ID. Always pair it with the exact media_source returned by search.; `media_source` (MediaSource|null): Metadata source identifier. Preserve the exact value returned with media_id.; `min_filesize` (integer|null; default `0`): Minimum source file size accepted by manual transfer, in bytes.; `music_type` (string(recording,album)|null): Music identity level: recording, album, or artist where supported.; `preview` (boolean|null; default `False`): Validate and preview manual-transfer output without committing file changes.; `reorganize` (boolean|null; default `False`): Allow manual transfer to organize an item that was already processed.; `scrape` (boolean|null; default `False`): Generate metadata and images after manual transfer.; `season` (integer|null): Season number used by the media, search, subscription, or transfer operation.; `target_path` (string|null): Destination path used by manual transfer.; `target_storage` (string|null): Configured storage name receiving the manual transfer.; `transfer_type` (string|null): Manual-transfer mode, such as move, copy, link, or softlink.; `type_name` (string|null): Explicit media type name used when source IDs alone are ambiguous. + +### `workflow.actions` +`GET /api/v1/workflow/actions`; policy effect: `safe_read`. +Purpose: List built-in workflow action definitions and their parameter contracts. +- `path_params`: none +- `query`: none +- `body`: none + +### `workflow.create` +`POST /api/v1/workflow/`; policy effect: `reversible_write`. +Purpose: Create one workflow from a complete workflow definition. +- `path_params`: none +- `query`: none +- `body`: `actions` (array|null): Ordered workflow action definitions executed by this workflow or flow.; `add_time` (string|null): Timestamp when the workflow definition was created.; `current_action` (string|null): Identifier of the workflow action currently selected or executing.; `description` (string|null): Human-readable media, torrent, or subscription description.; `event_conditions` (object|null): Additional workflow event-filter conditions.; `event_type` (string|null): Exact event type returned by workflow.event_types.; `execution_config` (WorkflowExecutionConfig|null): Workflow runtime limits, concurrency, and failure-policy configuration.; `execution_state` (WorkflowExecutionState-Input|null): Persisted resumable workflow execution state.; `flows` (array|null): Workflow connection definitions linking action nodes.; `id` (integer|null): Persistent database identifier of the supplied record.; `last_time` (string|null): Timestamp of the workflow's most recent execution.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `result` (string|null): Persisted workflow action result value.; `run_count` (integer|null; default `0`): Number of times the workflow has been executed.; `state` (string|null): Current site, subscription, marketplace, or transfer state filter.; `timer` (string|null): Workflow timer or cron expression used for scheduled execution.; `trigger_type` (string|null; default `timer`): Workflow trigger filter: timer, event, manual, or all. + +### `workflow.delete` +`DELETE /api/v1/workflow/{workflow_id}`; policy effect: `destructive_write`. +Purpose: Delete one configured workflow by persistent workflow ID. +- `path_params`: `workflow_id*` (integer): Persistent workflow ID returned by workflow.list. +- `query`: none +- `body`: none + +### `workflow.event_types` +`GET /api/v1/workflow/event_types`; policy effect: `safe_read`. +Purpose: List event types that can trigger workflows. +- `path_params`: none +- `query`: none +- `body`: none + +### `workflow.fork` +`POST /api/v1/workflow/fork`; policy effect: `external_side_effect`. +Purpose: Create a local workflow from one shared workflow definition. +- `path_params`: none +- `query`: none +- `body`: `actions` (string|null): Ordered workflow action definitions executed by this workflow or flow.; `context` (string|null): Persisted workflow execution context available to later actions.; `count` (integer|null; default `0`): Maximum number of records to return on the requested page.; `date` (string|null): Record creation or completion timestamp used by the history item.; `description` (string|null): Human-readable media, torrent, or subscription description.; `event_conditions` (string|null): Additional workflow event-filter conditions.; `event_type` (string|null): Exact event type returned by workflow.event_types.; `flows` (string|null): Workflow connection definitions linking action nodes.; `id` (integer|null): Persistent database identifier of the supplied record.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `share_comment` (string|null): Optional explanatory comment published with a shared item.; `share_title` (string|null): Public title used when publishing a subscription or workflow.; `share_uid` (string|null): Exact MoviePilot Server sharing-user ID to follow or unfollow.; `share_user` (string|null): Public contributor name used when publishing a subscription or workflow.; `timer` (string|null): Workflow timer or cron expression used for scheduled execution.; `trigger_type` (string|null): Workflow trigger filter: timer, event, manual, or all. + +### `workflow.get` +`GET /api/v1/workflow/{workflow_id}`; policy effect: `safe_read`. +Purpose: Read one complete configured workflow definition. +- `path_params`: `workflow_id*` (integer): Persistent workflow ID returned by workflow.list. +- `query`: none +- `body`: none + +### `workflow.list` +`GET /api/v1/workflow/agent`; policy effect: `safe_read`. +Purpose: List configured workflows and their execution state. +- `path_params`: none +- `query`: `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `state` (string(W,R,P,S,F,all); default `all`): Current site, subscription, marketplace, or transfer state filter.; `trigger_type` (string(timer,event,manual,all); default `all`): Workflow trigger filter: timer, event, manual, or all. +- `body`: none + +### `workflow.pause` +`POST /api/v1/workflow/{workflow_id}/pause`; policy effect: `reversible_write`. +Purpose: Disable automatic execution of one configured workflow. +- `path_params`: `workflow_id*` (integer): Persistent workflow ID returned by workflow.list. +- `query`: none +- `body`: none + +### `workflow.plugin.actions` +`GET /api/v1/workflow/plugin/actions`; policy effect: `safe_read`. +Purpose: List workflow actions contributed by installed plugins, optionally filtered by plugin ID. +- `path_params`: none +- `query`: `plugin_id` (string): Exact installed or marketplace plugin ID. +- `body`: none + +### `workflow.reset` +`POST /api/v1/workflow/{workflow_id}/reset`; policy effect: `reversible_write`. +Purpose: Reset one configured workflow definition and execution state. +- `path_params`: `workflow_id*` (integer): Persistent workflow ID returned by workflow.list. +- `query`: none +- `body`: none + +### `workflow.run` +`POST /api/v1/workflow/{workflow_id}/run`; policy effect: `external_side_effect`. +Purpose: Run one configured workflow from the beginning or resume point. +- `path_params`: `workflow_id*` (integer): Persistent workflow ID returned by workflow.list. +- `query`: `from_begin` (boolean|null; default `True`): Restart the workflow from its first action instead of resuming progress. +- `body`: none + +### `workflow.share` +`POST /api/v1/workflow/share`; policy effect: `external_side_effect`. +Purpose: Publish one configured workflow to the MoviePilot sharing service. +- `path_params`: none +- `query`: none +- `body`: `actions` (string|null): Ordered workflow action definitions executed by this workflow or flow.; `context` (string|null): Persisted workflow execution context available to later actions.; `count` (integer|null; default `0`): Maximum number of records to return on the requested page.; `date` (string|null): Record creation or completion timestamp used by the history item.; `description` (string|null): Human-readable media, torrent, or subscription description.; `event_conditions` (string|null): Additional workflow event-filter conditions.; `event_type` (string|null): Exact event type returned by workflow.event_types.; `flows` (string|null): Workflow connection definitions linking action nodes.; `id` (integer|null): Persistent database identifier of the supplied record.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `share_comment` (string|null): Optional explanatory comment published with a shared item.; `share_title` (string|null): Public title used when publishing a subscription or workflow.; `share_uid` (string|null): Exact MoviePilot Server sharing-user ID to follow or unfollow.; `share_user` (string|null): Public contributor name used when publishing a subscription or workflow.; `timer` (string|null): Workflow timer or cron expression used for scheduled execution.; `trigger_type` (string|null): Workflow trigger filter: timer, event, manual, or all. + +### `workflow.share.delete` +`DELETE /api/v1/workflow/share/{share_id}`; policy effect: `external_side_effect`. +Purpose: Delete one shared-workflow publication by share ID. +- `path_params`: `share_id*` (integer): Persistent MoviePilot Server share ID returned by a share-list operation. +- `query`: none +- `body`: none + +### `workflow.shares` +`GET /api/v1/workflow/shares`; policy effect: `safe_read`. +Purpose: List shared workflows with name and pagination filters. +- `path_params`: none +- `query`: `count` (integer|null; default `30`): Maximum number of records to return on the requested page.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `page` (integer|null; default `1`): One-based result page number. +- `body`: none + +### `workflow.start` +`POST /api/v1/workflow/{workflow_id}/start`; policy effect: `reversible_write`. +Purpose: Enable automatic execution of one configured workflow. +- `path_params`: `workflow_id*` (integer): Persistent workflow ID returned by workflow.list. +- `query`: none +- `body`: none + +### `workflow.update` +`PUT /api/v1/workflow/{workflow_id}`; policy effect: `reversible_write`. +Purpose: Replace one configured workflow definition. +- `path_params`: `workflow_id*` (integer): Persistent workflow ID returned by workflow.list. +- `query`: none +- `body`: `actions` (array|null): Ordered workflow action definitions executed by this workflow or flow.; `add_time` (string|null): Timestamp when the workflow definition was created.; `current_action` (string|null): Identifier of the workflow action currently selected or executing.; `description` (string|null): Human-readable media, torrent, or subscription description.; `event_conditions` (object|null): Additional workflow event-filter conditions.; `event_type` (string|null): Exact event type returned by workflow.event_types.; `execution_config` (WorkflowExecutionConfig|null): Workflow runtime limits, concurrency, and failure-policy configuration.; `execution_state` (WorkflowExecutionState-Input|null): Persisted resumable workflow execution state.; `flows` (array|null): Workflow connection definitions linking action nodes.; `id` (integer|null): Persistent database identifier of the supplied record.; `last_time` (string|null): Timestamp of the workflow's most recent execution.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `result` (string|null): Persisted workflow action result value.; `run_count` (integer|null; default `0`): Number of times the workflow has been executed.; `state` (string|null): Current site, subscription, marketplace, or transfer state filter.; `timer` (string|null): Workflow timer or cron expression used for scheduled execution.; `trigger_type` (string|null; default `timer`): Workflow trigger filter: timer, event, manual, or all. + +### Referenced Body Models + +#### `FileItem-Input` +One file or directory returned by a configured storage provider. +- `basename` (string|null): Base filename without its parent path. +- `children` (array|null): Child storage items nested below this item. +- `drive_id` (string|null): Provider-native storage drive identifier. +- `extension` (string|null): Filename extension, including or excluding the leading dot as returned by storage. +- `fileid` (string|null): Provider-native storage item identifier. +- `modify_time` (number|null): Storage item modification timestamp. +- `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group. +- `parent_fileid` (string|null): Provider-native identifier of the parent storage directory. +- `path` (string|null; default `/`): Storage or history path represented by this record. +- `pickcode` (string|null): 115 storage pickcode associated with the item. +- `size` (integer|null): File or torrent size in bytes. +- `storage` (string|null; default `local`): Configured storage name or storage type used by the operation. +- `thumbnail` (string|null): Thumbnail URL returned by the storage provider. +- `type` (string|null): MoviePilot media or storage item type required by the selected operation. +- `url` (string|null): Site, storage, or torrent URL represented by this field. + +#### `JsonData-Input` +Arbitrary JSON-compatible auxiliary data. +This runtime model has no directly writable fields. + +#### `MediaSource` +Canonical metadata source identifier paired with a source-native media ID. +This runtime model has no directly writable fields. + +#### `MediaType` +MoviePilot media type. +This runtime model has no directly writable fields. + +#### `TorrentInfo` +One torrent candidate returned by MoviePilot search. +- `category` (string|null): MoviePilot media category or filter-group category, depending on the operation. +- `date_elapsed` (string|null): Human-readable age of the torrent publication date. +- `description` (string|null): Human-readable media, torrent, or subscription description. +- `downloadvolumefactor` (number|null): Torrent download-volume multiplier reported by the site. +- `enclosure` (string|null): Torrent download URL or enclosure supplied by the indexer result. +- `freedate` (string|null): Torrent freeleech expiration timestamp reported by the site. +- `freedate_diff` (string|null): Seconds remaining until the torrent freeleech period ends. +- `grabs` (integer|null; default `0`): Number of completed downloads reported for the torrent. +- `hit_and_run` (boolean|null; default `False`): Whether the torrent is subject to hit-and-run requirements. +- `labels` (array|null): Torrent labels supplied by the site result. +- `media_id` (string|null): Source-native media ID. Always pair it with the exact media_source returned by search. +- `media_source` (MediaSource|null): Metadata source identifier. Preserve the exact value returned with media_id. +- `page_url` (string|null): Public details page for the torrent result. +- `peers` (integer|null; default `0`): Number of downloading peers reported for the torrent. +- `pri_order` (integer|null; default `0`): Indexer priority order assigned to the torrent result. +- `pubdate` (string|null): Torrent publication timestamp. +- `seeders` (integer|null; default `0`): Minimum seeder expression for a filter rule, or the torrent's seeder count. +- `site` (integer|null): Source site identifier associated with the torrent result. +- `site_cookie` (string|null): Site cookie bundled with the torrent result. Treat this value as a secret. +- `site_downloader` (string|null): Downloader instance selected by the source site. +- `site_name` (string|null): Human-readable source site name. +- `site_order` (integer|null; default `0`): Source site's configured search order. +- `site_proxy` (boolean|null; default `False`): Whether the torrent's source site uses the configured proxy. +- `site_ua` (string|null): User-Agent associated with the source site. +- `size` (number|null; default `0.0`): File or torrent size in bytes. +- `title` (string|null): Media, torrent, subscription, or history title used by the operation. +- `uploadvolumefactor` (number|null): Torrent upload-volume multiplier reported by the site. +- `volume_factor` (string|null): Combined upload/download volume-factor label shown for the torrent. + +#### `WorkflowExecutionConfig` +Workflow concurrency, join, branch, and failure policies. +- `max_workers` (integer|null): Maximum concurrent workflow actions allowed by the execution configuration. + +#### `WorkflowExecutionState-Input` +Persisted resumable workflow execution state. +- `errors` (object): Workflow execution errors keyed or ordered by action identity. +- `nodes` (object): Persisted workflow node runtime states keyed by action identity. +- `outputs` (object): Named output mappings produced by this workflow action. +- `runtime` (WorkflowRuntimeState): Persisted workflow runtime metadata used for safe resume. +- `version` (integer; default `1`): Plugin release or schema version selected by the operation. + +#### `WorkflowRuntimeState` +Complete persisted workflow runtime and progress state. +- `attempts` (object): Attempt counters keyed by workflow node or operation identity. +- `errors` (object): Workflow execution errors keyed or ordered by action identity. +- `finished_actions` (integer; default `0`): Workflow action IDs already completed in the persisted execution state. +- `node_states` (object): Persisted runtime states keyed by workflow node identity. +- `progress` (integer; default `0`): Current numeric or structured workflow execution progress. +- `running_tasks` (integer; default `0`): Workflow task IDs currently executing. + +## System Settings Contract + +Do not enumerate setting keys in this Skill. Settings change as MoviePilot evolves, so use `config.system.get` as the runtime discovery operation before updating an unfamiliar key. + +| `source` | Contents | Persistence | +| :--- | :--- | :--- | +| `settings` | Runtime `Settings` fields such as APP_DOMAIN or LLM_MODEL | Type-converted and persisted to `app.env`, then applied to the current process | +| `systemconfig` | Database-backed business configuration such as downloaders, media servers, directories, and notifications | Written through the configuration service with plugin admission and change events | + +The `systemconfig` database table is only the physical store for the second source. Use `config.system.get/update` for normal reads and writes. Direct SQL is reserved for an explicitly authorized repair when the managed API cannot complete the operation. + +### Discover definitions + +1. Call `config.system.get` with `query={"group":"settings","keyword":"LLM"}` or another group/keyword. Discovery defaults to summaries instead of full values. +2. Each returned setting includes `setting_key`, `source`, `group`, `label`, and a `definition` object with `declared_type`, current `value_shape`, `nullable`, `sensitive`, allowed `update_operations`, `default_match_field`, and `persistence`. +3. Read one exact value with `query={"setting_key":"LLM_MODEL"}`. Exact-key reads include the value by default. +4. Use `show_secrets=true` only when an administrator explicitly requests the plaintext value; secret reads remain confirmation-protected. + +### Update settings + +Choose an operation listed in the discovered setting definition, then send it in `body`: + +| operation | Fields | Meaning | +| :--- | :--- | :--- | +| `replace` | `setting_key*`, `value` | Replace the complete scalar, list, or object value | +| `merge_dict` | `setting_key*`, `value`; optional `remove_keys` | Shallow-merge an object and optionally remove keys | +| `upsert_list_item` | `setting_key*`, `value`; optional `match_field`, `match_value` | Replace a matched list item or append it when absent | +| `remove_list_item` | `setting_key*`, `value` or `match_value`; optional `match_field` | Remove one matched list item without replacing the list | + +After every update, call `config.system.get` again with the exact setting_key and verify the saved value. Do not guess a key, value shape, list match field, or update operation when discovery can return it. + +## Operation Order And Failure Handling + +1. Select the operation first, then place each value in its documented bucket. Never move query fields into path_params or send undeclared fields. +2. Reuse the exact `media_source` + `media_id` pair returned by search. For music, also preserve `music_type`. +3. Downloads, transfers, configuration/rule/plugin writes, scheduler/workflow runs, and deletions have side effects; obtain confirmation and inspect the result. +4. `success=false`, HTTP errors, validation errors, and empty results are real outcomes. Never report them as success. +5. Use `database-operation`, `downloader-operation`, or `mediaserver-operation` for their native capabilities. Never bypass the gateway with an arbitrary URL. diff --git a/skills/moviepilot-api/scripts/mp-api.py b/skills/moviepilot-api/scripts/mp-api.py deleted file mode 100644 index af965e000..000000000 --- a/skills/moviepilot-api/scripts/mp-api.py +++ /dev/null @@ -1,371 +0,0 @@ -#!/usr/bin/env python3 -""" -MoviePilot REST API CLI -- a lightweight command-line client for calling -any MoviePilot API endpoint directly. - -Usage: - python mp-api.py configure --host --apikey - python mp-api.py GET /api/v1/media/search title="Avatar" type="movie" - python mp-api.py POST /api/v1/download/add --json '{"torrent_url":"..."}' - python mp-api.py DELETE /api/v1/subscribe/123 - -Authentication: - The script sends the API key via the ``X-API-KEY`` header. - It can also fall back to ``?token=`` for endpoints that require it. - -Configuration priority: - CLI flags > Environment variables > local MoviePilot settings > Config file - -Config file location: ~/.config/moviepilot_api/config -""" - -from __future__ import annotations - -import json -import os -import ssl -import stat -import sys -import urllib.error -import urllib.parse -import urllib.request -from pathlib import Path - -SCRIPT_NAME = os.path.basename(sys.argv[0]) if sys.argv else "mp-api.py" -SCRIPT_PATH = Path(__file__).resolve() -PROJECT_ROOT = SCRIPT_PATH.parents[3] -CONFIG_DIR = Path.home() / ".config" / "moviepilot_api" -CONFIG_FILE = CONFIG_DIR / "config" -LOCAL_HOSTS = {"0.0.0.0", "::", "::1", "", "localhost"} - -# --------------------------------------------------------------------------- -# Configuration helpers -# --------------------------------------------------------------------------- - - -def read_config() -> tuple[str, str]: - """Return (host, apikey) from the config file.""" - host = "" - apikey = "" - if not CONFIG_FILE.exists(): - return host, apikey - for line in CONFIG_FILE.read_text(encoding="utf-8").splitlines(): - line = line.strip() - if not line or line.startswith("#"): - continue - if "=" not in line: - continue - key, _, value = line.partition("=") - key = key.strip() - value = value.strip() - if key == "MP_HOST": - host = value - elif key == "MP_API_KEY": - apikey = value - return host, apikey - - -def save_config(host: str, apikey: str) -> None: - """Persist host and API key to the legacy config file.""" - CONFIG_DIR.mkdir(parents=True, exist_ok=True) - CONFIG_FILE.write_text(f"MP_HOST={host}\nMP_API_KEY={apikey}\n", encoding="utf-8") - CONFIG_FILE.chmod(stat.S_IRUSR | stat.S_IWUSR) - - -def _ensure_project_import() -> None: - """Add the MoviePilot project root to sys.path for local auto-configuration.""" - project_path = str(PROJECT_ROOT) - if project_path not in sys.path: - sys.path.insert(0, project_path) - - -def _client_host(host: str) -> str: - """Return a loopback host usable by local clients.""" - host = (host or "").strip() - if host in LOCAL_HOSTS: - return "127.0.0.1" - return host - - -def read_local_config() -> tuple[str, str]: - """Return host and key from local MoviePilot settings when available.""" - try: - _ensure_project_import() - from app.runtime.config import settings # pylint: disable=import-outside-toplevel - except Exception: - return "", "" - - host = str(settings.HOST or "") - port = settings.PORT - apikey = str(settings.API_TOKEN or "") - if host and port: - return f"http://{_client_host(host)}:{port}", apikey - - return "", apikey - - -def resolve_config( - cli_host: str = "", - cli_key: str = "", -) -> tuple[str, str]: - """Resolve effective host and key without requiring prompt-visible secrets.""" - local_host, local_key = read_local_config() - cfg_host, cfg_key = read_config() - host = cli_host or os.environ.get("MP_HOST", "") or local_host or cfg_host - apikey = cli_key or os.environ.get("MP_API_KEY", "") or local_key or cfg_key - return host, apikey - - -# --------------------------------------------------------------------------- -# HTTP helpers -# --------------------------------------------------------------------------- - -# Allow self-signed certs (common in home-lab setups) -_SSL_CTX = ssl.create_default_context() -_SSL_CTX.check_hostname = False -_SSL_CTX.verify_mode = ssl.CERT_NONE - - -def http_request( - method: str, - url: str, - headers: dict[str, str] | None = None, - body: bytes | None = None, - timeout: int = 120, -) -> tuple[int, str]: - """Perform an HTTP request and return (status_code, response_body).""" - headers = headers or {} - req = urllib.request.Request(url, data=body, headers=headers, method=method) - try: - with urllib.request.urlopen(req, timeout=timeout, context=_SSL_CTX) as resp: - return resp.status, resp.read().decode("utf-8", errors="replace") - except urllib.error.HTTPError as exc: - return exc.code, exc.read().decode("utf-8", errors="replace") - except urllib.error.URLError as exc: - return 0, f"Connection error: {exc.reason}" - - -def build_url(host: str, path: str, query_params: dict[str, str] | None = None) -> str: - """Build a full URL from host + path + optional query parameters.""" - base = host.rstrip("/") - if not path.startswith("/"): - path = "/" + path - url = base + path - if query_params: - url += "?" + urllib.parse.urlencode(query_params) - return url - - -# --------------------------------------------------------------------------- -# Core API call -# --------------------------------------------------------------------------- - - -def api_call( - host: str, - apikey: str, - method: str, - path: str, - query_params: dict[str, str] | None = None, - json_body: object | None = None, - use_token_param: bool = False, - timeout: int = 120, -) -> tuple[int, object]: - """ - Call a MoviePilot REST API endpoint. - - Parameters - ---------- - host : str - MoviePilot base URL (e.g. ``http://localhost:3000``). - apikey : str - The API key (``settings.API_TOKEN`` value). - method : str - HTTP method: GET, POST, PUT, DELETE. - path : str - API path (e.g. ``/api/v1/media/search``). - query_params : dict, optional - Additional query-string parameters. - json_body : object, optional - A JSON-serialisable body for POST/PUT requests. - use_token_param : bool - If True, send the key as ``?token=`` instead of the header. - timeout : int - Request timeout in seconds. - - Returns - ------- - (status_code, parsed_json_or_text) - """ - headers: dict[str, str] = {} - qp = dict(query_params or {}) - - if use_token_param: - qp["token"] = apikey - else: - headers["X-API-KEY"] = apikey - - body_bytes: bytes | None = None - if json_body is not None: - headers["Content-Type"] = "application/json" - body_bytes = json.dumps(json_body, ensure_ascii=False).encode("utf-8") - - url = build_url(host, path, qp if qp else None) - status, raw = http_request(method, url, headers, body_bytes, timeout) - - # Try to parse JSON - try: - data = json.loads(raw) - except (json.JSONDecodeError, ValueError): - data = raw - return status, data - - -# --------------------------------------------------------------------------- -# CLI -# --------------------------------------------------------------------------- - - -def print_json(obj: object) -> None: - """Pretty-print a JSON-serialisable object to stdout.""" - if isinstance(obj, str): - print(obj) - else: - print(json.dumps(obj, indent=2, ensure_ascii=False)) - - -def print_usage() -> None: - print(f"""Usage: python {SCRIPT_NAME} [options] [key=value ...] [--json ''] - python {SCRIPT_NAME} configure --host --apikey # legacy fallback - -Options: - --host HOST MoviePilot backend URL (auto-read locally when omitted) - --apikey KEY API key (auto-read locally when omitted) - --token-param Send key as ?token= query param instead of X-API-KEY header - --timeout SECS Request timeout (default: 120) - --help Show this help message - -Methods: GET POST PUT DELETE - -Examples: - python {SCRIPT_NAME} GET /api/v1/media/search title="Avatar" type="movie" - python {SCRIPT_NAME} GET /api/v1/subscribe/ - python {SCRIPT_NAME} POST /api/v1/download/add --json '{{"torrent_url":"abc:1"}}' - python {SCRIPT_NAME} DELETE /api/v1/subscribe/123 - python {SCRIPT_NAME} GET /api/v1/dashboard/statistic2 --token-param -""") - - -def main() -> None: - argv = sys.argv[1:] - if not argv or "--help" in argv or "-h" in argv: - print_usage() - sys.exit(0) - - # Parse options - cli_host = "" - cli_key = "" - use_token_param = False - timeout = 120 - positional: list[str] = [] - json_body_str: str | None = None - - i = 0 - while i < len(argv): - arg = argv[i] - if arg == "--host": - i += 1 - cli_host = argv[i] if i < len(argv) else "" - elif arg == "--apikey": - i += 1 - cli_key = argv[i] if i < len(argv) else "" - elif arg == "--token-param": - use_token_param = True - elif arg == "--timeout": - i += 1 - timeout = int(argv[i]) if i < len(argv) else 120 - elif arg == "--json": - i += 1 - json_body_str = argv[i] if i < len(argv) else "{}" - else: - positional.append(arg) - i += 1 - - # Sub-command: configure - if positional and positional[0].lower() == "configure": - if not cli_host and not cli_key: - print( - "Error: --host and --apikey are required for configure", file=sys.stderr - ) - sys.exit(1) - cfg_host, cfg_key = read_config() - save_config(cli_host or cfg_host, cli_key or cfg_key) - print("Configuration saved.") - sys.exit(0) - - # Normal API call - if len(positional) < 2: - print("Error: expected ", file=sys.stderr) - print_usage() - sys.exit(1) - - method = positional[0].upper() - path = positional[1] - - # Remaining positional args are key=value query params - query_params: dict[str, str] = {} - for kv in positional[2:]: - if "=" in kv: - k, _, v = kv.partition("=") - query_params[k] = v - else: - print(f"Warning: ignoring argument without '=': {kv}", file=sys.stderr) - - # Parse JSON body - json_body = None - if json_body_str: - try: - json_body = json.loads(json_body_str) - except json.JSONDecodeError as exc: - print(f"Error: invalid JSON body: {exc}", file=sys.stderr) - sys.exit(1) - - # Resolve config - host, apikey = resolve_config(cli_host, cli_key) - if not host: - print("Error: backend host is not configured.", file=sys.stderr) - print(" Use: --host HOST or set MP_HOST environment variable", file=sys.stderr) - sys.exit(1) - if not apikey: - print("Error: API key is not configured.", file=sys.stderr) - print( - " Use: --apikey KEY or set MP_API_KEY environment variable", - file=sys.stderr, - ) - sys.exit(1) - - # Persist if CLI flags provided - if cli_host or cli_key: - save_config(host, apikey) - - status, data = api_call( - host=host, - apikey=apikey, - method=method, - path=path, - query_params=query_params if query_params else None, - json_body=json_body, - use_token_param=use_token_param, - timeout=timeout, - ) - - if status and status not in (200, 201): - print(f"HTTP {status}", file=sys.stderr) - - print_json(data) - if status and status >= 400: - sys.exit(1) - - -if __name__ == "__main__": - main() diff --git a/skills/moviepilot-update/SKILL.md b/skills/moviepilot-update/SKILL.md index ea07a2027..60c6cb218 100644 --- a/skills/moviepilot-update/SKILL.md +++ b/skills/moviepilot-update/SKILL.md @@ -1,81 +1,70 @@ --- name: moviepilot-update -version: 4 +version: 5 description: Use this skill to check MoviePilot versions, inspect Release update state, download a Release update in the background, confirm installation, restart MoviePilot, or retain the existing Dev branch update flow. Prefer the built-in system APIs instead of container commands or manual file replacement. +allowed-tools: moviepilot_api +allowed-api-operations: >- + system.versions system.update.status system.update.check system.update.download + system.restart system.update.install system.upgrade.dev --- # MoviePilot Update -> All script paths are relative to this skill file. - Use this skill for MoviePilot restart and upgrade operations. -## Setup +Use the built-in `moviepilot_api` tool only. The host selects fixed API routes, authenticates with the trusted Agent identity, and applies administrator and confirmation policy. Never request or pass an API token, URL, HTTP method, shell command, or legacy helper script. -This skill reuses the `moviepilot-api` client. When running inside the MoviePilot project, the API client imports `app.runtime.config.settings` and reads the local host, port, and API token directly. Do not ask the user for `API_TOKEN`. - -## Preferred Commands +## Operations ### Check versions -```bash -python scripts/mp-update.py versions +```json +{"operation_id":"system.versions","path_params":{},"query":{},"body":{}} ``` -This calls `GET /api/v1/system/versions`. +This read-only operation lists available MoviePilot releases. ### Restart MoviePilot -```bash -python scripts/mp-update.py restart +```json +{"operation_id":"system.restart","path_params":{},"query":{},"body":{}} ``` -This calls `GET /api/v1/system/restart`. +Restart requires explicit confirmation and interrupts the current Agent session. ### Release update Check for a stable Release and inspect current progress: -```bash -python scripts/mp-update.py check -python scripts/mp-update.py status +```json +{"operation_id":"system.update.check","path_params":{},"query":{},"body":{}} +{"operation_id":"system.update.status","path_params":{},"query":{},"body":{}} ``` Start the background download. This does not restart MoviePilot: -```bash -python scripts/mp-update.py download +```json +{"operation_id":"system.update.download","path_params":{},"query":{},"body":{}} ``` After `status` reports `state=ready`, installation requires a separate explicit confirmation: -```bash -python scripts/mp-update.py install +```json +{"operation_id":"system.update.install","path_params":{},"query":{},"body":{}} ``` `install` writes the verified install intent and restarts MoviePilot. Do not call it until the user explicitly confirms the restart. ### Dev update and restart -```bash -python scripts/mp-update.py upgrade dev +```json +{"operation_id":"system.upgrade.dev","path_params":{},"query":{},"body":"dev"} ``` -Dev mode retains the existing `POST /api/v1/system/upgrade` path with body `"dev"`. It tracks the current v3 development branch during restart. Release mode is no longer accepted by that endpoint. - -## Direct API Examples - -```bash -python ../moviepilot-api/scripts/mp-api.py GET /api/v1/system/restart -python ../moviepilot-api/scripts/mp-api.py POST /api/v1/system/update/check -python ../moviepilot-api/scripts/mp-api.py GET /api/v1/system/update/status -python ../moviepilot-api/scripts/mp-api.py POST /api/v1/system/update/download -python ../moviepilot-api/scripts/mp-api.py POST /api/v1/system/update/install -python ../moviepilot-api/scripts/mp-api.py POST /api/v1/system/upgrade --json '"dev"' -``` +The body must be the exact JSON string `"dev"`. Stable Release updates must use check, download, status, and install instead. ## Notes -- These operations require administrator authentication. +- All operations require a MoviePilot administrator or a verified notification-channel administrator. The host performs authorization; the model must never invent an administrator flag. - Only restart, Release installation, and Dev upgrade interrupt the current agent session. Checking and downloading remain online. - Prefer the API flow above. Only fall back to manual container commands when the API is unavailable. diff --git a/skills/moviepilot-update/scripts/mp-update.py b/skills/moviepilot-update/scripts/mp-update.py deleted file mode 100644 index db3d608c6..000000000 --- a/skills/moviepilot-update/scripts/mp-update.py +++ /dev/null @@ -1,77 +0,0 @@ -#!/usr/bin/env python3 - -from __future__ import annotations - -import json -import sys -from pathlib import Path - - -SCRIPT_DIR = Path(__file__).resolve().parent -API_SCRIPT = SCRIPT_DIR.parents[1] / "moviepilot-api" / "scripts" / "mp-api.py" - - -def run_api_call(args: list[str]) -> int: - """调用 MoviePilot REST API 客户端执行更新相关接口。""" - command = [sys.executable, str(API_SCRIPT), *args] - return_code = __import__("subprocess").run(command, check=False).returncode - return return_code - - -def print_usage() -> None: - """输出更新脚本的命令行用法。""" - print( - "Usage:\n" - f" python {Path(sys.argv[0]).name} versions\n" - f" python {Path(sys.argv[0]).name} status\n" - f" python {Path(sys.argv[0]).name} check\n" - f" python {Path(sys.argv[0]).name} download\n" - f" python {Path(sys.argv[0]).name} install\n" - f" python {Path(sys.argv[0]).name} restart\n" - f" python {Path(sys.argv[0]).name} upgrade dev" - ) - - -def main() -> int: - """执行 MoviePilot 更新脚本入口。""" - argv = sys.argv[1:] - if not argv or argv[0] in {"-h", "--help", "help"}: - print_usage() - return 0 - - command = argv[0].lower() - if command == "versions": - return run_api_call(["GET", "/api/v1/system/versions"]) - - if command == "restart": - return run_api_call(["GET", "/api/v1/system/restart"]) - - update_commands = { - "status": ("GET", "/api/v1/system/update/status"), - "check": ("POST", "/api/v1/system/update/check"), - "download": ("POST", "/api/v1/system/update/download"), - "install": ("POST", "/api/v1/system/update/install"), - } - if command in update_commands: - method, path = update_commands[command] - return run_api_call([method, path]) - - if command == "upgrade": - mode = (argv[1] if len(argv) > 1 else "").strip().lower() - if mode != "dev": - print("Error: only Dev uses upgrade; use check/download/install for Release", file=sys.stderr) - return 1 - return run_api_call([ - "POST", - "/api/v1/system/upgrade", - "--json", - json.dumps(mode, ensure_ascii=False), - ]) - - print(f"Error: unknown command: {command}", file=sys.stderr) - print_usage() - return 1 - - -if __name__ == "__main__": - sys.exit(main()) diff --git a/tests/fixtures/architecture/dependency-baseline.json b/tests/fixtures/architecture/dependency-baseline.json index 9239bec3b..05b25109c 100644 --- a/tests/fixtures/architecture/dependency-baseline.json +++ b/tests/fixtures/architecture/dependency-baseline.json @@ -1089,8 +1089,8 @@ "runtime_only": true } }, - "edge_count": 7671, - "edge_sha256": "cf48bcc1cf83bb9a70be9d15fd0d5a094cb63016178ce0f5f95cb6f1b1c5e654", + "edge_count": 7680, + "edge_sha256": "e587b2c539a0100a96f73ec7daf1634671e2570e9c89114774fdcd26b9072aac", "edges": [ "app -> app.foundation", "app -> app.foundation.environment", @@ -1692,6 +1692,7 @@ "app.agent.tools.factory -> app.agent.tools.impl.send_local_file", "app.agent.tools.factory -> app.agent.tools.impl.send_message", "app.agent.tools.factory -> app.agent.tools.impl.send_voice_message", + "app.agent.tools.factory -> app.agent.tools.impl.service", "app.agent.tools.factory -> app.agent.tools.impl.write_file", "app.agent.tools.factory -> app.application", "app.agent.tools.factory -> app.application.agent", @@ -1724,6 +1725,7 @@ "app.agent.tools.impl.api -> app.agent.api.executor", "app.agent.tools.impl.api -> app.agent.policy", "app.agent.tools.impl.api -> app.agent.policy.api", + "app.agent.tools.impl.api -> app.agent.policy.contracts", "app.agent.tools.impl.api -> app.agent.tools", "app.agent.tools.impl.api -> app.agent.tools.base", "app.agent.tools.impl.api -> app.agent.tools.tags", @@ -1853,6 +1855,12 @@ "app.agent.tools.impl.send_voice_message -> app.runtime.settings", "app.agent.tools.impl.send_voice_message -> app.schemas", "app.agent.tools.impl.send_voice_message -> app.schemas.message", + "app.agent.tools.impl.service -> app.agent", + "app.agent.tools.impl.service -> app.agent.tools", + "app.agent.tools.impl.service -> app.agent.tools.base", + "app.agent.tools.impl.service -> app.agent.tools.tags", + "app.agent.tools.impl.service -> app.runtime", + "app.agent.tools.impl.service -> app.runtime.settings", "app.agent.tools.impl.write_file -> app.agent", "app.agent.tools.impl.write_file -> app.agent.tools", "app.agent.tools.impl.write_file -> app.agent.tools.base", @@ -2822,6 +2830,7 @@ "app.api.endpoints.workflow -> app.chain", "app.api.endpoints.workflow -> app.chain.workflow", "app.api.endpoints.workflow -> app.schemas", + "app.api.endpoints.workflow -> app.schemas.common", "app.api.endpoints.workflow -> app.schemas.response", "app.api.endpoints.workflow -> app.schemas.types", "app.api.endpoints.workflow -> app.schemas.workflow", @@ -8764,7 +8773,7 @@ "app.workflow.actions.transfer_file -> app.workflow", "app.workflow.actions.transfer_file -> app.workflow.actions" ], - "module_count": 917, + "module_count": 919, "modules": [ "app", "app.adapters", @@ -8856,6 +8865,7 @@ "app.agent.policy", "app.agent.policy.api", "app.agent.policy.contracts", + "app.agent.policy.mcp", "app.agent.policy.orchestrator", "app.agent.policy.registry", "app.agent.policy.sanitizer", @@ -8891,6 +8901,7 @@ "app.agent.tools.impl.send_local_file", "app.agent.tools.impl.send_message", "app.agent.tools.impl.send_voice_message", + "app.agent.tools.impl.service", "app.agent.tools.impl.write_file", "app.agent.tools.manager", "app.agent.tools.tags", diff --git a/tests/test_agent_api_gateway.py b/tests/test_agent_api_gateway.py index e281aa386..2d6cec3a2 100644 --- a/tests/test_agent_api_gateway.py +++ b/tests/test_agent_api_gateway.py @@ -1,5 +1,6 @@ import asyncio import json +import re from types import SimpleNamespace from unittest.mock import AsyncMock, patch @@ -11,30 +12,252 @@ from app.agent.policy import ( PrincipalRole, ) from app.agent.policy.api import ( + API_EXTENDED_OPERATION_SPECS, API_FIRST_BATCH_OPERATION_SPECS, + API_MUSIC_OPERATION_SPECS, API_OPERATION_ROUTES, API_OPERATION_SPECS, API_PARITY_OPERATION_SPECS, + API_SYSTEM_OPERATION_SPECS, ) from app.agent.tools.factory import MoviePilotToolFactory from app.agent.tools.impl.agent_task import AgentTaskTool from app.agent.tools.impl.api import MoviePilotApiTool from app.agent.tools.impl.execute_command import ExecuteCommandTool +from app.agent.tools.manager import MoviePilotToolsManager def test_api_operation_registry_matches_migration_batches() -> None: """API 操作注册表必须覆盖两批 operation 且每项具有固定路由。""" - assert len(API_FIRST_BATCH_OPERATION_SPECS) == 44 + assert len(API_FIRST_BATCH_OPERATION_SPECS) == 52 assert len(API_PARITY_OPERATION_SPECS) == 15 - assert len(API_OPERATION_SPECS) == 59 + assert len(API_MUSIC_OPERATION_SPECS) == 10 + assert len(API_SYSTEM_OPERATION_SPECS) == 7 + assert len(API_EXTENDED_OPERATION_SPECS) == 119 + assert len(API_OPERATION_SPECS) == 203 assert {spec.operation_id for spec in API_OPERATION_SPECS} == set(API_OPERATION_ROUTES) assert { "download.list", "download.update", "download.delete", "downloaders.list", - "library.latest", }.isdisjoint(API_OPERATION_ROUTES) + assert { + "plugin.source.options", + "plugin.source.install", + "plugin.source.change", + "download.tasks.active", + "download.clients", + "download.paths", + "download.history.list", + "library.latest", + "system.versions", + "system.update.status", + "system.update.check", + "system.update.download", + "system.restart", + "system.update.install", + "system.upgrade.dev", + "dashboard.system", + "media.sources", + "search.title", + "site.add", + "subscription.get", + "storage.rename", + "transfer.manual_reviews", + "workflow.create", + "torrent.cache.get", + "database.backups.list", + "system.module.list", + "plugin.clone", + }.issubset(API_OPERATION_ROUTES) + + +def test_music_operations_expose_bidirectional_artist_album_navigation() -> None: + """音乐 Skill 必须完整暴露作品到作者、作者到作品及关联浏览合同。""" + schema = MoviePilotApiTool(session_id="session", user_id="api_user").get_mcp_input_schema() + branches = { + item["properties"]["operation_id"]["const"]: item + for item in schema["oneOf"] + } + + assert { + "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", + }.issubset(branches) + album_path = branches["music.album.get"]["properties"]["path_params"] + assert album_path["required"] == ["album_id"] + artist_albums = branches["music.artist.albums"] + assert artist_albums["properties"]["path_params"]["required"] == ["artist_id"] + album_type = artist_albums["properties"]["query"]["properties"]["album_type"] + assert "single" in str(album_type) + assert branches["music.cache.delete"]["properties"]["path_params"]["required"] == [ + "cache_key" + ] + + +def test_mcp_tools_list_preserves_all_moviepilot_api_operation_branches() -> None: + """外部 MCP tools/list 必须返回全部 operation 的精确 oneOf,而不是通用字典。""" + manager = MoviePilotToolsManager(session_id="session", user_id="api_user") + manager.tools = [MoviePilotApiTool(session_id="session", user_id="api_user")] + + definition = manager.list_tools()[0] + operation_ids = { + branch["properties"]["operation_id"]["const"] + for branch in definition.input_schema["oneOf"] + } + + assert definition.name == "moviepilot_api" + assert operation_ids == set(API_OPERATION_ROUTES) + + +def test_filter_read_parameters_are_query_fields_not_get_request_bodies() -> None: + """规则读取筛选列表必须作为 query 参数公开,避免 Agent 构造无语义 GET body。""" + schema = MoviePilotApiTool(session_id="session", user_id="api_user").get_mcp_input_schema() + branches = { + item["properties"]["operation_id"]["const"]: item + for item in schema["oneOf"] + } + + assert "body" not in branches["filter.builtin"]["properties"] + assert "rule_ids" in branches["filter.builtin"]["properties"]["query"]["properties"] + assert "body" not in branches["filter.custom"]["properties"] + assert "rule_ids" in branches["filter.custom"]["properties"]["query"]["properties"] + assert "body" not in branches["filter.groups"]["properties"] + assert "group_names" in branches["filter.groups"]["properties"]["query"]["properties"] + + +def test_plugin_operations_expose_discovery_before_precise_writes() -> None: + """插件配置和来源操作必须给 Agent 可先发现再精确写入的完整合同。""" + schema = MoviePilotApiTool(session_id="session", user_id="api_user").get_mcp_input_schema() + branches = { + item["properties"]["operation_id"]["const"]: item + for item in schema["oneOf"] + } + + installed_query = branches["plugin.installed"]["properties"]["query"] + assert installed_query["properties"]["state"]["const"] == "installed" + assert "query" in installed_query["properties"] + assert installed_query["properties"]["max_results"]["maximum"] == 200 + + config_get_path = API_OPERATION_ROUTES["plugin.config.get"].path + assert config_get_path == "/api/v1/plugin/form/{plugin_id}" + config_body = branches["plugin.config.update"]["properties"]["body"] + assert config_body["minProperties"] == 1 + assert "First call plugin.config.get" in config_body["description"] + + assert branches["plugin.source.install"]["properties"]["body"]["$ref"].endswith( + "/PluginSourceInstallRequest" + ) + assert branches["plugin.source.change"]["properties"]["body"]["$ref"].endswith( + "/PluginSourceChangeRequest" + ) + + +def test_dev_upgrade_mcp_schema_requires_exact_scalar_body() -> None: + """Dev 更新必须暴露精确字符串 body,不能回退为任意 JSON。""" + schema = MoviePilotApiTool( + session_id="session", + user_id="api_user", + ).get_mcp_input_schema() + branch = next( + item + for item in schema["oneOf"] + if item["properties"]["operation_id"].get("const") == "system.upgrade.dev" + ) + + assert branch["properties"]["body"]["const"] == "dev" + assert branch["properties"]["body"]["type"] == "string" + assert "body" in branch["required"] + + +def test_gateway_forwards_exact_scalar_body() -> None: + """网关必须原样传递少数固定 operation 声明的 JSON 标量请求体。""" + executor = AsyncMock() + executor.execute.return_value = json.dumps({"success": True}) + gateway = MoviePilotApiTool( + session_id="session", + user_id="api_user", + executor=executor, + ) + gateway.set_agent_context({"is_admin": True}) + + result = asyncio.run( + gateway.run( + operation_id="system.upgrade.dev", + body="dev", + ) + ) + + assert json.loads(result)["success"] is True + executor.execute.assert_awaited_once_with( + "system.upgrade.dev", + path_params=None, + query=None, + body="dev", + ) + + +def test_system_settings_mcp_schema_explains_both_setting_sources() -> None: + """外部 MCP Client 应直接看到系统设置发现与精确更新参数语义。""" + schema = MoviePilotApiTool( + session_id="session", + user_id="api_user", + ).get_mcp_input_schema() + branches = { + branch["properties"]["operation_id"]["const"]: branch + for branch in schema["oneOf"] + } + query = branches["config.system.get"]["properties"]["query"]["properties"] + update_ref = branches["config.system.update"]["properties"]["body"]["$ref"] + update = schema["$defs"][update_ref.rsplit("/", 1)[-1]]["properties"] + + assert "Settings field names" in query["setting_key"]["description"] + assert "systemconfig" in query["group"]["description"] + assert "confirmation-protected" in query["show_secrets"]["description"] + assert "config.system.get" in update["setting_key"]["description"] + assert "upsert_list_item" in update["operation"]["description"] + assert "NotificationSwitchs" in update["match_field"]["description"] + + +def test_api_mcp_schema_gives_every_field_concrete_english_guidance() -> None: + """MCP 合同字段不得回退为抽象占位说明或中英文混排。""" + schema = MoviePilotApiTool( + session_id="session", + user_id="api_user", + ).get_mcp_input_schema() + descriptions = [] + + def collect(node) -> None: + """递归收集对象字段说明并断言没有遗漏。""" + if isinstance(node, dict): + properties = node.get("properties") + if isinstance(properties, dict): + for field_name, field_schema in properties.items(): + assert isinstance(field_schema, dict), field_name + description = field_schema.get("description") + assert isinstance(description, str) and description.strip(), field_name + descriptions.append(description) + for value in node.values(): + collect(value) + elif isinstance(node, list): + for value in node: + collect(value) + + collect(schema) + rendered = "\n".join(descriptions) + assert "按接口模型语义传值" not in rendered + assert "declared by the selected operation" not in rendered + assert "use the matching oneOf branch for its exact type" not in rendered + assert not re.search(r"[\u3400-\u9fff]", rendered) def test_policy_classifies_api_operation_by_operation_id() -> None: @@ -94,6 +317,27 @@ def test_gateway_rejects_unknown_operation() -> None: assert "unknown_operation" in result +def test_gateway_rejects_admin_operation_for_non_admin_before_http() -> None: + """管理员 operation 必须在网关层拒绝普通用户,不能只依赖最终端点。""" + executor = AsyncMock() + gateway = MoviePilotApiTool( + session_id="session", + user_id="1", + executor=executor, + ) + gateway.set_agent_context({"is_admin": False}) + + result = asyncio.run( + gateway.run( + operation_id="config.system.get", + query={"group": "settings"}, + ) + ) + + assert json.loads(result)["error"] == "permission_denied" + executor.execute.assert_not_awaited() + + def test_gateway_resolves_http_manager_to_persisted_superuser() -> None: """MCP/HTTP 管理入口应绑定真实管理员,而不是伪造 api_user 身份。""" gateway = MoviePilotApiTool(session_id="session", user_id="api_user") @@ -113,6 +357,31 @@ def test_gateway_resolves_http_manager_to_persisted_superuser() -> None: assert identity == ("7", "admin", True) +def test_gateway_maps_verified_channel_admin_only_for_admin_operation() -> None: + """通知渠道管理员执行管理员 operation 时应延续旧工具的管理员语义。""" + gateway = MoviePilotApiTool(session_id="session", user_id="telegram-user") + gateway.set_message_attr( + channel="telegram", + source="main-bot", + username="channel-user", + ) + gateway.set_agent_context({"is_admin": True}) + + with patch( + "app.application.security.auth.build_superuser_token_payload", + return_value=SimpleNamespace( + sub=7, + username="admin", + super_user=True, + ), + ): + identity = asyncio.run( + gateway._resolve_api_identity(require_system_admin=True) + ) + + assert identity == ("7", "admin", True) + + def test_factory_uses_api_catalog_by_default(monkeypatch) -> None: """统一工具工厂默认只暴露原生能力和 API 网关。""" monkeypatch.setattr( diff --git a/tests/test_agent_api_projection_endpoints.py b/tests/test_agent_api_projection_endpoints.py new file mode 100644 index 000000000..9c951d6e3 --- /dev/null +++ b/tests/test_agent_api_projection_endpoints.py @@ -0,0 +1,162 @@ +"""Focused tests for safe user-level Agent API projections.""" + +import asyncio +from types import SimpleNamespace + +from app.api.endpoints import site as site_endpoint +from app.api.endpoints import storage as storage_endpoint +from app.api.endpoints import workflow as workflow_endpoint +from app.schemas.file import FileItem + + +class _SiteQuery: + """Return a stable mixed site list for projection tests.""" + + async def list_ordered(self): + """Return one active and one inactive site with authentication fields.""" + common = { + "domain": "example.invalid", + "url": "https://example.invalid/", + "pri": 0, + "downloader": "main", + "ua": "agent-test", + "proxy": False, + "filter": None, + "render": False, + "public": False, + "note": None, + "limit_interval": None, + "limit_count": None, + "limit_seconds": None, + "timeout": 30, + "rss": "https://example.invalid/rss", + "cookie": "secret-cookie", + "apikey": "secret-key", + "token": "secret-token", + } + return [ + SimpleNamespace(id=1, name="Active Site", is_active=True, **common), + SimpleNamespace(id=2, name="Inactive Site", is_active=False, **common), + ] + + +class _WorkflowQuery: + """Return workflows with private action context that the Agent projection must omit.""" + + async def list(self): + """Return one manual running workflow and one timer workflow.""" + return [ + SimpleNamespace( + id=1, + name="Manual Workflow", + description="visible", + trigger_type="manual", + state="R", + run_count=2, + timer=None, + event_type=None, + add_time="2026-08-31", + last_time="2026-08-31", + current_action=1, + actions=[{"private": "context"}], + result={"private": "result"}, + ), + SimpleNamespace( + id=2, + name="Timer Workflow", + description=None, + trigger_type=None, + state="W", + run_count=0, + timer="0 0 * * *", + event_type=None, + add_time="2026-08-31", + last_time=None, + current_action=None, + actions=[], + result=None, + ), + ] + + +def test_site_agent_projection_filters_and_hides_secrets_for_normal_users() -> None: + """Normal users may list sites but must not receive authentication material.""" + result = asyncio.run( + site_endpoint.read_agent_sites( + status="active", + name="active", + query=_SiteQuery(), + current_user=SimpleNamespace(is_superuser=False), + ) + ) + + assert [item["name"] for item in result] == ["Active Site"] + assert all(key not in result[0] for key in ("rss", "cookie", "apikey", "token")) + + +def test_site_agent_projection_returns_auth_fields_only_to_superusers() -> None: + """A verified superuser keeps the old administrator site-query fidelity.""" + result = asyncio.run( + site_endpoint.read_agent_sites( + status="inactive", + query=_SiteQuery(), + current_user=SimpleNamespace(is_superuser=True), + ) + ) + + assert result[0]["name"] == "Inactive Site" + assert result[0]["cookie"] == "secret-cookie" + assert result[0]["apikey"] == "secret-key" + + +def test_workflow_agent_projection_filters_without_returning_action_context() -> None: + """User-level workflow discovery must expose state without private execution payloads.""" + result = asyncio.run( + workflow_endpoint.list_agent_workflows( + state="R", + name="manual", + trigger_type="manual", + query=_WorkflowQuery(), + _=object(), + ) + ) + + assert result == [ + { + "id": 1, + "name": "Manual Workflow", + "description": "visible", + "trigger_type": "manual", + "state": "R", + "run_count": 2, + "timer": None, + "event_type": None, + "add_time": "2026-08-31", + "last_time": "2026-08-31", + "current_action": 1, + } + ] + + +def test_storage_agent_list_reuses_bounded_filter_and_sort(monkeypatch) -> None: + """User-level storage reads must preserve keyword filtering and stable sorting.""" + class _StorageChain: + """Return a fixed directory listing without touching a real storage provider.""" + + def list_files(self, _fileitem): + """Return two entries in reverse natural-name order.""" + return [ + FileItem(path="/b", name="Episode 10", modify_time=1), + FileItem(path="/a", name="Episode 2", modify_time=2), + ] + + monkeypatch.setattr(storage_endpoint, "StorageChain", _StorageChain) + + result = storage_endpoint.list_agent_files( + fileitem=FileItem(path="/"), + sort="name", + keyword="Episode*", + _=object(), + ) + + assert [item.name for item in result] == ["Episode 2", "Episode 10"] diff --git a/tests/test_agent_api_surface_audit.py b/tests/test_agent_api_surface_audit.py new file mode 100644 index 000000000..d02729eb0 --- /dev/null +++ b/tests/test_agent_api_surface_audit.py @@ -0,0 +1,109 @@ +"""Agent API surface inventory and generated contract drift tests.""" + +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/architecture/agent-api-surface-audit.json" +AUDIT_MARKDOWN = PROJECT_ROOT / "docs/architecture/agent-api-surface-audit.md" +API_SKILL = PROJECT_ROOT / "skills/moviepilot-api/SKILL.md" + + +def _load_generator() -> dict: + """Load the audit generator without invoking its file-writing entrypoint.""" + 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: + """The checked-in complete inventory must match live OpenAPI and the gateway registry.""" + 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_every_gateway_operation_has_one_exact_english_skill_and_mcp_contract() -> None: + """Every approved operation must be discoverable with matching exact English contracts.""" + skill = API_SKILL.read_text(encoding="utf-8") + schema = MoviePilotApiTool(session_id="audit", user_id="1").get_mcp_input_schema() + branches = { + branch["properties"]["operation_id"]["const"]: branch + for branch in schema["oneOf"] + } + + assert set(branches) == set(API_OPERATION_ROUTES) + for operation_id, route in API_OPERATION_ROUTES.items(): + assert skill.count(f"### `{operation_id}`") == 1 + branch = branches[operation_id] + assert branch["description"].strip() + assert not re.search(r"[\u3400-\u9fff]", branch["description"]), operation_id + assert route.method in skill.split(f"### `{operation_id}`", 1)[1].split("\n### `", 1)[0] + assert route.path in skill.split(f"### `{operation_id}`", 1)[1].split("\n### `", 1)[0] + + +def test_every_gateway_path_placeholder_is_required_by_its_mcp_branch() -> None: + """固定路由的每个路径占位符都必须在工具 schema 中以同名必填字段暴露。""" + schema = MoviePilotApiTool(session_id="audit", user_id="1").get_mcp_input_schema() + branches = { + branch["properties"]["operation_id"]["const"]: branch + for branch in schema["oneOf"] + } + + for operation_id, route in API_OPERATION_ROUTES.items(): + expected = set(re.findall(r"{([^}]+)}", route.path)) + 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: + """Unexposed REST routes must remain visible and deliberately owned, never silently omitted.""" + 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_agent_application_services.py b/tests/test_agent_application_services.py index f49946b34..e19838b97 100644 --- a/tests/test_agent_application_services.py +++ b/tests/test_agent_application_services.py @@ -188,9 +188,24 @@ async def test_save_system_config_and_settings_service(monkeypatch): assert filter_config.async_set.await_count == 1 secret = service.query(setting_key=SystemConfigKey.Downloaders.value, include_values=True) assert secret["settings"][0]["value"][0]["token"] == "***" + definition = secret["settings"][0]["definition"] + assert definition == { + "declared_type": "list[object]", + "value_shape": "list", + "nullable": False, + "sensitive": True, + "update_operations": ["replace", "upsert_list_item", "remove_list_item"], + "default_match_field": "name", + "persistence": "database:systemconfig", + } shown = service.query(setting_key=SystemConfigKey.Downloaders.value, include_values=True, show_secrets=True) assert shown["settings"][0]["value"][0]["token"] == "secret" assert service.query(group="ai_agent")["include_values"] is False + runtime_definition = service.query(setting_key="LLM_MODEL")["settings"][0]["definition"] + assert runtime_definition["declared_type"] + assert runtime_definition["value_shape"] == "str" + assert runtime_definition["update_operations"] == ["replace"] + assert runtime_definition["persistence"] == "app.env" spec = settings_module.resolve_setting_spec(SystemConfigKey.Downloaders.value) assert spec assert service._prepare_next_value(spec, {"name": "old", "x": 1}, {"name": "old", "y": 2}, "merge_dict", ["x"], None, None) == {"name": "old", "y": 2} diff --git a/tests/test_agent_prompt_secrets.py b/tests/test_agent_prompt_secrets.py index 0a9f4cd20..bf672c59a 100644 --- a/tests/test_agent_prompt_secrets.py +++ b/tests/test_agent_prompt_secrets.py @@ -73,6 +73,8 @@ def test_agent_prompt_delegates_explicit_secret_reads_to_host_confirmation() -> assert "config.system.get" in prompt assert "show_secrets" in prompt + assert "query.show_secrets=true" in prompt + assert "body.show_secrets=true" not in prompt assert "do not refuse" in prompt assert "host verifies administrator authority" in prompt assert "Never expose or repeat the secret" in prompt diff --git a/tests/test_agent_skills_middleware.py b/tests/test_agent_skills_middleware.py index cb03a8cb7..76e837db8 100644 --- a/tests/test_agent_skills_middleware.py +++ b/tests/test_agent_skills_middleware.py @@ -1,4 +1,5 @@ import json +from pathlib import Path from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch @@ -15,6 +16,8 @@ from app.agent.middleware.skills import ( ) from app.agent.tools.tags import ToolTag +PROJECT_ROOT = Path(__file__).resolve().parents[1] + @pytest.fixture def anyio_backend(): @@ -105,6 +108,21 @@ async def test_skill_tool_caps_large_result_before_model_context(tmp_path): assert "Skill 内容已截断" in payload["content"] +@pytest.mark.anyio +async def test_bundled_moviepilot_api_skill_loads_complete_contract() -> None: + """内置 API Skill 的完整 operation 合同必须在运行时上限内且不截断。""" + middleware = SkillsMiddleware(sources=[str(PROJECT_ROOT / "skills")]) + + result = await middleware.tools[0].ainvoke({"name": "moviepilot-api"}) + payload = json.loads(result) + + assert len(result) <= MAX_SKILL_RESULT_CHARS + assert payload["success"] is True + assert payload["truncated"] is False + assert len(payload["skill"]["allowed_api_operations"]) == 203 + assert "### `workflow.update`" in payload["content"] + + @pytest.mark.anyio async def test_skill_tool_returns_not_found_for_unknown_skill(tmp_path): """skill 工具找不到技能时应返回结构化失败信息。""" diff --git a/tests/test_builtin_skill_boundaries.py b/tests/test_builtin_skill_boundaries.py index c75b2c913..fe1b95f33 100644 --- a/tests/test_builtin_skill_boundaries.py +++ b/tests/test_builtin_skill_boundaries.py @@ -1,9 +1,94 @@ +import re +import runpy from pathlib import Path PROJECT_ROOT = Path(__file__).resolve().parents[1] SKILLS_ROOT = PROJECT_ROOT / "skills" CORE_PROMPT_PATH = PROJECT_ROOT / "app/agent/prompt/System Core Prompt.txt" +RETIRED_TOOL_COVERAGE: dict[str, tuple[str, ...]] = { + "add_custom_filter_rule": ("api:filter.custom.add",), + "add_download_tasks": ("api:download.add",), + "add_rule_group": ("api:filter.group.add",), + "add_subscribe": ("api:subscription.add",), + "create_agent_task": ("native:agent_task",), + "delete_agent_task": ("native:agent_task",), + "delete_custom_filter_rule": ("api:filter.custom.delete",), + "delete_download_history": ("api:download.history.delete",), + "delete_download_tasks": ("downloader:tasks.delete",), + "delete_rule_group": ("api:filter.group.delete",), + "delete_subscribe": ("api:subscription.delete",), + "delete_transfer_history": ("api:transfer.history.delete",), + "get_recommendations": ("api:recommendation.list",), + "get_search_results": ("api:search.results",), + "install_plugin": ("api:plugin.install", "api:plugin.source.install"), + "list_directory": ("api:storage.list",), + "list_slash_commands": ("api:slash.list",), + "query_agent_tasks": ("native:agent_task",), + "query_builtin_filter_rules": ("api:filter.builtin",), + "query_custom_filter_rules": ("api:filter.custom",), + "query_custom_identifiers": ("api:config.identifiers.get",), + "query_directory_settings": ("api:storage.settings",), + "query_download_tasks": ("downloader:tasks.list",), + "query_downloaders": ("api:download.clients", "downloader:instances.list"), + "query_episode_schedule": ("api:media.episode_schedule",), + "query_installed_plugins": ("api:plugin.installed",), + "query_library_exists": ("api:library.exists",), + "query_library_latest": ("api:library.latest", "mediaserver:activity.latest"), + "query_market_plugins": ("api:plugin.market",), + "query_media_detail": ("api:media.detail",), + "query_personas": ("native:persona",), + "query_plugin_capabilities": ("api:plugin.capabilities",), + "query_plugin_config": ("api:plugin.config.get",), + "query_plugin_data": ("api:plugin.data",), + "query_popular_subscribes": ("api:subscription.popular",), + "query_rule_groups": ("api:filter.groups",), + "query_schedulers": ("api:scheduler.list",), + "query_site_userdata": ("api:site.userdata",), + "query_sites": ("api:site.list",), + "query_subscribe_history": ("api:subscription.history",), + "query_subscribe_shares": ("api:subscription.shares",), + "query_subscribes": ("api:subscription.list",), + "query_system_settings": ("api:config.system.get",), + "query_transfer_history": ("api:transfer.history",), + "query_workflows": ("api:workflow.list",), + "recognize_media": ("api:media.recognize",), + "reload_plugin": ("api:plugin.reload",), + "run_agent_task": ("native:agent_task",), + "run_scheduler": ("api:scheduler.run",), + "run_slash_command": ("api:slash.run",), + "run_workflow": ("api:workflow.run",), + "scrape_metadata": ("api:media.scrape",), + "search_media": ("api:media.search",), + "search_person": ("api:media.person.search",), + "search_person_credits": ("api:media.person.credits",), + "search_subscribe": ("api:subscription.search",), + "search_torrents": ("api:search.torrents",), + "switch_persona": ("native:persona",), + "test_site": ("api:site.test",), + "transfer_file": ("api:transfer.file",), + "uninstall_plugin": ("api:plugin.uninstall",), + "update_agent_task": ("native:agent_task",), + "update_custom_filter_rule": ("api:filter.custom.update",), + "update_custom_identifiers": ("api:config.identifiers.update",), + "update_download_tasks": ( + "downloader:tasks.start", + "downloader:tasks.stop", + "downloader:tasks.tags.set", + "downloader:tasks.properties.set", + "downloader:tasks.trackers.update", + "downloader:tasks.location.set", + "downloader:tasks.category.set", + ), + "update_persona_definition": ("native:persona",), + "update_plugin_config": ("api:plugin.config.get", "api:plugin.config.update"), + "update_rule_group": ("api:filter.group.update",), + "update_site": ("api:site.update",), + "update_site_cookie": ("api:site.cookie.update",), + "update_subscribe": ("api:subscription.update",), + "update_system_settings": ("api:config.system.get", "api:config.system.update"), +} + def _read_skill(skill_name: str) -> str: """读取内置技能的 SKILL.md 内容。""" @@ -23,18 +108,18 @@ def test_modified_builtin_skills_have_incremented_versions() -> None: expected_versions = { "browser-use": "2", "command-dispatch": "2", - "database-operation": "5", + "database-operation": "6", "feedback-issue": "9", - "moviepilot-api": "16", - "moviepilot-update": "4", + "moviepilot-api": "23", + "moviepilot-update": "5", "organize-files": "5", "transfer-failed-retry": "5", "generate-identifiers": "4", "create-moviepilot-plugin": "5", "create-moviepilot-skill": "3", "publish-moviepilot-plugin": "3", - "downloader-operation": "2", - "mediaserver-operation": "2", + "downloader-operation": "3", + "mediaserver-operation": "3", } for skill_name, expected_version in expected_versions.items(): @@ -46,6 +131,41 @@ def test_modified_builtin_skills_have_incremented_versions() -> None: def test_retired_moviepilot_cli_skill_is_removed() -> None: """正式 API 方案不得保留旧 MCP CLI Skill。""" assert not (SKILLS_ROOT / "moviepilot-cli" / "SKILL.md").exists() + assert not (SKILLS_ROOT / "moviepilot-api" / "scripts" / "mp-api.py").exists() + assert not (SKILLS_ROOT / "moviepilot-update" / "scripts" / "mp-update.py").exists() + + +def test_every_retired_business_tool_has_a_live_precise_owner() -> None: + """全部 72 个退出业务工具必须由 API、provider Skill 或统一原生工具承接。""" + from app.agent.policy.api import API_OPERATION_ROUTES + from app.agent.tools.factory import MoviePilotToolFactory + + downloader_actions = runpy.run_path( + str(SKILLS_ROOT / "downloader-operation/scripts/mp-downloader.py") + )["ACTIONS"] + mediaserver_actions = runpy.run_path( + str(SKILLS_ROOT / "mediaserver-operation/scripts/mp-mediaserver.py") + )["ACTIONS"] + native_tools = { + MoviePilotToolFactory._tool_class_name(tool_class) + for tool_class in MoviePilotToolFactory.BUILTIN_TOOL_CLASSES + } + + assert len(RETIRED_TOOL_COVERAGE) == 72 + for retired_name, owners in RETIRED_TOOL_COVERAGE.items(): + assert not (PROJECT_ROOT / f"app/agent/tools/impl/{retired_name}.py").exists() + assert owners, retired_name + for owner in owners: + owner_type, owner_name = owner.split(":", 1) + if owner_type == "api": + assert owner_name in API_OPERATION_ROUTES, (retired_name, owner) + elif owner_type == "downloader": + assert owner_name in downloader_actions, (retired_name, owner) + elif owner_type == "mediaserver": + assert owner_name in mediaserver_actions, (retired_name, owner) + else: + assert owner_type == "native" + assert owner_name in native_tools, (retired_name, owner) def test_api_and_database_skills_declare_final_boundaries() -> None: @@ -59,11 +179,65 @@ def test_api_and_database_skills_declare_final_boundaries() -> None: assert "retired tool name" in api_content assert "moviepilot tool" in api_content + update_content = _read_skill("moviepilot-update") + assert "allowed-tools: moviepilot_api" in update_content + assert "system.update.install" in update_content + assert "mp-api.py" not in update_content + assert "mp-update.py" not in update_content + assert "direct SQL boundary" in db_content assert "Use this skill as the final fallback" in db_content assert "INSERT" in db_content assert "UPDATE" in db_content assert "DELETE" in db_content + assert "allowed-tools: execute_command" in db_content + from app.db.base import Base + from app.db.models import load_all_models + + load_all_models() + for table_name in Base.metadata.tables: + heading = f"### `{table_name}`" + assert heading in db_content + section = db_content.split(heading, 1)[1].split("\n### `", 1)[0] + assert "- Purpose:" in section + assert "- Useful queries:" in section + assert "- Write boundary:" in section + assert "- Columns:" in section + assert "### `alembic_version`" in db_content + alembic_section = db_content.split("### `alembic_version`", 1)[1].split( + "\n### `", 1 + )[0] + assert "- Purpose:" in alembic_section + assert "- Useful queries:" in alembic_section + assert "Never edit it directly" in alembic_section + + +def test_api_skill_uses_runtime_system_setting_discovery() -> None: + """系统设置 Skill 应指导动态发现定义,而不是复制不断变化的键清单。""" + api_content = _read_skill("moviepilot-api") + + assert "## System Settings Contract" in api_content + assert "Do not enumerate setting keys in this Skill" in api_content + assert '`query={"group":"settings","keyword":"LLM"}`' in api_content + assert "`declared_type`" in api_content + assert "`update_operations`" in api_content + assert "`persistence`" in api_content + assert "### Settings variables" not in api_content + assert "### SystemConfig keys" not in api_content + + +def test_refactored_agent_skills_use_english_guidance() -> None: + """四个重构 Skill 的模型指导文本必须统一为英文。""" + for skill_name in ( + "moviepilot-api", + "downloader-operation", + "mediaserver-operation", + "database-operation", + "moviepilot-update", + ): + content = _read_skill(skill_name) + assert not re.search(r"[\u3400-\u9fff]", content), skill_name + assert "按接口模型语义传值" not in content def test_agent_core_prompt_does_not_block_plugin_source_edits() -> None: diff --git a/tests/test_service_operation_mcp_tools.py b/tests/test_service_operation_mcp_tools.py index 6bf65f41a..6787b8b66 100644 --- a/tests/test_service_operation_mcp_tools.py +++ b/tests/test_service_operation_mcp_tools.py @@ -2,10 +2,14 @@ import asyncio import json +import threading from unittest.mock import patch +import pytest + from app.agent.tools.factory import MoviePilotToolFactory -from app.agent.tools.impl.service_operation import ( +from app.agent.tools.impl.service import ( + DatabaseOperationTool, DownloaderOperationTool, MediaServerOperationTool, ) @@ -32,7 +36,7 @@ def test_downloader_mcp_schema_exposes_every_action_argument_and_rule() -> None: assert arguments["properties"]["task_ids"] == { "type": "array", "items": {"type": "string"}, - "description": "多个任务的 provider 原生 hash 或 ID;与 task_id 二选一。", + "description": "Multiple provider-native task hashes or IDs; mutually exclusive with task_id.", "minItems": 1, } assert arguments["properties"]["position"]["enum"] == ["top", "up", "down", "bottom"] @@ -68,6 +72,30 @@ def test_mediaserver_mcp_schema_exposes_nested_refresh_item_fields() -> None: assert item_schema["additionalProperties"] is False +def test_database_mcp_schema_exposes_all_actions_and_sql_sources() -> None: + """数据库 MCP 工具应直接暴露四个 action 及 SQL/file 约束。""" + tool = DatabaseOperationTool(session_id="session", user_id="api_user") + schema = tool.get_mcp_input_schema() + + assert schema["properties"]["action"]["enum"] == [ + "query", + "schema", + "tables", + "write", + ] + query_arguments = _branch(schema, "query")["properties"]["arguments"] + assert set(query_arguments["properties"]) == {"sql", "file", "limit", "write"} + assert query_arguments["properties"]["limit"]["default"] == 100 + assert query_arguments["properties"]["limit"]["minimum"] == 1 + assert query_arguments["oneOf"] == [ + {"required": ["sql"], "not": {"required": ["file"]}}, + {"required": ["file"], "not": {"required": ["sql"]}}, + ] + assert _branch(schema, "schema")["properties"]["arguments"]["required"] == [ + "table_name" + ] + + def test_direct_manager_preserves_service_operation_mcp_schema() -> None: """工具管理器不得把服务操作的 oneOf 和嵌套 schema 压平成普通对象。""" manager = MoviePilotToolsManager(session_id="session", user_id="api_user") @@ -102,14 +130,50 @@ def test_factory_only_adds_service_operation_tools_for_external_manager() -> Non external_names = {tool.name for tool in external} assert "downloader_operation" not in internal_names assert "mediaserver_operation" not in internal_names - assert {"downloader_operation", "mediaserver_operation"}.issubset(external_names) + assert { + "downloader_operation", + "mediaserver_operation", + "database_operation", + }.issubset(external_names) + + +def test_database_operation_tool_calls_fixed_script_once() -> None: + """数据库 MCP tools/call 应只执行一次固定脚本并返回 JSON envelope。""" + tool = DatabaseOperationTool(session_id="session", user_id="api_user") + with patch( + "app.agent.tools.impl.service._run_database_script", + return_value={"tables": ["agentchat"]}, + ) as runner: + result = asyncio.run(tool.run(action="tables", arguments={})) + + assert json.loads(result) == {"tables": ["agentchat"]} + runner.assert_called_once() + assert runner.call_args.kwargs["arguments"] == { + "action": "tables", + "arguments": {}, + } + + +def test_database_operation_rejects_invalid_nested_arguments_before_script() -> None: + """数据库工具应在连接数据库前一次性拒绝错误的嵌套参数。""" + tool = DatabaseOperationTool(session_id="session", user_id="api_user") + with patch("app.agent.tools.impl.service.subprocess.run") as runner: + with pytest.raises(ValueError, match="sql 与 file"): + asyncio.run( + tool.run( + action="query", + arguments={"sql": "SELECT 1", "file": "query.sql"}, + ) + ) + + runner.assert_not_called() def test_service_operation_tool_calls_fixed_script_once() -> None: """一次 MCP tools/call 应只执行一次固定脚本并返回其 JSON envelope。""" tool = DownloaderOperationTool(session_id="session", user_id="api_user") with patch( - "app.agent.tools.impl.service_operation._run_service_script", + "app.agent.tools.impl.service._run_service_script", return_value={"success": True, "action": "tasks.list", "data": {"items": []}}, ) as runner: result = asyncio.run( @@ -128,3 +192,42 @@ def test_service_operation_tool_calls_fixed_script_once() -> None: action="tasks.list", arguments={"limit": 20}, ) + + +def test_service_operation_sync_script_does_not_block_event_loop() -> None: + """同步 provider 脚本必须在分域线程池运行,不能阻塞 Agent event loop。""" + started = threading.Event() + release = threading.Event() + + def slow_runner(**_kwargs) -> dict: + """模拟尚未返回的同步第三方 SDK 调用。""" + started.set() + release.wait(timeout=1) + return {"success": True, "data": {}} + + async def exercise() -> None: + """在同步脚本运行期间验证 loop 仍能调度其它协程。""" + tool = DownloaderOperationTool(session_id="session", user_id="api_user") + with patch( + "app.agent.tools.impl.service._run_service_script", + side_effect=slow_runner, + ): + task = asyncio.create_task( + tool.run(action="tasks.list", arguments={"limit": 1}) + ) + try: + for _ in range(50): + if started.is_set(): + break + await asyncio.sleep(0.002) + assert started.is_set() + assert await asyncio.wait_for( + asyncio.sleep(0, result="loop-responsive"), + timeout=0.1, + ) == "loop-responsive" + assert task.done() is False + finally: + release.set() + assert json.loads(await task)["success"] is True + + asyncio.run(exercise()) diff --git a/tests/test_service_operation_skills.py b/tests/test_service_operation_skills.py index ff15e8127..e218a70c7 100644 --- a/tests/test_service_operation_skills.py +++ b/tests/test_service_operation_skills.py @@ -182,7 +182,7 @@ def test_downloader_instances_and_capabilities_do_not_expose_credentials( ] rendered = str((instances, capabilities)) assert "private.invalid" not in rendered - assert "secret" not in rendered + assert '"secret"' not in json.dumps((instances, capabilities), ensure_ascii=False) assert any(item["action"] == "tasks.peers" for item in capabilities["actions"]) @@ -200,10 +200,10 @@ def test_downloader_capability_exposes_complete_action_arguments( "name": "position", "type": "string", "required": True, - "description": "目标队列位置。", + "description": "Target queue position.", "enum": ["top", "up", "down", "bottom"], } - assert action["argument_rules"] == ["task_id 与 task_ids 必须提供且只能选择一种。"] + assert action["argument_rules"] == ["Provide exactly one of task_id and task_ids."] def test_downloader_argument_validation_reports_all_errors_before_config_load( @@ -394,7 +394,7 @@ def test_mediaserver_capabilities_are_provider_specific( assert "playback.sessions" not in action_names assert "metadata.refresh" not in action_names assert "private.invalid" not in str(result) - assert "secret" not in str(result) + assert '"secret"' not in json.dumps(result, ensure_ascii=False) def test_mediaserver_capability_exposes_nested_refresh_contract( @@ -411,8 +411,8 @@ def test_mediaserver_capability_exposes_nested_refresh_contract( "type": "object[]", "required": True, "description": ( - "刷新条目;每项支持 title:string、year:string|integer、type:电影|电视剧|音乐、" - "category:string、target_path:string。" + "Items to refresh. Each item supports title:string, year:string|integer, " + "type using the exact MoviePilot media-type value, category:string, and target_path:string." ), } ] diff --git a/tests/test_skill_scripts_security.py b/tests/test_skill_scripts_security.py index a05e47d0d..e8f908dea 100644 --- a/tests/test_skill_scripts_security.py +++ b/tests/test_skill_scripts_security.py @@ -4,9 +4,7 @@ from pathlib import Path from types import ModuleType from typing import Any - PROJECT_ROOT = Path(__file__).resolve().parents[1] -MP_API_SCRIPT = PROJECT_ROOT / "skills" / "moviepilot-api" / "scripts" / "mp-api.py" MP_DB_SCRIPT = PROJECT_ROOT / "skills" / "database-operation" / "scripts" / "mp-db.py" @@ -19,36 +17,6 @@ def _load_script(path: Path, module_name: str) -> ModuleType: return module -def test_mp_api_uses_settings_without_prompt_token(monkeypatch, tmp_path) -> None: - """API 脚本应直接读取 settings,而不是要求提示词提供 token。""" - module = _load_script(MP_API_SCRIPT, "mp_api_script") - runtime_dir = tmp_path / "temp" - runtime_dir.mkdir() - - class FakeSettings: - """提供 API 脚本本地配置所需字段。""" - - TEMP_PATH = runtime_dir - HOST = "0.0.0.0" - PORT = 3001 - API_TOKEN = "settings-token" - - monkeypatch.setattr(module, "_ensure_project_import", lambda: None) - monkeypatch.setattr(module, "read_config", lambda: ("http://file-host", "file-token")) - monkeypatch.setattr( - "app.runtime.config.settings", - FakeSettings, - raising=False, - ) - monkeypatch.delenv("MP_HOST", raising=False) - monkeypatch.delenv("MP_API_KEY", raising=False) - - host, key = module.resolve_config() - - assert host == "http://127.0.0.1:3001" - assert key == "settings-token" - - def test_mp_db_rejects_write_statement_without_write_flag() -> None: """数据库脚本默认必须拒绝写操作。""" module = _load_script(MP_DB_SCRIPT, "mp_db_script")