临时提交

This commit is contained in:
zw
2025-08-15 20:04:59 +08:00
parent 6332bda929
commit 690b17ec58
66 changed files with 5075 additions and 156 deletions

View File

@@ -1,111 +1,199 @@
import subprocess
import threading
# -*- coding: utf-8 -*-
import os
import sys
import time
import json
import wda
import threading
import subprocess
from pathlib import Path
from typing import List, Dict, Optional
from tidevice import Usbmux
from Entity.DeviceModel import DeviceModel
from Entity.Variables import WdaAppBundleId
from Module.FlaskSubprocessManager import FlaskSubprocessManager
from Utils.LogManager import LogManager
threadLock = threading.Lock()
class Deviceinfo(object):
def __init__(self):
self.deviceIndex = 0
# 投屏端口
# 投屏端口(本地映射端口起始值,会递增)
self.screenProxy = 9110
# 存放pid的数组
self.pidList = []
# 设备列表
self.deviceArray = []
# 获取到县城管理类
self.manager = FlaskSubprocessManager.get_instance()
# 给前端的设备模型数组
self.deviceModelList = []
# 监听设备连接
# 记录 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] = []
# ----------------------------
# 监听设备连接(死循环,内部捕获异常)
# ----------------------------
def startDeviceListener(self):
LogManager.info("Device Listener started", "listener")
while True:
lists = Usbmux().device_list()
# 添加设备逻辑
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并在手机上“信任此电脑”", "listener")
time.sleep(2)
continue
# 新接入设备
for device in lists:
if device not in self.deviceArray:
self.screenProxy += 1
self.connectDevice(device.udid)
self.deviceArray.append(device)
try:
self.connectDevice(device.udid)
self.deviceArray.append(device)
except Exception as e:
LogManager.error(f"连接设备失败 {device.udid}: {e}", device.udid)
# 处理拔出设备的逻辑
def removeDevice():
set1 = set(self.deviceArray)
set2 = set(lists)
difference = set1 - set2
differenceList = list(difference)
for i in differenceList:
for j in self.deviceArray:
# 判断是否为差异设备
if i.udid == j.udid:
# 从设备模型中删除数据
for a in self.deviceModelList:
if i.udid == a.deviceId:
a.type = 2
# 发送数据
self.manager.send(a.toDict())
self.deviceModelList.remove(a)
# 拔出设备处理
self._removeDisconnected(lists)
for k in self.pidList:
# 干掉端口短发进程
if j.udid == k["id"]:
target = k["target"]
target.kill()
self.pidList.remove(k)
# 删除已经拔出的设备
self.deviceArray.remove(j)
removeDevice()
time.sleep(1)
# 连接设备
def connectDevice(self, identifier):
# ----------------------------
# 连接单台设备:启动 WDA、读取屏参、通知前端、映射投屏端口
# ----------------------------
def connectDevice(self, identifier: str):
# 1) 连接 WDAUSBClient -> 设备 8100
try:
d = wda.USBClient(identifier, 8100)
LogManager.info("启动wda成功", identifier)
size = d.window_size()
width = size.width
height = size.height
scale = d.scale
# 创建模型
model = DeviceModel(identifier, self.screenProxy, width, height, scale, type=1)
self.deviceModelList.append(model)
# 发送数据
self.manager.send(model.toDict())
LogManager.info("启动 WDA 成功", identifier)
except Exception as e:
LogManager.error("启动wda失败请检查wda是否正常", identifier)
return
LogManager.error(f"启动 WDA 失败请检查手机是否已信任、WDA 是否正常。错误: {e}", identifier)
return # 不抛出到外层,保持监听循环健壮
d.app_start(WdaAppBundleId)
d.home()
time.sleep(2)
target = self.relayDeviceScreenPort(identifier)
self.pidList.append({
"target": target,
"id": identifier
})
# 转发设备端口
def relayDeviceScreenPort(self, udid):
# 2) 读取屏幕信息(失败不影响主流程)
width, height, scale = 0, 0, 1.0
try:
command = f"iproxy.exe -u {udid} {self.screenProxy} 9100"
# 创建一个没有窗口的进程
startupinfo = subprocess.STARTUPINFO()
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
startupinfo.wShowWindow = 0
r = subprocess.Popen(command, shell=True, startupinfo=startupinfo)
return r
size = d.window_size()
width, height = size.width, size.height
scale = d.scale
except Exception as e:
print(e)
return 0
LogManager.warning(f"读取屏幕信息失败:{e}", identifier)
# 3) 组装模型并发送给前端
model = DeviceModel(identifier, self.screenProxy, width, height, scale, type=1)
self.deviceModelList.append(model)
try:
self.manager.send(model.toDict())
except Exception as e:
LogManager.warning(f"向前端发送设备模型失败:{e}", identifier)
# 4) 可选:启动你的 app 并回到桌面
try:
d.app_start(WdaAppBundleId)
d.home()
except Exception as e:
LogManager.warning(f"启动/切回桌面失败:{e}", identifier)
time.sleep(2)
# 5) 本地端口 -> 设备端口 的映射(投屏:本地 self.screenProxy -> 设备 9100
target = self.relayDeviceScreenPort(identifier)
self.pidList.append({"target": target, "id": identifier})
# ----------------------------
# 处理拔出设备:发通知、关掉 iproxy、移出状态
# ----------------------------
def _removeDisconnected(self, current_list):
set1 = set(self.deviceArray)
set2 = set(current_list)
difference = list(set1 - set2) # 在旧集合中但不在新集合中 -> 已拔出
for i in difference:
udid = i.udid
# 1) 通知前端type = 2
for a in list(self.deviceModelList):
if udid == a.deviceId:
a.type = 2
try:
self.manager.send(a.toDict())
except Exception as e:
LogManager.warning(f"发送下线事件失败:{e}", udid)
self.deviceModelList.remove(a)
# 2) 关掉对应的 iproxy
for k in list(self.pidList):
if udid == k["id"]:
target = k.get("target")
try:
if target and target.poll() is None:
target.kill()
except Exception:
pass
self.pidList.remove(k)
# 3) 从已连接集合中移除
try:
self.deviceArray.remove(i)
except Exception:
pass
# ----------------------------
# 路径:打包/源码都能找到根目录、iproxy 目录和 iproxy 可执行文件
# ----------------------------
def _base_dir(self) -> Path:
"""
打包后:返回 exe 所在目录;
源码运行返回项目根目录Module 的上一级)
"""
if getattr(sys, "frozen", False):
return Path(sys.executable).resolve().parent
return Path(__file__).resolve().parents[1] # iOSAI/ 作为根
def _iproxy_dir(self) -> Path:
"""返回打包后的 iproxy 目录(你现在放在 Module/iproxy/"""
return self._base_dir() / "Module" / "iproxy"
def _iproxy_path(self) -> Path:
"""返回 iproxy 可执行文件路径Windows 为 iproxy.exe"""
exe_name = "iproxy.exe" if os.name == "nt" else "iproxy"
return self._iproxy_dir() / exe_name
# ----------------------------
# 端口映射:启动 iproxy设置 cwd 和 PATH隐藏窗口
# ----------------------------
def relayDeviceScreenPort(self, udid: str) -> Optional[subprocess.Popen]:
try:
iproxy = self._iproxy_path()
iproxy_dir = self._iproxy_dir()
if not iproxy.exists():
raise FileNotFoundError(f"iproxy not found: {iproxy}")
# 继承环境并把 iproxy 目录加入 PATH方便 DLL 解析
env = os.environ.copy()
env["PATH"] = str(iproxy_dir) + os.pathsep + env.get("PATH", "")
# Windows 隐藏子进程窗口
CREATE_NO_WINDOW = 0x08000000 if os.name == "nt" else 0
p = subprocess.Popen(
[str(iproxy), "-u", udid, str(self.screenProxy), "9100"],
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
creationflags=CREATE_NO_WINDOW,
cwd=str(iproxy_dir), # 关键:工作目录设为 iproxy 所在目录
env=env, # 关键:把 iproxy_dir 注入 PATH
text=True, # 你后面如果要读 stdout/stderr 的话更方便
encoding="utf-8",
bufsize=1
)
LogManager.info(f"启动 iproxy 成功,本地 {self.screenProxy} -> 设备 9100", udid)
return p
except Exception as e:
LogManager.error(f"启动 iproxy 失败:{e}", udid)
return None

