20250904-初步功能已完成

This commit is contained in:
2025-09-18 21:45:31 +08:00
38 changed files with 186 additions and 299 deletions

2
.idea/iOSAI.iml generated
View File

@@ -4,7 +4,7 @@
<content url="file://$MODULE_DIR$"> <content url="file://$MODULE_DIR$">
<excludeFolder url="file://$MODULE_DIR$/.venv" /> <excludeFolder url="file://$MODULE_DIR$/.venv" />
</content> </content>
<orderEntry type="jdk" jdkName="Python 3.12 (IOS-AI)" jdkType="Python SDK" /> <orderEntry type="jdk" jdkName="Python 3.12" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" /> <orderEntry type="sourceFolder" forTests="false" />
</component> </component>
</module> </module>

2
.idea/misc.xml generated
View File

@@ -3,5 +3,5 @@
<component name="Black"> <component name="Black">
<option name="sdkName" value="Python 3.12" /> <option name="sdkName" value="Python 3.12" />
</component> </component>
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12 (IOS-AI)" project-jdk-type="Python SDK" /> <component name="ProjectRootManager" version="2" project-jdk-name="Python 3.12" project-jdk-type="Python SDK" />
</project> </project>

156
.idea/workspace.xml generated
View File

@@ -4,7 +4,10 @@
<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="20250918-新增主播库功能" /> <list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="20250904-初步功能已完成">
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Module/DeviceInfo.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/DeviceInfo.py" afterDir="false" />
</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" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" /> <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
@@ -34,9 +37,6 @@
<component name="HighlightingSettingsPerFile"> <component name="HighlightingSettingsPerFile">
<setting file="file://$PROJECT_DIR$/build.bat" root0="SKIP_INSPECTION" /> <setting file="file://$PROJECT_DIR$/build.bat" root0="SKIP_INSPECTION" />
</component> </component>
<component name="PerforceDirect.Settings">
<option name="CHARSET" value="无" />
</component>
<component name="ProjectColorInfo">{ <component name="ProjectColorInfo">{
&quot;customColor&quot;: &quot;&quot;, &quot;customColor&quot;: &quot;&quot;,
&quot;associatedIndex&quot;: 5 &quot;associatedIndex&quot;: 5
@@ -49,50 +49,44 @@
<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": { &quot;keyToString&quot;: {
"ASKED_ADD_EXTERNAL_FILES": "true", &quot;ASKED_ADD_EXTERNAL_FILES&quot;: &quot;true&quot;,
"ASKED_MARK_IGNORED_FILES_AS_EXCLUDED": "true", &quot;ASKED_MARK_IGNORED_FILES_AS_EXCLUDED&quot;: &quot;true&quot;,
"Python.12.executor": "Run", &quot;Python.12.executor&quot;: &quot;Run&quot;,
"Python.123 (1).executor": "Run", &quot;Python.123.executor&quot;: &quot;Run&quot;,
"Python.123.executor": "Run", &quot;Python.Main.executor&quot;: &quot;Run&quot;,
"Python.Main.executor": "Run", &quot;Python.Test.executor&quot;: &quot;Run&quot;,
"Python.Test.executor": "Run", &quot;Python.test.executor&quot;: &quot;Run&quot;,
"Python.test (1).executor": "Run", &quot;Python.tidevice_entry.executor&quot;: &quot;Run&quot;,
"Python.test (2).executor": "Run", &quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
"Python.test.executor": "Run", &quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
"Python.tidevice_entry.executor": "Run", &quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
"RunOnceActivity.ShowReadmeOnStart": "true", &quot;SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;,
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", &quot;git-widget-placeholder&quot;: &quot;main&quot;,
"RunOnceActivity.git.unshallow": "true", &quot;javascript.nodejs.core.library.configured.version&quot;: &quot;20.17.0&quot;,
"SHARE_PROJECT_CONFIGURATION_FILES": "true", &quot;javascript.nodejs.core.library.typings.version&quot;: &quot;20.17.58&quot;,
"git-widget-placeholder": "main", &quot;last_opened_file_path&quot;: &quot;C:/Users/zhangkai/Desktop/20250916ios/iOSAI/resources&quot;,
"javascript.nodejs.core.library.configured.version": "20.17.0", &quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
"javascript.nodejs.core.library.typings.version": "20.17.58", &quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
"last_opened_file_path": "C:/Users/zhangkai/Desktop/20250916ios/iOSAI/Utils", &quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
"node.js.detected.package.eslint": "true", &quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
"node.js.detected.package.tslint": "true", &quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
"node.js.selected.package.eslint": "(autodetect)", &quot;settings.editor.selected.configurable&quot;: &quot;preferences.editor.code.editing&quot;,
"node.js.selected.package.tslint": "(autodetect)", &quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
"nodejs_package_manager_path": "npm",
"settings.editor.selected.configurable": "preferences.editor.code.editing",
"vue.rearranger.settings.migration": "true"
} }
}]]></component> }</component>
<component name="RecentsManager"> <component name="RecentsManager">
<key name="CopyFile.RECENT_KEYS"> <key name="CopyFile.RECENT_KEYS">
<recent name="C:\Users\zhangkai\Desktop\20250916ios\iOSAI\Utils" />
<recent name="C:\Users\zhangkai\Desktop\20250916ios\iOSAI\script" />
<recent name="C:\Users\zhangkai\Desktop\20250916ios\iOSAI\resources" /> <recent name="C:\Users\zhangkai\Desktop\20250916ios\iOSAI\resources" />
</key> </key>
<key name="MoveFile.RECENT_KEYS"> <key name="MoveFile.RECENT_KEYS">
<recent name="C:\Users\zhangkai\Desktop\20250916ios\iOSAI\script" />
<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" selected="Python.Main"> <component name="RunManager" selected="Python.Main">
<configuration name="123 (1)" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true"> <configuration name="12" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="iOSAI" /> <module name="iOSAI" />
<option name="ENV_FILES" value="" /> <option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" /> <option name="INTERPRETER_OPTIONS" value="" />
@@ -101,12 +95,12 @@
<env name="PYTHONUNBUFFERED" value="1" /> <env name="PYTHONUNBUFFERED" value="1" />
</envs> </envs>
<option name="SDK_HOME" value="" /> <option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/script" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" /> <option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" /> <option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" /> <option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" /> <EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/script/123.py" /> <option name="SCRIPT_NAME" value="$PROJECT_DIR$/12.py" />
<option name="PARAMETERS" value="" /> <option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" /> <option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" /> <option name="EMULATE_TERMINAL" value="false" />
@@ -129,7 +123,7 @@
<option name="ADD_CONTENT_ROOTS" value="true" /> <option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" /> <option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" /> <EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="C:\Users\zhangkai\Desktop\20250916ios\iOSAI\script\123.py" /> <option name="SCRIPT_NAME" value="$PROJECT_DIR$/123.py" />
<option name="PARAMETERS" value="" /> <option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" /> <option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" /> <option name="EMULATE_TERMINAL" value="false" />
@@ -161,7 +155,7 @@
<option name="INPUT_FILE" value="" /> <option name="INPUT_FILE" value="" />
<method v="2" /> <method v="2" />
</configuration> </configuration>
<configuration name="test (1)" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true"> <configuration name="Test" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="iOSAI" /> <module name="iOSAI" />
<option name="ENV_FILES" value="" /> <option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" /> <option name="INTERPRETER_OPTIONS" value="" />
@@ -170,35 +164,12 @@
<env name="PYTHONUNBUFFERED" value="1" /> <env name="PYTHONUNBUFFERED" value="1" />
</envs> </envs>
<option name="SDK_HOME" value="" /> <option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" /> <option name="WORKING_DIRECTORY" value="$PROJECT_DIR$/Utils" />
<option name="IS_MODULE_SDK" value="true" /> <option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" /> <option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" /> <option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" /> <EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/test.py" /> <option name="SCRIPT_NAME" value="$PROJECT_DIR$/Utils/Test.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="test (2)" 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$/script" />
<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$/script/test.py" />
<option name="PARAMETERS" value="" /> <option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" /> <option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" /> <option name="EMULATE_TERMINAL" value="false" />
@@ -232,19 +203,17 @@
</configuration> </configuration>
<recent_temporary> <recent_temporary>
<list> <list>
<item itemvalue="Python.123 (1)" />
<item itemvalue="Python.123" />
<item itemvalue="Python.test (1)" />
<item itemvalue="Python.test (2)" />
<item itemvalue="Python.test" /> <item itemvalue="Python.test" />
<item itemvalue="Python.123" />
<item itemvalue="Python.Test" />
<item itemvalue="Python.12" />
</list> </list>
</recent_temporary> </recent_temporary>
</component> </component>
<component name="SharedIndexes"> <component name="SharedIndexes">
<attachedChunks> <attachedChunks>
<set> <set>
<option value="bundled-js-predefined-1d06a55b98c1-0b3e54e931b4-JavaScript-PY-241.18034.82" /> <option value="bundled-python-sdk-ce6832f46686-7b97d883f26b-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-252.25557.178" />
<option value="bundled-python-sdk-975db3bf15a3-2767605e8bc2-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-241.18034.82" />
</set> </set>
</attachedChunks> </attachedChunks>
</component> </component>
@@ -310,10 +279,6 @@
<workItem from="1757506636968" duration="5910000" /> <workItem from="1757506636968" duration="5910000" />
<workItem from="1757567423145" duration="16668000" /> <workItem from="1757567423145" duration="16668000" />
<workItem from="1757998910052" duration="3676000" /> <workItem from="1757998910052" duration="3676000" />
<workItem from="1758122148569" duration="213000" />
<workItem from="1758171936953" duration="7319000" />
<workItem from="1758180127232" duration="653000" />
<workItem from="1758182513694" duration="19233000" />
</task> </task>
<task id="LOCAL-00001" summary="ai 开始测试"> <task id="LOCAL-00001" summary="ai 开始测试">
<option name="closed" value="true" /> <option name="closed" value="true" />
@@ -355,31 +320,7 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1757587781103</updated> <updated>1757587781103</updated>
</task> </task>
<task id="LOCAL-00006" summary="20250904-初步功能已完成"> <option name="localTasksCounter" value="6" />
<option name="closed" value="true" />
<created>1758121742405</created>
<option name="number" value="00006" />
<option name="presentableId" value="LOCAL-00006" />
<option name="project" value="LOCAL" />
<updated>1758121742405</updated>
</task>
<task id="LOCAL-00007" summary="20250918-新增主播库功能">
<option name="closed" value="true" />
<created>1758197707496</created>
<option name="number" value="00007" />
<option name="presentableId" value="LOCAL-00007" />
<option name="project" value="LOCAL" />
<updated>1758197707496</updated>
</task>
<task id="LOCAL-00008" summary="20250918-新增主播库功能">
<option name="closed" value="true" />
<created>1758202317131</created>
<option name="number" value="00008" />
<option name="presentableId" value="LOCAL-00008" />
<option name="project" value="LOCAL" />
<updated>1758202317131</updated>
</task>
<option name="localTasksCounter" value="9" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -403,25 +344,22 @@
<option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" /> <option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" />
<MESSAGE value="ai 开始测试" /> <MESSAGE value="ai 开始测试" />
<MESSAGE value="20250904-初步功能已完成" /> <MESSAGE value="20250904-初步功能已完成" />
<MESSAGE value="20250918-新增主播库功能" /> <option name="LAST_COMMIT_MESSAGE" value="20250904-初步功能已完成" />
<option name="LAST_COMMIT_MESSAGE" value="20250918-新增主播库功能" />
</component> </component>
<component name="com.intellij.coverage.CoverageDataManagerImpl"> <component name="com.intellij.coverage.CoverageDataManagerImpl">
<SUITE FILE_PATH="coverage/iOSAI$LogManager.coverage" NAME="LogManager 覆盖结果" MODIFIED="1756711414832" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/Utils" /> <SUITE FILE_PATH="coverage/iOSAI$LogManager.coverage" NAME="LogManager 覆盖结果" MODIFIED="1756711414832" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/Utils" />
<SUITE FILE_PATH="coverage/iOSAI$FlaskService.coverage" NAME="FlaskService 覆盖结果" MODIFIED="1756730187792" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/Module" /> <SUITE FILE_PATH="coverage/iOSAI$FlaskService.coverage" NAME="FlaskService 覆盖结果" MODIFIED="1756730187792" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/Module" />
<SUITE FILE_PATH="coverage/iOSAI$test.coverage" NAME="test 覆盖结果" MODIFIED="1758183945062" 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$test.coverage" NAME="test 覆盖结果" MODIFIED="1756467664420" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/iOSAI$windows_run.coverage" NAME="windows_run Coverage Results" MODIFIED="1756473558532" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" /> <SUITE FILE_PATH="coverage/iOSAI$windows_run.coverage" NAME="windows_run Coverage Results" MODIFIED="1756473558532" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" />
<SUITE FILE_PATH="coverage/iOSAI$1352.coverage" NAME="1352 覆盖结果" MODIFIED="1757662777051" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" /> <SUITE FILE_PATH="coverage/iOSAI$1352.coverage" NAME="1352 覆盖结果" MODIFIED="1757662777051" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/iOSAI$mac_wda_agent.coverage" NAME="mac_wda_agent Coverage Results" MODIFIED="1756473148639" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" /> <SUITE FILE_PATH="coverage/iOSAI$mac_wda_agent.coverage" NAME="mac_wda_agent Coverage Results" MODIFIED="1756473148639" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" />
<SUITE FILE_PATH="coverage/iOSAI$123456.coverage" NAME="123456 覆盖结果" MODIFIED="1757672582575" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" /> <SUITE FILE_PATH="coverage/iOSAI$123456.coverage" NAME="123456 覆盖结果" MODIFIED="1757672582575" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/iOSAI$2111.coverage" NAME="2111 覆盖结果" MODIFIED="1757330714370" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" /> <SUITE FILE_PATH="coverage/iOSAI$2111.coverage" NAME="2111 覆盖结果" MODIFIED="1757330714370" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<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$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="1758201139058" 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$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$test__1_.coverage" NAME="test (1) 覆盖结果" MODIFIED="1758193027385" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/iOSAI$tidevice_entry.coverage" NAME="tidevice_entry 覆盖结果" MODIFIED="1757061969626" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" /> <SUITE FILE_PATH="coverage/iOSAI$tidevice_entry.coverage" NAME="tidevice_entry 覆盖结果" MODIFIED="1757061969626" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/iOSAI$ScriptManager.coverage" NAME="ScriptManager 覆盖结果" MODIFIED="1756896057801" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" /> <SUITE FILE_PATH="coverage/iOSAI$ScriptManager.coverage" NAME="ScriptManager 覆盖结果" MODIFIED="1756896057801" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" />
<SUITE FILE_PATH="coverage/iOSAI$Main.coverage" NAME="Main 覆盖结果" MODIFIED="1758201539285" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="" /> <SUITE FILE_PATH="coverage/iOSAI$Main.coverage" NAME="Main 覆盖结果" MODIFIED="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="1758200468014" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" /> <SUITE FILE_PATH="coverage/iOSAI$123.coverage" NAME="123 覆盖结果" MODIFIED="1758115088356" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/iOSAI$test__2_.coverage" NAME="test (2) 覆盖结果" MODIFIED="1758192701951" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" />
</component> </component>
</project> </project>

