20250904-初步功能已完成

This commit is contained in:
2025-09-17 22:24:16 +08:00
parent db67024157
commit 5d63cc7961
75 changed files with 267 additions and 461 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

93
.idea/workspace.xml generated
View File

@@ -5,9 +5,81 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="20250904-初步功能已完成"> <list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="20250904-初步功能已完成">
<change afterPath="$PROJECT_DIR$/Module/log/acList.json" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.gitignore" beforeDir="false" afterPath="$PROJECT_DIR$/.gitignore" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Entity/DeviceModel.py" beforeDir="false" afterPath="$PROJECT_DIR$/Entity/DeviceModel.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Module/DeviceInfo.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/DeviceInfo.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Module/DeviceInfo.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/DeviceInfo.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Module/FlaskService.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/FlaskService.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/14.0.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/14.1.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/14.2.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/14.3.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/14.4.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/14.5.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/14.6.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/14.7.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/14.8.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/15.0.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/15.1.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/15.2.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/15.3.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/15.4.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/15.5.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/15.6.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/15.7.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/15.8.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/16.0.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/16.1.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/16.2.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/16.3.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/16.4.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/16.5.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/16.6.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/SupportFiles/16.7.zip" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/Utils/ControlUtils.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/ControlUtils.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Utils/ControlUtils.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/ControlUtils.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Utils/LogManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/LogManager.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/build-tidevice.bat" beforeDir="false" afterPath="$PROJECT_DIR$/build-tidevice.bat" afterDir="false" />
<change beforePath="$PROJECT_DIR$/build.bat" beforeDir="false" afterPath="$PROJECT_DIR$/build.bat" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/FlashLink.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/FlashLink.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/add.png" beforeDir="false" afterPath="$PROJECT_DIR$/resources/add.png" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/advertisement.png" beforeDir="false" afterPath="$PROJECT_DIR$/resources/advertisement.png" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/back.png" beforeDir="false" afterPath="$PROJECT_DIR$/resources/back.png" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/comment.png" beforeDir="false" afterPath="$PROJECT_DIR$/resources/comment.png" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/icon.ico" beforeDir="false" afterPath="$PROJECT_DIR$/resources/icon.ico" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevice_id.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevice_id.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicebackup.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicebackup.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicebackup2.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicebackup2.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicebtlogger.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicebtlogger.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicecrashreport.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicecrashreport.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicedate.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicedate.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicedebug.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicedebug.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicedebugserverproxy.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicedebugserverproxy.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicedevmodectl.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicedevmodectl.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicediagnostics.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicediagnostics.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/ideviceenterrecovery.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/ideviceenterrecovery.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/ideviceimagemounter.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/ideviceimagemounter.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/ideviceinfo.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/ideviceinfo.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicename.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicename.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicenotificationproxy.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicenotificationproxy.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicepair.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicepair.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/ideviceprovision.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/ideviceprovision.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicerestore.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicerestore.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicescreenshot.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicescreenshot.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicesetlocation.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicesetlocation.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/idevicesyslog.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/idevicesyslog.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/inetcat.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/inetcat.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/irecovery.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/irecovery.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/libcrypto-3.dll" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/libcrypto-3.dll" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/libcurl.dll" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/libcurl.dll" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/libssl-3.dll" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/libssl-3.dll" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/plistutil.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/plistutil.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/readline.dll" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/readline.dll" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/iproxy/tidevice.exe" beforeDir="false" afterPath="$PROJECT_DIR$/resources/iproxy/tidevice.exe" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/like.png" beforeDir="false" afterPath="$PROJECT_DIR$/resources/like.png" afterDir="false" />
<change beforePath="$PROJECT_DIR$/resources/search.png" beforeDir="false" afterPath="$PROJECT_DIR$/resources/search.png" afterDir="false" />
<change beforePath="$PROJECT_DIR$/script/ScriptManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/script/ScriptManager.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/tidevice_entry.py" beforeDir="false" afterPath="$PROJECT_DIR$/tidevice_entry.py" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -65,9 +137,10 @@
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;, &quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;, &quot;SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;main&quot;, &quot;git-widget-placeholder&quot;: &quot;main&quot;,
&quot;ignore.virus.scanning.warn.message&quot;: &quot;true&quot;,
&quot;javascript.nodejs.core.library.configured.version&quot;: &quot;20.17.0&quot;, &quot;javascript.nodejs.core.library.configured.version&quot;: &quot;20.17.0&quot;,
&quot;javascript.nodejs.core.library.typings.version&quot;: &quot;20.17.58&quot;, &quot;javascript.nodejs.core.library.typings.version&quot;: &quot;20.17.58&quot;,
&quot;last_opened_file_path&quot;: &quot;F:/company code/AI item/20250820/iOSAI&quot;, &quot;last_opened_file_path&quot;: &quot;C:/Users/zhangkai/Desktop/20250916部署的ios项目/iOSAI&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;, &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;, &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;, &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
@@ -189,6 +262,7 @@
<option name="IS_MODULE_SDK" value="true" /> <option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" /> <option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" /> <option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/Utils/test.py" /> <option name="SCRIPT_NAME" value="$PROJECT_DIR$/Utils/test.py" />
<option name="PARAMETERS" value="" /> <option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" /> <option name="SHOW_COMMAND_LINE" value="false" />
@@ -200,8 +274,8 @@
</configuration> </configuration>
<recent_temporary> <recent_temporary>
<list> <list>
<item itemvalue="Python.test" />
<item itemvalue="Python.123" /> <item itemvalue="Python.123" />
<item itemvalue="Python.test" />
<item itemvalue="Python.Test" /> <item itemvalue="Python.Test" />
<item itemvalue="Python.12" /> <item itemvalue="Python.12" />
</list> </list>
@@ -210,7 +284,8 @@
<component name="SharedIndexes"> <component name="SharedIndexes">
<attachedChunks> <attachedChunks>
<set> <set>
<option value="bundled-python-sdk-ce6832f46686-7b97d883f26b-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-252.25557.178" /> <option value="bundled-js-predefined-1d06a55b98c1-0b3e54e931b4-JavaScript-PY-241.18034.82" />
<option value="bundled-python-sdk-975db3bf15a3-2767605e8bc2-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-241.18034.82" />
</set> </set>
</attachedChunks> </attachedChunks>
</component> </component>
@@ -276,6 +351,12 @@
<workItem from="1757506636968" duration="5910000" /> <workItem from="1757506636968" duration="5910000" />
<workItem from="1757567423145" duration="16668000" /> <workItem from="1757567423145" duration="16668000" />
<workItem from="1757998910052" duration="3676000" /> <workItem from="1757998910052" duration="3676000" />
<workItem from="1758095627214" duration="795000" />
<workItem from="1758111876484" duration="4474000" />
<workItem from="1758116478749" duration="425000" />
<workItem from="1758116920643" duration="529000" />
<workItem from="1758117476093" duration="632000" />
<workItem from="1758118143217" duration="888000" />
</task> </task>
<task id="LOCAL-00001" summary="ai 开始测试"> <task id="LOCAL-00001" summary="ai 开始测试">
<option name="closed" value="true" /> <option name="closed" value="true" />
@@ -347,8 +428,8 @@
<SUITE FILE_PATH="coverage/iOSAI$LogManager.coverage" NAME="LogManager 覆盖结果" MODIFIED="1756711414832" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/Utils" /> <SUITE FILE_PATH="coverage/iOSAI$LogManager.coverage" NAME="LogManager 覆盖结果" MODIFIED="1756711414832" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/Utils" />
<SUITE FILE_PATH="coverage/iOSAI$FlaskService.coverage" NAME="FlaskService 覆盖结果" MODIFIED="1756730187792" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/Module" /> <SUITE FILE_PATH="coverage/iOSAI$FlaskService.coverage" NAME="FlaskService 覆盖结果" MODIFIED="1756730187792" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/Module" />
<SUITE FILE_PATH="coverage/iOSAI$test.coverage" NAME="test 覆盖结果" MODIFIED="1756467664420" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" /> <SUITE FILE_PATH="coverage/iOSAI$test.coverage" NAME="test 覆盖结果" MODIFIED="1756467664420" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/iOSAI$windows_run.coverage" NAME="windows_run Coverage Results" MODIFIED="1756473558532" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" />
<SUITE FILE_PATH="coverage/iOSAI$1352.coverage" NAME="1352 覆盖结果" MODIFIED="1757662777051" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" /> <SUITE FILE_PATH="coverage/iOSAI$1352.coverage" NAME="1352 覆盖结果" MODIFIED="1757662777051" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/iOSAI$windows_run.coverage" NAME="windows_run Coverage Results" MODIFIED="1756473558532" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" />
<SUITE FILE_PATH="coverage/iOSAI$mac_wda_agent.coverage" NAME="mac_wda_agent Coverage Results" MODIFIED="1756473148639" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" /> <SUITE FILE_PATH="coverage/iOSAI$mac_wda_agent.coverage" NAME="mac_wda_agent Coverage Results" MODIFIED="1756473148639" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" />
<SUITE FILE_PATH="coverage/iOSAI$123456.coverage" NAME="123456 覆盖结果" MODIFIED="1757672582575" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" /> <SUITE FILE_PATH="coverage/iOSAI$123456.coverage" NAME="123456 覆盖结果" MODIFIED="1757672582575" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/iOSAI$2111.coverage" NAME="2111 覆盖结果" MODIFIED="1757330714370" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" /> <SUITE FILE_PATH="coverage/iOSAI$2111.coverage" NAME="2111 覆盖结果" MODIFIED="1757330714370" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
@@ -356,7 +437,7 @@
<SUITE FILE_PATH="coverage/iOSAI$123__1_.coverage" NAME="123 (1) 覆盖结果" MODIFIED="1756897091135" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" /> <SUITE FILE_PATH="coverage/iOSAI$123__1_.coverage" NAME="123 (1) 覆盖结果" MODIFIED="1756897091135" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/iOSAI$tidevice_entry.coverage" NAME="tidevice_entry 覆盖结果" MODIFIED="1757061969626" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" /> <SUITE FILE_PATH="coverage/iOSAI$tidevice_entry.coverage" NAME="tidevice_entry 覆盖结果" MODIFIED="1757061969626" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/iOSAI$ScriptManager.coverage" NAME="ScriptManager 覆盖结果" MODIFIED="1756896057801" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" /> <SUITE FILE_PATH="coverage/iOSAI$ScriptManager.coverage" NAME="ScriptManager 覆盖结果" MODIFIED="1756896057801" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" />
<SUITE FILE_PATH="coverage/iOSAI$Main.coverage" NAME="Main 覆盖结果" MODIFIED="1758002271600" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="" /> <SUITE FILE_PATH="coverage/iOSAI$Main.coverage" NAME="Main 覆盖结果" MODIFIED="1758119032647" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="" />
<SUITE FILE_PATH="coverage/iOSAI$123.coverage" NAME="123 覆盖结果" MODIFIED="1758002344317" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" /> <SUITE FILE_PATH="coverage/iOSAI$123.coverage" NAME="123 覆盖结果" MODIFIED="1758115088356" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
</component> </component>
</project> </project>

