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$">
<excludeFolder url="file://$MODULE_DIR$/.venv" />
</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" />
</component>
</module>

2
.idea/misc.xml generated
View File

@@ -3,5 +3,5 @@
<component name="Black">
<option name="sdkName" value="Python 3.12" />
</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>

156
.idea/workspace.xml generated
View File

@@ -4,7 +4,10 @@
<option name="autoReloadType" value="SELECTIVE" />
</component>
<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="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />
@@ -34,9 +37,6 @@
<component name="HighlightingSettingsPerFile">
<setting file="file://$PROJECT_DIR$/build.bat" root0="SKIP_INSPECTION" />
</component>
<component name="PerforceDirect.Settings">
<option name="CHARSET" value="无" />
</component>
<component name="ProjectColorInfo">{
&quot;customColor&quot;: &quot;&quot;,
&quot;associatedIndex&quot;: 5
@@ -49,50 +49,44 @@
<option name="hideEmptyMiddlePackages" value="true" />
<option name="showLibraryContents" value="true" />
</component>
<component name="PropertiesComponent"><![CDATA[{
"keyToString": {
"ASKED_ADD_EXTERNAL_FILES": "true",
"ASKED_MARK_IGNORED_FILES_AS_EXCLUDED": "true",
"Python.12.executor": "Run",
"Python.123 (1).executor": "Run",
"Python.123.executor": "Run",
"Python.Main.executor": "Run",
"Python.Test.executor": "Run",
"Python.test (1).executor": "Run",
"Python.test (2).executor": "Run",
"Python.test.executor": "Run",
"Python.tidevice_entry.executor": "Run",
"RunOnceActivity.ShowReadmeOnStart": "true",
"RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
"RunOnceActivity.git.unshallow": "true",
"SHARE_PROJECT_CONFIGURATION_FILES": "true",
"git-widget-placeholder": "main",
"javascript.nodejs.core.library.configured.version": "20.17.0",
"javascript.nodejs.core.library.typings.version": "20.17.58",
"last_opened_file_path": "C:/Users/zhangkai/Desktop/20250916ios/iOSAI/Utils",
"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": "preferences.editor.code.editing",
"vue.rearranger.settings.migration": "true"
<component name="PropertiesComponent">{
&quot;keyToString&quot;: {
&quot;ASKED_ADD_EXTERNAL_FILES&quot;: &quot;true&quot;,
&quot;ASKED_MARK_IGNORED_FILES_AS_EXCLUDED&quot;: &quot;true&quot;,
&quot;Python.12.executor&quot;: &quot;Run&quot;,
&quot;Python.123.executor&quot;: &quot;Run&quot;,
&quot;Python.Main.executor&quot;: &quot;Run&quot;,
&quot;Python.Test.executor&quot;: &quot;Run&quot;,
&quot;Python.test.executor&quot;: &quot;Run&quot;,
&quot;Python.tidevice_entry.executor&quot;: &quot;Run&quot;,
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;,
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;,
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;,
&quot;SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;,
&quot;git-widget-placeholder&quot;: &quot;main&quot;,
&quot;javascript.nodejs.core.library.configured.version&quot;: &quot;20.17.0&quot;,
&quot;javascript.nodejs.core.library.typings.version&quot;: &quot;20.17.58&quot;,
&quot;last_opened_file_path&quot;: &quot;C:/Users/zhangkai/Desktop/20250916ios/iOSAI/resources&quot;,
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;,
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;,
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;,
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;,
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;,
&quot;settings.editor.selected.configurable&quot;: &quot;preferences.editor.code.editing&quot;,
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot;
}
}]]></component>
}</component>
<component name="RecentsManager">
<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" />
</key>
<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" />
</key>
</component>
<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" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
@@ -101,12 +95,12 @@
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<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="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/123.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/12.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
@@ -129,7 +123,7 @@
<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="C:\Users\zhangkai\Desktop\20250916ios\iOSAI\script\123.py" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/123.py" />
<option name="PARAMETERS" value="" />
<option name="SHOW_COMMAND_LINE" value="false" />
<option name="EMULATE_TERMINAL" value="false" />
@@ -161,7 +155,7 @@
<option name="INPUT_FILE" value="" />
<method v="2" />
</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" />
<option name="ENV_FILES" value="" />
<option name="INTERPRETER_OPTIONS" value="" />
@@ -170,35 +164,12 @@
<env name="PYTHONUNBUFFERED" value="1" />
</envs>
<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="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$/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="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" />
@@ -232,19 +203,17 @@
</configuration>
<recent_temporary>
<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.123" />
<item itemvalue="Python.Test" />
<item itemvalue="Python.12" />
</list>
</recent_temporary>
</component>
<component name="SharedIndexes">
<attachedChunks>
<set>
<option value="bundled-js-predefined-1d06a55b98c1-0b3e54e931b4-JavaScript-PY-241.18034.82" />
<option value="bundled-python-sdk-975db3bf15a3-2767605e8bc2-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-241.18034.82" />
<option value="bundled-python-sdk-ce6832f46686-7b97d883f26b-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-252.25557.178" />
</set>
</attachedChunks>
</component>
@@ -310,10 +279,6 @@
<workItem from="1757506636968" duration="5910000" />
<workItem from="1757567423145" duration="16668000" />
<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 id="LOCAL-00001" summary="ai 开始测试">
<option name="closed" value="true" />
@@ -355,31 +320,7 @@
<option name="project" value="LOCAL" />
<updated>1757587781103</updated>
</task>
<task id="LOCAL-00006" summary="20250904-初步功能已完成">
<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" />
<option name="localTasksCounter" value="6" />
<servers />
</component>
<component name="TypeScriptGeneratedFilesManager">
@@ -403,25 +344,22 @@
<option name="ADD_EXTERNAL_FILES_SILENTLY" value="true" />
<MESSAGE value="ai 开始测试" />
<MESSAGE value="20250904-初步功能已完成" />
<MESSAGE value="20250918-新增主播库功能" />
<option name="LAST_COMMIT_MESSAGE" value="20250918-新增主播库功能" />
<option name="LAST_COMMIT_MESSAGE" value="20250904-初步功能已完成" />
</component>
<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$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$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="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$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$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$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$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$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" />
<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>
</project>

View File

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

View File

@@ -4,12 +4,12 @@ import signal
import sys
import time
from concurrent.futures import ThreadPoolExecutor, as_completed
import wda
import threading
import subprocess
from pathlib import Path
from typing import List, Dict, Optional
import threading
import subprocess
import wda
from tidevice import Usbmux, ConnectionType
from tidevice._device import BaseDevice
from Entity.DeviceModel import DeviceModel
@@ -23,10 +23,8 @@ class Deviceinfo(object):
"""设备生命周期管理:以 deviceModelList 为唯一真理源"""
def __init__(self):
...
# ✅ 新增:连接线程池(最大 6 并发)
# ✅ 连接线程池(最大 6 并发)
self._connect_pool = ThreadPoolExecutor(max_workers=6)
...
if os.name == "nt":
self._si = subprocess.STARTUPINFO()
@@ -44,11 +42,14 @@ class Deviceinfo(object):
self._lock = threading.Lock()
self._model_index: Dict[str, DeviceModel] = {} # udid -> model
# ✅ 1. 失踪时间戳记录(替代原来的 miss_count
# ✅ 失踪时间戳记录(替代原来的 miss_count
self._last_seen: Dict[str, float] = {}
self._port_pool: List[int] = []
self._port_in_use: set[int] = set()
# ✅ 新增:全局 iproxy 进程注册表 udid -> Popen
self._iproxy_registry: Dict[str, subprocess.Popen] = {}
# region iproxy 初始化(原逻辑不变)
try:
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)]
p = subprocess.Popen(args, **self._popen_kwargs)
# ✅ 注册到全局表
self._iproxy_registry[udid] = p
def _pipe_to_log(name: str, stream):
try:
for line in iter(stream.readline, ''):
@@ -127,6 +131,13 @@ class Deviceinfo(object):
for udid in need_remove:
self._remove_model(udid)
# ✅ 实时清理孤儿 iproxy原 10 秒改为每次循环)
self._cleanup_orphan_iproxy()
# ✅ 设备全空时核平所有 iproxy
if not self.deviceModelList:
self._kill_all_iproxy()
# 2. 发现新设备 → 并发连接
with self._lock:
new_udids = [d.udid for d in lists
@@ -146,7 +157,7 @@ class Deviceinfo(object):
time.sleep(1)
# ------------------------------------------------------------------
# ✅ 3. USB 层枚举 SN跨平台
# ✅ USB 层枚举 SN跨平台
# ------------------------------------------------------------------
def _usb_enumerate_sn(self) -> set[str]:
try:
@@ -155,7 +166,32 @@ class Deviceinfo(object):
except Exception:
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):
while True:
time.sleep(1)
@@ -229,6 +265,11 @@ class Deviceinfo(object):
print(f"【删】待杀进程数 count={len(to_kill)} udid={udid}")
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):
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")
@@ -335,13 +376,14 @@ class Deviceinfo(object):
if not self._spawn_iproxy:
LogManager.error("iproxy 启动器未就绪", udid)
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)
if pid and pid != os.getpid():
LogManager.warning(f"端口 {port} 仍被 PID {pid} 占用,尝试释放", udid)
self._kill_pid_gracefully(pid)
else:
break
time.sleep(0.2)
try:
p = self._spawn_iproxy(udid, port, 9100)
self._port_in_use.add(port)

