新增BlueBubbles适配器全套核心工具类与服务,包含会话管理、消息去重、Webhook验证、缓存系统、账号配置解析、聊天消息处理等完整功能模块,支持iMessage消息收发、群管理、反应特效、语音合成等能力,提供完善的健康检查与配置校验流程。
115 lines
3.9 KiB
Python
115 lines
3.9 KiB
Python
from __future__ import annotations
|
|
|
|
import asyncio
|
|
import time
|
|
from typing import Any
|
|
|
|
from yuxi.channels.adapters.bluebubbles.client import BlueBubblesClient
|
|
from yuxi.channels.adapters.bluebubbles.send import send_typing_indicator
|
|
from yuxi.channels.models import DeliveryResult
|
|
from yuxi.utils.logging_config import logger
|
|
|
|
|
|
class StreamingTypingSync:
|
|
TYPING_REFRESH_INTERVAL = 8.0
|
|
|
|
def __init__(self, client: BlueBubblesClient | None = None):
|
|
self._client = client
|
|
self._active_chats: set[str] = set()
|
|
self._last_typing: dict[str, float] = {}
|
|
self._keepalive_task: asyncio.Task | None = None
|
|
self._running = False
|
|
|
|
def set_client(self, client: BlueBubblesClient) -> None:
|
|
self._client = client
|
|
|
|
async def start_typing(self, chat_id: str) -> None:
|
|
if not self._client:
|
|
return
|
|
|
|
self._active_chats.add(chat_id)
|
|
try:
|
|
await send_typing_indicator(self._client, chat_id, display=True)
|
|
self._last_typing[chat_id] = time.monotonic()
|
|
except Exception:
|
|
logger.debug(f"[BlueBubbles] Failed to start typing indicator for {chat_id}")
|
|
|
|
if not self._running:
|
|
self._running = True
|
|
self._keepalive_task = asyncio.create_task(self._typing_keepalive())
|
|
|
|
async def stop_typing(self, chat_id: str) -> None:
|
|
self._active_chats.discard(chat_id)
|
|
if not self._client:
|
|
return
|
|
try:
|
|
await send_typing_indicator(self._client, chat_id, display=False)
|
|
self._last_typing.pop(chat_id, None)
|
|
except Exception:
|
|
logger.debug(f"[BlueBubbles] Failed to stop typing indicator for {chat_id}")
|
|
|
|
if not self._active_chats:
|
|
self._running = False
|
|
if self._keepalive_task and not self._keepalive_task.done():
|
|
self._keepalive_task.cancel()
|
|
self._keepalive_task = None
|
|
|
|
async def stop_all(self) -> None:
|
|
chats = list(self._active_chats)
|
|
for chat_id in chats:
|
|
await self.stop_typing(chat_id)
|
|
|
|
async def _typing_keepalive(self) -> None:
|
|
while self._running and self._active_chats:
|
|
await asyncio.sleep(self.TYPING_REFRESH_INTERVAL)
|
|
if not self._client:
|
|
continue
|
|
now = time.monotonic()
|
|
for chat_id in list(self._active_chats):
|
|
last = self._last_typing.get(chat_id, 0)
|
|
if now - last >= self.TYPING_REFRESH_INTERVAL:
|
|
try:
|
|
await send_typing_indicator(self._client, chat_id, display=True)
|
|
self._last_typing[chat_id] = now
|
|
except Exception:
|
|
logger.debug(f"[BlueBubbles] Typing keepalive failed for {chat_id}")
|
|
|
|
|
|
async def send_stream_with_typing(
|
|
adapter: Any,
|
|
chat_id: str,
|
|
msg_id: str,
|
|
chunk: str,
|
|
finished: bool,
|
|
typing_sync: StreamingTypingSync | None = None,
|
|
) -> DeliveryResult:
|
|
if typing_sync and not finished:
|
|
await typing_sync.start_typing(chat_id)
|
|
|
|
result = await adapter.send_stream_chunk(chat_id, msg_id, chunk, finished)
|
|
|
|
if finished and typing_sync:
|
|
await typing_sync.stop_typing(chat_id)
|
|
|
|
return result
|
|
|
|
|
|
class StreamBuffer:
|
|
def __init__(self, max_buffer_size: int = 65536):
|
|
self._max_buffer_size = max_buffer_size
|
|
self._buffers: dict[str, str] = {}
|
|
|
|
def append(self, stream_id: str, text: str) -> str:
|
|
current = self._buffers.get(stream_id, "")
|
|
new_content = current + text
|
|
if len(new_content) > self._max_buffer_size:
|
|
new_content = new_content[-self._max_buffer_size :]
|
|
self._buffers[stream_id] = new_content
|
|
return new_content
|
|
|
|
def get(self, stream_id: str) -> str:
|
|
return self._buffers.get(stream_id, "")
|
|
|
|
def clear(self, stream_id: str) -> None:
|
|
self._buffers.pop(stream_id, None)
|