From 855a19873ee696c9282fedb35828c7e1d3c0fbea Mon Sep 17 00:00:00 2001 From: milk <53408947@qq.com> Date: Wed, 22 Oct 2025 18:24:43 +0800 Subject: [PATCH] =?UTF-8?q?=E5=90=88=E5=B9=B6=E4=BB=A3=E7=A0=81=E3=80=82?= =?UTF-8?q?=E4=B8=B4=E6=97=B6=E4=B8=8A=E4=BC=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .idea/.gitignore | 5 - .idea/git_toolbox_blame.xml | 6 - .idea/git_toolbox_prj.xml | 15 - .idea/iOSAI.iml | 11 +- .idea/inspectionProfiles/Project_Default.xml | 18 +- .idea/jsLibraryMappings.xml | 6 - .idea/modules.xml | 8 - .idea/vcs.xml | 1 - .idea/workspace.xml | 207 +--- Entity/Variables.py | 6 + Entity/__pycache__/Variables.cpython-312.pyc | Bin 3183 -> 3257 bytes Module/DeviceInfo.py | 7 +- Module/FlaskService.py | 108 +- Module/__pycache__/DeviceInfo.cpython-312.pyc | Bin 17406 -> 17538 bytes .../__pycache__/FlaskService.cpython-312.pyc | Bin 30029 -> 31940 bytes Module/__pycache__/Main.cpython-312.pyc | Bin 3192 -> 3192 bytes Utils/AiUtils.py | 443 ++++++--- Utils/ControlUtils.py | 25 +- Utils/IOSAIStorage.py | 13 - Utils/JsonUtils.py | 54 +- Utils/Requester.py | 46 +- Utils/TencentOCRUtils.py | 327 ++++++ Utils/ThreadManager.py | 2 +- Utils/__pycache__/AiUtils.cpython-312.pyc | Bin 44368 -> 50292 bytes .../__pycache__/ControlUtils.cpython-312.pyc | Bin 12135 -> 12446 bytes Utils/__pycache__/JsonUtils.cpython-312.pyc | Bin 11423 -> 11440 bytes Utils/__pycache__/LogManager.cpython-312.pyc | Bin 14663 -> 14663 bytes Utils/__pycache__/Requester.cpython-312.pyc | Bin 5602 -> 6260 bytes .../__pycache__/ThreadManager.cpython-312.pyc | Bin 4864 -> 4864 bytes build.bat | 20 +- requirements.txt | 13 - script/ScriptManager.py | 934 +++++++++--------- .../__pycache__/ScriptManager.cpython-312.pyc | Bin 54494 -> 65941 bytes 33 files changed, 1347 insertions(+), 928 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/git_toolbox_blame.xml delete mode 100644 .idea/git_toolbox_prj.xml delete mode 100644 .idea/jsLibraryMappings.xml delete mode 100644 .idea/modules.xml create mode 100644 Utils/TencentOCRUtils.py delete mode 100644 requirements.txt diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 10b731c..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,5 +0,0 @@ -# 默认忽略的文件 -/shelf/ -/workspace.xml -# 基于编辑器的 HTTP 客户端请求 -/httpRequests/ diff --git a/.idea/git_toolbox_blame.xml b/.idea/git_toolbox_blame.xml deleted file mode 100644 index 7dc1249..0000000 --- a/.idea/git_toolbox_blame.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/git_toolbox_prj.xml b/.idea/git_toolbox_prj.xml deleted file mode 100644 index 02b915b..0000000 --- a/.idea/git_toolbox_prj.xml +++ /dev/null @@ -1,15 +0,0 @@ - - - - - - - \ No newline at end of file diff --git a/.idea/iOSAI.iml b/.idea/iOSAI.iml index 6cb8b9a..ec63674 100644 --- a/.idea/iOSAI.iml +++ b/.idea/iOSAI.iml @@ -1,10 +1,7 @@ - - - - - - - + + + \ No newline at end of file diff --git a/.idea/inspectionProfiles/Project_Default.xml b/.idea/inspectionProfiles/Project_Default.xml index 5732908..589e6f0 100644 --- a/.idea/inspectionProfiles/Project_Default.xml +++ b/.idea/inspectionProfiles/Project_Default.xml @@ -3,20 +3,12 @@ + + + + + + + + + + + + + + + + + + + + + + - { - "keyToString": { - "ASKED_ADD_EXTERNAL_FILES": "true", - "ASKED_MARK_IGNORED_FILES_AS_EXCLUDED": "true", - "Python.12.executor": "Run", - "Python.123.executor": "Run", - "Python.DeviceInfo.executor": "Run", - "Python.Main.executor": "Run", - "Python.Test.executor": "Run", - "Python.test.executor": "Run", - "Python.tidevice_entry.executor": "Run", - "RunOnceActivity.ShowReadmeOnStart": "true", - "RunOnceActivity.TerminalTabsStorage.copyFrom.TerminalArrangementManager.252": "true", - "RunOnceActivity.git.unshallow": "true", - "SHARE_PROJECT_CONFIGURATION_FILES": "true", - "git-widget-placeholder": "main", - "javascript.nodejs.core.library.configured.version": "20.17.0", - "javascript.nodejs.core.library.typings.version": "20.17.58", - "last_opened_file_path": "E:/code/Python/iOSAi/resources/iproxy", - "node.js.detected.package.eslint": "true", - "node.js.detected.package.tslint": "true", - "node.js.selected.package.eslint": "(autodetect)", - "node.js.selected.package.tslint": "(autodetect)", - "nodejs_package_manager_path": "npm", - "settings.editor.selected.configurable": "com.jetbrains.python.configuration.PyActiveSdkModuleConfigurable", - "two.files.diff.last.used.file": "E:/share/iOSAI/Module/FlaskService.py", - "vue.rearranger.settings.migration": "true" + +}]]> @@ -91,75 +114,7 @@ - - - - - - - - - - + - - - - - - - - - - - - - - - diff --git a/Entity/Variables.py b/Entity/Variables.py index a24f720..7ca00a3 100644 --- a/Entity/Variables.py +++ b/Entity/Variables.py @@ -4,6 +4,7 @@ from Entity.AnchorModel import AnchorModel # wda apple bundle id WdaAppBundleId = "com.yolojtAgent.wda.xctrunner" +# WdaAppBundleId = "com.yolozsAgent.wda.xctrunner" # wda投屏端口 wdaScreenPort = 9567 # wda功能端口 @@ -15,6 +16,11 @@ anchorListLock = threading.Lock() # 打招呼数据 prologueList: list[str] = [] +# 评论数据 +commentList = [] + +API_KEY = "app-sdRfZy2by9Kq7uJg7JdOSVr8" + # 本地储存的打招呼数据 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!", diff --git a/Entity/__pycache__/Variables.cpython-312.pyc b/Entity/__pycache__/Variables.cpython-312.pyc index 9ffafa4ebfcd0658af12f92b89e6cc5b116f6375..281bbfa0a37fb78306a5547c4904ca5500ce4d4a 100644 GIT binary patch delta 291 zcmaDau~U-wG%qg~0}yc4f6ur(kyn!O-bD50geZX&&M3ijh7_(vj8Q`A3{k?F3{fH} z+)<(_3{hf0ES|!X%9_TM!rQ_UC6U6H%9YBL%Ad-d#v};5r~U70*MdIjEszT7zFM!$lPVn`oP8@WZ1w5qy$AL-{&^s0kZf7 Q8@L~EX*94;7UYQo09xZozW@LL delta 195 zcmdlf`CfweG%qg~0}zB-e$R-X$ScWsZK8T}L<;93#wdYwhA6>IhA5#Ft|;LYhA0sr z7ER$!WldvB;b~!s5=-Gt_lesBH5Ri2i3Wxn!s3lSpMgvUP07iZxn@jm Optional[subprocess.Popen]: try: diff --git a/Module/FlaskService.py b/Module/FlaskService.py index 0d80379..a4e4f3a 100644 --- a/Module/FlaskService.py +++ b/Module/FlaskService.py @@ -6,6 +6,7 @@ from pathlib import Path from queue import Queue from typing import Any, Dict +from Entity import Variables from Utils.AiUtils import AiUtils from Utils.IOSAIStorage import IOSAIStorage from Utils.LogManager import LogManager @@ -247,6 +248,7 @@ def longPressAction(): def growAccount(): body = request.get_json() udid = body.get("udid") + Variables.commentList = body.get("comment") manager = ScriptManager() event = threading.Event() @@ -286,16 +288,25 @@ def passAnchorData(): try: LogManager.method_info("关注打招呼", "关注打招呼") data: Dict[str, Any] = request.get_json() + # 设备列表 idList = data.get("deviceList", []) + # 主播列表 acList = data.get("anchorList", []) + Variables.commentList = data.get("comment") + + LogManager.info(f"[INFO] 获取数据: {idList} {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", []) @@ -306,7 +317,8 @@ def passAnchorData(): manager = ScriptManager() 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) return ResultData(data="").toJson() @@ -330,6 +342,10 @@ def followAndGreetUnion(): # 是否需要回复 needReply = data.get("needReply", True) + + # 是否需要进行翻译 + needTranslate = data.get("needTranslate", True) + # 获取打招呼数据 ev.prologueList = data.get("prologueList", []) @@ -340,7 +356,8 @@ def followAndGreetUnion(): manager = ScriptManager() 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) return ResultData(data="").toJson() @@ -378,12 +395,28 @@ def addTempAnchorData(): def getChatTextInfo(): data = request.get_json() udid = data.get("udid") - client = wda.USBClient(udid, wdaFunctionPort) + client = wda.USBClient(udid,wdaFunctionPort) session = client.session() xml = session.source() try: 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() except Exception as e: @@ -410,6 +443,8 @@ def monitorMessages(): LogManager.method_info("开始监控消息,监控消息脚本启动", "监控消息") body = request.get_json() udid = body.get("udid") + # Variables.commentList = body.get("comment") + manager = ScriptManager() event = threading.Event() thread = threading.Thread(target=manager.replyMessages, args=(udid, event)) @@ -525,11 +560,29 @@ def aiConfig(): contactTool = data.get("contactTool") 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 = { "agentName": agentName, "guildName": guildName, "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) @@ -554,9 +607,9 @@ def update_last_message(): updated_count = JsonUtils.update_json_items( match={"sender": sender, "text": text}, # 匹配条件 - patch={"state": 1}, # 修改内容 - filename="last_message.json", # 要修改的文件 - multi=False # 只改第一条匹配的 + patch={"status": 1}, # 修改内容 + filename="log/last_message.json", # 要修改的文件 + multi=True # 只改第一条匹配的 ) if updated_count > 0: return ResultData(data=updated_count, message="修改成功").toJson() @@ -573,15 +626,16 @@ def delete_last_message(): updated_count = JsonUtils.delete_json_items( match={"sender": sender, "text": text}, # 匹配条件 - filename="last_message.json", # 要修改的文件 - multi=False # 只改第一条匹配的 + filename="log/last_message.json", # 要修改的文件 + multi=True # 只改第一条匹配的 ) if updated_count > 0: return ResultData(data=updated_count, message="修改成功").toJson() + return ResultData(data=updated_count, message="修改失败").toJson() -# 的停止所有任务 +# 停止所有任务 @app.route("/stopAllTask", methods=['POST']) def stopAllTask(): idList = request.get_json() @@ -594,20 +648,34 @@ def stopAllTask(): def changeAccount(): body = request.get_json() 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() - event = threading.Event() + threading.Event() # 启动脚本 - thread = threading.Thread(target=manager.changeAccount, args=(udid, event)) - # 添加到线程管理 - code, msg = ThreadManager.add(udid, thread, event) + code, msg = manager.changeAccount(udid) + # thread = threading.Thread(target=, args=(udid,)) + # # 添加到线程管理 + # thread.start() 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__': app.run("0.0.0.0", port=5000, debug=True, use_reloader=False) diff --git a/Module/__pycache__/DeviceInfo.cpython-312.pyc b/Module/__pycache__/DeviceInfo.cpython-312.pyc index 698fb0a73aefbb104f70829b495c1b3660d264c0..52cbd94036f8931ae56d944f1e1fc13f139bd0c3 100644 GIT binary patch delta 1066 zcmZuvTWl0n7(V}*ncbb;ncdmW&hE~Z?R~dx8rljfi71y{&<6wsl!Rj<4l`cJwZM&djkUwrq-sBVKi+TMEzI|>bu01c8Lpb{!cPKc^R^||bX zX@o?mMl`Au-M4`gB~cLvPK?BaRGcU^h(Y5d?)w8y9W{yR^PrQU7O{LDa*{MfQZ!A{ z)F!rXQ=QB%T2JbICG4!AS(5d6#A%?7q|xW#G?6AhVKZqFQgd2q8);kirmrzMR zhDy;Z*y|4=lIPDu0$>x{j(74eaaaPIPb!OYASb*7{cIwRVbc`0dCF88-9Bu`L+h;#=x^0N!H<%s1g4ziB2Q)l~sMg@97v$}$JU?kApsAd6WR zgjmk%)YX4|`@`%FE87`4sMP8Mv|EgNNcmXVul*wjVGw;J?M8#psk2Mgx|ZQj+_7Qz z^n`ojtHILS`(h&B6U`kzIe+{#zhQ+S^`Y0$w1C6tR!EwI~W@P=TwcW*F(L&RfT6|iXZqSN7(sYj^_}=9~ihmJO#FfN- zxP$$i`kei4YPb?(C)4e^SCxMbO7GXP-vXIK73+yfu1%m{+KF0GC0X;dp>lFaspvJm zg#ULk(tjo+Z`;=(9=X@PzI*DLJMx)({Hi-S;(mM19lPva9l3M;k~Dv0g2nB3GOvla zvNxOZ%N}g?pv8md*ctoP=&+yzLjn$?Dppz5?hR@2nYvZ8+G^IAc`;d#3qdp$R10}& z%FqhU(p0lj$nyi4A26Kdo3dLmT;^vQUjg9!c2fr68~$f=3x@A_Q=2Zs_q@B~s06?A z$*w*OfAB>24GfF?Uhf_Zm~UJCx&%=^_UuI&T6uBpk|Y-Hh4mM}PJ3lNC6?+KTEMDO zfQ58c$y7H0d$6G^ZYg%PJFL`yVLn7cR5NVV#;Va?KI`>$-h&eY*qXu)eyV_>UKSsD l3p%h%Kve>OIi$`ZeGbLhcrnd>EaqcFGIT8<56c}Q>Tk6E46FbE delta 932 zcmZutOKcNI82A*ynVhKdVqIdN&F3PzQXS`H0rgxU+O=%H~CAR!K1u)=}nf*5bhiH=6!_su{5 z7~mW*N;rdUelVVGv=h&xz^Vj8A+C+p-kv!lGWFvA_@M7lk6d6s1(t%n}> znSY1}EZ{koi-Jbj#k*N|q|LC0_p;td$HP9}&-x>cVV>oqf$P|S;zYQf53<4k?h33x z;4P?HXP}z=j`bk#Cgq>;t{Co+k5iKb56ZF5R}iPg5&J`2l)u>-q|Q8<&Wg`RZIN(##G_1H zOgg<{awJaXbTR964;%CJg%5;%>T6HX&*+@_FVEsEe5&n(Ssadw>&|w$&Lx;V*0OX( z!6{hLty{_UxAnm$=@7;2RN`LRzF;j_7mwafZx%mi$Fh}el@ctLjPV}rhMpMrwHrQF zTKv%Ck|K&XHV=y3-Y2nnZ8|9qjs|4L61Thp+M>_*Co*bCv<9H&o}e??7w0PeuWj;@ zZS5U#$v-R-fk$e4M1SyJ@~rAnpHXlMnnV{TgRSCP@TOZHEf?XJqEX3di!@PjwMCa! za?;M-Bq-!c_uB+7%ME?+B3_lp`vb%u%~bPnWfi>r>JjC-_>M z>Z>-?_bX^>h}Zz50agP#8eldc-GKOADBS_?E)@QNLIYANE_O`}r;f*P<0`bUHl&K5 E1HWbD{r~^~ diff --git a/Module/__pycache__/FlaskService.cpython-312.pyc b/Module/__pycache__/FlaskService.cpython-312.pyc index 6a814941ade3131ce533c77ec5c298184428b9ca..b8ca36e864741e4bae96ecd0d197e2b9c4c239df 100644 GIT binary patch delta 9612 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 NraW|h%xlC~{Xa~UMHv79 delta 7874 zcmbtZ4RlmRmVU4Q(w(G}?)?9gKS&zL9}vQCLdXvg2m}bQM6qf5y>vS1KkB_s40a|N z$8}+3M5biLRd?7~M@1s)iL;K(=#J=|IlJTRhJ(B2Jn=a0U{-d|apF;TUEI~Zw_ft{ z(l9gb*>}S2`uXbCt*U!(-MYN_U;MBCjZgWN-JZz7x9-q?_WjT9(Qa<49 zaSEKk2hw`d;MW>R4`%da1T%Xwc@FBf!0A_;TZ5%NrL4*aii2f6<*b|Pb0DGMK2Dozm4+%5iWZ<2=}`3=syGf+qC>6LP^EFGG99WxLzTy& z7VA)JG}MwfRD}+;Rzp?BpWG?FJpHSwpRfgP%Rp2LmX8rlGdR?dmoi zszXC{#PQm$Lv7bkJK|8CI@AsgwKER2ONZ*5LoMryL+#d~c8V3ko`Fh^BUym|0s4a6 zy+XH;EjY&aT`;LGgXf0%VcxZ$I;`ar&DN9z-Zs&dn#Y?PpucP)?0D2*SqVk2fgWC+ zIZ>8gWURMAT}c@7kdQy*Q*44q_Uw*|QBg5QqB2omaOzwn^?@YW{5HNJC z#ld@lxXdI8eiW-ul->QvycoN1QqqG2K@wg9vkYw!k0f=62gDFL46Tn--=eR4jFW|N zB5r6UDL@nl@N8v^YK}yh1R7hHQ87n6#1kZ5M4CL2h$Jl#+^z~fH_`{NC+S3&U(u;>VeJSwbzrqf$4_7Vtzv z-aeR@grVa(_$T$ybAb$cuG~(iN;mQznpO5y0nV?R)wW>P+VYITGyo{3zHm^S-%Xd1 z1aX{~ddf4hQ7`aJw<0DAm7l;rY5VMC>tUtW%CGRemtI`5ZrS_-ll4;}x1&f15}cD1 z)AWk6j`>AfX6M8{8ObpucOw}FB3Xdo`n4U9J&|Uw><3*Kfv(GRtYREwsjB=Pe~|6E zP4E*@1q>kEw>z=F~tq|}KXzm7Kc@1of&E|%X3dplwt zvB;SyIDUdLW{z3LO&4&(;p@+)N9AI+n zqDl@NupE)*wm~r@lPWAH`AB%E+3O8Qp(Pbtj2=P1_=Nf6qHomZE`5+;f|5^4fD${c zS?waHp@EVAfPyrlS=O4m!*FPi*7eRF+DXLy0j1X@B@ZEz$(c6fk-dF8{DWe9m}~)= zkNg*^*~abbGu3n8n>2@W^hs(8Q3S7{o#H6k?r7Kxgf$ zdCe*RVq*?`!Tc{wA-8IyV%-sn%|&Tspb;z~sRrlY)P8PygGQ1IOF1 zC+EJCTzDn9@T_pI{h79ZN?tKzx7b#GmO=+MZ!P__2pHenTGP6Mdvk@Yt={AUy{e_|B!5LW zZYmqaV2b<@$zLOR5(yqkw|d?S)rvfg-Om8|M*2j~Bm7w;3zSEvI|fokr#BTf{6Xa* zXq7i2D z0x+iky!nAUSX^jU9Bm=lFArDm_YlA5NI;Ynn^shPA6q8In=YRG4gBaM&Qu5^yYwde zG5|dErIt$Y!0)xZ#E<$Pf>lH@`1$Dhm#1I6clyi=)8D&z{U?|H_539;X68pvLGMkt zbA2_c1yy6Ps~ExqY8b2!UbTyYpmD2*fh*Aq<%A2cTMR~;!MjjLdxH6A0Pxd-_8#!U zL+z!sYqyR67j}CeFWW;~x9qH*zqw;(otx7fZOlAwm}~FPrF8=ZjxtWhE1Z4haJ|qw z*A%zIy>9^*ZV*g!MgYkevj9$9us&{NB^#D}h7oSwjmJ3Va4|xI5BNg{WtrD-5WT78 zTNdV3;aBHXlXt2P2A3W!p(P!e)X`C6QVjkOZR)r)bvLvtMu-I!lPumX)AJqouVH=@ zx7y{1_m*){Du#WEMG8lWS0oA4x4meT6hqtmT^Rq&Nl?8qb9!1hs2J}K1{4c~QPF^` zIzK$LS?8yQ^r%2BeIB_Rq}+x#?=Jx{jEa993S->$q_lUE@~_bGfE9xLqWYAKQ=t=~ zt0_feTc#47r>aj>Uro##Ynd@*nA`bl&YX9gE3P>1_oV$At?K-C z3A)19OV)1A=Pu{lTB?ng%bQwU#y84%D8Jz{w^UEO*Ljn-uqB+N@9nA@osV!*e*Z;t zyI99g#7JPcQw`*#+l4vgQIIA+Tuu#!Lw-3-W(Ouq7B}MzJ=3*}T!K@A(jf!L#tyPT$$otU$~2ulZ~$V4hzDX+TpeizqS%P&5!`4_w_hh>68(}tB*_rhh_EFX6qp3R zS0+=~X$}scMJ5gr{{dNKoD{PV4Mrpg_Swn&7AHf+R}w?wkQ*4uQHi{V4MrjCWxGc% zVl`oiU>HM`glGg3Aflj{{IVFNR(C9 z5J5Uv0vw0rJ~Hg~3(~yhyTHi=Bm#jb$b>y#LMbi?PFH!Tb06@_ecJ%{HQ>HRT^2bS z@IFXitF!UN;DjlCL@2ZKF#|-ZCc*r;X~ax-FNOeCu+SG*WtfD7aVxFav&7)C(e1re zqY#Gkz=jx*7?GHO9J71;E#XkF-^VTw-RtBQtHAt^Vhaz71P(&hX8^A z&_mGYyl$~Ti={EAxuR_^*nlB_-fYN(C zS`*GS=R;!y?F{cTF>#&>SJ2X6Hk{Un;n(-gy9^BMl*A+WpamOU@J2L}=THU~*0ATH z5vs*=>6;`(HwOaU9%+CS!DQR$mytH$-+a`?KM2Nfls;Rs$Yx-#LE{q!Gyv+a$)r7r z>F~BoG{|-uPAsU58T&D!iu+=G%+zauqyy+7eqcz|yGjgkn`o1n746)?6<9uNg;>>_%&R zYA%P-H08{EB%^|B$%SKW(3p}<-|xz$zPdU%tF6(Z<+UIX_-^q?)TbmzB@ylnyZ}Yw>QZlv zLX@}!$#VMrXsyG(Pxc3->SjOtg`n#FXwE3xDu>zw_iU#p1c@J_#v1)N7UMF-43Ab& zmqctp9aIuogWC)4nV_L6&e<4#4~w)~As^2CwUVZ?2EQmnjIPd{f&t(8WP-Bq8*x3kf8GX^ZqY~T#$Kg<1g z?qpK_x!!YsGM0Olul@uWS0c9>oT3tDu-)@?szYsCI%9y+OeOvKJ#MF+-_E$f6j+=o ZrM0m&&eT~6VK^3N%IROmN=>-n{{`gSP&NPn diff --git a/Module/__pycache__/Main.cpython-312.pyc b/Module/__pycache__/Main.cpython-312.pyc index cdb3f1866ce0592f294df1bbeb4235f85506d199..d13f7ec1d6039990d74b6b401a490f4bd4086c87 100644 GIT binary patch delta 20 acmew%@k4_9G%qg~0}#xc^J61-E)M`lO$LGh delta 20 acmew%@k4_9G%qg~0}#Aa__UEbmj?hwR0c}` diff --git a/Utils/AiUtils.py b/Utils/AiUtils.py index aa71610..56fca3e 100644 --- a/Utils/AiUtils.py +++ b/Utils/AiUtils.py @@ -11,52 +11,101 @@ import unicodedata import wda from lxml import etree from wda import Client + +from Entity.Variables import wdaFunctionPort from Utils.LogManager import LogManager + # 工具类 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 def findImageInScreen(cls, target, udid): try: + print("参数", target, udid) + # 加载原始图像和模板图像 - image_path = AiUtils.imagePathWithName(udid, "bgv") # 替换为你的图像路径 - template_path = AiUtils.imagePathWithName("", target) # 替换为你的模板路径 + 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("加载背景图失败") + LogManager.error("加载背景图失败", udid) return -1, -1 - if template is None: - LogManager.error("加载模板图失败") + + LogManager.error("加载模板图失败", udid) return -1, -1 # 获取模板的宽度和高度 w, h = template.shape[::-1] - # 使用模板匹配方法 + # 模板匹配 res = cv2.matchTemplate(image, template, cv2.TM_CCOEFF_NORMED) - threshold = 0.7 # 匹配度阈值,可以根据需要调整 + 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: + # 放在 cv2.matchTemplate 之前 + cv2.imwrite(f'/tmp/runtime_bg_{udid}.png', image) + cv2.imwrite(f'/tmp/runtime_tpl_{udid}.png', template) + print(f'>>> 设备{udid} 模板{target} 最高相似度:', cv2.minMaxLoc(res)[1]) + # 安全取出第一个匹配点 + matches = list(zip(*loc[::-1])) + if not matches: 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: LogManager.error(f"加载素材失败:{e}", udid) - print(e) return -1, -1 # 使用正则查找字符串中的数字 @@ -71,7 +120,7 @@ class AiUtils(object): # 选择截图 @classmethod def screenshot(cls): - client = wda.USBClient("eca000fcb6f55d7ed9b4c524055214c26a7de7aa") + client = wda.USBClient("eca000fcb6f55d7ed9b4c524055214c26a7de7aa",wdaFunctionPort) session = client.session() image = session.screenshot() image_path = "screenshot.png" @@ -195,10 +244,10 @@ class AiUtils(object): # click 是否点击该按钮 @classmethod def findHomeButton(cls, udid="eca000fcb6f55d7ed9b4c524055214c26a7de7aa"): - client = wda.USBClient(udid) + client = wda.USBClient(udid,wdaFunctionPort) session = client.session() 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: if homeButton.exists: print("找到首页了") @@ -213,7 +262,7 @@ class AiUtils(object): # 查找关闭按钮 @classmethod def findLiveCloseButton(cls, udid="eca000fcb6f55d7ed9b4c524055214c26a7de7aa"): - client = wda.USBClient(udid) + client = wda.USBClient(udid,wdaFunctionPort) session = client.session() session.appium_settings({"snapshotMaxDepth": 10}) r = session.xpath("//XCUIElementTypeButton[@name='关闭屏幕']") @@ -288,7 +337,7 @@ class AiUtils(object): # 获取当前屏幕上的节点 @classmethod def getCurrentScreenSource(cls): - client = wda.USBClient("eca000fcb6f55d7ed9b4c524055214c26a7de7aa") + client = wda.USBClient("eca000fcb6f55d7ed9b4c524055214c26a7de7aa",wdaFunctionPort) print(client.source()) # 查找app主页上的收件箱按钮 @@ -308,8 +357,13 @@ class AiUtils(object): print(f"btn:{btn}") 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 # def extract_messages_from_xml(cls, xml: str): # """ @@ -331,17 +385,21 @@ class AiUtils(object): # return html.unescape(s.strip()) # # def is_visible(el): - # """无 visible 属性按可见处理;有且为 'false' 才视为不可见。""" # v = el.get('visible') # 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') # 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 ( @@ -353,7 +411,6 @@ class AiUtils(object): # 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)) @@ -364,13 +421,12 @@ class AiUtils(object): # # 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 @@ -378,7 +434,6 @@ class AiUtils(object): # 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)) @@ -394,10 +449,10 @@ class AiUtils(object): # y = cls.parse_float(el, 'y', -1e9) # h = cls.parse_float(el, 'height', 0.0) # by = y + h - # tol = 8.0 # 容差,避免边缘误判 + # 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): @@ -410,10 +465,12 @@ class AiUtils(object): # EXCLUDES_LITERAL = { # 'Heart', 'Lol', 'ThumbsUp', # '分享发布内容', '视频贴纸标签页', '双击发送表情', '贴纸', + # '关注', # } # SYSTEM_PATTERNS = [ - # r"(消息请求已被接受|你开始了和.*的聊天|你打开了这个与.*的聊天).*" - # r"回复时接收通知", r"开启(私信)?通知", r"开启通知", + # r"消息请求已被接受。你们可以开始聊天了。", + # r"(消息请求已被接受|你开始了和.*的聊天|你打开了这个与.*的聊天).*", + # r"开启(私信)?通知", r"开启通知", # r"你打开了这个与 .* 的聊天。.*隐私", # r"在此用户接受你的消息请求之前,你最多只能发送 ?\d+ 条消息。?", # r"聊天消息条数已达上限,你将无法向该用户发送消息。?", @@ -423,10 +480,43 @@ class AiUtils(object): # r"Get notified when .* replies", # r"You opened this chat .* privacy", # r"Only \d+ message can be sent .* accepts .* request", + # r"此消息可能违反.*", + # r"无法发送", + # r"请告知我们" # ] # 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: # txt = get_text(o) # if txt in EXCLUDES_LITERAL: @@ -440,7 +530,6 @@ class AiUtils(object): # # ---------- 收集消息候选 ---------- # 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)]' @@ -450,12 +539,14 @@ class AiUtils(object): # continue # if is_toolbar_like(o): # continue + # cell = get_ancestor_cell(o) + # if is_profile_cell(cell): + # continue # 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 # 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)]' @@ -467,37 +558,37 @@ class AiUtils(object): # continue # if not in_view(o) or is_toolbar_like(o): # continue + # cell = get_ancestor_cell(o) + # if is_profile_cell(cell): + # continue # 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 # msg_nodes.append(o) # - # # ---------- 方向判定 & 组装 ---------- + # # ---------- 方向判定 & 组装(中心点法) ---------- + # CENTER_MARGIN = max(12.0, screen_w * 0.02) # 中线容差 + # # for o in msg_nodes: # 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 # - # # 找所在 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' + # center_x = x + w / 2.0 + # screen_center = screen_w / 2.0 + # + # if center_x < screen_center - CENTER_MARGIN: + # direction = 'in' # 左侧:对方 + # elif center_x > screen_center + CENTER_MARGIN: + # 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}) # @@ -507,31 +598,32 @@ class AiUtils(object): # 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: + @staticmethod + def parse_float(el, key: str, default: float = 0.0) -> float: + """稳健读取浮点属性""" + if el is None: 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 def extract_messages_from_xml(cls, xml: str): """ 解析 TikTok 聊天 XML,返回当前屏幕可见的消息与时间分隔: [{"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(): return [] @@ -550,6 +642,20 @@ class AiUtils(object): v = el.get('visible') 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') screen_w = cls.parse_float(app[0], 'width', 414.0) if app else 414.0 @@ -621,6 +727,7 @@ class AiUtils(object): EXCLUDES_LITERAL = { 'Heart', 'Lol', 'ThumbsUp', '分享发布内容', '视频贴纸标签页', '双击发送表情', '贴纸', + '关注', # 注意:仅用于按钮/工具条等短元素,后续还会叠加区域过滤,避免误杀消息 } SYSTEM_PATTERNS = [ r"消息请求已被接受。你们可以开始聊天了。", @@ -635,13 +742,105 @@ class AiUtils(object): r"Get notified when .* replies", r"You opened this chat .* privacy", r"Only \d+ message can be sent .* accepts .* request", - r"此消息可能违反.*", r"无法发送", r"请告知我们" ] 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: txt = get_text(o) if txt in EXCLUDES_LITERAL: @@ -664,8 +863,12 @@ class AiUtils(object): continue if is_toolbar_like(o): continue + cell = get_ancestor_cell(o) + # 仅在“资料卡 Cell 且节点位于资料区块范围内”时过滤 + if is_profile_cell(cell) and belongs_to_profile_region(o, cell): + continue 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 msg_nodes.append(o) else: @@ -680,41 +883,37 @@ class AiUtils(object): continue if not in_view(o) or is_toolbar_like(o): continue + cell = get_ancestor_cell(o) + if is_profile_cell(cell) and belongs_to_profile_region(o, cell): + continue 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 msg_nodes.append(o) - # ---------- 方向判定 & 组装 ---------- + # ---------- 方向判定 & 组装(中心点法) ---------- + CENTER_MARGIN = max(12.0, screen_w * 0.02) # 中线容差 + for o in msg_nodes: txt = get_text(o) if not txt or txt in EXCLUDES_LITERAL or SYSTEM_RE.search(txt): 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) 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="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' + center_x = x + w / 2.0 + screen_center = screen_w / 2.0 - 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' + if center_x < screen_center - CENTER_MARGIN: + direction = 'in' # 左侧:对方 + elif center_x > screen_center + CENTER_MARGIN: + 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}) @@ -725,11 +924,6 @@ class AiUtils(object): it.pop('y', None) return items - - - - - @classmethod def get_navbar_anchor_name(cls, session, timeout: float = 5) -> str: """从聊天页导航栏读取主播名称;找不到返回空字符串。""" @@ -818,8 +1012,6 @@ class AiUtils(object): return "" - - # 检查字符串中是否包含中文 @classmethod def contains_chinese(cls, text): @@ -863,38 +1055,6 @@ class AiUtils(object): with open(file_path, "w", encoding="utf-8") as f: 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 def _normalize_anchor_items(items): """ @@ -929,7 +1089,6 @@ class AiUtils(object): result.append({"anchorId": items}) return result - # -------- 追加(对象数组平铺追加) -------- @classmethod 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.info(f"[acList] 已追加 {len(to_add)} 条,当前总数={len(data)} -> {file_path}") - @classmethod def pop_aclist_first(cls, filename="log/acList.json", mode="pop"): """ @@ -1166,8 +1324,6 @@ class AiUtils(object): print(f"[peek] 读取失败: {e}") return None - - @staticmethod def run_tidevice_command(udid, action, bundle_id, timeout=30): """ @@ -1204,7 +1360,8 @@ class AiUtils(object): return False except FileNotFoundError: # 处理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 except Exception as e: # 捕获其他可能异常 @@ -1235,3 +1392,5 @@ class AiUtils(object): return cls.run_tidevice_command(udid, "launch", bundle_id, timeout) + + diff --git a/Utils/ControlUtils.py b/Utils/ControlUtils.py index 08a19b8..4f03b7b 100644 --- a/Utils/ControlUtils.py +++ b/Utils/ControlUtils.py @@ -58,13 +58,6 @@ class ControlUtils(object): @classmethod def clickBack(cls, session: Client): try: - # back = session.xpath( - # "//*[@label='返回']" - # " | " - # "//*[@label='返回上一屏幕']" - # " | " - # "//XCUIElementTypeButton[@visible='true' and @name='TTKProfileNavBarBaseItemComponent' and @label='IconChevronLeftOffsetLTR']" - # ) back = session.xpath( # ① 常见中文文案 @@ -80,7 +73,6 @@ class ControlUtils(object): ")]" ) - if back.exists: back.click() return True @@ -96,6 +88,13 @@ class ControlUtils(object): if back.exists: back.click() 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: return False except Exception as e: @@ -148,10 +147,9 @@ class ControlUtils(object): videoCell = session.xpath( '(//XCUIElementTypeCollectionView//XCUIElementTypeCell[.//XCUIElementTypeImage[@name="profile_video"]])[1]') - tab = session.xpath( '//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) num = 0 @@ -170,8 +168,6 @@ class ControlUtils(object): print("没有找到主页的第一个视频") return False, num - - @classmethod def clickFollow(cls, session, aid): # 1) 含“关注/已关注/Follow/Following”的首个 cell @@ -199,6 +195,7 @@ class ControlUtils(object): left_x = max(1, rect.x - 20) center_y = rect.y + rect.height // 2 session.tap(left_x, center_y) + @classmethod def userClickProfile(cls, session, aid): try: @@ -283,7 +280,3 @@ class ControlUtils(object): print("开始微滑动") session.swipe(center_x, center_y, end_x, end_y, duration_ms / 1000) print("随机微滑动:", trajectory) - - - - diff --git a/Utils/IOSAIStorage.py b/Utils/IOSAIStorage.py index 946b87b..c9fd6b7 100644 --- a/Utils/IOSAIStorage.py +++ b/Utils/IOSAIStorage.py @@ -13,19 +13,6 @@ class IOSAIStorage: iosai_dir.mkdir(parents=True, exist_ok=True) 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 def save(cls, data: dict | list, filename: str = "data.json", mode: str = "overwrite") -> Path: diff --git a/Utils/JsonUtils.py b/Utils/JsonUtils.py index 50b0f78..f36e9ba 100644 --- a/Utils/JsonUtils.py +++ b/Utils/JsonUtils.py @@ -1,6 +1,8 @@ import json import os from pathlib import Path +from typing import Dict, Any + import portalocker as locker # ① 引入跨平台锁 @@ -118,11 +120,33 @@ class JsonUtils: 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 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) @@ -130,20 +154,19 @@ class JsonUtils: if isinstance(items, dict): items = [items] elif not isinstance(items, list): - # 既不是 dict 也不是 list,直接忽略 return - # 只接受字典项 - items = [it for it in items if isinstance(it, dict)] + # 只保留 sender 非空的字典 + items = [ + it for it in items + if isinstance(it, dict) and it.get("sender") != "" + ] if not items: return data.extend(items) - - # LogManager.method_info(filename,"路径") cls._write_json_list(file_path, data) - @classmethod 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 - # @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 def query_all_json_items(cls, filename="log/last_message.json") -> list: diff --git a/Utils/Requester.py b/Utils/Requester.py index 78f37cb..ce06e93 100644 --- a/Utils/Requester.py +++ b/Utils/Requester.py @@ -1,5 +1,5 @@ import requests -from Entity.Variables import prologueList +from Entity.Variables import prologueList, API_KEY from Utils.IOSAIStorage import IOSAIStorage from Utils.JsonUtils import JsonUtils from Utils.LogManager import LogManager @@ -84,31 +84,63 @@ class Requester(): # ai聊天 @classmethod def chatToAi(cls, param): - aiConfig = JsonUtils.read_json("aiConfig") + + + + + # aiConfig = JsonUtils.read_json("aiConfig") aiConfig = IOSAIStorage.load("aiConfig.json") + + + + agentName = aiConfig.get("agentName") guildName = aiConfig.get("guildName") contactTool = aiConfig.get("contactTool", "") 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 = { "name": agentName, "Trade_union": guildName, "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 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聊天") + print(f"ai聊天的参数:{param}") json = result.json() - data = json.get("answer", {}) - session_id = json.get("conversation_id", {}) + data = json.get("answer", "") + session_id = json.get("conversation_id", "") LogManager.method_info(f"ai聊天返回的内容:{result.json()}", "ai聊天") return data, session_id diff --git a/Utils/TencentOCRUtils.py b/Utils/TencentOCRUtils.py new file mode 100644 index 0000000..e8eed22 --- /dev/null +++ b/Utils/TencentOCRUtils.py @@ -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)) + diff --git a/Utils/ThreadManager.py b/Utils/ThreadManager.py index d77e383..523fe26 100644 --- a/Utils/ThreadManager.py +++ b/Utils/ThreadManager.py @@ -20,7 +20,7 @@ class ThreadManager: @classmethod def add(cls, udid: str, thread: threading.Thread, event: threading.Event) -> Tuple[int, str]: LogManager.method_info(f"准备创建任务:{udid}", "task") - LogManager.method_info("创建线程成功","监控消息") + LogManager.method_info("创建线程成功", "监控消息") with cls._lock: # 判断当前设备是否有任务 if cls._tasks.get(udid, None) is not None: diff --git a/Utils/__pycache__/AiUtils.cpython-312.pyc b/Utils/__pycache__/AiUtils.cpython-312.pyc index 36e8dfc4f7b78c0dac07b15c874e333904bc284e..4f40b2826fb1f527135035508f258d5b81c54eb2 100644 GIT binary patch delta 14849 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 delta 9808 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 delta 312 zcmbQ2_&kpHG%qg~0}wb>T+5idk++MJ@x6yqsT7T?MGLN!OW~VS^(FkUVG!rVcZ6Ix8|C=HYghVLmLw>a4i=GyhFS z#;na}1)ni8R&3rPoXg1Ax>-r|CNpFImGBQ5iEUa^YNlb@Task^X1|apBnSo940~3(-A#UKmQR=EvspBR7?R0_!Y nUS#HASkD>51|=4;s1q1A74ihJ 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 IL`M7n0LfUQV*mgE diff --git a/Utils/__pycache__/LogManager.cpython-312.pyc b/Utils/__pycache__/LogManager.cpython-312.pyc index 3e4d8a68a682cc662ceec83ac79617aad49e18ca..7d6fe1f31b196ea03e0196bdb6dfeb4186e5f153 100644 GIT binary patch delta 20 acmX?Jbi9cBG%qg~0}w2l`(q=wjU@m~oCehZ delta 20 acmX?Jbi9cBG%qg~0}x0le%i=wV+jC7-UZ75 diff --git a/Utils/__pycache__/Requester.cpython-312.pyc b/Utils/__pycache__/Requester.cpython-312.pyc index 3d86a4f7e6d7126f0b5528375e60a94cd4c3a0fa..4af69638468c17be8ecb08741a2fd0a62a86a937 100644 GIT binary patch delta 2056 zcmZuyU2M}<6u!1&Cyt#T=Z}`O=|2$4I?AY~^1H!k1?$kz7^0D?%&pTV#fiIXM{OsU zGHDM%dzd;u6vU?GWl|ACVqy=I_M<&aAZ>eK3KEq!G&I2jdnyA7X-~VyY0|K9<$U*? z?;hWCbMN`C|LlFT)Bm&2=S8Fq$WOCpLQDRp#nZ%FMha3W6{Vo3RVq)X=sc5RDDWB8 zm1k3|W4lx??@qZLn^iq|KE)H8OL=KDj1>0=Nby{uVSa>C!aSAaAA)&2N!#KK)>Lh( zm>J3GhV6d!&7*nlJ%GSpcsa5*Nmnc*li7Xx=%WhG6uCu)2tS zCAJVWpsJiU4O{x3W(G<0rZ1YVz_yaMAQ?i{LTFm7S=Jcs-K{j6)Ft#Ot!%EXnnDH~ zpVMliSE#B_Ayb-`EQ*e^Av07xCMn+VH^Md6*V4d7tfO>-Q(B#uJA!Rn8+a zvQZ?Gw1t9f_czjQJ*tx}oJKva&IRd#uyaSBnB>A%ZzZ35MHQZfJ zSHc4&&zcuSTP-0xxOmSW`$#>nUKl8MR{T9BX0`73RQ%hBFEuZ>EVW$7t}qp8M~U0; z>59}#{HFHL#EW7%eQmtbG+6Sj#uCfwl3Lz>ZCfR_w2MS=XyYfZKo`0DwxyCkb3e zj|n220DwCl^Va1^dhh@{M?Gc`Cq1IU#HreNQpC;&CU~S^kdp5^CF7htu4Z%$(!nlk zU$kk_DaydxMo!iHju90&76x_L!74kVO}!x(i1x5krx^5I%|8m3lMdGOk7r+AQ;KTl z08SGZ?izh7fi;GrsNYcF547tp>b;A4?xL;tynD{MpK{0O05wPbjgHZzb7uM%GE3EG delta 1572 zcmah}O>7%Q6rR~X@A}_a=coBkV-nne)0EPZGz7Ixp+#vxN{gtaDq6CRy=Lu=X4lG( z)=iN(7&uiU)I&jpBT5e~<$!ua$^i)pR23VnJ%Rj4!wbN--k$-H#zW)P};wWlgjU)&L(j!xI`?|%9b)ShV8)q z%v*EE&&)X%(XuzczRh7jE%0MlqF?a+9U*p!gc;N%lCevsL*ihG-rD9VAAp0pAwo;O z4{($|_PrKt0lKhc*$$ED2|01jyP^WO4WOMtQXw6n?*|xUP!pFlq7}SYq!X<5K%jLq zuoVXV4Iw?ZZp#H8@2AfP$AknsqVER#W}?6z0eAu6MFvT^DbPz`^^$&C1eu*S1^7Pr zKBm{O!d>F%R5(Sy6#K@wP0UD4%ACvPA*DT{^Zzn zT;3I2(Uo-hGokEreJ<9ONtS_vbbncP0(3y?KJCiP9Jn6bD#f+17*MKf9iKe4Ah{P1Hhp-BJqPw#^C=%jXjCcx858#;071D zIW&kID8DBO)*$6jlQ(FhJ)*a`A@3X1Mp77EU?WowT?(x}S(ouLTH!CCWt@!9)@02% zX%!cZ>>9lvI~3_>4r{&ztLSJ8PEw$swu}weB%`=ga_qIvoZ~Fn6QiS=ky>T5vu39j ztitF*PIF!-3EHK8vmPWOcu+`2)93THRU`uJ1=G@W;&SADr6SL5CEkNEhpT?w`JR=X0_ z-ek3V@PQ(>_$$(G7thI7Lqt9m82oy`vFB zd(X_+gr`5YcdUCgi=1~1(@wp~is)(e%0}zf87q5MD{9#c@ybYI4cFu>XkH{p=P!A( zVCf|@Gezdu7G4c|n87ZOG5!Tbc2IH$jqIT3cF^#z{)x-J$5I|g@Cts2@|Z22r+)yl CKRvww diff --git a/Utils/__pycache__/ThreadManager.cpython-312.pyc b/Utils/__pycache__/ThreadManager.cpython-312.pyc index 790ff8f05fd8a598f8f75ca1badd3ff9631a2349..6082da1b4b23cd75e4a4a042c4861e1db685e9a5 100644 GIT binary patch delta 32 mcmZorYf$4o&CAQh00f`(-evf1输入->发送->返回""" + coord = self.get_comment_coords(filePath) + if not coord: + return # 没检测到评论按钮就拉倒 + print(11111111111) + cx, cy = coord + session.tap(int(cx / 3), int(cy / 3)) + time.sleep(2) + print(f"评论的坐标:{cx}, {cy}") + # 截图二判(防止键盘弹出后坐标变化) + img = session.screenshot() + time.sleep(2) + filePath = os.path.join(os.path.dirname(filePath), "bgv_comment.png") + img.save(filePath) + + # 从评论列表中随机取出一条数据,进行评论 + single_comment = random.choice(Variables.commentList) + + coord2 = self.get_comment_coords(filePath) + print(single_comment) + if coord2: # 二判命中 + cx2, cy2 = coord2 + print(f"添加评论:{cx2, cy2}") + session.tap(int(cx2 / 3), int(cy2 / 3)) + session.send_keys(f"{single_comment}\n") + time.sleep(2) + LogManager.method_info("评论成功", "养号", udid) + + # 点返回/取消按钮:优先用推荐按钮坐标,没有就兜底 100,100 + tap_x = int(recomend_cx) if recomend_cx else 100 + tap_y = int(recomend_cy) if recomend_cy else 100 + session.tap(tap_x, tap_y) + + # 养号 + def growAccount(self, udid, event, is_monitoring=False): + print("调用刷视频") while not event.is_set(): + + print(11111111111111111) try: # ========= 初始化 ========= - client = wda.USBClient(udid) + client = wda.USBClient(udid, ev.wdaFunctionPort) session = client.session() - + print(2222222222222222) # 关闭并重新打开 TikTok - ControlUtils.closeTikTok(session, udid) + if not is_monitoring: + ControlUtils.closeTikTok(session, udid) + event.wait(timeout=1) + ControlUtils.openTikTok(session, udid) + event.wait(timeout=3) - event.wait(timeout=1) - ControlUtils.openTikTok(session, udid) - event.wait(timeout=3) LogManager.method_info("养号重启tiktok", "养号", udid) - AiUtils.makeUdidDir(udid) + AiUtils.makeUdidDir(udid) + recomend_cx = 0 + recomend_cy = 0 # ========= 主循环 ========= while not event.is_set(): - # 设置手机的节点深度为15,判断该页面是否正确 session.appium_settings({"snapshotMaxDepth": 15}) @@ -64,6 +130,13 @@ class ScriptManager(): '//XCUIElementTypeButton[@name="top_tabs_recomend" or @name="推荐" or @label="推荐"]' ) + # 获取推荐按钮所在的坐标 + if el.exists: + bounds = el.bounds # 返回 [x, y, width, height] + recomend_cx = bounds[0] + bounds[2] // 2 + recomend_cy = bounds[1] + bounds[3] // 2 + + if not el.exists: # 记录日志 LogManager.method_error("找不到推荐按钮,养号出现问题,重启养号功能", "养号", udid=udid) @@ -96,6 +169,7 @@ class ScriptManager(): # ---- 视频逻辑 ---- try: addX, addY = AiUtils.findImageInScreen("add", udid) + isSame = False for i in range(2): tx, ty = AiUtils.findImageInScreen("add", udid) @@ -104,7 +178,7 @@ class ScriptManager(): event.wait(timeout=1) else: isSame = False - break + # break if addX > 0 and isSame: needLike = random.randint(0, 100) @@ -121,7 +195,7 @@ class ScriptManager(): LogManager.method_info("停止脚本成功", method="task") # 重置 session - client = wda.USBClient(udid) + client = wda.USBClient(udid, ev.wdaFunctionPort) session = client.session() session.appium_settings({"snapshotMaxDepth": 0}) @@ -130,23 +204,55 @@ class ScriptManager(): ControlUtils.clickLike(session, udid) LogManager.method_info("继续观看视频", "养号", udid) - videoTime = random.randint(25, 40) - for _ in range(videoTime): - if event.is_set(): - break - event.wait(timeout=1) LogManager.method_info("准备划到下一个视频", "养号", udid) client.swipe_up() else: LogManager.method_error("找不到首页按钮。出错了", "养号", udid) - else: - nextTime = random.randint(1, 5) - for _ in range(nextTime): - if event.is_set(): + # else: + # nextTime = random.randint(1, 5) + # for _ in range(nextTime): + # if event.is_set(): + # break + # event.wait(timeout=1) + # client.swipe_up() + + # 评论 + # 使用训练好的best.pt(yolo v8模型)进行识别评论区域 + + if random.random() > 0.70: + self.comment_flow(filePath, session, udid, recomend_cx, recomend_cy) + + videoTime = random.randint(15, 30) + for _ in range(videoTime): + if event.is_set(): + break + event.wait(timeout=1) + + session.swipe_up() + + # 如果is_monitoring 为False 则说明和监控消息没有联动,反正 则证明和监控消息进行联动 + if is_monitoring: + # 监控消息按钮,判断是否有消息 + 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: break - event.wait(timeout=1) - client.swipe_up() + else: + continue except Exception as e: LogManager.method_error(f"刷视频过程出现错误,重试", "养号", udid) @@ -166,7 +272,7 @@ class ScriptManager(): while not event.is_set(): try: # —— 每次重启都新建 client/session —— - client = wda.USBClient(udid) + client = wda.USBClient(udid,ev.wdaFunctionPort) session = client.session() session.appium_settings({"snapshotMaxDepth": 15}) @@ -284,7 +390,7 @@ class ScriptManager(): LogManager.method_error(f"watchLiveForGrowth 异常(第{retry_count}次):{repr(e)}", "直播养号", udid) # 尝试轻量恢复一次,避免一些短暂性 session 失效 try: - client = wda.USBClient(udid) + client = wda.USBClient(udid,ev.wdaFunctionPort) _ = client.session() except Exception: pass @@ -300,12 +406,12 @@ class ScriptManager(): 关注打招呼以及回复主播消息 """ - def safe_greetNewFollowers(self, udid, needReply, event): + def safe_greetNewFollowers(self, udid, needReply, needTranslate, event): retries = 0 while not event.is_set(): try: - self.greetNewFollowers(udid, needReply, event) + self.greetNewFollowers(udid, needReply, needTranslate, event) except Exception as e: retries += 1 @@ -318,14 +424,15 @@ class ScriptManager(): LogManager.method_error("greetNewFollowers 重试次数耗尽,任务终止", "关注打招呼", udid) # 关注打招呼以及回复主播消息 - def greetNewFollowers(self, udid, needReply, event): + def greetNewFollowers(self, udid, needReply, needTranslate, event): - client = wda.USBClient(udid) + client = wda.USBClient(udid,ev.wdaFunctionPort) session = client.session() print(f"是否要自动回复消息:{needReply}") LogManager.method_info(f"是否要自动回复消息:{needReply}", "关注打招呼", udid) + LogManager.method_info(f"是否需要进行翻译:{needTranslate}", "关注打招呼", udid) # 先关闭Tik Tok ControlUtils.closeTikTok(session, udid) @@ -368,6 +475,7 @@ class ScriptManager(): LogManager.method_info(f"数据是:{result}", "关注打招呼", udid) state = result.get("state", 0) + if not state: LogManager.method_info(f"当前主播的状态是:{state} 不通行,取出数据移到列表尾部 继续下一个", "关注打招呼", udid) @@ -485,6 +593,11 @@ class ScriptManager(): break event.wait(timeout=1) LogManager.method_info("停止脚本成功", method="task") + + + # 使用yolo v8模型进行评论 + self.comment_flow(filePath, session, udid, 100, 100) + if count != 0: client.swipe_up() @@ -535,21 +648,18 @@ class ScriptManager(): # 准备打招呼的文案 text = random.choice(ev.prologueList) - # text = 'hello' + # text = '你好啊' - LogManager.method_info(f"取出打招呼的数据,{text}, 判断是否需要翻译", "关注打招呼", udid) + LogManager.method_info(f"取出打招呼的数据,{text}, 判断是否需要翻译:{needTranslate}", "关注打招呼", + udid) + # isContainChniese = AiUtils.contains_chinese(text) - isContainChniese = AiUtils.contains_chinese(text) - - if isContainChniese: + if needTranslate: # 翻译成主播国家的语言 LogManager.method_info(f"需要翻译:{text},参数为:国家为{anchorCountry}, 即将进行翻译", "关注打招呼", udid) - msg = Requester.translation(text, anchorCountry) - LogManager.method_info(f"翻译成功:{msg}, ", "关注打招呼", udid) - else: msg = text LogManager.method_info(f"即将发送的私信内容:{msg}", "关注打招呼", udid) @@ -625,7 +735,7 @@ class ScriptManager(): event.wait(timeout=3) print("重新创建wda会话 防止wda会话失效") - client = wda.USBClient(udid) + client = wda.USBClient(udid,ev.wdaFunctionPort) session = client.session() # 执行完成之后。继续点击搜索 session.appium_settings({"snapshotMaxDepth": 15}) @@ -634,12 +744,12 @@ class ScriptManager(): print("greetNewFollowers方法执行完毕") - def safe_followAndGreetUnion(self, udid, needReply, event): + def safe_followAndGreetUnion(self, udid, needReply, needTranslate, event): retries = 0 while not event.is_set(): try: - self.followAndGreetUnion(udid, needReply, event) + self.followAndGreetUnion(udid, needReply, needTranslate, event) except Exception as e: retries += 1 @@ -652,9 +762,9 @@ class ScriptManager(): LogManager.method_error("greetNewFollowers 重试次数耗尽,任务终止", "关注打招呼", udid) # 关注打招呼以及回复主播消息(联盟号) - def followAndGreetUnion(self, udid, needReply, event): + def followAndGreetUnion(self, udid, needReply, needTranslate, event): - client = wda.USBClient(udid) + client = wda.USBClient(udid,ev.wdaFunctionPort) session = client.session() print(f"是否要自动回复消息:{needReply}") @@ -801,13 +911,13 @@ class ScriptManager(): # 准备打招呼的文案 text = random.choice(ev.prologueList) - # text = 'hello' + # text = '你好' LogManager.method_info(f"取出打招呼的数据,{text}, 判断是否需要翻译", "关注打招呼(联盟号)", udid) isContainChniese = AiUtils.contains_chinese(text) - if isContainChniese: + if needTranslate: # 翻译成主播国家的语言 LogManager.method_info(f"需要翻译:{text},参数为:国家为{anchorCountry}, 即将进行翻译", "关注打招呼(联盟号)", udid) @@ -884,7 +994,7 @@ class ScriptManager(): event.wait(timeout=3) print("重新创建wda会话 防止wda会话失效") - client = wda.USBClient(udid) + client = wda.USBClient(udid,ev.wdaFunctionPort) session = client.session() # 执行完成之后。继续点击搜索 session.appium_settings({"snapshotMaxDepth": 15}) @@ -893,13 +1003,11 @@ class ScriptManager(): print("greetNewFollowers方法执行完毕") - - # 检测消息 def replyMessages(self, udid, event): try: - client = wda.USBClient(udid) + client = wda.USBClient(udid,ev.wdaFunctionPort) session = client.session() except Exception as e: LogManager.method_error(f"创建wda会话异常: {e}", "检测消息", udid) @@ -924,7 +1032,7 @@ class ScriptManager(): LogManager.method_info(f"出现异常时,稍等再重启 TikTok 并重试 异常是: {e}", "监控消息", udid) LogManager.method_info(f"出现异常,重新创建wda", "监控消息", udid) - client = wda.USBClient(udid) + client = wda.USBClient(udid,ev.wdaFunctionPort) session = client.session() LogManager.method_info(f"重启 TikTok", "监控消息", udid) @@ -938,322 +1046,6 @@ class ScriptManager(): LogManager.method_info("TikTok 重启成功", "监控消息", udid) 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 I’m one of the most engaging streamers you’ve 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): LogManager.method_info("脚本开始执行中", "监控消息") @@ -1271,18 +1063,21 @@ class ScriptManager(): # 如果收件箱有消息 则进行点击 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: + print("循环开始") + + info_count = 0 # 创建新的会话 @@ -1297,6 +1092,7 @@ class ScriptManager(): print("el", el) if not el.exists: LogManager.method_error(f"检测不到收件箱", "检测消息", udid) + raise Exception("当前页面找不到收件箱,重启") # break @@ -1307,19 +1103,23 @@ class ScriptManager(): " or @value='Inbox' or @label='Inbox' or @name='Inbox']" ) + print("11111111111111") # 查找所有收件箱节点 inbox_nodes = session.xpath(xpath_query).find_elements() - + print("222222222222222") if len(inbox_nodes) < 2: LogManager.method_error(f"当前页面不再收件箱页面,重启", "检测消息", udid) raise Exception("当前页面不再收件箱页面,重启") + print("33333333333333") m = re.search(r'(\d+)', el.label) # 抓到的第一个数字串 count = int(m.group(1)) if m else 0 + print("444444444444444444") if not count: LogManager.method_info(f"当前收件箱的总数量{count}", "检测消息", udid) break + # print("5555555555555555555555") # 新粉丝 xp_new_fan_badge = ( @@ -1362,6 +1162,7 @@ class ScriptManager(): "[@value and translate(@value,'0123456789','')='']" "/ancestor::XCUIElementTypeCell[1]" ) + print("6666666666666666") try: # 如果 2 秒内找不到,会抛异常 @@ -1375,44 +1176,87 @@ class ScriptManager(): print("当前屏幕没有找到 用户消息 未读徽标数字", udid) user_text = None info_count += 1 + print("777777777777777777777") if user_text: user_text.tap() event.wait(timeout=3) + # xml = session.source() + # msgs = AiUtils.extract_messages_from_xml(xml) + # + # # 检测出对方发的最后一条信息, + # + # # 获取了最后一条消息 + # last_msg = next((item['text'] for item in reversed(msgs) if item['type'] == 'msg'), + # "") + xml = session.source() + time.sleep(1) msgs = AiUtils.extract_messages_from_xml(xml) + time.sleep(1) - text_list = ['What do you think of my live stream?', - 'What do you think makes my streams special?', - 'Do you think I’m one of the most engaging streamers you’ve seen?'] + last_in = None # 对方最后一句 + last_out = None # 我方最后一句 + attempt = 0 # 已试次数(首次算第 1 次) - # 检测出对方发的最后一条信息, + while attempt < 3: + # 1. 本轮扫描 + for item in reversed(msgs): + 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 or last_out: # 任一条拿到就提前停 + break - # 获取了最后一条消息 - 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) + # 2. 只有两条都空才重试 + if not last_in and not last_out: + attempt += 1 + if attempt == 3: + break # 三次用完,放弃 + time.sleep(0.2) + xml = session.source() + msgs = AiUtils.extract_messages_from_xml(xml) + continue + else: + break # 至少一条有内容,成功退出 + LogManager.method_info(f"检测到对方最后发送的消息:{last_in}", "检测消息", udid) + LogManager.method_info(f"检测我发送的最后一条信息:{last_out}", "检测消息", udid) # 如果最后一条消息不是文字,随机取出一条当做最后一条消息,最后一条消息是last_msg_text - isLanguage = AiUtils.is_language(last_msg) - - if isLanguage: - last_msg_text = last_msg - else: - LogManager.method_info(f"对方发送的消息不是语言,随机挑选作为最后一条进行回复:{last_msg}", - "检测消息", udid) - last_msg_text = random.choice(text_list) + # isLanguage = AiUtils.is_language(last_msg) + # + # if isLanguage: + # last_msg_text = last_msg + # else: + # LogManager.method_info(f"对方发送的消息不是语言,随机挑选作为最后一条进行回复:{last_msg}", + # "检测消息", udid) + # # last_msg_text = random.choice(text_list) + # last_msg_text = last_msg # 获取主播的名称 - anchor_name = AiUtils.get_navbar_anchor_name(session) + # anchor_name = AiUtils.get_navbar_anchor_name(session) + anchor_name = "" + for _ in range(3): # 最多 2 次重试 + 1 次初始 + anchor_name = AiUtils.get_navbar_anchor_name(session) + if anchor_name: + break + time.sleep(1) + + + LogManager.method_info(f"获取主播的名称:{anchor_name}", "检测消息", udid) - LogManager.method_info(f"获取主播最后发送的消息 进行翻译:{last_msg}", "检测消息", udid) - chinese_last_msg_text = Requester.translationToChinese(last_msg_text) + LogManager.method_info(f"获取主播最后发送的消息 即将翻译:{last_in}", "检测消息", udid) + chinese_last_msg_text = Requester.translationToChinese(last_in) + + # 进行判断,判断翻译后是否 + LogManager.method_info(f"翻译中文后的内容,交给前端进行展示:{chinese_last_msg_text}", "检测消息", udid) @@ -1420,13 +1264,17 @@ class ScriptManager(): last_data = [{ "sender": anchor_name, "device": udid, + "time": datetime.now().strftime("%Y-%m-%d %H:%M"), "text": chinese_last_msg_text, "status": 0 }] - print(last_data) LogManager.method_info(f"主播最后发送的数据,传递给前端进行记录:{chinese_last_msg_text}", "检测消息", udid) + + + + # 把主播的名称存储到c盘 JsonUtils.append_json_items(last_data, "log/last_message.json") # 从C盘中读取数据 @@ -1437,20 +1285,34 @@ class ScriptManager(): # 如果是第一次发消息(没有sessionId的情况) LogManager.method_info(f"第一次发消息:{anchor_name},没有记忆 开始请求ai", "检测消息", udid) - LogManager.method_info(f"向ai发送的参数: 文本为:{last_msg_text}", "检测消息", udid) - aiResult, sessionId = Requester.chatToAi({"query": last_msg_text, "user": "1"}) - IOSAIStorage.save({anchor_name: sessionId}, mode="merge") + LogManager.method_info(f"向ai发送的参数: 文本为:{last_in}", "检测消息", udid) - # 找到输入框,输入ai返回出来的消息 - - if sel.exists: - sel.click() # 聚焦 - event.wait(timeout=1) - sel.clear_text() - sel.set_text(f"{aiResult or '暂无数据'}\n") + if last_in is None: + LogManager.method_info(f"检测不到对方发送的最后一条消息,发送一条打招呼", "检测消息", + udid) + text = random.choice(ev.prologueList) + if sel.exists: + sel.click() # 聚焦 + event.wait(timeout=1) + sel.clear_text() + sel.set_text(f"{text or '暂无数据'}\n") + else: + LogManager.method_error("找不到输入框,重启", "检测消息", udid) + raise Exception("找不到输入框,重启") else: - LogManager.method_error("找不到输入框,重启", "检测消息", udid) - raise Exception("找不到输入框,重启") + aiResult, sessionId = Requester.chatToAi({"query": last_in, "user": "1"}) + IOSAIStorage.save({anchor_name: sessionId}, mode="merge") + + # 找到输入框,输入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) @@ -1458,11 +1320,11 @@ class ScriptManager(): # TODO: user后续添加,暂时写死 - LogManager.method_info(f"向ai发送的参数: 文本为:{last_msg_text}", "检测消息", udid) + LogManager.method_info(f"向ai发送的参数: 文本为:{last_in}", "检测消息", udid) aiResult, sessionId = Requester.chatToAi( - {"query": last_msg_text, "conversation_id": sessionId, "user": "1"}) - # aiResult = response['result'] + {"query": last_in, "conversation_id": sessionId, "user": "1"}) + if sel.exists: sel.click() # 聚焦 event.wait(timeout=1) @@ -1498,7 +1360,6 @@ class ScriptManager(): info_count += 1 try: - # 如果 2 秒内找不到,会抛异常 badge_text = session.xpath(xp_activity_badge).get(timeout=2.0) val = (badge_text.info.get("value") or @@ -1560,7 +1421,18 @@ class ScriptManager(): session.double_tap(cx, cy) # 可能抛异常:方法不存在 LogManager.method_info(f"双击收件箱 定位到信息", "检测消息", udid) + + else: + + # print("回到首页刷视频") + # # ControlUtils.backToHome(session) + # homeButton = AiUtils.findHomeButton(udid) + # if homeButton.exists: + # homeButton.click() # 当前的消息为0,则调用刷视频的方法 + # # 调用刷视频的方法 + # self.growAccount(udid, event, True) + return else: LogManager.method_error(f"检测不到收件箱", "检测消息", udid) @@ -1576,82 +1448,192 @@ class ScriptManager(): left -= timeout return not event.is_set() # 返回 True 表示正常睡完,False 被中断 + # 切换账号工具方法 + def _norm_txt(self, s: str) -> str: + return (s or "").strip() + + def _dedup_preserve_order(self, seq): + seen = set() + out = [] + for x in seq: + if x not in seen: + out.append(x) + seen.add(x) + return out + # 切换账号 - def changeAccount(self, udid, event): + def changeAccount(self, udid): - 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() - - # 使用截图进行保存,保存进行图像的识别 + count = 0 + while count <= 5: 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) + LogManager.method_info("开始进行切换账号", "切换账号", udid) + + client = wda.USBClient(udid,ev.wdaFunctionPort) + session = client.session() + + # 重启打开 + ControlUtils.closeTikTok(session, udid) + time.sleep(1) + ControlUtils.openTikTok(session, udid) + 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: + user_home.click() + LogManager.method_info("进入主页成功", "切换账号", udid) + else: + LogManager.method_info("未检测到主页按钮,后续流程可能失败", "切换账号", udid) + count += 1 + continue + + # 点击“切换账号”按钮(能点就点;点不到再走 OCR) + session.appium_settings({"snapshotMaxDepth": 25}) + switch_btn = session.xpath( + '//XCUIElementTypeButton[' + '@name="Switch accounts" or @label="Switch accounts" or ' + '@name="切换账号" or @label="切换账号" or ' + '@name="切換帳號" or @label="切換帳號"' + ']' + ) + if switch_btn.exists: + switch_btn.click() + LogManager.method_info("已点击“切换账号”", "切换账号", udid) + else: + LogManager.method_info("未检测到“切换账号”按钮,转入 OCR 兜底", "切换账号", udid) + + # 截图 → OCR + try: + img = client.screenshot() + base_dir = os.path.abspath(os.path.join(os.path.dirname(__file__), "..")) + res_dir = os.path.join(base_dir, "resources", udid) + os.makedirs(res_dir, exist_ok=True) + file_path = os.path.join(res_dir, "account_list.png") + img.save(file_path) + time.sleep(1) + LogManager.method_info(f"保存屏幕图像成功 -> {file_path}", "切换账号", udid) + except Exception as e: + LogManager.method_info(f"截图或保存失败, 原因:{e}", "切换账号", udid) + count += 1 + continue + + image_path = AiUtils.imagePathWithName(udid, "account_list") + ocr_json = TencentOCR.recognize(image_path=image_path, action="GeneralBasicOCR") + + LogManager.method_info(f"OCR 结果: {ocr_json}", "切换账号", udid) + + # 提取“切换账号”与“添加账号”之间的用户名候选(只看像用户名的) + items = TencentOCR.slice_texts_between(ocr_json, "切换账号", "添加账号", username_like=True) + + # 稳定排序(Y 再 X) + items_sorted = sorted( + items, + key=lambda d: (d.get("ItemPolygon", {}).get("Y", 0), + d.get("ItemPolygon", {}).get("X", 0)) + ) + + # 规范化 & 去重保序 + def _norm_txt(s: str) -> str: + return (s or "").strip().lstrip("@") + + def _dedup(seq): + seen, out = set(), [] + for x in seq: + if x and x not in seen: + seen.add(x) + out.append(x) + return out + + usernames_from_ocr = _dedup([_norm_txt(d.get("DetectedText", "")) for d in items_sorted]) + LogManager.method_info(f"OCR 提取候选账号(排序后): {usernames_from_ocr}", "切换账号", udid) + + if not usernames_from_ocr: + LogManager.method_info("OCR 未发现任何账号,无法切换", "切换账号", udid) + count += 1 + continue + + # —— 读取与更新轮询状态 —— # + state_path = f"{udid}/accountState.json" + state = IOSAIStorage.load(state_path) or {"accounts": [], "idx": 0} + old_accounts = state.get("accounts") or [] + + # 只有“集合真的变化”才更新 accounts;否则保持旧顺序不动,保证轮询不抖动 + if set(old_accounts) != set(usernames_from_ocr): + merged = [a for a in old_accounts if a in usernames_from_ocr] + \ + [u for u in usernames_from_ocr if u not in old_accounts] + state["accounts"] = merged + state["idx"] = 0 # 集合变化才重置 + LogManager.method_info(f"账号集合变化,合并后顺序: {merged},重置 idx=0", "切换账号", udid) + else: + if not old_accounts: + state["accounts"] = usernames_from_ocr + state["idx"] = 0 + + accounts = state["accounts"] + if not accounts: + LogManager.method_info("账号列表为空", "切换账号", udid) + count += 1 + continue + + # —— 核心轮询:1→2→0→1→2→… —— # + n = len(accounts) + try: + idx = int(state.get("idx", 0)) % n + except Exception: + idx = 0 + next_idx = (idx + 1) % n # 本次选择“下一位” + target_account = accounts[next_idx] + + # 立刻推进并落盘:保存为 next_idx,下一次会从它的下一位继续 + state["idx"] = next_idx + IOSAIStorage.save(state, state_path) + + LogManager.method_info( + f"本次切换到账号: {target_account} (use={next_idx}, next={(next_idx + 1) % n})", + "切换账号", + udid + ) + + # 在同一份 OCR 结果里定位该用户名的坐标并点击 + result = TencentOCR.find_last_name_bbox(ocr_json, target_account) + if not result: + LogManager.method_info(f"OCR 未找到目标账号文本: {target_account}", "切换账号", udid) + count += 1 + continue + + center_x = result["center"]["x"] + center_y = result["center"]["y"] + + # 随机偏移(增强拟人) + num = random.randint(-60, 60) + # 分辨率/坐标映射(按你设备比例;你原来是 /3) + tap_x = int((center_x + num) / 3) + tap_y = int((center_y + num) / 3) + + LogManager.method_info(f"点击坐标: ({tap_x}, {tap_y}),账号: {target_account}", "切换账号", udid) + session.tap(tap_x, tap_y) + return 200, "成功" + except Exception as e: - LogManager.method_info(f"截图或保存失败,失败原因:{e}", "养号", udid) - raise Exception("截图或保存失败,重启养号功能") + LogManager.method_error(f"切换账号失败, 错误: {e}", "切换账号", udid) + count += 1 + return 500, "失败" - # 使用EasyOcr进行识别, + def test(self): + # 找到输入框 + anchor_name = "sss" + udid = "sss" + last_data = [{ + "sender": anchor_name, + "device": udid, + "time": datetime.now().strftime("%Y-%m-%d %H:%M"), + "text": "哈哈哈", + "status": 0 + }] + print(last_data) - # 取出图片路径 - 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("未找到目标名称") + JsonUtils.append_json_items(last_data, "log/last_message.json") diff --git a/script/__pycache__/ScriptManager.cpython-312.pyc b/script/__pycache__/ScriptManager.cpython-312.pyc index a495cb419b73cb1110e7df41fa5bd8769e23960f..b0c97bdfcc5f280e5c5364f546932e3f4f84e711 100644 GIT binary patch delta 27271 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 delta 17788 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