commit 3c60d3c7d23f4251686f57302ed26b96ccfb6189 Author: zw <12345678> Date: Fri Aug 1 13:43:51 2025 +0800 创建仓库 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b6bbb41 --- /dev/null +++ b/.gitignore @@ -0,0 +1,124 @@ +# 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 + +# 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/ \ No newline at end of file diff --git a/.idea/.gitignore b/.idea/.gitignore new file mode 100644 index 0000000..13566b8 --- /dev/null +++ b/.idea/.gitignore @@ -0,0 +1,8 @@ +# Default ignored files +/shelf/ +/workspace.xml +# Editor-based HTTP Client requests +/httpRequests/ +# Datasource local storage ignored files +/dataSources/ +/dataSources.local.xml diff --git a/.idea/iOSAI.iml b/.idea/iOSAI.iml new file mode 100644 index 0000000..6cb8b9a --- /dev/null +++ b/.idea/iOSAI.iml @@ -0,0 +1,10 @@ + + + + + + + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml new file mode 100644 index 0000000..5dcfaf3 --- /dev/null +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -0,0 +1,21 @@ + + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml new file mode 100644 index 0000000..105ce2d --- /dev/null +++ b/.idea/inspectionProfiles/profiles_settings.xml @@ -0,0 +1,6 @@ + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml new file mode 100644 index 0000000..491e71f --- /dev/null +++ b/.idea/misc.xml @@ -0,0 +1,7 @@ + + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml new file mode 100644 index 0000000..a841d58 --- /dev/null +++ b/.idea/modules.xml @@ -0,0 +1,8 @@ + + + + + + + + \ No newline at end of file diff --git a/Entity/DeviceModel.py b/Entity/DeviceModel.py new file mode 100644 index 0000000..50644b8 --- /dev/null +++ b/Entity/DeviceModel.py @@ -0,0 +1,18 @@ + +# 设备模型 +class DeviceModel(object): + def __init__(self, deviceId, screenPort, type): + super(DeviceModel, self).__init__() + self.deviceId = deviceId + self.screenPort = screenPort + # 1 添加 2删除 + self.type = type + + + # 转字典 + def toDict(self): + return { + 'deviceId': self.deviceId, + 'screenPort': self.screenPort, + 'type': self.type + } \ No newline at end of file diff --git a/Entity/ResultData.py b/Entity/ResultData.py new file mode 100644 index 0000000..f0c19f2 --- /dev/null +++ b/Entity/ResultData.py @@ -0,0 +1,16 @@ +import json + +# 返回数据模型 +class ResultData(object): + def __init__(self, code=200, data=None, msg="获取成功"): + super(ResultData, self).__init__() + self.code = code + self.data = data + self.msg = msg + + def toJson(self): + return json.dumps({ + "code": self.code, + "data": self.data, + "msg": self.msg + }, ensure_ascii=False) # ensure_ascii=False 确保中文不会被转义 \ No newline at end of file diff --git a/Entity/Variables.py b/Entity/Variables.py new file mode 100644 index 0000000..3cc17e4 --- /dev/null +++ b/Entity/Variables.py @@ -0,0 +1,4 @@ +# Tik Tok app bundle id +tikTokApp = "com.zhiliaoapp.musically" +# wda apple bundle id +WdaAppBundleId = "com.vv.wda.xctrunner" diff --git a/Flask/FlaskService.py b/Flask/FlaskService.py new file mode 100644 index 0000000..979adf8 --- /dev/null +++ b/Flask/FlaskService.py @@ -0,0 +1,175 @@ +import json +import os +import socket +import threading +from queue import Queue +import tidevice +import wda +from flask import Flask, request +from flask_cors import CORS +from Entity.ResultData import ResultData +from script.ScriptManager import ScriptManager + +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('/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"] + t = tidevice.Device(udid) + + # 获取已安装的应用列表 + apps = [] + for app in t.installation.iter_installed(): + apps.append({ + "name": app.get("CFBundleDisplayName", "Unknown"), + "bundleId": app.get("CFBundleIdentifier", "Unknown"), + "version": app.get("CFBundleShortVersionString", "Unknown"), + "path": app.get("Path", "Unknown") + }) + + # 筛选非系统级应用(过滤掉以 com.apple 开头的系统应用) + non_system_apps = [app for app in apps if not app["bundleId"].startswith("com.apple")] + return ResultData(data=non_system_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") + x = body.get("x") + y = body.get("y") + client = wda.USBClient(udid) + session = client.session() + session.appium_settings({"snapshotMaxDepth": 0}) + 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") + + # 启动脚本 + threading.Thread(target=ScriptManager.growAccount, args=(udid,)).start() + return ResultData(data="").toJson() + +if __name__ == '__main__': + app.run("0.0.0.0", port=5000, debug=True, use_reloader=False) diff --git a/Module/DeviceInfo.py b/Module/DeviceInfo.py new file mode 100644 index 0000000..381aaea --- /dev/null +++ b/Module/DeviceInfo.py @@ -0,0 +1,96 @@ +import subprocess +import threading +import time +import wda +from tidevice import Usbmux +from Entity.DeviceModel import DeviceModel +from Entity.Variables import tikTokApp, WdaAppBundleId +from Module.FlaskSubprocessManager import FlaskSubprocessManager + +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 = [] + + # 监听设备连接 + def startDeviceListener(self): + while True: + lists = Usbmux().device_list() + # 添加设备逻辑 + for device in lists: + if device not in self.deviceArray: + self.screenProxy += 1 + self.connectDevice(device.udid) + self.deviceArray.append(device) + # 创建模型 + model = DeviceModel(device.udid,self.screenProxy,type=1) + self.deviceModelList.append(model) + # 发送数据 + self.manager.send(model.toDict()) + + # 处理拔出设备的逻辑 + 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) + + 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): + d = wda.USBClient(identifier, 8100) + d.app_start(WdaAppBundleId) + time.sleep(2) + d.app_start(tikTokApp) + target = self.relayDeviceScreenPort() + self.pidList.append({ + "target": target, + "id": identifier + }) + + # 转发设备端口 + def relayDeviceScreenPort(self): + try: + command = f"iproxy.exe {self.screenProxy} 9100" + # 创建一个没有窗口的进程 + startupinfo = subprocess.STARTUPINFO() + startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW + startupinfo.wShowWindow = 0 + r = subprocess.Popen(command, shell=True, startupinfo=startupinfo) + return r + except Exception as e: + print(e) + return 0 diff --git a/Module/FlaskSubprocessManager.py b/Module/FlaskSubprocessManager.py new file mode 100644 index 0000000..32d6db7 --- /dev/null +++ b/Module/FlaskSubprocessManager.py @@ -0,0 +1,101 @@ +import subprocess +import threading +import atexit +import json +import os +import socket +import time +from typing import Optional, Union, Dict, List + +class FlaskSubprocessManager: + _instance: Optional['FlaskSubprocessManager'] = None + _lock: threading.Lock = threading.Lock() + + def __new__(cls): + with cls._lock: + if cls._instance is None: + cls._instance = super().__new__(cls) + cls._instance._init_manager() + return cls._instance + + def _init_manager(self): + self.process: Optional[subprocess.Popen] = None + self.comm_port = self._find_available_port() + self._stop_event = threading.Event() + atexit.register(self.stop) + + def _find_available_port(self): + """动态获取可用端口""" + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.bind(('0.0.0.0', 0)) + return s.getsockname()[1] + + def start(self): + """启动子进程(Windows兼容方案)""" + with self._lock: + if self.process is not None: + raise RuntimeError("子进程已在运行中!") + # 通过环境变量传递通信端口 + env = os.environ.copy() + env['FLASK_COMM_PORT'] = str(self.comm_port) + + 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 # 指定子进程的环境变量 + ) + print(f"Flask子进程启动 (PID: {self.process.pid}, 通信端口: {self.comm_port})") + + # 将日志通过主进程输出 + def print_output(): + while True: + output = self.process.stdout.readline() + if not output: + break + print(output.strip()) + + while True: + error = self.process.stderr.readline() + if not error: + break + print(f"Error: {error.strip()}") + + threading.Thread(target=print_output, daemon=True).start() + + def send(self, data: Union[str, Dict, List]) -> bool: + """通过Socket发送数据""" + try: + if not isinstance(data, str): + data = json.dumps(data) + # 等待子进程启动并准备好 + time.sleep(1) # 延时1秒,根据实际情况调整 + with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: + s.connect(('127.0.0.1', self.comm_port)) + s.sendall((data + "\n").encode('utf-8')) + return True + except ConnectionRefusedError: + print(f"连接被拒绝,确保子进程在端口 {self.comm_port} 上监听") + return False + except Exception as e: + print(f"发送失败: {e}") + return False + + def stop(self): + with self._lock: + if self.process and self.process.poll() is None: + print(f"[INFO] Stopping Flask child process (PID: {self.process.pid})...") + self.process.terminate() + self.process.wait() + print("[INFO] Flask child process stopped.") + self._stop_event.set() + else: + print("[INFO] No Flask child process to stop.") + + @classmethod + def get_instance(cls) -> 'FlaskSubprocessManager': + return cls() \ No newline at end of file diff --git a/Module/Main.py b/Module/Main.py new file mode 100644 index 0000000..8248797 --- /dev/null +++ b/Module/Main.py @@ -0,0 +1,11 @@ +from Module.DeviceInfo import Deviceinfo +from Module.FlaskSubprocessManager import FlaskSubprocessManager + +if __name__ == "__main__": + print("启动flask") + manager = FlaskSubprocessManager.get_instance() + manager.start() + + print("启动主线程") + info = Deviceinfo() + info.startDeviceListener() \ No newline at end of file diff --git a/Module/iproxy.exe b/Module/iproxy.exe new file mode 100644 index 0000000..3527899 Binary files /dev/null and b/Module/iproxy.exe differ diff --git a/resources/bgv.png b/resources/bgv.png new file mode 100644 index 0000000..c334413 Binary files /dev/null and b/resources/bgv.png differ diff --git a/resources/like.png b/resources/like.png new file mode 100644 index 0000000..c3e59a3 Binary files /dev/null and b/resources/like.png differ diff --git a/script/AiTools.py b/script/AiTools.py new file mode 100644 index 0000000..94f1251 --- /dev/null +++ b/script/AiTools.py @@ -0,0 +1,109 @@ +import os +import time +import cv2 +import numpy as np +from PIL import Image + + +# 工具类 +class AiTools(object): + + @classmethod + def find_image_in_image( + cls, + smallImageUrl, + bigImageUrl, + match_threshold=0.90, + consecutive_required=3, + scales=None + ): + + if scales is None: + scales = [0.5, 0.75, 1.0, 1.25, 1.5] + + template = cv2.imread(smallImageUrl, cv2.IMREAD_COLOR) + # if template is None: + # raise Exception(f"❌ 无法读取模板 '{smallImageUrl}'") + + template_gray = cv2.cvtColor(template, cv2.COLOR_BGR2GRAY) + + cap = cv2.imread(bigImageUrl, cv2.IMREAD_COLOR) + # if not cap.isOpened(): + # print(f"❌ 无法打开视频流: {bigImageUrl}") + # return None + + detected_consecutive_frames = 0 + + print("🚀 正在检测爱心图标...") + while True: + print("死了") + ret, frame = cap.read() + if not ret or frame is None: + time.sleep(0.01) + continue + print("哈哈哈") + frame_gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + + current_frame_has_match = False + best_match_val = 0 + best_match_loc = None + best_match_w_h = None + print("aaaaaaaaaaaa") + for scale in scales: + resized_template = cv2.resize(template_gray, (0, 0), fx=scale, fy=scale) + th, tw = resized_template.shape[:2] + + if th > frame_gray.shape[0] or tw > frame_gray.shape[1]: + continue + + result = cv2.matchTemplate(frame_gray, resized_template, cv2.TM_CCOEFF_NORMED) + _, max_val, _, max_loc = cv2.minMaxLoc(result) + + if max_val > best_match_val: + best_match_val = max_val + best_match_loc = max_loc + best_match_w_h = (tw, th) + if max_val >= match_threshold: + current_frame_has_match = True + print("break 了") + break + print("bbbbbbbbbbbbbbbbbbbbbb") + if current_frame_has_match: + print("111111") + detected_consecutive_frames += 1 + last_detection_info = (best_match_loc, best_match_w_h, best_match_val) + else: + print("2222222") + detected_consecutive_frames = 0 + last_detection_info = None + + if detected_consecutive_frames >= consecutive_required and last_detection_info: + print("333333333") + top_left, (w, h), match_val = last_detection_info + center_x = top_left[0] + w // 2 + center_y = top_left[1] + h // 2 + + print(f"🎯 成功识别爱心图标: 中心坐标=({center_x}, {center_y}), 匹配度={match_val:.4f}") + return center_y, center_y + else: + return -1, -1 + cap.release() + print("释放了") + return -1, -1 + + + + @classmethod + def imagePath(cls, name): + current_file_path = os.path.abspath(__file__) + # 获取当前文件所在的目录(即script目录) + current_dir = os.path.dirname(current_file_path) + # 由于script目录位于项目根目录下一级,因此需要向上一级目录移动两次 + project_root = os.path.abspath(os.path.join(current_dir, '..')) + # 构建资源文件的完整路径,向上两级目录,然后进入 resources 目录 + resource_path = os.path.abspath(os.path.join(project_root, 'resources', name + ".png")).replace('/', '\\\\') + return resource_path + + + + diff --git a/script/ScriptManager.py b/script/ScriptManager.py new file mode 100644 index 0000000..7ceacda --- /dev/null +++ b/script/ScriptManager.py @@ -0,0 +1,46 @@ +import cv2 +import lxml +import wda +from lxml import etree + +from script.AiTools import AiTools + + +# 脚本管理类 +class ScriptManager(): + def __init__(self): + super().__init__() + + # 养号 + @classmethod + def growAccount(self, udid): + client = wda.USBClient(udid) + session = client.session() + session.appium_settings({"snapshotMaxDepth": 0}) + + deviceWidth = client.window_size().width + deviceHeight = client.window_size().height + + img = client.screenshot() + tempPath = "resources/bgv.png" + img.save(tempPath) + + bgvPath = AiTools.imagePath("bgv") + likePath = AiTools.imagePath("like") + + x, y = AiTools.find_image_in_image(bgvPath, likePath) + print(x, y) + # client.tap(end[0] / 3 - 2, end[1] / 3 - 2) + + + # xml = session.source() + # print(xml) + # root = etree.fromstring(xml.encode('utf-8')) + # try: + # msg = client.xpath('label="收件箱"') + # msg.click() + # print(msg) + # except Exception as e: + # print(e) + + diff --git a/script/screenshot.png b/script/screenshot.png new file mode 100644 index 0000000..dc5d7f4 Binary files /dev/null and b/script/screenshot.png differ