from __future__ import annotations import asyncio import logging logger = logging.getLogger(__name__) ZOOM_CHUNK_MAX = 3800 ZOOM_CHUNK_MIN = 500 ZOOM_COALESCE_IDLE_MS = 1000 class ZoomStreamSession: """ Block Streaming 会话 收集 Agent 流式输出的文本 Token,达到 chunk_min 或 idle_ms 时 自动发送当前累积的文本块。 Zoom 的限制: - 单条消息最大 4096 字符 → chunk_max = 3800 (留余量) - 不支持消息编辑 (无法实现 Preview Streaming) - 使用连续发送新消息的方式模拟流式效果 """ def __init__(self, send_fn, target_id: str, chunk_max: int = ZOOM_CHUNK_MAX): self._send = send_fn self._target_id = target_id self._chunk_max = chunk_max self._buffer: str = "" self._sent_count: int = 0 self._finished: bool = False self._task: asyncio.Task | None = None async def start(self, initial_text: str = "") -> None: if initial_text: self._buffer += initial_text if len(self._buffer) >= ZOOM_CHUNK_MIN: await self._flush() async def feed(self, text: str) -> None: if self._finished or not text: return self._buffer += text if len(self._buffer) >= ZOOM_CHUNK_MIN: await self._flush() async def _flush(self) -> None: if not self._buffer: return chunk = self._buffer[: self._chunk_max] remainder = self._buffer[self._chunk_max :] if len(self._buffer) > self._chunk_max else "" if self._sent_count > 0: prefix = "...\n" chunk = prefix + chunk[: self._chunk_max - len(prefix)] try: await self._send(self._target_id, chunk) self._sent_count += 1 except Exception: logger.exception("Zoom streaming send failed") self._buffer = remainder async def finish(self) -> None: while self._buffer: await self._flush() self._finished = True async def cancel(self) -> None: self._buffer = "" self._finished = True @property def sent_count(self) -> int: return self._sent_count @property def is_finished(self) -> bool: return self._finished class ZoomStreaming: streaming_mode: str = "block" block_streaming_enabled: bool = True block_streaming_chunk_min_chars: int = ZOOM_CHUNK_MIN block_streaming_chunk_max_chars: int = ZOOM_CHUNK_MAX block_streaming_chunk_break_preference: str = "paragraph" block_streaming_coalesce_defaults: dict | None = { "min_chars": ZOOM_CHUNK_MIN, "max_chars": ZOOM_CHUNK_MAX, "idle_ms": ZOOM_COALESCE_IDLE_MS, } def create_draft_stream_session(self, target_id: str, send_fn=None) -> ZoomStreamSession: return ZoomStreamSession(send_fn, target_id) def create_block_chunker(self) -> object: return {"mode": "length", "min_chars": ZOOM_CHUNK_MIN, "max_chars": ZOOM_CHUNK_MAX}