279
Module/FlaskService.py Normal file
View File

@@ -0,0 +1,279 @@
import json
import os
import socket
import threading
import warnings
from queue import Queue
from typing import Any, Dict
from Utils.AiUtils import AiUtils
from Utils.Requester import Requester
import tidevice
import wda
from flask import Flask, request
from flask_cors import CORS
from Entity.ResultData import ResultData
from Utils.ControlUtils import ControlUtils
from Utils.ThreadManager import ThreadManager
from script.ScriptManager import ScriptManager
from Entity.Variables import accountToken
from Entity.Variables import anchorList, addModelToAnchorList
app = Flask(__name__)
CORS(app)
listData = []
dataQueue = Queue()
def start_socket_listener():
port = int(os.getenv('FLASK_COMM_PORT', 0))
print(f"Received port from environment: {port}")
if port <= 0:
print("⚠️ 未获取到通信端口跳过Socket监听")
return
try:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
# 设置端口复用,避免端口被占用时无法绑定
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
# 尝试绑定端口
try:
s.bind(('127.0.0.1', port))
print(f"[INFO] Socket successfully bound to port {port}")
except Exception as bind_error:
print(f"[ERROR] ❌ 端口绑定失败: {bind_error}")
return
# 开始监听
s.listen()
print(f"[INFO] Socket listener started on port {port}, waiting for connections...")
while True:
try:
print(f"[INFO] Waiting for a new connection on port {port}...")
conn, addr = s.accept()
print(f"[INFO] Connection accepted from: {addr}")
raw_data = conn.recv(1024).decode('utf-8').strip()
print(f"[INFO] Raw data received: {raw_data}")
data = json.loads(raw_data)
print(f"[INFO] Parsed data: {data}")
dataQueue.put(data)
except Exception as conn_error:
print(f"[ERROR] ❌ 连接处理失败: {conn_error}")
except Exception as e:
print(f"[ERROR] ❌ Socket服务启动失败: {e}")
# 在独立线程中启动Socket服务
listener_thread = threading.Thread(target=start_socket_listener, daemon=True)
listener_thread.start()
@app.route('/passToken', methods=['POST'])
def passToken():
data = request.get_json()
accountToken = data['token']
print(accountToken)
Requester.requestComments()
return ResultData(data="").toJson()
# 获取设备列表
@app.route('/deviceList', methods=['GET'])
def deviceList():
while not dataQueue.empty():
obj = dataQueue.get()
type = obj["type"]
if type == 1:
listData.append(obj)
else:
for data in listData:
if data.get("deviceId") == obj.get("deviceId") and data.get("screenPort") == obj.get("screenPort"):
listData.remove(data)
return ResultData(data=listData).toJson()
# 获取设备应用列表
@app.route('/deviceAppList', methods=['POST'])
def deviceAppList():
param = request.get_json()
udid = param["udid"]
apps = ControlUtils.getDeviceAppList(udid)
return ResultData(data=apps).toJson()
# 打开指定app
@app.route('/launchApp', methods=['POST'])
def launchApp():
body = request.get_json()
udid = body.get("udid")
bundleId = body.get("bundleId")
t = tidevice.Device(udid)
t.app_start(bundleId)
return ResultData(data="").toJson()
# 回到首页
@app.route('/toHome', methods=['POST'])
def toHome():
body = request.get_json()
udid = body.get("udid")
client = wda.USBClient(udid)
client.home()
return ResultData(data="").toJson()
# 点击事件
@app.route('/tapAction', methods=['POST'])
def tapAction():
body = request.get_json()
udid = body.get("udid")
client = wda.USBClient(udid)
session = client.session()
session.appium_settings({"snapshotMaxDepth": 0})
x = body.get("x")
y = body.get("y")
session.tap(x, y)
return ResultData(data="").toJson()
# 拖拽事件
@app.route('/swipeAction', methods=['POST'])
def swipeAction():
body = request.get_json()
udid = body.get("udid")
direction = body.get("direction")
client = wda.USBClient(udid)
session = client.session()
session.appium_settings({"snapshotMaxDepth": 0})
if direction == 1:
session.swipe_up()
elif direction == 2:
session.swipe_left()
elif direction == 3:
session.swipe_down()
else:
session.swipe_right()
return ResultData(data="").toJson()
# 长按事件
@app.route('/longPressAction', methods=['POST'])
def longPressAction():
body = request.get_json()
udid = body.get("udid")
x = body.get("x")
y = body.get("y")
client = wda.USBClient(udid)
session = client.session()
session.appium_settings({"snapshotMaxDepth": 5})
session.tap_hold(x, y, 1.0)
return ResultData(data="").toJson()
# 养号
@app.route('/growAccount', methods=['POST'])
def growAccount():
body = request.get_json()
udid = body.get("udid")
manager = ScriptManager()
event = threading.Event()
# 启动脚本
thread = threading.Thread(target=manager.growAccount, args=(udid, event))
thread.start()
# 添加到线程管理
ThreadManager.add(udid, thread, event)
return ResultData(data="").toJson()
# 观看直播
@app.route("/watchLiveForGrowth", methods=['POST'])
def watchLiveForGrowth():
body = request.get_json()
udid = body.get("udid")
manager = ScriptManager()
event = threading.Event()
thread = threading.Thread(target=manager.watchLiveForGrowth, args=(udid, event))
thread.start()
# 添加到线程管理
ThreadManager.add(udid, thread, event)
return ResultData(data="").toJson()
# 停止脚本
@app.route("/stopScript", methods=['POST'])
def stopScript():
body = request.get_json()
udid = body.get("udid")
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()
# 设备列表
idList = data.get("deviceList", [])
# 主播列表
acList = data.get("anchorList", [])
# 是否需要回复
needReply = data.get("needReply", False)
# 添加主播数据
addModelToAnchorList(acList)
# 启动线程,执行脚本
for udid in idList:
manager = ScriptManager()
event = threading.Event()
# 启动脚本
thread = threading.Thread(target=manager.greetNewFollowers, args=(udid, needReply, event))
thread.start()
# 添加到线程管理
ThreadManager.add(udid, thread, event)
return ResultData(data="").toJson()
# 添加临时数据
@app.route("/addTempAnchorData", methods=['POST'])
def addTempAnchorData():
data = request.get_json()
addModelToAnchorList(data)
return ResultData(data="").toJson()
# 获取当前屏幕上的聊天信息
@app.route("/getChatTextInfo", methods=['POST'])
def getChatTextInfo():
data = request.get_json()
udid = data.get("udid")
client = wda.USBClient(udid)
session = client.session()
xml = session.source()
result = AiUtils.extract_messages_from_xml(xml)
return ResultData(data=result).toJson()
# 监控消息
@app.route("/replyMessages", methods=['POST'])
def monitorMessages():
body = request.get_json()
udid = body.get("udid")
manager = ScriptManager()
event = threading.Event()
thread = threading.Thread(target=manager.replyMessages, args=(udid, event))
thread.start()
# 添加到线程管理
ThreadManager.add(udid, thread, event)
return ResultData(data="").toJson()
if __name__ == '__main__':
app.run("0.0.0.0", port=5000, debug=True, use_reloader=False)