View File

@@ -2,15 +2,15 @@ import json
# 返回数据模型 # 返回数据模型
class ResultData(object): class ResultData(object):
def __init__(self, code=200, data=None, massage="获取成功"): 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.massage = massage 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,
"massage": self.massage "message": self.message
}, ensure_ascii=False) # ensure_ascii=False 确保中文不会被转义 }, ensure_ascii=False) # ensure_ascii=False 确保中文不会被转义

View File

@@ -4,12 +4,12 @@ import signal
import sys import sys
import time import time
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
import wda
import threading
import subprocess
from pathlib import Path from pathlib import Path
from typing import List, Dict, Optional from typing import List, Dict, Optional
import threading
import subprocess
import wda
from tidevice import Usbmux, ConnectionType from tidevice import Usbmux, ConnectionType
from tidevice._device import BaseDevice from tidevice._device import BaseDevice
from Entity.DeviceModel import DeviceModel from Entity.DeviceModel import DeviceModel
@@ -23,10 +23,8 @@ class Deviceinfo(object):
"""设备生命周期管理:以 deviceModelList 为唯一真理源""" """设备生命周期管理:以 deviceModelList 为唯一真理源"""
def __init__(self): def __init__(self):
... # ✅ 连接线程池(最大 6 并发)
# ✅ 新增:连接线程池(最大 6 并发)
self._connect_pool = ThreadPoolExecutor(max_workers=6) self._connect_pool = ThreadPoolExecutor(max_workers=6)
...
if os.name == "nt": if os.name == "nt":
self._si = subprocess.STARTUPINFO() self._si = subprocess.STARTUPINFO()
@@ -44,11 +42,14 @@ class Deviceinfo(object):
self._lock = threading.Lock() self._lock = threading.Lock()
self._model_index: Dict[str, DeviceModel] = {} # udid -> model self._model_index: Dict[str, DeviceModel] = {} # udid -> model
# ✅ 1. 失踪时间戳记录(替代原来的 miss_count # ✅ 失踪时间戳记录(替代原来的 miss_count
self._last_seen: Dict[str, float] = {} self._last_seen: Dict[str, float] = {}
self._port_pool: List[int] = [] self._port_pool: List[int] = []
self._port_in_use: set[int] = set() self._port_in_use: set[int] = set()
# ✅ 新增:全局 iproxy 进程注册表 udid -> Popen
self._iproxy_registry: Dict[str, subprocess.Popen] = {}
# region iproxy 初始化(原逻辑不变) # region iproxy 初始化(原逻辑不变)
try: try:
self.iproxy_path = self._iproxy_path() self.iproxy_path = self._iproxy_path()
@@ -76,6 +77,9 @@ class Deviceinfo(object):
args = [str(self.iproxy_path), "-u", udid, str(local_port), str(remote_port)] args = [str(self.iproxy_path), "-u", udid, str(local_port), str(remote_port)]
p = subprocess.Popen(args, **self._popen_kwargs) p = subprocess.Popen(args, **self._popen_kwargs)
# ✅ 注册到全局表
self._iproxy_registry[udid] = p
def _pipe_to_log(name: str, stream): def _pipe_to_log(name: str, stream):
try: try:
for line in iter(stream.readline, ''): for line in iter(stream.readline, ''):
@@ -127,6 +131,13 @@ class Deviceinfo(object):
for udid in need_remove: for udid in need_remove:
self._remove_model(udid) self._remove_model(udid)
# ✅ 实时清理孤儿 iproxy原 10 秒改为每次循环)
self._cleanup_orphan_iproxy()
# ✅ 设备全空时核平所有 iproxy
if not self.deviceModelList:
self._kill_all_iproxy()
# 2. 发现新设备 → 并发连接 # 2. 发现新设备 → 并发连接
with self._lock: with self._lock:
new_udids = [d.udid for d in lists new_udids = [d.udid for d in lists
@@ -146,7 +157,7 @@ class Deviceinfo(object):
time.sleep(1) time.sleep(1)
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# ✅ 3. USB 层枚举 SN跨平台 # ✅ USB 层枚举 SN跨平台
# ------------------------------------------------------------------ # ------------------------------------------------------------------
def _usb_enumerate_sn(self) -> set[str]: def _usb_enumerate_sn(self) -> set[str]:
try: try:
@@ -155,7 +166,32 @@ class Deviceinfo(object):
except Exception: except Exception:
return set() return set()
# ===================== 以下代码与原文件完全一致 ===================== # ----------------------------------------------------------
# ✅ 清理孤儿 iproxy
# ----------------------------------------------------------
def _cleanup_orphan_iproxy(self):
live_udids = set(self._model_index.keys())
for udid, proc in list(self._iproxy_registry.items()):
if udid not in live_udids:
LogManager.warning(f"发现孤儿 iproxy 进程UDID 不在线:{udid},正在清理")
self._terminate_proc(proc)
self._iproxy_registry.pop(udid, None)
# ----------------------------------------------------------
# ✅ 核平所有 iproxyWindows / macOS 通用)
# ----------------------------------------------------------
def _kill_all_iproxy(self):
try:
if os.name == "nt":
subprocess.run(["taskkill", "/F", "/IM", "iproxy.exe"], check=False)
else:
subprocess.run(["pkill", "-f", "iproxy"], check=False)
self._iproxy_registry.clear()
LogManager.info("已强制清理所有 iproxy 进程")
except Exception as e:
LogManager.warning(f"强制清理 iproxy 失败:{e}")
# -------------------- 以下代码与原文件完全一致 --------------------
def _wda_health_checker(self): def _wda_health_checker(self):
while True: while True:
time.sleep(1) time.sleep(1)
@@ -229,6 +265,11 @@ class Deviceinfo(object):
print(f"【删】待杀进程数 count={len(to_kill)} udid={udid}") print(f"【删】待杀进程数 count={len(to_kill)} udid={udid}")
LogManager.method_info(f"【删】待杀进程数 count={len(to_kill)} udid={udid}", method="device_count") LogManager.method_info(f"【删】待杀进程数 count={len(to_kill)} udid={udid}", method="device_count")
# ✅ 先清理注册表中的 iproxy
iproxy_proc = self._iproxy_registry.pop(udid, None)
if iproxy_proc:
self._terminate_proc(iproxy_proc)
for idx, item in enumerate(to_kill, 1): for idx, item in enumerate(to_kill, 1):
print(f"【删】杀进程 {idx}/{len(to_kill)} pid={item.get('target').pid} udid={udid}") print(f"【删】杀进程 {idx}/{len(to_kill)} pid={item.get('target').pid} udid={udid}")
LogManager.method_info(f"【删】杀进程 {idx}/{len(to_kill)} pid={item.get('target').pid} udid={udid}", method="device_count") LogManager.method_info(f"【删】杀进程 {idx}/{len(to_kill)} pid={item.get('target').pid} udid={udid}", method="device_count")
@@ -335,13 +376,14 @@ class Deviceinfo(object):
if not self._spawn_iproxy: if not self._spawn_iproxy:
LogManager.error("iproxy 启动器未就绪", udid) LogManager.error("iproxy 启动器未就绪", udid)
return None return None
while self._port_in_use and self._is_port_open(port): for attempt in range(5):
if not self._is_port_open(port):
break
LogManager.warning(f"端口 {port} 仍被占用,第 {attempt+1} 次重试释放", udid)
pid = self._get_pid_by_port(port) pid = self._get_pid_by_port(port)
if pid and pid != os.getpid(): if pid and pid != os.getpid():
LogManager.warning(f"端口 {port} 仍被 PID {pid} 占用,尝试释放", udid)
self._kill_pid_gracefully(pid) self._kill_pid_gracefully(pid)
else: time.sleep(0.2)
break
try: try:
p = self._spawn_iproxy(udid, port, 9100) p = self._spawn_iproxy(udid, port, 9100)
self._port_in_use.add(port) self._port_in_use.add(port)

View File

@@ -244,8 +244,8 @@ def growAccount():
thread = threading.Thread(target=manager.growAccount, args=(udid, event)) thread = threading.Thread(target=manager.growAccount, args=(udid, event))
thread.start() thread.start()
# 添加到线程管理 # 添加到线程管理
ThreadManager.add(udid, thread, event) code , msg = ThreadManager.add(udid, thread, event)
return ResultData(data="").toJson() return ResultData(data="", code=code, message= msg).toJson()
# 观看直播 # 观看直播
@@ -269,7 +269,7 @@ def stopScript():
udid = body.get("udid") udid = body.get("udid")
LogManager.method_info(f"接口收到 /stopScript udid={udid}", method="task") LogManager.method_info(f"接口收到 /stopScript udid={udid}", method="task")
code, msg = ThreadManager.stop(udid) code, msg = ThreadManager.stop(udid)
return ResultData(code=code, data="", massage=msg).toJson() return ResultData(code=code, data="", message=msg).toJson()
# 关注打招呼 # 关注打招呼
@@ -305,6 +305,7 @@ def passAnchorData():
return ResultData(data="").toJson() return ResultData(data="").toJson()
except Exception as e: except Exception as e:
LogManager.error(e) LogManager.error(e)
return ResultData(data="", code=1001).toJson()
# 获取私信数据 # 获取私信数据
@@ -325,7 +326,7 @@ def addTempAnchorData():
""" """
data = request.get_json() data = request.get_json()
if not data: if not data:
return ResultData(code=400, massage="请求数据为空").toJson() return ResultData(code=400, message="请求数据为空").toJson()
# 追加到 JSON 文件 # 追加到 JSON 文件
AiUtils.save_aclist_flat_append(data, "log/acList.json") AiUtils.save_aclist_flat_append(data, "log/acList.json")
return ResultData(data="ok").toJson() return ResultData(data="ok").toJson()
@@ -359,7 +360,7 @@ def getChatTextInfo():
'text': 'Unable to retrieve chat messages on the current screen. Please navigate to the TikTok chat page and try again!!!' 'text': 'Unable to retrieve chat messages on the current screen. Please navigate to the TikTok chat page and try again!!!'
} }
] ]
return ResultData(data=data, massage="解析失败").toJson() return ResultData(data=data, message="解析失败").toJson()
# 监控消息 # 监控消息
@@ -386,7 +387,7 @@ def upLoadLogLogs():
if ok: if ok:
return ResultData(data="日志上传成功").toJson() return ResultData(data="日志上传成功").toJson()
else: else:
return ResultData(data="", massage="日志上传失败").toJson() return ResultData(data="", message="日志上传失败").toJson()
# 获取当前的主播列表数据 # 获取当前的主播列表数据
@@ -517,8 +518,8 @@ def update_last_message():
multi=False # 只改第一条匹配的 multi=False # 只改第一条匹配的
) )
if updated_count > 0: if updated_count > 0:
return ResultData(data=updated_count, massage="修改成功").toJson() return ResultData(data=updated_count, message="修改成功").toJson()
return ResultData(data=updated_count, massage="修改失败").toJson() return ResultData(data=updated_count, message="修改失败").toJson()
@app.route("/delete_last_message", methods=['POST']) @app.route("/delete_last_message", methods=['POST'])
@@ -534,8 +535,8 @@ def delete_last_message():
multi=False # 只改第一条匹配的 multi=False # 只改第一条匹配的
) )
if updated_count > 0: if updated_count > 0:
return ResultData(data=updated_count, massage="修改成功").toJson() return ResultData(data=updated_count, message="修改成功").toJson()
return ResultData(data=updated_count, massage="修改失败").toJson() return ResultData(data=updated_count, message="修改失败").toJson()
# @app.route("/killWda", methods=['POST']) # @app.route("/killWda", methods=['POST'])

