ForcePilot/backend/server/routers/channels/plugin_router.py

427 lines
17 KiB
Python
Raw Normal View History

"""插件管理路由PLG-QUERY-01/02 + PLG-LIFE-01~07 + PLG-INSTALL + PLG-UNINSTALL-FILE + PLG-CONFIG + PLG-BATCH-LIFE
统一采用模板 A控制面端口路由鉴权依赖 get_admin_user调用
use_cases.plugin_management.<method>raiseOnControlFailure 转译失败
serialize_control_data 序列化响应
路径设计静态路径 /plugins 先于动态路径 /plugins/{plugin_id} 及其子路径
声明规范 §6.5避免静态路径被动态参数捕获 plugin_id="schema"
/plugins 捕获 router 自身 prefix /plugins根前缀 /channels
channels_router 聚合 router 统一追加
端点清单对应09-插件管理域设计方案§2.1
- GET /plugins PLG-QUERY-01 listPlugins
- GET /plugins/catalog PLG-CATALOG listPluginCatalog
- GET /plugins/{plugin_id} PLG-QUERY-02 getPlugin
- POST /plugins/{plugin_id}/load PLG-LIFE-01 pluginLoad
- POST /plugins/{plugin_id}/start PLG-LIFE-02 pluginStart
- POST /plugins/{plugin_id}/pause PLG-LIFE-03 pluginPause
- POST /plugins/{plugin_id}/resume PLG-LIFE-04 pluginResume
- POST /plugins/{plugin_id}/stop PLG-LIFE-05 pluginStop
- POST /plugins/{plugin_id}/unload PLG-LIFE-06 pluginUnload
- POST /plugins/{plugin_id}/reload PLG-LIFE-07 pluginReload
- POST /plugins/install PLG-INSTALL installPlugin
- DELETE /plugins/{plugin_id} PLG-UNINSTALL-FILE uninstallPlugin
- GET /plugins/{plugin_id}/config PLG-CONFIG-GET getPluginConfig
- PUT /plugins/{plugin_id}/config PLG-CONFIG-PUT updatePluginConfig
- POST /plugins/batch/start PLG-BATCH-LIFE-START batchStartPlugins
- POST /plugins/batch/stop PLG-BATCH-LIFE-STOP batchStopPlugins
"""
from __future__ import annotations
from typing import Any, Literal
from fastapi import APIRouter, Depends, Query, Request
from pydantic import BaseModel, ConfigDict, Field
from yuxi.channels.contract.dtos.plugin import (
BatchPluginLifecycleCmd,
InstallPluginCmd,
PluginStateLiteral,
UpdatePluginConfigCmd,
)
from yuxi.storage.postgres.models_business import User
from server.routers.channels import (
build_operator,
get_channel_use_cases,
raiseOnControlFailure,
serialize_control_data,
)
from server.utils.auth_middleware import get_admin_user, get_superadmin_user
plugin_router = APIRouter(prefix="/plugins", tags=["channels-plugins"])
class InstallPluginRequest(BaseModel):
"""安装插件请求体PLG-INSTALL
``source_type`` ``path`` 时从本地目录复制插件文件``url`` / ``registry``
暂未实现 ``PluginLoader.installFromSource`` ``NotImplementedError`` 501
"""
model_config = ConfigDict(frozen=True)
source_type: Literal["path", "url", "registry"] = Field(..., description="来源类型path / url / registry")
source: str = Field(..., description="来源路径或 URL")
version: str | None = Field(default=None, description="指定版本(可选)")
force: bool = Field(default=False, description="是否强制覆盖已存在插件")
class UpdatePluginConfigRequest(BaseModel):
"""更新插件配置请求体PLG-CONFIG-PUT
``config`` 必须为非空字典 ``UpdatePluginConfigCmd.__post_init__``
校验 key 必须在插件 manifest ``config_schema`` 中声明
dispatch handler 校验否则抛 ``ValidationError``
"""
model_config = ConfigDict(frozen=True)
config: dict[str, Any] = Field(..., description="配置值字典")
apply_mode: Literal["hot", "restart_required"] = Field(..., description="应用模式")
class BatchPluginLifecycleRequest(BaseModel):
"""批量插件生命周期请求体PLG-BATCH-LIFE
``plugin_ids`` ``filter`` 二选一不可同时为空不可同时提供
``BatchPluginLifecycleCmd.__post_init__`` 在构造时校验并抛
``ValidationError``400避免非法输入穿透至控制面管道
"""
model_config = ConfigDict(frozen=True)
plugin_ids: list[str] = Field(default_factory=list, description="显式插件 ID 列表")
filter: dict[str, Any] | None = Field(default=None, description="筛选条件(含 channel_type / state")
# ---------------- 静态路径端点(须先于动态路径声明) ----------------
@plugin_router.get("", response_model=dict)
async def list_plugins(
request: Request,
state: PluginStateLiteral | None = Query(
default=None,
description="按生命周期状态过滤discovered/resolved/loaded/initialized/started/paused/stopped/unloaded/failed/not_installed/installed",
),
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""列出全部插件PLG-QUERY-01。对应控制面操作 plugin/list。"""
operator = build_operator(current_user, request)
result = await use_cases.plugin_management.listPlugins(
operator=operator,
state=state,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@plugin_router.get("/catalog", response_model=dict)
async def list_plugin_catalog(
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""列出插件目录PLG-CATALOG。对应控制面操作 plugin/catalog。
返回所有已注册插件的卡片视图元数据 capabilities供前端卡片
网格展示 ``GET /plugins`` 的区别在于返回 capabilities 字段
"""
operator = build_operator(current_user, request)
result = await use_cases.plugin_management.listPluginCatalog(
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@plugin_router.post("/install", response_model=dict)
async def install_plugin(
payload: InstallPluginRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_superadmin_user),
) -> dict[str, Any]:
"""安装插件PLG-INSTALL。对应控制面操作 plugin/install。
从来源``path`` / ``url`` / ``registry``安装插件文件到插件目录解析
manifest 并注册到 PluginRegistry返回 ``InstallPluginResult``
plugin_id / version / installed_at / requires_load=true需超级管理员权限
"""
operator = build_operator(current_user, request)
cmd = InstallPluginCmd(
operator=operator,
source_type=payload.source_type,
source=payload.source,
version=payload.version,
force=payload.force,
)
result = await use_cases.plugin_management.installPlugin(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@plugin_router.post("/batch/start", response_model=dict)
async def batch_start_plugins(
payload: BatchPluginLifecycleRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""批量启动插件PLG-BATCH-LIFE-START。对应控制面操作 plugin/batch_start。
逐条独立事务执行批量启动模式 D 部分成功复用单条 ``pluginStart``
逻辑``plugin_ids`` ``filter`` 二选一返回
``BatchPluginLifecycleResult`` total / succeeded / failed 列表
"""
operator = build_operator(current_user, request)
cmd = BatchPluginLifecycleCmd(
operator=operator,
plugin_ids=tuple(payload.plugin_ids),
filter=payload.filter,
)
result = await use_cases.plugin_management.batchStartPlugins(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@plugin_router.post("/batch/stop", response_model=dict)
async def batch_stop_plugins(
payload: BatchPluginLifecycleRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""批量停止插件PLG-BATCH-LIFE-STOP。对应控制面操作 plugin/batch_stop。
逐条独立事务执行批量停止模式 D 部分成功复用单条 ``pluginStop``
逻辑``plugin_ids`` ``filter`` 二选一返回
``BatchPluginLifecycleResult`` total / succeeded / failed 列表
"""
operator = build_operator(current_user, request)
cmd = BatchPluginLifecycleCmd(
operator=operator,
plugin_ids=tuple(payload.plugin_ids),
filter=payload.filter,
)
result = await use_cases.plugin_management.batchStopPlugins(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
# ---------------- 动态路径端点 ----------------
@plugin_router.get("/{plugin_id}", response_model=dict)
async def get_plugin(
plugin_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""查询插件详情PLG-QUERY-02。对应控制面操作 plugin/get。
返回插件 manifest生命周期状态绑定渠道类型
"""
operator = build_operator(current_user, request)
result = await use_cases.plugin_management.getPlugin(
plugin_id=plugin_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@plugin_router.delete("/{plugin_id}", response_model=dict)
async def uninstall_plugin(
plugin_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_superadmin_user),
) -> dict[str, Any]:
"""文件级卸载插件PLG-UNINSTALL-FILE。对应控制面操作 plugin/uninstall。
校验插件状态必须为 ``unloaded`` / ``not_installed`` / ``installed``
运行中状态需先 stop+unload删除插件文件并
注销 manifest返回 ``UninstallPluginResult`` plugin_id /
uninstalled_at / files_removed需超级管理员权限
"""
operator = build_operator(current_user, request)
result = await use_cases.plugin_management.uninstallPlugin(
plugin_id=plugin_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@plugin_router.get("/{plugin_id}/config", response_model=dict)
async def get_plugin_config(
plugin_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""查询插件配置PLG-CONFIG-GET。对应控制面操作 plugin/config_get。
PluginManifest 获取配置 schema ConfigManager 获取当前配置值
组装 ``PluginConfigResult`` 返回 plugin_id / config / schema /
requires_restart / updated_at
"""
operator = build_operator(current_user, request)
result = await use_cases.plugin_management.getPluginConfig(
plugin_id=plugin_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@plugin_router.put("/{plugin_id}/config", response_model=dict)
async def update_plugin_config(
plugin_id: str,
payload: UpdatePluginConfigRequest,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""更新插件配置PLG-CONFIG-PUT。对应控制面操作 plugin/config_update。
``apply_mode`` 判断``hot`` 模式立即生效发布 ConfigChanged 事件
``restart_required`` 模式仅持久化返回 ``requires_restart=true``返回
``PluginConfigResult``config key 必须在插件 manifest
``config_schema`` 中声明
"""
operator = build_operator(current_user, request)
cmd = UpdatePluginConfigCmd(
plugin_id=plugin_id,
operator=operator,
config=payload.config,
apply_mode=payload.apply_mode,
)
result = await use_cases.plugin_management.updatePluginConfig(cmd)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@plugin_router.post("/{plugin_id}/load", response_model=dict)
async def load_plugin(
plugin_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""加载插件PLG-LIFE-01。对应控制面操作 plugin/load。"""
operator = build_operator(current_user, request)
result = await use_cases.plugin_management.pluginLoad(
plugin_id=plugin_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@plugin_router.post("/{plugin_id}/start", response_model=dict)
async def start_plugin(
plugin_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""启动插件PLG-LIFE-02。对应控制面操作 plugin/start。"""
operator = build_operator(current_user, request)
result = await use_cases.plugin_management.pluginStart(
plugin_id=plugin_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@plugin_router.post("/{plugin_id}/pause", response_model=dict)
async def pause_plugin(
plugin_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""暂停插件PLG-LIFE-03。对应控制面操作 plugin/pause。"""
operator = build_operator(current_user, request)
result = await use_cases.plugin_management.pluginPause(
plugin_id=plugin_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@plugin_router.post("/{plugin_id}/resume", response_model=dict)
async def resume_plugin(
plugin_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""恢复插件PLG-LIFE-04。对应控制面操作 plugin/resume。"""
operator = build_operator(current_user, request)
result = await use_cases.plugin_management.pluginResume(
plugin_id=plugin_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@plugin_router.post("/{plugin_id}/stop", response_model=dict)
async def stop_plugin(
plugin_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""停止插件PLG-LIFE-05。对应控制面操作 plugin/stop。"""
operator = build_operator(current_user, request)
result = await use_cases.plugin_management.pluginStop(
plugin_id=plugin_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@plugin_router.post("/{plugin_id}/unload", response_model=dict)
async def unload_plugin(
plugin_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""卸载插件PLG-LIFE-06。对应控制面操作 plugin/unload。"""
operator = build_operator(current_user, request)
result = await use_cases.plugin_management.pluginUnload(
plugin_id=plugin_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}
@plugin_router.post("/{plugin_id}/reload", response_model=dict)
async def reload_plugin(
plugin_id: str,
request: Request,
use_cases=Depends(get_channel_use_cases),
current_user: User = Depends(get_admin_user),
) -> dict[str, Any]:
"""重载插件PLG-LIFE-07。对应控制面操作 plugin/reload。"""
operator = build_operator(current_user, request)
result = await use_cases.plugin_management.pluginReload(
plugin_id=plugin_id,
operator=operator,
)
raiseOnControlFailure(result)
return {"success": True, "data": serialize_control_data(result.data)}