增加切换账号功能

This commit is contained in:
2025-09-28 20:42:01 +08:00
parent d876743d3e
commit d543c6f757
28 changed files with 937 additions and 137 deletions

6
.idea/workspace.xml generated
View File

@@ -4,11 +4,7 @@
<option name="autoReloadType" value="SELECTIVE" /> <option name="autoReloadType" value="SELECTIVE" />
</component> </component>
<component name="ChangeListManager"> <component name="ChangeListManager">
<list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="20250904-初步功能已完成"> <list default="true" id="eceeff5e-51c1-459c-a911-d21ec090a423" name="Changes" comment="20250904-初步功能已完成" />
<change beforePath="$PROJECT_DIR$/.idea/workspace.xml" beforeDir="false" afterPath="$PROJECT_DIR$/.idea/workspace.xml" afterDir="false" />
<change beforePath="$PROJECT_DIR$/Module/DeviceInfo.py" beforeDir="false" afterPath="$PROJECT_DIR$/Module/DeviceInfo.py" afterDir="false" />
<change beforePath="$PROJECT_DIR$/build.bat" beforeDir="false" afterPath="$PROJECT_DIR$/build.bat" afterDir="false" />
</list>
<option name="SHOW_DIALOG" value="false" /> <option name="SHOW_DIALOG" value="false" />
<option name="HIGHLIGHT_CONFLICTS" value="true" /> <option name="HIGHLIGHT_CONFLICTS" value="true" />
<option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" /> <option name="HIGHLIGHT_NON_ACTIVE_CHANGELIST" value="false" />

View File

@@ -151,6 +151,7 @@ def deviceList():
LogManager.error("获取设备列表失败:", e) LogManager.error("获取设备列表失败:", e)
return ResultData(data=[]).toJson() return ResultData(data=[]).toJson()
# 传递token # 传递token
@app.route('/passToken', methods=['POST']) @app.route('/passToken', methods=['POST'])
def passToken(): def passToken():
@@ -158,6 +159,7 @@ def passToken():
print(data) print(data)
return ResultData(data="").toJson() return ResultData(data="").toJson()
# 获取设备应用列表 # 获取设备应用列表
@app.route('/deviceAppList', methods=['POST']) @app.route('/deviceAppList', methods=['POST'])
def deviceAppList(): def deviceAppList():
@@ -309,6 +311,7 @@ def passAnchorData():
LogManager.error(e) LogManager.error(e)
return ResultData(data="", code=1001).toJson() return ResultData(data="", code=1001).toJson()
@app.route('/followAndGreetUnion', methods=['POST']) @app.route('/followAndGreetUnion', methods=['POST'])
def followAndGreetUnion(): def followAndGreetUnion():
try: try:
@@ -401,11 +404,13 @@ def getChatTextInfo():
# 监控消息 # 监控消息
@app.route("/replyMessages", methods=['POST']) @app.route("/replyMessages", methods=['POST'])
def monitorMessages(): def monitorMessages():
LogManager.method_info("开始监控消息,监控消息脚本启动", "监控消息")
body = request.get_json() body = request.get_json()
udid = body.get("udid") udid = body.get("udid")
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))
LogManager.method_info("创建监控消息脚本线程成功", "监控消息")
# 添加到线程管理 # 添加到线程管理
ThreadManager.add(udid, thread, event) ThreadManager.add(udid, thread, event)
return ResultData(data="").toJson() return ResultData(data="").toJson()
@@ -444,7 +449,6 @@ def queryAnchorList():
# 修改当前的主播列表数据 # 修改当前的主播列表数据
@app.route("/updateAnchorList", methods=['POST']) @app.route("/updateAnchorList", methods=['POST'])
def updateAnchorList(): def updateAnchorList():
""" """
@@ -548,7 +552,7 @@ 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={"state": 1}, # 修改内容
filename="log/last_message.json", # 要修改的文件 filename="last_message.json", # 要修改的文件
multi=False # 只改第一条匹配的 multi=False # 只改第一条匹配的
) )
if updated_count > 0: if updated_count > 0:
@@ -566,7 +570,7 @@ 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="log/last_message.json", # 要修改的文件 filename="last_message.json", # 要修改的文件
multi=False # 只改第一条匹配的 multi=False # 只改第一条匹配的
) )
if updated_count > 0: if updated_count > 0:
@@ -582,18 +586,24 @@ def stopAllTask():
return ResultData(code, [], msg).toJson() return ResultData(code, [], msg).toJson()
# @app.route("/killWda", methods=['POST']) # 切换账号
# def killWda(): @app.route('/changeAccount', methods=['POST'])
# data = request.get_json() # 解析 JSON def changeAccount():
# udid = data.get("device") body = request.get_json()
# print(udid) udid = body.get("udid")
# account_id = body.get("account_id")
#
# AiUtils.kill_wda(udid) IOSAIStorage.save(account_id, f"{udid}/accountId.json")
# time.sleep(10)
# AiUtils.launch_wda(udid) # 存储到本地
# manager = ScriptManager()
# return ResultData(data="", msg="WDA重新启动").toJson() event = threading.Event()
# 启动脚本
thread = threading.Thread(target=manager.changeAccount, args=(udid, event))
# 添加到线程管理
code, msg = ThreadManager.add(udid, thread, event)
return ResultData(data="", code=code, message=msg).toJson()
if __name__ == '__main__': if __name__ == '__main__':

View File

@@ -36,7 +36,7 @@ def main(arg):
if __name__ == "__main__": if __name__ == "__main__":
# 获取启动时候传递的参数 # 获取启动时候传递的参数
# main(sys.argv) main(sys.argv)
# 添加iOS开发包到电脑上 # 添加iOS开发包到电脑上
deployer = DevDiskImageDeployer(verbose=True) deployer = DevDiskImageDeployer(verbose=True)

View File

