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