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,74 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Depends, HTTPException, Path, Request, Response
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from common.aspect.db_session import DBSessionDependency
|
||||
from common.aspect.pre_auth import CurrentUserDependency
|
||||
from common.router import APIRouterPro
|
||||
from common.vo import DataResponseModel
|
||||
from module_admin.entity.vo.user_vo import CurrentUserModel
|
||||
from module_admin.service.login_service import oauth2_scheme
|
||||
from plugins.core.runtime.browser_session import COOKIE_NAME_PREFIX, issue_browser_session, plugin_browser_base
|
||||
from plugins.core.runtime.explicit import ExplicitPluginRuntime
|
||||
from utils.response_util import ResponseUtil
|
||||
|
||||
plugin_browser_controller = APIRouterPro(prefix='/plugin/runtime', tags=['插件浏览器会话'])
|
||||
|
||||
|
||||
@plugin_browser_controller.post(
|
||||
'/{plugin_id}/session',
|
||||
summary='创建插件浏览器会话接口',
|
||||
description='用于为当前登录用户创建插件页面访问会话,并返回访问地址和防跨站请求令牌',
|
||||
response_model=DataResponseModel[dict],
|
||||
)
|
||||
async def create_plugin_browser_session(
|
||||
request: Request,
|
||||
plugin_id: Annotated[str, Path(description='插件ID')],
|
||||
current_user: Annotated[CurrentUserModel, CurrentUserDependency()],
|
||||
token: Annotated[str, Depends(oauth2_scheme)],
|
||||
query_db: Annotated[AsyncSession, DBSessionDependency()],
|
||||
) -> Response:
|
||||
runtime = getattr(request.app.state, 'plugin_explicit_runtime', None)
|
||||
loaded = runtime.loaded.get(plugin_id) if isinstance(runtime, ExplicitPluginRuntime) else None
|
||||
if loaded is None or loaded.plugin.discovered_plugin.manifest.frontend.delivery.type != 'bundle':
|
||||
raise HTTPException(status_code=404, detail='插件页面不存在')
|
||||
if not loaded.active or loaded.lifespan is None or not loaded.lifespan.ready:
|
||||
raise HTTPException(status_code=503, detail='插件页面尚未就绪')
|
||||
try:
|
||||
if not await runtime.route_state_gateway.is_plugin_enabled(query_db, plugin_id):
|
||||
raise PermissionError('插件未启用')
|
||||
runtime._request_context(loaded, current_user)
|
||||
cookie, csrf, seconds = await issue_browser_session(
|
||||
request,
|
||||
plugin_id=plugin_id,
|
||||
version=loaded.plugin.discovered_plugin.manifest.version,
|
||||
main_token=token,
|
||||
user_id=current_user.user.user_id,
|
||||
)
|
||||
except PermissionError as exc:
|
||||
raise HTTPException(status_code=403, detail=str(exc)) from exc
|
||||
except LookupError as exc:
|
||||
raise HTTPException(status_code=401, detail=str(exc)) from exc
|
||||
base = plugin_browser_base(request, plugin_id)
|
||||
response = ResponseUtil.success(
|
||||
data={
|
||||
'pluginId': plugin_id,
|
||||
'uiBase': f'{base}ui/',
|
||||
'apiBase': f'{base}api/',
|
||||
'bridgeVersion': 1,
|
||||
'csrfToken': csrf,
|
||||
'expiresIn': seconds,
|
||||
},
|
||||
headers={'Cache-Control': 'no-store'},
|
||||
)
|
||||
response.set_cookie(
|
||||
f'{COOKIE_NAME_PREFIX}{plugin_id}',
|
||||
cookie,
|
||||
max_age=seconds,
|
||||
path=base,
|
||||
httponly=True,
|
||||
secure=request.url.scheme == 'https',
|
||||
samesite='strict',
|
||||
)
|
||||
return response
|
||||
@@ -451,11 +451,26 @@ async def diagnose_system_plugin(
|
||||
query_db: Annotated[AsyncSession, DBSessionDependency()],
|
||||
plugin_id: Annotated[str, Path(description='插件ID')],
|
||||
) -> Response:
|
||||
from plugins.core.runtime.diagnostics_query import read_plugin_runtime_diagnostics # noqa: PLC0415
|
||||
|
||||
diagnose_plugin_result = await get_plugin_operation_service().diagnose_plugin_with_audit_services(
|
||||
query_db, plugin_id
|
||||
)
|
||||
application_runtime = getattr(request.app.state, 'plugin_application_runtime', None)
|
||||
diagnose_plugin_result['runtime'] = await read_plugin_runtime_diagnostics(
|
||||
plugin_id,
|
||||
reporter=getattr(request.app.state, 'plugin_diagnostics_reporter', None),
|
||||
redis=getattr(request.app.state, 'redis', None),
|
||||
ready_key=getattr(application_runtime, 'ready_key', None),
|
||||
)
|
||||
logger.info(diagnose_plugin_result.get('message', '插件诊断包生成完成'))
|
||||
|
||||
if all(key in diagnose_plugin_result for key in ('info', 'check', 'config', 'menuPlan', 'audit')):
|
||||
# 完整诊断包中的 ok=false 表示检查发现问题,仍需让页面读取其运行观测证据。
|
||||
return ResponseUtil.success(
|
||||
msg=str(diagnose_plugin_result.get('message') or '插件诊断包生成完成'),
|
||||
data=_public_plugin_payload(diagnose_plugin_result),
|
||||
)
|
||||
return _plugin_operation_response(diagnose_plugin_result, '插件诊断包生成完成')
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import Path, Query, Request, Response
|
||||
|
||||
from common.aspect.interface_auth import UserInterfaceAuthDependency
|
||||
from common.aspect.pre_auth import PreAuthDependency
|
||||
from common.router import APIRouterPro
|
||||
from common.vo import DataResponseModel
|
||||
from module_plugin.service.plugin_service import get_plugin_runtime_service
|
||||
from plugins.core.runtime.configuration import build_config_status, config_revision
|
||||
from plugins.core.runtime.metrics_store import PluginMetricsReporter
|
||||
from utils.response_util import ResponseUtil
|
||||
|
||||
plugin_metrics_controller = APIRouterPro(
|
||||
prefix='/system/plugin',
|
||||
order_num=6,
|
||||
tags=['系统管理-插件运行观测'],
|
||||
dependencies=[PreAuthDependency(), UserInterfaceAuthDependency('system:plugin:query')],
|
||||
)
|
||||
|
||||
|
||||
@plugin_metrics_controller.get(
|
||||
'/runtime/metrics',
|
||||
summary='获取插件运行指标接口',
|
||||
description='用于查询显式插件的请求与任务累计指标,按实际加载版本和可观测worker分组',
|
||||
response_model=DataResponseModel[dict],
|
||||
)
|
||||
async def get_plugin_runtime_metrics(
|
||||
request: Request,
|
||||
plugin_id: Annotated[
|
||||
str | None, Query(alias='pluginId', description='插件ID', pattern=r'^[a-z][a-z0-9_]{1,63}$')
|
||||
] = None,
|
||||
) -> Response:
|
||||
reporter = getattr(request.app.state, 'plugin_metrics_reporter', None)
|
||||
if not isinstance(reporter, PluginMetricsReporter):
|
||||
return ResponseUtil.success(
|
||||
data={'ok': True, 'supported': False, 'scope': 'unavailable', 'workers': [], 'series': []}
|
||||
)
|
||||
return ResponseUtil.success(data=await reporter.read(plugin_id))
|
||||
|
||||
|
||||
@plugin_metrics_controller.get(
|
||||
'/{plugin_id}/config/status',
|
||||
summary='获取插件配置生效状态接口',
|
||||
description='比较数据库配置与已观测进程的启动快照;按需读取版本仅表示读取完成,不代表业务设置已经应用',
|
||||
response_model=DataResponseModel[dict],
|
||||
)
|
||||
async def get_plugin_config_status(
|
||||
request: Request,
|
||||
plugin_id: Annotated[str, Path(description='插件ID', pattern=r'^[a-z][a-z0-9_]{1,63}$')],
|
||||
) -> Response:
|
||||
payload = await get_plugin_runtime_service().get_plugin_config(plugin_id, reveal_secret=True)
|
||||
if not payload.get('ok'):
|
||||
return ResponseUtil.failure(msg='插件配置状态读取失败')
|
||||
values = {item['key']: item.get('value') for item in payload.get('configs', [])}
|
||||
reporter = getattr(request.app.state, 'plugin_metrics_reporter', None)
|
||||
report = await reporter.read(plugin_id) if isinstance(reporter, PluginMetricsReporter) else {'scope': 'unavailable'}
|
||||
return ResponseUtil.success(data=build_config_status(plugin_id, config_revision(plugin_id, values), report))
|
||||
@@ -0,0 +1,94 @@
|
||||
from collections.abc import Awaitable, Callable
|
||||
from importlib import import_module
|
||||
from pathlib import Path
|
||||
from typing import Annotated, Any
|
||||
|
||||
from fastapi import Query, Response
|
||||
|
||||
from common.aspect.interface_auth import UserInterfaceAuthDependency
|
||||
from common.aspect.pre_auth import PreAuthDependency
|
||||
from common.router import APIRouterPro
|
||||
from common.vo import DataResponseModel
|
||||
from utils.log_util import logger
|
||||
from utils.response_util import ResponseUtil
|
||||
|
||||
plugin_release_controller = APIRouterPro(
|
||||
prefix='/system/plugin',
|
||||
order_num=6,
|
||||
tags=['系统管理-插件制品发布'],
|
||||
dependencies=[PreAuthDependency(), UserInterfaceAuthDependency('system:plugin:query')],
|
||||
)
|
||||
|
||||
|
||||
def get_plugin_deployment_service() -> Any:
|
||||
"""
|
||||
按需创建制品发布查询服务,功能关闭时不访问新增数据库表。
|
||||
|
||||
:return: 当前宿主配置下的制品发布服务
|
||||
:raises ValueError: 签名制品发布功能未启用或存储配置无效
|
||||
"""
|
||||
config_class = import_module('plugins.core.deployment.config').PluginDeploymentConfig
|
||||
config = config_class.from_settings(Path(__file__).resolve().parents[2])
|
||||
config.require_enabled()
|
||||
service_class = import_module('plugins.core.deployment.service').PluginDeploymentService
|
||||
return service_class(config)
|
||||
|
||||
|
||||
async def _read_release_payload(operation: Callable[[], Awaitable[dict[str, Any]]]) -> Response:
|
||||
"""
|
||||
统一执行发布只读查询,并转换预检失败及内部异常响应。
|
||||
|
||||
:param operation: 不接收参数并异步返回发布查询数据的操作
|
||||
:return: 成功数据或不暴露内部连接信息的失败响应
|
||||
"""
|
||||
try:
|
||||
payload = await operation()
|
||||
except ValueError as exc:
|
||||
return ResponseUtil.failure(msg=str(exc), data={'ok': False, 'status': 'blocked'})
|
||||
except Exception:
|
||||
logger.exception('读取插件制品发布状态失败')
|
||||
return ResponseUtil.failure(msg='读取插件制品发布状态失败')
|
||||
if not payload.get('ok', True):
|
||||
return ResponseUtil.failure(msg=str(payload.get('message') or '插件发布预检未通过'), data=payload)
|
||||
return ResponseUtil.success(data=payload)
|
||||
|
||||
|
||||
@plugin_release_controller.get(
|
||||
'/artifacts/list',
|
||||
summary='获取已验证插件制品列表接口',
|
||||
description='用于获取已验证的插件制品列表,支持按插件ID筛选',
|
||||
response_model=DataResponseModel[dict],
|
||||
)
|
||||
async def list_plugin_artifacts(
|
||||
plugin_id: Annotated[
|
||||
str | None, Query(alias='pluginId', description='插件ID', pattern=r'^[a-z][a-z0-9_]{1,63}$')
|
||||
] = None,
|
||||
) -> Response:
|
||||
return await _read_release_payload(lambda: get_plugin_deployment_service().catalog.list_artifacts(plugin_id))
|
||||
|
||||
|
||||
@plugin_release_controller.get(
|
||||
'/release/status',
|
||||
summary='获取插件发布状态接口',
|
||||
description='用于查询插件发布目标及各工作进程的实际状态,支持按插件ID筛选',
|
||||
response_model=DataResponseModel[dict],
|
||||
)
|
||||
async def get_plugin_release_status(
|
||||
plugin_id: Annotated[
|
||||
str | None, Query(alias='pluginId', description='插件ID', pattern=r'^[a-z][a-z0-9_]{1,63}$')
|
||||
] = None,
|
||||
) -> Response:
|
||||
return await _read_release_payload(lambda: get_plugin_deployment_service().status(plugin_id))
|
||||
|
||||
|
||||
@plugin_release_controller.get(
|
||||
'/release/plan',
|
||||
summary='获取插件发布预检计划接口',
|
||||
description='用于根据插件ID和制品摘要获取维护发布前的静态预检计划',
|
||||
response_model=DataResponseModel[dict],
|
||||
)
|
||||
async def plan_plugin_release(
|
||||
plugin_id: Annotated[str, Query(alias='pluginId', description='插件ID', pattern=r'^[a-z][a-z0-9_]{1,63}$')],
|
||||
digest: Annotated[str, Query(description='插件制品SHA256摘要', pattern=r'^[0-9a-f]{64}$')],
|
||||
) -> Response:
|
||||
return await _read_release_payload(lambda: get_plugin_deployment_service().plan(plugin_id, digest))
|
||||
Reference in New Issue
Block a user