ksjsb/tools/phone_human_drag.py
2026-07-30 20:25:56 +08:00

194 lines
7.3 KiB
Python

"""真机 人性化拖动 (sendevent 注入) 测试: 350014 是 bot 拖拽模式 还是 号码标记?
adb input swipe = 恒速线性 = 最像 bot。sendevent 注入可模拟人手:
ease-in-out 加速曲线 + 高斯抖动 + 不规则时间间隔。
若人性化拖动到正确落点 => close(result=1) => 是拖拽模式被检测, 路径=人性化自动化/人工。
仍刷新 => 号码被标记(或人性化仍不够, 需人工确认)。
MT-B 协议 (touchpanel /dev/input/event7):
X[0,23040] Y[0,50688], ABS_MT_SLOT=002f TRACKING_ID=0039 X=0035 Y=0036
PRESSURE=0030 TOUCH_MAJOR=0031, BTN_TOUCH=014a BTN_TOOL_FINGER=0145, SYN=0000
"""
from __future__ import annotations
import subprocess
import time
import math
import random
from io import BytesIO
import numpy as np
from PIL import Image
DEV = "/dev/input/event7"
SX = 23040 / 1080.0
SY = 50688 / 2376.0
PHOTO_LEFT, PHOTO_RIGHT = 66, 1013
PHOTO_TOP, PHOTO_BOT = 267, 819
HOME_TRUE = 66 + (24 + 61) * (947 / 686.0)
OFFSET = 23.0 # v7 收敛到的偏移修正
TRACK_Y = 950
def adb_text(*a):
r = subprocess.run(["adb", *a], capture_output=True, text=True,
encoding="utf-8", errors="replace", timeout=60)
return r.stdout
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 detect_notch(img, x_min_local=230, col_thr=25):
reg = photo(img); col = (reg < 90).sum(axis=0); col_r = col[x_min_local:]
hot = np.where(col_r > col_thr)[0]
if len(hot) == 0:
return None
br = np.where(np.diff(hot) > 12)[0]
segs = [s for s in np.split(hot, br + 1) if len(s) > 10]
seg = max(segs, key=lambda s: col_r[s].sum())
segL = x_min_local + int(seg[0]); segR = x_min_local + int(seg[-1])
ys, xs = np.where((reg[:, segL:segR + 1] < 95))
if len(xs) == 0:
return None
return PHOTO_LEFT + int(round(np.average(xs + segL)))
def detect_handle(img, lo=80, hi=340):
band = img[932:968, lo:hi]; col = (band < 200).sum(axis=0).astype(float)
hot = np.where(col > 12)[0]
if len(hot) == 0:
return None
br = np.where(np.diff(hot) > 6)[0]
cands = [s for s in np.split(hot, br + 1) if 10 <= len(s) <= 80]
if not cands:
return None
seg = max(cands, key=lambda s: col[s].max())
return lo + int(round(np.average(seg, weights=col[seg])))
def piece_moved(home, cur):
d = np.abs(photo(cur).astype(np.int16) - photo(home).astype(np.int16))
return int((d > 45).sum())
def piece_pos(home, cur):
"""diff 右簇 cx(拼图块当前位置, screen x)。"""
d = np.abs(photo(cur).astype(np.int16) - photo(home).astype(np.int16))
mask = d > 45; col = mask.sum(axis=0); hot = np.where(col > 6)[0]
if len(hot) == 0:
return None, None
w = np.where(col > 6, col, 0); br = np.where(np.diff(hot) > 12)[0]
c = [s for s in np.split(hot, br + 1) if len(s) > 12]
seg = max(c, key=lambda s: s[-1])
return (PHOTO_LEFT + int(round(np.average(seg, weights=w[seg])))), (PHOTO_LEFT + int(seg[-1]))
def top_activity():
out = adb_text("shell", "dumpsys", "activity", "activities")
return next((l.strip() for l in out.splitlines() if "topResumedActivity" in l), "(none)")
def gen_human_path(x0, y0, x1, y1, n=56, total_s=1.7, seed=1):
"""ease-in-out + 抖动 + 不规则 dt 的人手路径。返回 [(screen_x,screen_y,sleep_ms)]。"""
rnd = random.Random(seed)
pts = []
t = 0.0
# 时间间隔: 主体均匀 + 抖动, 末端略慢(减速)
for i in range(n):
s = i / (n - 1)
# smoothstep ease-in-out
e = s * s * (3 - 2 * s)
x = x0 + (x1 - x0) * e
y = y0 + (y1 - y0) * e
# 抖动: 平行 ±2px, 垂直 ±1.5px (末端减小)
jx = rnd.gauss(0, 2.0) * (1 - 0.6 * s)
jy = rnd.gauss(0, 1.2) * (1 - 0.6 * s)
x += jx; y += jy
# dt: 基础 + 不规则, 起步快末端慢
base = total_s / n * 1000
dt = base * (0.7 + 0.6 * s + rnd.gauss(0, 0.12))
pts.append((x, y, max(8, dt)))
# 末点精确到目标
pts[-1] = (x1, y1, 30)
return pts
def build_script(pts):
"""生成 input motionevent 人性化拖拽脚本(屏幕坐标, 无需缩放; 有权限)。"""
L = ["#!/system/bin/sh"]
x0, y0, _ = pts[0]
L.append(f"input motionevent DOWN {int(round(x0))} {int(round(y0))}")
L.append("sleep 0.04")
for (x, y, dt) in pts[1:]:
L.append(f"input motionevent MOVE {int(round(x))} {int(round(y))}")
L.append(f"sleep {dt/1000.0:.3f}")
xn, yn, _ = pts[-1]
L.append(f"input motionevent UP {int(round(xn))} {int(round(yn))}")
return "\n".join(L)
def main():
home = cap()
notch = detect_notch(home)
hh = detect_handle(home)
print(f"[init] notch={notch} handle={hh}", flush=True)
if notch is None or hh is None:
print("[!] 检测失败"); return 2
target = hh + (notch - HOME_TRUE) + OFFSET
print(f"[plan] humanized drag {hh},{TRACK_Y} -> {target:.0f},{TRACK_Y}", flush=True)
pts = gen_human_path(hh, TRACK_Y, target, TRACK_Y)
script = build_script(pts)
import os
local = os.path.join(os.path.dirname(__file__), "hdrag.sh")
with open(local, "w", newline="\n") as f:
f.write(script)
subprocess.run(["adb", "push", local, "/sdcard/hdrag.sh"],
capture_output=True, timeout=30)
adb_text("logcat", "-c")
# 后台跑脚本, 多帧跟踪 piece 是否到达缺口
sp = subprocess.Popen(["adb", "shell", "sh", "/sdcard/hdrag.sh"],
stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
reached_cx = 0; reached_r = 0
for k in range(7):
time.sleep(0.42)
c = cap()
cx, r = piece_pos(home, c)
if r and r > reached_r:
reached_cx = cx or 0; reached_r = r
print(f" [t~{0.42*(k+1):.1f}s] piece_cx={cx} piece_right={r}", flush=True)
sp.wait()
print(f"[max] piece_cx={reached_cx} piece_right={reached_r} (notch={notch}, "
f"want_right~{notch+84})", flush=True)
time.sleep(4.0)
aft = cap()
after = top_activity()
closed = "KwaiWebViewActivity" not in after
changed = not np.array_equal(photo(home), photo(aft))
print(f"[post] closed={closed} photo_changed={changed} top={after[:60]}", flush=True)
near = reached_r >= notch + 84 - 30 # 落点接近缺口右缘(±30)
if closed:
print("\n>>> VERDICT: PASS (result=1)! 人性化拖动过验证。", flush=True)
print(">>> 350014 是 bot 拖拽模式被检测(非号码标记)。可行路径 = 人性化 motionevent/人工。", flush=True)
elif changed:
print("\n>>> 刷新(verify 触发但未过)。落点正确(near) => 号码被标记; 落点偏 => 错位。", flush=True)
print(">>> 下一步: 请人工手指滑一次(决定性判据)确认号码是否标记。", flush=True)
elif near:
print("\n>>> 落点接近缺口却静默回弹(no verify) => bot 拖拽模式仍被检测。需更人性或人工。", flush=True)
else:
print(f"\n>>> 落点未到缺口(max_right={reached_r} < {notch+84}) => 欠滑, 增大 target。", flush=True)
return 0
if __name__ == "__main__":
raise SystemExit(main())