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

132 lines
3.9 KiB
Python

from __future__ import annotations
import time
from typing import Any
from yuxi.channels.adapters.bluebubbles.client import BlueBubblesClient
from yuxi.channels.models import HealthStatus
class ServerInfoCache:
MAX_ENTRIES = 64
TTL_SECONDS = 600
def __init__(self) -> None:
self._entries: dict[str, tuple[float, Any]] = {}
def get(self, server_url: str) -> Any | None:
entry = self._entries.get(server_url)
if entry is None:
return None
ts, value = entry
if time.monotonic() - ts > self.TTL_SECONDS:
del self._entries[server_url]
return None
return value
def set(self, server_url: str, value: Any) -> None:
if len(self._entries) >= self.MAX_ENTRIES:
now = time.monotonic()
expired = [k for k, v in self._entries.items() if now - v[0] > self.TTL_SECONDS]
for k in expired:
del self._entries[k]
if len(self._entries) >= self.MAX_ENTRIES:
oldest = sorted(self._entries.items(), key=lambda x: x[1][0])[0]
del self._entries[oldest[0]]
self._entries[server_url] = (time.monotonic(), value)
def invalidate(self, server_url: str) -> None:
self._entries.pop(server_url, None)
_server_info_cache = ServerInfoCache()
async def probe_server(client: BlueBubblesClient, use_cache: bool = True) -> dict[str, Any]:
if use_cache:
cached = _server_info_cache.get(client.base_url)
if cached is not None:
return cached
result = await client.get("/api/v1/server/info")
if use_cache:
_server_info_cache.set(client.base_url, result)
return result
async def probe_imessage_login(client: BlueBubblesClient) -> HealthStatus:
try:
server_info = await probe_server(client)
if server_info.get("status") != "ok":
return HealthStatus(
status="unhealthy",
last_error="BlueBubbles Server not responding",
)
data = server_info.get("data", {})
imessage_connected = data.get("iMessageConnected", False)
if not imessage_connected:
return HealthStatus(
status="degraded",
last_error="iMessage not connected on server",
metadata={"server_info": data},
)
return HealthStatus(
status="healthy",
metadata={"server_info": data},
)
except Exception as e:
return HealthStatus(
status="unhealthy",
last_error=f"Health check failed: {e}",
)
async def probe_private_api(client: BlueBubblesClient) -> bool:
try:
server_info = await probe_server(client)
data = server_info.get("data", {})
return bool(data.get("private_api", False))
except Exception:
return False
async def probe_macos_version(client: BlueBubblesClient) -> str:
try:
server_info = await probe_server(client)
data = server_info.get("data", {})
return data.get("os_version", "unknown")
except Exception:
return "unknown"
def is_macos_26_or_higher(version: str) -> bool:
if version == "unknown":
return False
try:
parts = version.split(".")
if len(parts) >= 2:
return int(parts[0]) >= 26
return False
except ValueError:
return False
async def check_server_reachable(server_url: str, password: str, timeout: float = 10.0) -> HealthStatus:
client = BlueBubblesClient(server_url=server_url, password=password, timeout=timeout)
try:
async with client:
return await probe_imessage_login(client)
except Exception as e:
return HealthStatus(
status="unhealthy",
last_error=f"Cannot reach BlueBubbles Server: {e}",
)
def get_server_info_cache() -> ServerInfoCache:
return _server_info_cache