View File

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

View File

@@ -30,12 +30,14 @@ class Deviceinfo(object):
self._lock = threading.Lock() self._lock = threading.Lock()
self._model_index: Dict[str, DeviceModel] = {} # udid -> model self._model_index: Dict[str, DeviceModel] = {} # udid -> model
# ✅ 1. 失踪时间戳记录(替代原来的 miss_count self._miss_count: Dict[str, int] = {} # udid -> 连续未扫描到次数
self._last_seen: Dict[str, float] = {} self._port_pool: List[int] = [] # 端口回收池
self._port_pool: List[int] = [] self._port_in_use: set[int] = set() # 正在使用的端口
self._port_in_use: set[int] = set()
# region iproxy 初始化(原逻辑不变) # 🔥1. 启动 WDA 健康检查线程
# threading.Thread(target=self._wda_health_checker, daemon=True).start()
# region iproxy 初始化
try: try:
self.iproxy_path = self._iproxy_path() self.iproxy_path = self._iproxy_path()
self.iproxy_dir = self.iproxy_path.parent self.iproxy_dir = self.iproxy_path.parent
@@ -83,11 +85,7 @@ class Deviceinfo(object):
LogManager.error(f"初始化 iproxy 失败:{e}") LogManager.error(f"初始化 iproxy 失败:{e}")
# endregion # endregion
# ------------------------------------------------------------------
# ✅ 2. 主监听循环(已用“时间窗口+USB 层兜底”重写)
# ------------------------------------------------------------------
def startDeviceListener(self): def startDeviceListener(self):
MISS_WINDOW = 5.0 # 5 秒连续失踪才判死刑
while True: while True:
try: try:
lists = Usbmux().device_list() lists = Usbmux().device_list()
@@ -97,23 +95,24 @@ class Deviceinfo(object):
continue continue
now_udids = {d.udid for d in lists if d.conn_type == ConnectionType.USB} now_udids = {d.udid for d in lists if d.conn_type == ConnectionType.USB}
# ✅ USB 层真断兜底
usb_sn_set = self._usb_enumerate_sn()
need_remove = None # 1. 失踪登记 & 累加
need_remove = None # ← 新增:放锁外记录
with self._lock: with self._lock:
for udid in list(self._model_index.keys()): for udid in list(self._model_index.keys()):
if udid not in now_udids: if udid not in now_udids:
last = self._last_seen.get(udid, time.time()) self._miss_count[udid] = self._miss_count.get(udid, 0) + 1
if time.time() - last > MISS_WINDOW and udid not in usb_sn_set: if self._miss_count[udid] >= 3:
need_remove = udid self._miss_count.pop(udid, None)
need_remove = udid # ← 只记录,不调用
else: else:
self._last_seen[udid] = time.time() self._miss_count.pop(udid, None)
# 🔓 锁已释放,再删设备(不会重入)
if need_remove: if need_remove:
self._remove_model(need_remove) self._remove_model(need_remove)
# 新增设备(原逻辑不变 # 2. 全新插入(只处理未在线且信任且未满
for d in lists: for d in lists:
if d.conn_type != ConnectionType.USB: if d.conn_type != ConnectionType.USB:
continue continue
@@ -132,22 +131,14 @@ class Deviceinfo(object):
time.sleep(1) time.sleep(1)
# ------------------------------------------------------------------ # 🔥2. WDA 健康检查
# ✅ 3. USB 层枚举 SN跨平台
# ------------------------------------------------------------------
def _usb_enumerate_sn(self) -> set[str]:
try:
out = subprocess.check_output(["idevice_id", "-l"], text=True, timeout=3)
return {line.strip() for line in out.splitlines() if line.strip()}
except Exception:
return set()
# ===================== 以下代码与原文件完全一致 =====================
def _wda_health_checker(self): def _wda_health_checker(self):
while True: while True:
time.sleep(1) time.sleep(1)
print(len(self.deviceModelList))
with self._lock: with self._lock:
online = [m for m in self.deviceModelList if m.ready] online = [m for m in self.deviceModelList if m.ready] # ← 只检查就绪的
print(len(online))
for model in online: for model in online:
udid = model.deviceId udid = model.deviceId
if not self._wda_ok(udid): if not self._wda_ok(udid):
@@ -156,24 +147,32 @@ class Deviceinfo(object):
self._remove_model(udid) self._remove_model(udid)
self.connectDevice(udid) self.connectDevice(udid)
# 🔥3. 真正做 health-check 的地方
def _wda_ok(self, udid: str) -> bool: def _wda_ok(self, udid: str) -> bool:
"""返回 True 表示 WDA 活着False 表示已死"""
try: try:
# 用 2 秒超时快速探测
c = wda.USBClient(udid, 8100) c = wda.USBClient(udid, 8100)
# 下面这句就是“xctest launched but check failed” 的触发点
# 如果 status 里返回了 WebDriverAgent 运行信息就认为 OK
st = c.status() st = c.status()
if st.get("state") != "success": if st.get("state") != "success":
return False return False
# 你也可以再苛刻一点,多做一次 /wda/healthcheck
# c.http.get("/wda/healthcheck")
return True return True
except Exception as e: except Exception as e:
# 任何异常连接拒绝、超时、json 解析失败)都认为已死
LogManager.error(f"WDA health-check 异常:{e}", udid) LogManager.error(f"WDA health-check 异常:{e}", udid)
return False return False
# -------------------- 增删改查唯一入口(未改动) -------------------- # region ===================== 增删改查唯一入口(线程安全) =====================
def _has_model(self, udid: str) -> bool: def _has_model(self, udid: str) -> bool:
return udid in self._model_index return udid in self._model_index
def _add_model(self, model: DeviceModel): def _add_model(self, model: DeviceModel):
if model.deviceId in self._model_index: if model.deviceId in self._model_index:
return return # 防重复
model.ready = True model.ready = True
self.deviceModelList.append(model) self.deviceModelList.append(model)
self._model_index[model.deviceId] = model self._model_index[model.deviceId] = model
@@ -183,12 +182,16 @@ class Deviceinfo(object):
LogManager.warning(f"{model.deviceId} 发送上线事件失败:{e}") LogManager.warning(f"{model.deviceId} 发送上线事件失败:{e}")
LogManager.method_info(f"{model.deviceId} 加入设备成功,当前在线数:{len(self.deviceModelList)}",method="device_count") LogManager.method_info(f"{model.deviceId} 加入设备成功,当前在线数:{len(self.deviceModelList)}",method="device_count")
# 删除设备
def _remove_model(self, udid: str): def _remove_model(self, udid: str):
print(f"【删】进入删除方法 udid={udid}") print(f"【删】进入删除方法 udid={udid}")
LogManager.method_info(f"【删】进入删除方法 udid={udid}", method="device_count") LogManager.method_info(f"【删】进入删除方法 udid={udid}", method="device_count")
# 1. 纯内存临界区——毫秒级
with self._lock: with self._lock:
print(f"【删】拿到锁 udid={udid}") print(f"【删】拿到锁 udid={udid}")
LogManager.method_info(f"【删】拿到锁 udid={udid}", method="device_count") LogManager.method_info(f"【删】拿到锁 udid={udid}",
method="device_count")
model = self._model_index.pop(udid, None) model = self._model_index.pop(udid, None)
if not model: if not model:
print(f"【删】模型已空,直接返回 udid={udid}") print(f"【删】模型已空,直接返回 udid={udid}")
@@ -199,30 +202,38 @@ class Deviceinfo(object):
LogManager.method_info(method="device_count", text=f"【删】正在删除中,幂等返回 udid={udid}") LogManager.method_info(method="device_count", text=f"【删】正在删除中,幂等返回 udid={udid}")
return return
model.deleting = True model.deleting = True
# 标记维删除设备
model.type = 2 model.type = 2
print(f"【删】标记 deleting=True udid={udid}") print(f"【删】标记 deleting=True udid={udid}")
LogManager.method_info("【删】标记 deleting=True udid={udid}","device_count") LogManager.method_info("【删】标记 deleting=True udid={udid}","device_count")
# 过滤列表
before = len(self.deviceModelList) before = len(self.deviceModelList)
self.deviceModelList = [m for m in self.deviceModelList if m.deviceId != udid] self.deviceModelList = [m for m in self.deviceModelList if m.deviceId != udid]
after = len(self.deviceModelList) after = len(self.deviceModelList)
print(f"【删】列表过滤 before={before} → after={after} udid={udid}") print(f"【删】列表过滤 before={before} → after={after} udid={udid}")
LogManager.method_info(f"【删】列表过滤 before={before} → after={after} udid={udid}","device_count") LogManager.method_info(f"【删】列表过滤 before={before} → after={after} udid={udid}","device_count")
# 端口
self._port_in_use.discard(model.screenPort) self._port_in_use.discard(model.screenPort)
self._port_pool.append(model.screenPort) self._port_pool.append(model.screenPort)
print(f"【删】回收端口 port={model.screenPort} udid={udid}") print(f"【删】回收端口 port={model.screenPort} udid={udid}")
LogManager.method_info(f"【删】回收端口 port={model.screenPort} udid={udid}", method="device_count") LogManager.method_info(f"【删】回收端口 port={model.screenPort} udid={udid}", method="device_count")
# 进程
to_kill = [item for item in self.pidList if item.get("id") == udid] to_kill = [item for item in self.pidList if item.get("id") == udid]
self.pidList = [item for item in self.pidList if item.get("id") != udid] self.pidList = [item for item in self.pidList if item.get("id") != udid]
print(f"【删】待杀进程数 count={len(to_kill)} udid={udid}") print(f"【删】待杀进程数 count={len(to_kill)} udid={udid}")
LogManager.method_info(f"【删】待杀进程数 count={len(to_kill)} udid={udid}", method="device_count") LogManager.method_info(f"【删】待杀进程数 count={len(to_kill)} udid={udid}", method="device_count")
# 2. IO 区无锁
for idx, item in enumerate(to_kill, 1): for idx, item in enumerate(to_kill, 1):
print(f"【删】杀进程 {idx}/{len(to_kill)} pid={item.get('target').pid} udid={udid}") print(f"【删】杀进程 {idx}/{len(to_kill)} pid={item.get('target').pid} udid={udid}")
LogManager.method_info(f"【删】杀进程 {idx}/{len(to_kill)} pid={item.get('target').pid} udid={udid}", method="device_count") LogManager.method_error(f"【删】杀进程 {idx}/{len(to_kill)} pid={item.get('target').pid} udid={udid}", method="device_count")
self._terminate_proc(item.get("target")) self._terminate_proc(item.get("target"))
print(f"【删】进程清理完成 udid={udid}") print(f"【删】进程清理完成 udid={udid}")
LogManager.method_info(f"【删】进程清理完成 udid={udid}", method="device_count") LogManager.method_info(f"【删】进程清理完成 udid={udid}", method="device_count")
# 3. 网络 IO
retry = 3 retry = 3
while retry: while retry:
try: try:
@@ -244,7 +255,7 @@ class Deviceinfo(object):
print(len(self.deviceModelList)) print(len(self.deviceModelList))
LogManager.method_info(f"当前剩余设备数量:{len(self.deviceModelList)}", method="device_count") LogManager.method_info(f"当前剩余设备数量:{len(self.deviceModelList)}", method="device_count")
# -------------------- 端口分配与回收(未改动) -------------------- # region ===================== 端口分配与回收 =====================
def _alloc_port(self) -> int: def _alloc_port(self) -> int:
if self._port_pool: if self._port_pool:
port = self._port_pool.pop() port = self._port_pool.pop()
@@ -258,17 +269,20 @@ class Deviceinfo(object):
if port in self._port_in_use: if port in self._port_in_use:
self._port_in_use.remove(port) self._port_in_use.remove(port)
self._port_pool.append(port) self._port_pool.append(port)
# endregion
# -------------------- 单台设备连接(未改动) -------------------- # region ===================== 单台设备连接 =====================
def connectDevice(self, udid: str): def connectDevice(self, udid: str):
if not self.is_device_trusted(udid): if not self.is_device_trusted(udid):
LogManager.warning("设备未信任,跳过 WDA 启动", udid) LogManager.warning("设备未信任,跳过 WDA 启动", udid)
return return
try: try:
d = wda.USBClient(udid, 8100) d = wda.USBClient(udid, 8100)
except Exception as e: except Exception as e:
LogManager.error(f"启动 WDA 失败: {e}", udid) LogManager.error(f"启动 WDA 失败: {e}", udid)
return return
width, height, scale = 0, 0, 1.0 width, height, scale = 0, 0, 1.0
try: try:
size = d.window_size() size = d.window_size()
@@ -276,21 +290,26 @@ class Deviceinfo(object):
scale = d.scale scale = d.scale
except Exception as e: except Exception as e:
LogManager.warning(f"读取屏幕信息失败:{e}", udid) LogManager.warning(f"读取屏幕信息失败:{e}", udid)
port = self._alloc_port() port = self._alloc_port()
model = DeviceModel(udid, port, width, height, scale, type=1) model = DeviceModel(udid, port, width, height, scale, type=1)
self._add_model(model) self._add_model(model)
try: try:
d.app_start(WdaAppBundleId) d.app_start(WdaAppBundleId)
d.home() d.home()
except Exception as e: except Exception as e:
LogManager.warning(f"启动/切回桌面失败:{e}", udid) LogManager.warning(f"启动/切回桌面失败:{e}", udid)
time.sleep(2) time.sleep(2)
# 先清旧进程再启动新进程
self.pidList = [item for item in self.pidList if item.get("id") != udid] self.pidList = [item for item in self.pidList if item.get("id") != udid]
target = self.relayDeviceScreenPort(udid, port) target = self.relayDeviceScreenPort(udid, port)
if target: if target:
self.pidList.append({"target": target, "id": udid}) self.pidList.append({"target": target, "id": udid})
# -------------------- 工具方法(未改动) -------------------- # region ===================== 工具方法 =====================
def is_device_trusted(self, udid: str) -> bool: def is_device_trusted(self, udid: str) -> bool:
try: try:
d = BaseDevice(udid) d = BaseDevice(udid)
@@ -300,16 +319,22 @@ class Deviceinfo(object):
return False return False
def relayDeviceScreenPort(self, udid: str, port: int) -> Optional[subprocess.Popen]: def relayDeviceScreenPort(self, udid: str, port: int) -> Optional[subprocess.Popen]:
"""启动 iproxy 前:端口若仍被占用则先杀掉占用者,再启动"""
if not self._spawn_iproxy: if not self._spawn_iproxy:
LogManager.error("iproxy 启动器未就绪", udid) LogManager.error("iproxy 启动器未就绪", udid)
return None return None
# --- 新增:端口冲突检查 + 强制清理 ---
while self._port_in_use and self._is_port_open(port): while self._port_in_use and self._is_port_open(port):
# 先查是哪个进程占用
pid = self._get_pid_by_port(port) pid = self._get_pid_by_port(port)
if pid and pid != os.getpid(): if pid and pid != os.getpid():
LogManager.warning(f"端口 {port} 仍被 PID {pid} 占用,尝试释放", udid) LogManager.warning(f"端口 {port} 仍被 PID {pid} 占用,尝试释放", udid)
self._kill_pid_gracefully(pid) self._kill_pid_gracefully(pid)
else: else:
break break
# -------------------------------------
try: try:
p = self._spawn_iproxy(udid, port, 9100) p = self._spawn_iproxy(udid, port, 9100)
self._port_in_use.add(port) self._port_in_use.add(port)
@@ -319,12 +344,14 @@ class Deviceinfo(object):
LogManager.error(f"启动 iproxy 失败:{e}", udid) LogManager.error(f"启动 iproxy 失败:{e}", udid)
return None return None
# ------------------- 新增三个小工具 -------------------
def _is_port_open(self, port: int) -> bool: def _is_port_open(self, port: int) -> bool:
import socket import socket
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s: with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
return s.connect_ex(("127.0.0.1", port)) == 0 return s.connect_ex(("127.0.0.1", port)) == 0
def _get_pid_by_port(self, port: int) -> Optional[int]: def _get_pid_by_port(self, port: int) -> Optional[int]:
"""跨平台根据端口号查 PID失败返回 None"""
try: try:
if os.name == "nt": if os.name == "nt":
cmd = ["netstat", "-ano", "-p", "tcp"] cmd = ["netstat", "-ano", "-p", "tcp"]
@@ -340,6 +367,7 @@ class Deviceinfo(object):
return None return None
def _kill_pid_gracefully(self, pid: int): def _kill_pid_gracefully(self, pid: int):
"""先 terminate 再 kill -9"""
try: try:
os.kill(pid, signal.SIGTERM) os.kill(pid, signal.SIGTERM)
time.sleep(1) time.sleep(1)
@@ -347,6 +375,7 @@ class Deviceinfo(object):
except Exception: except Exception:
pass pass
def _terminate_proc(self, p: Optional[subprocess.Popen]): def _terminate_proc(self, p: Optional[subprocess.Popen]):
if not p or p.poll() is not None: if not p or p.poll() is not None:
return return

