Implement webhook
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
"""Administration API for deployment-wide settings."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import UTC, datetime
|
||||
import uuid
|
||||
|
||||
from db.models import SiteSetting
|
||||
from db.session import get_session
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from schemas import (
|
||||
AnalysisWebhookSettingOut,
|
||||
AnalysisWebhookSettingUpdate,
|
||||
AnalysisWebhookTestIn,
|
||||
AnalysisWebhookTestOut,
|
||||
)
|
||||
from services.auth import require_admin
|
||||
from services.webhooks.delivery import deliver_webhook
|
||||
from services.webhooks.events import ANALYSIS_WEBHOOK_KEY
|
||||
from services.webhooks.security import UnsafeWebhookUrl, validate_webhook_url
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
|
||||
router = APIRouter(
|
||||
prefix="/api/settings",
|
||||
tags=["site-settings"],
|
||||
dependencies=[Depends(require_admin)],
|
||||
)
|
||||
|
||||
|
||||
def _setting_out(row: SiteSetting | None) -> AnalysisWebhookSettingOut:
|
||||
values = dict(row.value or {}) if row else {}
|
||||
secrets = dict(row.secrets or {}) if row else {}
|
||||
return AnalysisWebhookSettingOut(
|
||||
enabled=bool(values.get("enabled")),
|
||||
url=str(values.get("url") or ""),
|
||||
secret_configured=bool(secrets.get("secret")),
|
||||
updated_at=row.updated_at if row else None,
|
||||
)
|
||||
|
||||
|
||||
async def _safe_url_or_422(url: str) -> str:
|
||||
try:
|
||||
return await validate_webhook_url(url)
|
||||
except UnsafeWebhookUrl as exc:
|
||||
raise HTTPException(422, str(exc)) from exc
|
||||
|
||||
|
||||
@router.get("/analysis-webhook", response_model=AnalysisWebhookSettingOut)
|
||||
async def get_analysis_webhook(session: AsyncSession = Depends(get_session)):
|
||||
return _setting_out(await session.get(SiteSetting, ANALYSIS_WEBHOOK_KEY))
|
||||
|
||||
|
||||
@router.put("/analysis-webhook", response_model=AnalysisWebhookSettingOut)
|
||||
async def update_analysis_webhook(
|
||||
body: AnalysisWebhookSettingUpdate,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
row = await session.get(SiteSetting, ANALYSIS_WEBHOOK_KEY)
|
||||
stored_secrets = dict(row.secrets or {}) if row else {}
|
||||
incoming_secret = (body.secret or "").strip()
|
||||
if body.clear_secret:
|
||||
stored_secrets.pop("secret", None)
|
||||
if incoming_secret:
|
||||
stored_secrets["secret"] = incoming_secret
|
||||
|
||||
if body.url:
|
||||
await _safe_url_or_422(body.url)
|
||||
if body.enabled and not stored_secrets.get("secret"):
|
||||
raise HTTPException(422, "启用 Webhook 时必须配置签名密钥")
|
||||
|
||||
if row is None:
|
||||
row = SiteSetting(key=ANALYSIS_WEBHOOK_KEY)
|
||||
session.add(row)
|
||||
row.value = {"enabled": body.enabled, "url": body.url}
|
||||
row.secrets = stored_secrets
|
||||
await session.commit()
|
||||
await session.refresh(row)
|
||||
return _setting_out(row)
|
||||
|
||||
|
||||
@router.post("/analysis-webhook/test", response_model=AnalysisWebhookTestOut)
|
||||
async def test_analysis_webhook(
|
||||
body: AnalysisWebhookTestIn,
|
||||
session: AsyncSession = Depends(get_session),
|
||||
):
|
||||
url = await _safe_url_or_422(body.url)
|
||||
row = await session.get(SiteSetting, ANALYSIS_WEBHOOK_KEY)
|
||||
stored_secret = str((row.secrets or {}).get("secret") or "") if row else ""
|
||||
secret = (body.secret or "").strip() or stored_secret
|
||||
if not secret:
|
||||
raise HTTPException(422, "请先填写或保存签名密钥")
|
||||
|
||||
event_id = f"evt_{uuid.uuid4().hex}"
|
||||
payload = {
|
||||
"id": event_id,
|
||||
"event": "call.analysis.completed",
|
||||
"timestamp": datetime.now(UTC).isoformat(),
|
||||
"data": {
|
||||
"conversationId": "conv_test_001",
|
||||
"assistantId": "asst_test_001",
|
||||
"assistantName": "Webhook 测试助手",
|
||||
"analysis": {
|
||||
"customer_intent": "high",
|
||||
"need_follow_up": True,
|
||||
},
|
||||
},
|
||||
}
|
||||
result = await deliver_webhook(
|
||||
url=url,
|
||||
secret=secret,
|
||||
event_id=event_id,
|
||||
payload=payload,
|
||||
)
|
||||
message = (
|
||||
f"测试事件已送达,目标返回 HTTP {result.status_code}。"
|
||||
if result.ok
|
||||
else result.error or "测试事件发送失败。"
|
||||
)
|
||||
return AnalysisWebhookTestOut(
|
||||
ok=result.ok,
|
||||
status_code=result.status_code,
|
||||
latency_ms=result.latency_ms,
|
||||
message=message,
|
||||
payload=payload,
|
||||
)
|
||||
Reference in New Issue
Block a user