From ce277320648e955e9455d184bb83770225a62b5f Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 24 Aug 2026 01:42:34 +0800 Subject: [PATCH] refactor: unify douban module contracts --- app/runtime/extensions/module/contracts.py | 16 ++ .../runtime-contract-baseline.json | 202 +++++++++++------- tests/test_module_method_contracts.py | 24 +++ 3 files changed, 164 insertions(+), 78 deletions(-) diff --git a/app/runtime/extensions/module/contracts.py b/app/runtime/extensions/module/contracts.py index fb2f63784..4b862ecbd 100644 --- a/app/runtime/extensions/module/contracts.py +++ b/app/runtime/extensions/module/contracts.py @@ -138,6 +138,14 @@ _METHOD_CONTRACTS = { "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")), + "douban_discover": ModuleMethodContract(family="douban", input_contract="DoubanDiscoverRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("mtype", "sort", "tags", "page", "count")), + "douban_info": ModuleMethodContract(family="douban", input_contract="DoubanInfoRequest", result_contract="dict | None", result_shape=ModuleResultShape.MAPPING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("doubanid", "mtype", "raise_exception")), + "douban_movie_credits": ModuleMethodContract(family="douban", input_contract="DoubanMediaRequest", result_contract="list[MediaPerson]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("doubanid",)), + "douban_movie_recommend": ModuleMethodContract(family="douban", input_contract="DoubanMediaRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("doubanid",)), + "douban_person_credits": ModuleMethodContract(family="douban", input_contract="DoubanPersonCreditsRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("person_id", "page")), + "douban_person_detail": ModuleMethodContract(family="douban", input_contract="DoubanPersonRequest", result_contract="MediaPerson | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("person_id",)), + "douban_tv_credits": ModuleMethodContract(family="douban", input_contract="DoubanMediaRequest", result_contract="list[MediaPerson]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("doubanid",)), + "douban_tv_recommend": ModuleMethodContract(family="douban", input_contract="DoubanMediaRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("doubanid",)), "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",)), @@ -184,6 +192,14 @@ _METHOD_CONTRACTS.update({ "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"], + "async_douban_discover": _METHOD_CONTRACTS["douban_discover"], + "async_douban_info": _METHOD_CONTRACTS["douban_info"], + "async_douban_movie_credits": _METHOD_CONTRACTS["douban_movie_credits"], + "async_douban_movie_recommend": _METHOD_CONTRACTS["douban_movie_recommend"], + "async_douban_person_credits": _METHOD_CONTRACTS["douban_person_credits"], + "async_douban_person_detail": _METHOD_CONTRACTS["douban_person_detail"], + "async_douban_tv_credits": _METHOD_CONTRACTS["douban_tv_credits"], + "async_douban_tv_recommend": _METHOD_CONTRACTS["douban_tv_recommend"], }) _PREFIX_CONTRACTS = ( diff --git a/tests/fixtures/architecture/runtime-contract-baseline.json b/tests/fixtures/architecture/runtime-contract-baseline.json index 5bd4ecb10..60a15bff9 100644 --- a/tests/fixtures/architecture/runtime-contract-baseline.json +++ b/tests/fixtures/architecture/runtime-contract-baseline.json @@ -2735,95 +2735,114 @@ "version": 1 }, "async_douban_discover": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "douban", - "input_contract": "DoubanKeywordArguments", + "input_contract": "DoubanDiscoverRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "DoubanProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "mtype", + "page", + "sort", + "tags" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "async_douban_info": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "douban", - "input_contract": "DoubanKeywordArguments", + "input_contract": "DoubanInfoRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "DoubanProviderResult", - "result_shape": "any", + "required_parameters": [ + "doubanid", + "mtype", + "raise_exception" + ], + "result_contract": "dict | None", + "result_shape": "mapping", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "async_douban_movie_credits": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "douban", - "input_contract": "DoubanKeywordArguments", + "input_contract": "DoubanMediaRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "DoubanProviderResult", - "result_shape": "any", + "required_parameters": [ + "doubanid" + ], + "result_contract": "list[MediaPerson]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "async_douban_movie_recommend": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "douban", - "input_contract": "DoubanKeywordArguments", + "input_contract": "DoubanMediaRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "DoubanProviderResult", - "result_shape": "any", + "required_parameters": [ + "doubanid" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "async_douban_person_credits": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "douban", - "input_contract": "DoubanKeywordArguments", + "input_contract": "DoubanPersonCreditsRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "DoubanProviderResult", - "result_shape": "any", + "required_parameters": [ + "page", + "person_id" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "async_douban_person_detail": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "douban", - "input_contract": "DoubanKeywordArguments", + "input_contract": "DoubanPersonRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "DoubanProviderResult", + "required_parameters": [ + "person_id" + ], + "result_contract": "MediaPerson | None", "result_shape": "any", "supports_async": true, "supports_sync": true, @@ -2831,32 +2850,36 @@ "version": 1 }, "async_douban_tv_credits": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "douban", - "input_contract": "DoubanKeywordArguments", + "input_contract": "DoubanMediaRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "DoubanProviderResult", - "result_shape": "any", + "required_parameters": [ + "doubanid" + ], + "result_contract": "list[MediaPerson]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "async_douban_tv_recommend": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "douban", - "input_contract": "DoubanKeywordArguments", + "input_contract": "DoubanMediaRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "DoubanProviderResult", - "result_shape": "any", + "required_parameters": [ + "doubanid" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", @@ -3668,95 +3691,114 @@ "version": 1 }, "douban_discover": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "douban", - "input_contract": "DoubanKeywordArguments", + "input_contract": "DoubanDiscoverRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "DoubanProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "mtype", + "page", + "sort", + "tags" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "douban_info": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "douban", - "input_contract": "DoubanKeywordArguments", + "input_contract": "DoubanInfoRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "DoubanProviderResult", - "result_shape": "any", + "required_parameters": [ + "doubanid", + "mtype", + "raise_exception" + ], + "result_contract": "dict | None", + "result_shape": "mapping", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "douban_movie_credits": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "douban", - "input_contract": "DoubanKeywordArguments", + "input_contract": "DoubanMediaRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "DoubanProviderResult", - "result_shape": "any", + "required_parameters": [ + "doubanid" + ], + "result_contract": "list[MediaPerson]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "douban_movie_recommend": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "douban", - "input_contract": "DoubanKeywordArguments", + "input_contract": "DoubanMediaRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "DoubanProviderResult", - "result_shape": "any", + "required_parameters": [ + "doubanid" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "douban_person_credits": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "douban", - "input_contract": "DoubanKeywordArguments", + "input_contract": "DoubanPersonCreditsRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "DoubanProviderResult", - "result_shape": "any", + "required_parameters": [ + "page", + "person_id" + ], + "result_contract": "list[MediaInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "douban_person_detail": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "douban", - "input_contract": "DoubanKeywordArguments", + "input_contract": "DoubanPersonRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "DoubanProviderResult", + "required_parameters": [ + "person_id" + ], + "result_contract": "MediaPerson | None", "result_shape": "any", "supports_async": true, "supports_sync": true, @@ -3764,32 +3806,36 @@ "version": 1 }, "douban_tv_credits": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "douban", - "input_contract": "DoubanKeywordArguments", + "input_contract": "DoubanMediaRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "DoubanProviderResult", - "result_shape": "any", + "required_parameters": [ + "doubanid" + ], + "result_contract": "list[MediaPerson]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "douban_tv_recommend": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "douban", - "input_contract": "DoubanKeywordArguments", + "input_contract": "DoubanMediaRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "DoubanProviderResult", - "result_shape": "any", + "required_parameters": [ + "doubanid" + ], + "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 3d952b918..508cf6f09 100644 --- a/tests/test_module_method_contracts.py +++ b/tests/test_module_method_contracts.py @@ -328,6 +328,30 @@ def test_sync_and_async_discovery_lists_share_contracts() -> None: assert contract.required_parameters +def test_sync_and_async_douban_capabilities_share_contracts() -> None: + """Douban 同步与异步 provider 应共享参数和结果聚合契约。""" + list_methods = { + "douban_discover", + "douban_movie_credits", + "douban_movie_recommend", + "douban_person_credits", + "douban_tv_credits", + "douban_tv_recommend", + } + value_methods = {"douban_info", "douban_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}") + assert contract.required_parameters + 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") == ()