@@ -198,13 +198,13 @@ class AiUtils(object):
client = wda.USBClient(udid) client = wda.USBClient(udid)
session = client.session() session = client.session()
session.appium_settings({"snapshotMaxDepth": 10}) session.appium_settings({"snapshotMaxDepth": 10})
homeButton = session.xpath("//*[@label='首页']") homeButton = session.xpath( "//XCUIElementTypeButton[@name='a11y_vo_home' or @label='Home' or @label='首页']")
try: try:
if homeButton.label == "首页": if homeButton.exists:
print("1.找到了") print("找到首页")
return homeButton return homeButton
else: else:
print("1.没找到") print("没找到首页")
return None return None
except Exception as e: except Exception as e:
print(e) print(e)
@@ -259,11 +259,20 @@ class AiUtils(object):
@classmethod @classmethod
def getSendMesageButton(cls, session: Client): def getSendMesageButton(cls, session: Client):
# msgButton = session.xpath(
# '//XCUIElementTypeButton['
# '(@name="发消息" or @label="发消息" or '
# '@name="发送 👋" or @label="发送 👋" or '
# '@name="消息" or @label="消息")'
# ' and @visible="true"]'
# )
msgButton = session.xpath( msgButton = session.xpath(
'//XCUIElementTypeButton[' '//XCUIElementTypeButton['
'(@name="发消息" or @label="发消息" or ' '(@name="发消息" or @label="发消息" or '
'@name="发送 👋" or @label="发送 👋" or ' '@name="发送 👋" or @label="发送 👋" or '
'@name="消息" or @label="消息")' '@name="消息" or @label="消息" or '
'@name="Message" or @label="Message")'
' and @visible="true"]' ' and @visible="true"]'
) )
@@ -300,6 +309,222 @@ class AiUtils(object):
return cls.findNumber(btn.label) return cls.findNumber(btn.label)
# # 识别当前页面的消息
# @classmethod
# def extract_messages_from_xml(cls, xml: str):
# """
# 解析 TikTok 聊天 XML返回当前屏幕可见的消息与时间分隔
# [{"type":"time","text":"..."}, {"type":"msg","dir":"in|out","text":"..."}]
# 兼容 Table / CollectionView / ScrollView过滤系统提示/底部工具栏;可见性使用“重叠可视+容差”。
# """
# if not isinstance(xml, str) or not xml.strip():
# return []
#
# 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')
# 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
#
# # ---------- 主容器探测(评分选择最像聊天区的容器) ----------
#
# def pick_container():
# cands = []
# for xp, ctype in (
# ('//XCUIElementTypeTable', 'table'),
# ('//XCUIElementTypeCollectionView', 'collection'),
# ('//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:
# # 顶栏底缘作为上边界(选最靠上的宽>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:
# if not is_visible(el):
# return False
# y = cls.parse_float(el, 'y', -1e9)
# h = cls.parse_float(el, 'height', 0.0)
# by = y + h
# 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")]'):
# if not in_view(t):
# continue
# txt = get_text(t)
# if txt:
# items.append({'type': 'time', 'text': txt, 'y': cls.parse_float(t, 'y', 0.0)})
#
# # ---------- 系统提示/横幅过滤 ----------
# EXCLUDES_LITERAL = {
# 'Heart', 'Lol', 'ThumbsUp',
# '分享发布内容', '视频贴纸标签页', '双击发送表情', '贴纸',
# }
# SYSTEM_PATTERNS = [
# r"(消息请求已被接受|你开始了和.*的聊天|你打开了这个与.*的聊天).*"
# r"回复时接收通知", r"开启(私信)?通知", r"开启通知",
# r"你打开了这个与 .* 的聊天。.*隐私",
# r"在此用户接受你的消息请求之前,你最多只能发送 ?\d+ 条消息。?",
# r"聊天消息条数已达上限,你将无法向该用户发送消息。?",
# r"未发送$",
# r"Turn on (DM|message|direct message)?\s*notifications",
# r"Enable notifications",
# r"Get notified when .* replies",
# r"You opened this chat .* privacy",
# r"Only \d+ message can be sent .* accepts .* request",
# ]
# SYSTEM_RE = re.compile("|".join(SYSTEM_PATTERNS), re.IGNORECASE)
#
# # 排除底部贴纸/GIF/分享栏(通常是位于底部、较矮的一排 CollectionView
# def is_toolbar_like(o) -> bool:
# txt = get_text(o)
# 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:
# txt = get_text(o)
# if not txt or txt in EXCLUDES_LITERAL:
# 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)
# y = cls.parse_float(o, 'y', 0.0)
# w = cls.parse_float(o, 'width', 0.0)
# right_edge = x + w
#
# direction = None
# if cell is not None:
# avatars = [a for a in cell.xpath(
# './/XCUIElementTypeButton[@visible="true" and (@name="图片头像" or @label="图片头像")]'
# ) if is_visible(a)]
# if avatars:
# ax = cls.parse_float(avatars[0], 'x', 0.0)
# direction = 'in' if ax < (screen_w / 2) else 'out'
# if direction is None:
# direction = 'out' if right_edge > (screen_w * 0.75) else 'in'
#
# items.append({'type': 'msg', 'dir': direction, 'text': txt, 'y': y})
#
# # ---------- 排序 & 收尾 ----------
# if items:
# items.sort(key=lambda i: i.get('y', 0.0))
# for it in items:
# it.pop('y', None)
# 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
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):
@@ -310,6 +535,7 @@ class AiUtils(object):
""" """
if not isinstance(xml, str) or not xml.strip(): if not isinstance(xml, str) or not xml.strip():
return [] return []
try: try:
root = etree.fromstring(xml.encode("utf-8")) root = etree.fromstring(xml.encode("utf-8"))
except Exception: except Exception:
@@ -321,7 +547,6 @@ 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')
@@ -330,8 +555,7 @@ class AiUtils(object):
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 (
@@ -343,7 +567,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))
@@ -354,13 +577,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
@@ -368,7 +590,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))
@@ -384,10 +605,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):
@@ -402,8 +623,9 @@ class AiUtils(object):
'分享发布内容', '视频贴纸标签页', '双击发送表情', '贴纸', '分享发布内容', '视频贴纸标签页', '双击发送表情', '贴纸',
} }
SYSTEM_PATTERNS = [ SYSTEM_PATTERNS = [
r"(消息请求已被接受|你开始了和.*的聊天|你打开了这个与.*的聊天).*" r"消息请求已被接受。你们可以开始聊天了。",
r"回复时接收通知", r"开启(私信)?通知", r"开启通知", r"(消息请求已被接受|你开始了和.*的聊天|你打开了这个与.*的聊天).*",
r"开启(私信)?通知", r"开启通知",
r"你打开了这个与 .* 的聊天。.*隐私", r"你打开了这个与 .* 的聊天。.*隐私",
r"在此用户接受你的消息请求之前,你最多只能发送 ?\d+ 条消息。?", r"在此用户接受你的消息请求之前,你最多只能发送 ?\d+ 条消息。?",
r"聊天消息条数已达上限,你将无法向该用户发送消息。?", r"聊天消息条数已达上限,你将无法向该用户发送消息。?",
@@ -413,10 +635,13 @@ 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
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:
@@ -430,7 +655,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)]'
@@ -445,7 +669,6 @@ class AiUtils(object):
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)]'
@@ -465,10 +688,9 @@ class AiUtils(object):
# ---------- 方向判定 & 组装 ---------- # ---------- 方向判定 & 组装 ----------
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() 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()
@@ -481,12 +703,17 @@ class AiUtils(object):
direction = None direction = None
if cell is not None: if cell is not None:
avatars = [a for a in cell.xpath( avatars = [a for a in cell.xpath(
'.//XCUIElementTypeButton[@visible="true" and (@name="图片头像" or @label="图片头像")]' './/XCUIElementTypeButton[@visible="true" and (@name="Profile photo" or @label="Profile photo")]'
) if is_visible(a)] ) if is_visible(a)]
if not avatars and SYSTEM_RE.search(txt):
continue # 没头像且系统消息,直接跳过
if avatars: if avatars:
ax = cls.parse_float(avatars[0], 'x', 0.0) 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:
if w > screen_w * 0.8 and SYSTEM_RE.search(txt):
continue
direction = 'out' if right_edge > (screen_w * 0.75) else 'in' direction = 'out' if right_edge > (screen_w * 0.75) else 'in'
items.append({'type': 'msg', 'dir': direction, 'text': txt, 'y': y}) items.append({'type': 'msg', 'dir': direction, 'text': txt, 'y': y})
@@ -498,21 +725,16 @@ class AiUtils(object):
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 @classmethod
def get_navbar_anchor_name(cls, session, timeout: float = 2.0) -> str: def get_navbar_anchor_name(cls, session, timeout: float = 5) -> str:
"""从导航栏读取主播名称找不到返回空字符串。""" """聊天页导航栏读取主播名称找不到返回空字符串。"""
# 可选:限制快照深度,提升解析速度/稳定性
# 限制快照深度(可选)
try: try:
session.appium_settings({"snapshotMaxDepth": 22}) session.appium_settings({"snapshotMaxDepth": 22})
except Exception: except Exception:
@@ -523,50 +745,81 @@ class AiUtils(object):
return (info.get("label") or info.get("name") or info.get("value") or "").strip() return (info.get("label") or info.get("name") or info.get("value") or "").strip()
def _clean_tail(s: str) -> str: def _clean_tail(s: str) -> str:
# 去掉末尾中英标点和空白TikTok 昵称右侧常带一个顿号/逗号,比如 “Alina
return re.sub(r"[,、,。.\s]+$", "", s).strip() return re.sub(r"[,、,。.\s]+$", "", s).strip()
# 导航容器:从“返回”按钮向上找最近祖先,且该祖先内包含“更多/举报”(多语言兜底) # ---- 关键修复:导航容器 ----
NAV_CONTAINER = ( # 1) “返回”可能是 Button 也可能是 Other同时页面右上角有 “更多/举报” 按钮
BACK_ELEM = (
"//*[@type='XCUIElementTypeButton' or @type='XCUIElementTypeOther']"
"[@name='返回' or @label='返回' or @name='Back' or @label='Back' "
" or @name='戻る' or @label='戻る']"
)
RIGHT_MENU_BTN = (
"//XCUIElementTypeButton" "//XCUIElementTypeButton"
"[@name='返回' or @label='返回' or @name='Back' or @label='Back' or @name='戻る' or @label='戻る']" "[@name='更多' or @label='更多' or @name='More' or @label='More' or "
"/ancestor::XCUIElementTypeOther" " @name='その他' or @label='その他' or @name='詳細' or @label='詳細' or "
"[ .//XCUIElementTypeButton" " @name='举报' or @label='举报' or @name='Report' or @label='Report' or "
" [@name='更多' or @label='更多' or @name='More' or @label='More' or " " @name='報告' or @label='報告']"
" @name='その他' or @label='その他' or @name='詳細' or @label='詳細' or " )
" @name='举报' or @label='举报' or @name='Report' or @label='Report' or " # 从“返回”向上找到最近祖先 Other且该祖先内包含“更多/举报”按钮
" @name='報告' or @label='報告']" NAV_CONTAINER = (
"][1]" BACK_ELEM +
"/ancestor::XCUIElementTypeOther[ .//XCUIElementTypeOther or .//XCUIElementTypeButton ][ ."
+ RIGHT_MENU_BTN +
"][1]"
) )
# ① 优先:可访问的 Other自身有文本且不含子 Button,更贴近 TikTok 的实现 # ① 优先:在导航容器里找“可访问的 Other有文本且不含子 Button
XPATH_TITLE_OTHER = ( XPATH_TITLE_OTHER = (
NAV_CONTAINER + NAV_CONTAINER +
"//XCUIElementTypeOther[@accessible='true' and count(.//XCUIElementTypeButton)=0 " "//XCUIElementTypeOther[@accessible='true' and count(.//XCUIElementTypeButton)=0 "
" and (string-length(@name)>0 or string-length(@label)>0 or string-length(@value)>0)" " and (string-length(@name)>0 or string-length(@label)>0 or string-length(@value)>0)][1]"
"][1]"
) )
# ② 退路:第一个 StaticText
# ② 退路:导航容器内第一个有文本的 StaticText
XPATH_TITLE_STATIC = ( XPATH_TITLE_STATIC = (
NAV_CONTAINER + NAV_CONTAINER +
"//XCUIElementTypeStaticText[string-length(@value)>0 or string-length(@label)>0 or string-length(@name)>0][1]" "//XCUIElementTypeStaticText[string-length(@value)>0 or string-length(@label)>0 or string-length(@name)>0][1]"
) )
# 尝试 ① # ③ 兜底:直接在“返回”与右侧菜单同一层级/附近范围内找可读的 Other适配部分机型结构差异
q = session.xpath(XPATH_TITLE_OTHER) XPATH_FALLBACK_NEAR_BACK = (
if q.wait(timeout): BACK_ELEM +
t = _clean_tail(_text_of(q.get())) "/ancestor::XCUIElementTypeOther[1]"
if t: "//XCUIElementTypeOther[@accessible='true' and "
return t " (string-length(@name)>0 or string-length(@label)>0 or string-length(@value)>0)]"
"[count(.//XCUIElementTypeButton)=0][1]"
)
# 尝试 ② # ④ 兜底:昵称往往以顿号/逗号结尾(例如 “Alina利用这个规律匹配
q2 = session.xpath(XPATH_TITLE_STATIC) XPATH_HINT_COMMA_END = (
if q2.wait(1.0): NAV_CONTAINER +
t = _clean_tail(_text_of(q2.get())) "//*[ (contains(@name,'') or contains(@label,'') or contains(@value,'')) "
if t: " and string-length(@name)+string-length(@label)+string-length(@value) < 64 ]"
return t "[1]"
)
# ---- 查询顺序:① -> ② -> ③ -> ④ ----
for xp, wait_s in [
(XPATH_TITLE_OTHER, timeout),
(XPATH_TITLE_STATIC, 1.0),
(XPATH_FALLBACK_NEAR_BACK, 1.0),
(XPATH_HINT_COMMA_END, 1.0),
]:
try:
q = session.xpath(xp)
if q.wait(wait_s):
txt = _clean_tail(_text_of(q.get()))
if txt:
return txt
except Exception:
pass
return "" return ""
# 检查字符串中是否包含中文 # 检查字符串中是否包含中文
@classmethod @classmethod
def contains_chinese(cls, text): def contains_chinese(cls, text):

