tmp_captcha_test2.py 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. """test.py 轨迹 + TouchPipe压感 + 可靠执行"""
  2. import sys, os, json, time, random, math, threading
  3. import uiautomator2 as u2
  4. from uiautomator2.core import AdbHTTPConnection
  5. import cv2, numpy as np, base64, requests
  6. from rapidocr_onnxruntime import RapidOCR
  7. os.environ["PYTHONIOENCODING"] = "utf-8"
  8. sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
  9. from detect_slider_button import detect_slider_button
  10. from detect_captcha_edge import detect_captcha_left_edge
  11. ocr_eng = RapidOCR()
  12. TOKEN = "1nDVocTE2mJ0yLEYb2sZJ5uUY2VIEoGTkIpW44X7Kgk"
  13. JFBYM_URL = "http://api.jfbym.com/api/YmServer/customApi"
  14. IMG_ROOT = os.path.join(os.path.dirname(os.path.abspath(__file__)), "image", "test2_image")
  15. OUT = os.path.join(IMG_ROOT, "d")
  16. ALIGN_DIR = os.path.join(IMG_ROOT, "align")
  17. SUCCESS_DIR = os.path.join(IMG_ROOT, "success")
  18. FAILURE_DIR = os.path.join(IMG_ROOT, "failure")
  19. os.makedirs(OUT, exist_ok=True)
  20. os.makedirs(ALIGN_DIR, exist_ok=True)
  21. os.makedirs(SUCCESS_DIR, exist_ok=True)
  22. os.makedirs(FAILURE_DIR, exist_ok=True)
  23. OFFSET_COMPENSATE = random.uniform(-7,-5) # MICRO_MODE="fixed" 偏移量(正=右,负=左)
  24. MICRO_MODE = "fixed" # "fixed"=固定偏移 / "visual"=红绿线对齐 / "none"=不用
  25. def _clamp(v, lo, hi):
  26. return max(lo, min(v, hi))
  27. def _pressure_curve(i, n, phase):
  28. """压力曲线:开头高→巡航低→末尾高。phase: 'right' 或 'return'。"""
  29. t = i / max(1, n - 1)
  30. if t < 0.1: return random.randint(60, 90) # 开头按重
  31. elif t > 0.90: return random.randint(55, 85) # 末尾对准按重
  32. else: return random.randint(15, 35) # 巡航轻按
  33. class TouchPipe:
  34. def __init__(self, dev):
  35. self._dev = dev; self._conn = None; self._sock = None
  36. self._stop = threading.Event(); self._drainer = None
  37. self._lock = threading.Lock(); self._fallback = False
  38. def open(self):
  39. try:
  40. self._conn = AdbHTTPConnection(self._dev.adb_device, port=9008)
  41. self._conn.timeout = 15; self._conn.connect()
  42. self._sock = self._conn.sock; self._sock.settimeout(0.5)
  43. self._drainer = threading.Thread(target=self._drain, daemon=True)
  44. self._drainer.start()
  45. except Exception: self._fallback = True
  46. return self
  47. def _drain(self):
  48. while not self._stop.is_set():
  49. try:
  50. if not self._sock.recv(65536): break
  51. except Exception: continue
  52. def move(self, x, y, pressure=0):
  53. if self._fallback:
  54. try: self._dev.touch.move(int(x), int(y))
  55. except Exception: pass
  56. return
  57. try:
  58. with self._lock:
  59. self._sock.sendall(self._req(x, y, pressure))
  60. except Exception: self._fallback = True; self.move(x, y, pressure)
  61. def _req(self, x, y, pressure):
  62. body = json.dumps({"jsonrpc":"2.0","id":1,"method":"injectInputEvent",
  63. "params":[2,int(x),int(y),int(pressure)]}).encode()
  64. return (f"POST /jsonrpc/0 HTTP/1.1\r\nHost: localhost\r\n"
  65. f"User-Agent: u2\r\nAccept-Encoding: \r\n"
  66. f"Content-Type: application/json\r\nContent-Length: {len(body)}\r\n"
  67. f"Connection: keep-alive\r\n\r\n").encode() + body
  68. def close(self):
  69. self._stop.set()
  70. if self._drainer: self._drainer.join(timeout=1)
  71. try:
  72. if self._sock: self._sock.close()
  73. except Exception: pass
  74. def _build_right_track(start_x, start_y, end_x):
  75. """test.py 原样 — 钟形鼓包 wobble, EMA 0.55"""
  76. dist = abs(end_x - start_x)
  77. steps = int(_clamp(dist / random.uniform(6.0, 8.0), 35, 50)) + 20
  78. end_yd = random.randint(-30, 30)
  79. wobbles = [(random.uniform(0.15, 0.85), random.uniform(-25, 25))
  80. for _ in range(random.randint(2, 4))]
  81. points = [(int(start_x), int(start_y))]
  82. smooth_y = float(start_y)
  83. for i in range(1, steps + 1):
  84. t = i / steps
  85. x = start_x + dist * (1.0 - (1.0 - t) ** 2.0)
  86. y = start_y + end_yd * (t ** 0.6)
  87. if 0.15 < t < 0.9:
  88. for wp, wv in wobbles:
  89. dw = abs(t - wp)
  90. if dw < 0.12: y += wv * (1 - dw / 0.12)
  91. smooth_y = smooth_y * 0.55 + y * 0.45
  92. points.append((int(round(x)), int(round(smooth_y))))
  93. return points
  94. def _build_human_return_track(start_x, start_y, target_x):
  95. """test.py 原样 — wobble, EMA 0.55, blend t>0.5 不打勾"""
  96. distance = start_x - target_x
  97. if distance <= 0:
  98. return [(int(target_x), int(start_y))]
  99. # 点数按折回距离自适应,对标真机但控制上限避免等太久
  100. if distance > 600:
  101. steps = random.randint(150, 200) # 长折, Y必往下漂
  102. end_dy = random.randint(80, 200); cat = '长折'
  103. elif distance > 450:
  104. steps = random.randint(120, 170) # 中折
  105. end_dy = random.randint(-20, 40); cat = '中折'
  106. else:
  107. steps = int(_clamp(distance / random.uniform(3.0, 5.0), 35, 55))
  108. end_dy = random.randint(-20, 40); cat = '短折'
  109. print(f' 折回: {cat} distance={distance:.0f}px {steps}点 Y漂={end_dy:+d}')
  110. # 两段: 巡航60%点数走88%距离(大步), 减速40%点数走12%距离(密集蠕动)
  111. cruise_n = int(steps * 0.60)
  112. decel_n = steps - cruise_n
  113. target_y = start_y + end_dy
  114. r_wobbles = [(random.uniform(0.15, 0.5), random.uniform(-20, 20))
  115. for _ in range(random.randint(2, 4))]
  116. track, smooth_y = [], float(start_y)
  117. cruise_dist = distance * 0.88
  118. decel_dist = distance * 0.12
  119. # ── 前90%大步巡航 ──
  120. for i in range(1, cruise_n + 1):
  121. t = i / cruise_n
  122. x = start_x - cruise_dist * t
  123. y = start_y + (target_y - start_y) * t
  124. if 0.1 < t < 0.5:
  125. for wp, wv in r_wobbles:
  126. dw = abs(t - wp)
  127. if dw < 0.12: y += wv * (1 - dw / 0.12)
  128. smooth_y = smooth_y * 0.55 + y * 0.45
  129. track.append((int(round(x)), int(round(smooth_y))))
  130. # ── 后10%小步减速对准 ──
  131. dsx, dsy = track[-1]
  132. for i in range(1, decel_n + 1):
  133. tt = i / decel_n
  134. ease = 1 - (1 - tt) ** 2
  135. x = dsx - decel_dist * ease
  136. y = dsy + (target_y - dsy) * tt
  137. track.append((int(round(x)), int(round(y))))
  138. if track: track[-1] = (int(target_x), int(target_y))
  139. return track
  140. def save(name, img):
  141. cv2.imwrite(os.path.join(OUT, name), img)
  142. def save_track_image(pts, filepath):
  143. if len(pts) < 2: return
  144. xs = [p[0] for p in pts]; ys = [p[1] for p in pts]
  145. m = 50
  146. w = max(xs)-min(xs)+m*2; h = max(ys)-min(ys)+m*2
  147. w, h = max(w,200), max(h,100)
  148. c = np.ones((h, w, 3), dtype=np.uint8)*255
  149. for i in range(1, len(pts)):
  150. r = i/len(pts); g = 200 if r<0.5 else int(200*(1-r)*2); b = int(200*r*2) if r<0.5 else 200
  151. cv2.line(c, (pts[i-1][0]-min(xs)+m, pts[i-1][1]-min(ys)+m),
  152. (pts[i][0]-min(xs)+m, pts[i][1]-min(ys)+m), (0,g,b), 1)
  153. cv2.circle(c, (pts[0][0]-min(xs)+m, pts[0][1]-min(ys)+m), 4, (0,200,0), -1)
  154. cv2.circle(c, (pts[-1][0]-min(xs)+m, pts[-1][1]-min(ys)+m), 4, (0,0,200), -1)
  155. cv2.imwrite(filepath, c)
  156. def solve_slider(driver, sx=None):
  157. global d, W, H
  158. d = driver; W, H = d.window_size(); d.screen_on()
  159. # 自动判断分辨率: 宽>1000=高分屏(sx=163), 否则=低分屏(sx=87)
  160. if sx is None:
  161. sx = 163 if W > 1000 else 87
  162. print(f" 屏幕: {W}x{H} → sx={sx} ({'高分' if W>1000 else '低分'})")
  163. screen = d.screenshot(format="opencv")
  164. slider_y = y_top = slider_bottom = None
  165. try:
  166. cv2.imwrite(os.path.join(OUT, "_tmp_slider.png"), screen)
  167. info = detect_slider_button(os.path.join(OUT, "_tmp_slider.png"))
  168. if info and len(info) >= 3: sx = info[0] + int(info[2]/2)
  169. except: pass
  170. ocr_r = ocr_eng(screen)
  171. if ocr_r and ocr_r[0]:
  172. for item in ocr_r[0]:
  173. t = item[1]; cy = int((item[0][0][1]+item[0][2][1])/2)
  174. if "请按照说明拖动滑块" in t: slider_y = slider_y or cy; slider_bottom = int(item[0][2][1])
  175. if "松开" in t: y_top = int(item[0][0][1])
  176. if slider_y is None: return False
  177. sy = slider_y
  178. # 右滑 — TouchPipe + 压感
  179. d.touch.down(sx, sy); time.sleep(0.08)
  180. pipe = TouchPipe(d).open()
  181. t0 = time.perf_counter(); track_pts = []
  182. right_track = _build_right_track(sx, sy, W-30)
  183. for i, (x, y) in enumerate(right_track):
  184. p = _pressure_curve(i, len(right_track), 'right')
  185. pipe.move(x, y, p)
  186. track_pts.append({"x": x, "y": y, "pressure": p,
  187. "rel_ms": (time.perf_counter()-t0)*1000})
  188. time.sleep(random.uniform(0.002, 0.004))
  189. # 等设备消化完右滑点再截图 — 每点预留 28ms 处理时间
  190. settle_ms = max(500, len(right_track) * 18)
  191. time.sleep(settle_ms / 1000.0)
  192. # 展开后的幕布截图(后面叠加轨迹用)
  193. unfolded = d.screenshot(format="opencv")
  194. # JFBYM
  195. crop = d.screenshot(format="opencv")
  196. if y_top and slider_bottom: crop = crop[y_top:slider_bottom, :]
  197. _, buf = cv2.imencode(".png", crop)
  198. gap = None
  199. for a in range(3):
  200. try:
  201. r = requests.post(JFBYM_URL, json={"token":TOKEN,"type":"20226","image":base64.b64encode(buf).decode()}, timeout=35).json()
  202. if r.get("data") and r["data"].get("data"): gap = int(r["data"]["data"])
  203. elif r.get("data") and isinstance(r["data"],(int,float)): gap = int(r["data"])
  204. if gap is not None: break
  205. time.sleep(2)
  206. except: time.sleep(2)
  207. if gap is None: pipe.close(); d.touch.up(W-20, sy); return False
  208. # 折回
  209. target_x = gap + OFFSET_COMPENSATE
  210. return_track = _build_human_return_track(W-30, right_track[-1][1], target_x)
  211. for i, (x, y) in enumerate(return_track):
  212. p = _pressure_curve(i, len(return_track), 'return')
  213. pipe.move(x, y, p)
  214. track_pts.append({"x": x, "y": y, "pressure": p,
  215. "rel_ms": (time.perf_counter()-t0)*1000})
  216. # 前60%巡航快发, 后40%减速慢发
  217. frac = i / max(1, len(return_track)-1)
  218. if frac > 0.60:
  219. time.sleep(random.uniform(0.004, 0.008))
  220. else:
  221. time.sleep(random.uniform(0.000, 0.002))
  222. # 等设备消化完折回点
  223. # settle 按距离缩放: 每px约2~4ms, 上下限保护
  224. ret_distance = (W-30) - target_x
  225. settle_ms2 = int(ret_distance * random.uniform(2.0, 4.0))
  226. settle_ms2 = max(500, min(3500, settle_ms2))
  227. if ret_distance > 600: scat = '长等'
  228. elif ret_distance > 450: scat = '中等'
  229. else: scat = '短等'
  230. print(f' settle={settle_ms2}ms({settle_ms2/1000:.1f}s)')
  231. time.sleep(settle_ms2 / 1000.0)
  232. # 微调: 三种模式
  233. cur_x, cur_y = return_track[-1]
  234. aligned_img = None
  235. if MICRO_MODE == "fixed":
  236. target_cur_x = int(_clamp(cur_x + OFFSET_COMPENSATE, 10, W-10))
  237. steps = abs(target_cur_x - cur_x)
  238. sign = 1 if target_cur_x > cur_x else -1
  239. for s in range(steps):
  240. cur_x += sign
  241. pipe.move(cur_x, cur_y, 60)
  242. time.sleep(random.uniform(0.005, 0.010))
  243. print(f" 固定偏移: {OFFSET_COMPENSATE:+.0f}px 分{steps}步 -> x={cur_x}")
  244. elif MICRO_MODE == "visual":
  245. fallback_x = cur_x; prev_pl = None
  246. for attempt in range(3):
  247. time.sleep(0.3)
  248. check = d.screenshot(format="opencv")
  249. cv2.imwrite(os.path.join(ALIGN_DIR, f"align_{attempt}.png"), check)
  250. try: pl, _ = detect_captcha_left_edge(check)
  251. except Exception: break
  252. diff = pl - gap
  253. marked = check.copy()
  254. cv2.line(marked, (pl, 0), (pl, marked.shape[0]), (0, 255, 0), 3)
  255. cv2.line(marked, (gap, 0), (gap, marked.shape[0]), (0, 0, 255), 3)
  256. cv2.imwrite(os.path.join(ALIGN_DIR, f"align_{attempt}_marked.png"), marked)
  257. if abs(diff) <= 5: break
  258. if prev_pl is not None and pl == prev_pl:
  259. cur_x = int(_clamp(fallback_x,10,W-10)); pipe.move(cur_x,cur_y,60); break
  260. prev_pl = pl
  261. cur_x = int(_clamp(cur_x-diff,10,W-10)); pipe.move(cur_x,cur_y,50); time.sleep(0.05)
  262. # 滑块对准缺口的截图,做轨迹背景
  263. aligned_img = d.screenshot(format="opencv")
  264. pipe.close(); d.touch.up(int(cur_x),int(cur_y)); time.sleep(2)
  265. # 验证
  266. final = d.screenshot(format="opencv"); check_r = ocr_eng(final)
  267. passed = True
  268. if check_r and check_r[0]:
  269. if any("拖动滑块" in it[1] or "请按住滑块" in it[1] or "安全验证" in it[1] for it in check_r[0]): passed = False
  270. if not passed:
  271. time.sleep(2); final = d.screenshot(format="opencv"); check_r = ocr_eng(final)
  272. passed = True
  273. if check_r and check_r[0]:
  274. if any("拖动滑块" in it[1] or "请按住滑块" in it[1] or "安全验证" in it[1] for it in check_r[0]): passed = False
  275. all_track = right_track + return_track
  276. rd = SUCCESS_DIR if passed else FAILURE_DIR
  277. now = time.localtime()
  278. dev_id = getattr(d, 'serial', getattr(d, '_serial', 'unknown'))
  279. prefix = f"{dev_id}_{now.tm_year:04d}{now.tm_mon:02d}{now.tm_mday:02d}_{now.tm_hour:02d}{now.tm_min:02d}{now.tm_sec:02d}"
  280. save_track_image(all_track, os.path.join(rd, f"{prefix}_track.png"))
  281. # 轨迹叠加在滑块对齐缺口时的截图上
  282. if aligned_img is not None:
  283. b_img = aligned_img.copy()
  284. for i in range(1, len(all_track)):
  285. cv2.line(b_img, all_track[i-1], all_track[i], (0, 200, 200), 2)
  286. cv2.circle(b_img, all_track[0], 6, (0, 255, 0), -1)
  287. cv2.circle(b_img, all_track[-1], 6, (0, 0, 255), -1)
  288. cv2.imwrite(os.path.join(rd, f"{prefix}_b.png"), b_img)
  289. tj = {"device": dev_id, "screen": {"w": W, "h": H},
  290. "time": time.strftime("%Y-%m-%d %H:%M:%S", now),
  291. "total_points": len(track_pts),
  292. "duration_ms": track_pts[-1]["rel_ms"] if track_pts else 0,
  293. "start": {"x": track_pts[0]["x"], "y": track_pts[0]["y"]},
  294. "end": {"x": track_pts[-1]["x"], "y": track_pts[-1]["y"]},
  295. "dx": track_pts[-1]["x"]-track_pts[0]["x"],
  296. "dy": track_pts[-1]["y"]-track_pts[0]["y"],
  297. "passed": passed, "gap": gap,
  298. "points": track_pts}
  299. with open(os.path.join(rd, f"{prefix}_track.json"), 'w', encoding='utf-8') as fp:
  300. json.dump(tj, fp, ensure_ascii=False)
  301. return passed
  302. if __name__ == "__main__":
  303. DEVICE = "NJZX8DZXT47HQGWO"
  304. d = u2.connect(DEVICE)
  305. print(f"设备: {DEVICE}")
  306. solve_slider(d, sx=None) # None=自动判断分辨率