ForcePilot/backend/package/yuxi/channel/extensions/telegram/streaming.py
Kris 8a3601250e feat(channel): 添加 Telegram 渠道扩展
新增 Telegram 渠道扩展,支持在 Yuxi 平台中集成 Telegram 即时通讯渠道。

包含以下功能模块:
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- polling: 长轮询模式
- outbound: 外发消息管理
- streaming: 流式消息处理
- pairing: 用户配对与绑定
- security: 安全校验
- dedupe: 消息去重
- monitor: 渠道状态监控
- status: 会话状态管理
- session: 会话管理
- actions: 动作处理
- inline_keyboard: 内联键盘
- native_commands: 原生指令
- chat: 聊天管理
- delivery: 消息送达确认
- media: 媒体资源处理
- profile: 用户资料
- reactions: 表情反应
- sticker: 贴纸处理
- types: 类型定义
2026-05-21 11:48:09 +08:00

157 lines
5.0 KiB
Python

from __future__ import annotations
import asyncio
import logging
logger = logging.getLogger(__name__)
class TelegramStreaming:
streaming_mode = "partial"
preview_stream_throttle_ms = 160
preview_min_initial_chars = 18
block_streaming_enabled = True
block_streaming_break = "text_end"
block_streaming_chunk_min_chars = 800
block_streaming_chunk_max_chars = 1200
block_streaming_chunk_break_preference = "paragraph"
block_streaming_coalesce_defaults = {
"min_chars": 400,
"max_chars": 800,
"idle_ms": 1000,
}
def create_draft_stream_session(self, target_id: str) -> dict:
return {"target_id": target_id, "mode": "partial", "message_id": None}
def create_block_chunker(self) -> dict:
return {
"mode": "length",
"min_chars": self.block_streaming_chunk_min_chars,
"max_chars": self.block_streaming_chunk_max_chars,
}
def create_preview_stream(
self,
outbound,
target_id: str,
account_id: str,
*,
thread_id: str | None = None,
throttle_ms: int | None = None,
min_initial_chars: int | None = None,
chunk_limit: int = 4096,
):
return PreviewStream(
outbound=outbound,
target_id=target_id,
account_id=account_id,
thread_id=thread_id,
throttle_ms=throttle_ms or self.preview_stream_throttle_ms,
min_initial_chars=min_initial_chars or self.preview_min_initial_chars,
chunk_limit=chunk_limit,
)
class PreviewStream:
def __init__(
self,
outbound,
target_id: str,
account_id: str,
*,
thread_id: str | None = None,
throttle_ms: int = 160,
min_initial_chars: int = 18,
chunk_limit: int = 4096,
):
self._outbound = outbound
self._target_id = target_id
self._account_id = account_id
self._thread_id = thread_id
self._throttle_ms = throttle_ms
self._min_initial_chars = min_initial_chars
self._chunk_limit = chunk_limit
self._buffer = ""
self._draft_message_id: str | None = None
self._sent_length = 0
self._last_edit_time = 0.0
self._queue: asyncio.Queue[str] = asyncio.Queue()
self._done = asyncio.Event()
self._final_text = ""
async def feed(self, token: str) -> None:
self._buffer += token
await self._queue.put(token)
async def finalize(self, final_text: str) -> None:
self._final_text = final_text
self._done.set()
if self._draft_message_id:
try:
await self._outbound.edit_message(
self._target_id,
self._draft_message_id,
final_text,
account_id=self._account_id,
)
except Exception:
logger.exception("PreviewStream finalize edit failed for msg %s", self._draft_message_id)
else:
try:
await self._outbound.send_text_raw(
self._target_id,
final_text,
account_id=self._account_id,
thread_id=self._thread_id,
)
except Exception:
logger.exception("PreviewStream finalize send failed")
async def run(self) -> None:
loop = asyncio.get_running_loop()
while not self._done.is_set():
try:
await asyncio.wait_for(self._queue.get(), timeout=0.5)
except TimeoutError:
continue
current_len = len(self._buffer)
if self._draft_message_id is None:
if current_len >= self._min_initial_chars:
try:
self._draft_message_id = await self._outbound.send_text_raw(
self._target_id,
self._buffer,
account_id=self._account_id,
thread_id=self._thread_id,
)
self._last_edit_time = loop.time()
self._sent_length = current_len
except Exception:
logger.exception("PreviewStream initial send failed")
continue
if current_len <= self._sent_length:
continue
now = loop.time()
if now - self._last_edit_time >= self._throttle_ms / 1000:
try:
display = self._buffer[:self._chunk_limit]
await self._outbound.edit_message(
self._target_id,
self._draft_message_id,
display,
account_id=self._account_id,
)
self._last_edit_time = now
self._sent_length = current_len
except Exception:
pass