""" 风控哨兵 — 实时监控操作频率,行为随机化,触发阈值告警 功能: 1. 操作频率控制 (消息/好友/群发 分开计数) 2. 行为随机抖动 (间隔 +-15%~30%) 3. 多级告警 (warn → throttle → pause) 4. 冷却与恢复 """ import logging import random import time from collections import defaultdict from typing import Dict, Optional, Tuple logger = logging.getLogger(__name__) class RiskSentinel: """风控哨兵 — 守护操作频率边界""" # 默认阈值: (时间窗口秒, 最大次数) DEFAULT_LIMITS: Dict[str, Tuple[int, int]] = { "send_message": (3600, 60), "add_friend": (3600, 20), "group_send": (3600, 10), "moment_post": (3600, 5), "moment_like": (3600, 30), "profile_view": (3600, 40), # ── 奥创协议补全写类频控(真机铁律:写类必须受频控约束)── "add_friend_in_room": (3600, 20), "add_friend_from_phonebook": (3600, 20), "add_friend_with_scene": (3600, 20), "add_friend_by_card": (3600, 20), "send_friend_verify": (3600, 20), "join_group_by_qr": (3600, 10), "send_jielong": (3600, 10), "send_multi_image": (3600, 30), "mass_send": (3600, 10), "batch_send": (3600, 10), "reply_moment_comment": (3600, 30), "delete_moment_comment": (3600, 30), "stop_moments_praise": (3600, 20), "start_nurture": (3600, 5), "detect_zombie_fans": (3600, 5), "phone_action": (3600, 10), } JITTER_RANGE = (0.15, 0.30) def __init__(self, custom_limits: Optional[Dict[str, Tuple[int, int]]] = None): self.limits = {**self.DEFAULT_LIMITS, **(custom_limits or {})} self._counters: Dict[str, list] = defaultdict(list) self._paused_until: Dict[str, float] = {} self._total_ops = 0 def check(self, action: str) -> dict: """ 检查某操作是否允许执行。 Returns: {"allowed": bool, "wait_sec": float, "level": "ok"|"warn"|"throttle"|"pause"} """ now = time.time() if action in self._paused_until and now < self._paused_until[action]: remaining = self._paused_until[action] - now return {"allowed": False, "wait_sec": remaining, "level": "pause", "reason": f"{action} 处于冷却期,剩余 {remaining:.0f}s"} window, max_count = self.limits.get(action, (3600, 100)) timestamps = self._counters[action] cutoff = now - window timestamps[:] = [t for t in timestamps if t > cutoff] current = len(timestamps) if current >= max_count: pause_sec = random.uniform(300, 600) self._paused_until[action] = now + pause_sec logger.warning(f"🚨 {action} 达到上限 {max_count}/{window}s,暂停 {pause_sec:.0f}s") return {"allowed": False, "wait_sec": pause_sec, "level": "pause", "reason": f"{action} 触发上限 ({current}/{max_count})"} ratio = current / max_count if ratio > 0.8: logger.warning(f"⚠️ {action} 接近上限 ({current}/{max_count})") return {"allowed": True, "wait_sec": 0, "level": "warn", "reason": f"接近上限 {current}/{max_count}"} if ratio > 0.6: return {"allowed": True, "wait_sec": 0, "level": "throttle", "reason": f"频率偏高 {current}/{max_count}"} return {"allowed": True, "wait_sec": 0, "level": "ok", "reason": ""} def can_operate(self, action: str = "send_message") -> bool: """兼容旧技能调用:保留风控判断,返回是否允许执行。""" result = self.check(action) return bool(result.get("allowed")) def record(self, action: str): """记录一次操作""" self._counters[action].append(time.time()) self._total_ops += 1 def add_jitter(self, base_delay: float) -> float: """给基准延迟加随机抖动""" jitter_pct = random.uniform(*self.JITTER_RANGE) direction = random.choice([-1, 1]) return max(0.5, base_delay * (1 + direction * jitter_pct)) def get_recommended_delay(self, action: str) -> float: """根据当前频率推荐操作间隔 (秒)""" window, max_count = self.limits.get(action, (3600, 100)) now = time.time() cutoff = now - window recent = [t for t in self._counters[action] if t > cutoff] current = len(recent) ratio = current / max_count if max_count > 0 else 0 if ratio > 0.8: base = random.uniform(30, 60) elif ratio > 0.5: base = random.uniform(10, 25) else: base = random.uniform(3, 8) return self.add_jitter(base) def reset(self, action: Optional[str] = None): """重置计数器""" if action: self._counters[action].clear() self._paused_until.pop(action, None) else: self._counters.clear() self._paused_until.clear() def get_stats(self) -> dict: """获取当前风控统计""" now = time.time() stats = {} for action, (window, max_count) in self.limits.items(): cutoff = now - window recent = [t for t in self._counters[action] if t > cutoff] paused = action in self._paused_until and now < self._paused_until[action] stats[action] = { "count": len(recent), "limit": max_count, "window_sec": window, "paused": paused, "usage_pct": round(len(recent) / max_count * 100, 1) if max_count > 0 else 0, } stats["total_ops"] = self._total_ops return stats