""" 字符串模块方法协议的可检查契约清单。 """ from __future__ import annotations import inspect from collections.abc import Callable from dataclasses import dataclass from enum import StrEnum from typing import Any, Protocol from pydantic import BaseModel class ModuleResultAggregation(StrEnum): """ 描述多模块结果沿调用链的兼容聚合方式。 """ # 沿用历史模块方法的结果聚合规则 LEGACY = "legacy" # 采用首个非空结果并停止后续调用 FIRST_NON_EMPTY = "first_non_empty" # 按调用顺序合并列表结果 ORDERED_LIST_MERGE = "ordered_list_merge" # 按调用顺序合并映射结果 ORDERED_MAPPING_MERGE = "ordered_mapping_merge" # 将上一结果作为下一调用的输入 PIPELINE_RELAY = "pipeline_relay" # 向各实现方分发同一调用 FAN_OUT = "fan_out" class ModuleResultShape(StrEnum): """ 描述模块 provider 返回值的基础 Python 形状或模型对象。 MODEL 用于 Pydantic 分页等模型对象,MAPPING 仍只接受字典。 """ # 接受任意结果形状 ANY = "any" # 结果应为列表 LIST = "list" # 结果应为字符串 STRING = "string" # 结果应为键值映射 MAPPING = "mapping" MODEL = "model" # 结果应为布尔值 BOOLEAN = "boolean" # 结果应为字节序列 BYTES = "bytes" class ModuleExecutionMode(StrEnum): """ 描述 provider 可以采用的执行形态。 """ # 允许同步或异步实现,由调度器适配调用 SYNC_OR_ASYNC = "sync_or_async" class ModuleErrorPolicy(StrEnum): """ 描述单个 provider 失败后的兼容处理策略。 """ # 隔离单个实现方的异常,按聚合规则处理其余实现 ISOLATE_PROVIDER = "isolate_provider" class ModuleCapability(Protocol): """ 宿主和新插件可用于声明动态能力的最小 Protocol。 """ def __call__(self, *args: Any, **kwargs: Any) -> Any: """ 执行模块能力并返回契约声明的结果。 :param args: 模块方法契约规定的位置参数 :param kwargs: 模块方法契约规定的关键字参数 :return: 该模块方法契约规定的业务结果;异步实现返回供调度器等待的对象 """ @dataclass(frozen=True, slots=True) class ModuleMethodContract: """ 记录模块方法的输入、结果、执行与兼容错误协议。 :param family: 方法所属的业务能力族 :param aggregation: 多个提供方返回结果的聚合策略 :param version: 模块方法协议版本,默认从 1 开始 :param input_contract: 输入参数协议名称 :param result_contract: 返回值业务协议名称 :param result_shape: 用于运行时检查的返回值基础形状 :param required_parameters: 调用该能力必须提供的参数名 :param execution: 允许使用的同步或异步执行形态 :param timeout_policy: 执行预算的归属策略,默认沿用调用方预算 :param error_policy: 单个提供方失败时的隔离策略 :param public_to_plugins: 是否允许插件实现此模块能力 :param supports_sync: 是否支持同步调用入口 :param supports_async: 是否支持异步调用入口 :param plugin_short_circuit: 插件返回有效结果后是否短路宿主模块调用 :param optional_parameters: 兼容调用时可选择传入的参数名 """ family: str aggregation: ModuleResultAggregation = ModuleResultAggregation.LEGACY version: int = 1 input_contract: str = "legacy_args" result_contract: str = "Any" result_shape: ModuleResultShape = ModuleResultShape.ANY required_parameters: tuple[str, ...] = () execution: ModuleExecutionMode = ModuleExecutionMode.SYNC_OR_ASYNC timeout_policy: str = "caller_budget" error_policy: ModuleErrorPolicy = ModuleErrorPolicy.ISOLATE_PROVIDER public_to_plugins: bool = True supports_sync: bool = True supports_async: bool = True plugin_short_circuit: bool = True optional_parameters: tuple[str, ...] = () _DEFAULT_CONTRACT = ModuleMethodContract(family="legacy") # 首批登记高频能力族。方法名仍保持开放字符串,以兼容第三方插件自定义模块能力; # 未命中项继续使用冻结的 legacy 规则,并由架构快照记录新增调用位置。 _METHOD_CONTRACTS = { "recognize_media": ModuleMethodContract( family="media-recognition", input_contract="MediaRecognitionRequest", result_contract="MediaInfo | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("meta", "mtype", "media_source", "media_id", "episode_group", "cache"), ), "match_doubaninfo": ModuleMethodContract( family="media-recognition", input_contract="DoubanMatchRequest", result_contract="dict[str, Any] | None", result_shape=ModuleResultShape.MAPPING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("name", "imdbid", "mtype", "year", "season", "raise_exception"), ), "match_tmdbinfo": ModuleMethodContract( family="media-recognition", input_contract="TmdbMatchRequest", result_contract="dict[str, Any] | None", result_shape=ModuleResultShape.MAPPING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("name", "mtype", "year", "season"), ), "update_recognize_cache": ModuleMethodContract( family="media-recognition", input_contract="RecognitionCacheUpdateRequest", result_contract="bool | None", result_shape=ModuleResultShape.BOOLEAN, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("meta", "mediainfo"), ), "search_medias": ModuleMethodContract( family="media-recognition", input_contract="MediaSearchRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("meta", "media_source"), ), "get_media_auxiliary_info": ModuleMethodContract( family="media-recognition", input_contract="MediaAuxiliaryInfoRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("mediainfo", "media_source", "metainfo"), plugin_short_circuit=False, ), "get_media_classification_facts": ModuleMethodContract( family="media-classification", version=1, input_contract="ClassificationEnrichmentRequest", result_contract="ClassificationEnrichmentResponse | None", result_shape=ModuleResultShape.MAPPING, aggregation=ModuleResultAggregation.FAN_OUT, required_parameters=("request",), supports_async=False, plugin_short_circuit=False, ), "obtain_images": ModuleMethodContract( family="media-recognition", input_contract="MediaInfo", result_contract="MediaInfo | None", aggregation=ModuleResultAggregation.PIPELINE_RELAY, required_parameters=("mediainfo",), ), "mediaserver_items": ModuleMethodContract( family="media-server", input_contract="MediaServerItemsRequest", result_contract="Iterable[MediaServerItem] | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("server", "library_id", "start_index", "limit"), optional_parameters=("query",), ), "mediaserver_iteminfo": ModuleMethodContract( family="media-server", input_contract="MediaServerItemRequest", result_contract="MediaServerItem | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("server", "item_id"), optional_parameters=("query",), ), "mediaserver_play_url": ModuleMethodContract( family="media-server", input_contract="MediaServerPlayRequest", result_contract="str | None", result_shape=ModuleResultShape.STRING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("server", "item_id"), ), "mediaserver_tv_episodes": ModuleMethodContract( family="media-server", input_contract="MediaServerEpisodesRequest", result_contract="list[MediaServerPlayItem]", aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("server", "item_id"), optional_parameters=("query",), ), "media_exists": ModuleMethodContract( family="media-server", input_contract="MediaExistsRequest", result_contract="ExistMediaInfo | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("mediainfo", "itemid", "server"), optional_parameters=("query",), ), "media_statistic": ModuleMethodContract( family="media-server", input_contract="MediaStatisticRequest", result_contract="list[Statistic]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("server",), ), "mediaserver_image_cookies": ModuleMethodContract( family="media-server", input_contract="MediaServerImageCookiesRequest", result_contract="str | dict | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("server", "image_url"), ), "mediaserver_items_count": ModuleMethodContract( family="media-server", input_contract="MediaServerItemsCountRequest", result_contract="int | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("server", "library_id"), ), "mediaserver_latest": ModuleMethodContract( family="media-server", input_contract="MediaServerRecentRequest", result_contract="list[MediaServerPlayItem]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("server", "count", "username"), optional_parameters=("query",), ), "mediaserver_latest_images": ModuleMethodContract( family="media-server", input_contract="MediaServerRecentImagesRequest", result_contract="list[str]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("server", "count", "remote", "username"), optional_parameters=("query",), ), "mediaserver_librarys": ModuleMethodContract( family="media-server", input_contract="MediaServerLibrariesRequest", result_contract="list[MediaServerLibrary]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("server", "username", "hidden"), optional_parameters=("query",), ), "mediaserver_playing": ModuleMethodContract( family="media-server", input_contract="MediaServerRecentRequest", result_contract="list[MediaServerPlayItem]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("server", "count", "username"), optional_parameters=("query",), ), "mediaserver_season_episode_ids": ModuleMethodContract( family="media-server", input_contract="MediaServerSeasonEpisodesRequest", result_contract="dict[int, str] | None", result_shape=ModuleResultShape.MAPPING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("server", "item_id", "season"), optional_parameters=("query",), ), "any_files": ModuleMethodContract( family="storage", input_contract="StorageAnyFilesRequest", result_contract="bool | None", result_shape=ModuleResultShape.BOOLEAN, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("fileitem", "extensions"), ), "create_folder": ModuleMethodContract( family="storage", input_contract="StorageCreateFolderRequest", result_contract="FileItem | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("fileitem", "name"), ), "delete_file": ModuleMethodContract( family="storage", input_contract="StorageDeleteRequest", result_contract="bool | None", result_shape=ModuleResultShape.BOOLEAN, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("fileitem",), ), "download_file": ModuleMethodContract( family="storage", input_contract="StorageDownloadRequest", result_contract="FileItem | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("fileitem", "path"), ), "upload_file": ModuleMethodContract( family="storage", input_contract="StorageUploadRequest", result_contract="FileItem | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("fileitem", "path", "new_name"), ), "link_file": ModuleMethodContract( family="storage", input_contract="StorageLinkRequest", result_contract="bool | None", result_shape=ModuleResultShape.BOOLEAN, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("fileitem", "target_file"), public_to_plugins=False, ), "list_files": ModuleMethodContract( family="storage", input_contract="StorageListRequest", result_contract="list[FileItem]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("fileitem", "recursion"), ), "media_files": ModuleMethodContract( family="storage", input_contract="MediaFilesRequest", result_contract="list[FileItem]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("mediainfo",), ), "get_file_item": ModuleMethodContract( family="storage", input_contract="StorageItemRequest", result_contract="FileItem | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("storage", "path"), ), "get_file_item_strict": ModuleMethodContract( family="storage", input_contract="StorageItemRequest", result_contract="FileItemQueryResult | None", result_shape=ModuleResultShape.MODEL, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("storage", "path"), ), "get_folder": ModuleMethodContract( family="storage", input_contract="StorageFolderRequest", result_contract="FileItem | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("storage", "path"), ), "get_parent_item": ModuleMethodContract( family="storage", input_contract="StorageParentRequest", result_contract="FileItem | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("fileitem",), ), "rename_file": ModuleMethodContract( family="storage", input_contract="StorageRenameRequest", result_contract="bool | FileItem", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("fileitem", "name"), ), "storage_manage": ModuleMethodContract( family="storage", input_contract="StorageManageRequest", result_contract="StorageProviderResult", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("storage", "action"), ), "snapshot_storage": ModuleMethodContract( family="storage", input_contract="StorageSnapshotRequest", result_contract="dict[str, dict] | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("storage", "path", "last_snapshot_time", "max_depth", "previous_snapshot"), ), "plan_transfer": ModuleMethodContract( family="storage", input_contract="TransferPlanningInput", result_contract="TransferPlanCheckpoint | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=( "fileitem", "meta", "mediainfo", "target_directory", "target_storage", "target_path", "transfer_type", "scrape", "library_type_folder", "library_category_folder", "episodes_info", "source_oper", "preview", "planning_input", ), public_to_plugins=False, ), "execute_transfer_plan": ModuleMethodContract( family="storage", input_contract="TransferPlanCheckpoint", result_contract="TransferInfo | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=( "checkpoint", "meta", "mediainfo", "source_oper", "target_oper", ), public_to_plugins=False, ), "transfer": ModuleMethodContract( family="storage", input_contract="TransferRequest", result_contract="TransferInfo | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=( "fileitem", "meta", "mediainfo", "target_directory", "target_storage", "target_path", "transfer_type", "scrape", "library_type_folder", "library_category_folder", "episodes_info", "source_oper", "target_oper", "preview", ), ), "clear_cache": ModuleMethodContract( family="category", input_contract="CacheClearRequest", result_contract="None", aggregation=ModuleResultAggregation.FAN_OUT, plugin_short_circuit=False, ), "get_search_page_size": ModuleMethodContract( family="site", input_contract="SiteSearchPageSizeRequest", result_contract="int | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("site", "keyword"), ), "restore_search_torrent": ModuleMethodContract( family="site", input_contract="SearchTorrentRestoreRequest", result_contract="TorrentInfo | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("site", "record"), public_to_plugins=False, ), "refresh_userdata": ModuleMethodContract( family="site", input_contract="SiteUserDataRequest", result_contract="SiteUserData | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("site",), ), "site_subtitle_links": ModuleMethodContract( family="site", input_contract="SiteSubtitleLinksRequest", result_contract="list[str]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("context",), ), "metadata_img": ModuleMethodContract( family="metadata", input_contract="MetadataImageRequest", result_contract="dict[str, str] | None", result_shape=ModuleResultShape.MAPPING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("mediainfo", "season", "episode"), ), "metadata_nfo": ModuleMethodContract( family="metadata", input_contract="MetadataNfoRequest", result_contract="str | None", result_shape=ModuleResultShape.STRING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("meta", "mediainfo", "season", "episode"), ), "obtain_specific_image": ModuleMethodContract( family="metadata", input_contract="SpecificImageRequest", result_contract="str | None", result_shape=ModuleResultShape.STRING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("mediaid", "mtype", "image_type", "image_prefix", "season", "episode"), ), "recommend_name": ModuleMethodContract( family="metadata", input_contract="RecommendNameRequest", result_contract="str | None", result_shape=ModuleResultShape.STRING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("meta", "mediainfo", "episodes_info"), ), "user_authenticate": ModuleMethodContract( family="authentication", input_contract="UserAuthenticationRequest", result_contract="AuthCredentials | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("credentials",), ), "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 | MusicArtistInfo]", 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"), ), "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",), ), "douban_movie_interests": ModuleMethodContract( family="douban", input_contract="DoubanCommentRequest", result_contract="MediaCommentPage", result_shape=ModuleResultShape.MODEL, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("doubanid", "start", "count"), ), "douban_tv_interests": ModuleMethodContract( family="douban", input_contract="DoubanCommentRequest", result_contract="MediaCommentPage", result_shape=ModuleResultShape.MODEL, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("doubanid", "start", "count"), ), "douban_movie_reviews": ModuleMethodContract( family="douban", input_contract="DoubanCommentRequest", result_contract="MediaReviewPage", result_shape=ModuleResultShape.MODEL, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("doubanid", "start", "count"), ), "douban_tv_reviews": ModuleMethodContract( family="douban", input_contract="DoubanCommentRequest", result_contract="MediaReviewPage", result_shape=ModuleResultShape.MODEL, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("doubanid", "start", "count"), ), "bangumi_calendar": ModuleMethodContract( family="bangumi", input_contract="BangumiCalendarRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, ), "bangumi_credits": ModuleMethodContract( family="bangumi", input_contract="BangumiMediaRequest", result_contract="list[MediaPerson]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("bangumiid",), ), "bangumi_discover": ModuleMethodContract( family="bangumi", input_contract="BangumiDiscoverArguments", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, ), "bangumi_info": ModuleMethodContract( family="bangumi", input_contract="BangumiMediaRequest", result_contract="dict | None", result_shape=ModuleResultShape.MAPPING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("bangumiid",), ), "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"), ), "tmdb_collection": ModuleMethodContract( family="tmdb", input_contract="TmdbCollectionRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("collection_id",), ), "tmdb_discover": ModuleMethodContract( family="tmdb", input_contract="TmdbDiscoverRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=( "mtype", "sort_by", "with_genres", "with_original_language", "with_keywords", "with_watch_providers", "vote_average", "vote_count", "release_date", "page", ), ), "tmdb_episodes": ModuleMethodContract( family="tmdb", input_contract="TmdbEpisodesRequest", result_contract="list[TmdbEpisode]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("tmdbid", "season", "episode_group"), ), "tmdb_group_seasons": ModuleMethodContract( family="tmdb", input_contract="TmdbGroupRequest", result_contract="list[TmdbSeason]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("group_id",), ), "tmdb_info": ModuleMethodContract( family="tmdb", input_contract="TmdbInfoRequest", result_contract="dict | None", result_shape=ModuleResultShape.MAPPING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("tmdbid", "mtype", "season"), ), "tmdb_movie_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_movie_recommend": ModuleMethodContract( family="tmdb", input_contract="TmdbMediaRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("tmdbid",), ), "tmdb_movie_similar": ModuleMethodContract( family="tmdb", input_contract="TmdbMediaRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("tmdbid",), ), "tmdb_person_credits": ModuleMethodContract( family="tmdb", input_contract="TmdbPersonPageRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("person_id", "page"), ), "tmdb_person_detail": ModuleMethodContract( family="tmdb", input_contract="TmdbPersonRequest", result_contract="MediaPerson | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("person_id",), ), "tmdb_seasons": ModuleMethodContract( family="tmdb", input_contract="TmdbMediaRequest", result_contract="list[TmdbSeason]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("tmdbid",), ), "tmdb_trending": ModuleMethodContract( family="tmdb", input_contract="TmdbTrendingRequest", result_contract="list[MediaInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("page",), ), "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_cache_clear": ModuleMethodContract( family="music", input_contract="MusicCacheClearRequest", result_contract="None", aggregation=ModuleResultAggregation.FAN_OUT, plugin_short_circuit=False, ), "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",), ), "music_lyrics_candidates": ModuleMethodContract( family="music", input_contract="MusicLyricsRequest", result_contract="list[MusicLyrics]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("music",), plugin_short_circuit=False, ), "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"), ), "channel_manage": ModuleMethodContract( family="messaging", input_contract="ChannelManageRequest", result_contract="dict[str, Any] | None", result_shape=ModuleResultShape.MAPPING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("channel", "action"), ), "delete_message": ModuleMethodContract( family="messaging", input_contract="MessageDeleteRequest", result_contract="bool | None", result_shape=ModuleResultShape.BOOLEAN, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("channel", "source", "message_id", "chat_id"), ), "edit_message": ModuleMethodContract( family="messaging", input_contract="MessageEditRequest", result_contract="bool | None", result_shape=ModuleResultShape.BOOLEAN, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("channel", "source", "message_id", "chat_id", "text", "title", "buttons", "metadata"), ), "mark_message_processing_started": ModuleMethodContract( family="messaging", input_contract="MessageProcessingStartRequest", result_contract="dict[str, Any] | None", result_shape=ModuleResultShape.MAPPING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("channel", "source", "userid", "message_id", "chat_id", "text"), ), "mark_message_processing_finished": ModuleMethodContract( family="messaging", input_contract="MessageProcessingFinishRequest", result_contract="bool | None", result_shape=ModuleResultShape.BOOLEAN, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("channel", "source", "userid", "message_id", "chat_id", "status"), ), "message_parser": ModuleMethodContract( family="messaging", input_contract="MessageParseRequest", result_contract="IncomingMessage | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("source", "body", "form", "args"), ), "send_direct_message": ModuleMethodContract( family="messaging", input_contract="DirectMessageSendRequest", result_contract="MessageResponse | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("message",), ), "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", aggregation=ModuleResultAggregation.FAN_OUT, required_parameters=("commands",), plugin_short_circuit=False, ), "scheduler_job": ModuleMethodContract( family="scheduling", input_contract="SchedulerJobRequest", result_contract="None", aggregation=ModuleResultAggregation.FAN_OUT, plugin_short_circuit=False, ), "webhook_parser": ModuleMethodContract( family="integration", input_contract="WebhookRequest", result_contract="WebhookEventInfo | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("body", "form", "args"), ), "download_discord_file_bytes": ModuleMethodContract( family="messaging", input_contract="MessageFileDownloadRequest", result_contract="bytes | None", result_shape=ModuleResultShape.BYTES, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("file_ref", "source"), ), "download_feishu_file_bytes": ModuleMethodContract( family="messaging", input_contract="MessageFileDownloadRequest", result_contract="bytes | None", result_shape=ModuleResultShape.BYTES, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("file_ref", "source"), ), "download_feishu_image_to_data_url": ModuleMethodContract( family="messaging", input_contract="MessageImageDownloadRequest", result_contract="str | None", result_shape=ModuleResultShape.STRING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("image_ref", "source"), ), "download_qq_file_bytes": ModuleMethodContract( family="messaging", input_contract="MessageFileDownloadRequest", result_contract="bytes | None", result_shape=ModuleResultShape.BYTES, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("file_ref", "source"), ), "download_slack_file_bytes": ModuleMethodContract( family="messaging", input_contract="MessageFileDownloadRequest", result_contract="bytes | None", result_shape=ModuleResultShape.BYTES, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("file_ref", "source"), ), "download_slack_file_to_data_url": ModuleMethodContract( family="messaging", input_contract="MessageFileDownloadRequest", result_contract="str | None", result_shape=ModuleResultShape.STRING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("file_url", "source"), ), "download_synologychat_file_bytes": ModuleMethodContract( family="messaging", input_contract="MessageFileDownloadRequest", result_contract="bytes | None", result_shape=ModuleResultShape.BYTES, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("file_ref", "source"), ), "download_telegram_file_bytes": ModuleMethodContract( family="messaging", input_contract="MessageFileDownloadRequest", result_contract="bytes | None", result_shape=ModuleResultShape.BYTES, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("file_id", "source"), ), "download_telegram_file_to_base64": ModuleMethodContract( family="messaging", input_contract="MessageFileDownloadRequest", result_contract="str | None", result_shape=ModuleResultShape.STRING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("file_id", "source"), ), "download_vocechat_file_bytes": ModuleMethodContract( family="messaging", input_contract="MessageFileDownloadRequest", result_contract="bytes | None", result_shape=ModuleResultShape.BYTES, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("file_ref", "source"), ), "download_vocechat_image_to_data_url": ModuleMethodContract( family="messaging", input_contract="MessageImageDownloadRequest", result_contract="str | None", result_shape=ModuleResultShape.STRING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("image_ref", "source"), ), "download_wechat_image_to_data_url": ModuleMethodContract( family="messaging", input_contract="MessageImageDownloadRequest", result_contract="str | None", result_shape=ModuleResultShape.STRING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("image_ref", "source"), ), "download_wechat_media_bytes": ModuleMethodContract( family="messaging", input_contract="MessageMediaDownloadRequest", result_contract="bytes | None", result_shape=ModuleResultShape.BYTES, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("media_ref", "source"), ), "downloader_info": ModuleMethodContract( family="downloader", input_contract="DownloaderInfoRequest", result_contract="list[DownloaderInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("downloader",), ), "list_torrents": ModuleMethodContract( family="downloader", input_contract="TorrentListRequest", result_contract="list[DownloaderTorrent]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("status", "hashs", "downloader", "include_all_tags"), ), "filter_torrents": ModuleMethodContract( family="downloader", input_contract="TorrentFilterRequest", result_contract="list[TorrentInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("rule_groups", "torrent_list", "mediainfo"), ), "refresh_torrents": ModuleMethodContract( family="downloader", input_contract="TorrentRefreshRequest", result_contract="list[TorrentInfo]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, required_parameters=("site", "keyword", "cat", "page", "mtype"), ), "torrent_files": ModuleMethodContract( family="downloader", input_contract="TorrentFilesRequest", result_contract="list[DownloaderFile]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("tid", "downloader"), ), "get_torrent_trackers": ModuleMethodContract( family="downloader", input_contract="TorrentTrackersRequest", result_contract="dict[str, list[str]] | None", result_shape=ModuleResultShape.MAPPING, aggregation=ModuleResultAggregation.ORDERED_MAPPING_MERGE, required_parameters=("hash_string", "downloader"), ), "download": ModuleMethodContract( family="downloader", input_contract="DownloadTaskRequest", result_contract="DownloadTaskResult | None", aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("content", "download_dir", "cookie", "episodes", "category", "label", "downloader"), optional_parameters=("custom_words",), ), "download_added": ModuleMethodContract( family="downloader", input_contract="DownloadAddedHook", result_contract="None", aggregation=ModuleResultAggregation.FAN_OUT, required_parameters=("context", "torrent_content", "download_dir"), plugin_short_circuit=False, ), "remove_torrents": ModuleMethodContract( family="downloader", input_contract="TorrentRemoveRequest", result_contract="bool | None", result_shape=ModuleResultShape.BOOLEAN, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("hashs", "delete_file", "downloader"), ), "set_torrents_tag": ModuleMethodContract( family="downloader", input_contract="TorrentTagRequest", result_contract="bool | None", result_shape=ModuleResultShape.BOOLEAN, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("hashs", "tags", "downloader"), ), "start_torrents": ModuleMethodContract( family="downloader", input_contract="TorrentControlRequest", result_contract="bool | None", result_shape=ModuleResultShape.BOOLEAN, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("hashs", "downloader"), ), "stop_torrents": ModuleMethodContract( family="downloader", input_contract="TorrentControlRequest", result_contract="bool | None", result_shape=ModuleResultShape.BOOLEAN, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("hashs", "downloader"), ), "update_torrent": ModuleMethodContract( family="downloader", input_contract="TorrentUpdateRequest", result_contract="dict[str, bool] | None", result_shape=ModuleResultShape.MAPPING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=( "hash_string", "downloader", "download_limit", "upload_limit", "tracker_list", "save_path", "category", "ratio_limit", "seeding_time_limit", ), ), "transfer_completed": ModuleMethodContract( family="downloader", input_contract="TransferCompletedHook", result_contract="None", aggregation=ModuleResultAggregation.FAN_OUT, required_parameters=("hashs", "downloader"), plugin_short_circuit=False, ), "tmdb_cache_items": ModuleMethodContract( family="tmdb", input_contract="TmdbCacheListRequest", result_contract="list[dict[str, Any]]", result_shape=ModuleResultShape.LIST, aggregation=ModuleResultAggregation.ORDERED_LIST_MERGE, ), "tmdb_cache_delete": ModuleMethodContract( family="tmdb", input_contract="TmdbCacheDeleteRequest", result_contract="dict[str, Any] | None", result_shape=ModuleResultShape.MAPPING, aggregation=ModuleResultAggregation.FIRST_NON_EMPTY, required_parameters=("cache_key",), ), "tmdb_cache_clear": ModuleMethodContract( family="tmdb", input_contract="TmdbCacheClearRequest", result_contract="None", aggregation=ModuleResultAggregation.FAN_OUT, plugin_short_circuit=False, ), } # 同一能力的同步/异步入口共享不可变契约对象,避免参数和聚合语义各自漂移。 _METHOD_CONTRACTS.update( { "async_recognize_media": _METHOD_CONTRACTS["recognize_media"], "async_match_doubaninfo": _METHOD_CONTRACTS["match_doubaninfo"], "async_match_tmdbinfo": _METHOD_CONTRACTS["match_tmdbinfo"], "async_update_recognize_cache": _METHOD_CONTRACTS["update_recognize_cache"], "async_search_medias": _METHOD_CONTRACTS["search_medias"], "async_get_media_auxiliary_info": _METHOD_CONTRACTS["get_media_auxiliary_info"], "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"], "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_interests": _METHOD_CONTRACTS["douban_movie_interests"], "async_douban_movie_recommend": _METHOD_CONTRACTS["douban_movie_recommend"], "async_douban_movie_reviews": _METHOD_CONTRACTS["douban_movie_reviews"], "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_interests": _METHOD_CONTRACTS["douban_tv_interests"], "async_douban_tv_recommend": _METHOD_CONTRACTS["douban_tv_recommend"], "async_douban_tv_reviews": _METHOD_CONTRACTS["douban_tv_reviews"], "async_bangumi_calendar": _METHOD_CONTRACTS["bangumi_calendar"], "async_bangumi_credits": _METHOD_CONTRACTS["bangumi_credits"], "async_bangumi_discover": _METHOD_CONTRACTS["bangumi_discover"], "async_bangumi_info": _METHOD_CONTRACTS["bangumi_info"], "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"], "async_tmdb_collection": _METHOD_CONTRACTS["tmdb_collection"], "async_tmdb_discover": _METHOD_CONTRACTS["tmdb_discover"], "async_tmdb_episodes": _METHOD_CONTRACTS["tmdb_episodes"], "async_tmdb_group_seasons": _METHOD_CONTRACTS["tmdb_group_seasons"], "async_tmdb_info": _METHOD_CONTRACTS["tmdb_info"], "async_tmdb_movie_credits": _METHOD_CONTRACTS["tmdb_movie_credits"], "async_tmdb_movie_recommend": _METHOD_CONTRACTS["tmdb_movie_recommend"], "async_tmdb_movie_similar": _METHOD_CONTRACTS["tmdb_movie_similar"], "async_tmdb_person_credits": _METHOD_CONTRACTS["tmdb_person_credits"], "async_tmdb_person_detail": _METHOD_CONTRACTS["tmdb_person_detail"], "async_tmdb_seasons": _METHOD_CONTRACTS["tmdb_seasons"], "async_tmdb_trending": _METHOD_CONTRACTS["tmdb_trending"], "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"], "async_refresh_torrents": _METHOD_CONTRACTS["refresh_torrents"], } ) _PREFIX_CONTRACTS = ( ("async_tmdb_", ModuleMethodContract(family="tmdb")), ("tmdb_", ModuleMethodContract(family="tmdb")), ("async_douban_", ModuleMethodContract(family="douban")), ("douban_", ModuleMethodContract(family="douban")), ("async_bangumi_", ModuleMethodContract(family="bangumi")), ("bangumi_", ModuleMethodContract(family="bangumi")), ("async_anilist_", ModuleMethodContract(family="anilist")), ("anilist_", ModuleMethodContract(family="anilist")), ("tvdb_", ModuleMethodContract(family="tvdb")), ("music_", ModuleMethodContract(family="music")), ("torrent_", ModuleMethodContract(family="downloader")), ) # 宿主静态扫描到的全部字符串能力名。第三方插件仍可声明未在这里出现的自定义方法, # 自定义方法继续走开放的 legacy contract;宿主新增调用则必须先进入本清单。 _OBSERVED_HOST_METHODS = ( "anilist_credits", "anilist_discover", "anilist_info", "anilist_person_credits", "anilist_person_detail", "anilist_popular_this_season", "anilist_recommendations", "anilist_trending", "any_files", "async_anilist_credits", "async_anilist_discover", "async_anilist_info", "async_anilist_person_credits", "async_anilist_person_detail", "async_anilist_popular_this_season", "async_anilist_recommendations", "async_anilist_trending", "async_bangumi_calendar", "async_bangumi_credits", "async_bangumi_discover", "async_bangumi_info", "async_bangumi_person_credits", "async_bangumi_person_detail", "async_bangumi_recommend", "async_douban_discover", "async_douban_info", "async_douban_movie_credits", "async_douban_movie_interests", "async_douban_movie_recommend", "async_douban_movie_reviews", "async_douban_person_credits", "async_douban_person_detail", "async_douban_tv_credits", "async_douban_tv_interests", "async_douban_tv_recommend", "async_douban_tv_reviews", "async_identify_music_by_fingerprint", "async_match_doubaninfo", "async_match_music_album", "async_match_tmdbinfo", "async_movie_hot", "async_movie_showing", "async_movie_top250", "async_obtain_images", "async_recognize_media", "async_refresh_torrents", "async_search_collections", "async_search_medias", "async_search_persons", "async_search_subtitles", "async_search_torrents", "async_tmdb_collection", "async_tmdb_discover", "async_tmdb_episodes", "async_tmdb_group_seasons", "async_tmdb_info", "async_tmdb_movie_credits", "async_tmdb_movie_recommend", "async_tmdb_movie_similar", "async_tmdb_person_credits", "async_tmdb_person_detail", "async_tmdb_seasons", "async_tmdb_trending", "async_tmdb_tv_credits", "async_tmdb_tv_recommend", "async_tmdb_tv_similar", "async_tv_animation", "async_tv_hot", "async_tv_weekly_chinese", "async_tv_weekly_global", "async_update_recognize_cache", "bangumi_calendar", "bangumi_credits", "bangumi_discover", "bangumi_info", "bangumi_person_credits", "bangumi_person_detail", "bangumi_recommend", "channel_manage", "clear_cache", "create_folder", "delete_file", "delete_message", "douban_discover", "douban_info", "douban_movie_credits", "douban_movie_interests", "douban_movie_recommend", "douban_movie_reviews", "douban_person_credits", "douban_person_detail", "douban_tv_credits", "douban_tv_interests", "douban_tv_recommend", "douban_tv_reviews", "download", "download_added", "download_discord_file_bytes", "download_feishu_file_bytes", "download_feishu_image_to_data_url", "download_file", "download_qq_file_bytes", "download_slack_file_bytes", "download_slack_file_to_data_url", "download_synologychat_file_bytes", "download_telegram_file_bytes", "download_telegram_file_to_base64", "download_vocechat_file_bytes", "download_vocechat_image_to_data_url", "download_wechat_image_to_data_url", "download_wechat_media_bytes", "downloader_info", "edit_message", "filter_torrents", "finalize_message", "get_file_item", "get_folder", "get_parent_item", "get_search_page_size", "get_torrent_trackers", "identify_music_by_fingerprint", "list_files", "list_torrents", "mark_message_processing_finished", "mark_message_processing_started", "match_doubaninfo", "match_music_album", "match_tmdbinfo", "media_exists", "media_files", "media_statistic", "mediaserver_image_cookies", "mediaserver_iteminfo", "mediaserver_items", "mediaserver_items_count", "mediaserver_latest", "mediaserver_latest_images", "mediaserver_librarys", "mediaserver_play_url", "mediaserver_playing", "mediaserver_season_episode_ids", "mediaserver_tv_episodes", "message_parser", "metadata_img", "metadata_nfo", "movie_hot", "movie_showing", "movie_top250", "music_album", "music_album_related", "music_artist", "music_artist_albums", "music_artist_related", "music_cache_clear", "music_cache_delete", "music_cache_items", "music_chart", "music_discover", "music_fresh_releases", "music_lyrics", "obtain_images", "obtain_specific_image", "recognize_media", "recommend_name", "refresh_torrents", "refresh_userdata", "register_commands", "remove_torrents", "rename_file", "scheduler_job", "search_collections", "search_medias", "search_music", "search_persons", "search_subtitles", "search_torrents", "search_tvdb", "send_direct_message", "set_torrents_tag", "site_subtitle_links", "snapshot_storage", "start_torrents", "stop_torrents", "storage_manage", "tmdb_cache_clear", "tmdb_cache_delete", "tmdb_cache_items", "tmdb_collection", "tmdb_discover", "tmdb_episodes", "tmdb_group_seasons", "tmdb_info", "tmdb_movie_credits", "tmdb_movie_recommend", "tmdb_movie_similar", "tmdb_person_credits", "tmdb_person_detail", "tmdb_seasons", "tmdb_trending", "tmdb_tv_credits", "tmdb_tv_recommend", "tmdb_tv_similar", "torrent_files", "transfer", "transfer_completed", "tv_animation", "tv_hot", "tv_weekly_chinese", "tv_weekly_global", "tvdb_info", "tvdb_slug", "update_recognize_cache", "update_torrent", "upload_file", "user_authenticate", "webhook_parser", ) _FAMILY_IO_CONTRACTS = { "anilist": ("AniListKeywordArguments", "AniListProviderResult"), "authentication": ("AuthenticationKeywordArguments", "AuthenticationResult"), "bangumi": ("BangumiKeywordArguments", "BangumiProviderResult"), "category": ("CategoryKeywordArguments", "CategoryProviderResult"), "douban": ("DoubanKeywordArguments", "DoubanProviderResult"), "downloader": ("DownloaderKeywordArguments", "DownloaderProviderResult"), "integration": ("IntegrationKeywordArguments", "IntegrationProviderResult"), "media-discovery": ("MediaDiscoveryKeywordArguments", "MediaDiscoveryProviderResult"), "media-recognition": ("MediaRecognitionKeywordArguments", "MediaRecognitionProviderResult"), "media-server": ("MediaServerKeywordArguments", "MediaServerProviderResult"), "messaging": ("MessagingKeywordArguments", "MessagingProviderResult"), "metadata": ("MetadataKeywordArguments", "MetadataProviderResult"), "music": ("MusicKeywordArguments", "MusicProviderResult"), "site": ("SiteKeywordArguments", "SiteProviderResult"), "storage": ("StorageKeywordArguments", "StorageProviderResult"), "tmdb": ("TmdbKeywordArguments", "TmdbProviderResult"), "tvdb": ("TvdbKeywordArguments", "TvdbProviderResult"), } def _infer_observed_family(method: str) -> str: """ 按稳定能力前缀把已观察宿主方法归入可审计的输入/结果族。 """ for prefix, contract in _PREFIX_CONTRACTS: if method.startswith(prefix): return contract.family if method.startswith(("mediaserver_", "media_exists", "media_statistic")): return "media-server" if method.startswith( ( "download", "torrent_", "list_torrents", "refresh_torrents", "remove_torrents", "start_torrents", "stop_torrents", "set_torrents_tag", "update_torrent", "get_torrent_trackers", "downloader_info", "filter_torrents", "transfer_completed", ) ): return "downloader" if method.startswith( ( "channel_", "delete_message", "edit_message", "finalize_message", "mark_message_", "message_parser", "register_commands", "send_direct_message", "send_message", ) ): return "messaging" if method.startswith( ( "any_files", "create_folder", "delete_file", "get_file_item", "get_folder", "get_parent_item", "list_files", "media_files", "rename_file", "snapshot_storage", "storage_manage", "transfer", "upload_file", ) ): return "storage" if method.startswith( ( "metadata_", "obtain_specific_image", "recommend_name", ) ): return "metadata" if method.startswith( ( "async_identify_music", "async_match_music", "identify_music", "match_music", "search_music", ) ): return "music" if method.startswith( ( "async_get_media_auxiliary_info", "async_match_", "async_obtain_images", "async_recognize_media", "async_update_recognize_cache", "match_", "obtain_images", "get_media_auxiliary_info", "recognize_media", "update_recognize_cache", ) ): return "media-recognition" if method.startswith( ( "async_movie_", "async_search_", "async_tv_", "movie_", "search_collections", "search_medias", "search_persons", "search_subtitles", "search_torrents", "tv_", ) ): return "media-discovery" if method == "clear_cache": return "category" if method in {"get_search_page_size", "refresh_userdata", "site_subtitle_links"}: return "site" if method == "user_authenticate": return "authentication" return "integration" def _register_observed_host_contracts() -> None: """ 为全部宿主字符串调用登记完整 V2 字段,保留未知插件方法的 legacy fallback。 """ for method in _OBSERVED_HOST_METHODS: if method in _METHOD_CONTRACTS: continue family = _infer_observed_family(method) input_contract, result_contract = _FAMILY_IO_CONTRACTS[family] _METHOD_CONTRACTS[method] = ModuleMethodContract( family=family, input_contract=input_contract, result_contract=result_contract, ) _register_observed_host_contracts() def get_module_method_contract(method: str) -> ModuleMethodContract: """ 返回方法的显式能力族契约,未知方法保持既有 legacy 协议。 """ if contract := _METHOD_CONTRACTS.get(method): return contract for prefix, contract in _PREFIX_CONTRACTS: if method.startswith(prefix): return contract return _DEFAULT_CONTRACT def is_explicit_module_method(method: str) -> bool: """ 判断方法是否已进入首批显式能力族清单。 """ return get_module_method_contract(method) is not _DEFAULT_CONTRACT def diagnose_module_callable(method: str, callback: Callable[..., Any]) -> tuple[str, ...]: """ 诊断显式能力的基础签名;兼容阶段只返回问题,不拒绝 provider。 """ contract = get_module_method_contract(method) if contract is _DEFAULT_CONTRACT: return () try: parameters = inspect.signature(callback).parameters except (TypeError, ValueError): return ("signature-unavailable",) missing = tuple( name for name in contract.required_parameters if name not in parameters and not any(parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in parameters.values()) ) return tuple(f"missing-parameter:{name}" for name in missing) def compatible_module_kwargs( method: str, callback: Callable[..., Any], kwargs: dict[str, Any], ) -> dict[str, Any]: """ 仅移除显式登记的新可选参数,保留旧 provider 签名和其它参数错误。 接受该参数或 **kwargs 的 provider 收到完整输入;无法检查签名时不推断兼容性。 """ optional = get_module_method_contract(method).optional_parameters if not optional or not any(name in kwargs for name in optional): return kwargs try: parameters = inspect.signature(callback).parameters except (TypeError, ValueError): return kwargs if any(parameter.kind is inspect.Parameter.VAR_KEYWORD for parameter in parameters.values()): return kwargs return {name: value for name, value in kwargs.items() if name not in optional or name in parameters} def diagnose_module_result(method: str, result: Any) -> tuple[str, ...]: """ 诊断显式模块结果的基础形状,兼容阶段只告警而不改写返回值。 """ shape = get_module_method_contract(method).result_shape if shape is ModuleResultShape.ANY or result is None: return () matches = { ModuleResultShape.LIST: isinstance(result, list), ModuleResultShape.STRING: isinstance(result, str), ModuleResultShape.MAPPING: isinstance(result, dict), ModuleResultShape.MODEL: isinstance(result, BaseModel), ModuleResultShape.BOOLEAN: isinstance(result, bool), ModuleResultShape.BYTES: isinstance(result, bytes), } if matches.get(shape, True): return () return (f"unexpected-result:{shape.value}:{type(result).__name__}",) def list_explicit_module_contracts() -> dict[str, ModuleMethodContract]: """ 返回显式方法清单的副本,供架构基线和 SDK 文档使用。 """ return dict(_METHOD_CONTRACTS)