From a057e9a3587837a533e395c25bce0c4a4b51b64e Mon Sep 17 00:00:00 2001 From: Hyungi Ahn Date: Mon, 6 Apr 2026 15:05:16 +0900 Subject: [PATCH] =?UTF-8?q?fix:=20Synology=20Chat=20rate=20limit=20?= =?UTF-8?q?=EB=8C=80=EC=9D=91=20=E2=80=94=20=EC=B5=9C=EC=86=8C=201.5?= =?UTF-8?q?=EC=B4=88=20=EA=B0=84=EA=B2=A9=20+=20=EC=9E=AC=EC=8B=9C?= =?UTF-8?q?=EB=8F=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit "create post too fast" 에러로 응답이 누락되던 문제. 전송 간 최소 1.5초 간격 보장 + API success:false 시 2초 후 1회 재시도. Co-Authored-By: Claude Opus 4.6 (1M context) --- nanoclaude/services/synology_sender.py | 47 ++++++++++++++++++++++---- 1 file changed, 41 insertions(+), 6 deletions(-) diff --git a/nanoclaude/services/synology_sender.py b/nanoclaude/services/synology_sender.py index ee4a2a8..72d4df6 100644 --- a/nanoclaude/services/synology_sender.py +++ b/nanoclaude/services/synology_sender.py @@ -2,6 +2,7 @@ from __future__ import annotations +import asyncio import json import logging import re @@ -12,19 +13,25 @@ from config import settings logger = logging.getLogger(__name__) +# Synology Chat rate limit 대응: 최소 전송 간격 +_last_send_time: float = 0.0 +MIN_SEND_INTERVAL = 1.5 # 초 + def _strip_markdown(text: str) -> str: """마크다운 문법 제거 — Synology Chat은 렌더링 안 됨.""" - text = re.sub(r'\*\*(.+?)\*\*', r'\1', text) # **bold** - text = re.sub(r'\*(.+?)\*', r'\1', text) # *italic* - text = re.sub(r'`(.+?)`', r'\1', text) # `code` - text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE) # ### headers - text = re.sub(r'^\s*[-*]\s+', '• ', text, flags=re.MULTILINE) # - list → • + text = re.sub(r'\*\*(.+?)\*\*', r'\1', text) + text = re.sub(r'\*(.+?)\*', r'\1', text) + text = re.sub(r'`(.+?)`', r'\1', text) + text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE) + text = re.sub(r'^\s*[-*]\s+', '• ', text, flags=re.MULTILINE) return text async def send_to_synology(text: str, *, raw: bool = False) -> bool: - """Incoming webhook URL로 메시지 전송. raw=True면 마크다운 제거 안 함.""" + """Incoming webhook URL로 메시지 전송. rate limit 대응 포함.""" + global _last_send_time + if not settings.synology_incoming_url: logger.warning("Synology incoming URL not configured") return False @@ -33,15 +40,43 @@ async def send_to_synology(text: str, *, raw: bool = False) -> bool: text = _strip_markdown(text) payload = json.dumps({"text": text}, ensure_ascii=False) + # Rate limit 대응: 최소 간격 보장 + import time + now = time.time() + elapsed = now - _last_send_time + if elapsed < MIN_SEND_INTERVAL: + await asyncio.sleep(MIN_SEND_INTERVAL - elapsed) + try: async with httpx.AsyncClient(verify=False, timeout=10.0) as client: resp = await client.post( settings.synology_incoming_url, data={"payload": payload}, ) + _last_send_time = time.time() + if resp.status_code == 200: + body = resp.json() if resp.text else {} + if body.get("success") is False: + error_msg = body.get("error", {}).get("errors", "unknown") + logger.warning("Synology API error: %s — retrying", error_msg) + # 1회 재시도 + await asyncio.sleep(2.0) + resp2 = await client.post( + settings.synology_incoming_url, + data={"payload": payload}, + ) + _last_send_time = time.time() + body2 = resp2.json() if resp2.text else {} + if body2.get("success") is not False: + logger.info("Synology retry sent (%d chars): %s", len(text), text[:100]) + return True + logger.error("Synology retry also failed: %s", body2) + return False + logger.info("Synology sent (%d chars): %s", len(text), text[:100]) return True + logger.error("Synology send failed: %d %s", resp.status_code, resp.text) return False except Exception: