mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-10-02 00:27:17 +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: 更新后端依赖文件
147 lines
5.0 KiB
Python
147 lines
5.0 KiB
Python
import importlib
|
|
import importlib.util
|
|
import sys
|
|
from collections.abc import Callable
|
|
from dataclasses import dataclass
|
|
from pathlib import Path
|
|
from types import ModuleType
|
|
|
|
from plugins.core.discovery.scanner import DiscoveredPlugin
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class LoadedPluginCallable:
|
|
"""
|
|
已加载的插件 callable。
|
|
|
|
:param module_name: 完整 Python 模块名
|
|
:param callable_name: callable 名称
|
|
:param callable_object: callable 对象
|
|
"""
|
|
|
|
module_name: str
|
|
callable_name: str
|
|
callable_object: Callable[..., object]
|
|
|
|
|
|
class PluginCallableLoader:
|
|
"""
|
|
插件 callable 加载器。
|
|
|
|
使用 Loader 模式统一插件生命周期钩子、健康检查等 manifest callable 的模块边界校验、
|
|
本地文件加载和 Python import fallback。
|
|
"""
|
|
|
|
def __init__(self, discovered_plugin: DiscoveredPlugin, *, label: str) -> None:
|
|
"""
|
|
初始化插件 callable 加载器。
|
|
|
|
:param discovered_plugin: 已发现插件对象
|
|
:param label: callable 类型标签,用于错误信息
|
|
:return: None
|
|
"""
|
|
self.discovered_plugin = discovered_plugin
|
|
self.label = label
|
|
|
|
def load(self, callable_path: str) -> LoadedPluginCallable:
|
|
"""
|
|
加载插件 callable。
|
|
|
|
:param callable_path: manifest 中声明的 callable 路径,格式为 <module_path>:<callable_name>
|
|
:return: 已加载 callable
|
|
"""
|
|
module_path, callable_name = callable_path.split(':', maxsplit=1)
|
|
module_name = self.resolve_module_name(module_path)
|
|
module = self.import_module(module_name)
|
|
callable_object = getattr(module, callable_name, None)
|
|
if not callable(callable_object):
|
|
raise RuntimeError(f'插件 {self.label} 不存在或不可调用:{callable_path}')
|
|
|
|
return LoadedPluginCallable(
|
|
module_name=module_name,
|
|
callable_name=callable_name,
|
|
callable_object=callable_object,
|
|
)
|
|
|
|
def resolve_module_name(self, module_path: str) -> str:
|
|
"""
|
|
解析插件 callable 模块名。
|
|
|
|
:param module_path: manifest 中声明的模块路径
|
|
:return: 完整 Python 模块名
|
|
"""
|
|
plugin_module = self.discovered_plugin.manifest.backend.module
|
|
if module_path == plugin_module or module_path.startswith(f'{plugin_module}.'):
|
|
return module_path
|
|
if module_path.startswith('plugins.'):
|
|
raise RuntimeError(f'{self.label} 只能指向当前插件模块:{module_path}')
|
|
|
|
return f'{plugin_module}.{module_path}'
|
|
|
|
def import_module(self, module_name: str) -> ModuleType:
|
|
"""
|
|
导入插件 callable 模块。
|
|
|
|
:param module_name: 完整 Python 模块名
|
|
:return: Python 模块对象
|
|
"""
|
|
module_file = self.resolve_module_file(module_name)
|
|
if module_file:
|
|
return self.load_module_from_file(module_name, module_file)
|
|
|
|
backend_root = self.resolve_backend_root()
|
|
backend_root_text = str(backend_root)
|
|
path_inserted = backend_root_text not in sys.path
|
|
if path_inserted:
|
|
sys.path.insert(0, backend_root_text)
|
|
try:
|
|
return importlib.import_module(module_name)
|
|
finally:
|
|
if path_inserted:
|
|
sys.path.remove(backend_root_text)
|
|
|
|
def resolve_module_file(self, module_name: str) -> Path | None:
|
|
"""
|
|
解析当前插件 callable 模块文件。
|
|
|
|
:param module_name: 完整 Python 模块名
|
|
:return: 模块文件路径
|
|
"""
|
|
plugin_module = self.discovered_plugin.manifest.backend.module
|
|
if module_name != plugin_module and not module_name.startswith(f'{plugin_module}.'):
|
|
return None
|
|
|
|
relative_module = module_name.removeprefix(plugin_module).lstrip('.')
|
|
if not relative_module:
|
|
module_file = self.discovered_plugin.backend_path / '__init__.py'
|
|
else:
|
|
module_file = self.discovered_plugin.backend_path.joinpath(*relative_module.split('.')).with_suffix('.py')
|
|
|
|
return module_file if module_file.is_file() else None
|
|
|
|
@staticmethod
|
|
def load_module_from_file(module_name: str, module_file: Path) -> ModuleType:
|
|
"""
|
|
从文件加载插件 callable 模块。
|
|
|
|
:param module_name: 完整 Python 模块名
|
|
:param module_file: 模块文件路径
|
|
:return: Python 模块对象
|
|
"""
|
|
module_spec = importlib.util.spec_from_file_location(module_name, module_file)
|
|
if module_spec is None or module_spec.loader is None:
|
|
raise RuntimeError(f'插件 callable 模块加载失败:{module_file}')
|
|
module = importlib.util.module_from_spec(module_spec)
|
|
sys.modules[module_name] = module
|
|
module_spec.loader.exec_module(module)
|
|
|
|
return module
|
|
|
|
def resolve_backend_root(self) -> Path:
|
|
"""
|
|
解析后端工程根目录。
|
|
|
|
:return: 后端工程根目录
|
|
"""
|
|
return self.discovered_plugin.backend_path.resolve().parents[1]
|