View File

@@ -58,14 +58,29 @@ 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(
"//*[@label='返回']" # ① 常见中文文案
"//*[@label='返回' or @label='返回上一屏幕']"
" | " " | "
"//*[@label='返回上一屏幕']" # ② 英文 / 内部 name / 图标 label 的按钮(仅限 Button且可见
" | " "//XCUIElementTypeButton[@visible='true' and ("
"//XCUIElementTypeButton[@visible='true' and @name='TTKProfileNavBarBaseItemComponent' and @label='IconChevronLeftOffsetLTR']" "@name='Back' or @label='Back' or " # 英文
"@name='返回' or @label='返回' or " # 中文
"@label='返回上一屏幕' or " # 中文另一种
"@name='nav_bar_start_back' or " # 内部常见 name
"(@name='TTKProfileNavBarBaseItemComponent' and @label='IconChevronLeftOffsetLTR')" # 你给的特例
")]"
) )
if back.exists: if back.exists:
back.click() back.click()
return True return True
@@ -104,7 +119,8 @@ class ControlUtils(object):
# 点击搜索 # 点击搜索
@classmethod @classmethod
def clickSearch(cls, session: Client): def clickSearch(cls, session: Client):
obj = session.xpath("//*[@name='搜索']") # obj = session.xpath("//*[@name='搜索']")
obj = session(xpath='//*[@name="搜索" or @label="搜索" or @name="Search" or @label="Search"]')
try: try:
if obj.exists: if obj.exists:
obj.click() obj.click()

