Files
RuoYi-Vue3-FastAPI/ruoyi-fastapi-backend/plugins/core/validation/plugin_deps.py
T
insistence 2a055ba648 feat: 新增插件系统 (#112)
* feat: 初始化插件系统

* refactor: 收口插件系统运行时重构

* perf: 优化插件系统类型提示

* fix&perf: 修复和优化插件系统

* fix: 修复gitignore规则误忽略插件文件的问题

* fix: 修复运行时插件根路径算错的问题

* fix: 加强插件发现和路由注册的防护措施

* revert: 回滚定时任务白名单

* fix: 移除未使用的应用路由注册探测

* revert: 恢复部分代码

* perf: 优化插件系统

* docs: 新增插件开发文档

* perf: 优化插件管理模块

* perf: 提升插件系统核心能力

* refactor: 重构生命周期 step runner

* fix: 修复lint错误

* test: 清理测试用例

* test: 调整测试目录名称

* fix: 修复前后端目录硬编码的问题

* fix: 修复插件系统安全性缺口

* refactor: 重新设计插件生命周期 Migration 事务与回滚

* perf: 优化插件系统边界问题

* refactor: 重构当前插件系统的依赖体系设计

* perf: 优化代码

* perf: 优化代码

* fix: 修复代码合并问题

* fix: 修复bug

* perf: 优化代码

* perf&fix: 优化代码和修复bug

* docs: 优化文档格式

* feat: 适配Vue2版本

* docs: 更新README文档

* fix: 修复ruff lint错误

* chore: 更新后端依赖文件
2026-07-28 20:35:18 +08:00

791 lines
26 KiB
Python

from dataclasses import dataclass
from typing import Literal
from plugins.core.discovery.scanner import DiscoveredPlugin
from plugins.core.manifest.schema import PluginDependencyManifest, PluginManifest
from plugins.core.state import PluginStateResolver
from plugins.core.types import PluginStateRecord
from plugins.core.validation.dependencies import DependencyRequirementParser, ParsedDependency
from plugins.core.validation.versioning import PluginVersionConstraintMatcher
PluginDependencyStatus = Literal[
'satisfied',
'missing',
'not_installed',
'disabled',
'version_unsatisfied',
'cycle',
'dependent',
]
PluginBatchOperation = Literal['install', 'enable', 'upgrade', 'uninstall', 'purge']
PluginDependencyPlanBlockerStatus = Literal[
'missing',
'not_installed',
'disabled',
'version_unsatisfied',
'source_version_unsatisfied',
'cycle',
'unknown_operation',
]
class PluginDependencyVersionMatcher:
"""
插件依赖版本匹配器。
使用 Matcher 模式将插件依赖声明中的版本约束转换为统一版本匹配逻辑,
避免检查器和计划构建器分别解析约束。
"""
@staticmethod
def is_satisfied(installed_version: str | None, version_constraint: str | None) -> bool:
"""
判断插件版本是否满足约束。
:param installed_version: 已安装或源码版本
:param version_constraint: 版本约束
:return: 是否满足
"""
if not version_constraint:
return True
parsed_dependency = PluginDependencyVersionMatcher._parse_constraint(version_constraint)
return PluginVersionConstraintMatcher.is_satisfied(
installed_version,
parsed_dependency.operator,
parsed_dependency.version,
)
@staticmethod
def _parse_constraint(version_constraint: str) -> ParsedDependency:
"""
解析插件版本约束。
:param version_constraint: 版本约束
:return: 已解析依赖声明
"""
normalized_constraint = version_constraint.strip()
if normalized_constraint.startswith(('>=', '<=', '==', '!=', '>', '<', '=', '^', '~')):
return DependencyRequirementParser.parse(f'plugin{normalized_constraint}')
parsed_dependency = DependencyRequirementParser.parse(f'plugin=={normalized_constraint}')
return parsed_dependency
@dataclass(frozen=True)
class PluginDependencyCheckItem:
"""
插件间依赖检查项。
"""
plugin_id: str
dependency_id: str
required_version: str | None
installed_version: str | None
status: PluginDependencyStatus
message: str
@property
def ok(self) -> bool:
"""
判断插件依赖检查项是否通过。
:return: 是否通过
"""
return self.status == 'satisfied'
@dataclass(frozen=True)
class PluginDependencyCheckResult:
"""
插件间依赖检查结果。
"""
plugin_id: str
items: list[PluginDependencyCheckItem]
@property
def ok(self) -> bool:
"""
判断插件间依赖是否整体通过。
:return: 是否通过
"""
return all(item.ok for item in self.items)
@property
def failed_items(self) -> list[PluginDependencyCheckItem]:
"""
获取失败的插件依赖检查项。
:return: 失败检查项列表
"""
return [item for item in self.items if not item.ok]
@dataclass(frozen=True)
class PluginDependencyPlanBlocker:
"""
插件批量操作计划阻塞项。
"""
plugin_id: str
dependency_id: str
status: PluginDependencyPlanBlockerStatus
message: str
@dataclass(frozen=True)
class PluginDependencyPlanItem:
"""
插件批量操作计划项。
"""
plugin_id: str
name: str
version: str
operation: PluginBatchOperation
order: int
requested: bool
dependencies: list[str]
installed_version: str | None
enabled: str | None
status: str | None
blockers: list[PluginDependencyPlanBlocker]
@property
def ready(self) -> bool:
"""
判断当前计划项是否可执行。
:return: 是否可执行
"""
return not self.blockers
@dataclass(frozen=True)
class PluginDependencyPlan:
"""
插件批量操作拓扑计划。
"""
operation: PluginBatchOperation
requested_plugin_ids: list[str]
ordered_plugin_ids: list[str]
items: list[PluginDependencyPlanItem]
blockers: list[PluginDependencyPlanBlocker]
@property
def ok(self) -> bool:
"""
判断插件批量操作计划是否可执行。
:return: 是否可执行
"""
return not self.blockers
class PluginDependencyChecker:
"""
插件间依赖检查器。
使用 Checker 模式校验插件之间的存在性、安装状态、启用状态、版本约束和循环依赖。
"""
def __init__(
self,
discovered_plugins: list[DiscoveredPlugin],
database_plugins: list[PluginStateRecord] | None = None,
) -> None:
"""
初始化插件间依赖检查器。
:param discovered_plugins: 已发现插件列表
:param database_plugins: 数据库插件状态列表
:return: None
"""
self.discovered_plugin_map = {plugin.manifest.id: plugin for plugin in discovered_plugins}
self.database_plugin_map = {plugin.plugin_id: plugin for plugin in database_plugins or []}
def check_manifest(self, manifest: PluginManifest) -> PluginDependencyCheckResult:
"""
检查单个插件清单的插件间依赖。
:param manifest: 插件清单
:return: 插件间依赖检查结果
"""
items = [self._check_dependency(manifest.id, dependency) for dependency in manifest.dependencies.plugins]
items.extend(self._check_cycles(manifest.id))
return PluginDependencyCheckResult(plugin_id=manifest.id, items=items)
def check_enabled_dependents(self, plugin_id: str) -> PluginDependencyCheckResult:
"""
检查指定插件是否仍被已启用插件依赖。
:param plugin_id: 被停用或卸载的插件ID
:return: 被依赖方检查结果
"""
target_plugin = self.discovered_plugin_map.get(plugin_id)
target_database_plugin = self.database_plugin_map.get(plugin_id)
installed_version = self._resolve_installed_version(target_plugin, target_database_plugin)
items: list[PluginDependencyCheckItem] = []
for dependent_id, dependency in PluginDependencyGraph(self.discovered_plugin_map).find_direct_dependents(
plugin_id
):
database_plugin = self.database_plugin_map.get(dependent_id)
if not getattr(database_plugin, 'installed_version', None):
continue
if not PluginStateResolver.is_database_plugin_enabled(database_plugin):
continue
items.append(
PluginDependencyCheckItem(
plugin_id=dependent_id,
dependency_id=plugin_id,
required_version=dependency.version,
installed_version=installed_version,
status='dependent',
message=f'插件正在被已启用插件依赖:{dependent_id} -> {plugin_id}',
)
)
return PluginDependencyCheckResult(plugin_id=plugin_id, items=items)
def _check_dependency(
self,
plugin_id: str,
dependency: PluginDependencyManifest,
) -> PluginDependencyCheckItem:
"""
检查单条插件依赖。
:param plugin_id: 当前插件ID
:param dependency: 插件依赖声明
:return: 插件依赖检查项
"""
discovered_plugin = self.discovered_plugin_map.get(dependency.id)
database_plugin = self.database_plugin_map.get(dependency.id)
installed_version = self._resolve_installed_version(discovered_plugin, database_plugin)
if not discovered_plugin:
return self._build_item(plugin_id, dependency, None, 'missing', f'依赖插件不存在:{dependency.id}')
if not database_plugin or not getattr(database_plugin, 'installed_version', None):
return self._build_item(
plugin_id,
dependency,
installed_version,
'not_installed',
f'依赖插件未安装:{dependency.id}',
)
if not PluginStateResolver.is_database_plugin_enabled(database_plugin):
return self._build_item(
plugin_id,
dependency,
installed_version,
'disabled',
f'依赖插件未启用:{dependency.id}',
)
if not PluginDependencyVersionMatcher.is_satisfied(installed_version, dependency.version):
return self._build_item(
plugin_id,
dependency,
installed_version,
'version_unsatisfied',
f'依赖插件版本不满足:{dependency.id} installed={installed_version} required={dependency.version}',
)
return self._build_item(
plugin_id,
dependency,
installed_version,
'satisfied',
f'依赖插件已满足:{dependency.id}',
)
def _check_cycles(self, plugin_id: str) -> list[PluginDependencyCheckItem]:
"""
检查从当前插件出发的循环依赖。
:param plugin_id: 当前插件ID
:return: 循环依赖检查项列表
"""
cycle_path = PluginDependencyGraph(self.discovered_plugin_map).find_cycle_from(plugin_id)
if not cycle_path:
return []
cycle_text = ' -> '.join(cycle_path)
return [
PluginDependencyCheckItem(
plugin_id=plugin_id,
dependency_id=cycle_path[-1],
required_version=None,
installed_version=None,
status='cycle',
message=f'插件依赖存在循环:{cycle_text}',
)
]
@staticmethod
def _resolve_installed_version(
discovered_plugin: DiscoveredPlugin | None,
database_plugin: PluginStateRecord | None,
) -> str | None:
"""
解析依赖插件已安装版本。
:param discovered_plugin: 已发现插件
:param database_plugin: 数据库插件状态
:return: 已安装版本
"""
installed_version = getattr(database_plugin, 'installed_version', None)
if installed_version:
return installed_version
return discovered_plugin.manifest.version if discovered_plugin else None
@staticmethod
def _build_item(
plugin_id: str,
dependency: PluginDependencyManifest,
installed_version: str | None,
status: PluginDependencyStatus,
message: str,
) -> PluginDependencyCheckItem:
"""
构建插件依赖检查项。
:param plugin_id: 当前插件ID
:param dependency: 插件依赖声明
:param installed_version: 已安装版本
:param status: 检查状态
:param message: 检查消息
:return: 插件依赖检查项
"""
return PluginDependencyCheckItem(
plugin_id=plugin_id,
dependency_id=dependency.id,
required_version=dependency.version,
installed_version=installed_version,
status=status,
message=message,
)
class PluginDependencyGraph:
"""
插件依赖图。
使用 Graph 模式为插件间依赖提供循环检测,并为后续拓扑排序保留扩展点。
"""
def __init__(self, discovered_plugin_map: dict[str, DiscoveredPlugin]) -> None:
"""
初始化插件依赖图。
:param discovered_plugin_map: 已发现插件映射
:return: None
"""
self.discovered_plugin_map = discovered_plugin_map
def find_cycle_from(self, plugin_id: str) -> list[str]:
"""
查找从指定插件出发的循环依赖路径。
:param plugin_id: 插件ID
:return: 循环依赖路径,不存在时返回空列表
"""
return self._find_cycle(plugin_id, [], set())
def find_direct_dependents(self, plugin_id: str) -> list[tuple[str, PluginDependencyManifest]]:
"""
查找直接依赖指定插件的插件。
:param plugin_id: 被依赖插件ID
:return: 依赖方插件ID和依赖声明列表
"""
return [
(dependent_id, dependency)
for dependent_id, discovered_plugin in sorted(self.discovered_plugin_map.items())
for dependency in discovered_plugin.manifest.dependencies.plugins
if dependency.id == plugin_id
]
def _find_cycle(self, plugin_id: str, path: list[str], visited: set[str]) -> list[str]:
"""
深度优先查找循环依赖。
:param plugin_id: 当前插件ID
:param path: 当前访问路径
:param visited: 已访问插件ID集合
:return: 循环依赖路径
"""
if plugin_id in path:
cycle_start = path.index(plugin_id)
return [*path[cycle_start:], plugin_id]
if plugin_id in visited:
return []
visited.add(plugin_id)
discovered_plugin = self.discovered_plugin_map.get(plugin_id)
if not discovered_plugin:
return []
for dependency in discovered_plugin.manifest.dependencies.plugins:
cycle_path = self._find_cycle(dependency.id, [*path, plugin_id], visited)
if cycle_path:
return cycle_path
return []
class PluginDependencyPlanBuilder:
"""
插件批量操作拓扑计划生成器。
使用 Planner 模式为批量安装、启用和升级生成依赖优先的执行顺序,并输出阻塞原因。
"""
def __init__(
self,
discovered_plugins: list[DiscoveredPlugin],
database_plugins: list[PluginStateRecord] | None = None,
) -> None:
"""
初始化插件批量操作拓扑计划生成器。
:param discovered_plugins: 已发现插件列表
:param database_plugins: 数据库插件状态列表
:return: None
"""
self.discovered_plugin_map = {plugin.manifest.id: plugin for plugin in discovered_plugins}
self.database_plugin_map = {plugin.plugin_id: plugin for plugin in database_plugins or []}
def build_plan(
self,
operation: PluginBatchOperation,
plugin_ids: list[str] | None = None,
) -> PluginDependencyPlan:
"""
构建插件批量操作拓扑计划。
:param operation: 批量操作类型
:param plugin_ids: 指定插件ID列表,不传时计划全部已发现插件
:return: 插件批量操作拓扑计划
"""
requested_plugin_ids = plugin_ids or sorted(self.discovered_plugin_map)
closure, closure_blockers = self._collect_dependency_closure(requested_plugin_ids)
ordered_plugin_ids, topology_blockers = self._sort_dependency_first(closure)
blockers_by_plugin = self._group_blockers([*closure_blockers, *topology_blockers])
items = [
self._build_plan_item(
plugin_id,
order,
operation,
requested=plugin_id in requested_plugin_ids,
existing_blockers=blockers_by_plugin.get(plugin_id, []),
)
for order, plugin_id in enumerate(ordered_plugin_ids, start=1)
]
all_blockers = [*closure_blockers, *topology_blockers]
for item in items:
all_blockers.extend(item.blockers)
all_blockers = self._deduplicate_blockers(all_blockers)
return PluginDependencyPlan(
operation=operation,
requested_plugin_ids=requested_plugin_ids,
ordered_plugin_ids=ordered_plugin_ids,
items=items,
blockers=all_blockers,
)
def _collect_dependency_closure(
self,
plugin_ids: list[str],
) -> tuple[set[str], list[PluginDependencyPlanBlocker]]:
"""
收集目标插件及其递归依赖闭包。
:param plugin_ids: 目标插件ID列表
:return: 插件依赖闭包和阻塞项列表
"""
closure: set[str] = set()
blockers: list[PluginDependencyPlanBlocker] = []
visiting: list[str] = []
def visit(plugin_id: str, requested_by: str) -> None:
if plugin_id in visiting:
blockers.extend(self._build_cycle_blockers([*visiting[visiting.index(plugin_id) :], plugin_id]))
return
discovered_plugin = self.discovered_plugin_map.get(plugin_id)
if not discovered_plugin:
blockers.append(
PluginDependencyPlanBlocker(
plugin_id=requested_by,
dependency_id=plugin_id,
status='missing',
message=f'依赖插件不存在:{plugin_id}',
)
)
return
if plugin_id in closure:
return
visiting.append(plugin_id)
closure.add(plugin_id)
for dependency in discovered_plugin.manifest.dependencies.plugins:
visit(dependency.id, plugin_id)
visiting.pop()
for plugin_id in plugin_ids:
visit(plugin_id, plugin_id)
return closure, blockers
def _sort_dependency_first(
self,
plugin_ids: set[str],
) -> tuple[list[str], list[PluginDependencyPlanBlocker]]:
"""
对插件依赖闭包执行依赖优先拓扑排序。
:param plugin_ids: 插件ID集合
:return: 排序后的插件ID列表和阻塞项列表
"""
ordered_plugin_ids: list[str] = []
blockers: list[PluginDependencyPlanBlocker] = []
visiting: list[str] = []
visited: set[str] = set()
def visit(plugin_id: str) -> None:
if plugin_id in visited:
return
if plugin_id in visiting:
cycle_path = [*visiting[visiting.index(plugin_id) :], plugin_id]
blockers.extend(self._build_cycle_blockers(cycle_path))
return
discovered_plugin = self.discovered_plugin_map.get(plugin_id)
if not discovered_plugin:
return
visiting.append(plugin_id)
for dependency in discovered_plugin.manifest.dependencies.plugins:
if dependency.id in plugin_ids:
visit(dependency.id)
visiting.pop()
visited.add(plugin_id)
ordered_plugin_ids.append(plugin_id)
for plugin_id in sorted(plugin_ids):
visit(plugin_id)
return ordered_plugin_ids, blockers
@staticmethod
def _build_cycle_blockers(cycle_path: list[str]) -> list[PluginDependencyPlanBlocker]:
"""
构建循环依赖阻塞项。
:param cycle_path: 循环依赖路径
:return: 循环依赖阻塞项列表
"""
cycle_text = ' -> '.join(cycle_path)
return [
PluginDependencyPlanBlocker(
plugin_id=plugin_id,
dependency_id=cycle_path[-1],
status='cycle',
message=f'插件依赖存在循环:{cycle_text}',
)
for plugin_id in set(cycle_path)
]
def _build_plan_item(
self,
plugin_id: str,
order: int,
operation: PluginBatchOperation,
*,
requested: bool,
existing_blockers: list[PluginDependencyPlanBlocker],
) -> PluginDependencyPlanItem:
"""
构建单个插件批量操作计划项。
:param plugin_id: 插件ID
:param order: 执行顺序
:param operation: 批量操作类型
:param requested: 是否为用户显式指定插件
:param existing_blockers: 已收集的阻塞项
:return: 插件批量操作计划项
"""
discovered_plugin = self.discovered_plugin_map[plugin_id]
database_plugin = self.database_plugin_map.get(plugin_id)
blockers = [
*existing_blockers,
*self._build_dependency_blockers(discovered_plugin.manifest, operation),
]
return PluginDependencyPlanItem(
plugin_id=plugin_id,
name=discovered_plugin.manifest.name,
version=discovered_plugin.manifest.version,
operation=operation,
order=order,
requested=requested,
dependencies=[dependency.id for dependency in discovered_plugin.manifest.dependencies.plugins],
installed_version=getattr(database_plugin, 'installed_version', None),
enabled=getattr(database_plugin, 'enabled', None),
status=getattr(database_plugin, 'status', None),
blockers=blockers,
)
def _build_dependency_blockers(
self,
manifest: PluginManifest,
operation: PluginBatchOperation,
) -> list[PluginDependencyPlanBlocker]:
"""
根据操作类型构建插件依赖阻塞项。
:param manifest: 插件清单
:param operation: 批量操作类型
:return: 插件依赖阻塞项列表
"""
blockers = []
for dependency in manifest.dependencies.plugins:
blockers.extend(self._build_single_dependency_blocker(manifest, dependency, operation))
return blockers
def _build_single_dependency_blocker(
self,
manifest: PluginManifest,
dependency: PluginDependencyManifest,
operation: PluginBatchOperation,
) -> list[PluginDependencyPlanBlocker]:
"""
检查单个依赖的阻塞项。
:param manifest: 插件清单
:param dependency: 插件依赖声明
:param operation: 批量操作类型
:return: 依赖阻塞项列表
"""
discovered_dependency = self.discovered_plugin_map.get(dependency.id)
if not discovered_dependency:
return []
database_dependency = self.database_plugin_map.get(dependency.id)
source_version = discovered_dependency.manifest.version
installed_version = getattr(database_dependency, 'installed_version', None)
if not PluginDependencyVersionMatcher.is_satisfied(source_version, dependency.version):
return [
self._build_blocker(
manifest.id,
dependency.id,
'source_version_unsatisfied',
f'依赖插件源码版本不满足:{dependency.id} source={source_version} required={dependency.version}',
)
]
if operation == 'install':
if database_dependency and not PluginStateResolver.is_database_plugin_enabled(database_dependency):
return [
self._build_blocker(
manifest.id,
dependency.id,
'disabled',
f'依赖插件未启用:{dependency.id}',
)
]
return []
if not database_dependency or not installed_version:
return [
self._build_blocker(
manifest.id,
dependency.id,
'not_installed',
f'依赖插件未安装:{dependency.id}',
)
]
if operation == 'enable' and not PluginDependencyVersionMatcher.is_satisfied(
installed_version,
dependency.version,
):
return [
self._build_blocker(
manifest.id,
dependency.id,
'version_unsatisfied',
(
f'依赖插件版本不满足:{dependency.id} '
f'installed={installed_version} required={dependency.version}'
),
)
]
if operation == 'upgrade' and not PluginStateResolver.is_database_plugin_enabled(database_dependency):
return [
self._build_blocker(
manifest.id,
dependency.id,
'disabled',
f'依赖插件未启用:{dependency.id}',
)
]
return []
@staticmethod
def _build_blocker(
plugin_id: str,
dependency_id: str,
status: PluginDependencyPlanBlockerStatus,
message: str,
) -> PluginDependencyPlanBlocker:
"""
构建插件批量操作计划阻塞项。
:param plugin_id: 插件ID
:param dependency_id: 依赖插件ID
:param status: 阻塞状态
:param message: 阻塞说明
:return: 插件批量操作计划阻塞项
"""
return PluginDependencyPlanBlocker(
plugin_id=plugin_id,
dependency_id=dependency_id,
status=status,
message=message,
)
@staticmethod
def _group_blockers(
blockers: list[PluginDependencyPlanBlocker],
) -> dict[str, list[PluginDependencyPlanBlocker]]:
"""
按插件 ID 分组阻塞项。
:param blockers: 阻塞项列表
:return: 阻塞项分组
"""
blocker_map: dict[str, list[PluginDependencyPlanBlocker]] = {}
for blocker in blockers:
blocker_map.setdefault(blocker.plugin_id, []).append(blocker)
return blocker_map
@staticmethod
def _deduplicate_blockers(
blockers: list[PluginDependencyPlanBlocker],
) -> list[PluginDependencyPlanBlocker]:
"""
去重插件批量操作计划阻塞项。
:param blockers: 阻塞项列表
:return: 去重后的阻塞项列表
"""
blocker_map = {
(blocker.plugin_id, blocker.dependency_id, blocker.status, blocker.message): blocker for blocker in blockers
}
return list(blocker_map.values())