Files
gpu-services/nanoclaude/routers/synology.py
Hyungi Ahn a44f6446cf feat: NanoClaude Phase 3 — Synology Chat 연동
- POST /webhook/synology: outgoing webhook 수신 + token 검증
- 파이프라인 완료 시 incoming webhook으로 응답 자동 전송
- "분석 중..." typing 메시지 선전송
- 응답 길이 1500자 제한 (Synology Chat 제한 대응)
- 에러/실패 시에도 사용자에게 알림 메시지 전송
- 중복 요청 방지 (30초 TTL dedup)
- Synology에서 rewrite 이벤트 숨김 (SSE에서만 노출)
- callback 구조로 확장 가능 (Slack, Discord 등)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 12:25:48 +09:00

79 lines
2.5 KiB
Python

"""Synology Chat webhook — outgoing webhook 수신 + 파이프라인 연결."""
from __future__ import annotations
import logging
import time
from fastapi import APIRouter, Request
from fastapi.responses import JSONResponse
from config import settings
from services.job_manager import job_manager
from services import job_queue as jq_module
from services.state_stream import state_stream
from services.synology_sender import send_to_synology
logger = logging.getLogger(__name__)
router = APIRouter(tags=["synology"])
# 중복 요청 방지 (retry 대비) — {user_id}:{timestamp} → expire time
_recent: dict[str, float] = {}
DEDUP_TTL = 30.0 # 30초 내 동일 요청 무시
def _cleanup_recent():
now = time.time()
expired = [k for k, v in _recent.items() if now - v > DEDUP_TTL]
for k in expired:
del _recent[k]
@router.post("/webhook/synology")
async def synology_webhook(request: Request):
"""Synology Chat outgoing webhook 수신."""
if not settings.synology_incoming_url:
return JSONResponse(status_code=503, content={"detail": "Synology integration disabled"})
# Parse form data
form = await request.form()
token = form.get("token", "")
text = form.get("text", "")
username = form.get("username", "")
user_id = form.get("user_id", "")
timestamp = form.get("timestamp", "")
# Token 검증
if token != settings.synology_outgoing_token:
logger.warning("Invalid Synology token from %s", username)
return JSONResponse(status_code=403, content={"detail": "Invalid token"})
if not text or not text.strip():
return JSONResponse(status_code=200, content={"text": "빈 메시지입니다."})
# 중복 요청 방지
_cleanup_recent()
dedup_key = f"{user_id}:{timestamp}"
if dedup_key in _recent:
logger.info("Duplicate webhook ignored: %s", dedup_key)
return JSONResponse(status_code=200, content={})
_recent[dedup_key] = time.time()
# Job 생성
job = job_manager.create(text.strip())
job.callback = "synology"
job.callback_meta = {"username": username, "user_id": user_id}
state_stream.create(job.id)
logger.info("Synology job %s from %s: %s", job.id, username, text[:50])
# "처리 중" 메시지 먼저 전송 (typing 느낌)
await send_to_synology(f"🤖 분석 중... (from {username})")
# 파이프라인 시작 (비동기)
await jq_module.job_queue.submit(job)
# 즉시 200 반환 (Synology는 빠른 응답 기대)
return JSONResponse(status_code=200, content={})