"""健康检查域 Router(HLT-AGG-01 / HLT-AGG-02 / HLT-EXP-01 / HLT-SINGLE / HLT-PROBE)。 本 router 实现渠道健康检查域的全部 HTTP 端点,覆盖聚合健康(简略 / 详细)、 诊断导出、单渠道健康、主动探测共 5 个操作。 模板选型: - HLT-AGG-01 / HLT-AGG-02 / HLT-EXP-01:模板 B(数据面端口路由), 经 ``use_cases.health_check`` 端口调用 ``HealthCheckService`` 类型化 方法,不经控制面管道。 - HLT-SINGLE / HLT-PROBE:模板 A(控制面端口路由),经 ``use_cases.health_check_control`` 端口调用 ``ChannelControlService`` 类型化方法,走控制面管道五阶段(auth → permission → rate_limit → dispatch → audit)。 鉴权策略:HLT-AGG-01 无鉴权(K8s 探针 + 公开状态页);其余端点使用 ``get_admin_user`` 依赖,要求管理员角色。角色校验由依赖函数完成,router 内不做角色判断(规范 §4)。 路径设计:静态路径(``/health`` / ``/health/detail`` / ``/health/diagnostics/export``)先于动态路径 (``/{channel_type}/{account_id}/health`` / ``/{channel_type}/{account_id}/probe``)声明,避免 ``/health`` 被捕获为 ``channel_type="health"``(规范 §6.5)。子 router 不自行设置 prefix, 根前缀 ``/channels`` 由 ``channels_router`` 聚合 router 统一追加。 注册顺序:``health_router`` 必须在 ``channels_router`` 聚合中先于含 ``/{channel_type}`` 动态路径的 router(如 ``account_router`` / ``login_router`` / ``directory_router`` 等)注册。 端点清单(对应《健康检查域设计方案》§2.1 + P1 缺口 §2.3.17): - GET /health HLT-AGG-01 get_health - GET /health/detail HLT-AGG-02 get_health_detail - POST /health/diagnostics/export HLT-EXP-01 export_diagnostics - GET /{channel_type}/{account_id}/health HLT-SINGLE get_single_health - POST /{channel_type}/{account_id}/probe HLT-PROBE probe_channel """ 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.channel import ChannelType from yuxi.channels.contract.dtos.health import ( DiagnosticsExportRequest, HealthQuery, ) from yuxi.channels.contract.dtos.health_check import ProbeCmd, SingleHealthQuery from yuxi.storage.postgres.models_business import User from server.routers.channels import ( build_operator, dataclass_to_dict, get_channel_use_cases, raiseOnControlFailure, serialize_control_data, ) from server.utils.auth_middleware import get_admin_user # build_operator 仅在需要构造 Operator 注入端口方法时使用(HLT-EXP-01)。 # HLT-AGG-01 / HLT-AGG-02 的 checkHealth(query) 契约不接受 operator,不构造。 health_router = APIRouter(tags=["channels-health"]) # ---------------- 静态路径端点(须先于动态路径声明) ---------------- @health_router.get("/health", response_model=dict) async def get_health( use_cases=Depends(get_channel_use_cases), ) -> dict[str, Any]: """聚合健康检查(简略,HLT-AGG-01,FR-35)。 无鉴权端点,适用于 K8s liveness/readiness 探针与公开状态页。 返回简略视图:status / version / degraded_components / 简化渠道列表 (channel_type / plugin_state),不暴露 account_id / last_error / worker_status / db_connection_pool_status / circuit_breaker_state 等 运维细节与业务标识,避免无鉴权端点信息泄露。 编排链路:HTTP 入参 → 构造 HealthQuery(with_probe=False)→ use_cases.health_check.checkHealth(query) 直接调用端口方法 → 从 HealthSnapshot 提取简略字段构造响应 dict。 采用模板 B(数据面):不经过控制面管道,不调用 raiseOnControlFailure, 契约异常由全局 unified_error_handler 统一映射为 HTTP 响应。响应遵循 ``{"success": True, "data": ...}`` 统一结构,K8s 探针仅依赖 HTTP 200 状态码判定存活,不解析响应体结构。 """ query = HealthQuery() snapshot = await use_cases.health_check.checkHealth(query) return { "success": True, "data": { "status": snapshot.status, "version": snapshot.version, "degraded_components": list(snapshot.degraded_components), "channels": [ { "channel_type": ch.channel_type, "plugin_state": ch.plugin_state, } for ch in snapshot.channels ], }, } @health_router.get("/health/detail", response_model=dict) async def get_health_detail( channel_type: ChannelType | None = Query(default=None, description="按渠道过滤(None 表示全渠道)"), with_probe: bool = Query(default=False, description="是否绕过缓存触发实时聚合(非调用 probeChannel)"), use_cases=Depends(get_channel_use_cases), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """聚合健康检查(详细,HLT-AGG-02,FR-35)。 管理员鉴权端点,适用于运维排障。返回完整 HealthSnapshot,含 worker_status / redis_stream_status / db_connection_pool_status / 各渠道 circuit_breaker_state / queue_depth / connection_pool_status / trace_id / error_code 等运维字段。 编排链路:HTTP 入参(channel_type / with_probe)→ 构造 HealthQuery(channel_filter / with_probe)→ use_cases.health_check.checkHealth(query) 直接调用端口方法 → dataclass_to_dict(snapshot) 序列化完整快照返回。 采用模板 B(数据面):不经过控制面管道,不调用 raiseOnControlFailure。 with_probe 参数语义:with_probe=True 绕过短期缓存触发实时聚合, 非调用 probeChannel 端口方法(详见设计方案 §1.5 决策 5)。 checkHealth(query: HealthQuery) 契约不接受 operator 参数(只读无副作用 查询),current_user 仅用于鉴权(get_admin_user 依赖副作用),不在 函数体使用。 """ query = HealthQuery(channel_filter=channel_type, with_probe=with_probe) snapshot = await use_cases.health_check.checkHealth(query) return {"success": True, "data": dataclass_to_dict(snapshot)} # ---------------- Request Schemas ---------------- class DiagnosticsExportPayload(BaseModel): """诊断导出请求体(HLT-EXP-01,Router 层 Pydantic schema)。 字段对齐契约层 DiagnosticsExportRequest DTO,但 operator 由 router 构造(从认证用户),不在 body 中。命名为 ``Payload`` 后缀以区分 Router 层 schema 与契约层 DTO,避免同名冲突(规范反模式:别名导入降低可读性)。 """ model_config = ConfigDict(frozen=True) channel_filter: ChannelType | None = Field(default=None, description="按渠道过滤(None 表示全渠道)") include_audit_logs: bool = Field(default=True, description="是否包含审计日志") audit_log_count: int = Field(default=100, ge=1, le=500, description="审计日志条数上限") include_error_logs: bool = Field(default=True, description="是否包含错误日志") error_log_count: int = Field(default=100, ge=1, le=500, description="错误日志条数上限") # ---------------- Endpoints ---------------- @health_router.post("/health/diagnostics/export", response_model=dict) async def export_diagnostics( payload: DiagnosticsExportPayload, request: Request, use_cases=Depends(get_channel_use_cases), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """导出诊断信息包(HLT-EXP-01,FR-35)。 管理员鉴权端点,适用于故障排障。导出 manifest 快照 + 插件状态 + 审计日志 + 错误日志 + 队列深度 + Worker / Redis / DB 池状态, 敏感字段已脱敏(部分遮蔽 app_secret / encrypt_key / verification_token / access_token / refresh_token;全量遮蔽 webhook_secret / webhook_url / password / api_key),审计日志写入 fail-closed。 编排链路:HTTP 入参 → build_operator 构造操作人 → 构造 DiagnosticsExportRequest DTO → use_cases.health_check.exportDiagnostics(request) 直接调用端口方法 → dataclass_to_dict(bundle) 序列化诊断包返回。 对应数据面端口方法 exportDiagnostics(非控制面管道路径), 不调用 raiseOnControlFailure。敏感字段脱敏由 framework 层 DiagnosticsExporter 完成,router 层不重复脱敏。鉴权由 framework 层 ``get_admin_user`` 守门,对齐端口契约 ``@pre: operator 已通过权限校验 (管理员角色)``,用例服务不重复校验权限(避免双重鉴权)。 """ operator = build_operator(current_user, request) export_request = DiagnosticsExportRequest( operator=operator, channel_filter=payload.channel_filter, include_audit_logs=payload.include_audit_logs, audit_log_count=payload.audit_log_count, include_error_logs=payload.include_error_logs, error_log_count=payload.error_log_count, ) bundle = await use_cases.health_check.exportDiagnostics(export_request) return {"success": True, "data": dataclass_to_dict(bundle)} # ---------------- 动态路径端点(须在静态路径之后声明) ---------------- class ProbeRequest(BaseModel): """主动探测请求体(HLT-PROBE,Router 层 Pydantic schema)。 字段对齐契约层 ``ProbeCmd`` DTO 的非操作人部分,``operator`` 由 router 构造(从认证用户),不在 body 中。``probe_type`` 仅允许 ``connectivity`` 或 ``full``,由 Pydantic Literal 约束在 HTTP 边界即拒绝非法值, 契约层 ``ProbeCmd.__post_init__`` 与控制面管道 dispatch_stage 重复校验 (防御深度)。 """ model_config = ConfigDict(frozen=True) probe_type: Literal["connectivity", "full"] = Field( default="connectivity", description="探测类型(connectivity 仅测连通性,full 含凭据/权限/Webhook 全项检查)", ) timeout_ms: int = Field( default=5000, ge=1000, le=30000, description="探测超时毫秒数(默认 5000,上限 30000)", ) @health_router.get( "/{channel_type}/{account_id}/health", response_model=dict, ) async def get_single_health( 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]: """查询单渠道健康详情(HLT-SINGLE)。 对应控制面操作 ``health/single``(由 ``ChannelControlService.getSingleHealth`` 内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。 聚合指定渠道账户的账户状态、熔断器状态、队列深度、插件状态、连接池状态、 worker 状态等健康指标,供运维排障定位单渠道健康问题。本端点为纯读聚合 (GET 安全方法语义),不触发渠道 API 调用;主动深度探测由 ``POST /{channel_type}/{account_id}/probe``(HLT-PROBE)承担。 编排链路:HTTP 入参 → ``build_operator`` 构造操作人 → 构造 ``SingleHealthQuery`` → ``use_cases.health_check_control.getSingleHealth`` 调用控制面端口方法 → ``ControlResult`` 经 ``raiseOnControlFailure`` 转译 失败 → ``serialize_control_data`` 序列化单渠道健康结果返回。 采用模板 A(控制面):经 ``health_check_control`` 端口(与数据面 ``health_check`` 字段分离)调用 ``ChannelControlService`` 类型化方法, 走控制面管道五阶段。 """ operator = build_operator(current_user, request) query = SingleHealthQuery( channel_type=channel_type, account_id=account_id, ) result = await use_cases.health_check_control.getSingleHealth(query, operator=operator) raiseOnControlFailure(result) return {"success": True, "data": serialize_control_data(result.data)} @health_router.post( "/{channel_type}/{account_id}/probe", response_model=dict, ) async def probe_channel( channel_type: ChannelType, account_id: str, request: Request, body: ProbeRequest, use_cases=Depends(get_channel_use_cases), current_user: User = Depends(get_admin_user), ) -> dict[str, Any]: """主动深度探测指定渠道(HLT-PROBE)。 对应控制面操作 ``health/probe``(由 ``ChannelControlService.probeChannelDeep`` 内部构造 ``ControlCmd`` 并委托 ``_executeControl`` 执行控制面管道)。 管理员发起的主动深度探测,通过插件 ``ProbeableAdapter.probeDeep`` 执行深度探测(``connectivity`` 仅测连通性,``full`` 含凭据 / 权限 / Webhook 全项检查)。渠道未实现 ``probeDeep`` 时控制面返回 501。 编排链路:HTTP 入参 → ``build_operator`` 构造操作人 → 构造 ``ProbeCmd`` → ``use_cases.health_check_control.probeChannelDeep`` 调用控制面端口方法 → ``ControlResult`` 经 ``raiseOnControlFailure`` 转译失败 → ``serialize_control_data`` 序列化探测结果返回。 采用模板 A(控制面):经 ``health_check_control`` 端口调用 ``ChannelControlService`` 类型化方法,走控制面管道五阶段。探测可触发 副作用(调用渠道 API),审计事务策略 INDEPENDENT(best-effort)。 """ operator = build_operator(current_user, request) cmd = ProbeCmd( channel_type=channel_type, account_id=account_id, operator=operator, probe_type=body.probe_type, timeout_ms=body.timeout_ms, ) result = await use_cases.health_check_control.probeChannelDeep(cmd) raiseOnControlFailure(result) return {"success": True, "data": serialize_control_data(result.data)}