ForcePilot/backend/package/yuxi/channel/extensions/douyin/window.py
Kris 4552bde837 feat(douyin): 新增抖音渠道插件,支持私信收发与相关能力
该提交实现了完整的抖音开放平台IM渠道插件,包含:
1. 基础配置与账号管理能力
2. 消息去重、安全策略校验
3. 流式回复、多媒体消息发送
4. Webhook回调处理与事件解析
5. 配对认证与流量限流机制
2026-05-21 10:45:37 +08:00

107 lines
3.6 KiB
Python

import logging
import time
logger = logging.getLogger(__name__)
_WINDOW_SECONDS = 24 * 3600
_MAX_MSG_PER_WINDOW = 6
_ENTER_DM_REPLY_WINDOW = 30
_ENTER_DM_MAX_REPLIES_PER_WEBHOOK = 3
_ENTER_DM_MAX_REPLIES_PER_DAY = 3
_ENTER_DM_COOLDOWN_SECONDS = 3600
class DouyinWindowTracker:
def __init__(self):
self._last_msg_time: dict[str, float] = {}
self._msg_count: dict[str, int] = {}
self._last_send_time: dict[str, float] = {}
self._enter_dm_reply_count: dict[str, int] = {}
self._enter_dm_last_reply_time: dict[str, float] = {}
def record(self, open_id: str) -> None:
self._last_msg_time[open_id] = time.time()
def record_send(self, open_id: str) -> None:
now = time.time()
if open_id not in self._last_msg_time:
self._msg_count[open_id] = 0
return
if now - self._last_msg_time[open_id] > _WINDOW_SECONDS:
self._msg_count[open_id] = 0
self._msg_count[open_id] = self._msg_count.get(open_id, 0) + 1
self._last_send_time[open_id] = now
def can_reply(self, open_id: str) -> bool:
last = self._last_msg_time.get(open_id, 0)
now = time.time()
if now - last > _WINDOW_SECONDS:
logger.info(
"Douyin 24h reply window expired for user %s, last_msg_at=%s",
open_id,
last,
)
return False
count = self._msg_count.get(open_id, 0)
if count >= _MAX_MSG_PER_WINDOW:
logger.warning(
"Douyin max message count reached for user %s: %d/%d",
open_id,
count,
_MAX_MSG_PER_WINDOW,
)
return False
return True
def can_enter_dm_reply(self, open_id: str) -> bool:
now = time.time()
day_key = f"{open_id}:{int(now / 86400)}"
daily_count = self._enter_dm_reply_count.get(day_key, 0)
if daily_count >= _ENTER_DM_MAX_REPLIES_PER_DAY:
logger.info(
"Douyin enter-dm daily limit reached for user %s: %d",
open_id,
daily_count,
)
return False
last_reply = self._enter_dm_last_reply_time.get(open_id, 0)
if last_reply and now - last_reply < _ENTER_DM_COOLDOWN_SECONDS:
logger.info(
"Douyin enter-dm cooldown active for user %s, remaining=%ds",
open_id,
int(_ENTER_DM_COOLDOWN_SECONDS - (now - last_reply)),
)
return False
return True
def record_enter_dm_reply(self, open_id: str) -> None:
now = time.time()
day_key = f"{open_id}:{int(now / 86400)}"
self._enter_dm_reply_count[day_key] = self._enter_dm_reply_count.get(day_key, 0) + 1
self._enter_dm_last_reply_time[open_id] = now
def remaining_seconds(self, open_id: str) -> float:
last = self._last_msg_time.get(open_id, 0)
if last == 0:
return 0
elapsed = time.time() - last
remaining = _WINDOW_SECONDS - elapsed
return max(0.0, remaining)
def cleanup(self):
now = time.time()
expired = [k for k, ts in self._last_msg_time.items() if now - ts > _WINDOW_SECONDS]
for k in expired:
del self._last_msg_time[k]
self._msg_count.pop(k, None)
self._last_send_time.pop(k, None)
stale_enter = [
k for k, ts in self._enter_dm_last_reply_time.items() if now - ts > _ENTER_DM_COOLDOWN_SECONDS * 2
]
for k in stale_enter:
self._enter_dm_last_reply_time.pop(k, None)