ForcePilot/backend/package/yuxi/channel/extensions/tencent_sms/streaming.py
Kris 944c9cdbb6 feat(channel): 添加腾讯短信渠道扩展
新增腾讯短信(Tencent SMS)渠道扩展,支持在 Yuxi 平台中集成腾讯云短信渠道。

包含以下功能模块:
- client: 腾讯云短信 API 客户端封装
- plugin: 渠道插件核心
- config: 渠道配置管理
- gateway: SSE/WebSocket 网关接入
- webhook: Webhook 事件处理
- outbound: 外发消息管理
- streaming: 流式消息处理
- security: 安全校验
- dedupe: 消息去重
- delivery: 送达状态回调
- compliance: 合规管理
- frequency: 频率控制
- templates: 短信模板
- agent_prompt: Agent 提示词
- types: 类型定义
2026-05-21 11:50:32 +08:00

94 lines
3.2 KiB
Python

from __future__ import annotations
import asyncio
import logging
from .types import SmsScene
logger = logging.getLogger(__name__)
MAX_SMS_SEGMENTS = 10
UCS2_CHARS_PER_SEGMENT = 67
class TencentSmsBlockStreamer:
def __init__(self, max_segments: int = MAX_SMS_SEGMENTS, chars_per_segment: int = UCS2_CHARS_PER_SEGMENT):
self._max_segments = max_segments
self._chars_per_segment = chars_per_segment
def chunk_text(self, text: str) -> list[str]:
if not text:
return []
segments: list[str] = []
remaining = text
while remaining and len(segments) < self._max_segments:
if len(remaining) <= self._chars_per_segment:
segments.append(remaining.strip())
break
cut = self._chars_per_segment
for delimiter in ["\u3002", "\n", "\uff01", "\uff1f", "\uff0c", " ", ",", "."]:
idx = remaining[: self._chars_per_segment].rfind(delimiter)
if idx > self._chars_per_segment // 2:
cut = idx + len(delimiter)
break
segment = remaining[:cut].strip()
if segment:
segments.append(segment)
remaining = remaining[cut:].strip()
if remaining and len(segments) < self._max_segments:
truncated = remaining[: self._chars_per_segment - 3] + "..."
segments.append(truncated)
return segments
async def stream_send(
self,
phone: str,
segments: list[str],
send_func,
interval_ms: int = 500,
):
for i, segment in enumerate(segments):
prefix = f"[{i + 1}/{len(segments)}] " if i > 0 else ""
content = f"{prefix}{segment}"
try:
result = await send_func(phone, content)
logger.debug(
"stream segment %d/%d: %s",
i + 1,
len(segments),
result.serial_no if result.success else f"FAIL:{result.code}",
)
except Exception as e:
logger.exception("stream segment %d/%d failed: %s", i + 1, len(segments), e)
if i < len(segments) - 1:
await asyncio.sleep(interval_ms / 1000.0)
async def stream_send_by_template(
self,
phone: str,
scene: SmsScene,
segments: list[str],
send_by_template_func,
interval_ms: int = 500,
):
for i, segment in enumerate(segments):
prefix = f"[{i + 1}/{len(segments)}] " if i > 0 else ""
content = f"{prefix}{segment}"
try:
result = await send_by_template_func(phone, scene, [content])
logger.debug(
"stream segment %d/%d: %s",
i + 1,
len(segments),
result.serial_no if result.success else f"FAIL:{result.code}",
)
except Exception as e:
logger.exception("stream segment %d/%d failed: %s", i + 1, len(segments), e)
if i < len(segments) - 1:
await asyncio.sleep(interval_ms / 1000.0)