"""全生命周期人工滑动监控 (GBK 安全, 无 emoji)。 Phase 1: 等 KwaiWebViewActivity 出现 (= 验证码弹起). 最多等 APPEAR_TIMEOUT 秒. 若一直不出现 => 该号在新设备上没触发验证码 (可能直接发短信 / 号码被放行). Phase 2: 验证码已弹起 => 记基线 photo md5, 提示人工滑动. 监听: - 活动离开 KwaiWebViewActivity => PASS (result=1, 人工手指过 => 自动化被检测, 非号码标记) - 检测到拖动后图片 md5 变化 (新缺口) 但仍在验证码页 => REFRESH (350014/350002) - 超时 => 无明确结果 (可能远错位静默回弹 / 没滑) """ from __future__ import annotations import subprocess import time import hashlib from io import BytesIO import numpy as np from PIL import Image PHOTO_LEFT, PHOTO_RIGHT = 66, 1013 PHOTO_TOP, PHOTO_BOT = 267, 819 APPEAR_TIMEOUT = 180 # 等验证码弹起 WATCH_TIMEOUT = 120 # 验证码弹起后等滑动结果 def adb_text(*a): r = subprocess.run(["adb", *a], capture_output=True, text=True, encoding="utf-8", errors="replace", timeout=30) return r.stdout def top(): out = adb_text("shell", "dumpsys", "activity", "activities") return next((l.strip() for l in out.splitlines() if "topResumedActivity" in l), "(none)") def on_captcha(act): # 验证面出现过3种 host, 统一认 WebViewActivity (含 KwaiWebViewActivity / KwaiYodaWebViewActivity) # + FaceRecognitionActivity + Verify*. 别死写死一个 Activity 名。 return (("WebViewActivity" in act) or ("FaceRecognitionActivity" in act) or ("Verify" in act or "verify" in act)) def cap(): d = subprocess.run(["adb", "exec-out", "screencap", "-p"], capture_output=True, timeout=30).stdout return np.asarray(Image.open(BytesIO(d)).convert("L")) def photo(img): return img[PHOTO_TOP:PHOTO_BOT, PHOTO_LEFT:PHOTO_RIGHT] def moved(a, b): d = np.abs(photo(b).astype(np.int16) - photo(a).astype(np.int16)) return int((d > 45).sum()) def main(): print("[watch] Phase1: 等待验证页弹起 (KwaiWebViewActivity / FaceRecognitionActivity / Verify...)...", flush=True) t0 = time.monotonic(); seen = False while time.monotonic() - t0 < APPEAR_TIMEOUT: act = top() if on_captcha(act): seen = True print(f"\n========== 验证码已弹起 (t={time.monotonic()-t0:.0f}s) ==========", flush=True) print("========== 请现在用手指把拼图块滑进缺口! ==========\n", flush=True) break if int(time.monotonic() - t0) % 10 == 0: print(f"[wait] t={time.monotonic()-t0:.0f}s 还没弹验证码. top={act[:60]}", flush=True) time.sleep(1.0) if not seen: print("\n>>> [NO-CAPTCHA] 等了 %ds 没弹验证码." % APPEAR_TIMEOUT, flush=True) print(">>> 该号在清数据后的新设备上可能未触发验证码 (直接发短信/放行)。", flush=True) print(">>> top=%s" % top()[:70], flush=True) return 0 base = cap(); base_md5 = hashlib.md5(photo(base).tobytes()).hexdigest()[:10] print(f"[watch] baseline photo md5={base_md5}", flush=True) w0 = time.monotonic(); motion_seen = False; last_print = 0 while time.monotonic() - w0 < WATCH_TIMEOUT: time.sleep(0.6) act = top(); img = cap() if not on_captcha(act): print(f"\n>>> [PASS] t={time.monotonic()-w0:.0f}s 活动离开验证码页 (人工手指通过)!", flush=True) print(f">>> top={act[:70]}", flush=True) print(">>> 结论: 自动化被检测 (非号码标记). 可行路径 = 人工 in-loop / 根设备+Frida 注入真事件.", flush=True) return 0 mv = moved(base, img) if mv > 2000 and not motion_seen: motion_seen = True print(f"[watch] t={time.monotonic()-w0:.0f}s 检测到手指拖动 (diff={mv}). 等释放结果...", flush=True) md5 = hashlib.md5(photo(img).tobytes()).hexdigest()[:10] if motion_seen and md5 != base_md5: print(f"\n>>> [REFRESH] t={time.monotonic()-w0:.0f}s 图片刷新 (md5 {base_md5}->{md5}) 但仍在验证码页.", flush=True) print(">>> 即人工正确滑动仍被拒 => 350014 (号码被永久标记). 只能换号 (新号不触发验证码).", flush=True) return 0 if time.monotonic() - last_print > 8: print(f"[watch] t={time.monotonic()-w0:.0f}s 等待... motion={'是' if motion_seen else '否'} md5={'同' if md5==base_md5 else '变'}", flush=True) last_print = time.monotonic() print("\n>>> [TIMEOUT] 验证码弹起后 %ds 无明确结果." % WATCH_TIMEOUT, flush=True) print(">>> 可能远错位静默回弹 (没发 verify) 或没滑. 可重跑.", flush=True) return 0 if __name__ == "__main__": raise SystemExit(main())