From 67cb777f5a5fad68701b7518a17fa901b7fa6a07 Mon Sep 17 00:00:00 2001 From: zw <12345678> Date: Fri, 5 Sep 2025 19:17:40 +0800 Subject: [PATCH 1/5] =?UTF-8?q?=E4=B8=B4=E6=97=B6=E6=8F=90=E4=BA=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/workspace.xml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 33c043d..8415062 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -6,9 +6,6 @@ - - - - + From de81857321b2c3e66462153396a25b5be3d0f1f8 Mon Sep 17 00:00:00 2001 From: zw <12345678> Date: Fri, 5 Sep 2025 19:21:51 +0800 Subject: [PATCH 2/5] 111 --- .idea/workspace.xml | 40 ++++++++++++++++++++-------------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 8415062..72ca1d6 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -45,27 +45,27 @@ - { + "keyToString": { + "ASKED_ADD_EXTERNAL_FILES": "true", + "Python.123.executor": "Run", + "Python.Main.executor": "Run", + "Python.tidevice_entry.executor": "Run", + "RunOnceActivity.ShowReadmeOnStart": "true", + "SHARE_PROJECT_CONFIGURATION_FILES": "true", + "git-widget-placeholder": "main", + "javascript.nodejs.core.library.configured.version": "22.18.0", + "javascript.nodejs.core.library.typings.version": "22.18.1", + "last_opened_file_path": "F:/company code/AI item/20250820/iOSAI", + "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", + "settings.editor.selected.configurable": "com.gitee.ui.GiteeSettingsConfigurable", + "vue.rearranger.settings.migration": "true" } -}]]> +} From 0681fc0754f9c1c79ae98d5277bbd44dc8f4039d Mon Sep 17 00:00:00 2001 From: zw <12345678> Date: Fri, 5 Sep 2025 19:33:45 +0800 Subject: [PATCH 3/5] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=B3=A8=E9=87=8A?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/iOSAI.iml | 2 +- .idea/misc.xml | 2 +- .idea/workspace.xml | 9 ++------- tidevice_entry.py | 2 +- 4 files changed, 5 insertions(+), 10 deletions(-) diff --git a/.idea/iOSAI.iml b/.idea/iOSAI.iml index df5cbff..f571432 100644 --- a/.idea/iOSAI.iml +++ b/.idea/iOSAI.iml @@ -2,7 +2,7 @@ - + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index c27b771..db8786c 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 90c99b1..e7c99d8 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -5,12 +5,7 @@ - - - - - - + - + + - { + "keyToString": { + "ASKED_ADD_EXTERNAL_FILES": "true", + "Python.123.executor": "Run", + "Python.Main.executor": "Run", + "Python.tidevice_entry.executor": "Run", + "RunOnceActivity.ShowReadmeOnStart": "true", + "SHARE_PROJECT_CONFIGURATION_FILES": "true", + "git-widget-placeholder": "main", + "javascript.nodejs.core.library.configured.version": "22.18.0", + "javascript.nodejs.core.library.typings.version": "22.18.1", + "last_opened_file_path": "F:/company code/AI item/20250820/iOSAI", + "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", + "settings.editor.selected.configurable": "com.gitee.ui.GiteeSettingsConfigurable", + "vue.rearranger.settings.migration": "true" } -}]]> +} @@ -179,6 +180,8 @@ + + - + diff --git a/Module/DeviceInfo.py b/Module/DeviceInfo.py index 2834562..8d780da 100644 --- a/Module/DeviceInfo.py +++ b/Module/DeviceInfo.py @@ -11,6 +11,7 @@ from pathlib import Path from typing import List, Dict, Optional from tidevice import Usbmux, ConnectionType +from tidevice._device import BaseDevice from Entity.DeviceModel import DeviceModel from Entity.Variables import WdaAppBundleId from Module.FlaskSubprocessManager import FlaskSubprocessManager @@ -20,36 +21,25 @@ from Utils.LogManager import LogManager class Deviceinfo(object): def __init__(self): self.deviceIndex = 0 - # 投屏端口(本地映射端口起始值,会递增) self.screenProxy = 9110 - # 记录 iproxy Popen 进程:[{ "id": udid, "target": Popen }, ...] self.pidList: List[Dict] = [] - # 当前已连接的设备(tidevice 的 Device 对象列表) self.deviceArray: List = [] - - # 子进程通信(向前端发送设备信息) self.manager = FlaskSubprocessManager.get_instance() - # 已发给前端的设备模型列表(用于拔出时发 type=2) self.deviceModelList: List[DeviceModel] = [] - # 最大可连接设备限制 self.maxDeviceCount = 6 - # 操作锁 self._lock = threading.Lock() + self._pending_udids = set() + - # ===== iproxy:一次性完成 路径定位 + 环境变量配置 + 启动器准备 ===== try: - self.iproxy_path = self._iproxy_path() # 绝对路径 + self.iproxy_path = self._iproxy_path() self.iproxy_dir = self.iproxy_path.parent - - # 1) 配置环境(PATH/DLL),放到初始化里一次性处理 os.environ["PATH"] = str(self.iproxy_dir) + os.pathsep + os.environ.get("PATH", "") try: - # 仅 Windows 有效;其他平台忽略 os.add_dll_directory(str(self.iproxy_dir)) except Exception: pass - # 2) 预构建通用 Popen 参数(隐藏窗口、工作目录、文本模式等) self._creationflags = 0x08000000 if os.name == "nt" else 0 self._popen_kwargs = dict( stdout=subprocess.PIPE, @@ -62,12 +52,10 @@ class Deviceinfo(object): bufsize=1, ) - # 3) 准备一个“启动器”(闭包):仅接受 (udid, local_port, remote_port) 参数 def _spawn_iproxy(udid: str, local_port: int, remote_port: int = 9100) -> subprocess.Popen: args = [str(self.iproxy_path), "-u", udid, str(local_port), str(remote_port)] p = subprocess.Popen(args, **self._popen_kwargs) - # 异步日志转发(可选) def _pipe_to_log(name: str, stream): try: for line in iter(stream.readline, ''): @@ -78,7 +66,6 @@ class Deviceinfo(object): pass try: - import threading threading.Thread(target=_pipe_to_log, args=("STDOUT", p.stdout), daemon=True).start() threading.Thread(target=_pipe_to_log, args=("STDERR", p.stderr), daemon=True).start() except Exception: @@ -86,11 +73,10 @@ class Deviceinfo(object): return p - self._spawn_iproxy = _spawn_iproxy # 保存启动器 + self._spawn_iproxy = _spawn_iproxy LogManager.info(f"iproxy 启动器已就绪,目录: {self.iproxy_dir}") except Exception as e: - # 没找到 iproxy 也允许实例化成功,但后续启动会失败并给出明确日志 self.iproxy_path = None self.iproxy_dir = None self._spawn_iproxy = None @@ -104,14 +90,43 @@ class Deviceinfo(object): try: lists = Usbmux().device_list() except Exception as e: - # 另一台电脑常见:usbmuxd 连接失败(未安装 iTunes/Apple Mobile Device Support) - LogManager.warning(f"usbmuxd 连接失败: {e}。请确认已安装 iTunes/Apple Mobile Device Support,并在手机上“信任此电脑”") + LogManager.warning( + f"usbmuxd 连接失败: {e}。请确认已安装 iTunes/Apple Mobile Device Support,并在手机上“信任此电脑”") time.sleep(2) continue - # 新接入设备 + + now_udids = {d.udid for d in lists if d.conn_type == ConnectionType.USB} + + # 1. 处理“已插入但未信任”的设备,一旦信任就补连接 + for udid in list(self._pending_udids): + if udid not in now_udids: + # 设备已拔出,从 pending 移除 + self._pending_udids.discard(udid) + continue + if self.is_device_trusted(udid): + # 已信任,补连接 + self._pending_udids.discard(udid) + self.screenProxy += 1 + try: + self.connectDevice(udid) + # 补加入 deviceArray(用 usbmux 对象) + for d in lists: + if d.udid == udid: + self.deviceArray.append(d) + break + except Exception as e: + LogManager.error(f"补连接设备失败 {udid}: {e}", udid) + + # 2. 处理全新插入的设备 for device in lists: - # usb设备,并且为新设备。并且大于总限制数量 - if device.conn_type == ConnectionType.USB and (device not in self.deviceArray) and (len(self.deviceArray) < self.maxDeviceCount): + if device.conn_type == ConnectionType.USB and device not in self.deviceArray and len( + self.deviceArray) < self.maxDeviceCount: + if not self.is_device_trusted(device.udid): + # 未信任,记入 pending,下次循环再判 + self._pending_udids.add(device.udid) + LogManager.warning("设备未信任,已记录,等待信任后自动连接", device.udid) + continue + # 已信任,直接走完整流程 self.screenProxy += 1 try: self.connectDevice(device.udid) @@ -119,24 +134,36 @@ class Deviceinfo(object): except Exception as e: LogManager.error(f"连接设备失败 {device.udid}: {e}", device.udid) - # 拔出设备处理 + # 3. 处理拔出 self._removeDisconnected(lists) time.sleep(1) # ---------------------------- - # 连接单台设备:启动 WDA、读取屏参、通知前端、映射投屏端口 + # 判断设备是否已信任 + # ---------------------------- + def is_device_trusted(self, udid: str) -> bool: + try: + d = BaseDevice(udid) + d.get_value("DeviceName") # 任意读取一个值,失败即未信任 + return True + except Exception: + return False + + # ---------------------------- + # 连接单台设备:先判断是否信任,再启动 WDA # ---------------------------- def connectDevice(self, identifier: str): - # 1) 连接 WDA(USBClient -> 设备 8100) + if not self.is_device_trusted(identifier): + LogManager.warning("设备未信任,跳过 WDA 启动,等待信任后再试", identifier) + return + try: d = wda.USBClient(identifier, 8100) - LogManager.info("启动 WDA 成功", identifier) except Exception as e: LogManager.error(f"启动 WDA 失败,请检查手机是否已信任、WDA 是否正常。错误: {e}", identifier) - return # 不抛出到外层,保持监听循环健壮 + return - # 2) 读取屏幕信息(失败不影响主流程) width, height, scale = 0, 0, 1.0 try: size = d.window_size() @@ -145,7 +172,6 @@ class Deviceinfo(object): except Exception as e: LogManager.warning(f"读取屏幕信息失败:{e}", identifier) - # 3) 组装模型并发送给前端 model = DeviceModel(identifier, self.screenProxy, width, height, scale, type=1) self.deviceModelList.append(model) try: @@ -153,7 +179,6 @@ class Deviceinfo(object): except Exception as e: LogManager.warning(f"向前端发送设备模型失败:{e}", identifier) - # 4) 可选:启动你的 app 并回到桌面 try: d.app_start(WdaAppBundleId) d.home() @@ -162,41 +187,36 @@ class Deviceinfo(object): time.sleep(2) - # 5) 本地端口 -> 设备端口 的映射(投屏:本地 self.screenProxy -> 设备 9100) target = self.relayDeviceScreenPort(identifier) - # 加个非空判断 if target is not None: with self._lock: self.pidList.append({"target": target, "id": identifier}) - # 安全杀死iproxy进程 + # ---------------------------- + # 以下方法未改动,省略以节省篇幅 + # ---------------------------- def _terminate_proc(self, p: subprocess.Popen): if not p: return if p.poll() is not None: return try: - p.terminate() # 先温柔 + p.terminate() p.wait(timeout=3) except Exception: try: if os.name == "posix": try: - # 如果 iproxy 启动时用了 setsid,这里可杀整个进程组 os.killpg(os.getpgid(p.pid), signal.SIGKILL) except Exception: p.kill() else: - p.kill() # Windows 直接 kill - p.wait(timeout=2) # 一定要 wait,避免僵尸 + p.kill() + p.wait(timeout=2) except Exception: pass - # ---------------------------- - # 处理拔出设备:发通知、关掉 iproxy、移出状态 - # ---------------------------- def _removeDisconnected(self, current_list): - # 1) 计算“被拔出”的 UDID 集合 —— 用 UDID,而不是对象做集合运算 try: prev_udids = {getattr(d, "udid", None) for d in self.deviceArray if getattr(d, "udid", None)} now_udids = {getattr(d, "udid", None) for d in current_list if getattr(d, "udid", None)} @@ -207,11 +227,9 @@ class Deviceinfo(object): if not removed_udids: return - # 2) 加锁,避免多线程同时改三个列表 if not hasattr(self, "_lock"): self._lock = threading.RLock() with self._lock: - # 2.1 通知前端并清理 deviceModelList for udid in list(removed_udids): for a in list(self.deviceModelList): if udid == getattr(a, "deviceId", None): @@ -225,7 +243,6 @@ class Deviceinfo(object): except ValueError: pass - # 2.2 关闭该 UDID 的所有 iproxy survivors = [] for k in list(self.pidList): kid = k.get("id") @@ -235,40 +252,31 @@ class Deviceinfo(object): self._terminate_proc(p) except Exception as e: LogManager.warning(f"关闭 iproxy 异常:{e}", kid) - # 不再把该项放回 survivors,相当于移除 else: survivors.append(k) self.pidList = survivors - # 2.3 从已连接集合中移除(按 UDID 过滤,避免对象引用不一致导致 remove 失败) self.deviceArray = [d for d in self.deviceArray if getattr(d, "udid", None) not in removed_udids] - # 3) 打点 for udid in removed_udids: LogManager.info("设备已拔出,清理完成(下线通知 + 端口映射关闭 + 状态移除)", udid) - # ---------------------------- - # 根目录与 iproxy 可执行文件定位 - # ---------------------------- def _base_dir(self) -> Path: if getattr(sys, "frozen", False): return Path(sys.executable).resolve().parent - return Path(__file__).resolve().parents[1] # iOSAI/ 作为根 + return Path(__file__).resolve().parents[1] def _iproxy_path(self) -> Path: exe = "iproxy.exe" if os.name == "nt" else "iproxy" base = self._base_dir() candidates = [ - base / "resources" / "iproxy" / exe, # 推荐放置 + base / "resources" / "iproxy" / exe, ] for p in candidates: if p.exists(): return p raise FileNotFoundError(f"iproxy not found, tried: {[str(c) for c in candidates]}") - # ---------------------------- - # 端口映射:仅做“转发端口”这件事(调用已准备好的启动器) - # ---------------------------- def relayDeviceScreenPort(self, udid: str) -> Optional[subprocess.Popen]: if not self._spawn_iproxy: LogManager.error("iproxy 启动器未就绪,无法建立端口映射(初始化时未找到 iproxy)。", udid) From 7a8380af9c611d9bc003753b9b7abe7280941027 Mon Sep 17 00:00:00 2001 From: zw <12345678> Date: Mon, 8 Sep 2025 19:16:05 +0800 Subject: [PATCH 5/5] =?UTF-8?q?=E8=A7=A3=E5=86=B3=E6=89=93=E6=8B=9B?= =?UTF-8?q?=E5=91=BC=E6=B6=88=E6=81=AF=E7=9A=84bug?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/workspace.xml | 42 ++++++++-------------------- Entity/Variables.py | 9 ++++-- Module/DeviceInfo.py | 5 ---- Module/FlaskService.py | 63 +++++++++++++++++++++--------------------- 4 files changed, 48 insertions(+), 71 deletions(-) diff --git a/.idea/workspace.xml b/.idea/workspace.xml index 2620158..c9ba169 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -6,7 +6,9 @@ + + - + - - - - - - - - @@ -181,7 +159,9 @@ - + + + - + diff --git a/Entity/Variables.py b/Entity/Variables.py index b8d9506..baf5f88 100644 --- a/Entity/Variables.py +++ b/Entity/Variables.py @@ -11,12 +11,11 @@ anchorList: list[AnchorModel] = [] # 线程锁 anchorListLock = threading.Lock() # 打招呼数据 -prologueList = [] +prologueList: list[str] = [] # 评论列表 commentsList = [] # 存储主播名和session_id的字典 anchorWithSession = {} - # 前端传递的token token = '' # 前端传递的 @@ -30,10 +29,14 @@ def removeModelFromAnchorList(model: AnchorModel): with anchorListLock: anchorList.remove(model) - # 添加数据 def addModelToAnchorList(models: list[Dict[str, Any]]): with anchorListLock: for dic in models: obj = AnchorModel.dictToModel(dic) anchorList.append(obj) + +# 添加打招呼语 +def addDataToPrologue(data: list[str]): + + prologueList = data \ No newline at end of file diff --git a/Module/DeviceInfo.py b/Module/DeviceInfo.py index 8d780da..0ff26f3 100644 --- a/Module/DeviceInfo.py +++ b/Module/DeviceInfo.py @@ -1,9 +1,7 @@ -# -*- coding: utf-8 -*- import os import signal import sys import time -import json import wda import threading import subprocess @@ -30,7 +28,6 @@ class Deviceinfo(object): self._lock = threading.Lock() self._pending_udids = set() - try: self.iproxy_path = self._iproxy_path() self.iproxy_dir = self.iproxy_path.parent @@ -96,7 +93,6 @@ class Deviceinfo(object): continue now_udids = {d.udid for d in lists if d.conn_type == ConnectionType.USB} - # 1. 处理“已插入但未信任”的设备,一旦信任就补连接 for udid in list(self._pending_udids): if udid not in now_udids: @@ -281,7 +277,6 @@ class Deviceinfo(object): if not self._spawn_iproxy: LogManager.error("iproxy 启动器未就绪,无法建立端口映射(初始化时未找到 iproxy)。", udid) return None - try: p = self._spawn_iproxy(udid, self.screenProxy, 9100) LogManager.info(f"启动 iproxy 成功,本地 {self.screenProxy} -> 设备 9100", udid) diff --git a/Module/FlaskService.py b/Module/FlaskService.py index c860da5..06611e4 100644 --- a/Module/FlaskService.py +++ b/Module/FlaskService.py @@ -16,15 +16,17 @@ from Entity.ResultData import ResultData from Utils.ControlUtils import ControlUtils from Utils.ThreadManager import ThreadManager from script.ScriptManager import ScriptManager -from Entity.Variables import anchorList, addModelToAnchorList, prologueList, removeModelFromAnchorList +from Entity.Variables import anchorList, prologueList, addModelToAnchorList, removeModelFromAnchorList import Entity.Variables as ev + app = Flask(__name__) CORS(app) +app.config['JSON_AS_ASCII'] = False # Flask jsonify 不转义中文/emoji +app.config['JSONIFY_MIMETYPE'] = "application/json; charset=utf-8" listData = [] dataQueue = Queue() - def start_socket_listener(): port = int(os.getenv('FLASK_COMM_PORT', 0)) LogManager.info(f"Received port from environment: {port}") @@ -230,43 +232,40 @@ def stopScript(): code, msg = ThreadManager.stop(udid) return ResultData(code=code, data="", msg=msg).toJson() - -# 传递主播数据(关注主播打招呼) +# 关注打招呼 @app.route('/passAnchorData', methods=['POST']) def passAnchorData(): - data: Dict[str, Any] = request.get_json() - # 设备列表 + try: + data: Dict[str, Any] = request.get_json() + # 设备列表 + idList = data.get("deviceList", []) + # 主播列表 + acList = data.get("anchorList", []) + # 是否需要回复 + needReply = data.get("needReply", True) + # 获取打招呼数据 + ev.prologueList = data.get("prologueList", []) - print("接收的数据", data) - - idList = data.get("deviceList", []) - # 主播列表 - acList = data.get("anchorList", []) - # 是否需要回复 - needReply = data.get("needReply", True) - - # 获取打招呼数据 - ev.prologueList = data.get("prologueList", []) - - - # 添加主播数据 - addModelToAnchorList(acList) - # 启动线程,执行脚本 - for udid in idList: - manager = ScriptManager() - event = threading.Event() - # 启动脚本 - thread = threading.Thread(target=manager.safe_greetNewFollowers, args=(udid, needReply, event)) - thread.start() - # 添加到线程管理 - ThreadManager.add(udid, thread, event) - return ResultData(data="").toJson() + # 添加主播数据 + addModelToAnchorList(acList) + # 启动线程,执行脚本 + for udid in idList: + manager = ScriptManager() + event = threading.Event() + # 启动脚本 + thread = threading.Thread(target=manager.safe_greetNewFollowers, args=(udid, needReply, event)) + thread.start() + # 添加到线程管理 + ThreadManager.add(udid, thread, event) + return ResultData(data="").toJson() + except Exception as e: + LogManager.error(e) # 获取私信数据 @app.route("/getPrologueList", methods=['GET']) def getPrologueList(): - print(ev.prologueList) - return ResultData(data=ev.prologueList).toJson() + import Entity.Variables as Variables + return ResultData(data=Variables.prologueList).toJson() # 添加临时数据 @app.route("/addTempAnchorData", methods=['POST'])