Some checks failed
SDK CI / python-compile (push) Has been cancelled
- 新增 sdk/app/agent(Hook/Skills/Anti-ban 等)及 NAS/ADB 脚本 - 更新 Android 端、unified、PHP SDK、Docker Compose - Soul 文档迁移至 Soul调研/;移除资料目录内 APK - .gitignore 排除 sdk/tmp、sdk/logs、sdk/tmp_rom Co-authored-by: Cursor <cursoragent@cursor.com>
136 lines
4.8 KiB
Python
136 lines
4.8 KiB
Python
"""
|
||
风控哨兵 — 实时监控操作频率,行为随机化,触发阈值告警
|
||
|
||
功能:
|
||
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),
|
||
}
|
||
|
||
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 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
|