View File

@@ -274,6 +274,7 @@ def stopScript():
@app.route('/passAnchorData', methods=['POST']) @app.route('/passAnchorData', methods=['POST'])
def passAnchorData(): def passAnchorData():
try: try:
LogManager.method_info("关注打招呼","关注打招呼")
data: Dict[str, Any] = request.get_json() data: Dict[str, Any] = request.get_json()
# 设备列表 # 设备列表
idList = data.get("deviceList", []) idList = data.get("deviceList", [])
@@ -431,7 +432,6 @@ def aiConfig():
@app.route("/select_last_message", methods=['GET']) @app.route("/select_last_message", methods=['GET'])
def select_last_message(): def select_last_message():
data = JsonUtils.query_all_json_items() data = JsonUtils.query_all_json_items()
return ResultData(data=data).toJson() 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 random
import re import re
import time import time
from typing import Tuple, List
import tidevice import tidevice
import wda import wda
@@ -70,12 +72,15 @@ class ControlUtils(object):
return True return True
elif session.xpath("//*[@name='nav_bar_start_back']").exists: elif session.xpath("//*[@name='nav_bar_start_back']").exists:
back = session.xpath("//*[@name='nav_bar_start_back']") back = session.xpath("//*[@name='nav_bar_start_back']")
if back.exists:
back.click() back.click()
return True return True
elif session.xpath( 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: "//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( 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]") "//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]")
if back.exists:
back.click() back.click()
return True return True
else: else:
@@ -129,6 +134,7 @@ class ControlUtils(object):
videoCell = session.xpath( videoCell = session.xpath(
'(//XCUIElementTypeCollectionView//XCUIElementTypeCell[.//XCUIElementTypeImage[@name="profile_video"]])[1]') '(//XCUIElementTypeCollectionView//XCUIElementTypeCell[.//XCUIElementTypeImage[@name="profile_video"]])[1]')
tab = session.xpath( tab = session.xpath(
'//XCUIElementTypeButton[@name="TTKProfileTabVideoButton_0" or contains(@label,"作品") or contains(@name,"作品")]' '//XCUIElementTypeButton[@name="TTKProfileTabVideoButton_0" or contains(@label,"作品") or contains(@name,"作品")]'
).get(timeout=5) # 某些版本 tab.value 可能就是数量;或者 tab.label 类似 “作品 7” ).get(timeout=5) # 某些版本 tab.value 可能就是数量;或者 tab.label 类似 “作品 7”
@@ -204,33 +210,65 @@ class ControlUtils(object):
print(e) print(e)
return False return False
# 随机滑动一点点距离
@classmethod @classmethod
def tap_mini_cluster(cls, center_x: int, center_y: int, session, points=5, duration_ms=60): def random_micro_swipe(
try: cls,
response = session.http.post( center_x: int,
"touchAndHold", center_y: int,
data={ session,
"x": 100, points: int = 6,
"y": 100,+ duration_ms: int = 15,
"duration": 0.1 ) -> 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)]
) )
print(response)
return response
except Exception as e:
print(e)
return None
# 检测五分钟前和当前的状态是否相同
# @classmethod
# def compareCurrentWithPreviousState(cls,xml):
# 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 -*- # -*- coding: utf-8 -*-
import datetime import datetime
import io 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

