Files
RuoYi-Vue3-FastAPI/ruoyi-fastapi-backend/plugins/core/runtime/service/batch.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

399 lines
14 KiB
Python

from __future__ import annotations
from pathlib import Path
from typing import TYPE_CHECKING, Protocol, cast
from plugins.core.runtime.support import (
BatchOperationResultPayload,
PluginBatchReportBuilder,
PluginPayloadBuilder,
PluginRuntimePayloadBuilder,
)
from plugins.core.validation.plugin_deps import PluginBatchOperation, PluginDependencyPlanBuilder
if TYPE_CHECKING:
from collections.abc import Mapping
from plugins.core.capability import PluginRuntimeCapability
from plugins.core.discovery.scanner import DiscoveredPlugin
from plugins.core.types import PluginStateRecord
from .context import PluginRuntimeContextService
from .dependency_container import PluginRuntimeDependencies
from .responses import PluginBatchResponse, PluginLifecycleResponse, PluginPlanResponse
class PluginBatchRuntimeOperations(Protocol):
"""
批量工作流所需的运行时协作能力。
"""
async def install_plugin(
self,
plugin_id: str,
*,
dry_run: bool = False,
record_operation_log: bool = True,
) -> PluginLifecycleResponse:
"""
安装插件。
:param plugin_id: 插件ID
:param dry_run: 是否仅预演
:param record_operation_log: 是否记录操作日志
:return: 插件安装负载
"""
async def set_plugin_enabled(
self,
plugin_id: str,
*,
enabled: bool,
dry_run: bool = False,
record_operation_log: bool = True,
) -> PluginLifecycleResponse:
"""
设置插件启用状态。
:param plugin_id: 插件ID
:param enabled: 是否启用
:param dry_run: 是否仅预演
:param record_operation_log: 是否记录操作日志
:return: 插件启停负载
"""
async def upgrade_plugin(
self,
plugin_id: str,
*,
dry_run: bool = False,
record_operation_log: bool = True,
) -> PluginLifecycleResponse:
"""
升级插件。
:param plugin_id: 插件ID
:param dry_run: 是否仅预演
:param record_operation_log: 是否记录操作日志
:return: 插件升级负载
"""
async def record_plugin_operation_log(
self,
payload: Mapping[str, object],
*,
dry_run: bool,
continue_on_error: bool,
) -> None:
"""
记录插件操作审计日志。
:param payload: 插件操作结果负载
:param dry_run: 是否预演
:param continue_on_error: 失败后是否继续
:return: None
"""
async def execute_batch_plugin_item(
self,
operation: PluginBatchOperation,
plugin_id: str,
) -> BatchOperationResultPayload:
"""
执行单个批量插件操作项。
:param operation: 批量操作类型
:param plugin_id: 插件ID
:return: 单插件操作结果负载
"""
class PluginBatchUseCase:
"""
插件批量计划和批量执行 use case。
"""
def __init__(
self,
dependencies: PluginRuntimeDependencies,
runtime_operations: PluginBatchRuntimeOperations,
context: PluginRuntimeContextService,
) -> None:
"""
初始化插件批量 use case。
:param dependencies: 插件运行时依赖容器
:param runtime_operations: 批量工作流所需的运行时协作能力
:param context: 插件运行时上下文服务
"""
self.dependencies = dependencies
self.runtime_operations = runtime_operations
self.context = context
def _discover_plugins(self, backend_root: Path) -> list[DiscoveredPlugin]:
"""
发现本地插件。
:param backend_root: 后端项目根目录
:return: 已发现插件列表
"""
return self.context.discover_plugins(backend_root)
def _load_database_plugin_states_sync(self) -> list[PluginStateRecord]:
"""
以同步方式读取数据库插件状态列表。
:return: 数据库插件状态列表
"""
return self.context.load_database_plugin_states_sync()
def _load_database_plugin_states_sync_with_error(self) -> tuple[list[PluginStateRecord], str | None]:
"""
以同步方式读取数据库插件状态列表,并保留失败原因。
:return: 数据库插件状态列表和错误信息
"""
return self.context.load_database_plugin_states_sync_with_error()
async def _load_database_plugin_states_with_error(self) -> tuple[list[PluginStateRecord], str | None]:
"""
以异步方式读取数据库插件状态列表,并保留失败原因。
:return: 数据库插件状态列表和错误信息
"""
return await self.context.load_database_plugin_states_with_error()
def _resolve_plugin_capability(self, discovered_plugin: DiscoveredPlugin) -> PluginRuntimeCapability:
"""
解析插件运行时操作能力。
:param discovered_plugin: 已发现插件
:return: 插件运行时能力
"""
return self.context.resolve_plugin_capability(discovered_plugin)
def plan_plugins(
self,
operation: PluginBatchOperation,
plugin_ids: list[str] | None = None,
) -> PluginPlanResponse:
"""
生成插件批量操作拓扑计划。
.. note:: 本方法内部以同步方式读取数据库,禁止在异步上下文中调用,
异步场景请使用 :meth:`plan_plugins_async`。
:param operation: 批量操作类型
:param plugin_ids: 插件ID列表
:return: 插件批量操作拓扑计划负载
"""
try:
database_plugins, database_error = self._load_database_plugin_states_sync_with_error()
return self._build_plan_plugins_payload(operation, plugin_ids, database_plugins, database_error)
except Exception as exc:
return PluginRuntimePayloadBuilder.build_exception_payload('生成插件批量操作计划失败', exc)
async def plan_plugins_async(
self,
operation: PluginBatchOperation,
plugin_ids: list[str] | None = None,
) -> PluginPlanResponse:
"""
异步生成插件批量操作拓扑计划。
:param operation: 批量操作类型
:param plugin_ids: 插件ID列表
:return: 插件批量操作拓扑计划负载
"""
try:
database_plugins, database_error = await self._load_database_plugin_states_with_error()
return self._build_plan_plugins_payload(operation, plugin_ids, database_plugins, database_error)
except Exception as exc:
return PluginRuntimePayloadBuilder.build_exception_payload('生成插件批量操作计划失败', exc)
def _build_plan_plugins_payload(
self,
operation: PluginBatchOperation,
plugin_ids: list[str] | None,
database_plugins: list[PluginStateRecord],
database_error: str | None,
) -> PluginPlanResponse:
"""
构建插件批量操作拓扑计划负载。
:param operation: 批量操作类型
:param plugin_ids: 插件ID列表
:param database_plugins: 数据库插件状态列表
:param database_error: 数据库读取错误
:return: 插件批量操作拓扑计划负载
"""
try:
if operation not in {'install', 'enable', 'upgrade'}:
return PluginRuntimePayloadBuilder.build_invalid_operation_payload(
None,
operation,
message=f'插件计划操作不支持:{operation}',
)
backend_root = Path(self.dependencies.runtime_environment.get_backend_dir())
discovered_plugins = self._discover_plugins(backend_root)
plan = PluginDependencyPlanBuilder(discovered_plugins, database_plugins).build_plan(
operation,
plugin_ids,
)
payload = PluginPayloadBuilder.build_plan_payload(plan, database_error)
capability_blockers = self._collect_capability_blockers(
discovered_plugins,
operation,
plugin_ids or plan.requested_plugin_ids,
)
if capability_blockers:
payload['ok'] = False
payload['message'] = '插件批量操作计划存在环境阻断项'
payload['capabilityBlockers'] = capability_blockers
return cast('PluginPlanResponse', payload)
except Exception as exc:
return PluginRuntimePayloadBuilder.build_exception_payload('生成插件批量操作计划失败', exc)
def _collect_capability_blockers(
self,
discovered_plugins: list[DiscoveredPlugin],
operation: PluginBatchOperation,
requested_plugin_ids: list[str],
) -> list[dict[str, object]]:
"""
收集插件能力阻断项。
:param discovered_plugins: 已发现插件列表
:param operation: 批量操作类型
:param requested_plugin_ids: 请求的插件ID列表
:return: 能力阻断项列表
"""
blocked_operation = f'batch_{operation}'
target_plugin_ids = set(requested_plugin_ids)
blockers: list[dict[str, object]] = []
for discovered_plugin in discovered_plugins:
if target_plugin_ids and discovered_plugin.manifest.id not in target_plugin_ids:
continue
capability = self._resolve_plugin_capability(discovered_plugin)
if capability.allows(blocked_operation):
continue
blockers.append(
{
'pluginId': discovered_plugin.manifest.id,
'operation': blocked_operation,
'message': capability.primary_reason or '当前环境不允许执行该插件操作',
'capability': capability.to_payload(),
}
)
return blockers
async def batch_plugins(
self,
operation: PluginBatchOperation,
plugin_ids: list[str] | None = None,
*,
dry_run: bool = False,
continue_on_error: bool = False,
) -> PluginBatchResponse:
"""
批量执行插件安装、启用或升级。
执行前会先生成拓扑计划;当计划存在阻塞项时不会执行任何写操作。
:param operation: 批量操作类型
:param plugin_ids: 插件ID列表
:param dry_run: 是否仅预演
:param continue_on_error: 失败后是否继续执行后续插件
:return: 插件批量执行结果负载
"""
try:
plan_payload = cast('dict[str, object]', await self.plan_plugins_async(operation, plugin_ids))
if not plan_payload.get('ok', False):
plan_payload = PluginBatchReportBuilder.build_plan_blocked_payload(
plan_payload,
dry_run=dry_run,
continue_on_error=continue_on_error,
)
if not dry_run:
await self.runtime_operations.record_plugin_operation_log(
plan_payload,
dry_run=dry_run,
continue_on_error=continue_on_error,
)
return cast('PluginBatchResponse', plan_payload)
if dry_run:
return cast(
'PluginBatchResponse',
PluginBatchReportBuilder.build_dry_run_payload(
plan_payload,
continue_on_error=continue_on_error,
),
)
reports = []
failed = None
executable_plugin_ids = PluginBatchReportBuilder.resolve_executable_plugin_ids(plan_payload)
for plugin_id in executable_plugin_ids:
report, result = await PluginBatchReportBuilder.run_item(
operation,
plugin_id,
self.runtime_operations.execute_batch_plugin_item,
)
reports.append(report)
if not report.ok:
failed = failed or PluginBatchReportBuilder.build_failed_payload(report, result)
if not continue_on_error:
break
payload = PluginBatchReportBuilder.build_execution_payload(
plan_payload,
reports,
failed,
continue_on_error=continue_on_error,
)
await self.runtime_operations.record_plugin_operation_log(
payload,
dry_run=dry_run,
continue_on_error=continue_on_error,
)
return cast('PluginBatchResponse', payload)
except Exception as exc:
return PluginRuntimePayloadBuilder.build_exception_payload('插件批量操作失败', exc)
async def execute_batch_plugin_item(
self,
operation: PluginBatchOperation,
plugin_id: str,
) -> BatchOperationResultPayload:
"""
执行单个批量插件操作项。
:param operation: 批量操作类型
:param plugin_id: 插件ID
:return: 单插件操作结果负载
"""
if operation == 'install':
return cast(
'BatchOperationResultPayload',
await self.runtime_operations.install_plugin(plugin_id, dry_run=False, record_operation_log=False),
)
if operation == 'enable':
return cast(
'BatchOperationResultPayload',
await self.runtime_operations.set_plugin_enabled(
plugin_id,
enabled=True,
dry_run=False,
record_operation_log=False,
),
)
if operation == 'upgrade':
return cast(
'BatchOperationResultPayload',
await self.runtime_operations.upgrade_plugin(plugin_id, dry_run=False, record_operation_log=False),
)
return PluginRuntimePayloadBuilder.build_batch_item_unsupported_payload(operation, plugin_id)