refactor: 推进后端分层架构治理

This commit is contained in:
jxxghp
2026-08-18 00:29:14 +08:00
parent 5d0bacabd5
commit 5128ae9e1e
363 changed files with 34179 additions and 6019 deletions
+1 -1
View File
@@ -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 (
+52 -5
View File
@@ -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": {
+1 -1
View File
@@ -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
+1
View File
@@ -0,0 +1 @@
"""事件运行时内部组件。"""
+138
View File
@@ -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
+196
View File
@@ -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)
+65
View File
@@ -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,
}
)
+200
View File
@@ -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
View File
@@ -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()
+1 -1
View File
@@ -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
+285
View File
@@ -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))
+262
View File
@@ -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
+56
View File
@@ -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()
+89
View File
@@ -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
+86
View File
@@ -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
+24 -9
View File
@@ -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 -3
View File
@@ -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
View File
@@ -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
# 抽象基类