diff --git a/app/runtime/extensions/module/contracts.py b/app/runtime/extensions/module/contracts.py index b10e7ad73..dcd782fb7 100644 --- a/app/runtime/extensions/module/contracts.py +++ b/app/runtime/extensions/module/contracts.py @@ -176,6 +176,20 @@ _METHOD_CONTRACTS = { "tmdb_tv_credits": ModuleMethodContract(family="tmdb", input_contract="TmdbMediaPageRequest", result_contract="list[MediaPerson]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("tmdbid", "page")), "tmdb_tv_recommend": ModuleMethodContract(family="tmdb", input_contract="TmdbMediaRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("tmdbid",)), "tmdb_tv_similar": ModuleMethodContract(family="tmdb", input_contract="TmdbMediaRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("tmdbid",)), + "identify_music_by_fingerprint": ModuleMethodContract(family="music", input_contract="MusicFingerprintRequest", result_contract="str | None", result_shape=ModuleResultShape.STRING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("path",)), + "match_music_album": ModuleMethodContract(family="music", input_contract="MusicAlbumMatchRequest", result_contract="MusicAlbumInfo | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("meta", "tracks", "limit")), + "music_album": ModuleMethodContract(family="music", input_contract="MusicIdentityRequest", result_contract="MusicAlbumInfo | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("media_source", "media_id")), + "music_album_related": ModuleMethodContract(family="music", input_contract="MusicRelatedRequest", result_contract="list[MusicInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("media_source", "media_id", "count")), + "music_artist": ModuleMethodContract(family="music", input_contract="MusicIdentityRequest", result_contract="MusicArtistInfo | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("media_source", "media_id")), + "music_artist_albums": ModuleMethodContract(family="music", input_contract="MusicArtistAlbumsRequest", result_contract="list[MusicInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("media_source", "media_id", "page", "count", "album_type")), + "music_artist_related": ModuleMethodContract(family="music", input_contract="MusicRelatedRequest", result_contract="list[MusicArtistInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("media_source", "media_id", "count")), + "music_cache_delete": ModuleMethodContract(family="music", input_contract="MusicCacheDeleteRequest", result_contract="dict | None", result_shape=ModuleResultShape.MAPPING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("cache_key",)), + "music_cache_items": ModuleMethodContract(family="music", input_contract="MusicCacheReadRequest", result_contract="list[dict]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE), + "music_chart": ModuleMethodContract(family="music", input_contract="MusicChartRequest", result_contract="list[MusicInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("range_name", "offset", "count", "entity")), + "music_discover": ModuleMethodContract(family="music", input_contract="MusicDiscoverRequest", result_contract="list[MusicInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("media_source", "page", "count", "entity", "mode", "tags", "sort")), + "music_fresh_releases": ModuleMethodContract(family="music", input_contract="MusicFreshReleasesRequest", result_contract="list[MusicInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("days", "sort", "past", "future", "offset", "count")), + "music_lyrics": ModuleMethodContract(family="music", input_contract="MusicLyricsRequest", result_contract="MusicLyrics | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("music",)), + "search_music": ModuleMethodContract(family="music", input_contract="MusicSearchRequest", result_contract="list[MusicInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("meta", "limit", "media_source")), "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",)), @@ -260,6 +274,8 @@ _METHOD_CONTRACTS.update({ "async_tmdb_tv_credits": _METHOD_CONTRACTS["tmdb_tv_credits"], "async_tmdb_tv_recommend": _METHOD_CONTRACTS["tmdb_tv_recommend"], "async_tmdb_tv_similar": _METHOD_CONTRACTS["tmdb_tv_similar"], + "async_identify_music_by_fingerprint": _METHOD_CONTRACTS["identify_music_by_fingerprint"], + "async_match_music_album": _METHOD_CONTRACTS["match_music_album"], }) _PREFIX_CONTRACTS = ( diff --git a/tests/fixtures/architecture/runtime-contract-baseline.json b/tests/fixtures/architecture/runtime-contract-baseline.json index eaea11ef8..e971db9ef 100644 --- a/tests/fixtures/architecture/runtime-contract-baseline.json +++ b/tests/fixtures/architecture/runtime-contract-baseline.json @@ -2940,16 +2940,18 @@ "version": 1 }, "async_identify_music_by_fingerprint": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "music", - "input_contract": "MusicKeywordArguments", + "input_contract": "MusicFingerprintRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MusicProviderResult", - "result_shape": "any", + "required_parameters": [ + "path" + ], + "result_contract": "str | None", + "result_shape": "string", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", @@ -2972,15 +2974,19 @@ "version": 1 }, "async_match_music_album": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "music", - "input_contract": "MusicKeywordArguments", + "input_contract": "MusicAlbumMatchRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MusicProviderResult", + "required_parameters": [ + "limit", + "meta", + "tracks" + ], + "result_contract": "MusicAlbumInfo | None", "result_shape": "any", "supports_async": true, "supports_sync": true, @@ -4420,16 +4426,18 @@ "version": 1 }, "identify_music_by_fingerprint": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "music", - "input_contract": "MusicKeywordArguments", + "input_contract": "MusicFingerprintRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MusicProviderResult", - "result_shape": "any", + "required_parameters": [ + "path" + ], + "result_contract": "str | None", + "result_shape": "string", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", @@ -4540,15 +4548,19 @@ "version": 1 }, "match_music_album": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "music", - "input_contract": "MusicKeywordArguments", + "input_contract": "MusicAlbumMatchRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MusicProviderResult", + "required_parameters": [ + "limit", + "meta", + "tracks" + ], + "result_contract": "MusicAlbumInfo | None", "result_shape": "any", "supports_async": true, "supports_sync": true, @@ -4975,15 +4987,18 @@ "version": 1 }, "music_album": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "music", - "input_contract": "MusicKeywordArguments", + "input_contract": "MusicIdentityRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MusicProviderResult", + "required_parameters": [ + "media_id", + "media_source" + ], + "result_contract": "MusicAlbumInfo | None", "result_shape": "any", "supports_async": true, "supports_sync": true, @@ -4991,31 +5006,38 @@ "version": 1 }, "music_album_related": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "music", - "input_contract": "MusicKeywordArguments", + "input_contract": "MusicRelatedRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MusicProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "media_id", + "media_source" + ], + "result_contract": "list[MusicInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "music_artist": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "music", - "input_contract": "MusicKeywordArguments", + "input_contract": "MusicIdentityRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MusicProviderResult", + "required_parameters": [ + "media_id", + "media_source" + ], + "result_contract": "MusicArtistInfo | None", "result_shape": "any", "supports_async": true, "supports_sync": true, @@ -5023,32 +5045,42 @@ "version": 1 }, "music_artist_albums": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "music", - "input_contract": "MusicKeywordArguments", + "input_contract": "MusicArtistAlbumsRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MusicProviderResult", - "result_shape": "any", + "required_parameters": [ + "album_type", + "count", + "media_id", + "media_source", + "page" + ], + "result_contract": "list[MusicInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "music_artist_related": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "music", - "input_contract": "MusicKeywordArguments", + "input_contract": "MusicRelatedRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MusicProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "media_id", + "media_source" + ], + "result_contract": "list[MusicArtistInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", @@ -5071,95 +5103,119 @@ "version": 1 }, "music_cache_delete": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "music", - "input_contract": "MusicKeywordArguments", + "input_contract": "MusicCacheDeleteRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MusicProviderResult", - "result_shape": "any", + "required_parameters": [ + "cache_key" + ], + "result_contract": "dict | None", + "result_shape": "mapping", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "music_cache_items": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "music", - "input_contract": "MusicKeywordArguments", + "input_contract": "MusicCacheReadRequest", "plugin_short_circuit": true, "public_to_plugins": true, "required_parameters": [], - "result_contract": "MusicProviderResult", - "result_shape": "any", + "result_contract": "list[dict]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "music_chart": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "music", - "input_contract": "MusicKeywordArguments", + "input_contract": "MusicChartRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MusicProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "entity", + "offset", + "range_name" + ], + "result_contract": "list[MusicInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "music_discover": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "music", - "input_contract": "MusicKeywordArguments", + "input_contract": "MusicDiscoverRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MusicProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "entity", + "media_source", + "mode", + "page", + "sort", + "tags" + ], + "result_contract": "list[MusicInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "music_fresh_releases": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "music", - "input_contract": "MusicKeywordArguments", + "input_contract": "MusicFreshReleasesRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MusicProviderResult", - "result_shape": "any", + "required_parameters": [ + "count", + "days", + "future", + "offset", + "past", + "sort" + ], + "result_contract": "list[MusicInfo]", + "result_shape": "list", "supports_async": true, "supports_sync": true, "timeout_policy": "caller_budget", "version": 1 }, "music_lyrics": { - "aggregation": "legacy", + "aggregation": "first_non_empty", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "music", - "input_contract": "MusicKeywordArguments", + "input_contract": "MusicLyricsRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MusicProviderResult", + "required_parameters": [ + "music" + ], + "result_contract": "MusicLyrics | None", "result_shape": "any", "supports_async": true, "supports_sync": true, @@ -5414,16 +5470,20 @@ "version": 1 }, "search_music": { - "aggregation": "legacy", + "aggregation": "ordered_list_merge", "error_policy": "isolate_provider", "execution": "sync_or_async", "family": "music", - "input_contract": "MusicKeywordArguments", + "input_contract": "MusicSearchRequest", "plugin_short_circuit": true, "public_to_plugins": true, - "required_parameters": [], - "result_contract": "MusicProviderResult", - "result_shape": "any", + "required_parameters": [ + "limit", + "media_source", + "meta" + ], + "result_contract": "list[MusicInfo]", + "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 2dd64e1cf..1cee7a911 100644 --- a/tests/test_module_method_contracts.py +++ b/tests/test_module_method_contracts.py @@ -428,6 +428,42 @@ def test_sync_and_async_tmdb_capabilities_share_contracts() -> None: assert contract.required_parameters +def test_music_capabilities_distinguish_lists_values_and_async_aliases() -> None: + """音乐查询应声明真实聚合,独立异步方法名必须复用同步契约。""" + list_methods = { + "music_album_related", + "music_artist_albums", + "music_artist_related", + "music_cache_items", + "music_chart", + "music_discover", + "music_fresh_releases", + "search_music", + } + value_methods = { + "identify_music_by_fingerprint", + "match_music_album", + "music_album", + "music_artist", + "music_cache_delete", + "music_lyrics", + } + + for method in list_methods: + contract = get_module_method_contract(method) + assert contract.aggregation is ModuleResultAggregation.ORDERED_LIST_MERGE + assert contract.result_shape is ModuleResultShape.LIST + for method in value_methods: + assert ( + get_module_method_contract(method).aggregation + is ModuleResultAggregation.FIRST_NON_EMPTY + ) + for sync_method in ("identify_music_by_fingerprint", "match_music_album"): + assert get_module_method_contract(sync_method) is get_module_method_contract( + f"async_{sync_method}" + ) + + def test_attachment_result_diagnostics_distinguish_bytes_and_strings() -> None: """附件契约应区分二进制内容和可展示字符串,偏差仍仅供诊断。""" assert diagnose_module_result("download_qq_file_bytes", b"content") == ()