View File

@@ -1,4 +1,5 @@
import json import json
import os
from pathlib import Path from pathlib import Path
@@ -12,18 +13,52 @@ 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") -> Path: def save(cls, data: dict | list, filename: str = "data.json", mode: str = "overwrite") -> Path:
"""
存储数据到 C:/Users/<用户名>/IOSAI/filename
"""
file_path = cls._get_iosai_dir() / filename file_path = cls._get_iosai_dir() / filename
try: file_path.parent.mkdir(parents=True, exist_ok=True)
with open(file_path, "w", encoding="utf-8") as f:
json.dump(data, f, ensure_ascii=False, indent=2) def _load_json():
print(f"[IOSAIStorage] 已保存到: {file_path}") try:
except Exception as e: return json.loads(file_path.read_text("utf-8"))
print(f"[IOSAIStorage] 写入失败: {e}") except Exception:
return {} if isinstance(data, dict) else []
if mode == "merge" and isinstance(data, dict):
old = _load_json()
if not isinstance(old, dict):
old = {}
old.update(data)
to_write = old
elif mode == "append" and isinstance(data, list):
old = _load_json()
if not isinstance(old, list):
old = []
old.extend(data)
to_write = old
else:
to_write = data # 覆盖
# 原子写入
tmp = file_path.with_suffix(file_path.suffix + ".tmp")
with open(tmp, "w", encoding="utf-8") as f:
json.dump(to_write, f, ensure_ascii=False, indent=2)
os.replace(tmp, file_path)
print(f"[IOSAIStorage] 已写入: {file_path}")
return file_path return file_path
@classmethod @classmethod

View File

@@ -201,7 +201,10 @@ class JsonUtils:
if not isinstance(data, list): if not isinstance(data, list):
return [] return []
# 过滤 sender 为空字符串的项 # 过滤 sender 为空字符串的项
return [item for item in data if isinstance(item, dict) and item.get("sender", "").strip()] # return [item for item in data if isinstance(item, dict) and item.get("sender", "").strip()]
return [item for item in data if isinstance(item, dict)]
@classmethod @classmethod
def delete_json_items(cls, def delete_json_items(cls,

View File

@@ -26,7 +26,7 @@ def _force_utf8_everywhere():
except Exception: except Exception:
pass pass
# _force_utf8_everywhere() _force_utf8_everywhere()
class LogManager: class LogManager:
""" """

View File

@@ -53,6 +53,34 @@ class Requester():
except Exception as e: except Exception as e:
LogManager.method_error(f"翻译失败,报错的原因:{e}", "翻译失败异常") LogManager.method_error(f"翻译失败,报错的原因:{e}", "翻译失败异常")
# 翻译
@classmethod
def translationToChinese(cls, msg):
try:
param = {
"msg": msg,
}
url = "https://ai.yolozs.com/translationToChinese"
result = requests.post(url=url, json=param, verify=False)
LogManager.info(f"翻译 请求的参数:{param}", "翻译")
LogManager.info(f"翻译,状态码:{result.status_code},服务器返回的内容:{result.text}", "翻译")
if result.status_code != 200:
LogManager.error(f"翻译失败,状态码:{result.status_code},服务器返回的内容:{result.text}")
return None
json = result.json()
data = json.get("data")
return data
except Exception as e:
LogManager.method_error(f"翻译失败,报错的原因:{e}", "翻译失败异常")
# ai聊天 # ai聊天
@classmethod @classmethod
def chatToAi(cls, param): def chatToAi(cls, param):
@@ -75,10 +103,13 @@ class Requester():
try: try:
url = "https://ai.yolozs.com/chat" url = "https://ai.yolozs.com/chat"
result = requests.post(url=url, json=param, verify=False) result = requests.post(url=url, json=param, verify=False)
LogManager.method_info(f"ai聊天的参数{param}", "ai聊天")
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聊天的参数:{param},ai聊天返回的内容:{result.json()}", "ai聊天") LogManager.method_info(f"ai聊天返回的内容{result.json()}", "ai聊天")
return data, session_id return data, session_id
except Exception as e: except Exception as e:

View File

@@ -20,6 +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("创建线程成功","监控消息")
with cls._lock: with cls._lock:
# 判断当前设备是否有任务 # 判断当前设备是否有任务
if cls._tasks.get(udid, None) is not None: if cls._tasks.get(udid, None) is not None:
@@ -61,13 +62,19 @@ class ThreadManager:
@classmethod @classmethod
def _kill_thread(cls, tid: int) -> bool: def _kill_thread(cls, tid: int) -> bool:
"""向原生线程 ID 抛 KeyboardInterrupt强制跳出""" """向原生线程 ID 抛 KeyboardInterrupt强制跳出"""
res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), try:
ctypes.py_object(KeyboardInterrupt)) res = ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid),
if res == 0: # 线程已不存在 ctypes.py_object(KeyboardInterrupt))
print("线程不存在") # LogManager.method_info(f"向原生线程 {tid} 抛 KeyboardInterrupt强制跳出", "task")
return False if res == 0: # 线程已不存在
if res > 1: # 命中多个线程,重置 print("线程不存在")
print("命中了多个线程") return False
ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), 0) if res > 1: # 命中多个线程,重置
print("杀死线程成功") print("命中了多个线程")
return True ctypes.pythonapi.PyThreadState_SetAsyncExc(ctypes.c_long(tid), 0)
LogManager.method_info("杀死线程创建成功", "监控消息")
print("杀死线程成功")
return True
except Exception as e:
print("杀死线程出现问题 错误的原因:",e)

View File

@@ -1,5 +1,13 @@
facebook_wda==1.5.1 easyocr==1.7.2
facebook_wda==1.5.4
Flask==3.1.2 Flask==3.1.2
flask_cors==6.0.1 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 Requests==2.32.5
tidevice==0.12.10 tidevice==0.12.10
torch==2.8.0

View File