@@ -1,9 +1,9 @@
python -m nuitka "Module/Main.py" ^ python -m nuitka "C:\Users\zhangkai\Desktop\20250916部署的ios项目\iOSAI\Module\Main.py" ^
--standalone ^ --standalone ^
--msvc=latest ^ --msvc=latest ^
--windows-console-mode=disable ^ --windows-console-mode=disable ^
--remove-output ^ --remove-output ^
--output-dir="F:/company code/AI item/20250820/iOSAI/out" ^ --output-dir="C:\Users\zhangkai\Desktop\20250916部署的ios项目\iOSAI\out" ^
--output-filename=IOSAI ^ --output-filename=IOSAI ^
--include-package=Module,Utils,Entity,script ^ --include-package=Module,Utils,Entity,script ^
--include-module=flask ^ --include-module=flask ^
@@ -18,7 +18,7 @@ python -m nuitka "Module/Main.py" ^
--include-module=urllib3 ^ --include-module=urllib3 ^
--include-module=certifi ^ --include-module=certifi ^
--include-module=idna ^ --include-module=idna ^
--include-data-dir="F:/company code/AI item/20250820/iOSAI/SupportFiles=SupportFiles" ^ --include-data-dir="C:\Users\zhangkai\Desktop\20250916部署的ios项目\iOSAI\SupportFiles=SupportFiles" ^
--include-data-dir="F:/company code/AI item/20250820/iOSAI/resources=resources" ^ --include-data-dir="C:\Users\zhangkai\Desktop\20250916部署的ios项目\iOSAI\resources=resources" ^
--include-data-files="F:/company code/AI item/20250820/iOSAI/resources/iproxy/*=resources/iproxy/" ^ --include-data-files="C:\Users\zhangkai\Desktop\20250916部署的ios项目\iOSAI\resources\iproxy\*=resources/iproxy/" ^
--windows-icon-from-ico="F:/company code/AI item/20250820/iOSAI/resources/icon.ico" --windows-icon-from-ico="C:\Users\zhangkai\Desktop\20250916部署的ios项目\iOSAI\resources\icon.ico"

