ForcePilot/backend/package/yuxi/channel/extensions/zoomchat/streaming.py

102 lines
3.0 KiB
Python
Raw Normal View History

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}