Files
ai-video-fullstack/backend/routes/site_settings.py
2026-08-07 14:08:49 +08:00

127 lines
4.1 KiB
Python

"""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,
)