From 488b3667bbc9b521cfaea6c684e1f2c37b1e6090 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 24 Aug 2026 01:45:49 +0800 Subject: [PATCH] refactor: unify anilist module contracts --- app/runtime/extensions/module/contracts.py | 16 ++ .../runtime-contract-baseline.json | 196 +++++++++++------- tests/test_module_method_contracts.py | 23 ++ 3 files changed, 159 insertions(+), 76 deletions(-) diff --git a/app/runtime/extensions/module/contracts.py b/app/runtime/extensions/module/contracts.py index 9496065bf..d97f7b556 100644 --- a/app/runtime/extensions/module/contracts.py +++ b/app/runtime/extensions/module/contracts.py @@ -153,6 +153,14 @@ _METHOD_CONTRACTS = { "bangumi_person_credits": ModuleMethodContract(family="bangumi", input_contract="BangumiPersonRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("person_id",)), "bangumi_person_detail": ModuleMethodContract(family="bangumi", input_contract="BangumiPersonRequest", result_contract="MediaPerson | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("person_id",)), "bangumi_recommend": ModuleMethodContract(family="bangumi", input_contract="BangumiMediaRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("bangumiid",)), + "anilist_credits": ModuleMethodContract(family="anilist", input_contract="AniListMediaPageRequest", result_contract="list[MediaPerson]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("anilist_id", "page", "count")), + "anilist_discover": ModuleMethodContract(family="anilist", input_contract="AniListDiscoverArguments", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE), + "anilist_info": ModuleMethodContract(family="anilist", input_contract="AniListMediaRequest", result_contract="dict | None", result_shape=ModuleResultShape.MAPPING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("anilist_id",)), + "anilist_person_credits": ModuleMethodContract(family="anilist", input_contract="AniListPersonPageRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("person_id", "page", "count")), + "anilist_person_detail": ModuleMethodContract(family="anilist", input_contract="AniListPersonRequest", result_contract="MediaPerson | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("person_id",)), + "anilist_popular_this_season": ModuleMethodContract(family="anilist", input_contract="AniListPageRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("page", "count")), + "anilist_recommendations": ModuleMethodContract(family="anilist", input_contract="AniListMediaPageRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("anilist_id", "page", "count")), + "anilist_trending": ModuleMethodContract(family="anilist", input_contract="AniListPageRequest", 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",)), @@ -214,6 +222,14 @@ _METHOD_CONTRACTS.update({ "async_bangumi_person_credits": _METHOD_CONTRACTS["bangumi_person_credits"], "async_bangumi_person_detail": _METHOD_CONTRACTS["bangumi_person_detail"], "async_bangumi_recommend": _METHOD_CONTRACTS["bangumi_recommend"], + "async_anilist_credits": _METHOD_CONTRACTS["anilist_credits"], + "async_anilist_discover": _METHOD_CONTRACTS["anilist_discover"], + "async_anilist_info": _METHOD_CONTRACTS["anilist_info"], + "async_anilist_person_credits": _METHOD_CONTRACTS["anilist_person_credits"], + "async_anilist_person_detail": _METHOD_CONTRACTS["anilist_person_detail"], + "async_anilist_popular_this_season": _METHOD_CONTRACTS["anilist_popular_this_season"], + "async_anilist_recommendations": _METHOD_CONTRACTS["anilist_recommendations"], + "async_anilist_trending": _METHOD_CONTRACTS["anilist_trending"], }) _PREFIX_CONTRACTS = ( diff --git a/tests/fixtures/architecture/runtime-contract-baseline.json b/tests/fixtures/architecture/runtime-contract-baseline.json index 36558f162..4483fe053 100644 --- a/tests/fixtures/architecture/runtime-contract-baseline.json +++ b/tests/fixtures/architecture/runtime-contract-baseline.json @@ -2348,79 +2348,91 @@ }, "module_method_specs": { "anilist_credits": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "anilist", - "input_contract": "AniListKeywordArguments", + "input_contract": "AniListMediaPageRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "AniListProviderResult", - "result_shape": "any", + "required_parameters": [ + "anilist_id", + "count", + "page" + ], + "result_contract": "list[MediaPerson]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "anilist_discover": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "anilist", - "input_contract": "AniListKeywordArguments", + "input_contract": "AniListDiscoverArguments", "plugin_short_circuit": true, "public_to_plugins": true, "required_parameters": [], - "result_contract": "AniListProviderResult", - "result_shape": "any", + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "anilist_info": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "anilist", - "input_contract": "AniListKeywordArguments", + "input_contract": "AniListMediaRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "AniListProviderResult", - "result_shape": "any", + "required_parameters": [ + "anilist_id" + ], + "result_contract": "dict | None", + "result_shape": "mapping", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "anilist_person_credits": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "anilist", - "input_contract": "AniListKeywordArguments", + "input_contract": "AniListPersonPageRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "AniListProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "page", + "person_id" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "anilist_person_detail": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "anilist", - "input_contract": "AniListKeywordArguments", + "input_contract": "AniListPersonRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "AniListProviderResult", + "required_parameters": [ + "person_id" + ], + "result_contract": "MediaPerson | None", "result_shape": "any", "supports_async": true, "supports_sync": true, @@ -2428,48 +2440,58 @@ "version": 1 }, "anilist_popular_this_season": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "anilist", - "input_contract": "AniListKeywordArguments", + "input_contract": "AniListPageRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "AniListProviderResult", - "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 }, "anilist_recommendations": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "anilist", - "input_contract": "AniListKeywordArguments", + "input_contract": "AniListMediaPageRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "AniListProviderResult", - "result_shape": "any", + "required_parameters": [ + "anilist_id", + "count", + "page" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "anilist_trending": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "anilist", - "input_contract": "AniListKeywordArguments", + "input_contract": "AniListPageRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "AniListProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "page" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", @@ -2495,79 +2517,91 @@ "version": 1 }, "async_anilist_credits": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "anilist", - "input_contract": "AniListKeywordArguments", + "input_contract": "AniListMediaPageRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "AniListProviderResult", - "result_shape": "any", + "required_parameters": [ + "anilist_id", + "count", + "page" + ], + "result_contract": "list[MediaPerson]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "async_anilist_discover": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "anilist", - "input_contract": "AniListKeywordArguments", + "input_contract": "AniListDiscoverArguments", "plugin_short_circuit": true, "public_to_plugins": true, "required_parameters": [], - "result_contract": "AniListProviderResult", - "result_shape": "any", + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "async_anilist_info": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "anilist", - "input_contract": "AniListKeywordArguments", + "input_contract": "AniListMediaRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "AniListProviderResult", - "result_shape": "any", + "required_parameters": [ + "anilist_id" + ], + "result_contract": "dict | None", + "result_shape": "mapping", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "async_anilist_person_credits": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "anilist", - "input_contract": "AniListKeywordArguments", + "input_contract": "AniListPersonPageRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "AniListProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "page", + "person_id" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "async_anilist_person_detail": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "anilist", - "input_contract": "AniListKeywordArguments", + "input_contract": "AniListPersonRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "AniListProviderResult", + "required_parameters": [ + "person_id" + ], + "result_contract": "MediaPerson | None", "result_shape": "any", "supports_async": true, "supports_sync": true, @@ -2575,48 +2609,58 @@ "version": 1 }, "async_anilist_popular_this_season": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "anilist", - "input_contract": "AniListKeywordArguments", + "input_contract": "AniListPageRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "AniListProviderResult", - "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_anilist_recommendations": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "anilist", - "input_contract": "AniListKeywordArguments", + "input_contract": "AniListMediaPageRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "AniListProviderResult", - "result_shape": "any", + "required_parameters": [ + "anilist_id", + "count", + "page" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "async_anilist_trending": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "anilist", - "input_contract": "AniListKeywordArguments", + "input_contract": "AniListPageRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "AniListProviderResult", - "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 41d364355..bd2000abd 100644 --- a/tests/test_module_method_contracts.py +++ b/tests/test_module_method_contracts.py @@ -374,6 +374,29 @@ def test_sync_and_async_bangumi_capabilities_share_contracts() -> None: assert contract.aggregation is expected +def test_sync_and_async_anilist_capabilities_share_contracts() -> None: + """AniList 同步与异步 provider 应共享列表或首值契约。""" + list_methods = { + "anilist_credits", + "anilist_discover", + "anilist_person_credits", + "anilist_popular_this_season", + "anilist_recommendations", + "anilist_trending", + } + value_methods = {"anilist_info", "anilist_person_detail"} + + for sync_method in list_methods | value_methods: + contract = get_module_method_contract(sync_method) + assert contract is get_module_method_contract(f"async_{sync_method}") + expected = ( + ModuleResultAggregation.ORDERED_LIST_MERGE + if sync_method in list_methods + else ModuleResultAggregation.FIRST_NON_EMPTY + ) + assert contract.aggregation is expected + + def test_attachment_result_diagnostics_distinguish_bytes_and_strings() -> None: """附件契约应区分二进制内容和可展示字符串,偏差仍仅供诊断。""" assert diagnose_module_result("download_qq_file_bytes", b"content") == ()