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

1528 lines
52 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

"""
微信控制技能 - Agent端实现
功能模块:
1. 消息管理 - 发送/获取消息、批量发送
2. 好友管理 - 添加/通过好友、设置备注、删除好友
3. 群聊管理 - 创建群/邀请入群/群发消息/设置群欢迎语
4. 标签管理 - 添加/删除/查询标签
5. 朋友圈管理 - 发布/点赞/评论
@author 卡若
@version 3.0.0
"""
import time
import logging
import re
import 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):
"""
微信控制技能
支持的操作:
- send_message: 发送消息
- get_messages: 获取消息列表
- get_contacts: 获取联系人列表
- add_friend: 添加好友
- accept_friend: 通过好友请求
- set_remark: 设置备注
- delete_friend: 删除好友
- create_group: 创建群聊
- invite_to_group: 邀请入群
- remove_from_group: 移出群聊
- send_group_message: 发送群消息
- set_group_notice: 设置群公告
- set_group_name: 设置群名
- set_group_welcome: 设置群欢迎语
- get_groups: 获取群列表
- get_group_members: 获取群成员
- add_tag: 添加标签
- remove_tag: 移除标签
- create_tag: 创建标签
- delete_tag: 删除标签
- get_tags: 获取标签列表
- get_users_by_tag: 根据标签获取好友
- post_moments: 发布朋友圈
- like_moments: 点赞朋友圈
- comment_moments: 评论朋友圈
- get_moments: 获取朋友圈
- batch_send_message: 批量发送消息
"""
PACKAGE = "com.tencent.mm"
NAME = "微信"
SEND_MESSAGE_TIMEOUT = 45
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=2, delay=0.5)
def send_message(self, to_id: str, content: str, msg_type: str = "text") -> Dict[str, Any]:
"""
发送消息(先尝试视觉流程,失败则用规则流程)
"""
start = time.time()
try:
out = self.send_message_with_vision(to_id, content, msg_type)
if not out.get("success") and out.get("error") == "timeout":
return out
return out
except _VisionFallback:
pass
try:
if time.time() - start > self.SEND_MESSAGE_TIMEOUT:
return {"success": False, "error": "timeout"}
self.launch()
self.wait_for_app_ready(timeout=4)
if time.time() - start > self.SEND_MESSAGE_TIMEOUT:
return {"success": False, "error": "timeout"}
if not self.wait_for_ui(["微信", "通讯录", "搜索"], timeout=2):
self.back()
self.sleep(0.2)
if self.click_text("搜索", timeout=2) or self.click_desc("搜索"):
self.wait_for_ui(["请输入", "搜索"], timeout=2)
else:
self.d.xpath('//*[@content-desc="搜索"]').click_exists(timeout=2)
self.sleep(0.2)
if time.time() - start > self.SEND_MESSAGE_TIMEOUT:
return {"success": False, "error": "timeout"}
self.input_text(to_id)
self.wait_for_ui([to_id], timeout=3) or self.sleep(0.3)
if not self.click_text(to_id, timeout=2):
if not self.click_contains(to_id, timeout=2):
return {"success": False, "error": f"未找到联系人: {to_id}"}
if time.time() - start > self.SEND_MESSAGE_TIMEOUT:
return {"success": False, "error": "timeout"}
if not self.wait_for_ui(["发送", "按住 说话", "发消息"], timeout=3):
return {"success": False, "error": "未能进入聊天界面"}
input_clicked = False
for selector in ["发消息", "输入", "请输入"]:
if self.click_contains(selector, timeout=1):
input_clicked = True
break
if not input_clicked:
info = self.d.info
self.click(info['displayWidth'] // 2, info['displayHeight'] - 200)
self.sleep(0.15)
self.input_text(content, clear=False)
self.sleep(0.15)
if not self.click_text("发送", timeout=2):
self.d.xpath('//*[@text="发送"]').click_exists(timeout=2)
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) -> Dict[str, Any]:
"""
获取消息列表
Args:
limit: 消息数量限制
conversation_id: 会话ID联系人名称如果指定则获取该会话的消息
Returns:
消息列表
"""
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]:
"""添加好友"""
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) -> Dict[str, Any]:
"""通过好友请求"""
try:
self.launch()
self.sleep(2)
# 通讯录 -> 新的朋友
self.click_text("通讯录")
self.sleep(1)
self.click_text("新的朋友")
self.sleep(1)
# 找到并通过
if user_id:
if self.exists(user_id):
# 找到特定用户的接受按钮
pass
# 通过第一个
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) -> Dict[str, Any]:
"""
删除好友
Args:
user_id: 好友名称
"""
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)
# 点击头像进入资料页
# TODO: 点击头像
# 更多 -> 删除
self.click_text("...") or self.click_desc("更多")
self.sleep(0.5)
self.click_text("删除")
self.sleep(0.3)
# 确认删除
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)}