diff --git a/.idea/git_toolbox_blame.xml b/.idea/git_toolbox_blame.xml new file mode 100644 index 0000000..7dc1249 --- /dev/null +++ b/.idea/git_toolbox_blame.xml @@ -0,0 +1,6 @@ + + + + + \ No newline at end of file diff --git a/.idea/iOSAI.iml b/.idea/iOSAI.iml index f571432..df5cbff 100644 --- a/.idea/iOSAI.iml +++ b/.idea/iOSAI.iml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/.idea/jsLibraryMappings.xml b/.idea/jsLibraryMappings.xml new file mode 100644 index 0000000..d23208f --- /dev/null +++ b/.idea/jsLibraryMappings.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index db8786c..c27b771 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -3,5 +3,5 @@ - + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index c2e7dd9..874684b 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,15 +4,45 @@ - + + + + + + + + + + + + + + + + + + + + + + @@ -28,27 +58,55 @@ - { - "keyToString": { - "Python.Main.executor": "Run", - "RunOnceActivity.ShowReadmeOnStart": "true", - "git-widget-placeholder": "main", - "last_opened_file_path": "E:/Code/python/iOSAI/Module/Main.py", - "node.js.detected.package.eslint": "true", - "node.js.detected.package.tslint": "true", - "node.js.selected.package.eslint": "(autodetect)", - "node.js.selected.package.tslint": "(autodetect)", - "nodejs_package_manager_path": "npm", - "vue.rearranger.settings.migration": "true" + +}]]> - + + + + + + + + + @@ -103,6 +165,16 @@ + + + + + + + + + + @@ -113,6 +185,7 @@ - + + \ No newline at end of file diff --git a/Entity/Variables.py b/Entity/Variables.py index b297ef1..2ba3109 100644 --- a/Entity/Variables.py +++ b/Entity/Variables.py @@ -10,7 +10,7 @@ anchorList: list[AnchorModel] = [] # 线程锁 anchorListLock = threading.Lock() # 打招呼数据 -prologueList = [] +prologueList = ["hello"] # 评论列表 commentsList = [] # 存储主播名和session_id的字典 diff --git a/Module/FlaskService.py b/Module/FlaskService.py index 30053d3..605bb75 100644 --- a/Module/FlaskService.py +++ b/Module/FlaskService.py @@ -93,6 +93,7 @@ def passToken(): print(e) return ResultData(data="").toJson() + # 获取设备列表 @app.route('/deviceList', methods=['GET']) def deviceList(): @@ -112,6 +113,7 @@ def deviceList(): LogManager.error("获取设备列表失败:", e) return ResultData(data=[]).toJson() + # 获取设备应用列表 @app.route('/deviceAppList', methods=['POST']) def deviceAppList(): @@ -230,11 +232,14 @@ def stopScript(): return ResultData(code=code, data="", msg=msg).toJson() -# 传递主播数据 +# 传递主播数据(关注主播打招呼) @app.route('/passAnchorData', methods=['POST']) def passAnchorData(): data: Dict[str, Any] = request.get_json() # 设备列表 + + print("接收的数据", data) + idList = data.get("deviceList", []) # 主播列表 acList = data.get("anchorList", []) @@ -247,7 +252,8 @@ def passAnchorData(): manager = ScriptManager() event = threading.Event() # 启动脚本 - thread = threading.Thread(target=manager.greetNewFollowers, args=(udid, needReply, event)) + # thread = threading.Thread(target=manager.greetNewFollowers, args=(udid, needReply, event)) + thread = threading.Thread(target=manager.safe_greetNewFollowers, args=(udid, needReply, event)) thread.start() # 添加到线程管理 ThreadManager.add(udid, thread, event) @@ -270,8 +276,24 @@ def getChatTextInfo(): client = wda.USBClient(udid) session = client.session() xml = session.source() - result = AiUtils.extract_messages_from_xml(xml) - return ResultData(data=result).toJson() + try: + result = AiUtils.extract_messages_from_xml(xml) + print(result) + return ResultData(data=result).toJson() + except Exception as e: + data = [ + { + 'type': 'msg', + 'dir': 'in', + 'text': '当前页面无法获取聊天记录,请在tiktok聊天页面进行获取!!!' + }, + { + 'type': 'msg', + 'dir': 'in', + 'text': 'Unable to retrieve chat messages on the current screen. Please navigate to the TikTok chat page and try again!!!' + } + ] + return ResultData(data=data, msg="解析失败").toJson() # 监控消息 diff --git a/Module/Main.py b/Module/Main.py index 5999b17..fc17903 100644 --- a/Module/Main.py +++ b/Module/Main.py @@ -27,7 +27,7 @@ if "--role=flask" in sys.argv: # 项目入口 if __name__ == "__main__": # 清空日志等 - LogManager.clearLogs() + # LogManager.clearLogs() # 启动 Flask 子进程 manager = FlaskSubprocessManager.get_instance() diff --git a/Utils/AiUtils.py b/Utils/AiUtils.py index ad52ebf..4ea7541 100644 --- a/Utils/AiUtils.py +++ b/Utils/AiUtils.py @@ -200,7 +200,7 @@ class AiUtils(object): homeButton = session.xpath("//*[@label='首页']") try: if homeButton.label == "首页": - print("找到了") + print("1.找到了") return homeButton else: print("没找到") @@ -243,7 +243,7 @@ class AiUtils(object): def getFollowButton(cls, session: Client): followButton = session.xpath("//Window[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[2]/Other[2]/Other[1]/Other[1]/Other[3]/Other[1]/Other[1]/Button[1]") if followButton.exists: - print("找到了") + print("2.找到了") return followButton else: print("没找到") @@ -254,7 +254,7 @@ class AiUtils(object): def getSendMesageButton(cls, session: Client): msgButton = session.xpath("//Window[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[2]/Other[2]/Other[1]/Other[1]/Other[3]/Other[1]/Other[1]") if msgButton.exists: - print("找到了") + print("3.找到了") return msgButton else: print("没找到") @@ -294,6 +294,16 @@ class AiUtils(object): root = etree.fromstring(xml.encode("utf-8")) items = [] + # 判断是否是聊天页面 + is_chat_page = False + if root.xpath('//XCUIElementTypeStaticText[contains(@traits, "Header")]'): + is_chat_page = True + elif root.xpath('//XCUIElementTypeCell//XCUIElementTypeOther[@name or @label]'): + is_chat_page = True + + if not is_chat_page: + raise Exception("请先进入聊天页面") + # 屏幕宽度 app = root.xpath('/XCUIElementTypeApplication') screen_w = cls.parse_float(app[0], 'width', 414.0) if app else 414.0 diff --git a/Utils/ControlUtils.py b/Utils/ControlUtils.py index 9989c21..df64ad4 100644 --- a/Utils/ControlUtils.py +++ b/Utils/ControlUtils.py @@ -1,8 +1,11 @@ +import re + import tidevice from wda import Client from Utils.AiUtils import AiUtils from Utils.LogManager import LogManager + # 页面控制工具类 class ControlUtils(object): @@ -59,8 +62,10 @@ class ControlUtils(object): back = session.xpath("//*[@name='nav_bar_start_back']") 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]") + 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]") back.click() return True else: @@ -73,7 +78,7 @@ class ControlUtils(object): @classmethod def clickLike(cls, session: Client, udid): scale = session.scale - x, y = AiUtils.findImageInScreen("add",udid) + x, y = AiUtils.findImageInScreen("add", udid) print(x, y) if x > -1: print("点赞了") @@ -108,13 +113,29 @@ class ControlUtils(object): # 获取主播详情页的第一个视频 @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]') - if videoCell.exists: + # 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]').get( + timeout=5) + + tab = session.xpath('//XCUIElementTypeButton[@name="TTKProfileTabVideoButton_0"]').get(timeout=2) + # 某些版本 tab.value 可能就是数量;或者 tab.label 类似 “作品 7” + m = re.search(r"\d+", tab.label) + + num = 0 + + if m: + # 判断当前的作品的数量 + num = int(m.group()) + print("作品数量为:", num) + + if videoCell is not None: videoCell.click() # 点击视频 - return True + print("找到主页的第一个视频") + return True, num else: - return False - - - + print("没有找到主页的第一个视频") + return False, num diff --git a/Utils/Requester.py b/Utils/Requester.py index f628acf..6a13702 100644 --- a/Utils/Requester.py +++ b/Utils/Requester.py @@ -1,5 +1,5 @@ import requests -from Entity.Variables import prologueList +from Entity.Variables import prologueList BaseUrl = "https://crawlclient.api.yolozs.com/api/common/" # BaseUrl = "http://192.168.1.174:8101/api/common/" @@ -17,9 +17,12 @@ class Requester(): url = BaseUrl + cls.prologue result = requests.get(headers=headers, url=url) json = result.json() + print("json",json + ) data = json.get("data") + print("返回的数据",data) for i in data: - prologueList.append(i) + prologueList.append("hello") # 翻译 diff --git a/resources/03cb0d61638ab919e9c9c5e016b969ff40925b0c/bgv.png b/resources/03cb0d61638ab919e9c9c5e016b969ff40925b0c/bgv.png new file mode 100644 index 0000000..48631f4 Binary files /dev/null and b/resources/03cb0d61638ab919e9c9c5e016b969ff40925b0c/bgv.png differ diff --git a/resources/833c034d29ee6b79e1dfd88dc1d454f3da1e8a3d/bgv.png b/resources/833c034d29ee6b79e1dfd88dc1d454f3da1e8a3d/bgv.png new file mode 100644 index 0000000..31adfbf Binary files /dev/null and b/resources/833c034d29ee6b79e1dfd88dc1d454f3da1e8a3d/bgv.png differ diff --git a/script/ScriptManager.py b/script/ScriptManager.py index fa28815..42bb3a3 100644 --- a/script/ScriptManager.py +++ b/script/ScriptManager.py @@ -29,96 +29,125 @@ class ScriptManager(): self.initialized = True # 标记已初始化 # 养号 - def growAccount(self, udid, event): - client = wda.USBClient(udid) - session = client.session() - session.appium_settings({"snapshotMaxDepth": 0}) + def growAccount(self, udid, event, max_retries=None): + """ + 自动养号方法,带重生机制 + :param udid: 设备 UDID + :param event: 线程控制 Event + :param max_retries: 最大重试次数 (None 表示无限重试) + """ + retries = 10 - # 先关闭Tik Tok - ControlUtils.closeTikTok(session, udid) - time.sleep(1) - - # 重新打开Tik Tok - ControlUtils.openTikTok(session, udid) - time.sleep(3) - - # 创建udid名称的目录 - AiUtils.makeUdidDir(udid) - - # 假设此时有个开关 while not event.is_set(): try: - img = client.screenshot() - filePath = f"resources/{udid}/bgv.png" - img.save(filePath) - LogManager.info("保存屏幕图像成功", udid) - print("保存了背景图") + # ========= 初始化 ========= + client = wda.USBClient(udid) + session = client.session() + + # 关闭并重新打开 TikTok + ControlUtils.closeTikTok(session, udid) time.sleep(1) - except Exception as e: - LogManager.error(e, udid) - print(e) + ControlUtils.openTikTok(session, udid) + time.sleep(3) - try: - # 判断视频类型 - addX, addY = AiUtils.findImageInScreen("add", udid) - # 多次获取结果是否一致,如果有不一致的结果就切换视频 - isSame = False - for i in range(2): - tx, ty = AiUtils.findImageInScreen("add", udid) - if addX == tx and addY == ty: - isSame = True + AiUtils.makeUdidDir(udid) + + # ========= 主循环 ========= + while not event.is_set(): + + # 设置手机的节点深度为15,判断该页面是否正确 + session.appium_settings({"snapshotMaxDepth": 15}) + + # 判断当前页面上是否有推荐按钮 + el = session(xpath='//XCUIElementTypeButton[@name="top_tabs_recomend"]') + if not el.exists: + # 记录日志 + LogManager.error("找不到推荐按钮,养号出现问题,重启养号功能", udid=udid) + # 手动的抛出异常 重启流程 + raise Exception("找不到推荐按钮,养号出现问题,重启养号功能") + + if el.value != "1": + LogManager.error("当前页面不是推荐页面,养号出现问题,重启养号功能", udid=udid) + raise Exception("当前页面不是推荐页面,养号出现问题,重启养号功能") + + # 重新设置节点的深度,防止手机进行卡顿 + 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.info(f"保存屏幕图像成功 -> {filePath}", udid) + print("保存了背景图:", filePath) time.sleep(1) - else: + except Exception as e: + LogManager.error(f"截图或保存失败,失败原因:{e}", udid) + raise Exception("截图或保存失败,重启养号功能") + + # ---- 视频逻辑 ---- + try: + addX, addY = AiUtils.findImageInScreen("add", udid) isSame = False - break + for i in range(2): + tx, ty = AiUtils.findImageInScreen("add", udid) + if addX == tx and addY == ty: + isSame = True + time.sleep(1) + else: + isSame = False + break - # 如果找到普通视频 - if addX > 0 and isSame: - needLike = random.randint(0, 100) - # 查找首页按钮 - homeButton = AiUtils.findHomeButton(udid) - if homeButton: - print("有首页按钮,查看视频") - videoTime = random.randint(5, 15) - time.sleep(videoTime) - # 点赞之前重置我wda会话 防止会话失效 - client = wda.USBClient(udid) - session = client.session() - session.appium_settings({"snapshotMaxDepth": 0}) + if addX > 0 and isSame: + needLike = random.randint(0, 100) + homeButton = AiUtils.findHomeButton(udid) + if homeButton: + print("有首页按钮,查看视频") + videoTime = random.randint(5, 15) + time.sleep(videoTime) - # 百分之三的概率点赞 - if needLike < 3: - print("点赞") - ControlUtils.clickLike(session, udid) + # 重置 session + client = wda.USBClient(udid) + session = client.session() + session.appium_settings({"snapshotMaxDepth": 0}) + + if needLike < 3: + print("点赞") + ControlUtils.clickLike(session, udid) + + print("继续观看视频") + videoTime = random.randint(10, 30) + time.sleep(videoTime) + print("准备划到下一个视频") + client.swipe_up() + else: + print("找不到首页按钮。出错了") + else: + nextTime = random.randint(1, 5) + time.sleep(nextTime) + client.swipe_up() + + except Exception as e: + LogManager.error(f"刷视频过程出现错误,重试", udid) + raise e # 抛出给上层,触发重生机制 - print("继续观看视频") - videoTime = random.randint(10, 30) - time.sleep(videoTime) - print("准备划到下一个视频") - client.swipe_up() - else: - print("找不到首页按钮。出错了") - else: - nextTime = random.randint(1, 5) - time.sleep(nextTime) - client.swipe_up() except Exception as e: - print(f"发生异常:{e}") - client.swipe_up() + LogManager.error(f"养号出现异常: {e}", udid) + print(f"[{udid}] 养号出现异常,将重启流程: {e}") + retries += 1 + time.sleep(3) # 等待后重生 # 观看直播 def watchLiveForGrowth(self, udid, event, max_retries=None): import time, random, wda retry_count = 0 - backoff_sec = 100 # 异常后冷却,避免频繁重启 + backoff_sec = 10 # 异常后冷却,避免频繁重启 while not event.is_set(): - - if max_retries is not None and retry_count >= max_retries: - LogManager.error(f"达到最大重试次数,停止任务。retries={retry_count}", udid) - break - try: # —— 每次重启都新建 client/session —— client = wda.USBClient(udid) @@ -154,6 +183,9 @@ class ScriptManager(): # 找到一个看直播的时候肯定有的元素,当这个元素没有的时候,就代表当前的页面出现了问题 # 需要抛出异常,重启这个流程 + if not session(xpath="//XCUIElementTypeOther[@name='RoomContainer' and @visible='true']").exists: + LogManager.error("当前页面不是直播间,重启刷直播", udid=udid) + raise Exception("当前页面不是直播间") # PK 直接划走 if session(xpath='//XCUIElementTypeOther[@name="kGBLInteractionViewMatchScoreBar"]').exists: @@ -206,11 +238,231 @@ class ScriptManager(): time.sleep(backoff_sec) # 冷却后整段流程重来 continue + # # 关注打招呼以及回复主播消息 + # def greetNewFollowers(self, udid, needReply, event): + # client = wda.USBClient(udid) + # session = client.session() + # print(f"是否要自动回复消息:{needReply}") + # # 先关闭Tik Tok + # ControlUtils.closeTikTok(session, udid) + # time.sleep(1) + # + # # 重新打开Tik Tok + # ControlUtils.openTikTok(session, udid) + # time.sleep(3) + # + # # 设置查找深度 + # session.appium_settings({"snapshotMaxDepth": 15}) + # # 点击搜索按钮 + # ControlUtils.clickSearch(session) + # + # # 创建udid名称的目录 + # AiUtils.makeUdidDir(udid) + # + # # 返回上一步 + # def goBack(count): + # for i in range(count): + # ControlUtils.clickBack(session) + # time.sleep(2) + # + # # 循环条件。1、 循环关闭 2、 数据处理完毕 + # while not event.is_set() or len(anchorList) > 0: + # + # # 查找输入框 + # input = session.xpath('//XCUIElementTypeSearchField') + # # 如果找到了输入框,就点击并且输入内容 + # input.click() + # # 稍作停顿 + # time.sleep(1) + # + # # 获取一个主播 + # anchor = anchorList[0] + # aid = anchor.anchorId + # anchorCountry = anchor.country + # + # input.clear_text() + # time.sleep(2) + # # 输入主播id + # input.set_text(aid + "\n") + # + # # 切换UI查找深度 + # session.appium_settings({"snapshotMaxDepth": 25}) + # # 定位 "关注" 按钮 通过关注按钮的位置点击主播首页 + # follow_button = session.xpath("//XCUIElementTypeButton[@traits='Button' and @index='1']") + # time.sleep(2) + # if follow_button.exists: + # print(follow_button.bounds) + # # session.appium_settings({"snapshotMaxDepth": 10}) + # print("找到关注按钮!") + # x = follow_button.bounds.x - 100 + # y = follow_button.bounds.y + # print(x, y) + # client.click(x, y) + # print("进入主播首页啦") + # else: + # goBack(1) + # removeModelFromAnchorList(anchor) + # print("未找到关注按钮") + # continue + # time.sleep(3) + # + # session.appium_settings({"snapshotMaxDepth": 25}) + # time.sleep(2) + # + # # 找到并点击第一个视频 + # cellClickResult = ControlUtils.clickFirstVideoFromDetailPage(session) + # time.sleep(2) + # + # # 观看主播视频 + # def viewAnchorVideo(): + # print("开始查看视频,并且重新调整查询深度") + # session.appium_settings({"snapshotMaxDepth": 5}) + # count = 3 + # while count != 0: + # time.sleep(5) + # img = client.screenshot() + # time.sleep(1) + # filePath = f"resources/{udid}/bgv.png" + # img.save(filePath) + # LogManager.info("保存屏幕图像成功", udid) + # time.sleep(2) + # # 查找add图标 + # r = ControlUtils.clickLike(session, udid) + # # 点赞成功。 + # if r == True: + # count -= 1 + # + # # 假装看几秒视频 + # time.sleep(5) + # if count != 0: + # client.swipe_up() + # + # # 右滑返回 + # client.swipe_right() + # + # # 如果打开视频失败。说明该主播没有视频 + # if cellClickResult == True: + # # 观看主播视频 + # LogManager.info("去查看主播视频", udid) + # viewAnchorVideo() + # time.sleep(3) + # LogManager.info("视频看完了,重置试图查询深度", udid) + # session.appium_settings({"snapshotMaxDepth": 25}) + # # 点击关注按钮 + # followButton = AiUtils.getFollowButton(session) + # if followButton is not None: + # LogManager.info("找到关注按钮了", udid) + # followButton.click() + # else: + # LogManager.info("没找到关注按钮", udid) + # removeModelFromAnchorList(anchor) + # goBack(3) + # continue + # + # time.sleep(2) + # msgButton = AiUtils.getSendMesageButton(session) + # time.sleep(2) + # if msgButton is not None: + # print("找到发消息按钮了") + # # 进入聊天页面 + # msgButton.click() + # else: + # print("没有识别出发消息按钮") + # removeModelFromAnchorList(anchor) + # goBack(3) + # continue + # + # time.sleep(3) + # # 查找聊天界面中的输入框节点 + # chatInput = session.xpath("//TextView") + # if chatInput.exists: + # print("找到输入框了, 准备发送一条打招呼消息") + # # 准备打招呼的文案 + # text = random.choice(prologueList) + # # 翻译成主播国家的语言 + # msg = Requester.translation(text, anchorCountry) + # # 准备发送一条信息 + # chatInput.click() + # time.sleep(2) + # # 发送消息 + # chatInput.set_text(msg + "\n") + # time.sleep(1) + # else: + # print("无法发送信息") + # LogManager.error(f"给主播{anchor.anchorId} 发送消息失败", udid) + # + # # 接着下一个主播 + # removeModelFromAnchorList(anchor) + # goBack(4) + # + # else: + # print(f"{anchor.anchorId}:该主播没有视频") + # # 删除当前数据 + # removeModelFromAnchorList(anchor) + # goBack(3) + # continue + # + # # 设置查找深度 + # session.appium_settings({"snapshotMaxDepth": 15}) + # time.sleep(2) + # + # print("即将要回复消息") + # if needReply: + # print("如果需要回复主播消息。走此逻辑") + # if AiUtils.getUnReadMsgCount(session) > 0: + # + # print("监控回复消息") + # # 执行回复消息逻辑 + # self.monitorMessages(session, udid) + # homeButton = AiUtils.findHomeButton(udid) + # if homeButton.exists: + # homeButton.click() + # else: + # ControlUtils.closeTikTok(session, udid) + # time.sleep(2) + # + # ControlUtils.openTikTok(session, udid) + # time.sleep(3) + # + # print("重新创建wda会话 防止wda会话失效") + # client = wda.USBClient(udid) + # session = client.session() + # + # # 执行完成之后。继续点击搜索 + # session.appium_settings({"snapshotMaxDepth": 15}) + # # 点击搜索按钮 + # ControlUtils.clickSearch(session) + # + # else: + # session.appium_settings({"snapshotMaxDepth": 15}) + # # 点击搜索按钮 + # ControlUtils.clickSearch(session) + + """ + 外层包装,出现异常自动重试、 + 关注打招呼以及回复主播消息 + """ + + def safe_greetNewFollowers(self, udid, needReply, event): + + retries = 0 + while not event.is_set(): + try: + self.greetNewFollowers(udid, needReply, event) + return # 成功执行就退出 + except Exception as e: + retries += 1 + LogManager.error(f"greetNewFollowers 出现异常: {e},准备第 {retries} 次重试", udid) + time.sleep(3) + LogManager.error("greetNewFollowers 重试次数耗尽,任务终止", udid) + # 关注打招呼以及回复主播消息 def greetNewFollowers(self, udid, needReply, event): client = wda.USBClient(udid) session = client.session() print(f"是否要自动回复消息:{needReply}") + + LogManager.info(f"是否要自动回复消息:{needReply}", udid) # 先关闭Tik Tok ControlUtils.closeTikTok(session, udid) time.sleep(1) @@ -221,8 +473,6 @@ class ScriptManager(): # 设置查找深度 session.appium_settings({"snapshotMaxDepth": 15}) - # 点击搜索按钮 - ControlUtils.clickSearch(session) # 创建udid名称的目录 AiUtils.makeUdidDir(udid) @@ -234,74 +484,104 @@ class ScriptManager(): time.sleep(2) # 循环条件。1、 循环关闭 2、 数据处理完毕 - while not event.is_set() or len(anchorList) > 0: + while not event.is_set() and len(anchorList) > 0: - # 查找输入框 - input = session.xpath('//XCUIElementTypeSearchField') - # 如果找到了输入框,就点击并且输入内容 - input.click() - # 稍作停顿 - time.sleep(1) - - # 获取一个主播 - anchor = anchorList[0] + # 获取一个主播,并删除 + anchor = anchorList.pop(0) aid = anchor.anchorId anchorCountry = anchor.country + # 点击搜索按钮 + ControlUtils.clickSearch(session) + + # 强制刷新session + session.appium_settings({"snapshotMaxDepth": 15}) + # 查找输入框 + input = session.xpath('//XCUIElementTypeSearchField') + # 如果找到了输入框,就点击并且输入内容 + if input.exists: + input.click() + # 稍作停顿 + time.sleep(0.5) + else: + print(f"找不到输入框") input.clear_text() - time.sleep(2) + time.sleep(1) # 输入主播id input.set_text(aid + "\n") - # 切换UI查找深度 - session.appium_settings({"snapshotMaxDepth": 25}) # 定位 "关注" 按钮 通过关注按钮的位置点击主播首页 - follow_button = session.xpath("//XCUIElementTypeButton[@traits='Button' and @index='1']") - time.sleep(2) - if follow_button.exists: - print(follow_button.bounds) - # session.appium_settings({"snapshotMaxDepth": 10}) - print("找到关注按钮!") - x = follow_button.bounds.x - 100 - y = follow_button.bounds.y - print(x, y) - client.click(x, y) - print("进入主播首页啦") - else: - goBack(1) - removeModelFromAnchorList(anchor) - print("未找到关注按钮") - continue - time.sleep(3) - session.appium_settings({"snapshotMaxDepth": 25}) + session.appium_settings({"snapshotMaxDepth": 23}) + + try: + + # 查找“关注”按钮 + follow_btn = session.xpath( + '//XCUIElementTypeButton[@name="关注" or @name="Follow" or @name="已关注" or @name="Following"]').get( + timeout=5) + + # 获取按钮位置和大小 + rect = follow_btn.bounds + left_x = max(1, rect.x - 20) # 向左偏移 20px,确保不会点到屏幕外 + center_y = rect.y + rect.height // 2 + + # 打印调试信息 + print(f"关注按钮位置 x={rect.x}, y={rect.y}, width={rect.width}, height={rect.height}") + print(f"即将点击的位置 x={left_x}, y={center_y}") + + # 点击关注按钮左侧区域(通常是头像) + session.tap(left_x, center_y) + + except wda.WDAElementNotFoundError: + # 如果没有“关注”按钮,则不点击 + print("未找到‘关注’按钮,跳过点击。") + goBack(2) + session.appium_settings({"snapshotMaxDepth": 15}) + continue + time.sleep(2) # 找到并点击第一个视频 - cellClickResult = ControlUtils.clickFirstVideoFromDetailPage(session) + cellClickResult, workCount = ControlUtils.clickFirstVideoFromDetailPage(session) time.sleep(2) # 观看主播视频 - def viewAnchorVideo(): + def viewAnchorVideo(workCount): print("开始查看视频,并且重新调整查询深度") session.appium_settings({"snapshotMaxDepth": 5}) - count = 3 + + if workCount > 3: + count = 3 + else: + count = workCount + while count != 0: time.sleep(5) img = client.screenshot() time.sleep(1) - filePath = f"resources/{udid}/bgv.png" + + # filePath = f"resources/{udid}/bgv.png" + + base_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) # 当前脚本目录的上一级 + filePath = os.path.join(base_dir, "resources", udid, "bgv.png") + dirPath = os.path.dirname(filePath) + if not os.path.exists(dirPath): + os.makedirs(dirPath) + img.save(filePath) + LogManager.info("保存屏幕图像成功", udid) time.sleep(2) # 查找add图标 r = ControlUtils.clickLike(session, udid) # 点赞成功。 - if r == True: - count -= 1 + # if r == True: - # 假装看几秒视频 - time.sleep(5) + count -= 1 + + # 随机看视频 15~30秒 + time.sleep(random.randint(15, 30)) if count != 0: client.swipe_up() @@ -312,10 +592,14 @@ class ScriptManager(): if cellClickResult == True: # 观看主播视频 LogManager.info("去查看主播视频", udid) - viewAnchorVideo() + viewAnchorVideo(workCount) time.sleep(3) LogManager.info("视频看完了,重置试图查询深度", udid) session.appium_settings({"snapshotMaxDepth": 25}) + + # 向上滑动 + session.swipe_down() + # 点击关注按钮 followButton = AiUtils.getFollowButton(session) if followButton is not None: @@ -360,14 +644,13 @@ class ScriptManager(): LogManager.error(f"给主播{anchor.anchorId} 发送消息失败", udid) # 接着下一个主播 - removeModelFromAnchorList(anchor) + # removeModelFromAnchorList(anchor) goBack(4) else: print(f"{anchor.anchorId}:该主播没有视频") - # 删除当前数据 - removeModelFromAnchorList(anchor) goBack(3) + session.appium_settings({"snapshotMaxDepth": 15}) continue # 设置查找深度 @@ -404,8 +687,10 @@ class ScriptManager(): else: session.appium_settings({"snapshotMaxDepth": 15}) # 点击搜索按钮 - ControlUtils.clickSearch(session) + # ControlUtils.clickSearch(session) + print("greetNewFollowers方法执行完毕") + # 检测消息 def replyMessages(self, udid, event): client = wda.USBClient(udid) session = client.session() @@ -415,210 +700,269 @@ class ScriptManager(): time.sleep(3) while not event.is_set(): - self.monitorMessages(session, udid) + try: + # 调用检测消息的方法 + self.monitorMessages(session, udid) + except Exception as e: + LogManager.error(f"监控消息 出现异常: {e},重新启动监控直播", udid) + # 出现异常时,稍等再重启 TikTok 并重试 + ControlUtils.closeTikTok(session, udid) + time.sleep(2) + ControlUtils.openTikTok(session, udid) + time.sleep(3) + continue # 重新进入 while 循环,调用 monitorMessages # 检查未读消息并回复 def monitorMessages(self, session, udid): - LogManager.info("开始监控收件箱消息,", udid) - + # 调整节点的深度为 7 session.appium_settings({"snapshotMaxDepth": 7}) - el = session(xpath='//XCUIElementTypeButton[@name="a11y_vo_inbox"]') + print(111111111111111111111111) + # 如果收件箱有消息 则进行点击 if el.exists: m = re.search(r'(\d+)', el.label) # 抓到的第一个数字串 count = int(m.group(1)) if m else 0 if count: el.click() + session.appium_settings({"snapshotMaxDepth": 25}) + time.sleep(3) + while True: + + info_count = 0 + + # 创建新的会话 + el = session(xpath='//XCUIElementTypeButton[@name="a11y_vo_inbox"]') + print("el", el) + if not el.exists: + LogManager.error(f"检测不到收件箱", udid) + raise Exception("当前页面找不到收件箱,重启") + # break + + # 支持中文“收件箱”和英文“Inbox” + xpath_query = ( + "//XCUIElementTypeStaticText" + "[@value='收件箱' or @label='收件箱' or @name='收件箱'" + " or @value='Inbox' or @label='Inbox' or @name='Inbox']" + ) + + # 查找所有收件箱节点 + inbox_nodes = session.xpath(xpath_query).find_elements() + + if len(inbox_nodes) < 2: + LogManager.error(f"当前页面不再收件箱页面,重启", udid) + raise Exception("当前页面不再收件箱页面,重启") + + print(3333333333333333333333333333333) + m = re.search(r'(\d+)', el.label) # 抓到的第一个数字串 + count = int(m.group(1)) if m else 0 + + if not count: + LogManager.info(f"当前收件箱的总数量{count}", udid) + break + + # 新粉丝 + xp_new_fan_badge = ( + "//XCUIElementTypeCell[.//XCUIElementTypeLink[@name='新粉丝']]" + "//XCUIElementTypeStaticText[string-length(@value)>0 and translate(@value,'0123456789','')='']" + ) + + # 活动 + xp_activity_badge = ( + "//XCUIElementTypeCell[.//XCUIElementTypeLink[@name='活动']]" + "//XCUIElementTypeStaticText[string-length(@value)>0 and translate(@value,'0123456789','')='']" + ) + + # 系统通知 + xp_system_badge = ( + "//XCUIElementTypeCell[.//XCUIElementTypeLink[@name='系统通知']]" + "//XCUIElementTypeStaticText[string-length(@value)>0 and translate(@value,'0123456789','')='']" + ) + + # 消息请求 + xp_request_badge = ( + "//XCUIElementTypeCell" + "[.//*[self::XCUIElementTypeLink or self::XCUIElementTypeStaticText]" + " [@name='消息请求' or @label='消息请求' or @value='消息请求']]" + "//XCUIElementTypeStaticText[string-length(@value)>0 and translate(@value,'0123456789','')='']" + ) + + # 用户消息 + xp_badge_numeric = ( + '//XCUIElementTypeOther[' + ' @name="AWEIMChatListCellUnreadCountViewComponent"' + ' or @name="TikTokIMImpl.InboxCellUnreadCountViewBuilder"' + ']//XCUIElementTypeStaticText[@value and translate(@value,"0123456789","")=""]' + ) + + print(44444444444444444444444444444444) + + try: + # 如果 2 秒内找不到,会抛异常 + badge_text = session.xpath(xp_new_fan_badge).get(timeout=2.0) + val = (badge_text.info.get("value") or + badge_text.info.get("label") or + badge_text.info.get("name")) + LogManager.info(f"新粉丝未读数量:{val}", udid) + if badge_text: + badge_text.tap() + time.sleep(1) + ControlUtils.clickBack(session) + time.sleep(1) + except Exception: + LogManager.warning("当前屏幕没有找到 新粉丝 未读徽标数字", udid) + print("当前屏幕没有找到 新粉丝 未读徽标数字", udid) + badge_text = None + info_count += 1 + + try: + + # 如果 2 秒内找不到,会抛异常 + badge_text = session.xpath(xp_activity_badge).get(timeout=2.0) + val = (badge_text.info.get("value") or + badge_text.info.get("label") or + badge_text.info.get("name")) + LogManager.info(f"活动未读数量:{val}", udid) + if badge_text: + badge_text.tap() + time.sleep(1) + ControlUtils.clickBack(session) + time.sleep(1) + except Exception: + LogManager.warning("当前屏幕没有找到 活动 未读徽标数字", udid) + print("当前屏幕没有找到 活动 未读徽标数字", udid) + badge_text = None + info_count += 1 + + try: + + # 如果 2 秒内找不到,会抛异常 + badge_text = session.xpath(xp_system_badge).get(timeout=2.0) + val = (badge_text.info.get("value") or + badge_text.info.get("label") or + badge_text.info.get("name")) + LogManager.info(f"系统通知未读数量:{val}", udid) + if badge_text: + badge_text.tap() + time.sleep(1) + ControlUtils.clickBack(session) + time.sleep(1) + except Exception: + LogManager.warning("当前屏幕没有找到 系统通知 未读徽标数字", udid) + print("当前屏幕没有找到 系统通知 未读徽标数字", udid) + badge_text = None + info_count += 1 + + try: + # 如果 2 秒内找不到,会抛异常 + badge_text = session.xpath(xp_request_badge).get(timeout=2.0) + val = (badge_text.info.get("value") or + badge_text.info.get("label") or + badge_text.info.get("name")) + LogManager.info(f"消息请求未读数量:{val}", udid) + if badge_text: + badge_text.tap() + time.sleep(1) + ControlUtils.clickBack(session) + time.sleep(1) + except Exception: + LogManager.warning("当前屏幕没有找到 消息请求 未读徽标数字", udid) + print("当前屏幕没有找到 消息请求 未读徽标数字", udid) + badge_text = None + info_count += 1 + + try: + # 如果 2 秒内找不到,会抛异常 + user_text = session.xpath(xp_badge_numeric).get(timeout=2.0) + val = (user_text.info.get("value") or + user_text.info.get("label") or + user_text.info.get("name")) + LogManager.info(f"用户未读数量:{val}", udid) + except Exception: + LogManager.warning("当前屏幕没有找到 用户 未读徽标数字", udid) + print("当前屏幕没有找到 用户消息 未读徽标数字", udid) + user_text = None + info_count += 1 + + if user_text: + user_text.tap() + time.sleep(3) + xml = session.source() + msgs = AiUtils.extract_messages_from_xml(xml) + # 检测出对方发的最后一条信息 + last_msg_text = next(item['text'] for item in reversed(msgs) if item['type'] == 'msg') + # 向ai发送信息 + + # 获取主播的名称 + anchor_name = AiUtils.get_navbar_anchor_name(session) + + # 找到输入框 + + sel = session.xpath("//TextView") + + if anchor_name not in anchorWithSession: + # 如果是第一次发消息(没有sessionId的情况) + response = Requester.chatToAi({"msg": last_msg_text}) + aiResult = response['result'] + sessionId = response['session_id'] + + anchorWithSession[anchor_name] = sessionId + # 找到输入框,输入ai返回出来的消息 + if sel.exists: + sel.click() # 聚焦 + time.sleep(1) + sel.clear_text() + sel.set_text(aiResult + "\n") + else: + LogManager.error("找不到输入框,重启", udid) + raise Exception("找不到输入框,重启") + else: + # 如果不是第一次发消息(证明存储的有sessionId) + sessionId = anchorWithSession[anchor_name] + response = Requester.chatToAi({"msg": last_msg_text, "sid": sessionId}) + aiResult = response['result'] + if sel.exists: + sel.click() # 聚焦 + time.sleep(1) + sel.clear_text() + sel.set_text(aiResult + "\n") + + LogManager.info(f"存储的sessionId:{anchorWithSession}", udid) + time.sleep(1) + # 返回 + ControlUtils.clickBack(session) + + # 重新回到收件箱页面后,强制刷新节点 + session.appium_settings({"snapshotMaxDepth": 25}) + time.sleep(1) + + # 双击收件箱 定位到消息的位置 + if info_count == 5: + r = el.bounds # 可能是命名属性,也可能是 tuple + cx = int((r.x + r.width / 2) if hasattr(r, "x") else (r[0] + r[2] / 2)) + cy = int((r.y + r.height / 2) if hasattr(r, "y") else (r[1] + r[3] / 2)) + session.double_tap(cx, cy) # 可能抛异常:方法不存在 + LogManager.info(f"双击收件箱 定位到信息", udid) + else: + return else: LogManager.error(f"检测不到收件箱", udid) + raise Exception("当前页面找不到收件箱,重启") - time.sleep(3) - session.appium_settings({"snapshotMaxDepth": 22}) + # 双击收件箱 定位到消息的位置 - while True: - el = session(xpath='//XCUIElementTypeButton[@name="a11y_vo_inbox"]') - print("el", el) - if not el.exists: - LogManager.warning(f"检测不到收件箱", udid) - break - - m = re.search(r'(\d+)', el.label) # 抓到的第一个数字串 - count = int(m.group(1)) if m else 0 - - if not count: - LogManager.info(f"当前收件箱的总数量{count}", udid) - break - - - # 新粉丝 - xp_new_fan_badge = ( - "//XCUIElementTypeCell[.//XCUIElementTypeLink[@name='新粉丝']]" - "//XCUIElementTypeStaticText[string-length(@value)>0 and translate(@value,'0123456789','')='']" - ) - - # 活动 - xp_activity_badge = ( - "//XCUIElementTypeCell[.//XCUIElementTypeLink[@name='活动']]" - "//XCUIElementTypeStaticText[string-length(@value)>0 and translate(@value,'0123456789','')='']" - ) - - # 系统通知 - xp_system_badge = ( - "//XCUIElementTypeCell[.//XCUIElementTypeLink[@name='系统通知']]" - "//XCUIElementTypeStaticText[string-length(@value)>0 and translate(@value,'0123456789','')='']" - ) - - # 消息请求 - xp_request_badge = ( - "//XCUIElementTypeCell" - "[.//*[self::XCUIElementTypeLink or self::XCUIElementTypeStaticText]" - " [@name='消息请求' or @label='消息请求' or @value='消息请求']]" - "//XCUIElementTypeStaticText[string-length(@value)>0 and translate(@value,'0123456789','')='']" - ) - - # 用户消息 - xp_badge_numeric = ( - '//XCUIElementTypeOther[' - ' @name="AWEIMChatListCellUnreadCountViewComponent"' - ' or @name="TikTokIMImpl.InboxCellUnreadCountViewBuilder"' - ']//XCUIElementTypeStaticText[@value and translate(@value,"0123456789","")=""]' - ) - - try: - # 如果 2 秒内找不到,会抛异常 - badge_text = session.xpath(xp_new_fan_badge).get(timeout=2.0) - val = (badge_text.info.get("value") or - badge_text.info.get("label") or - badge_text.info.get("name")) - LogManager.info(f"新粉丝未读数量:{val}", udid) - if badge_text: - badge_text.tap() - time.sleep(1) - ControlUtils.clickBack(session) - time.sleep(1) - except Exception: - LogManager.warning("当前屏幕没有找到 新粉丝 未读徽标数字", udid) - badge_text = None - - try: - - # 如果 2 秒内找不到,会抛异常 - badge_text = session.xpath(xp_activity_badge).get(timeout=2.0) - val = (badge_text.info.get("value") or - badge_text.info.get("label") or - badge_text.info.get("name")) - LogManager.info(f"活动未读数量:{val}", udid) - if badge_text: - badge_text.tap() - time.sleep(1) - ControlUtils.clickBack(session) - time.sleep(1) - except Exception: - LogManager.warning("当前屏幕没有找到 活动 未读徽标数字", udid) - badge_text = None - - try: - - # 如果 2 秒内找不到,会抛异常 - badge_text = session.xpath(xp_system_badge).get(timeout=2.0) - val = (badge_text.info.get("value") or - badge_text.info.get("label") or - badge_text.info.get("name")) - LogManager.info(f"系统通知未读数量:{val}", udid) - if badge_text: - badge_text.tap() - time.sleep(1) - ControlUtils.clickBack(session) - time.sleep(1) - except Exception: - LogManager.warning("当前屏幕没有找到 系统通知 未读徽标数字", udid) - badge_text = None - - try: - # 如果 2 秒内找不到,会抛异常 - badge_text = session.xpath(xp_request_badge).get(timeout=2.0) - val = (badge_text.info.get("value") or - badge_text.info.get("label") or - badge_text.info.get("name")) - LogManager.info(f"消息请求未读数量:{val}", udid) - if badge_text: - badge_text.tap() - time.sleep(1) - ControlUtils.clickBack(session) - time.sleep(1) - except Exception: - LogManager.warning("当前屏幕没有找到 消息请求 未读徽标数字", udid) - badge_text = None - - try: - # 如果 2 秒内找不到,会抛异常 - badge_text = session.xpath(xp_badge_numeric).get(timeout=2.0) - val = (badge_text.info.get("value") or - badge_text.info.get("label") or - badge_text.info.get("name")) - LogManager.info(f"用户未读数量:{val}", udid) - - if badge_text: - badge_text.tap() - time.sleep(3) - xml = session.source() - msgs = AiUtils.extract_messages_from_xml(xml) - # 检测出对方发的最后一条信息 - last_msg_text = next(item['text'] for item in reversed(msgs) if item['type'] == 'msg') - # 向ai发送信息 - - # 获取主播的名称 - anchor_name = AiUtils.get_navbar_anchor_name(session) - - # 找到输入框 - - sel = session.xpath("//TextView") - - if anchor_name not in anchorWithSession: - # 如果是第一次发消息(没有sessionId的情况) - response = Requester.chatToAi({"msg": last_msg_text}) - aiResult = response['result'] - sessionId = response['session_id'] - - anchorWithSession[anchor_name] = sessionId - # 找到输入框,输入ai返回出来的消息 - if sel.exists: - sel.click() # 聚焦 - time.sleep(1) - sel.clear_text() - sel.set_text(aiResult + "\n") - else: - # 如果不是第一次发消息(证明存储的有sessionId) - sessionId = anchorWithSession[anchor_name] - response = Requester.chatToAi({"msg": last_msg_text, "sid": sessionId}) - aiResult = response['result'] - if sel.exists: - sel.click() # 聚焦 - time.sleep(1) - sel.clear_text() - sel.set_text(aiResult + "\n") - time.sleep(1) - # 返回 - ControlUtils.clickBack(session) - except Exception: - LogManager.warning("当前屏幕没有找到 用户 未读徽标数字", udid) - badge_text = None - - # 双击收件箱 定位到消息的位置 - - r = el.bounds # 可能是命名属性,也可能是 tuple - cx = int((r.x + r.width / 2) if hasattr(r, "x") else (r[0] + r[2] / 2)) - cy = int((r.y + r.height / 2) if hasattr(r, "y") else (r[1] + r[3] / 2)) - - session.double_tap(cx, cy) # 可能抛异常:方法不存在 - - LogManager.info(f"双击收件箱 定位到信息", udid) + # if count == 4: + # r = el.bounds # 可能是命名属性,也可能是 tuple + # cx = int((r.x + r.width / 2) if hasattr(r, "x") else (r[0] + r[2] / 2)) + # cy = int((r.y + r.height / 2) if hasattr(r, "y") else (r[1] + r[3] / 2)) + # session.double_tap(cx, cy) # 可能抛异常:方法不存在 + # LogManager.info(f"双击收件箱 定位到信息", udid) + # count = 0 def test(self, udid): client = wda.USBClient(udid) session = client.session() session.appium_settings({"snapshotMaxDepth": 10}) - print(client.source()) \ No newline at end of file + print(client.source())