diff --git a/script/ScriptManager.py b/script/ScriptManager.py index 7078a70..71174e8 100644 --- a/script/ScriptManager.py +++ b/script/ScriptManager.py @@ -2,6 +2,7 @@ import random import re import threading import time +from collections import defaultdict from pathlib import Path import wda @@ -34,6 +35,7 @@ class ScriptManager(): # return cls._instance _device_cache = {} _cache_lock = threading.Lock() # 线程安全锁(可选,如果你有多线程) + _udid_locks = defaultdict(threading.Lock) @classmethod def get_screen_info(cls, udid: str): @@ -146,301 +148,359 @@ class ScriptManager(): time.sleep(1) session.tap(100, 100) + # 养号 # 养号 def growAccount(self, udid, isComment, event, is_monitoring=False): - LogManager.method_info(f"调用刷视频", "养号", udid) + LogManager.method_info("调用刷视频", "养号", udid) - # ========= 初始化 ========= - client = wda.USBClient(udid, ev.wdaFunctionPort) - session = client.session() - AiUtils.makeUdidDir(udid) + # ✅ 同一台设备同一时间只允许一个脚本跑,防止WDA并发爆炸(call depth exceed) + lock = self._udid_locks[udid] + if not lock.acquire(blocking=False): + LogManager.method_error("同一UDID已有任务在运行,拒绝重复启动", "养号", udid=udid) + return - while not event.is_set(): - try: - if not is_monitoring: - LogManager.method_info(f"开始养号,重启tiktok", "养号", udid) + try: + AiUtils.makeUdidDir(udid) + + # 外层重启循环:任何异常都会回到这里重新建 session + while not event.is_set(): + client = None + session = None + try: + # ========= 初始化(每次重启都重新创建)========= + client = wda.USBClient(udid, ev.wdaFunctionPort) + session = client.session() + + if not is_monitoring: + LogManager.method_info("开始养号,重启tiktok", "养号", udid) + ControlUtils.closeTikTok(session, udid) + event.wait(timeout=1) + ControlUtils.openTikTok(session, udid) + event.wait(timeout=3) + + recomend_cx = 0 + recomend_cy = 0 + + # ========= 主循环 ========= + while not event.is_set(): + # 设置手机的节点深度为15,判断该页面是否正确 + session.appium_settings({"snapshotMaxDepth": 15}) + + el = session.xpath( + '//XCUIElementTypeButton[@name="top_tabs_recomend" or @name="推荐" or @label="推荐"]' + ) + + # 获取推荐按钮所在的坐标 + if el.exists: + bounds = el.bounds # [x, y, width, height] + recomend_cx = bounds[0] + bounds[2] // 2 + recomend_cy = bounds[1] + bounds[3] // 2 + + if not el.exists: + LogManager.method_error("找不到推荐按钮,养号出现问题,重启养号功能", "养号", udid=udid) + raise Exception("找不到推荐按钮,养号出现问题,重启养号功能") + + if el.value != "1": + LogManager.method_error("当前页面不是推荐页面,养号出现问题,重启养号功能", "养号", udid=udid) + raise Exception("当前页面不是推荐页面,养号出现问题,重启养号功能") + + LogManager.method_info("当前页面是推荐页面,开始养号", "养号", udid=udid) + + # 重新设置节点深度,防止手机卡顿 + session.appium_settings({"snapshotMaxDepth": 0}) + + # ---- 截图保存 ---- + try: + img = client.screenshot() + base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + resource_dir = os.path.join(base_dir, "resources", udid) + os.makedirs(resource_dir, exist_ok=True) + filePath = os.path.join(resource_dir, "bgv.png") + img.save(filePath) + LogManager.method_info(f"保存屏幕图像成功 -> {filePath}", "养号", udid) + event.wait(timeout=1) + except Exception as e: + LogManager.method_info(f"截图或保存失败,失败原因:{e}", "养号", udid) + raise Exception("截图或保存失败,重启养号功能") + + # ---- 视频逻辑 ---- + try: + width, height, scale = self.get_screen_info(udid) + + if scale == 3.0: + addX, addY = AiUtils.findImageInScreen("add", udid) + else: + addX, addY = AiUtils.findImageInScreen("like1", udid) + + isSame = False + for _ in range(2): + if scale == 3.0: + tx, ty = AiUtils.findImageInScreen("add", udid) + else: + tx, ty = AiUtils.findImageInScreen("like1", udid) + + if addX == tx and addY == ty: + isSame = True + event.wait(timeout=1) + else: + isSame = False + + if addX > 0 and isSame: + needLike = random.randint(0, 100) + homeButton = AiUtils.findHomeButton(session) + if homeButton: + LogManager.method_info("有首页按钮,查看视频", "养号", udid) + videoTime = random.randint(5, 15) + + LogManager.method_info("准备停止脚本", method="task") + for _ in range(videoTime): + if event.is_set(): + LogManager.method_info("停止脚本中", method="task") + break + event.wait(timeout=1) + LogManager.method_info("停止脚本成功", method="task") + + session.appium_settings({"snapshotMaxDepth": 0}) + + if needLike < 25: + LogManager.method_info("进行点赞", "养号", udid) + ControlUtils.clickLike(session, udid) + + LogManager.method_info("继续观看视频", "养号", udid) + LogManager.method_info("准备划到下一个视频", "养号", udid) + else: + LogManager.method_error("找不到首页按钮。出错了", "养号", udid) + + if isComment and random.random() > 0.70: + self.comment_flow(filePath, session, udid, recomend_cx, recomend_cy) + event.wait(timeout=2) + + home = AiUtils.findHomeButton(session) + if not home: + raise Exception("没有找到首页按钮,重置") + + videoTime = random.randint(15, 30) + for _ in range(videoTime): + if event.is_set(): + break + event.wait(timeout=1) + + ControlUtils.swipe_up(client) + + # 如果is_monitoring 为True:检测收件箱消息 + if is_monitoring: + el = session.xpath( + '//XCUIElementTypeButton[@name="a11y_vo_inbox"]' + ' | ' + '//XCUIElementTypeButton[contains(@name,"收件箱")]' + ' | ' + '//XCUIElementTypeButton[.//XCUIElementTypeStaticText[@value="收件箱"]]' + ) + + if el.exists: + m = None + try: + m = re.search(r"(\d+)", (el.label or "")) + except Exception as e: + LogManager.method_error(f"解析收件箱数量异常: {e}", "检测消息", udid) + + count = int(m.group(1)) if m else 0 + if count: + break + else: + continue + + except Exception as e: + print("刷视频脚本有错误:错误内容:", e) + LogManager.method_error("刷视频过程出现错误,重试", "养号", udid) + # 抛出给上层,触发重启(外层 while 会重建 session) + raise + + except Exception as e: + print("刷视频遇到错误了。错误内容:", e) + LogManager.method_error(f"[{udid}] 养号出现异常,将重启流程: {e}", "养号", udid) + + # ✅ 关键:这里等待后回到外层 while,重新创建client/session + event.wait(timeout=3) + + finally: + # 尽量释放 session(不同wda版本不一定有close) + try: + if session is not None: + session.close() + except Exception: + pass + + finally: + lock.release() + + # 观看直播 + def watchLiveForGrowth(self, udid, event, max_retries=None): + import random, wda + + retry_count = 0 + backoff_sec = 10 + + # ✅ 同一台设备同一时间只允许一个任务跑 + lock = self._udid_locks[udid] + if not lock.acquire(blocking=False): + LogManager.method_error("同一UDID已有任务在运行,拒绝重复启动", "直播养号", udid=udid) + return + + try: + # 外层重启循环:任何异常都会回到这里重新建 client/session + while not event.is_set(): + + client = None + session = None + try: + # —— 每次重启都新建 client/session —— + client = wda.USBClient(udid, ev.wdaFunctionPort) + session = client.session() + + # 1) 先关再开 ControlUtils.closeTikTok(session, udid) event.wait(timeout=1) ControlUtils.openTikTok(session, udid) event.wait(timeout=3) - recomend_cx = 0 - recomend_cy = 0 - # ========= 主循环 ========= - while not event.is_set(): - # 设置手机的节点深度为15,判断该页面是否正确 + # 2) 进入直播按钮(中英文都匹配) session.appium_settings({"snapshotMaxDepth": 15}) - el = session.xpath( - '//XCUIElementTypeButton[@name="top_tabs_recomend" or @name="推荐" or @label="推荐"]' + + live_button = session.xpath( + '//XCUIElementTypeButton[@name="LIVE" or @label="LIVE" or @name="直播" or @label="直播"]' + ' | ' + '//XCUIElementTypeOther[@name="LIVE" or @label="LIVE" or @name="直播" or @label="直播"]' ) - # 获取推荐按钮所在的坐标 - if el.exists: - bounds = el.bounds # 返回 [x, y, width, height] - recomend_cx = bounds[0] + bounds[2] // 2 - recomend_cy = bounds[1] + bounds[3] // 2 - if not el.exists: - # 记录日志 - LogManager.method_error("找不到推荐按钮,养号出现问题,重启养号功能", "养号", udid=udid) - # 手动的抛出异常 重启流程 - raise Exception("找不到推荐按钮,养号出现问题,重启养号功能") - - if el.value != "1": - LogManager.method_error("当前页面不是推荐页面,养号出现问题,重启养号功能", "养号", udid=udid) - raise Exception("当前页面不是推荐页面,养号出现问题,重启养号功能") - - LogManager.method_info("当前页面是推荐页面,开始养号", "养号", udid=udid) - # 重新设置节点的深度,防止手机进行卡顿 - session.appium_settings({"snapshotMaxDepth": 0}) - - # ---- 截图保存 ---- - try: - img = client.screenshot() - base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) - resource_dir = os.path.join(base_dir, "resources", udid) - os.makedirs(resource_dir, exist_ok=True) - filePath = os.path.join(resource_dir, "bgv.png") - img.save(filePath) - LogManager.method_info(f"保存屏幕图像成功 -> {filePath}", "养号", udid) - print("保存了背景图:", filePath) - event.wait(timeout=1) - except Exception as e: - LogManager.method_info(f"截图或保存失败,失败原因:{e}", "养号", udid) - raise Exception("截图或保存失败,重启养号功能") - - # ---- 视频逻辑 ---- - try: - width, height, scale = self.get_screen_info(udid) - if scale == 3.0: - addX, addY = AiUtils.findImageInScreen("add", udid) - else: - addX, addY = AiUtils.findImageInScreen("like1", udid) - - isSame = False - for i in range(2): - if scale == 3.0: - tx, ty = AiUtils.findImageInScreen("add", udid) - else: - tx, ty = AiUtils.findImageInScreen("like1", udid) - - if addX == tx and addY == ty: - isSame = True - event.wait(timeout=1) - else: - isSame = False - # break - - if addX > 0 and isSame: - needLike = random.randint(0, 100) - homeButton = AiUtils.findHomeButton(session) - if homeButton: - LogManager.method_info("有首页按钮,查看视频", "养号", udid) - videoTime = random.randint(5, 15) - LogManager.method_info("准备停止脚本", method="task") - for _ in range(videoTime): # 0.2 秒一片 - if event.is_set(): - LogManager.method_info("停止脚本中", method="task") - break - event.wait(timeout=1) - LogManager.method_info("停止脚本成功", method="task") - - # 重置 session - session.appium_settings({"snapshotMaxDepth": 0}) - - if needLike < 25: - LogManager.method_info("进行点赞", "养号", udid) - ControlUtils.clickLike(session, udid) - LogManager.method_info("继续观看视频", "养号", udid) - LogManager.method_info("准备划到下一个视频", "养号", udid) - else: - LogManager.method_error("找不到首页按钮。出错了", "养号", udid) - - if isComment and random.random() > 0.70: - self.comment_flow(filePath, session, udid, recomend_cx, recomend_cy) - event.wait(timeout=2) - - home = AiUtils.findHomeButton(session) - if not home: - raise Exception("没有找到首页按钮,重置") - - videoTime = random.randint(15, 30) - for _ in range(videoTime): - if event.is_set(): - break - event.wait(timeout=1) - - ControlUtils.swipe_up(client) - - # 如果is_monitoring 为False 则说明和监控消息没有联动,反正 则证明和监控消息进行联动 - if is_monitoring: - # 监控消息按钮,判断是否有消息 - el = session.xpath( - '//XCUIElementTypeButton[@name="a11y_vo_inbox"]' - ' | ' - '//XCUIElementTypeButton[contains(@name,"收件箱")]' - ' | ' - '//XCUIElementTypeButton[.//XCUIElementTypeStaticText[@value="收件箱"]]' - ) - # 判断收件箱是否有消息 - if el.exists: - try: - m = re.search(r'(\d+)', el.label) # 抓到的第一个数字串 - except Exception as e: - LogManager.method_error(f"解析收件箱数量异常: {e}", "检测消息", udid) - - count = int(m.group(1)) if m else 0 - if count: - break - else: - continue - - except Exception as e: - print("刷视频脚本有错误:错误内容:", e) - LogManager.method_error(f"刷视频过程出现错误,重试", "养号", udid) - raise e # 抛出给上层,触发重生机制 - - except Exception as e: - print("刷视频遇到错误了。错误内容:", e) - LogManager.method_error(f"[{udid}] 养号出现异常,将重启流程: {e}", "养号", udid) - event.wait(timeout=3) - - # 观看直播 - def watchLiveForGrowth(self, udid, event, max_retries=None): - import random, wda - retry_count = 0 - backoff_sec = 10 - - # —— 每次重启都新建 client/session —— - client = wda.USBClient(udid, ev.wdaFunctionPort) - session = client.session() - - while not event.is_set(): - try: - - # 1) 先关再开 - ControlUtils.closeTikTok(session, udid) - event.wait(timeout=1) - ControlUtils.openTikTok(session, udid) - event.wait(timeout=3) - - session.appium_settings({"snapshotMaxDepth": 15}) - # 2) 进入直播 (使用英文) - live_button = session( - xpath='//XCUIElementTypeButton[@name="LIVE" or @label="LIVE" or @name="直播" or @label="直播"] ' - '| //XCUIElementTypeOther[@name="LIVE" or @label="LIVE" or @name="直播" or @label="直播"]' - ) - - if live_button.exists: - live_button.click() - else: - LogManager.method_error("无法找到直播间按钮 抛出异常 重新启动", "直播养号", udid) - # 抛出异常 - raise Exception(f"找不到直播按钮,抛出异常 重新启动") - waitTime = random.randint(15, 20) - for _ in range(waitTime): # 0.2 秒一片 - if event.is_set(): - break - event.wait(timeout=1) - - # live_button = session(xpath='//XCUIElementTypeButton[@name="直播"]') - live_button = session( - xpath='//XCUIElementTypeButton[@name="LIVE" or @label="LIVE" or @name="直播" or @label="直播"] ' - '| //XCUIElementTypeOther[@name="LIVE" or @label="LIVE" or @name="直播" or @label="直播"]' - ) - - if live_button.exists: - continue - - # 下滑一下 - ControlUtils.swipe_up(client) - - # 3) 取分辨率;可选重建 session 规避句柄陈旧 - size = session.window_size() - width, height = size.width, size.height - - # 4) 主循环:刷直播 - while not event.is_set(): - event.wait(timeout=3) - - # 找到一个看直播的时候肯定有的元素,当这个元素没有的时候,就代表当前的页面出现了问题 - # 需要抛出异常,重启这个流程 - el1 = session.xpath('//XCUIElementTypeOther[@name="GBLFeedRootViewComponent"]') - el2 = session.xpath('//XCUIElementTypeOther[@value="0%"]') - - if not (el1.exists or el2.exists): - print("当前页面不是直播间,重启刷直播") - LogManager.method_error("当前页面不是直播间,重启刷直播", "直播养号", udid=udid) - raise Exception("当前页面不是直播间") + if live_button.exists: + live_button.click() else: - print("当前页面是直播间,继续刷直播") - LogManager.method_info("当前页面是直播间,继续刷直播", "直播养号", udid=udid) + LogManager.method_error("无法找到直播间按钮,重启流程", "直播养号", udid=udid) + raise Exception("找不到直播按钮") - # PK 直接划走 - if session(xpath='//XCUIElementTypeOther[@name="kGBLInteractionViewMatchScoreBar"]').exists: - print("✅ 当前是 PK,跳过") - LogManager.method_info("✅ 当前是 PK,跳过", "直播养号", udid=udid) - ControlUtils.swipe_up(client) - continue - - # 计算直播显示窗口数量(主画面+连麦小窗) - count = AiUtils.count_add_by_xml(session) - print(f"检测到直播显示区域窗口数:{count}") - - if count > 1: - print("❌ 多窗口(有人连麦/分屏),划走") - LogManager.method_info("❌ 多窗口(有人连麦/分屏),划走", "直播养号", udid=udid) - ControlUtils.swipe_up(client) - continue - else: - print("✅ 单窗口,(10%概率)开始点赞") - LogManager.method_info("✅ 单窗口,(3%概率)开始点赞", "直播养号", udid=udid) - - # 随机点赞(仍保留中途保护) - if random.random() >= 0.97: - print("开始点赞") - LogManager.method_info("开始点赞", "直播养号", udid=udid) - for _ in range(random.randint(10, 30)): - # 中途转PK/连麦立即跳过 - if session(xpath='//XCUIElementTypeOther[@name="kGBLInteractionViewMatchScoreBar"]').exists \ - or AiUtils.count_add_by_xml(session) > 1: - print("❗ 中途发生 PK/连麦,跳过") - LogManager.method_info("❗ 中途发生 PK/连麦,跳过", "直播养号", udid=udid) - ControlUtils.swipe_up(client) - break - x = width // 3 + random.randint(-10, 10) - y = height // 3 + random.randint(10, 20) - print("双击坐标:", x, y) - - session.double_tap(x, y) - - print("--------------------------------------------") - # 换成 - total_seconds = random.randint(300, 600) - for _ in range(total_seconds): # 0.2 秒一片 + # 进入后稍等 + wait_time = random.randint(15, 20) + for _ in range(wait_time): if event.is_set(): break event.wait(timeout=1) + # ✅ 恢复深度(避免一直高深度导致卡顿/递归) + session.appium_settings({"snapshotMaxDepth": 0}) + + # 如果还在直播入口页(说明没进去),继续主流程重来 + live_button2 = session.xpath( + '//XCUIElementTypeButton[@name="LIVE" or @label="LIVE" or @name="直播" or @label="直播"]' + ' | ' + '//XCUIElementTypeOther[@name="LIVE" or @label="LIVE" or @name="直播" or @label="直播"]' + ) + if live_button2.exists: + continue + + # 下滑一下进入直播流 ControlUtils.swipe_up(client) - # 正常退出(外部 event 触发) - break + # 取分辨率 + size = session.window_size() + width, height = size.width, size.height - except Exception as e: - retry_count += 1 - LogManager.method_error(f"watchLiveForGrowth 异常(第{retry_count}次):{repr(e)}", "直播养号", udid) - # 尝试轻量恢复一次,避免一些短暂性 session 失效 - try: - client = wda.USBClient(udid, ev.wdaFunctionPort) - _ = client.session() - except Exception: - pass - # 冷却后整段流程重来 - for _ in range(backoff_sec): # 0.2 秒一片 - if event.is_set(): + # 4) 主循环:刷直播 + while not event.is_set(): + + # 小停顿 + event.wait(timeout=3) + + # 找一个直播间必存在的元素,用来判断是否还在直播间 + el1 = session.xpath('//XCUIElementTypeOther[@name="GBLFeedRootViewComponent"]') + el2 = session.xpath('//XCUIElementTypeOther[@value="0%"]') + + if not (el1.exists or el2.exists): + LogManager.method_error("当前页面不是直播间,重启刷直播", "直播养号", udid=udid) + raise Exception("当前页面不是直播间") + else: + LogManager.method_info("当前页面是直播间,继续刷直播", "直播养号", udid=udid) + + # PK 直接划走 + if session.xpath('//XCUIElementTypeOther[@name="kGBLInteractionViewMatchScoreBar"]').exists: + LogManager.method_info("✅ 当前是 PK,跳过", "直播养号", udid=udid) + ControlUtils.swipe_up(client) + continue + + # 计算直播显示窗口数量(主画面+连麦小窗) + count = AiUtils.count_add_by_xml(session) + LogManager.method_info(f"检测到直播显示区域窗口数:{count}", "直播养号", udid=udid) + + if count > 1: + LogManager.method_info("❌ 多窗口(有人连麦/分屏),划走", "直播养号", udid=udid) + ControlUtils.swipe_up(client) + continue + + # 随机点赞:约 3% 概率触发 + if random.random() >= 0.97: + LogManager.method_info("✅ 单窗口,开始随机点赞", "直播养号", udid=udid) + like_times = random.randint(10, 30) + for _ in range(like_times): + if event.is_set(): + break + + # 中途转PK/连麦立即跳过 + is_pk = session.xpath( + '//XCUIElementTypeOther[@name="kGBLInteractionViewMatchScoreBar"]').exists + multi = AiUtils.count_add_by_xml(session) > 1 + if is_pk or multi: + LogManager.method_info("❗ 中途发生 PK/连麦,跳过", "直播养号", udid=udid) + ControlUtils.swipe_up(client) + break + + x = width // 3 + random.randint(-10, 10) + y = height // 3 + random.randint(10, 20) + session.double_tap(x, y) + event.wait(timeout=random.uniform(0.3, 0.8)) + + # 停留观看 5~10 分钟 + total_seconds = random.randint(300, 600) + for _ in range(total_seconds): + if event.is_set(): + break + event.wait(timeout=1) + + # 下一场 + ControlUtils.swipe_up(client) + + # 正常退出(外部 event 触发) + break + + except Exception as e: + retry_count += 1 + LogManager.method_error( + f"watchLiveForGrowth 异常(第{retry_count}次):{repr(e)}", + "直播养号", + udid + ) + + # 达到最大重试次数则退出(如果你传了 max_retries) + if max_retries is not None and retry_count >= max_retries: + LogManager.method_error("达到最大重试次数,结束直播养号", "直播养号", udid) break - event.wait(timeout=1) - continue + + # 冷却后整段流程重来(外层 while 会重建 session) + for _ in range(backoff_sec): + if event.is_set(): + break + event.wait(timeout=1) + continue + + finally: + # 尽量释放 session(不同wda版本不一定有close) + try: + if session is not None: + session.close() + except Exception: + pass + + finally: + lock.release() """ 外层包装,出现异常自动重试、