mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-10-16 05:45:32 +00:00
* feat: 支持 v2 插件、原生扩展与签名发布 * fix: 修复测试 * feat: 增强宿主 SDK、运行观测与制品维护能力 * feat: 增强文件传输、配置观测与本地开发能力 * feat: 支持 SSE 实时事件流并完善开发脚手架 * feat: 完善长连接鉴权复核与关闭回收 * feat: 增强生命周期与任务关闭可靠性 * feat: 收紧健康检查契约并补齐前端与真实网络验收 * feat: 增加SDK溯源与离线更新工具并验证跨版本兼容 * feat: 增加持久化业务示例与受控 Python 交付 * feat: 增加统一运行诊断与多worker状态观测 * feat: 增加固定目标发布等待与自动验收 * fix: 修复多环境插件部署前缀与代理配置不一致 * fix: 修复测试
189 lines
7.5 KiB
Python
189 lines
7.5 KiB
Python
import asyncio
|
|
from collections.abc import AsyncIterator, Awaitable, Callable, Mapping
|
|
from contextlib import asynccontextmanager
|
|
from dataclasses import dataclass, field, replace
|
|
from pathlib import Path
|
|
from types import MappingProxyType
|
|
from typing import Any
|
|
|
|
from plugins.core.sdk.config import PluginConfigSnapshot
|
|
from plugins.core.sdk.request import PluginHttpRequest
|
|
from plugins.core.sdk.version import HOST_API_VERSION
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PluginHostContext:
|
|
"""
|
|
宿主注入的能力集合;不保存当前用户或请求数据库会话。
|
|
|
|
services 只包含宿主显式登记的接口,不提供通过字符串导入任意服务的执行器。
|
|
具体服务仍负责权限和事务规则;该对象不是针对不可信代码的沙箱。
|
|
|
|
:param plugin_id: 插件 ID
|
|
:param resource_root: 插件资源根目录
|
|
:param config: 当前插件的只读配置快照
|
|
:param services: 宿主显式注册的只读服务映射
|
|
:param session_factory: 由宿主提供的数据库会话工厂
|
|
:param redis: 宿主共享的 Redis 客户端
|
|
:param logger: 绑定插件标识的日志对象
|
|
:param startup_write_enabled: 当前实例是否允许执行启动期全局写入
|
|
:param api_version: 宿主插件 API 版本
|
|
:param config_revision: 启动配置快照的不透明版本,手动构造上下文时可为空
|
|
:param config_reader: 宿主绑定的当前插件配置读取器
|
|
"""
|
|
|
|
plugin_id: str
|
|
resource_root: Path
|
|
config: Mapping[str, Any] = field(default_factory=dict, repr=False)
|
|
services: Mapping[str, Any] = field(default_factory=dict)
|
|
session_factory: Callable[..., Any] | None = None
|
|
redis: Any = None
|
|
logger: Any = None
|
|
startup_write_enabled: bool = False
|
|
api_version: str = HOST_API_VERSION
|
|
config_revision: str | None = None
|
|
config_reader: Callable[[], Awaitable[PluginConfigSnapshot]] | None = field(default=None, repr=False, compare=False)
|
|
|
|
def __post_init__(self) -> None:
|
|
"""
|
|
规范化资源根目录,并将配置和服务映射转换为只读快照。
|
|
|
|
:return: None
|
|
"""
|
|
object.__setattr__(self, 'resource_root', self.resource_root.resolve())
|
|
object.__setattr__(self, 'config', MappingProxyType(dict(self.config)))
|
|
object.__setattr__(self, 'services', MappingProxyType(dict(self.services)))
|
|
|
|
async def read_config(self) -> PluginConfigSnapshot:
|
|
"""
|
|
使用独立会话读取当前插件最新配置,不替换启动快照或自动重建业务资源。
|
|
|
|
:return: 本次读取的配置明文和版本;敏感值不得直接返回浏览器
|
|
:raises RuntimeError: 当前上下文未提供按需配置读取能力
|
|
"""
|
|
if self.config_reader is None:
|
|
raise RuntimeError('宿主未提供按需配置读取能力')
|
|
return await self.config_reader()
|
|
|
|
def resource(self, relative_path: str) -> Path:
|
|
"""
|
|
解析自身资源;读取前再次检查真实路径以拒绝链接逃逸。
|
|
|
|
:param relative_path: 相对于当前插件资源根目录的路径
|
|
:return: 通过插件目录边界校验的资源绝对路径
|
|
"""
|
|
path = (self.resource_root / relative_path).resolve()
|
|
if not path.is_relative_to(self.resource_root):
|
|
raise ValueError('插件资源路径不能越过插件根目录')
|
|
return path
|
|
|
|
def service(self, name: str) -> Any:
|
|
"""
|
|
获取宿主显式注册的服务能力。
|
|
|
|
:param name: 宿主注册的版本化服务名称
|
|
:return: 对应的宿主服务能力对象
|
|
"""
|
|
if name not in self.services:
|
|
raise LookupError(f'宿主未提供插件服务:{name}')
|
|
return self.services[name]
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PluginRequestContext:
|
|
"""
|
|
由宿主鉴权层建立的请求身份,不能由客户端字段直接构造。
|
|
|
|
:param host: 当前插件的宿主能力上下文
|
|
:param user: 宿主登录服务返回的当前用户
|
|
:param permissions: 当前用户的接口权限集合
|
|
:param request_id: 宿主提供的请求关联标识
|
|
:param query_db: 显式事务块内有效的数据库会话,不得跨任务共享
|
|
:param request: SDK 接口适配器提供的 HTTP 请求数据快照
|
|
:param _transaction: 宿主内部的会话所有权记录,业务代码不应自行构造
|
|
"""
|
|
|
|
host: PluginHostContext
|
|
user: Any
|
|
permissions: frozenset[str] = frozenset()
|
|
request_id: str | None = None
|
|
query_db: Any = None
|
|
request: PluginHttpRequest | None = None
|
|
_transaction: '_PluginTransactionState | None' = field(default=None, repr=False, compare=False)
|
|
|
|
def require_permission(self, permission: str) -> None:
|
|
"""
|
|
执行接口权限检查;数据权限由具体服务按当前用户处理。
|
|
|
|
:param permission: 当前操作要求的接口权限标识
|
|
:return: None
|
|
"""
|
|
if permission not in self.permissions and '*:*:*' not in self.permissions:
|
|
raise PermissionError(f'缺少权限:{permission}')
|
|
|
|
@asynccontextmanager
|
|
async def transaction(self) -> AsyncIterator['PluginRequestContext']:
|
|
"""
|
|
为当前任务创建显式事务,成功提交、异常或取消回滚,并关闭独占会话。
|
|
|
|
:return: 仅在当前事务块和当前 asyncio 任务内有效的请求上下文
|
|
:raises RuntimeError: 嵌套事务、缺少会话工厂或在失效上下文上重新开启事务
|
|
"""
|
|
if self.query_db is not None or self._transaction is not None:
|
|
raise RuntimeError('插件请求事务不能嵌套或复用,请使用原始请求上下文')
|
|
if self.host.session_factory is None:
|
|
raise RuntimeError('宿主未提供数据库会话工厂')
|
|
async with self.host.session_factory() as db:
|
|
state = _PluginTransactionState(db, asyncio.current_task())
|
|
try:
|
|
async with db.begin():
|
|
yield replace(self, query_db=db, _transaction=state)
|
|
finally:
|
|
state.active = False
|
|
|
|
def transaction_session(self) -> Any:
|
|
"""
|
|
为宿主服务取得当前任务拥有的事务会话,拒绝过期、伪造或跨任务复用。
|
|
|
|
:return: 当前有效事务的会话;未开启事务时为 None
|
|
:raises RuntimeError: 事务所有权或有效期不符合要求
|
|
"""
|
|
state = self._transaction
|
|
if state is None and self.query_db is None:
|
|
return None
|
|
if state is None or not state.active or state.session is not self.query_db:
|
|
raise RuntimeError('插件请求事务上下文已失效或不是由宿主创建')
|
|
if state.owner is not asyncio.current_task():
|
|
raise RuntimeError('插件请求事务会话不能跨 asyncio 任务共享')
|
|
return state.session
|
|
|
|
|
|
@dataclass
|
|
class _PluginTransactionState:
|
|
"""
|
|
记录显式事务的会话、所属任务和有效期。
|
|
|
|
:param session: 当前事务独占的数据库会话
|
|
:param owner: 开启事务的 asyncio 任务
|
|
:param active: 事务块是否仍有效
|
|
"""
|
|
|
|
session: Any
|
|
owner: asyncio.Task[Any] | None
|
|
active: bool = True
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class PluginTaskContext:
|
|
"""
|
|
后台任务身份;不隐式授予请求用户或管理员权限。
|
|
|
|
:param host: 当前插件的宿主能力上下文
|
|
:param job_id: 插件清单声明的任务 ID
|
|
:param request_id: 本次任务执行的独立追踪标识
|
|
"""
|
|
|
|
host: PluginHostContext
|
|
job_id: str
|
|
request_id: str | None = None
|