import asyncio import json import logging import time from fastapi import APIRouter, Request from fastapi.responses import JSONResponse, PlainTextResponse from yuxi.channel.extensions.generic_webhook.auth_adapter import get_auth_adapter from yuxi.channel.extensions.generic_webhook.security import GenericWebhookSecurity from yuxi.channel.extensions.generic_webhook.types import InboundPayload logger = logging.getLogger("yuxi.channel.generic_webhook") router = APIRouter(prefix="/webhook/generic", tags=["generic-webhook"]) _config_mgr = None _deduplicator = None _mapping_engine = None MAX_BODY_SIZE = 1024 * 1024 def set_runtime_state(*, config_mgr, deduplicator, mapping_engine): global _config_mgr, _deduplicator, _mapping_engine _config_mgr = config_mgr _deduplicator = deduplicator _mapping_engine = mapping_engine def _process_inbound(inbound: InboundPayload, endpoint) -> dict: result = _mapping_engine.apply(inbound, endpoint.mapping) if result is None: logger.debug("端点 %s: 事件被过滤规则丢弃", endpoint.endpoint_id) return {"status": "ok", "filtered": True} msg_id = result.unified.msg_id if _deduplicator.is_duplicate(endpoint.endpoint_id, msg_id): logger.debug("端点 %s: 重复事件 %s 已忽略", endpoint.endpoint_id, msg_id) return {"status": "ok", "deduplicated": True} logger.info( "端点 %s: 事件 %s 已接收 (extracted=%d, missing=%d, defaulted=%d)", endpoint.endpoint_id, msg_id, len(result.fields_extracted), len(result.fields_missing), len(result.fields_defaulted), ) security = GenericWebhookSecurity(endpoint) if not security.authorize(result.unified.sender.id): logger.debug("端点 %s: 发送者 %s 未通过 DM 安全策略", endpoint.endpoint_id, result.unified.sender.id) return {"status": "ok", "unauthorized": True} asyncio.create_task( _dispatch_to_agent(result.unified, endpoint), name=f"generic-dispatch-{endpoint.endpoint_id}-{msg_id}", ) return {"status": "ok", "msg_id": msg_id} async def generic_webhook_handler(payload: dict) -> dict: endpoint_id = payload.get("endpoint_id", "") if not endpoint_id: return {"error": "缺少 endpoint_id"} endpoint = _config_mgr.get_endpoint(endpoint_id) if not endpoint: return {"error": f"端点 '{endpoint_id}' 不存在", "status": 404} if not endpoint.enabled: return {"error": "端点已禁用", "status": 503} body = payload.get("_raw_body", b"") payload_data = payload.get("body") or {} headers = payload.get("_headers", {}) inbound = InboundPayload( endpoint_id=endpoint_id, body=payload_data, headers=headers, raw_body=body if isinstance(body, bytes) else json.dumps(payload_data).encode(), received_at=time.monotonic(), ) return _process_inbound(inbound, endpoint) @router.post("/{endpoint_id:path}") async def generic_webhook_receive(endpoint_id: str, request: Request): start_time = time.monotonic() endpoint = _config_mgr.get_endpoint(endpoint_id) if not endpoint: return JSONResponse({"error": f"端点 '{endpoint_id}' 不存在"}, status_code=404) if not endpoint.enabled: return JSONResponse({"error": "端点已禁用"}, status_code=503) try: body = await asyncio.wait_for(request.body(), timeout=5.0) except TimeoutError: return JSONResponse({"error": "读取请求体超时"}, status_code=408) if len(body) > MAX_BODY_SIZE: return JSONResponse({"error": f"请求体超过 {MAX_BODY_SIZE // 1024}KB 限制"}, status_code=413) adapter = get_auth_adapter(endpoint.auth.type) auth_result = await adapter.verify(request, body, endpoint.auth) if not auth_result.allowed: logger.warning("端点 %s: 认证失败 - %s", endpoint_id, auth_result.reason) return JSONResponse({"error": auth_result.reason}, status_code=auth_result.error_code) try: payload_data = json.loads(body) except json.JSONDecodeError: return JSONResponse({"error": "请求体不是有效的 JSON"}, status_code=400) if not isinstance(payload_data, dict): return JSONResponse({"error": "请求体必须是 JSON 对象"}, status_code=400) inbound = InboundPayload( endpoint_id=endpoint_id, body=payload_data, headers=dict(request.headers), raw_body=body, received_at=start_time, ) result = _process_inbound(inbound, endpoint) if result.get("filtered") or result.get("deduplicated") or result.get("unauthorized"): return PlainTextResponse("ok") logger.info( "端点 %s: 延迟 %.0fms", endpoint_id, (time.monotonic() - start_time) * 1000, ) return PlainTextResponse("ok") async def _dispatch_to_agent(msg, endpoint) -> None: from yuxi.channel.runtime.manager import gateway processor = gateway._processor if processor is None: logger.error("Message processor 不可用,无法分发端点 %s 的消息", endpoint.endpoint_id) return try: await asyncio.wait_for(processor.process(msg), timeout=120.0) except TimeoutError: logger.error("Agent 回复超时,端点 %s,sender %s", endpoint.endpoint_id, msg.sender.id) except Exception: logger.exception("处理端点 %s 消息失败,sender %s", endpoint.endpoint_id, msg.sender.id)