refactor: reorganize backend module boundaries

This commit is contained in:
jxxghp
2026-08-14 19:53:33 +08:00
parent fe0b444ceb
commit 369d7d6448
584 changed files with 2423 additions and 2275 deletions
+1
View File
@@ -0,0 +1 @@
"""插件和可选运行模块的发现及运行时生命周期管理。"""
+201
View File
@@ -0,0 +1,201 @@
import traceback
from typing import Generator, Optional, Tuple, Any, Union, List
from app.runtime.config import settings
from app.runtime.events import EventHandlerBinding, eventmanager
from app.foundation.reflection import ModuleHelper
from app.runtime.log import logger
from app.schemas.types import EventType, ModuleType, DownloaderType, MediaServerType, MessageChannel, StorageSchema, \
OtherModulesType, MediaRecognizeType
from app.foundation.reflection import ObjectUtils
from app.foundation.singleton import Singleton
class ModuleManager(metaclass=Singleton):
"""
模块管理器
"""
# 子模块类型集合
SubType = Union[
DownloaderType,
MediaServerType,
MessageChannel,
StorageSchema,
OtherModulesType,
MediaRecognizeType,
]
def __init__(self):
"""初始化模块注册表并装载当前启用的运行模块。"""
# 模块列表
self._modules: dict = {}
# 运行态模块列表
self._running_modules: dict = {}
# 事件总线通过该解析器绑定已启用的模块实例。
eventmanager.register_handler_instance_resolver(
"modules",
self.resolve_event_handler_instance,
)
self.load_modules()
def resolve_event_handler_instance(
self,
owner_class: type,
) -> Optional[EventHandlerBinding]:
"""为模块声明的事件方法解析当前运行实例。"""
module_id = owner_class.__name__
if module_id not in self._modules:
return None
module = self._running_modules.get(module_id)
owner_name = module_id
if module and callable(getattr(module, "get_name", None)):
owner_name = module.get_name()
return EventHandlerBinding(
instance=module,
owner_name=owner_name,
)
def load_modules(self):
"""
加载所有模块
"""
# 扫描模块目录
modules = ModuleHelper.load(
"app.modules",
filter_func=lambda _, obj: hasattr(obj, 'init_module') and hasattr(obj, 'init_setting')
)
self._running_modules = {}
self._modules = {}
for module in modules:
module_id = module.__name__
self._modules[module_id] = module
try:
# 生成实例
_module = module()
# 初始化模块
if self.check_setting(_module.init_setting()):
# 通过模板开关控制加载
_module.init_module()
self._running_modules[module_id] = _module
logger.debug(f"Moudle Loaded{module_id}")
except Exception as err:
logger.error(f"Load Moudle Error{module_id}{str(err)} - {traceback.format_exc()}", exc_info=True)
def stop(self):
"""
停止所有模块
"""
logger.info("正在停止所有模块...")
for module_id, module in self._running_modules.items():
try:
module.stop()
logger.debug(f"Moudle Stoped{module_id}")
except Exception as err:
logger.error(f"Stop Moudle Error{module_id}{str(err)} - {traceback.format_exc()}", exc_info=True)
logger.info("所有模块停止完成")
def reload(self):
"""
重新加载所有模块
"""
self.stop()
self.load_modules()
eventmanager.send_event(etype=EventType.ModuleReload, data={})
def test(self, modleid: str) -> Tuple[bool, str]:
"""
测试模块
"""
if modleid not in self._running_modules:
return False, ""
module = self._running_modules[modleid]
if hasattr(module, "test") \
and ObjectUtils.check_method(getattr(module, "test")):
result = module.test()
if not result:
return False, ""
return result
return True, "模块不支持测试"
@staticmethod
def check_setting(setting: Optional[tuple]) -> bool:
"""
检查开关是否己打开,开关使用,分隔多个值,符合其中即代表开启
"""
if not setting:
return True
switch, value = setting
option = getattr(settings, switch)
if not option:
return False
if option and value is True:
return True
if value in option:
return True
return False
def get_running_module(self, module_id: str) -> Any:
"""
根据模块id获取模块运行实例
"""
if not module_id:
return None
if not self._running_modules:
return None
return self._running_modules.get(module_id)
def get_running_modules(self, method: str) -> Generator:
"""
获取实现了同一方法的模块列表
"""
if not self._running_modules:
return
for _, module in self._running_modules.items():
if hasattr(module, method) \
and ObjectUtils.check_method(getattr(module, method)):
yield module
def get_running_type_modules(self, module_type: ModuleType) -> Generator:
"""
获取指定类型的模块列表
"""
if not self._running_modules:
return
for _, module in self._running_modules.items():
if hasattr(module, 'get_type') \
and module.get_type() == module_type:
yield module
def get_running_subtype_module(self, module_subtype: SubType) -> Generator:
"""
获取指定子类型的模块
"""
if not self._running_modules:
return
for _, module in self._running_modules.items():
if hasattr(module, 'get_subtype') \
and module.get_subtype() == module_subtype:
yield module
def get_module(self, module_id: str) -> Any:
"""
根据模块id获取模块
"""
if not module_id:
return None
if not self._modules:
return None
return self._modules.get(module_id)
def get_modules(self) -> dict:
"""
获取模块列表
"""
return self._modules
def get_module_ids(self) -> List[str]:
"""
获取模块id列表
"""
return list(self._modules.keys())
File diff suppressed because it is too large Load Diff
+177
View File
@@ -0,0 +1,177 @@
from typing import Dict, List, Optional, Type, TypeVar, Generic, Iterator
from pydantic import ValidationError
from app.runtime.extensions.module_manager import ModuleManager
from app.db.systemconfig_oper import SystemConfigOper
from app.runtime.log import logger
from app.schemas import DownloaderConf, MediaServerConf, NotificationConf, NotificationSwitchConf, ServiceInfo
from app.schemas.types import NotificationType, SystemConfigKey, ModuleType
TConf = TypeVar("TConf")
class ServiceConfigHelper:
"""
配置帮助类,获取不同类型的服务配置
"""
@staticmethod
def get_configs(config_key: SystemConfigKey, conf_type: Type) -> List:
"""
通用获取配置的方法,根据 config_key 获取相应的配置并返回指定类型的配置列表
:param config_key: 系统配置的 key
:param conf_type: 用于实例化配置对象的类类型
:return: 配置对象列表
"""
config_data = SystemConfigOper().get(config_key)
if not config_data:
return []
configs = []
for conf in config_data:
if not isinstance(conf, dict):
logger.warn(f"{config_key.value} 配置格式不正确,已跳过:{conf}")
continue
try:
# 直接使用 conf_type 来实例化配置对象
configs.append(conf_type(**conf))
except ValidationError as e:
# 单条配置存在非法值时跳过,避免影响其它服务的初始化
logger.error(f"{config_key.value} 配置 {conf.get('name')} 校验失败,已跳过:{e}")
return configs
@staticmethod
def get_downloader_configs() -> List[DownloaderConf]:
"""
获取下载器的配置
"""
return ServiceConfigHelper.get_configs(SystemConfigKey.Downloaders, DownloaderConf)
@staticmethod
def get_mediaserver_configs() -> List[MediaServerConf]:
"""
获取媒体服务器的配置
"""
return ServiceConfigHelper.get_configs(SystemConfigKey.MediaServers, MediaServerConf)
@staticmethod
def get_notification_configs() -> List[NotificationConf]:
"""
获取消息通知渠道的配置
"""
return ServiceConfigHelper.get_configs(SystemConfigKey.Notifications, NotificationConf)
@staticmethod
def get_notification_switches() -> List[NotificationSwitchConf]:
"""
获取消息通知场景的开关
"""
return ServiceConfigHelper.get_configs(SystemConfigKey.NotificationSwitchs, NotificationSwitchConf)
@staticmethod
def get_notification_switch(mtype: NotificationType) -> Optional[str]:
"""
获取指定类型的消息通知场景的开关
"""
switchs = ServiceConfigHelper.get_notification_switches()
for switch in switchs:
if switch.type == mtype.value:
return switch.action
return None
class ServiceBaseHelper(Generic[TConf]):
"""
通用服务帮助类,抽象获取配置和服务实例的通用逻辑
"""
def __init__(self, config_key: SystemConfigKey, conf_type: Type[TConf], module_type: ModuleType):
"""绑定服务配置类型与对应的运行模块类型。"""
self.modulemanager = ModuleManager()
self.config_key = config_key
self.conf_type = conf_type
self.module_type = module_type
def get_configs(self, include_disabled: bool = False) -> Dict[str, TConf]:
"""
获取配置列表
:param include_disabled: 是否包含禁用的配置,默认 False(仅返回启用的配置)
:return: 配置字典
"""
configs: List[TConf] = ServiceConfigHelper.get_configs(self.config_key, self.conf_type)
return {
config.name: config
for config in configs
if (config.name and config.type and config.enabled) or include_disabled
} if configs else {}
def get_config(self, name: str) -> Optional[TConf]:
"""
获取指定名称配置
"""
if not name:
return None
configs = self.get_configs()
return configs.get(name)
def iterate_module_instances(self) -> Iterator[ServiceInfo]:
"""
迭代所有模块的实例及其对应的配置,返回 ServiceInfo 实例
"""
configs = self.get_configs()
for module in self.modulemanager.get_running_type_modules(self.module_type):
if not module:
continue
module_instances = module.get_instances()
if not isinstance(module_instances, dict):
continue
for name, instance in module_instances.items():
if not instance:
continue
config = configs.get(name)
service_info = ServiceInfo(
name=name,
instance=instance,
module=module,
type=config.type if config else None,
config=config
)
yield service_info
def get_services(self, type_filter: Optional[str] = None, name_filters: Optional[List[str]] = None) \
-> Dict[str, ServiceInfo]:
"""
获取服务信息列表,并根据类型和名称列表进行过滤
:param type_filter: 需要过滤的服务类型
:param name_filters: 需要过滤的服务名称列表
:return: 过滤后的服务信息字典
"""
name_filters_set = set(name_filters) if name_filters else None
return {
service_info.name: service_info
for service_info in self.iterate_module_instances()
if service_info.config and (
type_filter is None or service_info.type == type_filter
) and (
name_filters_set is None or service_info.name in name_filters_set)
}
def get_service(self, name: str, type_filter: Optional[str] = None) -> Optional[ServiceInfo]:
"""
获取指定名称的服务信息,并根据类型过滤
:param name: 服务名称
:param type_filter: 需要过滤的服务类型
:return: 对应的服务信息,若不存在或类型不匹配则返回 None
"""
if not name:
return None
for service_info in self.iterate_module_instances():
if service_info.name == name:
if service_info.config and (type_filter is None or service_info.type == type_filter):
return service_info
return None