ForcePilot/backend/package/yuxi/channel/extensions/zulip/streaming.py
Kris d5e36d33b7 feat(channel): 添加 Zalo OA、Zoom Chat 和 Zulip 渠道扩展
新增 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
2026-05-21 12:06:26 +08:00

104 lines
3.2 KiB
Python

from __future__ import annotations
import logging
import time
from yuxi.channel.extensions.zulip.client import ZulipAsyncClient
from yuxi.channel.extensions.zulip.outbound import parse_target_id
logger = logging.getLogger(__name__)
class ZulipDraftStream:
def __init__(
self,
client: ZulipAsyncClient,
channel_target: str,
content_limit: int = 10000,
edit_interval_ms: int = 350,
thread_id: str | None = None,
):
self._client = client
self._channel_target = channel_target
self._content_limit = content_limit
self._edit_interval_ms = edit_interval_ms
self._thread_id = thread_id
self._message_id: str | None = None
self._last_edit_at: float = 0.0
self._buffer = ""
self._placeholder_sent = False
async def _send_placeholder(self) -> None:
chat_type, destination = parse_target_id(self._channel_target)
payload: dict = {"type": chat_type, "to": destination, "content": ""}
if chat_type == "stream" and self._thread_id:
payload["topic"] = self._thread_id
result = await self._client.send_message(payload)
self._message_id = str(result.get("id", ""))
self._placeholder_sent = True
async def append(self, chunk: str) -> None:
if not self._placeholder_sent:
raise RuntimeError("placeholder must be sent before appending")
self._buffer += chunk
now = time.monotonic()
elapsed_ms = (now - self._last_edit_at) * 1000
if self._last_edit_at > 0 and elapsed_ms < self._edit_interval_ms:
return
await self._flush()
async def _flush(self) -> None:
if not self._message_id:
return
content = self._buffer[: self._content_limit]
try:
await self._client.update_message(
message_id=int(self._message_id),
content=content,
)
self._last_edit_at = time.monotonic()
except Exception as exc:
logger.warning("Zulip block stream update failed: %s", exc)
async def finalize(self) -> str | None:
if self._message_id:
content = self._buffer[: self._content_limit]
try:
await self._client.update_message(
message_id=int(self._message_id),
content=content,
)
except Exception:
pass
return self._message_id
async def discard(self) -> None:
if self._message_id:
try:
await self._client.delete_message(message_id=int(self._message_id))
except Exception:
pass
self._message_id = None
async def init(self) -> None:
if not self._placeholder_sent:
await self._send_placeholder()
class ZulipStreaming:
streaming_mode = "block"
block_streaming_enabled = True
async def create_block_chunker(
self,
client: ZulipAsyncClient,
target_id: str,
thread_id: str | None = None,
) -> ZulipDraftStream:
stream = ZulipDraftStream(client, target_id, thread_id=thread_id)
await stream.init()
return stream