此提交对Synology Chat适配器进行了全面改进: 1. 新增消息去重、分布式轮询租约、bot名称配置等功能 2. 优化URL提取逻辑,自动清理尾部标点符号 3. 重构发送逻辑,提取通用重试工具函数并优化SID缓存 4. 完善文档提示与配置项,新增轮询租约类型支持 5. 修复认证API路径硬编码问题,调整交互组件提示文案 6. 增加Webhook模式下的DSM客户端兜底初始化 7. 优化导入顺序与代码结构,清理冗余空行
103 lines
3.9 KiB
Python
103 lines
3.9 KiB
Python
"""Polling-based message monitoring loop for Synology Chat.
|
|
|
|
Continuously polls the DSM Chat API for new messages while the channel is connected,
|
|
with circuit breaker integration for fault isolation, optional security filtering,
|
|
and per-account inflight limiting to prevent resource exhaustion across accounts.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import asyncio
|
|
from collections import defaultdict
|
|
from collections.abc import Awaitable, Callable
|
|
from typing import Any
|
|
|
|
from yuxi.channels.adapters.synologychat.client import DSMClient
|
|
from yuxi.channels.adapters.synologychat.dedup import MessageDeduplicator
|
|
from yuxi.channels.infra.circuit_breaker import CircuitBreaker, CircuitBreakerOpenError
|
|
from yuxi.channels.models import ChannelMessage, ChannelStatus
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
|
|
async def polling_loop(
|
|
client: DSMClient,
|
|
config: dict[str, Any],
|
|
normalize_fn: Callable[[dict], ChannelMessage],
|
|
handle_message_fn: Callable[[ChannelMessage], Awaitable[None]],
|
|
get_status_fn: Callable[[], ChannelStatus],
|
|
circuit_breaker: CircuitBreaker,
|
|
security_filter: Callable[[ChannelMessage], bool] | None = None,
|
|
max_inflight: int = 10,
|
|
account_id: str = "",
|
|
dedup: MessageDeduplicator | None = None,
|
|
) -> None:
|
|
poll_interval = config.get("polling_interval_seconds", 3)
|
|
max_backoff = config.get("polling_max_backoff_seconds", 60)
|
|
last_cursor: str = ""
|
|
consecutive_failures = 0
|
|
inflight_count: dict[str, int] = defaultdict(int)
|
|
inflight_lock = asyncio.Lock()
|
|
|
|
async def _acquire_inflight() -> None:
|
|
while True:
|
|
async with inflight_lock:
|
|
if inflight_count[account_id] < max_inflight:
|
|
inflight_count[account_id] += 1
|
|
return
|
|
await asyncio.sleep(0.1)
|
|
|
|
async def _release_inflight() -> None:
|
|
async with inflight_lock:
|
|
inflight_count[account_id] = max(0, inflight_count[account_id] - 1)
|
|
|
|
async def _handle_with_limit(msg: ChannelMessage) -> None:
|
|
await _acquire_inflight()
|
|
try:
|
|
await handle_message_fn(msg)
|
|
finally:
|
|
await _release_inflight()
|
|
|
|
while get_status_fn() == ChannelStatus.CONNECTED:
|
|
try:
|
|
|
|
async def _poll():
|
|
return await client.poll_messages(last_cursor)
|
|
|
|
result = await circuit_breaker.call(_poll)
|
|
|
|
if result.get("success") and result.get("data"):
|
|
consecutive_failures = 0
|
|
data = result["data"]
|
|
events = data.get("events", [])
|
|
for event in events:
|
|
try:
|
|
message_id = str(event.get("message_id", ""))
|
|
if dedup and message_id and dedup.check_and_mark(message_id):
|
|
continue
|
|
message = normalize_fn(event)
|
|
if security_filter and not security_filter(message):
|
|
continue
|
|
await _handle_with_limit(message)
|
|
except Exception as e:
|
|
logger.error(f"Error handling poll event: {e}")
|
|
|
|
last_cursor = data.get("next_cursor", last_cursor)
|
|
|
|
except CircuitBreakerOpenError:
|
|
backoff = min(poll_interval * (2**consecutive_failures), max_backoff)
|
|
consecutive_failures += 1
|
|
logger.warning(f"[SynologyChat] Circuit breaker open, backing off {backoff}s")
|
|
await asyncio.sleep(backoff)
|
|
continue
|
|
except asyncio.CancelledError:
|
|
break
|
|
except Exception as e:
|
|
logger.error(f"Polling error: {e}")
|
|
await circuit_breaker.record_failure()
|
|
consecutive_failures += 1
|
|
backoff = min(poll_interval * (2**consecutive_failures), max_backoff)
|
|
await asyncio.sleep(backoff)
|
|
continue
|
|
|
|
await asyncio.sleep(poll_interval)
|