Compare commits

..

2 Commits

Author SHA1 Message Date
f21b44cf19 Merge remote-tracking branch 'origin/main'
# Conflicts:
#	.idea/workspace.xml
#	Module/DeviceInfo.py
#	build.bat
#	resources/FlashLink.exe
2025-09-17 22:26:04 +08:00
5d63cc7961 20250904-初步功能已完成 2025-09-17 22:24:16 +08:00
72 changed files with 110 additions and 413 deletions

127
.gitignore vendored
View File

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

View File

@@ -1,4 +1,4 @@
from getpass import fallback_getpass
# from getpass import fallback_getpass
# 设备模型

View File

@@ -275,6 +275,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", [])
@@ -432,7 +433,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()

22
Module/log/acList.json Normal file
View File

@@ -0,0 +1,22 @@
[
{
"anchorId": "giulia.roma",
"country": "意大利"
},
{
"anchorId": "marcelo_brasil",
"country": "巴西"
},
{
"anchorId": "anna_krasnova",
"country": "俄罗斯"
},
{
"anchorId": "lee_jiwoo",
"country": "韩国"
},
{
"anchorId": "fatima_dxb",
"country": "阿联酋"
}
]

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.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

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

View File

@@ -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/<udid>/<method>.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/<udid>/<method>.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

View File

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

View File

@@ -3,6 +3,7 @@ python -m nuitka "Module/Main.py" ^
--msvc=latest ^
--windows-console-mode=disable ^
--remove-output ^
--output-dir="F:/company code/AI item/20250820/iOSAI/out" ^
--output-filename=IOSAI ^
--include-package=Module,Utils,Entity,script ^
--include-module=flask ^
@@ -17,7 +18,7 @@ python -m nuitka "Module/Main.py" ^
--include-module=urllib3 ^
--include-module=certifi ^
--include-module=idna ^
--include-data-dir="E:/code/Python/iOSAI/SupportFiles=SupportFiles" ^
--include-data-dir="E:/code/Python/iOSAI/resources=resources" ^
--include-data-files="E:/code/Python/iOSAI/resources/iproxy/*=resources/iproxy/" ^
--windows-icon-from-ico="E:/code/Python/iOSAI/resources/icon.ico"
--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"

Binary file not shown.

Before

Width:  |  Height:  |  Size: 899 B

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.6 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 51 KiB

After

Width:  |  Height:  |  Size: 0 B

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.

Before

Width:  |  Height:  |  Size: 5.1 KiB

After

Width:  |  Height:  |  Size: 0 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.3 KiB

After

Width:  |  Height:  |  Size: 0 B

View File

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

View File

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