feat: publish workphone SDK deployment and API docs

This commit is contained in:
Manus AI
2026-07-14 18:10:52 +08:00
commit 021d633cc1
534 changed files with 122391 additions and 0 deletions

View 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