View File

@@ -105,7 +105,6 @@ class FlaskSubprocessManager:
# 守护线程:把子进程 stdout → LogManager.info/system # 守护线程:把子进程 stdout → LogManager.info/system
threading.Thread(target=self._flush_stdout, daemon=True).start() threading.Thread(target=self._flush_stdout, daemon=True).start()
LogManager.info(f"Flask 子进程已启动PID={self.process.pid},端口={self.comm_port}", udid="system") LogManager.info(f"Flask 子进程已启动PID={self.process.pid},端口={self.comm_port}", udid="system")
if not self._wait_port_open(timeout=10): if not self._wait_port_open(timeout=10):
@@ -122,6 +121,8 @@ class FlaskSubprocessManager:
for line in iter(self.process.stdout.readline, ""): for line in iter(self.process.stdout.readline, ""):
if line: if line:
LogManager.info(line.rstrip(), udid="system") LogManager.info(line.rstrip(), udid="system")
# 同时输出到控制台
print(line.rstrip()) # 打印到主进程的控制台
self.process.stdout.close() self.process.stdout.close()
# ---------- 发送 ---------- # ---------- 发送 ----------

View File

@@ -1,12 +1,9 @@
import os import os
import sys import sys
from pathlib import Path from pathlib import Path
from Module.DeviceInfo import Deviceinfo from Module.DeviceInfo import Deviceinfo
from Module.FlaskSubprocessManager import FlaskSubprocessManager from Module.FlaskSubprocessManager import FlaskSubprocessManager
from Utils.DevDiskImageDeployer import DevDiskImageDeployer from Utils.DevDiskImageDeployer import DevDiskImageDeployer
from Utils.LogManager import LogManager
# 确定 exe 或 py 文件所在目录 # 确定 exe 或 py 文件所在目录
BASE = Path(getattr(sys, 'frozen', False) and sys.executable or __file__).resolve().parent BASE = Path(getattr(sys, 'frozen', False) and sys.executable or __file__).resolve().parent

