3830 lines
150 KiB
Python
3830 lines
150 KiB
Python
"""
|
||
微信控制技能 - Agent端实现
|
||
|
||
功能模块:
|
||
1. 消息管理 - 发送/获取消息、批量发送
|
||
2. 好友管理 - 添加/通过好友、设置备注、删除好友
|
||
3. 群聊管理 - 创建群/邀请入群/群发消息/设置群欢迎语
|
||
4. 标签管理 - 添加/删除/查询标签
|
||
5. 朋友圈管理 - 发布/点赞/评论
|
||
|
||
@author 卡若
|
||
@version 3.0.0
|
||
"""
|
||
|
||
import time
|
||
import logging
|
||
import re
|
||
import shlex
|
||
import sys
|
||
import os
|
||
from typing import Dict, Any, List, Optional
|
||
_agent_dir = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||
if _agent_dir not in sys.path:
|
||
sys.path.insert(0, _agent_dir)
|
||
from skills.base import BaseSkill
|
||
from error_handler import retry, with_error_handling, ErrorHandler
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
# 截屏 + AI 视觉(每次操作都先看屏再决定)
|
||
class _VisionFallback(Exception):
|
||
"""视觉流程失败,回退到规则流程"""
|
||
pass
|
||
|
||
|
||
def _vision_step(self, goal: str, step_hint: str = "") -> dict:
|
||
"""截屏 → AI 看屏 → 返回下一步动作"""
|
||
try:
|
||
from vision_helper import ask_vision
|
||
img = self.screenshot()
|
||
return ask_vision(img, goal, step_hint)
|
||
except Exception as e:
|
||
logger.warning(f"视觉调用失败: {e}")
|
||
return {"action": "error", "error": str(e)}
|
||
|
||
|
||
class WechatSkill(BaseSkill):
|
||
"""
|
||
微信控制技能 — 96个action全覆盖
|
||
|
||
控制通道(按优先级):
|
||
1. Frida Hook — 直接调用微信内部方法/读SQLite, 50-200ms
|
||
2. u2 UI自动化 — uiautomator2模拟点击/输入, 2-15s(本文件实现)
|
||
3. ADB Shell — adb命令+坐标点击, 1-10s(wechat_adb_engine.py)
|
||
|
||
功能模块(29个模块 / 96个action):
|
||
消息: send_message, get_messages, forward_message, recall_message, send_card, batch_send_message
|
||
好友: add_friend, accept_friend, set_remark, delete_friend, get_contacts, search_contact,
|
||
get_friend_info, batch_add_friend
|
||
群聊: create_group, invite_to_group, remove_from_group, set_group_notice, set_group_name,
|
||
send_group_message, set_group_welcome, get_groups, get_group_members, quit_group
|
||
标签: add_tag, remove_tag, create_tag, delete_tag, get_tags, get_users_by_tag
|
||
朋友圈: post_moments, like_moments, comment_moments, get_moments, delete_moments,
|
||
set_moments_cover, set_moments_privacy, forward_moments_link
|
||
个人: get_profile, set_nickname, set_signature, set_avatar, set_gender, set_region
|
||
安全: check_account_status, unblock_account, safety_center, change_password,
|
||
unblock_self, unblock_appeal, check_restrictions, appeal_restriction, unblock_with_sms
|
||
支付: send_red_packet, transfer, show_payment_code, receive_payment, view_wallet,
|
||
view_transactions, receive_red_packet
|
||
聊天: set_chat_top, set_mute_chat, clear_chat_history
|
||
收藏: add_to_favorites, get_favorites
|
||
小程序: open_mini_program | 公众号: follow_official_account
|
||
视频号: open_video_channel, get_video_list, like_video, comment_video,
|
||
follow_video_creator, share_video
|
||
扫码: scan_qr_code, scan_add_friend, show_my_qr, extract_qr_from_image
|
||
通话: voice_call, video_call
|
||
群发: mass_send | 搜索: wechat_search | 发现: top_stories
|
||
运动: get_steps, like_steps
|
||
位置: send_location, share_real_time_location
|
||
表情: send_emoji, get_sticker_list
|
||
语音: send_voice_message | 文件: send_file_from_chat, download_file
|
||
设置: toggle_do_not_disturb, clear_cache, check_for_update, logout, switch_account
|
||
截图: screenshot
|
||
"""
|
||
|
||
PACKAGE = "com.tencent.mm"
|
||
NAME = "微信"
|
||
SEND_MESSAGE_TIMEOUT = 45
|
||
MAX_TEST_PAYMENT_AMOUNT = 1.0
|
||
|
||
def _payment_guard(self, amount: str = "", confirm: bool = False, **kwargs) -> Dict[str, Any]:
|
||
"""资金类动作保护:默认只导航到页面;真确认必须显式授权且金额在测试上限内。"""
|
||
try:
|
||
amt = float(str(amount or "0").strip() or "0")
|
||
except Exception:
|
||
return {"pass": False, "error": "amount 必须是数字"}
|
||
if amt < 0:
|
||
return {"pass": False, "error": "amount 不能为负数"}
|
||
if not confirm:
|
||
return {
|
||
"pass": True,
|
||
"dry_run": True,
|
||
"confirm_required": True,
|
||
"amount": amt,
|
||
"note": "已按内部测试保护策略停止在最终确认前",
|
||
}
|
||
if amt > self.MAX_TEST_PAYMENT_AMOUNT:
|
||
return {
|
||
"pass": False,
|
||
"error": f"测试金额超过上限 {self.MAX_TEST_PAYMENT_AMOUNT}",
|
||
"amount": amt,
|
||
}
|
||
if not kwargs.get("test_whitelist_ok", False):
|
||
return {
|
||
"pass": False,
|
||
"error": "资金动作需要 test_whitelist_ok=true",
|
||
"amount": amt,
|
||
}
|
||
return {"pass": True, "dry_run": False, "confirm_required": False, "amount": amt}
|
||
|
||
def send_message_with_vision(self, to_id: str, content: str, msg_type: str = "text") -> Dict[str, Any]:
|
||
"""
|
||
发送消息(以截屏+AI为主,每次操作都先看屏再决定)
|
||
"""
|
||
try:
|
||
start = time.time()
|
||
self.launch()
|
||
self.sleep(0.8)
|
||
goal = f"给 {to_id} 发消息:{content}"
|
||
max_steps = 15
|
||
for step in range(max_steps):
|
||
if time.time() - start > self.SEND_MESSAGE_TIMEOUT:
|
||
return {"success": False, "error": "timeout"}
|
||
hint = f"步骤 {step+1}/{max_steps}。"
|
||
act = _vision_step(self, goal, hint)
|
||
a = act.get("action", "")
|
||
if a == "done":
|
||
logger.info(f"微信消息发送成功(视觉): {to_id}")
|
||
return {"success": True, "message_id": f"wx_{int(time.time() * 1000)}"}
|
||
if a == "error":
|
||
logger.warning(f"视觉返回错误: {act.get('error')},回退到规则流程")
|
||
raise _VisionFallback()
|
||
if a == "click":
|
||
x, y = int(act.get("x", 540)), int(act.get("y", 1200))
|
||
self.click(x, y)
|
||
self.sleep(0.3)
|
||
elif a == "input":
|
||
t = act.get("text", "")
|
||
if t:
|
||
self.input_text(t, clear=(step < 2))
|
||
self.sleep(0.2)
|
||
elif a == "back":
|
||
self.back()
|
||
self.sleep(0.3)
|
||
raise _VisionFallback()
|
||
except _VisionFallback:
|
||
raise
|
||
except Exception as e:
|
||
logger.error(f"视觉发送失败: {e}")
|
||
raise _VisionFallback()
|
||
|
||
@with_error_handling("发送微信消息")
|
||
@retry(max_retries=1, delay=1)
|
||
def send_message(self, to_id: str, content: str, msg_type: str = "text") -> Dict[str, Any]:
|
||
"""
|
||
发送消息(坐标导航 + accessibility 双通道)。
|
||
微信的自定义 View 不暴露给 accessibility tree,核心流程用坐标点击。
|
||
"""
|
||
self.natural_behavior_before("send_message")
|
||
start = time.time()
|
||
info = self.d.info
|
||
W, H = info['displayWidth'], info['displayHeight']
|
||
|
||
try:
|
||
# ── 1. 确保在微信主界面 ──
|
||
curr = self.d.app_current()
|
||
if curr.get("package") != self.PACKAGE:
|
||
self.launch()
|
||
self.sleep(2)
|
||
elif "ChattingUI" in (curr.get("activity") or ""):
|
||
self.press("back")
|
||
self.sleep(0.5)
|
||
|
||
for _ in range(3):
|
||
curr = self.d.app_current()
|
||
act = curr.get("activity", "")
|
||
if act.endswith(".LauncherUI"):
|
||
break
|
||
self.press("back")
|
||
self.sleep(0.5)
|
||
else:
|
||
self.launch()
|
||
self.sleep(2)
|
||
|
||
# ── 2. 打开搜索(坐标:标题栏放大镜,在 + 号左侧) ──
|
||
search_x = int(W * 0.82)
|
||
search_y = int(H * 0.06)
|
||
self.click(search_x, search_y)
|
||
self.sleep(1.5)
|
||
|
||
curr = self.d.app_current()
|
||
if "FTSMainUI" not in (curr.get("activity") or ""):
|
||
self.click(search_x - 60, search_y)
|
||
self.sleep(1)
|
||
|
||
# ── 3. 输入联系人 ──
|
||
self.click(int(W * 0.5), int(H * 0.035))
|
||
self.sleep(0.3)
|
||
try:
|
||
self.d.set_input_ime(True)
|
||
except Exception as e:
|
||
logger.warning(f"ADBKeyboard 启用失败,使用 root input text 兜底: {e}")
|
||
self.sleep(0.2)
|
||
self.input_text(to_id, clear=False)
|
||
self.sleep(2.5)
|
||
|
||
# ── 4. 点击搜索结果(微信自定义控件不在 a11y tree,用坐标) ──
|
||
result_x = int(W * 0.3)
|
||
entered_chat = False
|
||
for result_y in [int(H * 0.20), int(H * 0.22), int(H * 0.18), int(H * 0.25)]:
|
||
self.click(result_x, result_y)
|
||
self.sleep(2)
|
||
curr = self.d.app_current()
|
||
if "ChattingUI" in (curr.get("activity") or ""):
|
||
entered_chat = True
|
||
break
|
||
if "FTSMainUI" not in (curr.get("activity") or ""):
|
||
self.press("back")
|
||
self.sleep(0.5)
|
||
|
||
if not entered_chat:
|
||
if self.click_text(to_id, timeout=2) or self.click_contains(to_id, timeout=2):
|
||
self.sleep(2)
|
||
curr = self.d.app_current()
|
||
entered_chat = "ChattingUI" in (curr.get("activity") or "")
|
||
|
||
if not entered_chat:
|
||
return {"success": False, "error": f"未找到联系人: {to_id}"}
|
||
|
||
if time.time() - start > self.SEND_MESSAGE_TIMEOUT:
|
||
return {"success": False, "error": "timeout"}
|
||
|
||
# ── 5. 聚焦输入框 ──
|
||
input_y = int(H * 0.955)
|
||
self.click(int(W * 0.3), input_y)
|
||
self.sleep(0.5)
|
||
|
||
# ── 6. 输入消息 ──
|
||
try:
|
||
self.d.set_input_ime(True)
|
||
except Exception as e:
|
||
logger.warning(f"ADBKeyboard 启用失败,使用 root input text 兜底: {e}")
|
||
self.sleep(0.2)
|
||
self.input_text(content, clear=False)
|
||
self.sleep(0.8)
|
||
|
||
# ── 7. 点击发送按钮(先尝试 xpath/text,再坐标兜底) ──
|
||
sent = False
|
||
try:
|
||
if self.d.xpath('//*[@text="发送"]').exists:
|
||
self.d.xpath('//*[@text="发送"]').click()
|
||
sent = True
|
||
elif self.d(text="发送").exists(timeout=1):
|
||
self.d(text="发送").click()
|
||
sent = True
|
||
except Exception:
|
||
pass
|
||
|
||
if not sent:
|
||
send_x = int(W * 0.91)
|
||
self.click(send_x, input_y)
|
||
self.sleep(0.3)
|
||
|
||
self.sleep(0.5)
|
||
logger.info(f"微信消息发送成功: {to_id}")
|
||
self.say(f"已发消息给 {to_id}", data={"to_id": to_id, "content_preview": content[:30]})
|
||
return {"success": True, "message_id": f"wx_{int(time.time() * 1000)}"}
|
||
except Exception as e:
|
||
logger.error(f"微信发送失败: {e}")
|
||
return {"success": False, "error": str(e)}
|
||
|
||
|
||
def get_messages(self, limit: int = 20, conversation_id: str = None, count: int = None, **kwargs) -> Dict[str, Any]:
|
||
"""
|
||
获取消息列表
|
||
|
||
Args:
|
||
limit: 消息数量限制
|
||
count: limit 的别名(AI Brain 兼容)
|
||
conversation_id: 会话ID(联系人名称),如果指定则获取该会话的消息
|
||
|
||
Returns:
|
||
消息列表
|
||
"""
|
||
if count is not None and limit == 20:
|
||
limit = count
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
messages = []
|
||
|
||
# 如果指定了会话,先进入
|
||
if conversation_id:
|
||
# 使用搜索进入会话
|
||
if self.click_text("搜索") or self.click_desc("搜索"):
|
||
self.sleep(0.5)
|
||
self.input_text(conversation_id)
|
||
self.sleep(1.5)
|
||
|
||
# 点击搜索结果
|
||
if not self.click_text(conversation_id):
|
||
if not self.click_contains(conversation_id):
|
||
return {"success": False, "error": f"未找到会话: {conversation_id}", "messages": []}
|
||
|
||
self.sleep(1)
|
||
|
||
# 获取UI树分析消息
|
||
ui_tree = self.d.dump_hierarchy()
|
||
|
||
# 解析XML提取消息
|
||
import xml.etree.ElementTree as ET
|
||
try:
|
||
root = ET.fromstring(ui_tree)
|
||
|
||
# 查找消息元素(通常在聊天界面)
|
||
# 微信消息通常有特定的resource-id或class
|
||
for elem in root.iter():
|
||
# 查找消息文本
|
||
text = elem.get("text", "")
|
||
resource_id = elem.get("resource-id", "")
|
||
bounds = elem.get("bounds", "")
|
||
|
||
if text and len(text) > 0:
|
||
# 判断是否是消息内容(排除按钮、输入框等)
|
||
if "发送" not in text and "输入" not in text:
|
||
# 提取坐标判断位置(消息通常在屏幕中间)
|
||
if bounds:
|
||
try:
|
||
coords = bounds.replace("[", "").replace("]", "").split(",")
|
||
if len(coords) >= 4:
|
||
y = int(coords[1])
|
||
# 消息通常在屏幕上半部分
|
||
info = self.d.info
|
||
if y < info['displayHeight'] * 0.8:
|
||
messages.append({
|
||
"text": text,
|
||
"bounds": bounds,
|
||
"timestamp": int(time.time() * 1000)
|
||
})
|
||
except:
|
||
pass
|
||
|
||
# 限制数量
|
||
messages = messages[:limit]
|
||
|
||
except Exception as e:
|
||
logger.warning(f"解析UI树失败: {e}")
|
||
|
||
return {
|
||
"success": True,
|
||
"messages": messages,
|
||
"count": len(messages)
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取消息失败: {e}")
|
||
return {"success": False, "error": str(e), "messages": []}
|
||
|
||
def get_contacts(self, limit: int = 100) -> Dict[str, Any]:
|
||
"""
|
||
获取联系人列表
|
||
|
||
Args:
|
||
limit: 联系人数量限制
|
||
|
||
Returns:
|
||
联系人列表
|
||
"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 点击通讯录
|
||
if not self.click_text("通讯录"):
|
||
return {"success": False, "error": "无法进入通讯录", "contacts": []}
|
||
|
||
self.sleep(1)
|
||
|
||
contacts = []
|
||
|
||
# 获取UI树
|
||
ui_tree = self.d.dump_hierarchy()
|
||
|
||
# 解析联系人
|
||
import xml.etree.ElementTree as ET
|
||
try:
|
||
root = ET.fromstring(ui_tree)
|
||
|
||
# 查找联系人元素
|
||
for elem in root.iter():
|
||
text = elem.get("text", "")
|
||
resource_id = elem.get("resource-id", "")
|
||
|
||
# 微信联系人通常有特定的resource-id或class
|
||
if text and len(text) > 0:
|
||
# 排除系统元素
|
||
if "通讯录" not in text and "搜索" not in text:
|
||
# 检查是否是联系人(通常有头像或特定布局)
|
||
parent = elem
|
||
for _ in range(3): # 向上查找3层
|
||
if parent is not None:
|
||
parent_id = parent.get("resource-id", "")
|
||
if "contact" in parent_id.lower() or "item" in parent_id.lower():
|
||
contacts.append({
|
||
"name": text,
|
||
"resource_id": resource_id
|
||
})
|
||
break
|
||
parent = parent.getparent() if hasattr(parent, 'getparent') else None
|
||
|
||
# 去重
|
||
seen = set()
|
||
unique_contacts = []
|
||
for contact in contacts:
|
||
name = contact["name"]
|
||
if name not in seen:
|
||
seen.add(name)
|
||
unique_contacts.append(contact)
|
||
|
||
contacts = unique_contacts[:limit]
|
||
|
||
except Exception as e:
|
||
logger.warning(f"解析联系人失败: {e}")
|
||
# 降级方案:使用搜索功能获取常用联系人
|
||
# 可以通过搜索历史记录获取
|
||
|
||
return {
|
||
"success": True,
|
||
"contacts": contacts,
|
||
"count": len(contacts)
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"获取联系人失败: {e}")
|
||
return {"success": False, "error": str(e), "contacts": []}
|
||
|
||
def add_friend(self, user_id: str, message: str = "") -> Dict[str, Any]:
|
||
"""添加好友"""
|
||
self.natural_behavior_before("add_friend")
|
||
try:
|
||
self.launch()
|
||
self.sleep(2)
|
||
|
||
# 点击+号
|
||
self.click_desc("+") or self.click_text("+")
|
||
self.sleep(0.5)
|
||
|
||
# 添加朋友
|
||
self.click_text("添加朋友")
|
||
self.sleep(0.5)
|
||
|
||
# 输入微信号
|
||
self.input_text(user_id)
|
||
self.sleep(1)
|
||
|
||
# 搜索
|
||
self.click_text("搜索") or self.click_contains("搜索")
|
||
self.sleep(2)
|
||
|
||
# 添加
|
||
if self.click_text("添加到通讯录"):
|
||
if message:
|
||
self.sleep(0.5)
|
||
self.input_text(message)
|
||
self.click_text("发送")
|
||
return {"success": True}
|
||
|
||
return {"success": False, "error": "未找到用户或已是好友"}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def accept_friend(self, user_id: str = None, **kwargs) -> Dict[str, Any]:
|
||
"""通过好友请求 [控制形式: u2 UI自动化, 耗时~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.sleep(2)
|
||
self.click_text("通讯录")
|
||
self.sleep(1)
|
||
self.click_text("新的朋友")
|
||
self.sleep(1)
|
||
|
||
if user_id:
|
||
ui = self.d.dump_hierarchy()
|
||
import xml.etree.ElementTree as ET
|
||
root = ET.fromstring(ui)
|
||
found = False
|
||
for elem in root.iter():
|
||
if elem.get("text", "") == user_id:
|
||
bounds = elem.get("bounds", "")
|
||
if bounds:
|
||
coords = bounds.replace("[", "").replace("]", ",").split(",")
|
||
y_center = (int(coords[1]) + int(coords[4])) // 2
|
||
accept_x = int(self.d.info["displayWidth"] * 0.85)
|
||
self.click(accept_x, y_center)
|
||
self.sleep(1)
|
||
found = True
|
||
break
|
||
if not found:
|
||
if self.click_text("接受"):
|
||
return {"success": True}
|
||
return {"success": False, "error": f"未找到用户 {user_id} 的好友请求"}
|
||
return {"success": True, "user_id": user_id}
|
||
|
||
if self.click_text("接受"):
|
||
return {"success": True}
|
||
return {"success": False, "error": "无待处理的好友请求"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
# ==========================================================================
|
||
# 三、群聊管理
|
||
# ==========================================================================
|
||
|
||
def create_group(self, group_name: str, member_ids: List[str]) -> Dict[str, Any]:
|
||
"""
|
||
创建群聊
|
||
|
||
Args:
|
||
group_name: 群名称
|
||
member_ids: 成员名称列表(至少2人)
|
||
|
||
Returns:
|
||
执行结果
|
||
"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 点击右上角+号
|
||
self.click_desc("+") or self.click_text("+")
|
||
self.sleep(0.5)
|
||
|
||
# 发起群聊
|
||
self.click_text("发起群聊")
|
||
self.sleep(1)
|
||
|
||
# 选择群成员
|
||
for member_id in member_ids:
|
||
# 搜索成员
|
||
if self.click_text("搜索") or self.click_desc("搜索"):
|
||
self.sleep(0.3)
|
||
self.input_text(member_id)
|
||
self.sleep(1)
|
||
|
||
# 选中成员
|
||
self.click_text(member_id) or self.click_contains(member_id)
|
||
self.sleep(0.5)
|
||
|
||
# 清空搜索框
|
||
self.back()
|
||
self.sleep(0.3)
|
||
|
||
# 确定创建
|
||
self.click_text("完成") or self.click_text("确定")
|
||
self.sleep(2)
|
||
|
||
# 修改群名
|
||
if group_name:
|
||
# 点击群名区域
|
||
self.click_text("群聊") or self.click_contains("的群聊")
|
||
self.sleep(0.5)
|
||
self.input_text(group_name, clear=True)
|
||
self.click_text("完成") or self.click_text("确定")
|
||
|
||
logger.info(f"创建群聊成功: {group_name}")
|
||
return {
|
||
"success": True,
|
||
"group_name": group_name,
|
||
"member_count": len(member_ids) + 1
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"创建群聊失败: {e}")
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def invite_to_group(self, group_id: str, member_ids: List[str]) -> Dict[str, Any]:
|
||
"""
|
||
邀请入群
|
||
|
||
Args:
|
||
group_id: 群名称
|
||
member_ids: 要邀请的成员名称列表
|
||
"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 搜索进入群聊
|
||
if self.click_text("搜索") or self.click_desc("搜索"):
|
||
self.sleep(0.5)
|
||
self.input_text(group_id)
|
||
self.sleep(1.5)
|
||
self.click_text(group_id) or self.click_contains(group_id)
|
||
self.sleep(1)
|
||
|
||
# 点击右上角群设置
|
||
self.d.xpath('//*[@content-desc="聊天信息"]').click_exists(timeout=3) or \
|
||
self.click_text("...")
|
||
self.sleep(1)
|
||
|
||
# 点击+号添加成员
|
||
self.click_desc("+") or self.click_text("+")
|
||
self.sleep(1)
|
||
|
||
# 选择成员
|
||
invited_count = 0
|
||
for member_id in member_ids:
|
||
if self.click_text("搜索") or self.click_desc("搜索"):
|
||
self.sleep(0.3)
|
||
self.input_text(member_id)
|
||
self.sleep(1)
|
||
|
||
if self.click_text(member_id) or self.click_contains(member_id):
|
||
invited_count += 1
|
||
|
||
self.back()
|
||
self.sleep(0.3)
|
||
|
||
# 确定
|
||
self.click_text("完成") or self.click_text("确定")
|
||
|
||
return {
|
||
"success": True,
|
||
"invited_count": invited_count
|
||
}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def remove_from_group(self, group_id: str, member_ids: List[str]) -> Dict[str, Any]:
|
||
"""移出群聊"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 进入群聊
|
||
self.search(group_id)
|
||
self.click_text(group_id) or self.click_contains(group_id)
|
||
self.sleep(1)
|
||
|
||
# 进入群设置
|
||
self.d.xpath('//*[@content-desc="聊天信息"]').click_exists(timeout=3)
|
||
self.sleep(1)
|
||
|
||
# 点击-号移除成员
|
||
self.click_desc("-")
|
||
self.sleep(1)
|
||
|
||
removed_count = 0
|
||
for member_id in member_ids:
|
||
if self.click_text(member_id):
|
||
removed_count += 1
|
||
|
||
self.click_text("完成") or self.click_text("删除")
|
||
|
||
return {"success": True, "removed_count": removed_count}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def send_group_message(
|
||
self,
|
||
group_id: str,
|
||
content: str,
|
||
msg_type: str = "text",
|
||
at_all: bool = False,
|
||
at_list: List[str] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
发送群消息
|
||
|
||
Args:
|
||
group_id: 群名称
|
||
content: 消息内容
|
||
msg_type: 消息类型
|
||
at_all: 是否@所有人
|
||
at_list: @的成员列表
|
||
"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 搜索进入群聊
|
||
self.search(group_id)
|
||
self.sleep(1)
|
||
self.click_text(group_id) or self.click_contains(group_id)
|
||
self.sleep(1)
|
||
|
||
# 处理@
|
||
if at_all:
|
||
content = "@所有人 " + content
|
||
elif at_list:
|
||
at_str = " ".join([f"@{name}" for name in at_list])
|
||
content = at_str + " " + content
|
||
|
||
# 输入消息
|
||
self.input_text(content, clear=False)
|
||
self.sleep(0.3)
|
||
|
||
# 发送
|
||
self.click_text("发送")
|
||
|
||
return {
|
||
"success": True,
|
||
"message_id": f"wx_group_{int(time.time() * 1000)}"
|
||
}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def set_group_notice(self, group_id: str, notice: str) -> Dict[str, Any]:
|
||
"""设置群公告"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 进入群聊
|
||
self.search(group_id)
|
||
self.click_text(group_id) or self.click_contains(group_id)
|
||
self.sleep(1)
|
||
|
||
# 进入群设置
|
||
self.d.xpath('//*[@content-desc="聊天信息"]').click_exists(timeout=3)
|
||
self.sleep(1)
|
||
|
||
# 群公告
|
||
self.click_text("群公告")
|
||
self.sleep(1)
|
||
|
||
# 输入公告
|
||
self.input_text(notice, clear=True)
|
||
|
||
# 发布
|
||
self.click_text("完成") or self.click_text("发布")
|
||
|
||
return {"success": True}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def set_group_name(self, group_id: str, group_name: str) -> Dict[str, Any]:
|
||
"""设置群名"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 进入群聊
|
||
self.search(group_id)
|
||
self.click_text(group_id) or self.click_contains(group_id)
|
||
self.sleep(1)
|
||
|
||
# 进入群设置
|
||
self.d.xpath('//*[@content-desc="聊天信息"]').click_exists(timeout=3)
|
||
self.sleep(1)
|
||
|
||
# 点击群名
|
||
self.click_text("群聊名称") or self.click_contains("群名")
|
||
self.sleep(0.5)
|
||
|
||
# 输入新群名
|
||
self.input_text(group_name, clear=True)
|
||
|
||
# 确定
|
||
self.click_text("完成") or self.click_text("确定")
|
||
|
||
return {"success": True}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def set_group_welcome(self, group_id: str, welcome_text: str, welcome_image: str = None) -> Dict[str, Any]:
|
||
"""设置群欢迎语(需要群主/管理员权限)"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 进入群聊设置
|
||
self.search(group_id)
|
||
self.click_text(group_id) or self.click_contains(group_id)
|
||
self.sleep(1)
|
||
|
||
self.d.xpath('//*[@content-desc="聊天信息"]').click_exists(timeout=3)
|
||
self.sleep(1)
|
||
|
||
# 群管理
|
||
if not self.click_text("群管理"):
|
||
return {"success": False, "error": "未找到群管理选项,可能不是群主/管理员"}
|
||
self.sleep(1)
|
||
|
||
# 入群欢迎语
|
||
self.click_text("入群欢迎语") or self.click_contains("欢迎语")
|
||
self.sleep(1)
|
||
|
||
# 输入欢迎语
|
||
self.input_text(welcome_text, clear=True)
|
||
|
||
# 保存
|
||
self.click_text("完成") or self.click_text("保存")
|
||
|
||
return {"success": True}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def get_groups(self, limit: int = 100) -> Dict[str, Any]:
|
||
"""获取群聊列表"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 通讯录 -> 群聊
|
||
self.click_text("通讯录")
|
||
self.sleep(1)
|
||
|
||
self.click_text("群聊")
|
||
self.sleep(1)
|
||
|
||
# 获取UI树解析群列表
|
||
groups = []
|
||
ui_tree = self.d.dump_hierarchy()
|
||
|
||
# 解析群名(简化实现)
|
||
import xml.etree.ElementTree as ET
|
||
try:
|
||
root = ET.fromstring(ui_tree)
|
||
for elem in root.iter():
|
||
text = elem.get("text", "")
|
||
if text and "群聊" not in text and "通讯录" not in text:
|
||
# 可能是群名
|
||
groups.append({"name": text})
|
||
except:
|
||
pass
|
||
|
||
return {
|
||
"success": True,
|
||
"groups": groups[:limit],
|
||
"count": len(groups[:limit])
|
||
}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e), "groups": []}
|
||
|
||
def get_group_members(self, group_id: str) -> Dict[str, Any]:
|
||
"""获取群成员列表"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 进入群聊
|
||
self.search(group_id)
|
||
self.click_text(group_id) or self.click_contains(group_id)
|
||
self.sleep(1)
|
||
|
||
# 进入群设置
|
||
self.d.xpath('//*[@content-desc="聊天信息"]').click_exists(timeout=3)
|
||
self.sleep(1)
|
||
|
||
# 点击查看全部成员
|
||
self.click_contains("查看全部") or self.click_text("全部群成员")
|
||
self.sleep(1)
|
||
|
||
# 获取成员列表
|
||
members = []
|
||
ui_tree = self.d.dump_hierarchy()
|
||
|
||
import xml.etree.ElementTree as ET
|
||
try:
|
||
root = ET.fromstring(ui_tree)
|
||
for elem in root.iter():
|
||
text = elem.get("text", "")
|
||
if text and len(text) > 0:
|
||
if "群成员" not in text and "搜索" not in text:
|
||
members.append({"name": text})
|
||
except:
|
||
pass
|
||
|
||
return {
|
||
"success": True,
|
||
"members": members,
|
||
"count": len(members)
|
||
}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e), "members": []}
|
||
|
||
# ==========================================================================
|
||
# 四、标签管理
|
||
# ==========================================================================
|
||
|
||
def add_tag(self, user_id: str, tags: List[str]) -> Dict[str, Any]:
|
||
"""
|
||
给好友添加标签
|
||
|
||
Args:
|
||
user_id: 好友名称
|
||
tags: 标签列表
|
||
"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 搜索好友
|
||
self.search(user_id)
|
||
self.click_text(user_id) or self.click_contains(user_id)
|
||
self.sleep(1)
|
||
|
||
# 进入聊天界面后点击头像进入详情
|
||
# 或者直接在搜索结果点击头像
|
||
|
||
# 点击更多设置
|
||
self.d.xpath('//*[@content-desc="聊天信息"]').click_exists(timeout=3) or \
|
||
self.click_text("...")
|
||
self.sleep(1)
|
||
|
||
# 设置标签
|
||
self.click_text("设置标签") or self.click_contains("标签")
|
||
self.sleep(1)
|
||
|
||
# 选择/创建标签
|
||
for tag in tags:
|
||
if self.exists(tag):
|
||
self.click_text(tag)
|
||
else:
|
||
# 创建新标签
|
||
self.click_text("添加标签") or self.click_text("新建标签")
|
||
self.sleep(0.5)
|
||
self.input_text(tag)
|
||
self.click_text("完成") or self.click_text("确定")
|
||
self.sleep(0.3)
|
||
|
||
# 保存
|
||
self.click_text("完成") or self.click_text("保存")
|
||
|
||
return {"success": True, "tags_added": tags}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def remove_tag(self, user_id: str, tags: List[str]) -> Dict[str, Any]:
|
||
"""移除好友标签"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 进入好友详情
|
||
self.search(user_id)
|
||
self.click_text(user_id) or self.click_contains(user_id)
|
||
self.sleep(1)
|
||
|
||
self.d.xpath('//*[@content-desc="聊天信息"]').click_exists(timeout=3)
|
||
self.sleep(1)
|
||
|
||
# 设置标签
|
||
self.click_text("设置标签") or self.click_contains("标签")
|
||
self.sleep(1)
|
||
|
||
# 取消选择标签
|
||
for tag in tags:
|
||
if self.exists(tag):
|
||
self.click_text(tag) # 再次点击取消选择
|
||
self.sleep(0.3)
|
||
|
||
# 保存
|
||
self.click_text("完成") or self.click_text("保存")
|
||
|
||
return {"success": True, "tags_removed": tags}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def create_tag(self, tag_name: str) -> Dict[str, Any]:
|
||
"""创建标签"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 通讯录 -> 标签
|
||
self.click_text("通讯录")
|
||
self.sleep(1)
|
||
|
||
self.click_text("标签")
|
||
self.sleep(1)
|
||
|
||
# 新建标签
|
||
self.click_text("新建") or self.click_text("添加标签")
|
||
self.sleep(0.5)
|
||
|
||
self.input_text(tag_name)
|
||
self.click_text("完成") or self.click_text("保存")
|
||
|
||
return {"success": True, "tag_name": tag_name}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def delete_tag(self, tag_name: str) -> Dict[str, Any]:
|
||
"""删除标签"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 通讯录 -> 标签
|
||
self.click_text("通讯录")
|
||
self.sleep(1)
|
||
|
||
self.click_text("标签")
|
||
self.sleep(1)
|
||
|
||
# 找到并长按标签
|
||
if self.exists(tag_name):
|
||
# 长按删除
|
||
element = self.d(text=tag_name)
|
||
if element.exists:
|
||
element.long_click()
|
||
self.sleep(0.5)
|
||
self.click_text("删除")
|
||
self.click_text("确定")
|
||
return {"success": True}
|
||
|
||
return {"success": False, "error": f"未找到标签: {tag_name}"}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def get_tags(self) -> Dict[str, Any]:
|
||
"""获取标签列表"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 通讯录 -> 标签
|
||
self.click_text("通讯录")
|
||
self.sleep(1)
|
||
|
||
self.click_text("标签")
|
||
self.sleep(1)
|
||
|
||
# 解析标签列表
|
||
tags = []
|
||
ui_tree = self.d.dump_hierarchy()
|
||
|
||
import xml.etree.ElementTree as ET
|
||
try:
|
||
root = ET.fromstring(ui_tree)
|
||
for elem in root.iter():
|
||
text = elem.get("text", "")
|
||
if text and "标签" not in text and "新建" not in text:
|
||
# 提取标签名(格式可能是"标签名(数量)")
|
||
match = re.match(r'^(.+?)(?:\(\d+\))?$', text)
|
||
if match:
|
||
tags.append(match.group(1))
|
||
except:
|
||
pass
|
||
|
||
return {"success": True, "tags": tags}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e), "tags": []}
|
||
|
||
def get_users_by_tag(self, tag_name: str, limit: int = 100) -> Dict[str, Any]:
|
||
"""根据标签获取好友列表"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 通讯录 -> 标签
|
||
self.click_text("通讯录")
|
||
self.sleep(1)
|
||
|
||
self.click_text("标签")
|
||
self.sleep(1)
|
||
|
||
# 点击标签
|
||
self.click_text(tag_name) or self.click_contains(tag_name)
|
||
self.sleep(1)
|
||
|
||
# 解析好友列表
|
||
users = []
|
||
ui_tree = self.d.dump_hierarchy()
|
||
|
||
import xml.etree.ElementTree as ET
|
||
try:
|
||
root = ET.fromstring(ui_tree)
|
||
for elem in root.iter():
|
||
text = elem.get("text", "")
|
||
if text and len(text) > 0:
|
||
if tag_name not in text and "人" not in text:
|
||
users.append({"name": text})
|
||
except:
|
||
pass
|
||
|
||
return {
|
||
"success": True,
|
||
"users": users[:limit],
|
||
"count": len(users[:limit])
|
||
}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e), "users": []}
|
||
|
||
# ==========================================================================
|
||
# 五、朋友圈管理
|
||
# ==========================================================================
|
||
|
||
def post_moments(
|
||
self,
|
||
content: str,
|
||
images: List[str] = None,
|
||
video_url: str = None,
|
||
location: str = None,
|
||
visible_list: List[str] = None,
|
||
invisible_list: List[str] = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
发布朋友圈
|
||
|
||
Args:
|
||
content: 文字内容
|
||
images: 图片URL列表(需要先下载到本地)
|
||
video_url: 视频URL
|
||
location: 位置
|
||
visible_list: 可见名单
|
||
invisible_list: 不可见名单
|
||
"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 发现 -> 朋友圈
|
||
self.click_text("发现")
|
||
self.sleep(1)
|
||
self.click_text("朋友圈")
|
||
self.sleep(2)
|
||
|
||
# 点击相机图标发布
|
||
camera = self.d.xpath('//*[@content-desc="拍照分享"]')
|
||
if camera.exists:
|
||
if images or video_url:
|
||
# 短按:选择图片/视频
|
||
camera.click()
|
||
else:
|
||
# 长按:纯文字
|
||
camera.long_click()
|
||
self.sleep(1)
|
||
|
||
if images:
|
||
# 从相册选择
|
||
self.click_text("从相册选择")
|
||
self.sleep(1)
|
||
# TODO: 选择图片(需要图片已在相册中)
|
||
# 这里简化处理,假设选择第一张
|
||
self.click_text("完成") or self.click_text("发送")
|
||
|
||
self.sleep(1)
|
||
|
||
# 输入文字内容
|
||
if self.click_contains("这一刻的想法") or self.click_contains("说点什么"):
|
||
self.input_text(content)
|
||
else:
|
||
self.input_text(content)
|
||
|
||
# 设置位置
|
||
if location:
|
||
self.click_text("所在位置")
|
||
self.sleep(1)
|
||
self.input_text(location)
|
||
self.sleep(1)
|
||
self.click_text(location) or self.click_contains(location)
|
||
|
||
# 设置可见范围
|
||
if visible_list or invisible_list:
|
||
self.click_text("谁可以看")
|
||
self.sleep(0.5)
|
||
if visible_list:
|
||
self.click_text("部分可见")
|
||
# TODO: 选择可见的人
|
||
elif invisible_list:
|
||
self.click_text("不给谁看")
|
||
# TODO: 选择不可见的人
|
||
self.click_text("完成")
|
||
|
||
# 发表
|
||
self.click_text("发表")
|
||
self.sleep(2)
|
||
|
||
return {
|
||
"success": True,
|
||
"post_id": f"moments_{int(time.time() * 1000)}"
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"发布朋友圈失败: {e}")
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def like_moments(self, user_id: str, post_index: int = 0) -> Dict[str, Any]:
|
||
"""
|
||
点赞朋友圈
|
||
|
||
Args:
|
||
user_id: 好友名称
|
||
post_index: 第几条朋友圈(0表示最新一条)
|
||
"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 进入好友朋友圈
|
||
self._enter_user_moments(user_id)
|
||
|
||
# 滚动到指定位置
|
||
for _ in range(post_index):
|
||
self.swipe("up", scale=0.3)
|
||
self.sleep(0.5)
|
||
|
||
# 点击评论按钮
|
||
self.click_desc("评论") or self.d.xpath('//*[@content-desc="评论"]').click()
|
||
self.sleep(0.5)
|
||
|
||
# 点击赞
|
||
self.click_text("赞")
|
||
|
||
return {"success": True}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def comment_moments(
|
||
self,
|
||
user_id: str,
|
||
comment: str,
|
||
post_index: int = 0,
|
||
reply_to: str = None
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
评论朋友圈
|
||
|
||
Args:
|
||
user_id: 好友名称
|
||
comment: 评论内容
|
||
post_index: 第几条朋友圈
|
||
reply_to: 回复某人
|
||
"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 进入好友朋友圈
|
||
self._enter_user_moments(user_id)
|
||
|
||
# 滚动到指定位置
|
||
for _ in range(post_index):
|
||
self.swipe("up", scale=0.3)
|
||
self.sleep(0.5)
|
||
|
||
# 点击评论按钮
|
||
self.click_desc("评论") or self.d.xpath('//*[@content-desc="评论"]').click()
|
||
self.sleep(0.5)
|
||
|
||
# 点击评论
|
||
self.click_text("评论")
|
||
self.sleep(0.3)
|
||
|
||
# 如果要回复某人
|
||
if reply_to:
|
||
# 找到该人的评论并点击
|
||
self.click_text(reply_to)
|
||
self.sleep(0.3)
|
||
|
||
# 输入评论
|
||
self.input_text(comment)
|
||
|
||
# 发送
|
||
self.click_text("发送")
|
||
|
||
return {"success": True}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def get_moments(self, user_id: str = None, limit: int = 10) -> Dict[str, Any]:
|
||
"""
|
||
获取朋友圈列表
|
||
|
||
Args:
|
||
user_id: 好友名称,不指定则获取自己的朋友圈
|
||
limit: 获取数量
|
||
"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
if user_id:
|
||
self._enter_user_moments(user_id)
|
||
else:
|
||
# 进入自己的朋友圈
|
||
self.click_text("发现")
|
||
self.sleep(1)
|
||
self.click_text("朋友圈")
|
||
self.sleep(2)
|
||
# 点击自己的头像
|
||
# TODO: 进入个人相册
|
||
|
||
# 获取朋友圈内容
|
||
moments = []
|
||
ui_tree = self.d.dump_hierarchy()
|
||
|
||
# 简化解析
|
||
import xml.etree.ElementTree as ET
|
||
try:
|
||
root = ET.fromstring(ui_tree)
|
||
for elem in root.iter():
|
||
text = elem.get("text", "")
|
||
if text and len(text) > 10: # 可能是朋友圈内容
|
||
moments.append({"content": text[:100]})
|
||
except:
|
||
pass
|
||
|
||
return {
|
||
"success": True,
|
||
"moments": moments[:limit],
|
||
"count": len(moments[:limit])
|
||
}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e), "moments": []}
|
||
|
||
def _enter_user_moments(self, user_id: str):
|
||
"""进入好友的朋友圈(内部方法)"""
|
||
# 搜索好友
|
||
self.search(user_id)
|
||
self.click_text(user_id) or self.click_contains(user_id)
|
||
self.sleep(1)
|
||
|
||
# 进入好友详情
|
||
self.d.xpath('//*[@content-desc="聊天信息"]').click_exists(timeout=3)
|
||
self.sleep(1)
|
||
|
||
# 点击头像进入详情页
|
||
# 然后点击朋友圈
|
||
self.click_text("朋友圈") or self.click_contains("个人相册")
|
||
self.sleep(2)
|
||
|
||
# ==========================================================================
|
||
# 六、批量操作
|
||
# ==========================================================================
|
||
|
||
def batch_send_message(
|
||
self,
|
||
to_ids: List[str],
|
||
content: str,
|
||
msg_type: str = "text",
|
||
interval: float = 2.0
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
批量发送消息
|
||
|
||
Args:
|
||
to_ids: 接收者ID列表
|
||
content: 消息内容
|
||
msg_type: 消息类型
|
||
interval: 发送间隔(秒)
|
||
|
||
Returns:
|
||
批量发送结果
|
||
"""
|
||
results = []
|
||
success_count = 0
|
||
failed_count = 0
|
||
|
||
for to_id in to_ids:
|
||
try:
|
||
result = self.send_message(to_id, content, msg_type)
|
||
|
||
if result.get("success"):
|
||
success_count += 1
|
||
else:
|
||
failed_count += 1
|
||
|
||
results.append({
|
||
"to_id": to_id,
|
||
"success": result.get("success", False),
|
||
"error": result.get("error"),
|
||
"message_id": result.get("message_id")
|
||
})
|
||
|
||
# 发送间隔
|
||
time.sleep(interval)
|
||
|
||
except Exception as e:
|
||
failed_count += 1
|
||
results.append({
|
||
"to_id": to_id,
|
||
"success": False,
|
||
"error": str(e)
|
||
})
|
||
|
||
return {
|
||
"success": True,
|
||
"success_count": success_count,
|
||
"failed_count": failed_count,
|
||
"results": results
|
||
}
|
||
|
||
def batch_add_friend(
|
||
self,
|
||
user_ids: List[str],
|
||
message: str = "",
|
||
interval: float = 5.0
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
批量添加好友
|
||
|
||
Args:
|
||
user_ids: 用户ID列表
|
||
message: 验证消息
|
||
interval: 添加间隔(秒)
|
||
"""
|
||
results = []
|
||
success_count = 0
|
||
failed_count = 0
|
||
|
||
for user_id in user_ids:
|
||
try:
|
||
result = self.add_friend(user_id, message)
|
||
|
||
if result.get("success"):
|
||
success_count += 1
|
||
else:
|
||
failed_count += 1
|
||
|
||
results.append({
|
||
"user_id": user_id,
|
||
"success": result.get("success", False),
|
||
"error": result.get("error")
|
||
})
|
||
|
||
time.sleep(interval)
|
||
|
||
except Exception as e:
|
||
failed_count += 1
|
||
results.append({
|
||
"user_id": user_id,
|
||
"success": False,
|
||
"error": str(e)
|
||
})
|
||
|
||
return {
|
||
"success": True,
|
||
"success_count": success_count,
|
||
"failed_count": failed_count,
|
||
"results": results
|
||
}
|
||
|
||
# ==========================================================================
|
||
# 七、好友管理扩展
|
||
# ==========================================================================
|
||
|
||
def set_remark(self, user_id: str, remark: str) -> Dict[str, Any]:
|
||
"""
|
||
设置好友备注
|
||
|
||
Args:
|
||
user_id: 好友名称
|
||
remark: 新备注
|
||
"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 进入好友详情
|
||
self.search(user_id)
|
||
self.click_text(user_id) or self.click_contains(user_id)
|
||
self.sleep(1)
|
||
|
||
self.d.xpath('//*[@content-desc="聊天信息"]').click_exists(timeout=3)
|
||
self.sleep(1)
|
||
|
||
# 设置备注和标签
|
||
self.click_text("设置备注和标签") or self.click_contains("备注")
|
||
self.sleep(0.5)
|
||
|
||
# 输入备注
|
||
self.input_text(remark, clear=True)
|
||
|
||
# 保存
|
||
self.click_text("完成") or self.click_text("保存")
|
||
|
||
return {"success": True, "remark": remark}
|
||
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def delete_friend(self, user_id: str, **kwargs) -> Dict[str, Any]:
|
||
"""删除好友 [控制形式: u2 UI自动化, 耗时~8s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.search(user_id)
|
||
self.click_text(user_id) or self.click_contains(user_id)
|
||
self.sleep(1)
|
||
self.d.xpath('//*[@content-desc="聊天信息"]').click_exists(timeout=3)
|
||
self.sleep(1)
|
||
W = self.d.info["displayWidth"]
|
||
self.click(int(W * 0.2), int(self.d.info["displayHeight"] * 0.12))
|
||
self.sleep(1.5)
|
||
self.click_text("...") or self.click_desc("更多")
|
||
self.sleep(0.5)
|
||
self.click_text("删除")
|
||
self.sleep(0.5)
|
||
self.click_text("删除") or self.click_text("确定")
|
||
return {"success": True}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
# ==========================================================================
|
||
# 八、辅助方法
|
||
# ==========================================================================
|
||
|
||
def send_message_with_search(self, to_id: str, content: str) -> Dict[str, Any]:
|
||
"""
|
||
通过搜索发送消息(更可靠)
|
||
|
||
Args:
|
||
to_id: 联系人名称/备注
|
||
content: 消息内容
|
||
"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
|
||
# 使用搜索功能
|
||
search_result = self.search(to_id)
|
||
if not search_result.get("success"):
|
||
return {"success": False, "error": "搜索联系人失败"}
|
||
|
||
self.sleep(1)
|
||
|
||
# 点击第一个搜索结果
|
||
if not self.click_text(to_id):
|
||
if not self.click_contains(to_id):
|
||
return {"success": False, "error": f"未找到联系人: {to_id}"}
|
||
|
||
self.sleep(1)
|
||
|
||
# 确保在聊天界面
|
||
if not self.exists("发送") and not self.exists("按住 说话"):
|
||
return {"success": False, "error": "未能进入聊天界面"}
|
||
|
||
# 输入并发送消息
|
||
self.input_text(content, clear=False)
|
||
self.sleep(0.3)
|
||
self.click_text("发送")
|
||
|
||
return {
|
||
"success": True,
|
||
"message_id": f"wx_{int(time.time() * 1000)}"
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"微信发送失败: {e}")
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def execute_compound_task(self, task_description: str) -> Dict[str, Any]:
|
||
"""
|
||
执行复合任务(如"打开微信给张三发消息:下午开会")
|
||
|
||
Args:
|
||
task_description: 任务描述
|
||
|
||
Returns:
|
||
执行结果
|
||
"""
|
||
try:
|
||
# 解析任务
|
||
# 提取联系人
|
||
contact_match = re.search(r'给(.+?)发', task_description)
|
||
if not contact_match:
|
||
return {"success": False, "error": "无法解析联系人"}
|
||
|
||
contact = contact_match.group(1).strip()
|
||
|
||
# 提取消息内容
|
||
content_match = re.search(r'发消息[::](.+)', task_description)
|
||
if not content_match:
|
||
content_match = re.search(r'发(.+)', task_description)
|
||
|
||
if not content_match:
|
||
return {"success": False, "error": "无法解析消息内容"}
|
||
|
||
content = content_match.group(1).strip()
|
||
|
||
# 执行发送
|
||
return self.send_message_with_search(contact, content)
|
||
|
||
except Exception as e:
|
||
logger.error(f"执行复合任务失败: {e}")
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def get_current_screen_info(self) -> Dict[str, Any]:
|
||
"""获取当前屏幕信息(调试用)"""
|
||
try:
|
||
ui_tree = self.d.dump_hierarchy()
|
||
info = self.d.info
|
||
return {
|
||
"success": True,
|
||
"display_width": info.get("displayWidth"),
|
||
"display_height": info.get("displayHeight"),
|
||
"current_package": info.get("currentPackageName"),
|
||
"ui_tree_length": len(ui_tree)
|
||
}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
# ======================================================================
|
||
# 九、消息高级操作(新增)
|
||
# 控制形式: u2 UI自动化 | Hook可加速(forward/recall走Frida RPC更快)
|
||
# ======================================================================
|
||
|
||
def forward_message(self, to_id: str = "", content: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""转发消息 [u2, ~6s]"""
|
||
try:
|
||
self.launch()
|
||
self.sleep(1)
|
||
self.long_click(int(self.d.info["displayWidth"] * 0.5),
|
||
int(self.d.info["displayHeight"] * 0.5), duration=1.0)
|
||
self.sleep(0.5)
|
||
if not (self.click_text("转发") or self.click_text("逐条转发")):
|
||
return {"success": False, "error": "未找到转发选项"}
|
||
self.sleep(1)
|
||
if to_id:
|
||
self.click_text("搜索") or self.click_desc("搜索")
|
||
self.sleep(0.3)
|
||
self.input_text(to_id)
|
||
self.sleep(1)
|
||
self.click_text(to_id) or self.click_contains(to_id)
|
||
self.sleep(0.5)
|
||
self.click_text("发送") or self.click_text("确定")
|
||
return {"success": True}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def forward_multiple(self, msg_svr_ids: List[str] = None, to_id: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""合并/逐条转发多条消息 [u2, 默认基于当前聊天上下文进入转发流程]"""
|
||
result = self.forward_message(to_id=to_id, **kwargs)
|
||
if result.get("success"):
|
||
result["msg_svr_ids"] = msg_svr_ids or []
|
||
result.setdefault("note", "已进入转发流程;多选依赖当前聊天上下文")
|
||
return result
|
||
|
||
def recall_message(self, **kwargs) -> Dict[str, Any]:
|
||
"""撤回最近一条消息 [u2, ~3s]"""
|
||
try:
|
||
self.launch()
|
||
self.sleep(0.5)
|
||
H = self.d.info["displayHeight"]
|
||
W = self.d.info["displayWidth"]
|
||
self.long_click(int(W * 0.7), int(H * 0.85), duration=1.0)
|
||
self.sleep(0.5)
|
||
if self.click_text("撤回"):
|
||
self.sleep(0.5)
|
||
self.click_text("确定")
|
||
return {"success": True}
|
||
return {"success": False, "error": "未找到撤回选项"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def send_card(self, to_id: str = "", card_wxid: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""发送名片 [u2, ~8s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.search(to_id)
|
||
self.click_text(to_id) or self.click_contains(to_id)
|
||
self.sleep(1)
|
||
self.click_text("+") or self.click_desc("更多功能")
|
||
self.sleep(0.5)
|
||
self.click_text("名片") or self.click_contains("名片")
|
||
self.sleep(1)
|
||
if card_wxid:
|
||
self.click_text("搜索") or self.click_desc("搜索")
|
||
self.sleep(0.3)
|
||
self.input_text(card_wxid)
|
||
self.sleep(1)
|
||
self.click_text(card_wxid) or self.click_contains(card_wxid)
|
||
self.sleep(0.5)
|
||
self.click_text("发送") or self.click_text("确定")
|
||
return {"success": True}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
# ======================================================================
|
||
# 十、好友管理扩展(新增)
|
||
# 控制形式: u2 UI自动化 | Hook可读(search_contact/get_friend_info走SQLite)
|
||
# ======================================================================
|
||
|
||
def search_contact(self, keyword: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""搜索联系人 [Hook优先(SQLite), u2降级, ~2s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.search(keyword)
|
||
self.sleep(1.5)
|
||
results = []
|
||
ui = self.d.dump_hierarchy()
|
||
import xml.etree.ElementTree as ET
|
||
root = ET.fromstring(ui)
|
||
skip = {"搜索", "取消", "清除", keyword}
|
||
for elem in root.iter():
|
||
t = elem.get("text", "").strip()
|
||
if t and len(t) > 0 and t not in skip:
|
||
results.append({"name": t})
|
||
return {"success": True, "contacts": results[:20]}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e), "contacts": []}
|
||
|
||
def get_friend_info(self, user_id: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""获取好友详细资料 [Hook优先(SQLite rcontact), u2降级, ~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.search(user_id)
|
||
self.click_text(user_id) or self.click_contains(user_id)
|
||
self.sleep(1)
|
||
self.d.xpath('//*[@content-desc="聊天信息"]').click_exists(timeout=3)
|
||
self.sleep(1)
|
||
W = self.d.info["displayWidth"]
|
||
H = self.d.info["displayHeight"]
|
||
self.click(int(W * 0.2), int(H * 0.12))
|
||
self.sleep(1.5)
|
||
info = {}
|
||
ui = self.d.dump_hierarchy()
|
||
import xml.etree.ElementTree as ET
|
||
root = ET.fromstring(ui)
|
||
texts = [e.get("text", "") for e in root.iter() if e.get("text", "").strip()]
|
||
info["raw_texts"] = texts[:20]
|
||
for t in texts:
|
||
if "微信号" in t:
|
||
info["wxid"] = t.replace("微信号:", "").replace("微信号:", "").strip()
|
||
if "地区" in t or "Region" in t:
|
||
info["region"] = t
|
||
info["nickname"] = user_id
|
||
return {"success": True, **info}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
# ======================================================================
|
||
# 十一、群聊扩展(新增)
|
||
# ======================================================================
|
||
|
||
def quit_group(self, group_id: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""退出群聊 [u2, ~6s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.search(group_id)
|
||
self.click_text(group_id) or self.click_contains(group_id)
|
||
self.sleep(1)
|
||
self.d.xpath('//*[@content-desc="聊天信息"]').click_exists(timeout=3)
|
||
self.sleep(1)
|
||
for _ in range(5):
|
||
self.swipe("up", scale=0.5)
|
||
self.sleep(0.5)
|
||
if self.click_text("删除并退出") or self.click_text("退出群聊"):
|
||
self.sleep(0.5)
|
||
self.click_text("确定") or self.click_text("退出")
|
||
return {"success": True}
|
||
return {"success": False, "error": "未找到退出群聊按钮"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
# ======================================================================
|
||
# 十二、朋友圈增强(新增)
|
||
# 控制形式: u2 UI自动化
|
||
# ======================================================================
|
||
|
||
def delete_moments(self, post_index: int = 0, **kwargs) -> Dict[str, Any]:
|
||
"""删除朋友圈 [u2, ~8s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("发现")
|
||
self.sleep(1)
|
||
self.click_text("朋友圈")
|
||
self.sleep(2)
|
||
self.click(int(self.d.info["displayWidth"] * 0.15),
|
||
int(self.d.info["displayHeight"] * 0.08))
|
||
self.sleep(2)
|
||
for _ in range(post_index):
|
||
self.swipe("up", scale=0.3)
|
||
self.sleep(0.5)
|
||
self.long_click(int(self.d.info["displayWidth"] * 0.5),
|
||
int(self.d.info["displayHeight"] * 0.4), duration=1.0)
|
||
self.sleep(0.5)
|
||
if self.click_text("删除"):
|
||
self.sleep(0.3)
|
||
self.click_text("确定") or self.click_text("删除")
|
||
return {"success": True}
|
||
return {"success": False, "error": "未找到删除选项"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def set_moments_cover(self, **kwargs) -> Dict[str, Any]:
|
||
"""设置朋友圈封面 [u2, ~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("发现")
|
||
self.sleep(1)
|
||
self.click_text("朋友圈")
|
||
self.sleep(2)
|
||
self.click(int(self.d.info["displayWidth"] * 0.5),
|
||
int(self.d.info["displayHeight"] * 0.15))
|
||
self.sleep(1)
|
||
self.click_text("换封面") or self.click_text("更换封面") or self.click_contains("封面")
|
||
self.sleep(1)
|
||
self.click_text("从手机相册选择") or self.click_text("相册")
|
||
self.sleep(1)
|
||
return {"success": True, "note": "已打开相册选择页面"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def set_moments_privacy(self, days: int = 180, **kwargs) -> Dict[str, Any]:
|
||
"""设置朋友圈可见天数 [u2, ~8s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("设置")
|
||
self.sleep(1)
|
||
self.click_text("朋友权限") or self.click_text("隐私")
|
||
self.sleep(1)
|
||
self.click_text("朋友圈") or self.click_contains("允许朋友查看")
|
||
self.sleep(1)
|
||
day_map = {3: "最近三天", 30: "最近一个月", 180: "最近半年", 0: "全部"}
|
||
target = day_map.get(days, "最近半年")
|
||
if self.click_text(target) or self.click_contains(target):
|
||
return {"success": True, "days": days}
|
||
return {"success": False, "error": f"未找到选项: {target}"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def forward_moments_link(self, url: str = "", title: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""分享链接到朋友圈 [u2, ~6s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("发现")
|
||
self.sleep(1)
|
||
self.click_text("朋友圈")
|
||
self.sleep(2)
|
||
camera = self.d.xpath('//*[@content-desc="拍照分享"]')
|
||
if camera.exists:
|
||
camera.click()
|
||
self.sleep(1)
|
||
if title:
|
||
self.input_text(f"{title}\n{url}" if url else title)
|
||
elif url:
|
||
self.input_text(url)
|
||
self.sleep(0.5)
|
||
self.click_text("发表")
|
||
self.sleep(2)
|
||
return {"success": True}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
# ======================================================================
|
||
# 十三、个人设置(新增6个)
|
||
# 控制形式: u2 UI自动化 | Hook只能读(get_profile走SQLite)
|
||
# ======================================================================
|
||
|
||
def _goto_personal_info(self):
|
||
"""进入个人信息页(内部方法)"""
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
W = self.d.info["displayWidth"]
|
||
self.click(int(W * 0.3), int(self.d.info["displayHeight"] * 0.12))
|
||
self.sleep(1.5)
|
||
|
||
def get_profile(self, **kwargs) -> Dict[str, Any]:
|
||
"""获取当前微信账号资料 [Hook优先(SQLite userinfo, ~50ms), u2降级~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self._goto_personal_info()
|
||
ui = self.d.dump_hierarchy()
|
||
import xml.etree.ElementTree as ET
|
||
root = ET.fromstring(ui)
|
||
texts = [e.get("text", "").strip() for e in root.iter() if e.get("text", "").strip()]
|
||
profile = {"raw_texts": texts[:30]}
|
||
for t in texts:
|
||
if "微信号" in t:
|
||
profile["wxid"] = t.split(":")[-1].split(":")[-1].strip()
|
||
if "地区" in t:
|
||
profile["region"] = t
|
||
if texts:
|
||
profile["nickname"] = texts[0]
|
||
return {"success": True, **profile}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def set_nickname(self, nickname: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""修改微信昵称 [u2, ~6s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self._goto_personal_info()
|
||
self.click_text("名字") or self.click_contains("昵称")
|
||
self.sleep(0.5)
|
||
self.input_text(nickname, clear=True)
|
||
self.sleep(0.3)
|
||
self.click_text("保存") or self.click_text("完成")
|
||
return {"success": True, "nickname": nickname}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def set_signature(self, signature: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""修改个性签名 [u2, ~6s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self._goto_personal_info()
|
||
if not (self.click_text("个性签名") or self.click_contains("签名")):
|
||
self.click_text("更多信息")
|
||
self.sleep(0.5)
|
||
self.click_text("个性签名")
|
||
self.sleep(0.5)
|
||
self.input_text(signature, clear=True)
|
||
self.sleep(0.3)
|
||
self.click_text("保存") or self.click_text("完成")
|
||
return {"success": True, "signature": signature}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def set_status(self, status: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""设置微信状态 [u2, 默认打开状态入口]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("+状态") or self.click_text("状态") or self.click_contains("状态")
|
||
self.sleep(1)
|
||
if status and bool(kwargs.get("confirm")):
|
||
self.input_text(status)
|
||
self.sleep(0.3)
|
||
self.click_text("就这样") or self.click_text("完成")
|
||
return {
|
||
"success": True,
|
||
"status": status,
|
||
"dry_run": not bool(kwargs.get("confirm")),
|
||
"confirm_required": bool(status) and not bool(kwargs.get("confirm")),
|
||
"note": "已打开微信状态入口",
|
||
}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def set_avatar(self, image_path: str = None, **kwargs) -> Dict[str, Any]:
|
||
"""修改头像 [u2, ~8s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self._goto_personal_info()
|
||
self.click_text("头像")
|
||
self.sleep(1)
|
||
self.click_text("从手机相册选择") or self.click_text("相册")
|
||
self.sleep(1)
|
||
return {"success": True, "note": "已打开相册选择页面"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def set_gender(self, gender: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""设置性别 [u2, ~6s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self._goto_personal_info()
|
||
if not (self.click_text("性别") or self.click_contains("性别")):
|
||
self.click_text("更多信息")
|
||
self.sleep(0.5)
|
||
self.click_text("性别")
|
||
self.sleep(0.5)
|
||
target = "男" if gender.lower() in ("male", "男", "m") else "女"
|
||
self.click_text(target)
|
||
self.sleep(0.3)
|
||
self.click_text("保存") or self.click_text("完成")
|
||
return {"success": True, "gender": target}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def set_region(self, region: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""设置地区 [u2, ~6s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self._goto_personal_info()
|
||
if not (self.click_text("地区") or self.click_contains("地区")):
|
||
self.click_text("更多信息")
|
||
self.sleep(0.5)
|
||
self.click_text("地区")
|
||
self.sleep(0.5)
|
||
parts = region.split(" ") if region else ["中国", "福建", "厦门"]
|
||
for part in parts:
|
||
self.click_text(part) or self.click_contains(part)
|
||
self.sleep(0.5)
|
||
self.click_text("完成") or self.click_text("确定")
|
||
return {"success": True, "region": region}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
# ======================================================================
|
||
# 十四、账号安全(新增9个)
|
||
# 控制形式: u2 UI自动化 | Hook仅check_account_status可读
|
||
# ======================================================================
|
||
|
||
def check_account_status(self, **kwargs) -> Dict[str, Any]:
|
||
"""检查账号状态 [Hook优先(SQLite, ~50ms), u2降级~4s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
curr = self.d.app_current()
|
||
act = curr.get("activity", "")
|
||
logged_in = "LauncherUI" in act or "ChattingUI" in act
|
||
return {
|
||
"success": True,
|
||
"logged_in": logged_in,
|
||
"activity": act,
|
||
"status": "normal" if logged_in else "unknown"
|
||
}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def unblock_account(self, helper_wxid: str = None, **kwargs) -> Dict[str, Any]:
|
||
"""微信解封 [u2, ~10s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("设置")
|
||
self.sleep(1)
|
||
self.click_text("账号与安全") or self.click_text("帐号与安全")
|
||
self.sleep(1)
|
||
if self.click_text("微信安全中心") or self.click_contains("安全中心"):
|
||
self.sleep(1)
|
||
self.click_text("解封账号") or self.click_contains("解封")
|
||
self.sleep(1)
|
||
return {"success": True, "note": "已进入解封页面"}
|
||
return {"success": False, "error": "未找到安全中心"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def safety_center(self, **kwargs) -> Dict[str, Any]:
|
||
"""打开微信安全中心 [u2, ~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("设置")
|
||
self.sleep(1)
|
||
self.click_text("账号与安全") or self.click_text("帐号与安全")
|
||
self.sleep(1)
|
||
self.click_text("微信安全中心") or self.click_contains("安全中心")
|
||
self.sleep(1)
|
||
return {"success": True, "note": "已打开安全中心"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def change_password(self, old_pwd: str = "", new_pwd: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""修改微信密码 [u2, ~8s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("设置")
|
||
self.sleep(1)
|
||
self.click_text("账号与安全") or self.click_text("帐号与安全")
|
||
self.sleep(1)
|
||
self.click_text("微信密码") or self.click_contains("密码")
|
||
self.sleep(1)
|
||
return {"success": True, "note": "已进入密码修改页面"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def bind_phone(self, phone: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""绑定手机号 [u2, 默认打开手机号设置入口]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("设置")
|
||
self.sleep(1)
|
||
self.click_text("账号与安全") or self.click_text("帐号与安全")
|
||
self.sleep(1)
|
||
self.click_text("手机号") or self.click_contains("手机")
|
||
self.sleep(1)
|
||
return {"success": True, "phone": phone, "note": "已打开手机号设置入口"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def unbind_phone(self, **kwargs) -> Dict[str, Any]:
|
||
"""解绑银行卡式手机号入口 [u2, 默认只导航]"""
|
||
return self.bind_phone(**kwargs)
|
||
|
||
def get_login_devices(self, **kwargs) -> Dict[str, Any]:
|
||
"""查看登录设备 [u2, ~6s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("设置")
|
||
self.sleep(1)
|
||
self.click_text("账号与安全") or self.click_text("帐号与安全")
|
||
self.sleep(1)
|
||
self.click_text("登录设备管理") or self.click_contains("登录设备")
|
||
self.sleep(1)
|
||
items = []
|
||
try:
|
||
import xml.etree.ElementTree as ET
|
||
root = ET.fromstring(self.d.dump_hierarchy())
|
||
for elem in root.iter():
|
||
t = elem.get("text", "").strip()
|
||
if t and t not in ("登录设备管理", "当前设备"):
|
||
items.append(t[:80])
|
||
except Exception:
|
||
pass
|
||
return {"success": True, "devices": items[:30], "count": len(items[:30])}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e), "devices": []}
|
||
|
||
def remove_login_device(self, device_id: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""移除登录设备 [u2, 默认停在列表/确认前]"""
|
||
res = self.get_login_devices(**kwargs)
|
||
if not res.get("success") or not bool(kwargs.get("confirm")):
|
||
res.update({"dry_run": True, "confirm_required": True, "target_device_id": device_id})
|
||
return res
|
||
try:
|
||
if device_id:
|
||
self.click_text(device_id) or self.click_contains(device_id)
|
||
self.sleep(0.5)
|
||
self.click_text("删除") or self.click_text("移除")
|
||
self.sleep(0.5)
|
||
self.click_text("确定") or self.click_text("删除")
|
||
return {"success": True, "device_id": device_id}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def enable_fingerprint(self, enable: bool = True, **kwargs) -> Dict[str, Any]:
|
||
"""开启/关闭指纹支付入口 [u2, 默认只导航]"""
|
||
return self._open_payment_settings("指纹", enable=enable)
|
||
|
||
def set_account_protection(self, enable: bool = True, **kwargs) -> Dict[str, Any]:
|
||
"""账号保护入口 [u2, 默认只导航]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("设置")
|
||
self.sleep(1)
|
||
self.click_text("账号与安全") or self.click_text("帐号与安全")
|
||
self.sleep(1)
|
||
self.click_text("账号保护") or self.click_contains("保护")
|
||
self.sleep(1)
|
||
return {"success": True, "enable": enable, "note": "已打开账号保护入口"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def _open_payment_settings(self, keyword: str = "", **kwargs) -> Dict[str, Any]:
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("服务") or self.click_text("支付")
|
||
self.sleep(1)
|
||
self.click_text("钱包")
|
||
self.sleep(1)
|
||
self.click_text("支付设置") or self.click_contains("支付设置")
|
||
self.sleep(1)
|
||
if keyword:
|
||
self.click_text(keyword) or self.click_contains(keyword)
|
||
self.sleep(0.5)
|
||
return {"success": True, "keyword": keyword, "note": "已打开支付设置入口"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def unblock_self(self, **kwargs) -> Dict[str, Any]:
|
||
"""自助解封 [u2, ~8s]"""
|
||
return self.unblock_account(**kwargs)
|
||
|
||
def unblock_appeal(self, **kwargs) -> Dict[str, Any]:
|
||
"""申诉解封 [u2, ~8s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("设置")
|
||
self.sleep(1)
|
||
self.click_text("账号与安全") or self.click_text("帐号与安全")
|
||
self.sleep(1)
|
||
self.click_text("微信安全中心") or self.click_contains("安全中心")
|
||
self.sleep(1)
|
||
self.click_text("申诉") or self.click_contains("申诉")
|
||
return {"success": True, "note": "已进入申诉页面"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def check_restrictions(self, **kwargs) -> Dict[str, Any]:
|
||
"""检查当前功能限制 [u2, ~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
curr = self.d.app_current()
|
||
return {
|
||
"success": True,
|
||
"activity": curr.get("activity", ""),
|
||
"restricted": False,
|
||
"note": "功能限制需在安全中心查看"
|
||
}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def appeal_restriction(self, **kwargs) -> Dict[str, Any]:
|
||
"""申诉功能限制 [u2, ~8s]"""
|
||
return self.unblock_appeal(**kwargs)
|
||
|
||
def check_login_state(self, **kwargs) -> Dict[str, Any]:
|
||
"""检测微信是否已登录 [u2]"""
|
||
try:
|
||
from .wechat_auto_login import WeChatAutoLogin
|
||
return WeChatAutoLogin(self).check_login_state()
|
||
except Exception as exc:
|
||
return {"logged_in": False, "state": "error", "error": str(exc)}
|
||
|
||
def login_by_password(
|
||
self,
|
||
account: str = "",
|
||
phone: str = "",
|
||
password: str = "",
|
||
**kwargs,
|
||
) -> Dict[str, Any]:
|
||
"""微信号/手机号 + 密码登录 [u2]"""
|
||
try:
|
||
from .wechat_auto_login import WeChatAutoLogin
|
||
acct = (account or phone or kwargs.get("username") or "").strip()
|
||
pwd = password or kwargs.get("pwd") or ""
|
||
return WeChatAutoLogin(self).login_by_password(acct, pwd)
|
||
except Exception as exc:
|
||
return {"success": False, "error": str(exc)}
|
||
|
||
def ensure_logged_in(
|
||
self,
|
||
account: str = "",
|
||
phone: str = "",
|
||
password: str = "",
|
||
**kwargs,
|
||
) -> Dict[str, Any]:
|
||
"""未登录则按配置/参数自动登录 [u2]"""
|
||
acct = (account or phone or kwargs.get("username") or "").strip()
|
||
pwd = password or kwargs.get("pwd") or ""
|
||
if not acct or not pwd:
|
||
try:
|
||
import os
|
||
import yaml
|
||
cfg_path = os.path.join(
|
||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.dirname(__file__)))),
|
||
"config", "wechat_login.yaml",
|
||
)
|
||
if os.path.isfile(cfg_path):
|
||
with open(cfg_path, "r", encoding="utf-8") as f:
|
||
cfg = yaml.safe_load(f) or {}
|
||
dev_id = getattr(getattr(self, "d", None), "serial", None) or ""
|
||
dev_cfg = (cfg.get("devices") or {}).get(dev_id) or {}
|
||
default = cfg.get("default") or {}
|
||
acct = acct or dev_cfg.get("account") or default.get("account") or ""
|
||
pwd = pwd or dev_cfg.get("password") or default.get("password") or ""
|
||
except Exception as exc:
|
||
logger.warning(f"[ensure_logged_in] 读配置失败: {exc}")
|
||
try:
|
||
from .wechat_auto_login import WeChatAutoLogin
|
||
return WeChatAutoLogin(self).ensure_logged_in(acct, pwd)
|
||
except Exception as exc:
|
||
return {"success": False, "error": str(exc)}
|
||
|
||
def unblock_with_sms(self, phone: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""短信验证解封 [u2, ~8s]"""
|
||
return self.unblock_account(**kwargs)
|
||
|
||
def unblock_via_customer_service(
|
||
self,
|
||
reason: str = "",
|
||
phone: str = "",
|
||
wxid: str = "",
|
||
max_rounds: int = 20,
|
||
chat_interval_sec: int = 8,
|
||
use_web_search: bool = True,
|
||
**kwargs,
|
||
) -> Dict[str, Any]:
|
||
"""
|
||
联系客服解封(自动化全链路 + AI 持续对话)[u2 + AI Brain]
|
||
|
||
链路:设置 → 账号与安全 → 微信安全中心 → 联系客服 → 客服会话/小程序
|
||
→ 抓客服消息 → AI 生成回复 → 发送 → 循环直到解封/拒绝/超时
|
||
|
||
参数:
|
||
reason: 账号被封原因(用户自述,会带入 AI 上下文)
|
||
phone / wxid: 申诉时可能用到的账号信息
|
||
max_rounds: 最大对话轮次(默认 20)
|
||
chat_interval_sec: 每轮等待客服回复时间(默认 8s)
|
||
use_web_search: 是否在开始前联网搜「类似申诉经验」作为参考
|
||
|
||
返回: {
|
||
"success": bool,
|
||
"status": "success|reject|timeout|fallback|error",
|
||
"message": "客服最终消息",
|
||
"session": { 完整对话上下文 }
|
||
}
|
||
"""
|
||
import asyncio
|
||
try:
|
||
from .unblock_customer_service import (
|
||
UnblockCustomerServiceFlow,
|
||
UnblockSession,
|
||
)
|
||
except Exception as exc:
|
||
return {"success": False, "error": f"加载解封模块失败: {exc}"}
|
||
|
||
# 从 agent 反向取 AIBrain(可选,缺省时使用兜底话术)
|
||
ai_brain = None
|
||
agent = getattr(self, "agent", None) or getattr(self, "_agent", None)
|
||
if agent is not None:
|
||
ai_brain = getattr(agent, "ai_brain", None)
|
||
|
||
session = UnblockSession(
|
||
reason=reason,
|
||
phone=phone,
|
||
wxid=wxid,
|
||
max_rounds=max(1, min(int(max_rounds or 20), 50)),
|
||
chat_interval_sec=max(2, min(int(chat_interval_sec or 8), 60)),
|
||
use_web_search=bool(use_web_search),
|
||
)
|
||
flow = UnblockCustomerServiceFlow(self, ai_brain)
|
||
|
||
try:
|
||
loop = asyncio.get_event_loop()
|
||
if loop.is_running():
|
||
# 在已有事件循环中(Agent 内部)— 用 run_coroutine_threadsafe 或同步降级
|
||
import concurrent.futures
|
||
fut = asyncio.run_coroutine_threadsafe(flow.run(session), loop)
|
||
return fut.result(timeout=max_rounds * (chat_interval_sec + 4) + 60)
|
||
except RuntimeError:
|
||
pass
|
||
|
||
return asyncio.run(flow.run(session))
|
||
|
||
# ======================================================================
|
||
# 十五、支付(新增7个)
|
||
# 控制形式: u2 UI自动化(支付类操作不走Hook,安全考虑)
|
||
# ======================================================================
|
||
|
||
def send_red_packet(self, to_id: str = "", amount: str = "", message: str = "恭喜发财", **kwargs) -> Dict[str, Any]:
|
||
"""发红包 [u2, ~10s]"""
|
||
try:
|
||
guard = self._payment_guard(amount, bool(kwargs.get("confirm")), **kwargs)
|
||
if not guard.get("pass"):
|
||
return {"success": False, **guard}
|
||
if guard.get("dry_run"):
|
||
return {
|
||
"success": True,
|
||
"action": "send_red_packet",
|
||
"to_id": to_id,
|
||
"message": message,
|
||
**guard,
|
||
}
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.search(to_id)
|
||
self.click_text(to_id) or self.click_contains(to_id)
|
||
self.sleep(1)
|
||
self.click_text("+") or self.click_desc("更多功能")
|
||
self.sleep(0.5)
|
||
self.click_text("红包")
|
||
self.sleep(1)
|
||
if amount:
|
||
self.input_text(amount)
|
||
if not guard.get("dry_run") and (self.click_text("塞钱进红包") or self.click_contains("塞钱")):
|
||
return {"success": True, "note": "已进入红包支付确认", **guard}
|
||
return {"success": True, "note": "已打开红包页面", **guard}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def transfer(self, to_id: str = "", amount: str = "", message: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""转账 [u2, ~10s]"""
|
||
try:
|
||
guard = self._payment_guard(amount, bool(kwargs.get("confirm")), **kwargs)
|
||
if not guard.get("pass"):
|
||
return {"success": False, **guard}
|
||
if guard.get("dry_run"):
|
||
return {
|
||
"success": True,
|
||
"action": "transfer",
|
||
"to_id": to_id,
|
||
"message": message,
|
||
**guard,
|
||
}
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.search(to_id)
|
||
self.click_text(to_id) or self.click_contains(to_id)
|
||
self.sleep(1)
|
||
self.click_text("+") or self.click_desc("更多功能")
|
||
self.sleep(0.5)
|
||
self.click_text("转账")
|
||
self.sleep(1)
|
||
if amount:
|
||
self.input_text(amount)
|
||
if not guard.get("dry_run") and (self.click_text("转账") or self.click_text("确认转账")):
|
||
return {"success": True, "note": "已进入转账支付确认", **guard}
|
||
return {"success": True, "note": "已打开转账页面", **guard}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def send_transfer(self, to_id: str = "", amount: str = "", description: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""SDK 标准动作名:转账 [u2, 默认停在确认前]"""
|
||
return self.transfer(to_id=to_id, amount=amount, message=description, **kwargs)
|
||
|
||
def show_payment_code(self, **kwargs) -> Dict[str, Any]:
|
||
"""显示付款码 [u2, ~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("服务") or self.click_text("支付")
|
||
self.sleep(1)
|
||
self.click_text("收付款") or self.click_contains("付款码")
|
||
self.sleep(1)
|
||
return {"success": True, "note": "已显示付款码"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def receive_payment(self, amount: str = "", desc: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""收款 [u2, ~5s]"""
|
||
try:
|
||
guard = self._payment_guard(amount or "0", bool(kwargs.get("confirm")), **kwargs)
|
||
if not guard.get("pass"):
|
||
return {"success": False, **guard}
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("服务") or self.click_text("支付")
|
||
self.sleep(1)
|
||
self.click_text("收付款") or self.click_contains("付款码")
|
||
self.sleep(1)
|
||
self.click_text("二维码收款") or self.click_contains("收款")
|
||
self.sleep(1)
|
||
return {"success": True, "note": "已显示收款码", **guard}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def view_wallet(self, **kwargs) -> Dict[str, Any]:
|
||
"""查看钱包 [u2, ~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("服务") or self.click_text("支付")
|
||
self.sleep(1)
|
||
self.click_text("钱包")
|
||
self.sleep(1)
|
||
return {"success": True, "note": "已打开钱包"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def view_transactions(self, limit: int = 20, **kwargs) -> Dict[str, Any]:
|
||
"""查看账单 [u2, ~6s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("服务") or self.click_text("支付")
|
||
self.sleep(1)
|
||
self.click_text("钱包")
|
||
self.sleep(1)
|
||
self.click_text("账单") or self.click_contains("账单")
|
||
self.sleep(1)
|
||
return {"success": True, "note": "已打开账单页面"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def receive_red_packet(self, from_id: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""领取红包 [u2, ~5s]"""
|
||
try:
|
||
if not bool(kwargs.get("confirm")):
|
||
return {
|
||
"success": True,
|
||
"dry_run": True,
|
||
"confirm_required": True,
|
||
"note": "领取红包默认不自动点击“开”,需 confirm=true",
|
||
}
|
||
self.launch()
|
||
self.sleep(1)
|
||
if self.click_text("微信红包") or self.click_contains("红包"):
|
||
self.sleep(0.5)
|
||
self.click_text("开") or self.click_contains("开")
|
||
self.sleep(1)
|
||
return {"success": True}
|
||
return {"success": False, "error": "未找到红包"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def receive_transfer(self, msg_svr_id: str = "", from_id: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""领取转账 [u2, 默认停在确认前]"""
|
||
if not bool(kwargs.get("confirm")):
|
||
return {
|
||
"success": True,
|
||
"dry_run": True,
|
||
"confirm_required": True,
|
||
"note": "领取转账默认不自动确认收款,需 confirm=true",
|
||
}
|
||
try:
|
||
self.launch()
|
||
self.sleep(1)
|
||
if self.click_text("转账") or self.click_contains("转账"):
|
||
self.sleep(0.5)
|
||
self.click_text("确认收款") or self.click_contains("收款")
|
||
self.sleep(1)
|
||
return {"success": True}
|
||
return {"success": False, "error": "未找到转账消息"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
# ======================================================================
|
||
# 十六、聊天设置(新增3个)
|
||
# 控制形式: u2 UI自动化
|
||
# ======================================================================
|
||
|
||
def set_chat_top(self, user_id: str = "", enable: bool = True, **kwargs) -> Dict[str, Any]:
|
||
"""置顶/取消置顶聊天 [u2, ~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.search(user_id)
|
||
self.click_text(user_id) or self.click_contains(user_id)
|
||
self.sleep(1)
|
||
self.d.xpath('//*[@content-desc="聊天信息"]').click_exists(timeout=3)
|
||
self.sleep(1)
|
||
self.click_text("置顶聊天") or self.click_contains("置顶")
|
||
self.sleep(0.5)
|
||
return {"success": True, "top": enable}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def set_mute_chat(self, user_id: str = "", enable: bool = True, **kwargs) -> Dict[str, Any]:
|
||
"""消息免打扰 [u2, ~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.search(user_id)
|
||
self.click_text(user_id) or self.click_contains(user_id)
|
||
self.sleep(1)
|
||
self.d.xpath('//*[@content-desc="聊天信息"]').click_exists(timeout=3)
|
||
self.sleep(1)
|
||
self.click_text("消息免打扰") or self.click_contains("免打扰")
|
||
self.sleep(0.5)
|
||
return {"success": True, "muted": enable}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def clear_chat_history(self, user_id: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""清空聊天记录 [u2, ~6s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.search(user_id)
|
||
self.click_text(user_id) or self.click_contains(user_id)
|
||
self.sleep(1)
|
||
self.d.xpath('//*[@content-desc="聊天信息"]').click_exists(timeout=3)
|
||
self.sleep(1)
|
||
self.click_text("清空聊天记录") or self.click_contains("清空")
|
||
self.sleep(0.5)
|
||
self.click_text("确定") or self.click_text("清空")
|
||
return {"success": True}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
# ======================================================================
|
||
# 十七、收藏管理(新增2个)
|
||
# 控制形式: u2 UI自动化 | Hook可读(get_favorites走SQLite Favorite.db)
|
||
# ======================================================================
|
||
|
||
def add_to_favorites(self, content_desc: str = None, **kwargs) -> Dict[str, Any]:
|
||
"""收藏消息 [u2, ~4s]"""
|
||
try:
|
||
H = self.d.info["displayHeight"]
|
||
W = self.d.info["displayWidth"]
|
||
self.long_click(int(W * 0.5), int(H * 0.5), duration=1.0)
|
||
self.sleep(0.5)
|
||
if self.click_text("收藏"):
|
||
return {"success": True}
|
||
return {"success": False, "error": "未找到收藏选项"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def delete_favorite(self, local_id: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""删除收藏 [u2, 默认进入收藏列表并停在确认前]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("收藏")
|
||
self.sleep(1.5)
|
||
if not bool(kwargs.get("confirm")):
|
||
return {
|
||
"success": True,
|
||
"dry_run": True,
|
||
"confirm_required": True,
|
||
"note": "已打开收藏列表,删除收藏需 confirm=true",
|
||
"local_id": local_id,
|
||
}
|
||
H = self.d.info["displayHeight"]
|
||
W = self.d.info["displayWidth"]
|
||
self.long_click(int(W * 0.5), int(H * 0.25), duration=1.0)
|
||
self.sleep(0.5)
|
||
self.click_text("删除") or self.click_contains("删除")
|
||
self.sleep(0.5)
|
||
self.click_text("删除") or self.click_text("确定")
|
||
return {"success": True, "local_id": local_id}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def get_favorites(self, limit: int = 20, **kwargs) -> Dict[str, Any]:
|
||
"""获取收藏列表 [Hook优先(SQLite Favorite.db, ~100ms), u2降级~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("收藏")
|
||
self.sleep(1.5)
|
||
items = []
|
||
ui = self.d.dump_hierarchy()
|
||
import xml.etree.ElementTree as ET
|
||
root = ET.fromstring(ui)
|
||
for elem in root.iter():
|
||
t = elem.get("text", "").strip()
|
||
if t and len(t) > 2 and t not in ("收藏", "搜索", "最近使用"):
|
||
items.append({"content": t[:100]})
|
||
return {"success": True, "favorites": items[:limit]}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e), "favorites": []}
|
||
|
||
# ======================================================================
|
||
# 十八、小程序 & 公众号(新增2个)
|
||
# 控制形式: u2 UI自动化
|
||
# ======================================================================
|
||
|
||
def open_mini_program(self, name: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""打开小程序 [u2, ~8s]"""
|
||
try:
|
||
name = name or kwargs.get("app_id", "")
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("发现")
|
||
self.sleep(1)
|
||
self.click_text("小程序")
|
||
self.sleep(1)
|
||
if name:
|
||
self.click_text("搜索小程序") or self.click_desc("搜索")
|
||
self.sleep(0.5)
|
||
self.input_text(name)
|
||
self.sleep(1.5)
|
||
self.click_text(name) or self.click_contains(name)
|
||
self.sleep(2)
|
||
return {"success": True, "name": name}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def get_recent_mini_programs(self, limit: int = 20, **kwargs) -> Dict[str, Any]:
|
||
"""获取最近小程序列表 [u2, ~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("发现")
|
||
self.sleep(1)
|
||
self.click_text("小程序")
|
||
self.sleep(1.5)
|
||
items = []
|
||
ui = self.d.dump_hierarchy()
|
||
import xml.etree.ElementTree as ET
|
||
root = ET.fromstring(ui)
|
||
for elem in root.iter():
|
||
t = elem.get("text", "").strip()
|
||
if t and t not in ("小程序", "搜索", "最近使用", "我的小程序"):
|
||
items.append({"name": t[:80]})
|
||
return {"success": True, "mini_programs": items[:limit], "count": len(items[:limit])}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e), "mini_programs": []}
|
||
|
||
def share_mini_program(self, app_id: str = "", to_id: str = "", title: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""分享小程序 [u2, 默认打开小程序,不自动选择接收人]"""
|
||
if not bool(kwargs.get("confirm")):
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("发现")
|
||
self.sleep(1)
|
||
self.click_text("小程序")
|
||
self.sleep(1)
|
||
return {
|
||
"success": True,
|
||
"dry_run": True,
|
||
"confirm_required": True,
|
||
"note": "已打开小程序入口,分享需 confirm=true 后搜索并发送",
|
||
"app_id": app_id,
|
||
}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
res = self.open_mini_program(name=app_id or title, **kwargs)
|
||
if not res.get("success"):
|
||
return res
|
||
if not to_id or not bool(kwargs.get("confirm")):
|
||
return {
|
||
"success": True,
|
||
"dry_run": True,
|
||
"confirm_required": True,
|
||
"note": "已打开小程序,分享需 to_id + confirm=true 后继续",
|
||
"app_id": app_id,
|
||
}
|
||
try:
|
||
self.click_desc("更多") or self.click_text("...")
|
||
self.sleep(0.5)
|
||
self.click_text("转发") or self.click_text("发送给朋友")
|
||
self.sleep(0.8)
|
||
self.click_text(to_id) or self.click_contains(to_id)
|
||
self.sleep(0.5)
|
||
self.click_text("发送") or self.click_text("确定")
|
||
return {"success": True, "to_id": to_id, "app_id": app_id}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def follow_official_account(self, account_name: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""关注公众号 [u2, ~8s]"""
|
||
try:
|
||
account_name = account_name or kwargs.get("account_id", "")
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.search(account_name)
|
||
self.sleep(1)
|
||
self.click_text(account_name) or self.click_contains(account_name)
|
||
self.sleep(2)
|
||
if self.click_text("关注") or self.click_contains("关注"):
|
||
return {"success": True, "account": account_name}
|
||
return {"success": True, "note": "可能已关注"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def unfollow_official_account(self, account_id: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""取消关注公众号 [u2, 默认导航到公众号资料页,人工/二次确认后取消]"""
|
||
account_name = account_id or kwargs.get("account_name", "")
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.search(account_name)
|
||
self.sleep(1)
|
||
self.click_text(account_name) or self.click_contains(account_name)
|
||
self.sleep(1)
|
||
if not bool(kwargs.get("confirm")):
|
||
return {
|
||
"success": True,
|
||
"dry_run": True,
|
||
"confirm_required": True,
|
||
"note": "已打开公众号页面,取消关注需 confirm=true",
|
||
"account": account_name,
|
||
}
|
||
self.click_desc("更多") or self.click_text("...")
|
||
self.sleep(0.5)
|
||
self.click_text("不再关注") or self.click_contains("取消关注")
|
||
self.sleep(0.5)
|
||
self.click_text("不再关注") or self.click_text("确定")
|
||
return {"success": True, "account": account_name}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def get_official_account_articles(self, account_id: str = "", limit: int = 10, **kwargs) -> Dict[str, Any]:
|
||
"""获取/打开公众号文章列表 [u2, ~8s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.search(account_id)
|
||
self.sleep(1)
|
||
self.click_text(account_id) or self.click_contains(account_id)
|
||
self.sleep(2)
|
||
articles = []
|
||
try:
|
||
import xml.etree.ElementTree as ET
|
||
root = ET.fromstring(self.d.dump_hierarchy())
|
||
for elem in root.iter():
|
||
t = elem.get("text", "").strip()
|
||
if t and t not in ("消息", "服务", "发消息"):
|
||
articles.append({"title": t[:120]})
|
||
except Exception:
|
||
pass
|
||
return {
|
||
"success": True,
|
||
"account_id": account_id,
|
||
"articles": articles[:limit],
|
||
"count": len(articles[:limit]),
|
||
}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e), "articles": []}
|
||
|
||
# ======================================================================
|
||
# 十九、视频号(新增6个)
|
||
# 控制形式: u2 UI自动化
|
||
# ======================================================================
|
||
|
||
def _enter_video_channel(self):
|
||
"""进入视频号(内部方法)"""
|
||
self.click_text("发现")
|
||
self.sleep(1)
|
||
self.click_text("视频号") or self.click_contains("视频号")
|
||
self.sleep(2)
|
||
|
||
def open_video_channel(self, **kwargs) -> Dict[str, Any]:
|
||
"""打开视频号 [u2, ~3s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self._enter_video_channel()
|
||
return {"success": True}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def get_video_list(self, limit: int = 10, **kwargs) -> Dict[str, Any]:
|
||
"""获取视频号列表 [u2, ~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self._enter_video_channel()
|
||
videos = []
|
||
ui = self.d.dump_hierarchy()
|
||
import xml.etree.ElementTree as ET
|
||
root = ET.fromstring(ui)
|
||
for elem in root.iter():
|
||
t = elem.get("text", "").strip()
|
||
if t and len(t) > 3:
|
||
videos.append({"title": t[:100]})
|
||
return {"success": True, "videos": videos[:limit]}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e), "videos": []}
|
||
|
||
def like_video(self, index: int = 0, **kwargs) -> Dict[str, Any]:
|
||
"""点赞视频 [u2, ~4s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self._enter_video_channel()
|
||
for _ in range(index):
|
||
self.swipe("up", scale=0.8)
|
||
self.sleep(1)
|
||
W = self.d.info["displayWidth"]
|
||
H = self.d.info["displayHeight"]
|
||
self.click(int(W * 0.93), int(H * 0.45))
|
||
self.sleep(0.5)
|
||
return {"success": True}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def comment_video(self, index: int = 0, comment: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""评论视频 [u2, ~6s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self._enter_video_channel()
|
||
for _ in range(index):
|
||
self.swipe("up", scale=0.8)
|
||
self.sleep(1)
|
||
W = self.d.info["displayWidth"]
|
||
H = self.d.info["displayHeight"]
|
||
self.click(int(W * 0.93), int(H * 0.55))
|
||
self.sleep(1)
|
||
if comment:
|
||
self.input_text(comment)
|
||
self.sleep(0.3)
|
||
self.click_text("发送")
|
||
return {"success": True}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def reply_comment(self, video_id: str = "", comment_id: str = "", content: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""回复当前评论 [u2, 默认打开评论区并停在发送前]"""
|
||
try:
|
||
res = self.comment_video(index=int(kwargs.get("index", 0) or 0), comment=content if bool(kwargs.get("confirm")) else "")
|
||
if res.get("success") and not bool(kwargs.get("confirm")):
|
||
res.update({
|
||
"dry_run": True,
|
||
"confirm_required": bool(content),
|
||
"video_id": video_id,
|
||
"comment_id": comment_id,
|
||
"note": "已打开评论区,回复发送需 confirm=true",
|
||
})
|
||
return res
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def follow_video_creator(self, index: int = 0, **kwargs) -> Dict[str, Any]:
|
||
"""关注视频号创作者 [u2, ~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self._enter_video_channel()
|
||
for _ in range(index):
|
||
self.swipe("up", scale=0.8)
|
||
self.sleep(1)
|
||
self.click(int(self.d.info["displayWidth"] * 0.08),
|
||
int(self.d.info["displayHeight"] * 0.85))
|
||
self.sleep(1)
|
||
self.click_text("关注") or self.click_contains("关注")
|
||
self.sleep(0.5)
|
||
return {"success": True}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def unfollow_video_creator(self, index: int = 0, **kwargs) -> Dict[str, Any]:
|
||
"""取消关注视频号创作者 [u2, 默认停在确认前]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self._enter_video_channel()
|
||
for _ in range(index):
|
||
self.swipe("up", scale=0.8)
|
||
self.sleep(1)
|
||
self.click(int(self.d.info["displayWidth"] * 0.08),
|
||
int(self.d.info["displayHeight"] * 0.85))
|
||
self.sleep(1)
|
||
if not bool(kwargs.get("confirm")):
|
||
return {
|
||
"success": True,
|
||
"dry_run": True,
|
||
"confirm_required": True,
|
||
"note": "已打开视频号创作者入口,取消关注需 confirm=true",
|
||
}
|
||
self.click_text("已关注") or self.click_contains("已关注")
|
||
self.sleep(0.5)
|
||
self.click_text("不再关注") or self.click_text("确定")
|
||
return {"success": True}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def share_video(self, index: int = 0, to_id: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""分享视频 [u2, ~6s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self._enter_video_channel()
|
||
for _ in range(index):
|
||
self.swipe("up", scale=0.8)
|
||
self.sleep(1)
|
||
W = self.d.info["displayWidth"]
|
||
H = self.d.info["displayHeight"]
|
||
self.click(int(W * 0.93), int(H * 0.65))
|
||
self.sleep(1)
|
||
if to_id:
|
||
self.click_text(to_id) or self.click_contains(to_id)
|
||
self.sleep(0.5)
|
||
self.click_text("发送") or self.click_text("确定")
|
||
return {"success": True}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
# ======================================================================
|
||
# 二十、扫一扫(新增4个)
|
||
# 控制形式: u2 UI自动化
|
||
# ======================================================================
|
||
|
||
def scan_qr_code(self, **kwargs) -> Dict[str, Any]:
|
||
"""扫描二维码 [u2, ~3s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_desc("+") or self.click_text("+")
|
||
self.sleep(0.5)
|
||
self.click_text("扫一扫")
|
||
self.sleep(2)
|
||
return {"success": True, "note": "已打开扫一扫"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def scan_add_friend(self, **kwargs) -> Dict[str, Any]:
|
||
"""扫码加好友 [u2, ~3s]"""
|
||
return self.scan_qr_code(**kwargs)
|
||
|
||
def show_my_qr(self, **kwargs) -> Dict[str, Any]:
|
||
"""显示我的二维码并截图返回 [u2, ~6s]
|
||
|
||
WX-13:不仅导航到二维码页,还截图返回完整 base64 PNG(真正的二维码素材,
|
||
供 SDK/hub 展示或下游识别)。截图失败不影响导航成功标记。
|
||
"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self._goto_personal_info()
|
||
self.click_text("二维码名片") or self.click_text("我的二维码") or self.click_contains("二维码")
|
||
self.sleep(1.5)
|
||
result = {"success": True, "note": "已显示二维码"}
|
||
try:
|
||
import base64
|
||
from io import BytesIO
|
||
img = self.d.screenshot()
|
||
buf = BytesIO()
|
||
img.save(buf, format="PNG")
|
||
result["qr_image_b64"] = base64.b64encode(buf.getvalue()).decode()
|
||
result["qr_image_format"] = "png"
|
||
except Exception as _se:
|
||
result["qr_image_error"] = str(_se)
|
||
return result
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def show_group_qr(self, group_id: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""显示群二维码并截图返回 [u2, ~8s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.search(group_id)
|
||
self.click_text(group_id) or self.click_contains(group_id)
|
||
self.sleep(1)
|
||
self.d.xpath('//*[@content-desc="聊天信息"]').click_exists(timeout=3)
|
||
self.sleep(1)
|
||
self.click_text("群二维码") or self.click_contains("二维码")
|
||
self.sleep(1.5)
|
||
result = {"success": True, "note": "已显示群二维码", "group_id": group_id}
|
||
try:
|
||
import base64
|
||
from io import BytesIO
|
||
img = self.d.screenshot()
|
||
buf = BytesIO()
|
||
img.save(buf, format="PNG")
|
||
result["qr_image_b64"] = base64.b64encode(buf.getvalue()).decode()
|
||
result["qr_image_format"] = "png"
|
||
except Exception as _se:
|
||
result["qr_image_error"] = str(_se)
|
||
return result
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def _save_qr_image_to_album(self, image_base64: str = "", image_url: str = "") -> str:
|
||
"""WP-WX-04:二维码图落盘相册并触发媒体扫描,失败返回空串。"""
|
||
import base64
|
||
import shutil
|
||
import subprocess
|
||
import tempfile
|
||
|
||
dst = f"/sdcard/Pictures/wp_qr_{int(time.time())}.png"
|
||
try:
|
||
self.d.shell(f"mkdir -p {shlex.quote(os.path.dirname(dst))}", timeout=8)
|
||
|
||
def adb_push_fallback(local_path: str) -> bool:
|
||
adb_bin = shutil.which("adb")
|
||
serial = (
|
||
getattr(self.d, "serial", "")
|
||
or getattr(self, "device_id", "")
|
||
or os.environ.get("ANDROID_SERIAL", "")
|
||
)
|
||
if not adb_bin or not serial:
|
||
return False
|
||
try:
|
||
subprocess.run(
|
||
[adb_bin, "-s", serial, "push", local_path, dst],
|
||
check=True,
|
||
capture_output=True,
|
||
text=True,
|
||
timeout=30,
|
||
)
|
||
return True
|
||
except Exception as adb_err:
|
||
logger.warning("save_qr_image_to_album adb push fallback failed: %s", adb_err)
|
||
return False
|
||
|
||
if image_url:
|
||
self.d.shell(
|
||
f"curl -sL -o {shlex.quote(dst)} {shlex.quote(image_url)}",
|
||
timeout=30,
|
||
)
|
||
elif image_base64:
|
||
b64 = image_base64.split(",", 1)[-1].strip()
|
||
raw = base64.b64decode(b64)
|
||
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as f:
|
||
f.write(raw)
|
||
local = f.name
|
||
try:
|
||
pushed = False
|
||
try:
|
||
self.d.push(local, dst)
|
||
pushed = True
|
||
except Exception as push_err:
|
||
logger.warning("save_qr_image_to_album u2 push failed: %s", push_err)
|
||
if not pushed and not adb_push_fallback(local):
|
||
return ""
|
||
finally:
|
||
os.unlink(local)
|
||
else:
|
||
return ""
|
||
size_out = (self.d.shell(f"wc -c < {shlex.quote(dst)}").output or "").strip()
|
||
if not size_out or size_out == "0":
|
||
return ""
|
||
self.d.shell(
|
||
"am broadcast -a android.intent.action.MEDIA_SCANNER_SCAN_FILE "
|
||
f"-d file://{dst}",
|
||
timeout=8,
|
||
)
|
||
self.sleep(1)
|
||
return dst
|
||
except Exception as e:
|
||
logger.warning("save_qr_image_to_album failed: %s", e)
|
||
return ""
|
||
|
||
def _open_scan_pick_album_first(self) -> Dict[str, Any]:
|
||
"""微信扫一扫 → 相册 → 点第一张图。
|
||
|
||
真机验证(2026-06-27 Redmi K70 1080x2400):
|
||
- u2 dump_hierarchy 不到微信节点(SurfaceView/自绘限制)
|
||
- u2 click 被 MIUI INJECT_EVENTS 拦截
|
||
- 改用 root am start + root input tap 坐标点击
|
||
- 权限弹窗与 AlbumPreviewUI 用多坐标兜底
|
||
"""
|
||
try:
|
||
def current_focus() -> str:
|
||
try:
|
||
return self.d.shell("dumpsys window | grep mCurrentFocus").output or ""
|
||
except Exception:
|
||
return ""
|
||
|
||
def tap(x: int, y: int, wait_sec: float = 1.5) -> None:
|
||
self.d.shell(f"su -c 'input tap {x} {y}'")
|
||
self.sleep(wait_sec)
|
||
|
||
# 1. root am start 直接打开扫一扫(绕过 u2 click "+" 菜单)
|
||
self.d.shell("su -c 'am start -n com.tencent.mm/.plugin.scanner.ui.BaseScanUI'")
|
||
self.sleep(2)
|
||
|
||
# 2. 点相册按钮(937,2012 · 真机验证)
|
||
tap(937, 2012, 2)
|
||
|
||
# 3. 处理 MIUI/微信自有权限弹窗;焦点有时不叫 GrantPermissions
|
||
focus = current_focus()
|
||
focus_norm = focus.lower()
|
||
if any(key in focus_norm for key in ("grantpermissions", "permission", "permis")):
|
||
for allow_x, allow_y in ((830, 1710), (540, 1710)):
|
||
tap(allow_x, allow_y, 1.5)
|
||
self.d.shell("su -c 'am start -n com.tencent.mm/.plugin.scanner.ui.BaseScanUI'")
|
||
self.sleep(1.5)
|
||
tap(937, 2012, 2)
|
||
focus_norm = current_focus().lower()
|
||
if "gallery" in focus_norm or "album" in focus_norm:
|
||
break
|
||
|
||
# 4. 检查是否进入相册(焦点应含 AlbumPreviewUI / AlbumPickerUI / gallery)
|
||
focus2 = current_focus()
|
||
focus2_norm = focus2.lower()
|
||
if "gallery" not in focus2_norm and "album" not in focus2_norm:
|
||
return {"success": False, "error": f"未进入相册界面(焦点={focus2[:120]})"}
|
||
|
||
# 5. 点第一张图(270,400 · 3列网格第一张 · 真机验证)
|
||
tap(270, 400, 2)
|
||
|
||
# 6. 在 AlbumPreviewUI 点"完成"按钮
|
||
ok = self.click_text("完成") or self.click_contains("完成")
|
||
if not ok:
|
||
# `/tmp/find_btn_out.txt` 像素分析显示底部中心高亮明显,优先点底部中心,再试右下/右上。
|
||
for finish_x, finish_y in ((528, 2330), (950, 2300), (950, 120)):
|
||
tap(finish_x, finish_y, 1.5)
|
||
xml_try = self.d.dump_hierarchy(compressed=True)
|
||
if any(flag in xml_try for flag in ("添加到通讯录", "发消息", "二维码已过期", "二维码无效")):
|
||
ok = True
|
||
break
|
||
|
||
xml = self.d.dump_hierarchy(compressed=True)
|
||
texts = re.findall(r'text="([^"]+)"', xml)[:15]
|
||
if any(flag in xml for flag in ("添加到通讯录", "发消息", "二维码已过期", "二维码无效")):
|
||
return {"success": True, "note": "已从相册选取图片识别二维码", "page_texts": texts}
|
||
return {
|
||
"success": False,
|
||
"error": "已选取图片但未进入二维码结果页",
|
||
"page_texts": texts,
|
||
}
|
||
except Exception as e:
|
||
return {"success": False, "error": f"扫一扫相册流程异常: {e}"}
|
||
|
||
def extract_qr_from_image(
|
||
self, image_base64: str = "", image_url: str = "", **kwargs
|
||
) -> Dict[str, Any]:
|
||
"""从图片识别二维码(支持 image_base64 / image_url 落盘后再扫) [u2, ~8s]"""
|
||
try:
|
||
saved = ""
|
||
if image_base64 or image_url:
|
||
saved = self._save_qr_image_to_album(
|
||
image_base64=image_base64, image_url=image_url
|
||
)
|
||
if not saved:
|
||
return {
|
||
"success": False,
|
||
"error": "图片落盘相册失败(image_base64/image_url 无效或写入失败)",
|
||
}
|
||
result = self._open_scan_pick_album_first()
|
||
if saved:
|
||
result["saved_path"] = saved
|
||
return result
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def add_friend_from_image(
|
||
self,
|
||
image_base64: str = "",
|
||
image_url: str = "",
|
||
verify_message: str = "",
|
||
**kwargs,
|
||
) -> Dict[str, Any]:
|
||
"""WP-WX-04:二维码图 → 扫一扫相册识别 → 发送好友申请。"""
|
||
try:
|
||
ext = self.extract_qr_from_image(
|
||
image_base64=image_base64, image_url=image_url
|
||
)
|
||
if not ext.get("success"):
|
||
return ext
|
||
self.sleep(2)
|
||
xml = self.d.dump_hierarchy(compressed=True)
|
||
if "添加到通讯录" in xml:
|
||
self.click_text("添加到通讯录") or self.click_contains("添加到通讯录")
|
||
self.sleep(1)
|
||
if verify_message:
|
||
xml2 = self.d.dump_hierarchy(compressed=True)
|
||
if "发送添加朋友申请" in xml2 or "申请添加朋友" in xml2:
|
||
(
|
||
self.click_text("发送添加朋友申请")
|
||
or self.click_contains("申请添加朋友")
|
||
)
|
||
self.sleep(0.5)
|
||
self.human_type(verify_message, clear=True)
|
||
self.sleep(0.3)
|
||
self.click_text("发送") or self.click_text("完成")
|
||
return {
|
||
"success": True,
|
||
"note": "已识别二维码并发送好友请求",
|
||
"verified": True,
|
||
"verify_message": verify_message,
|
||
}
|
||
if "该二维码已过期" in xml or "二维码无效" in xml:
|
||
return {"success": False, "error": "二维码已过期/无效"}
|
||
if "已是好友" in xml or ("发消息" in xml and "添加到通讯录" not in xml):
|
||
return {"success": True, "note": "对方已是好友", "already_friend": True}
|
||
texts = re.findall(r'text="([^"]+)"', xml)[:10]
|
||
return {
|
||
"success": False,
|
||
"error": "未识别到好友二维码或未进入添加页",
|
||
"page_texts": texts,
|
||
}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
# ======================================================================
|
||
# 二十一、通话(新增2个)
|
||
# 控制形式: u2 UI自动化
|
||
# ======================================================================
|
||
|
||
def voice_call(self, user_id: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""语音通话 [u2, ~6s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.search(user_id)
|
||
self.click_text(user_id) or self.click_contains(user_id)
|
||
self.sleep(1)
|
||
self.click_text("+") or self.click_desc("更多功能")
|
||
self.sleep(0.5)
|
||
self.click_text("语音通话") or self.click_contains("语音通话")
|
||
self.sleep(1)
|
||
return {"success": True, "note": "已发起语音通话"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def video_call(self, user_id: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""视频通话 [u2, ~6s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.search(user_id)
|
||
self.click_text(user_id) or self.click_contains(user_id)
|
||
self.sleep(1)
|
||
self.click_text("+") or self.click_desc("更多功能")
|
||
self.sleep(0.5)
|
||
self.click_text("视频通话") or self.click_contains("视频通话")
|
||
self.sleep(1)
|
||
return {"success": True, "note": "已发起视频通话"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
# ======================================================================
|
||
# 二十二、群发/搜索/发现(新增3个)
|
||
# 控制形式: u2 UI自动化 | Hook可加速(wechat_search走SQLite)
|
||
# ======================================================================
|
||
|
||
def mass_send(self, content: str = "", user_ids: List[str] = None, **kwargs) -> Dict[str, Any]:
|
||
"""群发消息(微信自带群发助手)[u2, ~10s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("设置")
|
||
self.sleep(1)
|
||
self.click_text("通用")
|
||
self.sleep(1)
|
||
self.click_text("辅助功能") or self.click_contains("辅助")
|
||
self.sleep(1)
|
||
self.click_text("群发助手")
|
||
self.sleep(1)
|
||
self.click_text("新建群发") or self.click_text("开始群发")
|
||
self.sleep(1)
|
||
if user_ids:
|
||
for uid in user_ids:
|
||
self.click_text(uid) or self.click_contains(uid)
|
||
self.sleep(0.3)
|
||
self.click_text("下一步") or self.click_text("完成")
|
||
self.sleep(1)
|
||
if content:
|
||
self.input_text(content)
|
||
self.sleep(0.3)
|
||
self.click_text("发送")
|
||
return {"success": True}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def wechat_search(self, keyword: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""搜一搜 [Hook优先(SQLite多表联查, ~100ms), u2降级~3s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("发现")
|
||
self.sleep(1)
|
||
self.click_text("搜一搜") or self.click_contains("搜索")
|
||
self.sleep(1)
|
||
if keyword:
|
||
self.input_text(keyword)
|
||
self.sleep(2)
|
||
return {"success": True, "keyword": keyword}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def top_stories(self, **kwargs) -> Dict[str, Any]:
|
||
"""看一看 [u2, ~3s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("发现")
|
||
self.sleep(1)
|
||
self.click_text("看一看") or self.click_contains("看一看")
|
||
self.sleep(2)
|
||
return {"success": True}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
# ======================================================================
|
||
# 二十三、微信运动(新增2个)
|
||
# 控制形式: u2 UI自动化
|
||
# ======================================================================
|
||
|
||
def get_steps(self, **kwargs) -> Dict[str, Any]:
|
||
"""获取步数 [u2, ~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.search("微信运动")
|
||
self.click_text("微信运动") or self.click_contains("微信运动")
|
||
self.sleep(2)
|
||
ui = self.d.dump_hierarchy()
|
||
import xml.etree.ElementTree as ET
|
||
root = ET.fromstring(ui)
|
||
steps = None
|
||
for elem in root.iter():
|
||
t = elem.get("text", "").strip()
|
||
if t and t.isdigit() and len(t) >= 2:
|
||
steps = int(t)
|
||
break
|
||
return {"success": True, "steps": steps}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def like_steps(self, user_id: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""点赞步数 [u2, ~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.search("微信运动")
|
||
self.click_text("微信运动") or self.click_contains("微信运动")
|
||
self.sleep(2)
|
||
self.click_text("步数排行榜") or self.click_contains("排行")
|
||
self.sleep(1)
|
||
if user_id:
|
||
self.click_text(user_id) or self.click_contains(user_id)
|
||
self.sleep(0.5)
|
||
self.click_text("赞") or self.click_contains("赞")
|
||
return {"success": True}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
# ======================================================================
|
||
# 二十四、位置分享(新增2个)
|
||
# 控制形式: u2 UI自动化
|
||
# ======================================================================
|
||
|
||
def send_location(self, user_id: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""发送位置 [u2, ~6s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
if user_id:
|
||
self.search(user_id)
|
||
self.click_text(user_id) or self.click_contains(user_id)
|
||
self.sleep(1)
|
||
self.click_text("+") or self.click_desc("更多功能")
|
||
self.sleep(0.5)
|
||
self.click_text("位置") or self.click_contains("位置")
|
||
self.sleep(0.5)
|
||
self.click_text("发送位置")
|
||
self.sleep(2)
|
||
self.click_text("发送")
|
||
return {"success": True}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def share_real_time_location(self, user_id: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""共享实时位置 [u2, ~6s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
if user_id:
|
||
self.search(user_id)
|
||
self.click_text(user_id) or self.click_contains(user_id)
|
||
self.sleep(1)
|
||
self.click_text("+") or self.click_desc("更多功能")
|
||
self.sleep(0.5)
|
||
self.click_text("位置") or self.click_contains("位置")
|
||
self.sleep(0.5)
|
||
self.click_text("共享实时位置")
|
||
self.sleep(1)
|
||
return {"success": True, "note": "已发起实时位置共享"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
# ======================================================================
|
||
# 二十五、表情管理(新增2个)
|
||
# 控制形式: u2 UI自动化
|
||
# ======================================================================
|
||
|
||
def send_emoji(self, user_id: str = "", emoji_name: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""发送表情 [u2, ~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
if user_id:
|
||
self.search(user_id)
|
||
self.click_text(user_id) or self.click_contains(user_id)
|
||
self.sleep(1)
|
||
W = self.d.info["displayWidth"]
|
||
H = self.d.info["displayHeight"]
|
||
self.click(int(W * 0.12), int(H * 0.955))
|
||
self.sleep(0.5)
|
||
if emoji_name:
|
||
self.click_text(emoji_name) or self.click_contains(emoji_name)
|
||
else:
|
||
self.click(int(W * 0.1), int(H * 0.88))
|
||
self.sleep(0.3)
|
||
return {"success": True}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def add_custom_emoji(self, image_path: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""添加自定义表情 [u2, 默认打开添加入口]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("表情")
|
||
self.sleep(1)
|
||
self.click_text("设置") or self.click_desc("设置")
|
||
self.sleep(0.8)
|
||
self.click_text("添加的单个表情") or self.click_contains("单个表情")
|
||
self.sleep(0.8)
|
||
self.click_desc("+") or self.click_text("+")
|
||
self.sleep(1)
|
||
return {"success": True, "note": "已打开添加自定义表情入口", "image_path": image_path}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def get_sticker_list(self, **kwargs) -> Dict[str, Any]:
|
||
"""获取表情包列表 [u2, ~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("表情")
|
||
self.sleep(1)
|
||
stickers = []
|
||
ui = self.d.dump_hierarchy()
|
||
import xml.etree.ElementTree as ET
|
||
root = ET.fromstring(ui)
|
||
for elem in root.iter():
|
||
t = elem.get("text", "").strip()
|
||
if t and len(t) > 0 and t not in ("表情", "搜索", "我的表情", "更多"):
|
||
stickers.append({"name": t})
|
||
return {"success": True, "stickers": stickers[:50]}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e), "stickers": []}
|
||
|
||
# ======================================================================
|
||
# 二十六、语音/文件(新增3个)
|
||
# 控制形式: u2 UI自动化
|
||
# ======================================================================
|
||
|
||
def send_voice_message(self, user_id: str = "", duration: int = 3, **kwargs) -> Dict[str, Any]:
|
||
"""发送语音消息 [u2, ~duration+3s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
if user_id:
|
||
self.search(user_id)
|
||
self.click_text(user_id) or self.click_contains(user_id)
|
||
self.sleep(1)
|
||
W = self.d.info["displayWidth"]
|
||
H = self.d.info["displayHeight"]
|
||
if self.click_text("按住 说话") or self.click_contains("按住"):
|
||
pass
|
||
else:
|
||
self.click(int(W * 0.06), int(H * 0.955))
|
||
self.sleep(0.5)
|
||
btn_x = int(W * 0.5)
|
||
btn_y = int(H * 0.955)
|
||
self.long_click(btn_x, btn_y, duration=float(duration))
|
||
self.sleep(0.5)
|
||
return {"success": True, "duration": duration}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def send_file_from_chat(self, user_id: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""发送文件 [u2, ~6s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
if user_id:
|
||
self.search(user_id)
|
||
self.click_text(user_id) or self.click_contains(user_id)
|
||
self.sleep(1)
|
||
self.click_text("+") or self.click_desc("更多功能")
|
||
self.sleep(0.5)
|
||
self.click_text("文件") or self.click_contains("文件")
|
||
self.sleep(1)
|
||
return {"success": True, "note": "已打开文件选择"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def download_file(self, user_id: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""下载文件 [u2, ~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.sleep(1)
|
||
if self.click_text("文件") or self.click_contains("[文件]"):
|
||
self.sleep(1)
|
||
return {"success": True, "note": "已点击文件"}
|
||
return {"success": False, "error": "未找到可下载的文件"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
# ======================================================================
|
||
# 二十七、设置管理(新增5个)
|
||
# 控制形式: u2 UI自动化
|
||
# ======================================================================
|
||
|
||
def toggle_do_not_disturb(self, enable: bool = True, **kwargs) -> Dict[str, Any]:
|
||
"""勿扰模式 [u2, ~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("设置")
|
||
self.sleep(1)
|
||
self.click_text("新消息通知") or self.click_text("消息通知")
|
||
self.sleep(1)
|
||
self.click_text("勿扰模式") or self.click_contains("勿扰")
|
||
self.sleep(0.5)
|
||
return {"success": True, "dnd": enable}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def add_to_float(self, wxid: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""添加聊天浮窗 [u2, 默认进入聊天更多菜单]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.search(wxid)
|
||
self.click_text(wxid) or self.click_contains(wxid)
|
||
self.sleep(1)
|
||
self.d.xpath('//*[@content-desc="聊天信息"]').click_exists(timeout=3)
|
||
self.sleep(1)
|
||
self.click_text("浮窗") or self.click_contains("浮窗")
|
||
return {"success": True, "wxid": wxid}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def remove_from_float(self, wxid: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""移除浮窗 [u2]"""
|
||
return self.add_to_float(wxid=wxid, **kwargs)
|
||
|
||
def set_notification(self, type: str = "all", enable: bool = True, **kwargs) -> Dict[str, Any]:
|
||
"""设置新消息通知 [u2]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("设置")
|
||
self.sleep(1)
|
||
self.click_text("新消息通知") or self.click_text("消息通知")
|
||
self.sleep(1)
|
||
return {"success": True, "type": type, "enable": enable, "note": "已打开新消息通知设置"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def set_chat_background(self, wxid: str = "", image_path: str = "", **kwargs) -> Dict[str, Any]:
|
||
"""设置聊天背景 [u2, 默认打开背景设置入口]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
if wxid:
|
||
self.search(wxid)
|
||
self.click_text(wxid) or self.click_contains(wxid)
|
||
self.sleep(1)
|
||
self.d.xpath('//*[@content-desc="聊天信息"]').click_exists(timeout=3)
|
||
self.sleep(1)
|
||
self.click_text("设置当前聊天背景") or self.click_contains("聊天背景")
|
||
self.sleep(1)
|
||
return {"success": True, "wxid": wxid, "image_path": image_path, "note": "已打开聊天背景入口"}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def clear_cache(self, **kwargs) -> Dict[str, Any]:
|
||
"""清理缓存 [u2, ~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("设置")
|
||
self.sleep(1)
|
||
self.click_text("通用")
|
||
self.sleep(1)
|
||
self.click_text("存储空间") or self.click_contains("存储")
|
||
self.sleep(2)
|
||
self.click_text("清理") or self.click_contains("清理")
|
||
self.sleep(0.5)
|
||
self.click_text("确定")
|
||
return {"success": True}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def check_for_update(self, **kwargs) -> Dict[str, Any]:
|
||
"""检查更新 [u2, ~5s]"""
|
||
try:
|
||
self.launch()
|
||
self.wait_for_app_ready()
|
||
self.click_text("我")
|
||
self.sleep(1)
|
||
self.click_text("设置")
|
||
self.sleep(1)
|
||
self.click_text("关于微信")
|
||
self.sleep(1)
|
||
self.click_text("检查新版本") or self.click_contains("版本")
|
||
self.sleep(2)
|
||
return {"success": True}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def logout(self, **kwargs) -> Dict[str, Any]:
|
||
"""退出登录 [root am start 深链 SettingsUI + root tap]
|
||
|
||
WeChat 8.0.69 UI 非无障碍(click_text 失效),故:
|
||
1. `su -c am start SettingsUI` 直达设置页(绕非无障碍底栏 tab 导航);
|
||
2. root swipe 滚到底(退出登录在底部);
|
||
3. 截图返回(qr/screenshot 供人工/下游确认 退出登录 位置);
|
||
4. **默认非破坏**:仅当 confirm=True 才 root tap 退出登录 + 确认(真退登录,需再登凭证)。
|
||
"""
|
||
import base64
|
||
from io import BytesIO
|
||
confirm = bool(kwargs.get("confirm"))
|
||
try:
|
||
# 1) root 深链直达设置页
|
||
self.d.shell("su -c 'am start -n com.tencent.mm/.plugin.setting.ui.setting.SettingsUI'")
|
||
self.sleep(2.0)
|
||
cur = ""
|
||
try:
|
||
cur = self.d.app_current().get("activity", "")
|
||
except Exception:
|
||
pass
|
||
if "SettingsUI" not in cur:
|
||
# 回退:常规我→设置(老版本/可访问时)
|
||
self.launch(); self.wait_for_app_ready()
|
||
self.click_text("我"); self.sleep(1); self.click_text("设置"); self.sleep(1)
|
||
# 2) 滚到底(退出登录在底部)
|
||
info = self.d.info
|
||
W, H = info.get("displayWidth", 1080), info.get("displayHeight", 2400)
|
||
for _ in range(4):
|
||
if self._root_input_available():
|
||
self._root_swipe(W // 2, int(H * 0.8), W // 2, int(H * 0.25), 250)
|
||
else:
|
||
self.swipe("up", scale=0.6)
|
||
self.sleep(0.4)
|
||
# 3) 截图返回(供确认 退出登录 位置)
|
||
result = {"success": True, "navigated_to": "SettingsUI", "confirm": confirm}
|
||
try:
|
||
img = self.d.screenshot(); buf = BytesIO(); img.save(buf, format="PNG")
|
||
result["screenshot_b64"] = base64.b64encode(buf.getvalue()).decode()
|
||
except Exception as _se:
|
||
result["screenshot_error"] = str(_se)
|
||
# 4) 默认非破坏;confirm=True 才真退
|
||
if not confirm:
|
||
result["note"] = "已到设置底部(退出登录区域),未点击退出(confirm=False,非破坏);传 confirm=true 真退(需再登凭证)"
|
||
return result
|
||
# 真退:root tap「退出」(底部按钮,截图校准 ~H*0.89)+ 确认对话框
|
||
if self._root_input_available():
|
||
self._root_tap(W // 2, int(H * 0.89)) # 8.0.69 设置底部「退出」按钮(截图校准)
|
||
self.sleep(1.2)
|
||
self._root_tap(W // 2, int(H * 0.60)) # 确认对话框「退出登录」按钮(居中偏下)
|
||
else:
|
||
self.click_text("退出登录") or self.click_text("退出")
|
||
self.sleep(0.5)
|
||
self.click_text("退出登录") or self.click_text("确定")
|
||
self.sleep(2.0)
|
||
result["note"] = "已尝试退出登录(confirm=True)"
|
||
return result
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
def switch_account(self, **kwargs) -> Dict[str, Any]:
|
||
"""切换账号 [root am start 深链 SettingsUI + root tap「切换账号」]
|
||
|
||
WeChat 8.0.69 非无障碍:root 深链直达设置 + root swipe 滚到底 + root tap
|
||
「切换账号」(截图校准 ~H*0.79)。打开账号选择页(非破坏,不实际切换)。
|
||
"""
|
||
import base64
|
||
from io import BytesIO
|
||
try:
|
||
self.d.shell("su -c 'am start -n com.tencent.mm/.plugin.setting.ui.setting.SettingsUI'")
|
||
self.sleep(2.0)
|
||
cur = ""
|
||
try:
|
||
cur = self.d.app_current().get("activity", "")
|
||
except Exception:
|
||
pass
|
||
if "SettingsUI" not in cur:
|
||
self.launch(); self.wait_for_app_ready()
|
||
self.click_text("我"); self.sleep(1); self.click_text("设置"); self.sleep(1)
|
||
info = self.d.info
|
||
W, H = info.get("displayWidth", 1080), info.get("displayHeight", 2400)
|
||
for _ in range(4):
|
||
if self._root_input_available():
|
||
self._root_swipe(W // 2, int(H * 0.8), W // 2, int(H * 0.25), 250)
|
||
else:
|
||
self.swipe("up", scale=0.6)
|
||
self.sleep(0.4)
|
||
if self._root_input_available():
|
||
self._root_tap(W // 2, int(H * 0.79)) # 设置底部「切换账号」按钮(截图校准)
|
||
else:
|
||
self.click_text("切换账号") or self.click_contains("切换")
|
||
self.sleep(1.5)
|
||
result = {"success": True, "note": "已 root 深链导航并点击「切换账号」(打开账号选择页)"}
|
||
try:
|
||
img = self.d.screenshot(); buf = BytesIO(); img.save(buf, format="PNG")
|
||
result["screenshot_b64"] = base64.b64encode(buf.getvalue()).decode()
|
||
except Exception:
|
||
pass
|
||
return result
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|
||
|
||
# ======================================================================
|
||
# 二十八、截图(新增1个)
|
||
# 控制形式: u2直接调用设备截图API
|
||
# ======================================================================
|
||
|
||
def screenshot(self, **kwargs) -> Dict[str, Any]:
|
||
"""截图 [u2直接API, ~500ms]"""
|
||
try:
|
||
import base64
|
||
from io import BytesIO
|
||
img = self.d.screenshot()
|
||
buf = BytesIO()
|
||
img.save(buf, format="PNG")
|
||
b64 = base64.b64encode(buf.getvalue()).decode()
|
||
return {"success": True, "screenshot": b64[:100] + "...", "size": len(b64)}
|
||
except Exception as e:
|
||
return {"success": False, "error": str(e)}
|