mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-08-29 03:56:43 +08:00
318 lines
12 KiB
Python
318 lines
12 KiB
Python
"""宿主模块与插件模块的统一调用算法。"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import inspect
|
|
from collections.abc import Callable, Mapping
|
|
from typing import Any, Protocol
|
|
|
|
from app.foundation.reflection import ObjectUtils
|
|
from app.runtime.execution import run_in_threadpool
|
|
from app.runtime.log import logger
|
|
from app.runtime.extensions.module.contracts import (
|
|
diagnose_module_callable,
|
|
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
|
|
try:
|
|
# 防御坏插件把方法表声明成非映射类型,避免击穿整个模块调度
|
|
if not isinstance(module_dict, Mapping):
|
|
raise TypeError(
|
|
f"插件 {plugin_id} 的模块声明必须是映射,实际是 {type(module_dict).__name__}"
|
|
)
|
|
func = module_dict.get(method)
|
|
if not func:
|
|
continue
|
|
self._diagnose_callable(method, func, f"插件 {plugin_id}")
|
|
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
|
|
try:
|
|
# 防御坏插件把方法表声明成非映射类型,避免击穿整个模块调度
|
|
if not isinstance(module_dict, Mapping):
|
|
raise TypeError(
|
|
f"插件 {plugin_id} 的模块声明必须是映射,实际是 {type(module_dict).__name__}"
|
|
)
|
|
func = module_dict.get(method)
|
|
if not func:
|
|
continue
|
|
self._diagnose_callable(method, func, f"插件 {plugin_id}")
|
|
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)
|
|
self._diagnose_callable(method, func, f"宿主模块 {module_id}")
|
|
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)
|
|
self._diagnose_callable(method, func, f"宿主模块 {module_id}")
|
|
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
|
|
|
|
@staticmethod
|
|
def _diagnose_callable(
|
|
method: str,
|
|
callback: Callable[..., Any],
|
|
owner: str,
|
|
) -> None:
|
|
"""记录 Contract V2 签名偏差,兼容阶段不阻断旧插件执行。"""
|
|
problems = diagnose_module_callable(method, callback)
|
|
if problems:
|
|
logger.warning(
|
|
"%s 的模块方法 %s 与契约不一致:%s;当前仅诊断",
|
|
owner,
|
|
method,
|
|
", ".join(problems),
|
|
)
|
|
|
|
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
|