Compare commits
197 Commits
9a7cbd65a5
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 24082dc2a4 | |||
| 746314f0ff | |||
| 68b985115e | |||
| 648f6cdfee | |||
| 0a200cfc6f | |||
| e56a309825 | |||
| 3b2b6ce741 | |||
| 22da742532 | |||
| 412624045e | |||
| af4ab583a5 | |||
| d96a19c659 | |||
| 33f261e8af | |||
| 611f4e46ac | |||
| 51f638f389 | |||
| e90dbf14e9 | |||
| 37f91c4b8c | |||
| 317fc2586a | |||
| 13c7930f88 | |||
| 083723edca | |||
| 621d94cc24 | |||
| ecc8dadd0b | |||
| 96c62ced12 | |||
| bc83fcd9d7 | |||
| f799a6df77 | |||
| 58d124919b | |||
| b9e2d86857 | |||
| fee1f77e87 | |||
| 35b9d4098d | |||
| aeea2181cc | |||
| 2aba193e96 | |||
| 48c86aae56 | |||
| 86bc5791c6 | |||
| 2251962306 | |||
| e5a9ccdcb1 | |||
| fa539aef73 | |||
| a9c9e39143 | |||
| 9ed5602b86 | |||
| 972c2d0d97 | |||
| 36a57b2015 | |||
| 01580e2fb1 | |||
| 01e18bdc03 | |||
| 267d87e43d | |||
| 242c2e99c5 | |||
| 6337c951fb | |||
| 486c84efb6 | |||
| 81761a576b | |||
| f49755cb30 | |||
| 1baf9c7fc5 | |||
| 740f45b88b | |||
| 0ccd4ee97c | |||
| 8e8d676c79 | |||
| 929061e4b5 | |||
| bdc6414f69 | |||
| 3de4a67304 | |||
| 54ee0f7490 | |||
| bde168df15 | |||
| 141d7abe3e | |||
| 964a5647fc | |||
| 9578e098b9 | |||
| 177308a69d | |||
| 06d92baf29 | |||
| 9b923ebe49 | |||
| 7de4fff7cf | |||
| 0c78a025f4 | |||
| 218e918ec0 | |||
| 6ceb583b9e | |||
| 2d61dce1ee | |||
| a233f19924 | |||
| caf6ce8deb | |||
| b22efdec01 | |||
| 4ff50ecdfc | |||
| fa6f0ce9df | |||
| 2ad53eb1db | |||
| 2254284625 | |||
| 7b732aad62 | |||
| 77d2978db1 | |||
| a808747d43 | |||
| c902e6f4d3 | |||
| 3c3bde7df9 | |||
| 27426f1f8f | |||
| 23f63e42c8 | |||
| fe3c19fb21 | |||
| fe8c07d2a6 | |||
| ba4bcff7e1 | |||
| 34b1d1ec77 | |||
| dcb3f8e5af | |||
| bfb105f324 | |||
| 26057d4afa | |||
| 4966a659aa | |||
| 6cf4d9cb03 | |||
| 2310333a60 | |||
| 855a19873e | |||
| a0fe54d504 | |||
| bb8029b498 | |||
| ac92747892 | |||
| 3da3fabe79 | |||
| d543c6f757 | |||
| d876743d3e | |||
| 67e0df8af9 | |||
| b94434692c | |||
| b2ec94c62c | |||
| 0d2782ddb8 | |||
| b0525ce817 | |||
| 4dd3eb59a4 | |||
| 81e3462f15 | |||
| 8f290cf610 | |||
| bfdf684952 | |||
| 68b329f4f9 | |||
| a4effb8058 | |||
| fa667d2520 | |||
| e94902fedf | |||
| 1391a2b37c | |||
| 08f76009b4 | |||
| 5c9b6cd4c7 | |||
| 02aa85dae2 | |||
| 31302fb43e | |||
| b9ecce6eeb | |||
| 811935ac60 | |||
| 11e72d0fae | |||
| c54a0aceb5 | |||
| 9250be8780 | |||
| 4569d28811 | |||
| d7e1d993fb | |||
| 2fe1576eaa | |||
| 2c3a71fe3c | |||
| 81bc1f5f50 | |||
| dfb03cb362 | |||
| f21b44cf19 | |||
| 5d63cc7961 | |||
| 6e2486a036 | |||
| db67024157 | |||
| deffd48bb7 | |||
| 1830c27fc5 | |||
| bb73fa6d9e | |||
| daa6345c15 | |||
| c29a663300 | |||
| 46b8576a16 | |||
| c65eb95699 | |||
| 9490da09f4 | |||
| e6768b14a5 | |||
| e0dc14ea73 | |||
| a0e1f9ef8d | |||
| b91aa99048 | |||
| b203579649 | |||
| 096b10f5f0 | |||
| 824cff5ab6 | |||
| 155f11de91 | |||
| e71bd48468 | |||
| 177757a143 | |||
| 3f5c880c0f | |||
| 06fd7ce885 | |||
| 6766ac8c20 | |||
| 6da5926b4b | |||
| aa2f291f49 | |||
| 398d980caa | |||
| f450226d2d | |||
| 5732db2cc2 | |||
| 9cf094986c | |||
| 1653da2f37 | |||
| 9eb2a82880 | |||
| 9770ce3ad3 | |||
| 6bb7ae23db | |||
| c1cf6c04e2 | |||
| 19780a8a85 | |||
| 7e7d183f5f | |||
| 2b3fb6871f | |||
| a3019ffee4 | |||
| 9775b1a063 | |||
| a408a01110 | |||
| a2287b47d5 | |||
| fcbcc2f6b6 | |||
| 57759593ed | |||
| 7954d4f244 | |||
| 0e51f60f35 | |||
| f6dda59c9f | |||
| 9cb27eb97f | |||
| e2e9e0b348 | |||
| 71e9bf9045 | |||
| 4b3247d0bf | |||
|
|
7a8380af9c | ||
|
|
a6d5330c73 | ||
|
|
0681fc0754 | ||
|
|
52bb129186 | ||
|
|
de81857321 | ||
|
|
67cb777f5a | ||
| 31f0e19b13 | |||
| 11057f8ce6 | |||
| 8050d8c14c | |||
|
|
ad5d487f25 | ||
|
|
f9fff55553 | ||
|
|
edf128b9f4 | ||
|
|
3381386b88 | ||
|
|
7564319638 | ||
|
|
0d9fa81992 | ||
| 4e705b51f1 | |||
| b514b7c329 | |||
| b555712749 |
11
.gitignore
vendored
11
.gitignore
vendored
@@ -1,11 +1,15 @@
|
|||||||
# Byte-compiled / optimized / DLL files
|
# Byte-compiled / optimized / DLL files
|
||||||
__pycache__/
|
__pycache__/
|
||||||
|
# Python bytecode & caches
|
||||||
|
*.pyc
|
||||||
|
*.pyo
|
||||||
|
*.pyd
|
||||||
*.py[cod]
|
*.py[cod]
|
||||||
*$py.class
|
*$py.class
|
||||||
|
build.bat
|
||||||
|
|
||||||
# Distribution / packaging
|
# Distribution / packaging
|
||||||
.Python
|
.Python
|
||||||
build/
|
|
||||||
develop-eggs/
|
develop-eggs/
|
||||||
dist/
|
dist/
|
||||||
downloads/
|
downloads/
|
||||||
@@ -20,6 +24,8 @@ var/
|
|||||||
.installed.cfg
|
.installed.cfg
|
||||||
*.egg
|
*.egg
|
||||||
out/
|
out/
|
||||||
|
Main.build/
|
||||||
|
Main.dist/
|
||||||
|
|
||||||
# PyInstaller
|
# PyInstaller
|
||||||
# Usually these files are written by a python script from a template
|
# Usually these files are written by a python script from a template
|
||||||
@@ -122,4 +128,5 @@ dmypy.json
|
|||||||
.pytype/
|
.pytype/
|
||||||
|
|
||||||
# Cython debug symbols
|
# Cython debug symbols
|
||||||
cython_debug/
|
cython_debug/
|
||||||
|
build-tidevice.bat
|
||||||
|
|||||||
7
.idea/IOS__AI.iml
generated
Normal file
7
.idea/IOS__AI.iml
generated
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<module version="4">
|
||||||
|
<component name="PyDocumentationSettings">
|
||||||
|
<option name="format" value="PLAIN" />
|
||||||
|
<option name="myDocStringFormat" value="Plain" />
|
||||||
|
</component>
|
||||||
|
</module>
|
||||||
15
.idea/git_toolbox_prj.xml
generated
15
.idea/git_toolbox_prj.xml
generated
@@ -1,15 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="GitToolBoxProjectSettings">
|
|
||||||
<option name="commitMessageIssueKeyValidationOverride">
|
|
||||||
<BoolValueOverride>
|
|
||||||
<option name="enabled" value="true" />
|
|
||||||
</BoolValueOverride>
|
|
||||||
</option>
|
|
||||||
<option name="commitMessageValidationEnabledOverride">
|
|
||||||
<BoolValueOverride>
|
|
||||||
<option name="enabled" value="true" />
|
|
||||||
</BoolValueOverride>
|
|
||||||
</option>
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
9
.idea/iOSAI.iml
generated
9
.idea/iOSAI.iml
generated
@@ -1,8 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<module type="PYTHON_MODULE" version="4">
|
<module version="4">
|
||||||
<component name="NewModuleRootManager">
|
<component name="PyDocumentationSettings">
|
||||||
<content url="file://$MODULE_DIR$" />
|
<option name="format" value="PLAIN" />
|
||||||
<orderEntry type="jdk" jdkName="Python 3.12" jdkType="Python SDK" />
|
<option name="myDocStringFormat" value="Plain" />
|
||||||
<orderEntry type="sourceFolder" forTests="false" />
|
|
||||||
</component>
|
</component>
|
||||||
</module>
|
</module>
|
||||||
10
.idea/inspectionProfiles/Project_Default.xml
generated
10
.idea/inspectionProfiles/Project_Default.xml
generated
@@ -4,11 +4,9 @@
|
|||||||
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
<inspection_tool class="PyPackageRequirementsInspection" enabled="true" level="WARNING" enabled_by_default="true">
|
||||||
<option name="ignoredPackages">
|
<option name="ignoredPackages">
|
||||||
<value>
|
<value>
|
||||||
<list size="4">
|
<list size="2">
|
||||||
<item index="0" class="java.lang.String" itemvalue="PySide6" />
|
<item index="0" class="java.lang.String" itemvalue="numpy" />
|
||||||
<item index="1" class="java.lang.String" itemvalue="pyusb" />
|
<item index="1" class="java.lang.String" itemvalue="facebook_wda" />
|
||||||
<item index="2" class="java.lang.String" itemvalue="PyGObject-stubs" />
|
|
||||||
<item index="3" class="java.lang.String" itemvalue="PyGObject" />
|
|
||||||
</list>
|
</list>
|
||||||
</value>
|
</value>
|
||||||
</option>
|
</option>
|
||||||
@@ -16,7 +14,7 @@
|
|||||||
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
|
||||||
<option name="ignoredErrors">
|
<option name="ignoredErrors">
|
||||||
<list>
|
<list>
|
||||||
<option value="N806" />
|
<option value="N803" />
|
||||||
</list>
|
</list>
|
||||||
</option>
|
</option>
|
||||||
</inspection_tool>
|
</inspection_tool>
|
||||||
|
|||||||
6
.idea/jsLibraryMappings.xml
generated
6
.idea/jsLibraryMappings.xml
generated
@@ -1,6 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<project version="4">
|
|
||||||
<component name="JavaScriptLibraryMappings">
|
|
||||||
<includedPredefinedLibrary name="Node.js Core" />
|
|
||||||
</component>
|
|
||||||
</project>
|
|
||||||
2
.idea/misc.xml
generated
2
.idea/misc.xml
generated
@@ -1,7 +1,7 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project version="4">
|
<project version="4">
|
||||||
<component name="Black">
|
<component name="Black">
|
||||||
<option name="sdkName" value="Python 3.12" />
|
<option name="sdkName" value="Python 3.12 (AI-IOS)" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
|
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
|
||||||
</project>
|
</project>
|
||||||
8
.idea/modules.xml
generated
8
.idea/modules.xml
generated
@@ -1,8 +0,0 @@
|
|||||||
<?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>
|
|
||||||
210
.idea/workspace.xml
generated
210
.idea/workspace.xml
generated
@@ -4,12 +4,20 @@
|
|||||||
<option name="autoReloadType" value="SELECTIVE" />
|
<option name="autoReloadType" value="SELECTIVE" />
|
||||||
</component>
|
</component>
|
||||||
<component name="ChangeListManager">
|
<component name="ChangeListManager">
|
||||||
<list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="ai 开始测试">
|
<list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="20250904-初步功能已完成">
|
||||||
<change afterPath="$PROJECT_DIR$/resources/iproxy/tidevice.exe" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/.idea/git_toolbox_blame.xml" afterDir="false" />
|
||||||
<change afterPath="$PROJECT_DIR$/tidevice_entry.py" afterDir="false" />
|
<change afterPath="$PROJECT_DIR$/resources/insert_comment2.png" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/iOSAI.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/iOSAI.iml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/inspectionProfiles/Project_Default.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/inspectionProfiles/Project_Default.xml" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" 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/Variables.py" beforeDir="false" afterPath="$PROJECT_DIR$/Entity/Variables.py" afterDir="false" />
|
<<<<<<< HEAD
|
||||||
<change beforePath="$PROJECT_DIR$/build.bat" beforeDir="false" afterPath="$PROJECT_DIR$/build.bat" afterDir="false" />
|
<change beforePath="$PROJECT_DIR$/Utils/LogManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/LogManager.py" afterDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/resources/00008110-000120603C13801E/bgv.png" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/resources/00008110-000120603C13801E/bgv_comment.png" beforeDir="false" />
|
||||||
|
<change beforePath="$PROJECT_DIR$/script/ScriptManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/script/ScriptManager.py" afterDir="false" />
|
||||||
|
=======
|
||||||
|
>>>>>>> e024b5d (111)
|
||||||
</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" />
|
||||||
@@ -37,6 +45,10 @@
|
|||||||
<component name="Git.Settings">
|
<component name="Git.Settings">
|
||||||
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
<option name="RECENT_GIT_ROOT_PATH" value="$PROJECT_DIR$" />
|
||||||
</component>
|
</component>
|
||||||
|
<component name="HighlightingSettingsPerFile">
|
||||||
|
<setting file="file://$PROJECT_DIR$/build.bat" root0="SKIP_INSPECTION" />
|
||||||
|
<setting file="file://$PROJECT_DIR$/script/ScriptManager.py" root0="FORCE_HIGHLIGHTING" />
|
||||||
|
</component>
|
||||||
<component name="ProjectColorInfo">{
|
<component name="ProjectColorInfo">{
|
||||||
"customColor": "",
|
"customColor": "",
|
||||||
"associatedIndex": 5
|
"associatedIndex": 5
|
||||||
@@ -49,34 +61,76 @@
|
|||||||
<option name="hideEmptyMiddlePackages" value="true" />
|
<option name="hideEmptyMiddlePackages" value="true" />
|
||||||
<option name="showLibraryContents" value="true" />
|
<option name="showLibraryContents" value="true" />
|
||||||
</component>
|
</component>
|
||||||
<component name="PropertiesComponent"><![CDATA[{
|
<component name="PropertiesComponent">{
|
||||||
"keyToString": {
|
"keyToString": {
|
||||||
"ASKED_ADD_EXTERNAL_FILES": "true",
|
"ASKED_ADD_EXTERNAL_FILES": "true",
|
||||||
"Python.123.executor": "Run",
|
"ASKED_MARK_IGNORED_FILES_AS_EXCLUDED": "true",
|
||||||
"Python.Main.executor": "Run",
|
"ModuleVcsDetector.initialDetectionPerformed": "true",
|
||||||
"Python.tidevice_entry.executor": "Run",
|
"Python.12.executor": "Run",
|
||||||
"RunOnceActivity.ShowReadmeOnStart": "true",
|
"Python.123.executor": "Run",
|
||||||
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
"Python.DeviceInfo.executor": "Run",
|
||||||
"git-widget-placeholder": "main",
|
"Python.IOSActivator.executor": "Run",
|
||||||
"javascript.nodejs.core.library.configured.version": "22.18.0",
|
"Python.Main.executor": "Run",
|
||||||
"javascript.nodejs.core.library.typings.version": "22.18.0",
|
"Python.Test.executor": "Run",
|
||||||
"last_opened_file_path": "F:/company code/AI item/20250820/iOSAI",
|
"Python.test.executor": "Run",
|
||||||
"node.js.detected.package.eslint": "true",
|
"Python.tidevice_entry.executor": "Run",
|
||||||
"node.js.detected.package.tslint": "true",
|
"RunOnceActivity.ShowReadmeOnStart": "true",
|
||||||
"node.js.selected.package.eslint": "(autodetect)",
|
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
|
||||||
"node.js.selected.package.tslint": "(autodetect)",
|
"RunOnceActivity.git.unshallow": "true",
|
||||||
"nodejs_package_manager_path": "npm",
|
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
|
||||||
"settings.editor.selected.configurable": "com.gitee.ui.GiteeSettingsConfigurable",
|
"git-widget-placeholder": "main",
|
||||||
"vue.rearranger.settings.migration": "true"
|
"javascript.nodejs.core.library.configured.version": "20.17.0",
|
||||||
|
"javascript.nodejs.core.library.typings.version": "20.17.58",
|
||||||
|
"last_opened_file_path": "E:/python/IOSAI/Module/Main.py",
|
||||||
|
"node.js.detected.package.eslint": "true",
|
||||||
|
"node.js.detected.package.tslint": "true",
|
||||||
|
"node.js.selected.package.eslint": "(autodetect)",
|
||||||
|
"node.js.selected.package.tslint": "(autodetect)",
|
||||||
|
"nodejs_package_manager_path": "npm",
|
||||||
|
"settings.editor.selected.configurable": "editing.templates",
|
||||||
|
"two.files.diff.last.used.file": "E:/share/iOSAI/Module/FlaskService.py",
|
||||||
|
"two.files.diff.last.used.folder": "C:/Users/zhangkai/Desktop/last-item/iosai/Utils",
|
||||||
|
"vue.rearranger.settings.migration": "true"
|
||||||
}
|
}
|
||||||
}]]></component>
|
}</component>
|
||||||
<component name="RecentsManager">
|
<component name="RecentsManager">
|
||||||
|
<key name="CopyFile.RECENT_KEYS">
|
||||||
|
<recent name="E:\python\IOSAI\resources" />
|
||||||
|
<recent name="E:\code\Python\iOSAi\resources\iproxy" />
|
||||||
|
<recent name="C:\Users\zhangkai\Desktop\last-item\iosai\Utils" />
|
||||||
|
<recent name="C:\Users\zhangkai\Desktop\20250916ios\iOSAI\resources" />
|
||||||
|
</key>
|
||||||
<key name="MoveFile.RECENT_KEYS">
|
<key name="MoveFile.RECENT_KEYS">
|
||||||
|
<recent name="E:\code\Python\iOSAi\Module" />
|
||||||
|
<recent name="E:\code\Python\iOSAi" />
|
||||||
<recent name="E:\Code\python\iOSAI\resources" />
|
<recent name="E:\Code\python\iOSAI\resources" />
|
||||||
<recent name="E:\Code\python\iOSAI" />
|
<recent name="E:\Code\python\iOSAI" />
|
||||||
</key>
|
</key>
|
||||||
</component>
|
</component>
|
||||||
<component name="RunManager">
|
<component name="RunManager" selected="Python.Main">
|
||||||
|
<configuration name="IOSActivator" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
|
||||||
|
<module name="iOSAI" />
|
||||||
|
<option name="ENV_FILES" value="" />
|
||||||
|
<option name="INTERPRETER_OPTIONS" value="" />
|
||||||
|
<option name="PARENT_ENVS" value="true" />
|
||||||
|
<envs>
|
||||||
|
<env name="PYTHONUNBUFFERED" value="1" />
|
||||||
|
</envs>
|
||||||
|
<option name="SDK_HOME" value="" />
|
||||||
|
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Module" />
|
||||||
|
<option name="IS_MODULE_SDK" value="true" />
|
||||||
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
|
<option name="ADD_SOURCE_ROOTS" value="true" />
|
||||||
|
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
|
||||||
|
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/Module/IOSActivator.py" />
|
||||||
|
<option name="PARAMETERS" value="" />
|
||||||
|
<option name="SHOW_COMMAND_LINE" value="false" />
|
||||||
|
<option name="EMULATE_TERMINAL" value="false" />
|
||||||
|
<option name="MODULE_MODE" value="false" />
|
||||||
|
<option name="REDIRECT_INPUT" value="false" />
|
||||||
|
<option name="INPUT_FILE" value="" />
|
||||||
|
<method v="2" />
|
||||||
|
</configuration>
|
||||||
<configuration name="Main" type="PythonConfigurationType" factoryName="Python" nameIsGenerated="true">
|
<configuration name="Main" type="PythonConfigurationType" factoryName="Python" nameIsGenerated="true">
|
||||||
<module name="iOSAI" />
|
<module name="iOSAI" />
|
||||||
<option name="ENV_FILES" value="" />
|
<option name="ENV_FILES" value="" />
|
||||||
@@ -86,7 +140,7 @@
|
|||||||
<env name="PYTHONUNBUFFERED" value="1" />
|
<env name="PYTHONUNBUFFERED" value="1" />
|
||||||
</envs>
|
</envs>
|
||||||
<option name="SDK_HOME" value="" />
|
<option name="SDK_HOME" value="" />
|
||||||
<option name="SDK_NAME" value="Python 3.12" />
|
<option name="SDK_NAME" value="IOSAI" />
|
||||||
<option name="WORKING_DIRECTORY" value="" />
|
<option name="WORKING_DIRECTORY" value="" />
|
||||||
<option name="IS_MODULE_SDK" value="false" />
|
<option name="IS_MODULE_SDK" value="false" />
|
||||||
<option name="ADD_CONTENT_ROOTS" value="true" />
|
<option name="ADD_CONTENT_ROOTS" value="true" />
|
||||||
@@ -101,6 +155,15 @@
|
|||||||
<option name="INPUT_FILE" value="" />
|
<option name="INPUT_FILE" value="" />
|
||||||
<method v="2" />
|
<method v="2" />
|
||||||
</configuration>
|
</configuration>
|
||||||
|
<list>
|
||||||
|
<item itemvalue="Python.Main" />
|
||||||
|
<item itemvalue="Python.IOSActivator" />
|
||||||
|
</list>
|
||||||
|
<recent_temporary>
|
||||||
|
<list>
|
||||||
|
<item itemvalue="Python.IOSActivator" />
|
||||||
|
</list>
|
||||||
|
</recent_temporary>
|
||||||
</component>
|
</component>
|
||||||
<component name="SharedIndexes">
|
<component name="SharedIndexes">
|
||||||
<attachedChunks>
|
<attachedChunks>
|
||||||
@@ -151,6 +214,33 @@
|
|||||||
<workItem from="1756817404737" duration="95000" />
|
<workItem from="1756817404737" duration="95000" />
|
||||||
<workItem from="1756876974097" duration="8214000" />
|
<workItem from="1756876974097" duration="8214000" />
|
||||||
<workItem from="1756886673655" duration="1733000" />
|
<workItem from="1756886673655" duration="1733000" />
|
||||||
|
<workItem from="1756902713950" duration="1669000" />
|
||||||
|
<workItem from="1756905956255" duration="1709000" />
|
||||||
|
<workItem from="1756962238298" duration="14230000" />
|
||||||
|
<workItem from="1756979981948" duration="4536000" />
|
||||||
|
<workItem from="1757053266703" duration="9092000" />
|
||||||
|
<workItem from="1757308358620" duration="654000" />
|
||||||
|
<workItem from="1757309200278" duration="8241000" />
|
||||||
|
<workItem from="1757317510820" duration="50000" />
|
||||||
|
<workItem from="1757317658542" duration="8554000" />
|
||||||
|
<workItem from="1757394756623" duration="3551000" />
|
||||||
|
<workItem from="1757398358797" duration="1537000" />
|
||||||
|
<workItem from="1757400284539" duration="1222000" />
|
||||||
|
<workItem from="1757401525466" duration="46000" />
|
||||||
|
<workItem from="1757401583931" duration="29000" />
|
||||||
|
<workItem from="1757401717530" duration="4191000" />
|
||||||
|
<workItem from="1757411020282" duration="11755000" />
|
||||||
|
<workItem from="1757480739367" duration="14580000" />
|
||||||
|
<workItem from="1757498954175" duration="6736000" />
|
||||||
|
<workItem from="1757506636968" duration="5910000" />
|
||||||
|
<workItem from="1757567423145" duration="16668000" />
|
||||||
|
<workItem from="1757998910052" duration="3676000" />
|
||||||
|
<workItem from="1758354349526" duration="2512000" />
|
||||||
|
<workItem from="1758358259572" duration="8994000" />
|
||||||
|
<workItem from="1761294846575" duration="176000" />
|
||||||
|
<workItem from="1761295123036" duration="2130000" />
|
||||||
|
<workItem from="1761302874416" duration="154000" />
|
||||||
|
<workItem from="1761303188517" duration="637000" />
|
||||||
</task>
|
</task>
|
||||||
<task id="LOCAL-00001" summary="ai 开始测试">
|
<task id="LOCAL-00001" summary="ai 开始测试">
|
||||||
<option name="closed" value="true" />
|
<option name="closed" value="true" />
|
||||||
@@ -160,7 +250,39 @@
|
|||||||
<option name="project" value="LOCAL" />
|
<option name="project" value="LOCAL" />
|
||||||
<updated>1756303135240</updated>
|
<updated>1756303135240</updated>
|
||||||
</task>
|
</task>
|
||||||
<option name="localTasksCounter" value="2" />
|
<task id="LOCAL-00002" summary="20250904-初步功能已完成">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1757421902950</created>
|
||||||
|
<option name="number" value="00002" />
|
||||||
|
<option name="presentableId" value="LOCAL-00002" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1757421902950</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00003" summary="20250904-初步功能已完成">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1757494445986</created>
|
||||||
|
<option name="number" value="00003" />
|
||||||
|
<option name="presentableId" value="LOCAL-00003" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1757494445986</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00004" summary="20250904-初步功能已完成">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1757513848107</created>
|
||||||
|
<option name="number" value="00004" />
|
||||||
|
<option name="presentableId" value="LOCAL-00004" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1757513848107</updated>
|
||||||
|
</task>
|
||||||
|
<task id="LOCAL-00005" summary="20250904-初步功能已完成">
|
||||||
|
<option name="closed" value="true" />
|
||||||
|
<created>1757587781103</created>
|
||||||
|
<option name="number" value="00005" />
|
||||||
|
<option name="presentableId" value="LOCAL-00005" />
|
||||||
|
<option name="project" value="LOCAL" />
|
||||||
|
<updated>1757587781103</updated>
|
||||||
|
</task>
|
||||||
|
<option name="localTasksCounter" value="6" />
|
||||||
<servers />
|
<servers />
|
||||||
</component>
|
</component>
|
||||||
<component name="TypeScriptGeneratedFilesManager">
|
<component name="TypeScriptGeneratedFilesManager">
|
||||||
@@ -169,16 +291,38 @@
|
|||||||
<component name="UnknownFeatures">
|
<component name="UnknownFeatures">
|
||||||
<option featureType="com.intellij.fileTypeFactory" implementationName="*.bat" />
|
<option featureType="com.intellij.fileTypeFactory" implementationName="*.bat" />
|
||||||
</component>
|
</component>
|
||||||
|
<component name="Vcs.Log.Tabs.Properties">
|
||||||
|
<option name="TAB_STATES">
|
||||||
|
<map>
|
||||||
|
<entry key="MAIN">
|
||||||
|
<value>
|
||||||
|
<State />
|
||||||
|
</value>
|
||||||
|
</entry>
|
||||||
|
</map>
|
||||||
|
</option>
|
||||||
|
</component>
|
||||||
<component name="VcsManagerConfiguration">
|
<component name="VcsManagerConfiguration">
|
||||||
<option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" />
|
<option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" />
|
||||||
<MESSAGE value="ai 开始测试" />
|
<MESSAGE value="ai 开始测试" />
|
||||||
<option name="LAST_COMMIT_MESSAGE" value="ai 开始测试" />
|
<MESSAGE value="20250904-初步功能已完成" />
|
||||||
|
<option name="LAST_COMMIT_MESSAGE" value="20250904-初步功能已完成" />
|
||||||
</component>
|
</component>
|
||||||
<component name="com.intellij.coverage.CoverageDataManagerImpl">
|
<component name="com.intellij.coverage.CoverageDataManagerImpl">
|
||||||
<SUITE FILE_PATH="coverage/iOSAI$Main.coverage" NAME="Main Coverage Results" MODIFIED="1756877405122" 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$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$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$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$tidevice_entry.coverage" NAME="tidevice_entry Coverage Results" MODIFIED="1756886706033" 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$123.coverage" NAME="123 覆盖结果" MODIFIED="1756300694280" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/test" />
|
|
||||||
<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$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$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$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$456.coverage" NAME="456 覆盖结果" MODIFIED="1757654671631" 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$Main.coverage" NAME="Main 覆盖结果" MODIFIED="1758369933536" 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$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="1758120400301" 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="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>
|
||||||
@@ -14,3 +14,8 @@ class AnchorModel:
|
|||||||
model.anchorId = d.get('anchorId', "")
|
model.anchorId = d.get('anchorId', "")
|
||||||
model.country = d.get('country', "")
|
model.country = d.get('country', "")
|
||||||
return model
|
return model
|
||||||
|
|
||||||
|
# 模型转字典
|
||||||
|
@classmethod
|
||||||
|
def modelToDict(cls, model):
|
||||||
|
return {"anchorId": model.anchorId, "country": model.country}
|
||||||
|
|||||||
@@ -15,7 +15,8 @@ class DeviceModel(object):
|
|||||||
self.scale = scale
|
self.scale = scale
|
||||||
# 1 添加 2删除
|
# 1 添加 2删除
|
||||||
self.type = type
|
self.type = type
|
||||||
|
self.ready = False
|
||||||
|
self.deleting = False
|
||||||
|
|
||||||
# 转字典
|
# 转字典
|
||||||
def toDict(self):
|
def toDict(self):
|
||||||
|
|||||||
@@ -2,15 +2,15 @@ import json
|
|||||||
|
|
||||||
# 返回数据模型
|
# 返回数据模型
|
||||||
class ResultData(object):
|
class ResultData(object):
|
||||||
def __init__(self, code=200, data=None, msg="获取成功"):
|
def __init__(self, code=200, data=None, message="获取成功"):
|
||||||
super(ResultData, self).__init__()
|
super(ResultData, self).__init__()
|
||||||
self.code = code
|
self.code = code
|
||||||
self.data = data
|
self.data = data
|
||||||
self.msg = msg
|
self.message = message
|
||||||
|
|
||||||
def toJson(self):
|
def toJson(self):
|
||||||
return json.dumps({
|
return json.dumps({
|
||||||
"code": self.code,
|
"code": self.code,
|
||||||
"data": self.data,
|
"data": self.data,
|
||||||
"msg": self.msg
|
"message": self.message
|
||||||
}, ensure_ascii=False) # ensure_ascii=False 确保中文不会被转义
|
}, ensure_ascii=False) # ensure_ascii=False 确保中文不会被转义
|
||||||
@@ -3,24 +3,60 @@ from typing import Dict, Any
|
|||||||
from Entity.AnchorModel import AnchorModel
|
from Entity.AnchorModel import AnchorModel
|
||||||
|
|
||||||
# wda apple bundle id
|
# wda apple bundle id
|
||||||
WdaAppBundleId = "com.yolozsAgent.wda.xctrunner"
|
WdaAppBundleId = "com.yolojtAgent.wda.xctrunner"
|
||||||
|
# WdaAppBundleId = "com.yolozsAgent.wda.xctrunner"
|
||||||
|
# wda投屏端口
|
||||||
|
wdaScreenPort = 9567
|
||||||
|
# wda功能端口
|
||||||
|
wdaFunctionPort = 8567
|
||||||
# 全局主播列表
|
# 全局主播列表
|
||||||
anchorList: list[AnchorModel] = []
|
anchorList: list[AnchorModel] = []
|
||||||
# 线程锁
|
# 线程锁
|
||||||
anchorListLock = threading.Lock()
|
anchorListLock = threading.Lock()
|
||||||
# 打招呼数据
|
# 打招呼数据
|
||||||
prologueList = []
|
prologueList = {}
|
||||||
|
|
||||||
|
# 评论数据
|
||||||
|
commentList = []
|
||||||
|
|
||||||
|
API_KEY = "app-sdRfZy2by9Kq7uJg7JdOSVr8"
|
||||||
|
|
||||||
|
# 本地储存的打招呼数据
|
||||||
|
localPrologueList = [
|
||||||
|
"If you are interested in this, you can join our team for a period of time. During this period, if you like our team, you can continue to stay in our team. If you don't like it, you can leave at any time, and you won't lose anything!",
|
||||||
|
"What's even better is that after joining our team, you have no obligations and you don't have to pay me. Everything is free",
|
||||||
|
"I'm from the MARS agency. I noticed that you're very active on TikTok, so I contacted you and hope to become your agent❤",
|
||||||
|
"Hello, can we talk about cooperation and support?",
|
||||||
|
"Hello, I’m a supporter. Can we have a chat?",
|
||||||
|
"Hi 👋 I’m an official TikTok partner. I really enjoy your livestreams. Do you have time to chat about support and cooperation?",
|
||||||
|
"Hello, I really like your livestreams. I’d love to talk about cooperation and also support you.",
|
||||||
|
"Hello, I think you have the potential to become a top streamer. Can we talk?",
|
||||||
|
"Nice to meet you 😊 I watched your livestream and really enjoyed it. Can we chat about cooperation?",
|
||||||
|
"Hello 👋 I’m a livestream team manager. I recently watched your livestream—it’s great 👍 I hope we can talk about how to grow together 💪",
|
||||||
|
"I watched your livestream and would like to invite you to join our team. I’ll reward you with more gifts based on your performance.",
|
||||||
|
"Hello, I’d like to promote your livestream. Can we talk?",
|
||||||
|
"Hello, I think I can help you get more gifts and support for free. Would you be interested in talking with me?",
|
||||||
|
"Hello, I really enjoyed your livestream. Can we chat about cooperation?"
|
||||||
|
]
|
||||||
|
|
||||||
# 评论列表
|
# 评论列表
|
||||||
commentsList = []
|
commentsList = []
|
||||||
# 存储主播名和session_id的字典
|
# 存储主播名和session_id的字典
|
||||||
anchorWithSession = {}
|
anchorWithSession = {}
|
||||||
|
# 前端传递的token
|
||||||
|
token = ''
|
||||||
|
# 前端传递的
|
||||||
|
tenantId = 0
|
||||||
|
|
||||||
|
userId = 0
|
||||||
|
|
||||||
|
|
||||||
# 安全删除数据
|
# 安全删除数据
|
||||||
def removeModelFromAnchorList(model: AnchorModel):
|
def removeModelFromAnchorList(model: AnchorModel):
|
||||||
with anchorListLock:
|
with anchorListLock:
|
||||||
anchorList.remove(model)
|
anchorList.remove(model)
|
||||||
|
|
||||||
|
|
||||||
# 添加数据
|
# 添加数据
|
||||||
def addModelToAnchorList(models: list[Dict[str, Any]]):
|
def addModelToAnchorList(models: list[Dict[str, Any]]):
|
||||||
with anchorListLock:
|
with anchorListLock:
|
||||||
@@ -28,3 +64,7 @@ def addModelToAnchorList(models: list[Dict[str, Any]]):
|
|||||||
obj = AnchorModel.dictToModel(dic)
|
obj = AnchorModel.dictToModel(dic)
|
||||||
anchorList.append(obj)
|
anchorList.append(obj)
|
||||||
|
|
||||||
|
|
||||||
|
# 添加打招呼语
|
||||||
|
def addDataToPrologue(data: list[str]):
|
||||||
|
prologueList = data
|
||||||
|
|||||||
@@ -1,281 +1,513 @@
|
|||||||
# -*- coding: utf-8 -*-
|
|
||||||
import os
|
|
||||||
import signal
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import json
|
import json
|
||||||
import wda
|
import os
|
||||||
|
import socket
|
||||||
import threading
|
import threading
|
||||||
|
import time
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from typing import Dict
|
||||||
from typing import List, Dict, Optional
|
import tidevice
|
||||||
|
import wda
|
||||||
from tidevice import Usbmux
|
from tidevice import Usbmux, ConnectionType
|
||||||
from Entity.DeviceModel import DeviceModel
|
from Entity.DeviceModel import DeviceModel
|
||||||
from Entity.Variables import WdaAppBundleId
|
from Entity.Variables import WdaAppBundleId, wdaFunctionPort
|
||||||
from Module.FlaskSubprocessManager import FlaskSubprocessManager
|
from Module.FlaskSubprocessManager import FlaskSubprocessManager
|
||||||
|
from Module.IOSActivator import IOSActivator
|
||||||
from Utils.LogManager import LogManager
|
from Utils.LogManager import LogManager
|
||||||
|
|
||||||
|
|
||||||
class Deviceinfo(object):
|
class DeviceInfo:
|
||||||
def __init__(self):
|
_instance = None
|
||||||
self.deviceIndex = 0
|
_instance_lock = threading.Lock()
|
||||||
# 投屏端口(本地映射端口起始值,会递增)
|
|
||||||
self.screenProxy = 9110
|
|
||||||
# 记录 iproxy Popen 进程:[{ "id": udid, "target": Popen }, ...]
|
|
||||||
self.pidList: List[Dict] = []
|
|
||||||
# 当前已连接的设备(tidevice 的 Device 对象列表)
|
|
||||||
self.deviceArray: List = []
|
|
||||||
|
|
||||||
# 子进程通信(向前端发送设备信息)
|
# 离线宽限期(保持你原来的数值)
|
||||||
self.manager = FlaskSubprocessManager.get_instance()
|
REMOVE_GRACE_SEC = 5.0
|
||||||
# 已发给前端的设备模型列表(用于拔出时发 type=2)
|
|
||||||
self.deviceModelList: List[DeviceModel] = []
|
|
||||||
# 最大可连接设备限制
|
|
||||||
self.maxDeviceCount = 6
|
|
||||||
# 操作锁
|
|
||||||
self._lock = threading.Lock()
|
|
||||||
|
|
||||||
# ===== iproxy:一次性完成 路径定位 + 环境变量配置 + 启动器准备 =====
|
def __new__(cls, *args, **kwargs):
|
||||||
try:
|
if not cls._instance:
|
||||||
self.iproxy_path = self._iproxy_path() # 绝对路径
|
with cls._instance_lock:
|
||||||
self.iproxy_dir = self.iproxy_path.parent
|
if not cls._instance:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
# 1) 配置环境(PATH/DLL),放到初始化里一次性处理
|
def __init__(self) -> None:
|
||||||
os.environ["PATH"] = str(self.iproxy_dir) + os.pathsep + os.environ.get("PATH", "")
|
if getattr(self, "_initialized", False):
|
||||||
|
return
|
||||||
|
|
||||||
|
self._lock = threading.RLock()
|
||||||
|
self._models: Dict[str, DeviceModel] = {}
|
||||||
|
self._manager = FlaskSubprocessManager.get_instance()
|
||||||
|
self.screenPort = 9110
|
||||||
|
|
||||||
|
# 设备心跳时间
|
||||||
|
self._last_seen: Dict[str, float] = {}
|
||||||
|
|
||||||
|
# iproxy 子进程:udid -> Popen
|
||||||
|
self._iproxy_process: Dict[str, subprocess.Popen] = {}
|
||||||
|
|
||||||
|
# iproxy HTTP 健康检查失败次数:udid -> 连续失败次数
|
||||||
|
self._iproxy_fail_count: Dict[str, int] = {}
|
||||||
|
|
||||||
|
# Windows 下隐藏子进程窗口(给 iproxy 用)
|
||||||
|
self._creationflags = 0
|
||||||
|
self._startupinfo = None
|
||||||
|
if os.name == "nt":
|
||||||
try:
|
try:
|
||||||
# 仅 Windows 有效;其他平台忽略
|
# type: ignore[attr-defined]
|
||||||
os.add_dll_directory(str(self.iproxy_dir))
|
self._creationflags = subprocess.CREATE_NO_WINDOW
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
self._creationflags = 0
|
||||||
|
|
||||||
# 2) 预构建通用 Popen 参数(隐藏窗口、工作目录、文本模式等)
|
si = subprocess.STARTUPINFO()
|
||||||
self._creationflags = 0x08000000 if os.name == "nt" else 0
|
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
self._popen_kwargs = dict(
|
si.wShowWindow = 0 # SW_HIDE
|
||||||
stdout=subprocess.PIPE,
|
self._startupinfo = si
|
||||||
stderr=subprocess.PIPE,
|
|
||||||
cwd=str(self.iproxy_dir),
|
|
||||||
shell=False,
|
|
||||||
text=True,
|
|
||||||
creationflags=self._creationflags,
|
|
||||||
encoding="utf-8",
|
|
||||||
bufsize=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
# 3) 准备一个“启动器”(闭包):仅接受 (udid, local_port, remote_port) 参数
|
LogManager.info("DeviceInfo 初始化完成", udid="system")
|
||||||
def _spawn_iproxy(udid: str, local_port: int, remote_port: int = 9100) -> subprocess.Popen:
|
print("[Init] DeviceInfo 初始化完成")
|
||||||
args = [str(self.iproxy_path), "-u", udid, str(local_port), str(remote_port)]
|
self._initialized = True
|
||||||
p = subprocess.Popen(args, **self._popen_kwargs)
|
|
||||||
|
|
||||||
# 异步日志转发(可选)
|
# ==========================
|
||||||
def _pipe_to_log(name: str, stream):
|
# 主循环
|
||||||
try:
|
# ==========================
|
||||||
for line in iter(stream.readline, ''):
|
def listen(self):
|
||||||
s = line.strip()
|
LogManager.method_info("进入主循环", "listen", udid="system")
|
||||||
if s:
|
print("[Listen] 开始监听设备上下线...")
|
||||||
LogManager.info(f"[iproxy {name}] {s}", udid)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
try:
|
|
||||||
import threading
|
|
||||||
threading.Thread(target=_pipe_to_log, args=("STDOUT", p.stdout), daemon=True).start()
|
|
||||||
threading.Thread(target=_pipe_to_log, args=("STDERR", p.stderr), daemon=True).start()
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return p
|
|
||||||
|
|
||||||
self._spawn_iproxy = _spawn_iproxy # 保存启动器
|
|
||||||
LogManager.info(f"iproxy 启动器已就绪,目录: {self.iproxy_dir}")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
# 没找到 iproxy 也允许实例化成功,但后续启动会失败并给出明确日志
|
|
||||||
self.iproxy_path = None
|
|
||||||
self.iproxy_dir = None
|
|
||||||
self._spawn_iproxy = None
|
|
||||||
LogManager.error(f"初始化 iproxy 失败:{e}")
|
|
||||||
|
|
||||||
# ----------------------------
|
|
||||||
# 监听设备连接(死循环,内部捕获异常)
|
|
||||||
# ----------------------------
|
|
||||||
def startDeviceListener(self):
|
|
||||||
while True:
|
while True:
|
||||||
try:
|
try:
|
||||||
lists = Usbmux().device_list()
|
usb = Usbmux().device_list()
|
||||||
|
# 只看 USB 连接的设备
|
||||||
|
online = {d.udid for d in usb if d.conn_type == ConnectionType.USB}
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
# 另一台电脑常见:usbmuxd 连接失败(未安装 iTunes/Apple Mobile Device Support)
|
LogManager.warning(f"[device_list] 异常:{e}", udid="system")
|
||||||
LogManager.warning(f"usbmuxd 连接失败: {e}。请确认已安装 iTunes/Apple Mobile Device Support,并在手机上“信任此电脑”")
|
time.sleep(1)
|
||||||
time.sleep(2)
|
|
||||||
continue
|
continue
|
||||||
# 新接入设备
|
|
||||||
for device in lists:
|
now = time.time()
|
||||||
if (device not in self.deviceArray) and (len(self.deviceArray) < self.maxDeviceCount):
|
|
||||||
self.screenProxy += 1
|
# 当前已知的设备(本轮循环开始时)
|
||||||
try:
|
with self._lock:
|
||||||
self.connectDevice(device.udid)
|
known = set(self._models.keys())
|
||||||
self.deviceArray.append(device)
|
current_count = len(self._models)
|
||||||
except Exception as e:
|
|
||||||
LogManager.error(f"连接设备失败 {device.udid}: {e}", device.udid)
|
# 1. 处理在线设备
|
||||||
# 拔出设备处理
|
for udid in online:
|
||||||
self._removeDisconnected(lists)
|
# 更新心跳时间
|
||||||
|
self._last_seen[udid] = now
|
||||||
|
|
||||||
|
# 新设备但数量已达上限
|
||||||
|
if udid not in known and current_count >= 6:
|
||||||
|
print(f"[Add] 设备数量已达 6 台,忽略新设备: {udid}")
|
||||||
|
LogManager.info(
|
||||||
|
"[Add] 设备数量已达上限(6),忽略新设备",
|
||||||
|
udid=udid,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 已经在列表里的设备,跳过添加流程
|
||||||
|
if udid in known:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 只对新发现的设备做一次信任检查
|
||||||
|
try:
|
||||||
|
if not self._is_trusted(udid):
|
||||||
|
LogManager.info(
|
||||||
|
"[Add] 设备未信任或未就绪,跳过本轮添加",
|
||||||
|
udid=udid,
|
||||||
|
)
|
||||||
|
print(f"[Add] 设备未信任或未就绪,跳过: {udid}")
|
||||||
|
continue
|
||||||
|
except Exception as e:
|
||||||
|
LogManager.warning(
|
||||||
|
f"[Add] 检测设备 {udid} 信任状态异常: {e}",
|
||||||
|
udid=udid,
|
||||||
|
)
|
||||||
|
print(f"[Add] 检测设备 {udid} 信任状态异常: {e}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 二次确认数量上限
|
||||||
|
with self._lock:
|
||||||
|
if len(self._models) >= 6:
|
||||||
|
print(f"[Add] 二次检查: 设备数量已达 6 台,忽略新设备: {udid}")
|
||||||
|
LogManager.info(
|
||||||
|
"[Add] 二次检查数量上限,忽略新设备",
|
||||||
|
udid=udid,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 真正添加设备
|
||||||
|
try:
|
||||||
|
self._add_device(udid)
|
||||||
|
current_count += 1
|
||||||
|
except Exception as e:
|
||||||
|
LogManager.warning(
|
||||||
|
f"[Add] 处理设备 {udid} 异常: {e}",
|
||||||
|
udid=udid,
|
||||||
|
)
|
||||||
|
print(f"[Add] 处理设备 {udid} 异常: {e}")
|
||||||
|
|
||||||
|
# 2. 处理可能离线的设备(只看本轮开始时 known 里的)
|
||||||
|
for udid in list(known):
|
||||||
|
if udid not in online:
|
||||||
|
last = self._last_seen.get(udid, 0)
|
||||||
|
if now - last > self.REMOVE_GRACE_SEC:
|
||||||
|
try:
|
||||||
|
self._remove_device(udid)
|
||||||
|
except Exception as e:
|
||||||
|
LogManager.method_error(
|
||||||
|
f"移除失败:{e}",
|
||||||
|
"listen",
|
||||||
|
udid=udid,
|
||||||
|
)
|
||||||
|
print(f"[Remove] 移除失败 {udid}: {e}")
|
||||||
|
|
||||||
|
# 3. iproxy 看门狗(进程 + HTTP 探活)
|
||||||
|
try:
|
||||||
|
self._check_iproxy_health()
|
||||||
|
except Exception as e:
|
||||||
|
LogManager.warning(
|
||||||
|
f"[iproxy] 看门狗异常: {e}",
|
||||||
|
udid="system",
|
||||||
|
)
|
||||||
|
print(f"[iproxy] 看门狗异常: {e}")
|
||||||
|
|
||||||
time.sleep(1)
|
time.sleep(1)
|
||||||
|
|
||||||
# ----------------------------
|
# 判断设备是否信任
|
||||||
# 连接单台设备:启动 WDA、读取屏参、通知前端、映射投屏端口
|
def _is_trusted(self, udid: str) -> bool:
|
||||||
# ----------------------------
|
|
||||||
def connectDevice(self, identifier: str):
|
|
||||||
# 1) 连接 WDA(USBClient -> 设备 8100)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
d = wda.USBClient(identifier, 8100)
|
d = tidevice.Device(udid)
|
||||||
LogManager.info("启动 WDA 成功", identifier)
|
_ = d.product_version
|
||||||
|
return True
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LogManager.error(f"启动 WDA 失败,请检查手机是否已信任、WDA 是否正常。错误: {e}", identifier)
|
msg = str(e)
|
||||||
return # 不抛出到外层,保持监听循环健壮
|
if "NotTrusted" in msg or "Please trust" in msg or "InvalidHostID" in msg:
|
||||||
|
print(f"[Trust] 设备未信任,udid={udid}, err={msg}")
|
||||||
|
return False
|
||||||
|
|
||||||
# 2) 读取屏幕信息(失败不影响主流程)
|
print(f"[Trust] 检测信任状态出错,当作未信任处理 udid={udid}, err={msg}")
|
||||||
width, height, scale = 0, 0, 1.0
|
return False
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# 添加设备
|
||||||
|
# ==========================
|
||||||
|
def _add_device(self, udid: str):
|
||||||
|
with self._lock:
|
||||||
|
if udid in self._models:
|
||||||
|
print(f"[Add] 已存在,跳过: {udid}")
|
||||||
|
return
|
||||||
|
print(f"[Add] 新增设备 {udid}")
|
||||||
|
|
||||||
|
# 判断 iOS 版本
|
||||||
try:
|
try:
|
||||||
size = d.window_size()
|
t = tidevice.Device(udid)
|
||||||
width, height = size.width, size.height
|
version_major = float(t.product_version.split(".")[0])
|
||||||
scale = d.scale
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LogManager.warning(f"读取屏幕信息失败:{e}", identifier)
|
print(f"[Add] 获取系统版本失败 {udid}: {e}")
|
||||||
|
version_major = 0
|
||||||
|
|
||||||
# 3) 组装模型并发送给前端
|
# 分配投屏端口 & 写入模型
|
||||||
model = DeviceModel(identifier, self.screenProxy, width, height, scale, type=1)
|
with self._lock:
|
||||||
self.deviceModelList.append(model)
|
self.screenPort += 1
|
||||||
|
screen_port = self.screenPort
|
||||||
|
|
||||||
|
model = DeviceModel(
|
||||||
|
deviceId=udid,
|
||||||
|
screenPort=screen_port,
|
||||||
|
width=0,
|
||||||
|
height=0,
|
||||||
|
scale=0,
|
||||||
|
type=1,
|
||||||
|
)
|
||||||
|
self._models[udid] = model
|
||||||
|
|
||||||
|
print(f"[Add] 新设备完成 {udid}, screenPort={screen_port}")
|
||||||
|
self._manager_send()
|
||||||
|
|
||||||
|
# 启动 iproxy(投屏转发)
|
||||||
try:
|
try:
|
||||||
self.manager.send(model.toDict())
|
self._start_iproxy(udid, screen_port)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LogManager.warning(f"向前端发送设备模型失败:{e}", identifier)
|
print(f"[iproxy] 启动失败 {udid}: {e}")
|
||||||
|
LogManager.warning(f"[iproxy] 启动失败: {e}", udid=udid)
|
||||||
|
|
||||||
# 4) 可选:启动你的 app 并回到桌面
|
# 启动 WDA
|
||||||
|
if version_major >= 17.0:
|
||||||
|
threading.Thread(
|
||||||
|
target=IOSActivator().activate_ios17,
|
||||||
|
args=(udid, self._on_wda_ready),
|
||||||
|
daemon=True,
|
||||||
|
).start()
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
tidevice.Device(udid).app_start(WdaAppBundleId)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Add] 使用 tidevice 启动 WDA 失败 {udid}: {e}")
|
||||||
|
LogManager.warning(
|
||||||
|
f"[Add] 使用 tidevice 启动 WDA 失败: {e}",
|
||||||
|
udid=udid,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
threading.Thread(
|
||||||
|
target=self._fetch_screen_and_notify,
|
||||||
|
args=(udid,),
|
||||||
|
daemon=True,
|
||||||
|
).start()
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# WDA 启动回调(iOS17+)
|
||||||
|
# ==========================
|
||||||
|
def _on_wda_ready(self, udid: str):
|
||||||
|
print(f"[WDA] 回调触发,准备获取屏幕信息 udid={udid}")
|
||||||
|
time.sleep(1)
|
||||||
|
threading.Thread(
|
||||||
|
target=self._fetch_screen_and_notify,
|
||||||
|
args=(udid,),
|
||||||
|
daemon=True,
|
||||||
|
).start()
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# 通过 WDA 获取屏幕信息
|
||||||
|
# ==========================
|
||||||
|
def _screen_info(self, udid: str):
|
||||||
try:
|
try:
|
||||||
d.app_start(WdaAppBundleId)
|
c = wda.USBClient(udid, wdaFunctionPort)
|
||||||
d.home()
|
size = c.window_size()
|
||||||
|
|
||||||
|
w = int(size.width)
|
||||||
|
h = int(size.height)
|
||||||
|
s = float(c.scale)
|
||||||
|
|
||||||
|
print(f"[Screen] 成功获取屏幕 {w}x{h} scale={s} {udid}")
|
||||||
|
return w, h, s
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
LogManager.warning(f"启动/切回桌面失败:{e}", identifier)
|
print(f"[Screen] 获取屏幕失败: {e} udid={udid}")
|
||||||
|
return 0, 0, 0.0
|
||||||
|
|
||||||
time.sleep(2)
|
# ==========================
|
||||||
|
# 异步获取屏幕尺寸并通知 Flask
|
||||||
|
# ==========================
|
||||||
|
def _fetch_screen_and_notify(self, udid: str):
|
||||||
|
"""
|
||||||
|
后台线程里多次尝试通过 WDA 获取屏幕尺寸,
|
||||||
|
成功后更新 model 并发一次 snapshot。
|
||||||
|
"""
|
||||||
|
max_retry = 15
|
||||||
|
interval = 1.0
|
||||||
|
|
||||||
# 5) 本地端口 -> 设备端口 的映射(投屏:本地 self.screenProxy -> 设备 9100)
|
time.sleep(2.0)
|
||||||
target = self.relayDeviceScreenPort(identifier)
|
for _ in range(max_retry):
|
||||||
# 加个非空判断
|
|
||||||
if target is not None:
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
self.pidList.append({"target": target, "id": identifier})
|
if udid not in self._models:
|
||||||
|
print(f"[Screen] 设备已移除,停止获取屏幕信息 udid={udid}")
|
||||||
|
return
|
||||||
|
|
||||||
# 安全杀死iproxy进程
|
w, h, s = self._screen_info(udid)
|
||||||
def _terminate_proc(self, p: subprocess.Popen):
|
if w > 0 and h > 0:
|
||||||
|
with self._lock:
|
||||||
|
m = self._models.get(udid)
|
||||||
|
if not m:
|
||||||
|
print(f"[Screen] 模型已不存在,无法更新 udid={udid}")
|
||||||
|
return
|
||||||
|
m.width = w
|
||||||
|
m.height = h
|
||||||
|
m.scale = s
|
||||||
|
|
||||||
|
print(f"[Screen] 屏幕信息更新完成,准备推送到 Flask udid={udid}")
|
||||||
|
try:
|
||||||
|
self._manager_send()
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[Screen] 发送屏幕更新到 Flask 失败 udid={udid}, err={e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
time.sleep(interval)
|
||||||
|
|
||||||
|
print(f"[Screen] 多次尝试仍未获取到屏幕信息 udid={udid}")
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# iproxy 管理
|
||||||
|
# ==========================
|
||||||
|
def _start_iproxy(self, udid: str, local_port: int):
|
||||||
|
iproxy_path = self._find_iproxy()
|
||||||
|
|
||||||
|
p = self._iproxy_process.get(udid)
|
||||||
|
if p is not None and p.poll() is None:
|
||||||
|
print(f"[iproxy] 已存在运行中的进程,跳过 {udid}")
|
||||||
|
return
|
||||||
|
|
||||||
|
args = [
|
||||||
|
iproxy_path,
|
||||||
|
"-u",
|
||||||
|
udid,
|
||||||
|
str(local_port), # 本地端口(投屏)
|
||||||
|
"9567", # 手机端口(go-ios screencast)
|
||||||
|
]
|
||||||
|
|
||||||
|
print(f"[iproxy] 启动进程: {args}")
|
||||||
|
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
args,
|
||||||
|
stdout=subprocess.DEVNULL,
|
||||||
|
stderr=subprocess.DEVNULL,
|
||||||
|
creationflags=self._creationflags,
|
||||||
|
startupinfo=self._startupinfo,
|
||||||
|
)
|
||||||
|
|
||||||
|
self._iproxy_process[udid] = proc
|
||||||
|
|
||||||
|
def _stop_iproxy(self, udid: str):
|
||||||
|
p = self._iproxy_process.get(udid)
|
||||||
if not p:
|
if not p:
|
||||||
return
|
return
|
||||||
if p.poll() is not None:
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
p.terminate() # 先温柔
|
p.terminate()
|
||||||
p.wait(timeout=3)
|
try:
|
||||||
|
p.wait(timeout=2)
|
||||||
|
except Exception:
|
||||||
|
p.kill()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
self._iproxy_process.pop(udid, None)
|
||||||
|
print(f"[iproxy] 已停止 {udid}")
|
||||||
|
|
||||||
|
def _is_iproxy_http_healthy(self, local_port: int, timeout: float = 1.0) -> bool:
|
||||||
|
"""
|
||||||
|
通过向本地 iproxy 转发端口发一个最小的 HTTP 请求,
|
||||||
|
来判断隧道是否“活着”:
|
||||||
|
- 正常:能在超时时间内读到一些 HTTP 头 / 任意字节;
|
||||||
|
- 异常:连接失败、超时、完全收不到字节,都认为不健康。
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
with socket.create_connection(("127.0.0.1", local_port), timeout=timeout) as s:
|
||||||
|
s.settimeout(timeout)
|
||||||
|
|
||||||
|
req = b"GET / HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n"
|
||||||
|
s.sendall(req)
|
||||||
|
|
||||||
|
data = s.recv(128)
|
||||||
|
if not data:
|
||||||
|
return False
|
||||||
|
|
||||||
|
if data.startswith(b"HTTP/") or b"\r\n" in data:
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 即使不是标准 HTTP 头,只要有返回字节,也说明隧道有响应
|
||||||
|
return True
|
||||||
|
|
||||||
|
except (socket.timeout, OSError):
|
||||||
|
return False
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
def _check_iproxy_health(self):
|
||||||
|
"""
|
||||||
|
iproxy 看门狗:
|
||||||
|
- 先看进程是否存在 / 已退出;
|
||||||
|
- 再做一次 HTTP 层探活;
|
||||||
|
- 连续多次失败才重启,避免抖动时频繁重启。
|
||||||
|
"""
|
||||||
|
with self._lock:
|
||||||
|
items = list(self._models.items())
|
||||||
|
|
||||||
|
for udid, model in items:
|
||||||
|
proc = self._iproxy_process.get(udid)
|
||||||
|
|
||||||
|
# 1) 进程不存在或已退出:直接重启
|
||||||
|
if proc is None or proc.poll() is not None:
|
||||||
|
msg = f"[iproxy] 进程已退出,准备重启 | udid={udid}"
|
||||||
|
print(msg)
|
||||||
|
LogManager.warning(msg, "iproxy")
|
||||||
|
|
||||||
|
self._iproxy_fail_count[udid] = 0
|
||||||
|
try:
|
||||||
|
self._start_iproxy(udid, model.screenPort)
|
||||||
|
except Exception as e:
|
||||||
|
msg = f"[iproxy] 重启失败 | udid={udid} | err={e}"
|
||||||
|
print(msg)
|
||||||
|
LogManager.warning(msg, "iproxy")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 2) 进程还在,做一次 HTTP 探活
|
||||||
|
is_ok = self._is_iproxy_http_healthy(model.screenPort)
|
||||||
|
|
||||||
|
if is_ok:
|
||||||
|
if self._iproxy_fail_count.get(udid):
|
||||||
|
msg = f"[iproxy] HTTP 探活恢复正常 | udid={udid}"
|
||||||
|
print(msg)
|
||||||
|
LogManager.info(msg, "iproxy")
|
||||||
|
self._iproxy_fail_count[udid] = 0
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 3) HTTP 探活失败:记录一次失败
|
||||||
|
fail = self._iproxy_fail_count.get(udid, 0) + 1
|
||||||
|
self._iproxy_fail_count[udid] = fail
|
||||||
|
|
||||||
|
msg = f"[iproxy] HTTP 探活失败 {fail} 次 | udid={udid}"
|
||||||
|
print(msg)
|
||||||
|
LogManager.warning(msg, "iproxy")
|
||||||
|
|
||||||
|
FAIL_THRESHOLD = 3
|
||||||
|
if fail >= FAIL_THRESHOLD:
|
||||||
|
msg = f"[iproxy] 连续 {fail} 次 HTTP 探活失败,准备重启 | udid={udid}"
|
||||||
|
print(msg)
|
||||||
|
LogManager.warning(msg, "iproxy")
|
||||||
|
|
||||||
|
self._iproxy_fail_count[udid] = 0
|
||||||
|
try:
|
||||||
|
self._stop_iproxy(udid)
|
||||||
|
self._start_iproxy(udid, model.screenPort)
|
||||||
|
except Exception as e:
|
||||||
|
msg = f"[iproxy] HTTP 探活重启失败 | udid={udid} | err={e}"
|
||||||
|
print(msg)
|
||||||
|
LogManager.warning(msg, "iproxy")
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# 移除设备
|
||||||
|
# ==========================
|
||||||
|
def _remove_device(self, udid: str):
|
||||||
|
print(f"[Remove] 移除设备 {udid}")
|
||||||
|
|
||||||
|
self._stop_iproxy(udid)
|
||||||
|
|
||||||
|
with self._lock:
|
||||||
|
self._models.pop(udid, None)
|
||||||
|
self._last_seen.pop(udid, None)
|
||||||
|
self._iproxy_fail_count.pop(udid, None)
|
||||||
|
|
||||||
|
self._manager_send()
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# 工具方法
|
||||||
|
# ==========================
|
||||||
|
def _find_iproxy(self) -> str:
|
||||||
|
base = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
name = "iproxy.exe" if os.name == "nt" else "iproxy"
|
||||||
|
return os.path.join(base, "resources", "iproxy", name)
|
||||||
|
|
||||||
|
# ==========================
|
||||||
|
# 同步数据到 Flask
|
||||||
|
# ==========================
|
||||||
|
def _manager_send(self):
|
||||||
|
try:
|
||||||
|
self._send_snapshot_to_flask()
|
||||||
except Exception:
|
except Exception:
|
||||||
try:
|
try:
|
||||||
if os.name == "posix":
|
self._manager.start()
|
||||||
try:
|
except Exception:
|
||||||
# 如果 iproxy 启动时用了 setsid,这里可杀整个进程组
|
pass
|
||||||
os.killpg(os.getpgid(p.pid), signal.SIGKILL)
|
try:
|
||||||
except Exception:
|
self._send_snapshot_to_flask()
|
||||||
p.kill()
|
|
||||||
else:
|
|
||||||
p.kill() # Windows 直接 kill
|
|
||||||
p.wait(timeout=2) # 一定要 wait,避免僵尸
|
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
# ----------------------------
|
def _send_snapshot_to_flask(self):
|
||||||
# 处理拔出设备:发通知、关掉 iproxy、移出状态
|
|
||||||
# ----------------------------
|
|
||||||
def _removeDisconnected(self, current_list):
|
|
||||||
# 1) 计算“被拔出”的 UDID 集合 —— 用 UDID,而不是对象做集合运算
|
|
||||||
try:
|
|
||||||
prev_udids = {getattr(d, "udid", None) for d in self.deviceArray if getattr(d, "udid", None)}
|
|
||||||
now_udids = {getattr(d, "udid", None) for d in current_list if getattr(d, "udid", None)}
|
|
||||||
except Exception as e:
|
|
||||||
LogManager.error(f"收集 UDID 失败:{e}", "")
|
|
||||||
return
|
|
||||||
removed_udids = prev_udids - now_udids
|
|
||||||
if not removed_udids:
|
|
||||||
return
|
|
||||||
|
|
||||||
# 2) 加锁,避免多线程同时改三个列表
|
|
||||||
if not hasattr(self, "_lock"):
|
|
||||||
self._lock = threading.RLock()
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
# 2.1 通知前端并清理 deviceModelList
|
devices = [m.toDict() for m in self._models.values()]
|
||||||
for udid in list(removed_udids):
|
|
||||||
for a in list(self.deviceModelList):
|
|
||||||
if udid == getattr(a, "deviceId", None):
|
|
||||||
a.type = 2
|
|
||||||
try:
|
|
||||||
self.manager.send(a.toDict())
|
|
||||||
except Exception as e:
|
|
||||||
LogManager.warning(f"发送下线事件失败:{e}", udid)
|
|
||||||
try:
|
|
||||||
self.deviceModelList.remove(a)
|
|
||||||
except ValueError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 2.2 关闭该 UDID 的所有 iproxy
|
payload = json.dumps({"devices": devices}, ensure_ascii=False)
|
||||||
survivors = []
|
port = int(os.getenv("FLASK_COMM_PORT", "34566"))
|
||||||
for k in list(self.pidList):
|
|
||||||
kid = k.get("id")
|
|
||||||
if kid in removed_udids:
|
|
||||||
p = k.get("target")
|
|
||||||
try:
|
|
||||||
self._terminate_proc(p)
|
|
||||||
except Exception as e:
|
|
||||||
LogManager.warning(f"关闭 iproxy 异常:{e}", kid)
|
|
||||||
# 不再把该项放回 survivors,相当于移除
|
|
||||||
else:
|
|
||||||
survivors.append(k)
|
|
||||||
self.pidList = survivors
|
|
||||||
|
|
||||||
# 2.3 从已连接集合中移除(按 UDID 过滤,避免对象引用不一致导致 remove 失败)
|
with socket.create_connection(("127.0.0.1", port), timeout=1.5) as s:
|
||||||
self.deviceArray = [d for d in self.deviceArray if getattr(d, "udid", None) not in removed_udids]
|
s.sendall(payload.encode() + b"\n")
|
||||||
|
|
||||||
# 3) 打点
|
print(f"[SNAPSHOT] 已发送 {len(devices)} 台设备")
|
||||||
for udid in removed_udids:
|
|
||||||
LogManager.info("设备已拔出,清理完成(下线通知 + 端口映射关闭 + 状态移除)", udid)
|
|
||||||
|
|
||||||
# ----------------------------
|
|
||||||
# 根目录与 iproxy 可执行文件定位
|
|
||||||
# ----------------------------
|
|
||||||
def _base_dir(self) -> Path:
|
|
||||||
if getattr(sys, "frozen", False):
|
|
||||||
return Path(sys.executable).resolve().parent
|
|
||||||
return Path(__file__).resolve().parents[1] # iOSAI/ 作为根
|
|
||||||
|
|
||||||
def _iproxy_path(self) -> Path:
|
|
||||||
exe = "iproxy.exe" if os.name == "nt" else "iproxy"
|
|
||||||
base = self._base_dir()
|
|
||||||
candidates = [
|
|
||||||
base / "resources" / "iproxy" / exe, # 推荐放置
|
|
||||||
]
|
|
||||||
for p in candidates:
|
|
||||||
if p.exists():
|
|
||||||
return p
|
|
||||||
raise FileNotFoundError(f"iproxy not found, tried: {[str(c) for c in candidates]}")
|
|
||||||
|
|
||||||
# ----------------------------
|
|
||||||
# 端口映射:仅做“转发端口”这件事(调用已准备好的启动器)
|
|
||||||
# ----------------------------
|
|
||||||
def relayDeviceScreenPort(self, udid: str) -> Optional[subprocess.Popen]:
|
|
||||||
if not self._spawn_iproxy:
|
|
||||||
LogManager.error("iproxy 启动器未就绪,无法建立端口映射(初始化时未找到 iproxy)。", udid)
|
|
||||||
return None
|
|
||||||
|
|
||||||
try:
|
|
||||||
p = self._spawn_iproxy(udid, self.screenProxy, 9100)
|
|
||||||
LogManager.info(f"启动 iproxy 成功,本地 {self.screenProxy} -> 设备 9100", udid)
|
|
||||||
return p
|
|
||||||
except Exception as e:
|
|
||||||
LogManager.error(f"启动 iproxy 失败:{e}", udid)
|
|
||||||
return None
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -1,10 +1,11 @@
|
|||||||
import subprocess
|
# -*- coding: utf-8 -*-
|
||||||
import sys
|
|
||||||
import threading
|
|
||||||
import atexit
|
import atexit
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import socket
|
import socket
|
||||||
|
import subprocess
|
||||||
|
import sys
|
||||||
|
import threading
|
||||||
import time
|
import time
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Union, Dict, List
|
from typing import Optional, Union, Dict, List
|
||||||
@@ -13,119 +14,257 @@ from Utils.LogManager import LogManager
|
|||||||
|
|
||||||
|
|
||||||
class FlaskSubprocessManager:
|
class FlaskSubprocessManager:
|
||||||
_instance: Optional['FlaskSubprocessManager'] = None
|
"""
|
||||||
_lock: threading.Lock = threading.Lock()
|
超稳定版 Flask 子进程守护
|
||||||
|
- 单线程 watchdog(唯一监控点)
|
||||||
|
- 强制端口检测
|
||||||
|
- 端口不通 / 子进程退出 → 100% 重启
|
||||||
|
- 完整支持 exe + Python 模式
|
||||||
|
- 自动恢复设备列表快照
|
||||||
|
"""
|
||||||
|
|
||||||
|
_instance = None
|
||||||
|
_lock = threading.RLock()
|
||||||
|
|
||||||
def __new__(cls):
|
def __new__(cls):
|
||||||
with cls._lock:
|
with cls._lock:
|
||||||
if cls._instance is None:
|
if cls._instance is None:
|
||||||
cls._instance = super().__new__(cls)
|
cls._instance = super().__new__(cls)
|
||||||
cls._instance._init_manager()
|
cls._instance._initialize()
|
||||||
return cls._instance
|
return cls._instance
|
||||||
|
|
||||||
def _init_manager(self):
|
# ========================= 初始化 =========================
|
||||||
|
def _initialize(self):
|
||||||
self.process: Optional[subprocess.Popen] = None
|
self.process: Optional[subprocess.Popen] = None
|
||||||
self.comm_port = 34566
|
self.comm_port = 34566
|
||||||
|
self._watchdog_running = False
|
||||||
self._stop_event = threading.Event()
|
self._stop_event = threading.Event()
|
||||||
|
|
||||||
|
self._restart_cooldown = 5 # 每次重启最少间隔
|
||||||
|
self._restart_fail_threshold = 3 # 端口检查连续失败几次才重启
|
||||||
|
self._restart_fail_count = 0
|
||||||
|
|
||||||
|
self._restart_window = 600 # 10 分钟
|
||||||
|
self._restart_limit = 5 # 最多次数
|
||||||
|
self._restart_record: List[float] = []
|
||||||
|
|
||||||
|
if os.name == "nt":
|
||||||
|
si = subprocess.STARTUPINFO()
|
||||||
|
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
|
si.wShowWindow = 0
|
||||||
|
self._si = si
|
||||||
|
else:
|
||||||
|
self._si = None
|
||||||
|
|
||||||
atexit.register(self.stop)
|
atexit.register(self.stop)
|
||||||
|
self._kill_orphans()
|
||||||
|
|
||||||
# 可以把 _find_available_port 留着备用,但 start 前先校验端口是否被占用
|
LogManager.info("FlaskSubprocessManager 初始化完成", udid="flask")
|
||||||
def _is_port_busy(self, port: int) -> bool:
|
|
||||||
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
|
|
||||||
s.settimeout(0.2)
|
|
||||||
return s.connect_ex(("127.0.0.1", port)) == 0
|
|
||||||
|
|
||||||
# 启动flask
|
# ========================= 工具 =========================
|
||||||
|
def _log(self, level, msg):
|
||||||
|
print(msg)
|
||||||
|
if level == "info":
|
||||||
|
LogManager.info(msg, udid="flask")
|
||||||
|
elif level == "warn":
|
||||||
|
LogManager.warning(msg, udid="flask")
|
||||||
|
else:
|
||||||
|
LogManager.error(msg, udid="flask")
|
||||||
|
|
||||||
|
# 杀死残留 python.exe 占用端口
|
||||||
|
def _kill_orphans(self):
|
||||||
|
try:
|
||||||
|
if os.name == "nt":
|
||||||
|
out = subprocess.check_output(["netstat", "-ano"], text=True)
|
||||||
|
for line in out.splitlines():
|
||||||
|
if f"127.0.0.1:{self.comm_port}" in line and "LISTENING" in line:
|
||||||
|
pid = int(line.strip().split()[-1])
|
||||||
|
if pid != os.getpid():
|
||||||
|
subprocess.run(
|
||||||
|
["taskkill", "/F", "/PID", str(pid)],
|
||||||
|
capture_output=True
|
||||||
|
)
|
||||||
|
self._log("warn", f"[FlaskMgr] 杀死残留 Flask 实例 PID={pid}")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def _port_alive(self):
|
||||||
|
"""检测 Flask 与 Quart 的两个端口是否活着"""
|
||||||
|
def _check(p):
|
||||||
|
try:
|
||||||
|
with socket.create_connection(("127.0.0.1", p), timeout=0.4):
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
|
return _check(self.comm_port) or _check(self.comm_port + 1)
|
||||||
|
|
||||||
|
# ========================= 启动 =========================
|
||||||
|
# ========================= 启动 =========================
|
||||||
def start(self):
|
def start(self):
|
||||||
"""启动 Flask 子进程(兼容打包后的 exe 和源码运行)"""
|
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if self.process is not None:
|
# 已经有一个在跑了就别重复起
|
||||||
LogManager.warning("子进程正在运行中!")
|
if self.process and self.process.poll() is None:
|
||||||
raise RuntimeError("子进程已在运行中!")
|
self._log("warn", "[FlaskMgr] Flask 已在运行,跳过")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 设定环境变量,给子进程用
|
||||||
env = os.environ.copy()
|
env = os.environ.copy()
|
||||||
env["FLASK_COMM_PORT"] = str(self.comm_port)
|
env["FLASK_COMM_PORT"] = str(self.comm_port)
|
||||||
|
|
||||||
# —— 解析打包 exe 的稳健写法 ——
|
# ✅ 正确判断是否是 Nuitka/打包后的 exe
|
||||||
exe_path = Path(sys.executable).resolve()
|
# - 被 Nuitka 打包:sys.frozen 会存在/为 True
|
||||||
if exe_path.name.lower() in ("python.exe", "pythonw.exe"):
|
# - 直接用 python 跑 .py:sys.frozen 不存在
|
||||||
# Nuitka 某些场景里 sys.executable 可能指向 dist\python.exe(并不存在)
|
is_frozen = bool(getattr(sys, "frozen", False))
|
||||||
exe_path = Path(sys.argv[0]).resolve()
|
|
||||||
|
|
||||||
is_frozen = exe_path.suffix.lower() == ".exe" and exe_path.exists()
|
|
||||||
|
|
||||||
if is_frozen:
|
if is_frozen:
|
||||||
# 打包后的 exe:用当前 exe 自举
|
# 打包后的 exe 模式:直接调用自己
|
||||||
cmd = [str(exe_path), "--role=flask"]
|
exe = Path(sys.executable).resolve()
|
||||||
cwd = str(exe_path.parent)
|
cmd = [str(exe), "--role=flask"]
|
||||||
|
cwd = str(exe.parent)
|
||||||
else:
|
else:
|
||||||
# 源码运行:模块方式更稳
|
# 开发模式:用 python 去跑 Module/Main.py --role=flask
|
||||||
cmd = [sys.executable, "-m", "Module.Main", "--role=flask"]
|
project_root = Path(__file__).resolve().parents[1]
|
||||||
cwd = str(Path(__file__).resolve().parent) # Module 目录
|
main_py = project_root / "Module" / "Main.py"
|
||||||
|
cmd = [sys.executable, "-u", str(main_py), "--role=flask"]
|
||||||
|
cwd = str(project_root)
|
||||||
|
|
||||||
LogManager.info(f"[DEBUG] spawn: {cmd} (cwd={cwd}) exists(exe)={os.path.exists(cmd[0])}")
|
self._log("info", f"[FlaskMgr] 启动 Flask: {cmd}")
|
||||||
print(f"[DEBUG] spawn: {cmd} (cwd={cwd}) exists(exe)={os.path.exists(cmd[0])}")
|
|
||||||
|
|
||||||
self.process = subprocess.Popen(
|
self.process = subprocess.Popen(
|
||||||
cmd,
|
cmd,
|
||||||
stdin=subprocess.PIPE,
|
|
||||||
stdout=subprocess.PIPE,
|
stdout=subprocess.PIPE,
|
||||||
stderr=subprocess.PIPE,
|
stderr=subprocess.STDOUT,
|
||||||
text=True,
|
text=True,
|
||||||
encoding="utf-8",
|
|
||||||
errors="replace", # 新增:遇到非 UTF-8 字节用 <20> 代替,避免崩溃
|
|
||||||
bufsize=1,
|
|
||||||
env=env,
|
env=env,
|
||||||
cwd=cwd,
|
cwd=cwd,
|
||||||
|
bufsize=1,
|
||||||
|
startupinfo=self._si,
|
||||||
|
start_new_session=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
LogManager.info(f"Flask子进程启动 (PID: {self.process.pid}, 端口: {self.comm_port})")
|
# 异步吃子进程 stdout,顺便打日志
|
||||||
print(f"Flask子进程启动 (PID: {self.process.pid}, 端口: {self.comm_port})")
|
threading.Thread(target=self._read_stdout, daemon=True).start()
|
||||||
|
|
||||||
def print_output(stream, stream_name):
|
# 看门狗只需要起一次
|
||||||
while True:
|
if not self._watchdog_running:
|
||||||
line = stream.readline()
|
threading.Thread(target=self._watchdog_loop, daemon=True).start()
|
||||||
if not line:
|
self._watchdog_running = True
|
||||||
break
|
|
||||||
print(f"{stream_name}: {line.strip()}")
|
|
||||||
|
|
||||||
threading.Thread(target=print_output, args=(self.process.stdout, "STDOUT"), daemon=True).start()
|
self._log("info", f"[FlaskMgr] Flask 子进程已启动 PID={self.process.pid}")
|
||||||
threading.Thread(target=print_output, args=(self.process.stderr, "STDERR"), daemon=True).start()
|
|
||||||
|
|
||||||
def send(self, data: Union[str, Dict, List]) -> bool:
|
def _read_stdout(self):
|
||||||
"""通过Socket发送数据"""
|
if not self.process or not self.process.stdout:
|
||||||
try:
|
return
|
||||||
if not isinstance(data, str):
|
for line in iter(self.process.stdout.readline, ""):
|
||||||
data = json.dumps(data)
|
if line:
|
||||||
# 等待子进程启动并准备好
|
self._log("info", f"[Flask] {line.rstrip()}")
|
||||||
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:
|
|
||||||
LogManager.error(f"连接被拒绝,确保子进程在端口 {self.comm_port} 上监听")
|
|
||||||
print(f"连接被拒绝,确保子进程在端口 {self.comm_port} 上监听")
|
|
||||||
return False
|
|
||||||
except Exception as e:
|
|
||||||
LogManager.error(f"发送失败: {e}")
|
|
||||||
print(f"发送失败: {e}")
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
# ========================= 停止 =========================
|
||||||
def stop(self):
|
def stop(self):
|
||||||
with self._lock:
|
with self._lock:
|
||||||
if self.process and self.process.poll() is None:
|
if not self.process:
|
||||||
print(f"[INFO] Stopping Flask child process (PID: {self.process.pid})...")
|
return
|
||||||
LogManager.info(f"[INFO] Stopping Flask child process (PID: {self.process.pid})...")
|
|
||||||
|
try:
|
||||||
self.process.terminate()
|
self.process.terminate()
|
||||||
self.process.wait()
|
except Exception:
|
||||||
print("[INFO] Flask child process stopped.")
|
pass
|
||||||
LogManager.info("[INFO] Flask child process stopped.")
|
|
||||||
self._stop_event.set()
|
try:
|
||||||
else:
|
self.process.wait(timeout=3)
|
||||||
LogManager.info("[INFO] No Flask child process to stop.")
|
except Exception:
|
||||||
print("[INFO] No Flask child process to stop.")
|
pass
|
||||||
|
|
||||||
|
if self.process.poll() is None:
|
||||||
|
try:
|
||||||
|
self.process.kill()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
self._log("warn", "[FlaskMgr] 已停止 Flask 子进程")
|
||||||
|
self.process = None
|
||||||
|
|
||||||
|
# ========================= 看门狗 =========================
|
||||||
|
def _watchdog_loop(self):
|
||||||
|
self._log("info", "[FlaskWD] 看门狗已启动")
|
||||||
|
|
||||||
|
while not self._stop_event.is_set():
|
||||||
|
time.sleep(1.2)
|
||||||
|
|
||||||
|
# 1) 子进程退出
|
||||||
|
if not self.process or self.process.poll() is not None:
|
||||||
|
self._log("error", "[FlaskWD] Flask 子进程退出,准备重启")
|
||||||
|
self._restart()
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 2) 端口不通
|
||||||
|
if not self._port_alive():
|
||||||
|
self._restart_fail_count += 1
|
||||||
|
self._log("warn", f"[FlaskWD] 端口检测失败 {self._restart_fail_count}/"
|
||||||
|
f"{self._restart_fail_threshold}")
|
||||||
|
|
||||||
|
if self._restart_fail_count >= self._restart_fail_threshold:
|
||||||
|
self._restart()
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 3) 端口正常
|
||||||
|
self._restart_fail_count = 0
|
||||||
|
|
||||||
|
# ========================= 重启核心逻辑 =========================
|
||||||
|
def _restart(self):
|
||||||
|
now = time.time()
|
||||||
|
|
||||||
|
# 10 分钟限频
|
||||||
|
self._restart_record = [t for t in self._restart_record if now - t < self._restart_window]
|
||||||
|
if len(self._restart_record) >= self._restart_limit:
|
||||||
|
self._log("error", "[FlaskWD] 10 分钟内重启次数太多,暂停监控")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 冷却
|
||||||
|
if self._restart_record and now - self._restart_record[-1] < self._restart_cooldown:
|
||||||
|
self._log("warn", "[FlaskWD] 冷却中,暂不重启")
|
||||||
|
return
|
||||||
|
|
||||||
|
self._log("warn", "[FlaskWD] >>> 重启 Flask 子进程 <<<")
|
||||||
|
|
||||||
|
# 执行重启
|
||||||
|
try:
|
||||||
|
self.stop()
|
||||||
|
time.sleep(1)
|
||||||
|
self.start()
|
||||||
|
self._restart_record.append(now)
|
||||||
|
self._restart_fail_count = 0
|
||||||
|
except Exception as e:
|
||||||
|
self._log("error", f"[FlaskWD] 重启失败: {e}")
|
||||||
|
|
||||||
|
# 重启后推送设备快照
|
||||||
|
self._push_snapshot()
|
||||||
|
|
||||||
|
# ========================= 推送设备快照 =========================
|
||||||
|
def _push_snapshot(self):
|
||||||
|
"""Flask 重启后重新同步 DeviceInfo 内容"""
|
||||||
|
try:
|
||||||
|
from Module.DeviceInfo import DeviceInfo
|
||||||
|
info = DeviceInfo()
|
||||||
|
with info._lock:
|
||||||
|
for m in info._models.values():
|
||||||
|
self.send(m.toDict())
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
# ========================= 发送数据 =========================
|
||||||
|
def send(self, data: Union[str, Dict]):
|
||||||
|
if isinstance(data, dict):
|
||||||
|
data = json.dumps(data, ensure_ascii=False)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with socket.create_connection(("127.0.0.1", self.comm_port), timeout=2) as s:
|
||||||
|
s.sendall((data + "\n").encode())
|
||||||
|
return True
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def get_instance(cls) -> 'FlaskSubprocessManager':
|
def get_instance(cls):
|
||||||
return cls()
|
return cls()
|
||||||
355
Module/IOSActivator.py
Normal file
355
Module/IOSActivator.py
Normal file
@@ -0,0 +1,355 @@
|
|||||||
|
import os
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
import threading
|
||||||
|
import subprocess
|
||||||
|
from typing import Optional, Callable
|
||||||
|
|
||||||
|
from Entity.Variables import WdaAppBundleId
|
||||||
|
|
||||||
|
|
||||||
|
class IOSActivator:
|
||||||
|
"""
|
||||||
|
给 iOS17+ 用的 go-ios 激活器(单例):
|
||||||
|
- 维护一条全局 tunnel 进程
|
||||||
|
- 流程:tunnel start -> pair(可多次重试) -> image auto(非致命) -> runwda(多次重试+日志判定成功)
|
||||||
|
- WDA 启动成功后触发回调 on_wda_ready(udid)
|
||||||
|
"""
|
||||||
|
|
||||||
|
# ===== 单例 & 全局 tunnel =====
|
||||||
|
_instance = None
|
||||||
|
_instance_lock = threading.Lock()
|
||||||
|
|
||||||
|
_tunnel_proc: Optional[subprocess.Popen] = None
|
||||||
|
_tunnel_lock = threading.Lock()
|
||||||
|
|
||||||
|
def __new__(cls, *args, **kwargs):
|
||||||
|
with cls._instance_lock:
|
||||||
|
if cls._instance is None:
|
||||||
|
cls._instance = super().__new__(cls)
|
||||||
|
return cls._instance
|
||||||
|
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
ios_path: Optional[str] = None,
|
||||||
|
pair_timeout: int = 60, # 配对最多等多久
|
||||||
|
pair_retry_interval: int = 3, # 配对重试间隔
|
||||||
|
runwda_max_retry: int = 10, # runwda 最大重试次数
|
||||||
|
runwda_retry_interval: int = 3, # runwda 重试间隔
|
||||||
|
runwda_wait_timeout: int = 25 # 单次 runwda 等待“成功日志”的超时时间
|
||||||
|
):
|
||||||
|
if getattr(self, "_inited", False):
|
||||||
|
return
|
||||||
|
|
||||||
|
# 运行路径处理(源码 / Nuitka EXE)
|
||||||
|
if "__compiled__" in globals():
|
||||||
|
base_dir = os.path.dirname(sys.executable)
|
||||||
|
else:
|
||||||
|
cur_file = os.path.abspath(__file__)
|
||||||
|
base_dir = os.path.dirname(os.path.dirname(cur_file))
|
||||||
|
|
||||||
|
resource_dir = os.path.join(base_dir, "resources")
|
||||||
|
if not ios_path:
|
||||||
|
ios_path = os.path.join(resource_dir, "ios.exe")
|
||||||
|
|
||||||
|
self.ios_path = ios_path
|
||||||
|
self.pair_timeout = pair_timeout
|
||||||
|
self.pair_retry_interval = pair_retry_interval
|
||||||
|
self.runwda_max_retry = runwda_max_retry
|
||||||
|
self.runwda_retry_interval = runwda_retry_interval
|
||||||
|
self.runwda_wait_timeout = runwda_wait_timeout
|
||||||
|
|
||||||
|
self._lock = threading.Lock()
|
||||||
|
|
||||||
|
# ========= 关键:这里改成“真正隐藏窗口”的安全版 =========
|
||||||
|
self._creationflags = 0
|
||||||
|
self._startupinfo = None
|
||||||
|
if os.name == "nt":
|
||||||
|
try:
|
||||||
|
# 只用 CREATE_NO_WINDOW,不搞 DETACHED_PROCESS / NEW_PROCESS_GROUP 之类的骚操作
|
||||||
|
self._creationflags = subprocess.CREATE_NO_WINDOW # type: ignore[attr-defined]
|
||||||
|
except Exception:
|
||||||
|
self._creationflags = 0
|
||||||
|
|
||||||
|
si = subprocess.STARTUPINFO()
|
||||||
|
try:
|
||||||
|
si.dwFlags |= subprocess.STARTF_USESHOWWINDOW # type: ignore[attr-defined]
|
||||||
|
except Exception:
|
||||||
|
# 某些极端环境下可能没有 STARTF_USESHOWWINDOW,忽略即可
|
||||||
|
pass
|
||||||
|
si.wShowWindow = 0 # SW_HIDE
|
||||||
|
self._startupinfo = si
|
||||||
|
# ========= 关键部分结束 =========
|
||||||
|
|
||||||
|
self._inited = True
|
||||||
|
|
||||||
|
# ===== 通用同步命令执行 =====
|
||||||
|
def _run(
|
||||||
|
self,
|
||||||
|
args,
|
||||||
|
desc: str = "",
|
||||||
|
timeout: Optional[int] = None,
|
||||||
|
check: bool = True,
|
||||||
|
):
|
||||||
|
cmd = [self.ios_path] + list(args)
|
||||||
|
cmd_str = " ".join(cmd)
|
||||||
|
if desc:
|
||||||
|
print(f"[ios] 执行命令({desc}): {cmd_str}")
|
||||||
|
else:
|
||||||
|
print(f"[ios] 执行命令: {cmd_str}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc = subprocess.run(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
timeout=timeout,
|
||||||
|
creationflags=self._creationflags,
|
||||||
|
startupinfo=self._startupinfo,
|
||||||
|
)
|
||||||
|
except subprocess.TimeoutExpired:
|
||||||
|
if check:
|
||||||
|
raise
|
||||||
|
return -1, "", "timeout"
|
||||||
|
|
||||||
|
out = proc.stdout or ""
|
||||||
|
err = proc.stderr or ""
|
||||||
|
if check and proc.returncode != 0:
|
||||||
|
print(f"[ios] 命令失败({desc}), rc={proc.returncode}")
|
||||||
|
raise RuntimeError(f"[ios] 命令失败({desc}), returncode={proc.returncode}")
|
||||||
|
|
||||||
|
return proc.returncode, out, err
|
||||||
|
|
||||||
|
# ===== tunnel 相关 =====
|
||||||
|
def _drain_process_output(self, proc: subprocess.Popen, name: str):
|
||||||
|
"""吃掉后台进程输出,防止缓冲区阻塞"""
|
||||||
|
try:
|
||||||
|
if proc.stdout:
|
||||||
|
for line in proc.stdout:
|
||||||
|
line = line.rstrip()
|
||||||
|
if line:
|
||||||
|
print(f"[ios][{name}] {line}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ios][{name}] 读取 stdout 异常: {e}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
if proc.stderr:
|
||||||
|
for line in proc.stderr:
|
||||||
|
line = line.rstrip()
|
||||||
|
if line:
|
||||||
|
print(f"[ios][{name}][stderr] {line}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ios][{name}] 读取 stderr 异常: {e}")
|
||||||
|
|
||||||
|
def _spawn_tunnel(self):
|
||||||
|
"""启动 / 复用全局 tunnel(不隐藏窗口)"""
|
||||||
|
with IOSActivator._tunnel_lock:
|
||||||
|
# 已有并且还在跑就复用
|
||||||
|
if IOSActivator._tunnel_proc is not None and IOSActivator._tunnel_proc.poll() is None:
|
||||||
|
print("[ios] tunnel 已经在运行,跳过重新启动")
|
||||||
|
return
|
||||||
|
|
||||||
|
cmd = [self.ios_path, "tunnel", "start"]
|
||||||
|
print("[ios] 启动 go-ios tunnel:", " ".join(cmd))
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
creationflags=self._creationflags, # 0:不隐藏
|
||||||
|
startupinfo=self._startupinfo, # None:不隐藏
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print("[ios] 启动 tunnel 失败(忽略):", e)
|
||||||
|
return
|
||||||
|
|
||||||
|
IOSActivator._tunnel_proc = proc
|
||||||
|
print("[ios] tunnel 启动成功, PID=", proc.pid)
|
||||||
|
|
||||||
|
# 后台吃日志
|
||||||
|
threading.Thread(
|
||||||
|
target=self._drain_process_output,
|
||||||
|
args=(proc, "tunnel"),
|
||||||
|
daemon=True,
|
||||||
|
).start()
|
||||||
|
|
||||||
|
# ===== pair & image =====
|
||||||
|
def _pair_until_success(self, udid: str):
|
||||||
|
deadline = time.time() + self.pair_timeout
|
||||||
|
attempt = 0
|
||||||
|
|
||||||
|
while True:
|
||||||
|
attempt += 1
|
||||||
|
print(f"[ios] 开始配对设备({udid}),第 {attempt} 次尝试")
|
||||||
|
|
||||||
|
rc, out, err = self._run(
|
||||||
|
["--udid", udid, "pair"],
|
||||||
|
desc=f"pair({udid})",
|
||||||
|
timeout=20,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
text = (out or "") + "\n" + (err or "")
|
||||||
|
# 打印一份完整输出,方便调试
|
||||||
|
if text.strip():
|
||||||
|
print("[ios][pair] output:\n", text.strip())
|
||||||
|
|
||||||
|
if "Successfully paired" in text:
|
||||||
|
print(f"[ios] 设备 {udid} 配对成功")
|
||||||
|
return
|
||||||
|
|
||||||
|
if time.time() >= deadline:
|
||||||
|
raise RuntimeError(f"[ios] 设备 {udid} 在超时时间内配对失败(rc={rc})")
|
||||||
|
|
||||||
|
time.sleep(self.pair_retry_interval)
|
||||||
|
|
||||||
|
def _mount_dev_image(self, udid: str):
|
||||||
|
print(f"[ios] 开始为设备 {udid} 挂载开发者镜像 (image auto)")
|
||||||
|
rc, out, err = self._run(
|
||||||
|
["--udid", udid, "image", "auto"],
|
||||||
|
desc=f"image auto({udid})",
|
||||||
|
timeout=300,
|
||||||
|
check=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
text = (out or "") + "\n" + (err or "")
|
||||||
|
text_lower = text.lower()
|
||||||
|
success_keywords = [
|
||||||
|
"success mounting image",
|
||||||
|
"there is already a developer image mounted",
|
||||||
|
]
|
||||||
|
if any(k in text_lower for k in success_keywords):
|
||||||
|
print(f"[ios] 设备 {udid} 开发者镜像挂载完成")
|
||||||
|
if text.strip():
|
||||||
|
print("[ios][image auto] output:\n", text.strip())
|
||||||
|
return
|
||||||
|
|
||||||
|
print(f"[ios] 设备 {udid} 挂载开发者镜像可能失败(rc={rc}),输出:\n{text.strip()}")
|
||||||
|
|
||||||
|
# ===== runwda(关键逻辑) =====
|
||||||
|
def _run_wda_once_async(self, udid: str, on_wda_ready: Optional[Callable[[str], None]]) -> bool:
|
||||||
|
"""
|
||||||
|
单次 runwda:
|
||||||
|
- 异步启动 ios.exe
|
||||||
|
- 实时读 stdout/stderr
|
||||||
|
- 捕获关键日志(got capabilities / authorized true)视为成功
|
||||||
|
- 超时/进程退出且未成功 -> 失败
|
||||||
|
"""
|
||||||
|
cmd = [
|
||||||
|
self.ios_path,
|
||||||
|
f"--udid={udid}",
|
||||||
|
"runwda",
|
||||||
|
f"--bundleid={WdaAppBundleId}",
|
||||||
|
f"--testrunnerbundleid={WdaAppBundleId}",
|
||||||
|
"--xctestconfig=yolo.xctest",
|
||||||
|
]
|
||||||
|
print("[ios] 异步启动 runwda:", " ".join(cmd))
|
||||||
|
|
||||||
|
try:
|
||||||
|
proc = subprocess.Popen(
|
||||||
|
cmd,
|
||||||
|
stdout=subprocess.PIPE,
|
||||||
|
stderr=subprocess.PIPE,
|
||||||
|
text=True,
|
||||||
|
creationflags=self._creationflags, # 0:不隐藏
|
||||||
|
startupinfo=self._startupinfo,
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ios] 启动 runwda 进程失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
success_evt = threading.Event()
|
||||||
|
|
||||||
|
def _reader(pipe, tag: str):
|
||||||
|
try:
|
||||||
|
for raw in pipe:
|
||||||
|
line = (raw or "").rstrip()
|
||||||
|
if not line:
|
||||||
|
continue
|
||||||
|
print(f"[WDA-LOG] {line}")
|
||||||
|
lower = line.lower()
|
||||||
|
# 你实测的“成功特征”
|
||||||
|
if "got capabilities" in lower or '"authorized":true' in lower:
|
||||||
|
success_evt.set()
|
||||||
|
print(f"[ios] 捕获到 WDA 启动成功日志({tag}),udid={udid}")
|
||||||
|
break
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[ios] 读取 {tag} 日志异常: {e}")
|
||||||
|
|
||||||
|
# 日志线程
|
||||||
|
if proc.stdout:
|
||||||
|
threading.Thread(target=_reader, args=(proc.stdout, "stdout"), daemon=True).start()
|
||||||
|
if proc.stderr:
|
||||||
|
threading.Thread(target=_reader, args=(proc.stderr, "stderr"), daemon=True).start()
|
||||||
|
|
||||||
|
# 等待成功 / 退出 / 超时
|
||||||
|
start = time.time()
|
||||||
|
while True:
|
||||||
|
if success_evt.is_set():
|
||||||
|
print(f"[ios] WDA 日志确认已启动,udid={udid}")
|
||||||
|
if on_wda_ready:
|
||||||
|
try:
|
||||||
|
on_wda_ready(udid)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WDA] 回调执行异常: {e}")
|
||||||
|
# 不主动杀进程,让 WDA 挂在那儿
|
||||||
|
return True
|
||||||
|
|
||||||
|
rc = proc.poll()
|
||||||
|
if rc is not None:
|
||||||
|
print(f"[ios] runwda 进程退出 rc={rc},未检测到成功日志,udid={udid}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
if time.time() - start > self.runwda_wait_timeout:
|
||||||
|
print(f"[ios] runwda 等待超时({self.runwda_wait_timeout}s),未确认成功,udid={udid}")
|
||||||
|
try:
|
||||||
|
proc.terminate()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
time.sleep(0.2)
|
||||||
|
|
||||||
|
def _run_wda_with_retry(self, udid: str, on_wda_ready: Optional[Callable[[str], None]]) -> bool:
|
||||||
|
for attempt in range(1, self.runwda_max_retry + 1):
|
||||||
|
print(f"[ios] runwda 尝试 {attempt}/{self.runwda_max_retry},udid={udid}")
|
||||||
|
ok = self._run_wda_once_async(udid, on_wda_ready)
|
||||||
|
if ok:
|
||||||
|
print(f"[ios] runwda 第 {attempt} 次尝试成功,udid={udid}")
|
||||||
|
return True
|
||||||
|
|
||||||
|
print(f"[ios] runwda 第 {attempt} 次尝试失败,udid={udid}")
|
||||||
|
if attempt < self.runwda_max_retry:
|
||||||
|
time.sleep(self.runwda_retry_interval)
|
||||||
|
|
||||||
|
print(f"[ios] runwda 多次失败,放弃,udid={udid}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
# ===== 对外主流程 =====
|
||||||
|
def activate_ios17(self, udid: str, on_wda_ready: Optional[Callable[[str], None]] = None) -> None:
|
||||||
|
print(f"[WDA] iOS17+ 激活开始,udid={udid}, 回调={on_wda_ready}")
|
||||||
|
|
||||||
|
# 1. 先确保 tunnel 在跑
|
||||||
|
self._spawn_tunnel()
|
||||||
|
|
||||||
|
# 2. 配对
|
||||||
|
try:
|
||||||
|
self._pair_until_success(udid)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WDA] pair 失败,终止激活流程 udid={udid}, err={e}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 3. 挂镜像(非致命)
|
||||||
|
try:
|
||||||
|
self._mount_dev_image(udid)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[WDA] 挂载开发者镜像异常(忽略) udid={udid}, err={e}")
|
||||||
|
|
||||||
|
# 4. runwda + 回调
|
||||||
|
ok = self._run_wda_with_retry(udid, on_wda_ready)
|
||||||
|
if not ok:
|
||||||
|
print(f"[WDA] runwda 多次失败,可能需要手动检查设备,udid={udid}")
|
||||||
|
|
||||||
|
print(f"[WDA] iOS17+ 激活流程结束(不代表一定成功),udid={udid}")
|
||||||
131
Module/Main.py
131
Module/Main.py
@@ -1,9 +1,15 @@
|
|||||||
|
import asyncio
|
||||||
|
import ctypes
|
||||||
|
# ===== Main.py 顶部放置(所有 import 之前)=====
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
from hypercorn.asyncio import serve
|
||||||
|
from hypercorn.config import Config
|
||||||
|
|
||||||
from Module.DeviceInfo import Deviceinfo
|
from Module.DeviceInfo import DeviceInfo
|
||||||
from Module.FlaskSubprocessManager import FlaskSubprocessManager
|
from Module.FlaskSubprocessManager import FlaskSubprocessManager
|
||||||
|
from Utils.AiUtils import AiUtils
|
||||||
from Utils.DevDiskImageDeployer import DevDiskImageDeployer
|
from Utils.DevDiskImageDeployer import DevDiskImageDeployer
|
||||||
from Utils.LogManager import LogManager
|
from Utils.LogManager import LogManager
|
||||||
|
|
||||||
@@ -15,41 +21,145 @@ LOG_DIR.mkdir(exist_ok=True) # 确保 log 目录存在
|
|||||||
print(f"日志目录: {LOG_DIR}")
|
print(f"日志目录: {LOG_DIR}")
|
||||||
|
|
||||||
def _run_flask_role():
|
def _run_flask_role():
|
||||||
from Module import FlaskService
|
from Module.FlaskService import get_app, bootstrap_server_side_effects
|
||||||
port = int(os.getenv("FLASK_COMM_PORT", "34567")) # 固定端口的兜底仍是 34567
|
print("Flask Pid:", os.getpid())
|
||||||
app_factory = getattr(FlaskService, "create_app", None)
|
port = int(os.getenv("FLASK_COMM_PORT", "34566")) # 固定端口的兜底仍是 34567
|
||||||
app = app_factory() if callable(app_factory) else FlaskService.app
|
app = get_app()
|
||||||
app.run(host="0.0.0.0", port=port + 1, debug=False, use_reloader=False)
|
flaskPort = port + 1
|
||||||
|
AiUtils.flask_port_free(flaskPort)
|
||||||
|
bootstrap_server_side_effects()
|
||||||
|
|
||||||
|
# ==== 关键:统一获取 resources 目录 ====
|
||||||
|
if "__compiled__" in globals():
|
||||||
|
# 被 Nuitka 编译后的 exe 运行时
|
||||||
|
base_dir = os.path.dirname(sys.executable) # exe 所在目录
|
||||||
|
else:
|
||||||
|
# 开发环境,直接跑 .py
|
||||||
|
cur_file = os.path.abspath(__file__) # Module/Main.py 所在目录
|
||||||
|
base_dir = os.path.dirname(os.path.dirname(cur_file)) # 回到项目根 iOSAi
|
||||||
|
|
||||||
|
resource_dir = os.path.join(base_dir, "resources")
|
||||||
|
|
||||||
|
# Hypercorn 配置
|
||||||
|
config = Config()
|
||||||
|
config.bind = [f"0.0.0.0:{flaskPort}"]
|
||||||
|
config.certfile = os.path.join(resource_dir, "server.crt")
|
||||||
|
config.keyfile = os.path.join(resource_dir, "server.key")
|
||||||
|
config.alpn_protocols = ["h2", "http/1.1"]
|
||||||
|
config.workers = 6 # 你机器 4GB → 推荐 3~4 个 worker
|
||||||
|
|
||||||
|
# 直接跑 Quart(ASGI 原生,不再用 WsgiToAsgi)
|
||||||
|
asyncio.run(serve(app, config))
|
||||||
|
|
||||||
if "--role=flask" in sys.argv:
|
if "--role=flask" in sys.argv:
|
||||||
_run_flask_role()
|
_run_flask_role()
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
|
def _ensure_wintun_installed():
|
||||||
|
"""
|
||||||
|
确保 wintun.dll 已经在系统目录里:
|
||||||
|
- 优先从当前目录的 resources 中找 wintun.dll
|
||||||
|
- 如果 System32 中没有,就复制过去(需要管理员权限)
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
# ==== 关键:统一获取 resources 目录 ====
|
||||||
|
if "__compiled__" in globals():
|
||||||
|
# Nuitka 编译后的 exe
|
||||||
|
base_dir = os.path.dirname(sys.executable) # exe 所在目录
|
||||||
|
else:
|
||||||
|
# 开发环境运行 .py
|
||||||
|
cur_file = os.path.abspath(__file__) # Module/Main.py 所在目录
|
||||||
|
base_dir = os.path.dirname(os.path.dirname(cur_file)) # 回到 iOSAi 根目录
|
||||||
|
|
||||||
|
resource_dir = os.path.join(base_dir, "resources")
|
||||||
|
src = os.path.join(resource_dir, "wintun.dll")
|
||||||
|
|
||||||
|
# 1. 检查源文件是否存在
|
||||||
|
if not os.path.exists(src):
|
||||||
|
print(f"[wintun] 未找到资源文件: {src}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 2. 系统 System32 目录
|
||||||
|
windir = os.environ.get("WINDIR", r"C:\Windows")
|
||||||
|
system32 = Path(windir) / "System32"
|
||||||
|
dst = system32 / "wintun.dll"
|
||||||
|
|
||||||
|
# 3. System32 中已经存在则无需复制
|
||||||
|
if dst.exists():
|
||||||
|
print(f"[wintun] System32 中已存在: {dst}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 4. 执行复制
|
||||||
|
import shutil
|
||||||
|
print(f"[wintun] 复制 {src} -> {dst}")
|
||||||
|
shutil.copy2(src, dst)
|
||||||
|
print("[wintun] 复制完成")
|
||||||
|
|
||||||
|
except PermissionError as e:
|
||||||
|
print(f"[wintun] 权限不足,无法写入 System32:{e}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[wintun] 安装 wintun.dll 时异常: {e}")
|
||||||
|
|
||||||
|
# 启动锁
|
||||||
|
def main(arg):
|
||||||
|
if len(arg) != 2 or arg[1] != "iosai":
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
# 判断是否为管理员身份原型
|
||||||
|
def isAdministrator():
|
||||||
|
"""
|
||||||
|
检测当前进程是否具有管理员权限。
|
||||||
|
- Windows 下调用 Shell32.IsUserAnAdmin()
|
||||||
|
- 如果不是管理员,直接退出程序
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
is_admin = ctypes.windll.shell32.IsUserAnAdmin()
|
||||||
|
except Exception:
|
||||||
|
# 非 Windows 或无法判断的情况,一律按“非管理员”处理
|
||||||
|
is_admin = False
|
||||||
|
|
||||||
|
if not is_admin:
|
||||||
|
print("[ERROR] 需要以管理员身份运行本程序!")
|
||||||
|
sys.exit(0)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
# 项目入口
|
# 项目入口
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
# 检测是否有管理员身份权限
|
||||||
|
isAdministrator()
|
||||||
|
|
||||||
|
# 检测程序合法性
|
||||||
|
main(sys.argv)
|
||||||
|
|
||||||
|
# 清空日志
|
||||||
|
LogManager.clearLogs()
|
||||||
|
|
||||||
# 添加iOS开发包到电脑上
|
# 添加iOS开发包到电脑上
|
||||||
deployer = DevDiskImageDeployer(verbose=True)
|
deployer = DevDiskImageDeployer(verbose=True)
|
||||||
deployer.deploy_all()
|
deployer.deploy_all()
|
||||||
|
|
||||||
|
# 复制wintun.dll到system32目录下
|
||||||
|
_ensure_wintun_installed()
|
||||||
|
|
||||||
# 启动 Flask 子进程
|
# 启动 Flask 子进程
|
||||||
manager = FlaskSubprocessManager.get_instance()
|
manager = FlaskSubprocessManager.get_instance()
|
||||||
manager.start()
|
manager.start()
|
||||||
|
|
||||||
# 设备监听(即使失败/很快返回,也不会导致主进程退出)
|
# 设备监听(即使失败/很快返回,也不会导致主进程退出)
|
||||||
try:
|
try:
|
||||||
info = Deviceinfo()
|
info = DeviceInfo()
|
||||||
info.startDeviceListener()
|
info.listen()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
print("[WARN] Device listener not running:", e)
|
print("[WARN] Device listener not running:", e)
|
||||||
|
|
||||||
# === 保活:阻塞主线程,直到收到 Ctrl+C/关闭 ===
|
# === 保活:阻塞主线程,直到收到 Ctrl+C/关闭 ===
|
||||||
import threading, time, signal
|
import threading, time, signal
|
||||||
|
|
||||||
stop = threading.Event()
|
stop = threading.Event()
|
||||||
|
|
||||||
def _handle(_sig, _frm):
|
def _handle(_sig, _frm):
|
||||||
stop.set()
|
stop.set()
|
||||||
|
|
||||||
# Windows 上 SIGINT/SIGTERM 都可以拦到
|
# Windows 上 SIGINT/SIGTERM 都可以拦到
|
||||||
try:
|
try:
|
||||||
signal.signal(signal.SIGINT, _handle)
|
signal.signal(signal.SIGINT, _handle)
|
||||||
@@ -63,3 +173,4 @@ if __name__ == "__main__":
|
|||||||
finally:
|
finally:
|
||||||
# 进程退出前记得把子进程关掉
|
# 进程退出前记得把子进程关掉
|
||||||
manager.stop()
|
manager.stop()
|
||||||
|
|
||||||
|
|||||||
1337
Utils/AiUtils.py
1337
Utils/AiUtils.py
File diff suppressed because it is too large
Load Diff
@@ -1,8 +1,13 @@
|
|||||||
|
import math
|
||||||
|
import random
|
||||||
import re
|
import re
|
||||||
|
import time
|
||||||
|
from typing import Tuple, List
|
||||||
import tidevice
|
import tidevice
|
||||||
import wda
|
import wda
|
||||||
from wda import Client
|
from wda import Client
|
||||||
|
|
||||||
|
from Entity.Variables import wdaFunctionPort
|
||||||
from Utils.AiUtils import AiUtils
|
from Utils.AiUtils import AiUtils
|
||||||
from Utils.LogManager import LogManager
|
from Utils.LogManager import LogManager
|
||||||
|
|
||||||
@@ -55,20 +60,87 @@ class ControlUtils(object):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def clickBack(cls, session: Client):
|
def clickBack(cls, session: Client):
|
||||||
try:
|
try:
|
||||||
back = session.xpath("//*[@label='返回']")
|
|
||||||
|
back = session.xpath(
|
||||||
|
# ① 常见中文文案
|
||||||
|
"//*[@label='返回' or @label='返回上一屏幕']"
|
||||||
|
" | "
|
||||||
|
# ② 英文 / 内部 name / 图标 label 的按钮(仅限 Button,且可见)
|
||||||
|
"//XCUIElementTypeButton[@visible='true' and ("
|
||||||
|
"@name='Back' or @label='Back' or " # 英文
|
||||||
|
"@name='返回' or @label='返回' or " # 中文
|
||||||
|
"@label='返回上一屏幕' or " # 中文另一种
|
||||||
|
"@name='returnButton' or"
|
||||||
|
"@name='nav_bar_start_back' or " # 内部常见 name
|
||||||
|
"(@name='TTKProfileNavBarBaseItemComponent' and @label='IconChevronLeftOffsetLTR')" # 你给的特例
|
||||||
|
")]"
|
||||||
|
)
|
||||||
|
|
||||||
if back.exists:
|
if back.exists:
|
||||||
back.click()
|
back.click()
|
||||||
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']")
|
||||||
back.click()
|
if back.exists:
|
||||||
|
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]")
|
||||||
back.click()
|
if back.exists:
|
||||||
|
back.click()
|
||||||
return True
|
return True
|
||||||
|
elif session.xpath(
|
||||||
|
"(//XCUIElementTypeOther[@y='20' and @height='44']//XCUIElementTypeButton[@visible='true'])[1]").exists:
|
||||||
|
back = session.xpath(
|
||||||
|
"(//XCUIElementTypeOther[@y='20' and @height='44']//XCUIElementTypeButton[@visible='true'])[1]")
|
||||||
|
if back.exists:
|
||||||
|
back.click()
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def isClickBackEnabled(cls, session: Client):
|
||||||
|
try:
|
||||||
|
|
||||||
|
back = session.xpath(
|
||||||
|
# ① 常见中文文案
|
||||||
|
"//*[@label='返回' or @label='返回上一屏幕']"
|
||||||
|
" | "
|
||||||
|
# ② 英文 / 内部 name / 图标 label 的按钮(仅限 Button,且可见)
|
||||||
|
"//XCUIElementTypeButton[@visible='true' and ("
|
||||||
|
"@name='Back' or @label='Back' or " # 英文
|
||||||
|
"@name='返回' or @label='返回' or " # 中文
|
||||||
|
"@label='返回上一屏幕' or " # 中文另一种
|
||||||
|
"@name='returnButton' or"
|
||||||
|
"@name='nav_bar_start_back' or " # 内部常见 name
|
||||||
|
"(@name='TTKProfileNavBarBaseItemComponent' and @label='IconChevronLeftOffsetLTR')" # 你给的特例
|
||||||
|
")]"
|
||||||
|
)
|
||||||
|
|
||||||
|
if back.exists:
|
||||||
|
return True
|
||||||
|
elif session.xpath("//*[@name='nav_bar_start_back']").exists:
|
||||||
|
back = session.xpath("//*[@name='nav_bar_start_back']")
|
||||||
|
if back.exists:
|
||||||
|
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]")
|
||||||
|
if back.exists:
|
||||||
|
return True
|
||||||
|
elif session.xpath(
|
||||||
|
"(//XCUIElementTypeOther[@y='20' and @height='44']//XCUIElementTypeButton[@visible='true'])[1]").exists:
|
||||||
|
back = session.xpath(
|
||||||
|
"(//XCUIElementTypeOther[@y='20' and @height='44']//XCUIElementTypeButton[@visible='true'])[1]")
|
||||||
|
if back.exists:
|
||||||
|
return True
|
||||||
else:
|
else:
|
||||||
return False
|
return False
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -78,21 +150,41 @@ class ControlUtils(object):
|
|||||||
# 点赞
|
# 点赞
|
||||||
@classmethod
|
@classmethod
|
||||||
def clickLike(cls, session: Client, udid):
|
def clickLike(cls, session: Client, udid):
|
||||||
scale = session.scale
|
try:
|
||||||
x, y = AiUtils.findImageInScreen("add", udid)
|
from script.ScriptManager import ScriptManager
|
||||||
print(x, y)
|
|
||||||
if x > -1:
|
width, height, scale = ScriptManager.get_screen_info(udid)
|
||||||
LogManager.info("点赞了", udid)
|
|
||||||
session.click(x // scale, y // scale + 50)
|
if scale == 3.0:
|
||||||
return True
|
x, y = AiUtils.findImageInScreen("add", udid)
|
||||||
else:
|
if x > -1:
|
||||||
LogManager.info("没有找到目标", udid)
|
LogManager.method_info(f"点赞了,点赞的坐标是:{x // scale, y // scale + 50}", "关注打招呼", udid)
|
||||||
return False
|
session.click(int(x // scale), int(y // scale + 50))
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
LogManager.method_info("没有找到目标", "关注打招呼", udid)
|
||||||
|
return False
|
||||||
|
else:
|
||||||
|
x, y = AiUtils.findImageInScreen("like1", udid)
|
||||||
|
if x > -1:
|
||||||
|
LogManager.method_info(f"点赞了,点赞的坐标是:{x // scale, y // scale}", "关注打招呼", udid)
|
||||||
|
session.click(int(x // scale), int(y // scale))
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
LogManager.method_info("没有找到目标", "关注打招呼", udid)
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
LogManager.method_info(f"点赞出现异常,异常的原因:{e}", "关注打招呼", udid)
|
||||||
|
raise False
|
||||||
|
|
||||||
# 点击搜索
|
# 点击搜索
|
||||||
@classmethod
|
@classmethod
|
||||||
def clickSearch(cls, session: Client):
|
def clickSearch(cls, session: Client):
|
||||||
obj = session.xpath("//*[@name='搜索']")
|
# obj = session.xpath("//*[@name='搜索']")
|
||||||
|
obj = session(xpath='//*[@name="搜索" or @label="搜索" or @name="Search" or @label="Search"]')
|
||||||
try:
|
try:
|
||||||
if obj.exists:
|
if obj.exists:
|
||||||
obj.click()
|
obj.click()
|
||||||
@@ -114,15 +206,13 @@ class ControlUtils(object):
|
|||||||
# 获取主播详情页的第一个视频
|
# 获取主播详情页的第一个视频
|
||||||
@classmethod
|
@classmethod
|
||||||
def clickFirstVideoFromDetailPage(cls, session: Client):
|
def clickFirstVideoFromDetailPage(cls, session: Client):
|
||||||
# videoCell = session.xpath(
|
|
||||||
# '//Window/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[2]/Other[1]/ScrollView[1]/Other[1]/CollectionView[1]/Cell[2]')
|
|
||||||
|
|
||||||
videoCell = session.xpath(
|
videoCell = session.xpath(
|
||||||
'(//XCUIElementTypeCollectionView//XCUIElementTypeCell[.//XCUIElementTypeImage[@name="profile_video"]])[1]').get(
|
'(//XCUIElementTypeCollectionView//XCUIElementTypeCell[.//XCUIElementTypeImage[@name="profile_video"]])[1]')
|
||||||
timeout=5)
|
|
||||||
|
|
||||||
tab = session.xpath('//XCUIElementTypeButton[@name="TTKProfileTabVideoButton_0"]').get(timeout=2)
|
tab = session.xpath(
|
||||||
# 某些版本 tab.value 可能就是数量;或者 tab.label 类似 “作品 7”
|
'//XCUIElementTypeButton[@name="TTKProfileTabVideoButton_0" or contains(@label,"作品") or contains(@name,"作品")]'
|
||||||
|
).get(timeout=5) # 某些版本 tab.value 可能就是数量;或者 tab.label 类似 “作品 7”
|
||||||
m = re.search(r"\d+", tab.label)
|
m = re.search(r"\d+", tab.label)
|
||||||
|
|
||||||
num = 0
|
num = 0
|
||||||
@@ -132,7 +222,7 @@ class ControlUtils(object):
|
|||||||
num = int(m.group())
|
num = int(m.group())
|
||||||
print("作品数量为:", num)
|
print("作品数量为:", num)
|
||||||
|
|
||||||
if videoCell is not None:
|
if videoCell.exists:
|
||||||
videoCell.click()
|
videoCell.click()
|
||||||
# 点击视频
|
# 点击视频
|
||||||
print("找到主页的第一个视频")
|
print("找到主页的第一个视频")
|
||||||
@@ -168,3 +258,99 @@ class ControlUtils(object):
|
|||||||
left_x = max(1, rect.x - 20)
|
left_x = max(1, rect.x - 20)
|
||||||
center_y = rect.y + rect.height // 2
|
center_y = rect.y + rect.height // 2
|
||||||
session.tap(left_x, center_y)
|
session.tap(left_x, center_y)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def userClickProfile(cls, session, aid):
|
||||||
|
try:
|
||||||
|
user_btn = session.xpath("(//XCUIElementTypeButton[@name='用户' and @visible='true'])[1]")
|
||||||
|
if user_btn.exists:
|
||||||
|
user_btn.click()
|
||||||
|
time.sleep(3)
|
||||||
|
follow_btn = session.xpath(
|
||||||
|
"(//XCUIElementTypeTable//XCUIElementTypeButton[@name='关注' or @name='已关注'])[1]"
|
||||||
|
).get(timeout=5)
|
||||||
|
if follow_btn:
|
||||||
|
x, y, w, h = follow_btn.bounds
|
||||||
|
# 垂直方向中心 + 随机 3~8 像素偏移
|
||||||
|
cy = int(y + h / 2 + random.randint(-8, 8))
|
||||||
|
# 横向往左偏移 80~120 像素之间的随机值
|
||||||
|
cx = int(x - random.randint(80, 120))
|
||||||
|
# 点击
|
||||||
|
session.tap(cx, cy)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
except Exception as e:
|
||||||
|
print(e)
|
||||||
|
return False
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
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)
|
||||||
|
|
||||||
|
# 向上滑动 脚本内部使用
|
||||||
|
@classmethod
|
||||||
|
def swipe_up(cls, client):
|
||||||
|
client.swipe(200, 350, 200, 250, 0.05)
|
||||||
|
|
||||||
|
# 向下滑动,脚本内使用
|
||||||
|
@classmethod
|
||||||
|
def swipe_down(cls, udid):
|
||||||
|
dev = wda.USBClient(udid, wdaFunctionPort)
|
||||||
|
dev.swipe(200, 250, 200, 350, 0.05)
|
||||||
|
|||||||
271
Utils/CountryEnum.py
Normal file
271
Utils/CountryEnum.py
Normal file
@@ -0,0 +1,271 @@
|
|||||||
|
class CountryLanguageMapper:
|
||||||
|
|
||||||
|
# 初始化一个字典,映射国家到语言代码
|
||||||
|
country_to_language = {
|
||||||
|
"中国大陆": "zh-CN",
|
||||||
|
"台湾": "zh-TW",
|
||||||
|
"香港": "zh-TW",
|
||||||
|
"澳门": "zh-TW",
|
||||||
|
"美国": "en",
|
||||||
|
"英国": "en",
|
||||||
|
"澳大利亚": "en",
|
||||||
|
"日本": "ja",
|
||||||
|
"韩国": "ko",
|
||||||
|
"俄罗斯": "ru",
|
||||||
|
"法国": "fr",
|
||||||
|
"德国": "de",
|
||||||
|
"意大利": "it",
|
||||||
|
"西班牙": "es",
|
||||||
|
"墨西哥": "es",
|
||||||
|
"巴西": "pt",
|
||||||
|
"葡萄牙": "pt",
|
||||||
|
"印度": "hi",
|
||||||
|
"泰国": "th",
|
||||||
|
"越南": "vi",
|
||||||
|
"马来西亚": "ms",
|
||||||
|
"印度尼西亚": "id",
|
||||||
|
"阿联酋": "ar",
|
||||||
|
"沙特阿拉伯": "ar",
|
||||||
|
"埃及": "ar",
|
||||||
|
"以色列": "he",
|
||||||
|
"缅甸": "my",
|
||||||
|
"斯里兰卡": "ta",
|
||||||
|
"巴基斯坦": "ur",
|
||||||
|
"孟加拉国": "bn",
|
||||||
|
"波兰": "pl",
|
||||||
|
"荷兰": "nl",
|
||||||
|
"罗马尼亚": "ro",
|
||||||
|
"土耳其": "tr",
|
||||||
|
"老挝": "lo",
|
||||||
|
"乌克兰": "uk",
|
||||||
|
"芬兰": "fi",
|
||||||
|
"南非": "af",
|
||||||
|
"阿尔巴尼亚": "sq",
|
||||||
|
"安道尔": "ca",
|
||||||
|
"安提瓜和巴布达": "en",
|
||||||
|
"阿根廷": "es",
|
||||||
|
"亚美尼亚": "hy",
|
||||||
|
"奥地利": "de",
|
||||||
|
"阿塞拜疆": "az",
|
||||||
|
"巴哈马": "en",
|
||||||
|
"巴林": "ar",
|
||||||
|
"巴巴多斯": "en",
|
||||||
|
"白俄罗斯": "be",
|
||||||
|
"比利时": "fr",
|
||||||
|
"伯利兹": "en",
|
||||||
|
"贝宁": "fr",
|
||||||
|
"不丹": "dz",
|
||||||
|
"玻利维亚": "es",
|
||||||
|
"波斯尼亚和黑塞哥维那": "bs",
|
||||||
|
"博茨瓦纳": "en",
|
||||||
|
"文莱": "ms",
|
||||||
|
"保加利亚": "bg",
|
||||||
|
"布基纳法索": "fr",
|
||||||
|
"布隆迪": "fr",
|
||||||
|
"柬埔寨": "km",
|
||||||
|
"喀麦隆": "fr",
|
||||||
|
"加拿大": "en",
|
||||||
|
"佛得角": "pt",
|
||||||
|
"开曼群岛": "en",
|
||||||
|
"中非共和国": "fr",
|
||||||
|
"乍得": "fr",
|
||||||
|
"智利": "es",
|
||||||
|
"中国": "zh-CN",
|
||||||
|
"圣诞岛": "en",
|
||||||
|
"科科斯群岛": "en",
|
||||||
|
"哥伦比亚": "es",
|
||||||
|
"科摩罗": "ar",
|
||||||
|
"刚果": "fr",
|
||||||
|
"库克群岛": "en",
|
||||||
|
"哥斯达黎加": "es",
|
||||||
|
"科特迪瓦": "fr",
|
||||||
|
"克罗地亚": "hr",
|
||||||
|
"古巴": "es",
|
||||||
|
"库拉索": "nl",
|
||||||
|
"塞浦路斯": "el",
|
||||||
|
"捷克": "cs",
|
||||||
|
"丹麦": "da",
|
||||||
|
"吉布提": "fr",
|
||||||
|
"多米尼克": "en",
|
||||||
|
"多米尼加共和国": "es",
|
||||||
|
"厄瓜多尔": "es",
|
||||||
|
"萨尔瓦多": "es",
|
||||||
|
"赤道几内亚": "es",
|
||||||
|
"厄立特里亚": "ti",
|
||||||
|
"爱沙尼亚": "et",
|
||||||
|
"埃斯瓦蒂尼": "en",
|
||||||
|
"埃塞俄比亚": "am",
|
||||||
|
"福克兰群岛": "en",
|
||||||
|
"法罗群岛": "fo",
|
||||||
|
"斐济": "en",
|
||||||
|
"法属圭亚那": "fr",
|
||||||
|
"法属波利尼西亚": "fr",
|
||||||
|
"法属南部领地": "fr",
|
||||||
|
"加蓬": "fr",
|
||||||
|
"冈比亚": "en",
|
||||||
|
"格鲁吉亚": "ka",
|
||||||
|
"加纳": "en",
|
||||||
|
"直布罗陀": "en",
|
||||||
|
"希腊": "el",
|
||||||
|
"格陵兰": "kl",
|
||||||
|
"格林纳达": "en",
|
||||||
|
"瓜德罗普": "fr",
|
||||||
|
"关岛": "en",
|
||||||
|
"危地马拉": "es",
|
||||||
|
"根西岛": "en",
|
||||||
|
"几内亚": "fr",
|
||||||
|
"几内亚比绍": "pt",
|
||||||
|
"圭亚那": "en",
|
||||||
|
"海地": "fr",
|
||||||
|
"赫德岛和麦克唐纳群岛": "en",
|
||||||
|
"梵蒂冈": "it",
|
||||||
|
"洪都拉斯": "es",
|
||||||
|
"中国香港特别行政区": "zh-TW",
|
||||||
|
"匈牙利": "hu",
|
||||||
|
"冰岛": "is",
|
||||||
|
"伊朗": "fa",
|
||||||
|
"伊拉克": "ar",
|
||||||
|
"爱尔兰": "en",
|
||||||
|
"曼岛": "en",
|
||||||
|
"牙买加": "en",
|
||||||
|
"泽西岛": "en",
|
||||||
|
"约旦": "ar",
|
||||||
|
"哈萨克斯坦": "kk",
|
||||||
|
"肯尼亚": "en",
|
||||||
|
"基里巴斯": "en",
|
||||||
|
"朝鲜": "ko",
|
||||||
|
"科威特": "ar",
|
||||||
|
"吉尔吉斯斯坦": "ky",
|
||||||
|
"拉脱维亚": "lv",
|
||||||
|
"黎巴嫩": "ar",
|
||||||
|
"莱索托": "en",
|
||||||
|
"利比里亚": "en",
|
||||||
|
"利比亚": "ar",
|
||||||
|
"列支敦士登": "de",
|
||||||
|
"立陶宛": "lt",
|
||||||
|
"卢森堡": "fr",
|
||||||
|
"中国澳门特别行政区": "zh-TW",
|
||||||
|
"马达加斯加": "fr",
|
||||||
|
"马拉维": "en",
|
||||||
|
"马尔代夫": "dv",
|
||||||
|
"马里": "fr",
|
||||||
|
"马耳他": "mt",
|
||||||
|
"马绍尔群岛": "en",
|
||||||
|
"马提尼克": "fr",
|
||||||
|
"毛里塔尼亚": "ar",
|
||||||
|
"毛里求斯": "en",
|
||||||
|
"马约特": "fr",
|
||||||
|
"密克罗尼西亚": "en",
|
||||||
|
"摩尔多瓦": "ro",
|
||||||
|
"摩纳哥": "fr",
|
||||||
|
"蒙古": "mn",
|
||||||
|
"黑山": "sr",
|
||||||
|
"蒙特塞拉特": "en",
|
||||||
|
"摩洛哥": "ar",
|
||||||
|
"莫桑比克": "pt",
|
||||||
|
"纳米比亚": "en",
|
||||||
|
"瑙鲁": "en",
|
||||||
|
"尼泊尔": "ne",
|
||||||
|
"新喀里多尼亚": "fr",
|
||||||
|
"新西兰": "en",
|
||||||
|
"尼加拉瓜": "es",
|
||||||
|
"尼日尔": "fr",
|
||||||
|
"尼日利亚": "en",
|
||||||
|
"纽埃": "en",
|
||||||
|
"诺福克岛": "en",
|
||||||
|
"北马其顿": "mk",
|
||||||
|
"北马里亚纳群岛": "en",
|
||||||
|
"挪威": "no",
|
||||||
|
"阿曼": "ar",
|
||||||
|
"帕劳": "en",
|
||||||
|
"巴勒斯坦": "ar",
|
||||||
|
"巴拿马": "es",
|
||||||
|
"巴布亚新几内亚": "en",
|
||||||
|
"巴拉圭": "es",
|
||||||
|
"秘鲁": "es",
|
||||||
|
"菲律宾": "tl",
|
||||||
|
"皮特凯恩群岛": "en",
|
||||||
|
"波多黎各": "es",
|
||||||
|
"卡塔尔": "ar",
|
||||||
|
"留尼汪": "fr",
|
||||||
|
"卢旺达": "rw",
|
||||||
|
"圣巴泰勒米": "fr",
|
||||||
|
"圣赫勒拿": "en",
|
||||||
|
"圣基茨和尼维斯": "en",
|
||||||
|
"圣卢西亚": "en",
|
||||||
|
"法属圣马丁": "fr",
|
||||||
|
"圣皮埃尔和密克隆": "fr",
|
||||||
|
"圣文森特和格林纳丁斯": "en",
|
||||||
|
"萨摩亚": "sm",
|
||||||
|
"圣马力诺": "it",
|
||||||
|
"圣多美和普林西比": "pt",
|
||||||
|
"塞内加尔": "fr",
|
||||||
|
"塞尔维亚": "sr",
|
||||||
|
"塞舌尔": "fr",
|
||||||
|
"塞拉利昂": "en",
|
||||||
|
"新加坡": "en",
|
||||||
|
"荷属圣马丁": "nl",
|
||||||
|
"斯洛伐克": "sk",
|
||||||
|
"斯洛文尼亚": "sl",
|
||||||
|
"所罗门群岛": "en",
|
||||||
|
"索马里": "so",
|
||||||
|
"南乔治亚岛和南桑威奇群岛": "en",
|
||||||
|
"南苏丹": "en",
|
||||||
|
"苏丹": "ar",
|
||||||
|
"苏里南": "nl",
|
||||||
|
"斯瓦尔巴群岛和扬马延岛": "no",
|
||||||
|
"瑞典": "sv",
|
||||||
|
"瑞士": "de",
|
||||||
|
"叙利亚": "ar",
|
||||||
|
"台湾省": "zh-TW",
|
||||||
|
"塔吉克斯坦": "tg",
|
||||||
|
"坦桑尼亚": "sw",
|
||||||
|
"东帝汶": "tet",
|
||||||
|
"多哥": "fr",
|
||||||
|
"托克劳": "en",
|
||||||
|
"汤加": "to",
|
||||||
|
"特立尼达和多巴哥": "en",
|
||||||
|
"突尼斯": "ar",
|
||||||
|
"土库曼斯坦": "tk",
|
||||||
|
"特克斯和凯科斯群岛": "en",
|
||||||
|
"图瓦卢": "en",
|
||||||
|
"乌干达": "en",
|
||||||
|
"美国本土外小岛屿": "en",
|
||||||
|
"乌拉圭": "es",
|
||||||
|
"乌兹别克斯坦": "uz",
|
||||||
|
"瓦努阿图": "bi",
|
||||||
|
"委内瑞拉": "es",
|
||||||
|
"英属维尔京群岛": "en",
|
||||||
|
"美属维尔京群岛": "en",
|
||||||
|
"瓦利斯和富图纳": "fr",
|
||||||
|
"西撒哈拉": "ar",
|
||||||
|
"也门": "ar",
|
||||||
|
"赞比亚": "en",
|
||||||
|
"津巴布韦": "en",
|
||||||
|
"阿富汗": "fa",
|
||||||
|
"阿尔及利亚": "ar",
|
||||||
|
"美属萨摩亚": "en",
|
||||||
|
"安哥拉": "pt",
|
||||||
|
"安圭拉": "en",
|
||||||
|
"南极洲": "en",
|
||||||
|
"百慕大": "en",
|
||||||
|
"荷属加勒比区": "nl",
|
||||||
|
"布韦岛": "no",
|
||||||
|
"英属印度洋领地": "en",
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_language_code(cls, country):
|
||||||
|
return cls.country_to_language.get(country)
|
||||||
|
|
||||||
|
# 使用示例
|
||||||
|
if __name__ == "__main__":
|
||||||
|
mapper = CountryLanguageMapper()
|
||||||
|
countries = ['英国', '美国', '日本', '未知国家']
|
||||||
|
for country in countries:
|
||||||
|
code = mapper.get_language_code(country)
|
||||||
|
if code:
|
||||||
|
print(f"{country} 对应的语言代码是 {code}")
|
||||||
|
else:
|
||||||
|
print(f"没有找到 {country} 对应的语言代码")
|
||||||
@@ -88,8 +88,8 @@ class DevDiskImageDeployer:
|
|||||||
exists = dst.exists()
|
exists = dst.exists()
|
||||||
if exists and not self.overwrite:
|
if exists and not self.overwrite:
|
||||||
skipped += 1
|
skipped += 1
|
||||||
if self.verbose:
|
# if self.verbose:
|
||||||
print(f"[SKIP] {dst} 已存在(目录)")
|
# print(f"[SKIP] {dst} 已存在(目录)")
|
||||||
continue
|
continue
|
||||||
if exists and self.overwrite and not self.dry_run:
|
if exists and self.overwrite and not self.dry_run:
|
||||||
shutil.rmtree(dst)
|
shutil.rmtree(dst)
|
||||||
@@ -105,8 +105,8 @@ class DevDiskImageDeployer:
|
|||||||
exists = dst.exists()
|
exists = dst.exists()
|
||||||
if exists and not self.overwrite:
|
if exists and not self.overwrite:
|
||||||
skipped += 1
|
skipped += 1
|
||||||
if self.verbose:
|
# if self.verbose:
|
||||||
print(f"[SKIP] {dst} 已存在(zip)")
|
# print(f"[SKIP] {dst} 已存在(zip)")
|
||||||
continue
|
continue
|
||||||
if exists and self.overwrite and not self.dry_run:
|
if exists and self.overwrite and not self.dry_run:
|
||||||
dst.unlink()
|
dst.unlink()
|
||||||
|
|||||||
88
Utils/IOSAIStorage.py
Normal file
88
Utils/IOSAIStorage.py
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
|
||||||
|
class IOSAIStorage:
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_iosai_dir() -> Path:
|
||||||
|
"""获取 C:/Users/<用户名>/IOSAI/ 目录"""
|
||||||
|
user_dir = Path.home()
|
||||||
|
iosai_dir = user_dir / "IOSAI"
|
||||||
|
iosai_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
return iosai_dir
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def save(cls, data: dict | list, filename: str = "data.json", mode: str = "overwrite") -> Path:
|
||||||
|
file_path = cls._get_iosai_dir() / filename
|
||||||
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
def _load_json():
|
||||||
|
try:
|
||||||
|
return json.loads(file_path.read_text("utf-8"))
|
||||||
|
except Exception:
|
||||||
|
return {} if isinstance(data, dict) else []
|
||||||
|
|
||||||
|
if mode == "merge" and isinstance(data, dict):
|
||||||
|
old = _load_json()
|
||||||
|
if not isinstance(old, dict):
|
||||||
|
old = {}
|
||||||
|
old.update(data)
|
||||||
|
to_write = old
|
||||||
|
elif mode == "append" and isinstance(data, list):
|
||||||
|
old = _load_json()
|
||||||
|
if not isinstance(old, list):
|
||||||
|
old = []
|
||||||
|
old.extend(data)
|
||||||
|
to_write = old
|
||||||
|
else:
|
||||||
|
to_write = data # 覆盖
|
||||||
|
|
||||||
|
# 原子写入
|
||||||
|
tmp = file_path.with_suffix(file_path.suffix + ".tmp")
|
||||||
|
with open(tmp, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(to_write, f, ensure_ascii=False, indent=2)
|
||||||
|
os.replace(tmp, file_path)
|
||||||
|
print(f"[IOSAIStorage] 已写入: {file_path}")
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def load(cls, filename: str = "data.json") -> dict | list | None:
|
||||||
|
"""
|
||||||
|
从 C:/Users/<用户名>/IOSAI/filename 读取数据
|
||||||
|
"""
|
||||||
|
file_path = cls._get_iosai_dir() / filename
|
||||||
|
if not file_path.exists():
|
||||||
|
print(f"[IOSAIStorage] 文件不存在: {file_path}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
return json.load(f)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[IOSAIStorage] 读取失败: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def overwrite(cls, data: dict | list, filename: str = "data.json") -> Path:
|
||||||
|
"""
|
||||||
|
强制覆盖写入数据到 C:/Users/<用户名>/IOSAI/filename
|
||||||
|
(无论是否存在,都会写入)
|
||||||
|
"""
|
||||||
|
file_path = cls._get_iosai_dir() / filename
|
||||||
|
try:
|
||||||
|
# "w" 模式本身就是覆盖,但这里单独做一个方法
|
||||||
|
with open(file_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=2)
|
||||||
|
print(f"[IOSAIStorage] 已覆盖写入: {file_path}")
|
||||||
|
except Exception as e:
|
||||||
|
print(f"[IOSAIStorage] 覆盖失败: {e}")
|
||||||
|
return file_path
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
262
Utils/JsonUtils.py
Normal file
262
Utils/JsonUtils.py
Normal file
@@ -0,0 +1,262 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import portalocker as locker # ① 引入跨平台锁
|
||||||
|
|
||||||
|
|
||||||
|
class JsonUtils:
|
||||||
|
@staticmethod
|
||||||
|
def _normalize_filename(filename: str) -> str:
|
||||||
|
"""
|
||||||
|
确保文件名以 .json 结尾
|
||||||
|
"""
|
||||||
|
if not filename.endswith(".json"):
|
||||||
|
filename = f"{filename}.json"
|
||||||
|
return filename
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_data_path(filename: str) -> str:
|
||||||
|
"""
|
||||||
|
根据文件名生成 data 目录下的完整路径
|
||||||
|
"""
|
||||||
|
filename = JsonUtils._normalize_filename(filename)
|
||||||
|
base_dir = os.path.abspath(os.path.dirname(os.path.dirname(__file__))) # 当前项目根目录
|
||||||
|
data_dir = os.path.join(base_dir, "data")
|
||||||
|
Path(data_dir).mkdir(parents=True, exist_ok=True) # 确保 data 目录存在
|
||||||
|
return os.path.join(data_dir, filename)
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def read_json(filename: str) -> dict:
|
||||||
|
"""
|
||||||
|
读取 JSON 文件,返回字典
|
||||||
|
如果文件不存在,返回空字典
|
||||||
|
"""
|
||||||
|
file_path = JsonUtils._get_data_path(filename)
|
||||||
|
try:
|
||||||
|
if not os.path.exists(file_path):
|
||||||
|
return {}
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return data if isinstance(data, dict) else {}
|
||||||
|
except Exception as e:
|
||||||
|
print(f"读取 JSON 文件失败: {e}")
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def write_json(filename: str, data: dict, overwrite: bool = True) -> bool:
|
||||||
|
"""
|
||||||
|
将字典写入 JSON 文件
|
||||||
|
:param filename: 文件名(不用写后缀,自动补 .json)
|
||||||
|
:param data: 要写入的字典
|
||||||
|
:param overwrite: True=覆盖写,False=合并更新
|
||||||
|
"""
|
||||||
|
file_path = JsonUtils._get_data_path(filename)
|
||||||
|
try:
|
||||||
|
if not overwrite and os.path.exists(file_path):
|
||||||
|
with open(file_path, "r", encoding="utf-8") as f:
|
||||||
|
old_data = json.load(f)
|
||||||
|
if not isinstance(old_data, dict):
|
||||||
|
old_data = {}
|
||||||
|
old_data.update(data)
|
||||||
|
data = old_data
|
||||||
|
|
||||||
|
with open(file_path, "w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=4)
|
||||||
|
return True
|
||||||
|
except Exception as e:
|
||||||
|
print(f"写入 JSON 文件失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def update_json(filename: str, new_data: dict) -> bool:
|
||||||
|
"""
|
||||||
|
修改 JSON 文件:
|
||||||
|
- 如果 key 已存在,则修改其值
|
||||||
|
- 如果 key 不存在,则新增
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = JsonUtils.read_json(filename)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
data = {}
|
||||||
|
data.update(new_data)
|
||||||
|
return JsonUtils.write_json(filename, data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"更新 JSON 文件失败: {e}")
|
||||||
|
return False
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def delete_json_key(filename: str, key: str) -> bool:
|
||||||
|
"""
|
||||||
|
删除 JSON 文件中的某个 key
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
data = JsonUtils.read_json(filename)
|
||||||
|
if not isinstance(data, dict):
|
||||||
|
data = {}
|
||||||
|
if key in data:
|
||||||
|
del data[key]
|
||||||
|
return JsonUtils.write_json(filename, data)
|
||||||
|
except Exception as e:
|
||||||
|
print(f"删除 JSON key 失败: {e}")
|
||||||
|
return False
|
||||||
|
# "-------------------------------------------------"
|
||||||
|
@classmethod
|
||||||
|
def _read_json_list(cls, file_path: Path) -> list:
|
||||||
|
try:
|
||||||
|
if not file_path.exists():
|
||||||
|
return []
|
||||||
|
with file_path.open("r", encoding="utf-8") as f:
|
||||||
|
data = json.load(f)
|
||||||
|
return data if isinstance(data, list) else []
|
||||||
|
except Exception:
|
||||||
|
return []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _write_json_list(cls, file_path: Path, data: list) -> None:
|
||||||
|
file_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
with file_path.open("w", encoding="utf-8") as f:
|
||||||
|
json.dump(data, f, ensure_ascii=False, indent=4)
|
||||||
|
|
||||||
|
# --- 新增:通用追加(不做字段校验) ---
|
||||||
|
# @classmethod
|
||||||
|
# def append_json_items(cls, items, filename="log/last_message.json"):
|
||||||
|
# """
|
||||||
|
# 将 dict 或 [dict, ...] 追加到 JSON 文件(数组)中;不校验字段。
|
||||||
|
# """
|
||||||
|
# file_path = Path(filename)
|
||||||
|
# data = cls._read_json_list(file_path)
|
||||||
|
#
|
||||||
|
# # 统一成 list
|
||||||
|
# if isinstance(items, dict):
|
||||||
|
# items = [items]
|
||||||
|
# elif not isinstance(items, list):
|
||||||
|
# # 既不是 dict 也不是 list,直接忽略
|
||||||
|
# return
|
||||||
|
#
|
||||||
|
# # 只接受字典项
|
||||||
|
# items = [it for it in items if isinstance(it, dict)]
|
||||||
|
# if not items:
|
||||||
|
# return
|
||||||
|
#
|
||||||
|
# data.extend(items)
|
||||||
|
#
|
||||||
|
# # LogManager.method_info(filename,"路径")
|
||||||
|
# cls._write_json_list(file_path, data)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def append_json_items(cls, items, filename="log/last_message.json"):
|
||||||
|
file_path = Path(filename)
|
||||||
|
data = cls._read_json_list(file_path)
|
||||||
|
|
||||||
|
# 统一成 list
|
||||||
|
if isinstance(items, dict):
|
||||||
|
items = [items]
|
||||||
|
elif not isinstance(items, list):
|
||||||
|
return
|
||||||
|
|
||||||
|
# 只保留 sender 非空的字典
|
||||||
|
items = [
|
||||||
|
it for it in items
|
||||||
|
if isinstance(it, dict) and it.get("sender") != ""
|
||||||
|
]
|
||||||
|
if not items:
|
||||||
|
return
|
||||||
|
|
||||||
|
data.extend(items)
|
||||||
|
cls._write_json_list(file_path, data)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def update_json_items(cls, match: dict, patch: dict, filename="log/last_message.json", multi: bool = True) -> int:
|
||||||
|
"""
|
||||||
|
修改 JSON 文件(数组)中符合条件的项
|
||||||
|
:param match: 匹配条件(如 {"sender": "xxx"})
|
||||||
|
:param patch: 要修改/更新的字段(如 {"status": 1})
|
||||||
|
:param filename: JSON 文件路径
|
||||||
|
:param multi: True=修改所有匹配项,False=只修改第一项
|
||||||
|
:return: 修改的条数
|
||||||
|
"""
|
||||||
|
file_path = Path(filename)
|
||||||
|
data = cls._read_json_list(file_path)
|
||||||
|
|
||||||
|
if not isinstance(match, dict) or not isinstance(patch, dict):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
updated = 0
|
||||||
|
for idx, item in enumerate(data):
|
||||||
|
if not isinstance(item, dict):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 判断是否匹配
|
||||||
|
if all(item.get(k) == v for k, v in match.items()):
|
||||||
|
data[idx].update(patch)
|
||||||
|
updated += 1
|
||||||
|
if not multi:
|
||||||
|
break
|
||||||
|
|
||||||
|
if updated > 0:
|
||||||
|
cls._write_json_list(file_path, data)
|
||||||
|
|
||||||
|
return updated
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def query_all_json_items(cls, filename="log/last_message.json"):
|
||||||
|
"""
|
||||||
|
读取 JSON 数组文件,过滤掉 sender 或 text 为空的记录
|
||||||
|
:param filename: 文件路径
|
||||||
|
:return: 有效记录列表,可能为空
|
||||||
|
"""
|
||||||
|
file_path = Path(filename)
|
||||||
|
data = cls._read_json_list(file_path)
|
||||||
|
if not isinstance(data, list):
|
||||||
|
return []
|
||||||
|
|
||||||
|
def _is_valid(d):
|
||||||
|
if not isinstance(d, dict):
|
||||||
|
return False
|
||||||
|
sender = d.get("sender") or ""
|
||||||
|
text = d.get("text") or ""
|
||||||
|
return (
|
||||||
|
isinstance(sender, str)
|
||||||
|
and isinstance(text, str)
|
||||||
|
and sender.strip() != ""
|
||||||
|
and text.strip() != ""
|
||||||
|
)
|
||||||
|
|
||||||
|
return [item for item in data if _is_valid(item)]
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def delete_json_items(cls,
|
||||||
|
match: dict,
|
||||||
|
filename: str = "log/last_message.json",
|
||||||
|
multi: bool = True) -> int:
|
||||||
|
file_path = Path(filename)
|
||||||
|
with file_path.open('r+', encoding='utf-8') as f:
|
||||||
|
locker.lock(f, locker.LOCK_EX) # ② 加独占锁(Windows/Linux 通用)
|
||||||
|
try:
|
||||||
|
data = json.load(f)
|
||||||
|
if not isinstance(match, dict):
|
||||||
|
return 0
|
||||||
|
|
||||||
|
deleted = 0
|
||||||
|
new_data = []
|
||||||
|
for item in data:
|
||||||
|
if isinstance(item, dict) and all(item.get(k) == v for k, v in match.items()):
|
||||||
|
if multi or deleted == 0: # 删多条 / 第一条
|
||||||
|
deleted += 1
|
||||||
|
continue
|
||||||
|
new_data.append(item)
|
||||||
|
|
||||||
|
if deleted:
|
||||||
|
f.seek(0)
|
||||||
|
json.dump(new_data, f, ensure_ascii=False, indent=2)
|
||||||
|
f.truncate()
|
||||||
|
|
||||||
|
return deleted
|
||||||
|
finally:
|
||||||
|
locker.unlock(f) # ③ 解锁
|
||||||
@@ -1,98 +1,41 @@
|
|||||||
# import logging
|
# -*- coding: utf-8 -*-
|
||||||
# import os
|
import datetime
|
||||||
# import sys
|
import io
|
||||||
# import shutil
|
|
||||||
# from pathlib import Path
|
|
||||||
#
|
|
||||||
#
|
|
||||||
# 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 = {}
|
|
||||||
#
|
|
||||||
# @classmethod
|
|
||||||
# def _setupLogger(cls, udid, name, logName, level=logging.INFO):
|
|
||||||
# """创建或获取 logger,并绑定到文件"""
|
|
||||||
# deviceLogDir = os.path.join(cls.logDir, 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 clearLogs(cls):
|
|
||||||
# """启动时清空 log 目录下所有文件"""
|
|
||||||
# print("开始清空日志...")
|
|
||||||
#
|
|
||||||
# # 关闭所有 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_path = Path(cls.logDir)
|
|
||||||
# if log_path.exists():
|
|
||||||
# for item in log_path.iterdir():
|
|
||||||
# if item.is_file():
|
|
||||||
# item.unlink()
|
|
||||||
# elif item.is_dir():
|
|
||||||
# import shutil
|
|
||||||
# shutil.rmtree(item)
|
|
||||||
#
|
|
||||||
# print("日志清空完成")
|
|
||||||
|
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import re
|
import re
|
||||||
import sys
|
import sys
|
||||||
import shutil
|
import shutil
|
||||||
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
|
|
||||||
|
# ========= 全局:强制 UTF-8(打包 EXE / 无控制台也生效) =========
|
||||||
|
def _force_utf8_everywhere():
|
||||||
|
os.environ.setdefault("PYTHONUTF8", "1")
|
||||||
|
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
|
||||||
|
# windowed 模式下 stdout/stderr 可能没有 buffer,这里做保护包装
|
||||||
|
try:
|
||||||
|
if getattr(sys.stdout, "buffer", None):
|
||||||
|
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
try:
|
||||||
|
if getattr(sys.stderr, "buffer", None):
|
||||||
|
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
_force_utf8_everywhere()
|
||||||
|
|
||||||
class LogManager:
|
class LogManager:
|
||||||
|
"""
|
||||||
|
设备级与“设备+方法”级日志管理:
|
||||||
|
- log/<udid>/info.log | warning.log | error.log
|
||||||
|
- log/<udid>/<method>.log
|
||||||
|
- 文件统一 UTF-8 编码,避免 GBK/CP936 导致的 emoji 报错
|
||||||
|
- 提供 clearLogs() 与 upload_all_logs()
|
||||||
|
"""
|
||||||
# 运行根目录:打包后取 exe 目录;源码运行取项目目录
|
# 运行根目录:打包后取 exe 目录;源码运行取项目目录
|
||||||
if getattr(sys, "frozen", False):
|
if getattr(sys, "frozen", False):
|
||||||
projectRoot = os.path.dirname(sys.executable)
|
projectRoot = os.path.dirname(sys.executable)
|
||||||
@@ -100,95 +43,111 @@ class LogManager:
|
|||||||
projectRoot = os.path.dirname(os.path.dirname(__file__))
|
projectRoot = os.path.dirname(os.path.dirname(__file__))
|
||||||
|
|
||||||
logDir = os.path.join(projectRoot, "log")
|
logDir = os.path.join(projectRoot, "log")
|
||||||
_loggers = {}
|
_method_loggers = {} # 缓存“设备+方法”的 logger
|
||||||
_method_loggers = {} # 新增:缓存“设备+方法”的 logger
|
|
||||||
|
# ---------- 工具:安全文本/文件名 ----------
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _safe_text(obj) -> str:
|
||||||
|
"""把任意对象安全转为可写字符串(避免因编码问题再次抛异常)"""
|
||||||
|
try:
|
||||||
|
if isinstance(obj, bytes):
|
||||||
|
return obj.decode("utf-8", "replace")
|
||||||
|
s = str(obj)
|
||||||
|
# 确保解码上屏不再出错
|
||||||
|
_ = s.encode("utf-8", "replace")
|
||||||
|
return s
|
||||||
|
except Exception:
|
||||||
|
try:
|
||||||
|
return repr(obj)
|
||||||
|
except Exception:
|
||||||
|
return "<unprintable>"
|
||||||
|
|
||||||
# ---------- 工具函数 ----------
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _safe_filename(cls, name: str, max_len: int = 80) -> str:
|
def _safe_filename(cls, name: str, max_len: int = 80) -> str:
|
||||||
"""
|
"""
|
||||||
将方法名/udid等转成安全文件名:
|
将方法名/udid等转成安全文件名:
|
||||||
- 允许字母数字、点、下划线、连字符
|
- 允许字母数字、点、下划线、连字符
|
||||||
- 允许常见 CJK 字符(中日韩)
|
- 保留常见 CJK 字符(中日韩)
|
||||||
- 其他非法字符替换为下划线
|
- 其余替换为下划线;合并下划线;避免保留名;限长
|
||||||
- 合并多余下划线,裁剪长度
|
|
||||||
"""
|
"""
|
||||||
if not name:
|
if not name:
|
||||||
return "unknown"
|
return "unknown"
|
||||||
name = str(name).strip()
|
name = str(name).strip()
|
||||||
|
name = re.sub(r'[\\/:*?"<>|\r\n\t]+', '_', name) # Windows 非法字符
|
||||||
# 替换 Windows 非法字符和控制符
|
name = re.sub(
|
||||||
name = re.sub(r'[\\/:*?"<>|\r\n\t]+', '_', name)
|
r'[^a-zA-Z0-9_.\-'
|
||||||
|
r'\u4e00-\u9fff' # 中
|
||||||
# 只保留 ① 英数._- ② CJK 统一表意文字、日文平/片假名、韩文音节
|
r'\u3040-\u30ff' # 日
|
||||||
name = re.sub(rf'[^a-zA-Z0-9_.\-'
|
r'\uac00-\ud7a3' # 韩
|
||||||
r'\u4e00-\u9fff' # 中
|
r']+', '_', name
|
||||||
r'\u3040-\u30ff' # 日
|
)
|
||||||
r'\uac00-\ud7a3' # 韩
|
|
||||||
r']+', '_', name)
|
|
||||||
# 合并多余下划线,去两端空白与下划线
|
|
||||||
name = re.sub(r'_+', '_', name).strip(' _.')
|
name = re.sub(r'_+', '_', name).strip(' _.')
|
||||||
# 避免空
|
|
||||||
name = name or "unknown"
|
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):
|
if re.fullmatch(r'(?i)(CON|PRN|AUX|NUL|COM[1-9]|LPT[1-9])', name):
|
||||||
name = f"_{name}"
|
name = f"_{name}"
|
||||||
# 限长
|
|
||||||
return name[:max_len] or "unknown"
|
return name[:max_len] or "unknown"
|
||||||
|
|
||||||
# ---------- 旧的:按级别写固定文件 ----------
|
# ---------- 设备级固定文件:info/warning/error ----------
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _setupLogger(cls, udid, name, logName, level=logging.INFO):
|
def _setupLogger(cls, udid, name, logName, level=logging.INFO):
|
||||||
"""创建或获取 logger,并绑定到设备目录下的固定文件(info.log / warning.log / error.log)"""
|
"""创建或获取 logger,并绑定到设备目录下的固定文件(info.log / warning.log / error.log)"""
|
||||||
deviceLogDir = os.path.join(cls.logDir, cls._safe_filename(udid))
|
udid_key = cls._safe_filename(udid or "system")
|
||||||
|
deviceLogDir = os.path.join(cls.logDir, udid_key)
|
||||||
os.makedirs(deviceLogDir, exist_ok=True)
|
os.makedirs(deviceLogDir, exist_ok=True)
|
||||||
logFile = os.path.join(deviceLogDir, logName)
|
logFile = os.path.join(deviceLogDir, logName)
|
||||||
|
|
||||||
logger_name = f"{udid}_{name}"
|
logger_name = f"{udid_key}_{name}"
|
||||||
logger = logging.getLogger(logger_name)
|
logger = logging.getLogger(logger_name)
|
||||||
logger.setLevel(level)
|
logger.setLevel(level)
|
||||||
|
logger.propagate = False # 不向根 logger 传播,避免重复
|
||||||
|
|
||||||
# 避免重复添加 handler
|
# 避免重复添加同一路径的 file handler
|
||||||
if not any(
|
abs_target = os.path.abspath(logFile)
|
||||||
isinstance(h, logging.FileHandler) and h.baseFilename == os.path.abspath(logFile)
|
for h in logger.handlers:
|
||||||
for h in logger.handlers
|
if isinstance(h, logging.FileHandler) and getattr(h, "baseFilename", "") == abs_target:
|
||||||
):
|
return logger
|
||||||
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)
|
|
||||||
|
|
||||||
|
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
|
return logger
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def info(cls, text, udid="system"):
|
def info(cls, text, udid="system"):
|
||||||
cls._setupLogger(udid, "infoLogger", "info.log", level=logging.INFO).info(f"[{udid}] {text}")
|
msg = cls._safe_text(f"[{udid}] {text}")
|
||||||
|
cls._setupLogger(udid, "infoLogger", "info.log", level=logging.INFO).info(msg)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def warning(cls, text, udid="system"):
|
def warning(cls, text, udid="system"):
|
||||||
cls._setupLogger(udid, "warningLogger", "warning.log", level=logging.WARNING).warning(f"[{udid}] {text}")
|
msg = cls._safe_text(f"[{udid}] {text}")
|
||||||
|
cls._setupLogger(udid, "warningLogger", "warning.log", level=logging.WARNING).warning(msg)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def error(cls, text, udid="system"):
|
def error(cls, text, udid="system"):
|
||||||
cls._setupLogger(udid, "errorLogger", "error.log", level=logging.ERROR).error(f"[{udid}] {text}")
|
msg = cls._safe_text(f"[{udid}] {text}")
|
||||||
|
cls._setupLogger(udid, "errorLogger", "error.log", level=logging.ERROR).error(msg)
|
||||||
|
|
||||||
|
# ---------- “设备+方法”独立文件:<udid>/<method>.log ----------
|
||||||
|
|
||||||
# ---------- 新增:按“设备+方法”分别写独立日志文件 ----------
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def _setupMethodLogger(cls, udid: str, method: str, level=logging.INFO):
|
def _setupMethodLogger(cls, udid: str, method: str, level=logging.INFO):
|
||||||
"""
|
"""
|
||||||
为某设备的某个方法单独创建 logger:
|
为某设备的某个方法单独创建 logger:log/<udid>/<method>.log
|
||||||
log/<udid>/<method>.log
|
|
||||||
"""
|
"""
|
||||||
udid_key = cls._safe_filename(udid or "system")
|
udid_key = cls._safe_filename(udid or "system")
|
||||||
method_key = cls._safe_filename(method or "general")
|
method_key = cls._safe_filename(method or "general")
|
||||||
cache_key = (udid_key, method_key)
|
cache_key = (udid_key, method_key)
|
||||||
|
|
||||||
# 命中缓存
|
# 命中缓存
|
||||||
if cache_key in cls._method_loggers:
|
logger = cls._method_loggers.get(cache_key)
|
||||||
return cls._method_loggers[cache_key]
|
if logger:
|
||||||
|
return logger
|
||||||
|
|
||||||
deviceLogDir = os.path.join(cls.logDir, udid_key)
|
deviceLogDir = os.path.join(cls.logDir, udid_key)
|
||||||
os.makedirs(deviceLogDir, exist_ok=True)
|
os.makedirs(deviceLogDir, exist_ok=True)
|
||||||
@@ -197,100 +156,122 @@ class LogManager:
|
|||||||
logger_name = f"{udid_key}.{method_key}"
|
logger_name = f"{udid_key}.{method_key}"
|
||||||
logger = logging.getLogger(logger_name)
|
logger = logging.getLogger(logger_name)
|
||||||
logger.setLevel(level)
|
logger.setLevel(level)
|
||||||
logger.propagate = False # 避免向根 logger 传播导致控制台重复打印
|
logger.propagate = False
|
||||||
|
|
||||||
# 避免重复添加 handler
|
abs_target = os.path.abspath(logFile)
|
||||||
if not any(
|
for h in logger.handlers:
|
||||||
isinstance(h, logging.FileHandler) and h.baseFilename == os.path.abspath(logFile)
|
if isinstance(h, logging.FileHandler) and getattr(h, "baseFilename", "") == abs_target:
|
||||||
for h in logger.handlers
|
cls._method_loggers[cache_key] = logger
|
||||||
):
|
return logger
|
||||||
fileHandler = logging.FileHandler(logFile, mode="a", encoding="utf-8")
|
|
||||||
formatter = logging.Formatter(
|
fileHandler = logging.FileHandler(logFile, mode="a", encoding="utf-8")
|
||||||
"%(asctime)s - %(levelname)s - %(name)s - %(message)s",
|
formatter = logging.Formatter(
|
||||||
datefmt="%Y-%m-%d %H:%M:%S"
|
"%(asctime)s - %(levelname)s - %(name)s - %(message)s",
|
||||||
)
|
datefmt="%Y-%m-%d %H:%M:%S"
|
||||||
fileHandler.setFormatter(formatter)
|
)
|
||||||
logger.addHandler(fileHandler)
|
fileHandler.setFormatter(formatter)
|
||||||
|
logger.addHandler(fileHandler)
|
||||||
|
|
||||||
cls._method_loggers[cache_key] = logger
|
cls._method_loggers[cache_key] = logger
|
||||||
return logger
|
return logger
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def method_info(cls, text, method, udid="system"):
|
def method_info(cls, text, method, udid="system"):
|
||||||
"""按设备+方法写 INFO 到 log/<udid>/<method>.log"""
|
msg = cls._safe_text(f"[{udid}][{method}] {text}")
|
||||||
cls._setupMethodLogger(udid, method, level=logging.INFO).info(f"[{udid}][{method}] {text}")
|
cls._setupMethodLogger(udid, method, level=logging.INFO).info(msg)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def method_warning(cls, text, method, udid="system"):
|
def method_warning(cls, text, method, udid="system"):
|
||||||
cls._setupMethodLogger(udid, method, level=logging.WARNING).warning(f"[{udid}][{method}] {text}")
|
msg = cls._safe_text(f"[{udid}][{method}] {text}")
|
||||||
|
cls._setupMethodLogger(udid, method, level=logging.WARNING).warning(msg)
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def method_error(cls, text, method, udid="system"):
|
def method_error(cls, text, method, udid="system"):
|
||||||
cls._setupMethodLogger(udid, method, level=logging.ERROR).error(f"[{udid}][{method}] {text}")
|
msg = cls._safe_text(f"[{udid}][{method}] {text}")
|
||||||
|
cls._setupMethodLogger(udid, method, level=logging.ERROR).error(msg)
|
||||||
|
|
||||||
|
# ---------- 清空日志 ----------
|
||||||
|
|
||||||
# 清空日志
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def clearLogs(cls):
|
def clearLogs(cls):
|
||||||
|
print("清空日志")
|
||||||
"""启动时清空 log 目录下所有文件"""
|
"""启动时清空 log 目录下所有文件"""
|
||||||
print("开始清空日志...")
|
# 先关闭所有 logger 的文件句柄
|
||||||
|
for _, logger in logging.Logger.manager.loggerDict.items():
|
||||||
# 关闭所有 handler
|
|
||||||
for name, logger in logging.Logger.manager.loggerDict.items():
|
|
||||||
if isinstance(logger, logging.Logger):
|
if isinstance(logger, logging.Logger):
|
||||||
for handler in logger.handlers[:]:
|
for handler in list(logger.handlers):
|
||||||
try:
|
try:
|
||||||
handler.close()
|
handler.close()
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
pass
|
||||||
logger.removeHandler(handler)
|
logger.removeHandler(handler)
|
||||||
|
|
||||||
# 删除 log 目录
|
|
||||||
log_path = Path(cls.logDir)
|
log_path = Path(cls.logDir)
|
||||||
if log_path.exists():
|
if log_path.exists():
|
||||||
for item in log_path.iterdir():
|
for item in log_path.iterdir():
|
||||||
if item.is_file():
|
|
||||||
item.unlink()
|
|
||||||
elif item.is_dir():
|
|
||||||
shutil.rmtree(item)
|
|
||||||
|
|
||||||
# 清缓存
|
|
||||||
cls._method_loggers.clear()
|
|
||||||
print("日志清空完成")
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def upload_all_logs(cls, server_url: str, extra_data: dict = None):
|
|
||||||
"""
|
|
||||||
上传 log/ 目录下所有文件和子目录中的日志
|
|
||||||
"""
|
|
||||||
log_path = Path(cls.logDir)
|
|
||||||
if not log_path.exists():
|
|
||||||
print("[upload_all_logs] 日志目录不存在")
|
|
||||||
return False
|
|
||||||
|
|
||||||
success_files, failed_files = [], []
|
|
||||||
|
|
||||||
for file in log_path.rglob("*.log"): # 递归找到所有 .log 文件
|
|
||||||
try:
|
|
||||||
files = {"file": open(file, "rb")}
|
|
||||||
data = {"relative_path": str(file.relative_to(log_path))}
|
|
||||||
if extra_data:
|
|
||||||
data.update(extra_data)
|
|
||||||
|
|
||||||
resp = requests.post(server_url, files=files, data=data, timeout=15)
|
|
||||||
if resp.status_code == 200:
|
|
||||||
success_files.append(file.name)
|
|
||||||
else:
|
|
||||||
failed_files.append((file.name, resp.status_code))
|
|
||||||
except Exception as e:
|
|
||||||
failed_files.append((file.name, str(e)))
|
|
||||||
finally:
|
|
||||||
try:
|
try:
|
||||||
files["file"].close()
|
if item.is_file():
|
||||||
except:
|
item.unlink(missing_ok=True) # py>=3.8
|
||||||
|
elif item.is_dir():
|
||||||
|
shutil.rmtree(item, ignore_errors=True)
|
||||||
|
except Exception:
|
||||||
|
# 不阻塞清理
|
||||||
pass
|
pass
|
||||||
|
|
||||||
print(f"[upload_all_logs] 成功上传: {success_files}")
|
cls._method_loggers.clear()
|
||||||
if failed_files:
|
|
||||||
print(f"[upload_all_logs] 失败文件: {failed_files}")
|
|
||||||
|
|
||||||
return len(failed_files) == 0
|
# ---------- 上传所有日志(内存打包 zip) ----------
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def upload_all_logs(cls, server_url, token, userId, tenantId):
|
||||||
|
"""
|
||||||
|
将 log/ 目录下所有日志打包为 zip(内存),上传至服务器:
|
||||||
|
- headers: {"vvtoken": <token>}
|
||||||
|
- form: {"tenantId": <tenantId>, "userId": <userId>, "file": <zip>}
|
||||||
|
返回 True/False
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
log_path = Path(cls.logDir)
|
||||||
|
if not log_path.exists():
|
||||||
|
logging.info("[upload_all_logs] 日志目录不存在:%s", log_path)
|
||||||
|
return False
|
||||||
|
|
||||||
|
# 文件名仅用于表单,不落盘,可包含安全字符
|
||||||
|
timestamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||||
|
filename = f"{timestamp}_logs.zip"
|
||||||
|
logging.info("[upload_all_logs] 打包文件名:%s", filename)
|
||||||
|
|
||||||
|
# 1) 内存中打包 zip
|
||||||
|
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()
|
||||||
|
|
||||||
|
# 2) 组织请求
|
||||||
|
headers = {"vvtoken": token} if token else {}
|
||||||
|
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, timeout=120)
|
||||||
|
ok = False
|
||||||
|
try:
|
||||||
|
js = resp.json()
|
||||||
|
ok = bool(js.get("data"))
|
||||||
|
except Exception:
|
||||||
|
# 响应不是 JSON,也许是纯文本;降级按状态码判断
|
||||||
|
ok = resp.ok
|
||||||
|
|
||||||
|
if ok:
|
||||||
|
logging.info("[upload_all_logs] 上传成功:%s", server_url)
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
logging.error("[upload_all_logs] 上传失败,status=%s, text=%s", resp.status_code, LogManager._safe_text(resp.text))
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logging.error("[upload_all_logs] 异常:%s", LogManager._safe_text(e))
|
||||||
|
return False
|
||||||
|
|||||||
243
Utils/OCRUtils.py
Normal file
243
Utils/OCRUtils.py
Normal file
@@ -0,0 +1,243 @@
|
|||||||
|
import os
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from typing import List, Tuple, Union, Optional
|
||||||
|
from PIL import Image
|
||||||
|
|
||||||
|
ArrayLikeImage = Union[np.ndarray, str, Image.Image]
|
||||||
|
|
||||||
|
class OCRUtils:
|
||||||
|
@classmethod
|
||||||
|
def _to_gray(cls, img: ArrayLikeImage) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
接受路径/np.ndarray/PIL.Image,统一转为灰度 np.ndarray。
|
||||||
|
"""
|
||||||
|
# 路径
|
||||||
|
if isinstance(img, str):
|
||||||
|
arr = cv2.imread(img, cv2.IMREAD_GRAYSCALE)
|
||||||
|
if arr is None:
|
||||||
|
raise FileNotFoundError(f"图像加载失败,请检查路径: {img}")
|
||||||
|
return arr
|
||||||
|
|
||||||
|
# PIL.Image
|
||||||
|
if isinstance(img, Image.Image):
|
||||||
|
return cv2.cvtColor(np.array(img.convert("RGB")), cv2.COLOR_RGB2GRAY)
|
||||||
|
|
||||||
|
# numpy 数组
|
||||||
|
if isinstance(img, np.ndarray):
|
||||||
|
if img.ndim == 2:
|
||||||
|
return img # 已是灰度
|
||||||
|
if img.ndim == 3:
|
||||||
|
return cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
|
||||||
|
raise ValueError("不支持的图像维度(期望 2D 灰度或 3D BGR/RGB)")
|
||||||
|
|
||||||
|
raise TypeError("large_image 类型必须是 str / np.ndarray / PIL.Image.Image")
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def non_max_suppression(
|
||||||
|
cls,
|
||||||
|
boxes: List[List[float]],
|
||||||
|
scores: Optional[np.ndarray] = None,
|
||||||
|
overlapThresh: float = 0.5
|
||||||
|
) -> np.ndarray:
|
||||||
|
"""
|
||||||
|
boxes: [ [x1,y1,x2,y2], ... ]
|
||||||
|
scores: 每个框的置信度(用于“按分数做 NMS”)。若为 None,则退化为按 y2 排序的经典近似。
|
||||||
|
返回: 经过 NMS 保留的 boxes(int) ndarray,形状 (N,4)
|
||||||
|
"""
|
||||||
|
if len(boxes) == 0:
|
||||||
|
return np.empty((0, 4), dtype=int)
|
||||||
|
|
||||||
|
boxes = np.asarray(boxes, dtype=np.float32)
|
||||||
|
x1, y1, x2, y2 = boxes.T
|
||||||
|
areas = (x2 - x1 + 1) * (y2 - y1 + 1)
|
||||||
|
|
||||||
|
if scores is None:
|
||||||
|
order = np.argsort(y2) # 经典写法
|
||||||
|
else:
|
||||||
|
scores = np.asarray(scores, dtype=np.float32)
|
||||||
|
order = np.argsort(scores)[::-1] # 分数从高到低
|
||||||
|
|
||||||
|
keep = []
|
||||||
|
while order.size > 0:
|
||||||
|
i = order[0] if scores is not None else order[-1]
|
||||||
|
keep.append(i)
|
||||||
|
|
||||||
|
rest = order[1:] if scores is not None else order[:-1]
|
||||||
|
|
||||||
|
xx1 = np.maximum(x1[i], x1[rest])
|
||||||
|
yy1 = np.maximum(y1[i], y1[rest])
|
||||||
|
xx2 = np.minimum(x2[i], x2[rest])
|
||||||
|
yy2 = np.minimum(y2[i], y2[rest])
|
||||||
|
|
||||||
|
w = np.maximum(0, xx2 - xx1 + 1)
|
||||||
|
h = np.maximum(0, yy2 - yy1 + 1)
|
||||||
|
inter = w * h
|
||||||
|
ovr = inter / areas[rest]
|
||||||
|
|
||||||
|
inds = np.where(ovr <= overlapThresh)[0]
|
||||||
|
order = rest[inds]
|
||||||
|
|
||||||
|
return boxes[keep].astype(int)
|
||||||
|
|
||||||
|
# @classmethod
|
||||||
|
# def find_template(
|
||||||
|
# cls,
|
||||||
|
# template_path: str,
|
||||||
|
# large_image: ArrayLikeImage,
|
||||||
|
# threshold: float = 0.8,
|
||||||
|
# overlapThresh: float = 0.5,
|
||||||
|
# return_boxes: bool = False
|
||||||
|
# ) -> Union[List[Tuple[int, int]], Tuple[List[Tuple[int, int]], np.ndarray]]:
|
||||||
|
# """
|
||||||
|
# 在 large_image 中查找 template_path 模板的位置。
|
||||||
|
# - large_image 可为文件路径、np.ndarray 或 PIL.Image
|
||||||
|
# - threshold: 模板匹配阈值(TM_CCOEFF_NORMED)
|
||||||
|
# - overlapThresh: NMS 重叠阈值
|
||||||
|
# - return_boxes: True 时同时返回保留的框数组 (N,4)
|
||||||
|
#
|
||||||
|
# 返回:
|
||||||
|
# centers 或 (centers, boxes)
|
||||||
|
# centers: [(cx, cy), ...]
|
||||||
|
# boxes: [[x1,y1,x2,y2], ...] (np.ndarray, int)
|
||||||
|
# """
|
||||||
|
# # 模板(灰度)
|
||||||
|
# template = cv2.imread(template_path, cv2.IMREAD_GRAYSCALE)
|
||||||
|
# if template is None:
|
||||||
|
# raise FileNotFoundError(f"模板图像加载失败,请检查路径: {template_path}")
|
||||||
|
#
|
||||||
|
# # 大图(灰度)
|
||||||
|
# gray = cls._to_gray(large_image)
|
||||||
|
#
|
||||||
|
# # 模板尺寸
|
||||||
|
# tw, th = template.shape[::-1]
|
||||||
|
#
|
||||||
|
# # 模板匹配(相关系数归一化)
|
||||||
|
# result = cv2.matchTemplate(gray, template, cv2.TM_CCOEFF_NORMED)
|
||||||
|
#
|
||||||
|
# # 阈值筛选
|
||||||
|
# ys, xs = np.where(result >= threshold)
|
||||||
|
# if len(xs) == 0:
|
||||||
|
# return ([], np.empty((0, 4), dtype=int)) if return_boxes else []
|
||||||
|
#
|
||||||
|
# # 收集候选框与分数
|
||||||
|
# boxes = []
|
||||||
|
# scores = []
|
||||||
|
# for (x, y) in zip(xs, ys):
|
||||||
|
# boxes.append([x, y, x + tw, y + th])
|
||||||
|
# scores.append(result[y, x])
|
||||||
|
#
|
||||||
|
# # 按分数做 NMS
|
||||||
|
# boxes_nms = cls.non_max_suppression(boxes, scores=np.array(scores), overlapThresh=overlapThresh)
|
||||||
|
#
|
||||||
|
# # 计算中心点
|
||||||
|
# centers = [((x1 + x2) // 2, (y1 + y2) // 2) for (x1, y1, x2, y2) in boxes_nms]
|
||||||
|
#
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# if return_boxes:
|
||||||
|
# return centers, boxes_nms
|
||||||
|
#
|
||||||
|
#
|
||||||
|
# return centers
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def find_template(
|
||||||
|
cls,
|
||||||
|
template_path: str,
|
||||||
|
large_image: ArrayLikeImage,
|
||||||
|
threshold: float = 0.8,
|
||||||
|
overlapThresh: float = 0.5,
|
||||||
|
return_boxes: bool = False
|
||||||
|
) -> Union[List[Tuple[int, int]], Tuple[List[Tuple[int, int]], np.ndarray]]:
|
||||||
|
"""
|
||||||
|
在 large_image 中查找 template_path 模板的位置。
|
||||||
|
- large_image 可为文件路径、np.ndarray 或 PIL.Image
|
||||||
|
- threshold: 模板匹配阈值(TM_CCOEFF_NORMED)
|
||||||
|
- overlapThresh: NMS 重叠阈值
|
||||||
|
- return_boxes: True 时同时返回保留的框数组 (N,4)
|
||||||
|
|
||||||
|
若检测结果为空,则在相同阈值下最多重试三次(共 3 次尝试)。
|
||||||
|
返回:
|
||||||
|
centers 或 (centers, boxes)
|
||||||
|
centers: [(cx, cy), ...]
|
||||||
|
boxes: [[x1,y1,x2,y2], ...] (np.ndarray, int)
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not os.path.isfile(template_path):
|
||||||
|
print(f"模板文件不存在 → {template_path}")
|
||||||
|
raise FileNotFoundError(f"模板文件不存在 → {template_path}")
|
||||||
|
|
||||||
|
size = os.path.getsize(template_path)
|
||||||
|
if size == 0:
|
||||||
|
print(f"模板文件大小为 0 → {template_path} ")
|
||||||
|
raise ValueError(f"模板文件大小为 0 → {template_path}")
|
||||||
|
# 模板(灰度)
|
||||||
|
template = cv2.imread(template_path, cv2.IMREAD_GRAYSCALE)
|
||||||
|
if template is None:
|
||||||
|
raise FileNotFoundError(f"模板图像加载失败,请检查路径: {template_path}")
|
||||||
|
|
||||||
|
# 大图(灰度)
|
||||||
|
gray = cls._to_gray(large_image)
|
||||||
|
|
||||||
|
# 模板尺寸
|
||||||
|
tw, th = template.shape[::-1]
|
||||||
|
|
||||||
|
# 内部:执行一次匹配并返回 (centers, boxes_nms)
|
||||||
|
def _match_once(cur_threshold: float):
|
||||||
|
# 模板匹配(相关系数归一化)
|
||||||
|
result = cv2.matchTemplate(gray, template, cv2.TM_CCOEFF_NORMED)
|
||||||
|
|
||||||
|
# 阈值筛选
|
||||||
|
ys, xs = np.where(result >= cur_threshold)
|
||||||
|
if len(xs) == 0:
|
||||||
|
return [], np.empty((0, 4), dtype=int)
|
||||||
|
|
||||||
|
# 收集候选框与分数
|
||||||
|
boxes = []
|
||||||
|
scores = []
|
||||||
|
for (x, y) in zip(xs, ys):
|
||||||
|
boxes.append([int(x), int(y), int(x + tw), int(y + th)])
|
||||||
|
scores.append(float(result[y, x]))
|
||||||
|
|
||||||
|
# 按分数做 NMS
|
||||||
|
boxes_nms = cls.non_max_suppression(
|
||||||
|
boxes,
|
||||||
|
scores=np.asarray(scores, dtype=np.float32),
|
||||||
|
overlapThresh=overlapThresh
|
||||||
|
)
|
||||||
|
|
||||||
|
# 计算中心点(转为 Python int)
|
||||||
|
centers = [(int((x1 + x2) // 2), int((y1 + y2) // 2))
|
||||||
|
for (x1, y1, x2, y2) in boxes_nms]
|
||||||
|
|
||||||
|
# 统一为 np.ndarray[int]
|
||||||
|
boxes_nms = np.asarray(boxes_nms, dtype=int)
|
||||||
|
return centers, boxes_nms
|
||||||
|
|
||||||
|
# ===== 重试控制(最多 3 次)=====
|
||||||
|
MAX_RETRIES = 3
|
||||||
|
THRESHOLD_DECAY = 0.0 # 如需越试越宽松,可改为 0.02~0.05;不需要则保持 0
|
||||||
|
MIN_THRESHOLD = 0.6
|
||||||
|
|
||||||
|
cur_threshold = float(threshold)
|
||||||
|
last_centers, last_boxes = [], np.empty((0, 4), dtype=int)
|
||||||
|
|
||||||
|
for attempt in range(MAX_RETRIES):
|
||||||
|
centers, boxes_nms = _match_once(cur_threshold)
|
||||||
|
if centers:
|
||||||
|
if return_boxes:
|
||||||
|
return centers, boxes_nms
|
||||||
|
return centers
|
||||||
|
|
||||||
|
# 记录最后一次(若最终失败按规范返回空)
|
||||||
|
last_centers, last_boxes = centers, boxes_nms
|
||||||
|
|
||||||
|
# 为下一次尝试准备(这里默认不衰减阈值;如需可打开 THRESHOLD_DECAY)
|
||||||
|
if attempt < MAX_RETRIES - 1 and THRESHOLD_DECAY > 0.0:
|
||||||
|
cur_threshold = max(MIN_THRESHOLD, cur_threshold - THRESHOLD_DECAY)
|
||||||
|
|
||||||
|
# 全部尝试失败
|
||||||
|
if return_boxes:
|
||||||
|
return last_centers, last_boxes
|
||||||
|
return last_centers
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
import requests
|
import requests
|
||||||
from Entity.Variables import prologueList
|
from Entity.Variables import prologueList, API_KEY
|
||||||
|
from Utils.IOSAIStorage import IOSAIStorage
|
||||||
|
from Utils.LogManager import LogManager
|
||||||
|
|
||||||
BaseUrl = "https://crawlclient.api.yolozs.com/api/common/"
|
BaseUrl = "https://crawlclient.api.yolozs.com/api/common/"
|
||||||
# BaseUrl = "http://192.168.1.174:8101/api/common/"
|
|
||||||
|
|
||||||
|
|
||||||
class Requester():
|
class Requester():
|
||||||
@@ -11,36 +12,136 @@ class Requester():
|
|||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def requestPrologue(cls, token):
|
def requestPrologue(cls, token):
|
||||||
headers = {
|
try:
|
||||||
"vvtoken": token,
|
headers = {
|
||||||
}
|
"vvtoken": token,
|
||||||
url = BaseUrl + cls.prologue
|
}
|
||||||
result = requests.get(headers=headers, url=url)
|
url = BaseUrl + cls.prologue
|
||||||
json = result.json()
|
result = requests.get(headers=headers, url=url, verify=False)
|
||||||
data = json.get("data")
|
json = result.json()
|
||||||
for i in data:
|
data = json.get("data")
|
||||||
prologueList.append(i)
|
for i in data:
|
||||||
|
prologueList.append(i)
|
||||||
|
except Exception as e:
|
||||||
|
LogManager.method_error(f"获取requestPrologue失败,报错的原因:{e}", "获取requestPrologue异常")
|
||||||
|
|
||||||
|
# 翻译
|
||||||
|
@classmethod
|
||||||
|
def translation(cls, msg, country="英国"):
|
||||||
|
try:
|
||||||
|
if country == "":
|
||||||
|
country = "英国"
|
||||||
|
|
||||||
|
param = {
|
||||||
|
"msg": msg,
|
||||||
|
"country": country,
|
||||||
|
}
|
||||||
|
url = "https://ai.yolozs.com/translation"
|
||||||
|
result = requests.post(url=url, json=param, verify=False)
|
||||||
|
|
||||||
|
LogManager.info(f"翻译 请求的参数:{param}", "翻译")
|
||||||
|
LogManager.info(f"翻译,状态码:{result.status_code},服务器返回的内容:{result.text}", "翻译")
|
||||||
|
|
||||||
|
if result.status_code != 200:
|
||||||
|
LogManager.error(f"翻译失败,状态码:{result.status_code},服务器返回的内容:{result.text}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
json = result.json()
|
||||||
|
data = json.get("data")
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
LogManager.method_error(f"翻译失败,报错的原因:{e}", "翻译失败异常")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# 翻译
|
# 翻译
|
||||||
@classmethod
|
@classmethod
|
||||||
def translation(cla, msg, country="英国"):
|
def translationToChinese(cls, msg):
|
||||||
parame = {
|
try:
|
||||||
"msg":msg,
|
param = {
|
||||||
"country":country,
|
"msg": msg,
|
||||||
}
|
}
|
||||||
url = "http://ai.zhukeping.com/translation"
|
url = "https://ai.yolozs.com/translationToChinese"
|
||||||
result = requests.request(url=url, json=parame, method="POST")
|
result = requests.post(url=url, json=param, verify=False)
|
||||||
json = result.json()
|
|
||||||
data = json.get("data")
|
LogManager.info(f"翻译 请求的参数:{param}", "翻译")
|
||||||
print(data)
|
LogManager.info(f"翻译,状态码:{result.status_code},服务器返回的内容:{result.text}", "翻译")
|
||||||
return data
|
|
||||||
|
if result.status_code != 200:
|
||||||
|
LogManager.error(f"翻译失败,状态码:{result.status_code},服务器返回的内容:{result.text}")
|
||||||
|
return None
|
||||||
|
|
||||||
|
json = result.json()
|
||||||
|
data = json.get("data")
|
||||||
|
return data
|
||||||
|
except Exception as e:
|
||||||
|
LogManager.method_error(f"翻译失败,报错的原因:{e}", "翻译失败异常")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ai聊天
|
# ai聊天
|
||||||
@classmethod
|
@classmethod
|
||||||
def chatToAi(cls, param):
|
def chatToAi(cls, param):
|
||||||
url = "http://ai.zhukeping.com/chat"
|
|
||||||
result = requests.request(url=url, json=param, method="POST")
|
|
||||||
json = result.json()
|
|
||||||
data = json.get("data", {})
|
|
||||||
return data
|
# aiConfig = JsonUtils.read_json("aiConfig")
|
||||||
|
aiConfig = IOSAIStorage.load("aiConfig.json")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
agentName = aiConfig.get("agentName")
|
||||||
|
guildName = aiConfig.get("guildName")
|
||||||
|
contactTool = aiConfig.get("contactTool", "")
|
||||||
|
contact = aiConfig.get("contact", "")
|
||||||
|
|
||||||
|
age = aiConfig.get("age", 20)
|
||||||
|
sex = aiConfig.get("sex", "女")
|
||||||
|
height = aiConfig.get("height", 160)
|
||||||
|
weight = aiConfig.get("weight", 55)
|
||||||
|
body_features = aiConfig.get("body_features", "")
|
||||||
|
nationality = aiConfig.get("nationality", "中国")
|
||||||
|
personality = aiConfig.get("personality", "")
|
||||||
|
strengths = aiConfig.get("strengths", "")
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
"name": agentName,
|
||||||
|
"Trade_union": guildName,
|
||||||
|
"contcat_method": contactTool,
|
||||||
|
"contcat_info": contact,
|
||||||
|
"age": age,
|
||||||
|
"sex": sex,
|
||||||
|
"height": height,
|
||||||
|
"weight": weight,
|
||||||
|
"body_features": body_features,
|
||||||
|
"nationality": nationality,
|
||||||
|
"personality": personality,
|
||||||
|
"strengths": strengths,
|
||||||
|
}
|
||||||
|
|
||||||
|
param["inputs"] = inputs
|
||||||
|
|
||||||
|
try:
|
||||||
|
|
||||||
|
# url = "https://ai.yolozs.com/chat"
|
||||||
|
url = "https://ai.yolozs.com/customchat"
|
||||||
|
|
||||||
|
result = requests.post(url=url, json=param, verify=False)
|
||||||
|
|
||||||
|
LogManager.method_info(f"ai聊天的参数:{param}", "ai聊天")
|
||||||
|
print(f"ai聊天的参数:{param}")
|
||||||
|
|
||||||
|
json = result.json()
|
||||||
|
data = json.get("answer", "")
|
||||||
|
session_id = json.get("conversation_id", "")
|
||||||
|
LogManager.method_info(f"ai聊天返回的内容:{result.json()}", "ai聊天")
|
||||||
|
|
||||||
|
return data, session_id
|
||||||
|
except Exception as e:
|
||||||
|
LogManager.method_error(f"ai聊天失败,ai聊天出现异常,报错的原因:{e}", "ai聊天接口异常")
|
||||||
|
|||||||
24
Utils/SubprocessKit.py
Normal file
24
Utils/SubprocessKit.py
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
import os
|
||||||
|
import subprocess
|
||||||
|
|
||||||
|
__all__ = ['check_output', 'popen', 'PIPE']
|
||||||
|
|
||||||
|
# 模块级单例,导入时只创建一次
|
||||||
|
if os.name == "nt":
|
||||||
|
_si = subprocess.STARTUPINFO()
|
||||||
|
_si.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
|
_si.wShowWindow = subprocess.SW_HIDE
|
||||||
|
else:
|
||||||
|
_si = None
|
||||||
|
|
||||||
|
PIPE = subprocess.PIPE
|
||||||
|
|
||||||
|
def check_output(cmd, **kw):
|
||||||
|
if os.name == "nt":
|
||||||
|
kw.setdefault('startupinfo', _si)
|
||||||
|
return subprocess.check_output(cmd, **kw)
|
||||||
|
|
||||||
|
def popen(*args, **kw):
|
||||||
|
if os.name == "nt":
|
||||||
|
kw.setdefault('startupinfo', _si)
|
||||||
|
return subprocess.Popen(*args, **kw)
|
||||||
327
Utils/TencentOCRUtils.py
Normal file
327
Utils/TencentOCRUtils.py
Normal file
@@ -0,0 +1,327 @@
|
|||||||
|
# -*- coding: utf-8 -*-
|
||||||
|
import base64
|
||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import socket
|
||||||
|
import time
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
from http.client import HTTPSConnection
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
Point = Dict[str, int]
|
||||||
|
ItemPolygon = Dict[str, int]
|
||||||
|
|
||||||
|
|
||||||
|
class TencentOCR:
|
||||||
|
"""腾讯云 OCR 封装,自动从环境变量或配置文件加载密钥"""
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _load_secret() -> Dict[str, str]:
|
||||||
|
# 优先从环境变量读取
|
||||||
|
sid = "AKIDXw86q6D8pJYZOEvOm25wZy96oIZcQ1OX"
|
||||||
|
skey = "ye7MNAj4ub5PVO2TmriLkwtc8QTItGPO"
|
||||||
|
|
||||||
|
# 如果没有,就尝试从 ~/.tencent_ocr.json 加载
|
||||||
|
if not sid or not skey:
|
||||||
|
cfg_path = os.path.expanduser("~/.tencent_ocr.json")
|
||||||
|
if os.path.exists(cfg_path):
|
||||||
|
with open(cfg_path, "r", encoding="utf-8") as f:
|
||||||
|
cfg = json.load(f)
|
||||||
|
sid = sid or cfg.get("secret_id")
|
||||||
|
skey = skey or cfg.get("secret_key")
|
||||||
|
|
||||||
|
if not sid or not skey:
|
||||||
|
raise RuntimeError(
|
||||||
|
"❌ 未找到腾讯云 OCR 密钥,请设置环境变量 TENCENT_SECRET_ID / TENCENT_SECRET_KEY,"
|
||||||
|
"或在用户目录下创建 ~/.tencent_ocr.json(格式:{\"secret_id\":\"...\",\"secret_key\":\"...\"})"
|
||||||
|
)
|
||||||
|
|
||||||
|
return {"secret_id": sid, "secret_key": skey}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _hmac_sha256(key: bytes, msg: str) -> bytes:
|
||||||
|
return hmac.new(key, msg.encode("utf-8"), hashlib.sha256).digest()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _strip_data_uri_prefix(b64: str) -> str:
|
||||||
|
if "," in b64 and b64.strip().lower().startswith("data:"):
|
||||||
|
return b64.split(",", 1)[1]
|
||||||
|
return b64
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _now_ts_and_date():
|
||||||
|
ts = int(time.time())
|
||||||
|
date = datetime.fromtimestamp(ts, tz=timezone.utc).strftime("%Y-%m-%d")
|
||||||
|
return ts, date
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def recognize(
|
||||||
|
*,
|
||||||
|
image_path: Optional[str] = None,
|
||||||
|
image_bytes: Optional[bytes] = None,
|
||||||
|
image_url: Optional[str] = None,
|
||||||
|
region: Optional[str] = None,
|
||||||
|
token: Optional[str] = None,
|
||||||
|
action: str = "GeneralBasicOCR",
|
||||||
|
version: str = "2018-11-19",
|
||||||
|
service: str = "ocr",
|
||||||
|
host: str = "ocr.tencentcloudapi.com",
|
||||||
|
timeout: int = 15,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
调用腾讯云 OCR,三选一:image_path / image_bytes / image_url
|
||||||
|
自动加载密钥(优先环境变量 -> ~/.tencent_ocr.json)
|
||||||
|
"""
|
||||||
|
# 读取密钥
|
||||||
|
sec = TencentOCR._load_secret()
|
||||||
|
secret_id = sec["secret_id"]
|
||||||
|
secret_key = sec["secret_key"]
|
||||||
|
|
||||||
|
assert sum(v is not None for v in (image_path, image_bytes, image_url)) == 1, \
|
||||||
|
"必须且只能提供 image_path / image_bytes / image_url 之一"
|
||||||
|
|
||||||
|
# 1. payload
|
||||||
|
payload: Dict[str, Any] = {}
|
||||||
|
if image_url:
|
||||||
|
payload["ImageUrl"] = image_url
|
||||||
|
else:
|
||||||
|
if image_bytes is None:
|
||||||
|
with open(image_path, "rb") as f:
|
||||||
|
image_bytes = f.read()
|
||||||
|
img_b64 = base64.b64encode(image_bytes).decode("utf-8")
|
||||||
|
img_b64 = TencentOCR._strip_data_uri_prefix(img_b64)
|
||||||
|
payload["ImageBase64"] = img_b64
|
||||||
|
|
||||||
|
payload_str = json.dumps(payload, ensure_ascii=False, separators=(",", ":"))
|
||||||
|
|
||||||
|
# 2. 参数准备
|
||||||
|
algorithm = "TC3-HMAC-SHA256"
|
||||||
|
http_method = "POST"
|
||||||
|
canonical_uri = "/"
|
||||||
|
canonical_querystring = ""
|
||||||
|
content_type = "application/json; charset=utf-8"
|
||||||
|
signed_headers = "content-type;host;x-tc-action"
|
||||||
|
|
||||||
|
timestamp, date = TencentOCR._now_ts_and_date()
|
||||||
|
credential_scope = f"{date}/{service}/tc3_request"
|
||||||
|
|
||||||
|
# 3. 规范请求串
|
||||||
|
canonical_headers = (
|
||||||
|
f"content-type:{content_type}\n"
|
||||||
|
f"host:{host}\n"
|
||||||
|
f"x-tc-action:{action.lower()}\n"
|
||||||
|
)
|
||||||
|
hashed_request_payload = hashlib.sha256(payload_str.encode("utf-8")).hexdigest()
|
||||||
|
canonical_request = (
|
||||||
|
f"{http_method}\n{canonical_uri}\n{canonical_querystring}\n"
|
||||||
|
f"{canonical_headers}\n{signed_headers}\n{hashed_request_payload}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 4. 签名
|
||||||
|
hashed_canonical_request = hashlib.sha256(canonical_request.encode("utf-8")).hexdigest()
|
||||||
|
string_to_sign = (
|
||||||
|
f"{algorithm}\n{timestamp}\n{credential_scope}\n{hashed_canonical_request}"
|
||||||
|
)
|
||||||
|
secret_date = TencentOCR._hmac_sha256(("TC3" + secret_key).encode("utf-8"), date)
|
||||||
|
secret_service = hmac.new(secret_date, service.encode("utf-8"), hashlib.sha256).digest()
|
||||||
|
secret_signing = hmac.new(secret_service, b"tc3_request", hashlib.sha256).digest()
|
||||||
|
signature = hmac.new(secret_signing, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
|
||||||
|
|
||||||
|
authorization = (
|
||||||
|
f"{algorithm} "
|
||||||
|
f"Credential={secret_id}/{credential_scope}, "
|
||||||
|
f"SignedHeaders={signed_headers}, "
|
||||||
|
f"Signature={signature}"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 5. headers
|
||||||
|
headers = {
|
||||||
|
"Authorization": authorization,
|
||||||
|
"Content-Type": content_type,
|
||||||
|
"Host": host,
|
||||||
|
"X-TC-Action": action,
|
||||||
|
"X-TC-Timestamp": str(timestamp),
|
||||||
|
"X-TC-Version": version,
|
||||||
|
}
|
||||||
|
if region:
|
||||||
|
headers["X-TC-Region"] = region
|
||||||
|
if token:
|
||||||
|
headers["X-TC-Token"] = token
|
||||||
|
|
||||||
|
# 6. 发请求
|
||||||
|
try:
|
||||||
|
conn = HTTPSConnection(host, timeout=timeout)
|
||||||
|
conn.request("POST", "/", body=payload_str.encode("utf-8"), headers=headers)
|
||||||
|
resp = conn.getresponse()
|
||||||
|
raw = resp.read().decode("utf-8", errors="replace")
|
||||||
|
try:
|
||||||
|
data = json.loads(raw)
|
||||||
|
except Exception:
|
||||||
|
data = {"NonJSONBody": raw}
|
||||||
|
return {
|
||||||
|
"http_status": resp.status,
|
||||||
|
"http_reason": resp.reason,
|
||||||
|
"headers": dict(resp.getheaders()),
|
||||||
|
"body": data,
|
||||||
|
}
|
||||||
|
except socket.gaierror as e:
|
||||||
|
return {"error": "DNS_RESOLUTION_FAILED", "detail": str(e)}
|
||||||
|
except socket.timeout:
|
||||||
|
return {"error": "NETWORK_TIMEOUT", "detail": f"Timeout after {timeout}s"}
|
||||||
|
except Exception as e:
|
||||||
|
return {"error": "REQUEST_FAILED", "detail": str(e)}
|
||||||
|
finally:
|
||||||
|
try:
|
||||||
|
conn.close()
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _norm(s: str) -> str:
|
||||||
|
return (s or "").strip().lstrip("@").lower()
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _rect_from_polygon(poly: List[Point]) -> Optional[ItemPolygon]:
|
||||||
|
if not poly:
|
||||||
|
return None
|
||||||
|
xs = [p["X"] for p in poly]
|
||||||
|
ys = [p["Y"] for p in poly]
|
||||||
|
return {"X": min(xs), "Y": min(ys), "Width": max(xs) - min(xs), "Height": max(ys) - min(ys)}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def find_last_name_bbox(cls, ocr: Dict[str, Any], name: str) -> Optional[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
从 OCR JSON 中找到指定名字的“最后一次”出现并返回坐标信息。
|
||||||
|
:param ocr: 完整 OCR JSON(含 Response.TextDetections)
|
||||||
|
:param name: 前端传入的名字,比如 'lee39160'
|
||||||
|
:return: dict 或 None,例如:
|
||||||
|
{
|
||||||
|
"index": 21,
|
||||||
|
"text": "lee39160",
|
||||||
|
"item": {"X": 248, "Y": 1701, "Width": 214, "Height": 49},
|
||||||
|
"polygon": [...],
|
||||||
|
"center": {"x": 355.0, "y": 1725.5}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
dets = (ocr.get("body") or ocr).get("Response", {}).get("TextDetections", [])
|
||||||
|
if not dets or not name:
|
||||||
|
return None
|
||||||
|
|
||||||
|
target = cls._norm(name)
|
||||||
|
found = -1
|
||||||
|
|
||||||
|
# 从后往前找最后一个严格匹配
|
||||||
|
for i in range(len(dets) - 1, -1, -1):
|
||||||
|
txt = cls._norm(dets[i].get("DetectedText", ""))
|
||||||
|
if txt == target:
|
||||||
|
found = i
|
||||||
|
break
|
||||||
|
|
||||||
|
# 兜底:再匹配原始文本(可能带 @)
|
||||||
|
if found == -1:
|
||||||
|
for i in range(len(dets) - 1, -1, -1):
|
||||||
|
raw = (dets[i].get("DetectedText") or "").strip().lower()
|
||||||
|
if raw.lstrip("@") == target:
|
||||||
|
found = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if found == -1:
|
||||||
|
return None
|
||||||
|
|
||||||
|
det = dets[found]
|
||||||
|
item: Optional[ItemPolygon] = det.get("ItemPolygon")
|
||||||
|
poly: List[Point] = det.get("Polygon") or []
|
||||||
|
|
||||||
|
# 没有 ItemPolygon 就从 Polygon 算
|
||||||
|
if not item:
|
||||||
|
item = cls._rect_from_polygon(poly)
|
||||||
|
if not item:
|
||||||
|
return None
|
||||||
|
|
||||||
|
center = {"x": item["X"] + item["Width"] / 2.0, "y": item["Y"] + item["Height"] / 2.0}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"index": found,
|
||||||
|
"text": det.get("DetectedText", ""),
|
||||||
|
"item": item,
|
||||||
|
"polygon": poly,
|
||||||
|
"center": center,
|
||||||
|
}
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _get_detections(ocr: Dict[str, Any]) -> List[Dict[str, Any]]:
|
||||||
|
"""兼容含 body 层的 OCR 结构,提取 TextDetections 列表"""
|
||||||
|
return (ocr.get("body") or ocr).get("Response", {}).get("TextDetections", []) or []
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def _norm_txt(s: str) -> str:
|
||||||
|
"""清洗文本:去空格"""
|
||||||
|
return (s or "").strip()
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def slice_texts_between(
|
||||||
|
cls,
|
||||||
|
ocr: Dict[str, Any],
|
||||||
|
start_keyword: str = "切换账号",
|
||||||
|
end_keyword: str = "添加账号",
|
||||||
|
*,
|
||||||
|
username_like: bool = False, # True 时只保留像用户名的文本
|
||||||
|
min_conf: int = 0 # 置信度下限
|
||||||
|
) -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
返回位于 start_keyword 与 end_keyword 之间的所有文本项(不含两端),
|
||||||
|
每项保留原始 DetectedText、Confidence、ItemPolygon 等信息。
|
||||||
|
"""
|
||||||
|
dets = cls._get_detections(ocr)
|
||||||
|
if not dets:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 找“切换账号”最后一次出现的下标
|
||||||
|
start_idx = -1
|
||||||
|
for i, d in enumerate(dets):
|
||||||
|
txt = cls._norm_txt(d.get("DetectedText", ""))
|
||||||
|
if txt == start_keyword:
|
||||||
|
start_idx = i
|
||||||
|
|
||||||
|
# 找“添加账号”第一次出现的下标
|
||||||
|
end_idx = -1
|
||||||
|
for i, d in enumerate(dets):
|
||||||
|
txt = cls._norm_txt(d.get("DetectedText", ""))
|
||||||
|
if txt == end_keyword:
|
||||||
|
end_idx = i
|
||||||
|
break
|
||||||
|
|
||||||
|
if start_idx == -1 or end_idx == -1 or end_idx <= start_idx:
|
||||||
|
return []
|
||||||
|
|
||||||
|
# 提取两者之间的内容
|
||||||
|
mid = []
|
||||||
|
for d in dets[start_idx + 1:end_idx]:
|
||||||
|
if int(d.get("Confidence", 0)) < min_conf:
|
||||||
|
continue
|
||||||
|
txt = cls._norm_txt(d.get("DetectedText", ""))
|
||||||
|
if not txt:
|
||||||
|
continue
|
||||||
|
mid.append(d)
|
||||||
|
|
||||||
|
if not username_like:
|
||||||
|
return mid
|
||||||
|
|
||||||
|
# 只保留像用户名的文本
|
||||||
|
pat = re.compile(r"^[A-Za-z0-9_.-]{3,}$")
|
||||||
|
filtered = [d for d in mid if pat.match(cls._norm_txt(d.get("DetectedText", "")))]
|
||||||
|
return filtered
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
result = TencentOCR.recognize(
|
||||||
|
image_path=r"C:\Users\zhangkai\Desktop\last-item\iosai\test.png",
|
||||||
|
action="GeneralAccurateOCR",
|
||||||
|
)
|
||||||
|
print(json.dumps(result, ensure_ascii=False, indent=2))
|
||||||
|
|
||||||
@@ -1,33 +1,226 @@
|
|||||||
from threading import Thread, Event
|
# -*- coding: utf-8 -*-
|
||||||
|
|
||||||
|
import threading
|
||||||
|
import ctypes
|
||||||
|
import inspect
|
||||||
|
import time
|
||||||
|
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||||
|
from typing import Dict, Optional, List, Tuple, Any
|
||||||
|
|
||||||
from Utils.LogManager import LogManager
|
from Utils.LogManager import LogManager
|
||||||
from script.ScriptManager import ScriptManager
|
|
||||||
|
|
||||||
|
|
||||||
class ThreadManager():
|
def _raise_async_exception(tid: int, exc_type) -> int:
|
||||||
threads = {}
|
if not inspect.isclass(exc_type):
|
||||||
|
raise TypeError("exc_type must be a class")
|
||||||
|
return ctypes.pythonapi.PyThreadState_SetAsyncExc(
|
||||||
|
ctypes.c_long(tid), ctypes.py_object(exc_type)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _kill_thread_by_tid(tid: Optional[int]) -> bool:
|
||||||
|
if tid is None:
|
||||||
|
return False
|
||||||
|
res = _raise_async_exception(tid, SystemExit)
|
||||||
|
if res == 0:
|
||||||
|
return False
|
||||||
|
if res > 1:
|
||||||
|
_raise_async_exception(tid, None)
|
||||||
|
raise SystemError("PyThreadState_SetAsyncExc affected multiple threads; reverted.")
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
class ThreadManager:
|
||||||
|
"""
|
||||||
|
- add(udid, thread_or_target, *args, **kwargs) -> (code, msg)
|
||||||
|
- stop(udid, join_timeout=2.0, retries=5, wait_step=0.2) -> (code, msg) # 强杀
|
||||||
|
- batch_stop(udids, join_timeout_each=2.0, retries_each=5, wait_step_each=0.2) -> (code, msg)
|
||||||
|
- get_thread / get_tid / is_running / list_udids
|
||||||
|
"""
|
||||||
|
_threads: Dict[str, threading.Thread] = {}
|
||||||
|
_lock = threading.RLock()
|
||||||
|
|
||||||
|
# ========== 基础 ==========
|
||||||
@classmethod
|
@classmethod
|
||||||
def add(cls, udid, t: Thread, stopEvent: Event):
|
def add(cls, udid: str, thread_or_target: Any, *args, **kwargs) -> Tuple[int, str]:
|
||||||
if udid in cls.threads:
|
"""
|
||||||
print("▲ 线程已存在")
|
兼容两种用法:
|
||||||
return
|
1) add(udid, t) # t 是 threading.Thread 实例
|
||||||
cls.threads[udid] = {"thread": t, "stopEvent": stopEvent}
|
2) add(udid, target, *args, **kwargs) # target 是可调用
|
||||||
|
返回:(200, "创建任务成功") / (1001, "任务已存在") / (1001, "创建任务失败")
|
||||||
|
"""
|
||||||
|
with cls._lock:
|
||||||
|
exist = cls._threads.get(udid)
|
||||||
|
if exist and exist.is_alive():
|
||||||
|
return 1001, "任务已存在"
|
||||||
|
|
||||||
|
if isinstance(thread_or_target, threading.Thread):
|
||||||
|
t = thread_or_target
|
||||||
|
try:
|
||||||
|
t.daemon = True
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
if not t.name:
|
||||||
|
t.name = f"task-{udid}"
|
||||||
|
|
||||||
|
# 包装 run,退出时从表移除
|
||||||
|
orig_run = t.run
|
||||||
|
def run_wrapper():
|
||||||
|
try:
|
||||||
|
orig_run()
|
||||||
|
finally:
|
||||||
|
with cls._lock:
|
||||||
|
if cls._threads.get(udid) is t:
|
||||||
|
cls._threads.pop(udid, None)
|
||||||
|
t.run = run_wrapper # type: ignore
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def stop(cls, udid):
|
|
||||||
try:
|
|
||||||
info = cls.threads[udid]
|
|
||||||
if info:
|
|
||||||
info["stopEvent"].set() # 停止线程
|
|
||||||
info["thread"].join(timeout=3) # 等待线程退出
|
|
||||||
del cls.threads[udid]
|
|
||||||
LogManager.info("停止线程成功", udid)
|
|
||||||
return 200, "停止线程成功 " + udid
|
|
||||||
else:
|
else:
|
||||||
LogManager.info("无此线程,无需关闭", udid)
|
target = thread_or_target
|
||||||
return 1001, "无此线程,无需关闭 " + udid
|
def _wrapper():
|
||||||
except KeyError as e:
|
try:
|
||||||
LogManager.info("无此线程,无需关闭", udid)
|
target(*args, **kwargs)
|
||||||
return 1001, "停止脚本失败 " + udid
|
finally:
|
||||||
|
with cls._lock:
|
||||||
|
cur = cls._threads.get(udid)
|
||||||
|
if cur is threading.current_thread():
|
||||||
|
cls._threads.pop(udid, None)
|
||||||
|
t = threading.Thread(target=_wrapper, daemon=True, name=f"task-{udid}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
t.start()
|
||||||
|
except Exception:
|
||||||
|
return 1001, "创建任务失败"
|
||||||
|
|
||||||
|
cls._threads[udid] = t
|
||||||
|
# 保留你原有的创建成功日志
|
||||||
|
try:
|
||||||
|
LogManager.method_info(f"创建任务成功 [{udid}],线程ID={t.ident}", "task")
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
return 200, "创建任务成功"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_thread(cls, udid: str) -> Optional[threading.Thread]:
|
||||||
|
with cls._lock:
|
||||||
|
return cls._threads.get(udid)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def get_tid(cls, udid: str) -> Optional[int]:
|
||||||
|
t = cls.get_thread(udid)
|
||||||
|
return t.ident if t else None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def is_running(cls, udid: str) -> bool:
|
||||||
|
t = cls.get_thread(udid)
|
||||||
|
return bool(t and t.is_alive())
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def list_udids(cls) -> List[str]:
|
||||||
|
with cls._lock:
|
||||||
|
return list(cls._threads.keys())
|
||||||
|
|
||||||
|
# ========== 内部:强杀一次 ==========
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def _stop_once(cls, udid: str, join_timeout: float, retries: int, wait_step: float) -> bool:
|
||||||
|
"""
|
||||||
|
对指定 udid 执行一次强杀流程;返回 True=已停止/不存在,False=仍存活或被拒。
|
||||||
|
"""
|
||||||
|
with cls._lock:
|
||||||
|
t = cls._threads.get(udid)
|
||||||
|
|
||||||
|
if not t:
|
||||||
|
return True # 视为已停止
|
||||||
|
|
||||||
|
main_tid = threading.main_thread().ident
|
||||||
|
cur_tid = threading.get_ident()
|
||||||
|
if t.ident in (main_tid, cur_tid):
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
_kill_thread_by_tid(t.ident)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
|
if join_timeout < 0:
|
||||||
|
join_timeout = 0.0
|
||||||
|
t.join(join_timeout)
|
||||||
|
|
||||||
|
while t.is_alive() and retries > 0:
|
||||||
|
evt = threading.Event()
|
||||||
|
evt.wait(wait_step)
|
||||||
|
retries -= 1
|
||||||
|
|
||||||
|
dead = not t.is_alive()
|
||||||
|
if dead:
|
||||||
|
with cls._lock:
|
||||||
|
if cls._threads.get(udid) is t:
|
||||||
|
cls._threads.pop(udid, None)
|
||||||
|
return dead
|
||||||
|
|
||||||
|
# ========== 对外:stop / batch_stop(均返回二元组) ==========
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def stop(cls, udid: str, join_timeout: float = 2.0,
|
||||||
|
retries: int = 5, wait_step: float = 0.2) -> Tuple[int, str]:
|
||||||
|
"""
|
||||||
|
强杀单个:返回 (200, "stopped") 或 (1001, "failed")
|
||||||
|
"""
|
||||||
|
ok = cls._stop_once(udid, join_timeout, retries, wait_step)
|
||||||
|
if ok:
|
||||||
|
return 200, "stopped"
|
||||||
|
else:
|
||||||
|
return 1001, "failed"
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def batch_stop(cls, udids: List[str]) -> Tuple[int, str, List[str]]:
|
||||||
|
"""
|
||||||
|
并行批量停止(简化版):
|
||||||
|
- 只接收 udids 参数
|
||||||
|
- 其他参数写死:join_timeout=2.0, retries=5, wait_step=0.2
|
||||||
|
- 所有设备同时执行,失败的重试 3 轮,每轮间隔 1 秒
|
||||||
|
- 返回:
|
||||||
|
(200, "停止任务成功", [])
|
||||||
|
(1001, "停止任务失败", [失败udid...])
|
||||||
|
"""
|
||||||
|
if not udids:
|
||||||
|
return 200, "停止任务成功", []
|
||||||
|
|
||||||
|
join_timeout = 2.0
|
||||||
|
retries = 5
|
||||||
|
wait_step = 0.2
|
||||||
|
retry_rounds = 3
|
||||||
|
round_interval = 1.0
|
||||||
|
|
||||||
|
def _stop_one(u: str) -> Tuple[str, bool]:
|
||||||
|
ok = cls._stop_once(u, join_timeout, retries, wait_step)
|
||||||
|
return u, ok
|
||||||
|
|
||||||
|
# === 第一轮:并行执行所有设备 ===
|
||||||
|
fail: List[str] = []
|
||||||
|
with ThreadPoolExecutor(max_workers=len(udids)) as pool:
|
||||||
|
futures = [pool.submit(_stop_one, u) for u in udids]
|
||||||
|
for f in as_completed(futures):
|
||||||
|
u, ok = f.result()
|
||||||
|
if not ok:
|
||||||
|
fail.append(u)
|
||||||
|
|
||||||
|
# === 对失败的设备重试 3 轮(每轮间隔 1 秒) ===
|
||||||
|
for _ in range(retry_rounds):
|
||||||
|
if not fail:
|
||||||
|
break
|
||||||
|
time.sleep(round_interval)
|
||||||
|
remain: List[str] = []
|
||||||
|
with ThreadPoolExecutor(max_workers=len(fail)) as pool:
|
||||||
|
futures = [pool.submit(_stop_one, u) for u in fail]
|
||||||
|
for f in as_completed(futures):
|
||||||
|
u, ok = f.result()
|
||||||
|
if not ok:
|
||||||
|
remain.append(u)
|
||||||
|
fail = remain
|
||||||
|
|
||||||
|
# === 返回结果 ===
|
||||||
|
if not fail:
|
||||||
|
return 200, "停止任务成功", []
|
||||||
|
else:
|
||||||
|
return 1001, "停止任务失败", fail
|
||||||
@@ -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 ^
|
|
||||||
-c ^
|
|
||||||
--add-data="C:\Users\milk\AppData\Local\Programs\Python\Python312\Lib\site-packages\tidevice;tidevice" ^
|
|
||||||
tidevice_entry.py
|
|
||||||
31
build.bat
31
build.bat
@@ -1,31 +0,0 @@
|
|||||||
@echo off
|
|
||||||
python -m nuitka Module\Main.py ^
|
|
||||||
--standalone ^
|
|
||||||
--msvc=latest ^
|
|
||||||
--windows-console-mode=disable ^
|
|
||||||
--remove-output ^
|
|
||||||
--output-dir=out ^
|
|
||||||
--output-filename=IOSAI ^
|
|
||||||
--include-package=Module,Utils,Entity,script,tidevice ^
|
|
||||||
--include-module=tidevice.__main__ ^
|
|
||||||
--include-module=tidevice._proto ^
|
|
||||||
--include-module=tidevice._instruments ^
|
|
||||||
--include-module=tidevice._usbmux ^
|
|
||||||
--include-module=tidevice._wdaproxy ^
|
|
||||||
--include-module=flask ^
|
|
||||||
--include-module=flask_cors ^
|
|
||||||
--include-module=jinja2 ^
|
|
||||||
--include-module=werkzeug ^
|
|
||||||
--include-module=cv2 ^
|
|
||||||
--include-module=numpy ^
|
|
||||||
--include-module=lxml ^
|
|
||||||
--include-module=lxml.etree ^
|
|
||||||
--include-module=requests ^
|
|
||||||
--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=resources/icon.ico
|
|
||||||
pause
|
|
||||||
BIN
resources/comment2.png
Normal file
BIN
resources/comment2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
BIN
resources/insert_comment.png
Normal file
BIN
resources/insert_comment.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.2 KiB |
BIN
resources/insert_comment2.png
Normal file
BIN
resources/insert_comment2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 10 KiB |
BIN
resources/insert_comment2x.png
Normal file
BIN
resources/insert_comment2x.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.5 KiB |
BIN
resources/ios.exe
Normal file
BIN
resources/ios.exe
Normal file
Binary file not shown.
Binary file not shown.
BIN
resources/like1.png
Normal file
BIN
resources/like1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 1.2 KiB |
21
resources/server.crt
Normal file
21
resources/server.crt
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
-----BEGIN CERTIFICATE-----
|
||||||
|
MIIDaTCCAlGgAwIBAgIUAQMZUx/qRQIv49P6EIWt+mjYlKYwDQYJKoZIhvcNAQEL
|
||||||
|
BQAwXTELMAkGA1UEBhMCQ04xDjAMBgNVBAgMBUxvY2FsMQ4wDAYDVQQHDAVMb2Nh
|
||||||
|
bDEMMAoGA1UECgwDRGV2MQwwCgYDVQQLDANEZXYxEjAQBgNVBAMMCWxvY2FsaG9z
|
||||||
|
dDAeFw0yNTExMTgwNjMyMjlaFw0zNTExMTYwNjMyMjlaMF0xCzAJBgNVBAYTAkNO
|
||||||
|
MQ4wDAYDVQQIDAVMb2NhbDEOMAwGA1UEBwwFTG9jYWwxDDAKBgNVBAoMA0RldjEM
|
||||||
|
MAoGA1UECwwDRGV2MRIwEAYDVQQDDAlsb2NhbGhvc3QwggEiMA0GCSqGSIb3DQEB
|
||||||
|
AQUAA4IBDwAwggEKAoIBAQDxY0znIdwlVW+EVcQww7Xg/16iN3JpX0svpEOIbo1u
|
||||||
|
40S3bRn057cS//1AG+c+55iisfOaoUX59ZUQhihcAje7IfTKO1/dCzDy4d/PT2hl
|
||||||
|
UOO9Zo8GQzflFM0U6fIi2Ifly09JTGJEyxr+SrJHcJPENualiR6zwNLlqupE9bDP
|
||||||
|
40ydznYWZRvw3N0QmrkOg+eY6FwaYtWspvf/KiJWucscc31zGyA0MhF552k6sIVg
|
||||||
|
9Vskr9Bd3g52Umv/1yPZmESkuM905ImCwSCK0VPAY+rooUeTYw3ktE7q/iy5+l71
|
||||||
|
s6hN9YHVo3m4pIJz4G0YT039TnjtZxxHt8IIVSv6Ymr3AgMBAAGjITAfMB0GA1Ud
|
||||||
|
DgQWBBQ6Q3V0wyXXyNm3jvmSKc2KoAMvAzANBgkqhkiG9w0BAQsFAAOCAQEAacsO
|
||||||
|
ja4qpX/vWUTelhdvzg5alD5WDrP8iSIXmGF+HSHgJbbjxbDm4vlMZjzwh8iqODQR
|
||||||
|
yJ9iuRiFFXGCktEqFx2NTCIUBmyoBg/LFeLtOn0Ncqs11ypoSoRxqE0IaeDjirBH
|
||||||
|
hNUIXzJ+3pOqgyHU+3WqgEzEjW63pNmjX1esVZqA0SQJejsv4hJOvBzGoFFgSRcC
|
||||||
|
Zp7NrusZ8IDkdLbUgD9pgZHPI8YNH/MVocV3wd45o9Y3nkMPhIqkp/1GOWIdN6qj
|
||||||
|
co66o0hYsJduQC9fEBceWpRNWUirEKd231SeaW9vZMPMrfmOZanDY6pXdvhsZR3L
|
||||||
|
9ZGsJk6ktoTagz2AYw==
|
||||||
|
-----END CERTIFICATE-----
|
||||||
28
resources/server.key
Normal file
28
resources/server.key
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
-----BEGIN PRIVATE KEY-----
|
||||||
|
MIIEuwIBADANBgkqhkiG9w0BAQEFAASCBKUwggShAgEAAoIBAQDxY0znIdwlVW+E
|
||||||
|
VcQww7Xg/16iN3JpX0svpEOIbo1u40S3bRn057cS//1AG+c+55iisfOaoUX59ZUQ
|
||||||
|
hihcAje7IfTKO1/dCzDy4d/PT2hlUOO9Zo8GQzflFM0U6fIi2Ifly09JTGJEyxr+
|
||||||
|
SrJHcJPENualiR6zwNLlqupE9bDP40ydznYWZRvw3N0QmrkOg+eY6FwaYtWspvf/
|
||||||
|
KiJWucscc31zGyA0MhF552k6sIVg9Vskr9Bd3g52Umv/1yPZmESkuM905ImCwSCK
|
||||||
|
0VPAY+rooUeTYw3ktE7q/iy5+l71s6hN9YHVo3m4pIJz4G0YT039TnjtZxxHt8II
|
||||||
|
VSv6Ymr3AgMBAAECgf9Wf6myg+iV5TRN77TAGbAP6udHacgYn+7Ad5eQC+ZlobuU
|
||||||
|
Y3Tnh325tXqvPRFSGFwiAlMQVDECt1u/PDm6vJ077t/UHQ5zjr3sYPU1oONuptJ7
|
||||||
|
Jo0WsoFKLkH/oMeBi7h643+Oo9/GH04/nTdnMS9kqL1lYy/aUOVGW5JXIZUWhwMc
|
||||||
|
gTEXuyuaajqdRbQex6vSQANpauYMewj9jlU9RVfjB6LGUHLDJwWbi61dTOYakxkn
|
||||||
|
ssfzEDPMFuGQ8qjG5Li6ceUgNHlmY73WZBXNHKt6FcwNqldy5FB4PiIxoWx0ZHmq
|
||||||
|
z/i89BQlurp20RyFD8f+iMTIbU/z0gHM86JkziECgYEA+qO5KVSg0XQlQmsyXT7Z
|
||||||
|
nKI+DakGxn4ipJWuUDLdi0QoymNIF3diUi1o9P4Q8QzfNFCTVdUsQft24ekXAkJX
|
||||||
|
QtGL61BXBJX5vZvihbBuCEGatE+31LRQZOxWLaeWE6y8XbFfez22SRpa+IrTma+h
|
||||||
|
BxBRyKSqO03xyojKTPPMbDkCgYEA9ozsY02MUJKDXxtE2BRgwGQumoHi7HPRUISk
|
||||||
|
ij0MWw/AYdYoCsrALP4IKB65tK0M7lJsK+g5M2Kyh8XD/Z4hfT1oNnJW8SOtPBwd
|
||||||
|
heuPUzjnhIhnnxoLFeKMo6bGNqVkIYAOKbcX9s2J7U9NwUlYxMxmjNrV2/Nb2MFE
|
||||||
|
Raun8K8CgYBbX1ydaLDIKyN6N0JBJCyJIcylhj1mF43hmn/V1PVXVB3ayp75jxhV
|
||||||
|
BSECT601c4/brpRH8lMUKuyIJ0WwGSdewK3Vt5BBp7tIGJBYVJ2IfQI9QeKutJ2q
|
||||||
|
bU5tjm7z9UEmlwdMEo9lzyni+hlyKcj2nkhycTVuMVg4ke3OaALaYQKBgDD3XBt8
|
||||||
|
01lNP/ormEiyA2UygG7/TOpZNkEflu49oa6UOkk0F0/NZM2KxmPxdkCD/gV3KTSv
|
||||||
|
Mm0aNQryJDLCrTQKdiAaJVpPE6DUlKh8WELXEmQoEyxuJ7V5ASWfgc5omrJslGOE
|
||||||
|
kaXavIH9NhwlTRQI5HUlIURF2P/7omuT5A7RAoGBAJhtThrzxExrzmf4bTIXe+EQ
|
||||||
|
M5lAPqIn46ThlujcNCLroxWz32Ekzs1ywn2NKwK6F1gMJEsFXqQDXtumJYTJ94tY
|
||||||
|
L1fv0knHgRnURsq6xZ4IQgq3YzV0OXQwN4PBzqcRR6sMgFAXfCzXMy/eE7iz8eZT
|
||||||
|
FsrkWh/9tgVvITaUrqr5
|
||||||
|
-----END PRIVATE KEY-----
|
||||||
BIN
resources/wintun.dll
Normal file
BIN
resources/wintun.dll
Normal file
Binary file not shown.
File diff suppressed because it is too large
Load Diff
@@ -1,38 +0,0 @@
|
|||||||
# windows_run.py(替换你起 iproxy 的那几行)
|
|
||||||
import os, subprocess, time, requests, wda
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
UDID = "00008110-00067D0014D3B01E"
|
|
||||||
MAC = "http://192.168.1.219:8765"
|
|
||||||
|
|
||||||
# 让 Mac 起 WDA(不转发)
|
|
||||||
requests.post(f"{MAC}/startWDA", json={"udid": UDID}, timeout=600).raise_for_status()
|
|
||||||
|
|
||||||
# 计算 iproxy 绝对路径(项目根/resources/iproxy/iproxy.exe)
|
|
||||||
BASE = Path(__file__).resolve().parents[1] # iOSAI/
|
|
||||||
IPROXY = BASE / "resources" / "iproxy" / "iproxy.exe"
|
|
||||||
if not IPROXY.exists():
|
|
||||||
raise FileNotFoundError(f"iproxy 不在这里: {IPROXY}")
|
|
||||||
|
|
||||||
# 可选:把 iproxy 目录加入 PATH,避免 DLL 依赖找不到
|
|
||||||
env = os.environ.copy()
|
|
||||||
env["PATH"] = str(IPROXY.parent) + os.pathsep + env.get("PATH", "")
|
|
||||||
try:
|
|
||||||
os.add_dll_directory(str(IPROXY.parent)) # 仅 Windows 有效
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 起 iproxy:本地 9111 -> 设备 8100
|
|
||||||
p = subprocess.Popen([str(IPROXY), "-u", UDID, "9111", "8100"],
|
|
||||||
cwd=str(IPROXY.parent),
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
||||||
text=True, creationflags=0x08000000)
|
|
||||||
|
|
||||||
# 探活 WDA
|
|
||||||
c = wda.Client("http://127.0.0.1:9111")
|
|
||||||
|
|
||||||
for _ in range(120):
|
|
||||||
try:
|
|
||||||
print(c.status()); break
|
|
||||||
except:
|
|
||||||
time.sleep(1)
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
from pathlib import Path
|
|
||||||
import os, subprocess, time, requests, wda
|
|
||||||
|
|
||||||
UDID = "00008110-00067D0014D3B01E"
|
|
||||||
MAC = "http://192.168.1.90:8765"
|
|
||||||
|
|
||||||
# 让 Mac 起 WDA
|
|
||||||
requests.post(f"{MAC}/startWDA", json={"udid": UDID}, timeout=600).raise_for_status()
|
|
||||||
|
|
||||||
# 计算 iproxy 绝对路径:项目根/resources/iproxy/iproxy.exe
|
|
||||||
BASE = Path(__file__).resolve().parents[1] # iOSAI/
|
|
||||||
IPROXY = BASE / "resources" / "iproxy" / "iproxy.exe"
|
|
||||||
if not IPROXY.exists():
|
|
||||||
raise FileNotFoundError(f"iproxy 不在这里:{IPROXY}")
|
|
||||||
|
|
||||||
# 避免 DLL 找不到:把目录加入 PATH(以及 Windows 的 DLL 搜索路径)
|
|
||||||
env = os.environ.copy()
|
|
||||||
env["PATH"] = str(IPROXY.parent) + os.pathsep + env.get("PATH", "")
|
|
||||||
try:
|
|
||||||
os.add_dll_directory(str(IPROXY.parent))
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# 起 iproxy:本地 9111 -> 设备 8100
|
|
||||||
p = subprocess.Popen([str(IPROXY), "-u", UDID, "9111", "8100"],
|
|
||||||
cwd=str(IPROXY.parent),
|
|
||||||
stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True)
|
|
||||||
|
|
||||||
# 探活 WDA
|
|
||||||
c = wda.Client("http://127.0.0.1:9111")
|
|
||||||
for _ in range(120):
|
|
||||||
try:
|
|
||||||
print(c.status()); break
|
|
||||||
except Exception:
|
|
||||||
time.sleep(1)
|
|
||||||
@@ -1,4 +1,37 @@
|
|||||||
# tidevice_entry.py
|
# import sys, traceback, os
|
||||||
|
# from tidevice.__main__ import main
|
||||||
|
#
|
||||||
|
# import sys, os
|
||||||
|
# with open(os.path.join(os.path.dirname(sys.executable), '_entry_log.txt'), 'w') as f:
|
||||||
|
# f.write('entry reached\nargs=%r\n' % sys.argv)
|
||||||
|
#
|
||||||
|
# if hasattr(sys, 'frozen') and sys.executable.endswith('.exe'):
|
||||||
|
# # 打包后且无控制台时,把标准流扔掉
|
||||||
|
# sys.stdout = sys.stderr = open(os.devnull, 'w', encoding='utf-8')
|
||||||
|
#
|
||||||
|
# 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
|
||||||
|
# sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
import sys, traceback, os
|
||||||
from tidevice.__main__ import main
|
from tidevice.__main__ import main
|
||||||
if __name__ == '__main__':
|
|
||||||
main()
|
if __name__ == "__main__":
|
||||||
|
try:
|
||||||
|
main()
|
||||||
|
except SystemExit as se: # 允许正常 exit(code) 继续生效
|
||||||
|
raise
|
||||||
|
except Exception: # 真正异常时才写日志
|
||||||
|
crash_log = os.path.expanduser("~/tidevice_crash.log")
|
||||||
|
with open(crash_log, "a", encoding="utf-8") as f:
|
||||||
|
f.write("----- tidevice exe crash -----\n")
|
||||||
|
traceback.print_exc(file=f)
|
||||||
|
# 如果想让用户知道崩溃了,可以返回非 0
|
||||||
|
sys.exit(1)
|
||||||
Reference in New Issue
Block a user