ForcePilot/backend/package/yuxi/channel/extensions/xiaohongshu/window.py
Kris 5946478772 feat(channel): 添加小红书、XMPP、元宝和 Zalo 渠道扩展
新增小红书、XMPP、元宝、Zalo 四个渠道扩展。

小红书渠道扩展主要模块:config, gateway, webhook, outbound, streaming, pairing, security, dedupe, media, status, window

XMPP 渠道扩展主要模块:plugin, config, gateway, outbound, streaming, pairing, security, dedupe, accounts, commands, muc, rate_limiter, stanza_utils, status, monitor

元宝渠道扩展主要模块:plugin, client, config_schema, gateway, outbound(chunk/queue/transport), inbound(dispatcher), streaming, pairing, security, accounts, actions, commands, codec(biz/conn), session, shared, utils

Zalo 渠道扩展主要模块:api, config, gateway, webhook, outbound, pairing, security, session, polling, monitor, status
2026-05-21 12:04:05 +08:00

111 lines
3.6 KiB
Python

import logging
import time
logger = logging.getLogger(__name__)
_WINDOW_SECONDS = 48 * 3600
_MAX_MSG_PER_WINDOW = 6
_ENTER_DM_REPLY_WINDOW = 30
_ENTER_DM_MAX_REPLIES_PER_DAY = 3
_ENTER_DM_COOLDOWN_SECONDS = 3600
class XiaohongshuWindowTracker:
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(
"Xiaohongshu 48h 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(
"Xiaohongshu 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(
"Xiaohongshu 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(
"Xiaohongshu 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)