import math import random import re import time from typing import Tuple, List import tidevice import wda from wda import Client from Utils.AiUtils import AiUtils from Utils.LogManager import LogManager # 页面控制工具类 class ControlUtils(object): # 获取设备上的app列表 @classmethod def getDeviceAppList(self, udid): device = tidevice.Device(udid) # 获取已安装的应用列表 apps = [] for app in device.installation.iter_installed(): apps.append({ "name": app.get("CFBundleDisplayName", "Unknown"), "bundleId": app.get("CFBundleIdentifier", "Unknown"), "version": app.get("CFBundleShortVersionString", "Unknown"), "path": app.get("Path", "Unknown") }) # 筛选非系统级应用(过滤掉以 com.apple 开头的系统应用) noSystemApps = [app for app in apps if not app["bundleId"].startswith("com.apple")] return noSystemApps # 打开Tik Tok @classmethod def openTikTok(cls, session: Client, udid): apps = cls.getDeviceAppList(udid) tk = "" for app in apps: if app.get("name", "") == "TikTok": tk = app.get("bundleId", "") currentApp = session.app_current() if currentApp != tk: session.app_start(tk) # 关闭Tik Tok @classmethod def closeTikTok(cls, session: Client, udid): apps = cls.getDeviceAppList(udid) tk = "" for app in apps: if app.get("name", "") == "TikTok": tk = app.get("bundleId", "") session.app_stop(tk) # 返回 @classmethod def clickBack(cls, session: Client): try: # back = session.xpath( # "//*[@label='返回']" # " | " # "//*[@label='返回上一屏幕']" # " | " # "//XCUIElementTypeButton[@visible='true' and @name='TTKProfileNavBarBaseItemComponent' and @label='IconChevronLeftOffsetLTR']" # ) back = session.xpath( # ① 常见中文文案 "//*[@label='返回' or @label='返回上一屏幕']" " | " # ② 英文 / 内部 name / 图标 label 的按钮(仅限 Button,且可见) "//XCUIElementTypeButton[@visible='true' and (" "@name='Back' or @label='Back' or " # 英文 "@name='返回' or @label='返回' or " # 中文 "@label='返回上一屏幕' or " # 中文另一种 "@name='nav_bar_start_back' or " # 内部常见 name "(@name='TTKProfileNavBarBaseItemComponent' and @label='IconChevronLeftOffsetLTR')" # 你给的特例 ")]" ) if back.exists: back.click() return True elif session.xpath("//*[@name='nav_bar_start_back']").exists: back = session.xpath("//*[@name='nav_bar_start_back']") if back.exists: back.click() return True elif session.xpath( "//Window[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]").exists: back = session.xpath( "//Window[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]") if back.exists: back.click() return True else: return False except Exception as e: print(e) return False # 点赞 @classmethod def clickLike(cls, session: Client, udid): scale = session.scale x, y = AiUtils.findImageInScreen("add", udid) print(x, y) if x > -1: LogManager.info("点赞了", udid) session.click(x // scale, y // scale + 50) return True else: LogManager.info("没有找到目标", udid) return False # 点击搜索 @classmethod def clickSearch(cls, session: Client): # obj = session.xpath("//*[@name='搜索']") obj = session(xpath='//*[@name="搜索" or @label="搜索" or @name="Search" or @label="Search"]') try: if obj.exists: obj.click() return True except Exception as e: print(e) return False # 点击收件箱按钮 @classmethod def clickMsgBox(cls, session: Client): box = session.xpath("//XCUIElementTypeButton[name='a11y_vo_inbox']") if box.exists: box.click() return True else: return False # 获取主播详情页的第一个视频 @classmethod def clickFirstVideoFromDetailPage(cls, session: Client): # videoCell = session.xpath( # '//Window/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[2]/Other[1]/ScrollView[1]/Other[1]/CollectionView[1]/Cell[2]') videoCell = session.xpath( '(//XCUIElementTypeCollectionView//XCUIElementTypeCell[.//XCUIElementTypeImage[@name="profile_video"]])[1]') tab = session.xpath( '//XCUIElementTypeButton[@name="TTKProfileTabVideoButton_0" or contains(@label,"作品") or contains(@name,"作品")]' ).get(timeout=5) # 某些版本 tab.value 可能就是数量;或者 tab.label 类似 “作品 7” m = re.search(r"\d+", tab.label) num = 0 if m: # 判断当前的作品的数量 num = int(m.group()) print("作品数量为:", num) if videoCell.exists: videoCell.click() # 点击视频 print("找到主页的第一个视频") return True, num else: print("没有找到主页的第一个视频") return False, num @classmethod def clickFollow(cls, session, aid): # 1) 含“关注/已关注/Follow/Following”的首个 cell cell_xpath = ( '(//XCUIElementTypeCollectionView[@name="TTKSearchCollectionComponent"]' '//XCUIElementTypeCell[.//XCUIElementTypeButton[@name="关注" or @name="Follow" or @name="已关注" or @name="Following"]])[1]' ) cell = session.xpath(cell_xpath).get(timeout=5) # 2) 先试“用户信息 Button”(label/name 里包含 aid) profile_btn_xpath = ( f'{cell_xpath}//XCUIElementTypeButton[contains(@label, "{aid}") or contains(@name, "{aid}")]' ) try: profile_btn = session.xpath(profile_btn_xpath).get(timeout=3) profile_btn.click() except wda.WDAElementNotFoundError: # 3) 兜底:用“关注”按钮做锚点,向左偏移点击头像/用户名区域 follow_btn_xpath = ( f'{cell_xpath}//XCUIElementTypeButton[@name="关注" or @name="Follow" or @name="已关注" or @name="Following"]' ) follow_btn = session.xpath(follow_btn_xpath).get(timeout=5) rect = follow_btn.bounds left_x = max(1, rect.x - 20) center_y = rect.y + rect.height // 2 session.tap(left_x, center_y) @classmethod def userClickProfile(cls, session, aid): try: user_btn = session.xpath("(//XCUIElementTypeButton[@name='用户' and @visible='true'])[1]") if user_btn.exists: user_btn.click() time.sleep(3) follow_btn = session.xpath( "(//XCUIElementTypeTable//XCUIElementTypeButton[@name='关注' or @name='已关注'])[1]" ).get(timeout=5) if follow_btn: x, y, w, h = follow_btn.bounds # 垂直方向中心 + 随机 3~8 像素偏移 cy = int(y + h / 2 + random.randint(-8, 8)) # 横向往左偏移 80~120 像素之间的随机值 cx = int(x - random.randint(80, 120)) # 点击 session.tap(cx, cy) return True return False except Exception as e: print(e) return False @classmethod def random_micro_swipe( cls, center_x: int, center_y: int, session, points: int = 6, duration_ms: int = 15, ) -> None: """ 在 (center_x, center_y) 附近做 20px 左右的不规则微滑动。 使用 facebook-wda 的 session.swipe(x1, y1, x2, y2, duration) 接口。 """ # 1. 随机方向 angle = random.uniform(0, 2 * math.pi) length = random.uniform(18, 22) # 20px 左右 end_x = center_x + length * math.cos(angle) end_y = center_y + length * math.sin(angle) # 2. 限制在 20px 圆内(防止超出) def clamp_to_circle(x, y, cx, cy, r): dx = x - cx dy = y - cy if dx * dx + dy * dy > r * r: scale = r / math.hypot(dx, dy) x = cx + dx * scale y = cy + dy * scale return int(round(x)), int(round(y)) end_x, end_y = clamp_to_circle(end_x, end_y, center_x, center_y, 20) # 3. 加入轻微噪声,制造“不规则”曲线 noise = 3 # 最大偏移像素 mid_count = points - 2 mid_points: List[Tuple[int, int]] = [] for i in range(1, mid_count + 1): t = i / (mid_count + 1) # 线性插值 + 垂直方向噪声 x = center_x * (1 - t) + end_x * t y = center_y * (1 - t) + end_y * t perp_angle = angle + math.pi / 2 # 垂直方向 offset = random.uniform(-noise, noise) x += offset * math.cos(perp_angle) y += offset * math.sin(perp_angle) x, y = clamp_to_circle(x, y, center_x, center_y, 20) mid_points.append((int(round(x)), int(round(y)))) # 4. 构造完整轨迹 trajectory: List[Tuple[int, int]] = ( [(center_x, center_y)] + mid_points + [(end_x, end_y)] ) # 5. 使用 facebook-wda 的 swipe 接口(逐段 swipe) # 由于总时长太短,我们一次性 swipe 到终点,但用多点轨迹模拟 # facebook-wda 支持 swipe(x1, y1, x2, y2, duration) # 我们直接用起点 -> 终点,duration 用总时长 print("开始微滑动") session.swipe(center_x, center_y, end_x, end_y, duration_ms / 1000) print("随机微滑动:", trajectory)