BIN
SupportFiles/14.0.zip Normal file

Binary file not shown.

BIN
SupportFiles/14.1.zip Normal file

Binary file not shown.

BIN
SupportFiles/14.2.zip Normal file

Binary file not shown.

BIN
SupportFiles/14.3.zip Normal file

Binary file not shown.

BIN
SupportFiles/14.4.zip Normal file

Binary file not shown.

BIN
SupportFiles/14.5.zip Normal file

Binary file not shown.

BIN
SupportFiles/14.6.zip Normal file

Binary file not shown.

BIN
SupportFiles/14.7.zip Normal file

Binary file not shown.

BIN
SupportFiles/14.8.zip Normal file

Binary file not shown.

BIN
SupportFiles/15.0.zip Normal file

Binary file not shown.

BIN
SupportFiles/15.1.zip Normal file

Binary file not shown.

BIN
SupportFiles/15.2.zip Normal file

Binary file not shown.

BIN
SupportFiles/15.3.zip Normal file

Binary file not shown.

BIN
SupportFiles/15.4.zip Normal file

Binary file not shown.

BIN
SupportFiles/15.5.zip Normal file

Binary file not shown.

BIN
SupportFiles/15.6.zip Normal file

Binary file not shown.

BIN
SupportFiles/15.7.zip Normal file

