feat: publish workphone SDK deployment and API docs
This commit is contained in:
157
sdk/agent/anti_ban/risk_sentinel.py
Normal file
157
sdk/agent/anti_ban/risk_sentinel.py
Normal file
@@ -0,0 +1,157 @@
|
||||
"""
|
||||
风控哨兵 — 实时监控操作频率,行为随机化,触发阈值告警
|
||||
|
||||
功能:
|
||||
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
|
||||
Reference in New Issue
Block a user