mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-10-15 13:25:45 +00:00
* feat: 支持 v2 插件、原生扩展与签名发布 * fix: 修复测试 * feat: 增强宿主 SDK、运行观测与制品维护能力 * feat: 增强文件传输、配置观测与本地开发能力 * feat: 支持 SSE 实时事件流并完善开发脚手架 * feat: 完善长连接鉴权复核与关闭回收 * feat: 增强生命周期与任务关闭可靠性 * feat: 收紧健康检查契约并补齐前端与真实网络验收 * feat: 增加SDK溯源与离线更新工具并验证跨版本兼容 * feat: 增加持久化业务示例与受控 Python 交付 * feat: 增加统一运行诊断与多worker状态观测 * feat: 增加固定目标发布等待与自动验收 * fix: 修复多环境插件部署前缀与代理配置不一致 * fix: 修复测试
500 lines
21 KiB
Python
500 lines
21 KiB
Python
import asyncio
|
||
from pathlib import Path
|
||
from time import monotonic
|
||
|
||
from plugins.core.capability import STATE_CHANGE_OPERATIONS, PluginRuntimeCapability, PluginRuntimeCapabilityResolver
|
||
from plugins.core.discovery.registry import PluginRegistry
|
||
from plugins.core.discovery.scanner import (
|
||
DiscoveredPlugin,
|
||
PluginDiscoveryError,
|
||
PluginDiscoveryResult,
|
||
PluginScanner,
|
||
is_artifact_plugin,
|
||
)
|
||
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 .gateway import UnavailablePluginStateQueryGateway
|
||
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]] = {}
|
||
self._discovery_override: PluginDiscoveryResult | None = None
|
||
|
||
def set_discovery_snapshot(self, plugins: list[DiscoveredPlugin]) -> None:
|
||
"""
|
||
维护流程注入已验证目录,不修改全局扫描器或运行中进程的发现缓存。
|
||
|
||
:param plugins: 维护流程已验证的插件发现快照
|
||
:return: None
|
||
"""
|
||
ids = [plugin.manifest.id for plugin in plugins]
|
||
if len(ids) != len(set(ids)):
|
||
raise ValueError('插件发现快照包含重复 ID')
|
||
self._discovery_override = PluginDiscoveryResult(plugins=list(plugins))
|
||
|
||
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 guard_artifact_operation(
|
||
self, plugin_id: str, operation: str, *, dry_run: bool = False, check_capability: bool = True
|
||
) -> PluginRuntimeBlockedPayloadDict | None:
|
||
"""
|
||
数据库来源是最终依据,缺少源码目录不能授权清理已发布制品。
|
||
|
||
:param plugin_id: 待操作的插件 ID
|
||
:param operation: 待执行的生命周期操作名称
|
||
:param dry_run: 是否仅预览操作
|
||
:param check_capability: 是否同时检查已发现插件的运行时管理能力
|
||
:return: 制品操作拦截结果,允许操作时返回 None
|
||
"""
|
||
if self.dependencies.runtime_environment.get_backend_runtime_mode() == 'maintenance':
|
||
return None
|
||
discovered = self.get_discovered_plugin(plugin_id)
|
||
if discovered is not None and check_capability:
|
||
blocked = self.build_operation_blocked_payload(discovered, operation, dry_run=dry_run)
|
||
if blocked:
|
||
return blocked
|
||
if dry_run and isinstance(self.dependencies.state_query_gateway, UnavailablePluginStateQueryGateway):
|
||
return None
|
||
plugin, error = await self.load_database_plugin_state(plugin_id)
|
||
artifact = getattr(plugin, 'source', None) == 'artifact' or is_artifact_plugin(discovered)
|
||
if not artifact and not error:
|
||
return None
|
||
message = (
|
||
'签名制品插件只能通过维护发布命令变更,完成后重启全部worker'
|
||
if artifact
|
||
else '无法确认插件来源,已阻止生命周期变更,请恢复插件状态查询后重试'
|
||
)
|
||
return self._artifact_operation_blocked_payload(plugin_id, operation, message, dry_run=dry_run)
|
||
|
||
def build_dependency_install_event_loop_blocked_payload(self, plugin_id: str) -> PluginRuntimeBlockedPayloadDict:
|
||
"""
|
||
已有事件循环时,同步依赖安装不能另建循环查询同一个数据库连接池。
|
||
|
||
:param plugin_id: 待安装依赖的插件 ID
|
||
:return: 阻止在已有事件循环内同步安装依赖的结果载荷
|
||
"""
|
||
return self._artifact_operation_blocked_payload(
|
||
plugin_id,
|
||
'dependency_install',
|
||
'当前事件循环不能同步确认插件来源,请使用维护CLI执行依赖安装',
|
||
)
|
||
|
||
@staticmethod
|
||
def _artifact_operation_blocked_payload(
|
||
plugin_id: str, operation: str, message: str, *, dry_run: bool = False
|
||
) -> PluginRuntimeBlockedPayloadDict:
|
||
"""
|
||
构建制品生命周期操作的统一拦截结果。
|
||
|
||
:param plugin_id: 待操作的插件 ID
|
||
:param operation: 被拦截的操作名称
|
||
:param message: 当前操作的拦截原因
|
||
:param dry_run: 是否仅预览操作
|
||
:return: 包含操作能力及维护建议的拦截结果载荷
|
||
"""
|
||
return PluginRuntimeBlockedPayload(
|
||
ok=False,
|
||
status='blocked',
|
||
operation=operation,
|
||
plugin_id=plugin_id,
|
||
message=message,
|
||
suggestion='请使用 plugin release 命令,并在执行前停止全部宿主worker。',
|
||
capability={
|
||
'pluginId': plugin_id,
|
||
'backendRuntimeManageable': False,
|
||
'runtimeManageable': False,
|
||
'blockedOperations': sorted(STATE_CHANGE_OPERATIONS),
|
||
'warnings': [message],
|
||
'primaryReason': message,
|
||
},
|
||
dry_run=dry_run,
|
||
exit_code=1,
|
||
).to_payload(exclude_none=True)
|
||
|
||
async def get_readonly_discovered_plugin(self, plugin_id: str) -> DiscoveredPlugin | None:
|
||
"""
|
||
配置读取可解析已选且重新验签的制品清单,不执行任何入口。
|
||
|
||
:param plugin_id: 待读取配置的插件 ID
|
||
:return: 已发现源码插件或重新验签的当前制品,未找到时返回 None
|
||
"""
|
||
if isinstance(self.dependencies.state_query_gateway, UnavailablePluginStateQueryGateway):
|
||
return self.get_discovered_plugin(plugin_id)
|
||
plugin, error = await self.load_database_plugin_state(plugin_id)
|
||
if error:
|
||
raise ValueError('无法确认插件来源,配置读取已中止')
|
||
if getattr(plugin, 'source', None) != 'artifact':
|
||
return self.get_discovered_plugin(plugin_id)
|
||
resolver = getattr(self.dependencies.state_query_gateway, 'get_selected_artifact_plugin', None)
|
||
if not callable(resolver):
|
||
raise ValueError('插件状态适配器未提供已选制品的只读解析能力')
|
||
return await resolver(plugin_id, Path(self.dependencies.runtime_environment.get_backend_dir()))
|
||
|
||
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: 插件发现结果
|
||
"""
|
||
if self._discovery_override is not None:
|
||
return PluginDiscoveryResult(plugins=list(self._discovery_override.plugins))
|
||
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(), []))
|