ForcePilot/backend/server/routers/webhook_router.py

143 lines
5.6 KiB
Python
Raw Normal View History

from __future__ import annotations
import asyncio
from fastapi import APIRouter, Request, status
from fastapi.responses import JSONResponse
from yuxi.channels.manager import channel_manager
from yuxi.channels.registry import BUILTIN_ADAPTERS
from yuxi.utils.logging_config import logger
webhook = APIRouter(prefix="/webhook", tags=["webhook"])
@webhook.post("/feishu")
async def feishu_webhook(request: Request):
adapter = channel_manager._adapters.get("feishu")
if not adapter:
return JSONResponse(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
content={"code": 503, "data": None, "message": "Feishu channel is not running"},
)
body = await request.body()
headers = dict(request.headers)
source_ip = request.client.host if request.client else "default"
try:
from yuxi.channels.adapters.feishu.webhook_server import handle_feishu_webhook
result = await handle_feishu_webhook(body, headers, adapter, source_ip)
code = result.get("code", 0)
if code == 0:
response_content = {"code": 0, "data": None, "message": "ok"}
if "challenge" in result:
response_content["challenge"] = result["challenge"]
return JSONResponse(content=response_content)
if code == 413:
return JSONResponse(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
content={"code": 413, "message": result.get("message", "")},
)
if code == 403:
return JSONResponse(
status_code=status.HTTP_403_FORBIDDEN,
content={"code": 403, "message": result.get("message", "")},
)
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"code": 400, "message": result.get("message", "")},
)
except Exception as e:
logger.error(f"[FeishuWebhook] Handler error: {e}")
return JSONResponse(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
content={"code": 500, "message": str(e)},
)
@webhook.post("/{channel_type}")
async def webhook_dispatch(channel_type: str, request: Request):
adapter = channel_manager._adapters.get(channel_type)
adapter_cls = BUILTIN_ADAPTERS.get(channel_type)
if not adapter:
if adapter_cls and adapter_cls.webhook_path:
return JSONResponse(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
content={"code": 503, "data": None, "message": f"Channel '{channel_type}' is not running"},
)
return JSONResponse(
status_code=status.HTTP_404_NOT_FOUND,
content={"code": 404, "data": None, "message": f"Webhook channel '{channel_type}' not found"},
)
body = await request.body()
headers = dict(request.headers)
source_ip = request.client.host if request.client else "unknown"
adapter._last_webhook_headers = headers
adapter._last_source_ip = source_ip
max_body_bytes = getattr(adapter, "_MAX_WEBHOOK_BODY_BYTES", 256 * 1024)
if len(body) > max_body_bytes:
logger.warning(f"Webhook body too large for {channel_type}: {len(body)} > {max_body_bytes}")
return JSONResponse(
status_code=status.HTTP_413_REQUEST_ENTITY_TOO_LARGE,
content={"code": 413, "message": "Request body too large"},
)
try:
if not await adapter.verify_webhook_signature(headers, body):
return JSONResponse(
status_code=status.HTTP_403_FORBIDDEN,
content={"code": 403, "data": None, "message": "Invalid webhook signature"},
)
except NotImplementedError:
pass
import json
try:
body_data = json.loads(body)
except json.JSONDecodeError as e:
logger.error(f"Webhook JSON decode failed for {channel_type}: {e}")
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"code": 400, "data": None, "message": f"Invalid JSON payload: {e}"},
)
try:
if hasattr(adapter, "handle_webhook"):
handle_timeout = getattr(adapter, "_WEBHOOK_HANDLE_TIMEOUT_SECONDS", 30)
message = await asyncio.wait_for(adapter.handle_webhook(body_data), timeout=handle_timeout)
if message is None or isinstance(message, int):
if isinstance(message, int):
return JSONResponse(
status_code=message,
content={"code": message, "message": "Webhook rejected"},
)
return JSONResponse(content={"code": 0, "data": None, "message": "ok"})
asyncio.create_task(adapter._handle_message(message))
return JSONResponse(content={"code": 0, "data": None, "message": "ok"})
else:
message = adapter.normalize_inbound(body_data)
except asyncio.TimeoutError:
logger.error(f"Webhook processing timed out for {channel_type}")
return JSONResponse(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
content={"code": 503, "message": "Webhook processing timed out"},
)
except Exception as e:
logger.error(f"Failed to parse webhook for {channel_type}: {e}")
return JSONResponse(
status_code=status.HTTP_400_BAD_REQUEST,
content={"code": 400, "data": None, "message": f"Failed to parse webhook payload: {e}"},
)
asyncio.create_task(adapter._handle_message(message))
return JSONResponse(content={"code": 0, "data": None, "message": "ok"})