mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
refactor: define module contract v2
This commit is contained in:
@@ -1766,6 +1766,338 @@
|
||||
},
|
||||
"producer_count": 66
|
||||
},
|
||||
"module_method_specs": {
|
||||
"download_file": {
|
||||
"aggregation": "first_non_empty",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "storage",
|
||||
"input_contract": "StorageDownloadRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "FileItem | None",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"finalize_message": {
|
||||
"aggregation": "first_non_empty",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "messaging",
|
||||
"input_contract": "MessageFinalizeRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "Message | None",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"get_file_item": {
|
||||
"aggregation": "first_non_empty",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "storage",
|
||||
"input_contract": "StorageItemRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "FileItem | None",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"get_folder": {
|
||||
"aggregation": "first_non_empty",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "storage",
|
||||
"input_contract": "StorageFolderRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "FileItem | None",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"get_parent_item": {
|
||||
"aggregation": "first_non_empty",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "storage",
|
||||
"input_contract": "StorageParentRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "FileItem | None",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"list_files": {
|
||||
"aggregation": "ordered_list_merge",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "storage",
|
||||
"input_contract": "StorageListRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "list[FileItem]",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"media_category": {
|
||||
"aggregation": "legacy",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "media-recognition",
|
||||
"input_contract": "MediaCategoryRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "CategoryConfig | None",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"mediaserver_iteminfo": {
|
||||
"aggregation": "first_non_empty",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "media-server",
|
||||
"input_contract": "MediaServerItemRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "MediaServerItem | None",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"mediaserver_items": {
|
||||
"aggregation": "ordered_list_merge",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "media-server",
|
||||
"input_contract": "MediaServerItemsRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "list[MediaServerItem]",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"mediaserver_play_url": {
|
||||
"aggregation": "first_non_empty",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "media-server",
|
||||
"input_contract": "MediaServerPlayRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "str | None",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"mediaserver_tv_episodes": {
|
||||
"aggregation": "ordered_list_merge",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "media-server",
|
||||
"input_contract": "MediaServerEpisodesRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "list[MediaServerPlayItem]",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"obtain_images": {
|
||||
"aggregation": "legacy",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "media-recognition",
|
||||
"input_contract": "MediaInfo",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "MediaInfo | None",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"recognize_media": {
|
||||
"aggregation": "first_non_empty",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "media-recognition",
|
||||
"input_contract": "MediaRecognitionRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "MediaInfo | None",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"register_commands": {
|
||||
"aggregation": "legacy",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "messaging",
|
||||
"input_contract": "CommandRegistrationRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "None",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"rename_file": {
|
||||
"aggregation": "first_non_empty",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "storage",
|
||||
"input_contract": "StorageRenameRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "bool | FileItem",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"scheduler_job": {
|
||||
"aggregation": "legacy",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "scheduling",
|
||||
"input_contract": "SchedulerJobRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "None",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"search_medias": {
|
||||
"aggregation": "ordered_list_merge",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "media-recognition",
|
||||
"input_contract": "MediaSearchRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "list[MediaInfo]",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"send_message": {
|
||||
"aggregation": "first_non_empty",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "messaging",
|
||||
"input_contract": "MessageSendRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "Message | None",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"snapshot_storage": {
|
||||
"aggregation": "first_non_empty",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "storage",
|
||||
"input_contract": "StorageSnapshotRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "dict[str, dict] | None",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"storage_manage": {
|
||||
"aggregation": "first_non_empty",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "storage",
|
||||
"input_contract": "StorageManageRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "Any",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"upload_file": {
|
||||
"aggregation": "first_non_empty",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "storage",
|
||||
"input_contract": "StorageUploadRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "FileItem | None",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
},
|
||||
"webhook_parser": {
|
||||
"aggregation": "first_non_empty",
|
||||
"error_policy": "isolate_provider",
|
||||
"execution": "sync_or_async",
|
||||
"family": "integration",
|
||||
"input_contract": "WebhookRequest",
|
||||
"plugin_short_circuit": true,
|
||||
"public_to_plugins": true,
|
||||
"required_parameters": [],
|
||||
"result_contract": "WebhookEventInfo | None",
|
||||
"supports_async": true,
|
||||
"supports_sync": true,
|
||||
"timeout_policy": "caller_budget",
|
||||
"version": 1
|
||||
}
|
||||
},
|
||||
"run_module": {
|
||||
"call_count": 260,
|
||||
"dynamic_call_count": 0,
|
||||
|
||||
@@ -4,9 +4,13 @@ import json
|
||||
from pathlib import Path
|
||||
|
||||
from app.runtime.extensions.module.contracts import (
|
||||
ModuleErrorPolicy,
|
||||
ModuleExecutionMode,
|
||||
ModuleResultAggregation,
|
||||
diagnose_module_callable,
|
||||
get_module_method_contract,
|
||||
is_explicit_module_method,
|
||||
list_explicit_module_contracts,
|
||||
)
|
||||
|
||||
|
||||
@@ -23,7 +27,7 @@ def test_all_scanned_module_methods_resolve_a_contract() -> None:
|
||||
assert methods
|
||||
for method in methods:
|
||||
contract = get_module_method_contract(method)
|
||||
assert contract.aggregation is ModuleResultAggregation.LEGACY
|
||||
assert isinstance(contract.aggregation, ModuleResultAggregation)
|
||||
assert contract.plugin_short_circuit is True
|
||||
|
||||
|
||||
@@ -53,3 +57,38 @@ def test_unknown_plugin_method_keeps_legacy_compatibility() -> None:
|
||||
assert contract.family == "legacy"
|
||||
assert contract.supports_sync is True
|
||||
assert contract.supports_async is True
|
||||
|
||||
|
||||
def test_contract_v2_freezes_at_least_twenty_high_value_methods() -> None:
|
||||
"""首批能力必须具备可生成文档和诊断的完整 V2 字段。"""
|
||||
contracts = list_explicit_module_contracts()
|
||||
|
||||
assert len(contracts) >= 20
|
||||
for contract in contracts.values():
|
||||
assert contract.version == 1
|
||||
assert contract.input_contract != "legacy_args"
|
||||
assert contract.result_contract
|
||||
assert contract.execution is ModuleExecutionMode.SYNC_OR_ASYNC
|
||||
assert contract.timeout_policy == "caller_budget"
|
||||
assert contract.error_policy is ModuleErrorPolicy.ISOLATE_PROVIDER
|
||||
assert contract.public_to_plugins is True
|
||||
|
||||
|
||||
def test_signature_diagnostics_do_not_reject_legacy_callable() -> None:
|
||||
"""无法检查的旧插件 callable 只产生诊断,仍由 dispatcher 决定是否执行。"""
|
||||
class _OpaqueCallable:
|
||||
"""模拟 inspect 无法解析签名的第三方 callable。"""
|
||||
|
||||
@property
|
||||
def __signature__(self):
|
||||
"""模拟扩展对象不提供 Python signature。"""
|
||||
raise ValueError("opaque")
|
||||
|
||||
def __call__(self):
|
||||
"""保留可调用行为。"""
|
||||
return "ok"
|
||||
|
||||
assert diagnose_module_callable("recognize_media", _OpaqueCallable()) == (
|
||||
"signature-unavailable",
|
||||
)
|
||||
assert _OpaqueCallable()() == "ok"
|
||||
|
||||
Reference in New Issue
Block a user