From b7ceeb11f31cc277dee4cb572312fd227fb16b03 Mon Sep 17 00:00:00 2001 From: Kris <2893855659@qq.com> Date: Thu, 9 Jul 2026 04:21:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(webhook/content-review):=20=E6=96=B0?= =?UTF-8?q?=E5=A2=9E=E5=8A=9F=E8=83=BD=E4=B8=8E=E6=9F=A5=E8=AF=A2=E8=BF=87?= =?UTF-8?q?=E6=BB=A4=E8=83=BD=E5=8A=9B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 为webhook入站请求合并URL query参数到headers,支持微信公众号等渠道的验签 2. 为内容审核列表接口新增resource_type和trace_id查询过滤支持 --- .../routers/channels/content_review_router.py | 35 +++++++++++++++++-- .../server/routers/channels/webhook_router.py | 11 ++++++ 2 files changed, 43 insertions(+), 3 deletions(-) diff --git a/backend/server/routers/channels/content_review_router.py b/backend/server/routers/channels/content_review_router.py index 55ecba29..562351d2 100644 --- a/backend/server/routers/channels/content_review_router.py +++ b/backend/server/routers/channels/content_review_router.py @@ -158,6 +158,23 @@ def _parse_verdict(raw: str) -> ContentReviewVerdict: ) from exc +def _parse_resource_type(raw: str) -> ContentReviewResourceType: + """string → ContentReviewResourceType,失败抛 ValidationError。 + + 将 CR-02 的 ``resource_type`` 查询参数(string)翻译为 + ``ContentReviewResourceType`` 枚举;非法值由全局异常处理器映射为 + 400 ``VALIDATION_ERROR``。 + """ + try: + return ContentReviewResourceType(raw) + except ValueError as exc: + raise ValidationError( + "resource_type", + f"unsupported resource_type: {raw} " + "(expected: message_text | message_attachment | user_profile)", + ) from exc + + # ---------------- 静态路径端点(须先于动态路径声明) ---------------- @@ -211,6 +228,14 @@ async def list_review_history( default=None, description="按审核结论过滤:pass / review / block", ), + resource_type: str | None = Query( + default=None, + description="按资源类型过滤:message_text / message_attachment / user_profile", + ), + trace_id: str | None = Query( + default=None, + description="按链路追踪 ID 过滤", + ), start_time: str | None = Query( default=None, description="起始时间过滤(ISO 8601,含)", @@ -228,11 +253,13 @@ async def list_review_history( 非控制面管道路径(模板 B):由 ``ContentReviewQueryService`` 直接读取 ``ContentReviewRepositoryPort``,不经控制面管道,不写审计日志(查询 - 操作无副作用)。``verdict`` / ``start_time`` / ``end_time`` 查询参数 - 经协议翻译辅助函数转换为枚举 / datetime,失败抛 ``ValidationError``。 + 操作无副作用)。``verdict`` / ``resource_type`` / ``start_time`` / + ``end_time`` 查询参数经协议翻译辅助函数转换为枚举 / datetime,失败抛 + ``ValidationError``。 编排链路:HTTP 入参 → 协议翻译(``_parse_verdict`` / - ``parse_datetime``)→ 构造 ``ContentReviewHistoryQueryCmd`` + ``_parse_resource_type`` / ``parse_datetime``)→ 构造 + ``ContentReviewHistoryQueryCmd`` (DTO ``__post_init__`` 校验 limit/offset/time_range)→ ``use_cases.content_review_query.listHistory`` → ``dataclass_to_dict`` 序列化 ``ContentReviewHistoryList`` 返回。 @@ -242,6 +269,8 @@ async def list_review_history( channel_type=channel_type, account_id=account_id, verdict=_parse_verdict(verdict) if verdict is not None else None, + resource_type=_parse_resource_type(resource_type) if resource_type is not None else None, + trace_id=trace_id, start_time=parse_datetime("start_time", start_time), end_time=parse_datetime("end_time", end_time), limit=limit, diff --git a/backend/server/routers/channels/webhook_router.py b/backend/server/routers/channels/webhook_router.py index 53582ebe..67c37fa8 100644 --- a/backend/server/routers/channels/webhook_router.py +++ b/backend/server/routers/channels/webhook_router.py @@ -23,6 +23,7 @@ from yuxi.channels.contract.dtos.channel import ChannelType, WebhookTestCmd from yuxi.channels.contract.dtos.inbound import ReceiveInboundCmd from yuxi.channels.contract.errors import InternalError, ValidationError from yuxi.storage.postgres.models_business import User +from yuxi.utils.crypto import SENSITIVE_HTTP_HEADERS from yuxi.utils.trace_context import get_trace_id from server.routers.channels import ( @@ -140,6 +141,16 @@ async def receive_webhook( "webhook body is not valid UTF-8", ) from exc sanitized_headers = sanitize_headers(request.headers) + # 合并 URL query 参数到 headers(HTTP headers 优先,query 参数补充)。 + # 部分渠道(如微信公众号)将签名 / 时间戳 / nonce 等放在 URL query 中 + # 而非 HTTP headers,合并后供入站管道 ``verifySignature`` 校验。 + # 合并时跳过敏感字段(如 token / authorization),避免 query 参数绕过 + # ``sanitize_headers`` 过滤导致敏感信息泄露到日志。 + for key, value in request.query_params.items(): + if key.lower() in SENSITIVE_HTTP_HEADERS: + continue + if key not in sanitized_headers: + sanitized_headers[key] = value cmd = ReceiveInboundCmd( channel_type=channel_type, raw_event=raw_event,