合并代码。临时上传

This commit is contained in:
2025-10-22 18:24:43 +08:00
parent a0fe54d504
commit 855a19873e
33 changed files with 1347 additions and 928 deletions

5
.idea/.gitignore generated vendored
View File

@@ -1,5 +0,0 @@
# 默认忽略的文件
/shelf/
/workspace.xml
# 基于编辑器的 HTTP 客户端请求
/httpRequests/

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="GitToolBoxBlameSettings">
<option name="version" value="2" />
</component>
</project>

View File

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

11
.idea/iOSAI.iml generated
View File

@@ -1,10 +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" />
<excludeFolder url="file://$MODULE_DIR$/.venv" /> <option name="myDocStringFormat" value="Plain" />
</content>
<orderEntry type="jdk" jdkName="Python 3.12" jdkType="Python SDK" />
<orderEntry type="sourceFolder" forTests="false" />
</component> </component>
</module> </module>

View File

@@ -3,20 +3,12 @@
<option name="myName" value="Project Default" /> <option name="myName" value="Project Default" />
<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>
<list size="4">
<item index="0" class="java.lang.String" itemvalue="PySide6" />
<item index="1" class="java.lang.String" itemvalue="pyusb" />
<item index="2" class="java.lang.String" itemvalue="PyGObject-stubs" />
<item index="3" class="java.lang.String" itemvalue="PyGObject" />
</list>
</value>
</option>
</inspection_tool>
<inspection_tool class="PyPep8NamingInspection" enabled="true" level="WEAK WARNING" enabled_by_default="true">
<option name="ignoredErrors">
<list> <list>
<option value="N806" /> <option value="facebook_wda" />
<option value="Flask" />
<option value="flask_cors" />
<option value="Requests" />
<option value="tidevice" />
</list> </list>
</option> </option>
</inspection_tool> </inspection_tool>

View File

@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="JavaScriptLibraryMappings">
<includedPredefinedLibrary name="Node.js Core" />
</component>
</project>

8
.idea/modules.xml generated
View File

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

1
.idea/vcs.xml generated
View File

@@ -2,6 +2,5 @@
<project version="4"> <project version="4">
<component name="VcsDirectoryMappings"> <component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" /> <mapping directory="" vcs="Git" />
<mapping directory="$PROJECT_DIR$" vcs="Git" />
</component> </component>
</project> </project>

207
.idea/workspace.xml generated
View File

