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

122 lines
3.8 KiB
Python

from __future__ import annotations
import asyncio
import time
from dataclasses import dataclass, field
from typing import Any
from yuxi.channels.adapters.bluebubbles.client import BlueBubblesClient
from yuxi.utils.logging_config import logger
@dataclass
class PooledClient:
client: BlueBubblesClient
last_used: float = field(default_factory=time.monotonic)
in_use: bool = False
request_count: int = 0
class ClientPool:
MAX_IDLE_SECONDS = 300
CLEANUP_INTERVAL = 60.0
DEFAULT_POOL_SIZE = 5
def __init__(self, pool_size: int | None = None):
self._pool: list[PooledClient] = []
self._max_size = pool_size or self.DEFAULT_POOL_SIZE
self._lock = asyncio.Lock()
self._cleanup_task: asyncio.Task | None = None
self._config: dict[str, Any] = {}
def configure(
self,
server_url: str,
password: str,
timeout: float = 30.0,
allow_private_network: bool = False,
) -> None:
self._config = {
"server_url": server_url,
"password": password,
"timeout": timeout,
"allow_private_network": allow_private_network,
}
async def acquire(self) -> BlueBubblesClient:
async with self._lock:
for entry in self._pool:
if not entry.in_use:
entry.in_use = True
entry.last_used = time.monotonic()
entry.request_count += 1
return entry.client
client = self._create_client()
entry = PooledClient(client=client, in_use=True, request_count=1)
self._pool.append(entry)
return client
async def release(self, client: BlueBubblesClient) -> None:
async with self._lock:
for entry in self._pool:
if entry.client is client:
entry.in_use = False
entry.last_used = time.monotonic()
return
def _create_client(self) -> BlueBubblesClient:
return BlueBubblesClient(
server_url=self._config.get("server_url", "http://localhost:1234"),
password=self._config.get("password", ""),
timeout=self._config.get("timeout", 30.0),
allow_private_network=self._config.get("allow_private_network", False),
)
async def start_cleanup(self) -> None:
if self._cleanup_task and not self._cleanup_task.done():
return
self._cleanup_task = asyncio.create_task(self._cleanup_loop())
async def stop(self) -> None:
if self._cleanup_task and not self._cleanup_task.done():
self._cleanup_task.cancel()
try:
await self._cleanup_task
except asyncio.CancelledError:
pass
self._cleanup_task = None
async with self._lock:
for entry in self._pool:
try:
await entry.client.close()
except Exception:
pass
self._pool.clear()
async def _cleanup_loop(self) -> None:
while True:
await asyncio.sleep(self.CLEANUP_INTERVAL)
await self._cleanup_idle()
async def _cleanup_idle(self) -> None:
now = time.monotonic()
async with self._lock:
idle_entries = [e for e in self._pool if not e.in_use and now - e.last_used > self.MAX_IDLE_SECONDS]
for entry in idle_entries:
try:
await entry.client.close()
except Exception:
pass
self._pool.remove(entry)
logger.debug("[BlueBubbles] Pool: removed idle client")
@property
def size(self) -> int:
return len(self._pool)
@property
def available(self) -> int:
return sum(1 for e in self._pool if not e.in_use)