executor.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. import uiautomator2 as u2
  2. import time
  3. import subprocess
  4. import random
  5. from typing import Optional, Tuple, Union
  6. def find_template_position(small_img_path, big_img_path, threshold=0.8):
  7. """
  8. 在小图中查找大图的位置,返回中心坐标
  9. Args:
  10. small_img_path: 小块碎片路径(要查找的图片)
  11. big_img_path: 整张图片路径(被查找的图片)
  12. threshold: 匹配阈值 0-1,默认0.8
  13. Returns:
  14. (x, y): 中心坐标,如果没找到返回 None
  15. """
  16. # 1. 读取图片
  17. small = cv2.imread(small_img_path)
  18. big = cv2.imread(big_img_path)
  19. if small is None or big is None:
  20. print("❌ 图片读取失败")
  21. return None
  22. # 2. 模板匹配
  23. result = cv2.matchTemplate(big, small, cv2.TM_CCOEFF_NORMED)
  24. min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(result)
  25. print(f"匹配度: {max_val:.3f}")
  26. # 3. 判断是否匹配成功
  27. if max_val < threshold:
  28. print(f"❌ 未找到匹配 (阈值: {threshold})")
  29. return None
  30. # 4. 计算中心坐标
  31. h, w = small.shape[:2]
  32. center_x = max_loc[0] + w // 2
  33. center_y = max_loc[1] + h // 2
  34. return (center_x, center_y)
  35. class SafeExecutor:
  36. """
  37. 安全执行器:为 uiautomator2 提供重试、降级、随机延迟等能力
  38. 所有方法失败时直接抛出异常,不返回结果对象
  39. """
  40. def __init__(self, device_id, default_retry: int = 2, human_delay: bool = True):
  41. """
  42. 参数:
  43. driver: uiautomator2 连接对象
  44. default_retry: 默认重试次数
  45. human_delay: 是否添加随机延迟(模拟人类操作)
  46. """
  47. self.code = 5
  48. self.device_id = device_id
  49. self.driver = u2.connect_usb(self.device_id)
  50. self.default_retry = default_retry
  51. self.human_delay = human_delay
  52. w, h = self.driver.window_size()
  53. self.h = h
  54. # ========== 私有工具方法 ==========
  55. def _random_sleep(self, min_sec: float = 0.05, max_sec: float = 0.2):
  56. """随机睡眠,模拟人类操作间隔"""
  57. if self.human_delay:
  58. time.sleep(random.uniform(min_sec, max_sec))
  59. def _wait_before_retry(self, attempt: int):
  60. """重试前等待(指数退避)"""
  61. wait_time = min(0.5 * (2 ** attempt), 3)
  62. time.sleep(wait_time)
  63. # ========== 基础操作 ==========
  64. def safe_click(self, xpath):
  65. try:
  66. if self.No_slider():
  67. if type(xpath) != str:
  68. elem = xpath
  69. else:
  70. elem = self.driver.xpath(xpath)
  71. if not elem.exists:
  72. return False
  73. elem.click()
  74. time.sleep(2)
  75. if self.No_slider():
  76. return True
  77. else:
  78. return False
  79. except Exception as exc:
  80. print(f"点击失败,已跳过: {xpath}, error: {exc}")
  81. return False
  82. def tap(self, x: int, y: int, retry: Optional[int] = None, jitter: int = 15):
  83. """
  84. 点击坐标,带重试;落点在目标 ±15px 内随机偏移(拟人,避免每次点同一像素)
  85. 失败抛出异常
  86. """
  87. try:
  88. jx = max(0, x + random.randint(-jitter, jitter))
  89. jy = max(0, y + random.randint(-jitter, jitter))
  90. self.driver.click(jx, jy)
  91. self._random_sleep(0.1, 0.3)
  92. return # 成功,直接返回
  93. except Exception as e:
  94. raise Exception(f"点击失败 {x},{y},{e}")
  95. def swipe(self, distance, times=2):
  96. """
  97. 向上滑动指定距离,分多次完成
  98. Args:
  99. distance: 总滑动距离(如 1200)
  100. times: 分成几次(默认2次)
  101. """
  102. w, h = self.driver.window_size()
  103. x = w // 2 + random.randint(-30, 30) # X轴随机偏移
  104. start_y = int(h * 0.7) + random.randint(-20, 20) # 起始位置随机偏移
  105. per_distance = distance // times
  106. current_y = start_y
  107. # print(f"📏 向上滑动 {distance}px,分 {times} 次")
  108. for i in range(times):
  109. # 每次滑动距离随机波动
  110. if i == times - 1:
  111. end_y = current_y - (distance - (per_distance * (times - 1)))
  112. else:
  113. # 每段距离 ±20px 波动
  114. seg_distance = per_distance + random.randint(-15, 15)
  115. end_y = current_y - seg_distance
  116. end_y = max(50, end_y)
  117. # 滑动时长随机
  118. duration = random.uniform(0.06, 0.12)
  119. # print(f" 第{i+1}次: {current_y} → {end_y}")
  120. self.driver.drag(x, current_y, x, end_y, duration=duration)
  121. if i < times - 1:
  122. # 暂停时间随机
  123. pause_time = random.uniform(0.2, 0.5)
  124. # print(f" ⏸️ 暂停 {pause_time:.2f}s")
  125. time.sleep(pause_time)
  126. current_y = end_y
  127. def input_text(self, xpaths, text):
  128. """
  129. 输入文本,带重试
  130. 失败抛出异常
  131. """
  132. try:
  133. search_input = self.driver.xpath(xpaths)
  134. if search_input.exists:
  135. search_input.set_text('')
  136. time.sleep(1)
  137. # 模拟打字间隔
  138. search_input.set_text(text)
  139. return True
  140. return False
  141. except Exception as e:
  142. raise Exception(f"输入失败 '{text}',: {e}")
  143. def is_connected(self) -> bool:
  144. """检查与设备的连接是否正常"""
  145. try:
  146. # 方式1:检查 atx-agent 是否响应
  147. info = self.driver.info # 会请求 atx-agent
  148. return True
  149. except Exception:
  150. self._recover_service()
  151. return False
  152. def _recover_service(self):
  153. # 重新连接
  154. # 停止 atx-agent
  155. subprocess.run(["adb", "-s", self.device_id, "shell",
  156. "/data/local/tmp/atx-agent", "server", "-d", "--stop"],
  157. capture_output=True, timeout=5)
  158. time.sleep(1)
  159. # 启动 atx-agent
  160. subprocess.run(["adb", "-s", self.device_id, "shell",
  161. "/data/local/tmp/atx-agent", "server", "-d"],
  162. capture_output=True, timeout=5)
  163. time.sleep(2)
  164. # 重新连接 uiautomator2
  165. self.driver = u2.connect_usb(self.device_id)
  166. def No_slider(self) -> bool:
  167. """检测当前是否有滑块(默认实现,可覆盖)"""
  168. # 这里用你实际的滑块检测逻辑
  169. # 示例:检测特定图片或UI元素
  170. for i in range(4):
  171. if '检测成功' in '检测成功':
  172. self._solve_slider()
  173. else:
  174. break
  175. return True
  176. return False
  177. def _solve_slider(self):
  178. """解决滑块验证(默认实现,可覆盖"""
  179. num = 0
  180. while (self.driver.xpath(
  181. '//*[@text="检测到您的账号存在风险,为保障账号安全,请进行安全验证"]').exists or self.driver.xpath(
  182. '//*[@text="身份安全验证"]').exists):
  183. self.driver.press('back')
  184. time.sleep(0.3)
  185. num += 1
  186. if (num >= 6):
  187. break
  188. time.sleep(random.uniform(2, 3))
  189. if self.driver.xpath(
  190. '//*[@text="检测到您的账号存在风险,为保障账号安全,请进行安全验证"]').exists or self.driver.xpath(
  191. '//*[@text="身份安全验证"]').exists:
  192. self._solve_slider()
  193. if (self.driver.xpath('//*[@text="验证码"]').exists):
  194. print('其他验证码,无xpath')
  195. self.driver.screenshot(f'test{code}.jpg')
  196. code += 1
  197. # self.driver.screenshot('test.jpg')
  198. # time.sleep(1)
  199. # x= find_template_position('t.jpg','test.jpg')
  200. # self.driver.click(x[0],x[1])
  201. # 这里写你的滑块处理逻辑
  202. # 例如:找到滑块位置,模拟滑动轨迹
  203. pass
  204. def safe_get_all(self, xpath):
  205. try:
  206. return self.driver.xpath(xpath).all()
  207. except Exception as exc:
  208. print(f"获取元素列表失败,已跳过: {xpath}, error: {exc}")
  209. return []
  210. def safe_exists(self, xpath):
  211. try:
  212. return self.driver.xpath(xpath).exists
  213. except Exception as exc:
  214. print(f"xpath不存在或检查失败,已跳过: {xpath}, error: {exc}")
  215. return False
  216. def ele(self, xpath):
  217. try:
  218. return self.driver.xpath(xpath)
  219. except Exception as exc:
  220. print(f"xpath不存在或检查失败,已跳过: {xpath}, error: {exc}")
  221. return False
  222. def safe_get_text(self, xpath):
  223. try:
  224. elem = self.driver.xpath(xpath)
  225. if not elem.exists:
  226. return ""
  227. node = elem.get()
  228. return (node.text or "").strip() if node else ""
  229. except Exception as exc:
  230. print(f"获取文本失败,已跳过: {xpath}, error: {exc}")
  231. return ""
  232. def safe_press_back(self):
  233. try:
  234. self.driver.press('back')
  235. return True
  236. except Exception as exc:
  237. print(f"safe_press_back 返回失败: {exc}")
  238. return False