ForcePilot/backend/package/yuxi/channel/extensions/tencent_sms/gateway.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

172 lines
5.7 KiB
Python

from __future__ import annotations
import asyncio
import logging
from .client import TencentSmsClient
from .config import TencentSmsConfigAdapter
from .delivery import SmsDeliveryTracker
from .types import TencentSmsAccount
from .webhook import TencentSmsWebhookHandler, set_webhook_handler
logger = logging.getLogger("yuxi.channel.tencent-sms")
class TencentSmsGatewayAdapter:
def __init__(self):
self._config = TencentSmsConfigAdapter()
self._running = False
self._tasks: list[asyncio.Task] = []
self._accounts: dict[str, TencentSmsAccount] = {}
self._clients: dict[str, TencentSmsClient] = {}
self._delivery_tracker = SmsDeliveryTracker()
async def start(self, ctx) -> dict:
config = ctx.config if hasattr(ctx, "config") else {}
self._config.set_config(config)
account = self._config._parse_account(config)
if not account.is_configured:
raise ValueError(
f"腾讯云短信账户 {account.account_id} 未正确配置"
)
self._accounts[account.account_id] = account
client = TencentSmsClient(account)
self._clients[account.account_id] = client
probe_ok = await client.probe()
if not probe_ok:
logger.warning("腾讯云短信 Gateway probe 未通过")
if hasattr(ctx, "outbound") and ctx.outbound:
ctx.outbound.set_client(account.account_id, client, account)
webhook_handler = TencentSmsWebhookHandler(
allowed_ips=getattr(account, "allowed_callback_ips", None),
delivery_tracker=self._delivery_tracker,
)
set_webhook_handler(webhook_handler)
self._running = True
task = asyncio.create_task(self._periodic_pull(client, account))
self._tasks.append(task)
logger.info(
"腾讯云短信 Gateway 启动成功, account=%s, app_id=%s, sign=%s",
account.account_id,
account.sms_sdk_app_id,
account.sign_name,
)
return {
"running": True,
"account": account,
"client": client,
"accounts": self._accounts,
"clients": self._clients,
"delivery_tracker": self._delivery_tracker,
}
async def stop(self, ctx) -> None:
self._running = False
for task in self._tasks:
task.cancel()
self._tasks.clear()
self._accounts.clear()
self._clients.clear()
logger.info("腾讯云短信 Gateway 已停止")
def get_client(self, account_id: str = "default") -> TencentSmsClient | None:
return self._clients.get(account_id)
def get_account(self, account_id: str = "default") -> TencentSmsAccount | None:
return self._accounts.get(account_id)
async def probe(self, account: dict) -> bool:
try:
ta = self._config._parse_account(account)
client = TencentSmsClient(ta)
return await client.probe()
except Exception as e:
logger.warning("腾讯云短信 probe 失败: %s", e)
return False
async def _periodic_pull(
self,
client: TencentSmsClient,
account: TencentSmsAccount,
):
interval = max(account.poll_interval_seconds, 30)
while self._running:
try:
await self._pull_all_statuses(client)
await self._pull_all_replies(client)
except asyncio.CancelledError:
break
except Exception as e:
logger.exception("定时拉取任务异常: %s", e)
await asyncio.sleep(interval)
async def _pull_all_statuses(self, client: TencentSmsClient) -> int:
total_processed = 0
batch_count = 0
max_batches = 10
while batch_count < max_batches:
statuses = await client.pull_send_status(limit=100)
if not statuses:
break
logger.info("Pull 拉取到 %d 条送达状态", len(statuses))
for item in statuses:
self._delivery_tracker.handle_status_callback(
{
"SerialNo": item["serial_no"],
"ReportStatus": item["report_status"],
"PhoneNumber": item["phone_number"],
"Description": item["description"],
"ReportTime": item["user_receive_time"],
"SessionContext": item["session_context"],
}
)
total_processed += 1
batch_count += 1
if len(statuses) < 100:
break
if total_processed > 0:
logger.info("本轮共处理 %d 条送达状态", total_processed)
return total_processed
async def _pull_all_replies(self, client: TencentSmsClient) -> int:
total_processed = 0
batch_count = 0
max_batches = 10
while batch_count < max_batches:
replies = await client.pull_reply_status(limit=100)
if not replies:
break
logger.info("Pull 拉取到 %d 条上行回复", len(replies))
for item in replies:
logger.info(
"上行回复: phone=%s, content=%s, time=%s",
item["phone_number"],
item["reply_content"],
item["reply_time"],
)
total_processed += 1
batch_count += 1
if len(replies) < 100:
break
if total_processed > 0:
logger.info("本轮共处理 %d 条上行回复", total_processed)
return total_processed