新增腾讯短信(Tencent SMS)渠道扩展,支持在 Yuxi 平台中集成腾讯云短信渠道。 包含以下功能模块: - client: 腾讯云短信 API 客户端封装 - plugin: 渠道插件核心 - config: 渠道配置管理 - gateway: SSE/WebSocket 网关接入 - webhook: Webhook 事件处理 - outbound: 外发消息管理 - streaming: 流式消息处理 - security: 安全校验 - dedupe: 消息去重 - delivery: 送达状态回调 - compliance: 合规管理 - frequency: 频率控制 - templates: 短信模板 - agent_prompt: Agent 提示词 - types: 类型定义
94 lines
3.2 KiB
Python
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)
|