ForcePilot/backend/package/yuxi/channel/extensions/generic_webhook/webhook.py

159 lines
5.4 KiB
Python
Raw Normal View History

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 回复超时,端点 %ssender %s", endpoint.endpoint_id, msg.sender.id)
except Exception:
logger.exception("处理端点 %s 消息失败sender %s", endpoint.endpoint_id, msg.sender.id)