mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
refactor: 推进后端分层架构治理
This commit is contained in:
@@ -5,7 +5,7 @@ import importlib.util
|
||||
import sys
|
||||
import threading
|
||||
from types import ModuleType
|
||||
from typing import Dict, Optional
|
||||
from typing import Dict
|
||||
|
||||
from app.runtime.compat.diagnostics import record_legacy_import
|
||||
from app.runtime.compat.manifest import (
|
||||
|
||||
@@ -757,9 +757,9 @@ SYMBOL_ALIASES: Dict[str, Dict[str, SymbolAlias]] = {
|
||||
replacement="app.chain.agent.AgentChain",
|
||||
),
|
||||
"ReplyMode": SymbolAlias(
|
||||
target_module="app.schemas.agent",
|
||||
target_module="app.schemas.types",
|
||||
target_name="ReplyMode",
|
||||
replacement="app.schemas.agent.ReplyMode",
|
||||
replacement="app.schemas.types.ReplyMode",
|
||||
),
|
||||
},
|
||||
# 刮削能力从 MediaChain 拆出为独立 ScrapingChain 后,
|
||||
@@ -807,12 +807,59 @@ SYMBOL_ALIASES: Dict[str, Dict[str, SymbolAlias]] = {
|
||||
**_MESSAGE_NOTIFICATION_SYMBOL_ALIASES,
|
||||
},
|
||||
"app.schemas.transfer": {
|
||||
**{
|
||||
name: SymbolAlias(
|
||||
target_module="app.sdk._legacy.transfer",
|
||||
target_name=name,
|
||||
replacement=f"app.application.transfer.{name}",
|
||||
)
|
||||
for name in ("TransferTask", "TransferQueue")
|
||||
},
|
||||
"DownloadHistory": SymbolAlias(
|
||||
target_module="app.schemas.history",
|
||||
target_name="DownloadHistory",
|
||||
replacement="app.schemas.history.DownloadHistory",
|
||||
),
|
||||
"TransferDirectoryConf": SymbolAlias(
|
||||
target_module="app.schemas.system",
|
||||
target_name="TransferDirectoryConf",
|
||||
replacement="app.schemas.system.TransferDirectoryConf",
|
||||
),
|
||||
"TmdbEpisode": SymbolAlias(
|
||||
target_module="app.schemas.tmdb",
|
||||
target_name="TmdbEpisode",
|
||||
replacement="app.schemas.tmdb.TmdbEpisode",
|
||||
),
|
||||
"MediaType": SymbolAlias(
|
||||
target_module="app.schemas.types",
|
||||
target_name="MediaType",
|
||||
replacement="app.schemas.types.MediaType",
|
||||
),
|
||||
},
|
||||
"app.schemas.agent": {
|
||||
"ReplyMode": SymbolAlias(
|
||||
target_module="app.schemas.types",
|
||||
target_name="ReplyMode",
|
||||
replacement="app.schemas.types.ReplyMode",
|
||||
),
|
||||
},
|
||||
"app.sdk.logging": {
|
||||
name: SymbolAlias(
|
||||
target_module="app.sdk._legacy.transfer",
|
||||
target_module="app.runtime.log",
|
||||
target_name=name,
|
||||
replacement=f"app.application.transfer.{name}",
|
||||
replacement=f"app.runtime.log.{name}",
|
||||
)
|
||||
for name in (
|
||||
"CustomFormatter",
|
||||
"LogConfigModel",
|
||||
"LogEntry",
|
||||
"LogSettings",
|
||||
"LoggerManager",
|
||||
"NonBlockingFileHandler",
|
||||
"configure_log_settings",
|
||||
"configure_log_writer",
|
||||
"log_settings",
|
||||
)
|
||||
for name in ("TransferTask", "TransferQueue")
|
||||
},
|
||||
# message/notification 命名统一:通知渠道能力归 notification,消息收发归 message
|
||||
"app.schemas.types": {
|
||||
|
||||
@@ -24,7 +24,7 @@ from app.runtime.log import (
|
||||
log_settings,
|
||||
NonBlockingFileHandler,
|
||||
)
|
||||
from app.schemas import MediaType
|
||||
from app.schemas.types import MediaType
|
||||
from app.adapters.system.host import SystemUtils
|
||||
from app.foundation.url import UrlUtils
|
||||
from version import APP_VERSION
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
"""事件运行时内部组件。"""
|
||||
@@ -0,0 +1,138 @@
|
||||
"""事件处理器声明到运行实例的显式绑定解析。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from collections.abc import Callable
|
||||
from dataclasses import dataclass
|
||||
from typing import Any, Optional, Type
|
||||
|
||||
from app.runtime.event.registry import EventRegistry
|
||||
from app.runtime.log import logger
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class EventHandlerBinding:
|
||||
"""描述上层运行时为某个事件处理器提供的实例绑定。"""
|
||||
|
||||
instance: Optional[Any]
|
||||
owner_name: str
|
||||
run_sync_in_threadpool: bool = False
|
||||
|
||||
|
||||
HandlerInstanceResolver = Callable[
|
||||
[Type[Any]], Optional[EventHandlerBinding]
|
||||
]
|
||||
|
||||
|
||||
class EventBindingResolver:
|
||||
"""只通过已登记 resolver 把类处理器绑定到托管运行实例。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
lock: Any,
|
||||
resolvers: Callable[[], dict[str, HandlerInstanceResolver]],
|
||||
) -> None:
|
||||
"""绑定 resolver 存储,并记录未命中的处理器用于启动诊断。"""
|
||||
self._lock = lock
|
||||
self._resolvers = resolvers
|
||||
self._unresolved: set[str] = set()
|
||||
|
||||
def register(self, name: str, resolver: HandlerInstanceResolver) -> None:
|
||||
"""注册或替换命名实例解析器。"""
|
||||
with self._lock:
|
||||
self._resolvers()[name] = resolver
|
||||
|
||||
def unresolved_handlers(self) -> tuple[str, ...]:
|
||||
"""返回本进程中未被显式 resolver 接管的类处理器。"""
|
||||
with self._lock:
|
||||
return tuple(sorted(self._unresolved))
|
||||
|
||||
@staticmethod
|
||||
def parse_handler_names(handler: Callable) -> tuple[str, str]:
|
||||
"""解析处理器限定名中的类名和方法名。"""
|
||||
names = handler.__qualname__.split(".")
|
||||
if len(names) < 2:
|
||||
return "", names[0]
|
||||
return names[0], names[1]
|
||||
|
||||
@staticmethod
|
||||
def owner_class(handler: Callable) -> Optional[Type[Any]]:
|
||||
"""从处理器对象本身解析声明类,不按字符串动态导入模块。"""
|
||||
if inspect.ismethod(handler):
|
||||
owner = handler.__self__
|
||||
return owner if isinstance(owner, type) else type(owner)
|
||||
module = inspect.getmodule(handler)
|
||||
if not module:
|
||||
return None
|
||||
owner: Any = module
|
||||
for part in handler.__qualname__.split(".")[:-1]:
|
||||
if part == "<locals>":
|
||||
return None
|
||||
owner = getattr(owner, part, None)
|
||||
if owner is None:
|
||||
return None
|
||||
return owner if isinstance(owner, type) else None
|
||||
|
||||
def resolve(
|
||||
self,
|
||||
handler: Callable,
|
||||
) -> Optional[tuple[Callable, EventHandlerBinding, str, str]]:
|
||||
"""通过显式 resolver 解析当前实例方法;自由函数直接返回。"""
|
||||
owner_class = self.owner_class(handler)
|
||||
method_name = getattr(
|
||||
handler,
|
||||
"__name__",
|
||||
self.parse_handler_names(handler)[1],
|
||||
)
|
||||
if owner_class is None:
|
||||
binding = EventHandlerBinding(
|
||||
instance=None,
|
||||
owner_name=EventRegistry.handler_identifier(handler),
|
||||
run_sync_in_threadpool=True,
|
||||
)
|
||||
return handler, binding, "", method_name
|
||||
|
||||
with self._lock:
|
||||
resolvers = tuple(self._resolvers().items())
|
||||
binding = None
|
||||
resolver_name = ""
|
||||
for name, resolver in resolvers:
|
||||
candidate = resolver(owner_class)
|
||||
if candidate is not None:
|
||||
binding = candidate
|
||||
resolver_name = name
|
||||
break
|
||||
if binding is None:
|
||||
identifier = EventRegistry.handler_identifier(handler)
|
||||
with self._lock:
|
||||
first_miss = identifier not in self._unresolved
|
||||
self._unresolved.add(identifier)
|
||||
if first_miss:
|
||||
logger.warning(
|
||||
"事件处理器未绑定显式 resolver,已跳过:%s",
|
||||
identifier,
|
||||
)
|
||||
return None
|
||||
logger.debug(
|
||||
"事件处理器绑定:%s -> %s",
|
||||
EventRegistry.handler_identifier(handler),
|
||||
resolver_name,
|
||||
)
|
||||
if binding.instance is None:
|
||||
return None
|
||||
method = getattr(binding.instance, method_name, None)
|
||||
if not callable(method):
|
||||
fallback_name = self.parse_handler_names(handler)[1]
|
||||
method = getattr(binding.instance, fallback_name, None)
|
||||
if fallback_name == method_name or not callable(method):
|
||||
logger.warning(
|
||||
"事件处理器 %s 无法解析为实例方法 %s.%s,跳过执行",
|
||||
EventRegistry.handler_identifier(handler),
|
||||
owner_class.__name__,
|
||||
method_name,
|
||||
)
|
||||
return None
|
||||
method_name = fallback_name
|
||||
return method, binding, owner_class.__name__, method_name
|
||||
@@ -0,0 +1,196 @@
|
||||
"""链式和广播事件的独立调度算法。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import inspect
|
||||
import time
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
|
||||
from app.runtime.event.binding import EventBindingResolver
|
||||
from app.runtime.event.registry import EventRegistry
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import EventType
|
||||
|
||||
|
||||
class EventDispatcher:
|
||||
"""基于订阅快照执行链式或广播事件,不拥有注册和生命周期状态。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
registry: EventRegistry,
|
||||
binding_resolver: EventBindingResolver,
|
||||
executor: Callable[[], Any],
|
||||
event_loop: Callable[[], Any],
|
||||
event_factory: Callable[..., Any],
|
||||
error_handler: Callable[..., None],
|
||||
) -> None:
|
||||
"""注入注册表、绑定器、执行器和错误策略回调。"""
|
||||
self._registry = registry
|
||||
self._binding_resolver = binding_resolver
|
||||
self._executor = executor
|
||||
self._event_loop = event_loop
|
||||
self._event_factory = event_factory
|
||||
self._error_handler = error_handler
|
||||
|
||||
def dispatch_chain(self, event: Any) -> bool:
|
||||
"""同步按优先级顺序执行链式事件快照。"""
|
||||
handlers = self._registry.chain_snapshot(event.event_type)
|
||||
enabled = tuple(
|
||||
(handler_id, priority, handler)
|
||||
for handler_id, (priority, handler) in handlers
|
||||
if self._registry.is_handler_enabled(handler)
|
||||
)
|
||||
if not enabled:
|
||||
logger.debug("No enabled handlers found for chain event: %s", event)
|
||||
return False
|
||||
self._log_lifecycle(event, "Started")
|
||||
for _handler_id, priority, handler in enabled:
|
||||
started_at = time.time()
|
||||
self.invoke_sync(handler, event)
|
||||
logger.debug(
|
||||
"%s (Priority: %s), completed in %.3fs for event: %s",
|
||||
EventRegistry.handler_identifier(handler),
|
||||
priority,
|
||||
time.time() - started_at,
|
||||
event,
|
||||
)
|
||||
self._log_lifecycle(event, "Completed")
|
||||
return True
|
||||
|
||||
async def async_dispatch_chain(self, event: Any) -> bool:
|
||||
"""异步按优先级顺序执行链式事件快照。"""
|
||||
handlers = self._registry.chain_snapshot(event.event_type)
|
||||
enabled = tuple(
|
||||
(handler_id, priority, handler)
|
||||
for handler_id, (priority, handler) in handlers
|
||||
if self._registry.is_handler_enabled(handler)
|
||||
)
|
||||
if not enabled:
|
||||
logger.debug("No enabled handlers found for chain event: %s", event)
|
||||
return False
|
||||
self._log_lifecycle(event, "Started")
|
||||
for _handler_id, priority, handler in enabled:
|
||||
started_at = time.time()
|
||||
await self.invoke_async(handler, event)
|
||||
logger.debug(
|
||||
"%s (Priority: %s), completed in %.3fs for event: %s",
|
||||
EventRegistry.handler_identifier(handler),
|
||||
priority,
|
||||
time.time() - started_at,
|
||||
event,
|
||||
)
|
||||
self._log_lifecycle(event, "Completed")
|
||||
return True
|
||||
|
||||
def dispatch_broadcast(self, event: Any) -> None:
|
||||
"""按订阅快照把广播事件投递到线程池或主事件循环。"""
|
||||
handlers = self._registry.broadcast_snapshot(event.event_type)
|
||||
if not handlers:
|
||||
logger.debug("No handlers found for broadcast event: %s", event)
|
||||
return
|
||||
target_plugin_id = None
|
||||
if event.event_type == EventType.MessageAction and isinstance(
|
||||
event.event_data,
|
||||
dict,
|
||||
):
|
||||
target_plugin_id = event.event_data.get("__mp_target_plugin_id")
|
||||
for handler_id, handler in handlers:
|
||||
if target_plugin_id and not self.should_dispatch_to_target_plugin(
|
||||
handler,
|
||||
handler_id,
|
||||
str(target_plugin_id),
|
||||
):
|
||||
continue
|
||||
if isinstance(event.event_data, dict):
|
||||
event_data = event.event_data.copy()
|
||||
event_data.pop("__mp_target_plugin_id", None)
|
||||
else:
|
||||
event_data = event.event_data
|
||||
isolated = self._event_factory(
|
||||
event_type=event.event_type,
|
||||
event_data=event_data,
|
||||
priority=event.priority,
|
||||
)
|
||||
if inspect.iscoroutinefunction(handler):
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.safe_invoke_async(handler, isolated),
|
||||
self._event_loop(),
|
||||
)
|
||||
else:
|
||||
self._executor().submit(
|
||||
self.safe_invoke_sync,
|
||||
handler,
|
||||
isolated,
|
||||
)
|
||||
|
||||
def safe_invoke_sync(self, handler: Callable, event: Any) -> None:
|
||||
"""仅在处理器启用时执行同步调用。"""
|
||||
if self._registry.is_handler_enabled(handler):
|
||||
self.invoke_sync(handler, event)
|
||||
|
||||
async def safe_invoke_async(self, handler: Callable, event: Any) -> None:
|
||||
"""仅在处理器启用时执行异步调用。"""
|
||||
if self._registry.is_handler_enabled(handler):
|
||||
await self.invoke_async(handler, event)
|
||||
|
||||
def invoke_sync(self, handler: Callable, event: Any) -> None:
|
||||
"""解析实例绑定并同步调用处理器。"""
|
||||
resolved = self._binding_resolver.resolve(handler)
|
||||
if not resolved:
|
||||
return
|
||||
method, binding, class_name, method_name = resolved
|
||||
try:
|
||||
method(event)
|
||||
except Exception as err:
|
||||
self._error_handler(
|
||||
event=event,
|
||||
module_name=binding.owner_name,
|
||||
class_name=class_name,
|
||||
method_name=method_name,
|
||||
e=err,
|
||||
)
|
||||
|
||||
async def invoke_async(self, handler: Callable, event: Any) -> None:
|
||||
"""解析实例绑定,并按处理器类型选择协程、线程池或同步调用。"""
|
||||
resolved = self._binding_resolver.resolve(handler)
|
||||
if not resolved:
|
||||
return
|
||||
method, binding, class_name, method_name = resolved
|
||||
try:
|
||||
if inspect.iscoroutinefunction(method):
|
||||
await method(event)
|
||||
elif binding.run_sync_in_threadpool or not class_name:
|
||||
await run_in_threadpool(method, event)
|
||||
else:
|
||||
method(event)
|
||||
except Exception as err:
|
||||
self._error_handler(
|
||||
event=event,
|
||||
module_name=binding.owner_name,
|
||||
class_name=class_name,
|
||||
method_name=method_name,
|
||||
e=err,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def should_dispatch_to_target_plugin(
|
||||
handler: Callable,
|
||||
handler_identifier: str,
|
||||
target_plugin_id: str,
|
||||
) -> bool:
|
||||
"""只把定向输入事件投递给标识和声明均匹配的目标插件。"""
|
||||
class_name, method_name = EventBindingResolver.parse_handler_names(handler)
|
||||
if class_name != target_plugin_id:
|
||||
return False
|
||||
parts = (handler_identifier or "").split(".")
|
||||
return len(parts) >= 2 and parts[-2:] == [class_name, method_name]
|
||||
|
||||
@staticmethod
|
||||
def _log_lifecycle(event: Any, stage: str) -> None:
|
||||
"""记录事件调度的开始和完成阶段。"""
|
||||
logger.debug("%s - %s", stage, event)
|
||||
@@ -0,0 +1,65 @@
|
||||
"""事件处理异常的通知、降级和递归保护策略。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import traceback
|
||||
from collections.abc import Callable
|
||||
from typing import Any, Optional
|
||||
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import EventType
|
||||
|
||||
|
||||
EventErrorNotifier = Callable[[str, str], object]
|
||||
|
||||
|
||||
class EventErrorPolicy:
|
||||
"""隔离处理器异常,并阻止 SystemError 处理失败再次广播。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
notifier: Callable[[], Optional[EventErrorNotifier]],
|
||||
emit_system_error: Callable[[dict], object],
|
||||
) -> None:
|
||||
"""注入通知读取器和 SystemError 发送回调。"""
|
||||
self._notifier = notifier
|
||||
self._emit_system_error = emit_system_error
|
||||
|
||||
def handle(
|
||||
self,
|
||||
*,
|
||||
event: Any,
|
||||
module_name: str,
|
||||
class_name: str,
|
||||
method_name: str,
|
||||
error: Exception,
|
||||
) -> None:
|
||||
"""记录并通知异常;SystemError 自身失败时只降级写日志。"""
|
||||
trace = traceback.format_exc()
|
||||
logger.error("%s 事件处理出错:%s - %s", module_name, str(error), trace)
|
||||
notifier = self._notifier()
|
||||
if notifier:
|
||||
try:
|
||||
notifier(
|
||||
f"{module_name} 处理事件 {event.event_type} 时出错",
|
||||
f"{class_name}.{method_name}:{str(error)}",
|
||||
)
|
||||
except Exception as notify_error:
|
||||
logger.error("发送事件错误通知失败:%s", str(notify_error))
|
||||
if event.event_type == EventType.SystemError:
|
||||
logger.error(
|
||||
"SystemError 处理器再次失败,停止错误事件递归广播:%s.%s",
|
||||
class_name,
|
||||
method_name,
|
||||
)
|
||||
return
|
||||
self._emit_system_error(
|
||||
{
|
||||
"type": "event",
|
||||
"event_type": event.event_type,
|
||||
"event_handle": f"{class_name}.{method_name}",
|
||||
"error": str(error),
|
||||
"traceback": trace,
|
||||
}
|
||||
)
|
||||
@@ -0,0 +1,200 @@
|
||||
"""事件订阅、禁用状态和调度快照注册表。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import inspect
|
||||
from collections.abc import Callable
|
||||
from typing import Any
|
||||
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import ChainEventType, EventType
|
||||
|
||||
|
||||
class EventRegistry:
|
||||
"""集中管理事件处理器注册、启停和不可变调度快照。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
lock: Any,
|
||||
broadcast_subscribers: Callable[[], dict],
|
||||
chain_subscribers: Callable[[], dict],
|
||||
disabled_handlers: Callable[[], set],
|
||||
disabled_classes: Callable[[], set],
|
||||
) -> None:
|
||||
"""绑定由兼容门面持有的存储,便于热重载和旧测试替换快照。"""
|
||||
self._lock = lock
|
||||
self._broadcast_subscribers = broadcast_subscribers
|
||||
self._chain_subscribers = chain_subscribers
|
||||
self._disabled_handlers = disabled_handlers
|
||||
self._disabled_classes = disabled_classes
|
||||
|
||||
@staticmethod
|
||||
def handler_identifier(target: Callable | type) -> str:
|
||||
"""返回包含模块和限定名的稳定处理器标识。"""
|
||||
module = inspect.getmodule(target)
|
||||
module_name = module.__name__ if module else "unknown_module"
|
||||
return f"{module_name}.{target.__qualname__}"
|
||||
|
||||
@classmethod
|
||||
def handler_class_identifier(cls, handler: Callable) -> str | None:
|
||||
"""返回可调用对象所属类的稳定标识;自由函数返回空值。"""
|
||||
if inspect.ismethod(handler) and hasattr(handler, "__self__"):
|
||||
return cls.handler_identifier(handler.__self__.__class__)
|
||||
if not inspect.isfunction(handler) and hasattr(handler, "__call__"):
|
||||
return cls.handler_identifier(handler.__class__)
|
||||
qualname_parts = handler.__qualname__.split(".")
|
||||
if len(qualname_parts) <= 1:
|
||||
return None
|
||||
module = inspect.getmodule(handler)
|
||||
module_name = module.__name__ if module else "unknown_module"
|
||||
return f"{module_name}.{'.'.join(qualname_parts[:-1])}"
|
||||
|
||||
def is_handler_enabled(self, handler: Callable) -> bool:
|
||||
"""判断处理器及其所属类是否均处于启用状态。"""
|
||||
handler_id = self.handler_identifier(handler)
|
||||
class_id = self.handler_class_identifier(handler)
|
||||
return not (
|
||||
handler_id in self._disabled_handlers()
|
||||
or (
|
||||
class_id is not None
|
||||
and class_id in self._disabled_classes()
|
||||
)
|
||||
)
|
||||
|
||||
def check(self, event_type: EventType | ChainEventType) -> bool:
|
||||
"""检查指定事件是否存在启用的处理器。"""
|
||||
if isinstance(event_type, ChainEventType):
|
||||
handlers = self._chain_subscribers().get(event_type, {})
|
||||
return any(
|
||||
self.is_handler_enabled(handler)
|
||||
for _, handler in handlers.values()
|
||||
)
|
||||
handlers = self._broadcast_subscribers().get(event_type, {})
|
||||
return any(self.is_handler_enabled(handler) for handler in handlers.values())
|
||||
|
||||
def add(
|
||||
self,
|
||||
event_type: EventType | ChainEventType,
|
||||
handler: Callable,
|
||||
priority: int,
|
||||
) -> None:
|
||||
"""注册处理器,并为链式事件按优先级维护稳定顺序。"""
|
||||
with self._lock:
|
||||
handler_id = self.handler_identifier(handler)
|
||||
if isinstance(event_type, ChainEventType):
|
||||
subscribers = self._chain_subscribers()
|
||||
handlers = subscribers.setdefault(event_type, {})
|
||||
existed = handler_id in handlers
|
||||
handlers.pop(handler_id, None)
|
||||
if not existed:
|
||||
logger.debug(
|
||||
"Subscribed to chain event: %s, Priority: %s - %s",
|
||||
event_type.value,
|
||||
priority,
|
||||
handler_id,
|
||||
)
|
||||
handlers[handler_id] = (priority, handler)
|
||||
subscribers[event_type] = dict(
|
||||
sorted(handlers.items(), key=lambda item: item[1][0])
|
||||
)
|
||||
return
|
||||
subscribers = self._broadcast_subscribers()
|
||||
handlers = subscribers.setdefault(event_type, {})
|
||||
existed = handler_id in handlers
|
||||
handlers.pop(handler_id, None)
|
||||
if not existed:
|
||||
logger.debug(
|
||||
"Subscribed to broadcast event: %s - %s",
|
||||
event_type.value,
|
||||
handler_id,
|
||||
)
|
||||
handlers[handler_id] = handler
|
||||
|
||||
def remove(
|
||||
self,
|
||||
event_type: EventType | ChainEventType,
|
||||
handler: Callable,
|
||||
) -> None:
|
||||
"""从指定事件中移除处理器。"""
|
||||
with self._lock:
|
||||
handler_id = self.handler_identifier(handler)
|
||||
if isinstance(event_type, ChainEventType):
|
||||
self._chain_subscribers().get(event_type, {}).pop(
|
||||
handler_id,
|
||||
None,
|
||||
)
|
||||
logger.debug(
|
||||
"Unsubscribed from chain event: %s - %s",
|
||||
event_type.value,
|
||||
handler_id,
|
||||
)
|
||||
return
|
||||
self._broadcast_subscribers().get(event_type, {}).pop(
|
||||
handler_id,
|
||||
None,
|
||||
)
|
||||
logger.debug(
|
||||
"Unsubscribed from broadcast event: %s - %s",
|
||||
event_type.value,
|
||||
handler_id,
|
||||
)
|
||||
|
||||
def disable(self, target: Callable | type) -> None:
|
||||
"""禁用单个处理器或整个处理器类。"""
|
||||
identifier = self.handler_identifier(target)
|
||||
if isinstance(target, type):
|
||||
self._disabled_classes().add(identifier)
|
||||
logger.debug("Disabled event handler class - %s", identifier)
|
||||
else:
|
||||
self._disabled_handlers().add(identifier)
|
||||
logger.debug("Disabled event handler - %s", identifier)
|
||||
|
||||
def enable(self, target: Callable | type) -> None:
|
||||
"""重新启用单个处理器或整个处理器类。"""
|
||||
identifier = self.handler_identifier(target)
|
||||
if isinstance(target, type):
|
||||
self._disabled_classes().discard(identifier)
|
||||
logger.debug("Enabled event handler class - %s", identifier)
|
||||
else:
|
||||
self._disabled_handlers().discard(identifier)
|
||||
logger.debug("Enabled event handler - %s", identifier)
|
||||
|
||||
def chain_snapshot(self, event_type: ChainEventType) -> tuple:
|
||||
"""返回当前链式订阅快照,运行期变更从下一次事件生效。"""
|
||||
with self._lock:
|
||||
return tuple(self._chain_subscribers().get(event_type, {}).items())
|
||||
|
||||
def broadcast_snapshot(self, event_type: EventType) -> tuple:
|
||||
"""返回当前广播订阅快照,运行期变更从下一次事件生效。"""
|
||||
with self._lock:
|
||||
return tuple(
|
||||
self._broadcast_subscribers().get(event_type, {}).items()
|
||||
)
|
||||
|
||||
def visualize(self) -> list[dict]:
|
||||
"""导出所有订阅处理器的事件、优先级和启停状态。"""
|
||||
result = []
|
||||
combined = {
|
||||
**self._broadcast_subscribers(),
|
||||
**self._chain_subscribers(),
|
||||
}
|
||||
for event_type, subscribers in combined.items():
|
||||
for handler_id, handler_data in subscribers.items():
|
||||
if isinstance(handler_data, tuple) and len(handler_data) == 2:
|
||||
priority, handler = handler_data
|
||||
else:
|
||||
priority, handler = None, handler_data
|
||||
item = {
|
||||
"event_type": event_type.value,
|
||||
"handler_identifier": handler_id,
|
||||
"status": (
|
||||
"enabled"
|
||||
if self.is_handler_enabled(handler)
|
||||
else "disabled"
|
||||
),
|
||||
}
|
||||
if priority is not None:
|
||||
item["priority"] = priority
|
||||
result.append(item)
|
||||
return result
|
||||
+75
-397
@@ -1,23 +1,25 @@
|
||||
import asyncio
|
||||
import inspect
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
import traceback
|
||||
import uuid
|
||||
from dataclasses import dataclass
|
||||
from queue import Empty, PriorityQueue
|
||||
from typing import Callable, Dict, List, Optional, Tuple, Union, Any, Type
|
||||
|
||||
from fastapi.concurrency import run_in_threadpool
|
||||
|
||||
from app.runtime.config import global_vars
|
||||
from app.runtime.thread import ThreadHelper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas import ChainEventData
|
||||
from app.schemas.event import ChainEventData
|
||||
from app.schemas.types import ChainEventType, EventType
|
||||
from app.runtime.rate import ExponentialBackoffRateLimiter
|
||||
from app.foundation.singleton import Singleton
|
||||
from app.runtime.event.binding import (
|
||||
EventBindingResolver,
|
||||
EventHandlerBinding,
|
||||
HandlerInstanceResolver,
|
||||
)
|
||||
from app.runtime.event.dispatch import EventDispatcher
|
||||
from app.runtime.event.errors import EventErrorNotifier, EventErrorPolicy
|
||||
from app.runtime.event.registry import EventRegistry
|
||||
|
||||
DEFAULT_EVENT_PRIORITY = 10 # 事件的默认优先级
|
||||
MIN_EVENT_CONSUMER_THREADS = 1 # 最小事件消费者线程数
|
||||
@@ -25,21 +27,6 @@ INITIAL_EVENT_QUEUE_IDLE_TIMEOUT_SECONDS = 1 # 事件队列空闲时的初始
|
||||
MAX_EVENT_QUEUE_IDLE_TIMEOUT_SECONDS = 5 # 事件队列空闲时的最大超时时间(秒)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class EventHandlerBinding:
|
||||
"""描述上层运行时为某个事件处理器提供的实例绑定。"""
|
||||
|
||||
instance: Optional[Any]
|
||||
owner_name: str
|
||||
run_sync_in_threadpool: bool = False
|
||||
|
||||
|
||||
HandlerInstanceResolver = Callable[
|
||||
[Type[Any]], Optional[EventHandlerBinding]
|
||||
]
|
||||
EventErrorNotifier = Callable[[str, str], object]
|
||||
|
||||
|
||||
class Event:
|
||||
"""
|
||||
事件类,封装事件的基本信息
|
||||
@@ -111,6 +98,32 @@ class EventManager(metaclass=Singleton):
|
||||
self.__handler_instance_resolvers: Dict[str, HandlerInstanceResolver] = {}
|
||||
# 由启动组合层注入的错误通知回调
|
||||
self.__error_notifier: Optional[EventErrorNotifier] = None
|
||||
self.__registry = EventRegistry(
|
||||
lock=self.__lock,
|
||||
broadcast_subscribers=lambda: self.__broadcast_subscribers,
|
||||
chain_subscribers=lambda: self.__chain_subscribers,
|
||||
disabled_handlers=lambda: self.__disabled_handlers,
|
||||
disabled_classes=lambda: self.__disabled_classes,
|
||||
)
|
||||
self.__binding_resolver = EventBindingResolver(
|
||||
lock=self.__lock,
|
||||
resolvers=lambda: self.__handler_instance_resolvers,
|
||||
)
|
||||
self.__error_policy = EventErrorPolicy(
|
||||
notifier=lambda: self.__error_notifier,
|
||||
emit_system_error=lambda payload: self.send_event(
|
||||
EventType.SystemError,
|
||||
payload,
|
||||
),
|
||||
)
|
||||
self.__dispatcher = EventDispatcher(
|
||||
registry=self.__registry,
|
||||
binding_resolver=self.__binding_resolver,
|
||||
executor=lambda: self.__executor,
|
||||
event_loop=lambda: global_vars.loop,
|
||||
event_factory=Event,
|
||||
error_handler=lambda **kwargs: self.__handle_event_error(**kwargs),
|
||||
)
|
||||
|
||||
def register_handler_instance_resolver(
|
||||
self,
|
||||
@@ -122,8 +135,11 @@ class EventManager(metaclass=Singleton):
|
||||
|
||||
同名解析器会被替换,避免测试重建单例或热重载后保留旧实例引用。
|
||||
"""
|
||||
with self.__lock:
|
||||
self.__handler_instance_resolvers[name] = resolver
|
||||
self.__binding_resolver.register(name, resolver)
|
||||
|
||||
def unresolved_handler_bindings(self) -> tuple[str, ...]:
|
||||
"""返回未命中显式 resolver 的类处理器诊断清单。"""
|
||||
return self.__binding_resolver.unresolved_handlers()
|
||||
|
||||
def set_error_notifier(self, notifier: Optional[EventErrorNotifier]) -> None:
|
||||
"""设置事件处理异常的外部通知回调。"""
|
||||
@@ -161,18 +177,7 @@ class EventManager(metaclass=Singleton):
|
||||
:param etype: 事件类型 (EventType 或 ChainEventType)
|
||||
:return: 返回是否存在可用的处理器
|
||||
"""
|
||||
if isinstance(etype, ChainEventType):
|
||||
handlers = self.__chain_subscribers.get(etype, {})
|
||||
return any(
|
||||
self.__is_handler_enabled(handler)
|
||||
for _, handler in handlers.values()
|
||||
)
|
||||
else:
|
||||
handlers = self.__broadcast_subscribers.get(etype, {})
|
||||
return any(
|
||||
self.__is_handler_enabled(handler)
|
||||
for handler in handlers.values()
|
||||
)
|
||||
return self.__registry.check(etype)
|
||||
|
||||
def send_event(self, etype: Union[EventType, ChainEventType], data: Optional[Union[Dict, ChainEventData]] = None,
|
||||
priority: Optional[int] = DEFAULT_EVENT_PRIORITY) -> Optional[Event]:
|
||||
@@ -219,35 +224,7 @@ class EventManager(metaclass=Singleton):
|
||||
:param handler: 处理器
|
||||
:param priority: 可选,链式事件的优先级,默认为 10;广播事件不需要优先级
|
||||
"""
|
||||
with self.__lock:
|
||||
handler_identifier = self.__get_handler_identifier(handler)
|
||||
|
||||
if isinstance(event_type, ChainEventType):
|
||||
# 链式事件,按优先级排序
|
||||
if event_type not in self.__chain_subscribers:
|
||||
self.__chain_subscribers[event_type] = {}
|
||||
handlers = self.__chain_subscribers[event_type]
|
||||
if handler_identifier in handlers:
|
||||
handlers.pop(handler_identifier)
|
||||
else:
|
||||
logger.debug(
|
||||
f"Subscribed to chain event: {event_type.value}, "
|
||||
f"Priority: {priority} - {handler_identifier}")
|
||||
handlers[handler_identifier] = (priority, handler)
|
||||
# 根据优先级排序
|
||||
self.__chain_subscribers[event_type] = dict(
|
||||
sorted(self.__chain_subscribers[event_type].items(), key=lambda x: x[1][0])
|
||||
)
|
||||
else:
|
||||
# 广播事件
|
||||
if event_type not in self.__broadcast_subscribers:
|
||||
self.__broadcast_subscribers[event_type] = {}
|
||||
handlers = self.__broadcast_subscribers[event_type]
|
||||
if handler_identifier in handlers:
|
||||
handlers.pop(handler_identifier)
|
||||
else:
|
||||
logger.debug(f"Subscribed to broadcast event: {event_type.value} - {handler_identifier}")
|
||||
handlers[handler_identifier] = handler
|
||||
self.__registry.add(event_type, handler, priority or DEFAULT_EVENT_PRIORITY)
|
||||
|
||||
def remove_event_listener(self, event_type: Union[EventType, ChainEventType], handler: Callable):
|
||||
"""
|
||||
@@ -255,43 +232,21 @@ class EventManager(metaclass=Singleton):
|
||||
:param event_type: 事件类型 (EventType 或 ChainEventType)
|
||||
:param handler: 要移除的处理器
|
||||
"""
|
||||
with self.__lock:
|
||||
handler_identifier = self.__get_handler_identifier(handler)
|
||||
|
||||
if isinstance(event_type, ChainEventType) and event_type in self.__chain_subscribers:
|
||||
self.__chain_subscribers[event_type].pop(handler_identifier, None)
|
||||
logger.debug(f"Unsubscribed from chain event: {event_type.value} - {handler_identifier}")
|
||||
elif event_type in self.__broadcast_subscribers:
|
||||
self.__broadcast_subscribers[event_type].pop(handler_identifier, None)
|
||||
logger.debug(f"Unsubscribed from broadcast event: {event_type.value} - {handler_identifier}")
|
||||
self.__registry.remove(event_type, handler)
|
||||
|
||||
def disable_event_handler(self, target: Union[Callable, type]):
|
||||
"""
|
||||
禁用指定的事件处理器或事件处理器类
|
||||
:param target: 处理器函数或类
|
||||
"""
|
||||
identifier = self.__get_handler_identifier(target)
|
||||
if identifier in self.__disabled_handlers or identifier in self.__disabled_classes:
|
||||
return
|
||||
if isinstance(target, type):
|
||||
self.__disabled_classes.add(identifier)
|
||||
logger.debug(f"Disabled event handler class - {identifier}")
|
||||
else:
|
||||
self.__disabled_handlers.add(identifier)
|
||||
logger.debug(f"Disabled event handler - {identifier}")
|
||||
self.__registry.disable(target)
|
||||
|
||||
def enable_event_handler(self, target: Union[Callable, type]):
|
||||
"""
|
||||
启用指定的事件处理器或事件处理器类
|
||||
:param target: 处理器函数或类
|
||||
"""
|
||||
identifier = self.__get_handler_identifier(target)
|
||||
if isinstance(target, type):
|
||||
self.__disabled_classes.discard(identifier)
|
||||
logger.debug(f"Enabled event handler class - {identifier}")
|
||||
else:
|
||||
self.__disabled_handlers.discard(identifier)
|
||||
logger.debug(f"Enabled event handler - {identifier}")
|
||||
self.__registry.enable(target)
|
||||
|
||||
def visualize_handlers(self) -> List[Dict]:
|
||||
"""
|
||||
@@ -299,34 +254,7 @@ class EventManager(metaclass=Singleton):
|
||||
:return: 处理器列表,包含事件类型、处理器标识符、优先级(如果有)和状态
|
||||
"""
|
||||
|
||||
def parse_handler_data(data):
|
||||
"""
|
||||
解析处理器数据,判断是否包含优先级
|
||||
:param data: 订阅者数据,可能是元组或单一值
|
||||
:return: (priority, handler),若没有优先级则返回 (None, handler)
|
||||
"""
|
||||
if isinstance(data, tuple) and len(data) == 2:
|
||||
return data
|
||||
return None, data
|
||||
|
||||
handler_info = []
|
||||
# 统一处理广播事件和链式事件
|
||||
for event_type, subscribers in {**self.__broadcast_subscribers, **self.__chain_subscribers}.items():
|
||||
for handler_identifier, handler_data in subscribers.items():
|
||||
# 解析优先级和处理器
|
||||
priority, handler = parse_handler_data(handler_data)
|
||||
# 检查处理器的启用状态
|
||||
status = "enabled" if self.__is_handler_enabled(handler) else "disabled"
|
||||
# 构建处理器信息字典
|
||||
handler_dict = {
|
||||
"event_type": event_type.value,
|
||||
"handler_identifier": handler_identifier,
|
||||
"status": status
|
||||
}
|
||||
if priority is not None:
|
||||
handler_dict["priority"] = priority
|
||||
handler_info.append(handler_dict)
|
||||
return handler_info
|
||||
return self.__registry.visualize()
|
||||
|
||||
@classmethod
|
||||
def __get_handler_identifier(cls, target: Union[Callable, type]) -> Optional[str]:
|
||||
@@ -335,13 +263,7 @@ class EventManager(metaclass=Singleton):
|
||||
:param target: 处理器函数或类
|
||||
:return: 唯一标识符
|
||||
"""
|
||||
# 统一使用 inspect.getmodule 来获取模块名
|
||||
module = inspect.getmodule(target)
|
||||
module_name = module.__name__ if module else "unknown_module"
|
||||
|
||||
# 使用 __qualname__ 获取目标的限定名
|
||||
qualname = target.__qualname__
|
||||
return f"{module_name}.{qualname}"
|
||||
return EventRegistry.handler_identifier(target)
|
||||
|
||||
@classmethod
|
||||
def __get_class_from_callable(cls, handler: Callable) -> Optional[str]:
|
||||
@@ -350,23 +272,7 @@ class EventManager(metaclass=Singleton):
|
||||
:param handler: 可调用对象(函数、方法等)
|
||||
:return: 类的唯一标识符
|
||||
"""
|
||||
# 对于绑定方法,通过 __self__.__class__ 获取类
|
||||
if inspect.ismethod(handler) and hasattr(handler, "__self__"):
|
||||
return cls.__get_handler_identifier(handler.__self__.__class__)
|
||||
|
||||
# 对于类实例(实现了 __call__ 方法)
|
||||
if not inspect.isfunction(handler) and hasattr(handler, "__call__"):
|
||||
handler_cls = handler.__class__ # noqa
|
||||
return cls.__get_handler_identifier(handler_cls)
|
||||
|
||||
# 对于未绑定方法、静态方法、类方法,使用 __qualname__ 提取类信息
|
||||
qualname_parts = handler.__qualname__.split(".")
|
||||
if len(qualname_parts) > 1:
|
||||
class_name = ".".join(qualname_parts[:-1])
|
||||
module = inspect.getmodule(handler)
|
||||
module_name = module.__name__ if module else "unknown_module"
|
||||
return f"{module_name}.{class_name}"
|
||||
return None
|
||||
return EventRegistry.handler_class_identifier(handler)
|
||||
|
||||
def __is_handler_enabled(self, handler: Callable) -> bool:
|
||||
"""
|
||||
@@ -374,17 +280,7 @@ class EventManager(metaclass=Singleton):
|
||||
:param handler: 处理器函数
|
||||
:return: 如果处理器启用则返回 True,否则返回 False
|
||||
"""
|
||||
# 获取处理器的唯一标识符
|
||||
handler_id = self.__get_handler_identifier(handler)
|
||||
|
||||
# 获取处理器所属类的唯一标识符
|
||||
class_id = self.__get_class_from_callable(handler)
|
||||
|
||||
# 检查处理器或类是否被禁用,只要其中之一被禁用则返回 False
|
||||
if handler_id in self.__disabled_handlers or (class_id is not None and class_id in self.__disabled_classes):
|
||||
return False
|
||||
|
||||
return True
|
||||
return self.__registry.is_handler_enabled(handler)
|
||||
|
||||
def __trigger_chain_event(self, event: Event) -> Optional[Event]:
|
||||
"""
|
||||
@@ -415,113 +311,21 @@ class EventManager(metaclass=Singleton):
|
||||
同步方式调度链式事件,按优先级顺序逐个调用事件处理器,并记录每个处理器的处理时间
|
||||
:param event: 要调度的事件对象
|
||||
"""
|
||||
# 运行期可以动态注册或移除处理器;当前事件始终使用调度开始时的快照。
|
||||
with self.__lock:
|
||||
handlers = tuple(
|
||||
self.__chain_subscribers.get(event.event_type, {}).items()
|
||||
)
|
||||
if not handlers:
|
||||
logger.debug(f"No handlers found for chain event: {event}")
|
||||
return False
|
||||
|
||||
# 过滤出启用的处理器
|
||||
enabled_handlers = tuple(
|
||||
(handler_id, priority, handler)
|
||||
for handler_id, (priority, handler) in handlers
|
||||
if self.__is_handler_enabled(handler)
|
||||
)
|
||||
|
||||
if not enabled_handlers:
|
||||
logger.debug(f"No enabled handlers found for chain event: {event}. Skipping execution.")
|
||||
return False
|
||||
|
||||
self.__log_event_lifecycle(event, "Started")
|
||||
for handler_id, priority, handler in enabled_handlers:
|
||||
start_time = time.time()
|
||||
self.__safe_invoke_handler(handler, event)
|
||||
logger.debug(
|
||||
f"{self.__get_handler_identifier(handler)} (Priority: {priority}), "
|
||||
f"completed in {time.time() - start_time:.3f}s for event: {event}"
|
||||
)
|
||||
self.__log_event_lifecycle(event, "Completed")
|
||||
return True
|
||||
return self.__dispatcher.dispatch_chain(event)
|
||||
|
||||
async def __dispatch_chain_event_async(self, event: Event) -> bool:
|
||||
"""
|
||||
异步方式调度链式事件,按优先级顺序逐个调用事件处理器,并记录每个处理器的处理时间
|
||||
:param event: 要调度的事件对象
|
||||
"""
|
||||
# 快照在锁内建立、在锁外执行,处理器可以安全地修改后续订阅。
|
||||
with self.__lock:
|
||||
handlers = tuple(
|
||||
self.__chain_subscribers.get(event.event_type, {}).items()
|
||||
)
|
||||
if not handlers:
|
||||
logger.debug(f"No handlers found for chain event: {event}")
|
||||
return False
|
||||
|
||||
# 过滤出启用的处理器
|
||||
enabled_handlers = tuple(
|
||||
(handler_id, priority, handler)
|
||||
for handler_id, (priority, handler) in handlers
|
||||
if self.__is_handler_enabled(handler)
|
||||
)
|
||||
|
||||
if not enabled_handlers:
|
||||
logger.debug(f"No enabled handlers found for chain event: {event}. Skipping execution.")
|
||||
return False
|
||||
|
||||
self.__log_event_lifecycle(event, "Started")
|
||||
for handler_id, priority, handler in enabled_handlers:
|
||||
start_time = time.time()
|
||||
await self.__safe_invoke_handler_async(handler, event)
|
||||
logger.debug(
|
||||
f"{self.__get_handler_identifier(handler)} (Priority: {priority}), "
|
||||
f"completed in {time.time() - start_time:.3f}s for event: {event}"
|
||||
)
|
||||
self.__log_event_lifecycle(event, "Completed")
|
||||
return True
|
||||
return await self.__dispatcher.async_dispatch_chain(event)
|
||||
|
||||
def __dispatch_broadcast_event(self, event: Event):
|
||||
"""
|
||||
异步方式调度广播事件,通过线程池逐个调用事件处理器
|
||||
:param event: 要调度的事件对象
|
||||
"""
|
||||
# 快照隔离当前调度与运行期订阅变更;变更从下一个事件开始生效。
|
||||
with self.__lock:
|
||||
handlers = tuple(
|
||||
self.__broadcast_subscribers.get(event.event_type, {}).items()
|
||||
)
|
||||
if not handlers:
|
||||
logger.debug(f"No handlers found for broadcast event: {event}")
|
||||
return
|
||||
target_plugin_id = None
|
||||
if event.event_type == EventType.MessageAction and isinstance(event.event_data, dict):
|
||||
target_plugin_id = event.event_data.get("__mp_target_plugin_id")
|
||||
# 为每个处理器提供独立的事件实例,防止某个处理器对 event_data 的修改影响其他处理器
|
||||
for handler_id, handler in handlers:
|
||||
if target_plugin_id and not self.__should_dispatch_to_target_plugin(
|
||||
handler, handler_id, str(target_plugin_id)
|
||||
):
|
||||
continue
|
||||
# 仅浅拷贝顶层字典,避免不必要的深拷贝开销;这样可以隔离键级别的替换/赋值
|
||||
if isinstance(event.event_data, dict):
|
||||
event_data_copy = event.event_data.copy()
|
||||
event_data_copy.pop("__mp_target_plugin_id", None)
|
||||
else:
|
||||
event_data_copy = event.event_data
|
||||
isolated_event = Event(event_type=event.event_type,
|
||||
event_data=event_data_copy,
|
||||
priority=event.priority)
|
||||
if inspect.iscoroutinefunction(handler):
|
||||
# 对于异步函数,直接在事件循环中运行
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.__safe_invoke_handler_async(handler, isolated_event),
|
||||
global_vars.loop
|
||||
)
|
||||
else:
|
||||
# 对于同步函数,在线程池中运行
|
||||
self.__executor.submit(self.__safe_invoke_handler, handler, isolated_event)
|
||||
self.__dispatcher.dispatch_broadcast(event)
|
||||
|
||||
@classmethod
|
||||
def __should_dispatch_to_target_plugin(
|
||||
@@ -533,23 +337,11 @@ class EventManager(metaclass=Singleton):
|
||||
"""
|
||||
限定插件输入事件只投递给目标插件,避免自由文本被其他插件观察到。
|
||||
"""
|
||||
class_name, method_name = cls.__parse_handler_names(handler)
|
||||
if class_name != target_plugin_id:
|
||||
return False
|
||||
identifier_parts = (handler_identifier or "").split(".")
|
||||
if len(identifier_parts) < 2:
|
||||
logger.debug(
|
||||
"Target plugin dispatch skipped because handler identifier is invalid: "
|
||||
f"target={target_plugin_id}, handler={handler_identifier}"
|
||||
)
|
||||
return False
|
||||
if identifier_parts[-2:] != [class_name, method_name]:
|
||||
logger.debug(
|
||||
"Target plugin dispatch skipped because handler identifier does not match handler: "
|
||||
f"target={target_plugin_id}, handler={handler_identifier}, parsed={class_name}.{method_name}"
|
||||
)
|
||||
return False
|
||||
return True
|
||||
return EventDispatcher.should_dispatch_to_target_plugin(
|
||||
handler,
|
||||
handler_identifier,
|
||||
target_plugin_id,
|
||||
)
|
||||
|
||||
def __safe_invoke_handler(self, handler: Callable, event: Event):
|
||||
"""
|
||||
@@ -557,11 +349,7 @@ class EventManager(metaclass=Singleton):
|
||||
:param handler: 处理器
|
||||
:param event: 事件对象
|
||||
"""
|
||||
if not self.__is_handler_enabled(handler):
|
||||
logger.debug(f"Handler {self.__get_handler_identifier(handler)} is disabled. Skipping execution")
|
||||
return
|
||||
|
||||
self.__invoke_handler_by_type_sync(handler, event)
|
||||
self.__dispatcher.safe_invoke_sync(handler, event)
|
||||
|
||||
async def __safe_invoke_handler_async(self, handler: Callable, event: Event):
|
||||
"""
|
||||
@@ -569,11 +357,7 @@ class EventManager(metaclass=Singleton):
|
||||
:param handler: 处理器
|
||||
:param event: 事件对象
|
||||
"""
|
||||
if not self.__is_handler_enabled(handler):
|
||||
logger.debug(f"Handler {self.__get_handler_identifier(handler)} is disabled. Skipping execution")
|
||||
return
|
||||
|
||||
await self.__invoke_handler_by_type_async(handler, event)
|
||||
await self.__dispatcher.safe_invoke_async(handler, event)
|
||||
|
||||
def __invoke_handler_by_type_sync(self, handler: Callable, event: Event):
|
||||
"""
|
||||
@@ -581,20 +365,7 @@ class EventManager(metaclass=Singleton):
|
||||
:param handler: 处理器
|
||||
:param event: 要处理的事件对象
|
||||
"""
|
||||
resolved = self.__resolve_handler(handler)
|
||||
if not resolved:
|
||||
return
|
||||
method, binding, class_name, method_name = resolved
|
||||
try:
|
||||
method(event)
|
||||
except Exception as e:
|
||||
self.__handle_event_error(
|
||||
event=event,
|
||||
module_name=binding.owner_name,
|
||||
class_name=class_name,
|
||||
method_name=method_name,
|
||||
e=e,
|
||||
)
|
||||
self.__dispatcher.invoke_sync(handler, event)
|
||||
|
||||
async def __invoke_handler_by_type_async(self, handler: Callable, event: Event):
|
||||
"""
|
||||
@@ -602,25 +373,7 @@ class EventManager(metaclass=Singleton):
|
||||
:param handler: 处理器
|
||||
:param event: 要处理的事件对象
|
||||
"""
|
||||
resolved = self.__resolve_handler(handler)
|
||||
if not resolved:
|
||||
return
|
||||
method, binding, class_name, method_name = resolved
|
||||
try:
|
||||
if inspect.iscoroutinefunction(method):
|
||||
await method(event)
|
||||
elif binding.run_sync_in_threadpool or not class_name:
|
||||
await run_in_threadpool(method, event)
|
||||
else:
|
||||
method(event)
|
||||
except Exception as e:
|
||||
self.__handle_event_error(
|
||||
event=event,
|
||||
module_name=binding.owner_name,
|
||||
class_name=class_name,
|
||||
method_name=method_name,
|
||||
e=e,
|
||||
)
|
||||
await self.__dispatcher.invoke_async(handler, event)
|
||||
|
||||
@staticmethod
|
||||
def __parse_handler_names(handler: Callable) -> Tuple[str, str]:
|
||||
@@ -629,82 +382,19 @@ class EventManager(metaclass=Singleton):
|
||||
:param handler: 处理器
|
||||
:return: (class_name, method_name)
|
||||
"""
|
||||
names = handler.__qualname__.split(".")
|
||||
if len(names) < 2:
|
||||
return "", names[0]
|
||||
return names[0], names[1]
|
||||
return EventBindingResolver.parse_handler_names(handler)
|
||||
|
||||
@staticmethod
|
||||
def __get_handler_owner_class(handler: Callable) -> Optional[Type[Any]]:
|
||||
"""从处理器对象本身解析声明它的类,不按命名约定动态导入模块。"""
|
||||
if inspect.ismethod(handler):
|
||||
owner = handler.__self__
|
||||
return owner if isinstance(owner, type) else type(owner)
|
||||
module = inspect.getmodule(handler)
|
||||
if not module:
|
||||
return None
|
||||
owner: Any = module
|
||||
for part in handler.__qualname__.split(".")[:-1]:
|
||||
if part == "<locals>":
|
||||
return None
|
||||
owner = getattr(owner, part, None)
|
||||
if owner is None:
|
||||
return None
|
||||
return owner if isinstance(owner, type) else None
|
||||
return EventBindingResolver.owner_class(handler)
|
||||
|
||||
def __resolve_handler(
|
||||
self,
|
||||
handler: Callable,
|
||||
) -> Optional[Tuple[Callable, EventHandlerBinding, str, str]]:
|
||||
"""将装饰阶段保存的函数解析为当前运行实例上的可调用方法。"""
|
||||
owner_class = self.__get_handler_owner_class(handler)
|
||||
method_name = getattr(handler, "__name__", self.__parse_handler_names(handler)[1])
|
||||
if owner_class is None:
|
||||
binding = EventHandlerBinding(
|
||||
instance=None,
|
||||
owner_name=self.__get_handler_identifier(handler),
|
||||
run_sync_in_threadpool=True,
|
||||
)
|
||||
return handler, binding, "", method_name
|
||||
|
||||
with self.__lock:
|
||||
resolvers = list(self.__handler_instance_resolvers.values())
|
||||
binding = next(
|
||||
(result for resolver in resolvers if (result := resolver(owner_class)) is not None),
|
||||
None,
|
||||
)
|
||||
if binding is None:
|
||||
try:
|
||||
get_existing = getattr(owner_class, "get_existing_instance", None)
|
||||
instance = get_existing() if callable(get_existing) else None
|
||||
if instance is None:
|
||||
instance = owner_class()
|
||||
binding = EventHandlerBinding(
|
||||
instance=instance,
|
||||
owner_name=owner_class.__name__,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"事件处理出错:创建 {owner_class.__name__} 实例失败:"
|
||||
f"{str(e)} - {traceback.format_exc()}"
|
||||
)
|
||||
return None
|
||||
if binding.instance is None:
|
||||
return None
|
||||
method = getattr(binding.instance, method_name, None)
|
||||
if not callable(method):
|
||||
# 动态生成的处理器可能只同步了 __qualname__,__name__ 与类上方法名不一致时
|
||||
# 回退到限定名末段重试;仍无法解析时记录告警,避免静默跳过
|
||||
fallback_name = self.__parse_handler_names(handler)[1]
|
||||
method = getattr(binding.instance, fallback_name, None)
|
||||
if fallback_name == method_name or not callable(method):
|
||||
logger.warning(
|
||||
f"事件处理器 {self.__get_handler_identifier(handler)} "
|
||||
f"无法解析为实例方法 {owner_class.__name__}.{method_name},跳过执行"
|
||||
)
|
||||
return None
|
||||
method_name = fallback_name
|
||||
return method, binding, owner_class.__name__, method_name
|
||||
return self.__binding_resolver.resolve(handler)
|
||||
|
||||
def __broadcast_consumer_loop(self):
|
||||
"""
|
||||
@@ -737,28 +427,12 @@ class EventManager(metaclass=Singleton):
|
||||
"""
|
||||
全局错误处理器,用于处理事件处理中的异常
|
||||
"""
|
||||
logger.error(f"{module_name} 事件处理出错:{str(e)} - {traceback.format_exc()}")
|
||||
|
||||
# 消息实现由启动组合层注入,事件总线不反向依赖消息模块。
|
||||
with self.__lock:
|
||||
notifier = self.__error_notifier
|
||||
if notifier:
|
||||
try:
|
||||
notifier(
|
||||
f"{module_name} 处理事件 {event.event_type} 时出错",
|
||||
f"{class_name}.{method_name}:{str(e)}",
|
||||
)
|
||||
except Exception as notify_error:
|
||||
logger.error(f"发送事件错误通知失败:{str(notify_error)}")
|
||||
self.send_event(
|
||||
EventType.SystemError,
|
||||
{
|
||||
"type": "event",
|
||||
"event_type": event.event_type,
|
||||
"event_handle": f"{class_name}.{method_name}",
|
||||
"error": str(e),
|
||||
"traceback": traceback.format_exc()
|
||||
}
|
||||
self.__error_policy.handle(
|
||||
event=event,
|
||||
module_name=module_name,
|
||||
class_name=class_name,
|
||||
method_name=method_name,
|
||||
error=e,
|
||||
)
|
||||
|
||||
def register(self, etype: Union[EventType, ChainEventType, List[Union[EventType, ChainEventType]], type],
|
||||
@@ -797,5 +471,9 @@ class EventManager(metaclass=Singleton):
|
||||
return decorator
|
||||
|
||||
|
||||
# 全局实例定义
|
||||
# 模块热重载时类对象会重新创建,但插件和 SDK 可能仍持有旧全局实例。把旧实例登记到
|
||||
# 新 EventManager 类的单例键,确保所有公开入口继续共享同一个事件总线。
|
||||
_existing_eventmanager = globals().get("eventmanager")
|
||||
if _existing_eventmanager is not None:
|
||||
Singleton._instances[(EventManager, (), frozenset())] = _existing_eventmanager
|
||||
eventmanager = EventManager()
|
||||
|
||||
@@ -4,7 +4,7 @@ import time
|
||||
from functools import wraps
|
||||
from typing import Any, Callable
|
||||
|
||||
from app.schemas import ImmediateException
|
||||
from app.schemas.exception import ImmediateException
|
||||
|
||||
|
||||
def retry(ExceptionToCheck: Any,
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
"""插件运行时内部组件。"""
|
||||
@@ -0,0 +1,41 @@
|
||||
"""插件运行时钩子契约。"""
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from app.foundation.reflection import ObjectUtils
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class PluginHookContract:
|
||||
"""描述宿主识别一个插件钩子时必须保持的运行语义。"""
|
||||
|
||||
name: str
|
||||
requires_enabled: bool = False
|
||||
isolates_errors: bool = True
|
||||
|
||||
|
||||
PLUGIN_HOOK_CONTRACTS = {
|
||||
contract.name: contract
|
||||
for contract in (
|
||||
PluginHookContract("get_command", requires_enabled=True),
|
||||
PluginHookContract("get_api"),
|
||||
PluginHookContract("get_service", requires_enabled=True),
|
||||
PluginHookContract("get_module", requires_enabled=True),
|
||||
PluginHookContract("get_actions", requires_enabled=True),
|
||||
PluginHookContract("get_agent_tools", requires_enabled=True),
|
||||
PluginHookContract("get_auth_providers", requires_enabled=True),
|
||||
PluginHookContract("get_sidebar_nav", requires_enabled=True),
|
||||
PluginHookContract("get_dashboard", requires_enabled=True),
|
||||
PluginHookContract("get_dashboard_meta", requires_enabled=True),
|
||||
PluginHookContract("get_form"),
|
||||
PluginHookContract("get_page"),
|
||||
PluginHookContract("get_render_mode", isolates_errors=False),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
def supports_plugin_hook(plugin: Any, name: str) -> bool:
|
||||
"""按旧插件的方法判定规则检查实例是否实现指定钩子。"""
|
||||
method = getattr(plugin, name, None)
|
||||
return bool(method and ObjectUtils.check_method(method))
|
||||
@@ -0,0 +1,262 @@
|
||||
"""插件公开能力投影。"""
|
||||
|
||||
from typing import Any, Callable, Dict, List, Mapping, Optional
|
||||
|
||||
from app.runtime.extensions.plugin.contracts import supports_plugin_hook
|
||||
from app.runtime.log import logger as default_logger
|
||||
|
||||
|
||||
class PluginProjection:
|
||||
"""把运行态插件投影为宿主命令、API、服务、模块和动作清单。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
running_plugins: Mapping[str, Any],
|
||||
log: Any = default_logger,
|
||||
remote_entry_factory: Optional[Callable[[str, str], str]] = None,
|
||||
) -> None:
|
||||
"""保存运行态插件映射和错误日志端口。"""
|
||||
self._running_plugins = running_plugins
|
||||
self._logger = log
|
||||
self._remote_entry_factory = remote_entry_factory
|
||||
|
||||
def _items(self, pid: Optional[str]) -> list[tuple[str, Any]]:
|
||||
"""返回指定插件或运行态插件的稳定快照。"""
|
||||
snapshot = dict(self._running_plugins)
|
||||
if pid:
|
||||
plugin = snapshot.get(pid)
|
||||
return [(pid, plugin)] if plugin is not None else []
|
||||
return list(snapshot.items())
|
||||
|
||||
def commands(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""聚合插件命令并补充插件 ID。"""
|
||||
commands: list[dict] = []
|
||||
for plugin_id, plugin in self._items(pid):
|
||||
if not supports_plugin_hook(plugin, "get_command"):
|
||||
continue
|
||||
try:
|
||||
if not plugin.get_state():
|
||||
continue
|
||||
for command in plugin.get_command() or []:
|
||||
command["pid"] = plugin_id
|
||||
commands.append(command)
|
||||
except Exception as error:
|
||||
self._logger.error(f"获取插件命令出错:{str(error)}")
|
||||
return commands
|
||||
|
||||
def apis(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""聚合插件 API 并补充宿主路径和默认认证方式。"""
|
||||
apis: list[dict] = []
|
||||
for plugin_id, plugin in self._items(pid):
|
||||
if not supports_plugin_hook(plugin, "get_api"):
|
||||
continue
|
||||
try:
|
||||
for api in plugin.get_api() or []:
|
||||
api["path"] = f"/{plugin_id}{api['path']}"
|
||||
if not api.get("auth"):
|
||||
api["auth"] = "apikey"
|
||||
apis.append(api)
|
||||
except Exception as error:
|
||||
self._logger.error(f"获取插件 {plugin_id} API出错:{str(error)}")
|
||||
return apis
|
||||
|
||||
def services(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""聚合启用插件的定时服务。"""
|
||||
services: list[dict] = []
|
||||
for plugin_id, plugin in self._items(pid):
|
||||
if not supports_plugin_hook(plugin, "get_service"):
|
||||
continue
|
||||
try:
|
||||
if plugin.get_state():
|
||||
services.extend(plugin.get_service() or [])
|
||||
except Exception as error:
|
||||
self._logger.error(f"获取插件 {plugin_id} 服务出错:{str(error)}")
|
||||
return services
|
||||
|
||||
def modules(self, pid: Optional[str] = None) -> Dict[tuple, Dict[str, Any]]:
|
||||
"""聚合启用插件的模块方法清单。"""
|
||||
modules: dict[tuple, dict] = {}
|
||||
for plugin_id, plugin in self._items(pid):
|
||||
if not supports_plugin_hook(plugin, "get_module"):
|
||||
continue
|
||||
try:
|
||||
if plugin.get_state():
|
||||
modules[(plugin_id, plugin.get_name())] = plugin.get_module() or []
|
||||
except Exception as error:
|
||||
self._logger.error(f"获取插件 {plugin_id} 模块出错:{str(error)}")
|
||||
return modules
|
||||
|
||||
def actions(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""聚合启用插件的工作流动作。"""
|
||||
actions: list[dict] = []
|
||||
for plugin_id, plugin in self._items(pid):
|
||||
if not supports_plugin_hook(plugin, "get_actions"):
|
||||
continue
|
||||
try:
|
||||
if not plugin.get_state():
|
||||
continue
|
||||
plugin_actions = plugin.get_actions()
|
||||
if plugin_actions:
|
||||
actions.append({
|
||||
"plugin_id": plugin_id,
|
||||
"plugin_name": plugin.plugin_name,
|
||||
"actions": plugin_actions,
|
||||
})
|
||||
except Exception as error:
|
||||
self._logger.error(f"获取插件 {plugin_id} 动作出错:{str(error)}")
|
||||
return actions
|
||||
|
||||
def remotes(self, pid: Optional[str] = None) -> List[Dict[str, Any]]:
|
||||
"""投影插件联邦远程入口,并保持旧渲染模式筛选语义。"""
|
||||
remotes = []
|
||||
for plugin_id, plugin in self._items(pid):
|
||||
if not supports_plugin_hook(plugin, "get_render_mode"):
|
||||
continue
|
||||
render_mode, dist_path = plugin.get_render_mode()
|
||||
if render_mode != "vue":
|
||||
continue
|
||||
if not self._remote_entry_factory:
|
||||
raise RuntimeError("插件联邦入口生成器尚未配置")
|
||||
remotes.append({
|
||||
"id": plugin_id,
|
||||
"url": self._remote_entry_factory(plugin_id, dist_path),
|
||||
"name": plugin.plugin_name,
|
||||
})
|
||||
return remotes
|
||||
|
||||
def auth_providers(self) -> List[Dict[str, Any]]:
|
||||
"""投影启用插件声明的登录认证提供方。"""
|
||||
providers = []
|
||||
for plugin_id, plugin in self._items(None):
|
||||
if not plugin.get_state() or not supports_plugin_hook(
|
||||
plugin, "get_auth_providers"
|
||||
):
|
||||
continue
|
||||
try:
|
||||
plugin_providers = plugin.get_auth_providers() or []
|
||||
except Exception as error:
|
||||
self._logger.error(
|
||||
f"获取插件 {plugin_id} 登录认证提供方出错:{str(error)}"
|
||||
)
|
||||
continue
|
||||
render_mode = None
|
||||
dist_path = None
|
||||
if supports_plugin_hook(plugin, "get_render_mode"):
|
||||
render_mode, dist_path = plugin.get_render_mode()
|
||||
for raw_provider in plugin_providers:
|
||||
if not raw_provider or not isinstance(raw_provider, dict):
|
||||
continue
|
||||
provider = raw_provider.copy()
|
||||
provider["type"] = "plugin"
|
||||
provider["plugin_id"] = plugin_id
|
||||
provider.setdefault("id", f"plugin:{plugin_id}")
|
||||
provider.setdefault("name", plugin.plugin_name)
|
||||
provider.setdefault("enabled", True)
|
||||
if render_mode == "vue" and dist_path:
|
||||
if not self._remote_entry_factory:
|
||||
raise RuntimeError("插件联邦入口生成器尚未配置")
|
||||
provider.setdefault("component", "AuthPage")
|
||||
provider["remote"] = {
|
||||
"id": plugin_id,
|
||||
"url": self._remote_entry_factory(plugin_id, dist_path),
|
||||
"name": plugin.plugin_name,
|
||||
}
|
||||
providers.append(provider)
|
||||
return providers
|
||||
|
||||
def sidebar(self) -> List[Dict[str, Any]]:
|
||||
"""投影启用 Vue 插件的侧栏导航,并规整权限、分区和顺序。"""
|
||||
valid_sections = {"start", "discovery", "subscribe", "organize", "system"}
|
||||
valid_permissions = {"subscribe", "discovery", "search", "manage", "admin"}
|
||||
items = []
|
||||
for plugin_id, plugin in self._items(None):
|
||||
if not plugin.get_state() or not supports_plugin_hook(
|
||||
plugin, "get_sidebar_nav"
|
||||
):
|
||||
continue
|
||||
if not supports_plugin_hook(plugin, "get_render_mode"):
|
||||
continue
|
||||
render_mode, _ = plugin.get_render_mode()
|
||||
if render_mode != "vue":
|
||||
continue
|
||||
try:
|
||||
nav_list = plugin.get_sidebar_nav()
|
||||
if not nav_list:
|
||||
continue
|
||||
for raw in nav_list:
|
||||
if not raw or not isinstance(raw, dict):
|
||||
continue
|
||||
nav_key = str(
|
||||
raw.get("nav_key") or raw.get("key") or "main"
|
||||
).strip()
|
||||
if not nav_key or any(
|
||||
character in nav_key for character in ["/", "?", "#", " "]
|
||||
):
|
||||
self._logger.warning(
|
||||
f"插件[{plugin_id}]侧栏项 nav_key 无效,已跳过: "
|
||||
f"{nav_key!r}"
|
||||
)
|
||||
continue
|
||||
section = str(raw.get("section") or "system").lower()
|
||||
if section not in valid_sections:
|
||||
section = "system"
|
||||
permission = raw.get("permission")
|
||||
if permission is not None and str(permission) not in valid_permissions:
|
||||
permission = None
|
||||
elif permission is not None:
|
||||
permission = str(permission)
|
||||
try:
|
||||
order = int(raw.get("order", 0))
|
||||
except (TypeError, ValueError):
|
||||
order = 0
|
||||
items.append({
|
||||
"plugin_id": plugin_id,
|
||||
"nav_key": nav_key,
|
||||
"title": raw.get("title") or plugin.plugin_name,
|
||||
"icon": raw.get("icon") or "mdi-puzzle",
|
||||
"section": section,
|
||||
"permission": permission,
|
||||
"order": order,
|
||||
})
|
||||
except Exception as error:
|
||||
self._logger.error(
|
||||
f"获取插件[{plugin_id}]侧栏导航出错:{str(error)}"
|
||||
)
|
||||
items.sort(
|
||||
key=lambda item: (
|
||||
item["section"],
|
||||
item["order"],
|
||||
item["plugin_id"],
|
||||
item["nav_key"],
|
||||
)
|
||||
)
|
||||
return items
|
||||
|
||||
def dashboard_metadata(self) -> List[Dict[str, str]]:
|
||||
"""投影启用插件的单仪表板或多仪表板元信息。"""
|
||||
metadata = []
|
||||
for plugin_id, plugin in self._items(None):
|
||||
if not supports_plugin_hook(plugin, "get_dashboard"):
|
||||
continue
|
||||
try:
|
||||
if not plugin.get_state():
|
||||
continue
|
||||
if supports_plugin_hook(plugin, "get_dashboard_meta"):
|
||||
plugin_metadata = plugin.get_dashboard_meta()
|
||||
if plugin_metadata:
|
||||
metadata.extend({
|
||||
"id": plugin_id,
|
||||
"name": item.get("name"),
|
||||
"key": item.get("key"),
|
||||
} for item in plugin_metadata if item)
|
||||
else:
|
||||
metadata.append({
|
||||
"id": plugin_id,
|
||||
"name": plugin.plugin_name,
|
||||
"key": "",
|
||||
})
|
||||
except Exception as error:
|
||||
self._logger.error(
|
||||
f"获取插件[{plugin_id}]仪表盘元数据出错:{str(error)}"
|
||||
)
|
||||
return metadata
|
||||
@@ -0,0 +1,56 @@
|
||||
"""插件类与运行实例注册表。"""
|
||||
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
|
||||
class PluginRegistry:
|
||||
"""集中持有插件类和运行实例,并为读取方提供稳定快照。"""
|
||||
|
||||
def __init__(self) -> None:
|
||||
"""创建彼此独立但生命周期一致的类表和实例表。"""
|
||||
self._classes: Dict[str, Any] = {}
|
||||
self._running: Dict[str, Any] = {}
|
||||
|
||||
@property
|
||||
def classes(self) -> Dict[str, Any]:
|
||||
"""返回兼容旧调用方可变访问语义的插件类表。"""
|
||||
return self._classes
|
||||
|
||||
@property
|
||||
def running(self) -> Dict[str, Any]:
|
||||
"""返回兼容旧调用方可变访问语义的运行实例表。"""
|
||||
return self._running
|
||||
|
||||
def has_class(self, plugin_id: str) -> bool:
|
||||
"""判断插件类是否已经登记。"""
|
||||
return plugin_id in self._classes
|
||||
|
||||
def plugin_class(self, plugin_id: str) -> Optional[Any]:
|
||||
"""读取指定插件类,未登记时返回空。"""
|
||||
return self._classes.get(plugin_id)
|
||||
|
||||
def instance(self, plugin_id: str) -> Optional[Any]:
|
||||
"""读取指定运行实例,未运行时返回空。"""
|
||||
return self._running.get(plugin_id)
|
||||
|
||||
def plugin_ids(self) -> list[str]:
|
||||
"""返回保持登记顺序的插件类 ID 快照。"""
|
||||
return list(self._classes)
|
||||
|
||||
def running_ids(self) -> list[str]:
|
||||
"""返回保持登记顺序的运行实例 ID 快照。"""
|
||||
return list(self._running)
|
||||
|
||||
def running_snapshot(self) -> Dict[str, Any]:
|
||||
"""复制运行实例表,避免插件重载期间迭代失效。"""
|
||||
return dict(self._running)
|
||||
|
||||
def remove(self, plugin_id: str) -> None:
|
||||
"""同时移除指定插件类和运行实例。"""
|
||||
self._classes.pop(plugin_id, None)
|
||||
self._running.pop(plugin_id, None)
|
||||
|
||||
def clear(self) -> None:
|
||||
"""原地清空注册表,保持外部持有的兼容字典引用有效。"""
|
||||
self._classes.clear()
|
||||
self._running.clear()
|
||||
@@ -0,0 +1,89 @@
|
||||
"""插件运行时持久化端口。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Awaitable, Callable
|
||||
from typing import Any
|
||||
|
||||
|
||||
ConfigReader = Callable[[Any], Any]
|
||||
ConfigWriter = Callable[[Any, Any], Any]
|
||||
AsyncConfigWriter = Callable[[Any, Any], Awaitable[Any]]
|
||||
ConfigDeleter = Callable[[Any], bool]
|
||||
PluginDataDeleter = Callable[[str], Any]
|
||||
|
||||
|
||||
def _empty_read(_key: Any) -> Any:
|
||||
"""组合根尚未装配时返回空配置。"""
|
||||
return None
|
||||
|
||||
|
||||
def _ignore_write(_key: Any, _value: Any) -> None:
|
||||
"""组合根尚未装配时忽略同步配置写入。"""
|
||||
|
||||
|
||||
async def _ignore_async_write(_key: Any, _value: Any) -> None:
|
||||
"""组合根尚未装配时忽略异步配置写入。"""
|
||||
|
||||
|
||||
def _ignore_delete(_key: Any) -> bool:
|
||||
"""组合根尚未装配时报告配置未删除。"""
|
||||
return False
|
||||
|
||||
|
||||
def _ignore_plugin_data_delete(_plugin_id: str) -> None:
|
||||
"""组合根尚未装配时忽略插件数据删除。"""
|
||||
|
||||
|
||||
class PluginStorage:
|
||||
"""封装插件运行时所需的最小持久化能力。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
read: ConfigReader = _empty_read,
|
||||
write: ConfigWriter = _ignore_write,
|
||||
async_write: AsyncConfigWriter = _ignore_async_write,
|
||||
delete: ConfigDeleter = _ignore_delete,
|
||||
delete_data: PluginDataDeleter = _ignore_plugin_data_delete,
|
||||
) -> None:
|
||||
"""保存由启动组合根提供的读写函数。"""
|
||||
self._read = read
|
||||
self._write = write
|
||||
self._async_write = async_write
|
||||
self._delete = delete
|
||||
self._delete_data = delete_data
|
||||
|
||||
def read(self, key: Any) -> Any:
|
||||
"""读取插件运行时配置。"""
|
||||
return self._read(key)
|
||||
|
||||
def write(self, key: Any, value: Any) -> Any:
|
||||
"""同步保存插件运行时配置。"""
|
||||
return self._write(key, value)
|
||||
|
||||
async def async_write(self, key: Any, value: Any) -> Any:
|
||||
"""异步保存插件运行时配置。"""
|
||||
return await self._async_write(key, value)
|
||||
|
||||
def delete(self, key: Any) -> bool:
|
||||
"""删除插件运行时配置。"""
|
||||
return self._delete(key)
|
||||
|
||||
def delete_data(self, plugin_id: str) -> Any:
|
||||
"""删除指定插件的业务数据。"""
|
||||
return self._delete_data(plugin_id)
|
||||
|
||||
|
||||
_plugin_storage = PluginStorage()
|
||||
|
||||
|
||||
def configure_plugin_storage(storage: PluginStorage) -> None:
|
||||
"""由启动组合根替换插件运行时持久化实现。"""
|
||||
global _plugin_storage
|
||||
_plugin_storage = storage
|
||||
|
||||
|
||||
def get_plugin_storage() -> PluginStorage:
|
||||
"""返回当前插件运行时持久化端口。"""
|
||||
return _plugin_storage
|
||||
@@ -0,0 +1,86 @@
|
||||
"""插件市场、包和依赖系统能力的运行时注入端口。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from pathlib import Path
|
||||
from typing import Any, Optional
|
||||
|
||||
|
||||
class PluginSystemServices:
|
||||
"""保存由启动组合根注入的插件外部系统适配器。"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
market: Any,
|
||||
package: Any,
|
||||
dependency: Any,
|
||||
compatible_flags: Callable[[Optional[str]], list[str]],
|
||||
frozen: Callable[[], bool],
|
||||
) -> None:
|
||||
"""记录市场、包、依赖和代际兼容计算端口。"""
|
||||
self.market = market
|
||||
self.package = package
|
||||
self.dependency = dependency
|
||||
self.compatible_flags = compatible_flags
|
||||
self.frozen = frozen
|
||||
|
||||
def local_repo_paths(self) -> list[Path]:
|
||||
"""返回可监测的本地插件仓库路径。"""
|
||||
return self.market.get_local_repo_paths()
|
||||
|
||||
def local_candidate(self, plugin_id: str, **kwargs: Any) -> Optional[dict]:
|
||||
"""读取指定本地插件候选。"""
|
||||
return self.market.get_local_candidate(plugin_id, **kwargs)
|
||||
|
||||
def local_candidates(self) -> dict[str, dict]:
|
||||
"""读取全部本地插件候选。"""
|
||||
return self.market.get_local_candidates()
|
||||
|
||||
def local_repo_url(
|
||||
self,
|
||||
plugin_id: str,
|
||||
repo_path: Optional[object] = None,
|
||||
package_version: Optional[str] = None,
|
||||
) -> str:
|
||||
"""构造本地插件来源标识。"""
|
||||
return self.market.make_local_repo_url(
|
||||
plugin_id,
|
||||
repo_path,
|
||||
package_version,
|
||||
)
|
||||
|
||||
def annotate_system_version(self, plugin_info: dict) -> dict:
|
||||
"""补充插件条目的主程序版本兼容信息。"""
|
||||
return self.market.annotate_system_version(plugin_info)
|
||||
|
||||
def is_package_compatible(self, plugin_info: dict, package_version: str) -> bool:
|
||||
"""判断插件条目是否兼容指定代际。"""
|
||||
return self.market.is_package_compatible(plugin_info, package_version)
|
||||
|
||||
def is_frozen(self) -> bool:
|
||||
"""判断当前宿主是否为不可写的冻结运行模式。"""
|
||||
return self.frozen()
|
||||
|
||||
|
||||
_services: Optional[PluginSystemServices] = None
|
||||
|
||||
|
||||
def configure_plugin_system(services: PluginSystemServices) -> None:
|
||||
"""由启动组合根装配插件外部系统能力。"""
|
||||
global _services
|
||||
_services = services
|
||||
|
||||
|
||||
def reset_plugin_system() -> None:
|
||||
"""清除已装配服务,仅供隔离测试恢复进程状态。"""
|
||||
global _services
|
||||
_services = None
|
||||
|
||||
|
||||
def get_plugin_system() -> PluginSystemServices:
|
||||
"""返回已装配的插件外部系统端口。"""
|
||||
if _services is None:
|
||||
raise RuntimeError("插件外部系统服务尚未由启动组合根装配")
|
||||
return _services
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,25 +1,40 @@
|
||||
from typing import List, Optional, Type
|
||||
from collections.abc import Callable
|
||||
from typing import Any, List, Optional, Type
|
||||
|
||||
from pydantic import ValidationError
|
||||
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas import (
|
||||
DownloaderConf,
|
||||
MediaServerConf,
|
||||
NotificationConf,
|
||||
NotificationSwitchConf,
|
||||
)
|
||||
from app.schemas.system import DownloaderConf
|
||||
from app.schemas.system import MediaServerConf
|
||||
from app.schemas.system import NotificationConf
|
||||
from app.schemas.system import NotificationSwitchConf
|
||||
from app.schemas.types import MessageType, SystemConfigKey
|
||||
|
||||
|
||||
ServiceConfigReader = Callable[[SystemConfigKey], Any]
|
||||
|
||||
|
||||
def _empty_service_config(_config_key: SystemConfigKey) -> Any:
|
||||
"""组合根尚未装配时返回空服务配置。"""
|
||||
return None
|
||||
|
||||
|
||||
_service_config_reader: ServiceConfigReader = _empty_service_config
|
||||
|
||||
|
||||
def configure_service_config_reader(reader: ServiceConfigReader) -> None:
|
||||
"""由启动组合根注入服务配置读取能力。"""
|
||||
global _service_config_reader
|
||||
_service_config_reader = reader
|
||||
|
||||
|
||||
class ServiceConfigHelper:
|
||||
"""读取并校验通知、下载器和媒体服务器的宿主配置。"""
|
||||
|
||||
@staticmethod
|
||||
def get_configs(config_key: SystemConfigKey, conf_type: Type) -> List:
|
||||
"""按指定 Schema 过滤单条非法配置,避免影响同组其它服务。"""
|
||||
config_data = SystemConfigOper().get(config_key)
|
||||
config_data = _service_config_reader(config_key)
|
||||
if not config_data:
|
||||
return []
|
||||
configs = []
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
from typing import Dict, List, Optional, Type, TypeVar, Generic, Iterator
|
||||
|
||||
from app.db.oper.systemconfig import SystemConfigOper
|
||||
from app.runtime.extensions.module_manager import ModuleManager
|
||||
from app.runtime.extensions.service_config import ServiceConfigHelper
|
||||
from app.schemas import ServiceInfo
|
||||
from app.schemas.system import ServiceInfo
|
||||
from app.schemas.types import SystemConfigKey, ModuleType
|
||||
|
||||
TConf = TypeVar("TConf")
|
||||
@@ -11,7 +10,6 @@ TConf = TypeVar("TConf")
|
||||
__all__ = [
|
||||
"ServiceBaseHelper",
|
||||
"ServiceConfigHelper",
|
||||
"SystemConfigOper",
|
||||
]
|
||||
|
||||
|
||||
|
||||
+2
-1
@@ -6,7 +6,8 @@ from collections import deque
|
||||
from typing import Any, Tuple, List, Callable, Optional
|
||||
|
||||
from app.runtime.log import logger
|
||||
from app.schemas import RateLimitExceededException, LimitException
|
||||
from app.schemas.exception import RateLimitExceededException
|
||||
from app.schemas.exception import LimitException
|
||||
|
||||
|
||||
# 抽象基类
|
||||
|
||||
Reference in New Issue
Block a user