新增 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: 类型定义
157 lines
5.0 KiB
Python
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
|