View File

@@ -1,10 +1,12 @@
import subprocess
import sys
import threading
import atexit
import json
import os
import socket
import time
from pathlib import Path
from typing import Optional, Union, Dict, List
class FlaskSubprocessManager:
@@ -20,38 +22,59 @@ class FlaskSubprocessManager:
def _init_manager(self):
self.process: Optional[subprocess.Popen] = None
self.comm_port = self._find_available_port()
self.comm_port = 34567
self._stop_event = threading.Event()
atexit.register(self.stop)
def _find_available_port(self):
"""动态获取可用端口"""
# 可以把 _find_available_port 留着备用,但 start 前先校验端口是否被占用
def _is_port_busy(self, port: int) -> bool:
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
s.bind(('0.0.0.0', 0))
return s.getsockname()[1]
s.settimeout(0.2)
return s.connect_ex(("127.0.0.1", port)) == 0
# 启动flask
def start(self):
"""启动子进程Windows兼容方案"""
"""启动 Flask 子进程(兼容打包后的 exe 和源码运行"""
with self._lock:
if self.process is not None:
raise RuntimeError("子进程已在运行中!")
# 通过环境变量传递通信端口
env = os.environ.copy()
env['FLASK_COMM_PORT'] = str(self.comm_port)
env["FLASK_COMM_PORT"] = str(self.comm_port)
# —— 解析打包 exe 的稳健写法 ——
exe_path = Path(sys.executable).resolve()
if exe_path.name.lower() in ("python.exe", "pythonw.exe"):
# Nuitka 某些场景里 sys.executable 可能指向 dist\python.exe并不存在
exe_path = Path(sys.argv[0]).resolve()
is_frozen = exe_path.suffix.lower() == ".exe" and exe_path.exists()
if is_frozen:
# 打包后的 exe用当前 exe 自举
cmd = [str(exe_path), "--role=flask"]
cwd = str(exe_path.parent)
else:
# 源码运行:模块方式更稳
cmd = [sys.executable, "-m", "Module.Main", "--role=flask"]
cwd = str(Path(__file__).resolve().parent) # Module 目录
print(f"[DEBUG] spawn: {cmd} (cwd={cwd}) exists(exe)={os.path.exists(cmd[0])}")
self.process = subprocess.Popen(
['python', 'Flask/FlaskService.py'], # 启动一个子进程 FlaskService.py
stdin=subprocess.PIPE, # 标准输入流,用于向子进程发送数据
stdout=subprocess.PIPE, # 标准输出流,用于接收子进程的输出
stderr=subprocess.PIPE, # 标准错误流,用于接收子进程的错误信息
text=True, # 以文本模式打开流,否则以二进制模式打开
bufsize=1, # 缓冲区大小设置为 1表示行缓冲
encoding='utf-8', # 指定编码为 UTF-8确保控制台输出不会报错
env=env # 指定子进程的环境变量
cmd,
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
text=True,
encoding="utf-8",
errors="replace", # 新增:遇到非 UTF-8 字节用 <20> 代替,避免崩溃
bufsize=1,
env=env,
cwd=cwd,
)
print(f"Flask子进程启动 (PID: {self.process.pid}, 通信端口: {self.comm_port})")
print(f"Flask子进程启动 (PID: {self.process.pid}, 端口: {self.comm_port})")
# 将日志通过主进程输出
def print_output(stream, stream_name):
while True:
line = stream.readline()
@@ -59,7 +82,6 @@ class FlaskSubprocessManager:
break
print(f"{stream_name}: {line.strip()}")
# 启动两个线程分别处理 stdout 和 stderr
threading.Thread(target=print_output, args=(self.process.stdout, "STDOUT"), daemon=True).start()
threading.Thread(target=print_output, args=(self.process.stderr, "STDERR"), daemon=True).start()