Binary file not shown.

BIN
SupportFiles/15.8.zip Normal file

Binary file not shown.

BIN
SupportFiles/16.0.zip Normal file

Binary file not shown.

BIN
SupportFiles/16.1.zip Normal file

Binary file not shown.

BIN
SupportFiles/16.2.zip Normal file

Binary file not shown.

BIN
SupportFiles/16.3.zip Normal file

Binary file not shown.

BIN
SupportFiles/16.4.zip Normal file

Binary file not shown.

BIN
SupportFiles/16.5.zip Normal file

Binary file not shown.

BIN
SupportFiles/16.6.zip Normal file

Binary file not shown.

BIN
SupportFiles/16.7.zip Normal file

Binary file not shown.

View File

@@ -79,7 +79,6 @@ class ControlUtils(object):
"//Window[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]").exists: "//Window[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]").exists:
back = session.xpath( back = session.xpath(
"//Window[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]") "//Window[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]/Other[1]")
if back.exists: if back.exists:
back.click() back.click()
return True return True

View File

@@ -28,43 +28,6 @@ def _force_utf8_everywhere():
# _force_utf8_everywhere() # _force_utf8_everywhere()
# ========= 全局:强制 UTF-8 + 关闭缓冲(运行期立刻生效) =========
def _force_utf8_everywhere():
os.environ.setdefault("PYTHONUTF8", "1")
# 等价于 -u让 stdout/stderr 无缓冲
os.environ.setdefault("PYTHONUNBUFFERED", "1")
os.environ.setdefault("PYTHONIOENCODING", "utf-8")
# 若是 3.7+,优先用 reconfigure 实时改流
try:
if hasattr(sys.stdout, "reconfigure"):
sys.stdout.reconfigure(encoding="utf-8", errors="replace",
line_buffering=True, write_through=True)
elif getattr(sys.stdout, "buffer", None):
# 退路:重新包一层,启用行缓冲 + 直写
sys.stdout = io.TextIOWrapper(
sys.stdout.buffer, encoding="utf-8",
errors="replace", line_buffering=True
)
except Exception:
pass
try:
if hasattr(sys.stderr, "reconfigure"):
sys.stderr.reconfigure(encoding="utf-8", errors="replace",
line_buffering=True, write_through=True)
elif getattr(sys.stderr, "buffer", None):
sys.stderr = io.TextIOWrapper(
sys.stderr.buffer, encoding="utf-8",
errors="replace", line_buffering=True
)
except Exception:
pass
# ===========================================================
class LogManager: class LogManager:
""" """
设备级与“设备+方法”级日志管理: 设备级与“设备+方法”级日志管理:

