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