mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-06 16:07:01 +08:00
refactor: 推进后端分层架构治理
This commit is contained in:
@@ -0,0 +1 @@
|
||||
"""模块调用契约与调度实现。"""
|
||||
@@ -0,0 +1,82 @@
|
||||
"""字符串模块方法协议的可检查契约清单。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from enum import StrEnum
|
||||
|
||||
|
||||
class ModuleResultAggregation(StrEnum):
|
||||
"""描述多模块结果沿调用链的兼容聚合方式。"""
|
||||
|
||||
LEGACY = "legacy"
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ModuleMethodContract:
|
||||
"""记录一个模块方法族的调用模式和结果规则。"""
|
||||
|
||||
family: str
|
||||
aggregation: ModuleResultAggregation = ModuleResultAggregation.LEGACY
|
||||
supports_sync: bool = True
|
||||
supports_async: bool = True
|
||||
plugin_short_circuit: bool = True
|
||||
|
||||
|
||||
_DEFAULT_CONTRACT = ModuleMethodContract(family="legacy")
|
||||
|
||||
# 首批登记高频能力族。方法名仍保持开放字符串,以兼容第三方插件自定义模块能力;
|
||||
# 未命中项继续使用冻结的 legacy 规则,并由架构快照记录新增调用位置。
|
||||
_METHOD_CONTRACTS = {
|
||||
"recognize_media": ModuleMethodContract(family="media-recognition"),
|
||||
"search_medias": ModuleMethodContract(family="media-recognition"),
|
||||
"obtain_images": ModuleMethodContract(family="media-recognition"),
|
||||
"media_category": ModuleMethodContract(family="media-recognition"),
|
||||
"mediaserver_items": ModuleMethodContract(family="media-server"),
|
||||
"mediaserver_iteminfo": ModuleMethodContract(family="media-server"),
|
||||
"mediaserver_play_url": ModuleMethodContract(family="media-server"),
|
||||
"mediaserver_tv_episodes": ModuleMethodContract(family="media-server"),
|
||||
"download_file": ModuleMethodContract(family="storage"),
|
||||
"upload_file": ModuleMethodContract(family="storage"),
|
||||
"list_files": ModuleMethodContract(family="storage"),
|
||||
"get_file_item": ModuleMethodContract(family="storage"),
|
||||
"get_folder": ModuleMethodContract(family="storage"),
|
||||
"get_parent_item": ModuleMethodContract(family="storage"),
|
||||
"rename_file": ModuleMethodContract(family="storage"),
|
||||
"storage_manage": ModuleMethodContract(family="storage"),
|
||||
"snapshot_storage": ModuleMethodContract(family="storage"),
|
||||
"send_message": ModuleMethodContract(family="messaging"),
|
||||
"finalize_message": ModuleMethodContract(family="messaging"),
|
||||
"register_commands": ModuleMethodContract(family="messaging"),
|
||||
"scheduler_job": ModuleMethodContract(family="scheduling"),
|
||||
"webhook_parser": ModuleMethodContract(family="integration"),
|
||||
}
|
||||
|
||||
_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")),
|
||||
)
|
||||
|
||||
|
||||
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
|
||||
@@ -0,0 +1,285 @@
|
||||
"""宿主模块与插件模块的统一调用算法。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from collections.abc import Callable, Mapping
|
||||
from typing import Any, Protocol
|
||||
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
|
||||
from app.foundation.reflection import ObjectUtils
|
||||
from app.runtime.log import logger
|
||||
from app.runtime.extensions.module.contracts import get_module_method_contract
|
||||
from app.schemas.exception import RateLimitExceededException
|
||||
|
||||
|
||||
class ModuleCatalog(Protocol):
|
||||
"""声明模块调度器消费的最小模块目录能力。"""
|
||||
|
||||
def get_running_modules(self, method: str) -> Any:
|
||||
"""返回实现指定方法的运行中宿主模块。"""
|
||||
|
||||
|
||||
class PluginModuleCatalog(Protocol):
|
||||
"""声明模块调度器消费的最小插件模块目录能力。"""
|
||||
|
||||
def get_plugin_modules(
|
||||
self,
|
||||
) -> Mapping[tuple[str, str], Mapping[str, Callable[..., Any]]]:
|
||||
"""返回插件标识到模块方法表的当前快照。"""
|
||||
|
||||
|
||||
ModuleErrorHandler = Callable[..., None]
|
||||
AsyncFunctionRunner = Callable[..., Any]
|
||||
|
||||
|
||||
class ModuleInvocationDispatcher:
|
||||
"""按既有聚合、短路和异常规则执行插件与宿主模块。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
module_catalog: ModuleCatalog,
|
||||
plugin_catalog: PluginModuleCatalog,
|
||||
plugin_error_handler: ModuleErrorHandler,
|
||||
system_error_handler: ModuleErrorHandler,
|
||||
rate_limit_handler: ModuleErrorHandler,
|
||||
async_function_runner: AsyncFunctionRunner = run_in_threadpool,
|
||||
) -> None:
|
||||
"""保存模块目录和策略回调,不主动发现或创建任何运行时资源。"""
|
||||
self._module_catalog = module_catalog
|
||||
self._plugin_catalog = plugin_catalog
|
||||
self._plugin_error_handler = plugin_error_handler
|
||||
self._system_error_handler = system_error_handler
|
||||
self._rate_limit_handler = rate_limit_handler
|
||||
self._async_function_runner = async_function_runner
|
||||
|
||||
@staticmethod
|
||||
def is_valid_empty(result: Any) -> bool:
|
||||
"""保持旧协议中 ``None`` 与全 ``None`` 元组的空结果定义。"""
|
||||
if isinstance(result, tuple):
|
||||
return all(value is None for value in result)
|
||||
return result is None
|
||||
|
||||
def dispatch(self, method: str, *args: Any, **kwargs: Any) -> Any:
|
||||
"""先执行插件模块,再按优先级执行宿主模块。"""
|
||||
contract = get_module_method_contract(method)
|
||||
logger.debug("模块方法契约:%s -> %s", method, contract.family)
|
||||
result = self.execute_plugin_modules(method, None, *args, **kwargs)
|
||||
if not self.is_valid_empty(result) and not isinstance(result, list):
|
||||
return result
|
||||
return self.execute_system_modules(method, result, *args, **kwargs)
|
||||
|
||||
async def async_dispatch(self, method: str, *args: Any, **kwargs: Any) -> Any:
|
||||
"""以与同步路径相同的聚合规则执行同步或异步模块方法。"""
|
||||
contract = get_module_method_contract(method)
|
||||
logger.debug("异步模块方法契约:%s -> %s", method, contract.family)
|
||||
result = await self.async_execute_plugin_modules(
|
||||
method,
|
||||
None,
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
if not self.is_valid_empty(result) and not isinstance(result, list):
|
||||
return result
|
||||
return await self.async_execute_system_modules(
|
||||
method,
|
||||
result,
|
||||
*args,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
def execute_plugin_modules(
|
||||
self,
|
||||
method: str,
|
||||
result: Any,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""同步执行插件方法,保留插件顺序、短路和列表合并语义。"""
|
||||
for plugin, module_dict in self._plugin_catalog.get_plugin_modules().items():
|
||||
plugin_id, plugin_name = plugin
|
||||
func = module_dict.get(method)
|
||||
if not func:
|
||||
continue
|
||||
try:
|
||||
logger.info("请求插件 %s 执行:%s ...", plugin_name, method)
|
||||
if self.is_valid_empty(result):
|
||||
result = func(*args, **kwargs)
|
||||
elif isinstance(result, list):
|
||||
temp = func(*args, **kwargs)
|
||||
if isinstance(temp, list):
|
||||
result.extend(temp)
|
||||
else:
|
||||
break
|
||||
except RateLimitExceededException as err:
|
||||
self._rate_limit_handler(
|
||||
err,
|
||||
"插件",
|
||||
plugin_id,
|
||||
method,
|
||||
**kwargs,
|
||||
)
|
||||
except Exception as err:
|
||||
self._plugin_error_handler(
|
||||
err,
|
||||
plugin_id,
|
||||
plugin_name,
|
||||
method,
|
||||
**kwargs,
|
||||
)
|
||||
return result
|
||||
|
||||
async def async_execute_plugin_modules(
|
||||
self,
|
||||
method: str,
|
||||
result: Any,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""异步执行插件方法,并把同步函数移入线程池。"""
|
||||
for plugin, module_dict in self._plugin_catalog.get_plugin_modules().items():
|
||||
plugin_id, plugin_name = plugin
|
||||
func = module_dict.get(method)
|
||||
if not func:
|
||||
continue
|
||||
try:
|
||||
logger.info("请求插件 %s 执行:%s ...", plugin_name, method)
|
||||
if self.is_valid_empty(result):
|
||||
result = await self._async_call(func, *args, **kwargs)
|
||||
elif isinstance(result, list):
|
||||
temp = await self._async_call(func, *args, **kwargs)
|
||||
if isinstance(temp, list):
|
||||
result.extend(temp)
|
||||
else:
|
||||
break
|
||||
except RateLimitExceededException as err:
|
||||
self._rate_limit_handler(
|
||||
err,
|
||||
"插件",
|
||||
plugin_id,
|
||||
method,
|
||||
**kwargs,
|
||||
)
|
||||
except Exception as err:
|
||||
self._plugin_error_handler(
|
||||
err,
|
||||
plugin_id,
|
||||
plugin_name,
|
||||
method,
|
||||
**kwargs,
|
||||
)
|
||||
return result
|
||||
|
||||
def execute_system_modules(
|
||||
self,
|
||||
method: str,
|
||||
result: Any,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""同步执行按优先级排序的宿主模块,并支持签名接力。"""
|
||||
logger.debug("请求系统模块执行:%s ...", method)
|
||||
modules = sorted(
|
||||
self._module_catalog.get_running_modules(method),
|
||||
key=lambda module: module.get_priority(),
|
||||
)
|
||||
for module in modules:
|
||||
module_id = module.__class__.__name__
|
||||
module_name = self._module_name(module, module_id)
|
||||
try:
|
||||
func = getattr(module, method)
|
||||
if self.is_valid_empty(result):
|
||||
result = func(*args, **kwargs)
|
||||
elif ObjectUtils.check_signature(func, result):
|
||||
result = func(result)
|
||||
elif isinstance(result, list):
|
||||
temp = func(*args, **kwargs)
|
||||
if isinstance(temp, list):
|
||||
result.extend(temp)
|
||||
else:
|
||||
break
|
||||
except RateLimitExceededException as err:
|
||||
self._rate_limit_handler(
|
||||
err,
|
||||
"模块",
|
||||
module_id,
|
||||
method,
|
||||
**kwargs,
|
||||
)
|
||||
except Exception as err:
|
||||
self._system_error_handler(
|
||||
err,
|
||||
module_id,
|
||||
module_name,
|
||||
method,
|
||||
**kwargs,
|
||||
)
|
||||
return result
|
||||
|
||||
async def async_execute_system_modules(
|
||||
self,
|
||||
method: str,
|
||||
result: Any,
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""异步执行宿主模块,并保持同步路径的签名接力与聚合顺序。"""
|
||||
logger.debug("请求系统模块执行:%s ...", method)
|
||||
modules = sorted(
|
||||
self._module_catalog.get_running_modules(method),
|
||||
key=lambda module: module.get_priority(),
|
||||
)
|
||||
for module in modules:
|
||||
module_id = module.__class__.__name__
|
||||
module_name = self._module_name(module, module_id)
|
||||
try:
|
||||
func = getattr(module, method)
|
||||
if self.is_valid_empty(result):
|
||||
result = await self._async_call(func, *args, **kwargs)
|
||||
elif ObjectUtils.check_signature(func, result):
|
||||
result = await self._async_call(func, result)
|
||||
elif isinstance(result, list):
|
||||
temp = await self._async_call(func, *args, **kwargs)
|
||||
if isinstance(temp, list):
|
||||
result.extend(temp)
|
||||
else:
|
||||
break
|
||||
except RateLimitExceededException as err:
|
||||
self._rate_limit_handler(
|
||||
err,
|
||||
"模块",
|
||||
module_id,
|
||||
method,
|
||||
**kwargs,
|
||||
)
|
||||
except Exception as err:
|
||||
self._system_error_handler(
|
||||
err,
|
||||
module_id,
|
||||
module_name,
|
||||
method,
|
||||
**kwargs,
|
||||
)
|
||||
return result
|
||||
|
||||
async def _async_call(
|
||||
self,
|
||||
func: Callable[..., Any],
|
||||
*args: Any,
|
||||
**kwargs: Any,
|
||||
) -> Any:
|
||||
"""调用协程函数,或通过注入的线程池执行器运行同步函数。"""
|
||||
if inspect.iscoroutinefunction(func):
|
||||
return await func(*args, **kwargs)
|
||||
return await self._async_function_runner(func, *args, **kwargs)
|
||||
|
||||
@staticmethod
|
||||
def _module_name(module: Any, fallback: str) -> str:
|
||||
"""读取模块展示名,失败时回退到稳定类名。"""
|
||||
try:
|
||||
return module.get_name()
|
||||
except Exception as err:
|
||||
logger.debug("获取模块名称出错:%s", str(err))
|
||||
return fallback
|
||||
Reference in New Issue
Block a user