Files
workphone-sdk/sdk/app/agent/anti_ban/risk_sentinel.py
卡若 9118df9eee
Some checks failed
SDK CI / python-compile (push) Has been cancelled
feat: sdk agent、Android 与文档同步至 Gitea
- 新增 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>
2026-05-06 18:20:14 +08:00

136 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
风控哨兵 — 实时监控操作频率,行为随机化,触发阈值告警
功能:
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