新增 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
104 lines
3.2 KiB
Python
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
|