"""宿主模块与插件模块的统一调用算法。""" from __future__ import annotations import inspect from collections.abc import Callable, Mapping from enum import StrEnum from typing import Any, Protocol, cast from app.foundation.reflection import ObjectUtils from app.runtime.execution import run_in_threadpool_to_completion from app.runtime.log import logger from app.runtime.observability import observe_duration, record_metric from app.runtime.extensions.module.contracts import ( ModuleResultAggregation, diagnose_module_callable, diagnose_module_result, get_module_method_contract, is_explicit_module_method, ) 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 _ProviderCallMode(StrEnum): """描述当前 provider 应采用的兼容调用方式。""" ORIGINAL = "original" RELAY = "relay" STOP = "stop" 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_to_completion, ) -> 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) with observe_duration( "module.provider.duration", method=method, provider_type="plugin" ): result = self.execute_plugin_modules(method, None, *args, **kwargs) if ( contract.plugin_short_circuit and not self.is_valid_empty(result) and not isinstance(result, list) ): return result with observe_duration( "module.provider.duration", method=method, provider_type="system" ): 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) with observe_duration( "module.provider.duration", method=method, provider_type="plugin" ): result = await self.async_execute_plugin_modules( method, None, *args, **kwargs, ) if ( contract.plugin_short_circuit and not self.is_valid_empty(result) and not isinstance(result, list) ): return result with observe_duration( "module.provider.duration", method=method, provider_type="system" ): 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: """同步执行插件方法,保留插件顺序、短路和列表合并语义。""" aggregation = get_module_method_contract(method).aggregation 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._record_legacy_hit( method, caller_type="plugin", abi_source="third_party_plugin", ) self._diagnose_callable(method, func, f"插件 {plugin_id}") logger.info("请求插件 %s 执行:%s ...", plugin_name, method) call_mode = self._provider_call_mode( aggregation, result, func, allow_relay=False, ) if call_mode is _ProviderCallMode.STOP: break provider_result = func(*args, **kwargs) self._diagnose_result(method, provider_result, "plugin") result = self._aggregate_provider_result( result, provider_result, call_mode, aggregation, ) except RateLimitExceededException as err: self._rate_limit_handler( err, "插件", plugin_id, method, **kwargs, ) except Exception as err: self._record_timeout(method, "plugin", 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: """异步执行插件方法,并把同步函数移入线程池。""" aggregation = get_module_method_contract(method).aggregation 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._record_legacy_hit( method, caller_type="plugin", abi_source="third_party_plugin", ) self._diagnose_callable(method, func, f"插件 {plugin_id}") logger.info("请求插件 %s 执行:%s ...", plugin_name, method) call_mode = self._provider_call_mode( aggregation, result, func, allow_relay=False, ) if call_mode is _ProviderCallMode.STOP: break provider_result = await self._async_call(func, *args, **kwargs) self._diagnose_result(method, provider_result, "plugin") result = self._aggregate_provider_result( result, provider_result, call_mode, aggregation, ) except RateLimitExceededException as err: self._rate_limit_handler( err, "插件", plugin_id, method, **kwargs, ) except Exception as err: self._record_timeout(method, "plugin", 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) aggregation = get_module_method_contract(method).aggregation 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._record_legacy_hit( method, caller_type="system", abi_source="host_module", ) self._diagnose_callable(method, func, f"宿主模块 {module_id}") call_mode = self._provider_call_mode( aggregation, result, func, allow_relay=True, ) if call_mode is _ProviderCallMode.STOP: break if call_mode is _ProviderCallMode.RELAY: provider_result = func(result) else: provider_result = func(*args, **kwargs) self._diagnose_result(method, provider_result, "system") result = self._aggregate_provider_result( result, provider_result, call_mode, aggregation, ) except RateLimitExceededException as err: self._rate_limit_handler( err, "模块", module_id, method, **kwargs, ) except Exception as err: self._record_timeout(method, "system", 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) aggregation = get_module_method_contract(method).aggregation 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._record_legacy_hit( method, caller_type="system", abi_source="host_module", ) self._diagnose_callable(method, func, f"宿主模块 {module_id}") call_mode = self._provider_call_mode( aggregation, result, func, allow_relay=True, ) if call_mode is _ProviderCallMode.STOP: break if call_mode is _ProviderCallMode.RELAY: provider_result = await self._async_call(func, result) else: provider_result = await self._async_call(func, *args, **kwargs) self._diagnose_result(method, provider_result, "system") result = self._aggregate_provider_result( result, provider_result, call_mode, aggregation, ) except RateLimitExceededException as err: self._rate_limit_handler( err, "模块", module_id, method, **kwargs, ) except Exception as err: self._record_timeout(method, "system", err) self._system_error_handler( err, module_id, module_name, method, **kwargs, ) return result @classmethod def _provider_call_mode( cls, aggregation: ModuleResultAggregation, result: Any, func: Callable[..., Any], *, allow_relay: bool, ) -> _ProviderCallMode: """按契约选择下一 provider 的调用方式,并冻结 legacy 接力语义。""" if aggregation is ModuleResultAggregation.FAN_OUT: return _ProviderCallMode.ORIGINAL if cls.is_valid_empty(result): return _ProviderCallMode.ORIGINAL if aggregation is ModuleResultAggregation.FIRST_NON_EMPTY: return _ProviderCallMode.STOP if aggregation is ModuleResultAggregation.ORDERED_LIST_MERGE: return ( _ProviderCallMode.ORIGINAL if isinstance(result, list) else _ProviderCallMode.STOP ) if aggregation is ModuleResultAggregation.ORDERED_MAPPING_MERGE: return ( _ProviderCallMode.ORIGINAL if isinstance(result, dict) else _ProviderCallMode.STOP ) if aggregation in { ModuleResultAggregation.LEGACY, ModuleResultAggregation.PIPELINE_RELAY, }: if allow_relay and ObjectUtils.check_signature(func, result): return _ProviderCallMode.RELAY if isinstance(result, list): return _ProviderCallMode.ORIGINAL return _ProviderCallMode.STOP @staticmethod def _aggregate_provider_result( result: Any, provider_result: Any, call_mode: _ProviderCallMode, aggregation: ModuleResultAggregation, ) -> Any: """合并单个 provider 结果,接力调用则用新结果替换旧结果。""" if aggregation is ModuleResultAggregation.FAN_OUT: return result if call_mode is _ProviderCallMode.RELAY: return provider_result if isinstance(result, list) and isinstance(provider_result, list): result.extend(provider_result) elif isinstance(result, dict) and isinstance(provider_result, dict): result.update(provider_result) elif not isinstance(result, (list, dict)): return provider_result return result @staticmethod def _record_timeout(method: str, provider_type: str, error: Exception) -> None: """仅把真实超时归入低基数模块超时指标。""" if isinstance(error, TimeoutError): record_metric( "module.provider.timeout", method=method, provider_type=provider_type, ) @staticmethod def _record_legacy_hit( method: str, *, caller_type: str, abi_source: str, ) -> None: """记录未知动态方法的兼容命中,便于按真实调用逐项迁移。""" if not is_explicit_module_method(method): record_metric( "module.contract.legacy_hit", method=method, caller_type=caller_type, abi_source=abi_source, ) @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), ) @staticmethod def _diagnose_result(method: str, result: Any, provider_type: str) -> None: """记录 provider 结果形状偏差,保持旧插件返回值原样继续执行。""" problems = diagnose_module_result(method, result) if problems: record_metric( "module.contract.result_mismatch", method=method, provider_type=provider_type, problem=problems[0], ) logger.warning( "模块方法 %s 的 %s provider 返回值与契约不一致:%s;当前仅诊断", method, provider_type, ", ".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 cast(str, module.get_name()) except Exception as err: logger.debug("获取模块名称出错:%s", str(err)) return fallback