ForcePilot/backend/server/routers/channels/health_router.py
Kris e5e9f45411 refactor(channel-routers): 批量优化各渠道路由代码与契约对齐
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相关端点,重构路由路径与校验逻辑
2026-07-04 00:16:00 +08:00

322 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)}