View File

@@ -1,126 +1,77 @@
import os
import signal
import sys
import threading import threading
import time from typing import Dict, Tuple
import psutil
import subprocess
from pathlib import Path
from threading import Event, Thread
from typing import Dict, Optional
from Utils.LogManager import LogManager from Utils.LogManager import LogManager
class ThreadManager: class ThreadManager:
""" _tasks: Dict[str, Dict] = {}
对调用方完全透明:
add(udid, thread_obj, stop_event) 保持原签名
stop(udid) 保持原签名
但内部把 thread_obj 当成“壳”,真正拉起的是子进程。
"""
_pool: Dict[str, psutil.Process] = {}
_lock = threading.Lock() _lock = threading.Lock()
@classmethod @classmethod
def add(cls, udid: str, dummy_thread, dummy_event: Event) -> None: def add(cls, udid: str, thread: threading.Thread, event: threading.Event) -> Tuple[int, str]:
LogManager.method_info(f"【1】入口 udid={udid} 长度={len(udid)}", method="task") """
if udid in cls._pool: 添加一个线程到线程管理器。
LogManager.method_warning(f"{udid} 仍在运行,先强制清理旧任务", method="task") :param udid: 设备的唯一标识符
cls.stop(udid) :param thread: 线程对象
LogManager.method_info(f"【2】判断旧任务后 udid={udid} 长度={len(udid)}", method="task") :param event: 用于控制线程退出的 Event 对象
port = cls._find_free_port() :return: 状态码和信息
LogManager.method_info(f"【3】找端口后 udid={udid} 长度={len(udid)}", method="task") """
proc = cls._start_worker_process(udid, port) with cls._lock:
LogManager.method_info(f"【4】子进程启动后 udid={udid} 长度={len(udid)}", method="task") if udid in cls._tasks and cls._tasks[udid].get("running", False):
cls._pool[udid] = proc LogManager.method_info(f"任务添加失败:设备 {udid} 已存在运行中的任务", method="task")
LogManager.method_info(f"【5】已写入字典udid={udid} 长度={len(udid)}", method="task") return 400, f"该设备中已存在任务 {udid}"
# 如果任务已经存在但已停止,清理旧任务记录
if udid in cls._tasks and not cls._tasks[udid].get("running", False):
LogManager.method_info(f"清理设备 {udid} 的旧任务记录", method="task")
del cls._tasks[udid]
# 添加新任务记录
cls._tasks[udid] = {
"thread": thread,
"event": event,
"running": True
}
LogManager.method_info(f"设备 {udid} 开始任务成功", method="task")
return 200, f"创建任务成功 {udid}"
@classmethod @classmethod
def stop(cls, udid: str) -> tuple[int, str]: def stop(cls, udid: str) -> Tuple[int, str]:
with cls._lock: # 类级锁
proc = cls._pool.get(udid) # 1. 只读,不删
if proc is None:
return 1001, f"无此任务 {udid}"
try:
proc.terminate()
gone, alive = psutil.wait_procs([proc], timeout=3)
if alive:
for p in alive:
for child in p.children(recursive=True):
child.kill()
p.kill()
psutil.wait_procs(alive, timeout=2)
# 正常退出
cls._pool.pop(udid)
LogManager.method_info("任务停止成功", method="task")
return 200, f"停止线程成功 {udid}"
except psutil.NoSuchProcess: # 精准捕获
cls._pool.pop(udid)
LogManager.method_info("进程已自然退出", method="task")
return 200, f"进程已退出 {udid}"
except Exception as e: # 真正的异常
LogManager.method_error(f"停止异常: {e}", method="task")
return 1002, f"停止异常 {udid}"
# ------------------------------------------------------
# 以下全是内部工具,外部无需调用
# ------------------------------------------------------
@staticmethod
def _find_free_port(start: int = 50000) -> int:
"""找个随机空闲端口,给子进程当通信口(可选)"""
import socket
for p in range(start, start + 1000):
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as s:
if s.connect_ex(("127.0.0.1", p)) != 0:
return p
raise RuntimeError("无可用端口")
@staticmethod
def _start_worker_process(udid: str, port: int) -> psutil.Process:
""" """
真正拉起子进程: 停止指定设备的线程。
打包环境exe --udid=xxx :param udid: 设备的唯一标识符
源码环境python -m Module.Worker --udid=xxx :return: 状态码和信息
""" """
exe_path = Path(sys.executable).resolve() with cls._lock:
is_frozen = exe_path.suffix.lower() == ".exe" and exe_path.exists() if udid not in cls._tasks or not cls._tasks[udid].get("running", False):
LogManager.method_info(f"任务停止失败:设备 {udid} 没有执行相关任务", method="task")
return 400, f"当前设备没有执行相关任务 {udid}"
if is_frozen: task = cls._tasks[udid]
# 打包后 event = task["event"]
cmd = [str(exe_path), "--role=worker", f"--udid={udid}", f"--port={port}"] thread = task["thread"]
cwd = str(exe_path.parent)
else:
# 源码运行
cmd = [sys.executable, "-u", "-m", "Module.Worker", f"--udid={udid}", f"--port={port}"]
cwd = str(Path(__file__).resolve().parent.parent)
# 核心CREATE_NO_WINDOW + 独立会话,父进程死也不影响 LogManager.method_info(f"设备 {udid} 的任务正在停止", method="task")
creation_flags = 0x08000000 if os.name == "nt" else 0
proc = subprocess.Popen(
cmd,
stdin=subprocess.DEVNULL,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
encoding="utf-8",
errors="replace",
bufsize=1,
cwd=cwd,
start_new_session=True, # 独立进程组
creationflags=creation_flags
)
# 守护线程:把子进程 stdout 实时打到日志
Thread(target=lambda: ThreadManager._log_stdout(proc, udid), daemon=True).start()
return psutil.Process(proc.pid)
@staticmethod # 设置停止标志位
def _log_stdout(proc: subprocess.Popen, udid: str): event.set()
for line in iter(proc.stdout.readline, ""):
if line: # 等待线程结束
LogManager.info(line.rstrip(), udid) thread.join(timeout=5) # 可设置超时时间,避免阻塞
proc.stdout.close()
# 清理任务记录
del cls._tasks[udid] # 删除任务记录
LogManager.method_info(f"设备 {udid} 的任务停止成功", method="task")
return 200, f"当前任务停止成功 {udid}"
@classmethod
def is_task_running(cls, udid: str) -> bool:
"""
检查任务是否正在运行。
:param udid: 设备的唯一标识符
:return: True 表示任务正在运行False 表示没有任务运行
"""
with cls._lock:
is_running = cls._tasks.get(udid, {}).get("running", False)
LogManager.method_info(f"检查设备 {udid} 的任务状态:{'运行中' if is_running else '未运行'}", method="task")
return is_running

