"""隔离验证 + 诊断: 扫描 auto-drag 落点偏置, 找到能让 kSecretApiVerify result==1 的 offset。 自驱浏览器(不经 _run_system_browser_challenge, 以便加诊断): - 每次 drag 用一个 offset (对 target_x 的原生像素修正) - mouseup 后立即读拼图块真实落点 (vs gap_vp) - 捕获每次 kSecretApiVerify 的 result/desc - 失败后等自动刷新(新 bgPic)再试下一个 offset 判读: 若某个 offset 通过(result==1) -> 纯位置偏置(几何); 若整段都不通过 -> 行为/轨迹检测。 """ from __future__ import annotations import argparse, json, re, shutil, subprocess, sys, tempfile, time from pathlib import Path ROOT = Path(__file__).resolve().parents[1] if str(ROOT) not in sys.path: sys.path.insert(0, str(ROOT)) OUT = ROOT / "out" / "captcha_autotest" from core.captcha_assist import ( # noqa: E402 CaptchaVerificationState, _auto_solve_slider, _find_system_chromium, _free_local_port, _update_captcha_assets, _wait_for_cdp_endpoint, build_captcha_browser_cookies, ) from core.device_profile import load_device_profile # noqa: E402 def fresh_error_url(mobile, profile, base): cmd = [sys.executable, "-m", "tools.sms_login_cli", "--mobile", mobile, "--code", "000000", "--base-url", base, "--transport", "okhttp4-android10"] print("[autotest] 取新鲜 error_url ...") p = subprocess.run(cmd, capture_output=True, text=True, cwd=str(ROOT)) out = p.stdout + p.stderr m = re.search(r'https://app\.m\.kuaishou\.com/verify/captcha\.html\?[^"\s\\]+', out) if not m: print(out[-1500:]) raise SystemExit("[autotest] 未提取到 error_url") return m.group(0) def main() -> int: ap = argparse.ArgumentParser() ap.add_argument("--mobile", default="17666663175") ap.add_argument("--device-profile", default="out/sms_device_latest.json") ap.add_argument("--base-url", default="https://az1-api.ksapisrv.com") ap.add_argument("--timeout", type=int, default=120) ap.add_argument("--offsets", default="-48,-36,-24,-12,0,12,24,36,48", help="逗号分隔, 对 target_x 的原生像素修正; 逐个试") a = ap.parse_args() OUT.mkdir(parents=True, exist_ok=True) offsets = [int(x) for x in a.offsets.split(",") if x.strip()] profile = load_device_profile(a.device_profile) url = fresh_error_url(a.mobile, a.device_profile, a.base_url) print(f"[autotest] error_url = {url[:90]}") cookies = build_captcha_browser_cookies(profile) from playwright.sync_api import sync_playwright import subprocess as sp state = CaptchaVerificationState() assets: dict = {"config": {}, "bg": b"", "cut": b""} verify_log: list = [] # [{seq,result,desc,path}] seq = {"n": 0} bg_gen = {"n": 0} # 每来一张新 bgPic 自增 netlog: list = [] def observe(u, payload, *, status, request_payload=None): state.observe(u, payload, status=status, request_payload=request_payload) return state.verified def on_response(resp): try: u = str(resp.url) except Exception: return if any(k in u for k in ("captcha", "sliding", "verify", "/zt/", "/wd/")): try: st = int(resp.status) except Exception: st = -1 netlog.append({"url": u[:160], "status": st}) if "/sliding/bgPic" in u: bg_gen["n"] += 1 if "kSecretApiVerify" in u or "/wd/captcha/verify" in u: try: body = resp.json() except Exception: body = None seq["n"] += 1 verify_log.append({ "seq": seq["n"], "result": (body or {}).get("result"), "desc": (body or {}).get("desc"), "path": u.split("/")[-1], }) print(f"[verify #{seq['n']}] {u.split('/')[-1]} " f"result={(body or {}).get('result')} desc={(body or {}).get('desc')}") _update_captcha_assets(resp, assets) try: payload = resp.json() except Exception: return observe(u, payload, status=int(resp.status), request_payload=_request_payload(resp)) browser = _find_system_chromium() port = _free_local_port() prof = tempfile.mkdtemp(prefix="ksjsb-auto-") proc = sp.Popen([browser, f"--remote-debugging-port={port}", "--remote-debugging-address=127.0.0.1", f"--user-data-dir={prof}", "--no-first-run", "--no-default-browser-check", "--window-size=430,920", "about:blank"], stdout=sp.DEVNULL, stderr=sp.DEVNULL) summary: list = [] try: ep = _wait_for_cdp_endpoint(port, timeout=a.timeout) with sync_playwright() as pw: b = pw.chromium.connect_over_cdp(ep, timeout=20000) page = b.contexts[0].pages[0] if b.contexts[0].pages else b.contexts[0].new_page() b.contexts[0].add_cookies(cookies) page.on("response", on_response) page.goto(url, wait_until="domcontentloaded", timeout=30000) print("[autotest] 等 bg/cut/config ...") _wait_assets(page, assets, timeout=20) print(f"[autotest] offsets = {offsets}") for off in offsets: cap_fr = None for fr in page.frames: if "captcha" in fr.url and fr is not page.main_frame: cap_fr = fr break if cap_fr is None: print("[sweep] 无 captcha frame, 中止") break try: cap_fr.locator(".slider-btn").wait_for(state="visible", timeout=10000) except Exception as e: print(f"[sweep off={off:+d}] slider-btn 未就绪: {e}") break prev_seq = seq["n"] geo = None try: geo = _auto_solve_slider(page, assets, timeout=20, offset=off) except Exception as e: print(f"[sweep off={off:+d}] solve 失败: {e}") # 存本帧 bg + ddddocr target_x 红线, 供视觉核对 ddddocr 是否锁对缺口 if geo: try: from PIL import Image, ImageDraw import io bgim = Image.open(io.BytesIO(assets["bg"])).convert("RGB") tx = geo["target_x"] ImageDraw.Draw(bgim).line([(tx, 0), (tx, bgim.height)], fill="red", width=4) mp = OUT / f"marked_off{off:+d}.png" bgim.save(str(mp)) print(f"[mark] off={off:+d} target_x={tx} (bg {bgim.width}x{bgim.height}) -> {mp}") except Exception as e: print("[mark] 失败", e) # 等 verify 触发 dl = time.monotonic() + 8 while seq["n"] == prev_seq and time.monotonic() < dl: page.wait_for_timeout(150) v = verify_log[-1] if (verify_log and verify_log[-1]["seq"] > prev_seq) else None res = v["result"] if v else None tx = geo.get("target_x") if geo else None gp = f"{geo['gap_vp']:.1f}" if geo else "?" ld = (f"{geo['landed_x']:.1f}" if geo and geo.get("landed_x") is not None else "?") print(f"[sweep off={off:+d}] target_x={tx} gap_vp={gp} landed={ld} -> result={res}") summary.append((off, res, ld, gp)) if res == 1: print(f"[sweep] ✓ SUCCESS — offset {off:+d} 通过") break # 等自动刷新(新 bgPic)再试下一个 gen0 = bg_gen["n"] dl = time.monotonic() + 8 while bg_gen["n"] == gen0 and time.monotonic() < dl: page.wait_for_timeout(150) if bg_gen["n"] == gen0: print("[sweep] 未检测到刷新(可能次数用尽), 中止") break _wait_assets(page, assets, timeout=10) print("===== SWEEP SUMMARY =====") for off, res, ld, gp in summary: mark = "✓" if res == 1 else ("x" if res else "?") print(f" {mark} off={off:+d} landed={ld} gap_vp={gp} result={res}") try: page.screenshot(path=str(OUT / "after_sweep.png"), full_page=True) except Exception: pass finally: if proc.poll() is None: proc.terminate() try: proc.wait(timeout=3) except sp.TimeoutExpired: proc.kill() shutil.rmtree(prof, ignore_errors=True) return 0 if any(r == 1 for _, r, _, _ in summary) else 1 def _request_payload(resp): req = getattr(resp, "request", None) try: return req.post_data_json if req else None except Exception: return None def _wait_assets(page, assets, *, timeout): deadline = time.monotonic() + timeout while not (assets["bg"] and assets["cut"] and assets["config"].get("bgPicWidth")): if time.monotonic() >= deadline: raise TimeoutError("等 bg/cut/config 超时") page.wait_for_timeout(200) if __name__ == "__main__": raise SystemExit(main())