mirror of
https://github.com/insistence/RuoYi-Vue3-FastAPI.git
synced 2026-10-14 21:07:36 +00:00
feat: 支持 v2 插件、独立前端和签名制品发布 (#130)
* feat: 支持 v2 插件、原生扩展与签名发布 * fix: 修复测试 * feat: 增强宿主 SDK、运行观测与制品维护能力 * feat: 增强文件传输、配置观测与本地开发能力 * feat: 支持 SSE 实时事件流并完善开发脚手架 * feat: 完善长连接鉴权复核与关闭回收 * feat: 增强生命周期与任务关闭可靠性 * feat: 收紧健康检查契约并补齐前端与真实网络验收 * feat: 增加SDK溯源与离线更新工具并验证跨版本兼容 * feat: 增加持久化业务示例与受控 Python 交付 * feat: 增加统一运行诊断与多worker状态观测 * feat: 增加固定目标发布等待与自动验收 * fix: 修复多环境插件部署前缀与代理配置不一致 * fix: 修复测试
This commit is contained in:
@@ -0,0 +1,33 @@
|
||||
# ASGI 子应用示例
|
||||
|
||||
本示例验证 v2 显式入口、子应用挂载、宿主 Bearer 认证、请求权限及 lifespan。它只提供 API;需要已接入的 PluginFrame、独立打包页面与浏览器会话桥时,参见 [bundle_demo](../bundle_demo/README.md)。
|
||||
|
||||
示例要求 Host API `^1.3.0`。`POST /apps/asgi_demo/api/echo/item-7?tag=first&tag=second` 使用相同的 `asgi_demo:view` 权限,接收 `{"message":"你好"}` JSON 请求体(消息长度 1–200),返回路径参数、多值查询参数、校验后的请求体及 `requestId`。实现通过 `plugin_endpoint(..., body_model=EchoBody)` 演示统一校验;认证方式与 `/api/info` 相同。
|
||||
|
||||
清单、入口与 SDK 约定见[插件开发手册](../../../../docs/plugin_development.md#510-v2-清单与能力组合),资源初始化和关闭方式见[ASGI 生命周期说明](../../../../docs/plugin_development.md#67-asgi-子应用与生命周期)。
|
||||
|
||||
以下命令在后端目录执行,适用于 Windows PowerShell、macOS 和 Linux。先按 [CLI 环境准备](../../../../docs/cli_usage.md#22-安装依赖)激活已安装宿主依赖的 Python 环境。
|
||||
|
||||
1. 将此目录复制到后端 `plugins/asgi_demo`。
|
||||
2. 在已激活的 Python 环境中执行 `ruoyi plugin check asgi_demo --env=dev`。
|
||||
3. 使用现有安装与启用命令执行 `ruoyi plugin install asgi_demo --env=dev --yes` 和 `ruoyi plugin enable asgi_demo --env=dev --yes`。
|
||||
4. 重启后端。按原有角色权限管理授予 `asgi_demo:view`;管理员具有通配权限。
|
||||
5. 请求 `GET /apps/asgi_demo/api/info`,携带当前宿主登录的 `Authorization: Bearer <token>`。若启用了传输加密,仍需遵守宿主加密协议。
|
||||
|
||||
入口函数快速返回 PluginDefinition,资源在 lifespan 中初始化和关闭。API 从 request.state.plugin_context 获取宿主鉴权后的身份,执行具体权限检查。数据库会话通过 host.session_factory 按请求创建,不在应用实例内保存请求会话。
|
||||
|
||||
服务模式仍执行既有生命周期写入限制;示例命令用于开发环境,不应将生产配置切到 dev 以绕过限制。生产变更使用维护部署流程。当前没有热挂载或热卸载。
|
||||
|
||||
## 签名制品交付
|
||||
|
||||
本例是 Python 源码插件,签名不改变源码交付方式。使用[开发手册第 15 节](../../../../docs/plugin_development.md#15-v2-签名制品与维护发布)配置宿主制品存储和外部发布者公钥,并授权 `asgi_demo`。发布者私钥必须在插件目录之外,以下路径需替换为实际的 Ed25519 PKCS8 PEM:
|
||||
|
||||
```bash
|
||||
ruoyi plugin artifact build plugins/examples/python/asgi_demo target/asgi_demo-1.0.0.rpk --key-file ../signing-keys/publisher.pem --key-id publisher --env=dev --output=json
|
||||
ruoyi plugin artifact verify target/asgi_demo-1.0.0.rpk --env=prod --output=json
|
||||
ruoyi plugin artifact import target/asgi_demo-1.0.0.rpk --env=prod --allow-prod --yes --output=json
|
||||
```
|
||||
|
||||
导入只验签、检查并登记不可变目录,不加载子应用、不运行迁移或 Hook。选择制品部署时不能同时保留 `plugins/asgi_demo` 源码目录;如已按上面的开发步骤安装过,需在维护窗口处理目录冲突,并为缺少制品准备证据的既有安装提升版本。
|
||||
|
||||
后续严格使用导入返回的 digest,停止全部 worker 后执行 `release prepare`,从最新状态读取 generation,再执行 `release select` 并重启。每个 worker 分别启动 lifespan,只有实际报告匹配目标并就绪才算发布完成。生产操作不支持热挂载/卸载;代码回滚也不降级数据库。完整命令与签名轮换、中断恢复方式均见开发手册。
|
||||
@@ -0,0 +1,79 @@
|
||||
from collections.abc import AsyncGenerator
|
||||
from contextlib import asynccontextmanager
|
||||
from typing import Any
|
||||
|
||||
from fastapi import FastAPI, HTTPException, Request
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from plugins.core.sdk import PluginDefinition, PluginHostContext, PluginRequestContext, plugin_endpoint
|
||||
|
||||
|
||||
class EchoBody(BaseModel):
|
||||
"""
|
||||
SDK 请求体模型,约束示例消息长度。
|
||||
"""
|
||||
|
||||
message: str = Field(min_length=1, max_length=200)
|
||||
|
||||
|
||||
async def echo(context: PluginRequestContext) -> dict[str, Any]:
|
||||
return {**context.request.to_payload(), 'requestId': context.request_id}
|
||||
|
||||
|
||||
def create_plugin(host: PluginHostContext) -> PluginDefinition:
|
||||
"""
|
||||
声明子应用工厂,资源初始化交给子应用生命周期。
|
||||
|
||||
:param host: 宿主提供的插件上下文
|
||||
:return: 插件能力声明
|
||||
"""
|
||||
return PluginDefinition(app_factory=create_app)
|
||||
|
||||
|
||||
def create_app(host: PluginHostContext) -> FastAPI:
|
||||
"""
|
||||
为当前 worker 创建独立的 ASGI 子应用。
|
||||
|
||||
:param host: 宿主提供的插件上下文
|
||||
:return: 包含权限检查和生命周期管理的子应用
|
||||
"""
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncGenerator[dict[str, str], None]:
|
||||
"""
|
||||
演示插件自有资源的启动与关闭。
|
||||
|
||||
:param app: 正在启动或关闭的 ASGI 子应用
|
||||
:return: 可供请求访问的生命周期状态
|
||||
"""
|
||||
host.logger.info('ASGI 示例启动')
|
||||
try:
|
||||
yield {'message': '子应用资源已就绪'}
|
||||
finally:
|
||||
host.logger.info('ASGI 示例关闭')
|
||||
|
||||
app = FastAPI(title='ASGI 插件示例', lifespan=lifespan, docs_url=None, redoc_url=None)
|
||||
|
||||
@app.get(
|
||||
'/api/info',
|
||||
summary='获取ASGI插件信息接口',
|
||||
description='用于获取当前插件标识、宿主API版本和子应用资源状态',
|
||||
)
|
||||
async def info(request: Request) -> dict[str, str]:
|
||||
context = request.state.plugin_context
|
||||
try:
|
||||
context.require_permission('asgi_demo:view')
|
||||
except PermissionError as exc:
|
||||
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
||||
return {'pluginId': host.plugin_id, 'hostApiVersion': host.api_version, 'message': request.state.message}
|
||||
|
||||
app.add_api_route(
|
||||
'/api/echo/{item_id}',
|
||||
plugin_endpoint(echo, permission='asgi_demo:view', body_model=EchoBody),
|
||||
methods=['POST'],
|
||||
summary='验证ASGI插件请求上下文接口',
|
||||
description='返回校验后的请求体、路径参数、多值查询参数与宿主追踪标识',
|
||||
response_model=dict[str, Any],
|
||||
)
|
||||
|
||||
return app
|
||||
@@ -0,0 +1,22 @@
|
||||
manifestVersion: 2
|
||||
id: asgi_demo
|
||||
name: ASGI 子应用示例
|
||||
version: 1.0.0
|
||||
description: 使用显式入口和宿主认证的独立 API 子应用。
|
||||
backend:
|
||||
runtime: python
|
||||
integration: asgi
|
||||
module: plugins.asgi_demo
|
||||
entrypoint: plugins.asgi_demo:create_plugin
|
||||
asgi:
|
||||
mountPath: /apps/asgi_demo
|
||||
lifespan: managed
|
||||
frontend:
|
||||
delivery:
|
||||
type: none
|
||||
permissions:
|
||||
- code: asgi_demo:view
|
||||
name: 查看 ASGI 示例
|
||||
compatibility:
|
||||
hostApiVersion: ^1.3.0
|
||||
pythonVersion: '>=3.10'
|
||||
Reference in New Issue
Block a user