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
@@ -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",
]