View File

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

View File

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

View File

@@ -1,12 +1,9 @@
import os
import sys
from pathlib import Path
from Module.DeviceInfo import Deviceinfo
from Module.FlaskSubprocessManager import FlaskSubprocessManager
from Utils.DevDiskImageDeployer import DevDiskImageDeployer
from Utils.LogManager import LogManager
# 确定 exe 或 py 文件所在目录
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:
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:
back.click()
return True

View File

@@ -28,43 +28,6 @@ def _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:
"""
设备级与“设备+方法”级日志管理:

View File

@@ -1,126 +1,77 @@
import os
import signal
import sys
import threading
import time
import psutil
import subprocess
from pathlib import Path
from threading import Event, Thread
from typing import Dict, Optional
from typing import Dict, Tuple
from Utils.LogManager import LogManager
class ThreadManager:
"""
对调用方完全透明:
add(udid, thread_obj, stop_event) 保持原签名
stop(udid) 保持原签名
但内部把 thread_obj 当成“壳”,真正拉起的是子进程。
"""
_pool: Dict[str, psutil.Process] = {}
_tasks: Dict[str, Dict] = {}
_lock = threading.Lock()
@classmethod
def add(cls, udid: str, dummy_thread, dummy_event: Event) -> None:
LogManager.method_info(f"【1】入口 udid={udid} 长度={len(udid)}", method="task")
if udid in cls._pool:
LogManager.method_warning(f"{udid} 仍在运行,先强制清理旧任务", method="task")
cls.stop(udid)
LogManager.method_info(f"【2】判断旧任务后 udid={udid} 长度={len(udid)}", method="task")
port = cls._find_free_port()
LogManager.method_info(f"【3】找端口后 udid={udid} 长度={len(udid)}", method="task")
proc = cls._start_worker_process(udid, port)
LogManager.method_info(f"【4】子进程启动后 udid={udid} 长度={len(udid)}", method="task")
cls._pool[udid] = proc
LogManager.method_info(f"【5】已写入字典udid={udid} 长度={len(udid)}", method="task")
def add(cls, udid: str, thread: threading.Thread, event: threading.Event) -> Tuple[int, str]:
"""
添加一个线程到线程管理器。
:param udid: 设备的唯一标识符
:param thread: 线程对象
:param event: 用于控制线程退出的 Event 对象
:return: 状态码和信息
"""
with cls._lock:
if udid in cls._tasks and cls._tasks[udid].get("running", False):
LogManager.method_info(f"任务添加失败:设备 {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
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:
def stop(cls, udid: str) -> Tuple[int, str]:
"""
真正拉起子进程:
打包环境exe --udid=xxx
源码环境python -m Module.Worker --udid=xxx
停止指定设备的线程。
:param udid: 设备的唯一标识符
:return: 状态码和信息
"""
exe_path = Path(sys.executable).resolve()
is_frozen = exe_path.suffix.lower() == ".exe" and exe_path.exists()
with cls._lock:
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:
# 打包后
cmd = [str(exe_path), "--role=worker", f"--udid={udid}", f"--port={port}"]
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)
task = cls._tasks[udid]
event = task["event"]
thread = task["thread"]
# 核心CREATE_NO_WINDOW + 独立会话,父进程死也不影响
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)
LogManager.method_info(f"设备 {udid} 的任务正在停止", method="task")
@staticmethod
def _log_stdout(proc: subprocess.Popen, udid: str):
for line in iter(proc.stdout.readline, ""):
if line:
LogManager.info(line.rstrip(), udid)
proc.stdout.close()
# 设置停止标志位
event.set()
# 等待线程结束
thread.join(timeout=5) # 可设置超时时间,避免阻塞
# 清理任务记录
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,判断该页面是否正确
session.appium_settings({"snapshotMaxDepth": 15})
# 判断当前页面上是否有推荐按钮
el = session.xpath(
'//XCUIElementTypeButton[@name="top_tabs_recomend" or @name="推荐" or @label="推荐"]'
)
@@ -274,7 +272,6 @@ class ScriptManager():
retries = 0
while not event.is_set():
try:
self.greetNewFollowers(udid, needReply, event)
except Exception as e:
@@ -294,13 +291,11 @@ class ScriptManager():
LogManager.method_info(f"是否要自动回复消息:{needReply}", "关注打招呼", udid)
# 先关闭Tik Tok
ControlUtils.closeTikTok(session, udid)
time.sleep(1)
# 重新打开Tik Tok
ControlUtils.openTikTok(session, udid)
time.sleep(3)
LogManager.method_info(f"重启tiktok", "关注打招呼", udid)
# 设置查找深度
@@ -374,6 +369,7 @@ class ScriptManager():
input.set_text(f"{aid or '暂无数据'}\n")
# 定位 "关注" 按钮 通过关注按钮的位置点击主播首页
session.appium_settings({"snapshotMaxDepth": 25})
try:
@@ -606,7 +602,6 @@ class ScriptManager():
time.sleep(3)
continue # 重新进入 while 循环,调用 monitorMessages
# 检查未读消息并回复
def monitorMessages(self, session, udid):