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