本次提交对蓝泡泡适配器进行了多维度改进: 1. 新增缓存超限自动清理逻辑,防止内存溢出 2. 增强流式打字同步功能,增加超时自动停止逻辑 3. 重构markdown渲染逻辑,修复格式匹配问题并支持更多语法 4. 新增配置校验工具函数,增加streaming_mode和auth_strategy的合法性校验 5. 新增多项环境配置参数,完善配置项 6. 重构初始化流程,新增多个工具类实例 7. 优化TTS禁用逻辑,新增aiohttp依赖检查 8. 重构流式消息发送逻辑,整合打字指示器逻辑 9. 新增健康状态检查的问题检测功能
132 lines
4.6 KiB
Python
132 lines
4.6 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
|
|
DEFAULT_STREAM_TIMEOUT = 60.0
|
|
|
|
def __init__(
|
|
self,
|
|
client: BlueBubblesClient | None = None,
|
|
stream_timeout: float = DEFAULT_STREAM_TIMEOUT,
|
|
):
|
|
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
|
|
self._stream_timeout = stream_timeout
|
|
self._stream_started: dict[str, float] = {}
|
|
|
|
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)
|
|
self._stream_started.setdefault(chat_id, time.monotonic())
|
|
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)
|
|
self._stream_started.pop(chat_id, None)
|
|
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}")
|
|
|
|
stream_started = self._stream_started.get(chat_id)
|
|
if stream_started and (now - stream_started) > self._stream_timeout:
|
|
logger.warning(
|
|
f"[BlueBubbles] Stream timeout for {chat_id}, "
|
|
f"({now - stream_started:.0f}s), auto-stopping typing"
|
|
)
|
|
await self.stop_typing(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)
|