diff --git a/.gitignore b/.gitignore index 90e4b38..e69de29 100644 --- a/.gitignore +++ b/.gitignore @@ -1,127 +0,0 @@ -# Byte-compiled / optimized / DLL files -__pycache__/ -*.py[cod] -*$py.class - -# Distribution / packaging -.Python -build/ -develop-eggs/ -dist/ -downloads/ -eggs/ -.eggs/ -lib/ -lib64/ -parts/ -sdist/ -var/ -*.egg-info/ -.installed.cfg -*.egg -out/ - -# PyInstaller -# Usually these files are written by a python script from a template -# before PyInstaller builds the exe, so as to inject date/other infos into it. -*.manifest -*.spec - -# Installer logs -pip-log.txt -pip-delete-this-directory.txt - -# Unit test / coverage reports -htmlcov/ -.tox/ -.nox/ -.coverage -.coverage.* -.cache -nosetests.xml -coverage.xml -*.cover -.hypothesis/ -.pytest_cache/ - -# Translations -*.mo -*.pot - -# Django stuff: -*.log -local_settings.py -db.sqlite3 -db.sqlite3-journal - -# Flask stuff: -instance/ -.webassets-cache - -# Scrapy stuff: -.scrapy - -# Sphinx documentation -docs/_build/ -docs/.doctrees/ - -# PyBuilder -target/ - -# Jupyter Notebook -.ipynb_checkpoints - -# IPython -profile_default/ -ipython_config.py - -# pyenv -.python-version - -# pipenv -# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. -# However, in case of collaboration, if having platform-specific dependencies or dependencies -# having no cross-platform support, pipenv may install dependencies that don't work, or not -# install all needed dependencies. -#Pipfile.lock - -# celery beat schedule file -celerybeat-schedule - -# SageMath parsed files -*.sage.py - -# Environments -.env -.venv -env/ -venv/ -ENV/ -env.bak/ -venv.bak/ - -# Spyder project settings -.spyderproject -.spyproject - -# Rope project settings -.ropeproject - -# mkdocs documentation -/site - -# mypy -.mypy_cache/ -.dmypy.json -dmypy.json - -# Pyre type checker -.pyre/ - -# pytype static type checker -.pytype/ - -# Cython debug symbols -cython_debug/ - -*.bat \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 54c1336..7ebb8ba 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -5,9 +5,81 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/Entity/DeviceModel.py b/Entity/DeviceModel.py index cddf3c0..d4d595f 100644 --- a/Entity/DeviceModel.py +++ b/Entity/DeviceModel.py @@ -1,4 +1,4 @@ -from getpass import fallback_getpass +# from getpass import fallback_getpass # 设备模型 diff --git a/Module/DeviceInfo.py b/Module/DeviceInfo.py index 4182e26..abf031f 100644 --- a/Module/DeviceInfo.py +++ b/Module/DeviceInfo.py @@ -30,12 +30,14 @@ class Deviceinfo(object): self._lock = threading.Lock() self._model_index: Dict[str, DeviceModel] = {} # udid -> model - # ✅ 1. 失踪时间戳记录(替代原来的 miss_count) - self._last_seen: Dict[str, float] = {} - self._port_pool: List[int] = [] - self._port_in_use: set[int] = set() + self._miss_count: Dict[str, int] = {} # udid -> 连续未扫描到次数 + self._port_pool: List[int] = [] # 端口回收池 + self._port_in_use: set[int] = set() # 正在使用的端口 - # region iproxy 初始化(原逻辑不变) + # 🔥1. 启动 WDA 健康检查线程 + # threading.Thread(target=self._wda_health_checker, daemon=True).start() + + # region iproxy 初始化 try: self.iproxy_path = self._iproxy_path() self.iproxy_dir = self.iproxy_path.parent @@ -83,11 +85,7 @@ class Deviceinfo(object): LogManager.error(f"初始化 iproxy 失败:{e}") # endregion - # ------------------------------------------------------------------ - # ✅ 2. 主监听循环(已用“时间窗口+USB 层兜底”重写) - # ------------------------------------------------------------------ def startDeviceListener(self): - MISS_WINDOW = 5.0 # 5 秒连续失踪才判死刑 while True: try: lists = Usbmux().device_list() @@ -97,23 +95,24 @@ class Deviceinfo(object): continue now_udids = {d.udid for d in lists if d.conn_type == ConnectionType.USB} - # ✅ USB 层真断兜底 - usb_sn_set = self._usb_enumerate_sn() - need_remove = None + # 1. 失踪登记 & 累加 + need_remove = None # ← 新增:放锁外记录 with self._lock: for udid in list(self._model_index.keys()): if udid not in now_udids: - last = self._last_seen.get(udid, time.time()) - if time.time() - last > MISS_WINDOW and udid not in usb_sn_set: - need_remove = udid + self._miss_count[udid] = self._miss_count.get(udid, 0) + 1 + if self._miss_count[udid] >= 3: + self._miss_count.pop(udid, None) + need_remove = udid # ← 只记录,不调用 else: - self._last_seen[udid] = time.time() + self._miss_count.pop(udid, None) + # 🔓 锁已释放,再删设备(不会重入) if need_remove: self._remove_model(need_remove) - # 新增设备(原逻辑不变) + # 2. 全新插入(只处理未在线且信任且未满) for d in lists: if d.conn_type != ConnectionType.USB: continue @@ -132,22 +131,14 @@ class Deviceinfo(object): time.sleep(1) - # ------------------------------------------------------------------ - # ✅ 3. USB 层枚举 SN(跨平台) - # ------------------------------------------------------------------ - def _usb_enumerate_sn(self) -> set[str]: - try: - out = subprocess.check_output(["idevice_id", "-l"], text=True, timeout=3) - return {line.strip() for line in out.splitlines() if line.strip()} - except Exception: - return set() - - # ===================== 以下代码与原文件完全一致 ===================== + # 🔥2. WDA 健康检查 def _wda_health_checker(self): while True: time.sleep(1) + print(len(self.deviceModelList)) with self._lock: - online = [m for m in self.deviceModelList if m.ready] + online = [m for m in self.deviceModelList if m.ready] # ← 只检查就绪的 + print(len(online)) for model in online: udid = model.deviceId if not self._wda_ok(udid): @@ -156,24 +147,32 @@ class Deviceinfo(object): self._remove_model(udid) self.connectDevice(udid) + # 🔥3. 真正做 health-check 的地方 def _wda_ok(self, udid: str) -> bool: + """返回 True 表示 WDA 活着,False 表示已死""" try: + # 用 2 秒超时快速探测 c = wda.USBClient(udid, 8100) + # 下面这句就是“xctest launched but check failed” 的触发点 + # 如果 status 里返回了 WebDriverAgent 运行信息就认为 OK st = c.status() if st.get("state") != "success": return False + # 你也可以再苛刻一点,多做一次 /wda/healthcheck + # c.http.get("/wda/healthcheck") return True except Exception as e: + # 任何异常(连接拒绝、超时、json 解析失败)都认为已死 LogManager.error(f"WDA health-check 异常:{e}", udid) return False - # -------------------- 增删改查唯一入口(未改动) -------------------- + # region ===================== 增删改查唯一入口(线程安全) ===================== def _has_model(self, udid: str) -> bool: return udid in self._model_index def _add_model(self, model: DeviceModel): if model.deviceId in self._model_index: - return + return # 防重复 model.ready = True self.deviceModelList.append(model) self._model_index[model.deviceId] = model @@ -181,48 +180,60 @@ class Deviceinfo(object): self.manager.send(model.toDict()) except Exception as e: LogManager.warning(f"{model.deviceId} 发送上线事件失败:{e}") - LogManager.method_info(f"{model.deviceId} 加入设备成功,当前在线数:{len(self.deviceModelList)}", method="device_count") + LogManager.method_info(f"{model.deviceId} 加入设备成功,当前在线数:{len(self.deviceModelList)}",method="device_count") + + # 删除设备 def _remove_model(self, udid: str): print(f"【删】进入删除方法 udid={udid}") LogManager.method_info(f"【删】进入删除方法 udid={udid}", method="device_count") + # 1. 纯内存临界区——毫秒级 with self._lock: print(f"【删】拿到锁 udid={udid}") - LogManager.method_info(f"【删】拿到锁 udid={udid}", method="device_count") + LogManager.method_info(f"【删】拿到锁 udid={udid}", + method="device_count") model = self._model_index.pop(udid, None) if not model: print(f"【删】模型已空,直接返回 udid={udid}") - LogManager.method_info(f"【删】模型已空,直接返回 udid={udid}", method="device_count") + LogManager.method_info(f"【删】模型已空,直接返回 udid={udid}",method="device_count") return if model.deleting: print(f"【删】正在删除中,幂等返回 udid={udid}") LogManager.method_info(method="device_count", text=f"【删】正在删除中,幂等返回 udid={udid}") return model.deleting = True + # 标记维删除设备 model.type = 2 print(f"【删】标记 deleting=True udid={udid}") - LogManager.method_info("【删】标记 deleting=True udid={udid}", "device_count") + LogManager.method_info("【删】标记 deleting=True udid={udid}","device_count") + # 过滤列表 before = len(self.deviceModelList) self.deviceModelList = [m for m in self.deviceModelList if m.deviceId != udid] after = len(self.deviceModelList) print(f"【删】列表过滤 before={before} → after={after} udid={udid}") - LogManager.method_info(f"【删】列表过滤 before={before} → after={after} udid={udid}", "device_count") + LogManager.method_info(f"【删】列表过滤 before={before} → after={after} udid={udid}","device_count") + + # 端口 self._port_in_use.discard(model.screenPort) self._port_pool.append(model.screenPort) print(f"【删】回收端口 port={model.screenPort} udid={udid}") LogManager.method_info(f"【删】回收端口 port={model.screenPort} udid={udid}", method="device_count") + + # 进程 to_kill = [item for item in self.pidList if item.get("id") == udid] self.pidList = [item for item in self.pidList if item.get("id") != udid] print(f"【删】待杀进程数 count={len(to_kill)} udid={udid}") LogManager.method_info(f"【删】待杀进程数 count={len(to_kill)} udid={udid}", method="device_count") + # 2. IO 区无锁 for idx, item in enumerate(to_kill, 1): print(f"【删】杀进程 {idx}/{len(to_kill)} pid={item.get('target').pid} udid={udid}") - LogManager.method_info(f"【删】杀进程 {idx}/{len(to_kill)} pid={item.get('target').pid} udid={udid}", method="device_count") + LogManager.method_error(f"【删】杀进程 {idx}/{len(to_kill)} pid={item.get('target').pid} udid={udid}", method="device_count") self._terminate_proc(item.get("target")) print(f"【删】进程清理完成 udid={udid}") LogManager.method_info(f"【删】进程清理完成 udid={udid}", method="device_count") + # 3. 网络 IO retry = 3 while retry: try: @@ -244,7 +255,7 @@ class Deviceinfo(object): print(len(self.deviceModelList)) LogManager.method_info(f"当前剩余设备数量:{len(self.deviceModelList)}", method="device_count") - # -------------------- 端口分配与回收(未改动) -------------------- + # region ===================== 端口分配与回收 ===================== def _alloc_port(self) -> int: if self._port_pool: port = self._port_pool.pop() @@ -258,17 +269,20 @@ class Deviceinfo(object): if port in self._port_in_use: self._port_in_use.remove(port) self._port_pool.append(port) + # endregion - # -------------------- 单台设备连接(未改动) -------------------- + # region ===================== 单台设备连接 ===================== def connectDevice(self, udid: str): if not self.is_device_trusted(udid): LogManager.warning("设备未信任,跳过 WDA 启动", udid) return + try: d = wda.USBClient(udid, 8100) except Exception as e: LogManager.error(f"启动 WDA 失败: {e}", udid) return + width, height, scale = 0, 0, 1.0 try: size = d.window_size() @@ -276,21 +290,26 @@ class Deviceinfo(object): scale = d.scale except Exception as e: LogManager.warning(f"读取屏幕信息失败:{e}", udid) + port = self._alloc_port() model = DeviceModel(udid, port, width, height, scale, type=1) self._add_model(model) + try: d.app_start(WdaAppBundleId) d.home() except Exception as e: LogManager.warning(f"启动/切回桌面失败:{e}", udid) + time.sleep(2) + + # 先清旧进程再启动新进程 self.pidList = [item for item in self.pidList if item.get("id") != udid] target = self.relayDeviceScreenPort(udid, port) if target: self.pidList.append({"target": target, "id": udid}) - # -------------------- 工具方法(未改动) -------------------- + # region ===================== 工具方法 ===================== def is_device_trusted(self, udid: str) -> bool: try: d = BaseDevice(udid) @@ -300,16 +319,22 @@ class Deviceinfo(object): return False def relayDeviceScreenPort(self, udid: str, port: int) -> Optional[subprocess.Popen]: + """启动 iproxy 前:端口若仍被占用则先杀掉占用者,再启动""" if not self._spawn_iproxy: LogManager.error("iproxy 启动器未就绪", udid) return None + + # --- 新增:端口冲突检查 + 强制清理 --- while self._port_in_use and self._is_port_open(port): + # 先查是哪个进程占用 pid = self._get_pid_by_port(port) if pid and pid != os.getpid(): LogManager.warning(f"端口 {port} 仍被 PID {pid} 占用,尝试释放", udid) self._kill_pid_gracefully(pid) else: break + # ------------------------------------- + try: p = self._spawn_iproxy(udid, port, 9100) self._port_in_use.add(port) @@ -319,12 +344,14 @@ class Deviceinfo(object): LogManager.error(f"启动 iproxy 失败:{e}", udid) return None + # ------------------- 新增三个小工具 ------------------- def _is_port_open(self, port: int) -> bool: import socket with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: return s.connect_ex(("127.0.0.1", port)) == 0 def _get_pid_by_port(self, port: int) -> Optional[int]: + """跨平台根据端口号查 PID,失败返回 None""" try: if os.name == "nt": cmd = ["netstat", "-ano", "-p", "tcp"] @@ -340,6 +367,7 @@ class Deviceinfo(object): return None def _kill_pid_gracefully(self, pid: int): + """先 terminate 再 kill -9""" try: os.kill(pid, signal.SIGTERM) time.sleep(1) @@ -347,6 +375,7 @@ class Deviceinfo(object): except Exception: pass + def _terminate_proc(self, p: Optional[subprocess.Popen]): if not p or p.poll() is not None: return diff --git a/Module/FlaskService.py b/Module/FlaskService.py index 8ee6c8a..39dd30a 100644 --- a/Module/FlaskService.py +++ b/Module/FlaskService.py @@ -274,6 +274,7 @@ def stopScript(): @app.route('/passAnchorData', methods=['POST']) def passAnchorData(): try: + LogManager.method_info("关注打招呼","关注打招呼") data: Dict[str, Any] = request.get_json() # 设备列表 idList = data.get("deviceList", []) @@ -431,7 +432,6 @@ def aiConfig(): @app.route("/select_last_message", methods=['GET']) def select_last_message(): data = JsonUtils.query_all_json_items() - return ResultData(data=data).toJson() diff --git a/Module/log/acList.json b/Module/log/acList.json new file mode 100644 index 0000000..e50ce15 --- /dev/null +++ b/Module/log/acList.json @@ -0,0 +1,22 @@ +[ + { + "anchorId": "giulia.roma", + "country": "意大利" + }, + { + "anchorId": "marcelo_brasil", + "country": "巴西" + }, + { + "anchorId": "anna_krasnova", + "country": "俄罗斯" + }, + { + "anchorId": "lee_jiwoo", + "country": "韩国" + }, + { + "anchorId": "fatima_dxb", + "country": "阿联酋" + } +] \ No newline at end of file diff --git a/SupportFiles/14.0.zip b/SupportFiles/14.0.zip deleted file mode 100644 index bd7a539..0000000 Binary files a/SupportFiles/14.0.zip and /dev/null differ diff --git a/SupportFiles/14.1.zip b/SupportFiles/14.1.zip deleted file mode 100644 index ceb8328..0000000 Binary files a/SupportFiles/14.1.zip and /dev/null differ diff --git a/SupportFiles/14.2.zip b/SupportFiles/14.2.zip deleted file mode 100644 index 6190de1..0000000 Binary files a/SupportFiles/14.2.zip and /dev/null differ diff --git a/SupportFiles/14.3.zip b/SupportFiles/14.3.zip deleted file mode 100644 index 1203d70..0000000 Binary files a/SupportFiles/14.3.zip and /dev/null differ diff --git a/SupportFiles/14.4.zip b/SupportFiles/14.4.zip deleted file mode 100644 index a1025db..0000000 Binary files a/SupportFiles/14.4.zip and /dev/null differ diff --git a/SupportFiles/14.5.zip b/SupportFiles/14.5.zip deleted file mode 100644 index d91b781..0000000 Binary files a/SupportFiles/14.5.zip and /dev/null differ diff --git a/SupportFiles/14.6.zip b/SupportFiles/14.6.zip deleted file mode 100644 index 9ed5e41..0000000 Binary files a/SupportFiles/14.6.zip and /dev/null differ diff --git a/SupportFiles/14.7.zip b/SupportFiles/14.7.zip deleted file mode 100644 index 3a8f01a..0000000 Binary files a/SupportFiles/14.7.zip and /dev/null differ diff --git a/SupportFiles/14.8.zip b/SupportFiles/14.8.zip deleted file mode 100644 index ea9c3a6..0000000 Binary files a/SupportFiles/14.8.zip and /dev/null differ diff --git a/SupportFiles/15.0.zip b/SupportFiles/15.0.zip deleted file mode 100644 index a91df82..0000000 Binary files a/SupportFiles/15.0.zip and /dev/null differ diff --git a/SupportFiles/15.1.zip b/SupportFiles/15.1.zip deleted file mode 100644 index bd07873..0000000 Binary files a/SupportFiles/15.1.zip and /dev/null differ diff --git a/SupportFiles/15.2.zip b/SupportFiles/15.2.zip deleted file mode 100644 index 134c25b..0000000 Binary files a/SupportFiles/15.2.zip and /dev/null differ diff --git a/SupportFiles/15.3.zip b/SupportFiles/15.3.zip deleted file mode 100644 index 1a75175..0000000 Binary files a/SupportFiles/15.3.zip and /dev/null differ diff --git a/SupportFiles/15.4.zip b/SupportFiles/15.4.zip deleted file mode 100644 index b62fa2d..0000000 Binary files a/SupportFiles/15.4.zip and /dev/null differ diff --git a/SupportFiles/15.5.zip b/SupportFiles/15.5.zip deleted file mode 100644 index d84ff97..0000000 Binary files a/SupportFiles/15.5.zip and /dev/null differ diff --git a/SupportFiles/15.6.zip b/SupportFiles/15.6.zip deleted file mode 100644 index bc450f3..0000000 Binary files a/SupportFiles/15.6.zip and /dev/null differ diff --git a/SupportFiles/15.7.zip b/SupportFiles/15.7.zip deleted file mode 100644 index 13ab765..0000000 Binary files a/SupportFiles/15.7.zip and /dev/null differ diff --git a/SupportFiles/15.8.zip b/SupportFiles/15.8.zip deleted file mode 100644 index f413db6..0000000 Binary files a/SupportFiles/15.8.zip and /dev/null differ diff --git a/SupportFiles/16.0.zip b/SupportFiles/16.0.zip deleted file mode 100644 index ab9a02a..0000000 Binary files a/SupportFiles/16.0.zip and /dev/null differ diff --git a/SupportFiles/16.1.zip b/SupportFiles/16.1.zip deleted file mode 100644 index 07c9458..0000000 Binary files a/SupportFiles/16.1.zip and /dev/null differ diff --git a/SupportFiles/16.2.zip b/SupportFiles/16.2.zip deleted file mode 100644 index b5f95da..0000000 Binary files a/SupportFiles/16.2.zip and /dev/null differ diff --git a/SupportFiles/16.3.zip b/SupportFiles/16.3.zip deleted file mode 100644 index bc4e184..0000000 Binary files a/SupportFiles/16.3.zip and /dev/null differ diff --git a/SupportFiles/16.4.zip b/SupportFiles/16.4.zip deleted file mode 100644 index 8a785e3..0000000 Binary files a/SupportFiles/16.4.zip and /dev/null differ diff --git a/SupportFiles/16.5.zip b/SupportFiles/16.5.zip deleted file mode 100644 index fc2ec0b..0000000 Binary files a/SupportFiles/16.5.zip and /dev/null differ diff --git a/SupportFiles/16.6.zip b/SupportFiles/16.6.zip deleted file mode 100644 index 1e4924d..0000000 Binary files a/SupportFiles/16.6.zip and /dev/null differ diff --git a/SupportFiles/16.7.zip b/SupportFiles/16.7.zip deleted file mode 100644 index f2a36f8..0000000 Binary files a/SupportFiles/16.7.zip and /dev/null differ diff --git a/Utils/ControlUtils.py b/Utils/ControlUtils.py index 028983f..103fe2f 100644 --- a/Utils/ControlUtils.py +++ b/Utils/ControlUtils.py @@ -1,6 +1,8 @@ +import math import random import re import time +from typing import Tuple, List import tidevice import wda @@ -70,14 +72,17 @@ class ControlUtils(object): return True elif session.xpath("//*[@name='nav_bar_start_back']").exists: back = session.xpath("//*[@name='nav_bar_start_back']") - back.click() + 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]") - back.click() - return True + + if back.exists: + back.click() + return True else: return False except Exception as e: @@ -129,6 +134,7 @@ class ControlUtils(object): 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” @@ -204,33 +210,65 @@ class ControlUtils(object): print(e) return False - - - - - # 随机滑动一点点距离 @classmethod - def tap_mini_cluster(cls, center_x: int, center_y: int, session, points=5, duration_ms=60): - try: - response = session.http.post( - "touchAndHold", - data={ - "x": 100, - "y": 100,+ - "duration": 0.1 - } - ) - print(response) - return response - except Exception as e: - print(e) - return None - - # 检测五分钟前和当前的状态是否相同 - # @classmethod - # def compareCurrentWithPreviousState(cls,xml): - + 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) diff --git a/Utils/LogManager.py b/Utils/LogManager.py index 67e8a24..6ace65c 100644 --- a/Utils/LogManager.py +++ b/Utils/LogManager.py @@ -1,217 +1,3 @@ -# -# import datetime -# import io -# import logging -# import os -# import re -# import sys -# import shutil -# import zipfile -# from pathlib import Path -# import requests -# -# -# class LogManager: -# # 运行根目录:打包后取 exe 目录;源码运行取项目目录 -# if getattr(sys, "frozen", False): -# projectRoot = os.path.dirname(sys.executable) -# else: -# projectRoot = os.path.dirname(os.path.dirname(__file__)) -# -# logDir = os.path.join(projectRoot, "log") -# _loggers = {} -# _method_loggers = {} # 新增:缓存“设备+方法”的 logger -# -# # ---------- 工具函数 ---------- -# @classmethod -# def _safe_filename(cls, name: str, max_len: int = 80) -> str: -# """ -# 将方法名/udid等转成安全文件名: -# - 允许字母数字、点、下划线、连字符 -# - 允许常见 CJK 字符(中日韩) -# - 其他非法字符替换为下划线 -# - 合并多余下划线,裁剪长度 -# """ -# if not name: -# return "unknown" -# name = str(name).strip() -# -# # 替换 Windows 非法字符和控制符 -# name = re.sub(r'[\\/:*?"<>|\r\n\t]+', '_', name) -# -# # 只保留 ① 英数._- ② CJK 统一表意文字、日文平/片假名、韩文音节 -# name = re.sub(rf'[^a-zA-Z0-9_.\-' -# r'\u4e00-\u9fff' # 中 -# r'\u3040-\u30ff' # 日 -# r'\uac00-\ud7a3' # 韩 -# r']+', '_', name) -# # 合并多余下划线,去两端空白与下划线 -# name = re.sub(r'_+', '_', name).strip(' _.') -# # 避免空 -# name = name or "unknown" -# # Windows 预留名避免(CON/PRN/AUX/NUL/COM1…) -# if re.fullmatch(r'(?i)(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])', name): -# name = f"_{name}" -# # 限长 -# return name[:max_len] or "unknown" -# -# # ---------- 旧的:按级别写固定文件 ---------- -# @classmethod -# def _setupLogger(cls, udid, name, logName, level=logging.INFO): -# """创建或获取 logger,并绑定到设备目录下的固定文件(info.log / warning.log / error.log)""" -# deviceLogDir = os.path.join(cls.logDir, cls._safe_filename(udid)) -# os.makedirs(deviceLogDir, exist_ok=True) -# logFile = os.path.join(deviceLogDir, logName) -# -# logger_name = f"{udid}_{name}" -# logger = logging.getLogger(logger_name) -# logger.setLevel(level) -# -# # 避免重复添加 handler -# if not any( -# isinstance(h, logging.FileHandler) and h.baseFilename == os.path.abspath(logFile) -# for h in logger.handlers -# ): -# fileHandler = logging.FileHandler(logFile, mode="a", encoding="utf-8") -# formatter = logging.Formatter( -# "%(asctime)s - %(name)s - %(levelname)s - %(message)s", -# datefmt="%Y-%m-%d %H:%M:%S" -# ) -# fileHandler.setFormatter(formatter) -# logger.addHandler(fileHandler) -# -# return logger -# -# @classmethod -# def info(cls, text, udid="system"): -# cls._setupLogger(udid, "infoLogger", "info.log", level=logging.INFO).info(f"[{udid}] {text}") -# -# @classmethod -# def warning(cls, text, udid="system"): -# cls._setupLogger(udid, "warningLogger", "warning.log", level=logging.WARNING).warning(f"[{udid}] {text}") -# -# @classmethod -# def error(cls, text, udid="system"): -# cls._setupLogger(udid, "errorLogger", "error.log", level=logging.ERROR).error(f"[{udid}] {text}") -# -# # ---------- 新增:按“设备+方法”分别写独立日志文件 ---------- -# @classmethod -# def _setupMethodLogger(cls, udid: str, method: str, level=logging.INFO): -# """ -# 为某设备的某个方法单独创建 logger: -# log//.log -# """ -# udid_key = cls._safe_filename(udid or "system") -# method_key = cls._safe_filename(method or "general") -# cache_key = (udid_key, method_key) -# -# # 命中缓存 -# if cache_key in cls._method_loggers: -# return cls._method_loggers[cache_key] -# -# deviceLogDir = os.path.join(cls.logDir, udid_key) -# os.makedirs(deviceLogDir, exist_ok=True) -# logFile = os.path.join(deviceLogDir, f"{method_key}.log") -# -# logger_name = f"{udid_key}.{method_key}" -# logger = logging.getLogger(logger_name) -# logger.setLevel(level) -# logger.propagate = False # 避免向根 logger 传播导致控制台重复打印 -# -# # 避免重复添加 handler -# if not any( -# isinstance(h, logging.FileHandler) and h.baseFilename == os.path.abspath(logFile) -# for h in logger.handlers -# ): -# fileHandler = logging.FileHandler(logFile, mode="a", encoding="utf-8") -# formatter = logging.Formatter( -# "%(asctime)s - %(levelname)s - %(name)s - %(message)s", -# datefmt="%Y-%m-%d %H:%M:%S" -# ) -# fileHandler.setFormatter(formatter) -# logger.addHandler(fileHandler) -# -# cls._method_loggers[cache_key] = logger -# return logger -# -# @classmethod -# def method_info(cls, text, method, udid="system"): -# """按设备+方法写 INFO 到 log//.log""" -# cls._setupMethodLogger(udid, method, level=logging.INFO).info(f"[{udid}][{method}] {text}") -# -# @classmethod -# def method_warning(cls, text, method, udid="system"): -# cls._setupMethodLogger(udid, method, level=logging.WARNING).warning(f"[{udid}][{method}] {text}") -# -# @classmethod -# def method_error(cls, text, method, udid="system"): -# cls._setupMethodLogger(udid, method, level=logging.ERROR).error(f"[{udid}][{method}] {text}") -# -# # 清空日志 -# @classmethod -# def clearLogs(cls): -# """启动时清空 log 目录下所有文件""" -# -# # 关闭所有 handler -# for name, logger in logging.Logger.manager.loggerDict.items(): -# if isinstance(logger, logging.Logger): -# for handler in logger.handlers[:]: -# try: -# handler.close() -# except Exception: -# pass -# logger.removeHandler(handler) -# -# # 删除 log 目录 -# log_path = Path(cls.logDir) -# if log_path.exists(): -# for item in log_path.iterdir(): -# if item.is_file(): -# item.unlink() -# elif item.is_dir(): -# shutil.rmtree(item) -# -# # 清缓存 -# cls._method_loggers.clear() -# -# @classmethod -# def upload_all_logs(cls, server_url, token, userId, tenantId): -# log_path = Path(cls.logDir) -# if not log_path.exists(): -# return False -# -# timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S") -# filename = f"{timestamp}_logs.zip" -# print(filename) -# zip_buf = io.BytesIO() -# with zipfile.ZipFile(zip_buf, "w", compression=zipfile.ZIP_DEFLATED) as zf: -# for p in log_path.rglob("*"): -# if p.is_file(): -# arcname = str(p.relative_to(log_path)) -# zf.write(p, arcname=arcname) -# -# zip_bytes = zip_buf.getvalue() -# -# headers = {"vvtoken": token} -# data = {"tenantId": tenantId, "userId": userId} -# -# -# files = { -# "file": (filename, io.BytesIO(zip_bytes), "application/zip") -# } -# -# # 3) 上传 -# resp = requests.post(server_url, headers=headers, data=data, files=files) -# if resp.json()['data']: -# return True -# return False - - - - - - - # -*- coding: utf-8 -*- import datetime import io diff --git a/build-tidevice.bat b/build-tidevice.bat index ca3b087..e69de29 100644 --- a/build-tidevice.bat +++ b/build-tidevice.bat @@ -1,9 +0,0 @@ -pyinstaller -F -n tidevice ^ - --hidden-import=tidevice._proto ^ - --hidden-import=tidevice._instruments ^ - --hidden-import=tidevice._usbmux ^ - --hidden-import=tidevice._wdaproxy ^ - --collect-all tidevice ^ - --noconsole ^ - --add-data="C:\Users\milk\AppData\Local\Programs\Python\Python312\Lib\site-packages\tidevice;tidevice" ^ - tidevice_entry.py \ No newline at end of file diff --git a/build.bat b/build.bat index 6e939bf..8c15162 100644 --- a/build.bat +++ b/build.bat @@ -1,9 +1,9 @@ -python -m nuitka "Module/Main.py" ^ +python -m nuitka "C:\Users\zhangkai\Desktop\20250916部署的ios项目\iOSAI\Module\Main.py" ^ --standalone ^ --msvc=latest ^ --windows-console-mode=disable ^ --remove-output ^ ---output-dir="F:/company code/AI item/20250820/iOSAI/out" ^ +--output-dir="C:\Users\zhangkai\Desktop\20250916部署的ios项目\iOSAI\out" ^ --output-filename=IOSAI ^ --include-package=Module,Utils,Entity,script ^ --include-module=flask ^ @@ -18,7 +18,7 @@ python -m nuitka "Module/Main.py" ^ --include-module=urllib3 ^ --include-module=certifi ^ --include-module=idna ^ ---include-data-dir="F:/company code/AI item/20250820/iOSAI/SupportFiles=SupportFiles" ^ ---include-data-dir="F:/company code/AI item/20250820/iOSAI/resources=resources" ^ ---include-data-files="F:/company code/AI item/20250820/iOSAI/resources/iproxy/*=resources/iproxy/" ^ ---windows-icon-from-ico="F:/company code/AI item/20250820/iOSAI/resources/icon.ico" +--include-data-dir="C:\Users\zhangkai\Desktop\20250916部署的ios项目\iOSAI\SupportFiles=SupportFiles" ^ +--include-data-dir="C:\Users\zhangkai\Desktop\20250916部署的ios项目\iOSAI\resources=resources" ^ +--include-data-files="C:\Users\zhangkai\Desktop\20250916部署的ios项目\iOSAI\resources\iproxy\*=resources/iproxy/" ^ +--windows-icon-from-ico="C:\Users\zhangkai\Desktop\20250916部署的ios项目\iOSAI\resources\icon.ico" diff --git a/resources/FlashLink.exe b/resources/FlashLink.exe index 370f320..e69de29 100644 Binary files a/resources/FlashLink.exe and b/resources/FlashLink.exe differ diff --git a/resources/add.png b/resources/add.png index aa6b19e..e69de29 100644 Binary files a/resources/add.png and b/resources/add.png differ diff --git a/resources/advertisement.png b/resources/advertisement.png index 87de404..e69de29 100644 Binary files a/resources/advertisement.png and b/resources/advertisement.png differ diff --git a/resources/back.png b/resources/back.png index a679439..e69de29 100644 Binary files a/resources/back.png and b/resources/back.png differ diff --git a/resources/comment.png b/resources/comment.png index 7043f81..e69de29 100644 Binary files a/resources/comment.png and b/resources/comment.png differ diff --git a/resources/icon.ico b/resources/icon.ico index b40c169..e69de29 100644 Binary files a/resources/icon.ico and b/resources/icon.ico differ diff --git a/resources/iproxy/idevice_id.exe b/resources/iproxy/idevice_id.exe index 4d0da43..e69de29 100644 Binary files a/resources/iproxy/idevice_id.exe and b/resources/iproxy/idevice_id.exe differ diff --git a/resources/iproxy/idevicebackup.exe b/resources/iproxy/idevicebackup.exe index c2e0c9e..e69de29 100644 Binary files a/resources/iproxy/idevicebackup.exe and b/resources/iproxy/idevicebackup.exe differ diff --git a/resources/iproxy/idevicebackup2.exe b/resources/iproxy/idevicebackup2.exe index 93be5d2..e69de29 100644 Binary files a/resources/iproxy/idevicebackup2.exe and b/resources/iproxy/idevicebackup2.exe differ diff --git a/resources/iproxy/idevicebtlogger.exe b/resources/iproxy/idevicebtlogger.exe index dcf0451..e69de29 100644 Binary files a/resources/iproxy/idevicebtlogger.exe and b/resources/iproxy/idevicebtlogger.exe differ diff --git a/resources/iproxy/idevicecrashreport.exe b/resources/iproxy/idevicecrashreport.exe index a8d2eb4..e69de29 100644 Binary files a/resources/iproxy/idevicecrashreport.exe and b/resources/iproxy/idevicecrashreport.exe differ diff --git a/resources/iproxy/idevicedate.exe b/resources/iproxy/idevicedate.exe index 8333d7a..e69de29 100644 Binary files a/resources/iproxy/idevicedate.exe and b/resources/iproxy/idevicedate.exe differ diff --git a/resources/iproxy/idevicedebug.exe b/resources/iproxy/idevicedebug.exe index 2a8fb79..e69de29 100644 Binary files a/resources/iproxy/idevicedebug.exe and b/resources/iproxy/idevicedebug.exe differ diff --git a/resources/iproxy/idevicedebugserverproxy.exe b/resources/iproxy/idevicedebugserverproxy.exe index f284c71..e69de29 100644 Binary files a/resources/iproxy/idevicedebugserverproxy.exe and b/resources/iproxy/idevicedebugserverproxy.exe differ diff --git a/resources/iproxy/idevicedevmodectl.exe b/resources/iproxy/idevicedevmodectl.exe index b02a9a6..e69de29 100644 Binary files a/resources/iproxy/idevicedevmodectl.exe and b/resources/iproxy/idevicedevmodectl.exe differ diff --git a/resources/iproxy/idevicediagnostics.exe b/resources/iproxy/idevicediagnostics.exe index 155e132..e69de29 100644 Binary files a/resources/iproxy/idevicediagnostics.exe and b/resources/iproxy/idevicediagnostics.exe differ diff --git a/resources/iproxy/ideviceenterrecovery.exe b/resources/iproxy/ideviceenterrecovery.exe index 70d5d5e..e69de29 100644 Binary files a/resources/iproxy/ideviceenterrecovery.exe and b/resources/iproxy/ideviceenterrecovery.exe differ diff --git a/resources/iproxy/ideviceimagemounter.exe b/resources/iproxy/ideviceimagemounter.exe index 5a650c9..e69de29 100644 Binary files a/resources/iproxy/ideviceimagemounter.exe and b/resources/iproxy/ideviceimagemounter.exe differ diff --git a/resources/iproxy/ideviceinfo.exe b/resources/iproxy/ideviceinfo.exe index 55a4eed..e69de29 100644 Binary files a/resources/iproxy/ideviceinfo.exe and b/resources/iproxy/ideviceinfo.exe differ diff --git a/resources/iproxy/idevicename.exe b/resources/iproxy/idevicename.exe index 8abae73..e69de29 100644 Binary files a/resources/iproxy/idevicename.exe and b/resources/iproxy/idevicename.exe differ diff --git a/resources/iproxy/idevicenotificationproxy.exe b/resources/iproxy/idevicenotificationproxy.exe index 5303f2c..e69de29 100644 Binary files a/resources/iproxy/idevicenotificationproxy.exe and b/resources/iproxy/idevicenotificationproxy.exe differ diff --git a/resources/iproxy/idevicepair.exe b/resources/iproxy/idevicepair.exe index 150d8ba..e69de29 100644 Binary files a/resources/iproxy/idevicepair.exe and b/resources/iproxy/idevicepair.exe differ diff --git a/resources/iproxy/ideviceprovision.exe b/resources/iproxy/ideviceprovision.exe index 1805e12..e69de29 100644 Binary files a/resources/iproxy/ideviceprovision.exe and b/resources/iproxy/ideviceprovision.exe differ diff --git a/resources/iproxy/idevicerestore.exe b/resources/iproxy/idevicerestore.exe index 96974ff..e69de29 100644 Binary files a/resources/iproxy/idevicerestore.exe and b/resources/iproxy/idevicerestore.exe differ diff --git a/resources/iproxy/idevicescreenshot.exe b/resources/iproxy/idevicescreenshot.exe index c67f274..e69de29 100644 Binary files a/resources/iproxy/idevicescreenshot.exe and b/resources/iproxy/idevicescreenshot.exe differ diff --git a/resources/iproxy/idevicesetlocation.exe b/resources/iproxy/idevicesetlocation.exe index 21ec82a..e69de29 100644 Binary files a/resources/iproxy/idevicesetlocation.exe and b/resources/iproxy/idevicesetlocation.exe differ diff --git a/resources/iproxy/idevicesyslog.exe b/resources/iproxy/idevicesyslog.exe index 2a81ef0..e69de29 100644 Binary files a/resources/iproxy/idevicesyslog.exe and b/resources/iproxy/idevicesyslog.exe differ diff --git a/resources/iproxy/inetcat.exe b/resources/iproxy/inetcat.exe index d0fa729..e69de29 100644 Binary files a/resources/iproxy/inetcat.exe and b/resources/iproxy/inetcat.exe differ diff --git a/resources/iproxy/irecovery.exe b/resources/iproxy/irecovery.exe index 8982a6a..e69de29 100644 Binary files a/resources/iproxy/irecovery.exe and b/resources/iproxy/irecovery.exe differ diff --git a/resources/iproxy/libcrypto-3.dll b/resources/iproxy/libcrypto-3.dll index dce1596..e69de29 100644 Binary files a/resources/iproxy/libcrypto-3.dll and b/resources/iproxy/libcrypto-3.dll differ diff --git a/resources/iproxy/libcurl.dll b/resources/iproxy/libcurl.dll index 773a027..e69de29 100644 Binary files a/resources/iproxy/libcurl.dll and b/resources/iproxy/libcurl.dll differ diff --git a/resources/iproxy/libssl-3.dll b/resources/iproxy/libssl-3.dll index b9667cd..e69de29 100644 Binary files a/resources/iproxy/libssl-3.dll and b/resources/iproxy/libssl-3.dll differ diff --git a/resources/iproxy/plistutil.exe b/resources/iproxy/plistutil.exe index d3e5a38..e69de29 100644 Binary files a/resources/iproxy/plistutil.exe and b/resources/iproxy/plistutil.exe differ diff --git a/resources/iproxy/readline.dll b/resources/iproxy/readline.dll index 3ea56da..e69de29 100644 Binary files a/resources/iproxy/readline.dll and b/resources/iproxy/readline.dll differ diff --git a/resources/iproxy/tidevice.exe b/resources/iproxy/tidevice.exe index c40b70f..e69de29 100644 Binary files a/resources/iproxy/tidevice.exe and b/resources/iproxy/tidevice.exe differ diff --git a/resources/like.png b/resources/like.png index 46c6c25..e69de29 100644 Binary files a/resources/like.png and b/resources/like.png differ diff --git a/resources/search.png b/resources/search.png index 8aef0b5..e69de29 100644 Binary files a/resources/search.png and b/resources/search.png differ diff --git a/script/ScriptManager.py b/script/ScriptManager.py index b48a3d4..4a98825 100644 --- a/script/ScriptManager.py +++ b/script/ScriptManager.py @@ -361,10 +361,12 @@ class ScriptManager(): else: print(f"找不到输入框") - input.clear_text() - time.sleep(1) - # 输入主播id - input.set_text(f"{aid or '暂无数据'}\n") + input = session.xpath('//XCUIElementTypeSearchField') + if input.exists: + input.clear_text() + time.sleep(1) + # 输入主播id + input.set_text(f"{aid or '暂无数据'}\n") # 定位 "关注" 按钮 通过关注按钮的位置点击主播首页 @@ -449,11 +451,11 @@ class ScriptManager(): time.sleep(2) msgButton = AiUtils.getSendMesageButton(session) time.sleep(2) - if msgButton is not None: - LogManager.method_info("找到发消息按钮了", "关注打招呼", udid) - print("找到发消息按钮了") + if msgButton.exists: # 进入聊天页面 msgButton.click() + LogManager.method_info("找到发消息按钮了", "关注打招呼", udid) + print("找到发消息按钮了") else: LogManager.method_info("没有识别出发消息按钮", "关注打招呼", udid) print("没有识别出发消息按钮") @@ -494,10 +496,12 @@ class ScriptManager(): LogManager.method_info(f"即将发送的私信内容:{msg}", "关注打招呼", udid) # 准备发送一条信息 - chatInput.click() - time.sleep(2) - # 发送消息 - chatInput.set_text(f"{msg or '暂无数据'}\n") + chatInput = session.xpath("//TextView") + if chatInput.exists: + chatInput.click() + time.sleep(2) + # 发送消息 + chatInput.set_text(f"{msg or '暂无数据'}\n") # input.set_text(f"{aid or '暂无数据'}\n") diff --git a/tidevice_entry.py b/tidevice_entry.py index 2cc9028..e69de29 100644 --- a/tidevice_entry.py +++ b/tidevice_entry.py @@ -1,18 +0,0 @@ -# from tidevice.__main__ import main -# if __name__ == '__main__': -# main() - - -# tidevice_entry.py -import sys, traceback, os -from tidevice.__main__ import main - -if __name__ == "__main__": - try: - main() - except Exception: - # 把 traceback 写到日志文件,但**不输出到控制台** - with open(os.path.expanduser("~/tidevice_crash.log"), "a", encoding="utf-8") as f: - traceback.print_exc(file=f) - # 静默退出,**返回码 1**(父进程只认 returncode) - sys.exit(1) \ No newline at end of file