ForcePilot/backend/server/routers/channels/plugin_router.py
Kris bba1775220 refactor(channel-router): 清理冗余空行并优化代码结构
本次提交包含多类优化:
1.  移除多个路由文件中多余的空导入行,统一代码格式
2.  重构Query参数定义,将长参数拆分为多行提升可读性
3.  新增多个业务端点:
    - 渠道能力画像矩阵查询CAP-03
    - 配对审批计数接口用于待办角标
    - 批量查询对端目录资料接口
    - 向导扫码登录相关端点
    - 会话实时事件SSE推送端点
    - 工作台待办统计接口
4.  完善异常处理逻辑,补充OperationTimeoutError导入并优化NotImplementedError的细节返回
5.  调整路由导入顺序,修复动态路由路径冲突隐患
6.  更新文档注释与接口清单,修正部分接口描述细节
2026-07-06 20:50:03 +08:00

427 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""插件管理路由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)}