View File

@@ -1,17 +1,66 @@
import os
import sys
import time
from pathlib import Path
from Module.DeviceInfo import Deviceinfo
from Module.FlaskSubprocessManager import FlaskSubprocessManager
from Utils.LogManager import LogManager
# 项目入口
if __name__ == "__main__":
# 清空日志
LogManager.clearLogs()
time.sleep(1)
# 确定 exe 或 py 文件所在目录
BASE = Path(getattr(sys, 'frozen', False) and sys.executable or __file__).resolve().parent
LOG_DIR = BASE / "log"
LOG_DIR.mkdir(exist_ok=True) # 确保 log 目录存在
print(f"日志目录: {LOG_DIR}")
def _run_flask_role():
from Module import FlaskService
port = int(os.getenv("FLASK_COMM_PORT", "34567")) # 固定端口的兜底仍是 34567
app_factory = getattr(FlaskService, "create_app", None)
app = app_factory() if callable(app_factory) else FlaskService.app
app.run(host="0.0.0.0", port=port, debug=False, use_reloader=False)
if "--role=flask" in sys.argv:
_run_flask_role()
sys.exit(0)
# 项目入口
# ... 省略前面的 import 和函数 ...
if __name__ == "__main__":
# 清空日志等
LogManager.clearLogs()
# 启动 Flask 子进程
manager = FlaskSubprocessManager.get_instance()
manager.start()
info = Deviceinfo()
info.startDeviceListener()
# 设备监听(即使失败/很快返回,也不会导致主进程退出)
try:
info = Deviceinfo()
info.startDeviceListener()
except Exception as e:
print("[WARN] Device listener not running:", e)
# === 保活:阻塞主线程,直到收到 Ctrl+C/关闭 ===
import threading, time, signal
stop = threading.Event()
def _handle(_sig, _frm):
stop.set()
# Windows 上 SIGINT/SIGTERM 都可以拦到
try:
signal.signal(signal.SIGINT, _handle)
signal.signal(signal.SIGTERM, _handle)
except Exception:
pass # 某些环境可能不支持,忽略
try:
while not stop.is_set():
time.sleep(1)
finally:
# 进程退出前记得把子进程关掉
manager.stop()

BIN
Module/iproxy/bz2.dll Normal file

Binary file not shown.

BIN
Module/iproxy/getopt.dll Normal file

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
Module/iproxy/inetcat.exe Normal file

Binary file not shown.

BIN
Module/iproxy/irecovery.exe Normal file

Binary file not shown.

Binary file not shown.

BIN
Module/iproxy/libcurl.dll Normal file

Binary file not shown.

BIN
Module/iproxy/libssl-3.dll Normal file

Binary file not shown.

BIN
Module/iproxy/plistutil.exe Normal file

Binary file not shown.

BIN
Module/iproxy/readline.dll Normal file

Binary file not shown.

BIN
Module/iproxy/zip.dll Normal file

Binary file not shown.

BIN
Module/iproxy/zlib1.dll Normal file

Binary file not shown.