"""配置诊断 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 入站端点,无匿名访问 """ from __future__ import annotations from typing import Any from fastapi import APIRouter, Depends, Request from yuxi.channels.contract.dtos.channel import ChannelType from yuxi.channels.contract.errors import InternalError, NotFoundError 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]: """查询单项检查项元信息(DOC-03,FR-17)。 复用控制面操作 ``doctor/checks``(由 ``ChannelControlService.getDoctorChecks`` 内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。 返回检查项清单后由 router 层按 ``check_id`` 筛选单项元信息(check_id / name / severity / description / auto_repairable)。检查项不存在返回 404。 注意:本端点与 DOC-CHECK-01 共享同一端口方法 ``getDoctorChecks``, router 层从检查项列表中筛选指定 ``check_id``。这是协议层的字段筛选 (从已有数据中按 ID 过滤,不涉及业务规则判断),符合 INV-8(驱动 适配器无业务规则)。检查项不存在时抛 ``NotFoundError``(契约层异常, 非 HTTPException),交由全局 handler 映射为 404。 已知技术债务(GAP-06):当前复用 ``getDoctorChecks`` 全量查询后筛选, 未来应在端口层新增 ``getDoctorCheck`` 单项查询方法,避免全量查询开销。 当前复用现有端口方法以最小化契约变更。 """ operator = build_operator(current_user, request) result = await use_cases.doctor.getDoctorChecks( channel_type=channel_type, account_id=account_id, operator=operator, ) raiseOnControlFailure(result) if not isinstance(result.data, dict): raise InternalError(message="control pipeline returned malformed data for doctor checks") checks = result.data.get("checks", []) target = next((c for c in checks if c.get("check_id") == check_id), None) if target is None: raise NotFoundError( resource="doctor_check", id=check_id, ) return {"success": True, "data": serialize_control_data(target)} @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]: """运行单项诊断检查(DOC-CHECK-02,FR-17)。 对应控制面操作 ``doctor/checks/{check_id}/run``(由 ``ChannelControlService.runDoctorCheck`` 内部构造 ``ControlCmd`` 并 委托 ``_executeControl`` 执行控制面管道)。``check_id`` 格式由 ``ChannelControlService._validateCheckId`` 校验(正则 ``^[A-Za-z0-9_-]+$``),格式非法返回 400;检查项不存在返回 404; 渠道未注册诊断适配器返回 501。 """ 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]: """修复方案预览(DOC-02,FR-17 / AC-40,dry-run)。 对应控制面操作 ``doctor/checks/{check_id}/repair``(由 ``ChannelControlService.repairDoctorCheck`` 内部构造 ``ControlCmd`` 并 委托 ``_executeControl`` 执行控制面管道),传入 ``confirmed=False``。 返回含修复方案(repair_plan)的检查结果,不执行修复。检查项不支持 自动修复时返回 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=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]: """执行修复(DOC-REP-01,FR-17 / AC-40)。 对应控制面操作 ``doctor/checks/{check_id}/repair``(由 ``ChannelControlService.repairDoctorCheck`` 内部构造 ``ControlCmd`` 并 委托 ``_executeControl`` 执行控制面管道),传入 ``confirmed=True``。 执行 ``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)}