创建仓库

This commit is contained in:
zw
2025-08-01 13:43:51 +08:00
commit 3c60d3c7d2
20 changed files with 760 additions and 0 deletions

124
.gitignore vendored Normal file
View File

@@ -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/

8
.idea/.gitignore generated vendored Normal file
View File

@@ -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

10
.idea/iOSAI.iml generated Normal file
View File

@@ -0,0 +1,10 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="PYTHON_MODULE" version="4">
<component name="NewModuleRootManager">
<content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</content>
<orderEntry type="jdk" jdkName="Python 3.12" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>

View File

@@ -0,0 +1,21 @@
<component name="InspectionProjectProfileManager">
<profile version="1.0">
<option name="myName" value="Project Default" />
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
<option name="ignoredPackages">
<value>
<list size="1">
<item index="0" class="java.lang.String" itemvalue="PySide6" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list>
<option value="N806" />
</list>
</option>
</inspection_tool>
</profile>
</component>

View File

@@ -0,0 +1,6 @@
<component name="InspectionProjectProfileManager">
<settings>
<option name="USE_PROJECT_PROFILE" value="false" />
<version value="1.0" />
</settings>
</component>

7
.idea/misc.xml generated Normal file
View File

@@ -0,0 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="Black">
<option name="sdkName" value="Python 3.12 (iOSAI)" />
</component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
</project>

8
.idea/modules.xml generated Normal file
View File

@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/iOSAI.iml" filepath="$PROJECT_DIR$/.idea/iOSAI.iml" />
</modules>
</component>
</project>

18
Entity/DeviceModel.py Normal file
View File

@@ -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
}

16
Entity/ResultData.py Normal file
View File

@@ -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 确保中文不会被转义

4
Entity/Variables.py Normal file
View File

@@ -0,0 +1,4 @@
# Tik Tok app bundle id
tikTokApp = "com.zhiliaoapp.musically"
# wda apple bundle id
WdaAppBundleId = "com.vv.wda.xctrunner"

175
Flask/FlaskService.py Normal file
View File

@@ -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)

96
Module/DeviceInfo.py Normal file
View File

@@ -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

View File

@@ -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()

11
Module/Main.py Normal file
View File

@@ -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()

BIN
Module/iproxy.exe Normal file

Binary file not shown.

BIN
resources/bgv.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

BIN
resources/like.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

109
script/AiTools.py Normal file
View File

@@ -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

46
script/ScriptManager.py Normal file
View File

@@ -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)

BIN
script/screenshot.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB