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

105 lines
3.3 KiB
Python

from __future__ import annotations
import datetime
import json
import logging
import os
from pathlib import Path
from .types import HELP_KEYWORDS, OPT_IN_KEYWORDS, OPT_OUT_KEYWORDS
logger = logging.getLogger(__name__)
_unsubscribe_blocklist: set[str] = set()
_unsubscribe_file: Path | None = None
MARKETING_START_HOUR = 8
MARKETING_END_HOUR = 21
def _get_unsubscribe_file() -> Path:
global _unsubscribe_file
if _unsubscribe_file is None:
data_dir = Path(os.environ.get("YUXI_DATA_DIR", "data"))
_unsubscribe_file = data_dir / "tencent_sms_unsubscribe.json"
return _unsubscribe_file
def _load_blocklist() -> set[str]:
file_path = _get_unsubscribe_file()
try:
if file_path.exists():
data = json.loads(file_path.read_text(encoding="utf-8"))
return set(data) if isinstance(data, list) else set()
except (json.JSONDecodeError, OSError) as e:
logger.warning("\u52a0\u8f7d\u9000\u8ba2\u9ed1\u540d\u5355\u5931\u8d25: %s", e)
return set()
def _save_blocklist() -> None:
file_path = _get_unsubscribe_file()
try:
file_path.parent.mkdir(parents=True, exist_ok=True)
file_path.write_text(
json.dumps(sorted(_unsubscribe_blocklist), ensure_ascii=False, indent=2),
encoding="utf-8",
)
except OSError as e:
logger.warning("\u4fdd\u5b58\u9000\u8ba2\u9ed1\u540d\u5355\u5931\u8d25: %s", e)
def init_blocklist() -> None:
global _unsubscribe_blocklist
_unsubscribe_blocklist = _load_blocklist()
logger.info("\u9000\u8ba2\u9ed1\u540d\u5355\u5df2\u52a0\u8f7d: %d \u6761", len(_unsubscribe_blocklist))
def check_marketing_time_window() -> tuple[bool, str]:
hour = datetime.datetime.now().hour
if MARKETING_START_HOUR <= hour < MARKETING_END_HOUR:
return True, "ok"
return (
False,
f"\u8425\u9500\u77ed\u4fe1\u4ec5\u5141\u8bb8 {MARKETING_START_HOUR}:00-{MARKETING_END_HOUR}:00 \u53d1\u9001",
)
def detect_opt_out(reply_content: str) -> tuple[bool, str | None]:
text = reply_content.strip().upper()
content_lower = reply_content.strip().lower()
if content_lower in OPT_OUT_KEYWORDS or text in ("T", "TD", "N", "NO"):
return (
True,
"\u60a8\u5df2\u6210\u529f\u9000\u8ba2\uff0c\u5c06\u4e0d\u518d\u6536\u5230\u672c\u53f7\u7801\u7684\u6d88\u606f\u3002\u56de\u590d START \u91cd\u65b0\u8ba2\u9605\u3002",
)
if content_lower in OPT_IN_KEYWORDS:
return True, "\u60a8\u5df2\u91cd\u65b0\u8ba2\u9605\uff0c\u6b22\u8fce\u56de\u6765\uff01"
if content_lower in HELP_KEYWORDS:
return (
True,
"\u6b22\u8fce\u4f7f\u7528 AI \u667a\u80fd\u52a9\u624b\u3002\u56de\u590d TD \u9000\u8ba2\u6d88\u606f\uff0c\u56de\u590d HELP \u83b7\u53d6\u5e2e\u52a9\u3002",
)
return False, None
def add_unsubscribe(phone: str):
_unsubscribe_blocklist.add(phone)
_save_blocklist()
logger.info("\u7528\u6237 %s \u5df2\u52a0\u5165\u9000\u8ba2\u9ed1\u540d\u5355", phone)
def remove_unsubscribe(phone: str):
_unsubscribe_blocklist.discard(phone)
_save_blocklist()
logger.info("\u7528\u6237 %s \u5df2\u79fb\u9664\u9000\u8ba2\u9ed1\u540d\u5355", phone)
def is_unsubscribed(phone: str) -> bool:
if not _unsubscribe_blocklist:
init_blocklist()
return phone in _unsubscribe_blocklist