mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-04 23:17:20 +08:00
Merge remote-tracking branch 'origin/v3' into v3
# Conflicts: # app/api/endpoints/agent.py # app/api/endpoints/anthropic.py # app/api/endpoints/openai.py # app/chain/__init__.py # app/chain/message.py # app/chain/site.py # app/chain/subscribe.py # app/chain/transfer.py # app/modules/discord/__init__.py # app/modules/qqbot/__init__.py # app/modules/slack/__init__.py # app/modules/telegram/__init__.py # app/modules/wechat/__init__.py # app/runtime/extensions/module_manager.py # app/runtime/extensions/service_registry.py # tests/test_agent_interaction.py # tests/test_slash_command_interactions.py # tests/test_web_agent_stream.py
This commit is contained in:
@@ -0,0 +1,260 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import importlib
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
from types import MappingProxyType
|
||||
from typing import Any, Mapping
|
||||
|
||||
from app.runtime.capabilities.model import (
|
||||
ActivationPolicy,
|
||||
AdapterExecutionMode,
|
||||
CapabilitySpec,
|
||||
SelectorSchema,
|
||||
)
|
||||
from app.runtime.capabilities.registry import CapabilityRegistry
|
||||
from app.runtime.config import settings
|
||||
from app.runtime.extensions.service_config import ServiceConfigHelper
|
||||
from app.schemas.types import (
|
||||
DownloaderType,
|
||||
MediaRecognizeType,
|
||||
MediaServerType,
|
||||
NotificationChannel,
|
||||
ModuleType,
|
||||
OtherModulesType,
|
||||
StorageSchema,
|
||||
SystemConfigKey,
|
||||
)
|
||||
|
||||
|
||||
HOST_MODULE_KIND = "host_module"
|
||||
_SETTING_SELECTOR = "setting_truthy"
|
||||
_SERVICE_SELECTOR = "system_config_item"
|
||||
_MODULE_ROOT = Path(__file__).resolve().parents[2] / "modules"
|
||||
_SERVICE_CONFIG_GETTERS = MappingProxyType({
|
||||
SystemConfigKey.Downloaders.value: ServiceConfigHelper.get_downloader_configs,
|
||||
SystemConfigKey.MediaServers.value: ServiceConfigHelper.get_mediaserver_configs,
|
||||
SystemConfigKey.Notifications.value: ServiceConfigHelper.get_notification_configs,
|
||||
})
|
||||
_SUBTYPE_NAMES = frozenset(
|
||||
item.name
|
||||
for enum_type in (
|
||||
DownloaderType,
|
||||
MediaServerType,
|
||||
NotificationChannel,
|
||||
StorageSchema,
|
||||
OtherModulesType,
|
||||
MediaRecognizeType,
|
||||
)
|
||||
for item in enum_type
|
||||
)
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class HostModuleConfigSnapshot:
|
||||
"""一次 reconcile 使用的不可变配置视图,避免每个能力重复查询配置。"""
|
||||
|
||||
settings: Mapping[str, Any]
|
||||
services: Mapping[str, tuple[Any, ...]]
|
||||
|
||||
|
||||
def _validate_setting_selector(config: Mapping[str, Any]) -> None:
|
||||
"""限制 setting selector 只能读取已声明的应用设置。"""
|
||||
key = config["key"]
|
||||
if not isinstance(key, str) or not key or not hasattr(settings, key):
|
||||
raise ValueError(f"未知应用设置:{key!r}")
|
||||
|
||||
|
||||
def _validate_service_selector(config: Mapping[str, Any]) -> None:
|
||||
"""限制服务 selector 使用经过 Schema 校验的三个宿主服务配置。"""
|
||||
key = config["key"]
|
||||
if key not in _SERVICE_CONFIG_GETTERS:
|
||||
raise ValueError(f"不支持的服务配置:{key!r}")
|
||||
if config["match_field"] != "type":
|
||||
raise ValueError("服务 selector 的 match_field 必须是 type")
|
||||
if config["enabled_field"] != "enabled":
|
||||
raise ValueError("服务 selector 的 enabled_field 必须是 enabled")
|
||||
match_value = config["match_value"]
|
||||
if not isinstance(match_value, str) or not match_value:
|
||||
raise ValueError("服务 selector 的 match_value 必须是非空字符串")
|
||||
|
||||
|
||||
HOST_MODULE_SELECTOR_SCHEMAS = MappingProxyType({
|
||||
_SETTING_SELECTOR: SelectorSchema(
|
||||
required_fields=frozenset({"key"}),
|
||||
validator=_validate_setting_selector,
|
||||
),
|
||||
_SERVICE_SELECTOR: SelectorSchema(
|
||||
required_fields=frozenset({
|
||||
"key",
|
||||
"match_field",
|
||||
"match_value",
|
||||
"enabled_field",
|
||||
}),
|
||||
validator=_validate_service_selector,
|
||||
),
|
||||
})
|
||||
|
||||
|
||||
def _validate_manifest_inventory(registry: CapabilityRegistry) -> None:
|
||||
"""校验一级模块包与 manifest 一一对应,并固定宿主声明合同。"""
|
||||
module_packages = {
|
||||
child.name
|
||||
for child in _MODULE_ROOT.iterdir()
|
||||
if child.is_dir()
|
||||
and not child.name.startswith("_")
|
||||
and (child / "__init__.py").is_file()
|
||||
}
|
||||
specs = registry.list_specs()
|
||||
manifest_packages = {spec.source.parent.name for spec in specs}
|
||||
if module_packages != manifest_packages:
|
||||
missing = sorted(module_packages - manifest_packages)
|
||||
unknown = sorted(manifest_packages - module_packages)
|
||||
raise ValueError(
|
||||
f"Host Module manifest inventory 不一致:missing={missing} unknown={unknown}"
|
||||
)
|
||||
|
||||
allowed_metadata = {"name", "type", "subtype", "priority"}
|
||||
module_type_values = {item.value for item in ModuleType}
|
||||
for spec in specs:
|
||||
if spec.source.parent.parent != _MODULE_ROOT:
|
||||
raise ValueError(f"Host Module manifest 必须位于一级模块包:{spec.source}")
|
||||
module_name, symbol_name = spec.entrypoint.split(":", maxsplit=1)
|
||||
expected_module = f"app.modules.{spec.source.parent.name}"
|
||||
if module_name != expected_module or symbol_name != spec.id:
|
||||
raise ValueError(
|
||||
f"{spec.source}: entrypoint 必须指向同包且类名等于 capability id"
|
||||
)
|
||||
if set(spec.metadata) != allowed_metadata:
|
||||
raise ValueError(
|
||||
f"{spec.source}: metadata 字段必须是 {sorted(allowed_metadata)}"
|
||||
)
|
||||
if spec.metadata["type"] not in module_type_values:
|
||||
raise ValueError(f"{spec.source}: 非法 metadata.type={spec.metadata['type']!r}")
|
||||
if spec.metadata["subtype"] not in _SUBTYPE_NAMES:
|
||||
raise ValueError(
|
||||
f"{spec.source}: 非法 metadata.subtype={spec.metadata['subtype']!r}"
|
||||
)
|
||||
priority = spec.metadata["priority"]
|
||||
if isinstance(priority, bool) or not isinstance(priority, int):
|
||||
raise ValueError(f"{spec.source}: metadata.priority 必须是整数")
|
||||
if spec.activation is ActivationPolicy.WHEN_CONFIGURED:
|
||||
selector_key = str(spec.selector.config["key"])
|
||||
if selector_key not in spec.watch:
|
||||
raise ValueError(
|
||||
f"{spec.source}: activation.watch 必须包含 selector 配置键"
|
||||
)
|
||||
|
||||
|
||||
def build_host_module_registry() -> CapabilityRegistry:
|
||||
"""从现有物理模块包构建 import-free Host Module Registry。"""
|
||||
registry = CapabilityRegistry.discover(
|
||||
(_MODULE_ROOT,),
|
||||
kinds={HOST_MODULE_KIND},
|
||||
selector_schemas=HOST_MODULE_SELECTOR_SCHEMAS,
|
||||
)
|
||||
_validate_manifest_inventory(registry)
|
||||
return registry
|
||||
|
||||
|
||||
def capture_host_module_config(
|
||||
specs: tuple[CapabilitySpec, ...],
|
||||
) -> HostModuleConfigSnapshot:
|
||||
"""对本轮涉及的设置和服务配置各读取一次并冻结容器。"""
|
||||
setting_keys: set[str] = set()
|
||||
service_keys: set[str] = set()
|
||||
for spec in specs:
|
||||
selector = spec.selector
|
||||
if selector is None:
|
||||
continue
|
||||
key = str(selector.config["key"])
|
||||
if selector.kind == _SETTING_SELECTOR:
|
||||
setting_keys.add(key)
|
||||
elif selector.kind == _SERVICE_SELECTOR:
|
||||
service_keys.add(key)
|
||||
|
||||
setting_values = {
|
||||
key: getattr(settings, key)
|
||||
for key in sorted(setting_keys)
|
||||
}
|
||||
service_values = {
|
||||
key: tuple(_SERVICE_CONFIG_GETTERS[key]())
|
||||
for key in sorted(service_keys)
|
||||
}
|
||||
return HostModuleConfigSnapshot(
|
||||
settings=MappingProxyType(setting_values),
|
||||
services=MappingProxyType(service_values),
|
||||
)
|
||||
|
||||
|
||||
def should_run_host_module(
|
||||
spec: CapabilitySpec,
|
||||
snapshot: HostModuleConfigSnapshot,
|
||||
) -> bool:
|
||||
"""依据有限 selector 语法判断能力是否应拥有运行资源。"""
|
||||
if spec.activation is ActivationPolicy.BOOTSTRAP:
|
||||
return True
|
||||
if spec.activation is ActivationPolicy.ON_FIRST_USE:
|
||||
return False
|
||||
selector = spec.selector
|
||||
if selector is None:
|
||||
return False
|
||||
if selector.kind == _SETTING_SELECTOR:
|
||||
return bool(snapshot.settings[selector.config["key"]])
|
||||
if selector.kind == _SERVICE_SELECTOR:
|
||||
config = selector.config
|
||||
return any(
|
||||
getattr(item, config["match_field"]) == config["match_value"]
|
||||
and bool(getattr(item, config["enabled_field"]))
|
||||
for item in snapshot.services[config["key"]]
|
||||
)
|
||||
raise ValueError(f"未支持的 Host Module selector:{selector.kind}")
|
||||
|
||||
|
||||
class HostModuleAdapter:
|
||||
"""把现有模块类接入 Capability Runtime,保持类路径与对象 identity 不变。"""
|
||||
|
||||
execution_mode = AdapterExecutionMode.SYNC
|
||||
|
||||
@staticmethod
|
||||
def materialize(spec: CapabilitySpec) -> type:
|
||||
"""按 manifest entrypoint 导入并返回原始模块类。"""
|
||||
module_name, symbol_name = spec.entrypoint.split(":", maxsplit=1)
|
||||
implementation = getattr(importlib.import_module(module_name), symbol_name)
|
||||
if not isinstance(implementation, type):
|
||||
raise TypeError(f"{spec.entrypoint} 不是模块类")
|
||||
return implementation
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
spec: CapabilitySpec,
|
||||
implementation: type,
|
||||
generation: int,
|
||||
previous: Any = None,
|
||||
) -> Any:
|
||||
"""首次创建实例;配置重载继续使用原实例以保留既有模块语义。"""
|
||||
del spec, generation
|
||||
return previous if previous is not None else implementation()
|
||||
|
||||
@staticmethod
|
||||
def start(spec: CapabilitySpec, candidate: Any, generation: int) -> None:
|
||||
"""初始化候选实例拥有的连接、线程或客户端资源。"""
|
||||
del spec, generation
|
||||
candidate.init_module()
|
||||
|
||||
@staticmethod
|
||||
def stop(spec: CapabilitySpec, instance: Any, generation: int) -> None:
|
||||
"""停止实例拥有的资源;Runtime 会先撤销其运行态可见性。"""
|
||||
del spec, generation
|
||||
instance.stop()
|
||||
|
||||
@staticmethod
|
||||
def cleanup(
|
||||
spec: CapabilitySpec,
|
||||
candidate: Any,
|
||||
generation: int,
|
||||
error: BaseException,
|
||||
) -> None:
|
||||
"""启动失败后尽力回收候选实例已创建的部分资源。"""
|
||||
del spec, generation, error
|
||||
candidate.stop()
|
||||
@@ -0,0 +1,194 @@
|
||||
"""Managed Resource 的声明发现与 Capability Runtime 适配器。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import importlib
|
||||
import inspect
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
from app.runtime.capabilities.errors import CapabilityAdapterContractError
|
||||
from app.runtime.capabilities.model import (
|
||||
ActivationPolicy,
|
||||
AdapterExecutionMode,
|
||||
CapabilitySpec,
|
||||
)
|
||||
from app.runtime.capabilities.registry import CapabilityRegistry
|
||||
from app.runtime.managed_resources import (
|
||||
MANAGED_RESOURCE_ASYNC_KIND,
|
||||
MANAGED_RESOURCE_SYNC_KIND,
|
||||
)
|
||||
|
||||
|
||||
_DEFAULT_RESOURCE_ROOT = Path(__file__).resolve().parents[2] / "adapters"
|
||||
_RESOURCE_KINDS = {MANAGED_RESOURCE_SYNC_KIND, MANAGED_RESOURCE_ASYNC_KIND}
|
||||
|
||||
|
||||
def _load_entrypoint(spec: CapabilitySpec) -> Any:
|
||||
"""解析声明中的 canonical 实现对象,不创建资源实例。"""
|
||||
module_name, symbol_name = spec.entrypoint.split(":", maxsplit=1)
|
||||
module = importlib.import_module(module_name)
|
||||
try:
|
||||
return getattr(module, symbol_name)
|
||||
except AttributeError as error:
|
||||
raise CapabilityAdapterContractError(
|
||||
f"{spec.entrypoint} 未公开 Managed Resource 实现"
|
||||
) from error
|
||||
|
||||
|
||||
def _create_candidate(spec: CapabilitySpec, implementation: Any) -> Any:
|
||||
"""通过零参数工厂创建资源候选,实例在 start 成功前不可见。"""
|
||||
if not callable(implementation):
|
||||
raise CapabilityAdapterContractError(
|
||||
f"{spec.entrypoint} 不是可调用的 Managed Resource 工厂"
|
||||
)
|
||||
candidate = implementation()
|
||||
if candidate is None or inspect.isawaitable(candidate):
|
||||
close = getattr(candidate, "close", None)
|
||||
if callable(close):
|
||||
close()
|
||||
raise CapabilityAdapterContractError(
|
||||
f"{spec.entrypoint} 必须同步返回资源候选"
|
||||
)
|
||||
return candidate
|
||||
|
||||
|
||||
def _resource_method(spec: CapabilitySpec, candidate: Any, name: str) -> Any:
|
||||
"""读取必需生命周期方法并生成稳定合同错误。"""
|
||||
callback = getattr(candidate, name, None)
|
||||
if not callable(callback):
|
||||
raise CapabilityAdapterContractError(
|
||||
f"{spec.entrypoint} 的资源候选缺少 {name}()"
|
||||
)
|
||||
return callback
|
||||
|
||||
|
||||
class SyncManagedResourceAdapter:
|
||||
"""把同步 start/stop 资源接入 Capability Runtime。"""
|
||||
|
||||
execution_mode = AdapterExecutionMode.SYNC
|
||||
|
||||
@staticmethod
|
||||
def materialize(spec: CapabilitySpec) -> Any:
|
||||
"""解析资源工厂。"""
|
||||
return _load_entrypoint(spec)
|
||||
|
||||
@staticmethod
|
||||
def create(
|
||||
spec: CapabilitySpec,
|
||||
implementation: Any,
|
||||
_generation: int,
|
||||
_previous: Any = None,
|
||||
) -> Any:
|
||||
"""创建尚未发布的同步资源候选。"""
|
||||
return _create_candidate(spec, implementation)
|
||||
|
||||
@staticmethod
|
||||
def start(spec: CapabilitySpec, candidate: Any, _generation: int) -> None:
|
||||
"""启动同步候选;同步 kind 不接受 awaitable 返回值。"""
|
||||
result = _resource_method(spec, candidate, "start")()
|
||||
if inspect.isawaitable(result):
|
||||
close = getattr(result, "close", None)
|
||||
if callable(close):
|
||||
close()
|
||||
raise CapabilityAdapterContractError(
|
||||
f"{spec.entrypoint}.start() 返回 awaitable,与同步 kind 不匹配"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def stop(spec: CapabilitySpec, instance: Any, _generation: int) -> None:
|
||||
"""停止同步资源,异常交由 Runtime 保留资源所有权并支持重试。"""
|
||||
result = _resource_method(spec, instance, "stop")()
|
||||
if inspect.isawaitable(result):
|
||||
close = getattr(result, "close", None)
|
||||
if callable(close):
|
||||
close()
|
||||
raise CapabilityAdapterContractError(
|
||||
f"{spec.entrypoint}.stop() 返回 awaitable,与同步 kind 不匹配"
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def cleanup(
|
||||
spec: CapabilitySpec,
|
||||
candidate: Any,
|
||||
generation: int,
|
||||
_error: BaseException,
|
||||
) -> None:
|
||||
"""启动失败时按同一 stop 合同清理尚未发布的候选。"""
|
||||
SyncManagedResourceAdapter.stop(spec, candidate, generation)
|
||||
|
||||
|
||||
class AsyncManagedResourceAdapter:
|
||||
"""把异步 start/stop 资源接入 Capability Runtime。"""
|
||||
|
||||
execution_mode = AdapterExecutionMode.ASYNC
|
||||
|
||||
@staticmethod
|
||||
async def materialize(spec: CapabilitySpec) -> Any:
|
||||
"""在线程中解析资源工厂,避免第三方导入阻塞事件循环。"""
|
||||
return await asyncio.to_thread(_load_entrypoint, spec)
|
||||
|
||||
@staticmethod
|
||||
async def create(
|
||||
spec: CapabilitySpec,
|
||||
implementation: Any,
|
||||
_generation: int,
|
||||
_previous: Any = None,
|
||||
) -> Any:
|
||||
"""创建尚未发布的异步资源候选。"""
|
||||
return _create_candidate(spec, implementation)
|
||||
|
||||
@staticmethod
|
||||
async def start(spec: CapabilitySpec, candidate: Any, _generation: int) -> None:
|
||||
"""等待异步候选完成启动。"""
|
||||
result = _resource_method(spec, candidate, "start")()
|
||||
if not inspect.isawaitable(result):
|
||||
raise CapabilityAdapterContractError(
|
||||
f"{spec.entrypoint}.start() 必须返回 awaitable"
|
||||
)
|
||||
await result
|
||||
|
||||
@staticmethod
|
||||
async def stop(spec: CapabilitySpec, instance: Any, _generation: int) -> None:
|
||||
"""等待异步资源完成停止。"""
|
||||
result = _resource_method(spec, instance, "stop")()
|
||||
if not inspect.isawaitable(result):
|
||||
raise CapabilityAdapterContractError(
|
||||
f"{spec.entrypoint}.stop() 必须返回 awaitable"
|
||||
)
|
||||
await result
|
||||
|
||||
@staticmethod
|
||||
async def cleanup(
|
||||
spec: CapabilitySpec,
|
||||
candidate: Any,
|
||||
generation: int,
|
||||
_error: BaseException,
|
||||
) -> None:
|
||||
"""启动失败时等待同一 stop 合同清理候选。"""
|
||||
await AsyncManagedResourceAdapter.stop(spec, candidate, generation)
|
||||
|
||||
|
||||
def _validate_registry(registry: CapabilityRegistry) -> None:
|
||||
"""固定类别级声明合同,资源只能由显式首用触发。"""
|
||||
for spec in registry.list_specs():
|
||||
if set(spec.metadata) != {"name"}:
|
||||
raise ValueError(f"{spec.source}: Managed Resource metadata 只能包含 name")
|
||||
if spec.activation is not ActivationPolicy.ON_FIRST_USE:
|
||||
raise ValueError(f"{spec.source}: Managed Resource 必须使用 on_first_use")
|
||||
if spec.selector is not None or spec.watch:
|
||||
raise ValueError(f"{spec.source}: Managed Resource 不接受配置 selector 或 watch")
|
||||
|
||||
|
||||
def build_managed_resource_registry(
|
||||
roots: Iterable[Path | str] | None = None,
|
||||
) -> CapabilityRegistry:
|
||||
"""从 data-only manifest 构建不导入资源实现的注册表。"""
|
||||
registry = CapabilityRegistry.discover(
|
||||
tuple(roots) if roots is not None else (_DEFAULT_RESOURCE_ROOT,),
|
||||
kinds=_RESOURCE_KINDS,
|
||||
selector_schemas={},
|
||||
)
|
||||
_validate_registry(registry)
|
||||
return registry
|
||||
@@ -1,22 +1,42 @@
|
||||
import traceback
|
||||
from typing import Generator, Optional, Tuple, Any, Union, List
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import threading
|
||||
from typing import Any, Generator, List, Optional, Tuple, Union
|
||||
|
||||
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, NotificationChannel, StorageSchema, \
|
||||
OtherModulesType, MediaRecognizeType
|
||||
from app.foundation.reflection import ObjectUtils
|
||||
from app.foundation.singleton import Singleton
|
||||
from app.runtime.capabilities.model import (
|
||||
CapabilityLifecycleState,
|
||||
CapabilityObservation,
|
||||
CapabilitySpec,
|
||||
)
|
||||
from app.runtime.capabilities.runtime import CapabilityRuntime
|
||||
from app.runtime.config import settings
|
||||
from app.runtime.events import Event, EventHandlerBinding, eventmanager
|
||||
from app.runtime.extensions.host_module_adapter import (
|
||||
HOST_MODULE_KIND,
|
||||
HostModuleAdapter,
|
||||
build_host_module_registry,
|
||||
capture_host_module_config,
|
||||
should_run_host_module,
|
||||
)
|
||||
from app.runtime.log import logger
|
||||
from app.schemas.types import (
|
||||
DownloaderType,
|
||||
EventType,
|
||||
MediaRecognizeType,
|
||||
MediaServerType,
|
||||
NotificationChannel,
|
||||
ModuleType,
|
||||
OtherModulesType,
|
||||
StorageSchema,
|
||||
)
|
||||
|
||||
|
||||
class ModuleManager(metaclass=Singleton):
|
||||
"""
|
||||
模块管理器
|
||||
"""
|
||||
"""以 Capability Runtime 管理宿主模块,并保留旧插件同步查询合同。"""
|
||||
|
||||
# 子模块类型集合
|
||||
SubType = Union[
|
||||
DownloaderType,
|
||||
MediaServerType,
|
||||
@@ -26,176 +46,286 @@ class ModuleManager(metaclass=Singleton):
|
||||
MediaRecognizeType,
|
||||
]
|
||||
|
||||
def __init__(self):
|
||||
"""初始化模块注册表并装载当前启用的运行模块。"""
|
||||
# 模块列表
|
||||
self._modules: dict = {}
|
||||
# 运行态模块列表
|
||||
self._running_modules: dict = {}
|
||||
# 事件总线通过该解析器绑定已启用的模块实例。
|
||||
def __init__(self) -> None:
|
||||
"""发现 data-only manifest,并按当前配置激活所需宿主模块。"""
|
||||
self._lock = threading.RLock()
|
||||
self._lifecycle_lock = threading.RLock()
|
||||
self._modules: dict[str, type] = {}
|
||||
self._running_modules: dict[str, Any] = {}
|
||||
registry = build_host_module_registry()
|
||||
self._runtime = CapabilityRuntime(
|
||||
registry,
|
||||
adapters={HOST_MODULE_KIND: HostModuleAdapter()},
|
||||
observer=self._observe_transition,
|
||||
)
|
||||
# pkgutil 的既有发现顺序按一级包名稳定排列,兼容视图继续保持该顺序。
|
||||
self._specs = tuple(
|
||||
sorted(self._runtime.list_specs(), key=lambda item: item.source.parent.name)
|
||||
)
|
||||
eventmanager.register_handler_instance_resolver(
|
||||
"modules",
|
||||
self.resolve_event_handler_instance,
|
||||
)
|
||||
eventmanager.add_event_listener(
|
||||
EventType.ConfigChanged,
|
||||
self.handle_config_changed,
|
||||
)
|
||||
self.load_modules()
|
||||
|
||||
@staticmethod
|
||||
def _observe_transition(observation: CapabilityObservation) -> None:
|
||||
"""把 Runtime 的稳定转换结果接入现有日志面。"""
|
||||
if observation.outcome == "failed":
|
||||
logger.error(
|
||||
"Host Module %s %s 失败:%s",
|
||||
observation.capability_id,
|
||||
observation.operation,
|
||||
observation.error,
|
||||
)
|
||||
elif observation.outcome == "succeeded":
|
||||
logger.debug(
|
||||
"Host Module %s %s 完成,generation=%s,耗时=%.2fms",
|
||||
observation.capability_id,
|
||||
observation.operation,
|
||||
observation.generation,
|
||||
observation.duration_ms,
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
def _event_changed_keys(event: Optional[Event]) -> set[str]:
|
||||
"""兼容对象和 dict 两种配置事件载荷。"""
|
||||
if not event:
|
||||
return set()
|
||||
event_data = event.event_data
|
||||
if isinstance(event_data, dict):
|
||||
keys = event_data.get("key", set())
|
||||
else:
|
||||
keys = getattr(event_data, "key", set())
|
||||
if isinstance(keys, str):
|
||||
return {keys}
|
||||
return {str(key) for key in (keys or set())}
|
||||
|
||||
def _remember_materialized(self, module_id: str, implementation: type) -> type:
|
||||
"""更新旧 `_modules` 视图,但不改变能力资源生命周期。"""
|
||||
with self._lock:
|
||||
self._modules[module_id] = implementation
|
||||
return implementation
|
||||
|
||||
def _consumer_materialized_class(self, spec: CapabilitySpec) -> Optional[type]:
|
||||
"""识别插件显式旧导入产生的真实类,不触发新的 Python import。"""
|
||||
module_name, symbol_name = spec.entrypoint.split(":", maxsplit=1)
|
||||
module = sys.modules.get(module_name)
|
||||
namespace = getattr(module, "__dict__", None) if module is not None else None
|
||||
if not isinstance(namespace, dict):
|
||||
return None
|
||||
implementation = namespace.get(symbol_name)
|
||||
return implementation if isinstance(implementation, type) else None
|
||||
|
||||
def _refresh_running_projection(self) -> None:
|
||||
"""从 Runtime 已发布实例重建插件可见的运行模块字典。"""
|
||||
running = {
|
||||
spec.id: instance
|
||||
for spec in self._specs
|
||||
if (instance := self._runtime.get_running(spec.id)) is not None
|
||||
}
|
||||
with self._lock:
|
||||
self._running_modules = running
|
||||
|
||||
def resolve_event_handler_instance(
|
||||
self,
|
||||
owner_class: type,
|
||||
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,
|
||||
"""按 canonical class identity 绑定当前 generation,停止态阻断 fallback 构造。"""
|
||||
for spec in self._specs:
|
||||
with self._lock:
|
||||
implementation = self._modules.get(spec.id)
|
||||
if implementation is None:
|
||||
implementation = self._consumer_materialized_class(spec)
|
||||
if implementation is not None:
|
||||
self._remember_materialized(spec.id, implementation)
|
||||
# 同步 Runtime 的物化观测,但不创建或启动实例。
|
||||
self._runtime.snapshot(spec.id)
|
||||
if implementation is not owner_class:
|
||||
continue
|
||||
return EventHandlerBinding(
|
||||
instance=self._runtime.get_running(spec.id),
|
||||
owner_name=str(spec.metadata["name"]),
|
||||
)
|
||||
return None
|
||||
|
||||
def _reconcile(
|
||||
self,
|
||||
*,
|
||||
reason: str,
|
||||
changed_keys: Optional[set[str]] = None,
|
||||
reload_running: bool = False,
|
||||
) -> None:
|
||||
"""以一次配置快照串行协调需要启动、重载或停止的能力。"""
|
||||
with self._lifecycle_lock:
|
||||
selected = tuple(
|
||||
spec
|
||||
for spec in self._specs
|
||||
if changed_keys is None or changed_keys.intersection(spec.watch)
|
||||
)
|
||||
snapshot = capture_host_module_config(selected)
|
||||
for spec in selected:
|
||||
desired = should_run_host_module(spec, snapshot)
|
||||
running = self._runtime.get_running(spec.id)
|
||||
try:
|
||||
if desired and running is None:
|
||||
instance = self._runtime.activate(
|
||||
spec.id,
|
||||
reason=reason,
|
||||
retry=True,
|
||||
)
|
||||
self._remember_materialized(spec.id, type(instance))
|
||||
elif desired and running is not None and reload_running:
|
||||
instance = self._runtime.reload(spec.id, reason=reason)
|
||||
self._remember_materialized(spec.id, type(instance))
|
||||
elif not desired and running is not None:
|
||||
self._runtime.stop(spec.id, reason=reason)
|
||||
except Exception:
|
||||
# 单能力失败由 Runtime 完整记录;其它无依赖能力继续 reconcile。
|
||||
continue
|
||||
self._refresh_running_projection()
|
||||
|
||||
def load_modules(self) -> None:
|
||||
"""按当前配置启动未运行模块;已运行模块保持当前 generation。"""
|
||||
self._reconcile(reason="module_manager_load")
|
||||
|
||||
def handle_config_changed(self, event: Event) -> None:
|
||||
"""配置变更时仅协调 watch 命中的能力,并保证单一生命周期 writer。"""
|
||||
changed_keys = self._event_changed_keys(event)
|
||||
if not changed_keys:
|
||||
return
|
||||
self._reconcile(
|
||||
reason="config_changed",
|
||||
changed_keys=changed_keys,
|
||||
reload_running=True,
|
||||
)
|
||||
|
||||
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):
|
||||
"""
|
||||
停止所有模块
|
||||
"""
|
||||
def stop(self) -> None:
|
||||
"""停止全部运行模块但保留 Runtime,使旧插件可随后再次 load。"""
|
||||
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)
|
||||
with self._lifecycle_lock:
|
||||
for spec in reversed(self._specs):
|
||||
snapshot = self._runtime.snapshot(spec.id)
|
||||
if (
|
||||
self._runtime.get_running(spec.id) is None
|
||||
and snapshot.lifecycle is not CapabilityLifecycleState.FAILED
|
||||
):
|
||||
continue
|
||||
try:
|
||||
self._runtime.stop(spec.id, reason="module_manager_stop")
|
||||
except Exception:
|
||||
continue
|
||||
self._refresh_running_projection()
|
||||
logger.info("所有模块停止完成")
|
||||
|
||||
def reload(self):
|
||||
"""
|
||||
重新加载所有模块
|
||||
"""
|
||||
self.stop()
|
||||
self.load_modules()
|
||||
eventmanager.send_event(etype=EventType.ModuleReload, data={})
|
||||
def shutdown(self) -> None:
|
||||
"""进程关闭时不可逆停止 Runtime,阻止并发能力重新发布。"""
|
||||
logger.info("正在关闭模块运行时...")
|
||||
with self._lifecycle_lock:
|
||||
self._runtime.shutdown(reason="application_shutdown")
|
||||
self._refresh_running_projection()
|
||||
logger.info("模块运行时关闭完成")
|
||||
|
||||
def reload(self) -> None:
|
||||
"""保留旧插件可观察的 stop、load、ModuleReload 同步顺序。"""
|
||||
with self._lifecycle_lock:
|
||||
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:
|
||||
"""测试已运行模块;未启用模块保持旧合同返回 `(False, "")`。"""
|
||||
module = self.get_running_module(modleid)
|
||||
if module is None:
|
||||
return False, ""
|
||||
module = self._running_modules[modleid]
|
||||
if hasattr(module, "test") \
|
||||
and ObjectUtils.check_method(getattr(module, "test")):
|
||||
if hasattr(module, "test") and ObjectUtils.check_method(module.test):
|
||||
result = module.test()
|
||||
if not result:
|
||||
return False, ""
|
||||
return result
|
||||
return result if result else (False, "")
|
||||
return True, "模块不支持测试"
|
||||
|
||||
@staticmethod
|
||||
def check_setting(setting: Optional[tuple]) -> bool:
|
||||
"""
|
||||
检查开关是否己打开,开关使用,分隔多个值,符合其中即代表开启
|
||||
"""
|
||||
"""保留旧模块开关的 truthy 与 membership 判定语义。"""
|
||||
if not setting:
|
||||
return True
|
||||
switch, value = setting
|
||||
option = getattr(settings, switch)
|
||||
if not option:
|
||||
return False
|
||||
if option and value is True:
|
||||
if value is True:
|
||||
return True
|
||||
if value in option:
|
||||
return True
|
||||
return False
|
||||
return value in option
|
||||
|
||||
def get_running_module(self, module_id: str) -> Any:
|
||||
"""
|
||||
根据模块id获取模块运行实例
|
||||
"""
|
||||
if not module_id:
|
||||
"""根据模块 ID 返回已发布的运行实例,不触发物化。"""
|
||||
if not module_id or self._runtime.get_spec(module_id) is None:
|
||||
return None
|
||||
if not self._running_modules:
|
||||
return None
|
||||
return self._running_modules.get(module_id)
|
||||
return self._runtime.get_running(module_id)
|
||||
|
||||
def _running_snapshot(self) -> tuple[Any, ...]:
|
||||
"""直接读取 Runtime 发布视图,转换期间不暴露旧或候选实例。"""
|
||||
return tuple(
|
||||
instance
|
||||
for spec in self._specs
|
||||
if (instance := self._runtime.get_running(spec.id)) is not None
|
||||
)
|
||||
|
||||
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)):
|
||||
"""返回实现了指定方法的运行模块快照。"""
|
||||
for module in self._running_snapshot():
|
||||
candidate = getattr(module, method, None)
|
||||
if callable(candidate) and ObjectUtils.check_method(candidate):
|
||||
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:
|
||||
"""返回指定类型的运行模块快照。"""
|
||||
for module in self._running_snapshot():
|
||||
if 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:
|
||||
"""返回指定子类型的运行模块快照。"""
|
||||
for module in self._running_snapshot():
|
||||
if module.get_subtype() == module_subtype:
|
||||
yield module
|
||||
|
||||
def get_module(self, module_id: str) -> Any:
|
||||
"""
|
||||
根据模块id获取模块
|
||||
"""
|
||||
if not module_id:
|
||||
"""显式物化并返回 canonical 模块类;失败保持旧合同返回 None。"""
|
||||
if not module_id or self._runtime.get_spec(module_id) is None:
|
||||
return None
|
||||
if not self._modules:
|
||||
with self._lock:
|
||||
implementation = self._modules.get(module_id)
|
||||
if implementation is not None:
|
||||
return implementation
|
||||
try:
|
||||
implementation = self._runtime.materialize(
|
||||
module_id,
|
||||
reason="compat_get_module",
|
||||
retry=True,
|
||||
)
|
||||
except Exception:
|
||||
return None
|
||||
return self._modules.get(module_id)
|
||||
return self._remember_materialized(module_id, implementation)
|
||||
|
||||
def get_modules(self) -> dict:
|
||||
"""
|
||||
获取模块列表
|
||||
"""
|
||||
return self._modules
|
||||
def get_modules(self) -> dict[str, type]:
|
||||
"""兼容性显式物化全部真实类;单个失败不阻断其它模块。"""
|
||||
for spec in self._specs:
|
||||
self.get_module(spec.id)
|
||||
with self._lock:
|
||||
return dict(self._modules)
|
||||
|
||||
def get_module_ids(self) -> List[str]:
|
||||
"""
|
||||
获取模块id列表
|
||||
"""
|
||||
return list(self._modules.keys())
|
||||
"""从 manifest 返回全部模块 ID,不物化实现。"""
|
||||
return [spec.id for spec in self._specs]
|
||||
|
||||
def list_specs(self) -> tuple[CapabilitySpec, ...]:
|
||||
"""返回全部轻量模块声明,包含物化或启动失败的能力。"""
|
||||
return self._specs
|
||||
|
||||
def get_specs(self) -> tuple[CapabilitySpec, ...]:
|
||||
"""兼容内部调用命名,返回与 `list_specs` 相同的声明快照。"""
|
||||
return self.list_specs()
|
||||
|
||||
@@ -37,6 +37,7 @@ from app.schemas.types import EventType, SystemConfigKey
|
||||
|
||||
LegacyDiagnosticsConfigurator = Callable[..., None]
|
||||
LegacyImportScanner = Callable[..., None]
|
||||
LegacyPluginImportPreparer = Callable[..., None]
|
||||
PluginInstallReporter = Callable[..., None]
|
||||
SiteAuthLevelProvider = Callable[[], int]
|
||||
|
||||
@@ -45,6 +46,10 @@ def _ignore_legacy_diagnostics(**_kwargs) -> None:
|
||||
"""在启动组合根尚未注入兼容服务时保持插件加载可用。"""
|
||||
|
||||
|
||||
def _ignore_plugin_resource_imports(**_kwargs) -> None:
|
||||
"""未进入应用启动组合时不主动创建进程级宿主资源。"""
|
||||
|
||||
|
||||
def _unavailable_site_auth_level() -> int:
|
||||
"""站点能力尚未装配时返回未认证等级。"""
|
||||
return 0
|
||||
@@ -54,6 +59,9 @@ _legacy_diagnostics_configurator: LegacyDiagnosticsConfigurator = (
|
||||
_ignore_legacy_diagnostics
|
||||
)
|
||||
_legacy_import_scanner: LegacyImportScanner = _ignore_legacy_diagnostics
|
||||
_legacy_plugin_import_preparer: LegacyPluginImportPreparer = (
|
||||
_ignore_plugin_resource_imports
|
||||
)
|
||||
_plugin_install_reporter: PluginInstallReporter = _ignore_legacy_diagnostics
|
||||
_site_auth_level_provider: SiteAuthLevelProvider = _unavailable_site_auth_level
|
||||
|
||||
@@ -69,6 +77,14 @@ def configure_plugin_legacy_import_services(
|
||||
_legacy_import_scanner = import_scanner
|
||||
|
||||
|
||||
def configure_plugin_resource_import_preparer(
|
||||
preparer: LegacyPluginImportPreparer,
|
||||
) -> None:
|
||||
"""注入旧插件导入前的宿主资源准备器。"""
|
||||
global _legacy_plugin_import_preparer
|
||||
_legacy_plugin_import_preparer = preparer
|
||||
|
||||
|
||||
def configure_plugin_install_reporter(reporter: PluginInstallReporter) -> None:
|
||||
"""由启动组合根注入插件安装上报器,避免扩展层依赖远程服务。"""
|
||||
global _plugin_install_reporter
|
||||
@@ -318,6 +334,13 @@ class PluginManager(ConfigReloadMixin, metaclass=Singleton):
|
||||
module_name = f"app.plugins.{plugin_dir.name}"
|
||||
logger.debug(f"正在导入插件模块:{module_name}")
|
||||
|
||||
# 旧插件可能直接导入带宿主资源前置条件的第三方包。资源必须在
|
||||
# Python 执行插件模块顶层代码前就绪,否则导入副作用无法安全回滚。
|
||||
_legacy_plugin_import_preparer(
|
||||
plugin_id=plugin_dir.name,
|
||||
plugin_dir=plugin_dir,
|
||||
)
|
||||
|
||||
_legacy_import_scanner(
|
||||
plugin_id=plugin_dir.name,
|
||||
plugin_dir=plugin_dir,
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
from typing import 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.types import MessageType, SystemConfigKey
|
||||
|
||||
|
||||
class ServiceConfigHelper:
|
||||
"""读取并校验通知、下载器和媒体服务器的宿主配置。"""
|
||||
|
||||
@staticmethod
|
||||
def get_configs(config_key: SystemConfigKey, conf_type: Type) -> List:
|
||||
"""按指定 Schema 过滤单条非法配置,避免影响同组其它服务。"""
|
||||
config_data = SystemConfigOper().get(config_key)
|
||||
if not config_data:
|
||||
return []
|
||||
configs = []
|
||||
for conf in config_data:
|
||||
if not isinstance(conf, dict):
|
||||
logger.warning(f"{config_key.value} 配置格式不正确,已跳过:{conf}")
|
||||
continue
|
||||
try:
|
||||
configs.append(conf_type(**conf))
|
||||
except ValidationError as err:
|
||||
logger.error(
|
||||
f"{config_key.value} 配置 {conf.get('name')} 校验失败,已跳过:{err}"
|
||||
)
|
||||
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: MessageType) -> Optional[str]:
|
||||
"""返回指定通知场景的目标范围。"""
|
||||
for switch in ServiceConfigHelper.get_notification_switches():
|
||||
if switch.type == mtype.value:
|
||||
return switch.action
|
||||
return None
|
||||
@@ -1,84 +1,18 @@
|
||||
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.oper.systemconfig import SystemConfigOper
|
||||
from app.runtime.log import logger
|
||||
from app.schemas import DownloaderConf, MediaServerConf, NotificationConf, NotificationSwitchConf, ServiceInfo
|
||||
from app.schemas.types import MessageType, SystemConfigKey, ModuleType
|
||||
from app.runtime.extensions.module_manager import ModuleManager
|
||||
from app.runtime.extensions.service_config import ServiceConfigHelper
|
||||
from app.schemas import ServiceInfo
|
||||
from app.schemas.types import 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: MessageType) -> Optional[str]:
|
||||
"""
|
||||
获取指定类型的消息通知场景的开关
|
||||
"""
|
||||
switchs = ServiceConfigHelper.get_notification_switches()
|
||||
for switch in switchs:
|
||||
if switch.type == mtype.value:
|
||||
return switch.action
|
||||
return None
|
||||
__all__ = [
|
||||
"ServiceBaseHelper",
|
||||
"ServiceConfigHelper",
|
||||
"SystemConfigOper",
|
||||
]
|
||||
|
||||
|
||||
class ServiceBaseHelper(Generic[TConf]):
|
||||
|
||||
Reference in New Issue
Block a user