ForcePilot/backend/package/yuxi/channel/extensions/aliyun_sms/outbound.py
Kris b8864ac12f feat(aliyun-sms): 新增阿里云短信渠道插件
实现完整的阿里云短信服务接入能力,包含短信发送、状态回执处理、上行消息接收、安全策略校验、会话管理等功能
2026-05-21 10:39:43 +08:00

123 lines
3.8 KiB
Python

from __future__ import annotations
import hashlib
import logging
import time
from .format import AliyunSmsFormatter, mask_phone
from .types import OutboundSmsResult
logger = logging.getLogger(__name__)
MAX_SMS_CHARS_UNICODE = 70
RESERVED_SIGN_CHARS = 8
class AliyunSmsOutbound:
def __init__(self, gateway):
self._gateway = gateway
self._formatter = AliyunSmsFormatter()
async def send_text(
self,
target_id: str,
content: str,
template_type: str | None = None,
) -> OutboundSmsResult:
account = self._gateway.account
segments = self._formatter.adapt_sms_content(content, max_segments=account.max_segments)
if not segments:
return OutboundSmsResult(
success=False,
message="内容为空",
code="EMPTY_CONTENT",
)
template_code = account.get_template_code(template_type)
if not template_code:
return OutboundSmsResult(
success=False,
message=f"未配置模板 CODE: template_type={template_type or account.default_template}",
code="NO_TEMPLATE_CODE",
)
results = []
for segment in segments:
template_param = {
"content": segment,
}
result = await self._gateway.client.send_sms(
phone_numbers=target_id,
sign_name=account.sign_name,
template_code=template_code,
template_param=template_param,
out_id=_generate_out_id(target_id, content),
)
results.append(result)
if not result.success:
logger.error(
"短信发送失败: phone=%s code=%s msg=%s",
mask_phone(target_id, account.phone_mask_enabled),
result.code,
result.message,
)
return result
final = results[-1] if results else results[0]
logger.info(
"短信发送成功: phone=%s biz_id=%s segments=%d",
mask_phone(target_id, account.phone_mask_enabled),
final.biz_id,
len(results),
)
return final
async def send_batch(
self,
phone_numbers: list[str],
contents: list[str],
template_type: str | None = None,
) -> list[OutboundSmsResult]:
if not phone_numbers:
return []
account = self._gateway.account
template_code = account.get_template_code(template_type)
if not template_code:
return [
OutboundSmsResult(
success=False,
message=f"未配置模板 CODE: {template_type or account.default_template}",
code="NO_TEMPLATE_CODE",
)
for _ in phone_numbers
]
formatted_contents = []
for content in contents:
segments = self._formatter.adapt_sms_content(content, max_segments=1)
formatted_contents.append(segments[0] if segments else "")
sign_names = [account.sign_name] * len(phone_numbers)
template_params = [{"content": c} for c in formatted_contents]
return await self._gateway.client.batch_send_sms(
phone_numbers=phone_numbers,
sign_names=sign_names,
template_code=template_code,
template_params=template_params,
)
def chunker(self, text: str, max_chars: int = MAX_SMS_CHARS_UNICODE - RESERVED_SIGN_CHARS) -> list[str]:
return self._formatter.chunker(text, max_chars)
def _generate_out_id(target_id: str, content: str) -> str:
raw = f"{target_id}:{content}:{int(time.time() / 60)}"
return hashlib.sha256(raw.encode()).hexdigest()[:16]