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