@@ -1,5 +1,7 @@
import atexit
import random import random
import re import re
import subprocess
import threading import threading
import time import time
from enum import Enum from enum import Enum
@@ -11,6 +13,7 @@ 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
from Entity.Variables import anchorList, removeModelFromAnchorList, anchorWithSession from Entity.Variables import anchorList, removeModelFromAnchorList, anchorWithSession
# from Utils.OCRUtils import OCRUtils
from Utils.Requester import Requester from Utils.Requester import Requester
import Entity.Variables as ev import Entity.Variables as ev
@@ -42,8 +45,11 @@ class ScriptManager():
# 关闭并重新打开 TikTok # 关闭并重新打开 TikTok
ControlUtils.closeTikTok(session, udid) ControlUtils.closeTikTok(session, udid)
event.wait(timeout=1) event.wait(timeout=1)
ControlUtils.openTikTok(session, udid) ControlUtils.openTikTok(session, udid)
event.wait(timeout=3) event.wait(timeout=3)
LogManager.method_info("养号重启tiktok", "养号", udid) LogManager.method_info("养号重启tiktok", "养号", udid)
AiUtils.makeUdidDir(udid) AiUtils.makeUdidDir(udid)
@@ -124,7 +130,7 @@ class ScriptManager():
ControlUtils.clickLike(session, udid) ControlUtils.clickLike(session, udid)
LogManager.method_info("继续观看视频", "养号", udid) LogManager.method_info("继续观看视频", "养号", udid)
videoTime = random.randint(10, 30) videoTime = random.randint(25, 40)
for _ in range(videoTime): for _ in range(videoTime):
if event.is_set(): if event.is_set():
break break
@@ -137,9 +143,9 @@ class ScriptManager():
else: else:
nextTime = random.randint(1, 5) nextTime = random.randint(1, 5)
for _ in range(nextTime): for _ in range(nextTime):
if event.is_set(): if event.is_set():
break break
event.wait(timeout=1) event.wait(timeout=1)
client.swipe_up() client.swipe_up()
except Exception as e: except Exception as e:
@@ -172,7 +178,9 @@ class ScriptManager():
# 2) 进入直播 (使用英文) # 2) 进入直播 (使用英文)
live_button = session( live_button = session(
xpath='//XCUIElementTypeButton[@name="直播"] | //XCUIElementTypeOther[@name="直播"]') xpath='//XCUIElementTypeButton[@name="LIVE" or @label="LIVE" or @name="直播" or @label="直播"] '
'| //XCUIElementTypeOther[@name="LIVE" or @label="LIVE" or @name="直播" or @label="直播"]'
)
if live_button.exists: if live_button.exists:
live_button.click() live_button.click()
@@ -186,7 +194,12 @@ class ScriptManager():
break break
event.wait(timeout=1) event.wait(timeout=1)
live_button = session(xpath='//XCUIElementTypeButton[@name="直播"]') # live_button = session(xpath='//XCUIElementTypeButton[@name="直播"]')
live_button = session(
xpath='//XCUIElementTypeButton[@name="LIVE" or @label="LIVE" or @name="直播" or @label="直播"] '
'| //XCUIElementTypeOther[@name="LIVE" or @label="LIVE" or @name="直播" or @label="直播"]'
)
if live_button.exists: if live_button.exists:
continue continue
@@ -880,14 +893,26 @@ class ScriptManager():
print("greetNewFollowers方法执行完毕") print("greetNewFollowers方法执行完毕")
# 检测消息 # 检测消息
def replyMessages(self, udid, event): def replyMessages(self, udid, event):
client = wda.USBClient(udid)
session = client.session() try:
client = wda.USBClient(udid)
session = client.session()
except Exception as e:
LogManager.method_error(f"创建wda会话异常: {e}", "检测消息", udid)
return
LogManager.method_info("开始重启tiktok", "监控消息")
ControlUtils.closeTikTok(session, udid) ControlUtils.closeTikTok(session, udid)
event.wait(timeout=2) event.wait(timeout=2)
# time.sleep(1)
ControlUtils.openTikTok(session, udid) ControlUtils.openTikTok(session, udid)
event.wait(timeout=3) event.wait(timeout=3)
# time.sleep(1)
LogManager.method_info("重启tiktok成功", "监控消息")
while not event.is_set(): while not event.is_set():
try: try:
@@ -895,16 +920,344 @@ class ScriptManager():
self.monitorMessages(session, udid, event) self.monitorMessages(session, udid, event)
except Exception as e: except Exception as e:
LogManager.method_error(f"监控消息 出现异常: {e},重新启动监控直播", "检测消息", udid) LogManager.method_error(f"监控消息 出现异常: {e},重新启动监控直播", "检测消息", udid)
LogManager.method_info(f"出现异常时,稍等再重启 TikTok 并重试 异常是: {e}", "监控消息", udid)
LogManager.method_info(f"出现异常重新创建wda", "监控消息", udid)
client = wda.USBClient(udid)
session = client.session()
LogManager.method_info(f"重启 TikTok", "监控消息", udid)
# 出现异常时,稍等再重启 TikTok 并重试 # 出现异常时,稍等再重启 TikTok 并重试
ControlUtils.closeTikTok(session, udid) ControlUtils.closeTikTok(session, udid)
event.wait(timeout=2) event.wait(timeout=2)
# time.sleep(1)
ControlUtils.openTikTok(session, udid) ControlUtils.openTikTok(session, udid)
event.wait(timeout=3) event.wait(timeout=3)
# time.sleep(1)
LogManager.method_info("TikTok 重启成功", "监控消息", udid)
continue # 重新进入 while 循环,调用 monitorMessages continue # 重新进入 while 循环,调用 monitorMessages
# 检查未读消息并回复 # 检查未读消息并回复
# 此方法暂时只取最后一天进行发送给ai
# def monitorMessages(self, session, udid, event):
#
# LogManager.method_info("脚本开始执行中", "监控消息")
#
# # 调整节点的深度为 7
# session.appium_settings({"snapshotMaxDepth": 7})
#
# el = session.xpath(
# '//XCUIElementTypeButton[@name="a11y_vo_inbox"]'
# ' | '
# '//XCUIElementTypeButton[contains(@name,"收件箱")]'
# ' | '
# '//XCUIElementTypeButton[.//XCUIElementTypeStaticText[@value="收件箱"]]'
# )
#
# # 如果收件箱有消息 则进行点击
# if el.exists:
#
# try:
# m = re.search(r'(\d+)', el.label) # 抓到的第一个数字串
# except Exception as e:
# LogManager.method_error(f"解析收件箱数量异常: {e}", "检测消息", udid)
#
# count = int(m.group(1)) if m else 0
# if count:
# el.click()
# session.appium_settings({"snapshotMaxDepth": 25})
# event.wait(timeout=3)
# while True:
# info_count = 0
#
# # 创建新的会话
# el = session.xpath(
# '//XCUIElementTypeButton[@name="a11y_vo_inbox"]'
# ' | '
# '//XCUIElementTypeButton[contains(@name,"收件箱")]'
# ' | '
# '//XCUIElementTypeButton[.//XCUIElementTypeStaticText[@value="收件箱"]]'
# )
#
# print("el", el)
# if not el.exists:
# LogManager.method_error(f"检测不到收件箱", "检测消息", udid)
# raise Exception("当前页面找不到收件箱,重启")
# # break
#
# # 支持中文“收件箱”和英文“Inbox”
# xpath_query = (
# "//XCUIElementTypeStaticText"
# "[@value='收件箱' or @label='收件箱' or @name='收件箱'"
# " or @value='Inbox' or @label='Inbox' or @name='Inbox']"
# )
#
# # 查找所有收件箱节点
# inbox_nodes = session.xpath(xpath_query).find_elements()
#
# if len(inbox_nodes) < 2:
# LogManager.method_error(f"当前页面不再收件箱页面,重启", "检测消息", udid)
# raise Exception("当前页面不再收件箱页面,重启")
#
# m = re.search(r'(\d+)', el.label) # 抓到的第一个数字串
# count = int(m.group(1)) if m else 0
#
# if not count:
# LogManager.method_info(f"当前收件箱的总数量{count}", "检测消息", udid)
# break
#
# # 新粉丝
# xp_new_fan_badge = (
# "//XCUIElementTypeCell[.//XCUIElementTypeLink[@name='新粉丝']]"
# "//XCUIElementTypeStaticText[string-length(@value)>0 and translate(@value,'0123456789','')='']"
# )
#
# # 活动
# xp_activity_badge = (
# "//XCUIElementTypeCell[.//XCUIElementTypeLink[@name='活动']]"
# "//XCUIElementTypeStaticText[string-length(@value)>0 and translate(@value,'0123456789','')='']"
# )
#
# # 系统通知
# xp_system_badge = (
# "//XCUIElementTypeCell[.//XCUIElementTypeLink[@name='系统通知']]"
# "//XCUIElementTypeStaticText[string-length(@value)>0 and translate(@value,'0123456789','')='']"
# )
#
# # 消息请求
# xp_request_badge = (
# "//XCUIElementTypeCell"
# "[.//*[self::XCUIElementTypeLink or self::XCUIElementTypeStaticText]"
# " [@name='消息请求' or @label='消息请求' or @value='消息请求']]"
# "//XCUIElementTypeStaticText[string-length(@value)>0 and translate(@value,'0123456789','')='']"
# )
#
# # 用户消息
# xp_badge_numeric = (
# "("
# # 你的两类未读容器组件 + 数字徽标value 纯数字)
# "//XCUIElementTypeOther["
# " @name='AWEIMChatListCellUnreadCountViewComponent'"
# " or @name='TikTokIMImpl.InboxCellUnreadCountViewBuilder'"
# "]//XCUIElementTypeStaticText[@value and translate(@value,'0123456789','')='']"
# ")/ancestor::XCUIElementTypeCell[1]"
# " | "
# # 兜底:任何在 CollectionView 下、value 纯数字的徽标 → 找其最近的 Cell
# "//XCUIElementTypeCollectionView//XCUIElementTypeStaticText"
# "[@value and translate(@value,'0123456789','')='']"
# "/ancestor::XCUIElementTypeCell[1]"
# )
#
# try:
# # 如果 2 秒内找不到,会抛异常
# user_text = session.xpath(xp_badge_numeric).get(timeout=2.0)
# val = (user_text.info.get("value") or
# user_text.info.get("label") or
# user_text.info.get("name"))
# LogManager.method_info(f"用户未读数量:{val}", "检测消息", udid)
# except Exception:
# LogManager.method_warning("当前屏幕没有找到 用户 未读徽标数字", "检测消息", udid)
# print("当前屏幕没有找到 用户消息 未读徽标数字", udid)
# user_text = None
# info_count += 1
#
# if user_text:
#
# user_text.tap()
# event.wait(timeout=3)
#
# xml = session.source()
# msgs = AiUtils.extract_messages_from_xml(xml)
#
# text_list = ['What do you think of my live stream?',
# 'What do you think makes my streams special?',
# 'Do you think Im one of the most engaging streamers youve seen?']
#
# # 检测出对方发的最后一条信息
#
#
# last_msg = next((item['text'] for item in reversed(msgs) if item['type'] == 'msg'),
# random.choice(text_list))
#
# LogManager.method_info(f"检测到对方最后发送的消息:{last_msg}", "检测消息", udid)
#
# isLanguage = AiUtils.is_language(last_msg)
# if isLanguage:
# # LogManager.method_info(f"{last_msg}", "检测消息", udid)
#
# last_msg_text = last_msg
# else:
# LogManager.method_info(f"对方发送的消息不是语言,随机挑选作为最后一条进行回复:{last_msg}",
# "检测消息", udid)
# last_msg_text = random.choice(text_list)
#
# if AiUtils.contains_chinese(last_msg_text):
# LogManager.method_info(f"需要翻译:{last_msg_text}, 即将进行翻译", "检测消息", udid)
# last_msg_text = Requester.translation(last_msg_text)
# LogManager.method_info(f"翻译成功:{last_msg_text}, ", "检测消息", udid)
#
# # 向ai发送信息
# # 获取主播的名称
# anchor_name = AiUtils.get_navbar_anchor_name(session)
#
# LogManager.method_info(f"获取主播的名称:{anchor_name}", "检测消息", udid)
# LogManager.method_info(f"获取主播最后发送的消息 进行翻译:{last_msg}", "检测消息", udid)
# last_msg = Requester.translationToChinese(last_msg)
# LogManager.method_info(f"翻译后的内容:{last_msg}", "检测消息", udid)
#
# # 找到输入框
# last_data = [{
# "sender": anchor_name,
# "device": udid,
# "text": last_msg,
# "status": 0
# }]
# print(last_data)
#
# LogManager.method_info(f"主播最后发送的数据,传递给前端进行记录:{last_data}", "检测消息", udid)
# JsonUtils.append_json_items(last_data, "log/last_message.json")
#
# # 从C盘中读取数据
# anchorWithSession = IOSAIStorage.load()
#
# sel = session.xpath("//TextView")
# if anchor_name not in anchorWithSession:
# # 如果是第一次发消息(没有sessionId的情况)
# LogManager.method_info(f"第一次发消息:{anchor_name},没有记忆 开始请求ai", "检测消息", udid)
# LogManager.method_info(f"向ai发送的参数", "检测消息", udid)
# aiResult, sessionId = Requester.chatToAi({"query": last_msg_text, "user": "1"})
# IOSAIStorage.save({anchor_name: sessionId})
#
# # 找到输入框输入ai返回出来的消息
#
# if sel.exists:
# sel.click() # 聚焦
# event.wait(timeout=1)
# sel.clear_text()
# sel.set_text(f"{aiResult or '暂无数据'}\n")
# else:
# LogManager.method_error("找不到输入框,重启", "检测消息", udid)
# raise Exception("找不到输入框,重启")
# else:
# LogManager.method_info(f"不是一次发消息:{anchor_name},有记忆", "检测消息", udid)
# # 如果不是第一次发消息证明存储的有sessionId
# sessionId = anchorWithSession[anchor_name]
#
# # TODO: user后续添加暂时写死
#
# aiResult, sessionId = Requester.chatToAi(
# {"query": last_msg_text, "conversation_id": sessionId, "user": "1"})
# # aiResult = response['result']
# if sel.exists:
# sel.click() # 聚焦
# event.wait(timeout=1)
# sel.clear_text()
# sel.set_text(f"{aiResult or '暂无数据'}\n")
#
# LogManager.method_info(f"存储的sessionId:{anchorWithSession}", "检测消息", udid)
# event.wait(timeout=1)
# # 返回
# ControlUtils.clickBack(session)
#
# # 重新回到收件箱页面后,强制刷新节点
# session.appium_settings({"snapshotMaxDepth": 25})
# event.wait(timeout=1)
# try:
# # 如果 2 秒内找不到,会抛异常
# badge_text = session.xpath(xp_new_fan_badge).get(timeout=2.0)
# val = (badge_text.info.get("value") or
# badge_text.info.get("label") or
# badge_text.info.get("name"))
#
# LogManager.method_info(f"新粉丝未读数量:{val}", "检测消息", udid)
# if badge_text:
# badge_text.tap()
# event.wait(timeout=1)
# ControlUtils.clickBack(session)
# event.wait(timeout=1)
# except Exception:
# LogManager.method_warning("当前屏幕没有找到 新粉丝 未读徽标数字", "检测消息", udid)
# print("当前屏幕没有找到 新粉丝 未读徽标数字", udid)
# badge_text = None
# info_count += 1
#
# try:
#
# # 如果 2 秒内找不到,会抛异常
# badge_text = session.xpath(xp_activity_badge).get(timeout=2.0)
# val = (badge_text.info.get("value") or
# badge_text.info.get("label") or
# badge_text.info.get("name"))
# LogManager.method_info(f"活动未读数量:{val}", "检测消息", udid)
# if badge_text:
# badge_text.tap()
# event.wait(timeout=1)
# ControlUtils.clickBack(session)
# event.wait(timeout=1)
# except Exception:
# LogManager.method_warning("当前屏幕没有找到 活动 未读徽标数字", "检测消息", udid)
# print("当前屏幕没有找到 活动 未读徽标数字", udid)
# badge_text = None
# info_count += 1
#
# try:
# # 如果 2 秒内找不到,会抛异常
# badge_text = session.xpath(xp_system_badge).get(timeout=2.0)
# val = (badge_text.info.get("value") or
# badge_text.info.get("label") or
# badge_text.info.get("name"))
# LogManager.method_info(f"系统通知未读数量:{val}", "检测消息", udid)
# if badge_text:
# badge_text.tap()
# event.wait(timeout=1)
# ControlUtils.clickBack(session)
# event.wait(timeout=1)
# except Exception:
# LogManager.method_warning("当前屏幕没有找到 系统通知 未读徽标数字", "检测消息", udid)
# print("当前屏幕没有找到 系统通知 未读徽标数字", udid)
# badge_text = None
# info_count += 1
#
# try:
# # 如果 2 秒内找不到,会抛异常
# badge_text = session.xpath(xp_request_badge).get(timeout=2.0)
# val = (badge_text.info.get("value") or
# badge_text.info.get("label") or
# badge_text.info.get("name"))
# LogManager.method_info(f"消息请求未读数量:{val}", "检测消息", udid)
# if badge_text:
# badge_text.tap()
# event.wait(timeout=1)
# ControlUtils.clickBack(session)
# event.wait(timeout=1)
# except Exception:
# LogManager.method_warning("当前屏幕没有找到 消息请求 未读徽标数字", "检测消息", udid)
# print("当前屏幕没有找到 消息请求 未读徽标数字", udid)
# badge_text = None
# info_count += 1
#
# # 双击收件箱 定位到消息的位置
# if info_count == 5:
# r = el.bounds # 可能是命名属性,也可能是 tuple
# cx = int((r.x + r.width / 2) if hasattr(r, "x") else (r[0] + r[2] / 2))
# cy = int((r.y + r.height / 2) if hasattr(r, "y") else (r[1] + r[3] / 2))
# session.double_tap(cx, cy) # 可能抛异常:方法不存在
# LogManager.method_info(f"双击收件箱 定位到信息", "检测消息", udid)
#
# else:
# return
# else:
# LogManager.method_error(f"检测不到收件箱", "检测消息", udid)
# raise Exception("当前页面找不到收件箱,重启")
# 检测消息进行优化检测直到我发送的内容把这些发送给ai
def monitorMessages(self, session, udid, event): def monitorMessages(self, session, udid, event):
LogManager.method_info("脚本开始执行中", "监控消息")
# 调整节点的深度为 7 # 调整节点的深度为 7
session.appium_settings({"snapshotMaxDepth": 7}) session.appium_settings({"snapshotMaxDepth": 7})
@@ -918,7 +1271,12 @@ class ScriptManager():
# 如果收件箱有消息 则进行点击 # 如果收件箱有消息 则进行点击
if el.exists: if el.exists:
m = re.search(r'(\d+)', el.label) # 抓到的第一个数字串
try:
m = re.search(r'(\d+)', el.label) # 抓到的第一个数字串
except Exception as e:
LogManager.method_error(f"解析收件箱数量异常: {e}", "检测消息", udid)
count = int(m.group(1)) if m else 0 count = int(m.group(1)) if m else 0
if count: if count:
el.click() el.click()
@@ -1019,6 +1377,7 @@ class ScriptManager():
info_count += 1 info_count += 1
if user_text: if user_text:
user_text.tap() user_text.tap()
event.wait(timeout=3) event.wait(timeout=3)
@@ -1028,46 +1387,46 @@ class ScriptManager():
text_list = ['What do you think of my live stream?', text_list = ['What do you think of my live stream?',
'What do you think makes my streams special?', 'What do you think makes my streams special?',
'Do you think Im one of the most engaging streamers youve seen?'] 'Do you think Im one of the most engaging streamers youve seen?']
# 检测出对方发的最后一条信息
# 检测出对方发的最后一条信息,
# 获取了最后一条消息
last_msg = next((item['text'] for item in reversed(msgs) if item['type'] == 'msg'), last_msg = next((item['text'] for item in reversed(msgs) if item['type'] == 'msg'),
random.choice(text_list)) random.choice(text_list))
LogManager.method_info(f"检测到对方最后发送的消息:{last_msg}", "检测消息", udid) LogManager.method_info(f"检测到对方最后发送的消息:{last_msg}", "检测消息", udid)
isLanguage = AiUtils.is_language(last_msg) # 如果最后一条消息不是文字,随机取出一条当做最后一条消息,最后一条消息是last_msg_text
if isLanguage:
# LogManager.method_info(f"{last_msg}", "检测消息", udid)
isLanguage = AiUtils.is_language(last_msg)
if isLanguage:
last_msg_text = last_msg last_msg_text = last_msg
else: else:
LogManager.method_info(f"对方发送的消息不是语言,随机挑选作为最后一条进行回复:{last_msg}", LogManager.method_info(f"对方发送的消息不是语言,随机挑选作为最后一条进行回复:{last_msg}",
"检测消息", udid) "检测消息", udid)
last_msg_text = random.choice(text_list) last_msg_text = random.choice(text_list)
if AiUtils.contains_chinese(last_msg_text):
LogManager.method_info(f"需要翻译:{last_msg_text}, 即将进行翻译", "检测消息", udid)
last_msg_text = Requester.translation(last_msg_text)
LogManager.method_info(f"翻译成功:{last_msg_text}, ", "检测消息", udid)
# 向ai发送信息
# 获取主播的名称 # 获取主播的名称
anchor_name = AiUtils.get_navbar_anchor_name(session) anchor_name = AiUtils.get_navbar_anchor_name(session)
LogManager.method_info(f"获取主播的名称:{anchor_name}", "检测消息", udid) LogManager.method_info(f"获取主播的名称:{anchor_name}", "检测消息", udid)
LogManager.method_info(f"获取主播最后发送的消息 进行翻译:{last_msg}", "检测消息", udid) LogManager.method_info(f"获取主播最后发送的消息 进行翻译:{last_msg}", "检测消息", udid)
last_msg = Requester.translation(last_msg, "中国") chinese_last_msg_text = Requester.translationToChinese(last_msg_text)
LogManager.method_info(f"翻译后的内容:{last_msg}", "检测消息", udid) LogManager.method_info(f"翻译中文后的内容,交给前端进行展示:{chinese_last_msg_text}", "检测消息",
udid)
# 找到输入框 # 找到输入框
last_data = [{ last_data = [{
"sender": anchor_name, "sender": anchor_name,
"device": udid, "device": udid,
"text": last_msg, "text": chinese_last_msg_text,
"status": 0 "status": 0
}] }]
print(last_data) print(last_data)
LogManager.method_info(f"主播最后发送的数据,传递给前端进行记录:{last_data}", "检测消息", udid) LogManager.method_info(f"主播最后发送的数据,传递给前端进行记录:{chinese_last_msg_text}",
"检测消息", udid)
JsonUtils.append_json_items(last_data, "log/last_message.json") JsonUtils.append_json_items(last_data, "log/last_message.json")
# 从C盘中读取数据 # 从C盘中读取数据
@@ -1075,11 +1434,12 @@ class ScriptManager():
sel = session.xpath("//TextView") sel = session.xpath("//TextView")
if anchor_name not in anchorWithSession: if anchor_name not in anchorWithSession:
# 如果是第一次发消息(没有sessionId的情况) # 如果是第一次发消息(没有sessionId的情况)
LogManager.method_info(f"第一次发消息:{anchor_name},没有记忆 开始请求ai", "检测消息", udid) LogManager.method_info(f"第一次发消息:{anchor_name},没有记忆 开始请求ai", "检测消息", udid)
LogManager.method_info(f"向ai发送的参数", "检测消息", udid) LogManager.method_info(f"向ai发送的参数: 文本为:{last_msg_text}", "检测消息", udid)
aiResult, sessionId = Requester.chatToAi({"query": last_msg_text, "user": "1"}) aiResult, sessionId = Requester.chatToAi({"query": last_msg_text, "user": "1"})
IOSAIStorage.save({anchor_name: sessionId}) IOSAIStorage.save({anchor_name: sessionId}, mode="merge")
# 找到输入框输入ai返回出来的消息 # 找到输入框输入ai返回出来的消息
@@ -1098,6 +1458,8 @@ class ScriptManager():
# TODO: user后续添加暂时写死 # TODO: user后续添加暂时写死
LogManager.method_info(f"向ai发送的参数: 文本为:{last_msg_text}", "检测消息", udid)
aiResult, sessionId = Requester.chatToAi( aiResult, sessionId = Requester.chatToAi(
{"query": last_msg_text, "conversation_id": sessionId, "user": "1"}) {"query": last_msg_text, "conversation_id": sessionId, "user": "1"})
# aiResult = response['result'] # aiResult = response['result']
@@ -1115,6 +1477,7 @@ class ScriptManager():
# 重新回到收件箱页面后,强制刷新节点 # 重新回到收件箱页面后,强制刷新节点
session.appium_settings({"snapshotMaxDepth": 25}) session.appium_settings({"snapshotMaxDepth": 25})
event.wait(timeout=1) event.wait(timeout=1)
try: try:
# 如果 2 秒内找不到,会抛异常 # 如果 2 秒内找不到,会抛异常
badge_text = session.xpath(xp_new_fan_badge).get(timeout=2.0) badge_text = session.xpath(xp_new_fan_badge).get(timeout=2.0)
@@ -1203,7 +1566,6 @@ class ScriptManager():
LogManager.method_error(f"检测不到收件箱", "检测消息", udid) LogManager.method_error(f"检测不到收件箱", "检测消息", udid)
raise Exception("当前页面找不到收件箱,重启") raise Exception("当前页面找不到收件箱,重启")
# 放在 ScriptManager 类外面或 utils 里 # 放在 ScriptManager 类外面或 utils 里
def interruptible_sleep(self, event: threading.Event, seconds: float, slice_: float = 1.0): def interruptible_sleep(self, event: threading.Event, seconds: float, slice_: float = 1.0):
"""把一次长 sleep 拆成 1 秒一片,随时响应 event""" """把一次长 sleep 拆成 1 秒一片,随时响应 event"""
@@ -1214,3 +1576,82 @@ class ScriptManager():
left -= timeout left -= timeout
return not event.is_set() # 返回 True 表示正常睡完False 被中断 return not event.is_set() # 返回 True 表示正常睡完False 被中断
# 切换账号
def changeAccount(self, udid, event):
LogManager.method_info("开始进行切换账号", "切换账号", udid)
client = wda.USBClient(udid)
session = client.session()
# 重启进行切换账号
ControlUtils.closeTikTok(session, udid)
# event.wait(timeout=2)
time.sleep(1)
ControlUtils.openTikTok(session, udid)
# 使用pop取出列表中的第一个元素
account_ids = IOSAIStorage.load(f"{udid}/accountId.json")
account_id = account_ids.pop(0)
# 再放到末尾
account_ids.append(account_id)
# 重新进行保存覆盖
IOSAIStorage.save(account_ids, f"{udid}/accountId.json")
LogManager.method_info("重启进行切换账号", "切换账号", udid)
session.appium_settings({"snapshotMaxDepth": 15})
user_home = session.xpath('//XCUIElementTypeButton[@name="a11y_vo_profile" or @label="主页"]')
LogManager.method_info("检测主页按钮", "切换账号", udid)
if user_home.exists:
LogManager.method_info("检测主页按钮成功,点击主页", "切换账号", udid)
user_home.click()
session.appium_settings({"snapshotMaxDepth": 25})
LogManager.method_info("检测切换账号按钮", "切换账号", udid)
check_account_btn = session.xpath('//XCUIElementTypeButton[@name="切换账号" or @label="切换账号"]')
if check_account_btn.exists:
LogManager.method_info("检测切换账号按钮成功,点击切换账号", "切换账号", udid)
check_account_btn.click()
# 使用截图进行保存,保存进行图像的识别
try:
img = client.screenshot()
base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), ".."))
resource_dir = os.path.join(base_dir, "resources", udid)
os.makedirs(resource_dir, exist_ok=True)
filePath = os.path.join(resource_dir, "home.png")
img.save(filePath)
LogManager.method_info(f"保存屏幕图像成功 -> {filePath}", "养号", udid)
print("保存了背景图:", filePath)
event.wait(timeout=1)
except Exception as e:
LogManager.method_info(f"截图或保存失败,失败原因:{e}", "养号", udid)
raise Exception("截图或保存失败,重启养号功能")
# 使用EasyOcr进行识别
# 取出图片路径
image_path = AiUtils.imagePathWithName(udid, "home") # 替换为你的图像路径
# ocr = OCRUtils(langs=['ch_sim', 'en'], force_gpu=None)
# print(f"image_path:{image_path}")
#
# # 进行识别,切换
# hit = ocr.find_best(image_path, account_id, match_mode='fuzzy', weight_sim=0.75)
#
# if hit:
# print(f"[BEST] {hit.text} | sim={hit.sim:.2f} conf={hit.conf:.2f} score={hit.score:.2f}")
# print(f"bbox={hit.bbox}, center={hit.center}")
# # 做一次缩放映射
# mapped_hit = hit.mapped(1 / 6, 1 / 6)
# print(f"center(/6)={mapped_hit.center}")
#
# # 传给 session.tap
# session.tap(mapped_hit.center[0], mapped_hit.center[1])
#
# else:
# print("未找到目标名称")