1. 为渠道账户ID查询添加最小长度校验,统一分析模块常量引用 2. 新增扫码登录向导端点,完善文档说明 3. 优化配对统计接口,移除无效参数 4. 为出站箱接口添加批量上限与202状态码 5. 新增测试用例、访问规则、配额等模块的查询与校验参数 6. 新增适配器健康批量查询、健康检查触发接口 7. 统一告警、审计日志的错误处理方式 8. 新增插件配置账户ID支持,优化批量操作响应 9. 新增环境健康批量查询、Webhook限流与参数校验 10. 完善会话管理、审计日志的参数与文档说明 11. 修复导入模块的校验错误处理逻辑
447 lines
18 KiB
Python
447 lines
18 KiB
Python
"""插件管理路由(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``。
|
||
``account_id`` 用于 ACCOUNT 作用域配置字段的读写,未提供时 ACCOUNT
|
||
作用域字段更新会抛 ``ValidationError``。
|
||
"""
|
||
|
||
model_config = ConfigDict(frozen=True)
|
||
|
||
config: dict[str, Any] = Field(..., description="配置值字典")
|
||
apply_mode: Literal["hot", "restart_required"] = Field(..., description="应用模式")
|
||
account_id: str | None = Field(default=None, description="账户 ID(ACCOUNT 作用域配置字段需要)")
|
||
|
||
|
||
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)
|
||
response: dict[str, Any] = {"success": True, "data": serialize_control_data(result.data)}
|
||
if result.status == "partial":
|
||
response["partial"] = True
|
||
return response
|
||
|
||
|
||
@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)
|
||
response: dict[str, Any] = {"success": True, "data": serialize_control_data(result.data)}
|
||
if result.status == "partial":
|
||
response["partial"] = True
|
||
return response
|
||
|
||
|
||
# ---------------- 动态路径端点 ----------------
|
||
|
||
|
||
@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,
|
||
account_id: str | None = Query(default=None, description="账户 ID(ACCOUNT 作用域配置字段需要)"),
|
||
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。``account_id`` 用于读取 ACCOUNT 作用域
|
||
配置值,未提供时该类字段回退 schema 默认值。
|
||
"""
|
||
operator = build_operator(current_user, request)
|
||
result = await use_cases.plugin_management.getPluginConfig(
|
||
plugin_id=plugin_id,
|
||
operator=operator,
|
||
account_id=account_id,
|
||
)
|
||
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`` 中声明。``account_id`` 用于 ACCOUNT 作用域配置字段,
|
||
未提供时更新该类字段会抛 ``ValidationError``。
|
||
"""
|
||
operator = build_operator(current_user, request)
|
||
cmd = UpdatePluginConfigCmd(
|
||
plugin_id=plugin_id,
|
||
operator=operator,
|
||
config=payload.config,
|
||
apply_mode=payload.apply_mode,
|
||
account_id=payload.account_id,
|
||
)
|
||
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)}
|