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

221 lines
9.3 KiB
Python

from __future__ import annotations
import os
from .types import SmsScene, SmsTemplateConfig, TemplateType, TencentSmsAccount
class TencentSmsConfigAdapter:
ENV_MAP = {
"secret_id": "TENCENT_SMS_SECRET_ID",
"secret_key": "TENCENT_SMS_SECRET_KEY",
"sms_sdk_app_id": "TENCENT_SMS_SDK_APP_ID",
"sign_name": "TENCENT_SMS_SIGN_NAME",
"region": "TENCENT_SMS_REGION",
}
def __init__(self):
self._config: dict | None = None
def set_config(self, config: dict) -> None:
self._config = config
def list_account_ids(self, config: dict) -> list[str]:
accounts = config.get("accounts", [])
if not accounts:
sid = os.environ.get("TENCENT_SMS_SECRET_ID", "")
if sid:
return ["default"]
return []
return [a.get("account_id", str(i)) for i, a in enumerate(accounts)]
async def resolve_account(self, account_id: str) -> dict:
if self._config is None:
return {}
accounts = self._config.get("accounts", [])
if not accounts:
sid = os.environ.get("TENCENT_SMS_SECRET_ID", "")
if sid and account_id in ("default", "0", ""):
return {
"account_id": "default",
"secret_id": sid,
"secret_key": os.environ.get("TENCENT_SMS_SECRET_KEY", ""),
"sms_sdk_app_id": os.environ.get("TENCENT_SMS_SDK_APP_ID", ""),
"sign_name": os.environ.get("TENCENT_SMS_SIGN_NAME", ""),
"region": os.environ.get("TENCENT_SMS_REGION", "ap-guangzhou"),
}
return {}
for a in accounts:
if a.get("account_id", str(accounts.index(a))) == account_id:
return a
return {}
def _parse_account(self, account: dict) -> TencentSmsAccount:
secret_id = account.get("secret_id") or os.environ.get("TENCENT_SMS_SECRET_ID", "")
secret_key = account.get("secret_key") or os.environ.get("TENCENT_SMS_SECRET_KEY", "")
sms_sdk_app_id = account.get("sms_sdk_app_id") or os.environ.get("TENCENT_SMS_SDK_APP_ID", "")
sign_name = account.get("sign_name") or os.environ.get("TENCENT_SMS_SIGN_NAME", "")
region = account.get("region") or os.environ.get("TENCENT_SMS_REGION", "ap-guangzhou")
templates = []
for tpl in account.get("templates", []):
templates.append(
SmsTemplateConfig(
scene=SmsScene(tpl.get("scene", "notification")),
template_id=tpl.get("template_id", ""),
sign_name=tpl.get("sign_name", sign_name),
template_type=TemplateType(tpl.get("template_type", "0")),
param_count=tpl.get("param_count", 1),
max_daily=tpl.get("max_daily", 1000),
description=tpl.get("description", ""),
template_format=tpl.get("template_format", ""),
)
)
return TencentSmsAccount(
account_id=account.get("account_id", "default"),
secret_id=secret_id,
secret_key=secret_key,
sms_sdk_app_id=sms_sdk_app_id,
sign_name=sign_name,
region=region,
endpoint=account.get("endpoint", "sms.tencentcloudapi.com"),
callback_url=account.get("callback_url", "/webhook/tencent-sms/callback"),
reply_callback_url=account.get("reply_callback_url", "/webhook/tencent-sms/reply-callback"),
templates=templates,
dm_policy=account.get("dm_policy", "open"),
allow_from=account.get("allow_from", []),
daily_limit=account.get("daily_limit", 1000),
interval_seconds=account.get("interval_seconds", 30),
phone_daily_limit=account.get("phone_daily_limit", 10),
poll_interval_seconds=account.get("poll_interval_seconds", 120),
enabled=account.get("enabled", True),
extend_code=account.get("extend_code", ""),
sender_id=account.get("sender_id", ""),
allowed_callback_ips=account.get("allowed_callback_ips", []),
)
def is_configured(self, account: dict) -> bool:
ta = self._parse_account(account)
return ta.is_configured
def is_enabled(self, account: dict) -> bool:
ta = self._parse_account(account)
return ta.enabled and ta.is_configured
def disabled_reason(self, account: dict) -> str:
ta = self._parse_account(account)
if not ta.secret_id:
return "\u7f3a\u5c11 SecretId"
if not ta.secret_key:
return "\u7f3a\u5c11 SecretKey"
if not ta.sms_sdk_app_id:
return "\u7f3a\u5c11 SmsSdkAppId"
if not ta.sign_name:
return "\u672a\u914d\u7f6e\u77ed\u4fe1\u7b7e\u540d (SignName)"
if not ta.templates:
return "\u672a\u914d\u7f6e\u77ed\u4fe1\u6a21\u677f"
if not ta.enabled:
return "\u6e20\u9053\u5df2\u7981\u7528"
return ""
def describe_account(self, account: dict) -> dict:
ta = self._parse_account(account)
return {
"account_id": ta.account_id,
"sms_sdk_app_id": ta.sms_sdk_app_id,
"sign_name": ta.sign_name,
"region": ta.region,
"dm_policy": ta.dm_policy,
"template_count": len(ta.templates),
"template_scenes": [t.scene.value for t in ta.templates],
}
def config_schema(self) -> dict:
return {
"type": "object",
"properties": {
"secret_id": {
"type": "string",
"description": "\u817e\u8baf\u4e91 API SecretId\uff08CAM \u63a7\u5236\u53f0\u83b7\u53d6\uff09",
},
"secret_key": {
"type": "string",
"description": "\u817e\u8baf\u4e91 API SecretKey",
"format": "password",
},
"sms_sdk_app_id": {
"type": "string",
"description": "\u77ed\u4fe1\u5e94\u7528 ID\uff08\u63a7\u5236\u53f0\u5e94\u7528\u7ba1\u7406 -> SDK AppID\uff09",
},
"sign_name": {
"type": "string",
"description": "\u77ed\u4fe1\u7b7e\u540d\uff08\u5df2\u5ba1\u6838\u901a\u8fc7\uff0c\u4e0d\u542b\u3010\u3011\uff09",
},
"region": {
"type": "string",
"default": "ap-guangzhou",
"description": "API \u5730\u57df",
},
"templates": {
"type": "array",
"items": {
"type": "object",
"properties": {
"scene": {
"type": "string",
"enum": ["verification", "notification", "alert", "marketing"],
},
"template_id": {"type": "string"},
"sign_name": {"type": "string"},
"param_count": {"type": "integer", "minimum": 1, "maximum": 12},
"description": {"type": "string"},
"template_format": {"type": "string"},
},
"required": ["scene", "template_id", "param_count"],
},
},
"dm_policy": {
"type": "string",
"enum": ["open", "pairing", "allowlist", "disabled"],
"default": "open",
},
"allow_from": {
"type": "array",
"items": {"type": "string"},
"description": "\u5141\u8bb8\u63a5\u6536\u77ed\u4fe1\u7684\u624b\u673a\u53f7\u767d\u540d\u5355\uff08E.164 \u683c\u5f0f\uff09",
},
"daily_limit": {
"type": "integer",
"minimum": 1,
"maximum": 100000,
"default": 1000,
},
"phone_daily_limit": {
"type": "integer",
"minimum": 1,
"maximum": 50,
"default": 10,
},
"extend_code": {
"type": "string",
"description": "\u77ed\u4fe1\u7801\u53f7\u6269\u5c55\u53f7\uff08\u56fd\u9645/\u6e2f\u6fb3\u53f0\u77ed\u4fe1\u7528\uff09",
},
"sender_id": {
"type": "string",
"description": "\u56fd\u9645/\u6e2f\u6fb3\u53f0 Sender ID",
},
"allowed_callback_ips": {
"type": "array",
"items": {"type": "string"},
"description": "\u5141\u8bb8\u7684 Webhook \u56de\u8c03 IP \u767d\u540d\u5355",
},
"enabled": {"type": "boolean", "default": True},
},
"required": ["secret_id", "secret_key", "sms_sdk_app_id", "sign_name"],
}
def default_account_id(self, config: dict) -> str:
ids = self.list_account_ids(config)
return ids[0] if ids else "default"