Files
DDSRem 2de976f6b3 Merge pull request #7050 from DDSRem-Dev/codex-comment-migration
docs: 统一全项目中文注释和结构化契约说明
2026-10-17 19:26:29 +08:00

198 lines
7.7 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
插件依赖检查与安装运行时服务。
"""
import time
from collections.abc import Callable
from dataclasses import dataclass
from typing import Any, Optional
from app.runtime.extensions.plugin.registry import PluginRegistry
from app.runtime.extensions.plugin.system import PluginSystemServices
from app.schemas.plugin import PluginInstance, PluginRuntimeStatus
@dataclass(frozen=True)
class PluginDependencyInstallResult:
"""
记录插件依赖检查结果,区分无缺失、安装成功和安装失败。
:param missing: 检测到缺失或不满足版本约束的依赖声明
:param success: 依赖已满足或安装成功时为 True,安装失败时为 False
"""
missing: list[str]
success: bool
@dataclass(frozen=True)
class PluginDependencyClassification:
"""
按当前源码和 Python 环境划分已安装插件。
:param ready: 源码存在且依赖满足的插件标识
:param missing_dependencies: 源码存在但依赖不满足的插件标识
:param missing_source: 缺少可加载源码的插件标识
"""
ready: tuple[str, ...]
missing_dependencies: tuple[str, ...]
missing_source: tuple[str, ...]
class PluginDependencyService:
"""
执行缺失插件依赖的发现和安装,不参与插件生命周期。
"""
def __init__(
self,
*,
system: Callable[[], PluginSystemServices],
instances: Optional[Callable[[], dict[str, PluginInstance]]] = None,
loadable_hosts: Optional[Callable[[], set[str]]] = None,
registry: Optional[PluginRegistry] = None,
log: Any,
) -> None:
"""
保存插件系统、应装载实例、应装载本体和运行状态端口。
"""
self._system = system
self._instances = instances or (lambda: {})
self._loadable_hosts = loadable_hosts
self._registry = registry
self._logger = log
def _begin_missing_install(self, missing: list[str]) -> Optional[float]:
"""
统一无缺失短路、安装清单日志和耗时起点。
"""
if not missing:
return None
self._logger.debug(f"检测到缺失的依赖项: {missing}")
self._logger.info(f"开始安装缺失的依赖项,共 {len(missing)} 个...")
return time.time()
def _complete_missing_install(
self,
missing: list[str],
success: bool,
started_at: float,
) -> PluginDependencyInstallResult:
"""
统一安装结果、耗时和成功或失败日志分类。
"""
elapsed = time.time() - started_at
if success:
self._logger.info(
f"已完成 {len(missing)} 个依赖项安装,总耗时:{elapsed:.2f} 秒"
)
else:
self._logger.warning(
f"存在缺失依赖项安装失败,请尝试手动安装,总耗时:{elapsed:.2f} 秒"
)
return PluginDependencyInstallResult(missing=missing, success=success)
def install_missing_with_status(self) -> PluginDependencyInstallResult:
"""
安装缺失依赖并返回安装器的明确结果。
"""
installer = self._system().dependency
missing = installer.find_missing()
started_at = self._begin_missing_install(missing)
if started_at is None:
return PluginDependencyInstallResult(missing=[], success=True)
success, _message = installer.install(missing)
return self._complete_missing_install(missing, success, started_at)
def install_missing(self) -> list[str]:
"""
安装当前环境缺失的插件依赖并保持历史列表返回合同。
"""
return self.install_missing_with_status().missing
async def async_install_missing_with_status(self) -> PluginDependencyInstallResult:
"""
在异步启动链中恢复缺失依赖,确保安装子进程可取消。
"""
installer = self._system().dependency
missing = await installer.async_find_missing()
started_at = self._begin_missing_install(missing)
if started_at is None:
return PluginDependencyInstallResult(missing=[], success=True)
success, _message = await installer.async_install(missing)
return self._complete_missing_install(missing, success, started_at)
def classify_plugins(self) -> PluginDependencyClassification:
"""
分类应当装载的物理插件,并把源码结论映射到应当装载的虚拟实例。
物理插件那一层按安装清单划分,而安装清单只回答「包在不在磁盘上」;分类结果
随后会被逐个 ``start()``,因此这里必须先按启用位过滤掉停用的本体,否则停用
的插件会在开机与配置热重载时被重新拉起来,启用位形同虚设。三个桶一起过滤:
停用的本体不应装载;启用分身仍按过滤前的共享源码结论分类。依赖安装只覆盖
存在启用本体或分身的源码插件,由启动层向安装器注入该集合。
"""
ready, missing_dependencies, missing_source = (
self._system().dependency.classify_plugins()
)
# 分身只共享源码和依赖,本体的停用位不能让启用分身丢失源码结论。
source_ready = set(ready)
source_pending = set(missing_dependencies)
loadable = self._loadable_hosts() if self._loadable_hosts is not None else None
if loadable is not None:
ready = [plugin_id for plugin_id in ready if plugin_id in loadable]
missing_dependencies = [
plugin_id for plugin_id in missing_dependencies if plugin_id in loadable
]
missing_source = [
plugin_id for plugin_id in missing_source if plugin_id in loadable
]
ready = list(ready)
missing_dependencies = list(missing_dependencies)
missing_source = list(missing_source)
for instance in self._instances().values():
if instance.source_plugin_id in source_ready:
ready.append(instance.instance_id)
elif instance.source_plugin_id in source_pending:
missing_dependencies.append(instance.instance_id)
else:
missing_source.append(instance.instance_id)
return PluginDependencyClassification(
ready=tuple(ready),
missing_dependencies=tuple(missing_dependencies),
missing_source=tuple(missing_source),
)
def apply_classification(
self,
classification: PluginDependencyClassification,
) -> None:
"""
把依赖分类写入唯一注册表,已激活插件保持当前状态。
"""
if self._registry is None:
raise RuntimeError("插件依赖状态注册表尚未装配")
running_ids = set(self._registry.running_ids())
for plugin_id in classification.missing_source:
self._registry.set_runtime_status(
plugin_id,
PluginRuntimeStatus.SOURCE_MISSING,
)
for plugin_id in classification.missing_dependencies:
self._registry.set_runtime_status(
plugin_id,
PluginRuntimeStatus.DEPENDENCY_PENDING,
)
for plugin_id in classification.ready:
current_status = self._registry.runtime_status(plugin_id)
if (
plugin_id in running_ids
and current_status is not PluginRuntimeStatus.DEPENDENCY_PENDING
):
continue
self._registry.set_runtime_status(
plugin_id,
PluginRuntimeStatus.READY,
)