ForcePilot/backend/server/routers/channels/health_router.py
Kris bba1775220 refactor(channel-router): 清理冗余空行并优化代码结构
本次提交包含多类优化:
1.  移除多个路由文件中多余的空导入行,统一代码格式
2.  重构Query参数定义,将长参数拆分为多行提升可读性
3.  新增多个业务端点:
    - 渠道能力画像矩阵查询CAP-03
    - 配对审批计数接口用于待办角标
    - 批量查询对端目录资料接口
    - 向导扫码登录相关端点
    - 会话实时事件SSE推送端点
    - 工作台待办统计接口
4.  完善异常处理逻辑,补充OperationTimeoutError导入并优化NotImplementedError的细节返回
5.  调整路由导入顺序,修复动态路由路径冲突隐患
6.  更新文档注释与接口清单,修正部分接口描述细节
2026-07-06 20:50:03 +08:00

309 lines
14 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""健康检查域 RouterHLT-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-01FR-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 入参 → 构造 HealthQuerywith_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-02FR-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
构造 HealthQuerychannel_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-01Router 层 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-01FR-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-PROBERouter 层 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审计事务策略 INDEPENDENTbest-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)}