2026-07-02 03:29:06 +08:00
|
|
|
|
"""配置诊断 Router。
|
|
|
|
|
|
|
|
|
|
|
|
路由分一类:
|
|
|
|
|
|
- 诊断端点(GET /checks / GET /checks/{check_id} / POST /checks/run-all /
|
|
|
|
|
|
POST /checks/{check_id}/run / POST /checks/{check_id}/repair/preview /
|
|
|
|
|
|
POST /checks/{check_id}/repair):控制面操作,通过 DoctorPort 走
|
|
|
|
|
|
控制面管道,统一执行认证 / 权限 / 限流 / 分派 / 审计五阶段(模板 A)。
|
|
|
|
|
|
失败时通过 ``raiseOnControlFailure`` 反向重构异常交由全局 handler 映射。
|
|
|
|
|
|
|
|
|
|
|
|
端口装配:
|
|
|
|
|
|
- ``use_cases.doctor`` 字段绑定到 ``ChannelControlService`` 同一实例
|
|
|
|
|
|
(窄端口依赖,由 ``infrastructure/channel_use_cases.py`` 装配)
|
|
|
|
|
|
|
|
|
|
|
|
鉴权策略:
|
|
|
|
|
|
- 全部端点使用 ``get_admin_user`` 依赖(管理员角色校验)
|
|
|
|
|
|
- 无 Webhook 入站端点,无匿名访问
|
2026-07-04 00:16:00 +08:00
|
|
|
|
|
|
|
|
|
|
``check_id`` 校验策略:
|
|
|
|
|
|
- 路径参数声明为 ``str``(无 Pydantic 注解),格式校验
|
|
|
|
|
|
(``^[A-Za-z0-9_-]+$``)统一在 ``DoctorHandler.match_pattern``
|
|
|
|
|
|
入口经模块级 ``_validateCheckId`` 函数执行,确保非法格式也经
|
|
|
|
|
|
控制面管道五阶段(AC-32 失败审计),避免 Router 层
|
|
|
|
|
|
``StringConstraints`` 注解返回 422 与管道内 ``ValidationError``
|
|
|
|
|
|
返回 400 的错误码不一致(P2 修正)。
|
2026-07-02 03:29:06 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from typing import Any
|
|
|
|
|
|
|
|
|
|
|
|
from fastapi import APIRouter, Depends, Request
|
|
|
|
|
|
|
|
|
|
|
|
from yuxi.channels.contract.dtos.channel import ChannelType
|
|
|
|
|
|
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
|
|
|
|
|
|
|
|
|
|
|
|
doctor_router = APIRouter(tags=["channels-doctor"])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@doctor_router.get(
|
|
|
|
|
|
"/{channel_type}/{account_id}/doctor/checks",
|
|
|
|
|
|
response_model=dict,
|
|
|
|
|
|
)
|
|
|
|
|
|
async def list_doctor_checks(
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
account_id: str,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""查询诊断检查项列表(DOC-CHECK-01,FR-17)。
|
|
|
|
|
|
|
|
|
|
|
|
对应控制面操作 ``doctor/checks``(由 ``ChannelControlService.getDoctorChecks``
|
|
|
|
|
|
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
|
|
|
|
|
|
返回指定渠道账户的诊断检查项列表,包括插件声明的检查项与框架级
|
|
|
|
|
|
默认检查项。渠道未注册诊断适配器时返回 501。
|
|
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
result = await use_cases.doctor.getDoctorChecks(
|
|
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
account_id=account_id,
|
|
|
|
|
|
operator=operator,
|
|
|
|
|
|
)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
|
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@doctor_router.post(
|
|
|
|
|
|
"/{channel_type}/{account_id}/doctor/checks/run-all",
|
|
|
|
|
|
response_model=dict,
|
|
|
|
|
|
)
|
|
|
|
|
|
async def run_all_doctor_checks(
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
account_id: str,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
|
|
|
|
|
"""运行全部诊断检查(DOC-CHECK-03,FR-17,AC-19 / AC-64)。
|
|
|
|
|
|
|
|
|
|
|
|
对应控制面操作 ``doctor/run``(由 ``ChannelControlService.runDoctorChecks``
|
|
|
|
|
|
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
|
|
|
|
|
|
执行 6 项框架级默认检查(credential_validity / required_permissions /
|
|
|
|
|
|
webhook_registered / webhook_reachable / rate_limit_status /
|
|
|
|
|
|
whitelist_status)+ 适配器自定义检查项,聚合为诊断报告(含整体健康度、
|
|
|
|
|
|
检查项结果列表、已应用迁移列表)。单项检查异常不中断整体流程
|
|
|
|
|
|
(_safeRunItem 捕获非 DependencyError 降级为"未通过 + ERROR")。
|
|
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
result = await use_cases.doctor.runDoctorChecks(
|
|
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
account_id=account_id,
|
|
|
|
|
|
operator=operator,
|
|
|
|
|
|
)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
|
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@doctor_router.get(
|
|
|
|
|
|
"/{channel_type}/{account_id}/doctor/checks/{check_id}",
|
|
|
|
|
|
response_model=dict,
|
|
|
|
|
|
)
|
|
|
|
|
|
async def get_doctor_check(
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
account_id: str,
|
|
|
|
|
|
check_id: str,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
2026-07-04 00:16:00 +08:00
|
|
|
|
"""查询单项检查项元信息(DOC-CHECK-02,FR-17)。
|
|
|
|
|
|
|
|
|
|
|
|
对应控制面操作 ``doctor/checks/{check_id}``(由
|
|
|
|
|
|
``ChannelControlService.getDoctorCheck`` 内部构造 ``ControlCmd`` 并
|
|
|
|
|
|
委托 ``_executeControl`` 执行控制面管道)。返回检查项元信息
|
|
|
|
|
|
(``check_id`` / ``name`` / ``severity`` / ``description`` /
|
|
|
|
|
|
``auto_repairable``)。检查项不存在返回 404(由 ``DoctorService.getCheck``
|
|
|
|
|
|
经 ``_getCheckItem`` 抛 ``NotFoundError``,携带 ``trace_id``);
|
|
|
|
|
|
``check_id`` 格式非法返回 400(由 ``doctor_handler._validateCheckId``
|
|
|
|
|
|
在管道内抛 ``ValidationError``);渠道未注册诊断适配器返回 501。
|
|
|
|
|
|
|
|
|
|
|
|
本端点走标准模板 A(控制面端口路由),不复用 ``getDoctorChecks`` 全量
|
|
|
|
|
|
查询后过滤(GAP-06 修正):单项查询直接走 ``DoctorService.getCheck``
|
|
|
|
|
|
端口方法,避免 router 层业务判断与 ``trace_id`` 丢失。
|
2026-07-02 03:29:06 +08:00
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
2026-07-04 00:16:00 +08:00
|
|
|
|
result = await use_cases.doctor.getDoctorCheck(
|
2026-07-02 03:29:06 +08:00
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
account_id=account_id,
|
2026-07-04 00:16:00 +08:00
|
|
|
|
check_id=check_id,
|
2026-07-02 03:29:06 +08:00
|
|
|
|
operator=operator,
|
|
|
|
|
|
)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
2026-07-04 00:16:00 +08:00
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|
2026-07-02 03:29:06 +08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@doctor_router.post(
|
|
|
|
|
|
"/{channel_type}/{account_id}/doctor/checks/{check_id}/run",
|
|
|
|
|
|
response_model=dict,
|
|
|
|
|
|
)
|
|
|
|
|
|
async def run_doctor_check(
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
account_id: str,
|
|
|
|
|
|
check_id: str,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
2026-07-04 00:16:00 +08:00
|
|
|
|
"""运行单项诊断检查(DOC-CHECK-04,FR-17)。
|
2026-07-02 03:29:06 +08:00
|
|
|
|
|
|
|
|
|
|
对应控制面操作 ``doctor/checks/{check_id}/run``(由
|
|
|
|
|
|
``ChannelControlService.runDoctorCheck`` 内部构造 ``ControlCmd`` 并
|
|
|
|
|
|
委托 ``_executeControl`` 执行控制面管道)。``check_id`` 格式由
|
2026-07-04 00:16:00 +08:00
|
|
|
|
``doctor_handler._validateCheckId`` 在管道内校验(正则
|
|
|
|
|
|
``^[A-Za-z0-9_-]+$``),格式非法返回 400(经审计五阶段,AC-32);
|
|
|
|
|
|
检查项不存在返回 404;渠道未注册诊断适配器返回 501。
|
2026-07-02 03:29:06 +08:00
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
result = await use_cases.doctor.runDoctorCheck(
|
|
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
account_id=account_id,
|
|
|
|
|
|
check_id=check_id,
|
|
|
|
|
|
operator=operator,
|
|
|
|
|
|
)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
|
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@doctor_router.post(
|
|
|
|
|
|
"/{channel_type}/{account_id}/doctor/checks/{check_id}/repair/preview",
|
|
|
|
|
|
response_model=dict,
|
|
|
|
|
|
)
|
|
|
|
|
|
async def preview_doctor_repair(
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
account_id: str,
|
|
|
|
|
|
check_id: str,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
2026-07-04 00:16:00 +08:00
|
|
|
|
"""修复方案预览(DOC-REP-02,FR-17 / AC-40,dry-run 只读)。
|
2026-07-02 03:29:06 +08:00
|
|
|
|
|
2026-07-04 00:16:00 +08:00
|
|
|
|
对应控制面操作 ``doctor/checks/{check_id}/repair/preview``(由
|
|
|
|
|
|
``ChannelControlService.repairDoctorCheck`` 在 ``confirmed=False`` 时
|
|
|
|
|
|
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
|
|
|
|
|
|
返回含修复方案(repair_plan)的检查结果,不执行修复。preview 与 execute
|
|
|
|
|
|
使用独立 operation,确保只读预览审计为 ``doctor_repair_previewed`` 而非
|
|
|
|
|
|
写操作 ``doctor_repair``。检查项不支持自动修复时返回 400;检查项不存在
|
|
|
|
|
|
返回 404;渠道未注册诊断适配器返回 501。
|
2026-07-02 03:29:06 +08:00
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
result = await use_cases.doctor.repairDoctorCheck(
|
|
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
account_id=account_id,
|
|
|
|
|
|
check_id=check_id,
|
|
|
|
|
|
confirmed=False,
|
|
|
|
|
|
operator=operator,
|
|
|
|
|
|
)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
|
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@doctor_router.post(
|
|
|
|
|
|
"/{channel_type}/{account_id}/doctor/checks/{check_id}/repair",
|
|
|
|
|
|
response_model=dict,
|
|
|
|
|
|
)
|
|
|
|
|
|
async def execute_doctor_repair(
|
|
|
|
|
|
channel_type: ChannelType,
|
|
|
|
|
|
account_id: str,
|
|
|
|
|
|
check_id: str,
|
|
|
|
|
|
request: Request,
|
|
|
|
|
|
use_cases=Depends(get_channel_use_cases),
|
|
|
|
|
|
current_user: User = Depends(get_admin_user),
|
|
|
|
|
|
) -> dict[str, Any]:
|
2026-07-04 00:16:00 +08:00
|
|
|
|
"""执行修复(DOC-REP-01,FR-17 / AC-40,写操作)。
|
2026-07-02 03:29:06 +08:00
|
|
|
|
|
|
|
|
|
|
对应控制面操作 ``doctor/checks/{check_id}/repair``(由
|
2026-07-04 00:16:00 +08:00
|
|
|
|
``ChannelControlService.repairDoctorCheck`` 在 ``confirmed=True`` 时
|
|
|
|
|
|
内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。
|
2026-07-02 03:29:06 +08:00
|
|
|
|
执行 ``adapter.autoFix`` 修复后复检返回结果;修复失败时返回原检查结果
|
|
|
|
|
|
"未通过",不修改渠道配置(AC-40)。检查项不支持自动修复时返回 400;
|
|
|
|
|
|
检查项不存在返回 404;渠道未注册诊断适配器返回 501。
|
|
|
|
|
|
"""
|
|
|
|
|
|
operator = build_operator(current_user, request)
|
|
|
|
|
|
result = await use_cases.doctor.repairDoctorCheck(
|
|
|
|
|
|
channel_type=channel_type,
|
|
|
|
|
|
account_id=account_id,
|
|
|
|
|
|
check_id=check_id,
|
|
|
|
|
|
confirmed=True,
|
|
|
|
|
|
operator=operator,
|
|
|
|
|
|
)
|
|
|
|
|
|
raiseOnControlFailure(result)
|
|
|
|
|
|
return {"success": True, "data": serialize_control_data(result.data)}
|