新增BlueBubbles适配器全套核心工具类与服务,包含会话管理、消息去重、Webhook验证、缓存系统、账号配置解析、聊天消息处理等完整功能模块,支持iMessage消息收发、群管理、反应特效、语音合成等能力,提供完善的健康检查与配置校验流程。
122 lines
3.8 KiB
Python
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)
|