@@ -5,8 +5,30 @@
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="20250904-初步功能已完成"> <list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="20250904-初步功能已完成">
<change afterPath="$PROJECT_DIR$/Utils/TencentOCRUtils.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/.gitignore" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/git_toolbox_blame.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/git_toolbox_prj.xml" beforeDir="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/jsLibraryMappings.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/modules.xml" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/vcs.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/vcs.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" />
<change beforePath="$PROJECT_DIR$/Module/DeviceInfo.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/DeviceInfo.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Module/FlaskService.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/FlaskService.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Module/FlaskService.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/FlaskService.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Module/IOSActivator.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/IOSActivator.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Utils/AiUtils.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/AiUtils.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Utils/ControlUtils.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/ControlUtils.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Utils/IOSAIStorage.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/IOSAIStorage.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Utils/JsonUtils.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/JsonUtils.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Utils/Requester.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/Requester.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Utils/ThreadManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/ThreadManager.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/build.bat" beforeDir="false" afterPath="$PROJECT_DIR$/build.bat" afterDir="false" />
<change beforePath="$PROJECT_DIR$/requirements.txt" beforeDir="false" />
<change beforePath="$PROJECT_DIR$/script/ScriptManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/script/ScriptManager.py" afterDir="false" />
</list> </list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
@@ -49,35 +71,36 @@
<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">{ <component name="PropertiesComponent"><![CDATA[{
&quot;keyToString&quot;: { "keyToString": {
&quot;ASKED_ADD_EXTERNAL_FILES&quot;: &quot;true&quot;, "ASKED_ADD_EXTERNAL_FILES": "true",
&quot;ASKED_MARK_IGNORED_FILES_AS_EXCLUDED&quot;: &quot;true&quot;, "ASKED_MARK_IGNORED_FILES_AS_EXCLUDED": "true",
&quot;Python.12.executor&quot;: &quot;Run&quot;, "ModuleVcsDetector.initialDetectionPerformed": "true",
&quot;Python.123.executor&quot;: &quot;Run&quot;, "Python.12.executor": "Run",
&quot;Python.DeviceInfo.executor&quot;: &quot;Run&quot;, "Python.123.executor": "Run",
&quot;Python.Main.executor&quot;: &quot;Run&quot;, "Python.DeviceInfo.executor": "Run",
&quot;Python.Test.executor&quot;: &quot;Run&quot;, "Python.Main.executor": "Run",
&quot;Python.test.executor&quot;: &quot;Run&quot;, "Python.Test.executor": "Run",
&quot;Python.tidevice_entry.executor&quot;: &quot;Run&quot;, "Python.test.executor": "Run",
&quot;RunOnceActivity.ShowReadmeOnStart&quot;: &quot;true&quot;, "Python.tidevice_entry.executor": "Run",
&quot;RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252&quot;: &quot;true&quot;, "RunOnceActivity.ShowReadmeOnStart": "true",
&quot;RunOnceActivity.git.unshallow&quot;: &quot;true&quot;, "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true",
&quot;SHARE_PROJECT_CONFIGURATION_FILES&quot;: &quot;true&quot;, "RunOnceActivity.git.unshallow": "true",
&quot;git-widget-placeholder&quot;: &quot;main&quot;, "SHARE_PROJECT_CONFIGURATION_FILES": "true",
&quot;javascript.nodejs.core.library.configured.version&quot;: &quot;20.17.0&quot;, "git-widget-placeholder": "main",
&quot;javascript.nodejs.core.library.typings.version&quot;: &quot;20.17.58&quot;, "javascript.nodejs.core.library.configured.version": "20.17.0",
&quot;last_opened_file_path&quot;: &quot;E:/code/Python/iOSAi/resources/iproxy&quot;, "javascript.nodejs.core.library.typings.version": "20.17.58",
&quot;node.js.detected.package.eslint&quot;: &quot;true&quot;, "last_opened_file_path": "E:/code/Python/iOSAi/resources/iproxy",
&quot;node.js.detected.package.tslint&quot;: &quot;true&quot;, "node.js.detected.package.eslint": "true",
&quot;node.js.selected.package.eslint&quot;: &quot;(autodetect)&quot;, "node.js.detected.package.tslint": "true",
&quot;node.js.selected.package.tslint&quot;: &quot;(autodetect)&quot;, "node.js.selected.package.eslint": "(autodetect)",
&quot;nodejs_package_manager_path&quot;: &quot;npm&quot;, "node.js.selected.package.tslint": "(autodetect)",
&quot;settings.editor.selected.configurable&quot;: &quot;com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable&quot;, "nodejs_package_manager_path": "npm",
&quot;two.files.diff.last.used.file&quot;: &quot;E:/share/iOSAI/Module/FlaskService.py&quot;, "settings.editor.selected.configurable": "editing.templates",
&quot;vue.rearranger.settings.migration&quot;: &quot;true&quot; "two.files.diff.last.used.file": "E:/share/iOSAI/Module/FlaskService.py",
"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="E:\code\Python\iOSAi\resources\iproxy" /> <recent name="E:\code\Python\iOSAi\resources\iproxy" />
@@ -91,75 +114,7 @@
<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">
<configuration name="12" 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$" />
<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$/12.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="123" 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$" />
<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$/123.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="DeviceInfo" 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" />
<option name="SCRIPT_NAME" value="$PROJECT_DIR$/Module/DeviceInfo.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="" />
@@ -169,12 +124,13 @@
<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="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" />
<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="E:\code\Python\iOSAi\Module\Main.py" /> <option name="SCRIPT_NAME" value="$PROJECT_DIR$/Module/Main.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" />
@@ -183,61 +139,6 @@
<option name="INPUT_FILE" value="" /> <option name="INPUT_FILE" value="" />
<method v="2" /> <method v="2" />
</configuration> </configuration>
<configuration name="Test" 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$/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$/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" 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$/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$/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>
<recent_temporary>
<list>
<item itemvalue="Python.DeviceInfo" />
<item itemvalue="Python.test" />
<item itemvalue="Python.123" />
<item itemvalue="Python.Test" />
<item itemvalue="Python.12" />
</list>
</recent_temporary>
</component> </component>
<component name="SharedIndexes"> <component name="SharedIndexes">
<attachedChunks> <attachedChunks>

View File

@@ -4,6 +4,7 @@ from Entity.AnchorModel import AnchorModel
# wda apple bundle id # wda apple bundle id
WdaAppBundleId = "com.yolojtAgent.wda.xctrunner" WdaAppBundleId = "com.yolojtAgent.wda.xctrunner"
# WdaAppBundleId = "com.yolozsAgent.wda.xctrunner"
# wda投屏端口 # wda投屏端口
wdaScreenPort = 9567 wdaScreenPort = 9567
# wda功能端口 # wda功能端口
@@ -15,6 +16,11 @@ anchorListLock = threading.Lock()
# 打招呼数据 # 打招呼数据
prologueList: list[str] = [] prologueList: list[str] = []
# 评论数据
commentList = []
API_KEY = "app-sdRfZy2by9Kq7uJg7JdOSVr8"
# 本地储存的打招呼数据 # 本地储存的打招呼数据
localPrologueList = [ 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!", "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!",

View File

@@ -1,14 +1,12 @@
import os import os
import signal import signal
import subprocess import subprocess
import sys
import threading import threading
import time import time
from concurrent.futures import ThreadPoolExecutor, as_completed from concurrent.futures import ThreadPoolExecutor, as_completed
from pathlib import Path from pathlib import Path
from typing import Dict, Optional, List from typing import Dict, Optional, List
import tidevice import tidevice
import usb
import wda import wda
from tidevice import Usbmux, ConnectionType from tidevice import Usbmux, ConnectionType
from tidevice._device import BaseDevice from tidevice._device import BaseDevice
@@ -40,6 +38,7 @@ class DeviceInfo:
orphan_gc_tick = 0 orphan_gc_tick = 0
while True: while True:
online = {d.udid for d in Usbmux().device_list() if d.conn_type == ConnectionType.USB} online = {d.udid for d in Usbmux().device_list() if d.conn_type == ConnectionType.USB}
# 拔掉——同步 # 拔掉——同步
for udid in list(self._models): for udid in list(self._models):
if udid not in online: if udid not in online:
@@ -123,7 +122,9 @@ class DeviceInfo:
args=(udid,) args=(udid,)
).start() ).start()
else: else:
print("准备启动wda")
dev.app_start(WdaAppBundleId) dev.app_start(WdaAppBundleId)
print("启动wda完成")
print("启动wda成功") print("启动wda成功")
time.sleep(3) time.sleep(3)
return True return True
@@ -137,12 +138,12 @@ class DeviceInfo:
c.home() c.home()
size = c.window_size() size = c.window_size()
scale = c.scale scale = c.scale
print("已获取到屏幕大小信息")
return int(size.width), int(size.height), float(scale) return int(size.width), int(size.height), float(scale)
except Exception as e: except Exception as e:
print("获取设备信息遇到错误:", e) print("获取设备信息遇到错误:", e)
return 0, 0, 0 return 0, 0, 0
...
# ---------------- 原来代码不变,只替换下面一个函数 ---------------- # ---------------- 原来代码不变,只替换下面一个函数 ----------------
def _start_iproxy(self, udid: str, port: int) -> Optional[subprocess.Popen]: def _start_iproxy(self, udid: str, port: int) -> Optional[subprocess.Popen]:
try: try:

View File

@@ -6,6 +6,7 @@ from pathlib import Path
from queue import Queue from queue import Queue
from typing import Any, Dict from typing import Any, Dict
from Entity import Variables
from Utils.AiUtils import AiUtils from Utils.AiUtils import AiUtils
from Utils.IOSAIStorage import IOSAIStorage from Utils.IOSAIStorage import IOSAIStorage
from Utils.LogManager import LogManager from Utils.LogManager import LogManager
@@ -247,6 +248,7 @@ def longPressAction():
def growAccount(): def growAccount():
body = request.get_json() body = request.get_json()
udid = body.get("udid") udid = body.get("udid")
Variables.commentList = body.get("comment")
manager = ScriptManager() manager = ScriptManager()
event = threading.Event() event = threading.Event()
@@ -286,16 +288,25 @@ def passAnchorData():
try: try:
LogManager.method_info("关注打招呼", "关注打招呼") LogManager.method_info("关注打招呼", "关注打招呼")
data: Dict[str, Any] = request.get_json() data: Dict[str, Any] = request.get_json()
# 设备列表 # 设备列表
idList = data.get("deviceList", []) idList = data.get("deviceList", [])
# 主播列表 # 主播列表
acList = data.get("anchorList", []) acList = data.get("anchorList", [])
Variables.commentList = data.get("comment")
LogManager.info(f"[INFO] 获取数据: {idList} {acList}") LogManager.info(f"[INFO] 获取数据: {idList} {acList}")
AiUtils.save_aclist_flat_append(acList) AiUtils.save_aclist_flat_append(acList)
# 是否需要回复 # 是否需要回复
needReply = data.get("needReply", True) needReply = data.get("needReply", False)
# 是否需要进行翻译
needTranslate = data.get("needTranslate", True)
# 获取打招呼数据 # 获取打招呼数据
ev.prologueList = data.get("prologueList", []) ev.prologueList = data.get("prologueList", [])
@@ -306,7 +317,8 @@ def passAnchorData():
manager = ScriptManager() manager = ScriptManager()
event = threading.Event() event = threading.Event()
# 启动脚本 # 启动脚本
thread = threading.Thread(target=manager.safe_greetNewFollowers, args=(udid, needReply, event)) thread = threading.Thread(target=manager.safe_greetNewFollowers,
args=(udid, needReply, needTranslate, event))
# 添加到线程管理 # 添加到线程管理
ThreadManager.add(udid, thread, event) ThreadManager.add(udid, thread, event)
return ResultData(data="").toJson() return ResultData(data="").toJson()
@@ -330,6 +342,10 @@ def followAndGreetUnion():
# 是否需要回复 # 是否需要回复
needReply = data.get("needReply", True) needReply = data.get("needReply", True)
# 是否需要进行翻译
needTranslate = data.get("needTranslate", True)
# 获取打招呼数据 # 获取打招呼数据
ev.prologueList = data.get("prologueList", []) ev.prologueList = data.get("prologueList", [])
@@ -340,7 +356,8 @@ def followAndGreetUnion():
manager = ScriptManager() manager = ScriptManager()
event = threading.Event() event = threading.Event()
# 启动脚本 # 启动脚本
thread = threading.Thread(target=manager.safe_followAndGreetUnion, args=(udid, needReply, event)) thread = threading.Thread(target=manager.safe_followAndGreetUnion,
args=(udid, needReply, needTranslate, event))
# 添加到线程管理 # 添加到线程管理
ThreadManager.add(udid, thread, event) ThreadManager.add(udid, thread, event)
return ResultData(data="").toJson() return ResultData(data="").toJson()
@@ -378,12 +395,28 @@ def addTempAnchorData():
def getChatTextInfo(): def getChatTextInfo():
data = request.get_json() data = request.get_json()
udid = data.get("udid") udid = data.get("udid")
client = wda.USBClient(udid, wdaFunctionPort) client = wda.USBClient(udid,wdaFunctionPort)
session = client.session() session = client.session()
xml = session.source() xml = session.source()
try: try:
result = AiUtils.extract_messages_from_xml(xml) result = AiUtils.extract_messages_from_xml(xml)
print(result)
last_in = None
last_out = None
for item in reversed(result): # 从后往前找
if item.get('type') != 'msg':
continue
if last_in is None and item['dir'] == 'in':
last_in = item['text']
if last_out is None and item['dir'] == 'out':
last_out = item['text']
if last_in is not None and last_out is not None:
break
print(f"检测出对方的最后一条数据:{last_in},{type(last_in)}")
print(f"检测出我的最后一条数据:{last_out},{type(last_out)}")
return ResultData(data=result).toJson() return ResultData(data=result).toJson()
except Exception as e: except Exception as e:
@@ -410,6 +443,8 @@ def monitorMessages():
LogManager.method_info("开始监控消息,监控消息脚本启动", "监控消息") LogManager.method_info("开始监控消息,监控消息脚本启动", "监控消息")
body = request.get_json() body = request.get_json()
udid = body.get("udid") udid = body.get("udid")
# Variables.commentList = body.get("comment")
manager = ScriptManager() manager = ScriptManager()
event = threading.Event() event = threading.Event()
thread = threading.Thread(target=manager.replyMessages, args=(udid, event)) thread = threading.Thread(target=manager.replyMessages, args=(udid, event))
@@ -525,11 +560,29 @@ def aiConfig():
contactTool = data.get("contactTool") contactTool = data.get("contactTool")
contact = data.get("contact") contact = data.get("contact")
age = data.get("age")
sex = data.get("sex")
height = data.get("height")
weight = data.get("weight")
body_features = data.get("body_features")
nationality = data.get("nationality")
personality = data.get("personality")
strengths = data.get("strengths")
dict = { dict = {
"agentName": agentName, "agentName": agentName,
"guildName": guildName, "guildName": guildName,
"contactTool": contactTool, "contactTool": contactTool,
"contact": contact "contact": contact,
"age": age,
"sex": sex,
"height": height,
"weight": weight,
"body_features": body_features,
"nationality": nationality,
"personality": personality,
"strengths": strengths,
"api-key": "app-sdRfZy2by9Kq7uJg7JdOSVr8"
} }
# JsonUtils.write_json("aiConfig", dict) # JsonUtils.write_json("aiConfig", dict)
@@ -554,9 +607,9 @@ def update_last_message():
updated_count = JsonUtils.update_json_items( updated_count = JsonUtils.update_json_items(
match={"sender": sender, "text": text}, # 匹配条件 match={"sender": sender, "text": text}, # 匹配条件
patch={"state": 1}, # 修改内容 patch={"status": 1}, # 修改内容
filename="last_message.json", # 要修改的文件 filename="log/last_message.json", # 要修改的文件
multi=False # 只改第一条匹配的 multi=True # 只改第一条匹配的
) )
if updated_count > 0: if updated_count > 0:
return ResultData(data=updated_count, message="修改成功").toJson() return ResultData(data=updated_count, message="修改成功").toJson()
@@ -573,15 +626,16 @@ def delete_last_message():
updated_count = JsonUtils.delete_json_items( updated_count = JsonUtils.delete_json_items(
match={"sender": sender, "text": text}, # 匹配条件 match={"sender": sender, "text": text}, # 匹配条件
filename="last_message.json", # 要修改的文件 filename="log/last_message.json", # 要修改的文件
multi=False # 只改第一条匹配的 multi=True # 只改第一条匹配的
) )
if updated_count > 0: if updated_count > 0:
return ResultData(data=updated_count, message="修改成功").toJson() return ResultData(data=updated_count, message="修改成功").toJson()
return ResultData(data=updated_count, message="修改失败").toJson() return ResultData(data=updated_count, message="修改失败").toJson()
# 停止所有任务 # 停止所有任务
@app.route("/stopAllTask", methods=['POST']) @app.route("/stopAllTask", methods=['POST'])
def stopAllTask(): def stopAllTask():
idList = request.get_json() idList = request.get_json()
@@ -594,20 +648,34 @@ def stopAllTask():
def changeAccount(): def changeAccount():
body = request.get_json() body = request.get_json()
udid = body.get("udid") udid = body.get("udid")
account_id = body.get("account_id") if not udid:
return ResultData(data="", code=400, message="缺少 udid").toJson()
IOSAIStorage.save(account_id, f"{udid}/accountId.json")
# 存储到本地
manager = ScriptManager() manager = ScriptManager()
event = threading.Event() threading.Event()
# 启动脚本 # 启动脚本
thread = threading.Thread(target=manager.changeAccount, args=(udid, event)) code, msg = manager.changeAccount(udid)
# 添加到线程管理 # thread = threading.Thread(target=, args=(udid,))
code, msg = ThreadManager.add(udid, thread, event) # # 添加到线程管理
# thread.start()
return ResultData(data="", code=code, message=msg).toJson() return ResultData(data="", code=code, message=msg).toJson()
@app.route('/test', methods=['POST'])
def test():
body = request.get_json()
manager = ScriptManager()
threading.Event()
# 启动脚本
manager.test()
# thread = threading.Thread(target=, args=(udid,))
# # 添加到线程管理
# thread.start()
return ResultData(data="", code=200, message="成功").toJson()
if __name__ == '__main__': if __name__ == '__main__':
app.run("0.0.0.0", port=5000, debug=True, use_reloader=False) app.run("0.0.0.0", port=5000, debug=True, use_reloader=False)

View File

@@ -11,52 +11,101 @@ import unicodedata
import wda import wda
from lxml import etree from lxml import etree
from wda import Client from wda import Client
from Entity.Variables import wdaFunctionPort
from Utils.LogManager import LogManager from Utils.LogManager import LogManager
# 工具类 # 工具类
class AiUtils(object): class AiUtils(object):
# 在屏幕中找到对应的图片 # 在屏幕中找到对应的图片
# @classmethod
# def findImageInScreen(cls, target, udid):
# try:
# # 加载原始图像和模板图像
# image_path = AiUtils.imagePathWithName(udid, "bgv") # 替换为你的图像路径
# template_path = AiUtils.imagePathWithName("", target) # 替换为你的模板路径
#
# # 读取图像和模板,确保它们都是单通道灰度图
# image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
# template = cv2.imread(template_path, cv2.IMREAD_GRAYSCALE)
#
# if image is None:
# LogManager.error("加载背景图失败")
# return -1, -1
#
# if template is None:
# LogManager.error("加载模板图失败")
# return -1, -1
#
# # 获取模板的宽度和高度
# w, h = template.shape[::-1]
#
# # 使用模板匹配方法
# res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED)
# threshold = 0.7 # 匹配度阈值,可以根据需要调整
# loc = np.where(res >= threshold)
#
# # 检查是否有匹配结果
# if loc[0].size > 0:
# # 取第一个匹配位置
# pt = zip(*loc[::-1]).__next__() # 获取第一个匹配点的坐标
# center_x = int(pt[0] + w // 2)
# center_y = int(pt[1] + h // 2)
# # print(f"第一个匹配到的小心心中心坐标: ({center_x}, {center_y})")
# return center_x, center_y
# else:
# return -1, -1
# except Exception as e:
# LogManager.error(f"加载素材失败:{e}", udid)
# print(e)
# return -1, -1
@classmethod @classmethod
def findImageInScreen(cls, target, udid): def findImageInScreen(cls, target, udid):
try: try:
print("参数", target, udid)
# 加载原始图像和模板图像 # 加载原始图像和模板图像
image_path = AiUtils.imagePathWithName(udid, "bgv") # 替换为你的图像路径 image_path = AiUtils.imagePathWithName(udid, "bgv")
template_path = AiUtils.imagePathWithName("", target) # 替换为你的模板路径 template_path = AiUtils.imagePathWithName("", target)
# 读取图像和模板,确保它们都是单通道灰度图 # 读取图像和模板,确保它们都是单通道灰度图
image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE) image = cv2.imread(image_path, cv2.IMREAD_GRAYSCALE)
template = cv2.imread(template_path, cv2.IMREAD_GRAYSCALE) template = cv2.imread(template_path, cv2.IMREAD_GRAYSCALE)
if image is None: if image is None:
LogManager.error("加载背景图失败") LogManager.error("加载背景图失败", udid)
return -1, -1 return -1, -1
if template is None: if template is None:
LogManager.error("加载模板图失败")
LogManager.error("加载模板图失败", udid)
return -1, -1 return -1, -1
# 获取模板的宽度和高度 # 获取模板的宽度和高度
w, h = template.shape[::-1] w, h = template.shape[::-1]
# 使用模板匹配方法 # 模板匹配
res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED) res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED)
threshold = 0.7 # 匹配度阈值,可以根据需要调整 threshold = 0.7
loc = np.where(res >= threshold) loc = np.where(res >= threshold)
# 放在 cv2.matchTemplate 之前
# 检查是否有匹配结果 cv2.imwrite(f'/tmp/runtime_bg_{udid}.png', image)
if loc[0].size > 0: cv2.imwrite(f'/tmp/runtime_tpl_{udid}.png', template)
# 取第一个匹配位置 print(f'>>> 设备{udid} 模板{target} 最高相似度:', cv2.minMaxLoc(res)[1])
pt = zip(*loc[::-1]).__next__() # 获取第一个匹配点的坐标 # 安全取出第一个匹配点
center_x = int(pt[0] + w // 2) matches = list(zip(*loc[::-1]))
center_y = int(pt[1] + h // 2) if not matches:
# print(f"第一个匹配到的小心心中心坐标: ({center_x}, {center_y})")
return center_x, center_y
else:
return -1, -1 return -1, -1
pt = matches[0]
center_x = int(pt[0] + w // 2)
center_y = int(pt[1] + h // 2)
return center_x, center_y
except Exception as e: except Exception as e:
LogManager.error(f"加载素材失败:{e}", udid) LogManager.error(f"加载素材失败:{e}", udid)
print(e)
return -1, -1 return -1, -1
# 使用正则查找字符串中的数字 # 使用正则查找字符串中的数字
@@ -71,7 +120,7 @@ class AiUtils(object):
# 选择截图 # 选择截图
@classmethod @classmethod
def screenshot(cls): def screenshot(cls):
client = wda.USBClient("eca000fcb6f55d7ed9b4c524055214c26a7de7aa") client = wda.USBClient("eca000fcb6f55d7ed9b4c524055214c26a7de7aa",wdaFunctionPort)
session = client.session() session = client.session()
image = session.screenshot() image = session.screenshot()
image_path = "screenshot.png" image_path = "screenshot.png"
@@ -195,10 +244,10 @@ class AiUtils(object):
# click 是否点击该按钮 # click 是否点击该按钮
@classmethod @classmethod
def findHomeButton(cls, udid="eca000fcb6f55d7ed9b4c524055214c26a7de7aa"): def findHomeButton(cls, udid="eca000fcb6f55d7ed9b4c524055214c26a7de7aa"):
client = wda.USBClient(udid) client = wda.USBClient(udid,wdaFunctionPort)
session = client.session() session = client.session()
session.appium_settings({"snapshotMaxDepth": 10}) session.appium_settings({"snapshotMaxDepth": 10})
homeButton = session.xpath( "//XCUIElementTypeButton[@name='a11y_vo_home' or @label='Home' or @label='首页']") homeButton = session.xpath("//XCUIElementTypeButton[@name='a11y_vo_home' or @label='Home' or @label='首页']")
try: try:
if homeButton.exists: if homeButton.exists:
print("找到首页了") print("找到首页了")
@@ -213,7 +262,7 @@ class AiUtils(object):
# 查找关闭按钮 # 查找关闭按钮
@classmethod @classmethod
def findLiveCloseButton(cls, udid="eca000fcb6f55d7ed9b4c524055214c26a7de7aa"): def findLiveCloseButton(cls, udid="eca000fcb6f55d7ed9b4c524055214c26a7de7aa"):
client = wda.USBClient(udid) client = wda.USBClient(udid,wdaFunctionPort)
session = client.session() session = client.session()
session.appium_settings({"snapshotMaxDepth": 10}) session.appium_settings({"snapshotMaxDepth": 10})
r = session.xpath("//XCUIElementTypeButton[@name='关闭屏幕']") r = session.xpath("//XCUIElementTypeButton[@name='关闭屏幕']")
@@ -288,7 +337,7 @@ class AiUtils(object):
# 获取当前屏幕上的节点 # 获取当前屏幕上的节点
@classmethod @classmethod
def getCurrentScreenSource(cls): def getCurrentScreenSource(cls):
client = wda.USBClient("eca000fcb6f55d7ed9b4c524055214c26a7de7aa") client = wda.USBClient("eca000fcb6f55d7ed9b4c524055214c26a7de7aa",wdaFunctionPort)
print(client.source()) print(client.source())
# 查找app主页上的收件箱按钮 # 查找app主页上的收件箱按钮
@@ -308,8 +357,13 @@ class AiUtils(object):
print(f"btn:{btn}") print(f"btn:{btn}")
return cls.findNumber(btn.label) return cls.findNumber(btn.label)
@classmethod
def parse_float(cls, el, attr, default=0.0):
try:
return float(el.get(attr, default))
except Exception:
return default
# # 识别当前页面的消息
# @classmethod # @classmethod
# def extract_messages_from_xml(cls, xml: str): # def extract_messages_from_xml(cls, xml: str):
# """ # """
@@ -331,17 +385,21 @@ class AiUtils(object):
# return html.unescape(s.strip()) # return html.unescape(s.strip())
# #
# def is_visible(el): # def is_visible(el):
# """无 visible 属性按可见处理;有且为 'false' 才视为不可见。"""
# v = el.get('visible') # v = el.get('visible')
# return (v is None) or (v.lower() == 'true') # return (v is None) or (v.lower() == 'true')
# #
# def get_ancestor_cell(el):
# p = el
# while p is not None and p.get('type') != 'XCUIElementTypeCell':
# p = p.getparent()
# return p
#
# # ---------- 屏幕尺寸 ---------- # # ---------- 屏幕尺寸 ----------
# app = root.xpath('/XCUIElementTypeApplication') # app = root.xpath('/XCUIElementTypeApplication')
# screen_w = cls.parse_float(app[0], 'width', 414.0) if app else 414.0 # screen_w = cls.parse_float(app[0], 'width', 414.0) if app else 414.0
# screen_h = cls.parse_float(app[0], 'height', 736.0) if app else 736.0 # screen_h = cls.parse_float(app[0], 'height', 736.0) if app else 736.0
# #
# # ---------- 主容器探测(评分选择最像聊天区的容器) ---------- # # ---------- 主容器探测 ----------
#
# def pick_container(): # def pick_container():
# cands = [] # cands = []
# for xp, ctype in ( # for xp, ctype in (
@@ -353,7 +411,6 @@ class AiUtils(object):
# for n in nodes: # for n in nodes:
# y = cls.parse_float(n, 'y', 0.0) # y = cls.parse_float(n, 'y', 0.0)
# h = cls.parse_float(n, 'height', screen_h) # h = cls.parse_float(n, 'height', screen_h)
# # Cell 数越多越像聊天列表;越靠中间越像
# cells = n.xpath('.//XCUIElementTypeCell') # cells = n.xpath('.//XCUIElementTypeCell')
# score = len(cells) * 10 - abs((y + h / 2) - screen_h / 2) # score = len(cells) * 10 - abs((y + h / 2) - screen_h / 2)
# cands.append((score, n, ctype)) # cands.append((score, n, ctype))
@@ -364,13 +421,12 @@ class AiUtils(object):
# #
# container, container_type = pick_container() # container, container_type = pick_container()
# #
# # ---------- 可视区area_top, area_bot ---------- # # ---------- 可视区 ----------
# if container is not None: # if container is not None:
# area_top = cls.parse_float(container, 'y', 0.0) # area_top = cls.parse_float(container, 'y', 0.0)
# area_h = cls.parse_float(container, 'height', screen_h) # area_h = cls.parse_float(container, 'height', screen_h)
# area_bot = area_top + area_h # area_bot = area_top + area_h
# else: # else:
# # 顶栏底缘作为上边界(选最靠上的宽>200的块
# blocks = [n for n in root.xpath('//XCUIElementTypeOther[@y and @height and @width>="200"]') if # blocks = [n for n in root.xpath('//XCUIElementTypeOther[@y and @height and @width>="200"]') if
# is_visible(n)] # is_visible(n)]
# area_top = 0.0 # area_top = 0.0
@@ -378,7 +434,6 @@ class AiUtils(object):
# blocks.sort(key=lambda n: cls.parse_float(n, 'y', 0.0)) # blocks.sort(key=lambda n: cls.parse_float(n, 'y', 0.0))
# b = blocks[0] # b = blocks[0]
# area_top = cls.parse_float(b, 'y', 0.0) + cls.parse_float(b, 'height', 0.0) # area_top = cls.parse_float(b, 'y', 0.0) + cls.parse_float(b, 'height', 0.0)
# # 输入框 TextView 顶边作为下边界
# tvs = [n for n in root.xpath('//XCUIElementTypeTextView') if is_visible(n)] # tvs = [n for n in root.xpath('//XCUIElementTypeTextView') if is_visible(n)]
# if tvs: # if tvs:
# tvs.sort(key=lambda n: cls.parse_float(n, 'y', 0.0)) # tvs.sort(key=lambda n: cls.parse_float(n, 'y', 0.0))
@@ -394,10 +449,10 @@ class AiUtils(object):
# y = cls.parse_float(el, 'y', -1e9) # y = cls.parse_float(el, 'y', -1e9)
# h = cls.parse_float(el, 'height', 0.0) # h = cls.parse_float(el, 'height', 0.0)
# by = y + h # by = y + h
# tol = 8.0 # 容差,避免边缘误判 # tol = 8.0
# return not (by <= area_top + tol or y >= area_bot - tol) # return not (by <= area_top + tol or y >= area_bot - tol)
# #
# # ---------- 时间分隔Header ---------- # # ---------- 时间分隔 ----------
# items = [] # items = []
# for t in root.xpath('//XCUIElementTypeStaticText[contains(@traits, "Header")]'): # for t in root.xpath('//XCUIElementTypeStaticText[contains(@traits, "Header")]'):
# if not in_view(t): # if not in_view(t):
@@ -410,10 +465,12 @@ class AiUtils(object):
# EXCLUDES_LITERAL = { # EXCLUDES_LITERAL = {
# 'Heart', 'Lol', 'ThumbsUp', # 'Heart', 'Lol', 'ThumbsUp',
# '分享发布内容', '视频贴纸标签页', '双击发送表情', '贴纸', # '分享发布内容', '视频贴纸标签页', '双击发送表情', '贴纸',
# '关注',
# } # }
# SYSTEM_PATTERNS = [ # SYSTEM_PATTERNS = [
# r"(消息请求已被接受|你开始了和.*的聊天|你打开了这个与.*的聊天).*" # r"消息请求已被接受。你们可以开始聊天了。",
# r"回复时接收通知", r"开启(私信)?通知", r"开启通知", # r"(消息请求已被接受|你开始了和.*的聊天|你打开了这个与.*的聊天).*",
# r"开启(私信)?通知", r"开启通知",
# r"你打开了这个与 .* 的聊天。.*隐私", # r"你打开了这个与 .* 的聊天。.*隐私",
# r"在此用户接受你的消息请求之前,你最多只能发送 ?\d+ 条消息。?", # r"在此用户接受你的消息请求之前,你最多只能发送 ?\d+ 条消息。?",
# r"聊天消息条数已达上限,你将无法向该用户发送消息。?", # r"聊天消息条数已达上限,你将无法向该用户发送消息。?",
@@ -423,10 +480,43 @@ class AiUtils(object):
# r"Get notified when .* replies", # r"Get notified when .* replies",
# r"You opened this chat .* privacy", # r"You opened this chat .* privacy",
# r"Only \d+ message can be sent .* accepts .* request", # r"Only \d+ message can be sent .* accepts .* request",
# r"此消息可能违反.*",
# r"无法发送",
# r"请告知我们"
# ] # ]
# SYSTEM_RE = re.compile("|".join(SYSTEM_PATTERNS), re.IGNORECASE) # SYSTEM_RE = re.compile("|".join(SYSTEM_PATTERNS), re.IGNORECASE)
# #
# # 排除底部贴纸/GIF/分享栏(通常是位于底部、较矮的一排 CollectionView # # ---------- 资料卡片(个人信息)剔除 ----------
# PROFILE_RE = re.compile(
# r"@[\w\.\-]+|粉丝|followers?|following|关注账号",
# re.IGNORECASE
# )
#
# def is_profile_cell(cell) -> bool:
# if cell is None:
# return False
# if cell.xpath(
# './/XCUIElementTypeButton[@name="关注" or @label="关注" or '
# 'contains(translate(@name,"FOLW","folw"),"follow") or '
# 'contains(translate(@label,"FOLW","folw"),"follow")]'
# ):
# return True
# texts = []
# for t in cell.xpath('.//*[@name or @label or @value]'):
# s = get_text(t)
# if s:
# texts.append(s)
# if len(texts) > 40:
# break
# joined = " ".join(texts)
# if PROFILE_RE.search(joined):
# return True
# cy = cls.parse_float(cell, 'y', 0.0)
# ch = cls.parse_float(cell, 'height', 0.0)
# if cy < area_top + 140 and ch >= 150:
# return True
# return False
#
# def is_toolbar_like(o) -> bool: # def is_toolbar_like(o) -> bool:
# txt = get_text(o) # txt = get_text(o)
# if txt in EXCLUDES_LITERAL: # if txt in EXCLUDES_LITERAL:
@@ -440,7 +530,6 @@ class AiUtils(object):
# # ---------- 收集消息候选 ---------- # # ---------- 收集消息候选 ----------
# msg_nodes = [] # msg_nodes = []
# if container is not None: # if container is not None:
# # 容器内优先找 Cell 下的文本节点Other/StaticText/TextView
# cand = container.xpath( # cand = container.xpath(
# './/XCUIElementTypeCell//*[self::XCUIElementTypeOther or self::XCUIElementTypeStaticText or self::XCUIElementTypeTextView]' # './/XCUIElementTypeCell//*[self::XCUIElementTypeOther or self::XCUIElementTypeStaticText or self::XCUIElementTypeTextView]'
# '[@y and (@name or @label or @value)]' # '[@y and (@name or @label or @value)]'
@@ -450,12 +539,14 @@ class AiUtils(object):
# continue # continue
# if is_toolbar_like(o): # if is_toolbar_like(o):
# continue # continue
# cell = get_ancestor_cell(o)
# if is_profile_cell(cell):
# continue
# txt = get_text(o) # txt = get_text(o)
# if not txt or SYSTEM_RE.search(txt): # if not txt or SYSTEM_RE.search(txt) or txt in EXCLUDES_LITERAL:
# continue # continue
# msg_nodes.append(o) # msg_nodes.append(o)
# else: # else:
# # 全局兜底:排除直接挂在 CollectionView底部工具栏下的节点
# cand = root.xpath( # cand = root.xpath(
# '//XCUIElementTypeOther[@y and (@name or @label or @value)]' # '//XCUIElementTypeOther[@y and (@name or @label or @value)]'
# ' | //XCUIElementTypeStaticText[@y and (@name or @label or @value)]' # ' | //XCUIElementTypeStaticText[@y and (@name or @label or @value)]'
@@ -467,37 +558,37 @@ class AiUtils(object):
# continue # continue
# if not in_view(o) or is_toolbar_like(o): # if not in_view(o) or is_toolbar_like(o):
# continue # continue
# cell = get_ancestor_cell(o)
# if is_profile_cell(cell):
# continue
# txt = get_text(o) # txt = get_text(o)
# if not txt or SYSTEM_RE.search(txt): # if not txt or SYSTEM_RE.search(txt) or txt in EXCLUDES_LITERAL:
# continue # continue
# msg_nodes.append(o) # msg_nodes.append(o)
# #
# # ---------- 方向判定 & 组装 ---------- # # ---------- 方向判定 & 组装(中心点法) ----------
# CENTER_MARGIN = max(12.0, screen_w * 0.02) # 中线容差
#
# for o in msg_nodes: # for o in msg_nodes:
# txt = get_text(o) # txt = get_text(o)
# if not txt or txt in EXCLUDES_LITERAL: # if not txt or txt in EXCLUDES_LITERAL or SYSTEM_RE.search(txt):
# continue # continue
# #
# # 找所在 Cell用于查头像
# cell = o.getparent()
# while cell is not None and cell.get('type') != 'XCUIElementTypeCell':
# cell = cell.getparent()
#
# x = cls.parse_float(o, 'x', 0.0) # x = cls.parse_float(o, 'x', 0.0)
# y = cls.parse_float(o, 'y', 0.0) # y = cls.parse_float(o, 'y', 0.0)
# w = cls.parse_float(o, 'width', 0.0) # w = cls.parse_float(o, 'width', 0.0)
# right_edge = x + w
# #
# direction = None # center_x = x + w / 2.0
# if cell is not None: # screen_center = screen_w / 2.0
# avatars = [a for a in cell.xpath( #
# './/XCUIElementTypeButton[@visible="true" and (@name="图片头像" or @label="图片头像")]' # if center_x < screen_center - CENTER_MARGIN:
# ) if is_visible(a)] # direction = 'in' # 左侧:对方
# if avatars: # elif center_x > screen_center + CENTER_MARGIN:
# ax = cls.parse_float(avatars[0], 'x', 0.0) # direction = 'out' # 右侧:自己
# direction = 'in' if ax < (screen_w / 2) else 'out' # else:
# if direction is None: # # 处在中线附近,用右缘兜底
# direction = 'out' if right_edge > (screen_w * 0.75) else 'in' # right_edge = x + w
# direction = 'out' if right_edge >= screen_center else 'in'
# #
# items.append({'type': 'msg', 'dir': direction, 'text': txt, 'y': y}) # items.append({'type': 'msg', 'dir': direction, 'text': txt, 'y': y})
# #
@@ -507,31 +598,32 @@ class AiUtils(object):
# for it in items: # for it in items:
# it.pop('y', None) # it.pop('y', None)
# return items # return items
#
#
# @classmethod
# def parse_float(cls, el, attr, default=0.0):
# try:
# v = el.get(attr)
# if v is None:
# return default
# return float(v)
# except Exception:
# return default
@classmethod @staticmethod
def parse_float(cls, el, attr, default=0.0): def parse_float(el, key: str, default: float = 0.0) -> float:
try: """稳健读取浮点属性"""
return float(el.get(attr, default)) if el is None:
except Exception:
return default return default
v = el.get(key)
if v is None or v == "":
return default
try:
return float(v)
except Exception:
try:
# 某些抓取会出现 '20.0px' / '20,' 等
v2 = re.sub(r"[^\d\.\-]+", "", v)
return float(v2) if v2 else default
except Exception:
return default
@classmethod @classmethod
def extract_messages_from_xml(cls, xml: str): def extract_messages_from_xml(cls, xml: str):
""" """
解析 TikTok 聊天 XML返回当前屏幕可见的消息与时间分隔 解析 TikTok 聊天 XML返回当前屏幕可见的消息与时间分隔
[{"type":"time","text":"..."}, {"type":"msg","dir":"in|out","text":"..."}] [{"type":"time","text":"..."}, {"type":"msg","dir":"in|out","text":"..."}]
兼容 Table / CollectionView / ScrollView过滤系统提示/底部工具栏;可见性使用“重叠可视+容差”。 兼容 Table / CollectionView / ScrollView过滤系统提示/底部工具栏;
资料卡只过滤“资料区块”而非整 Cell可见性使用“重叠可视+容差”。
""" """
if not isinstance(xml, str) or not xml.strip(): if not isinstance(xml, str) or not xml.strip():
return [] return []
@@ -550,6 +642,20 @@ class AiUtils(object):
v = el.get('visible') v = el.get('visible')
return (v is None) or (v.lower() == 'true') return (v is None) or (v.lower() == 'true')
def get_ancestor_cell(el):
p = el
while p is not None and p.get('type') != 'XCUIElementTypeCell':
p = p.getparent()
return p
def _bbox(el):
return (
cls.parse_float(el, 'x', 0.0),
cls.parse_float(el, 'y', 0.0),
cls.parse_float(el, 'width', 0.0),
cls.parse_float(el, 'height', 0.0),
)
# ---------- 屏幕尺寸 ---------- # ---------- 屏幕尺寸 ----------
app = root.xpath('/XCUIElementTypeApplication') app = root.xpath('/XCUIElementTypeApplication')
screen_w = cls.parse_float(app[0], 'width', 414.0) if app else 414.0 screen_w = cls.parse_float(app[0], 'width', 414.0) if app else 414.0
@@ -621,6 +727,7 @@ class AiUtils(object):
EXCLUDES_LITERAL = { EXCLUDES_LITERAL = {
'Heart', 'Lol', 'ThumbsUp', 'Heart', 'Lol', 'ThumbsUp',
'分享发布内容', '视频贴纸标签页', '双击发送表情', '贴纸', '分享发布内容', '视频贴纸标签页', '双击发送表情', '贴纸',
'关注', # 注意:仅用于按钮/工具条等短元素,后续还会叠加区域过滤,避免误杀消息
} }
SYSTEM_PATTERNS = [ SYSTEM_PATTERNS = [
r"消息请求已被接受。你们可以开始聊天了。", r"消息请求已被接受。你们可以开始聊天了。",
@@ -635,13 +742,105 @@ class AiUtils(object):
r"Get notified when .* replies", r"Get notified when .* replies",
r"You opened this chat .* privacy", r"You opened this chat .* privacy",
r"Only \d+ message can be sent .* accepts .* request", r"Only \d+ message can be sent .* accepts .* request",
r"此消息可能违反.*", r"此消息可能违反.*",
r"无法发送", r"无法发送",
r"请告知我们" r"请告知我们"
] ]
SYSTEM_RE = re.compile("|".join(SYSTEM_PATTERNS), re.IGNORECASE) SYSTEM_RE = re.compile("|".join(SYSTEM_PATTERNS), re.IGNORECASE)
# ---------- 资料卡片(个人信息)剔除:仅过滤“资料区块” ----------
PROFILE_RE = re.compile(
r"@[\w\.\-]+|粉丝|followers?|following|关注账号",
re.IGNORECASE
)
def is_profile_cell(cell) -> bool:
"""更严格:至少同时命中 >=2 个信号才认定为资料卡片 Cell。"""
if cell is None:
return False
has_follow_btn = bool(cell.xpath(
'.//XCUIElementTypeButton['
'@name="关注" or @label="关注" or '
'contains(translate(@name,"FOLW","folw"),"follow") or '
'contains(translate(@label,"FOLW","folw"),"follow")]'
))
has_view_profile = bool(cell.xpath(
'.//XCUIElementTypeButton['
'@name="查看主页" or @label="查看主页" or '
'contains(translate(@name,"VIEW PROFILE","view profile"),"view profile") or '
'contains(translate(@label,"VIEW PROFILE","view profile"),"view profile")]'
))
has_live_ended = bool(cell.xpath(
'.//XCUIElementTypeStaticText['
'@name="直播已结束" or @label="直播已结束" or '
'contains(translate(@name,"LIVE ENDED","live ended"),"live ended") or '
'contains(translate(@label,"LIVE ENDED","live ended"),"live ended")]'
))
cy = cls.parse_float(cell, 'y', 0.0)
ch = cls.parse_float(cell, 'height', 0.0)
looks_large_card = ch >= 180 # 大卡片外观
# 再做一次文本特征检查(防止仅一个“关注”误杀)
texts = []
for t in cell.xpath('.//*[@name or @label or @value]'):
s = get_text(t)
if s:
texts.append(s)
if len(texts) > 40:
break
joined = " ".join(texts)
has_profile_terms = bool(PROFILE_RE.search(joined))
# 命中信号计数至少2个
signals = sum([has_follow_btn, has_view_profile, has_live_ended, looks_large_card, has_profile_terms])
return signals >= 2
def profile_region_y_range(cell):
"""
在资料卡 Cell 内,估算“资料区块”的 y 范围min_y, max_y
用关键元素(关注按钮 / 查看主页 / 直播已结束 / 短用户名)来圈定范围。
"""
if cell is None:
return None
key_nodes = []
key_nodes += cell.xpath('.//XCUIElementTypeButton[@name="关注" or @label="关注"]')
key_nodes += cell.xpath('.//XCUIElementTypeButton[@name="查看主页" or @label="查看主页"]')
key_nodes += cell.xpath('.//XCUIElementTypeStaticText[@name="直播已结束" or @label="直播已结束"]')
# 用户名/昵称:长度较短更像资料区标签
for t in cell.xpath('.//XCUIElementTypeStaticText[@name or @label]'):
s = (t.get('label') or t.get('name') or '') or ''
st = s.strip()
if st and len(st) <= 30:
key_nodes.append(t)
ys = []
for n in key_nodes:
_, y, _, h = _bbox(n)
ys += [y, y + h]
if not ys:
return None # 没有关键元素则不定义资料区
min_y, max_y = min(ys), max(ys)
pad = 12.0
return (min_y - pad, max_y + pad)
def belongs_to_profile_region(node, cell) -> bool:
"""判断候选 node 是否落在资料区块的 y 范围内"""
rng = profile_region_y_range(cell)
if not rng:
return False
_, y, _, h = _bbox(node)
ny1, ny2 = y, y + h
ry1, ry2 = rng
return not (ny2 < ry1 or ny1 > ry2) # 任意重叠即算属于资料区
def is_toolbar_like(o) -> bool: def is_toolbar_like(o) -> bool:
txt = get_text(o) txt = get_text(o)
if txt in EXCLUDES_LITERAL: if txt in EXCLUDES_LITERAL:
@@ -664,8 +863,12 @@ class AiUtils(object):
continue continue
if is_toolbar_like(o): if is_toolbar_like(o):
continue continue
cell = get_ancestor_cell(o)
# 仅在“资料卡 Cell 且节点位于资料区块范围内”时过滤
if is_profile_cell(cell) and belongs_to_profile_region(o, cell):
continue
txt = get_text(o) txt = get_text(o)
if not txt or SYSTEM_RE.search(txt): if not txt or SYSTEM_RE.search(txt) or txt in EXCLUDES_LITERAL:
continue continue
msg_nodes.append(o) msg_nodes.append(o)
else: else:
@@ -680,41 +883,37 @@ class AiUtils(object):
continue continue
if not in_view(o) or is_toolbar_like(o): if not in_view(o) or is_toolbar_like(o):
continue continue
cell = get_ancestor_cell(o)
if is_profile_cell(cell) and belongs_to_profile_region(o, cell):
continue
txt = get_text(o) txt = get_text(o)
if not txt or SYSTEM_RE.search(txt): if not txt or SYSTEM_RE.search(txt) or txt in EXCLUDES_LITERAL:
continue continue
msg_nodes.append(o) msg_nodes.append(o)
# ---------- 方向判定 & 组装 ---------- # ---------- 方向判定 & 组装(中心点法) ----------
CENTER_MARGIN = max(12.0, screen_w * 0.02) # 中线容差
for o in msg_nodes: for o in msg_nodes:
txt = get_text(o) txt = get_text(o)
if not txt or txt in EXCLUDES_LITERAL or SYSTEM_RE.search(txt): if not txt or txt in EXCLUDES_LITERAL or SYSTEM_RE.search(txt):
continue continue
cell = o.getparent()
while cell is not None and cell.get('type') != 'XCUIElementTypeCell':
cell = cell.getparent()
x = cls.parse_float(o, 'x', 0.0) x = cls.parse_float(o, 'x', 0.0)
y = cls.parse_float(o, 'y', 0.0) y = cls.parse_float(o, 'y', 0.0)
w = cls.parse_float(o, 'width', 0.0) w = cls.parse_float(o, 'width', 0.0)
right_edge = x + w
direction = None center_x = x + w / 2.0
if cell is not None: screen_center = screen_w / 2.0
avatars = [a for a in cell.xpath(
'.//XCUIElementTypeButton[@visible="true" and (@name="Profile photo" or @label="Profile photo")]'
) if is_visible(a)]
if not avatars and SYSTEM_RE.search(txt):
continue # 没头像且系统消息,直接跳过
if avatars:
ax = cls.parse_float(avatars[0], 'x', 0.0)
direction = 'in' if ax < (screen_w / 2) else 'out'
if direction is None: if center_x < screen_center - CENTER_MARGIN:
if w > screen_w * 0.8 and SYSTEM_RE.search(txt): direction = 'in' # 左侧:对方
continue elif center_x > screen_center + CENTER_MARGIN:
direction = 'out' if right_edge > (screen_w * 0.75) else 'in' direction = 'out' # 右侧:自己
else:
# 处在中线附近,用右缘兜底
right_edge = x + w
direction = 'out' if right_edge >= screen_center else 'in'
items.append({'type': 'msg', 'dir': direction, 'text': txt, 'y': y}) items.append({'type': 'msg', 'dir': direction, 'text': txt, 'y': y})
@@ -725,11 +924,6 @@ class AiUtils(object):
it.pop('y', None) it.pop('y', None)
return items return items
@classmethod @classmethod
def get_navbar_anchor_name(cls, session, timeout: float = 5) -> str: def get_navbar_anchor_name(cls, session, timeout: float = 5) -> str:
"""从聊天页导航栏读取主播名称;找不到返回空字符串。""" """从聊天页导航栏读取主播名称;找不到返回空字符串。"""
@@ -818,8 +1012,6 @@ class AiUtils(object):
return "" return ""
# 检查字符串中是否包含中文 # 检查字符串中是否包含中文
@classmethod @classmethod
def contains_chinese(cls, text): def contains_chinese(cls, text):
@@ -863,38 +1055,6 @@ class AiUtils(object):
with open(file_path, "w", encoding="utf-8") as f: with open(file_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2) json.dump(data, f, ensure_ascii=False, indent=2)
# @staticmethod
# def _normalize_anchor_items(items):
# """
# 规范化输入为 [{anchorId, country}] 的列表:
# - 允许传入:单个对象、对象列表、字符串(当 anchorId 用)
# - 过滤不合规项
# """
# result = []
# if items is None:
# return result
#
# if isinstance(items, dict):
# # 单个对象
# aid = items.get("anchorId")
# if aid:
# result.append({"anchorId": str(aid), "country": items.get("country", "")})
# return result
#
# if isinstance(items, list):
# for it in items:
# if isinstance(it, dict):
# aid = it.get("anchorId")
# if aid:
# result.append({"anchorId": str(aid), "country": it.get("country", "")})
# elif isinstance(it, str):
# result.append({"anchorId": it, "country": ""})
# return result
#
# if isinstance(items, str):
# result.append({"anchorId": items, "country": ""})
# return result
@staticmethod @staticmethod
def _normalize_anchor_items(items): def _normalize_anchor_items(items):
""" """
@@ -929,7 +1089,6 @@ class AiUtils(object):
result.append({"anchorId": items}) result.append({"anchorId": items})
return result return result
# -------- 追加(对象数组平铺追加) -------- # -------- 追加(对象数组平铺追加) --------
@classmethod @classmethod
def save_aclist_flat_append(cls, acList, filename="log/acList.json"): def save_aclist_flat_append(cls, acList, filename="log/acList.json"):
@@ -958,7 +1117,6 @@ class AiUtils(object):
# LogManager.method_info(f"写入的路径是:{file_path}", "写入数据") # LogManager.method_info(f"写入的路径是:{file_path}", "写入数据")
LogManager.info(f"[acList] 已追加 {len(to_add)} 条,当前总数={len(data)} -> {file_path}") LogManager.info(f"[acList] 已追加 {len(to_add)} 条,当前总数={len(data)} -> {file_path}")
@classmethod @classmethod
def pop_aclist_first(cls, filename="log/acList.json", mode="pop"): def pop_aclist_first(cls, filename="log/acList.json", mode="pop"):
""" """
@@ -1166,8 +1324,6 @@ class AiUtils(object):
print(f"[peek] 读取失败: {e}") print(f"[peek] 读取失败: {e}")
return None return None
@staticmethod @staticmethod
def run_tidevice_command(udid, action, bundle_id, timeout=30): def run_tidevice_command(udid, action, bundle_id, timeout=30):
""" """
@@ -1204,7 +1360,8 @@ class AiUtils(object):
return False return False
except FileNotFoundError: except FileNotFoundError:
# 处理tidevice命令未找到的情况通常意味着tidevice未安装或不在PATH中 # 处理tidevice命令未找到的情况通常意味着tidevice未安装或不在PATH中
LogManager.error("The 'tidevice' command was not found. Please ensure it is installed and in your system PATH.") LogManager.error(
"The 'tidevice' command was not found. Please ensure it is installed and in your system PATH.")
return False return False
except Exception as e: except Exception as e:
# 捕获其他可能异常 # 捕获其他可能异常
@@ -1235,3 +1392,5 @@ class AiUtils(object):
return cls.run_tidevice_command(udid, "launch", bundle_id, timeout) return cls.run_tidevice_command(udid, "launch", bundle_id, timeout)

View File

@@ -58,13 +58,6 @@ class ControlUtils(object):
@classmethod @classmethod
def clickBack(cls, session: Client): def clickBack(cls, session: Client):
try: try:
# back = session.xpath(
# "//*[@label='返回']"
# " | "
# "//*[@label='返回上一屏幕']"
# " | "
# "//XCUIElementTypeButton[@visible='true' and @name='TTKProfileNavBarBaseItemComponent' and @label='IconChevronLeftOffsetLTR']"
# )
back = session.xpath( back = session.xpath(
# ① 常见中文文案 # ① 常见中文文案
@@ -80,7 +73,6 @@ class ControlUtils(object):
")]" ")]"
) )
if back.exists: if back.exists:
back.click() back.click()
return True return True
@@ -96,6 +88,13 @@ class ControlUtils(object):
if back.exists: if back.exists:
back.click() 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: else:
return False return False
except Exception as e: except Exception as e:
@@ -148,10 +147,9 @@ class ControlUtils(object):
videoCell = session.xpath( videoCell = session.xpath(
'(//XCUIElementTypeCollectionView//XCUIElementTypeCell[.//XCUIElementTypeImage[@name="profile_video"]])[1]') '(//XCUIElementTypeCollectionView//XCUIElementTypeCell[.//XCUIElementTypeImage[@name="profile_video"]])[1]')
tab = session.xpath( tab = session.xpath(
'//XCUIElementTypeButton[@name="TTKProfileTabVideoButton_0" or contains(@label,"作品") or contains(@name,"作品")]' '//XCUIElementTypeButton[@name="TTKProfileTabVideoButton_0" or contains(@label,"作品") or contains(@name,"作品")]'
).get(timeout=5) # 某些版本 tab.value 可能就是数量;或者 tab.label 类似 “作品 7” ).get(timeout=5) # 某些版本 tab.value 可能就是数量;或者 tab.label 类似 “作品 7”
m = re.search(r"\d+", tab.label) m = re.search(r"\d+", tab.label)
num = 0 num = 0
@@ -170,8 +168,6 @@ class ControlUtils(object):
print("没有找到主页的第一个视频") print("没有找到主页的第一个视频")
return False, num return False, num
@classmethod @classmethod
def clickFollow(cls, session, aid): def clickFollow(cls, session, aid):
# 1) 含“关注/已关注/Follow/Following”的首个 cell # 1) 含“关注/已关注/Follow/Following”的首个 cell
@@ -199,6 +195,7 @@ 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 @classmethod
def userClickProfile(cls, session, aid): def userClickProfile(cls, session, aid):
try: try:
@@ -283,7 +280,3 @@ class ControlUtils(object):
print("开始微滑动") print("开始微滑动")
session.swipe(center_x, center_y, end_x, end_y, duration_ms / 1000) session.swipe(center_x, center_y, end_x, end_y, duration_ms / 1000)
print("随机微滑动:", trajectory) print("随机微滑动:", trajectory)

View File

@@ -13,19 +13,6 @@ class IOSAIStorage:
iosai_dir.mkdir(parents=True, exist_ok=True) iosai_dir.mkdir(parents=True, exist_ok=True)
return iosai_dir return iosai_dir
# @classmethod
# def save(cls, data: dict | list, filename: str = "data.json") -> Path:
# """
# 存储数据到 C:/Users/<用户名>/IOSAI/filename
# """
# file_path = cls._get_iosai_dir() / filename
# try:
# 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
@classmethod @classmethod
def save(cls, data: dict | list, filename: str = "data.json", mode: str = "overwrite") -> Path: def save(cls, data: dict | list, filename: str = "data.json", mode: str = "overwrite") -> Path:

View File

@@ -1,6 +1,8 @@
import json import json
import os import os
from pathlib import Path from pathlib import Path
from typing import Dict, Any
import portalocker as locker # ① 引入跨平台锁 import portalocker as locker # ① 引入跨平台锁
@@ -118,11 +120,33 @@ class JsonUtils:
json.dump(data, f, ensure_ascii=False, indent=4) 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 @classmethod
def append_json_items(cls, items, filename="log/last_message.json"): def append_json_items(cls, items, filename="log/last_message.json"):
"""
将 dict 或 [dict, ...] 追加到 JSON 文件(数组)中;不校验字段。
"""
file_path = Path(filename) file_path = Path(filename)
data = cls._read_json_list(file_path) data = cls._read_json_list(file_path)
@@ -130,20 +154,19 @@ class JsonUtils:
if isinstance(items, dict): if isinstance(items, dict):
items = [items] items = [items]
elif not isinstance(items, list): elif not isinstance(items, list):
# 既不是 dict 也不是 list直接忽略
return return
# 只接受字典 # 只保留 sender 非空的字典
items = [it for it in items if isinstance(it, dict)] items = [
it for it in items
if isinstance(it, dict) and it.get("sender") != ""
]
if not items: if not items:
return return
data.extend(items) data.extend(items)
# LogManager.method_info(filename,"路径")
cls._write_json_list(file_path, data) cls._write_json_list(file_path, data)
@classmethod @classmethod
def update_json_items(cls, match: dict, patch: dict, filename="log/last_message.json", multi: bool = True) -> int: def update_json_items(cls, match: dict, patch: dict, filename="log/last_message.json", multi: bool = True) -> int:
""" """
@@ -177,17 +200,8 @@ class JsonUtils:
return updated return updated
# @classmethod
# def query_all_json_items(cls, filename="log/last_message.json") -> list:
# """
# 查询 JSON 文件(数组)中的所有项
# :param filename: JSON 文件路径
# :return: list可能为空
# """
# file_path = Path(filename)
# print(file_path)
# data = cls._read_json_list(file_path)
# return data if isinstance(data, list) else []
@classmethod @classmethod
def query_all_json_items(cls, filename="log/last_message.json") -> list: def query_all_json_items(cls, filename="log/last_message.json") -> list:

View File

@@ -1,5 +1,5 @@
import requests import requests
from Entity.Variables import prologueList from Entity.Variables import prologueList, API_KEY
from Utils.IOSAIStorage import IOSAIStorage from Utils.IOSAIStorage import IOSAIStorage
from Utils.JsonUtils import JsonUtils from Utils.JsonUtils import JsonUtils
from Utils.LogManager import LogManager from Utils.LogManager import LogManager
@@ -84,31 +84,63 @@ class Requester():
# ai聊天 # ai聊天
@classmethod @classmethod
def chatToAi(cls, param): def chatToAi(cls, param):
aiConfig = JsonUtils.read_json("aiConfig")
# aiConfig = JsonUtils.read_json("aiConfig")
aiConfig = IOSAIStorage.load("aiConfig.json") aiConfig = IOSAIStorage.load("aiConfig.json")
agentName = aiConfig.get("agentName") agentName = aiConfig.get("agentName")
guildName = aiConfig.get("guildName") guildName = aiConfig.get("guildName")
contactTool = aiConfig.get("contactTool", "") contactTool = aiConfig.get("contactTool", "")
contact = aiConfig.get("contact", "") 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 = { inputs = {
"name": agentName, "name": agentName,
"Trade_union": guildName, "Trade_union": guildName,
"contcat_method": contactTool, "contcat_method": contactTool,
"contcat_info": contact "contcat_info": contact,
"age": age,
"sex": sex,
"height": height,
"weight": weight,
"body_features": body_features,
"nationality": nationality,
"personality": personality,
"strengths": strengths,
} }
param["inputs"] = inputs param["inputs"] = inputs
try: try:
url = "https://ai.yolozs.com/chat"
result = requests.post(url=url, json=param, verify=False) # 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聊天") LogManager.method_info(f"ai聊天的参数{param}", "ai聊天")
print(f"ai聊天的参数{param}")
json = result.json() json = result.json()
data = json.get("answer", {}) data = json.get("answer", "")
session_id = json.get("conversation_id", {}) session_id = json.get("conversation_id", "")
LogManager.method_info(f"ai聊天返回的内容{result.json()}", "ai聊天") LogManager.method_info(f"ai聊天返回的内容{result.json()}", "ai聊天")
return data, session_id return data, session_id

327
Utils/TencentOCRUtils.py Normal file
View 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))

View File

@@ -20,7 +20,7 @@ class ThreadManager:
@classmethod @classmethod
def add(cls, udid: str, thread: threading.Thread, event: threading.Event) -> Tuple[int, str]: def add(cls, udid: str, thread: threading.Thread, event: threading.Event) -> Tuple[int, str]:
LogManager.method_info(f"准备创建任务:{udid}", "task") LogManager.method_info(f"准备创建任务:{udid}", "task")
LogManager.method_info("创建线程成功","监控消息") LogManager.method_info("创建线程成功", "监控消息")
with cls._lock: with cls._lock:
# 判断当前设备是否有任务 # 判断当前设备是否有任务
if cls._tasks.get(udid, None) is not None: if cls._tasks.get(udid, None) is not None:

View File

@@ -1,13 +1,27 @@
python -m nuitka Module\Main.py ^ @echo off
setlocal
set cache=.nuitka-cache
set build=.nuitka-build
set out=dist
C:\Users\milk\AppData\Local\Programs\Python\Python312\python.exe -m nuitka ^
Module\Main.py ^
--standalone ^ --standalone ^
--msvc=latest ^ --msvc=latest ^
--lto=no ^
--nofollow-imports ^
--windows-console-mode=disable ^ --windows-console-mode=disable ^
--output-filename=IOSAI ^ --output-filename=IOSAI ^
--cache-dir=%cache% ^
--build-dir=%build% ^
--output-dir=%out% ^
--include-package=Module,Utils,Entity,script ^ --include-package=Module,Utils,Entity,script ^
--include-module=flask,wda,psutil,portalocker,flask_cors,cv2,lxml.etree,requests,urllib3,certifi,idna,setuptools,tidevice ^ --include-module=flask,wda,psutil,portalocker,flask_cors,cv2,lxml.etree,requests,urllib3,certifi,idna,setuptools,tidevice ^
--include-data-dir=resources=resources ^ --include-data-dir=resources=resources ^
--include-data-dir=SupportFiles=SupportFiles ^ --include-data-dir=SupportFiles=SupportFiles ^
--include-data-files="E:/code/Python/iOSAi/resources/iproxy/*=resources/iproxy/" ^ --include-data-files="E:/code/Python/iOSAi/resources/iproxy/*=resources/iproxy/" ^
--include-data-files=resources/icon.ico=resources/icon.ico ^ --include-data-files=resources/icon.ico=resources/icon.ico ^
--jobs=20 ^ --jobs=%NUMBER_OF_PROCESSORS% ^
--windows-icon-from-ico=resources/icon.ico --windows-icon-from-ico=resources/icon.ico
endlocal

View File

@@ -1,13 +0,0 @@
easyocr==1.7.2
facebook_wda==1.5.4
Flask==3.1.2
flask_cors==6.0.1
lxml==6.0.2
numpy==2.3.3
opencv_python==4.12.0.88
opencv_python_headless==4.12.0.88
portalocker==3.2.0
psutil==7.1.0
Requests==2.32.5
tidevice==0.12.10
torch==2.8.0

File diff suppressed because it is too large Load Diff