357 lines
14 KiB
Python
357 lines
14 KiB
Python
import math
|
||
import random
|
||
import re
|
||
import time
|
||
from typing import Tuple, List
|
||
import tidevice
|
||
import wda
|
||
from wda import Client
|
||
|
||
from Entity.Variables import wdaFunctionPort
|
||
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='返回' or @label='返回上一屏幕']"
|
||
" | "
|
||
# ② 英文 / 内部 name / 图标 label 的按钮(仅限 Button,且可见)
|
||
"//XCUIElementTypeButton[@visible='true' and ("
|
||
"@name='Back' or @label='Back' or " # 英文
|
||
"@name='返回' or @label='返回' or " # 中文
|
||
"@label='返回上一屏幕' or " # 中文另一种
|
||
"@name='returnButton' 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
|
||
elif session.xpath(
|
||
"(//XCUIElementTypeOther[@y='20' and @height='44']//XCUIElementTypeButton[@visible='true'])[1]").exists:
|
||
back = session.xpath(
|
||
"(//XCUIElementTypeOther[@y='20' and @height='44']//XCUIElementTypeButton[@visible='true'])[1]")
|
||
if back.exists:
|
||
back.click()
|
||
return True
|
||
else:
|
||
return False
|
||
except Exception as e:
|
||
print(e)
|
||
return False
|
||
|
||
@classmethod
|
||
def isClickBackEnabled(cls, session: Client):
|
||
try:
|
||
|
||
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='returnButton' or"
|
||
"@name='nav_bar_start_back' or " # 内部常见 name
|
||
"(@name='TTKProfileNavBarBaseItemComponent' and @label='IconChevronLeftOffsetLTR')" # 你给的特例
|
||
")]"
|
||
)
|
||
|
||
if back.exists:
|
||
return True
|
||
elif session.xpath("//*[@name='nav_bar_start_back']").exists:
|
||
back = session.xpath("//*[@name='nav_bar_start_back']")
|
||
if back.exists:
|
||
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:
|
||
return True
|
||
elif session.xpath(
|
||
"(//XCUIElementTypeOther[@y='20' and @height='44']//XCUIElementTypeButton[@visible='true'])[1]").exists:
|
||
back = session.xpath(
|
||
"(//XCUIElementTypeOther[@y='20' and @height='44']//XCUIElementTypeButton[@visible='true'])[1]")
|
||
if back.exists:
|
||
return True
|
||
else:
|
||
return False
|
||
except Exception as e:
|
||
print(e)
|
||
return False
|
||
|
||
# 点赞
|
||
@classmethod
|
||
def clickLike(cls, session: Client, udid):
|
||
try:
|
||
from script.ScriptManager import ScriptManager
|
||
|
||
width, height, scale = ScriptManager.get_screen_info(udid)
|
||
|
||
if scale == 3.0:
|
||
x, y = AiUtils.findImageInScreen("add", udid)
|
||
if x > -1:
|
||
LogManager.method_info(f"点赞了,点赞的坐标是:{x // scale, y // scale + 50}", "关注打招呼", udid)
|
||
session.click(int(x // scale), int(y // scale + 50))
|
||
return True
|
||
else:
|
||
LogManager.method_info("没有找到目标", "关注打招呼", udid)
|
||
return False
|
||
else:
|
||
x, y = AiUtils.findImageInScreen("like1", udid)
|
||
if x > -1:
|
||
LogManager.method_info(f"点赞了,点赞的坐标是:{x // scale, y // scale}", "关注打招呼", udid)
|
||
session.click(int(x // scale), int(y // scale))
|
||
return True
|
||
else:
|
||
LogManager.method_info("没有找到目标", "关注打招呼", udid)
|
||
return False
|
||
|
||
|
||
|
||
except Exception as e:
|
||
LogManager.method_info(f"点赞出现异常,异常的原因:{e}", "关注打招呼", udid)
|
||
raise 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(
|
||
'(//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)
|
||
|
||
# 向上滑动 脚本内部使用
|
||
@classmethod
|
||
def swipe_up(cls, client):
|
||
client.swipe(200, 350, 200, 250, 0.05)
|
||
|
||
# 向下滑动,脚本内使用
|
||
@classmethod
|
||
def swipe_down(cls, udid):
|
||
dev = wda.USBClient(udid, wdaFunctionPort)
|
||
dev.swipe(200, 250, 200, 350, 0.05)
|