ForcePilot/backend/server/routers/channels/plugin_router.py
Kris ddafd95ff0 refactor(routers): 整理并新增多组渠道相关路由功能
1.  移除多个导出接口的显式response_model声明
2.  调整access_rule和test_case的创建接口位置,修复静态路径冲突
3.  优化适配器配置校验的异常处理逻辑
4.  重构集成路由的查询逻辑,统一使用get_integration_or_raise
5.  新增channels路由组下的capability、reports、dashboard、webhook、wizard、doctor、directory、session共8个子路由模块
6.  注册channels_router到全局路由列表
2026-07-02 03:29:06 +08:00

400 lines
15 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/{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
from fastapi import APIRouter, Depends, Query, Request
from pydantic import BaseModel, ConfigDict, Field
from yuxi.channels.contract.dtos.plugin import (
BatchPluginLifecycleCmd,
InstallPluginCmd,
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``
暂未实现dispatch handler 抛 ``NotImplementedError`` 501
"""
model_config = ConfigDict(frozen=True)
source_type: str = 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`` 的 key 必须在插件 manifest 的 ``config_schema`` 中声明,
否则 dispatch handler 抛 ``ValidationError``。
"""
model_config = ConfigDict(frozen=True)
config: dict[str, Any] = Field(..., description="配置值字典")
apply_mode: str = Field(..., description="应用模式hot / restart_required")
class BatchPluginLifecycleRequest(BaseModel):
"""批量插件生命周期请求体PLG-BATCH-LIFE
``plugin_ids`` 与 ``filter`` 二选一(同时为空时由 dispatch handler 校验)。
"""
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: str | None = Query(default=None, description="按生命周期状态过滤discovered/resolved/loaded/initialized/started/paused/stopped/unloaded/failed"),
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.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)}