Binary file not shown.

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,6 +361,8 @@ class ScriptManager():
else: else:
print(f"找不到输入框") print(f"找不到输入框")
input = session.xpath('//XCUIElementTypeSearchField')
if input.exists:
input.clear_text() input.clear_text()
time.sleep(1) time.sleep(1)
# 输入主播id # 输入主播id
@@ -449,11 +451,11 @@ class ScriptManager():
time.sleep(2) time.sleep(2)
msgButton = AiUtils.getSendMesageButton(session) msgButton = AiUtils.getSendMesageButton(session)
time.sleep(2) time.sleep(2)
if msgButton is not None: if msgButton.exists:
LogManager.method_info("找到发消息按钮了", "关注打招呼", udid)
print("找到发消息按钮了")
# 进入聊天页面 # 进入聊天页面
msgButton.click() msgButton.click()
LogManager.method_info("找到发消息按钮了", "关注打招呼", udid)
print("找到发消息按钮了")
else: else:
LogManager.method_info("没有识别出发消息按钮", "关注打招呼", udid) LogManager.method_info("没有识别出发消息按钮", "关注打招呼", udid)
print("没有识别出发消息按钮") print("没有识别出发消息按钮")
@@ -494,6 +496,8 @@ class ScriptManager():
LogManager.method_info(f"即将发送的私信内容:{msg}", "关注打招呼", udid) LogManager.method_info(f"即将发送的私信内容:{msg}", "关注打招呼", udid)
# 准备发送一条信息 # 准备发送一条信息
chatInput = session.xpath("//TextView")
if chatInput.exists:
chatInput.click() chatInput.click()
time.sleep(2) time.sleep(2)
# 发送消息 # 发送消息

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)