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

212 lines
7.5 KiB
Python

from dataclasses import dataclass
from typing import Literal
from plugins.core.validation.versioning import PluginVersionComparator
PluginStatus = Literal['discovered', 'installed', 'pending_upgrade', 'error']
PluginStateOperation = Literal[
'discover',
'install',
'disable',
'enable',
'upgrade_available',
'upgrade',
'mark_error',
]
@dataclass(frozen=True)
class PluginStateTransition:
"""
插件状态流转规则。
:param source: 来源状态,None 表示数据库中尚无插件状态
:param operation: 状态流转操作
:param target: 目标状态
:param description: 流转说明
"""
source: PluginStatus | None
operation: PluginStateOperation
target: PluginStatus
description: str
@dataclass(frozen=True)
class PluginStateSnapshot:
"""
插件状态输入快照。
:param source_version: 当前源码版本
:param installed_version: 已安装版本
:param enabled: 是否启用
:param current_status: 当前持久化状态
"""
source_version: str | None
installed_version: str | None
enabled: bool
current_status: str | None = None
class PluginStateResolver:
"""
插件状态解析器。
使用 State Resolver 模式集中维护插件管理和运行时注册表共享的状态流转规则。
"""
@classmethod
def resolve(cls, snapshot: PluginStateSnapshot) -> PluginStatus:
"""
根据源码版本、安装版本、启停状态和当前状态解析插件状态。
:param snapshot: 插件状态输入快照
:return: 插件状态
"""
if snapshot.current_status == 'error':
return 'error'
if not snapshot.installed_version and snapshot.current_status in {None, 'discovered'}:
return 'discovered'
if cls._needs_upgrade(snapshot.installed_version, snapshot.source_version):
return 'pending_upgrade'
return 'installed' if snapshot.installed_version else 'discovered'
@staticmethod
def is_enabled(database_plugin: object | None) -> bool:
"""
解析插件启用状态。
:param database_plugin: 数据库插件状态对象
:return: 是否启用
"""
if not database_plugin:
return False
if getattr(database_plugin, 'status', None) == 'error':
return False
if not getattr(database_plugin, 'installed_version', None):
return False
database_enabled = getattr(database_plugin, 'enabled', None) if database_plugin else None
if database_enabled is not None:
return database_enabled == '0'
return False
@staticmethod
def enabled_to_db_value(enabled: bool) -> str:
"""
将布尔启停状态转换为数据库枚举值。
:param enabled: 是否启用
:return: 数据库启停枚举值,`0` 表示启用,`1` 表示停用
"""
return '0' if enabled else '1'
@staticmethod
def db_value_to_enabled(enabled: str | None, fallback: bool) -> bool:
"""
将数据库启停枚举值转换为布尔值。
:param enabled: 数据库启停枚举值
:param fallback: 数据库值为空时使用的默认值
:return: 是否启用
"""
if enabled is None:
return fallback
return enabled == '0'
@classmethod
def is_database_plugin_enabled(cls, database_plugin: object | None, fallback: bool = False) -> bool:
"""
判断数据库插件状态是否表示可用启用态。
:param database_plugin: 数据库插件状态对象
:param fallback: 数据库对象或启停值为空时使用的默认值
:return: 是否为可用启用态
"""
if not database_plugin:
return fallback
if getattr(database_plugin, 'status', None) == 'error':
return False
return cls.db_value_to_enabled(getattr(database_plugin, 'enabled', None), fallback=fallback)
@staticmethod
def _needs_upgrade(installed_version: str | None, source_version: str | None) -> bool:
"""
判断源码版本是否高于已安装版本。
:param installed_version: 已安装版本
:param source_version: 当前源码版本
:return: 是否需要升级
"""
return bool(
installed_version
and source_version
and PluginVersionComparator.is_upgrade_available(installed_version, source_version)
)
class PluginStateTransitionTable:
"""
插件状态流转表。
使用 Table Driven 模式集中维护可观察的插件状态流转,供文档、测试和后续预检复用。
"""
_TRANSITIONS = (
PluginStateTransition(None, 'discover', 'discovered', '首次扫描到本地插件'),
PluginStateTransition('discovered', 'install', 'installed', '插件安装完成'),
PluginStateTransition('discovered', 'disable', 'discovered', '未安装插件被显式停用'),
PluginStateTransition('discovered', 'mark_error', 'error', '插件安装或启动失败'),
PluginStateTransition('installed', 'disable', 'installed', '插件被停用'),
PluginStateTransition('installed', 'enable', 'installed', '插件被启用'),
PluginStateTransition('installed', 'upgrade_available', 'pending_upgrade', '发现源码版本高于已安装版本'),
PluginStateTransition('installed', 'mark_error', 'error', '插件运行或操作失败'),
PluginStateTransition('pending_upgrade', 'upgrade', 'installed', '插件升级完成'),
PluginStateTransition('pending_upgrade', 'disable', 'pending_upgrade', '待升级插件被停用'),
PluginStateTransition('pending_upgrade', 'enable', 'pending_upgrade', '待升级插件被启用'),
PluginStateTransition('pending_upgrade', 'mark_error', 'error', '插件升级或运行失败'),
PluginStateTransition('error', 'disable', 'error', '异常插件被显式停用'),
PluginStateTransition('error', 'install', 'installed', '异常插件重新安装成功'),
PluginStateTransition('error', 'upgrade', 'installed', '异常插件升级修复完成'),
PluginStateTransition('error', 'mark_error', 'error', '异常插件再次记录失败信息'),
)
@classmethod
def list_transitions(cls) -> list[PluginStateTransition]:
"""
获取全部插件状态流转规则。
:return: 插件状态流转规则列表
"""
return list(cls._TRANSITIONS)
@classmethod
def can_transition(cls, source: PluginStatus | None, operation: PluginStateOperation) -> bool:
"""
判断状态操作是否允许执行。
:param source: 来源状态,None 表示数据库中尚无插件状态
:param operation: 状态流转操作
:return: 是否允许流转
"""
return cls.resolve_target(source, operation) is not None
@classmethod
def resolve_target(
cls,
source: PluginStatus | None,
operation: PluginStateOperation,
) -> PluginStatus | None:
"""
解析状态操作的目标状态。
:param source: 来源状态,None 表示数据库中尚无插件状态
:param operation: 状态流转操作
:return: 目标状态,不允许流转时返回 None
"""
for transition in cls._TRANSITIONS:
if transition.source == source and transition.operation == operation:
return transition.target
return None