Files
workphone-sdk/sdk/agent/skills/wechat/skill.py

3830 lines
150 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
微信控制技能 - 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-10swechat_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)}