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

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)