这是一个批量整理提交,包含以下主要改动: 1. 删除多处冗余的空行和未使用的导入 2. 修复文件末尾缺少换行符的问题 3. 调整部分模块的导入顺序与代码排版 4. 修复部分配置默认值与策略逻辑 5. 新增多个功能模块与辅助工具 6. 完善异常处理与日志记录 7. 修复速率限制、消息缓存、权限校验等逻辑bug 8. 废弃部分旧有API与配置项并添加警告提示
85 lines
3.2 KiB
Python
85 lines
3.2 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import logging
|
|
import random
|
|
from collections.abc import Callable, Awaitable
|
|
|
|
import aiohttp
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
MAX_RECONNECT_DELAY = 60.0
|
|
INITIAL_RECONNECT_DELAY = 1.0
|
|
JITTER = 0.1
|
|
MAX_RECONNECT_COUNT = 20
|
|
|
|
|
|
async def sse_event_stream(
|
|
url: str,
|
|
params: dict,
|
|
on_event: Callable[[str], Awaitable[None]],
|
|
) -> None:
|
|
reconnect_delay = INITIAL_RECONNECT_DELAY
|
|
last_event_id: str | None = None
|
|
unauth_backoff = False
|
|
retry_count = 0
|
|
|
|
while retry_count < MAX_RECONNECT_COUNT:
|
|
try:
|
|
headers: dict[str, str] = {"Accept": "text/event-stream"}
|
|
if last_event_id:
|
|
headers["Last-Event-ID"] = last_event_id
|
|
|
|
async with aiohttp.ClientSession() as session:
|
|
async with session.get(url, params=params, headers=headers) as response:
|
|
if response.status == 401:
|
|
unauth_backoff = True
|
|
logger.error("SSE connection received 401 Unauthorized, backing off for 30s")
|
|
await asyncio.sleep(30.0)
|
|
reconnect_delay = INITIAL_RECONNECT_DELAY
|
|
retry_count += 1
|
|
continue
|
|
|
|
if response.status != 200:
|
|
logger.error(f"SSE connection failed: {response.status}")
|
|
await asyncio.sleep(reconnect_delay)
|
|
reconnect_delay = min(reconnect_delay * 2, MAX_RECONNECT_DELAY)
|
|
retry_count += 1
|
|
continue
|
|
|
|
if unauth_backoff:
|
|
logger.info("SSE reconnected successfully after 401 backoff")
|
|
unauth_backoff = False
|
|
|
|
reconnect_delay = INITIAL_RECONNECT_DELAY
|
|
retry_count = 0
|
|
|
|
async for line in response.content:
|
|
line_text = line.decode("utf-8").strip()
|
|
|
|
if line_text.startswith("id:"):
|
|
last_event_id = line_text.removeprefix("id:").strip()
|
|
continue
|
|
|
|
if line_text.startswith("data:"):
|
|
event_data = line_text.removeprefix("data:").strip()
|
|
if event_data:
|
|
try:
|
|
await on_event(event_data)
|
|
except Exception:
|
|
logger.exception("Error handling SSE event")
|
|
|
|
except (TimeoutError, aiohttp.ClientError) as e:
|
|
logger.warning(f"SSE connection lost: {e}, reconnecting in {reconnect_delay:.1f}s")
|
|
jitter_ms = reconnect_delay * JITTER * random.random()
|
|
await asyncio.sleep(reconnect_delay + jitter_ms)
|
|
reconnect_delay = min(reconnect_delay * 2, MAX_RECONNECT_DELAY)
|
|
retry_count += 1
|
|
except asyncio.CancelledError:
|
|
logger.info("SSE event stream cancelled")
|
|
break
|
|
|
|
if retry_count >= MAX_RECONNECT_COUNT:
|
|
logger.error(f"SSE reconnect limit ({MAX_RECONNECT_COUNT}) reached, giving up")
|