feat: 防封模块、架构图、防封策略、account_lifecycle/anti_ban_alert/content_guard/rate_limiter
Made-with: Cursor
2
.gitignore
vendored
@@ -23,6 +23,8 @@ sdk/android-app/build/
|
||||
# >20MB 大文件(GitHub/Gitea 限制)
|
||||
开发文档/6、后端/github-repos/
|
||||
sdk/apks/*.apk
|
||||
apk_analysis/
|
||||
sdk/admin/
|
||||
|
||||
# 敏感配置
|
||||
sdk/.env
|
||||
|
||||
@@ -324,7 +324,32 @@ class WorkPhoneAgent:
|
||||
info = self.d.info
|
||||
status["screen_on"] = info.get("screenOn", False)
|
||||
status["current_app"] = info.get("currentPackageName", "")
|
||||
except:
|
||||
|
||||
wechat_pkg = "com.tencent.mm"
|
||||
try:
|
||||
running = self.d.shell(f"pidof {wechat_pkg}").output.strip()
|
||||
status["wechat_running"] = bool(running)
|
||||
status["wechat_pid"] = running if running else None
|
||||
except Exception:
|
||||
status["wechat_running"] = False
|
||||
|
||||
status["wechat_foreground"] = (
|
||||
status.get("current_app") == wechat_pkg
|
||||
)
|
||||
|
||||
try:
|
||||
net_out = self.d.shell(
|
||||
"dumpsys connectivity | grep -m1 'type: WIFI\\|type: MOBILE' || echo 'none'"
|
||||
).output.strip()
|
||||
status["network_type"] = (
|
||||
"wifi" if "WIFI" in net_out
|
||||
else "mobile" if "MOBILE" in net_out
|
||||
else "none"
|
||||
)
|
||||
except Exception:
|
||||
status["network_type"] = "unknown"
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return status
|
||||
@@ -371,8 +396,42 @@ class WorkPhoneAgent:
|
||||
try:
|
||||
wifi = self.d.shell("dumpsys wifi | grep 'Wi-Fi is'").output.strip()
|
||||
status["wifi"] = "enabled" in wifi.lower()
|
||||
except:
|
||||
ssid_out = self.d.shell(
|
||||
"dumpsys wifi | grep -m1 'mWifiInfo' || echo ''"
|
||||
).output.strip()
|
||||
if "SSID:" in ssid_out:
|
||||
import re
|
||||
m = re.search(r'SSID:\s*([^,]+)', ssid_out)
|
||||
if m:
|
||||
status["wifi_ssid"] = m.group(1).strip().strip('"')
|
||||
ip_out = self.d.shell("ip route | grep -m1 'src'").output.strip()
|
||||
if "src " in ip_out:
|
||||
status["ip_address"] = ip_out.split("src ")[-1].split()[0]
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 微信详细状态
|
||||
wechat_pkg = "com.tencent.mm"
|
||||
try:
|
||||
pid = self.d.shell(f"pidof {wechat_pkg}").output.strip()
|
||||
status["wechat"] = {
|
||||
"installed": True,
|
||||
"running": bool(pid),
|
||||
"pid": pid or None,
|
||||
"foreground": info.get("currentPackageName") == wechat_pkg,
|
||||
}
|
||||
if pid:
|
||||
mem = self.d.shell(
|
||||
f"dumpsys meminfo {wechat_pkg} | grep 'TOTAL PSS' || echo ''"
|
||||
).output.strip()
|
||||
if "TOTAL" in mem:
|
||||
parts = mem.split()
|
||||
for p in parts:
|
||||
if p.replace(",", "").isdigit():
|
||||
status["wechat"]["memory_kb"] = int(p.replace(",", ""))
|
||||
break
|
||||
except Exception:
|
||||
status["wechat"] = {"installed": False, "running": False}
|
||||
|
||||
except Exception as e:
|
||||
status["error"] = str(e)
|
||||
|
||||
@@ -28,11 +28,17 @@ logger = logging.getLogger(__name__)
|
||||
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||
DEFAULT_SCRIPT = os.path.join(SCRIPT_DIR, "wechat_hook_v2.js")
|
||||
WECHAT_PACKAGE = "com.tencent.mm"
|
||||
GADGET_PORT = 27042
|
||||
import random as _random
|
||||
_ANTI_DETECT_PORT_RANGE = (10000, 60000)
|
||||
GADGET_PORT = _random.randint(*_ANTI_DETECT_PORT_RANGE)
|
||||
|
||||
ConnectionMode = Literal["usb", "gadget", "remote"]
|
||||
|
||||
|
||||
def _random_port() -> int:
|
||||
return _random.randint(*_ANTI_DETECT_PORT_RANGE)
|
||||
|
||||
|
||||
class FridaManager:
|
||||
"""Frida 会话管理器,支持 USB / Gadget / Remote 三种连接模式"""
|
||||
|
||||
@@ -46,8 +52,10 @@ class FridaManager:
|
||||
reconnect_interval: float = 5.0,
|
||||
mode: ConnectionMode = "gadget",
|
||||
gadget_host: str = "127.0.0.1",
|
||||
gadget_port: int = GADGET_PORT,
|
||||
gadget_port: int = 0,
|
||||
):
|
||||
if gadget_port == 0:
|
||||
gadget_port = _random_port()
|
||||
self.device_serial = device_serial
|
||||
self.script_path = script_path
|
||||
self.on_event = on_event
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
"""
|
||||
Agent端技能基类
|
||||
Agent端技能基类(含防封拟人化行为层)
|
||||
"""
|
||||
|
||||
import math
|
||||
import time
|
||||
import random
|
||||
import logging
|
||||
from typing import Dict, Any, List, Optional
|
||||
from typing import Dict, Any, List, Optional, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
@@ -25,6 +27,119 @@ class BaseSkill:
|
||||
"""
|
||||
self.d = device
|
||||
self.bus = bus
|
||||
|
||||
# ==========================================================
|
||||
# 防封拟人化行为层(所有 Skill 操作必须使用这些方法)
|
||||
# ==========================================================
|
||||
|
||||
@staticmethod
|
||||
def human_delay(min_sec: float = 0.5, max_sec: float = 3.0):
|
||||
"""拟人延迟:高斯分布,均值在 min-max 中间,避免机械化固定间隔"""
|
||||
mean = (min_sec + max_sec) / 2
|
||||
std = (max_sec - min_sec) / 6
|
||||
delay = max(min_sec, min(max_sec, random.gauss(mean, std)))
|
||||
time.sleep(delay)
|
||||
|
||||
def human_type(self, text: str, clear: bool = True):
|
||||
"""拟人输入:逐字输入 + 随机间隔 + 偶尔打错重输"""
|
||||
if clear:
|
||||
self.d.clear_text()
|
||||
self.human_delay(0.2, 0.5)
|
||||
for i, char in enumerate(text):
|
||||
self.d.send_keys(char)
|
||||
time.sleep(random.uniform(0.04, 0.18))
|
||||
if random.random() < 0.03 and i < len(text) - 1:
|
||||
wrong = random.choice("abcdefg1234")
|
||||
self.d.send_keys(wrong)
|
||||
time.sleep(random.uniform(0.1, 0.25))
|
||||
self.d.press("del")
|
||||
time.sleep(random.uniform(0.08, 0.15))
|
||||
|
||||
def human_click(self, x: int, y: int, offset: int = 8):
|
||||
"""拟人点击:加微小随机偏移,避免每次坐标完全一致"""
|
||||
dx = random.randint(-offset, offset)
|
||||
dy = random.randint(-offset, offset)
|
||||
self.d.click(x + dx, y + dy)
|
||||
self.human_delay(0.1, 0.4)
|
||||
|
||||
def human_swipe(
|
||||
self,
|
||||
start: Tuple[int, int],
|
||||
end: Tuple[int, int],
|
||||
duration: Optional[float] = None,
|
||||
):
|
||||
"""拟人滑动:贝塞尔曲线轨迹 + 随机中间控制点 + 随机持续时间"""
|
||||
if duration is None:
|
||||
duration = random.uniform(0.3, 0.8)
|
||||
ctrl_x = (start[0] + end[0]) // 2 + random.randint(-30, 30)
|
||||
ctrl_y = (start[1] + end[1]) // 2 + random.randint(-30, 30)
|
||||
steps = max(8, int(duration * 40))
|
||||
points = []
|
||||
for i in range(steps + 1):
|
||||
t = i / steps
|
||||
bx = (1 - t) ** 2 * start[0] + 2 * (1 - t) * t * ctrl_x + t ** 2 * end[0]
|
||||
by = (1 - t) ** 2 * start[1] + 2 * (1 - t) * t * ctrl_y + t ** 2 * end[1]
|
||||
points.append((int(bx), int(by)))
|
||||
try:
|
||||
self.d.swipe_points(points, duration)
|
||||
except (AttributeError, TypeError):
|
||||
self.d.swipe(start[0], start[1], end[0], end[1], duration=duration)
|
||||
|
||||
def random_browse(self, duration_sec: float = 30):
|
||||
"""随机浏览:模拟真人无目的翻看,用于养号/填充自然行为"""
|
||||
try:
|
||||
info = self.d.info
|
||||
w = info.get("displayWidth", 1080)
|
||||
h = info.get("displayHeight", 2400)
|
||||
except Exception:
|
||||
w, h = 1080, 2400
|
||||
end_time = time.time() + duration_sec
|
||||
while time.time() < end_time:
|
||||
action = random.choice(["scroll", "pause", "scroll", "pause", "tap_safe"])
|
||||
if action == "scroll":
|
||||
self.human_swipe((w // 2, int(h * 0.75)), (w // 2, int(h * 0.25)))
|
||||
self.human_delay(1.5, 4.0)
|
||||
elif action == "pause":
|
||||
self.human_delay(2.0, 6.0)
|
||||
elif action == "tap_safe":
|
||||
safe_x = random.randint(int(w * 0.1), int(w * 0.9))
|
||||
safe_y = random.randint(int(h * 0.2), int(h * 0.7))
|
||||
self.human_click(safe_x, safe_y, offset=5)
|
||||
self.human_delay(1.0, 3.0)
|
||||
self.d.press("back")
|
||||
self.human_delay(0.5, 1.5)
|
||||
|
||||
def natural_behavior_before(self, action: str = "send_message"):
|
||||
"""
|
||||
自然行为链前置:在执行关键操作前模拟真人浏览行为。
|
||||
打开对话 → 浏览历史(上滑) → 停顿 → 再执行操作
|
||||
30%概率触发;send_message/add_friend/post_moments 触发率更高(50%)
|
||||
"""
|
||||
high_risk = {"send_message", "add_friend", "post_moments", "batch_send", "mass_send"}
|
||||
trigger_rate = 0.5 if action in high_risk else 0.3
|
||||
if random.random() > trigger_rate:
|
||||
return
|
||||
try:
|
||||
info = self.d.info
|
||||
w = info.get("displayWidth", 1080)
|
||||
h = info.get("displayHeight", 2400)
|
||||
except Exception:
|
||||
w, h = 1080, 2400
|
||||
self.human_delay(0.8, 2.0)
|
||||
if random.random() < 0.6:
|
||||
self.human_swipe((w // 2, int(h * 0.3)), (w // 2, int(h * 0.7)))
|
||||
self.human_delay(1.0, 3.0)
|
||||
self.human_delay(0.5, 1.5)
|
||||
|
||||
def natural_behavior_after(self, action: str = "send_message"):
|
||||
"""
|
||||
自然行为链后置:操作完成后随机穿插自然行为。
|
||||
30%概率浏览朋友圈/首页等
|
||||
"""
|
||||
if random.random() > 0.3:
|
||||
return
|
||||
self.human_delay(1.0, 3.0)
|
||||
self.random_browse(duration_sec=random.uniform(5, 15))
|
||||
|
||||
def say(self, message: str, to_skill: Optional[str] = None, data: Optional[Dict[str, Any]] = None):
|
||||
"""向总线发一条消息,其它 Skill 可通过 read_chat 看到"""
|
||||
|
||||
@@ -128,6 +128,7 @@ class WechatSkill(BaseSkill):
|
||||
"""
|
||||
发送消息(先尝试视觉流程,失败则用规则流程)
|
||||
"""
|
||||
self.natural_behavior_before("send_message")
|
||||
start = time.time()
|
||||
try:
|
||||
out = self.send_message_with_vision(to_id, content, msg_type)
|
||||
@@ -347,6 +348,7 @@ class WechatSkill(BaseSkill):
|
||||
|
||||
def add_friend(self, user_id: str, message: str = "") -> Dict[str, Any]:
|
||||
"""添加好友"""
|
||||
self.natural_behavior_before("add_friend")
|
||||
try:
|
||||
self.launch()
|
||||
self.sleep(2)
|
||||
|
||||
@@ -124,18 +124,22 @@ async def get_connection_status() -> Dict[str, Any]:
|
||||
for item in online_devices:
|
||||
device_id = item.get("device_id", "")
|
||||
last_heartbeat = item.get("last_heartbeat", "")
|
||||
device_rows.append(
|
||||
{
|
||||
"device_id": device_id,
|
||||
"project_id": item.get("project_id", ""),
|
||||
"model": item.get("model", ""),
|
||||
"platform": item.get("platform", ""),
|
||||
"status": item.get("status", "online"),
|
||||
"last_heartbeat": last_heartbeat,
|
||||
"heartbeat_age_seconds": _heartbeat_age_seconds(last_heartbeat),
|
||||
"capabilities": item.get("capabilities", []),
|
||||
}
|
||||
)
|
||||
row = {
|
||||
"device_id": device_id,
|
||||
"project_id": item.get("project_id", ""),
|
||||
"model": item.get("model", ""),
|
||||
"platform": item.get("platform", ""),
|
||||
"status": item.get("status", "online"),
|
||||
"last_heartbeat": last_heartbeat,
|
||||
"heartbeat_age_seconds": _heartbeat_age_seconds(last_heartbeat),
|
||||
"capabilities": item.get("capabilities", []),
|
||||
"wechat_running": item.get("wechat_running"),
|
||||
"wechat_foreground": item.get("wechat_foreground"),
|
||||
"network_type": item.get("network_type"),
|
||||
"screen_on": item.get("screen_on"),
|
||||
"current_app": item.get("current_app"),
|
||||
}
|
||||
device_rows.append(row)
|
||||
|
||||
return {
|
||||
"code": 200,
|
||||
|
||||
169
sdk/app/services/account_lifecycle.py
Normal file
@@ -0,0 +1,169 @@
|
||||
"""
|
||||
防封模块 — 账号生命周期管理
|
||||
管理账号从注册到成熟的各阶段操作限制。
|
||||
|
||||
阶段定义:
|
||||
- 新号期 (0-7天):严格限制,以养号为主
|
||||
- 成长期 (8-30天):逐步放宽,日常操作
|
||||
- 成熟期 (30天+):正常使用限制
|
||||
"""
|
||||
|
||||
import logging
|
||||
import time
|
||||
from enum import Enum
|
||||
from typing import Dict, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_redis = None
|
||||
_redis_last_check: float = 0.0
|
||||
_REDIS_CHECK_INTERVAL = 60.0
|
||||
|
||||
|
||||
async def _get_redis():
|
||||
global _redis, _redis_last_check
|
||||
now = time.time()
|
||||
if _redis is not None:
|
||||
if now - _redis_last_check > _REDIS_CHECK_INTERVAL:
|
||||
try:
|
||||
await _redis.ping()
|
||||
_redis_last_check = now
|
||||
except Exception:
|
||||
logger.warning("Redis 连接失效,重置")
|
||||
_redis = None
|
||||
if _redis is not None:
|
||||
return _redis
|
||||
try:
|
||||
import redis.asyncio as aioredis
|
||||
from config import settings
|
||||
_redis = aioredis.from_url(settings.REDIS_URL, decode_responses=True)
|
||||
await _redis.ping()
|
||||
_redis_last_check = now
|
||||
return _redis
|
||||
except Exception:
|
||||
_redis = None
|
||||
return None
|
||||
|
||||
|
||||
_memory_accounts: Dict[str, dict] = {}
|
||||
|
||||
|
||||
class AccountPhase(str, Enum):
|
||||
NEW = "new" # 0-7天
|
||||
GROWING = "growing" # 8-30天
|
||||
MATURE = "mature" # 30天+
|
||||
|
||||
|
||||
PHASE_RULES = {
|
||||
AccountPhase.NEW: {
|
||||
"max_daily_add_friend": 10,
|
||||
"max_daily_send_message": 20,
|
||||
"max_daily_post_moments": 2,
|
||||
"max_daily_group_send": 5,
|
||||
"required_warm_up": True,
|
||||
"description": "新号期:严格限制,优先养号",
|
||||
},
|
||||
AccountPhase.GROWING: {
|
||||
"max_daily_add_friend": 30,
|
||||
"max_daily_send_message": 80,
|
||||
"max_daily_post_moments": 4,
|
||||
"max_daily_group_send": 30,
|
||||
"required_warm_up": False,
|
||||
"description": "成长期:逐步放宽",
|
||||
},
|
||||
AccountPhase.MATURE: {
|
||||
"max_daily_add_friend": 50,
|
||||
"max_daily_send_message": 200,
|
||||
"max_daily_post_moments": 5,
|
||||
"max_daily_group_send": 50,
|
||||
"required_warm_up": False,
|
||||
"description": "成熟期:正常操作",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
class AccountLifecycleManager:
|
||||
"""账号生命周期管理器(单例)"""
|
||||
|
||||
async def register_account(
|
||||
self, device_id: str, platform: str, register_timestamp: Optional[float] = None
|
||||
):
|
||||
"""
|
||||
注册/绑定一个账号。
|
||||
device_id+platform 唯一标识。
|
||||
register_timestamp: 账号注册时间戳,不传则用当前时间。
|
||||
"""
|
||||
ts = register_timestamp or time.time()
|
||||
key = f"acct:{device_id}:{platform}"
|
||||
data = {"register_ts": ts, "platform": platform, "device_id": device_id}
|
||||
r = await _get_redis()
|
||||
if r:
|
||||
try:
|
||||
import json
|
||||
await r.set(key, json.dumps(data), ex=86400 * 365)
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
_memory_accounts[key] = data
|
||||
|
||||
async def get_phase(self, device_id: str, platform: str) -> AccountPhase:
|
||||
key = f"acct:{device_id}:{platform}"
|
||||
data = None
|
||||
r = await _get_redis()
|
||||
if r:
|
||||
try:
|
||||
import json
|
||||
raw = await r.get(key)
|
||||
if raw:
|
||||
data = json.loads(raw)
|
||||
except Exception:
|
||||
pass
|
||||
if not data:
|
||||
data = _memory_accounts.get(key)
|
||||
if not data:
|
||||
return AccountPhase.NEW
|
||||
|
||||
age_days = (time.time() - data.get("register_ts", 0)) / 86400
|
||||
if age_days <= 7:
|
||||
return AccountPhase.NEW
|
||||
elif age_days <= 30:
|
||||
return AccountPhase.GROWING
|
||||
return AccountPhase.MATURE
|
||||
|
||||
async def is_new_account(self, device_id: str, platform: str) -> bool:
|
||||
phase = await self.get_phase(device_id, platform)
|
||||
return phase == AccountPhase.NEW
|
||||
|
||||
async def get_rules(self, device_id: str, platform: str) -> dict:
|
||||
phase = await self.get_phase(device_id, platform)
|
||||
return {
|
||||
"phase": phase.value,
|
||||
**PHASE_RULES[phase],
|
||||
}
|
||||
|
||||
async def check_allowed(
|
||||
self, device_id: str, platform: str, action: str
|
||||
) -> Dict[str, any]:
|
||||
"""
|
||||
检查某操作在当前生命周期是否允许(仅做检查,不消耗配额)。
|
||||
返回 {"allowed": bool, "phase": str, "reason": str}
|
||||
"""
|
||||
phase = await self.get_phase(device_id, platform)
|
||||
rules = PHASE_RULES[phase]
|
||||
|
||||
if phase == AccountPhase.NEW and rules.get("required_warm_up"):
|
||||
high_risk_actions = {
|
||||
"batch_add_friend", "batch_send", "send_red_packet",
|
||||
"transfer", "open_mini_program",
|
||||
}
|
||||
if action in high_risk_actions:
|
||||
return {
|
||||
"allowed": False,
|
||||
"phase": phase.value,
|
||||
"reason": f"新号期禁止高风险操作: {action}",
|
||||
}
|
||||
|
||||
return {"allowed": True, "phase": phase.value, "reason": ""}
|
||||
|
||||
|
||||
account_lifecycle = AccountLifecycleManager()
|
||||
133
sdk/app/services/anti_ban_alert.py
Normal file
@@ -0,0 +1,133 @@
|
||||
"""
|
||||
防封模块 — 风控告警推送
|
||||
当触发日限额耗尽、指纹碰撞、操作时段违规、连续失败等事件时自动推送到飞书群。
|
||||
包含冷却机制防止重复告警刷屏。
|
||||
"""
|
||||
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
FEISHU_WEBHOOK = "https://open.feishu.cn/open-apis/bot/v2/hook/d0f607da-ae26-43a0-9dbe-2c2c0b90743d"
|
||||
|
||||
ALERT_COOLDOWN = 300
|
||||
_last_alert_ts: dict = {}
|
||||
|
||||
|
||||
def _can_alert(alert_key: str) -> bool:
|
||||
now = time.time()
|
||||
last = _last_alert_ts.get(alert_key, 0)
|
||||
if now - last < ALERT_COOLDOWN:
|
||||
return False
|
||||
_last_alert_ts[alert_key] = now
|
||||
return True
|
||||
|
||||
|
||||
async def send_alert(
|
||||
level: str,
|
||||
title: str,
|
||||
detail: str,
|
||||
device_id: Optional[str] = None,
|
||||
platform: Optional[str] = None,
|
||||
):
|
||||
"""
|
||||
发送风控告警到飞书群。
|
||||
level: "warning" | "critical" | "info"
|
||||
"""
|
||||
alert_key = f"{level}:{title}:{device_id or 'global'}"
|
||||
if not _can_alert(alert_key):
|
||||
logger.debug(f"[告警冷却中] {alert_key}")
|
||||
return
|
||||
|
||||
icon = {"critical": "🚨", "warning": "⚠️", "info": "ℹ️"}.get(level, "📢")
|
||||
text = (
|
||||
f"{icon}【工作手机·风控告警】\n\n"
|
||||
f"级别:{level.upper()}\n"
|
||||
f"事件:{title}\n"
|
||||
)
|
||||
if device_id:
|
||||
text += f"设备:{device_id}\n"
|
||||
if platform:
|
||||
text += f"平台:{platform}\n"
|
||||
text += f"详情:{detail}\n"
|
||||
text += f"时间:{time.strftime('%Y-%m-%d %H:%M:%S')}"
|
||||
|
||||
color_map = {"critical": "red", "warning": "orange", "info": "blue"}
|
||||
card = {
|
||||
"header": {
|
||||
"template": color_map.get(level, "blue"),
|
||||
"title": {"content": f"{icon} 风控告警 · {title}", "tag": "plain_text"},
|
||||
},
|
||||
"elements": [
|
||||
{"tag": "div", "text": {"content": text, "tag": "lark_md"}},
|
||||
],
|
||||
}
|
||||
payload = {"msg_type": "interactive", "card": card}
|
||||
try:
|
||||
import httpx
|
||||
async with httpx.AsyncClient(timeout=5) as client:
|
||||
resp = await client.post(FEISHU_WEBHOOK, json=payload)
|
||||
body = resp.json()
|
||||
if body.get("code") != 0:
|
||||
logger.warning(f"飞书告警发送失败: {body}")
|
||||
except ImportError:
|
||||
try:
|
||||
import urllib.request
|
||||
data = json.dumps(payload, ensure_ascii=False).encode("utf-8")
|
||||
req = urllib.request.Request(
|
||||
FEISHU_WEBHOOK, data=data,
|
||||
headers={"Content-Type": "application/json; charset=utf-8"},
|
||||
)
|
||||
urllib.request.urlopen(req, timeout=5)
|
||||
except Exception as e:
|
||||
logger.warning(f"飞书告警发送失败(urllib): {e}")
|
||||
except Exception as e:
|
||||
logger.warning(f"飞书告警发送异常: {e}")
|
||||
|
||||
|
||||
async def alert_daily_limit(device_id: str, platform: str, action: str, count: int, limit: int):
|
||||
await send_alert(
|
||||
"warning",
|
||||
"日操作上限已达",
|
||||
f"{platform}.{action} 当前 {count}/{limit}",
|
||||
device_id, platform,
|
||||
)
|
||||
|
||||
|
||||
async def alert_fingerprint_collision(device_id: str, collided_with: list):
|
||||
await send_alert(
|
||||
"critical",
|
||||
"设备指纹碰撞",
|
||||
f"与设备 {collided_with} 指纹相同,存在关联封号风险",
|
||||
device_id,
|
||||
)
|
||||
|
||||
|
||||
async def alert_outside_hours(device_id: str, action: str):
|
||||
await send_alert(
|
||||
"warning",
|
||||
"非操作时段操作",
|
||||
f"尝试在禁止时段执行 {action}",
|
||||
device_id,
|
||||
)
|
||||
|
||||
|
||||
async def alert_sensitive_content(device_id: str, platform: str, keywords: list):
|
||||
await send_alert(
|
||||
"warning",
|
||||
"敏感内容拦截",
|
||||
f"检测到敏感词: {keywords[:5]}",
|
||||
device_id, platform,
|
||||
)
|
||||
|
||||
|
||||
async def alert_consecutive_failures(device_id: str, platform: str, fail_count: int):
|
||||
await send_alert(
|
||||
"critical",
|
||||
"连续操作失败",
|
||||
f"连续失败 {fail_count} 次,建议暂停该设备操作",
|
||||
device_id, platform,
|
||||
)
|
||||
149
sdk/app/services/content_guard.py
Normal file
@@ -0,0 +1,149 @@
|
||||
"""
|
||||
防封模块 — 内容防封守卫
|
||||
1. 敏感词过滤(黑名单 + 正则模式匹配)
|
||||
2. 内容差异化(变量替换 + 零宽字符 + 同义词 + 表情随机)
|
||||
3. 消息唯一性保障
|
||||
"""
|
||||
|
||||
import random
|
||||
import re
|
||||
import time
|
||||
import logging
|
||||
from typing import List, Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
SENSITIVE_WORDS: List[str] = [
|
||||
"加我微信", "微信号", "扫码", "二维码", "转账", "红包",
|
||||
"免费领", "赚钱", "兼职", "日赚", "月入", "暴利",
|
||||
"刷单", "代理", "优惠券", "点击链接", "限时", "秒杀",
|
||||
"抢购", "加群", "进群", "私聊我", "代购", "佣金",
|
||||
"保证赚", "零风险", "稳赚不赔", "日入过万", "躺赚",
|
||||
"加Telegram", "加TG", "加WhatsApp", "加WX",
|
||||
"V信", "威信", "薇信", "WX号",
|
||||
"银行卡", "信用卡套现", "贷款", "网贷",
|
||||
"色情", "赌博", "博彩", "棋牌",
|
||||
]
|
||||
|
||||
SENSITIVE_PATTERNS: List[re.Pattern] = [
|
||||
re.compile(r"https?://\S+"),
|
||||
re.compile(r"www\.\S+"),
|
||||
re.compile(r"[a-zA-Z0-9]{6,}\.(?:com|cn|net|org|xyz|top|cc|vip)\b"),
|
||||
re.compile(r"(?<!\d)1[3-9]\d{9}(?!\d)"),
|
||||
re.compile(r"[\w.+-]+@[\w-]+\.[\w.-]+"),
|
||||
re.compile(r"(?:微信|wx|WX|vx|VX)[号:\s]*[a-zA-Z0-9_]{5,20}"),
|
||||
re.compile(r"(?:QQ|qq)[号:\s]*\d{5,12}"),
|
||||
]
|
||||
|
||||
SAFE_REPLACEMENTS = {
|
||||
"加我微信": "联系我",
|
||||
"微信号": "联系方式",
|
||||
"扫码": "查看",
|
||||
"二维码": "图片",
|
||||
"免费领": "获取",
|
||||
"赚钱": "收益",
|
||||
"兼职": "合作",
|
||||
"优惠券": "福利",
|
||||
"点击链接": "查看详情",
|
||||
"限时": "近期",
|
||||
"秒杀": "特价",
|
||||
"抢购": "选购",
|
||||
"V信": "联系方式",
|
||||
"威信": "联系方式",
|
||||
"薇信": "联系方式",
|
||||
}
|
||||
|
||||
GREETINGS_BY_HOUR = {
|
||||
range(6, 9): ["早上好", "早安", "上午好"],
|
||||
range(9, 12): ["上午好", "您好"],
|
||||
range(12, 14): ["中午好", "午安"],
|
||||
range(14, 18): ["下午好", "您好"],
|
||||
range(18, 22): ["晚上好", "您好"],
|
||||
range(22, 24): ["晚安", "您好"],
|
||||
range(0, 6): ["您好"],
|
||||
}
|
||||
|
||||
EMOJI_POOL = [
|
||||
"😊", "👍", "🙏", "✨", "💯", "🎉", "😄", "🤝",
|
||||
"💪", "🌟", "👏", "😁", "🙂", "❤️", "🔥", "💐",
|
||||
]
|
||||
|
||||
ZWS = "\u200b" # 零宽空格
|
||||
ZWNJ = "\u200c" # 零宽非连接符
|
||||
ZWJ = "\u200d" # 零宽连接符
|
||||
INVISIBLE_CHARS = [ZWS, ZWNJ, ZWJ]
|
||||
|
||||
|
||||
def filter_sensitive(text: str) -> str:
|
||||
result = text
|
||||
for word, replacement in SAFE_REPLACEMENTS.items():
|
||||
result = result.replace(word, replacement)
|
||||
for word in SENSITIVE_WORDS:
|
||||
if word in result and word not in SAFE_REPLACEMENTS:
|
||||
result = result.replace(word, "*" * len(word))
|
||||
for pat in SENSITIVE_PATTERNS:
|
||||
result = pat.sub("[已过滤]", result)
|
||||
return result
|
||||
|
||||
|
||||
def has_sensitive_words(text: str) -> List[str]:
|
||||
found = []
|
||||
for word in SENSITIVE_WORDS:
|
||||
if word in text:
|
||||
found.append(word)
|
||||
for pat in SENSITIVE_PATTERNS:
|
||||
matches = pat.findall(text)
|
||||
if matches:
|
||||
found.extend(matches[:3])
|
||||
return found
|
||||
|
||||
|
||||
def _get_greeting() -> str:
|
||||
hour = time.localtime().tm_hour
|
||||
for hr_range, greetings in GREETINGS_BY_HOUR.items():
|
||||
if hour in hr_range:
|
||||
return random.choice(greetings)
|
||||
return "您好"
|
||||
|
||||
|
||||
def diversify_content(
|
||||
text: str,
|
||||
variables: Optional[dict] = None,
|
||||
add_invisible: bool = True,
|
||||
add_emoji: bool = False,
|
||||
) -> str:
|
||||
"""
|
||||
内容差异化处理,确保每条消息唯一。
|
||||
|
||||
Args:
|
||||
text: 原始消息文本
|
||||
variables: 自定义变量 {"{昵称}": "小明"} 等
|
||||
add_invisible: 是否插入零宽字符增加唯一性
|
||||
add_emoji: 是否在末尾随机添加表情
|
||||
"""
|
||||
result = text
|
||||
|
||||
result = result.replace("{时间}", _get_greeting())
|
||||
result = result.replace("{日期}", time.strftime("%m月%d日"))
|
||||
|
||||
if variables:
|
||||
for k, v in variables.items():
|
||||
result = result.replace(k, str(v))
|
||||
|
||||
result = filter_sensitive(result)
|
||||
|
||||
if add_invisible and len(result) > 2:
|
||||
num_inserts = random.randint(1, min(3, len(result) // 4))
|
||||
for _ in range(num_inserts):
|
||||
pos = random.randint(1, len(result) - 1)
|
||||
char = random.choice(INVISIBLE_CHARS)
|
||||
result = result[:pos] + char + result[pos:]
|
||||
|
||||
if add_emoji and random.random() < 0.4:
|
||||
result = result.rstrip() + random.choice(EMOJI_POOL)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def batch_diversify(texts: List[str], **kwargs) -> List[str]:
|
||||
return [diversify_content(t, **kwargs) for t in texts]
|
||||
@@ -1,7 +1,8 @@
|
||||
"""
|
||||
工作手机SDK v3.0 - 设备管理服务
|
||||
工作手机SDK v3.0 - 设备管理服务(含设备指纹校验)
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
from motor.motor_asyncio import AsyncIOMotorClient
|
||||
from typing import Optional, List
|
||||
import logging
|
||||
@@ -50,13 +51,17 @@ class DeviceManager:
|
||||
# ========== 设备管理 ==========
|
||||
|
||||
async def register_device(self, device_data: dict) -> dict:
|
||||
"""注册或更新设备"""
|
||||
if self.db is None:
|
||||
return {"device_id": device_data.get("device_id"), "updated": False}
|
||||
|
||||
"""注册或更新设备(自动计算并校验指纹)"""
|
||||
device_id = device_data.get("device_id")
|
||||
|
||||
fp_hash = self.compute_fingerprint(device_data)
|
||||
device_data["fingerprint_hash"] = fp_hash
|
||||
|
||||
if self.db is None:
|
||||
return {"device_id": device_id, "updated": False, "fingerprint": fp_hash}
|
||||
|
||||
device_data["updated_at"] = datetime.now()
|
||||
|
||||
|
||||
result = await self.db.devices.update_one(
|
||||
{"device_id": device_id},
|
||||
{
|
||||
@@ -65,8 +70,20 @@ class DeviceManager:
|
||||
},
|
||||
upsert=True
|
||||
)
|
||||
|
||||
return {"device_id": device_id, "updated": result.modified_count > 0}
|
||||
|
||||
collision = await self.check_fingerprint_collision(device_id, device_data)
|
||||
if collision["collision"]:
|
||||
logger.warning(
|
||||
f"[防封] 注册设备 {device_id} 指纹碰撞: {collision['collided_with']}"
|
||||
)
|
||||
|
||||
return {
|
||||
"device_id": device_id,
|
||||
"updated": result.modified_count > 0,
|
||||
"fingerprint": fp_hash,
|
||||
"fingerprint_collision": collision["collision"],
|
||||
"collided_with": collision.get("collided_with", []),
|
||||
}
|
||||
|
||||
async def get_device(self, device_id: str) -> Optional[dict]:
|
||||
"""获取设备信息"""
|
||||
@@ -101,6 +118,65 @@ class DeviceManager:
|
||||
upsert=True
|
||||
)
|
||||
|
||||
# ========== 设备指纹校验(防封 AF11) ==========
|
||||
|
||||
@staticmethod
|
||||
def compute_fingerprint(info: dict) -> str:
|
||||
"""从设备上报信息计算指纹哈希(MD5)"""
|
||||
keys = sorted([
|
||||
"brand", "model", "manufacturer", "android_version",
|
||||
"sdk_version", "serial", "imei", "mac", "bluetooth_mac",
|
||||
"screen_width", "screen_height", "density",
|
||||
"cpu_abi", "fingerprint", "android_id",
|
||||
])
|
||||
parts = []
|
||||
for k in keys:
|
||||
v = info.get(k, "")
|
||||
if v:
|
||||
parts.append(f"{k}={v}")
|
||||
raw = "|".join(parts)
|
||||
return hashlib.md5(raw.encode("utf-8")).hexdigest()
|
||||
|
||||
async def check_fingerprint_collision(self, device_id: str, info: dict) -> dict:
|
||||
"""
|
||||
检查设备指纹是否与其他设备碰撞。
|
||||
返回 {"collision": bool, "collided_with": [device_ids...]}
|
||||
"""
|
||||
fp = self.compute_fingerprint(info)
|
||||
|
||||
if self.db is None:
|
||||
return {"collision": False, "collided_with": [], "fingerprint": fp}
|
||||
|
||||
try:
|
||||
await self.db.devices.update_one(
|
||||
{"device_id": device_id},
|
||||
{"$set": {"fingerprint_hash": fp, "fingerprint_info": info}},
|
||||
upsert=True,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.warning(f"指纹写入失败: {e}")
|
||||
|
||||
try:
|
||||
cursor = self.db.devices.find(
|
||||
{"fingerprint_hash": fp, "device_id": {"$ne": device_id}},
|
||||
{"device_id": 1, "_id": 0},
|
||||
)
|
||||
collisions = [doc["device_id"] async for doc in cursor]
|
||||
except Exception as e:
|
||||
logger.warning(f"指纹碰撞查询失败: {e}")
|
||||
collisions = []
|
||||
|
||||
if collisions:
|
||||
logger.warning(
|
||||
f"[防封] 设备指纹碰撞! {device_id} 与 {collisions} 指纹相同 (hash={fp})"
|
||||
)
|
||||
|
||||
return {
|
||||
"collision": len(collisions) > 0,
|
||||
"collided_with": collisions,
|
||||
"fingerprint": fp,
|
||||
}
|
||||
|
||||
# ========== 命令日志 ==========
|
||||
|
||||
async def log_command(
|
||||
|
||||
284
sdk/app/services/rate_limiter.py
Normal file
@@ -0,0 +1,284 @@
|
||||
"""
|
||||
防封模块 — 三级频率限制器
|
||||
L1 全局限流 / L2 设备限流 / L3 动作限流(按平台+操作类型)
|
||||
|
||||
所有自动化操作必须经过本限流器检查,超频则等待或拒绝。
|
||||
依赖 Redis;Redis 不可用时退化为内存字典(重启丢失)。
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import random
|
||||
import time
|
||||
from typing import Dict, Optional, Tuple
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
_redis = None
|
||||
_redis_last_check: float = 0.0
|
||||
_REDIS_CHECK_INTERVAL = 60.0
|
||||
|
||||
|
||||
async def _get_redis():
|
||||
global _redis, _redis_last_check
|
||||
now = time.time()
|
||||
if _redis is not None:
|
||||
if now - _redis_last_check > _REDIS_CHECK_INTERVAL:
|
||||
try:
|
||||
await _redis.ping()
|
||||
_redis_last_check = now
|
||||
except Exception:
|
||||
logger.warning("Redis 连接失效,重置并退化内存模式")
|
||||
_redis = None
|
||||
if _redis is not None:
|
||||
return _redis
|
||||
try:
|
||||
import redis.asyncio as aioredis
|
||||
from config import settings
|
||||
_redis = aioredis.from_url(settings.REDIS_URL, decode_responses=True)
|
||||
await _redis.ping()
|
||||
_redis_last_check = now
|
||||
return _redis
|
||||
except Exception as e:
|
||||
logger.warning(f"Redis 不可用,退化为内存限流: {e}")
|
||||
_redis = None
|
||||
return None
|
||||
|
||||
|
||||
_memory_store: Dict[str, float] = {}
|
||||
_memory_daily: Dict[str, int] = {}
|
||||
_daily_date: str = ""
|
||||
|
||||
|
||||
def _today() -> str:
|
||||
return time.strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
PLATFORM_LIMITS: Dict[str, Dict[str, Dict]] = {
|
||||
"wechat": {
|
||||
"add_friend": {"interval": (180, 300), "daily_max": 50, "new_daily_max": 10},
|
||||
"send_message": {"interval": (30, 60), "daily_max": 200, "new_daily_max": 20},
|
||||
"post_moments": {"interval": (7200, 10800), "daily_max": 5, "new_daily_max": 2},
|
||||
"batch_send": {"interval": (300, 600), "daily_max": 50, "new_daily_max": 5},
|
||||
"like_moments": {"interval": (15, 45), "daily_max": 100, "new_daily_max": 20},
|
||||
"comment_moments": {"interval": (60, 180), "daily_max": 50, "new_daily_max": 10},
|
||||
"send_group_message": {"interval": (60, 120), "daily_max": 100, "new_daily_max": 15},
|
||||
},
|
||||
"douyin": {
|
||||
"send_message": {"interval": (60, 120), "daily_max": 100, "new_daily_max": 15},
|
||||
"follow": {"interval": (30, 60), "daily_max": 200, "new_daily_max": 30},
|
||||
"like": {"interval": (5, 15), "daily_max": 500, "new_daily_max": 100},
|
||||
"comment": {"interval": (60, 180), "daily_max": 50, "new_daily_max": 10},
|
||||
},
|
||||
"xhs": {
|
||||
"send_message": {"interval": (60, 180), "daily_max": 50, "new_daily_max": 10},
|
||||
"like": {"interval": (15, 30), "daily_max": 300, "new_daily_max": 50},
|
||||
"collect": {"interval": (30, 60), "daily_max": 200, "new_daily_max": 30},
|
||||
"comment": {"interval": (60, 180), "daily_max": 30, "new_daily_max": 5},
|
||||
},
|
||||
"xianyu": {
|
||||
"send_message": {"interval": (60, 120), "daily_max": 80, "new_daily_max": 15},
|
||||
},
|
||||
"soul": {
|
||||
"send_message": {"interval": (30, 90), "daily_max": 100, "new_daily_max": 20},
|
||||
},
|
||||
}
|
||||
|
||||
L1_GLOBAL_RPM = 100
|
||||
L2_DEVICE_RPM = 10
|
||||
|
||||
OPERATION_HOURS = (7, 23)
|
||||
|
||||
|
||||
class DailyLimitExceeded(Exception):
|
||||
"""日操作上限已达"""
|
||||
pass
|
||||
|
||||
|
||||
class OutsideOperationHours(Exception):
|
||||
"""非操作时段"""
|
||||
pass
|
||||
|
||||
|
||||
class RateLimitWaiting(Exception):
|
||||
"""需要等待(信息性,不应到达调用方)"""
|
||||
pass
|
||||
|
||||
|
||||
class AntiDetectRateLimiter:
|
||||
"""三级防封限流器(单例)"""
|
||||
|
||||
def get_limit(self, platform: str, action: str) -> Optional[Dict]:
|
||||
plat = PLATFORM_LIMITS.get(platform)
|
||||
if not plat:
|
||||
return None
|
||||
for key in (action, action.replace("-", "_")):
|
||||
if key in plat:
|
||||
return plat[key]
|
||||
return None
|
||||
|
||||
def check_operation_hours(self) -> bool:
|
||||
"""检查当前时间是否在允许操作时段内(默认 7:00-23:00)"""
|
||||
import datetime
|
||||
hour = datetime.datetime.now().hour
|
||||
return OPERATION_HOURS[0] <= hour < OPERATION_HOURS[1]
|
||||
|
||||
async def _check_l1_global(self) -> bool:
|
||||
"""L1 全局限流:所有设备总请求 <= L1_GLOBAL_RPM/分钟"""
|
||||
now = time.time()
|
||||
window_key = f"rl:l1:global:{int(now // 60)}"
|
||||
r = await _get_redis()
|
||||
if r:
|
||||
try:
|
||||
count = await r.incr(window_key)
|
||||
if count == 1:
|
||||
await r.expire(window_key, 120)
|
||||
if count > L1_GLOBAL_RPM:
|
||||
logger.warning(f"[RateLimiter] L1 全局限流触发: {count}/{L1_GLOBAL_RPM} RPM")
|
||||
return False
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
win_key = f"l1:{int(now // 60)}"
|
||||
_memory_daily[win_key] = _memory_daily.get(win_key, 0) + 1
|
||||
return _memory_daily[win_key] <= L1_GLOBAL_RPM
|
||||
|
||||
async def _check_l2_device(self, device_id: str) -> bool:
|
||||
"""L2 设备限流:单设备总请求 <= L2_DEVICE_RPM/分钟"""
|
||||
now = time.time()
|
||||
window_key = f"rl:l2:{device_id}:{int(now // 60)}"
|
||||
r = await _get_redis()
|
||||
if r:
|
||||
try:
|
||||
count = await r.incr(window_key)
|
||||
if count == 1:
|
||||
await r.expire(window_key, 120)
|
||||
if count > L2_DEVICE_RPM:
|
||||
logger.warning(f"[RateLimiter] L2 设备限流触发 {device_id}: {count}/{L2_DEVICE_RPM} RPM")
|
||||
return False
|
||||
return True
|
||||
except Exception:
|
||||
pass
|
||||
win_key = f"l2:{device_id}:{int(now // 60)}"
|
||||
_memory_daily[win_key] = _memory_daily.get(win_key, 0) + 1
|
||||
return _memory_daily[win_key] <= L2_DEVICE_RPM
|
||||
|
||||
async def check_and_wait(
|
||||
self,
|
||||
device_id: str,
|
||||
platform: str,
|
||||
action: str,
|
||||
is_new_account: bool = False,
|
||||
) -> float:
|
||||
"""
|
||||
三级检查:L1全局 → L2设备 → L3动作 + 时段检查。
|
||||
返回实际等待的秒数。
|
||||
若日上限已到,抛 DailyLimitExceeded。
|
||||
"""
|
||||
if not self.check_operation_hours():
|
||||
raise OutsideOperationHours(
|
||||
f"当前不在操作时段 ({OPERATION_HOURS[0]}:00-{OPERATION_HOURS[1]}:00)"
|
||||
)
|
||||
|
||||
if not await self._check_l1_global():
|
||||
wait = random.uniform(3.0, 8.0)
|
||||
logger.info(f"[RateLimiter] L1 全局超限,等待 {wait:.1f}s")
|
||||
await asyncio.sleep(wait)
|
||||
|
||||
if not await self._check_l2_device(device_id):
|
||||
wait = random.uniform(5.0, 15.0)
|
||||
logger.info(f"[RateLimiter] L2 设备超限 {device_id},等待 {wait:.1f}s")
|
||||
await asyncio.sleep(wait)
|
||||
|
||||
limit = self.get_limit(platform, action)
|
||||
if limit is None:
|
||||
await asyncio.sleep(random.uniform(0.5, 2.0))
|
||||
return 0.0
|
||||
|
||||
daily_max = limit.get("new_daily_max" if is_new_account else "daily_max", 9999)
|
||||
daily_count = await self._get_daily_count(device_id, platform, action)
|
||||
if daily_count >= daily_max:
|
||||
raise DailyLimitExceeded(
|
||||
f"{platform}.{action} 已达日上限 {daily_max}(当前 {daily_count})"
|
||||
)
|
||||
|
||||
interval_range: Tuple[float, float] = limit["interval"]
|
||||
min_wait = random.uniform(*interval_range)
|
||||
|
||||
last_ts = await self._get_last_ts(device_id, platform, action)
|
||||
elapsed = time.time() - last_ts if last_ts else float("inf")
|
||||
wait = max(0.0, min_wait - elapsed)
|
||||
|
||||
if wait > 0:
|
||||
jitter = random.uniform(-wait * 0.1, wait * 0.15)
|
||||
wait = max(0.3, wait + jitter)
|
||||
logger.info(
|
||||
f"[RateLimiter] {device_id}/{platform}.{action} 等待 {wait:.1f}s "
|
||||
f"(interval={interval_range}, daily={daily_count}/{daily_max})"
|
||||
)
|
||||
await asyncio.sleep(wait)
|
||||
|
||||
await self._record(device_id, platform, action)
|
||||
return wait
|
||||
|
||||
async def get_daily_count(self, device_id: str, platform: str, action: str) -> int:
|
||||
return await self._get_daily_count(device_id, platform, action)
|
||||
|
||||
def get_platform_config(self, platform: str, action: str) -> Optional[Dict]:
|
||||
"""获取某平台某动作的限流配置(供风控看板展示)"""
|
||||
return self.get_limit(platform, action)
|
||||
|
||||
# ---- 存储层 ----
|
||||
|
||||
async def _get_last_ts(self, device_id: str, platform: str, action: str) -> float:
|
||||
key = f"rl:ts:{device_id}:{platform}:{action}"
|
||||
r = await _get_redis()
|
||||
if r:
|
||||
try:
|
||||
val = await r.get(key)
|
||||
return float(val) if val else 0.0
|
||||
except Exception:
|
||||
pass
|
||||
return _memory_store.get(key, 0.0)
|
||||
|
||||
async def _get_daily_count(self, device_id: str, platform: str, action: str) -> int:
|
||||
global _daily_date
|
||||
today = _today()
|
||||
key = f"rl:daily:{today}:{device_id}:{platform}:{action}"
|
||||
r = await _get_redis()
|
||||
if r:
|
||||
try:
|
||||
val = await r.get(key)
|
||||
return int(val) if val else 0
|
||||
except Exception:
|
||||
pass
|
||||
if _daily_date != today:
|
||||
_memory_daily.clear()
|
||||
_daily_date = today
|
||||
return _memory_daily.get(key, 0)
|
||||
|
||||
async def _record(self, device_id: str, platform: str, action: str):
|
||||
now = time.time()
|
||||
today = _today()
|
||||
ts_key = f"rl:ts:{device_id}:{platform}:{action}"
|
||||
daily_key = f"rl:daily:{today}:{device_id}:{platform}:{action}"
|
||||
r = await _get_redis()
|
||||
if r:
|
||||
try:
|
||||
pipe = r.pipeline()
|
||||
pipe.set(ts_key, str(now), ex=86400)
|
||||
pipe.incr(daily_key)
|
||||
pipe.expire(daily_key, 86400)
|
||||
await pipe.execute()
|
||||
return
|
||||
except Exception:
|
||||
pass
|
||||
_memory_store[ts_key] = now
|
||||
global _daily_date
|
||||
if _daily_date != today:
|
||||
_memory_daily.clear()
|
||||
_daily_date = today
|
||||
_memory_daily[daily_key] = _memory_daily.get(daily_key, 0) + 1
|
||||
|
||||
|
||||
rate_limiter = AntiDetectRateLimiter()
|
||||
@@ -129,6 +129,18 @@ class WebSocketHub:
|
||||
elif msg_type == "heartbeat":
|
||||
if device_id in self.device_info:
|
||||
self.device_info[device_id]["last_heartbeat"] = datetime.now().isoformat()
|
||||
hb_status = data.get("status") or {}
|
||||
if hb_status:
|
||||
self.device_info[device_id]["quick_status"] = hb_status
|
||||
if hb_status.get("wechat_running") is not None:
|
||||
self.device_info[device_id]["wechat_running"] = hb_status["wechat_running"]
|
||||
self.device_info[device_id]["wechat_foreground"] = hb_status.get("wechat_foreground", False)
|
||||
if hb_status.get("network_type"):
|
||||
self.device_info[device_id]["network_type"] = hb_status["network_type"]
|
||||
if hb_status.get("screen_on") is not None:
|
||||
self.device_info[device_id]["screen_on"] = hb_status["screen_on"]
|
||||
if hb_status.get("current_app"):
|
||||
self.device_info[device_id]["current_app"] = hb_status["current_app"]
|
||||
try:
|
||||
dm = _get_device_manager()
|
||||
await dm.update_heartbeat(device_id)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"version": "8.0.56",
|
||||
"screen": "1080x2400",
|
||||
"updated": "2026-03-14 16:39:18",
|
||||
"updated": "2026-03-14 17:57:15",
|
||||
"pages": {
|
||||
"me": {
|
||||
"recorded_at": "2026-03-14 16:32",
|
||||
@@ -323,9 +323,9 @@
|
||||
}
|
||||
],
|
||||
"target_page": "account_security",
|
||||
"success_count": 0,
|
||||
"success_count": 1,
|
||||
"fail_count": 0,
|
||||
"last_success": null
|
||||
"last_success": "2026-03-14 17:57:15"
|
||||
},
|
||||
"to_safety_center": {
|
||||
"name": "to_safety_center",
|
||||
|
||||
5
sdk/data/operation_logs/ops_20260314.jsonl
Normal file
@@ -0,0 +1,5 @@
|
||||
{"action": "send_message", "params": {"to_id": "阿猫", "content": "你好阿猫", "msg_type": "text"}, "device": "xgfe65eimrrofyws", "started_at": "2026-03-14 17:56:03", "steps": [], "success": true, "duration_ms": 24480, "ended_at": "2026-03-14 17:56:27", "result_summary": "{'success': True, 'mode': 'adb', 'timestamp': 1773482187961, 'message_id': 'adb_1773482187961'}"}
|
||||
{"action": "check_restrictions", "params": {}, "device": "xgfe65eimrrofyws", "started_at": "2026-03-14 17:57:01", "steps": [{"type": "tap_tab", "detail": "微信", "coords": [135, 2148], "success": true, "ts": 3.15}], "success": true, "duration_ms": 4025, "ended_at": "2026-03-14 17:57:05", "result_summary": "{'success': True, 'mode': 'adb', 'timestamp': 1773482225487, 'restricted': False, 'restrictions': [], 'note': '未检测到功能限制'}"}
|
||||
{"action": "get_profile", "params": {}, "device": "xgfe65eimrrofyws", "started_at": "2026-03-14 17:57:00", "steps": [{"type": "tap_tab", "detail": "我", "coords": [945, 2148], "success": true, "ts": 3.18}], "success": true, "duration_ms": 7088, "ended_at": "2026-03-14 17:57:07", "result_summary": "{'success': True, 'mode': 'adb', 'timestamp': 1773482227259, 'profile': {'nickname': 'BL-卡若私域%1.0 | NT11|45'}}"}
|
||||
{"action": "check_account_status", "params": {}, "device": "xgfe65eimrrofyws", "started_at": "2026-03-14 17:57:00", "steps": [{"type": "tap_tab", "detail": "我", "coords": [945, 2148], "success": true, "ts": 3.68}, {"type": "tap_tab", "detail": "我", "coords": [945, 2148], "success": true, "ts": 5.19}, {"type": "scroll_up", "detail": "", "coords": null, "success": true, "ts": 6.7}, {"type": "tap_text", "detail": "设置", "coords": [197, 1676], "success": true, "ts": 9.09}, {"type": "scroll_to_top", "detail": "", "coords": null, "success": true, "ts": 12.56}, {"type": "tap_text", "detail": "账号与安全", "coords": [250, 300], "success": true, "ts": 14.96}], "success": true, "duration_ms": 18596, "ended_at": "2026-03-14 17:57:19", "result_summary": "{'success': True, 'mode': 'adb', 'timestamp': 1773482239343, 'status': {'account_active': True}}"}
|
||||
{"action": "get_contacts", "params": {"limit": 5}, "device": "xgfe65eimrrofyws", "started_at": "2026-03-14 17:57:22", "steps": [{"type": "tap_tab", "detail": "通讯录", "coords": [405, 2148], "success": true, "ts": 5.26}], "success": true, "duration_ms": 6260, "ended_at": "2026-03-14 17:57:29", "result_summary": "{'success': True, 'mode': 'adb', 'timestamp': 1773482249186, 'contacts': [], 'count': 0}"}
|
||||
@@ -5,6 +5,40 @@
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-15 | 防封模块深度验证+13项BUG修复
|
||||
|
||||
### 执行人: 火炬(机擎全员)
|
||||
|
||||
### 验证方式
|
||||
- 代码深度审计(9个文件,5个维度)
|
||||
- 防封策略文档 vs 代码逐章差距分析(55项需求对照)
|
||||
- 全网搜索最新微信2025-2026风控规则+Frida反检测技术
|
||||
|
||||
### 发现问题(审计前覆盖率仅40%)
|
||||
- **3个P0崩溃BUG**: unified.py get_phase缺await+缺参数、L1/L2限流仅定义常量未实现、80%端点绕过防封守卫
|
||||
- **7个P1缺陷**: 默认账号阶段MATURE(应为NEW)、Redis断线永不恢复、贝塞尔滑动死代码、Frida端口模块共享、无操作时段控制、无告警推送、敏感词库不足
|
||||
- **多项缺失**: 自然行为链、正则匹配、内容格式校验
|
||||
|
||||
### 修复内容(13项,全部完成)
|
||||
1. [P0] unified.py: get_phase/get_rules 补 await + platform 参数
|
||||
2. [P0] rate_limiter: 实现 L1 全局限流(100RPM) + L2 设备限流(10RPM) 滑动窗口
|
||||
3. [P0] rate_limiter: 新增操作时段控制(7:00-23:00),OutsideOperationHours 异常
|
||||
4. [P0] unified.py: 补齐6个端点防封守卫(comment/reply, batch-add, group/send, moments/like, moments/comment, mass-send)
|
||||
5. [P0] unified.py: guard返回字段统一为"reason"
|
||||
6. [P1] account_lifecycle: 未注册账号默认 NEW→最严格(原MATURE→最宽松)
|
||||
7. [P1] account_lifecycle: Redis set 加 TTL(1年)
|
||||
8. [P1] rate_limiter + account_lifecycle: Redis 健康检查(60s间隔ping),断线自动恢复
|
||||
9. [P1] base.py: 贝塞尔滑动 swipe_points 替代直线 swipe(降级兼容)
|
||||
10. [P1] frida_manager: 端口从模块级改为实例级(_random_port())
|
||||
11. [P1] 新增 anti_ban_alert.py 风控告警(5种告警:日限额/指纹碰撞/时段违规/敏感内容/连续失败)
|
||||
12. [P1] content_guard: 扩充敏感词40+,新增7个正则模式(URL/手机号/邮箱/微信号/QQ号)
|
||||
13. [P1] base.py + wechat/skill.py: 新增 natural_behavior_before/after 自然行为链
|
||||
|
||||
### 进度: Phase 5 覆盖率 40% → 70%+ | 整体项目 95%
|
||||
### 下一步: Phase 3B 独立管理端 或 Phase 4 部署上线
|
||||
|
||||
---
|
||||
|
||||
## 2026-03-14 | 第8次对话 — 微信全功能v2 + 导航缓存系统
|
||||
|
||||
### 完成内容
|
||||
|
||||
BIN
开发文档/2、架构/工作手机SDKv3_系统架构图.png
Normal file
|
After Width: | Height: | Size: 933 KiB |
BIN
开发文档/2、架构/工作手机SDK架构图.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
BIN
开发文档/2、架构/工作手机微信控制系统架构图.png
Normal file
|
After Width: | Height: | Size: 244 KiB |
BIN
开发文档/2、架构/工作手机设备端运转逻辑.png
Normal file
|
After Width: | Height: | Size: 1.3 MiB |
115
开发文档/2、架构/微信控制-设备服务器交互图.html
Normal file
@@ -0,0 +1,115 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>工作手机 · 微信控制 · 设备与服务器交互流程图</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; }
|
||||
body {
|
||||
font-family: -apple-system, "PingFang SC", "Microsoft YaHei", sans-serif;
|
||||
background: #f5f7fa;
|
||||
padding: 40px;
|
||||
margin: 0;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
color: #1a365d;
|
||||
font-size: 24px;
|
||||
margin-bottom: 32px;
|
||||
}
|
||||
.flow {
|
||||
max-width: 900px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
.box {
|
||||
border-radius: 12px;
|
||||
padding: 20px 24px;
|
||||
margin: 16px 0;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.08);
|
||||
}
|
||||
.box-1 { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: #fff; }
|
||||
.box-2 { background: linear-gradient(135deg, #11998e 0%, #38ef7d 100%); color: #fff; }
|
||||
.box-3 { background: #fff; border: 2px solid #e2e8f0; }
|
||||
.arrow {
|
||||
text-align: center;
|
||||
color: #64748b;
|
||||
font-size: 14px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
.devices {
|
||||
display: flex;
|
||||
gap: 24px;
|
||||
justify-content: center;
|
||||
flex-wrap: wrap;
|
||||
margin: 24px 0;
|
||||
}
|
||||
.device {
|
||||
width: 200px;
|
||||
background: #fff;
|
||||
border-radius: 12px;
|
||||
padding: 20px;
|
||||
border: 2px solid #e2e8f0;
|
||||
box-shadow: 0 2px 8px rgba(0,0,0,0.06);
|
||||
}
|
||||
.device-title {
|
||||
font-weight: bold;
|
||||
color: #1e293b;
|
||||
margin-bottom: 12px;
|
||||
font-size: 16px;
|
||||
}
|
||||
.device-item {
|
||||
font-size: 13px;
|
||||
color: #475569;
|
||||
margin: 6px 0;
|
||||
}
|
||||
.note {
|
||||
text-align: center;
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
margin-top: 32px;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h1>工作手机 · 微信控制 · 设备与服务器交互流程图</h1>
|
||||
<div class="flow">
|
||||
<div class="box box-1">
|
||||
<strong>1) 存客宝后端</strong><br>
|
||||
WorkPhoneSDK::sendMessage('wechat', to_id, content)
|
||||
</div>
|
||||
<div class="arrow">↓ HTTPS POST /api/v3/message/send</div>
|
||||
<div class="box box-2">
|
||||
<strong>2) 工作手机SDK服务器</strong><br>
|
||||
unified 路由 → 通道选择 → 组包 { script: wechat, action: send_message, params }<br>
|
||||
WebSocket Hub 下发 execute
|
||||
</div>
|
||||
<div class="arrow">↓ WebSocket execute</div>
|
||||
<div class="devices">
|
||||
<div class="device">
|
||||
<div class="device-title">设备 A</div>
|
||||
<div class="device-item">工作手机 Agent APP</div>
|
||||
<div class="device-item">WeChat Skill</div>
|
||||
<div class="device-item">uiautomator2 操作微信</div>
|
||||
</div>
|
||||
<div class="device">
|
||||
<div class="device-title">设备 B</div>
|
||||
<div class="device-item">工作手机 Agent APP</div>
|
||||
<div class="device-item">WeChat Skill</div>
|
||||
<div class="device-item">uiautomator2 操作微信</div>
|
||||
</div>
|
||||
<div class="device">
|
||||
<div class="device-title">设备 C</div>
|
||||
<div class="device-item">工作手机 Agent APP</div>
|
||||
<div class="device-item">WeChat Skill</div>
|
||||
<div class="device-item">uiautomator2 操作微信</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="arrow">↑ response { success, data }</div>
|
||||
<div class="box box-3">
|
||||
<strong>3) 返回存客宝</strong><br>
|
||||
服务器收到设备 response → 返回给存客宝
|
||||
</div>
|
||||
</div>
|
||||
<p class="note">架构图风格标准 · 扁平化 · 蓝绿配色 · 开发文档/2、架构/</p>
|
||||
</body>
|
||||
</html>
|
||||
45
开发文档/2、架构/架构图风格标准.md
Normal file
@@ -0,0 +1,45 @@
|
||||
# 工作手机 · 架构图风格标准
|
||||
|
||||
> **用途**:后续所有架构图、流程图、应用示意均按此风格生成,保持统一视觉。
|
||||
|
||||
---
|
||||
|
||||
## 一、风格要点
|
||||
|
||||
| 维度 | 标准 |
|
||||
|------|------|
|
||||
| **语言** | 中文 |
|
||||
| **风格** | 扁平化、科技感、现代 UI |
|
||||
| **主色** | 蓝色、绿色、浅灰 |
|
||||
| **形状** | 圆角矩形、清晰层次 |
|
||||
| **箭头** | 实线箭头表示数据/控制流向 |
|
||||
| **字体** | 清晰可读、层级分明 |
|
||||
|
||||
---
|
||||
|
||||
## 二、分层与布局
|
||||
|
||||
- **自上而下**:业务层 → 网关层 → 服务层 → 数据层 → 设备层
|
||||
- **同层并排**:同级模块水平排列(如设备A/B/C、MongoDB/Redis/MinIO)
|
||||
- **连接线**:标明协议(HTTPS、WebSocket)与端口
|
||||
|
||||
---
|
||||
|
||||
## 三、模块标注规范
|
||||
|
||||
- **服务/组件**:方框内写中文名称 + 英文缩写(如「设备管理 DeviceSvc」)
|
||||
- **设备**:标注 设备A / 设备B / 设备C,可加地域(厦门/北京/上海)
|
||||
- **数据流**:箭头旁标注「发消息」「心跳」「execute」等动作
|
||||
|
||||
---
|
||||
|
||||
## 四、参考图
|
||||
|
||||
| 图名 | 路径 | 说明 |
|
||||
|------|------|------|
|
||||
| 系统架构图 | 工作手机SDK架构图.png | 整体四层架构 |
|
||||
| 设备-服务器交互 | 设备与服务器交互流程_微信控制.png | 设备↔服务器、微信控制完整链路 |
|
||||
|
||||
---
|
||||
|
||||
*后续生成架构图时,按以上标准执行。*
|
||||
@@ -7,7 +7,10 @@
|
||||
|
||||
## 一、整体架构图(AI+Skill版)
|
||||
|
||||
> **图形化架构图**:见 [工作手机SDK架构图.png](工作手机SDK架构图.png),可直接打开查看。
|
||||
> **图形化架构图**:
|
||||
> - 整体架构:[工作手机SDK架构图.png](工作手机SDK架构图.png)
|
||||
> - 微信控制·设备服务器交互:[微信控制-设备服务器交互图.html](微信控制-设备服务器交互图.html)(浏览器打开)
|
||||
> - 风格标准:[架构图风格标准.md](架构图风格标准.md)(以后生成架构图按此风格)
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────────────────────┐
|
||||
@@ -268,6 +271,9 @@ class CaptureService:
|
||||
|
||||
## 四、数据流设计
|
||||
|
||||
> **图形化流程图**:设备与服务器交互(微信控制)见 [设备与服务器交互流程_微信控制.png](设备与服务器交互流程_微信控制.png)。
|
||||
> **架构图风格标准**:见 [架构图风格标准.md](架构图风格标准.md),后续架构图均按此风格生成。
|
||||
|
||||
### 4.1 API请求流程
|
||||
|
||||
```mermaid
|
||||
|
||||
BIN
开发文档/2、架构/设备与服务器交互流程_微信控制.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
BIN
开发文档/2、架构/防封服务端核心能力拆细图.png
Normal file
|
After Width: | Height: | Size: 1.1 MiB |
89
开发文档/2、架构/防封模块架构图.html
Normal file
@@ -0,0 +1,89 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>防封模块架构图</title>
|
||||
<style>
|
||||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||||
body { font-family: -apple-system, "PingFang SC", sans-serif; background: #f5f7fa; padding: 32px; }
|
||||
.title { text-align: center; font-size: 22px; color: #1a365d; margin-bottom: 28px; font-weight: 600; }
|
||||
.wrap { max-width: 950px; margin: 0 auto; }
|
||||
.arrow-label { text-align: center; font-size: 13px; color: #64748b; padding: 6px 0; }
|
||||
.arrow-red { color: #dc2626; font-weight: 500; }
|
||||
.layer { margin-bottom: 16px; }
|
||||
.row { display: flex; justify-content: center; gap: 12px; flex-wrap: wrap; }
|
||||
.box {
|
||||
border-radius: 10px; padding: 14px 20px; text-align: center;
|
||||
box-shadow: 0 1px 4px rgba(0,0,0,.08);
|
||||
}
|
||||
.box-top { background: linear-gradient(135deg, #60a5fa 0%, #3b82f6 100%); color: #fff; font-size: 14px; }
|
||||
.box-sdk { background: linear-gradient(135deg, #0d9488 0%, #059669 100%); color: #fff; padding: 16px 28px; font-size: 15px; }
|
||||
.server-wrap {
|
||||
border: 2px solid #cbd5e1; border-radius: 12px; padding: 20px; margin: 12px 0;
|
||||
background: #f8fafc;
|
||||
}
|
||||
.server-title { font-size: 14px; color: #475569; margin-bottom: 14px; font-weight: 600; }
|
||||
.box-inner { background: linear-gradient(135deg, #5eead4 0%, #2dd4bf 100%); color: #0f766e; padding: 12px 18px; font-size: 13px; border-radius: 8px; }
|
||||
.box-inner2 { background: linear-gradient(135deg, #67e8f9 0%, #22d3ee 100%); color: #0e7490; padding: 12px 18px; font-size: 13px; border-radius: 8px; }
|
||||
.phone-wrap { border: 2px solid #cbd5e1; border-radius: 12px; padding: 16px; display: inline-block; margin: 0 8px; background: #fff; }
|
||||
.phone-title { font-size: 13px; color: #475569; margin-bottom: 10px; font-weight: 600; }
|
||||
.flow-h { display: flex; align-items: center; justify-content: center; gap: 8px; flex-wrap: wrap; margin: 10px 0; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="title">防封模块 · 四层架构</div>
|
||||
<div class="wrap">
|
||||
<div class="row layer">
|
||||
<div class="box box-top">存客宝 / 业务方</div>
|
||||
</div>
|
||||
<div class="arrow-label">sendMessage / 请求</div>
|
||||
<div class="row layer">
|
||||
<div class="box box-sdk">统一 API 接口</div>
|
||||
</div>
|
||||
<div class="arrow-label">HTTPS REST API</div>
|
||||
|
||||
<div class="server-wrap">
|
||||
<div class="server-title">防封服务端</div>
|
||||
<div class="flow-h">
|
||||
<div class="box box-inner">鉴权</div>
|
||||
<span>→</span>
|
||||
<div class="box box-inner">防封守卫</div>
|
||||
<span>→</span>
|
||||
<div class="box box-inner">通道选择</div>
|
||||
<span>→</span>
|
||||
<div class="box box-inner">执行</div>
|
||||
</div>
|
||||
<div class="arrow-label" style="margin-top:12px">防封守卫</div>
|
||||
<div class="flow-h">
|
||||
<div class="box box-inner2">rate_limiter<br>三级限流</div>
|
||||
<div class="box box-inner2">content_guard<br>内容过滤</div>
|
||||
<div class="box box-inner2">account_lifecycle<br>账号生命周期</div>
|
||||
<div class="box box-inner2">device_manager<br>指纹碰撞</div>
|
||||
<div class="box box-inner2">anti_ban_alert<br>风控告警</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="arrow-label arrow-red">WebSocket execute · 指令下发</div>
|
||||
|
||||
<div class="row layer">
|
||||
<div class="phone-wrap">
|
||||
<div class="phone-title">工作手机 · 设备端防封</div>
|
||||
<div class="box box-inner" style="margin:4px 0">Agent</div>
|
||||
<div class="arrow-label" style="font-size:11px">设备指纹 / Root隐藏 / Frida反检测 / U2拟人化</div>
|
||||
</div>
|
||||
<div class="phone-wrap">
|
||||
<div class="phone-title">应用层 Skill</div>
|
||||
<div class="box box-inner2" style="margin:4px 0">微信 抖音 小红书 闲鱼</div>
|
||||
</div>
|
||||
<div class="phone-wrap">
|
||||
<div class="phone-title">行为层防封</div>
|
||||
<div class="box box-inner2" style="margin:4px 0">human_delay · human_type · 贝塞尔轨迹</div>
|
||||
</div>
|
||||
<div class="phone-wrap">
|
||||
<div class="phone-title">微信APP</div>
|
||||
<div class="box box-inner" style="margin:4px 0">uiautomator2</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
43
开发文档/2、架构/防封模块架构图.mmd
Normal file
@@ -0,0 +1,43 @@
|
||||
flowchart TB
|
||||
subgraph 核心公式
|
||||
F[防封 = 设备隔离 × 环境伪装 × 行为拟人 × 频率控制 × 内容差异 × 监控应急]
|
||||
end
|
||||
|
||||
subgraph 一_设备端["一、设备端防封 (Agent)"]
|
||||
D1[设备指纹隔离<br/>一机一号一卡一IP]
|
||||
D2[Root 隐藏<br/>Magisk + Shamiko + HMA]
|
||||
D3[Frida 反检测<br/>Phantom-Frida]
|
||||
D4[U2 拟人化<br/>随机延迟/轨迹]
|
||||
end
|
||||
|
||||
subgraph 二_服务端["二、服务端防封"]
|
||||
REQ[请求进入 → 鉴权]
|
||||
GUARD[防封守卫]
|
||||
REQ --> GUARD
|
||||
GUARD --> R1[rate_limiter 三级限流]
|
||||
GUARD --> R2[content_guard 内容过滤]
|
||||
GUARD --> R3[account_lifecycle 账号生命周期]
|
||||
GUARD --> R4[device_manager 指纹碰撞]
|
||||
GUARD --> R5[anti_ban_alert 风控告警]
|
||||
R1 --> EXEC[通道路由 → 执行]
|
||||
R2 --> EXEC
|
||||
R3 --> EXEC
|
||||
end
|
||||
|
||||
subgraph 三_应用层["三、应用层防封 (Skill)"]
|
||||
A1[微信]
|
||||
A2[抖音]
|
||||
A3[小红书]
|
||||
A4[闲鱼]
|
||||
end
|
||||
|
||||
subgraph 四_行为层["四、行为层防封"]
|
||||
B1[human_delay 拟人延迟]
|
||||
B2[human_type 逐字输入]
|
||||
B3[贝塞尔轨迹滑动]
|
||||
end
|
||||
|
||||
F --> 一_设备端
|
||||
一_设备端 --> 二_服务端
|
||||
二_服务端 --> 三_应用层
|
||||
三_应用层 --> 四_行为层
|
||||
BIN
开发文档/2、架构/防封模块架构图.png
Normal file
|
After Width: | Height: | Size: 1.0 MiB |
657
开发文档/5、接口/API架构与交互流程图.html
Normal file
@@ -0,0 +1,657 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>机擎SDK · API架构与交互流程</title>
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Orbitron:wght@400;700;900&family=Noto+Sans+SC:wght@300;400;500;700&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
*,*::before,*::after{margin:0;padding:0;box-sizing:border-box}
|
||||
:root{
|
||||
--bg:#0a1628;--bg2:#0d1f3c;--bg3:#132744;
|
||||
--cyan:#00d4ff;--green:#00ff88;--orange:#ff6b35;--gold:#ffd700;
|
||||
--purple:#a855f7;--red:#ff4757;--blue:#3b82f6;--pink:#ec4899;
|
||||
--text:#e2e8f0;--text2:#94a3b8;--text3:#64748b;
|
||||
--card:#0f1f38;--border:rgba(0,212,255,.12);
|
||||
}
|
||||
html{scroll-behavior:smooth}
|
||||
body{
|
||||
font-family:'Noto Sans SC',system-ui,sans-serif;
|
||||
background:var(--bg);color:var(--text);
|
||||
line-height:1.6;overflow-x:hidden;
|
||||
}
|
||||
|
||||
/* ═══ scrollbar ═══ */
|
||||
::-webkit-scrollbar{width:6px}
|
||||
::-webkit-scrollbar-track{background:var(--bg)}
|
||||
::-webkit-scrollbar-thumb{background:var(--cyan);border-radius:3px}
|
||||
|
||||
/* ═══ canvas bg ═══ */
|
||||
#bg-canvas{position:fixed;inset:0;z-index:0;pointer-events:none;opacity:.35}
|
||||
|
||||
/* ═══ layout ═══ */
|
||||
.wrapper{position:relative;z-index:1;max-width:1440px;margin:0 auto;padding:0 32px 80px}
|
||||
|
||||
/* ═══ header ═══ */
|
||||
.hero{text-align:center;padding:72px 0 48px}
|
||||
.hero h1{
|
||||
font-family:'Orbitron',monospace;font-weight:900;
|
||||
font-size:clamp(28px,5vw,52px);
|
||||
background:linear-gradient(135deg,var(--cyan),var(--green));
|
||||
-webkit-background-clip:text;-webkit-text-fill-color:transparent;
|
||||
letter-spacing:2px;
|
||||
}
|
||||
.hero .sub{
|
||||
font-size:15px;color:var(--text2);margin-top:12px;letter-spacing:4px;
|
||||
font-family:'Orbitron',monospace;text-transform:uppercase;
|
||||
}
|
||||
.hero .version{
|
||||
display:inline-block;margin-top:16px;
|
||||
padding:4px 16px;border-radius:20px;font-size:12px;
|
||||
border:1px solid var(--cyan);color:var(--cyan);
|
||||
font-family:'Orbitron',monospace;
|
||||
}
|
||||
|
||||
/* ═══ section ═══ */
|
||||
.section{margin-top:64px}
|
||||
.section-title{
|
||||
font-family:'Orbitron',monospace;font-weight:700;
|
||||
font-size:clamp(18px,3vw,26px);color:var(--cyan);
|
||||
display:flex;align-items:center;gap:12px;
|
||||
margin-bottom:32px;
|
||||
}
|
||||
.section-title::before{
|
||||
content:'';width:4px;height:28px;border-radius:2px;
|
||||
background:linear-gradient(180deg,var(--cyan),var(--green));
|
||||
}
|
||||
|
||||
/* ═══ 区块1: 架构总览 ═══ */
|
||||
.arch-grid{
|
||||
display:grid;
|
||||
grid-template-columns:1fr auto 1.3fr auto 1fr;
|
||||
align-items:stretch;gap:0;
|
||||
}
|
||||
.arch-box{
|
||||
background:var(--card);border:1px solid var(--border);
|
||||
border-radius:16px;padding:28px 24px;
|
||||
position:relative;overflow:hidden;
|
||||
}
|
||||
.arch-box::before{
|
||||
content:'';position:absolute;top:0;left:0;right:0;height:3px;
|
||||
border-radius:16px 16px 0 0;
|
||||
}
|
||||
.arch-box.caller::before{background:linear-gradient(90deg,var(--green),var(--cyan))}
|
||||
.arch-box.engine::before{background:linear-gradient(90deg,var(--cyan),var(--purple))}
|
||||
.arch-box.device::before{background:linear-gradient(90deg,var(--orange),var(--gold))}
|
||||
|
||||
.arch-box .box-icon{font-size:36px;margin-bottom:12px}
|
||||
.arch-box h3{
|
||||
font-family:'Orbitron',monospace;font-size:15px;
|
||||
color:var(--cyan);margin-bottom:16px;letter-spacing:1px;
|
||||
}
|
||||
.arch-box.engine h3{color:var(--purple)}
|
||||
.arch-box.device h3{color:var(--orange)}
|
||||
|
||||
.arch-box ul{list-style:none;display:flex;flex-direction:column;gap:8px}
|
||||
.arch-box li{
|
||||
font-size:13px;color:var(--text2);
|
||||
padding:8px 12px;border-radius:8px;
|
||||
background:rgba(255,255,255,.03);
|
||||
border:1px solid rgba(255,255,255,.04);
|
||||
display:flex;align-items:center;gap:8px;
|
||||
}
|
||||
.arch-box li .dot{
|
||||
width:6px;height:6px;border-radius:50%;flex-shrink:0;
|
||||
}
|
||||
.arch-box.caller li .dot{background:var(--green)}
|
||||
.arch-box.engine li .dot{background:var(--cyan)}
|
||||
.arch-box.device li .dot{background:var(--orange)}
|
||||
|
||||
.arch-box .engine-core{
|
||||
margin-top:12px;padding:14px;border-radius:10px;
|
||||
background:linear-gradient(135deg,rgba(0,212,255,.08),rgba(168,85,247,.08));
|
||||
border:1px dashed rgba(0,212,255,.2);
|
||||
text-align:center;
|
||||
}
|
||||
.arch-box .engine-core span{
|
||||
font-family:'Orbitron',monospace;font-size:20px;font-weight:700;
|
||||
color:var(--cyan);
|
||||
}
|
||||
.arch-box .engine-core small{display:block;font-size:11px;color:var(--text3);margin-top:4px}
|
||||
|
||||
/* arrows */
|
||||
.arch-arrow{
|
||||
display:flex;align-items:center;justify-content:center;
|
||||
padding:0 8px;min-width:60px;
|
||||
}
|
||||
.arch-arrow svg{width:48px;height:48px;filter:drop-shadow(0 0 6px rgba(0,212,255,.4))}
|
||||
|
||||
/* ═══ 区块2: 模块矩阵 ═══ */
|
||||
.module-grid{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(auto-fill,minmax(180px,1fr));
|
||||
gap:16px;
|
||||
}
|
||||
.mod-card{
|
||||
background:var(--card);
|
||||
border:1px solid var(--border);
|
||||
border-radius:14px;padding:20px 16px;
|
||||
text-align:center;cursor:default;
|
||||
transition:all .3s ease;
|
||||
position:relative;overflow:hidden;
|
||||
}
|
||||
.mod-card::after{
|
||||
content:'';position:absolute;inset:0;border-radius:14px;
|
||||
opacity:0;transition:opacity .3s;
|
||||
pointer-events:none;
|
||||
}
|
||||
.mod-card:hover{transform:translateY(-4px);border-color:rgba(0,212,255,.3)}
|
||||
.mod-card:hover::after{opacity:1}
|
||||
|
||||
.mod-card .mod-icon{font-size:28px;margin-bottom:8px;display:block}
|
||||
.mod-card .mod-name{font-size:14px;font-weight:500;color:var(--text);margin-bottom:6px}
|
||||
.mod-card .mod-count{
|
||||
font-family:'Orbitron',monospace;font-size:22px;font-weight:700;
|
||||
display:block;margin-bottom:2px;
|
||||
}
|
||||
.mod-card .mod-label{font-size:11px;color:var(--text3)}
|
||||
|
||||
/* glow colors */
|
||||
.mod-card[data-cat="msg"]{--glow:var(--blue)}.mod-card[data-cat="msg"] .mod-count{color:var(--blue)}
|
||||
.mod-card[data-cat="msg"]::after{box-shadow:inset 0 0 30px rgba(59,130,246,.1)}
|
||||
.mod-card[data-cat="friend"]{--glow:var(--green)}.mod-card[data-cat="friend"] .mod-count{color:var(--green)}
|
||||
.mod-card[data-cat="friend"]::after{box-shadow:inset 0 0 30px rgba(0,255,136,.1)}
|
||||
.mod-card[data-cat="group"]{--glow:var(--purple)}.mod-card[data-cat="group"] .mod-count{color:var(--purple)}
|
||||
.mod-card[data-cat="group"]::after{box-shadow:inset 0 0 30px rgba(168,85,247,.1)}
|
||||
.mod-card[data-cat="pay"]{--glow:var(--gold)}.mod-card[data-cat="pay"] .mod-count{color:var(--gold)}
|
||||
.mod-card[data-cat="pay"]::after{box-shadow:inset 0 0 30px rgba(255,215,0,.1)}
|
||||
.mod-card[data-cat="secure"]{--glow:var(--red)}.mod-card[data-cat="secure"] .mod-count{color:var(--red)}
|
||||
.mod-card[data-cat="secure"]::after{box-shadow:inset 0 0 30px rgba(255,71,87,.1)}
|
||||
.mod-card[data-cat="social"]{--glow:var(--pink)}.mod-card[data-cat="social"] .mod-count{color:var(--pink)}
|
||||
.mod-card[data-cat="social"]::after{box-shadow:inset 0 0 30px rgba(236,72,153,.1)}
|
||||
.mod-card[data-cat="media"]{--glow:var(--orange)}.mod-card[data-cat="media"] .mod-count{color:var(--orange)}
|
||||
.mod-card[data-cat="media"]::after{box-shadow:inset 0 0 30px rgba(255,107,53,.1)}
|
||||
.mod-card[data-cat="tool"]{--glow:var(--cyan)}.mod-card[data-cat="tool"] .mod-count{color:var(--cyan)}
|
||||
.mod-card[data-cat="tool"]::after{box-shadow:inset 0 0 30px rgba(0,212,255,.1)}
|
||||
.mod-card[data-cat="setting"]{--glow:#8b5cf6}.mod-card[data-cat="setting"] .mod-count{color:#8b5cf6}
|
||||
.mod-card[data-cat="setting"]::after{box-shadow:inset 0 0 30px rgba(139,92,246,.1)}
|
||||
|
||||
/* ═══ 区块3: 调用流程 ═══ */
|
||||
.flow-track{
|
||||
display:flex;align-items:stretch;gap:0;
|
||||
overflow-x:auto;padding:20px 0;
|
||||
}
|
||||
.flow-step{
|
||||
flex:1;min-width:120px;
|
||||
display:flex;flex-direction:column;align-items:center;
|
||||
position:relative;
|
||||
}
|
||||
.flow-step .step-node{
|
||||
width:100%;padding:16px 10px;
|
||||
background:var(--card);border:1px solid var(--border);
|
||||
border-radius:12px;text-align:center;
|
||||
position:relative;z-index:1;
|
||||
transition:all .3s;
|
||||
}
|
||||
.flow-step .step-node:hover{
|
||||
border-color:var(--cyan);
|
||||
box-shadow:0 0 20px rgba(0,212,255,.15);
|
||||
}
|
||||
.flow-step .step-num{
|
||||
font-family:'Orbitron',monospace;font-size:11px;
|
||||
color:var(--cyan);opacity:.6;display:block;margin-bottom:4px;
|
||||
}
|
||||
.flow-step .step-icon{font-size:24px;display:block;margin-bottom:6px}
|
||||
.flow-step .step-text{font-size:12px;color:var(--text);font-weight:500}
|
||||
.flow-step .step-desc{font-size:10px;color:var(--text3);margin-top:4px}
|
||||
|
||||
.flow-arrow{
|
||||
display:flex;align-items:center;flex-shrink:0;
|
||||
padding:0 2px;color:var(--cyan);opacity:.5;
|
||||
font-size:18px;margin-top:20px;
|
||||
}
|
||||
|
||||
/* ═══ 区块4: 统计看板 ═══ */
|
||||
.stats-grid{
|
||||
display:grid;
|
||||
grid-template-columns:repeat(auto-fit,minmax(200px,1fr));
|
||||
gap:20px;
|
||||
}
|
||||
.stat-card{
|
||||
background:var(--card);border:1px solid var(--border);
|
||||
border-radius:16px;padding:28px 24px;
|
||||
text-align:center;position:relative;overflow:hidden;
|
||||
}
|
||||
.stat-card::before{
|
||||
content:'';position:absolute;top:-40px;right:-40px;
|
||||
width:100px;height:100px;border-radius:50%;
|
||||
opacity:.06;
|
||||
}
|
||||
.stat-card:nth-child(1)::before{background:var(--cyan)}
|
||||
.stat-card:nth-child(2)::before{background:var(--green)}
|
||||
.stat-card:nth-child(3)::before{background:var(--purple)}
|
||||
.stat-card:nth-child(4)::before{background:var(--gold)}
|
||||
.stat-card:nth-child(5)::before{background:var(--orange)}
|
||||
|
||||
.stat-card .stat-icon{font-size:32px;margin-bottom:10px;display:block}
|
||||
.stat-card .stat-value{
|
||||
font-family:'Orbitron',monospace;font-weight:900;
|
||||
font-size:clamp(32px,4vw,44px);display:block;
|
||||
margin-bottom:4px;
|
||||
}
|
||||
.stat-card:nth-child(1) .stat-value{color:var(--cyan)}
|
||||
.stat-card:nth-child(2) .stat-value{color:var(--green)}
|
||||
.stat-card:nth-child(3) .stat-value{color:var(--purple)}
|
||||
.stat-card:nth-child(4) .stat-value{color:var(--gold)}
|
||||
.stat-card:nth-child(5) .stat-value{color:var(--orange)}
|
||||
|
||||
.stat-card .stat-label{font-size:13px;color:var(--text2);letter-spacing:1px}
|
||||
|
||||
.progress-wrap{
|
||||
margin-top:32px;background:var(--card);
|
||||
border:1px solid var(--border);border-radius:16px;
|
||||
padding:28px 32px;
|
||||
}
|
||||
.progress-header{
|
||||
display:flex;justify-content:space-between;align-items:center;
|
||||
margin-bottom:16px;
|
||||
}
|
||||
.progress-header span{font-size:14px;color:var(--text2)}
|
||||
.progress-header strong{
|
||||
font-family:'Orbitron',monospace;font-size:24px;color:var(--green);
|
||||
}
|
||||
.progress-bar{
|
||||
height:12px;border-radius:6px;
|
||||
background:rgba(255,255,255,.06);overflow:hidden;
|
||||
}
|
||||
.progress-fill{
|
||||
height:100%;border-radius:6px;
|
||||
background:linear-gradient(90deg,var(--cyan),var(--green));
|
||||
width:0;transition:width 1.8s cubic-bezier(.25,.46,.45,.94);
|
||||
box-shadow:0 0 16px rgba(0,255,136,.3);
|
||||
position:relative;
|
||||
}
|
||||
.progress-fill::after{
|
||||
content:'';position:absolute;inset:0;
|
||||
background:linear-gradient(90deg,transparent 0%,rgba(255,255,255,.2) 50%,transparent 100%);
|
||||
animation:shimmer 2s infinite;
|
||||
}
|
||||
@keyframes shimmer{
|
||||
0%{transform:translateX(-100%)}
|
||||
100%{transform:translateX(100%)}
|
||||
}
|
||||
|
||||
/* ═══ footer ═══ */
|
||||
.footer{
|
||||
text-align:center;padding:48px 0 24px;
|
||||
font-size:12px;color:var(--text3);
|
||||
font-family:'Orbitron',monospace;
|
||||
}
|
||||
|
||||
/* ═══ animations ═══ */
|
||||
@keyframes fadeUp{
|
||||
from{opacity:0;transform:translateY(30px)}
|
||||
to{opacity:1;transform:translateY(0)}
|
||||
}
|
||||
.anim{opacity:0;transform:translateY(30px)}
|
||||
.anim.visible{animation:fadeUp .6s ease forwards}
|
||||
|
||||
/* ═══ responsive ═══ */
|
||||
@media(max-width:1024px){
|
||||
.arch-grid{
|
||||
grid-template-columns:1fr;gap:16px;
|
||||
}
|
||||
.arch-arrow{
|
||||
transform:rotate(90deg);padding:8px 0;min-width:unset;
|
||||
}
|
||||
.arch-arrow svg{width:36px;height:36px}
|
||||
.flow-track{flex-wrap:wrap;gap:8px}
|
||||
.flow-arrow{display:none}
|
||||
}
|
||||
@media(max-width:640px){
|
||||
.wrapper{padding:0 16px 60px}
|
||||
.module-grid{grid-template-columns:repeat(auto-fill,minmax(140px,1fr));gap:10px}
|
||||
.stats-grid{grid-template-columns:repeat(2,1fr);gap:12px}
|
||||
}
|
||||
|
||||
/* ═══ legend ═══ */
|
||||
.legend{
|
||||
display:flex;flex-wrap:wrap;gap:12px 24px;
|
||||
margin-bottom:24px;
|
||||
}
|
||||
.legend-item{display:flex;align-items:center;gap:6px;font-size:12px;color:var(--text2)}
|
||||
.legend-dot{width:10px;height:10px;border-radius:3px}
|
||||
|
||||
/* ═══ tooltip for category filter ═══ */
|
||||
.cat-filters{display:flex;flex-wrap:wrap;gap:8px;margin-bottom:24px}
|
||||
.cat-btn{
|
||||
padding:6px 14px;border-radius:20px;font-size:12px;
|
||||
border:1px solid rgba(255,255,255,.1);background:transparent;
|
||||
color:var(--text2);cursor:pointer;transition:all .25s;
|
||||
font-family:'Noto Sans SC',sans-serif;
|
||||
}
|
||||
.cat-btn:hover,.cat-btn.active{
|
||||
border-color:var(--cyan);color:var(--cyan);
|
||||
background:rgba(0,212,255,.08);
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
|
||||
<canvas id="bg-canvas"></canvas>
|
||||
|
||||
<div class="wrapper">
|
||||
|
||||
<!-- ═══════ HERO ═══════ -->
|
||||
<header class="hero anim">
|
||||
<h1>JIQI SDK · API ATLAS</h1>
|
||||
<p class="sub">WorkPhone SDK v3.0 Architecture</p>
|
||||
<span class="version">FastAPI :8899 · WeChat 8.0.56 · 98 Endpoints</span>
|
||||
</header>
|
||||
|
||||
<!-- ═══════ 区块1: 系统架构总览 ═══════ -->
|
||||
<section class="section anim">
|
||||
<div class="section-title">01 · SYSTEM ARCHITECTURE</div>
|
||||
<div class="arch-grid">
|
||||
|
||||
<!-- 调用方 -->
|
||||
<div class="arch-box caller">
|
||||
<div class="box-icon">📡</div>
|
||||
<h3>CALLERS</h3>
|
||||
<ul>
|
||||
<li><span class="dot"></span>存客宝服务器(ThinkPHP)</li>
|
||||
<li><span class="dot"></span>触客宝 H5 / 任意应用</li>
|
||||
<li><span class="dot"></span>curl / Postman 直调</li>
|
||||
<li><span class="dot"></span>WebSocket 推送订阅</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- 箭头 → -->
|
||||
<div class="arch-arrow">
|
||||
<svg viewBox="0 0 48 48" fill="none"><path d="M8 24h28M30 18l8 6-8 6" stroke="var(--cyan)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><animate attributeName="opacity" values="1;.4;1" dur="2s" repeatCount="indefinite"/></path></svg>
|
||||
</div>
|
||||
|
||||
<!-- 机擎SDK -->
|
||||
<div class="arch-box engine">
|
||||
<div class="box-icon">⚡</div>
|
||||
<h3>JIQI SDK ENGINE</h3>
|
||||
<ul>
|
||||
<li><span class="dot"></span>FastAPI 统一路由层 (:8899)</li>
|
||||
<li><span class="dot"></span>参数校验 · Pydantic Models</li>
|
||||
<li><span class="dot"></span>导航缓存 · NavigationCache</li>
|
||||
<li><span class="dot"></span>操作日志 · OperationLogger</li>
|
||||
<li><span class="dot"></span>防封策略 · AntiDetection</li>
|
||||
</ul>
|
||||
<div class="engine-core">
|
||||
<span>96</span>
|
||||
<small>WeChatADBEngine Actions</small>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 箭头 → -->
|
||||
<div class="arch-arrow">
|
||||
<svg viewBox="0 0 48 48" fill="none"><path d="M8 24h28M30 18l8 6-8 6" stroke="var(--orange)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><animate attributeName="opacity" values="1;.4;1" dur="2s" begin=".5s" repeatCount="indefinite"/></path></svg>
|
||||
</div>
|
||||
|
||||
<!-- Android 设备 -->
|
||||
<div class="arch-box device">
|
||||
<div class="box-icon">📱</div>
|
||||
<h3>ANDROID DEVICE</h3>
|
||||
<ul>
|
||||
<li><span class="dot"></span>ADB / USB 连接层</li>
|
||||
<li><span class="dot"></span>微信 8.0.56 (目标应用)</li>
|
||||
<li><span class="dot"></span>uiautomator2 · UI自动化</li>
|
||||
<li><span class="dot"></span>XPath / AccessibilityNode</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- ═══════ 区块2: API 模块矩阵 ═══════ -->
|
||||
<section class="section anim">
|
||||
<div class="section-title">02 · API MODULE MATRIX</div>
|
||||
|
||||
<div class="legend">
|
||||
<div class="legend-item"><span class="legend-dot" style="background:var(--blue)"></span>消息</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:var(--green)"></span>好友</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:var(--purple)"></span>群聊</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:var(--gold)"></span>支付</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:var(--red)"></span>安全</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:var(--pink)"></span>社交</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:var(--orange)"></span>媒体</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:var(--cyan)"></span>工具</div>
|
||||
<div class="legend-item"><span class="legend-dot" style="background:#8b5cf6"></span>设置</div>
|
||||
</div>
|
||||
|
||||
<div class="cat-filters">
|
||||
<button class="cat-btn active" data-filter="all">全部 (23)</button>
|
||||
<button class="cat-btn" data-filter="msg">消息</button>
|
||||
<button class="cat-btn" data-filter="friend">好友</button>
|
||||
<button class="cat-btn" data-filter="group">群聊</button>
|
||||
<button class="cat-btn" data-filter="pay">支付</button>
|
||||
<button class="cat-btn" data-filter="secure">安全</button>
|
||||
<button class="cat-btn" data-filter="social">社交</button>
|
||||
<button class="cat-btn" data-filter="media">媒体</button>
|
||||
<button class="cat-btn" data-filter="tool">工具</button>
|
||||
<button class="cat-btn" data-filter="setting">设置</button>
|
||||
</div>
|
||||
|
||||
<div class="module-grid" id="moduleGrid"></div>
|
||||
</section>
|
||||
|
||||
<!-- ═══════ 区块3: 调用流程 ═══════ -->
|
||||
<section class="section anim">
|
||||
<div class="section-title">03 · REQUEST LIFECYCLE</div>
|
||||
<div class="flow-track" id="flowTrack"></div>
|
||||
</section>
|
||||
|
||||
<!-- ═══════ 区块4: 统计看板 ═══════ -->
|
||||
<section class="section anim">
|
||||
<div class="section-title">04 · DASHBOARD</div>
|
||||
<div class="stats-grid">
|
||||
<div class="stat-card">
|
||||
<span class="stat-icon">🔗</span>
|
||||
<span class="stat-value" data-target="98">0</span>
|
||||
<span class="stat-label">总端点数</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-icon">⚙️</span>
|
||||
<span class="stat-value" data-target="96">0</span>
|
||||
<span class="stat-label">引擎方法</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-icon">📦</span>
|
||||
<span class="stat-value" data-target="23">0</span>
|
||||
<span class="stat-label">功能模块</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-icon">✅</span>
|
||||
<span class="stat-value" data-target="100" data-suffix="%">0</span>
|
||||
<span class="stat-label">端点覆盖率</span>
|
||||
</div>
|
||||
<div class="stat-card">
|
||||
<span class="stat-icon">🚀</span>
|
||||
<span class="stat-value" data-target="97" data-suffix="%">0</span>
|
||||
<span class="stat-label">开发进度</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="progress-wrap anim">
|
||||
<div class="progress-header">
|
||||
<span>SDK 整体开发进度</span>
|
||||
<strong>97%</strong>
|
||||
</div>
|
||||
<div class="progress-bar">
|
||||
<div class="progress-fill" id="progressFill"></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<footer class="footer">
|
||||
JIQI SDK v3.0 · WorkPhone Engine · Built for CunKeBao<br>
|
||||
Generated 2026-03-14
|
||||
</footer>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
/* ═══════════════════════════════════════
|
||||
Background particle canvas
|
||||
═══════════════════════════════════════ */
|
||||
(function(){
|
||||
const c=document.getElementById('bg-canvas'),ctx=c.getContext('2d');
|
||||
let w,h,pts=[];
|
||||
function resize(){w=c.width=innerWidth;h=c.height=innerHeight;pts=[];
|
||||
for(let i=0;i<60;i++) pts.push({x:Math.random()*w,y:Math.random()*h,vx:(Math.random()-.5)*.3,vy:(Math.random()-.5)*.3,r:Math.random()*1.5+.5});
|
||||
}
|
||||
function draw(){
|
||||
ctx.clearRect(0,0,w,h);
|
||||
for(const p of pts){
|
||||
p.x+=p.vx;p.y+=p.vy;
|
||||
if(p.x<0)p.x=w;if(p.x>w)p.x=0;if(p.y<0)p.y=h;if(p.y>h)p.y=0;
|
||||
ctx.beginPath();ctx.arc(p.x,p.y,p.r,0,Math.PI*2);
|
||||
ctx.fillStyle='rgba(0,212,255,.5)';ctx.fill();
|
||||
}
|
||||
for(let i=0;i<pts.length;i++) for(let j=i+1;j<pts.length;j++){
|
||||
const dx=pts[i].x-pts[j].x,dy=pts[i].y-pts[j].y,d=Math.sqrt(dx*dx+dy*dy);
|
||||
if(d<150){ctx.beginPath();ctx.moveTo(pts[i].x,pts[i].y);ctx.lineTo(pts[j].x,pts[j].y);
|
||||
ctx.strokeStyle=`rgba(0,212,255,${(.15*(1-d/150)).toFixed(3)})`;ctx.stroke();}
|
||||
}
|
||||
requestAnimationFrame(draw);
|
||||
}
|
||||
resize();draw();
|
||||
addEventListener('resize',resize);
|
||||
})();
|
||||
|
||||
/* ═══════════════════════════════════════
|
||||
Module data & render
|
||||
═══════════════════════════════════════ */
|
||||
const MODULES=[
|
||||
{name:'消息管理',count:8,icon:'💬',cat:'msg'},
|
||||
{name:'好友管理',count:8,icon:'👤',cat:'friend'},
|
||||
{name:'群聊管理',count:10,icon:'👥',cat:'group'},
|
||||
{name:'标签管理',count:6,icon:'🏷️',cat:'tool'},
|
||||
{name:'朋友圈管理',count:8,icon:'🌐',cat:'social'},
|
||||
{name:'个人设置',count:6,icon:'⚙️',cat:'setting'},
|
||||
{name:'账号安全',count:9,icon:'🛡️',cat:'secure'},
|
||||
{name:'收藏管理',count:2,icon:'⭐',cat:'tool'},
|
||||
{name:'支付',count:7,icon:'💰',cat:'pay'},
|
||||
{name:'聊天设置',count:3,icon:'🔧',cat:'setting'},
|
||||
{name:'小程序',count:1,icon:'📱',cat:'tool'},
|
||||
{name:'公众号',count:1,icon:'📰',cat:'media'},
|
||||
{name:'视频号',count:5,icon:'🎬',cat:'media'},
|
||||
{name:'扫一扫',count:4,icon:'📷',cat:'tool'},
|
||||
{name:'通话',count:2,icon:'📞',cat:'msg'},
|
||||
{name:'群发助手',count:1,icon:'📢',cat:'msg'},
|
||||
{name:'搜一搜',count:1,icon:'🔍',cat:'tool'},
|
||||
{name:'看一看',count:1,icon:'👀',cat:'social'},
|
||||
{name:'微信运动',count:2,icon:'🏃',cat:'social'},
|
||||
{name:'位置分享',count:2,icon:'📍',cat:'tool'},
|
||||
{name:'表情管理',count:2,icon:'😀',cat:'social'},
|
||||
{name:'文件管理',count:2,icon:'📁',cat:'tool'},
|
||||
{name:'设置管理',count:5,icon:'🎛️',cat:'setting'},
|
||||
];
|
||||
|
||||
const grid=document.getElementById('moduleGrid');
|
||||
MODULES.forEach((m,i)=>{
|
||||
const card=document.createElement('div');
|
||||
card.className='mod-card anim';
|
||||
card.dataset.cat=m.cat;
|
||||
card.style.animationDelay=`${i*40}ms`;
|
||||
card.innerHTML=`
|
||||
<span class="mod-icon">${m.icon}</span>
|
||||
<div class="mod-name">${m.name}</div>
|
||||
<span class="mod-count">${m.count}</span>
|
||||
<span class="mod-label">endpoints</span>`;
|
||||
grid.appendChild(card);
|
||||
});
|
||||
|
||||
/* category filter */
|
||||
document.querySelectorAll('.cat-btn').forEach(btn=>{
|
||||
btn.addEventListener('click',()=>{
|
||||
document.querySelectorAll('.cat-btn').forEach(b=>b.classList.remove('active'));
|
||||
btn.classList.add('active');
|
||||
const f=btn.dataset.filter;
|
||||
document.querySelectorAll('.mod-card').forEach(c=>{
|
||||
c.style.display=(f==='all'||c.dataset.cat===f)?'':'none';
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
/* ═══════════════════════════════════════
|
||||
Flow steps render
|
||||
═══════════════════════════════════════ */
|
||||
const STEPS=[
|
||||
{icon:'🌐',text:'HTTP 请求',desc:'POST /api/v1/*'},
|
||||
{icon:'🚦',text:'FastAPI 路由',desc:'统一路由分发'},
|
||||
{icon:'🔍',text:'参数校验',desc:'Pydantic 验证'},
|
||||
{icon:'⚡',text:'Engine.execute()',desc:'WeChatADBEngine'},
|
||||
{icon:'💾',text:'导航缓存',desc:'NavigationCache'},
|
||||
{icon:'🔌',text:'ADB 命令',desc:'shell / input'},
|
||||
{icon:'🤖',text:'UI 自动化',desc:'uiautomator2'},
|
||||
{icon:'📤',text:'结果返回',desc:'JSON Response'},
|
||||
{icon:'📝',text:'日志记录',desc:'OperationLogger'},
|
||||
];
|
||||
|
||||
const track=document.getElementById('flowTrack');
|
||||
STEPS.forEach((s,i)=>{
|
||||
if(i>0){
|
||||
const arrow=document.createElement('div');
|
||||
arrow.className='flow-arrow';arrow.textContent='→';
|
||||
track.appendChild(arrow);
|
||||
}
|
||||
const step=document.createElement('div');
|
||||
step.className='flow-step anim';
|
||||
step.style.animationDelay=`${i*80}ms`;
|
||||
step.innerHTML=`
|
||||
<div class="step-node">
|
||||
<span class="step-num">${String(i+1).padStart(2,'0')}</span>
|
||||
<span class="step-icon">${s.icon}</span>
|
||||
<div class="step-text">${s.text}</div>
|
||||
<div class="step-desc">${s.desc}</div>
|
||||
</div>`;
|
||||
track.appendChild(step);
|
||||
});
|
||||
|
||||
/* ═══════════════════════════════════════
|
||||
Intersection Observer for animations
|
||||
═══════════════════════════════════════ */
|
||||
const obs=new IntersectionObserver((entries)=>{
|
||||
entries.forEach(e=>{
|
||||
if(e.isIntersecting){e.target.classList.add('visible');obs.unobserve(e.target);}
|
||||
});
|
||||
},{threshold:.15});
|
||||
document.querySelectorAll('.anim').forEach(el=>obs.observe(el));
|
||||
|
||||
/* ═══════════════════════════════════════
|
||||
Number counter animation
|
||||
═══════════════════════════════════════ */
|
||||
const numObs=new IntersectionObserver((entries)=>{
|
||||
entries.forEach(e=>{
|
||||
if(!e.isIntersecting) return;
|
||||
const el=e.target, target=+el.dataset.target, suffix=el.dataset.suffix||'';
|
||||
let start=0;const dur=1200,t0=performance.now();
|
||||
(function tick(now){
|
||||
const p=Math.min((now-t0)/dur,1);
|
||||
const ease=1-Math.pow(1-p,3);
|
||||
el.textContent=Math.round(ease*target)+suffix;
|
||||
if(p<1) requestAnimationFrame(tick);
|
||||
})(t0);
|
||||
numObs.unobserve(el);
|
||||
});
|
||||
},{threshold:.5});
|
||||
document.querySelectorAll('.stat-value[data-target]').forEach(el=>numObs.observe(el));
|
||||
|
||||
/* progress bar */
|
||||
const pObs=new IntersectionObserver((entries)=>{
|
||||
entries.forEach(e=>{
|
||||
if(e.isIntersecting){
|
||||
document.getElementById('progressFill').style.width='97%';
|
||||
pObs.unobserve(e.target);
|
||||
}
|
||||
});
|
||||
},{threshold:.3});
|
||||
pObs.observe(document.querySelector('.progress-wrap'));
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
557
机擎/references/防封策略_全平台全层级.md
Normal file
@@ -0,0 +1,557 @@
|
||||
# 防封策略 · 全平台全层级(必读)
|
||||
|
||||
> **版本**: 1.0.0 | **更新**: 2026-03-14
|
||||
> **定位**: 工作手机SDK开发**强制前置读取文档**——每次开发涉及设备控制、消息发送、自动化操作的功能前,必须先读本文档
|
||||
> **维护**: 阿机(设备端)+ 阿桥(服务端/中间层)+ 阿服(部署环境)
|
||||
> **来源**: 全网调研 + GitHub 开源项目 + 行业实战经验
|
||||
|
||||
---
|
||||
|
||||
## 〇、核心原则
|
||||
|
||||
```
|
||||
防封 = 设备隔离 × 环境伪装 × 行为拟人 × 频率控制 × 内容差异 × 监控应急
|
||||
```
|
||||
|
||||
任何一个维度缺失都可能导致封号。**技术防封是基础,行为可信是内核**。
|
||||
|
||||
**风控三维检测模型**(所有平台通用):
|
||||
|
||||
| 维度 | 检测内容 | 封号权重 |
|
||||
|------|----------|:--------:|
|
||||
| **设备指纹** | IMEI/MAC/Android ID/蓝牙/传感器/屏幕/CPU | 40% |
|
||||
| **行为特征** | 操作频率/间隔规律/触摸轨迹/浏览深度/操作链 | 35% |
|
||||
| **网络环境** | IP归属/IP关联/代理特征/DNS/时区一致性 | 25% |
|
||||
|
||||
---
|
||||
|
||||
## 一、设备端防封(Agent层 — 阿机负责)
|
||||
|
||||
### 1.1 设备指纹隔离(最重要)
|
||||
|
||||
**铁律:一机一号一卡一IP**
|
||||
|
||||
平台通过 20+ 维度特征组合生成设备指纹,任何两台设备指纹相似度过高即判定关联:
|
||||
|
||||
| 指纹维度 | 具体项 | 伪装方式 |
|
||||
|----------|--------|----------|
|
||||
| 硬件标识 | IMEI、MEID、序列号 | Magisk模块修改/Xposed Hook |
|
||||
| 网络标识 | MAC地址、蓝牙地址 | 系统级随机化 |
|
||||
| 软件标识 | Android ID、GSF ID、GAID | 每次刷机重新生成 |
|
||||
| 系统属性 | Build.FINGERPRINT/MODEL/BRAND等 | Build-var-Spoof (LSPosed模块) |
|
||||
| 传感器 | 加速度计/陀螺仪基线噪声 | 添加微小随机偏移 |
|
||||
| 屏幕 | 分辨率、像素密度、刷新率 | 与真机型号一致 |
|
||||
|
||||
**推荐工具**:
|
||||
- **DeviceSpoofLab-Hooks** (github.com/yubunus/DeviceSpoofLab-Hooks) — 126+ 系统属性 Hook,需 LSPosed
|
||||
- **Build-var-Spoof** (github.com/LSPosed/Build-var-Spoof) — C++ 级 Build 属性伪装
|
||||
- **AndroidFaker** — 轻量级设备 ID 伪装 Xposed 模块
|
||||
|
||||
**开发注意**:
|
||||
- Agent 启动时采集并上报设备指纹摘要(MD5),服务端检测同批设备是否存在指纹碰撞
|
||||
- 每台设备在初始化时必须确保指纹唯一性,碰撞率要求 < 0.01%
|
||||
- 设备管理服务(DeviceSvc)增加 `fingerprint_hash` 字段用于关联检测
|
||||
|
||||
### 1.2 Root 隐藏(Frida/Hook 前置)
|
||||
|
||||
微信等应用会主动检测 Root 状态,检测到即降低账号权重或直接限制功能:
|
||||
|
||||
| 方案 | 工具 | 说明 |
|
||||
|------|------|------|
|
||||
| **Root 框架** | Magisk (Zygisk) 或 KernelSU/APatch | KernelSU 隐藏性更强 |
|
||||
| **Root 隐藏** | Shamiko v4.14+ | 替代已废弃的 MagiskHide,配合 Zygisk |
|
||||
| **应用列表隐藏** | HMA-OSS (Hide My Applist) | 阻止微信扫描已安装的 Root 相关 APP |
|
||||
| **完整性修复** | TrickStore + Play Integrity Fork | 通过 Google 安全检查 |
|
||||
| **Bootloader 伪装** | 各方案内置 | 让 Bootloader 状态报告为已锁定 |
|
||||
|
||||
**配置步骤**(每台设备部署时必须执行):
|
||||
1. 刷入 Magisk/KernelSU → 启用 Zygisk
|
||||
2. 安装 Shamiko → 配置 DenyList(将微信、抖音等加入)→ **关闭** DenyList 强制模式
|
||||
3. 安装 HMA-OSS → 隐藏 Magisk Manager、Frida Server、Terminal 等
|
||||
4. 安装 TrickStore + Play Integrity Fork
|
||||
5. 用 Hunter/TB Checker 验证隐藏效果
|
||||
|
||||
**开发注意**:
|
||||
- `sdk/agent/install.sh` 必须包含上述模块的自动安装流程
|
||||
- Agent 启动自检增加 Root 隐藏状态验证
|
||||
- 设备能力上报增加 `root_hidden: bool` 字段
|
||||
|
||||
### 1.3 Frida 反检测(Hook通道关键)
|
||||
|
||||
微信等应用有专门的 Frida 检测逻辑,检测到即触发风控。Frida 检测向量共 16 类:
|
||||
|
||||
| # | 检测向量 | 说明 | 绕过方式 |
|
||||
|---|----------|------|----------|
|
||||
| 1 | 进程名 frida-server | 进程列表扫描 | 重命名为随机字符串 |
|
||||
| 2 | /proc/maps 中 libfrida-agent.so | 内存映射检测 | 随机化库名 |
|
||||
| 3 | 线程名 gum-js-loop/gmain/gdbus | 线程枚举 | 替换为系统常见线程名 |
|
||||
| 4 | memfd 名称 | 内存文件描述符 | 随机化 |
|
||||
| 5 | 符号名 frida_agent_main | 符号表扫描 | 混淆所有导出符号 |
|
||||
| 6 | SELinux 标签 | 安全上下文检测 | 修改标签 |
|
||||
| 7 | libc hooks 残留 | 函数 Hook 痕迹 | 深度清理 |
|
||||
| 8 | D-Bus 服务名 | IPC 通信特征 | 替换服务名 |
|
||||
| 9 | 默认端口 27042 | 端口扫描 | 使用随机端口 |
|
||||
| 10 | 二进制字符串 "frida" | strings 扫描 | 全量替换 |
|
||||
| 11-16 | 内部C符号/GType/临时路径/构建标记/资产目录 | 深度扫描 | 全量 patch |
|
||||
|
||||
**推荐方案(按优先级)**:
|
||||
|
||||
| 方案 | GitHub 地址 | Stars | 特点 |
|
||||
|------|------------|:-----:|------|
|
||||
| **Phantom-Frida** | TheQmaks/phantom-frida | ★★★ | 90+ patches,16 向量全覆盖,每周自动构建 |
|
||||
| **StrongR-Frida** | hzzheyang/strongR-frida-android | 1k+ | 跟随官方版本更新,社区活跃 |
|
||||
| **Florida** | Ylarod/Florida | ★★ | 基础反检测,自动跟随 Frida 发布 |
|
||||
| **Undetected-Frida** | ultrafunkamsterdam/undetected-frida | ★★ | 8 个 patch 文件,较轻量 |
|
||||
|
||||
**开发注意**:
|
||||
- `sdk/agent/hook/frida_manager.py` 中 Frida Server 启动必须使用反检测版本
|
||||
- 配置文件 `config.json` 增加 `frida_server_path` 指向反检测版二进制
|
||||
- 端口配置为每台设备随机生成,范围 10000-60000,写入设备配置持久化
|
||||
- **严禁**使用默认端口 27042 和默认进程名
|
||||
|
||||
### 1.4 UIAutomator2 反检测(u2通道)
|
||||
|
||||
应用可通过以下方式检测 u2 自动化:
|
||||
|
||||
| 检测方式 | 说明 | 绕过策略 |
|
||||
|----------|------|----------|
|
||||
| AccessibilityService 检测 | 检查是否有活跃的无障碍服务 | 仅在操作时启用,操作完立即关闭 |
|
||||
| 触摸事件来源检测 | 程序注入的触摸事件有标记位 | 使用 `input tap` 替代 API 注入 |
|
||||
| 操作间隔一致性 | 每次间隔完全相同 | 随机延迟 + 高斯分布 |
|
||||
| 触摸轨迹检测 | 直线点击无移动轨迹 | 模拟贝塞尔曲线滑动轨迹 |
|
||||
| 输入速度检测 | 瞬间输入完整文本 | 逐字输入 + 随机间隔(50-200ms) |
|
||||
|
||||
**开发注意(所有 Skill 必须遵守)**:
|
||||
|
||||
```python
|
||||
# sdk/agent/skills/base.py 中必须实现的防检测基础方法
|
||||
|
||||
import random
|
||||
import time
|
||||
import math
|
||||
|
||||
def human_delay(min_sec=0.5, max_sec=3.0):
|
||||
"""拟人延迟:高斯分布,均值在 min-max 中间"""
|
||||
mean = (min_sec + max_sec) / 2
|
||||
std = (max_sec - min_sec) / 6
|
||||
delay = max(min_sec, min(max_sec, random.gauss(mean, std)))
|
||||
time.sleep(delay)
|
||||
|
||||
def human_type(device, text, field_selector):
|
||||
"""拟人输入:逐字输入 + 随机间隔 + 偶尔打错重输"""
|
||||
field_selector.click()
|
||||
human_delay(0.3, 0.8)
|
||||
for i, char in enumerate(text):
|
||||
device.send_keys(char)
|
||||
time.sleep(random.uniform(0.05, 0.2))
|
||||
# 5% 概率模拟打错并退格
|
||||
if random.random() < 0.05 and i < len(text) - 1:
|
||||
device.send_keys(random.choice('abcde'))
|
||||
time.sleep(random.uniform(0.1, 0.3))
|
||||
device.press('del')
|
||||
time.sleep(random.uniform(0.1, 0.2))
|
||||
|
||||
def human_swipe(device, start, end, duration=None):
|
||||
"""拟人滑动:贝塞尔曲线 + 随机持续时间"""
|
||||
if duration is None:
|
||||
duration = random.uniform(0.3, 0.8)
|
||||
# 添加随机中间控制点
|
||||
ctrl_x = (start[0] + end[0]) / 2 + random.randint(-30, 30)
|
||||
ctrl_y = (start[1] + end[1]) / 2 + random.randint(-30, 30)
|
||||
device.swipe_ext("bezier", points=[start, (ctrl_x, ctrl_y), end], duration=duration)
|
||||
|
||||
def random_browse(device, duration_sec=30):
|
||||
"""随机浏览:模拟真人无目的浏览行为"""
|
||||
end_time = time.time() + duration_sec
|
||||
while time.time() < end_time:
|
||||
action = random.choice(['scroll', 'pause', 'tap_random'])
|
||||
if action == 'scroll':
|
||||
human_swipe(device, (540, 1500), (540, 500))
|
||||
human_delay(2.0, 5.0)
|
||||
elif action == 'pause':
|
||||
human_delay(3.0, 8.0)
|
||||
elif action == 'tap_random':
|
||||
# 在安全区域随机点击
|
||||
device.click(random.randint(100, 980), random.randint(300, 1800))
|
||||
human_delay(1.0, 3.0)
|
||||
device.press('back')
|
||||
human_delay(1.0, 2.0)
|
||||
```
|
||||
|
||||
### 1.5 模拟器检测绕过
|
||||
|
||||
**强烈建议使用真机**。若必须用模拟器:
|
||||
|
||||
| 检测点 | 绕过方式 |
|
||||
|--------|----------|
|
||||
| Build.HARDWARE/PRODUCT 含 "goldfish"/"sdk" | 修改 Build.prop |
|
||||
| 传感器缺失(加速度计/陀螺仪) | 模拟传感器数据 |
|
||||
| 电池状态永远充电中 | Hook 电池 API |
|
||||
| IMEI 全零 | 使用 AndroidFaker 设置真实 IMEI |
|
||||
| /dev/qemu_pipe 等特征文件 | 隐藏相关文件路径 |
|
||||
|
||||
**参考项目**:
|
||||
- **Anti-EmuDetector** (github.com/u0pattern/Anti-EmuDetector) — 绕过模拟器检测,支持 Flutter
|
||||
- **fake-android** (github.com/kkoooqq/fake-android) — 多设备群控防检测,支持 RPC/NAT
|
||||
|
||||
---
|
||||
|
||||
## 二、服务端防封(SDK Server — 阿机+阿桥负责)
|
||||
|
||||
### 2.1 请求频率控制(核心防封机制)
|
||||
|
||||
**分三级限流**:
|
||||
|
||||
| 级别 | 对象 | 策略 | 实现 |
|
||||
|------|------|------|------|
|
||||
| **L1 全局** | 整个 SDK | 令牌桶,峰值 100 req/min | Redis + 滑动窗口 |
|
||||
| **L2 设备** | 单台设备 | 每分钟 ≤ 10 次操作 | 设备维度 Redis key |
|
||||
| **L3 动作** | 单一操作类型 | 见下表平台限制 | 动作维度独立计数器 |
|
||||
|
||||
**各平台操作频率上限**(写入 Redis 配置,可动态调整):
|
||||
|
||||
| 平台 | 操作 | 频率上限 | 冷却时间 | 日上限 |
|
||||
|------|------|:--------:|:--------:|:------:|
|
||||
| 微信 | 主动加好友 | 1次/3-5min | 随机 180-300s | 新号10/老号50 |
|
||||
| 微信 | 发消息 | 1次/30-60s | 随机 30-60s | 200条 |
|
||||
| 微信 | 发朋友圈 | 1次/2h | 随机 7200-10800s | 5条 |
|
||||
| 微信 | 群发 | 1次/5min | 随机 300-600s | 50次 |
|
||||
| 抖音 | 私信 | 1次/60-120s | 随机 60-120s | 100条 |
|
||||
| 抖音 | 关注 | 1次/30-60s | 随机 30-60s | 200次 |
|
||||
| 小红书 | 私信 | 1次/60-180s | 随机 60-180s | 50条 |
|
||||
| 小红书 | 点赞 | 1次/15-30s | 随机 15-30s | 300次 |
|
||||
| 闲鱼 | 私信 | 1次/60-120s | 随机 60-120s | 80条 |
|
||||
|
||||
**代码实现要点**(`sdk/app/services/` 增加 `rate_limiter.py`):
|
||||
|
||||
```python
|
||||
# 限流器核心逻辑
|
||||
class AntiDetectRateLimiter:
|
||||
"""三级防封限流器"""
|
||||
|
||||
PLATFORM_LIMITS = {
|
||||
"wechat": {
|
||||
"add_friend": {"interval": (180, 300), "daily_max": 50},
|
||||
"send_message": {"interval": (30, 60), "daily_max": 200},
|
||||
"post_moment": {"interval": (7200, 10800), "daily_max": 5},
|
||||
},
|
||||
"douyin": {
|
||||
"send_message": {"interval": (60, 120), "daily_max": 100},
|
||||
"follow": {"interval": (30, 60), "daily_max": 200},
|
||||
},
|
||||
# ... 其他平台
|
||||
}
|
||||
|
||||
async def check_and_wait(self, device_id, platform, action):
|
||||
"""检查频率限制,必要时等待"""
|
||||
limit = self.PLATFORM_LIMITS[platform][action]
|
||||
# 1. 检查日上限
|
||||
daily_count = await self.get_daily_count(device_id, platform, action)
|
||||
if daily_count >= limit["daily_max"]:
|
||||
raise DailyLimitExceeded(f"{platform}.{action} 已达日上限 {limit['daily_max']}")
|
||||
# 2. 计算随机等待时间
|
||||
wait = random.uniform(*limit["interval"])
|
||||
last_time = await self.get_last_action_time(device_id, platform, action)
|
||||
elapsed = time.time() - last_time
|
||||
if elapsed < wait:
|
||||
await asyncio.sleep(wait - elapsed)
|
||||
# 3. 记录本次操作
|
||||
await self.record_action(device_id, platform, action)
|
||||
```
|
||||
|
||||
### 2.2 任务调度防封
|
||||
|
||||
| 策略 | 说明 |
|
||||
|------|------|
|
||||
| **时段控制** | 操作集中在 8:00-22:00,夜间仅心跳 |
|
||||
| **渐进式启动** | 新号前7天操作量每天递增 20% |
|
||||
| **随机空闲** | 每30-60分钟插入5-15分钟空闲期 |
|
||||
| **穿插真实操作** | 操作间穿插浏览朋友圈/刷短视频等"养号"行为 |
|
||||
| **设备轮休** | 每台设备每天最多工作 14 小时 |
|
||||
| **批量错开** | 多设备同类操作错开执行,避免同一秒并发 |
|
||||
|
||||
### 2.3 IP 策略
|
||||
|
||||
| 策略 | 说明 | 实现 |
|
||||
|------|------|------|
|
||||
| **设备绑定 IP** | 每台设备长期使用固定 IP | 配置表绑定 |
|
||||
| **IP-设备一致性** | IP 归属地与设备 SIM 卡归属一致 | 自动校验 |
|
||||
| **代理池轮换** | 高频操作时使用住宅 IP 代理池 | Redis 管理 |
|
||||
| **异常切换** | 收到 403/429 时立即切换 IP | 熔断回调 |
|
||||
| **DNS 一致性** | 使用与 IP 归属地一致的 DNS | 设备端配置 |
|
||||
|
||||
### 2.4 unified.py 防封集成点
|
||||
|
||||
在 `sdk/app/routers/unified.py` 的请求处理流程中增加防封中间件:
|
||||
|
||||
```
|
||||
请求进入 → 鉴权 → 【防封检查】→ 通道路由 → 执行 → 结果回传
|
||||
│
|
||||
├── 1. 频率限制检查(RateLimiter)
|
||||
├── 2. 日上限检查
|
||||
├── 3. 随机延迟注入
|
||||
├── 4. 养号策略检查(新号限制)
|
||||
└── 5. 敏感词过滤
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 三、应用层防封(各平台 Skill — 阿机+阿桥负责)
|
||||
|
||||
### 3.1 微信防封策略(最关键)
|
||||
|
||||
#### 3.1.1 账号生命周期管理
|
||||
|
||||
| 阶段 | 天数 | 操作限制 | 养号任务 |
|
||||
|------|:----:|----------|----------|
|
||||
| **新号期** | 0-7天 | 每日加人≤5,发消息≤20 | 完善资料、实名认证、绑银行卡、发3条朋友圈 |
|
||||
| **成长期** | 8-30天 | 每日加人≤15,发消息≤80 | 日常聊天、支付、阅读文章、玩小程序 |
|
||||
| **成熟期** | 30天+ | 每日加人≤50,发消息≤200 | 保持活跃度、定期发朋友圈、正常社交 |
|
||||
|
||||
#### 3.1.2 微信高危操作红线
|
||||
|
||||
| 操作 | 红线 | 后果 |
|
||||
|------|------|------|
|
||||
| 同设备登3+账号 | **严禁** | 所有账号关联封号 |
|
||||
| 频繁切换账号 | 间隔 < 2h **严禁** | 降权/限制登录 |
|
||||
| 使用"附近的人" | **新号严禁** | 直接封号 |
|
||||
| 群发完全相同内容 | **严禁** | 内容违规 + 封号 |
|
||||
| 被动加人 > 60/min | **严禁** | 立即限制 |
|
||||
| 主动加人 > 40/hour | **严禁** | 功能限制 |
|
||||
| 使用非官方客户端 | **严禁** | 永久封号 |
|
||||
|
||||
#### 3.1.3 微信操作编码规范
|
||||
|
||||
所有微信 Skill 方法必须遵守:
|
||||
|
||||
```python
|
||||
# WeChatSkill 中每个 action 必须包含的防封逻辑
|
||||
|
||||
async def send_message(self, to_id, content, **kwargs):
|
||||
# 1. 频率检查
|
||||
await self.rate_limiter.check_and_wait(self.device_id, "wechat", "send_message")
|
||||
|
||||
# 2. 内容差异化处理
|
||||
content = self.diversify_content(content)
|
||||
|
||||
# 3. 操作前:模拟自然行为链
|
||||
await self.simulate_natural_flow([
|
||||
("open_chat", to_id), # 打开对话
|
||||
("scroll_history", 2), # 浏览2-3条历史消息
|
||||
("pause", (1.0, 3.0)), # 停顿"阅读"
|
||||
("type_message", content), # 逐字输入
|
||||
("pause", (0.5, 1.5)), # 输入后停顿
|
||||
("send", None), # 发送
|
||||
])
|
||||
|
||||
# 4. 操作后:随机后续行为
|
||||
if random.random() < 0.3:
|
||||
await self.browse_moments(duration=random.randint(10, 30))
|
||||
|
||||
def diversify_content(self, content):
|
||||
"""内容差异化:同义词替换 + 变量注入"""
|
||||
# 替换变量 {时间} {称呼} 等
|
||||
content = content.replace("{时间}", self._get_greeting_by_time())
|
||||
content = content.replace("{称呼}", random.choice(["亲", "您好", "hi"]))
|
||||
# 随机添加不可见字符(零宽空格)增加唯一性
|
||||
if random.random() < 0.5:
|
||||
pos = random.randint(0, len(content))
|
||||
content = content[:pos] + '\u200b' + content[pos:]
|
||||
return content
|
||||
```
|
||||
|
||||
### 3.2 抖音防封策略
|
||||
|
||||
| 检测维度 | 策略 |
|
||||
|----------|------|
|
||||
| 行为链分析 | 先浏览首页 30s → 看2-3个视频 → 再执行目标操作 |
|
||||
| 传感器数据 | 定期模拟陀螺仪/加速度变化(模拟手持抖动) |
|
||||
| 操作节奏 | 每小时强制休息 10 分钟 |
|
||||
| 互动比率 | 点赞率 ≤ 20%,评论率 5-10%,避免全量操作 |
|
||||
| 视频观看 | 每个视频停留 > 15s,不可秒滑 |
|
||||
|
||||
### 3.3 小红书防封策略
|
||||
|
||||
**小红书设备指纹检测最为严格**:
|
||||
|
||||
| 维度 | 要求 |
|
||||
|------|------|
|
||||
| 设备 | IMEI/MAC/蓝牙地址**必须全部唯一** |
|
||||
| 网络 | 必须使用住宅 IP,机房 IP 存活率 < 15% |
|
||||
| 内容 | 严禁搬运,必须原创或深度改写 |
|
||||
| 行为 | 新号浏览笔记 > 30min/天,持续 3-7 天再操作 |
|
||||
| 操作 | 单日私信 ≤ 50,点赞 ≤ 300,收藏 ≤ 200 |
|
||||
|
||||
### 3.4 闲鱼防封策略
|
||||
|
||||
| 维度 | 要求 |
|
||||
|------|------|
|
||||
| 账号 | 芝麻信用 > 600,实名认证,支付宝绑定 |
|
||||
| 行为 | 先浏览 10-15 分钟再发私信 |
|
||||
| 内容 | 私信避免"加微信"等引流词 |
|
||||
| 频率 | 单日私信 ≤ 80,发布 ≤ 10 |
|
||||
|
||||
### 3.5 小程序防封策略
|
||||
|
||||
微信小程序有独立的安全防护 API,风险等级 0-4(越高越危险):
|
||||
|
||||
| 风险等级 | 含义 | 对策 |
|
||||
|:--------:|------|------|
|
||||
| 0 | 正常 | 正常操作 |
|
||||
| 1 | 可疑 | 降低操作频率 50% |
|
||||
| 2 | 异常 | 暂停操作 30 分钟 |
|
||||
| 3 | 高危 | 暂停操作 2 小时,切换策略 |
|
||||
| 4 | 确认异常 | 停止该账号操作,人工介入 |
|
||||
|
||||
小程序特有防封要点:
|
||||
- 避免短时间大量调用同一小程序 API
|
||||
- 模拟正常小程序使用流程(授权→浏览→操作)
|
||||
- 不在小程序中直接发送营销内容
|
||||
|
||||
---
|
||||
|
||||
## 四、行为层防封(全局 — 全员遵守)
|
||||
|
||||
### 4.1 拟人化操作框架
|
||||
|
||||
**所有自动化操作必须通过防封行为层**,不得直接调用底层 API:
|
||||
|
||||
| 行为要素 | 机器特征 | 人类特征 | 实现方式 |
|
||||
|----------|----------|----------|----------|
|
||||
| 操作间隔 | 固定值 | 高斯随机分布 | `human_delay()` |
|
||||
| 触摸轨迹 | 精确直线/瞬移 | 贝塞尔曲线+抖动 | `human_swipe()` |
|
||||
| 文字输入 | 瞬间完成 | 逐字+偶尔退格 | `human_type()` |
|
||||
| 浏览行为 | 无 | 滑动+停留+返回 | `random_browse()` |
|
||||
| 操作时段 | 24h 不间断 | 8:00-22:00 为主 | 调度策略 |
|
||||
| 连续操作 | 无休息 | 30-60min 后休息 | 自动空闲 |
|
||||
|
||||
### 4.2 养号策略自动化
|
||||
|
||||
Agent 应内置自动养号功能,在非任务时段自动执行:
|
||||
|
||||
```
|
||||
每日养号流程(新号期,可配置):
|
||||
07:30 模拟闹钟唤醒,解锁手机
|
||||
07:35 打开微信,浏览朋友圈 5min
|
||||
07:45 看 3-5 条微信文章
|
||||
08:00 发 1 条朋友圈(生活类)
|
||||
09:00 刷抖音 15min
|
||||
12:00 微信聊天(回复已有对话)
|
||||
14:00 逛小红书 10min
|
||||
18:00 发 1 条朋友圈
|
||||
20:00 微信支付(小额)
|
||||
22:00 设备休息(仅保持心跳)
|
||||
```
|
||||
|
||||
### 4.3 内容防封策略
|
||||
|
||||
| 策略 | 说明 |
|
||||
|------|------|
|
||||
| **变量模板** | 使用 `{昵称}` `{时间}` `{地点}` 等变量,每次发送自动替换 |
|
||||
| **同义词库** | 维护敏感词→安全词映射表,自动替换 |
|
||||
| **零宽字符** | 在文本随机位置插入 `\u200b`(零宽空格),确保每条消息唯一 |
|
||||
| **表情随机** | 在消息末尾随机添加 1-2 个表情 |
|
||||
| **格式变化** | 同一意思用不同格式(纯文本/带图/语音) |
|
||||
|
||||
**敏感词黑名单**(定期更新):
|
||||
- 微信加我、扫码、转账、红包、优惠、免费、赚钱、兼职
|
||||
- 各平台最新封禁关键词(通过 API 动态拉取)
|
||||
|
||||
---
|
||||
|
||||
## 五、监控与应急(阿表+阿服负责)
|
||||
|
||||
### 5.1 风控监控仪表盘
|
||||
|
||||
需在管理端(F9 风控中心)实现的监控指标:
|
||||
|
||||
| 指标 | 阈值 | 告警方式 |
|
||||
|------|:----:|----------|
|
||||
| 单设备操作成功率 | < 80% | 飞书/邮件告警 |
|
||||
| 账号状态异常(被限制/被封) | 任何一个 | 立即告警 + 停止该设备 |
|
||||
| 单设备日操作量接近上限 | > 90% | 预警提醒 |
|
||||
| IP 被封检测 | HTTP 403/429 | 自动切换 IP |
|
||||
| Frida 连接断开 | 连续 3 次 | 重启 Agent |
|
||||
| 设备指纹碰撞 | 任何一对 | 立即停止碰撞设备 |
|
||||
|
||||
### 5.2 应急处理流程
|
||||
|
||||
#### 账号被临时限制
|
||||
|
||||
```
|
||||
1. 立即停止该账号所有自动化操作
|
||||
2. 24小时内不操作
|
||||
3. 手动完成 5 笔小额支付(< 1 元)
|
||||
4. 手动发布 3 条生活朋友圈(带定位)
|
||||
5. 等待 48 小时后逐步恢复(频率降低 50%)
|
||||
```
|
||||
|
||||
#### 账号被永久封号
|
||||
|
||||
```
|
||||
1. 72小时内通过微信安全中心申诉
|
||||
2. 准备材料:身份证正反面、手持身份证照、聊天记录、支付账单
|
||||
3. 若有企业资质,提交企业申诉(成功率 40-64%)
|
||||
4. 回溯封号原因 → 更新防封策略 → 记录到经验库
|
||||
```
|
||||
|
||||
### 5.3 封号归因分析
|
||||
|
||||
每次封号必须做归因分析并更新本文档:
|
||||
|
||||
| 分析维度 | 检查项 |
|
||||
|----------|--------|
|
||||
| 操作日志回溯 | 封号前 24h 的所有操作记录 |
|
||||
| 频率检查 | 是否超过平台限制 |
|
||||
| 内容检查 | 是否触发敏感词 |
|
||||
| 设备检查 | 指纹是否碰撞/Root是否暴露 |
|
||||
| 网络检查 | IP 是否被标记 |
|
||||
| 关联检查 | 是否与其他封号账号有关联 |
|
||||
|
||||
---
|
||||
|
||||
## 六、GitHub 开源项目索引
|
||||
|
||||
| 项目 | 地址 | 用途 | 对应人 |
|
||||
|------|------|------|--------|
|
||||
| **Phantom-Frida** | github.com/TheQmaks/phantom-frida | Frida反检测(16向量全覆盖) | 阿机 |
|
||||
| **StrongR-Frida** | github.com/hzzheyang/strongR-frida-android | Frida反检测(社区活跃) | 阿机 |
|
||||
| **Florida** | github.com/Ylarod/Florida | Frida基础反检测 | 阿机 |
|
||||
| **DeviceSpoofLab** | github.com/yubunus/DeviceSpoofLab-Hooks | 设备指纹伪装(126+属性) | 阿服 |
|
||||
| **Build-var-Spoof** | github.com/LSPosed/Build-var-Spoof | Build属性伪装 | 阿服 |
|
||||
| **Anti-EmuDetector** | github.com/u0pattern/Anti-EmuDetector | 模拟器检测绕过 | 阿机 |
|
||||
| **FridaAntiRootDetection** | github.com/AshenOneYe/FridaAntiRootDetection | Root检测绕过 | 阿机 |
|
||||
| **fake-android** | github.com/kkoooqq/fake-android | 多设备群控防检测 | 阿机 |
|
||||
| **Shamiko** | magisk.dev/modules/shamiko | Root隐藏模块 | 阿服 |
|
||||
| **HMA-OSS** | github.com/Dr-TSNG/Hide-My-Applist | 应用列表隐藏 | 阿服 |
|
||||
|
||||
---
|
||||
|
||||
## 七、开发 Checklist(每次开发前核验)
|
||||
|
||||
**任何涉及设备操作的代码修改,必须逐项检查**:
|
||||
|
||||
- [ ] 操作是否经过 `RateLimiter` 频率检查?
|
||||
- [ ] 操作间隔是否使用 `human_delay()` 随机延迟?
|
||||
- [ ] 文字输入是否使用 `human_type()` 逐字输入?
|
||||
- [ ] 滑动操作是否使用 `human_swipe()` 贝塞尔曲线?
|
||||
- [ ] 消息内容是否经过 `diversify_content()` 差异化?
|
||||
- [ ] 是否检查了日操作上限?
|
||||
- [ ] 新号是否走了养号策略限制?
|
||||
- [ ] 是否有敏感词过滤?
|
||||
- [ ] Frida Server 是否使用反检测版本?
|
||||
- [ ] 设备指纹是否唯一(无碰撞)?
|
||||
- [ ] Root 隐藏是否配置完毕?
|
||||
- [ ] 操作时段是否在合理范围?
|
||||
- [ ] 是否有操作后的"自然行为"穿插?
|
||||
|
||||
---
|
||||
|
||||
## 八、版本更新日志
|
||||
|
||||
| 日期 | 版本 | 更新内容 |
|
||||
|------|:----:|----------|
|
||||
| 2026-03-14 | v1.0.0 | 初版:四层防封策略(设备/服务器/应用/行为)+ GitHub项目索引 + 开发Checklist |
|
||||
|
||||
> **持续更新机制**:每次封号事件 → 归因分析 → 更新本文档对应章节 → 通知全员。
|
||||
> **季度审查**:每季度搜索全网最新防封策略,更新平台检测规则与绕过方案。
|
||||