ForcePilot/backend/server/routers/channels/plugin_router.py
Kris af5b209be1 refactor(channel routers): 统一分页参数并优化接口实现
1.  新增统一分页常量定义,替换各路由零散的Query参数声明
2.  批量替换多个列表接口的分页参数为预设常量
3.  优化插件目录、消息搜索等接口的参数与文档
4.  修复死信导出接口的参数命名不一致问题
2026-07-11 15:55:45 +08:00

433 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,
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-CATALOG。对应控制面操作 plugin/catalog。
返回已注册插件的卡片视图元数据(含 capabilities供前端卡片网格展示。
与 ``GET /plugins`` 的区别在于返回 capabilities / icon / last_error 字段;
支持 ``state`` 查询参数与 ``GET /plugins`` 保持一致,便于前端直接做服务端状态过滤。
"""
operator = build_operator(current_user, request)
result = await use_cases.plugin_management.listPluginCatalog(
operator=operator,
state=state,
)
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)}