20250904-初步功能已完成

This commit is contained in:
2025-09-10 22:17:27 +08:00
parent 0e51f60f35
commit 57759593ed
9 changed files with 339 additions and 110 deletions

2
.idea/iOSAI.iml generated
View File

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

2
.idea/misc.xml generated
View File

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

31
.idea/workspace.xml generated
View File

@@ -5,10 +5,14 @@
</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 beforePath="$PROJECT_DIR$/.idea/iOSAI.iml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/iOSAI.iml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/misc.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/misc.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" /> <change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Entity/Variables.py" beforeDir="false" afterPath="$PROJECT_DIR$/Entity/Variables.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Entity/Variables.py" beforeDir="false" afterPath="$PROJECT_DIR$/Entity/Variables.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Utils/ControlUtils.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/ControlUtils.py" afterDir="false" /> <change beforePath="$PROJECT_DIR$/Module/FlaskService.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/FlaskService.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/AiUtils.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/AiUtils.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Utils/LogManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/Utils/LogManager.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/build.bat" beforeDir="false" afterPath="$PROJECT_DIR$/build.bat" afterDir="false" />
<change beforePath="$PROJECT_DIR$/script/ScriptManager.py" beforeDir="false" afterPath="$PROJECT_DIR$/script/ScriptManager.py" afterDir="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" />
@@ -81,7 +85,7 @@
<recent name="E:\Code\python\iOSAI" /> <recent name="E:\Code\python\iOSAI" />
</key> </key>
</component> </component>
<component name="RunManager" selected="Python.123"> <component name="RunManager" selected="Python.Main">
<configuration name="123" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true"> <configuration name="123" type="PythonConfigurationType" factoryName="Python" temporary="true" nameIsGenerated="true">
<module name="iOSAI" /> <module name="iOSAI" />
<option name="ENV_FILES" value="" /> <option name="ENV_FILES" value="" />
@@ -137,7 +141,8 @@
<component name="SharedIndexes"> <component name="SharedIndexes">
<attachedChunks> <attachedChunks>
<set> <set>
<option value="bundled-python-sdk-ce6832f46686-7b97d883f26b-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-252.25557.178" /> <option value="bundled-js-predefined-1d06a55b98c1-0b3e54e931b4-JavaScript-PY-241.18034.82" />
<option value="bundled-python-sdk-975db3bf15a3-2767605e8bc2-com.jetbrains.pycharm.pro.sharedIndexes.bundled-PY-241.18034.82" />
</set> </set>
</attachedChunks> </attachedChunks>
</component> </component>
@@ -198,7 +203,9 @@
<workItem from="1757401583931" duration="29000" /> <workItem from="1757401583931" duration="29000" />
<workItem from="1757401717530" duration="4191000" /> <workItem from="1757401717530" duration="4191000" />
<workItem from="1757411020282" duration="11755000" /> <workItem from="1757411020282" duration="11755000" />
<workItem from="1757480739367" duration="13433000" /> <workItem from="1757480739367" duration="14580000" />
<workItem from="1757498954175" duration="6736000" />
<workItem from="1757506636968" duration="3541000" />
</task> </task>
<task id="LOCAL-00001" summary="ai 开始测试"> <task id="LOCAL-00001" summary="ai 开始测试">
<option name="closed" value="true" /> <option name="closed" value="true" />
@@ -216,7 +223,15 @@
<option name="project" value="LOCAL" /> <option name="project" value="LOCAL" />
<updated>1757421902950</updated> <updated>1757421902950</updated>
</task> </task>
<option name="localTasksCounter" value="3" /> <task id="LOCAL-00003" summary="20250904-初步功能已完成">
<option name="closed" value="true" />
<created>1757494445986</created>
<option name="number" value="00003" />
<option name="presentableId" value="LOCAL-00003" />
<option name="project" value="LOCAL" />
<updated>1757494445986</updated>
</task>
<option name="localTasksCounter" value="4" />
<servers /> <servers />
</component> </component>
<component name="TypeScriptGeneratedFilesManager"> <component name="TypeScriptGeneratedFilesManager">
@@ -251,8 +266,8 @@
<SUITE FILE_PATH="coverage/iOSAI$tidevice_entry.coverage" NAME="tidevice_entry 覆盖结果" MODIFIED="1757061969626" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" /> <SUITE FILE_PATH="coverage/iOSAI$tidevice_entry.coverage" NAME="tidevice_entry 覆盖结果" MODIFIED="1757061969626" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/iOSAI$mac_wda_agent.coverage" NAME="mac_wda_agent Coverage Results" MODIFIED="1756473148639" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" /> <SUITE FILE_PATH="coverage/iOSAI$mac_wda_agent.coverage" NAME="mac_wda_agent Coverage Results" MODIFIED="1756473148639" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" />
<SUITE FILE_PATH="coverage/iOSAI$ScriptManager.coverage" NAME="ScriptManager 覆盖结果" MODIFIED="1756896057801" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" /> <SUITE FILE_PATH="coverage/iOSAI$ScriptManager.coverage" NAME="ScriptManager 覆盖结果" MODIFIED="1756896057801" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$/script" />
<SUITE FILE_PATH="coverage/iOSAI$Main.coverage" NAME="Main 覆盖结果" MODIFIED="1757492700191" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="" /> <SUITE FILE_PATH="coverage/iOSAI$Main.coverage" NAME="Main 覆盖结果" MODIFIED="1757508269318" 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="1757494013218" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" /> <SUITE FILE_PATH="coverage/iOSAI$123.coverage" NAME="123 覆盖结果" MODIFIED="1757500830209" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
<SUITE FILE_PATH="coverage/iOSAI$2111.coverage" NAME="2111 覆盖结果" MODIFIED="1757330714370" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" /> <SUITE FILE_PATH="coverage/iOSAI$2111.coverage" NAME="2111 覆盖结果" MODIFIED="1757330714370" SOURCE_PROVIDER="com.intellij.coverage.DefaultCoverageFileProvider" RUNNER="coverage.py" COVERAGE_BY_TEST_ENABLED="false" COVERAGE_TRACING_ENABLED="false" WORKING_DIRECTORY="$PROJECT_DIR$" />
</component> </component>
</project> </project>

