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