mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-10-06 09:41:55 +00:00
* 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: 更新后端依赖文件
387 lines
16 KiB
Python
387 lines
16 KiB
Python
import asyncio
|
||
from pathlib import Path
|
||
from time import monotonic
|
||
|
||
from plugins.core.capability import PluginRuntimeCapability, PluginRuntimeCapabilityResolver
|
||
from plugins.core.discovery.registry import PluginRegistry
|
||
from plugins.core.discovery.scanner import (
|
||
DiscoveredPlugin,
|
||
PluginDiscoveryError,
|
||
PluginDiscoveryResult,
|
||
PluginScanner,
|
||
)
|
||
from plugins.core.lifecycle.migration import PluginMigrationRunner
|
||
from plugins.core.lifecycle.precheck import PluginLifecycleScriptPrechecker
|
||
from plugins.core.runtime.support import PluginPrecheckContext
|
||
from plugins.core.types import PluginStateRecord
|
||
from plugins.core.validation.manifest import PluginManifestChecker, PluginManifestCheckResult
|
||
from plugins.core.validation.menus import PluginMenuConflictChecker
|
||
from plugins.core.validation.plugin_deps import (
|
||
PluginDependencyChecker as InterPluginDependencyChecker,
|
||
)
|
||
from plugins.core.validation.plugin_deps import (
|
||
PluginDependencyCheckResult,
|
||
)
|
||
from plugins.core.validation.result import PluginValidationIssue
|
||
from plugins.core.validation.structure import PluginStructureChecker
|
||
from utils.log_util import logger
|
||
|
||
from .dependency_container import PluginRuntimeDependencies
|
||
from .migration_store import PluginMigrationHistoryGatewayStore
|
||
from .responses import PluginRuntimeBlockedPayload, PluginRuntimeBlockedPayloadDict
|
||
|
||
PLUGIN_DISCOVERY_CACHE_TTL_SECONDS = 2.0
|
||
DATABASE_PLUGIN_STATE_SYNC_LOOP_ERROR = '当前事件循环内不能同步读取数据库插件状态,已返回空列表'
|
||
|
||
|
||
class PluginRuntimeContextService:
|
||
"""
|
||
插件应用运行时上下文服务。
|
||
|
||
集中提供插件发现、注册表构建、数据库状态读取和预检上下文构建能力,
|
||
让 runtime facade 和组合式 use case 只关注插件操作编排。
|
||
"""
|
||
|
||
def __init__(self, dependencies: PluginRuntimeDependencies) -> None:
|
||
"""
|
||
初始化插件运行时上下文服务。
|
||
|
||
:param dependencies: 插件运行时依赖容器
|
||
"""
|
||
self.dependencies = dependencies
|
||
self._discovered_plugins_cache: dict[Path, tuple[float, list[DiscoveredPlugin]]] = {}
|
||
self._discovery_errors_cache: dict[Path, list[PluginDiscoveryError]] = {}
|
||
|
||
def build_registry(self) -> PluginRegistry:
|
||
"""
|
||
构建本地插件注册表。
|
||
|
||
:return: 插件注册表
|
||
"""
|
||
backend_root = Path(self.dependencies.runtime_environment.get_backend_dir())
|
||
return PluginRegistry.build(self.discover_plugins(backend_root))
|
||
|
||
def get_discovered_plugin(self, plugin_id: str) -> DiscoveredPlugin | None:
|
||
"""
|
||
根据插件 ID 获取已发现插件。
|
||
|
||
:param plugin_id: 插件ID
|
||
:return: 已发现插件对象
|
||
"""
|
||
backend_root = Path(self.dependencies.runtime_environment.get_backend_dir())
|
||
return self.get_discovered_plugin_from_list(self.discover_plugins(backend_root), plugin_id)
|
||
|
||
@staticmethod
|
||
def get_discovered_plugin_from_list(
|
||
discovered_plugins: list[DiscoveredPlugin],
|
||
plugin_id: str,
|
||
) -> DiscoveredPlugin | None:
|
||
"""
|
||
从已发现插件列表中查找指定插件。
|
||
|
||
:param discovered_plugins: 已发现插件列表
|
||
:param plugin_id: 插件ID
|
||
:return: 已发现插件对象
|
||
"""
|
||
for discovered_plugin in discovered_plugins:
|
||
if discovered_plugin.manifest.id == plugin_id:
|
||
return discovered_plugin
|
||
|
||
return None
|
||
|
||
async def load_database_plugin_state(self, plugin_id: str) -> tuple[PluginStateRecord | None, str | None]:
|
||
"""
|
||
读取数据库插件状态。
|
||
|
||
:param plugin_id: 插件ID
|
||
:return: 数据库插件状态和错误信息
|
||
"""
|
||
try:
|
||
return await self.dependencies.state_query_gateway.get_plugin_state(plugin_id), None
|
||
except Exception as exc:
|
||
logger.exception(f'读取数据库插件状态失败:{plugin_id},{exc}')
|
||
return None, str(exc)
|
||
|
||
async def load_database_plugin_states_with_error(self) -> tuple[list[PluginStateRecord], str | None]:
|
||
"""
|
||
读取数据库插件状态列表,并保留失败原因。
|
||
|
||
:return: 数据库插件状态列表和错误信息
|
||
"""
|
||
try:
|
||
return await self.dependencies.state_query_gateway.list_plugin_states(), None
|
||
except Exception as exc:
|
||
logger.exception(f'读取数据库插件状态列表失败:{exc}')
|
||
return [], str(exc)
|
||
|
||
async def load_database_plugin_states(self) -> list[PluginStateRecord]:
|
||
"""
|
||
读取数据库插件状态列表。
|
||
|
||
:return: 数据库插件状态列表
|
||
"""
|
||
database_plugins, _database_error = await self.load_database_plugin_states_with_error()
|
||
return database_plugins
|
||
|
||
def load_database_plugin_states_sync_with_error(self) -> tuple[list[PluginStateRecord], str | None]:
|
||
"""
|
||
以同步方式读取数据库插件状态列表,并保留失败原因。
|
||
|
||
:return: 数据库插件状态列表和错误信息
|
||
"""
|
||
if not self.has_plugin_dependencies():
|
||
return [], None
|
||
try:
|
||
asyncio.get_running_loop()
|
||
except RuntimeError:
|
||
return asyncio.run(self.load_database_plugin_states_with_error())
|
||
logger.warning(DATABASE_PLUGIN_STATE_SYNC_LOOP_ERROR)
|
||
return [], DATABASE_PLUGIN_STATE_SYNC_LOOP_ERROR
|
||
|
||
def load_database_plugin_states_sync(self) -> list[PluginStateRecord]:
|
||
"""
|
||
以同步方式读取数据库插件状态列表。
|
||
|
||
:return: 数据库插件状态列表
|
||
"""
|
||
database_plugins, _database_error = self.load_database_plugin_states_sync_with_error()
|
||
return database_plugins
|
||
|
||
def has_plugin_dependencies(self) -> bool:
|
||
"""
|
||
判断当前本地插件是否声明了插件间依赖。
|
||
|
||
:return: 是否存在插件间依赖声明
|
||
"""
|
||
backend_root = Path(self.dependencies.runtime_environment.get_backend_dir())
|
||
return any(plugin.manifest.dependencies.plugins for plugin in self.discover_plugins(backend_root))
|
||
|
||
def resolve_plugin_capability(self, discovered_plugin: DiscoveredPlugin) -> PluginRuntimeCapability:
|
||
"""
|
||
解析插件运行时操作能力。
|
||
|
||
:param discovered_plugin: 已发现插件
|
||
:return: 插件运行时能力
|
||
"""
|
||
return PluginRuntimeCapabilityResolver(
|
||
frontend_mode=self.dependencies.runtime_environment.get_frontend_mode(),
|
||
backend_runtime_mode=self.dependencies.runtime_environment.get_backend_runtime_mode(),
|
||
).resolve(discovered_plugin)
|
||
|
||
def with_plugin_capability(
|
||
self,
|
||
payload: dict[str, object],
|
||
discovered_plugin: DiscoveredPlugin | None,
|
||
) -> dict[str, object]:
|
||
"""
|
||
为运行时响应负载附加插件操作能力。
|
||
|
||
:param payload: 运行时响应负载
|
||
:param discovered_plugin: 已发现插件
|
||
:return: 附加能力后的响应负载
|
||
"""
|
||
if discovered_plugin:
|
||
payload['capability'] = self.resolve_plugin_capability(discovered_plugin).to_payload()
|
||
return payload
|
||
|
||
def build_operation_blocked_payload(
|
||
self,
|
||
discovered_plugin: DiscoveredPlugin,
|
||
operation: str,
|
||
*,
|
||
dry_run: bool | None = None,
|
||
) -> PluginRuntimeBlockedPayloadDict | None:
|
||
"""
|
||
构建运行模式阻断负载。
|
||
|
||
:param discovered_plugin: 已发现插件
|
||
:param operation: 操作类型
|
||
:param dry_run: 是否预演
|
||
:return: 阻断负载,不阻断时返回 None
|
||
"""
|
||
capability = self.resolve_plugin_capability(discovered_plugin)
|
||
if capability.allows(operation):
|
||
return None
|
||
return PluginRuntimeBlockedPayload(
|
||
ok=False,
|
||
status='blocked',
|
||
operation=operation,
|
||
plugin_id=discovered_plugin.manifest.id,
|
||
message='当前环境不允许执行该插件操作',
|
||
suggestion='请在开发模式或维护窗口中执行插件变更,并重启后端或重新构建前端。',
|
||
capability=capability.to_payload(),
|
||
dry_run=dry_run,
|
||
exit_code=1,
|
||
).to_payload(exclude_none=True)
|
||
|
||
async def check_inter_plugin_dependencies(
|
||
self,
|
||
discovered_plugin: DiscoveredPlugin,
|
||
discovered_plugins: list[DiscoveredPlugin],
|
||
) -> PluginDependencyCheckResult:
|
||
"""
|
||
检查插件间依赖。
|
||
|
||
:param discovered_plugin: 当前已发现插件
|
||
:param discovered_plugins: 全量已发现插件列表
|
||
:return: 插件间依赖检查结果
|
||
"""
|
||
if not discovered_plugin.manifest.dependencies.plugins:
|
||
return PluginDependencyCheckResult(plugin_id=discovered_plugin.manifest.id, items=[])
|
||
database_plugins = await self.load_database_plugin_states()
|
||
return InterPluginDependencyChecker(discovered_plugins, database_plugins).check_manifest(
|
||
discovered_plugin.manifest
|
||
)
|
||
|
||
async def check_enabled_plugin_dependents(
|
||
self,
|
||
plugin_id: str,
|
||
discovered_plugins: list[DiscoveredPlugin],
|
||
) -> PluginDependencyCheckResult:
|
||
"""
|
||
检查指定插件是否仍被已启用插件依赖。
|
||
|
||
:param plugin_id: 被停用或卸载的插件ID
|
||
:param discovered_plugins: 全量已发现插件列表
|
||
:return: 被依赖方检查结果
|
||
"""
|
||
has_direct_dependents = any(
|
||
dependency.id == plugin_id
|
||
for discovered_plugin in discovered_plugins
|
||
for dependency in discovered_plugin.manifest.dependencies.plugins
|
||
)
|
||
if not has_direct_dependents:
|
||
return PluginDependencyCheckResult(plugin_id=plugin_id, items=[])
|
||
|
||
database_plugins = await self.load_database_plugin_states()
|
||
return InterPluginDependencyChecker(discovered_plugins, database_plugins).check_enabled_dependents(plugin_id)
|
||
|
||
async def build_precheck_context(
|
||
self,
|
||
backend_root: Path,
|
||
discovered_plugin: DiscoveredPlugin,
|
||
discovered_plugins: list[DiscoveredPlugin],
|
||
) -> PluginPrecheckContext:
|
||
"""
|
||
构建插件操作预检上下文。
|
||
|
||
:param backend_root: 后端项目根目录
|
||
:param discovered_plugin: 当前插件
|
||
:param discovered_plugins: 已发现插件列表
|
||
:return: 插件操作预检上下文
|
||
"""
|
||
frontend_root = Path(self.dependencies.runtime_environment.get_frontend_dir())
|
||
frontend_plugins_root = Path(self.dependencies.runtime_environment.get_frontend_plugins_dir())
|
||
dependency_result = self.dependencies.dependency_checker.check_manifest(discovered_plugin.manifest)
|
||
manifest_result = PluginManifestChecker(backend_root=backend_root, frontend_root=frontend_root).check(
|
||
discovered_plugin.manifest
|
||
)
|
||
manifest_result = await self._check_lifecycle_scripts(discovered_plugin, manifest_result)
|
||
plugin_dependency_result = await self.check_inter_plugin_dependencies(discovered_plugin, discovered_plugins)
|
||
structure_result = PluginStructureChecker(backend_root, frontend_plugins_root).check(discovered_plugin)
|
||
menu_conflict_result = PluginMenuConflictChecker().check(discovered_plugin, discovered_plugins)
|
||
|
||
return PluginPrecheckContext.build(
|
||
dependency_result,
|
||
manifest_result,
|
||
plugin_dependency_result,
|
||
structure_result,
|
||
menu_conflict_result,
|
||
)
|
||
|
||
async def _check_lifecycle_scripts(
|
||
self,
|
||
discovered_plugin: DiscoveredPlugin,
|
||
manifest_result: PluginManifestCheckResult,
|
||
) -> PluginManifestCheckResult:
|
||
"""
|
||
检查 migration 历史和 seed 执行计划,并合并到 manifest 预检结果。
|
||
|
||
:param discovered_plugin: 当前插件
|
||
:param manifest_result: 原始 manifest 检查结果
|
||
:return: 合并生命周期脚本预检后的 manifest 检查结果
|
||
"""
|
||
if not discovered_plugin.manifest.backend.migrations and not discovered_plugin.manifest.backend.seeds:
|
||
return manifest_result
|
||
try:
|
||
migration_runner = PluginMigrationRunner(
|
||
discovered_plugin,
|
||
PluginMigrationHistoryGatewayStore(self.dependencies.migration_history_gateway),
|
||
)
|
||
script_result = await PluginLifecycleScriptPrechecker(discovered_plugin, migration_runner).check(object())
|
||
except Exception as exc:
|
||
logger.exception(f'插件生命周期脚本预检失败:{discovered_plugin.manifest.id},{exc}')
|
||
return PluginManifestCheckResult(
|
||
plugin_id=manifest_result.plugin_id,
|
||
issues=[
|
||
*manifest_result.issues,
|
||
PluginRuntimeContextService._build_lifecycle_script_precheck_issue(exc),
|
||
],
|
||
)
|
||
|
||
return PluginManifestCheckResult(
|
||
plugin_id=manifest_result.plugin_id,
|
||
issues=[*manifest_result.issues, *script_result.issues],
|
||
)
|
||
|
||
@staticmethod
|
||
def _build_lifecycle_script_precheck_issue(exc: Exception) -> PluginValidationIssue:
|
||
"""
|
||
构建生命周期脚本预检异常问题。
|
||
|
||
:param exc: 原始异常
|
||
:return: 统一校验问题
|
||
"""
|
||
return PluginValidationIssue(
|
||
level='error',
|
||
category='lifecycle',
|
||
kind='lifecycle_script_precheck_failed',
|
||
path='backend',
|
||
message=f'插件生命周期脚本预检失败:{exc}',
|
||
suggestion='请检查 migration/seed 声明和脚本文件',
|
||
)
|
||
|
||
def discover_plugins(self, backend_root: Path) -> list[DiscoveredPlugin]:
|
||
"""
|
||
发现本地插件。
|
||
|
||
使用容错扫描,单个损坏插件不会影响其他插件。损坏插件的错误明细可通过
|
||
:meth:`get_discovery_errors` 获取。
|
||
|
||
:param backend_root: 后端项目根目录
|
||
:return: 已发现插件列表
|
||
"""
|
||
return self.discover_plugins_with_errors(backend_root).plugins
|
||
|
||
def discover_plugins_with_errors(self, backend_root: Path) -> PluginDiscoveryResult:
|
||
"""
|
||
发现本地插件并返回错误明细。
|
||
|
||
:param backend_root: 后端项目根目录
|
||
:return: 插件发现结果
|
||
"""
|
||
resolved_backend_root = backend_root.resolve()
|
||
cached_entry = self._discovered_plugins_cache.get(resolved_backend_root)
|
||
if cached_entry is not None:
|
||
cached_at, cached_plugins = cached_entry
|
||
if monotonic() - cached_at <= PLUGIN_DISCOVERY_CACHE_TTL_SECONDS:
|
||
cached_errors = self._discovery_errors_cache.get(resolved_backend_root, [])
|
||
return PluginDiscoveryResult(plugins=list(cached_plugins), errors=list(cached_errors))
|
||
|
||
discovery_result = PluginScanner(resolved_backend_root / 'plugins').discover_with_errors()
|
||
self._discovered_plugins_cache[resolved_backend_root] = (monotonic(), discovery_result.plugins)
|
||
self._discovery_errors_cache[resolved_backend_root] = list(discovery_result.errors)
|
||
for error in discovery_result.errors:
|
||
logger.warning(f'插件扫描失败,已隔离损坏插件:目录={error.plugin_dir},错误:{error.error_message}')
|
||
return discovery_result
|
||
|
||
def get_discovery_errors(self, backend_root: Path) -> list[PluginDiscoveryError]:
|
||
"""
|
||
获取本地插件扫描错误明细。
|
||
|
||
:param backend_root: 后端项目根目录
|
||
:return: 扫描错误明细列表
|
||
"""
|
||
self.discover_plugins_with_errors(backend_root)
|
||
return list(self._discovery_errors_cache.get(backend_root.resolve(), []))
|