feat(webhook/content-review): 新增功能与查询过滤能力
1. 为webhook入站请求合并URL query参数到headers,支持微信公众号等渠道的验签 2. 为内容审核列表接口新增resource_type和trace_id查询过滤支持
This commit is contained in:
parent
b8ac375e8e
commit
b7ceeb11f3
@ -158,6 +158,23 @@ def _parse_verdict(raw: str) -> ContentReviewVerdict:
|
|||||||
) from exc
|
) 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,
|
default=None,
|
||||||
description="按审核结论过滤:pass / review / block",
|
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(
|
start_time: str | None = Query(
|
||||||
default=None,
|
default=None,
|
||||||
description="起始时间过滤(ISO 8601,含)",
|
description="起始时间过滤(ISO 8601,含)",
|
||||||
@ -228,11 +253,13 @@ async def list_review_history(
|
|||||||
|
|
||||||
非控制面管道路径(模板 B):由 ``ContentReviewQueryService`` 直接读取
|
非控制面管道路径(模板 B):由 ``ContentReviewQueryService`` 直接读取
|
||||||
``ContentReviewRepositoryPort``,不经控制面管道,不写审计日志(查询
|
``ContentReviewRepositoryPort``,不经控制面管道,不写审计日志(查询
|
||||||
操作无副作用)。``verdict`` / ``start_time`` / ``end_time`` 查询参数
|
操作无副作用)。``verdict`` / ``resource_type`` / ``start_time`` /
|
||||||
经协议翻译辅助函数转换为枚举 / datetime,失败抛 ``ValidationError``。
|
``end_time`` 查询参数经协议翻译辅助函数转换为枚举 / datetime,失败抛
|
||||||
|
``ValidationError``。
|
||||||
|
|
||||||
编排链路:HTTP 入参 → 协议翻译(``_parse_verdict`` /
|
编排链路:HTTP 入参 → 协议翻译(``_parse_verdict`` /
|
||||||
``parse_datetime``)→ 构造 ``ContentReviewHistoryQueryCmd``
|
``_parse_resource_type`` / ``parse_datetime``)→ 构造
|
||||||
|
``ContentReviewHistoryQueryCmd``
|
||||||
(DTO ``__post_init__`` 校验 limit/offset/time_range)→
|
(DTO ``__post_init__`` 校验 limit/offset/time_range)→
|
||||||
``use_cases.content_review_query.listHistory`` →
|
``use_cases.content_review_query.listHistory`` →
|
||||||
``dataclass_to_dict`` 序列化 ``ContentReviewHistoryList`` 返回。
|
``dataclass_to_dict`` 序列化 ``ContentReviewHistoryList`` 返回。
|
||||||
@ -242,6 +269,8 @@ async def list_review_history(
|
|||||||
channel_type=channel_type,
|
channel_type=channel_type,
|
||||||
account_id=account_id,
|
account_id=account_id,
|
||||||
verdict=_parse_verdict(verdict) if verdict is not None else None,
|
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),
|
start_time=parse_datetime("start_time", start_time),
|
||||||
end_time=parse_datetime("end_time", end_time),
|
end_time=parse_datetime("end_time", end_time),
|
||||||
limit=limit,
|
limit=limit,
|
||||||
|
|||||||
@ -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.dtos.inbound import ReceiveInboundCmd
|
||||||
from yuxi.channels.contract.errors import InternalError, ValidationError
|
from yuxi.channels.contract.errors import InternalError, ValidationError
|
||||||
from yuxi.storage.postgres.models_business import User
|
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 yuxi.utils.trace_context import get_trace_id
|
||||||
|
|
||||||
from server.routers.channels import (
|
from server.routers.channels import (
|
||||||
@ -140,6 +141,16 @@ async def receive_webhook(
|
|||||||
"webhook body is not valid UTF-8",
|
"webhook body is not valid UTF-8",
|
||||||
) from exc
|
) from exc
|
||||||
sanitized_headers = sanitize_headers(request.headers)
|
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(
|
cmd = ReceiveInboundCmd(
|
||||||
channel_type=channel_type,
|
channel_type=channel_type,
|
||||||
raw_event=raw_event,
|
raw_event=raw_event,
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user