View File

@@ -10,7 +10,7 @@ anchorList: list[AnchorModel] = []
# 线程锁 # 线程锁
anchorListLock = threading.Lock() anchorListLock = threading.Lock()
# 打招呼数据 # 打招呼数据
prologueList: list[str] = ["hello"] prologueList: list[str] = []
# 本地储存的打招呼数据 # 本地储存的打招呼数据
localPrologueList = [ localPrologueList = [

View File

@@ -30,6 +30,7 @@ listLock = threading.Lock()
dataQueue = Queue() dataQueue = Queue()
def start_socket_listener(): def start_socket_listener():
port = int(os.getenv('FLASK_COMM_PORT', 0)) port = int(os.getenv('FLASK_COMM_PORT', 0))
LogManager.info(f"Received port from environment: {port}") LogManager.info(f"Received port from environment: {port}")
@@ -85,11 +86,12 @@ def start_socket_listener():
listener_thread = threading.Thread(target=start_socket_listener, daemon=True) listener_thread = threading.Thread(target=start_socket_listener, daemon=True)
listener_thread.start() listener_thread.start()
# 获取设备列表 # 获取设备列表
@app.route('/deviceList', methods=['GET']) @app.route('/deviceList', methods=['GET'])
def deviceList(): def deviceList():
try: try:
with listLock: # 1. 加锁 with listLock: # 1. 加锁
# 先一次性把队列全部消费完 # 先一次性把队列全部消费完
while not dataQueue.empty(): while not dataQueue.empty():
obj = dataQueue.get() obj = dataQueue.get()
@@ -100,14 +102,15 @@ def deviceList():
for i in range(len(listData) - 1, -1, -1): for i in range(len(listData) - 1, -1, -1):
d = listData[i] d = listData[i]
if d.get("deviceId") == obj.get("deviceId") and \ if d.get("deviceId") == obj.get("deviceId") and \
d.get("screenPort") == obj.get("screenPort"): d.get("screenPort") == obj.get("screenPort"):
listData.pop(i) listData.pop(i)
break # 同一端口同一设备只删一次 break # 同一端口同一设备只删一次
return ResultData(data=listData.copy()).toJson() # 2. 返回副本 return ResultData(data=listData.copy()).toJson() # 2. 返回副本
except Exception as e: except Exception as e:
LogManager.error("获取设备列表失败:", e) LogManager.error("获取设备列表失败:", e)
return ResultData(data=[]).toJson() return ResultData(data=[]).toJson()
# 获取设备应用列表 # 获取设备应用列表
@app.route('/deviceAppList', methods=['POST']) @app.route('/deviceAppList', methods=['POST'])
def deviceAppList(): def deviceAppList():
@@ -116,6 +119,7 @@ def deviceAppList():
apps = ControlUtils.getDeviceAppList(udid) apps = ControlUtils.getDeviceAppList(udid)
return ResultData(data=apps).toJson() return ResultData(data=apps).toJson()
# 打开指定app # 打开指定app
@app.route('/launchApp', methods=['POST']) @app.route('/launchApp', methods=['POST'])
def launchApp(): def launchApp():
@@ -225,9 +229,6 @@ def stopScript():
return ResultData(code=code, data="", msg=msg).toJson() return ResultData(code=code, data="", msg=msg).toJson()
# 关注打招呼 # 关注打招呼
@app.route('/passAnchorData', methods=['POST']) @app.route('/passAnchorData', methods=['POST'])
def passAnchorData(): def passAnchorData():
@@ -259,12 +260,14 @@ def passAnchorData():
except Exception as e: except Exception as e:
LogManager.error(e) LogManager.error(e)
# 获取私信数据 # 获取私信数据
@app.route("/getPrologueList", methods=['GET']) @app.route("/getPrologueList", methods=['GET'])
def getPrologueList(): def getPrologueList():
import Entity.Variables as Variables import Entity.Variables as Variables
return ResultData(data=Variables.prologueList).toJson() return ResultData(data=Variables.prologueList).toJson()
# 添加临时数据 # 添加临时数据
# 批量追加主播到 JSON 文件 # 批量追加主播到 JSON 文件
@app.route("/addTempAnchorData", methods=['POST']) @app.route("/addTempAnchorData", methods=['POST'])
@@ -295,6 +298,9 @@ def getChatTextInfo():
print(result) print(result)
return ResultData(data=result).toJson() return ResultData(data=result).toJson()
except Exception as e: except Exception as e:
LogManager.error(f"获取屏幕翻译出现错误:{e}", "获取屏幕翻译")
data = [ data = [
{ {
'type': 'msg', 'type': 'msg',
@@ -336,6 +342,7 @@ def upLoadLogLogs():
else: else:
return ResultData(data="", msg="日志上传失败").toJson() return ResultData(data="", msg="日志上传失败").toJson()
# 获取当前的主播列表数据 # 获取当前的主播列表数据
@app.route("/anchorList", methods=['POST']) @app.route("/anchorList", methods=['POST'])
def queryAnchorList(): def queryAnchorList():
@@ -360,6 +367,5 @@ def deleteAnchorWithIds():
return ResultData(data={"deleted": deleted}).toJson() return ResultData(data={"deleted": deleted}).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

@@ -1,6 +1,5 @@
import json import json
import os import os
import re
from pathlib import Path from pathlib import Path
import cv2 import cv2
@@ -9,6 +8,7 @@ import unicodedata
import wda import wda
from Utils.LogManager import LogManager from Utils.LogManager import LogManager
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
import re, html
from lxml import etree from lxml import etree
from wda import Client from wda import Client
@@ -298,108 +298,313 @@ class AiUtils(object):
print(f"btn:{btn}") print(f"btn:{btn}")
return cls.findNumber(btn.label) return cls.findNumber(btn.label)
# @classmethod
# def extract_messages_from_xml(cls, xml: str):
# """
# 仅返回当前屏幕中“可见的”聊天内容(含时间分隔)
# """
# from lxml import etree
# root = etree.fromstring(xml.encode("utf-8"))
# items = []
#
# # 屏幕宽度
# app = root.xpath('/XCUIElementTypeApplication')
# screen_w = cls.parse_float(app[0], 'width', 414.0) if app else 414.0
#
# # 找 Table 的可见范围
# table = root.xpath('//XCUIElementTypeTable')
# if table:
# table = table[0]
# table_top = cls.parse_float(table, 'y', 0.0)
# table_h = cls.parse_float(table, 'height', 0.0)
# table_bottom = table_top + table_h
# else:
# table_top, table_bottom = 0.0, cls.parse_float(app[0], 'height', 736.0) if app else 736.0
#
# def in_view(el) -> bool:
# """元素在聊天区内并且可见"""
# if el.get('visible') != 'true':
# return False
# y = cls.parse_float(el, 'y', -1e9)
# h = cls.parse_float(el, 'height', 0.0)
# by = y + h
# return not (by <= table_top or y >= table_bottom)
#
# # 时间分隔
# for t in root.xpath('//XCUIElementTypeStaticText[contains(@traits, "Header")]'):
# if not in_view(t):
# continue
# txt = (t.get('label') or t.get('name') or t.get('value') or '').strip()
# if txt:
# items.append({'type': 'time', 'text': txt, 'y': cls.parse_float(t, 'y')})
#
# # 消息气泡
# EXCLUDES = {'Heart', 'Lol', 'ThumbsUp', '分享发布内容', '视频贴纸标签页', '双击发送表情'}
#
# # —— 新增:系统横幅/提示卡片过滤(只文本判断,最小改动)——
# SYSTEM_BANNER_PATTERNS = [
# r"回复时接收通知", r"开启私信通知", r"开启通知",
# r"Turn on (DM|message|direct message)?\s*notifications",
# r"Enable notifications",
# r"Get notified when .* replies",
# ]
# SYSTEM_BANNER_REGEX = re.compile("|".join(SYSTEM_BANNER_PATTERNS), re.IGNORECASE)
#
# msg_nodes = table.xpath(
# './/XCUIElementTypeCell[@visible="true"]'
# '//XCUIElementTypeOther[@visible="true" and (@name or @label) and not(ancestor::XCUIElementTypeCollectionView)]'
# ) if table is not None else []
#
# for o in msg_nodes:
# # 这里补上 value避免少数节点只在 value 上有文本时漏读
# text = (o.get('label') or o.get('name') or o.get('value') or '').strip()
# if not text or text in EXCLUDES:
# continue
# # 命中 TikTok 自带的“开启通知/回复时接收通知”类提示 → 直接剔除
# if SYSTEM_BANNER_REGEX.search(text):
# continue
# if not in_view(o):
# continue
#
# # 找所在 Cell
# cell = o.getparent()
# while cell is not None and cell.get('type') != 'XCUIElementTypeCell':
# cell = cell.getparent()
#
# x = cls.parse_float(o, 'x')
# y = cls.parse_float(o, 'y')
# w = cls.parse_float(o, 'width')
# right_edge = x + w
#
# direction = None
# # 头像位置判定
# if cell is not None:
# avatar_btns = cell.xpath(
# './/XCUIElementTypeButton[@visible="true" and (@name="图片头像" or @label="图片头像")]')
# if avatar_btns:
# ax = cls.parse_float(avatar_btns[0], 'x')
# direction = 'in' if ax < (screen_w / 2) else 'out'
# # 右对齐兜底
# if direction is None:
# direction = 'out' if right_edge > (screen_w - 20) else 'in'
#
# items.append({'type': 'msg', 'dir': direction, 'text': text, 'y': y})
#
# # 排序 & 清理
# items.sort(key=lambda i: i['y'])
# for it in items:
# it.pop('y', None)
# return items
#
# @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):
""" """
返回当前屏幕中“可见的”聊天内容(含时间分隔 解析 TikTok 聊天 XML返回当前屏幕可见的消息与时间分隔
[{"type":"time","text":"..."}, {"type":"msg","dir":"in|out","text":"..."}]
兼容 Table / CollectionView / ScrollView过滤系统提示/底部工具栏;可见性使用“重叠可视+容差”。
""" """
from lxml import etree if not isinstance(xml, str) or not xml.strip():
root = etree.fromstring(xml.encode("utf-8")) return []
items = [] try:
root = etree.fromstring(xml.encode("utf-8"))
except Exception:
return []
# 屏幕宽度 # ---------- 小工具 ----------
def get_text(el):
s = (el.get('label') or el.get('name') or el.get('value') or '') or ''
return html.unescape(s.strip())
def is_visible(el):
"""无 visible 属性按可见处理;有且为 'false' 才视为不可见。"""
v = el.get('visible')
return (v is None) or (v.lower() == 'true')
# ---------- 屏幕尺寸 ----------
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
# 找 Table 的可见范围 # ---------- 主容器探测(评分选择最像聊天区的容器) ----------
table = root.xpath('//XCUIElementTypeTable') def pick_container():
if table: cands = []
table = table[0] for xp, ctype in (
table_top = cls.parse_float(table, 'y', 0.0) ('//XCUIElementTypeTable', 'table'),
table_h = cls.parse_float(table, 'height', 0.0) ('//XCUIElementTypeCollectionView', 'collection'),
table_bottom = table_top + table_h ('//XCUIElementTypeScrollView', 'scroll'),
):
nodes = [n for n in root.xpath(xp) if is_visible(n)]
for n in nodes:
y = cls.parse_float(n, 'y', 0.0)
h = cls.parse_float(n, 'height', screen_h)
# Cell 数越多越像聊天列表;越靠中间越像
cells = n.xpath('.//XCUIElementTypeCell')
score = len(cells) * 10 - abs((y + h / 2) - screen_h / 2)
cands.append((score, n, ctype))
if cands:
cands.sort(key=lambda t: t[0], reverse=True)
return cands[0][1], cands[0][2]
return None, None
container, container_type = pick_container()
# ---------- 可视区area_top, area_bot ----------
if container is not None:
area_top = cls.parse_float(container, 'y', 0.0)
area_h = cls.parse_float(container, 'height', screen_h)
area_bot = area_top + area_h
else: else:
table_top, table_bottom = 0.0, cls.parse_float(app[0], 'height', 736.0) if app else 736.0 # 顶栏底缘作为上边界(选最靠上的宽>200的块
blocks = [n for n in root.xpath('//XCUIElementTypeOther[@y and @height and @width>="200"]') if
is_visible(n)]
area_top = 0.0
if blocks:
blocks.sort(key=lambda n: cls.parse_float(n, 'y', 0.0))
b = blocks[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)]
if tvs:
tvs.sort(key=lambda n: cls.parse_float(n, 'y', 0.0))
area_bot = cls.parse_float(tvs[-1], 'y', screen_h)
else:
area_bot = screen_h
if area_bot - area_top < 100:
area_top, area_bot = 0.0, screen_h
def in_view(el) -> bool: def in_view(el) -> bool:
"""元素在聊天区内并且可见""" if not is_visible(el):
if el.get('visible') != 'true':
return False return False
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
return not (by <= table_top or y >= table_bottom) tol = 8.0 # 容差,避免边缘误判
return not (by <= area_top + tol or y >= area_bot - tol)
# 时间分隔 # ---------- 时间分隔Header ----------
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):
continue continue
txt = (t.get('label') or t.get('name') or t.get('value') or '').strip() txt = get_text(t)
if txt: if txt:
items.append({'type': 'time', 'text': txt, 'y': cls.parse_float(t, 'y')}) items.append({'type': 'time', 'text': txt, 'y': cls.parse_float(t, 'y', 0.0)})
# 消息气泡 # ---------- 系统提示/横幅过滤 ----------
EXCLUDES = {'Heart', 'Lol', 'ThumbsUp', '分享发布内容', '视频贴纸标签页', '双击发送表情'} EXCLUDES_LITERAL = {
'Heart', 'Lol', 'ThumbsUp',
# —— 新增:系统横幅/提示卡片过滤(只文本判断,最小改动)—— '分享发布内容', '视频贴纸标签页', '双击发送表情', '贴纸',
SYSTEM_BANNER_PATTERNS = [ }
r"回复时接收通知", r"开启私信通知", r"开启通知", SYSTEM_PATTERNS = [
r"回复时接收通知", r"开启(私信)?通知", r"开启通知",
r"你打开了这个与 .* 的聊天。.*隐私",
r"在此用户接受你的消息请求之前,你最多只能发送 ?\d+ 条消息。?",
r"聊天消息条数已达上限,你将无法向该用户发送消息。?",
r"未发送$",
r"Turn on (DM|message|direct message)?\s*notifications", r"Turn on (DM|message|direct message)?\s*notifications",
r"Enable notifications", r"Enable notifications",
r"Get notified when .* replies", r"Get notified when .* replies",
r"You opened this chat .* privacy",
r"Only \d+ message can be sent .* accepts .* request",
] ]
SYSTEM_BANNER_REGEX = re.compile("|".join(SYSTEM_BANNER_PATTERNS), re.IGNORECASE) SYSTEM_RE = re.compile("|".join(SYSTEM_PATTERNS), re.IGNORECASE)
msg_nodes = table.xpath( # 排除底部贴纸/GIF/分享栏(通常是位于底部、较矮的一排 CollectionView
'.//XCUIElementTypeCell[@visible="true"]' def is_toolbar_like(o) -> bool:
'//XCUIElementTypeOther[@visible="true" and (@name or @label) and not(ancestor::XCUIElementTypeCollectionView)]' txt = get_text(o)
) if table is not None else [] if txt in EXCLUDES_LITERAL:
return True
y = cls.parse_float(o, 'y', 0.0)
h = cls.parse_float(o, 'height', 0.0)
near_bottom = (area_bot - (y + h)) < 48
is_short = h <= 40
return near_bottom and is_short
# ---------- 收集消息候选 ----------
msg_nodes = []
if container is not None:
# 容器内优先找 Cell 下的文本节点Other/StaticText/TextView
cand = container.xpath(
'.//XCUIElementTypeCell//*[self::XCUIElementTypeOther or self::XCUIElementTypeStaticText or self::XCUIElementTypeTextView]'
'[@y and (@name or @label or @value)]'
)
for o in cand:
if not in_view(o):
continue
if is_toolbar_like(o):
continue
txt = get_text(o)
if not txt or SYSTEM_RE.search(txt):
continue
msg_nodes.append(o)
else:
# 全局兜底:排除直接挂在 CollectionView底部工具栏下的节点
cand = root.xpath(
'//XCUIElementTypeOther[@y and (@name or @label or @value)]'
' | //XCUIElementTypeStaticText[@y and (@name or @label or @value)]'
' | //XCUIElementTypeTextView[@y and (@name or @label or @value)]'
)
for o in cand:
p = o.getparent()
if p is not None and p.get('type') == 'XCUIElementTypeCollectionView':
continue
if not in_view(o) or is_toolbar_like(o):
continue
txt = get_text(o)
if not txt or SYSTEM_RE.search(txt):
continue
msg_nodes.append(o)
# ---------- 方向判定 & 组装 ----------
for o in msg_nodes: for o in msg_nodes:
# 这里补上 value避免少数节点只在 value 上有文本时漏读 txt = get_text(o)
text = (o.get('label') or o.get('name') or o.get('value') or '').strip() if not txt or txt in EXCLUDES_LITERAL:
if not text or text in EXCLUDES:
continue
# 命中 TikTok 自带的“开启通知/回复时接收通知”类提示 → 直接剔除
if SYSTEM_BANNER_REGEX.search(text):
continue
if not in_view(o):
continue continue
# 找所在 Cell # 找所在 Cell(用于查头像)
cell = o.getparent() cell = o.getparent()
while cell is not None and cell.get('type') != 'XCUIElementTypeCell': while cell is not None and cell.get('type') != 'XCUIElementTypeCell':
cell = cell.getparent() cell = cell.getparent()
x = cls.parse_float(o, 'x') x = cls.parse_float(o, 'x', 0.0)
y = cls.parse_float(o, 'y') y = cls.parse_float(o, 'y', 0.0)
w = cls.parse_float(o, 'width') w = cls.parse_float(o, 'width', 0.0)
right_edge = x + w right_edge = x + w
direction = None direction = None
# 头像位置判定
if cell is not None: if cell is not None:
avatar_btns = cell.xpath( avatars = [a for a in cell.xpath(
'.//XCUIElementTypeButton[@visible="true" and (@name="图片头像" or @label="图片头像")]') './/XCUIElementTypeButton[@visible="true" and (@name="图片头像" or @label="图片头像")]'
if avatar_btns: ) if is_visible(a)]
ax = cls.parse_float(avatar_btns[0], 'x') if avatars:
ax = cls.parse_float(avatars[0], 'x', 0.0)
direction = 'in' if ax < (screen_w / 2) else 'out' direction = 'in' if ax < (screen_w / 2) else 'out'
# 右对齐兜底
if direction is None: if direction is None:
direction = 'out' if right_edge > (screen_w - 20) else 'in' direction = 'out' if right_edge > (screen_w * 0.75) else 'in'
items.append({'type': 'msg', 'dir': direction, 'text': text, 'y': y}) items.append({'type': 'msg', 'dir': direction, 'text': txt, 'y': y})
# 排序 & 清理 # ---------- 排序 & 收尾 ----------
items.sort(key=lambda i: i['y']) if items:
for it in items: items.sort(key=lambda i: i.get('y', 0.0))
it.pop('y', None) for it in items:
it.pop('y', None)
return items return items
@classmethod @classmethod
def parse_float(cls, el, attr, default=0.0): def parse_float(cls, el, attr, default=0.0):
try: try:
return float(el.get(attr, default)) v = el.get(attr)
if v is None:
return default
return float(v)
except Exception: except Exception:
return default return default
@@ -486,7 +691,6 @@ class AiUtils(object):
return True return True
return False return False
@classmethod @classmethod
def _read_json_list(cls, file_path: Path) -> list: def _read_json_list(cls, file_path: Path) -> list:
"""读取为 list读取失败或不是 list 则返回空数组""" """读取为 list读取失败或不是 list 则返回空数组"""
@@ -609,4 +813,3 @@ class AiUtils(object):
except Exception as e: except Exception as e:
LogManager.error(f"[delete_anchors_by_ids] 写入失败: {e}") LogManager.error(f"[delete_anchors_by_ids] 写入失败: {e}")
return deleted return deleted

View File

@@ -225,22 +225,22 @@ from pathlib import Path
import requests import requests
# ========= 全局:强制 UTF-8打包 EXE / 无控制台也生效) ========= # ========= 全局:强制 UTF-8打包 EXE / 无控制台也生效) =========
# def _force_utf8_everywhere(): def _force_utf8_everywhere():
# os.environ.setdefault("PYTHONUTF8", "1") os.environ.setdefault("PYTHONUTF8", "1")
# os.environ.setdefault("PYTHONIOENCODING", "utf-8") os.environ.setdefault("PYTHONIOENCODING", "utf-8")
# # windowed 模式下 stdout/stderr 可能没有 buffer这里做保护包装 # windowed 模式下 stdout/stderr 可能没有 buffer这里做保护包装
# try: try:
# if getattr(sys.stdout, "buffer", None): if getattr(sys.stdout, "buffer", None):
# sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace") sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
# except Exception: except Exception:
# pass pass
# try: try:
# if getattr(sys.stderr, "buffer", None): if getattr(sys.stderr, "buffer", None):
# sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace") sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
# except Exception: except Exception:
# pass pass
#
# _force_utf8_everywhere() _force_utf8_everywhere()
# ========= 全局:强制 UTF-8 + 关闭缓冲(运行期立刻生效) ========= # ========= 全局:强制 UTF-8 + 关闭缓冲(运行期立刻生效) =========

View File

@@ -3,7 +3,7 @@ python -m nuitka "Module/Main.py" ^
--msvc=latest ^ --msvc=latest ^
--windows-console-mode=disable ^ --windows-console-mode=disable ^
--remove-output ^ --remove-output ^
--output-dir=out ^ --output-dir="F:/company code/AI item/20250820/iOSAI/out" ^
--output-filename=IOSAI ^ --output-filename=IOSAI ^
--include-package=Module,Utils,Entity,script ^ --include-package=Module,Utils,Entity,script ^
--include-module=flask ^ --include-module=flask ^
@@ -18,7 +18,7 @@ python -m nuitka "Module/Main.py" ^
--include-module=urllib3 ^ --include-module=urllib3 ^
--include-module=certifi ^ --include-module=certifi ^
--include-module=idna ^ --include-module=idna ^
--include-data-dir="E:/code/Python/iOSAI/SupportFiles=SupportFiles" ^ --include-data-dir="F:/company code/AI item/20250820/iOSAI/SupportFiles=SupportFiles" ^
--include-data-dir="E:/code/Python/iOSAI/resources=resources" ^ --include-data-dir="F:/company code/AI item/20250820/iOSAI/resources=resources" ^
--include-data-files="E:/code/Python/iOSAI/resources/iproxy/*=resources/iproxy/" ^ --include-data-files="F:/company code/AI item/20250820/iOSAI/resources/iproxy/*=resources/iproxy/" ^
--windows-icon-from-ico="E:/code/Python/iOSAI/resources/icon.ico" --windows-icon-from-ico="F:/company code/AI item/20250820/iOSAI/resources/icon.ico"

View File

@@ -276,19 +276,20 @@ class ScriptManager():
retries = 0 retries = 0
while not event.is_set(): while not event.is_set():
# try: try:
# anchor = AiUtils.pop_aclist_first()
#
# if not anchor:
# break
self.greetNewFollowers(udid, needReply, event) # AiUtils.pop_aclist_first()
# return # 成功执行就退出 # anchor = AiUtils.pop_aclist_first()
# except Exception as e: # if not anchor:
# retries += 1 # break
# LogManager.method_error(f"greetNewFollowers 出现异常: {e},准备第 {retries} 次重试", "关注打招呼", udid) self.greetNewFollowers(udid, needReply, event)
# time.sleep(3)
# LogManager.method_error("greetNewFollowers 重试次数耗尽,任务终止", "关注打招呼", udid)
except Exception as e:
retries += 1
LogManager.method_error(f"greetNewFollowers 出现异常: {e},准备第 {retries} 次重试", "关注打招呼", udid)
time.sleep(3)
LogManager.method_error("greetNewFollowers 重试次数耗尽,任务终止", "关注打招呼", udid)
# 关注打招呼以及回复主播消息 # 关注打招呼以及回复主播消息
def greetNewFollowers(self, udid, needReply, event): def greetNewFollowers(self, udid, needReply, event):
@@ -330,7 +331,9 @@ class ScriptManager():
# 获取一个主播,并删除 # 获取一个主播,并删除
anchor = AiUtils.pop_aclist_first() anchor = AiUtils.pop_aclist_first()
if not anchor: if not anchor:
break LogManager.method_info(f"数据库中的数据不足", "关注打招呼", udid)
time.sleep(30)
continue
aid = anchor["anchorId"] aid = anchor["anchorId"]
anchorCountry = anchor.get("country", "") anchorCountry = anchor.get("country", "")
@@ -725,6 +728,8 @@ class ScriptManager():
# 获取主播的名称 # 获取主播的名称
anchor_name = AiUtils.get_navbar_anchor_name(session) anchor_name = AiUtils.get_navbar_anchor_name(session)
LogManager.method_info(f"获取主播的名称:{anchor_name}", "检测消息", udid)
# 找到输入框 # 找到输入框
sel = session.xpath("//TextView") sel = session.xpath("//TextView")