View File

@@ -60,8 +60,6 @@ class ScriptManager():
# 设置手机的节点深度为15,判断该页面是否正确 # 设置手机的节点深度为15,判断该页面是否正确
session.appium_settings({"snapshotMaxDepth": 15}) session.appium_settings({"snapshotMaxDepth": 15})
# 判断当前页面上是否有推荐按钮
el = session.xpath( el = session.xpath(
'//XCUIElementTypeButton[@name="top_tabs_recomend" or @name="推荐" or @label="推荐"]' '//XCUIElementTypeButton[@name="top_tabs_recomend" or @name="推荐" or @label="推荐"]'
) )
@@ -274,7 +272,6 @@ class ScriptManager():
retries = 0 retries = 0
while not event.is_set(): while not event.is_set():
try: try:
self.greetNewFollowers(udid, needReply, event) self.greetNewFollowers(udid, needReply, event)
except Exception as e: except Exception as e:
@@ -294,13 +291,11 @@ class ScriptManager():
LogManager.method_info(f"是否要自动回复消息:{needReply}", "关注打招呼", udid) LogManager.method_info(f"是否要自动回复消息:{needReply}", "关注打招呼", udid)
# 先关闭Tik Tok # 先关闭Tik Tok
ControlUtils.closeTikTok(session, udid) ControlUtils.closeTikTok(session, udid)
time.sleep(1) time.sleep(1)
# 重新打开Tik Tok # 重新打开Tik Tok
ControlUtils.openTikTok(session, udid) ControlUtils.openTikTok(session, udid)
time.sleep(3) time.sleep(3)
LogManager.method_info(f"重启tiktok", "关注打招呼", udid) LogManager.method_info(f"重启tiktok", "关注打招呼", udid)
# 设置查找深度 # 设置查找深度
@@ -374,6 +369,7 @@ class ScriptManager():
input.set_text(f"{aid or '暂无数据'}\n") input.set_text(f"{aid or '暂无数据'}\n")
# 定位 "关注" 按钮 通过关注按钮的位置点击主播首页 # 定位 "关注" 按钮 通过关注按钮的位置点击主播首页
session.appium_settings({"snapshotMaxDepth": 25}) session.appium_settings({"snapshotMaxDepth": 25})
try: try:
@@ -606,7 +602,6 @@ class ScriptManager():
time.sleep(3) time.sleep(3)
continue # 重新进入 while 循环,调用 monitorMessages continue # 重新进入 while 循环,调用 monitorMessages
# 检查未读消息并回复 # 检查未读消息并回复
def monitorMessages(self, session, udid): def monitorMessages(self, session, udid):