1. config_router: 为expected_version添加ge=1校验 2. directory_router: 补充scope校验逻辑与注释 3. login_router: 拆分强制下线权限,添加参数校验与注释更新 4. reports_router: 统一时间参数处理,修复分页限制使用契约常量 5. dashboard_router: 更新文档与响应格式,修正参数传递逻辑 6. health_router: 缩减健康检查响应字段,修复响应结构与参数校验 7. plugin_router: 新增插件目录端点,补充枚举校验与注释 8. pairing_router: 新增时间过滤参数,补充参数校验 9. __init__.py: 修复异常映射,更新trace_id获取逻辑与工具类 10. doctor_router: 重构单项检查端点,修正注释与校验逻辑 11. account_router: 新增恢复降级账户端点,补充批量操作校验 12. webhook_router: 优化webhook处理逻辑,修复流式读取与响应逻辑 13. content_review_router: 补充批量审核端点,完善参数校验与注释 14. analytics_router: 修正管道阶段描述,统一参数传递 15. wizard_router: 新增OAuth相关端点,重构路由路径与校验逻辑
424 lines
17 KiB
Python
424 lines
17 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``。
|
||
"""
|
||
|
||
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)}
|