diff --git a/app/agent/api/executor.py b/app/agent/api/executor.py index c6cc3003f..989cda407 100644 --- a/app/agent/api/executor.py +++ b/app/agent/api/executor.py @@ -140,8 +140,11 @@ class MoviePilotApiExecutor: continue if not collection: return payload - result = dict(payload) - result["collection"] = collection + # 集合数据可能触发通用工具结果截断;把元数据放在 data 前面,确保预览仍保留精确总数。 + result = {"collection": collection} + for key, value in payload.items(): + if key != "collection": + result[key] = value return result async def execute( diff --git a/app/agent/policy/api_mcp_schema.json b/app/agent/policy/api_mcp_schema.json index 5a951a633..0609a591a 100644 --- a/app/agent/policy/api_mcp_schema.json +++ b/app/agent/policy/api_mcp_schema.json @@ -5206,7 +5206,7 @@ }, { "additionalProperties": false, - "description": "Read the current host network receive and transmit counters. Method: GET. Path: /api/v1/dashboard/network. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "Read the current host network receive and transmit counters. Method: GET. Path: /api/v1/dashboard/network. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "dashboard.network", @@ -5262,7 +5262,7 @@ }, { "additionalProperties": false, - "description": "List host processes visible to the MoviePilot runtime. Method: GET. Path: /api/v1/dashboard/processes. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List host processes visible to the MoviePilot runtime. Method: GET. Path: /api/v1/dashboard/processes. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "dashboard.processes", @@ -5350,7 +5350,7 @@ }, { "additionalProperties": false, - "description": "Read aggregate file-transfer counts grouped by time period. Method: GET. Path: /api/v1/dashboard/transfer. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "Read aggregate file-transfer counts grouped by time period. Method: GET. Path: /api/v1/dashboard/transfer. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "dashboard.transfer.statistics", @@ -5467,7 +5467,7 @@ }, { "additionalProperties": false, - "description": "List managed database backup artifacts without exposing host paths. Method: GET. Path: /api/v1/system/database/backups. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List managed database backup artifacts without exposing host paths. Method: GET. Path: /api/v1/system/database/backups. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "database.backups.list", @@ -5576,7 +5576,7 @@ }, { "additionalProperties": false, - "description": "List enabled downloader instance names and provider types without credentials. Method: GET. Path: /api/v1/download/clients. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List enabled downloader instance names and provider types without credentials. Method: GET. Path: /api/v1/download/clients. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "download.clients", @@ -5653,7 +5653,7 @@ }, { "additionalProperties": false, - "description": "Page MoviePilot download-history records in reverse chronological order. Method: GET. Path: /api/v1/history/download. Effect: safe_read. Collection response: data remains a list and the endpoint's documented pagination or limit defaults remain in effect. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "Page MoviePilot download-history records in reverse chronological order. Method: GET. Path: /api/v1/history/download. Effect: safe_read. Collection response: data remains a list and the endpoint's documented pagination or limit defaults remain in effect. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count-only request, use the smallest valid page and read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "download.history.list", @@ -5708,7 +5708,7 @@ }, { "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. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List configured downloader save-path URIs that may be passed to download.add. Method: GET. Path: /api/v1/download/paths. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "download.paths", @@ -5764,7 +5764,7 @@ }, { "additionalProperties": false, - "description": "List currently downloading MoviePilot tasks with their canonical media context. Method: GET. Path: /api/v1/download/. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List currently downloading MoviePilot tasks with their canonical media context. Method: GET. Path: /api/v1/download/. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "download.tasks.active", @@ -6440,7 +6440,7 @@ }, { "additionalProperties": false, - "description": "List seasons defined by one exact TMDB episode-group identity. Method: GET. Path: /api/v1/media/group/seasons/{episode_group}. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List seasons defined by one exact TMDB episode-group identity. Method: GET. Path: /api/v1/media/group/seasons/{episode_group}. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "media.episode_group.seasons", @@ -6512,7 +6512,7 @@ }, { "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. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List alternate TMDB episode groups available for one TV media identity. Method: GET. Path: /api/v1/media/groups/{tmdbid}. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "media.episode_groups", @@ -6584,7 +6584,7 @@ }, { "additionalProperties": false, - "description": "Read TMDB episode release information for one season. Method: GET. Path: /api/v1/tmdb/{tmdbid}/{season}. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "Read TMDB episode release information for one season. Method: GET. Path: /api/v1/tmdb/{tmdbid}/{season}. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "media.episode_schedule", @@ -7090,7 +7090,7 @@ }, { "additionalProperties": false, - "description": "List seasons for one exact media identity or a title-and-year fallback. Method: GET. Path: /api/v1/media/seasons. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List seasons for one exact media identity or a title-and-year fallback. Method: GET. Path: /api/v1/media/seasons. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "media.seasons", @@ -7192,7 +7192,7 @@ }, { "additionalProperties": false, - "description": "List metadata sources currently registered for MoviePilot media operations. Method: GET. Path: /api/v1/media/source. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List metadata sources currently registered for MoviePilot media operations. Method: GET. Path: /api/v1/media/source. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "media.sources", @@ -8210,7 +8210,7 @@ }, { "additionalProperties": false, - "description": "List installed plugins and their runtime status. Method: GET. Path: /api/v1/plugin/. Effect: safe_read. Collection response: data remains a list and the endpoint's documented pagination or limit defaults remain in effect. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List installed plugins and their runtime status. Method: GET. Path: /api/v1/plugin/. Effect: safe_read. Collection response: data remains a list and the endpoint's documented pagination or limit defaults remain in effect. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count-only request, use the smallest valid page and read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "plugin.installed", @@ -8301,7 +8301,7 @@ }, { "additionalProperties": false, - "description": "List plugins available from configured marketplaces. Method: GET. Path: /api/v1/plugin/. Effect: safe_read. Collection response: data remains a list and the endpoint's documented pagination or limit defaults remain in effect. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List plugins available from configured marketplaces. Method: GET. Path: /api/v1/plugin/. Effect: safe_read. Collection response: data remains a list and the endpoint's documented pagination or limit defaults remain in effect. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count-only request, use the smallest valid page and read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "plugin.market", @@ -8923,7 +8923,7 @@ }, { "additionalProperties": false, - "description": "List registered scheduler jobs and their current state. Method: GET. Path: /api/v1/dashboard/schedule. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List registered scheduler jobs and their current state. Method: GET. Path: /api/v1/dashboard/schedule. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "scheduler.list", @@ -9157,7 +9157,7 @@ }, { "additionalProperties": false, - "description": "Search torrent sites for one canonical media identity. Method: GET. Path: /api/v1/search/media/{media_id}. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "Search torrent sites for one canonical media identity. Method: GET. Path: /api/v1/search/media/{media_id}. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "search.torrents", @@ -9356,7 +9356,7 @@ }, { "additionalProperties": false, - "description": "List torrent categories supported by one configured site. Method: GET. Path: /api/v1/site/category/{site_id}. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List torrent categories supported by one configured site. Method: GET. Path: /api/v1/site/category/{site_id}. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "site.category", @@ -9513,7 +9513,7 @@ }, { "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. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "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. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "site.list", @@ -9743,7 +9743,7 @@ }, { "additionalProperties": false, - "description": "List configured sites selected for RSS subscription processing. Method: GET. Path: /api/v1/site/rss. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List configured sites selected for RSS subscription processing. Method: GET. Path: /api/v1/site/rss. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "site.rss", @@ -9799,7 +9799,7 @@ }, { "additionalProperties": false, - "description": "List active configured sites supporting one exact media type. Method: GET. Path: /api/v1/site/media/{media_type}. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List active configured sites supporting one exact media type. Method: GET. Path: /api/v1/site/media/{media_type}. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "site.searchable", @@ -9903,7 +9903,7 @@ }, { "additionalProperties": false, - "description": "Read the latest account and traffic statistics for all configured sites. Method: GET. Path: /api/v1/site/statistic. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "Read the latest account and traffic statistics for all configured sites. Method: GET. Path: /api/v1/site/statistic. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "site.statistics", @@ -10028,7 +10028,7 @@ }, { "additionalProperties": false, - "description": "Read the latest account statistics collected from one site. Method: GET. Path: /api/v1/site/userdata/{site_id}. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "Read the latest account statistics collected from one site. Method: GET. Path: /api/v1/site/userdata/{site_id}. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "site.userdata", @@ -10112,7 +10112,7 @@ }, { "additionalProperties": false, - "description": "Read the latest collected account statistics for every configured site. Method: GET. Path: /api/v1/site/userdata/latest. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "Read the latest collected account statistics for every configured site. Method: GET. Path: /api/v1/site/userdata/latest. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "site.userdata.latest", @@ -10200,7 +10200,7 @@ }, { "additionalProperties": false, - "description": "List slash commands that the Agent may dispatch. Method: GET. Path: /api/v1/message/agent/commands. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List slash commands that the Agent may dispatch. Method: GET. Path: /api/v1/message/agent/commands. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "slash.list", @@ -10298,7 +10298,7 @@ }, { "additionalProperties": false, - "description": "List files or directories from one configured storage location. Method: POST. Path: /api/v1/storage/agent/list. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List files or directories from one configured storage location. Method: POST. Path: /api/v1/storage/agent/list. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "body": { "$ref": "#/$defs/FileItem-Input", @@ -10492,7 +10492,7 @@ }, { "additionalProperties": false, - "description": "Read configured directory or storage settings. Method: GET. Path: /api/v1/storage/directories. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "Read configured directory or storage settings. Method: GET. Path: /api/v1/storage/directories. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "storage.settings", @@ -10881,7 +10881,7 @@ }, { "additionalProperties": false, - "description": "List subscription-sharing user IDs followed by the current user. Method: GET. Path: /api/v1/subscribe/follow. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List subscription-sharing user IDs followed by the current user. Method: GET. Path: /api/v1/subscribe/follow. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "subscription.follow.list", @@ -10990,7 +10990,7 @@ }, { "additionalProperties": false, - "description": "List completed or archived subscription records. Method: GET. Path: /api/v1/subscribe/history/{mtype}. Effect: safe_read. Collection response: data remains a list and the endpoint's documented pagination or limit defaults remain in effect. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List completed or archived subscription records. Method: GET. Path: /api/v1/subscribe/history/{mtype}. Effect: safe_read. Collection response: data remains a list and the endpoint's documented pagination or limit defaults remain in effect. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count-only request, use the smallest valid page and read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "subscription.history", @@ -11093,7 +11093,7 @@ }, { "additionalProperties": false, - "description": "List active subscriptions. Method: GET. Path: /api/v1/subscribe/. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List active subscriptions. Method: GET. Path: /api/v1/subscribe/. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "subscription.list", @@ -11438,7 +11438,7 @@ }, { "additionalProperties": false, - "description": "Read aggregate contribution and reuse counts for subscription sharers. Method: GET. Path: /api/v1/subscribe/share/statistics. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "Read aggregate contribution and reuse counts for subscription sharers. Method: GET. Path: /api/v1/subscribe/share/statistics. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "subscription.share.statistics", @@ -11678,7 +11678,7 @@ }, { "additionalProperties": false, - "description": "List public subscriptions owned by one accessible MoviePilot username. Method: GET. Path: /api/v1/subscribe/user/{username}. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List public subscriptions owned by one accessible MoviePilot username. Method: GET. Path: /api/v1/subscribe/user/{username}. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "subscription.user.list", @@ -11750,7 +11750,7 @@ }, { "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. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "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. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "subtitle.search.media", @@ -11992,7 +11992,7 @@ }, { "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. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List approved built-in network-test targets without exposing their request URLs. Method: GET. Path: /api/v1/system/nettest/targets. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "system.network.targets", @@ -12225,7 +12225,7 @@ }, { "additionalProperties": false, - "description": "List available MoviePilot GitHub releases. Method: GET. Path: /api/v1/system/versions. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List available MoviePilot GitHub releases. Method: GET. Path: /api/v1/system/versions. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "system.versions", @@ -12514,7 +12514,7 @@ }, { "additionalProperties": false, - "description": "List file-transfer history with filters and pagination. Method: GET. Path: /api/v1/history/transfer. Effect: safe_read. Collection response: items stay in data.list and the exact total stays in data.total.", + "description": "List file-transfer history with filters and pagination. Method: GET. Path: /api/v1/history/transfer. Effect: safe_read. Collection response: items stay in data.list and the exact total stays in data.total. For a count-only request, use the smallest documented page and read that total; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "transfer.history", @@ -12806,7 +12806,7 @@ }, { "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. Collection response: items stay in data.items and the exact total stays in data.total.", + "description": "Page durable transfer tasks awaiting manual review or retry recovery. Method: GET. Path: /api/v1/transfer/tasks/manual-reviews. Effect: safe_read. Collection response: items stay in data.items and the exact total stays in data.total. For a count-only request, use the smallest documented page and read that total; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "transfer.manual_reviews", @@ -12898,7 +12898,7 @@ }, { "additionalProperties": false, - "description": "List items waiting in the file-transfer queue. Method: GET. Path: /api/v1/transfer/queue. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List items waiting in the file-transfer queue. Method: GET. Path: /api/v1/transfer/queue. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "transfer.queue", @@ -12996,7 +12996,7 @@ }, { "additionalProperties": false, - "description": "List built-in workflow action definitions and their parameter contracts. Method: GET. Path: /api/v1/workflow/actions. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List built-in workflow action definitions and their parameter contracts. Method: GET. Path: /api/v1/workflow/actions. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "workflow.actions", @@ -13105,7 +13105,7 @@ }, { "additionalProperties": false, - "description": "List event types that can trigger workflows. Method: GET. Path: /api/v1/workflow/event_types. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List event types that can trigger workflows. Method: GET. Path: /api/v1/workflow/event_types. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "workflow.event_types", @@ -13214,7 +13214,7 @@ }, { "additionalProperties": false, - "description": "List configured workflows and their execution state. Method: GET. Path: /api/v1/workflow/agent. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List configured workflows and their execution state. Method: GET. Path: /api/v1/workflow/agent. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "workflow.list", @@ -13340,7 +13340,7 @@ }, { "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. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count.", + "description": "List workflow actions contributed by installed plugins, optionally filtered by plugin ID. Method: GET. Path: /api/v1/workflow/plugin/actions. Effect: safe_read. Collection response: data remains a list; omit both page and count to preserve the legacy complete result. Successful gateway output adds collection.result_count and the exact collection.total_count. For a count or summary, send page=1 and count=1, then read collection.total_count; do not query the database merely because item data is truncated.", "properties": { "operation_id": { "const": "workflow.plugin.actions", diff --git a/app/agent/policy/mcp.py b/app/agent/policy/mcp.py index c6cd61058..3518a3193 100644 --- a/app/agent/policy/mcp.py +++ b/app/agent/policy/mcp.py @@ -1003,19 +1003,25 @@ def _collection_response_guidance(contract: Mapping[str, Any]) -> str: if contract.get("body_shape") == "page_object": return ( f" Collection response: items stay in {contract['items_field']} and the exact total " - f"stays in {contract['total_count_field']}." + f"stays in {contract['total_count_field']}. For a count-only request, use the smallest " + "documented page and read that total; do not query the database merely because item " + "data is truncated." ) if contract.get("total_count_field"): if contract.get("default_pagination") != "unpaginated": return ( " Collection response: data remains a list and the endpoint's documented " "pagination or limit defaults remain in effect. Successful gateway output adds " - "collection.result_count and the exact collection.total_count." + "collection.result_count and the exact collection.total_count. For a count-only " + "request, use the smallest valid page and read collection.total_count; do not " + "query the database merely because item data is truncated." ) return ( " Collection response: data remains a list; omit both page and count to preserve the " "legacy complete result. Successful gateway output adds collection.result_count and " - "the exact collection.total_count." + "the exact collection.total_count. For a count or summary, send page=1 and count=1, " + "then read collection.total_count; do not query the database merely because item data " + "is truncated." ) return ( " Collection response: data remains a list and successful gateway output adds " diff --git a/app/agent/prompt/System Core Prompt.txt b/app/agent/prompt/System Core Prompt.txt index ad590b17a..84f9b4075 100644 --- a/app/agent/prompt/System Core Prompt.txt +++ b/app/agent/prompt/System Core Prompt.txt @@ -66,6 +66,7 @@ You act as a proactive agent. Your goal is to fully resolve the user's media-rel - If `media.search` fails, fall back to `search_web` or `media.recognize`. Only ask the user when automated paths are exhausted. - If torrent search yields no useful result, check site scope, site health, and recognition quality before concluding that the resource is unavailable. - Reuse the latest torrent search cache through `search.results` before `download.add` instead of repeating the same search unnecessarily. +- For a MoviePilot collection count or summary, use the corresponding `moviepilot_api` list operation with the smallest documented page; optional legacy pagination should use `page=1,count=1`. Read the exact `collection.total_count` or existing `data.total`. Do not query the database merely because list items or a tool preview were truncated. If the operation does not expose a total, continue its native pagination or report that the upstream total is unavailable instead of guessing. - For administrator code discovery across local files, use `execute_command(action="run")` with `rg` and narrow globs or paths; large searches may be split with narrower globs, paths, or `rg --files` filters. Use `moviepilot_api` operation `storage.list` for one known local or remote storage directory, with its paging fields when more than the first page is needed. Always use `read_skill`, never `read_file`, to load a skill's SKILL.md; `read_skill` returns up to 512 KiB of the skill body and the relative paths of its supporting files in one call. If the body is truncated, do not use `read_file` to bypass the limit. Use `read_file` when an exact non-SKILL.md local file is known, including a supporting file listed by `read_skill`. If `read_file` reports truncation, continue with smaller `start_line` and `end_line` ranges instead of assuming the file ended. - Read the relevant file before changing it, then pick the editing tool by scope. Use `apply_patch` when one logical change spans multiple files, adds new files, or deletes files: submit a single patch wrapped in `*** Begin Patch` / `*** End Patch` with `*** Add File:`, `*** Update File:`, and `*** Delete File:` sections; every context and removed line must match the current content exactly, and the whole patch is validated before any file is written. Use `edit_file` for a single localized exact replacement within one already-read file; make `old_text` unique with enough surrounding context, and use `replace_all=true` only when every match must change. Use `write_file` for one standalone new file; set `overwrite=true` only for an intentional full rewrite, and use `read_file(include_metadata=true)` plus `expected_sha256` when preserving the previously read version matters. - When implementation depends on a Python or Node.js API, first identify the installed or locked dependency version from environment metadata, requirements, package manifests, lockfiles, local source, and type declarations. Use `rg` against the relevant package directory, `.venv`, or `node_modules` instead of scanning the entire project without bounds. If local evidence is insufficient, use `search_web` and then `browse_webpage` to read the matching version of the official documentation. Do not guess signatures from memory, mix examples from incompatible versions, or install a package only to inspect its API. diff --git a/app/agent/tools/impl/api.py b/app/agent/tools/impl/api.py index b10821eb7..37061dcef 100644 --- a/app/agent/tools/impl/api.py +++ b/app/agent/tools/impl/api.py @@ -72,8 +72,10 @@ class MoviePilotApiTool(MoviePilotTool): ] description: str = ( "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." + "parameters, and failure handling. For collection counts, use the smallest documented " + "page and read collection.total_count instead of querying the database after item " + "truncation. 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 diff --git a/docs/architecture/agent-tool-refactor-plan.md b/docs/architecture/agent-tool-refactor-plan.md index 37f817f2b..6c1ef9245 100644 --- a/docs/architecture/agent-tool-refactor-plan.md +++ b/docs/architecture/agent-tool-refactor-plan.md @@ -41,7 +41,7 @@ | MCP/HTTP 工具管理 | 存在旧业务工具与同名 first-wins 选择空间 | 与主 Agent 共用严格唯一新目录;重名直接以 TOOL_IDENTITY_AMBIGUOUS 失败 | | 退役代码 | 旧实现仍位于 app/agent/tools/impl | 77 个退役文件已直接删除,其中 72 个工具模块、5 个辅助模块 | | 架构图 | 982 个宿主模块、8,430 条内部依赖边 | 919 个宿主模块、7,688 条内部依赖边,Application/Chain 具体 Adapter 直连仍为 0 | -| 工作区状态 | 基线提交 871632af,与 origin/v3 对齐,初始工作区干净 | L1-L9 已完成;L10 正在把上一轮响应层兼容分页改为端点显式输入和数据库查询下推,当前基线 `91e33277f` 与 `origin/v3` 对齐 | +| 工作区状态 | 基线提交 871632af,与 origin/v3 对齐,初始工作区干净 | L1-L10 已完成并通过远端 CI;2026-09-01 继续修复集合总数在大结果截断预览中不可见的问题 | ## 3. 目标工具分层 @@ -357,4 +357,13 @@ action,并使用 MoviePilot 已配置的具体服务实例访问其自身 API - L10 正式完成:集合端点显式声明 `page/count`,数据库查询在 SQL 层完成筛选、稳定排序、分页与同条件精确计数;不传新增参数继续返回旧全量列表,外部来源没有总数时不伪造 - MoviePilot Agent 工具体系重构父目标至此全部完成,后续查询端点必须继续维护 REST、OpenAPI、MCP、Skill 和数据库查询合同的一致性 +### 2026-09-01:集合总数截断可见性修复 + +- Web Agent 实际调用 `subscription.list` 时未传分页,完整订阅列表超过通用工具结果 64KB 上限;此前网关把 `collection` 追加在 `data` 之后,截断预览只保留开头条目,导致 Agent 看不到已经由 API 返回的精确 `collection.total_count`,继而错误回退到数据库统计 +- 网关现将 `collection` 放在 `data` 前面,使 `X-Total-Count` 投影的精确总数在大结果预览中仍优先可见;列表响应和 REST `data` 结构均未改变 +- 核心提示、MCP oneOf、`moviepilot-api` Skill 和文档统一要求:数量或摘要查询使用最小分页窗口;兼容分页传 `page=1,count=1` 并读取 `collection.total_count`,不得因为条目或工具预览截断而改查数据库 +- `moviepilot-api` Skill 版本提升到 24;新增超大订阅结果回归,验证截断预览中总数早于 `data` 出现 +- 完整锁定测试已通过:4 个分片分别为 `1620 passed, 3 skipped`、`1860 passed, 4 skipped`、`1917 passed`、`2250 passed, 2 skipped`,合计 `7647 passed, 9 skipped`;变更文件 Pylint 为 `10.00/10`,Ruff、生成物幂等与 `git diff --check` 均通过 +- 当前未托管后端使用的 API 身份与工作区 CLI 配置不一致,真实 CLI 调用被既有 API key 校验拒绝;本轮未擅自重启正在运行的服务,运行进程需在代码部署并重载后再做在线会话复验 + 本文件作为本次重构的持续记录,保留阶段状态、实际变更、验证结果、提交状态与已知基线边界。 diff --git a/docs/mcp-api.md b/docs/mcp-api.md index 21b679eda..8cd74dd31 100644 --- a/docs/mcp-api.md +++ b/docs/mcp-api.md @@ -99,6 +99,7 @@ operation ID、权限、副作用、确认、恢复、结果敏感性及精确 - 数据库列表在查询层先应用授权范围和业务筛选,再执行稳定排序、`LIMIT/OFFSET` 和同条件精确 `COUNT`;不得先全表加载、响应后切片。纯内存、配置、缓存、文件系统或运行时列表可以在序列化边界切片。已有 `max_results` 等原生限量参数的接口继续保留其旧默认值,显式 `page/count` 的优先级由端点合同说明。 - REST 响应的 `data` 保持原列表结构,不改成 `{items,total}`。`X-Result-Count` 报告本次实际返回数量;仅当 MoviePilot 已经取得完整筛选结果时,才增加精确的 `X-Total-Count`。原有结构化分页接口继续在既有 `data.total` 与 `data.items` / `data.list` 中返回总数。 - `moviepilot_api` 把这些响应头投影为响应中的附加 `collection` 对象:`result_count` 为本次返回数量,`total_count` 仅在精确可知时出现,`page` / `count` 在可用时出现。`collection` 是附加元数据,不替换或改写 `data`。 +- Agent 仅查询数量或摘要时,应对支持精确总数的列表发送最小窗口;兼容分页接口使用 `page=1,count=1`,然后直接读取 `collection.total_count`。即使列表内容触发 64KB 工具预览截断,网关也会把 `collection` 放在 `data` 前面,确保总数仍可见;不得因为条目被截断就回退到数据库统计。 - 已经由第三方接口原生分页或限量、但上游没有返回总数的查询不会伪造 `total_count`;Agent 应以 `result_count` 判断当前页是否为空,并按原接口的分页参数继续读取。 ### `downloader_operation` / `mediaserver_operation` 调用形状 diff --git a/scripts/generate_agent_skill_docs.py b/scripts/generate_agent_skill_docs.py index ca05c1feb..4cda2a124 100644 --- a/scripts/generate_agent_skill_docs.py +++ b/scripts/generate_agent_skill_docs.py @@ -266,6 +266,7 @@ def _render_api_docs() -> str: "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 collection operations, `data` keeps its existing list or page-object shape. The gateway may add a sibling `collection` object with `result_count`, optional exact `total_count`, `page`, and `count`; it never replaces the list body with a new wrapper.", + "When a collection contract exposes an exact total, answer count or summary requests from that API metadata. For optional legacy pagination, send `page=1,count=1` and read `collection.total_count`; never query the database merely because item data or a tool preview was truncated.", "If an endpoint or external source does not expose a total, `collection.total_count` is omitted instead of being guessed from the current page.", "", ] @@ -298,13 +299,18 @@ def _render_api_docs() -> str: lines.append( "- `response`: `data` remains a list; omitting both `page` and `count` " "keeps the complete legacy result. `collection.result_count` reports the " - "returned items and `collection.total_count` reports the exact pre-pagination total." + "returned items and `collection.total_count` reports the exact pre-pagination " + "total. For counts or summaries, send `page=1,count=1`, read " + "`collection.total_count`, and do not fall back to a database query because " + "the item preview was truncated." ) else: lines.append( "- `response`: `data` remains a list and the endpoint's documented pagination " "or limit defaults remain in effect. `collection.result_count` reports the " - "returned items and `collection.total_count` reports the exact total." + "returned items and `collection.total_count` reports the exact total. For a " + "count-only request, use the smallest valid page and read that metadata instead " + "of querying the database after item truncation." ) else: lines.append( diff --git a/skills/moviepilot-api/SKILL.md b/skills/moviepilot-api/SKILL.md index 668ad5973..adadd6dbe 100644 --- a/skills/moviepilot-api/SKILL.md +++ b/skills/moviepilot-api/SKILL.md @@ -1,6 +1,6 @@ --- name: moviepilot-api -version: 23 +version: 24 description: >- Use this skill for MoviePilot product operations such as media search, torrent search, downloads, subscriptions, library checks, sites, storage, workflows, @@ -125,6 +125,24 @@ 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. +## Collection Counts And Pagination + +- For list inspection, explicitly send the operation's documented pagination + fields instead of requesting an unbounded legacy result. For optional legacy + pagination, start with `query={"page":1,"count":20}`. +- For a count or summary request when the operation documents an exact total, + send `query={"page":1,"count":1}` and read `collection.total_count`. This is + the authoritative count after the endpoint's authorization scope and filters. +- A large item list or `tool_result_truncated=true` does not make the total + unavailable. The gateway places `collection` before `data`, so its exact + metadata remains visible in the bounded preview. Never query the MoviePilot + database merely to recover a total already declared by the API contract. +- Use `database-operation` only for administrator diagnostics or aggregations + that the business API cannot express. Do not use it as a fallback for an API + list count. If an operation explicitly omits `collection.total_count`, do not + infer a total from one page; continue its native pagination or state that the + upstream total is unavailable. + ## Music Navigation - Search titles, albums, or artists with `media.search` using `type=music`. Preserve @@ -150,6 +168,7 @@ Call the gateway with this shape: 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 collection operations, `data` keeps its existing list or page-object shape. The gateway may add a sibling `collection` object with `result_count`, optional exact `total_count`, `page`, and `count`; it never replaces the list body with a new wrapper. +When a collection contract exposes an exact total, answer count or summary requests from that API metadata. For optional legacy pagination, send `page=1,count=1` and read `collection.total_count`; never query the database merely because item data or a tool preview was truncated. If an endpoint or external source does not expose a total, `collection.total_count` is omitted instead of being guessed from the current page. ### `config.identifiers.get` @@ -225,7 +244,7 @@ Purpose: Read current MoviePilot process and host memory utilization. ### `dashboard.network` `GET /api/v1/dashboard/network`; policy effect: `safe_read`. Purpose: Read the current host network receive and transmit counters. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -233,7 +252,7 @@ Purpose: Read the current host network receive and transmit counters. ### `dashboard.processes` `GET /api/v1/dashboard/processes`; policy effect: `safe_read`. Purpose: List host processes visible to the MoviePilot runtime. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -255,7 +274,7 @@ Purpose: Read MoviePilot host, runtime, platform, and uptime summary information ### `dashboard.transfer.statistics` `GET /api/v1/dashboard/transfer`; policy effect: `safe_read`. Purpose: Read aggregate file-transfer counts grouped by time period. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `days` (integer|null; default `7`): Recommendation time window in days.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -277,7 +296,7 @@ Purpose: Delete one exact managed database backup artifact. ### `database.backups.list` `GET /api/v1/system/database/backups`; policy effect: `safe_read`. Purpose: List managed database backup artifacts without exposing host paths. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -299,7 +318,7 @@ Purpose: Submit one torrent to MoviePilot's normal download workflow. ### `download.clients` `GET /api/v1/download/clients`; policy effect: `safe_read`. Purpose: List enabled downloader instance names and provider types without credentials. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -314,7 +333,7 @@ Purpose: Delete one MoviePilot download-history record. ### `download.history.list` `GET /api/v1/history/download`; policy effect: `safe_read`. Purpose: Page MoviePilot download-history records in reverse chronological order. -- `response`: `data` remains a list and the endpoint's documented pagination or limit defaults remain in effect. `collection.result_count` reports the returned items and `collection.total_count` reports the exact total. +- `response`: `data` remains a list and the endpoint's documented pagination or limit defaults remain in effect. `collection.result_count` reports the returned items and `collection.total_count` reports the exact total. For a count-only request, use the smallest valid page and read that metadata instead of querying the database after item truncation. - `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 @@ -322,7 +341,7 @@ Purpose: Page MoviePilot download-history records in reverse chronological order ### `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. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -330,7 +349,7 @@ Purpose: List configured downloader save-path URIs that may be passed to downloa ### `download.tasks.active` `GET /api/v1/download/`; policy effect: `safe_read`. Purpose: List currently downloading MoviePilot tasks with their canonical media context. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -451,7 +470,7 @@ Purpose: Read canonical media details from one selected metadata source. ### `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. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: `episode_group*` (string): TMDB episode-group identifier used for alternate episode ordering. - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -459,7 +478,7 @@ Purpose: List seasons defined by one exact TMDB episode-group identity. ### `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. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: `tmdbid*` (integer): TMDB media ID returned by media search or detail. - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -467,7 +486,7 @@ Purpose: List alternate TMDB episode groups available for one TV media identity. ### `media.episode_schedule` `GET /api/v1/tmdb/{tmdbid}/{season}`; policy effect: `safe_read`. Purpose: Read TMDB episode release information for one season. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `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`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `episode_group` (string|null): TMDB episode-group identifier used for alternate episode ordering.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -520,7 +539,7 @@ Purpose: Search canonical media across selected metadata sources. ### `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. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `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` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result.; `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 @@ -528,7 +547,7 @@ Purpose: List seasons for one exact media identity or a title-and-year fallback. ### `media.sources` `GET /api/v1/media/source`; policy effect: `safe_read`. Purpose: List metadata sources currently registered for MoviePilot media operations. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -694,7 +713,7 @@ Purpose: Install or update one plugin from an approved source. ### `plugin.installed` `GET /api/v1/plugin/`; policy effect: `safe_read`. Purpose: List installed plugins and their runtime status. -- `response`: `data` remains a list and the endpoint's documented pagination or limit defaults remain in effect. `collection.result_count` reports the returned items and `collection.total_count` reports the exact total. +- `response`: `data` remains a list and the endpoint's documented pagination or limit defaults remain in effect. `collection.result_count` reports the returned items and `collection.total_count` reports the exact total. For a count-only request, use the smallest valid page and read that metadata instead of querying the database after item truncation. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `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.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result.; `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 @@ -702,7 +721,7 @@ Purpose: List installed plugins and their runtime status. ### `plugin.market` `GET /api/v1/plugin/`; policy effect: `safe_read`. Purpose: List plugins available from configured marketplaces. -- `response`: `data` remains a list and the endpoint's documented pagination or limit defaults remain in effect. `collection.result_count` reports the returned items and `collection.total_count` reports the exact total. +- `response`: `data` remains a list and the endpoint's documented pagination or limit defaults remain in effect. `collection.result_count` reports the returned items and `collection.total_count` reports the exact total. For a count-only request, use the smallest valid page and read that metadata instead of querying the database after item truncation. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `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.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result.; `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 @@ -809,7 +828,7 @@ Purpose: Read personalized media or music recommendations. ### `scheduler.list` `GET /api/v1/dashboard/schedule`; policy effect: `safe_read`. Purpose: List registered scheduler jobs and their current state. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -853,7 +872,7 @@ Purpose: Search torrent sites directly from a free-form title and optional media ### `search.torrents` `GET /api/v1/search/media/{media_id}`; policy effect: `safe_read`. Purpose: Search torrent sites for one canonical media identity. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `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.; `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `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.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result.; `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 @@ -882,7 +901,7 @@ Purpose: Authenticate a supported site account and persist the resulting site au ### `site.category` `GET /api/v1/site/category/{site_id}`; policy effect: `safe_read`. Purpose: List torrent categories supported by one configured site. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: `site_id*` (integer): Persistent site ID returned by site.list. - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -911,7 +930,7 @@ Purpose: Delete one configured site by persistent site ID. ### `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. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result.; `status` (string(active,inactive,all); default `all`): Transfer success status used to filter history or describe a record. - `body`: none @@ -948,7 +967,7 @@ Purpose: Browse torrent resources from one configured site with category and key ### `site.rss` `GET /api/v1/site/rss`; policy effect: `safe_read`. Purpose: List configured sites selected for RSS subscription processing. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -956,7 +975,7 @@ Purpose: List configured sites selected for RSS subscription processing. ### `site.searchable` `GET /api/v1/site/media/{media_type}`; policy effect: `safe_read`. Purpose: List active configured sites supporting one exact media type. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: `media_type*` (string): MoviePilot media type used to filter recommendations or rule groups. - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -971,7 +990,7 @@ Purpose: Read account and traffic statistics for one exact configured site domai ### `site.statistics` `GET /api/v1/site/statistic`; policy effect: `safe_read`. Purpose: Read the latest account and traffic statistics for all configured sites. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -1000,7 +1019,7 @@ Purpose: Update one configured site's complete settings. ### `site.userdata` `GET /api/v1/site/userdata/{site_id}`; policy effect: `safe_read`. Purpose: Read the latest account statistics collected from one site. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: `site_id*` (integer): Persistent site ID returned by site.list. - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result.; `workdate` (string|null): Date used when retrieving one site's historical user statistics. - `body`: none @@ -1008,7 +1027,7 @@ Purpose: Read the latest account statistics collected from one site. ### `site.userdata.latest` `GET /api/v1/site/userdata/latest`; policy effect: `safe_read`. Purpose: Read the latest collected account statistics for every configured site. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -1023,7 +1042,7 @@ Purpose: Refresh and return account statistics for one configured site. ### `slash.list` `GET /api/v1/message/agent/commands`; policy effect: `safe_read`. Purpose: List slash commands that the Agent may dispatch. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -1045,7 +1064,7 @@ Purpose: Delete one exact file or directory from a configured storage provider. ### `storage.list` `POST /api/v1/storage/agent/list`; policy effect: `safe_read`. Purpose: List files or directories from one configured storage location. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `keyword` (string|null): Case-insensitive substring used to discover settings or filter storage entries.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result.; `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. @@ -1074,7 +1093,7 @@ Purpose: Rename one exact storage item, optionally applying media-aware recursiv ### `storage.settings` `GET /api/v1/storage/directories`; policy effect: `safe_read`. Purpose: Read configured directory or storage settings. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `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.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result.; `storage_type` (string; default `all`): Configured storage provider type to return. - `body`: none @@ -1131,7 +1150,7 @@ Purpose: Stop following one subscription-sharing user by exact share user ID. ### `subscription.follow.list` `GET /api/v1/subscribe/follow`; policy effect: `safe_read`. Purpose: List subscription-sharing user IDs followed by the current user. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -1153,7 +1172,7 @@ Purpose: Read one accessible subscription by persistent subscription ID. ### `subscription.history` `GET /api/v1/subscribe/history/{mtype}`; policy effect: `safe_read`. Purpose: List completed or archived subscription records. -- `response`: `data` remains a list and the endpoint's documented pagination or limit defaults remain in effect. `collection.result_count` reports the returned items and `collection.total_count` reports the exact total. +- `response`: `data` remains a list and the endpoint's documented pagination or limit defaults remain in effect. `collection.result_count` reports the returned items and `collection.total_count` reports the exact total. For a count-only request, use the smallest valid page and read that metadata instead of querying the database after item truncation. - `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 @@ -1168,7 +1187,7 @@ Purpose: Delete one accessible subscription-history record. ### `subscription.list` `GET /api/v1/subscribe/`; policy effect: `safe_read`. Purpose: List active subscriptions. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -1233,7 +1252,7 @@ Purpose: Delete one shared-subscription publication by share ID. ### `subscription.share.statistics` `GET /api/v1/subscribe/share/statistics`; policy effect: `safe_read`. Purpose: Read aggregate contribution and reuse counts for subscription sharers. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -1263,7 +1282,7 @@ Purpose: Update one existing movie, TV, or music subscription. ### `subscription.user.list` `GET /api/v1/subscribe/user/{username}`; policy effect: `safe_read`. Purpose: List public subscriptions owned by one accessible MoviePilot username. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: `username*` (string): MoviePilot or site username required by the selected operation. - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -1271,7 +1290,7 @@ Purpose: List public subscriptions owned by one accessible MoviePilot username. ### `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. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: `media_id*` (string): Source-native media ID. Always pair it with the exact media_source returned by search. - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `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.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result.; `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 @@ -1301,7 +1320,7 @@ Purpose: Run the built-in availability test for one loaded MoviePilot module. ### `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. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -1365,7 +1384,7 @@ Purpose: Read the installation version and runtime usage report available to the ### `system.versions` `GET /api/v1/system/versions`; policy effect: `safe_read`. Purpose: List available MoviePilot GitHub releases. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -1494,7 +1513,7 @@ Purpose: Preview the organized destination name for one source path and media id ### `transfer.queue` `GET /api/v1/transfer/queue`; policy effect: `safe_read`. Purpose: List items waiting in the file-transfer queue. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -1516,7 +1535,7 @@ Purpose: Resolve the configured transfer destination for supplied source storage ### `workflow.actions` `GET /api/v1/workflow/actions`; policy effect: `safe_read`. Purpose: List built-in workflow action definitions and their parameter contracts. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -1538,7 +1557,7 @@ Purpose: Delete one configured workflow by persistent workflow ID. ### `workflow.event_types` `GET /api/v1/workflow/event_types`; policy effect: `safe_read`. Purpose: List event types that can trigger workflows. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result. - `body`: none @@ -1560,7 +1579,7 @@ Purpose: Read one complete configured workflow definition. ### `workflow.list` `GET /api/v1/workflow/agent`; policy effect: `safe_read`. Purpose: List configured workflows and their execution state. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `name` (string|null): Human-readable name of the site, storage item, subscription, or rule group.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result.; `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 @@ -1575,7 +1594,7 @@ Purpose: Disable automatic execution of one configured workflow. ### `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. -- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. +- `response`: `data` remains a list; omitting both `page` and `count` keeps the complete legacy result. `collection.result_count` reports the returned items and `collection.total_count` reports the exact pre-pagination total. For counts or summaries, send `page=1,count=1`, read `collection.total_count`, and do not fall back to a database query because the item preview was truncated. - `path_params`: none - `query`: `count` (integer|null): Optional page size for a legacy full-list endpoint. Supplying page or count activates pagination; an omitted count then uses 50.; `page` (integer|null): Optional one-based page for a legacy full-list endpoint. Omit both page and count to keep the original unpaginated full result.; `plugin_id` (string): Exact installed or marketplace plugin ID. - `body`: none diff --git a/tests/test_agent_api_executor.py b/tests/test_agent_api_executor.py index 11629a588..0f0deb1f9 100644 --- a/tests/test_agent_api_executor.py +++ b/tests/test_agent_api_executor.py @@ -4,6 +4,7 @@ from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch from app.agent.api.executor import ApiExecutionContext, MoviePilotApiExecutor +from app.agent.tools.base import format_tool_result_for_agent def _execute_with_headers(headers: dict[str, str]) -> tuple[dict, AsyncMock]: @@ -77,3 +78,43 @@ def test_executor_keeps_non_collection_payload_unchanged_without_headers() -> No assert result == {"success": True, "message": "", "data": [{"id": 1}]} close.assert_awaited_once() + + +def test_executor_keeps_collection_total_visible_in_truncated_tool_preview() -> None: + """列表内容过大时,精确总数必须位于截断预览开头供 Agent 直接使用。""" + response = SimpleNamespace( + status_code=200, + headers={"X-Result-Count": "200", "X-Total-Count": "357"}, + json=lambda: { + "success": True, + "message": "", + "data": [{"title": "x" * 1024} for _ in range(100)], + }, + aclose=AsyncMock(), + ) + request = AsyncMock(return_value=response) + request_factory = MagicMock(return_value=SimpleNamespace(request=request)) + executor = MoviePilotApiExecutor( + context=ApiExecutionContext( + user_id="1", + username="admin", + is_admin=True, + ), + request_factory=request_factory, + ) + + with patch("app.agent.api.executor.create_access_token", return_value="token"): + raw_result = asyncio.run(executor.execute("subscription.list")) + + result = json.loads( + format_tool_result_for_agent(raw_result, tool_name="moviepilot_api") + ) + + assert result["tool_result_truncated"] is True + assert result["content_preview"].startswith( + '{"collection": {"result_count": 200, "total_count": 357}' + ) + assert result["content_preview"].index('"total_count": 357') < result[ + "content_preview" + ].index('"data"') + response.aclose.assert_awaited_once() diff --git a/tests/test_agent_api_gateway.py b/tests/test_agent_api_gateway.py index 8345a00c9..c40ae724b 100644 --- a/tests/test_agent_api_gateway.py +++ b/tests/test_agent_api_gateway.py @@ -137,6 +137,8 @@ def test_mcp_collection_contract_distinguishes_exact_and_unavailable_totals() -> "total_count_field": "collection.total_count", "default_pagination": "unpaginated", } + assert "page=1 and count=1" in subscription["description"] + assert "do not query the database" in subscription["description"] storage = branches["storage.list"] assert {"page", "count"}.issubset(storage["properties"]["query"]["properties"]) diff --git a/tests/test_builtin_skill_boundaries.py b/tests/test_builtin_skill_boundaries.py index 5d88451ac..91399460a 100644 --- a/tests/test_builtin_skill_boundaries.py +++ b/tests/test_builtin_skill_boundaries.py @@ -110,7 +110,7 @@ def test_modified_builtin_skills_have_incremented_versions() -> None: "command-dispatch": "2", "database-operation": "6", "feedback-issue": "9", - "moviepilot-api": "23", + "moviepilot-api": "24", "moviepilot-update": "5", "organize-files": "5", "transfer-failed-retry": "5", @@ -144,6 +144,18 @@ def test_core_prompt_requires_read_skill_for_skill_documents() -> None: assert "do not use `read_file` to bypass the limit" in core_prompt +def test_api_collection_counts_must_use_gateway_metadata_before_database() -> None: + """核心提示与 API Skill 必须阻止列表截断后错误回退数据库统计。""" + core_prompt = CORE_PROMPT_PATH.read_text(encoding="utf-8") + api_skill = _read_skill("moviepilot-api") + + assert "optional legacy pagination should use `page=1,count=1`" in core_prompt + assert "Do not query the database merely because" in core_prompt + assert 'send `query={"page":1,"count":1}`' in api_skill + assert "Never query the MoviePilot" in api_skill + assert "database merely to recover a total" in api_skill + + 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