From 8f6456be6c6c9938ca93aee88231d9a8e9afb45a Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 24 Aug 2026 01:40:39 +0800 Subject: [PATCH] refactor: unify discovery module contracts --- app/runtime/extensions/module/contracts.py | 22 ++ .../runtime-contract-baseline.json | 292 +++++++++++------- tests/test_module_method_contracts.py | 25 ++ 3 files changed, 229 insertions(+), 110 deletions(-) diff --git a/app/runtime/extensions/module/contracts.py b/app/runtime/extensions/module/contracts.py index ee273f972..fb2f63784 100644 --- a/app/runtime/extensions/module/contracts.py +++ b/app/runtime/extensions/module/contracts.py @@ -127,6 +127,17 @@ _METHOD_CONTRACTS = { "tvdb_info": ModuleMethodContract(family="tvdb", input_contract="TvdbInfoRequest", result_contract="dict | None", result_shape=ModuleResultShape.MAPPING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("tvdbid",)), "tvdb_slug": ModuleMethodContract(family="tvdb", input_contract="TvdbInfoRequest", result_contract="str | None", result_shape=ModuleResultShape.STRING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("tvdbid",)), "search_tvdb": ModuleMethodContract(family="tvdb", input_contract="TvdbSearchRequest", result_contract="list[dict]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("title",)), + "movie_hot": ModuleMethodContract(family="media-discovery", input_contract="MediaRankingRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("page", "count")), + "movie_showing": ModuleMethodContract(family="media-discovery", input_contract="MediaRankingRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("page", "count")), + "movie_top250": ModuleMethodContract(family="media-discovery", input_contract="MediaRankingRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("page", "count")), + "search_collections": ModuleMethodContract(family="media-discovery", input_contract="CollectionSearchRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("name", "media_source")), + "search_persons": ModuleMethodContract(family="media-discovery", input_contract="PersonSearchRequest", result_contract="list[MediaPerson]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("name", "media_source")), + "search_subtitles": ModuleMethodContract(family="media-discovery", input_contract="SubtitleSearchRequest", result_contract="list[SubtitleInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("site", "keyword", "page")), + "search_torrents": ModuleMethodContract(family="media-discovery", input_contract="TorrentSearchRequest", result_contract="list[TorrentInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("site", "keyword", "mtype", "page")), + "tv_animation": ModuleMethodContract(family="media-discovery", input_contract="MediaRankingRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("page", "count")), + "tv_hot": ModuleMethodContract(family="media-discovery", input_contract="MediaRankingRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("page", "count")), + "tv_weekly_chinese": ModuleMethodContract(family="media-discovery", input_contract="MediaRankingRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("page", "count")), + "tv_weekly_global": ModuleMethodContract(family="media-discovery", input_contract="MediaRankingRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("page", "count")), "send_message": ModuleMethodContract(family="messaging", input_contract="MessageSendRequest", result_contract="Message | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY), "finalize_message": ModuleMethodContract(family="messaging", input_contract="MessageFinalizeRequest", result_contract="Message | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("response",)), "register_commands": ModuleMethodContract(family="messaging", input_contract="CommandRegistrationRequest", result_contract="None", required_parameters=("commands",)), @@ -162,6 +173,17 @@ _METHOD_CONTRACTS.update({ "async_recognize_media": _METHOD_CONTRACTS["recognize_media"], "async_search_medias": _METHOD_CONTRACTS["search_medias"], "async_obtain_images": _METHOD_CONTRACTS["obtain_images"], + "async_movie_hot": _METHOD_CONTRACTS["movie_hot"], + "async_movie_showing": _METHOD_CONTRACTS["movie_showing"], + "async_movie_top250": _METHOD_CONTRACTS["movie_top250"], + "async_search_collections": _METHOD_CONTRACTS["search_collections"], + "async_search_persons": _METHOD_CONTRACTS["search_persons"], + "async_search_subtitles": _METHOD_CONTRACTS["search_subtitles"], + "async_search_torrents": _METHOD_CONTRACTS["search_torrents"], + "async_tv_animation": _METHOD_CONTRACTS["tv_animation"], + "async_tv_hot": _METHOD_CONTRACTS["tv_hot"], + "async_tv_weekly_chinese": _METHOD_CONTRACTS["tv_weekly_chinese"], + "async_tv_weekly_global": _METHOD_CONTRACTS["tv_weekly_global"], }) _PREFIX_CONTRACTS = ( diff --git a/tests/fixtures/architecture/runtime-contract-baseline.json b/tests/fixtures/architecture/runtime-contract-baseline.json index d3b8c6ecd..5bd4ecb10 100644 --- a/tests/fixtures/architecture/runtime-contract-baseline.json +++ b/tests/fixtures/architecture/runtime-contract-baseline.json @@ -2927,48 +2927,57 @@ "version": 1 }, "async_movie_hot": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "MediaRankingRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "page" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "async_movie_showing": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "MediaRankingRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "page" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "async_movie_top250": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "MediaRankingRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "page" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", @@ -3032,16 +3041,19 @@ "version": 1 }, "async_search_collections": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "CollectionSearchRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "media_source", + "name" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", @@ -3067,48 +3079,60 @@ "version": 1 }, "async_search_persons": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "PersonSearchRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "media_source", + "name" + ], + "result_contract": "list[MediaPerson]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "async_search_subtitles": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "SubtitleSearchRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "keyword", + "page", + "site" + ], + "result_contract": "list[SubtitleInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "async_search_torrents": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "TorrentSearchRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "keyword", + "mtype", + "page", + "site" + ], + "result_contract": "list[TorrentInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", @@ -3355,64 +3379,76 @@ "version": 1 }, "async_tv_animation": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "MediaRankingRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "page" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "async_tv_hot": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "MediaRankingRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "page" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "async_tv_weekly_chinese": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "MediaRankingRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "page" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "async_tv_weekly_global": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "MediaRankingRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "page" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", @@ -4726,48 +4762,57 @@ "version": 1 }, "movie_hot": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "MediaRankingRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "page" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "movie_showing": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "MediaRankingRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "page" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "movie_top250": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "MediaRankingRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "page" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", @@ -5175,16 +5220,19 @@ "version": 1 }, "search_collections": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "CollectionSearchRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "media_source", + "name" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", @@ -5226,48 +5274,60 @@ "version": 1 }, "search_persons": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "PersonSearchRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "media_source", + "name" + ], + "result_contract": "list[MediaPerson]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "search_subtitles": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "SubtitleSearchRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "keyword", + "page", + "site" + ], + "result_contract": "list[SubtitleInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "search_torrents": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "TorrentSearchRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "keyword", + "mtype", + "page", + "site" + ], + "result_contract": "list[TorrentInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", @@ -5795,64 +5855,76 @@ "version": 1 }, "tv_animation": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "MediaRankingRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "page" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "tv_hot": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "MediaRankingRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "page" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "tv_weekly_chinese": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "MediaRankingRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "page" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "tv_weekly_global": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "media-discovery", - "input_contract": "MediaDiscoveryKeywordArguments", + "input_contract": "MediaRankingRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MediaDiscoveryProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "page" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", diff --git a/tests/test_module_method_contracts.py b/tests/test_module_method_contracts.py index 762ac453b..3d952b918 100644 --- a/tests/test_module_method_contracts.py +++ b/tests/test_module_method_contracts.py @@ -303,6 +303,31 @@ def test_sync_and_async_media_capabilities_share_contracts() -> None: ) +def test_sync_and_async_discovery_lists_share_contracts() -> None: + """媒体榜单与搜索的同步、异步入口必须共享有序列表契约。""" + sync_methods = ( + "movie_hot", + "movie_showing", + "movie_top250", + "search_collections", + "search_persons", + "search_subtitles", + "search_torrents", + "tv_animation", + "tv_hot", + "tv_weekly_chinese", + "tv_weekly_global", + ) + + for sync_method in sync_methods: + async_method = f"async_{sync_method}" + contract = get_module_method_contract(sync_method) + assert contract is get_module_method_contract(async_method) + assert contract.aggregation is ModuleResultAggregation.ORDERED_LIST_MERGE + assert contract.result_shape is ModuleResultShape.LIST + assert contract.required_parameters + + def test_attachment_result_diagnostics_distinguish_bytes_and_strings() -> None: """附件契约应区分二进制内容和可展示字符串,偏差仍仅供诊断。""" assert diagnose_module_result("download_qq_file_bytes", b"content") == ()