176 lines
7.7 KiB
Python
176 lines
7.7 KiB
Python
"""验证 350014 的根因是"落点过准 + 轨迹机械"(trajectory), 不是指纹。
|
|
|
|
对照实验(同 PC、同真 Chrome 配置):
|
|
- 粗暴拖(打不中缺口) → 350002 (anti-bot 过, 仅缺口错) [probe 实测]
|
|
- ddddocr 精确拖(Δ-0.6px) → 350014 (anti-bot 拒) [e2e 实测]
|
|
唯一变量 = 拖动。→ 350014 是轨迹/落点检测, 落点"不可能地准"+线性回正被识破。
|
|
修复: 拟人拖——落点带 ±3px 人为误差(仍在验收窗内), 过冲+抖动回正+末端微震荡。
|
|
"""
|
|
from __future__ import annotations
|
|
import json
|
|
import random
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
import time
|
|
from pathlib import Path
|
|
|
|
ROOT = Path(__file__).resolve().parent.parent
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
from core.captcha_assist import _update_captcha_assets, _wait_captcha_assets # noqa: E402
|
|
from playwright.sync_api import sync_playwright # noqa: E402
|
|
|
|
UA = ("Mozilla/5.0 (Linux; Android 16; PJZ110 Build/UKQ1.230917.001; wv) "
|
|
"AppleWebKit/537.36 (KHTML, like Gecko) Version/4.0 Chrome/126.0.6478.134 "
|
|
"Mobile Safari/537.36")
|
|
DEV_COOKIES = {
|
|
"kpn": "NEBULA", "kpf": "ANDROID_PHONE", "userId": "0",
|
|
"did": "ANDROID_" + "0" * 16, "didv": "1751000000000",
|
|
"c": "CN", "ver": "14.5.50", "appver": "14.5.50.11631",
|
|
"language": "zh-cn", "countryCode": "+86", "sys": "ANDROID_16",
|
|
"mod": "OnePlus(PJZ110)", "deviceName": "OnePlus(PJZ110)",
|
|
"net": "WIFI", "client_key": "2ac2a76d", "os": "android",
|
|
}
|
|
OFFSET = -48 # ddddocr target_x 的经验偏置(使落点居中于缺口)
|
|
|
|
|
|
def harvest_error_url(mobile: str) -> str:
|
|
out = subprocess.run(
|
|
[sys.executable, str(ROOT / "tools" / "sms_login_cli.py"),
|
|
"--mobile", mobile, "--code", "000000"],
|
|
capture_output=True, text=True, cwd=str(ROOT), timeout=120,
|
|
).stdout
|
|
m = re.search(r'https://app\.m\.kuaishou\.com/verify/captcha\.html\?key=-?\d+[^"]*', out)
|
|
if not m:
|
|
raise RuntimeError(f"harvest fail:\n{out[-600:]}")
|
|
return m.group(0)
|
|
|
|
|
|
def human_drag(page, x0, y0, dx, *, seed=None):
|
|
"""拟人拖: 慢启加速 + 过冲 + 抖动回正 + 末端微震荡; 落点带 ±3px 误差。"""
|
|
rnd = random.Random(seed)
|
|
land = dx + rnd.uniform(-3.0, 3.0) # 人为不完美, 仍在验收窗内
|
|
peak = land + rnd.uniform(4.0, 10.0) # 过冲
|
|
page.mouse.move(x0, y0)
|
|
page.wait_for_timeout(rnd.randint(140, 320)) # 反应延迟
|
|
page.mouse.down()
|
|
# 加速相: smoothstep 到接近 peak, 偶发微小后退
|
|
n1 = rnd.randint(30, 42)
|
|
dur1 = rnd.randint(420, 560)
|
|
for i in range(1, n1 + 1):
|
|
t = i / n1
|
|
ease = t * t * (3 - 2 * t)
|
|
x = peak * ease + rnd.uniform(-1.5, 1.5)
|
|
if rnd.random() < 0.12:
|
|
x -= rnd.uniform(1.0, 3.0)
|
|
page.mouse.move(x0 + x, y0 + rnd.uniform(-2.0, 2.0))
|
|
page.wait_for_timeout(max(4, int(dur1 / n1 + rnd.uniform(-4, 10))))
|
|
# 回正相: peak -> land, 带抖动
|
|
n2 = rnd.randint(8, 14)
|
|
for j in range(1, n2 + 1):
|
|
t = j / n2
|
|
x = peak + (land - peak) * t + rnd.uniform(-1.5, 1.5)
|
|
page.mouse.move(x0 + x, y0 + rnd.uniform(-1.5, 1.5))
|
|
page.wait_for_timeout(max(4, int(rnd.uniform(10, 26))))
|
|
# 末端微震荡(人在缺口附近微调)
|
|
for _ in range(rnd.randint(2, 4)):
|
|
page.mouse.move(x0 + land + rnd.uniform(-1.5, 1.5),
|
|
y0 + rnd.uniform(-1.0, 1.0))
|
|
page.wait_for_timeout(rnd.randint(12, 30))
|
|
page.wait_for_timeout(rnd.randint(90, 200))
|
|
page.mouse.up()
|
|
return land
|
|
|
|
|
|
def main() -> int:
|
|
error_url = harvest_error_url("17666663175")
|
|
print(f"[h] error_url={error_url[:90]}", flush=True)
|
|
|
|
verify_results: list[dict] = []
|
|
assets: dict = {"config": {}, "bg": b"", "cut": b""}
|
|
solved_token = ""
|
|
|
|
def on_response(resp):
|
|
if "kSecretApiVerify" in resp.url and resp.request.method == "POST":
|
|
try:
|
|
body = resp.json()
|
|
except Exception:
|
|
return
|
|
verify_results.append({"result": body.get("result"),
|
|
"desc": body.get("desc"),
|
|
"token": (body.get("captchaToken") or "")[:24]})
|
|
print(f"[h] verify -> result={body.get('result')} "
|
|
f"desc={body.get('desc')}", flush=True)
|
|
_update_captcha_assets(resp, assets)
|
|
|
|
with sync_playwright() as pw:
|
|
# 用 probe 验证过的配置: 真 Chrome + 默认硬件 GL(过 anti-bot 的那一套)
|
|
browser = pw.chromium.launch(channel="chrome", headless=True,
|
|
args=["--no-sandbox", "--disable-web-security"])
|
|
ctx = browser.new_context(user_agent=UA, viewport={"width": 412, "height": 915},
|
|
device_scale_factor=3, is_mobile=True, has_touch=True,
|
|
locale="zh-CN")
|
|
ctx.add_cookies([{"name": k, "value": v, "domain": ".kuaishou.com", "path": "/"}
|
|
for k, v in DEV_COOKIES.items()])
|
|
page = ctx.new_page()
|
|
page.on("response", on_response)
|
|
page.goto(error_url, wait_until="domcontentloaded", timeout=30000)
|
|
|
|
import ddddocr
|
|
det = ddddocr.DdddOcr(det=False, ocr=False, show_ad=False)
|
|
|
|
for attempt in range(12):
|
|
try:
|
|
_wait_captcha_assets(page, assets, timeout=20)
|
|
except Exception as e:
|
|
print(f"[h] wait assets fail: {e}", flush=True)
|
|
break
|
|
frame = next((f for f in page.frames
|
|
if f is not page.main_frame and "captcha" in f.url), None)
|
|
if not frame:
|
|
print("[h] no captcha iframe", flush=True)
|
|
break
|
|
try:
|
|
frame.locator(".slider-btn").wait_for(state="visible", timeout=15000)
|
|
except Exception:
|
|
print("[h] slider btn not visible", flush=True)
|
|
break
|
|
bg_box = frame.locator("img[src*='bgPic']").bounding_box()
|
|
cut_box = frame.locator("img[src*='cutPic']").bounding_box()
|
|
btn_box = frame.locator(".slider-btn").bounding_box()
|
|
if not (bg_box and cut_box and btn_box):
|
|
print(f"[h] geom missing bg={bool(bg_box)} cut={bool(cut_box)} btn={bool(btn_box)}", flush=True)
|
|
break
|
|
native_w = int(assets["config"].get("bgPicWidth") or 686)
|
|
scale = bg_box["width"] / native_w
|
|
res = det.slide_match(assets["cut"], assets["bg"])
|
|
target_x = res.get("target_x") or (res.get("target") or [0])[0]
|
|
gap_vp = bg_box["x"] + (target_x + OFFSET) * scale
|
|
drag = gap_vp - cut_box["x"]
|
|
land = human_drag(page, btn_box["x"] + btn_box["width"] / 2,
|
|
btn_box["y"] + btn_box["height"] / 2, drag,
|
|
seed=int(time.time() * 1000) & 0xFFFFFF)
|
|
print(f"[h] drag#{attempt} target_x={target_x} drag={drag:.1f} land_off={land - drag:+.1f}",
|
|
flush=True)
|
|
deadline = time.monotonic() + 4
|
|
while time.monotonic() < deadline:
|
|
page.wait_for_timeout(200)
|
|
if verify_results and verify_results[-1]["result"] in (1,):
|
|
break
|
|
if verify_results and verify_results[-1]["result"] == 1:
|
|
solved_token = verify_results[-1]["token"]
|
|
break
|
|
assets["bg"] = b""; assets["cut"] = b""
|
|
page.wait_for_timeout(2500)
|
|
browser.close()
|
|
|
|
print("\n===== SUMMARY =====", flush=True)
|
|
print("verify_results:", json.dumps(verify_results, ensure_ascii=False), flush=True)
|
|
print("SOLVED:", bool(solved_token), "token:", solved_token or "(none)", flush=True)
|
|
return 0 if solved_token else 2
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|