新增 Zalo OA、Zoom Chat、Zulip 三个渠道扩展。 Zalo OA 渠道扩展主要模块:sidecar_client, config, gateway, outbound, streaming, pairing, security, auth, dedupe, directory, monitor, status, session, reactions, tools Zoom Chat 渠道扩展主要模块:config, gateway, webhook, outbound, streaming, pairing, security, crypto, dedupe, actions, media, mentions, monitor, status, session, reactions, threading Zulip 渠道扩展主要模块:client, config, gateway, outbound, streaming, pairing, security, monitor, status
102 lines
3.0 KiB
Python
102 lines
3.0 KiB
Python
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}
|