ForcePilot/backend/package/yuxi/channels/adapters/bluebubbles/monitor.py
Kris 6aa1d52a8b feat(bluebubbles): 实现完整的BlueBubbles适配器基础组件
新增BlueBubbles适配器全套核心工具类与服务,包含会话管理、消息去重、Webhook验证、缓存系统、账号配置解析、聊天消息处理等完整功能模块,支持iMessage消息收发、群管理、反应特效、语音合成等能力,提供完善的健康检查与配置校验流程。
2026-05-12 00:42:57 +08:00

139 lines
5.0 KiB
Python

from __future__ import annotations
import asyncio
import json
import random
import time
from collections.abc import Callable, Coroutine
from typing import Any
from yuxi.channels.adapters.bluebubbles.exceptions import BlueBubblesConnectionError
from yuxi.utils.logging_config import logger
try:
import websockets.asyncio.client as ws_client
except ImportError:
ws_client = None
class InboundReorderBuffer:
MAX_BUFFER_MS = 500
def __init__(self, max_buffer_ms: int | None = None):
self._buffer: list[dict[str, Any]] = []
self._flush_task: asyncio.Task | None = None
self._max_buffer_ms = max_buffer_ms or self.MAX_BUFFER_MS
self._on_flush: Callable[[list[dict[str, Any]]], Coroutine[Any, Any, None]] | None = None
def set_on_flush(self, handler: Callable[[list[dict[str, Any]]], Coroutine[Any, Any, None]]) -> None:
self._on_flush = handler
def enqueue(self, event: dict[str, Any]) -> None:
self._buffer.append(event)
if self._flush_task is None or self._flush_task.done():
self._flush_task = asyncio.create_task(self._delayed_flush())
async def _delayed_flush(self) -> None:
await asyncio.sleep(self._max_buffer_ms / 1000.0)
await self.flush()
async def flush(self) -> None:
if not self._buffer:
return
events = self._buffer[:]
self._buffer.clear()
events.sort(key=lambda e: self._extract_timestamp(e))
if self._on_flush:
for event in events:
await self._on_flush(event)
@staticmethod
def _extract_timestamp(event: dict[str, Any]) -> float:
data = event.get("data", {})
message = data.get("message", {})
ts = message.get("dateCreated", 0)
if isinstance(ts, (int, float)) and ts > 0:
return float(ts)
return time.time() * 1000
class BlueBubblesMonitor:
def __init__(
self,
server_url: str,
password: str,
on_event: Callable[[dict[str, Any]], Coroutine[Any, Any, None]],
on_disconnect: Callable[[], Coroutine[Any, Any, None]] | None = None,
on_reconnect: Callable[[], Coroutine[Any, Any, None]] | None = None,
):
self._password = password
self._on_event = on_event
self._on_disconnect = on_disconnect
self._on_reconnect = on_reconnect
ws_base = server_url.replace("http://", "ws://").replace("https://", "wss://")
self._ws_url = f"{ws_base.rstrip('/')}/ws?password={self._password}"
self._ws = None
self._running = False
self._reconnect_delay = 1.0
self._max_reconnect_delay = 60.0
self._task: asyncio.Task | None = None
async def start(self) -> None:
if ws_client is None:
raise BlueBubblesConnectionError("websockets library not installed")
self._running = True
self._reconnect_delay = 1.0
self._task = asyncio.create_task(self._listen_loop())
logger.info("[BlueBubbles] WebSocket monitor started")
async def stop(self) -> None:
self._running = False
if self._task and not self._task.done():
self._task.cancel()
try:
await self._task
except asyncio.CancelledError:
pass
self._task = None
if self._ws:
await self._ws.close()
self._ws = None
logger.info("[BlueBubbles] WebSocket monitor stopped")
async def _listen_loop(self) -> None:
while self._running:
try:
async for ws in ws_client.connect(self._ws_url):
self._ws = ws
self._reconnect_delay = 1.0
logger.info(f"[BlueBubbles] WebSocket connected: {self._ws_url}")
if self._on_reconnect:
await self._on_reconnect()
await self._handle_messages(ws)
except Exception as e:
if not self._running:
break
if self._on_disconnect:
try:
await self._on_disconnect()
except Exception:
logger.exception("[BlueBubbles] on_disconnect callback failed")
jitter = random.uniform(0, self._reconnect_delay * 0.5)
delay = self._reconnect_delay + jitter
logger.warning(f"[BlueBubbles] WebSocket disconnected, reconnecting in {delay:.1f}s: {e}")
await asyncio.sleep(delay)
self._reconnect_delay = min(
self._reconnect_delay * 2,
self._max_reconnect_delay,
)
async def _handle_messages(self, ws) -> None:
async for raw in ws:
try:
event = json.loads(raw)
await self._on_event(event)
except json.JSONDecodeError:
logger.warning("[BlueBubbles] Received invalid JSON from WebSocket")
except Exception:
logger.exception("[BlueBubbles] Error handling WebSocket event")