2760 lines
115 KiB
Python
2760 lines
115 KiB
Python
"""
|
||
微信 ADB 后端静默操作引擎 v2.0
|
||
通过 ADB + UI 自动化完整控制微信的所有功能(无需 Root)
|
||
适配微信 8.0.56 版本
|
||
|
||
功能覆盖:
|
||
1. 消息管理 - 发送/获取/转发/撤回/名片/文件
|
||
2. 好友管理 - 添加/通过/备注/删除/搜索/批量
|
||
3. 群聊管理 - 创建/邀请/移除/公告/群名/群消息/退群
|
||
4. 标签管理 - 创建/删除/列表/打标/取标/按标签查人
|
||
5. 朋友圈 - 发布/点赞/评论/获取/删除
|
||
6. 个人设置 - 昵称/签名/头像/性别/地区/资料
|
||
7. 账号安全 - 状态检查/解封/安全中心/改密码
|
||
8. 收藏管理 - 收藏/列表
|
||
9. 支付/红包 - 发红包/转账
|
||
10. 聊天设置 - 置顶/免打扰/清空记录
|
||
11. 小程序 - 打开
|
||
12. 公众号 - 关注
|
||
13. 解封完善 - 自助解封/申诉/限制检查/短信解封
|
||
14. 视频号 - 打开/点赞/评论/关注/分享/列表
|
||
15. 扫一扫 - 扫码/加好友/我的二维码/图片识别
|
||
16. 支付完善 - 付款码/收款码/钱包/账单/领红包
|
||
17. 通话 - 语音通话/视频通话
|
||
18. 群发助手 - 群发消息
|
||
19. 搜一搜/看一看 - 搜一搜/看一看
|
||
20. 微信运动 - 获取步数/点赞步数
|
||
21. 位置分享 - 发送位置/共享实时位置
|
||
22. 表情管理 - 发送表情/表情包列表
|
||
23. 朋友圈增强 - 设置封面/可见天数/分享链接
|
||
24. 语音消息 - 发送语音消息
|
||
25. 文件管理 - 发送文件/下载文件
|
||
26. 设置管理 - 勿扰模式/清理缓存/检查更新/退出/切换账号
|
||
|
||
全局容错: _retry / _recover_state / _verify_action
|
||
"""
|
||
|
||
import time
|
||
import re
|
||
import logging
|
||
import subprocess
|
||
import base64
|
||
from typing import Dict, Any, List, Optional, Tuple
|
||
|
||
logger = logging.getLogger(__name__)
|
||
|
||
WX_PKG = "com.tencent.mm"
|
||
WX_LAUNCHER = f"{WX_PKG}/.ui.LauncherUI"
|
||
WX_SEARCH = f"{WX_PKG}/.plugin.subapp.ui.friend.FTSMainUI"
|
||
|
||
|
||
class WeChatADBEngine:
|
||
"""微信 ADB 后端静默操作引擎 — 全功能(带导航缓存 & 操作日志)"""
|
||
|
||
def __init__(self, device):
|
||
self.d = device
|
||
self._w = 1080
|
||
self._h = 2400
|
||
self._detect_screen()
|
||
self._init_nav_cache()
|
||
|
||
def _init_nav_cache(self):
|
||
"""初始化导航缓存和操作日志"""
|
||
try:
|
||
from services.wechat_nav_cache import WeChatNavCache, OperationLogger
|
||
self.nav = WeChatNavCache("8.0.56", self._w, self._h)
|
||
self.op_log = OperationLogger(
|
||
getattr(self.d, 'serial', getattr(self.d, 'device_id', '')))
|
||
except Exception as e:
|
||
logger.warning(f"导航缓存初始化失败(降级为无缓存模式): {e}")
|
||
self.nav = None
|
||
self.op_log = None
|
||
|
||
# ═══════════════════ 底层工具方法 ═══════════════════
|
||
|
||
def _detect_screen(self):
|
||
try:
|
||
size = self.d._shell("wm size")
|
||
if "Physical size:" in size:
|
||
dims = size.split("Physical size:")[-1].strip()
|
||
if "x" in dims:
|
||
self._w, self._h = map(int, dims.split("x"))
|
||
except Exception:
|
||
pass
|
||
|
||
def _sh(self, cmd: str, timeout: int = 10) -> str:
|
||
return self.d._shell(cmd, timeout=timeout)
|
||
|
||
def _tap(self, x: int, y: int):
|
||
self._sh(f"input tap {x} {y}")
|
||
time.sleep(0.3)
|
||
|
||
def _swipe(self, x1, y1, x2, y2, ms=400):
|
||
self._sh(f"input swipe {x1} {y1} {x2} {y2} {ms}")
|
||
time.sleep(0.5)
|
||
|
||
def _scroll_up(self):
|
||
cx = self._w // 2
|
||
self._swipe(cx, self._h * 3 // 4, cx, self._h // 4)
|
||
|
||
def _scroll_down(self):
|
||
cx = self._w // 2
|
||
self._swipe(cx, self._h // 4, cx, self._h * 3 // 4)
|
||
|
||
def _back(self):
|
||
self._sh("input keyevent KEYCODE_BACK")
|
||
time.sleep(0.3)
|
||
|
||
def _enter(self):
|
||
self._sh("input keyevent KEYCODE_ENTER")
|
||
time.sleep(0.3)
|
||
|
||
def _long_press(self, x, y, duration_ms=1000):
|
||
self._swipe(x, y, x, y, duration_ms)
|
||
|
||
def _wait(self, sec: float = 1.0):
|
||
time.sleep(sec)
|
||
|
||
def _get_xml(self) -> str:
|
||
try:
|
||
self._sh("uiautomator dump /sdcard/ui.xml", timeout=5)
|
||
return self._sh("cat /sdcard/ui.xml", timeout=3)
|
||
except Exception:
|
||
return ""
|
||
|
||
def _find_text(self, text: str, xml: str = None) -> Optional[Tuple[int, int]]:
|
||
# 优先从实例导航缓存读取(勿用全局 nav_cache.get_page — 无此方法)
|
||
if self.nav:
|
||
for page_name in ("chat", "home", "search", "contacts", "current"):
|
||
coord = self.nav.get_element_coord(page_name, text)
|
||
if coord:
|
||
return tuple(coord)
|
||
tab = self.nav.get_tab_coord(text)
|
||
if tab:
|
||
return tuple(tab)
|
||
if xml is None:
|
||
xml = self._get_xml()
|
||
if not xml:
|
||
return None
|
||
for pat in [
|
||
f'text="{re.escape(text)}"[^>]*bounds="\\[(\\d+),(\\d+)\\]\\[(\\d+),(\\d+)\\]"',
|
||
f'content-desc="{re.escape(text)}"[^>]*bounds="\\[(\\d+),(\\d+)\\]\\[(\\d+),(\\d+)\\]"',
|
||
f'text="[^"]*{re.escape(text)}[^"]*"[^>]*bounds="\\[(\\d+),(\\d+)\\]\\[(\\d+),(\\d+)\\]"',
|
||
]:
|
||
m = re.search(pat, xml)
|
||
if m:
|
||
x1, y1, x2, y2 = map(int, m.groups())
|
||
return ((x1 + x2) // 2, (y1 + y2) // 2)
|
||
return None
|
||
|
||
def _click_text(self, text: str, timeout: float = 5) -> bool:
|
||
end = time.time() + timeout
|
||
while time.time() < end:
|
||
pos = self._find_text(text)
|
||
if pos:
|
||
self._tap(*pos)
|
||
return True
|
||
time.sleep(0.5)
|
||
return False
|
||
|
||
def _input(self, text: str, clear: bool = False):
|
||
if clear:
|
||
try:
|
||
self._sh("input keyevent KEYCODE_CTRL_A", timeout=2)
|
||
time.sleep(0.1)
|
||
self._sh("input keyevent KEYCODE_DEL", timeout=2)
|
||
time.sleep(0.1)
|
||
except Exception as e:
|
||
logger.info(f"清空输入框失败,继续输入: {e}")
|
||
has_cjk = any(ord(c) > 127 for c in text)
|
||
if has_cjk:
|
||
self._input_cjk(text)
|
||
else:
|
||
escaped = text.replace(" ", "%s").replace("'", "\\'").replace('"', '\\"')
|
||
escaped = escaped.replace("&", "\\&").replace("|", "\\|").replace(";", "\\;")
|
||
self._sh(f"input text '{escaped}'")
|
||
time.sleep(0.3)
|
||
|
||
def _input_cjk(self, text: str):
|
||
escaped = text.replace("'", "'\\''").replace('"', '\\"')
|
||
try:
|
||
result = self._sh(
|
||
f"am broadcast -a ADB_INPUT_TEXT --es msg '{escaped}' 2>&1",
|
||
timeout=5,
|
||
)
|
||
if "Broadcast completed" in result:
|
||
return
|
||
except Exception as e:
|
||
logger.info(f"ADB_INPUT_TEXT 不可用,回退系统输入: {e}")
|
||
|
||
try:
|
||
self._sh(f"input text '{escaped}'", timeout=8)
|
||
except Exception as e:
|
||
logger.warning(f"input text 也超时,尝试逐字符输入: {e}")
|
||
for ch in text:
|
||
try:
|
||
self._sh(f"am broadcast -a ADB_INPUT_TEXT --es msg '{ch}' 2>&1", timeout=3)
|
||
time.sleep(0.05)
|
||
except Exception:
|
||
pass
|
||
|
||
def _ensure_device_awake_unlocked(self) -> dict:
|
||
"""微信控制前的亮屏/解锁兜底,避免锁屏导致黑屏或点击落空。"""
|
||
state = {"actions": []}
|
||
try:
|
||
if hasattr(self.d, "_ensure_awake_unlocked"):
|
||
return self.d._ensure_awake_unlocked()
|
||
power = self._sh("dumpsys power | grep -E 'Display Power|mWakefulness|mHoldingDisplaySuspendBlocker'", timeout=5)
|
||
window = self._sh("dumpsys window | grep -E 'mDreamingLockscreen|mShowingLockscreen|mCurrentFocus'", timeout=5)
|
||
state["before_power"] = power.replace("\n", " | ")[:500]
|
||
state["before_window"] = window.replace("\n", " | ")[:500]
|
||
if "state=OFF" in power or "Asleep" in power or "mWakefulness=Asleep" in power:
|
||
self._sh("input keyevent KEYCODE_WAKEUP", timeout=3)
|
||
state["actions"].append("KEYCODE_WAKEUP")
|
||
time.sleep(0.6)
|
||
self._sh("wm dismiss-keyguard", timeout=3)
|
||
state["actions"].append("wm dismiss-keyguard")
|
||
time.sleep(0.3)
|
||
self._sh("input keyevent KEYCODE_MENU", timeout=3)
|
||
state["actions"].append("KEYCODE_MENU")
|
||
time.sleep(0.3)
|
||
window2 = self._sh("dumpsys window | grep -E 'mDreamingLockscreen|mShowingLockscreen|mCurrentFocus'", timeout=5)
|
||
state["after_window"] = window2.replace("\n", " | ")[:500]
|
||
state["success"] = "mDreamingLockscreen=true" not in window2
|
||
except Exception as e:
|
||
state["success"] = False
|
||
state["error"] = str(e)
|
||
return state
|
||
|
||
def _ensure_wechat(self) -> bool:
|
||
wake_state = self._ensure_device_awake_unlocked()
|
||
if wake_state.get("success") is False:
|
||
logger.warning(f"设备亮屏/解锁未确认成功,继续尝试拉起微信: {wake_state}")
|
||
cur = self._sh("dumpsys window | grep -E 'mCurrentFocus|mDreamingLockscreen|mShowingLockscreen'", timeout=5)
|
||
if "NotificationShade" in cur:
|
||
self._sh("input swipe 540 1200 540 100 200", timeout=3)
|
||
time.sleep(0.5)
|
||
self._sh("input keyevent KEYCODE_HOME", timeout=3)
|
||
time.sleep(0.5)
|
||
if WX_PKG not in cur or "mDreamingLockscreen=true" in cur:
|
||
self._sh(f"am start -n {WX_LAUNCHER}", timeout=10)
|
||
time.sleep(2)
|
||
cur2 = self._sh("dumpsys window | grep -E 'mCurrentFocus'", timeout=5)
|
||
return WX_PKG in cur2
|
||
|
||
def _go_home(self):
|
||
self._sh(f"am start -n {WX_LAUNCHER}")
|
||
time.sleep(1.5)
|
||
|
||
def _tab_positions(self) -> dict:
|
||
"""底部 4 个 tab 的中心坐标(优先从缓存取精确坐标)"""
|
||
if self.nav:
|
||
tabs = self.nav.element_cache.get("tabs", {})
|
||
if tabs:
|
||
return {k: tuple(v["center"]) for k, v in tabs.items()}
|
||
tw = self._w // 4
|
||
ty = self._h - 252
|
||
return {
|
||
"微信": (tw // 2, ty),
|
||
"通讯录": (tw + tw // 2, ty),
|
||
"发现": (tw * 2 + tw // 2, ty),
|
||
"我": (tw * 3 + tw // 2, ty),
|
||
}
|
||
|
||
def _tap_tab(self, name: str):
|
||
"""点击微信底部 tab(缓存优先)"""
|
||
if self.nav:
|
||
coord = self.nav.get_tab_coord(name)
|
||
if coord:
|
||
self._tap(*coord)
|
||
time.sleep(1)
|
||
if self.op_log:
|
||
self.op_log.log_step("tap_tab", name, coord)
|
||
return
|
||
pos = self._tab_positions().get(name)
|
||
if pos:
|
||
self._tap(*pos)
|
||
time.sleep(1)
|
||
|
||
def _navigate_cached(self, path_name: str) -> bool:
|
||
"""用缓存的路径快速导航到目标页面"""
|
||
if not self.nav:
|
||
return False
|
||
path = self.nav.get_path(path_name)
|
||
if not path:
|
||
return False
|
||
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
time.sleep(0.5)
|
||
|
||
for step in path.steps:
|
||
try:
|
||
if step.action == "tap_tab":
|
||
self._tap_tab(step.target)
|
||
elif step.action == "tap_text":
|
||
if step.coords:
|
||
self._tap(*step.coords)
|
||
elif not self._click_text(step.target, timeout=3):
|
||
self.nav.record_path_result(path_name, False)
|
||
return False
|
||
elif step.action == "tap_coord":
|
||
if step.coords:
|
||
self._tap(*step.coords)
|
||
elif step.action == "scroll_up":
|
||
self._scroll_up()
|
||
elif step.action == "scroll_down":
|
||
self._scroll_down()
|
||
elif step.action == "scroll_to_top":
|
||
for _ in range(3):
|
||
self._swipe(self._w // 2, 400, self._w // 2, 1600, 200)
|
||
time.sleep(0.2)
|
||
elif step.action == "back":
|
||
self._back()
|
||
elif step.action == "swipe":
|
||
pass
|
||
|
||
time.sleep(step.wait)
|
||
|
||
if self.op_log:
|
||
self.op_log.log_step(
|
||
step.action, step.target or "",
|
||
step.coords, True)
|
||
except Exception as e:
|
||
logger.warning(f"导航步骤失败 [{step.action}]: {e}")
|
||
self.nav.record_path_result(path_name, False)
|
||
return False
|
||
|
||
self.nav.record_path_result(path_name, True)
|
||
return True
|
||
|
||
def _record_page_elements(self, page_name: str) -> Dict[str, Any]:
|
||
"""录制当前页面所有元素到缓存"""
|
||
xml = self._get_xml()
|
||
items = re.findall(
|
||
r'text="([^"]+)"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"', xml)
|
||
elements = {}
|
||
for t, x1, y1, x2, y2 in items:
|
||
if t.strip() and int(y1) > 50 and int(y1) < 2100:
|
||
cx = (int(x1) + int(x2)) // 2
|
||
cy = (int(y1) + int(y2)) // 2
|
||
elements[t] = {"center": [cx, cy],
|
||
"bounds": [int(x1), int(y1), int(x2), int(y2)]}
|
||
if self.nav and elements:
|
||
self.nav.record_page(page_name, elements)
|
||
return elements
|
||
|
||
def _search_and_enter(self, keyword: str) -> bool:
|
||
self._go_home()
|
||
time.sleep(0.5)
|
||
if not self._click_text("搜索", timeout=3):
|
||
self._sh(f"am start -n {WX_SEARCH}")
|
||
time.sleep(1)
|
||
time.sleep(0.5)
|
||
self._input(keyword, clear=True)
|
||
time.sleep(1.5)
|
||
xml = self._get_xml()
|
||
pos = self._find_text(keyword, xml)
|
||
if pos:
|
||
self._tap(*pos)
|
||
time.sleep(1)
|
||
return True
|
||
self._tap(self._w // 2, 350)
|
||
time.sleep(1)
|
||
return True
|
||
|
||
def _ok(self, **kw) -> dict:
|
||
r = {"success": True, "mode": "adb", "timestamp": int(time.time() * 1000)}
|
||
r.update(kw)
|
||
return r
|
||
|
||
def _fail(self, msg: str) -> dict:
|
||
return {"success": False, "error": msg, "mode": "adb"}
|
||
|
||
def _retry(self, fn, max_retries=3, delay=1.0):
|
||
"""通用重试包装器"""
|
||
for i in range(max_retries):
|
||
try:
|
||
result = fn()
|
||
if isinstance(result, dict) and result.get("success"):
|
||
return result
|
||
if isinstance(result, dict) and not result.get("success") and i < max_retries - 1:
|
||
logger.warning(f"操作失败,重试 {i+1}/{max_retries}: {result.get('error')}")
|
||
self._recover_state()
|
||
time.sleep(delay)
|
||
continue
|
||
return result
|
||
except Exception as e:
|
||
if i < max_retries - 1:
|
||
logger.warning(f"异常,重试 {i+1}/{max_retries}: {e}")
|
||
self._recover_state()
|
||
time.sleep(delay)
|
||
else:
|
||
return self._fail(f"重试{max_retries}次后仍失败: {e}")
|
||
return self._fail("重试耗尽")
|
||
|
||
def _recover_state(self):
|
||
"""状态恢复:关闭弹窗/对话框,回到微信主界面"""
|
||
try:
|
||
self._click_text("取消", timeout=1)
|
||
except Exception:
|
||
pass
|
||
try:
|
||
self._click_text("关闭", timeout=1)
|
||
except Exception:
|
||
pass
|
||
self._back()
|
||
time.sleep(0.3)
|
||
self._back()
|
||
time.sleep(0.3)
|
||
self._go_home()
|
||
|
||
def _verify_action(self, expected_text: str = None) -> bool:
|
||
"""验证操作是否成功(通过检查当前页面)"""
|
||
xml = self._get_xml()
|
||
if expected_text:
|
||
return self._find_text(expected_text, xml) is not None
|
||
return len(xml) > 100
|
||
|
||
# ═══════════════════ 1. 消息管理 ═══════════════════
|
||
|
||
def send_message(self, to_id: str, content: str, msg_type: str = "text",
|
||
media_url: str = None, at_list: list = None) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
if not self._search_and_enter(to_id):
|
||
return self._fail(f"未找到联系人: {to_id}")
|
||
time.sleep(0.5)
|
||
if msg_type == "image" and media_url:
|
||
return self._send_media_in_chat("相册")
|
||
if msg_type == "file" and media_url:
|
||
return self._send_media_in_chat("文件")
|
||
msg = content
|
||
if at_list:
|
||
msg = " ".join(f"@{a}" for a in at_list) + " " + content
|
||
self._input(msg)
|
||
time.sleep(0.3)
|
||
if not self._click_text("发送", timeout=3):
|
||
self._enter()
|
||
return self._ok(message_id=f"adb_{int(time.time()*1000)}")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def _send_media_in_chat(self, media_type: str) -> dict:
|
||
self._click_text("+") or self._tap(self._w - 60, self._h - 180)
|
||
time.sleep(0.5)
|
||
self._click_text(media_type) or self._click_text("图片")
|
||
time.sleep(1)
|
||
self._tap(self._w // 4, self._h // 3)
|
||
time.sleep(0.5)
|
||
self._click_text("发送") or self._click_text("完成")
|
||
return self._ok(message_id=f"media_{int(time.time()*1000)}")
|
||
|
||
def get_messages(self, conversation_id: str = None, limit: int = 20) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
if conversation_id:
|
||
self._search_and_enter(conversation_id)
|
||
time.sleep(0.5)
|
||
xml = self._get_xml()
|
||
messages = self._parse_chat_messages(xml, limit)
|
||
return self._ok(messages=messages, count=len(messages))
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def _parse_chat_messages(self, xml: str, limit: int) -> list:
|
||
msgs = []
|
||
if not xml:
|
||
return msgs
|
||
all_texts = re.findall(r'text="([^"]{2,})"', xml)
|
||
skip = {"微信", "搜索", "发送", "返回", "更多", "+", "语音", "表情",
|
||
"切换到键盘", "按住说话", "通讯录", "发现", "我"}
|
||
for t in all_texts:
|
||
if t not in skip:
|
||
msgs.append({"content": t, "timestamp": int(time.time())})
|
||
if len(msgs) >= limit:
|
||
break
|
||
return msgs
|
||
|
||
def forward_message(self, to_id: str, content: str) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
xml = self._get_xml()
|
||
pos = self._find_text(content, xml)
|
||
if pos:
|
||
self._long_press(*pos)
|
||
time.sleep(0.5)
|
||
if self._click_text("转发", timeout=3):
|
||
time.sleep(0.5)
|
||
self._input(to_id, clear=True)
|
||
time.sleep(1)
|
||
self._click_text(to_id, timeout=3)
|
||
time.sleep(0.5)
|
||
self._click_text("发送", timeout=3)
|
||
return self._ok()
|
||
return self._fail("未找到要转发的消息")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def recall_message(self) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
xml = self._get_xml()
|
||
all_msgs = re.findall(
|
||
r'text="([^"]+)"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"', xml
|
||
)
|
||
for text, x1, y1, x2, y2 in reversed(all_msgs):
|
||
if int(x1) > self._w // 3:
|
||
cx, cy = (int(x1) + int(x2)) // 2, (int(y1) + int(y2)) // 2
|
||
self._long_press(cx, cy)
|
||
time.sleep(0.5)
|
||
if self._click_text("撤回", timeout=3):
|
||
time.sleep(0.5)
|
||
self._click_text("确定") or self._click_text("确认")
|
||
return self._ok()
|
||
break
|
||
return self._fail("未找到可撤回的消息")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def send_card(self, to_id: str, card_wxid: str) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(to_id)
|
||
self._click_text("+") or self._tap(self._w - 60, self._h - 180)
|
||
time.sleep(0.5)
|
||
if not self._click_text("名片", timeout=2):
|
||
self._scroll_up()
|
||
self._click_text("名片", timeout=2)
|
||
time.sleep(1)
|
||
self._input(card_wxid, clear=True)
|
||
time.sleep(1)
|
||
self._click_text(card_wxid, timeout=3)
|
||
time.sleep(0.5)
|
||
self._click_text("发送", timeout=3)
|
||
return self._ok()
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 2. 好友管理 ═══════════════════
|
||
|
||
def add_friend(self, user_id: str, message: str = "", source: str = None) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(0.5)
|
||
self._click_text("添加朋友", timeout=3)
|
||
time.sleep(1)
|
||
self._click_text("微信号/手机号", timeout=3) or self._tap(self._w // 2, 200)
|
||
time.sleep(0.3)
|
||
self._input(user_id, clear=True)
|
||
time.sleep(0.5)
|
||
self._click_text("搜索", timeout=3) or self._enter()
|
||
time.sleep(2)
|
||
if self._click_text("添加到通讯录", timeout=3):
|
||
time.sleep(1)
|
||
if message:
|
||
self._input(message, clear=True)
|
||
self._click_text("发送", timeout=3)
|
||
return self._ok(note=f"好友请求已发送: {user_id}")
|
||
return self._fail(f"未找到用户或已是好友: {user_id}")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def accept_friend(self, user_id: str = None) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("通讯录")
|
||
time.sleep(0.5)
|
||
self._click_text("新的朋友", timeout=3)
|
||
time.sleep(1)
|
||
if user_id:
|
||
self._click_text(user_id, timeout=2)
|
||
time.sleep(0.5)
|
||
if self._click_text("接受", timeout=3) or self._click_text("通过", timeout=2):
|
||
return self._ok(note="好友请求已通过")
|
||
return self._fail("未找到待处理的好友请求")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def set_remark(self, user_id: str, remark: str) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(user_id)
|
||
time.sleep(0.5)
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(1)
|
||
if not self._click_text("备注", timeout=3):
|
||
self._click_text("设置备注", timeout=2)
|
||
time.sleep(0.5)
|
||
self._input(remark, clear=True)
|
||
time.sleep(0.3)
|
||
self._click_text("完成") or self._click_text("保存") or self._click_text("确定")
|
||
return self._ok(note=f"备注已设为: {remark}")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def delete_friend(self, user_id: str) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(user_id)
|
||
time.sleep(0.5)
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(1)
|
||
self._tap(self._w // 2, 200)
|
||
time.sleep(1)
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(0.5)
|
||
self._click_text("删除", timeout=3)
|
||
time.sleep(0.5)
|
||
self._click_text("删除", timeout=3)
|
||
return self._ok(note=f"已删除好友: {user_id}")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def get_contacts(self, limit: int = 100) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("通讯录")
|
||
time.sleep(1)
|
||
contacts, seen = [], set()
|
||
for _ in range(min(limit // 10, 20)):
|
||
xml = self._get_xml()
|
||
texts = re.findall(r'text="([^"]{2,15})"', xml)
|
||
skip = {"微信", "通讯录", "发现", "我", "搜索", "新的朋友", "群聊",
|
||
"标签", "公众号", "返回", "添加"}
|
||
for n in texts:
|
||
if n not in skip and n not in seen:
|
||
seen.add(n)
|
||
contacts.append({"nickname": n})
|
||
if len(contacts) >= limit:
|
||
break
|
||
self._scroll_up()
|
||
time.sleep(0.5)
|
||
return self._ok(contacts=contacts[:limit], count=len(contacts[:limit]))
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def search_contact(self, keyword: str) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._click_text("搜索", timeout=3) or self._sh(f"am start -n {WX_SEARCH}")
|
||
time.sleep(0.5)
|
||
self._input(keyword, clear=True)
|
||
time.sleep(1.5)
|
||
xml = self._get_xml()
|
||
results = []
|
||
texts = re.findall(r'text="([^"]+)"', xml)
|
||
skip = {"搜索", "返回", keyword, "联系人", "群聊", "聊天记录", "公众号",
|
||
"朋友圈", "文章", "小程序"}
|
||
for n in texts:
|
||
if n not in skip and len(n) > 1:
|
||
results.append({"nickname": n})
|
||
self._back()
|
||
return self._ok(results=results, count=len(results))
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def batch_add_friend(self, user_ids: list, message: str = "", interval: float = 5) -> dict:
|
||
added, failed = [], []
|
||
for uid in user_ids:
|
||
r = self.add_friend(uid, message)
|
||
if r.get("success"):
|
||
added.append(uid)
|
||
else:
|
||
failed.append({"user_id": uid, "error": r.get("error")})
|
||
time.sleep(max(interval, 3))
|
||
return self._ok(added=added, failed=failed, total=len(user_ids))
|
||
|
||
def get_friend_info(self, user_id: str) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(user_id)
|
||
time.sleep(0.5)
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(1)
|
||
self._tap(self._w // 2, 200)
|
||
time.sleep(1)
|
||
xml = self._get_xml()
|
||
info = {"user_id": user_id}
|
||
texts = re.findall(r'text="([^"]+)"', xml)
|
||
for t in texts:
|
||
if t.startswith("微信号"):
|
||
info["wechat_id"] = t.replace("微信号:", "").replace("微信号:", "").strip()
|
||
elif t.startswith("地区"):
|
||
info["region"] = t.replace("地区:", "").replace("地区:", "").strip()
|
||
if texts:
|
||
for t in texts:
|
||
if t not in {"返回", "更多", "发消息", "音视频通话"} and len(t) > 1:
|
||
info.setdefault("nickname", t)
|
||
break
|
||
self._back()
|
||
return self._ok(info=info)
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 3. 群聊管理 ═══════════════════
|
||
|
||
def create_group(self, group_name: str, member_ids: list) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(0.5)
|
||
self._click_text("发起群聊", timeout=3)
|
||
time.sleep(1)
|
||
for mid in member_ids:
|
||
self._click_text("搜索", timeout=2) or self._tap(self._w // 2, 150)
|
||
self._input(mid, clear=True)
|
||
time.sleep(1)
|
||
pos = self._find_text(mid)
|
||
if pos:
|
||
self._tap(*pos)
|
||
time.sleep(0.3)
|
||
self._back()
|
||
time.sleep(0.3)
|
||
self._click_text("完成", timeout=3) or self._click_text("确定", timeout=2)
|
||
time.sleep(2)
|
||
if group_name:
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(1)
|
||
if self._click_text("群聊名称", timeout=3) or self._click_text("群名称", timeout=2):
|
||
time.sleep(0.5)
|
||
self._input(group_name, clear=True)
|
||
self._click_text("完成") or self._click_text("保存") or self._click_text("确定")
|
||
return self._ok(note=f"群聊已创建: {group_name}")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def invite_to_group(self, group_id: str, member_ids: list) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(group_id)
|
||
time.sleep(0.5)
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(1)
|
||
self._tap(self._w - 100, 350)
|
||
time.sleep(0.5)
|
||
for mid in member_ids:
|
||
self._input(mid, clear=True)
|
||
time.sleep(1)
|
||
pos = self._find_text(mid)
|
||
if pos:
|
||
self._tap(*pos)
|
||
time.sleep(0.3)
|
||
self._click_text("邀请") or self._click_text("完成") or self._click_text("确定")
|
||
return self._ok(note=f"已邀请 {len(member_ids)} 人入群")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def remove_from_group(self, group_id: str, member_ids: list) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(group_id)
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(1)
|
||
if not self._click_text("-", timeout=2):
|
||
self._scroll_up()
|
||
self._click_text("删除", timeout=2)
|
||
time.sleep(0.5)
|
||
for mid in member_ids:
|
||
pos = self._find_text(mid)
|
||
if pos:
|
||
self._tap(*pos)
|
||
time.sleep(0.3)
|
||
self._click_text("删除") or self._click_text("完成") or self._click_text("确定")
|
||
time.sleep(0.5)
|
||
self._click_text("确定") or self._click_text("确认")
|
||
return self._ok(note=f"已移除 {len(member_ids)} 人")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def set_group_notice(self, group_id: str, notice: str) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(group_id)
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(1)
|
||
self._scroll_up()
|
||
time.sleep(0.3)
|
||
self._click_text("群公告", timeout=3)
|
||
time.sleep(1)
|
||
self._input(notice, clear=True)
|
||
time.sleep(0.3)
|
||
self._click_text("完成") or self._click_text("发布")
|
||
time.sleep(0.5)
|
||
self._click_text("确定") or self._click_text("确认")
|
||
return self._ok(note="群公告已更新")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def set_group_name(self, group_id: str, group_name: str) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(group_id)
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(1)
|
||
if self._click_text("群聊名称", timeout=3) or self._click_text("群名称", timeout=2):
|
||
time.sleep(0.5)
|
||
self._input(group_name, clear=True)
|
||
self._click_text("完成") or self._click_text("保存")
|
||
return self._ok(note=f"群名已改为: {group_name}")
|
||
return self._fail("未找到群名称设置入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def send_group_message(self, group_id: str, content: str, msg_type: str = "text",
|
||
media_url: str = None, at_all: bool = False, at_list: list = None) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(group_id)
|
||
time.sleep(0.5)
|
||
msg = content
|
||
if at_all:
|
||
msg = "@所有人 " + content
|
||
elif at_list:
|
||
msg = " ".join(f"@{a}" for a in at_list) + " " + content
|
||
self._input(msg)
|
||
time.sleep(0.3)
|
||
self._click_text("发送", timeout=3) or self._enter()
|
||
return self._ok(message_id=f"group_{int(time.time()*1000)}")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def set_group_welcome(self, group_id: str, welcome_text: str, welcome_image: str = None) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(group_id)
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(1)
|
||
self._scroll_up()
|
||
if self._click_text("群管理", timeout=3):
|
||
time.sleep(0.5)
|
||
if self._click_text("进群欢迎语", timeout=3) or self._click_text("欢迎语", timeout=2):
|
||
time.sleep(0.5)
|
||
self._input(welcome_text, clear=True)
|
||
self._click_text("完成") or self._click_text("保存")
|
||
return self._ok(note="群欢迎语已设置")
|
||
return self._fail("未找到群欢迎语设置")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def get_groups(self, limit: int = 100) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("通讯录")
|
||
time.sleep(0.5)
|
||
self._click_text("群聊", timeout=3)
|
||
time.sleep(1)
|
||
groups, seen = [], set()
|
||
for _ in range(min(limit // 8, 15)):
|
||
xml = self._get_xml()
|
||
names = re.findall(r'text="([^"]{2,30})"', xml)
|
||
skip = {"群聊", "通讯录", "搜索", "返回"}
|
||
for n in names:
|
||
if n not in skip and n not in seen:
|
||
seen.add(n)
|
||
groups.append({"group_name": n})
|
||
if len(groups) >= limit:
|
||
break
|
||
self._scroll_up()
|
||
time.sleep(0.5)
|
||
return self._ok(groups=groups[:limit], count=len(groups[:limit]))
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def get_group_members(self, group_id: str) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(group_id)
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(1)
|
||
self._click_text("查看全部", timeout=3) or self._click_text("全部群成员", timeout=2)
|
||
time.sleep(1)
|
||
members, seen = [], set()
|
||
for _ in range(10):
|
||
xml = self._get_xml()
|
||
names = re.findall(r'text="([^"]{2,20})"', xml)
|
||
skip = {"群成员", "搜索", "返回", group_id, "邀请"}
|
||
for n in names:
|
||
if n not in skip and n not in seen:
|
||
seen.add(n)
|
||
members.append({"nickname": n})
|
||
self._scroll_up()
|
||
time.sleep(0.3)
|
||
return self._ok(members=members, count=len(members))
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def quit_group(self, group_id: str) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(group_id)
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(1)
|
||
for _ in range(5):
|
||
self._scroll_up()
|
||
time.sleep(0.3)
|
||
if self._click_text("删除并退出", timeout=3) or self._click_text("退出群聊", timeout=2):
|
||
time.sleep(0.5)
|
||
self._click_text("确定") or self._click_text("确认")
|
||
return self._ok(note=f"已退出群: {group_id}")
|
||
return self._fail("未找到退出群聊按钮")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 4. 标签管理 ═══════════════════
|
||
|
||
def create_tag(self, tag_name: str) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("通讯录")
|
||
time.sleep(0.5)
|
||
self._click_text("标签", timeout=3)
|
||
time.sleep(1)
|
||
self._click_text("新建标签", timeout=3) or self._tap(self._w // 2, self._h - 100)
|
||
time.sleep(0.5)
|
||
self._input(tag_name, clear=True)
|
||
self._click_text("确定") or self._click_text("保存")
|
||
return self._ok(note=f"标签已创建: {tag_name}")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def delete_tag(self, tag_name: str) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("通讯录")
|
||
time.sleep(0.5)
|
||
self._click_text("标签", timeout=3)
|
||
time.sleep(1)
|
||
if self._click_text(tag_name, timeout=3):
|
||
time.sleep(0.5)
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(0.5)
|
||
self._click_text("删除标签", timeout=3)
|
||
time.sleep(0.5)
|
||
self._click_text("确定") or self._click_text("确认")
|
||
return self._ok(note=f"标签已删除: {tag_name}")
|
||
return self._fail(f"未找到标签: {tag_name}")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def get_tags(self) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("通讯录")
|
||
time.sleep(0.5)
|
||
self._click_text("标签", timeout=3)
|
||
time.sleep(1)
|
||
xml = self._get_xml()
|
||
tags = []
|
||
names = re.findall(r'text="([^"]+)"', xml)
|
||
skip = {"标签", "新建标签", "通讯录", "搜索", "返回"}
|
||
for n in names:
|
||
if n not in skip and len(n) > 0:
|
||
tags.append({"tag_name": n})
|
||
return self._ok(tags=tags, count=len(tags))
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def add_tag(self, user_id: str, tags: list) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(user_id)
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(1)
|
||
self._tap(self._w // 2, 200)
|
||
time.sleep(1)
|
||
if self._click_text("标签", timeout=3) or self._click_text("设置备注和标签", timeout=2):
|
||
time.sleep(0.5)
|
||
for tag in tags:
|
||
if not self._click_text(tag, timeout=2):
|
||
self._click_text("添加标签", timeout=2)
|
||
self._input(tag)
|
||
self._enter()
|
||
time.sleep(0.3)
|
||
self._click_text("完成") or self._click_text("保存") or self._click_text("确定")
|
||
return self._ok(note=f"已为 {user_id} 添加标签: {tags}")
|
||
return self._fail("未找到标签设置入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def remove_tag(self, user_id: str, tags: list) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(user_id)
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(1)
|
||
self._tap(self._w // 2, 200)
|
||
time.sleep(1)
|
||
if self._click_text("标签", timeout=3):
|
||
time.sleep(0.5)
|
||
for tag in tags:
|
||
self._click_text(tag, timeout=2)
|
||
time.sleep(0.2)
|
||
self._click_text("完成") or self._click_text("保存")
|
||
return self._ok(note=f"已为 {user_id} 移除标签: {tags}")
|
||
return self._fail("未找到标签设置入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def get_users_by_tag(self, tag_name: str, limit: int = 100) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("通讯录")
|
||
time.sleep(0.5)
|
||
self._click_text("标签", timeout=3)
|
||
time.sleep(1)
|
||
if self._click_text(tag_name, timeout=3):
|
||
time.sleep(1)
|
||
xml = self._get_xml()
|
||
users = []
|
||
names = re.findall(r'text="([^"]+)"', xml)
|
||
skip = {tag_name, "标签", "返回", "更多"}
|
||
for n in names:
|
||
if n not in skip and len(n) > 1:
|
||
users.append({"nickname": n})
|
||
return self._ok(users=users[:limit], count=len(users[:limit]))
|
||
return self._fail(f"未找到标签: {tag_name}")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 5. 朋友圈管理 ═══════════════════
|
||
|
||
def post_moments(self, content: str, images: list = None, video_url: str = None,
|
||
location: str = None, visible_list: list = None,
|
||
invisible_list: list = None) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("发现")
|
||
time.sleep(0.5)
|
||
self._click_text("朋友圈", timeout=3)
|
||
time.sleep(1.5)
|
||
|
||
if images or video_url:
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(0.5)
|
||
self._click_text("从相册选择", timeout=3) or self._click_text("相册", timeout=2)
|
||
time.sleep(1)
|
||
count = min(len(images) if images else 1, 9)
|
||
for i in range(count):
|
||
col, row = i % 3, i // 3
|
||
x = self._w // 6 + col * (self._w // 3)
|
||
y = self._h // 4 + row * (self._h // 4)
|
||
self._tap(x, y)
|
||
time.sleep(0.3)
|
||
self._click_text("完成", timeout=3)
|
||
time.sleep(1)
|
||
else:
|
||
self._long_press(self._w - 60, 80, 2000)
|
||
time.sleep(1)
|
||
|
||
self._input(content)
|
||
time.sleep(0.3)
|
||
|
||
if location:
|
||
self._click_text("所在位置", timeout=2)
|
||
time.sleep(0.5)
|
||
self._input(location, clear=True)
|
||
time.sleep(1)
|
||
pos = self._find_text(location)
|
||
if pos:
|
||
self._tap(*pos)
|
||
time.sleep(0.3)
|
||
|
||
if visible_list or invisible_list:
|
||
self._click_text("谁可以看", timeout=2)
|
||
time.sleep(0.5)
|
||
if visible_list:
|
||
self._click_text("部分可见", timeout=2)
|
||
elif invisible_list:
|
||
self._click_text("不给谁看", timeout=2)
|
||
time.sleep(0.3)
|
||
self._click_text("确定")
|
||
|
||
self._click_text("发表", timeout=5) or self._click_text("发送", timeout=3)
|
||
time.sleep(2)
|
||
return self._ok(note="朋友圈已发布")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def like_moments(self, user_id: str = None, post_index: int = 0) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._open_moments(user_id)
|
||
|
||
for _ in range(post_index):
|
||
self._scroll_up()
|
||
time.sleep(0.5)
|
||
|
||
xml = self._get_xml()
|
||
hearts = re.findall(
|
||
r'content-desc="[^"]*(?:评论|赞)[^"]*"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"', xml
|
||
)
|
||
if hearts:
|
||
x1, y1, x2, y2 = map(int, hearts[0])
|
||
self._tap((x1 + x2) // 2, (y1 + y2) // 2)
|
||
time.sleep(0.3)
|
||
self._click_text("赞", timeout=3)
|
||
return self._ok(note="已点赞")
|
||
|
||
self._tap(self._w - 80, self._h // 2)
|
||
time.sleep(0.3)
|
||
self._click_text("赞", timeout=3)
|
||
return self._ok(note="已点赞")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def comment_moments(self, user_id: str = None, post_index: int = 0,
|
||
comment: str = "", reply_to: str = None) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._open_moments(user_id)
|
||
|
||
for _ in range(post_index):
|
||
self._scroll_up()
|
||
time.sleep(0.5)
|
||
|
||
self._tap(self._w - 80, self._h // 2)
|
||
time.sleep(0.3)
|
||
self._click_text("评论", timeout=3)
|
||
time.sleep(0.5)
|
||
|
||
if reply_to:
|
||
self._click_text(reply_to, timeout=2)
|
||
time.sleep(0.3)
|
||
|
||
self._input(comment)
|
||
self._click_text("发送", timeout=3) or self._enter()
|
||
return self._ok(note=f"已评论: {comment[:20]}...")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def get_moments(self, user_id: str = None, limit: int = 10) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._open_moments(user_id)
|
||
|
||
moments = []
|
||
for _ in range(min(limit, 20)):
|
||
xml = self._get_xml()
|
||
texts = re.findall(r'text="([^"]{5,})"', xml)
|
||
skip = {"朋友圈", "发现", "搜索", "返回"}
|
||
for t in texts:
|
||
if t not in skip and {"content": t} not in moments:
|
||
moments.append({"content": t, "timestamp": int(time.time())})
|
||
if len(moments) >= limit:
|
||
break
|
||
self._scroll_up()
|
||
time.sleep(0.8)
|
||
return self._ok(moments=moments[:limit], count=len(moments[:limit]))
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def delete_moments(self, post_index: int = 0) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("发现")
|
||
time.sleep(0.5)
|
||
self._click_text("朋友圈", timeout=3)
|
||
time.sleep(1.5)
|
||
self._tap(60, 180)
|
||
time.sleep(1)
|
||
for _ in range(post_index):
|
||
self._scroll_up()
|
||
time.sleep(0.5)
|
||
self._tap(self._w // 2, self._h // 2)
|
||
time.sleep(0.5)
|
||
self._click_text("删除", timeout=3)
|
||
time.sleep(0.5)
|
||
self._click_text("确定") or self._click_text("确认") or self._click_text("删除")
|
||
return self._ok(note="朋友圈已删除")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def _open_moments(self, user_id: str = None):
|
||
if user_id:
|
||
self._search_and_enter(user_id)
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(0.5)
|
||
self._tap(self._w // 2, 200)
|
||
time.sleep(1)
|
||
self._click_text("朋友圈", timeout=3)
|
||
else:
|
||
self._go_home()
|
||
self._tap_tab("发现")
|
||
time.sleep(0.5)
|
||
self._click_text("朋友圈", timeout=3)
|
||
time.sleep(1.5)
|
||
|
||
# ═══════════════════ 6. 个人设置 ═══════════════════
|
||
|
||
def _enter_profile(self):
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("我")
|
||
time.sleep(0.5)
|
||
self._tap(self._w // 2, 200)
|
||
time.sleep(1)
|
||
|
||
def set_nickname(self, nickname: str) -> dict:
|
||
try:
|
||
self._enter_profile()
|
||
self._click_text("名字", timeout=3) or self._click_text("昵称", timeout=2)
|
||
time.sleep(0.5)
|
||
self._input(nickname, clear=True)
|
||
time.sleep(0.3)
|
||
self._click_text("完成") or self._click_text("保存")
|
||
return self._ok(note=f"昵称已改为: {nickname}")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def set_signature(self, signature: str) -> dict:
|
||
try:
|
||
self._enter_profile()
|
||
if not self._click_text("个性签名", timeout=3):
|
||
self._scroll_up()
|
||
self._click_text("个性签名", timeout=3)
|
||
time.sleep(0.5)
|
||
self._input(signature, clear=True)
|
||
self._click_text("完成") or self._click_text("保存")
|
||
return self._ok(note=f"签名已改为: {signature}")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def set_avatar(self, image_path: str = None) -> dict:
|
||
try:
|
||
self._enter_profile()
|
||
self._click_text("头像", timeout=3)
|
||
time.sleep(1)
|
||
self._click_text("从相册选择", timeout=3) or self._click_text("相册", timeout=2)
|
||
time.sleep(1)
|
||
self._tap(self._w // 4, self._h // 3)
|
||
time.sleep(0.5)
|
||
self._click_text("使用") or self._click_text("完成") or self._click_text("确定")
|
||
time.sleep(1)
|
||
return self._ok(note="头像已更换")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def set_gender(self, gender: str) -> dict:
|
||
try:
|
||
self._enter_profile()
|
||
if not self._click_text("性别", timeout=3):
|
||
self._scroll_up()
|
||
self._click_text("性别", timeout=3)
|
||
time.sleep(0.5)
|
||
target = "男" if gender.lower() in ("male", "男") else "女"
|
||
self._click_text(target, timeout=3)
|
||
self._click_text("完成") or self._click_text("保存")
|
||
return self._ok(note=f"性别已设为: {target}")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def set_region(self, region: str) -> dict:
|
||
try:
|
||
self._enter_profile()
|
||
if not self._click_text("地区", timeout=3):
|
||
self._scroll_up()
|
||
self._click_text("地区", timeout=3)
|
||
time.sleep(0.5)
|
||
for part in region.split():
|
||
self._click_text(part, timeout=3)
|
||
time.sleep(0.5)
|
||
self._click_text("完成") or self._click_text("保存")
|
||
return self._ok(note=f"地区已设为: {region}")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def get_profile(self) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("我")
|
||
time.sleep(1)
|
||
xml = self._get_xml()
|
||
profile = {}
|
||
elements = re.findall(
|
||
r'text="([^"]+)"[^>]*bounds="\[(\d+),(\d+)\]\[(\d+),(\d+)\]"', xml
|
||
)
|
||
skip = {"我", "微信", "通讯录", "发现", "搜索", "状态", "浮窗", "表情",
|
||
"服务", "收藏", "卡包", "设置", "3个朋友"}
|
||
for txt, x1, y1, x2, y2 in elements:
|
||
y = int(y1)
|
||
if txt.startswith("微信号"):
|
||
profile["wechat_id"] = txt.replace("微信号:", "").replace("微信号:", "").strip()
|
||
elif 200 < y < 370 and txt not in skip and len(txt) > 1:
|
||
profile.setdefault("nickname", txt)
|
||
return self._ok(profile=profile)
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 7. 账号安全与解封 ═══════════════════
|
||
|
||
def check_account_status(self) -> dict:
|
||
try:
|
||
if not self._nav_to_account_security():
|
||
return self._fail("无法导航到账号与安全")
|
||
xml = self._get_xml()
|
||
info = {"account_active": True}
|
||
texts = re.findall(r'text="([^"]+)"', xml)
|
||
for t in texts:
|
||
if "微信号" in t:
|
||
info["wechat_id"] = t
|
||
elif "手机号" in t:
|
||
info["phone"] = t
|
||
elif "已绑定" in t or "已保护" in t:
|
||
info["security_status"] = t
|
||
self._back()
|
||
self._back()
|
||
return self._ok(status=info)
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def unblock_account(self, helper_wxid: str = None) -> dict:
|
||
try:
|
||
if self._nav_to_safety_center():
|
||
if self._click_text("解封账号", timeout=3) or \
|
||
self._click_text("自助解封", timeout=2):
|
||
time.sleep(1)
|
||
if helper_wxid:
|
||
pos = self._find_text("微信号")
|
||
if pos:
|
||
self._tap(pos[0], pos[1] + 80)
|
||
self._input(helper_wxid)
|
||
self._click_text("下一步", timeout=3) or \
|
||
self._click_text("提交", timeout=2)
|
||
time.sleep(1)
|
||
return self._ok(
|
||
note="解封流程已启动,请辅助好友在微信团队中确认")
|
||
return self._ok(note="账号当前未被封禁", status="normal")
|
||
return self._fail("无法导航到微信安全中心")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def safety_center(self) -> dict:
|
||
try:
|
||
if not self._nav_to_safety_center():
|
||
return self._fail("无法导航到微信安全中心")
|
||
xml = self._get_xml()
|
||
options = []
|
||
texts = re.findall(r'text="([^"]+)"', xml)
|
||
for t in texts:
|
||
if t not in {"微信安全中心", "返回"} and len(t) > 1:
|
||
options.append(t)
|
||
return self._ok(options=options)
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def change_password(self, old_pwd: str, new_pwd: str) -> dict:
|
||
try:
|
||
if not self._nav_to_account_security():
|
||
return self._fail("无法导航到账号与安全")
|
||
if self._click_text("密码", timeout=3) or self._click_text("微信密码", timeout=2):
|
||
time.sleep(0.5)
|
||
self._input(old_pwd)
|
||
self._enter()
|
||
time.sleep(0.5)
|
||
self._input(new_pwd)
|
||
self._enter()
|
||
time.sleep(0.5)
|
||
self._input(new_pwd)
|
||
self._click_text("完成") or self._click_text("确定") or self._enter()
|
||
return self._ok(note="密码已修改")
|
||
return self._fail("未找到密码设置入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 8. 收藏管理 ═══════════════════
|
||
|
||
def add_to_favorites(self, content_desc: str = None) -> dict:
|
||
try:
|
||
if content_desc:
|
||
xml = self._get_xml()
|
||
pos = self._find_text(content_desc, xml)
|
||
if pos:
|
||
self._long_press(*pos)
|
||
time.sleep(0.5)
|
||
if self._click_text("收藏", timeout=3):
|
||
return self._ok(note="已收藏")
|
||
return self._fail("未找到收藏选项")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def get_favorites(self, limit: int = 20) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("我")
|
||
time.sleep(0.5)
|
||
self._click_text("收藏", timeout=3)
|
||
time.sleep(1)
|
||
favorites = []
|
||
for _ in range(min(limit // 5, 10)):
|
||
xml = self._get_xml()
|
||
texts = re.findall(r'text="([^"]{2,})"', xml)
|
||
skip = {"收藏", "返回", "搜索", "全部收藏", "我"}
|
||
for t in texts:
|
||
if t not in skip and {"content": t} not in favorites:
|
||
favorites.append({"content": t})
|
||
if len(favorites) >= limit:
|
||
break
|
||
self._scroll_up()
|
||
time.sleep(0.5)
|
||
return self._ok(favorites=favorites[:limit], count=len(favorites[:limit]))
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 9. 支付/红包 ═══════════════════
|
||
|
||
def send_red_packet(self, to_id: str, amount: str, message: str = "恭喜发财") -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(to_id)
|
||
self._click_text("+") or self._tap(self._w - 60, self._h - 180)
|
||
time.sleep(0.5)
|
||
self._click_text("红包", timeout=3)
|
||
time.sleep(1)
|
||
self._input(amount, clear=True)
|
||
time.sleep(0.3)
|
||
if message != "恭喜发财":
|
||
pos = self._find_text("恭喜发财")
|
||
if pos:
|
||
self._tap(*pos)
|
||
self._input(message, clear=True)
|
||
self._click_text("塞钱进红包", timeout=3) or self._click_text("支付", timeout=2)
|
||
time.sleep(1)
|
||
return self._ok(note=f"红包已准备: {amount}元(需确认支付密码)")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def transfer(self, to_id: str, amount: str, message: str = "") -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(to_id)
|
||
self._click_text("+") or self._tap(self._w - 60, self._h - 180)
|
||
time.sleep(0.5)
|
||
self._click_text("转账", timeout=3)
|
||
time.sleep(1)
|
||
self._input(amount, clear=True)
|
||
time.sleep(0.3)
|
||
if message:
|
||
self._click_text("添加转账说明", timeout=2)
|
||
self._input(message)
|
||
self._click_text("转账", timeout=3)
|
||
time.sleep(1)
|
||
return self._ok(note=f"转账已准备: {amount}元(需确认支付密码)")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 10. 聊天设置 ═══════════════════
|
||
|
||
def set_chat_top(self, user_id: str, enable: bool = True) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(user_id)
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(1)
|
||
self._click_text("置顶聊天" if enable else "取消置顶", timeout=3) or \
|
||
self._click_text("置顶", timeout=2)
|
||
return self._ok(note=f"{'已置顶' if enable else '已取消置顶'}: {user_id}")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def set_mute_chat(self, user_id: str, enable: bool = True) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(user_id)
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(1)
|
||
self._click_text("消息免打扰", timeout=3)
|
||
return self._ok(note=f"{'已开启' if enable else '已关闭'}免打扰: {user_id}")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def clear_chat_history(self, user_id: str) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(user_id)
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(1)
|
||
self._scroll_up()
|
||
if self._click_text("清空聊天记录", timeout=3):
|
||
time.sleep(0.5)
|
||
self._click_text("确定") or self._click_text("清空")
|
||
return self._ok(note=f"聊天记录已清空: {user_id}")
|
||
return self._fail("未找到清空选项")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 11. 小程序 ═══════════════════
|
||
|
||
def open_mini_program(self, name: str) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._swipe(self._w // 2, 300, self._w // 2, self._h // 2, 300)
|
||
time.sleep(1)
|
||
if self._click_text(name, timeout=3):
|
||
time.sleep(2)
|
||
return self._ok(note=f"小程序已打开: {name}")
|
||
self._click_text("搜索", timeout=2)
|
||
self._input(name)
|
||
time.sleep(1)
|
||
self._click_text(name, timeout=3)
|
||
time.sleep(2)
|
||
return self._ok(note=f"小程序: {name}")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 12. 公众号 ═══════════════════
|
||
|
||
def follow_official_account(self, account_name: str) -> dict:
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(0.5)
|
||
self._click_text("添加朋友", timeout=3)
|
||
time.sleep(0.5)
|
||
self._click_text("公众号", timeout=3)
|
||
time.sleep(0.5)
|
||
self._input(account_name, clear=True)
|
||
time.sleep(1)
|
||
self._click_text("搜索", timeout=2) or self._enter()
|
||
time.sleep(1.5)
|
||
self._click_text(account_name, timeout=3)
|
||
time.sleep(1)
|
||
self._click_text("关注", timeout=3)
|
||
return self._ok(note=f"已关注公众号: {account_name}")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 13. 解封完善 ═══════════════════
|
||
|
||
def _nav_to_safety_center(self) -> bool:
|
||
"""导航到微信安全中心(缓存路径优先)"""
|
||
if self._navigate_cached("to_safety_center"):
|
||
return True
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("我")
|
||
time.sleep(1)
|
||
self._scroll_up()
|
||
time.sleep(0.5)
|
||
if not self._click_text("设置", timeout=3):
|
||
return False
|
||
time.sleep(1.5)
|
||
for _ in range(3):
|
||
self._swipe(self._w // 2, 400, self._w // 2, 1600, 200)
|
||
time.sleep(0.2)
|
||
time.sleep(0.5)
|
||
if not self._click_text("账号与安全", timeout=3):
|
||
return False
|
||
time.sleep(1.5)
|
||
if not self._click_text("微信安全中心", timeout=5):
|
||
return False
|
||
time.sleep(2)
|
||
return True
|
||
|
||
def _nav_to_settings(self) -> bool:
|
||
"""导航到设置页面"""
|
||
if self._navigate_cached("to_settings"):
|
||
return True
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("我")
|
||
time.sleep(1)
|
||
self._scroll_up()
|
||
time.sleep(0.5)
|
||
if not self._click_text("设置", timeout=3):
|
||
return False
|
||
time.sleep(1.5)
|
||
for _ in range(3):
|
||
self._swipe(self._w // 2, 400, self._w // 2, 1600, 200)
|
||
time.sleep(0.2)
|
||
return True
|
||
|
||
def _nav_to_account_security(self) -> bool:
|
||
"""导航到账号与安全"""
|
||
if self._navigate_cached("to_account_security"):
|
||
return True
|
||
if not self._nav_to_settings():
|
||
return False
|
||
if not self._click_text("账号与安全", timeout=3):
|
||
return False
|
||
time.sleep(1.5)
|
||
return True
|
||
|
||
def unblock_self(self) -> dict:
|
||
"""自助解封完整流程"""
|
||
try:
|
||
if not self._nav_to_safety_center():
|
||
return self._fail("无法导航到微信安全中心")
|
||
self._record_page_elements("safety_center")
|
||
if self._click_text("解封账号", timeout=3) or self._click_text("自助解封", timeout=2):
|
||
time.sleep(1)
|
||
self._click_text("下一步", timeout=3) or self._click_text("开始", timeout=2)
|
||
time.sleep(1)
|
||
return self._ok(note="自助解封流程已启动,请按页面提示完成验证")
|
||
return self._ok(note="账号当前未被封禁,无需解封", status="normal")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def unblock_appeal(self) -> dict:
|
||
"""申诉解封"""
|
||
try:
|
||
if not self._nav_to_safety_center():
|
||
return self._fail("无法导航到微信安全中心")
|
||
if self._click_text("申诉辅助", timeout=3) or self._click_text("申诉", timeout=2):
|
||
time.sleep(1)
|
||
self._click_text("开始申诉", timeout=3) or self._click_text("下一步", timeout=2)
|
||
time.sleep(1)
|
||
return self._ok(note="申诉解封流程已启动,请按页面提示填写信息")
|
||
return self._fail("未找到申诉入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def check_restrictions(self) -> dict:
|
||
"""检查当前限制详情"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("微信")
|
||
time.sleep(0.5)
|
||
xml = self._get_xml()
|
||
restrictions = []
|
||
restriction_keywords = [
|
||
"账号暂时无法使用聊天功能",
|
||
"功能受限",
|
||
"账号异常",
|
||
"暂时限制",
|
||
"封禁",
|
||
"被限制",
|
||
]
|
||
for kw in restriction_keywords:
|
||
if self._find_text(kw, xml):
|
||
restrictions.append(kw)
|
||
if restrictions:
|
||
return self._ok(restricted=True, restrictions=restrictions)
|
||
return self._ok(restricted=False, restrictions=[], note="未检测到功能限制")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def appeal_restriction(self) -> dict:
|
||
"""申诉功能限制"""
|
||
try:
|
||
self._ensure_wechat()
|
||
xml = self._get_xml()
|
||
if self._click_text("了解详情", timeout=3) or self._click_text("查看详情", timeout=2):
|
||
time.sleep(1)
|
||
if self._click_text("申诉", timeout=3) or self._click_text("去申诉", timeout=2):
|
||
time.sleep(1)
|
||
return self._ok(note="已进入功能限制申诉页面")
|
||
self._go_home()
|
||
self._tap_tab("我")
|
||
time.sleep(0.5)
|
||
self._click_text("设置", timeout=3)
|
||
time.sleep(0.5)
|
||
self._click_text("账号与安全", timeout=3)
|
||
time.sleep(0.5)
|
||
self._scroll_up()
|
||
self._click_text("微信安全中心", timeout=3)
|
||
time.sleep(1)
|
||
if self._click_text("解封账号", timeout=3) or self._click_text("申诉", timeout=2):
|
||
return self._ok(note="已进入申诉页面")
|
||
return self._fail("未找到申诉入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def unblock_with_sms(self, phone: str) -> dict:
|
||
"""通过短信验证解封"""
|
||
try:
|
||
if not self._nav_to_safety_center():
|
||
return self._fail("无法导航到微信安全中心")
|
||
self._click_text("解封账号", timeout=3) or self._click_text("自助解封", timeout=2)
|
||
time.sleep(1)
|
||
pos = self._find_text("手机号")
|
||
if pos:
|
||
self._tap(pos[0], pos[1] + 80)
|
||
time.sleep(0.3)
|
||
self._input(phone, clear=True)
|
||
time.sleep(0.3)
|
||
self._click_text("下一步", timeout=3) or self._click_text("发送验证码", timeout=2)
|
||
time.sleep(1)
|
||
return self._ok(note=f"短信验证解封已启动,验证码将发送至 {phone}")
|
||
return self._fail("未找到手机号输入框")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 14. 视频号 ═══════════════════
|
||
|
||
def open_video_channel(self) -> dict:
|
||
"""打开视频号"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("发现")
|
||
time.sleep(0.5)
|
||
if self._click_text("视频号", timeout=3):
|
||
time.sleep(2)
|
||
return self._ok(note="视频号已打开")
|
||
return self._fail("未找到视频号入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def like_video(self, index: int = 0) -> dict:
|
||
"""点赞视频"""
|
||
try:
|
||
self._ensure_wechat()
|
||
result = self.open_video_channel()
|
||
if not result.get("success"):
|
||
return result
|
||
for _ in range(index):
|
||
self._scroll_up()
|
||
time.sleep(1.5)
|
||
xml = self._get_xml()
|
||
heart_pos = self._find_text("❤", xml) or self._find_text("喜欢", xml)
|
||
if heart_pos:
|
||
self._tap(*heart_pos)
|
||
time.sleep(0.3)
|
||
return self._ok(note=f"已点赞第 {index+1} 个视频")
|
||
self._tap(self._w - 80, self._h // 2)
|
||
time.sleep(0.3)
|
||
return self._ok(note=f"已尝试点赞第 {index+1} 个视频")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def comment_video(self, index: int = 0, comment: str = "") -> dict:
|
||
"""评论视频"""
|
||
try:
|
||
self._ensure_wechat()
|
||
result = self.open_video_channel()
|
||
if not result.get("success"):
|
||
return result
|
||
for _ in range(index):
|
||
self._scroll_up()
|
||
time.sleep(1.5)
|
||
xml = self._get_xml()
|
||
comment_pos = self._find_text("评论", xml) or self._find_text("💬", xml)
|
||
if comment_pos:
|
||
self._tap(*comment_pos)
|
||
else:
|
||
self._tap(self._w - 80, self._h * 2 // 3)
|
||
time.sleep(1)
|
||
self._input(comment)
|
||
time.sleep(0.3)
|
||
self._click_text("发送", timeout=3) or self._enter()
|
||
return self._ok(note=f"已评论第 {index+1} 个视频: {comment[:20]}")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def follow_video_creator(self, index: int = 0) -> dict:
|
||
"""关注视频号创作者"""
|
||
try:
|
||
self._ensure_wechat()
|
||
result = self.open_video_channel()
|
||
if not result.get("success"):
|
||
return result
|
||
for _ in range(index):
|
||
self._scroll_up()
|
||
time.sleep(1.5)
|
||
self._tap(80, self._h // 2)
|
||
time.sleep(1)
|
||
if self._click_text("关注", timeout=3):
|
||
return self._ok(note=f"已关注第 {index+1} 个视频的创作者")
|
||
return self._fail("未找到关注按钮")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def share_video(self, index: int = 0, to_id: str = "") -> dict:
|
||
"""分享视频"""
|
||
try:
|
||
self._ensure_wechat()
|
||
result = self.open_video_channel()
|
||
if not result.get("success"):
|
||
return result
|
||
for _ in range(index):
|
||
self._scroll_up()
|
||
time.sleep(1.5)
|
||
xml = self._get_xml()
|
||
share_pos = self._find_text("转发", xml) or self._find_text("分享", xml)
|
||
if share_pos:
|
||
self._tap(*share_pos)
|
||
else:
|
||
self._tap(self._w - 80, self._h * 3 // 4)
|
||
time.sleep(1)
|
||
if to_id:
|
||
self._click_text(to_id, timeout=3) or self._click_text("搜索", timeout=2)
|
||
if not self._find_text(to_id):
|
||
self._input(to_id, clear=True)
|
||
time.sleep(1)
|
||
self._click_text(to_id, timeout=3)
|
||
time.sleep(0.5)
|
||
self._click_text("发送", timeout=3) or self._click_text("分享", timeout=2)
|
||
return self._ok(note=f"已分享第 {index+1} 个视频给 {to_id}")
|
||
return self._ok(note="分享面板已打开")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def get_video_list(self, limit: int = 10) -> dict:
|
||
"""获取视频列表"""
|
||
try:
|
||
self._ensure_wechat()
|
||
result = self.open_video_channel()
|
||
if not result.get("success"):
|
||
return result
|
||
videos = []
|
||
for i in range(min(limit, 20)):
|
||
xml = self._get_xml()
|
||
texts = re.findall(r'text="([^"]{3,})"', xml)
|
||
skip = {"视频号", "返回", "搜索", "发现", "直播"}
|
||
for t in texts:
|
||
if t not in skip and {"title": t} not in videos:
|
||
videos.append({"title": t, "index": i})
|
||
break
|
||
if len(videos) >= limit:
|
||
break
|
||
self._scroll_up()
|
||
time.sleep(1.5)
|
||
return self._ok(videos=videos[:limit], count=len(videos[:limit]))
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 15. 扫一扫 ═══════════════════
|
||
|
||
def scan_qr_code(self) -> dict:
|
||
"""打开扫一扫"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(0.5)
|
||
if self._click_text("扫一扫", timeout=3):
|
||
time.sleep(2)
|
||
return self._ok(note="扫一扫已打开,等待扫描")
|
||
return self._fail("未找到扫一扫入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def scan_add_friend(self) -> dict:
|
||
"""扫码加好友"""
|
||
try:
|
||
result = self.scan_qr_code()
|
||
if not result.get("success"):
|
||
return result
|
||
time.sleep(3)
|
||
xml = self._get_xml()
|
||
if self._find_text("添加到通讯录", xml):
|
||
self._click_text("添加到通讯录", timeout=3)
|
||
time.sleep(1)
|
||
self._click_text("发送", timeout=3)
|
||
return self._ok(note="已扫码并发送好友请求")
|
||
if self._find_text("关注", xml):
|
||
self._click_text("关注", timeout=3)
|
||
return self._ok(note="已扫码关注")
|
||
return self._ok(note="扫一扫已打开,请对准二维码")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def show_my_qr(self) -> dict:
|
||
"""显示我的二维码"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("我")
|
||
time.sleep(0.5)
|
||
self._tap(self._w // 2, 200)
|
||
time.sleep(1)
|
||
if self._click_text("二维码", timeout=3) or self._click_text("我的二维码", timeout=2):
|
||
time.sleep(1)
|
||
return self._ok(note="我的二维码已显示")
|
||
return self._fail("未找到二维码入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def extract_qr_from_image(self) -> dict:
|
||
"""从图片识别二维码"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap(self._w - 60, 80)
|
||
time.sleep(0.5)
|
||
self._click_text("扫一扫", timeout=3)
|
||
time.sleep(1)
|
||
if self._click_text("相册", timeout=3) or self._click_text("从相册选取", timeout=2):
|
||
time.sleep(1)
|
||
self._tap(self._w // 4, self._h // 3)
|
||
time.sleep(2)
|
||
xml = self._get_xml()
|
||
texts = re.findall(r'text="([^"]+)"', xml)
|
||
return self._ok(note="已从相册选取图片识别二维码", page_texts=texts[:10])
|
||
return self._fail("未找到相册入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 16. 支付完善 ═══════════════════
|
||
|
||
def show_payment_code(self) -> dict:
|
||
"""显示付款码"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("我")
|
||
time.sleep(0.5)
|
||
self._click_text("服务", timeout=3) or self._click_text("支付", timeout=2)
|
||
time.sleep(1)
|
||
if self._click_text("收付款", timeout=3):
|
||
time.sleep(1)
|
||
return self._ok(note="付款码已显示")
|
||
return self._fail("未找到收付款入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def receive_payment(self, amount: str = "", desc: str = "") -> dict:
|
||
"""收款码"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("我")
|
||
time.sleep(0.5)
|
||
self._click_text("服务", timeout=3) or self._click_text("支付", timeout=2)
|
||
time.sleep(1)
|
||
self._click_text("收付款", timeout=3)
|
||
time.sleep(1)
|
||
if self._click_text("二维码收款", timeout=3) or self._click_text("我要收款", timeout=2):
|
||
time.sleep(1)
|
||
if amount:
|
||
self._click_text("设置金额", timeout=3)
|
||
time.sleep(0.5)
|
||
self._input(amount, clear=True)
|
||
time.sleep(0.3)
|
||
if desc:
|
||
self._click_text("添加收款说明", timeout=2)
|
||
self._input(desc)
|
||
self._click_text("确定", timeout=3)
|
||
time.sleep(0.5)
|
||
return self._ok(note=f"收款码已生成{f',金额: {amount}元' if amount else ''}")
|
||
return self._fail("未找到收款入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def view_wallet(self) -> dict:
|
||
"""查看钱包余额"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("我")
|
||
time.sleep(0.5)
|
||
self._click_text("服务", timeout=3) or self._click_text("支付", timeout=2)
|
||
time.sleep(1)
|
||
if self._click_text("钱包", timeout=3):
|
||
time.sleep(1)
|
||
xml = self._get_xml()
|
||
wallet_info = {}
|
||
texts = re.findall(r'text="([^"]+)"', xml)
|
||
for t in texts:
|
||
if "零钱" in t or "余额" in t:
|
||
wallet_info["balance_label"] = t
|
||
if re.match(r'^¥?[\d,.]+$', t):
|
||
wallet_info["amount"] = t
|
||
return self._ok(wallet=wallet_info, note="钱包信息已获取")
|
||
return self._fail("未找到钱包入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def view_transactions(self, limit: int = 20) -> dict:
|
||
"""查看账单"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("我")
|
||
time.sleep(0.5)
|
||
self._click_text("服务", timeout=3) or self._click_text("支付", timeout=2)
|
||
time.sleep(1)
|
||
self._click_text("钱包", timeout=3)
|
||
time.sleep(1)
|
||
if self._click_text("账单", timeout=3):
|
||
time.sleep(1)
|
||
transactions = []
|
||
for _ in range(min(limit // 5, 10)):
|
||
xml = self._get_xml()
|
||
texts = re.findall(r'text="([^"]{2,})"', xml)
|
||
skip = {"账单", "返回", "搜索", "全部账单", "筛选"}
|
||
for t in texts:
|
||
if t not in skip and {"desc": t} not in transactions:
|
||
transactions.append({"desc": t})
|
||
if len(transactions) >= limit:
|
||
break
|
||
self._scroll_up()
|
||
time.sleep(0.5)
|
||
return self._ok(transactions=transactions[:limit], count=len(transactions[:limit]))
|
||
return self._fail("未找到账单入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def receive_red_packet(self, from_id: str = "") -> dict:
|
||
"""领取红包"""
|
||
try:
|
||
self._ensure_wechat()
|
||
if from_id:
|
||
self._search_and_enter(from_id)
|
||
time.sleep(0.5)
|
||
xml = self._get_xml()
|
||
rp_pos = self._find_text("微信红包", xml) or self._find_text("领取红包", xml)
|
||
if rp_pos:
|
||
self._tap(*rp_pos)
|
||
time.sleep(1)
|
||
if self._click_text("开", timeout=3) or self._click_text("领取", timeout=2):
|
||
time.sleep(1)
|
||
return self._ok(note="红包已领取")
|
||
xml2 = self._get_xml()
|
||
if self._find_text("已领取", xml2) or self._find_text("已被领完", xml2):
|
||
return self._ok(note="红包已被领取过或已领完")
|
||
return self._fail("红包领取失败")
|
||
return self._fail("未找到可领取的红包")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 17. 通话 ═══════════════════
|
||
|
||
def voice_call(self, user_id: str) -> dict:
|
||
"""语音通话"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(user_id)
|
||
time.sleep(0.5)
|
||
self._click_text("+") or self._tap(self._w - 60, self._h - 180)
|
||
time.sleep(0.5)
|
||
if self._click_text("语音通话", timeout=3) or self._click_text("音视频通话", timeout=2):
|
||
time.sleep(0.5)
|
||
self._click_text("语音通话", timeout=2)
|
||
time.sleep(2)
|
||
return self._ok(note=f"正在呼叫 {user_id}(语音通话)")
|
||
return self._fail("未找到语音通话入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def video_call(self, user_id: str) -> dict:
|
||
"""视频通话"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(user_id)
|
||
time.sleep(0.5)
|
||
self._click_text("+") or self._tap(self._w - 60, self._h - 180)
|
||
time.sleep(0.5)
|
||
if self._click_text("视频通话", timeout=3) or self._click_text("音视频通话", timeout=2):
|
||
time.sleep(0.5)
|
||
self._click_text("视频通话", timeout=2)
|
||
time.sleep(2)
|
||
return self._ok(note=f"正在呼叫 {user_id}(视频通话)")
|
||
return self._fail("未找到视频通话入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 18. 群发助手 ═══════════════════
|
||
|
||
def mass_send(self, content: str, user_ids: list = None) -> dict:
|
||
"""群发消息"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("我")
|
||
time.sleep(0.5)
|
||
self._click_text("设置", timeout=3)
|
||
time.sleep(0.5)
|
||
self._click_text("通用", timeout=3)
|
||
time.sleep(0.5)
|
||
self._click_text("辅助功能", timeout=3)
|
||
time.sleep(0.5)
|
||
self._click_text("群发助手", timeout=3)
|
||
time.sleep(1)
|
||
if not self._click_text("启用该功能", timeout=2):
|
||
pass
|
||
self._click_text("新建群发", timeout=3) or self._click_text("开始群发", timeout=2)
|
||
time.sleep(1)
|
||
if user_ids:
|
||
for uid in user_ids:
|
||
pos = self._find_text(uid)
|
||
if pos:
|
||
self._tap(*pos)
|
||
time.sleep(0.3)
|
||
else:
|
||
self._click_text("搜索", timeout=2)
|
||
self._input(uid, clear=True)
|
||
time.sleep(0.5)
|
||
pos = self._find_text(uid)
|
||
if pos:
|
||
self._tap(*pos)
|
||
time.sleep(0.3)
|
||
self._back()
|
||
time.sleep(0.3)
|
||
else:
|
||
self._click_text("全选", timeout=2)
|
||
time.sleep(0.3)
|
||
self._click_text("下一步", timeout=3) or self._click_text("确定", timeout=2)
|
||
time.sleep(0.5)
|
||
self._input(content)
|
||
time.sleep(0.3)
|
||
self._click_text("发送", timeout=3)
|
||
time.sleep(1)
|
||
self._click_text("确定", timeout=2) or self._click_text("发送", timeout=2)
|
||
return self._ok(note=f"群发消息已发送,内容: {content[:30]}")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 19. 搜一搜/看一看 ═══════════════════
|
||
|
||
def wechat_search(self, keyword: str) -> dict:
|
||
"""搜一搜"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("发现")
|
||
time.sleep(0.5)
|
||
if self._click_text("搜一搜", timeout=3):
|
||
time.sleep(1)
|
||
self._input(keyword, clear=True)
|
||
time.sleep(0.5)
|
||
self._click_text("搜索", timeout=3) or self._enter()
|
||
time.sleep(2)
|
||
xml = self._get_xml()
|
||
results = []
|
||
texts = re.findall(r'text="([^"]{3,})"', xml)
|
||
skip = {"搜一搜", "搜索", "返回", keyword, "全部", "朋友圈", "文章",
|
||
"公众号", "小程序", "视频", "百科"}
|
||
for t in texts:
|
||
if t not in skip and len(t) > 2:
|
||
results.append({"title": t})
|
||
return self._ok(results=results[:20], count=len(results[:20]),
|
||
note=f"搜一搜 '{keyword}' 结果")
|
||
return self._fail("未找到搜一搜入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def top_stories(self) -> dict:
|
||
"""看一看"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("发现")
|
||
time.sleep(0.5)
|
||
if self._click_text("看一看", timeout=3):
|
||
time.sleep(2)
|
||
xml = self._get_xml()
|
||
stories = []
|
||
texts = re.findall(r'text="([^"]{5,})"', xml)
|
||
skip = {"看一看", "返回", "发现", "精选", "朋友在看", "热点"}
|
||
for t in texts:
|
||
if t not in skip:
|
||
stories.append({"title": t})
|
||
return self._ok(stories=stories[:20], count=len(stories[:20]))
|
||
return self._fail("未找到看一看入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 20. 微信运动 ═══════════════════
|
||
|
||
def get_steps(self) -> dict:
|
||
"""获取步数"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._search_and_enter("微信运动")
|
||
time.sleep(1)
|
||
xml = self._get_xml()
|
||
steps_info = {}
|
||
texts = re.findall(r'text="([^"]+)"', xml)
|
||
for t in texts:
|
||
if re.match(r'^\d{1,6}$', t) and int(t) > 0:
|
||
steps_info.setdefault("steps", t)
|
||
elif "步" in t:
|
||
steps_info["steps_label"] = t
|
||
return self._ok(steps=steps_info, note="步数信息已获取")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def like_steps(self, user_id: str = "") -> dict:
|
||
"""点赞步数"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._search_and_enter("微信运动")
|
||
time.sleep(1)
|
||
self._click_text("步数排行榜", timeout=3) or self._click_text("排行榜", timeout=2)
|
||
time.sleep(1)
|
||
if user_id:
|
||
pos = self._find_text(user_id)
|
||
if pos:
|
||
self._tap(*pos)
|
||
time.sleep(0.5)
|
||
xml = self._get_xml()
|
||
like_pos = self._find_text("❤", xml) or self._find_text("赞", xml)
|
||
if like_pos:
|
||
self._tap(*like_pos)
|
||
return self._ok(note=f"已点赞{f' {user_id} 的' if user_id else ''}步数")
|
||
return self._fail("未找到点赞按钮")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 21. 位置分享 ═══════════════════
|
||
|
||
def send_location(self, user_id: str) -> dict:
|
||
"""发送位置"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(user_id)
|
||
time.sleep(0.5)
|
||
self._click_text("+") or self._tap(self._w - 60, self._h - 180)
|
||
time.sleep(0.5)
|
||
if self._click_text("位置", timeout=3):
|
||
time.sleep(0.5)
|
||
self._click_text("发送位置", timeout=3)
|
||
time.sleep(2)
|
||
self._click_text("发送", timeout=3)
|
||
return self._ok(note=f"已向 {user_id} 发送当前位置")
|
||
return self._fail("未找到位置入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def share_real_time_location(self, user_id: str) -> dict:
|
||
"""共享实时位置"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(user_id)
|
||
time.sleep(0.5)
|
||
self._click_text("+") or self._tap(self._w - 60, self._h - 180)
|
||
time.sleep(0.5)
|
||
if self._click_text("位置", timeout=3):
|
||
time.sleep(0.5)
|
||
if self._click_text("共享实时位置", timeout=3):
|
||
time.sleep(2)
|
||
return self._ok(note=f"已与 {user_id} 开始共享实时位置")
|
||
return self._fail("未找到共享实时位置选项")
|
||
return self._fail("未找到位置入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 22. 表情管理 ═══════════════════
|
||
|
||
def send_emoji(self, user_id: str, emoji_name: str = "") -> dict:
|
||
"""发送表情"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(user_id)
|
||
time.sleep(0.5)
|
||
xml = self._get_xml()
|
||
emoji_btn = self._find_text("表情", xml) or self._find_text("😊", xml)
|
||
if emoji_btn:
|
||
self._tap(*emoji_btn)
|
||
time.sleep(0.5)
|
||
if emoji_name:
|
||
pos = self._find_text(emoji_name)
|
||
if pos:
|
||
self._tap(*pos)
|
||
time.sleep(0.3)
|
||
self._click_text("发送", timeout=2)
|
||
return self._ok(note=f"已发送表情 {emoji_name}")
|
||
self._tap(self._w // 4, self._h * 3 // 4)
|
||
time.sleep(0.3)
|
||
self._click_text("发送", timeout=2)
|
||
return self._ok(note="已发送表情")
|
||
return self._fail("未找到表情入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def get_sticker_list(self) -> dict:
|
||
"""获取表情包列表"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("我")
|
||
time.sleep(0.5)
|
||
if self._click_text("表情", timeout=3):
|
||
time.sleep(1)
|
||
xml = self._get_xml()
|
||
stickers = []
|
||
texts = re.findall(r'text="([^"]{2,})"', xml)
|
||
skip = {"表情", "返回", "搜索", "我的表情", "我"}
|
||
for t in texts:
|
||
if t not in skip:
|
||
stickers.append({"name": t})
|
||
return self._ok(stickers=stickers, count=len(stickers))
|
||
return self._fail("未找到表情入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 23. 朋友圈增强 ═══════════════════
|
||
|
||
def set_moments_cover(self) -> dict:
|
||
"""设置朋友圈封面"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("发现")
|
||
time.sleep(0.5)
|
||
self._click_text("朋友圈", timeout=3)
|
||
time.sleep(1.5)
|
||
self._tap(self._w // 2, 300)
|
||
time.sleep(1)
|
||
if self._click_text("换封面", timeout=3) or self._click_text("更换相册封面", timeout=2):
|
||
time.sleep(0.5)
|
||
self._click_text("从手机相册选择", timeout=3) or self._click_text("相册", timeout=2)
|
||
time.sleep(1)
|
||
self._tap(self._w // 4, self._h // 3)
|
||
time.sleep(0.5)
|
||
self._click_text("确定", timeout=3) or self._click_text("完成", timeout=2)
|
||
time.sleep(1)
|
||
return self._ok(note="朋友圈封面已更换")
|
||
return self._fail("未找到更换封面入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def set_moments_privacy(self, days: int = 180) -> dict:
|
||
"""设置朋友圈可见天数"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("我")
|
||
time.sleep(0.5)
|
||
self._click_text("设置", timeout=3)
|
||
time.sleep(0.5)
|
||
self._click_text("隐私", timeout=3)
|
||
time.sleep(0.5)
|
||
if self._click_text("朋友圈", timeout=3) or self._click_text("允许朋友查看朋友圈的范围", timeout=2):
|
||
time.sleep(0.5)
|
||
days_map = {3: "最近三天", 30: "最近一个月", 180: "最近半年"}
|
||
target = days_map.get(days, "全部")
|
||
if self._click_text(target, timeout=3):
|
||
time.sleep(0.3)
|
||
self._click_text("确定", timeout=2) or self._back()
|
||
return self._ok(note=f"朋友圈可见范围已设为: {target}")
|
||
return self._fail(f"未找到 '{target}' 选项")
|
||
return self._fail("未找到朋友圈隐私设置入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def forward_moments_link(self, url: str, to_id: str = "") -> dict:
|
||
"""分享链接到朋友圈"""
|
||
try:
|
||
self._ensure_wechat()
|
||
if to_id:
|
||
self._search_and_enter(to_id)
|
||
time.sleep(0.5)
|
||
self._input(url)
|
||
time.sleep(0.3)
|
||
self._click_text("发送", timeout=3) or self._enter()
|
||
time.sleep(1)
|
||
xml = self._get_xml()
|
||
link_pos = self._find_text(url[:20], xml)
|
||
if link_pos:
|
||
self._long_press(*link_pos)
|
||
time.sleep(0.5)
|
||
if self._click_text("分享到朋友圈", timeout=3):
|
||
time.sleep(1)
|
||
self._click_text("发表", timeout=3) or self._click_text("发送", timeout=2)
|
||
return self._ok(note="链接已分享到朋友圈")
|
||
self._go_home()
|
||
self._tap_tab("发现")
|
||
time.sleep(0.5)
|
||
self._click_text("朋友圈", timeout=3)
|
||
time.sleep(1.5)
|
||
self._long_press(self._w - 60, 80, 2000)
|
||
time.sleep(1)
|
||
self._input(url)
|
||
self._click_text("发表", timeout=5) or self._click_text("发送", timeout=3)
|
||
return self._ok(note="链接已发布到朋友圈")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 24. 语音消息 ═══════════════════
|
||
|
||
def send_voice_message(self, user_id: str, duration: int = 3) -> dict:
|
||
"""发送语音消息(长按语音按钮)"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(user_id)
|
||
time.sleep(0.5)
|
||
if self._click_text("按住说话", timeout=2):
|
||
time.sleep(0.3)
|
||
else:
|
||
voice_btn = self._find_text("语音")
|
||
if voice_btn:
|
||
self._tap(*voice_btn)
|
||
time.sleep(0.3)
|
||
else:
|
||
self._tap(60, self._h - 120)
|
||
time.sleep(0.3)
|
||
hold_ms = max(duration * 1000, 1000)
|
||
self._long_press(self._w // 2, self._h - 200, hold_ms)
|
||
time.sleep(0.5)
|
||
return self._ok(note=f"已向 {user_id} 发送 {duration}s 语音消息")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 25. 文件管理 ═══════════════════
|
||
|
||
def send_file_from_chat(self, user_id: str) -> dict:
|
||
"""在聊天中发送文件"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(user_id)
|
||
time.sleep(0.5)
|
||
self._click_text("+") or self._tap(self._w - 60, self._h - 180)
|
||
time.sleep(0.5)
|
||
if not self._click_text("文件", timeout=3):
|
||
self._scroll_up()
|
||
self._click_text("文件", timeout=3)
|
||
time.sleep(1)
|
||
self._tap(self._w // 2, self._h // 3)
|
||
time.sleep(0.5)
|
||
self._click_text("发送", timeout=3) or self._click_text("确定", timeout=2)
|
||
return self._ok(note=f"已向 {user_id} 发送文件")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def download_file(self, user_id: str) -> dict:
|
||
"""下载收到的文件"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._search_and_enter(user_id)
|
||
time.sleep(0.5)
|
||
xml = self._get_xml()
|
||
file_pos = self._find_text("[文件]", xml)
|
||
if not file_pos:
|
||
texts = re.findall(
|
||
r'text="([^"]*\.(pdf|doc|docx|xls|xlsx|ppt|pptx|zip|rar|txt)[^"]*)"', xml
|
||
)
|
||
if texts:
|
||
file_pos = self._find_text(texts[0][0], xml)
|
||
if file_pos:
|
||
self._tap(*file_pos)
|
||
time.sleep(2)
|
||
if self._click_text("下载", timeout=3) or self._click_text("打开", timeout=2):
|
||
time.sleep(2)
|
||
return self._ok(note="文件已下载/打开")
|
||
return self._ok(note="已点击文件,等待加载")
|
||
return self._fail("未找到可下载的文件")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 26. 设置管理 ═══════════════════
|
||
|
||
def toggle_do_not_disturb(self, enable: bool = True) -> dict:
|
||
"""勿扰模式"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("我")
|
||
time.sleep(0.5)
|
||
self._click_text("设置", timeout=3)
|
||
time.sleep(0.5)
|
||
self._click_text("新消息通知", timeout=3) or self._click_text("消息通知", timeout=2)
|
||
time.sleep(0.5)
|
||
if self._click_text("勿扰模式", timeout=3):
|
||
time.sleep(0.5)
|
||
xml = self._get_xml()
|
||
current_on = self._find_text("开启", xml) is not None
|
||
if enable != current_on:
|
||
self._click_text("开启" if not current_on else "关闭", timeout=2)
|
||
return self._ok(note=f"勿扰模式已{'开启' if enable else '关闭'}")
|
||
return self._fail("未找到勿扰模式设置")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def clear_cache(self) -> dict:
|
||
"""清理缓存"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("我")
|
||
time.sleep(0.5)
|
||
self._click_text("设置", timeout=3)
|
||
time.sleep(0.5)
|
||
self._click_text("通用", timeout=3)
|
||
time.sleep(0.5)
|
||
if self._click_text("存储空间", timeout=3) or self._click_text("微信存储空间", timeout=2):
|
||
time.sleep(2)
|
||
if self._click_text("清理", timeout=5) or self._click_text("前往清理", timeout=3):
|
||
time.sleep(1)
|
||
self._click_text("全选", timeout=2)
|
||
time.sleep(0.3)
|
||
self._click_text("删除", timeout=3) or self._click_text("清理", timeout=2)
|
||
time.sleep(0.5)
|
||
self._click_text("确定", timeout=3) or self._click_text("确认", timeout=2)
|
||
time.sleep(2)
|
||
return self._ok(note="缓存清理完成")
|
||
return self._ok(note="存储空间页面已打开")
|
||
return self._fail("未找到存储空间入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def check_for_update(self) -> dict:
|
||
"""检查更新"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("我")
|
||
time.sleep(0.5)
|
||
self._click_text("设置", timeout=3)
|
||
time.sleep(0.5)
|
||
self._click_text("关于微信", timeout=3)
|
||
time.sleep(0.5)
|
||
if self._click_text("检查新版本", timeout=3) or self._click_text("版本更新", timeout=2):
|
||
time.sleep(2)
|
||
xml = self._get_xml()
|
||
if self._find_text("已是最新版本", xml):
|
||
return self._ok(note="当前已是最新版本")
|
||
if self._find_text("发现新版本", xml) or self._find_text("立即更新", xml):
|
||
return self._ok(note="发现新版本可更新", has_update=True)
|
||
return self._ok(note="版本检查完成")
|
||
return self._fail("未找到检查更新入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def logout(self) -> dict:
|
||
"""退出登录"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("我")
|
||
time.sleep(0.5)
|
||
self._click_text("设置", timeout=3)
|
||
time.sleep(0.5)
|
||
self._scroll_up()
|
||
if self._click_text("退出登录", timeout=3) or self._click_text("退出", timeout=2):
|
||
time.sleep(0.5)
|
||
self._click_text("确定", timeout=3) or self._click_text("退出登录", timeout=2)
|
||
time.sleep(2)
|
||
return self._ok(note="已退出微信登录")
|
||
return self._fail("未找到退出登录按钮")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
def switch_account(self) -> dict:
|
||
"""切换账号"""
|
||
try:
|
||
self._ensure_wechat()
|
||
self._go_home()
|
||
self._tap_tab("我")
|
||
time.sleep(0.5)
|
||
self._click_text("设置", timeout=3)
|
||
time.sleep(0.5)
|
||
self._scroll_up()
|
||
if self._click_text("切换账号", timeout=3):
|
||
time.sleep(1)
|
||
return self._ok(note="已进入切换账号页面")
|
||
if self._click_text("退出登录", timeout=3):
|
||
time.sleep(0.5)
|
||
self._click_text("确定", timeout=3)
|
||
time.sleep(2)
|
||
return self._ok(note="已退出登录,可切换账号")
|
||
return self._fail("未找到切换账号入口")
|
||
except Exception as e:
|
||
return self._fail(str(e))
|
||
|
||
# ═══════════════════ 统一调度入口 ═══════════════════
|
||
|
||
def execute(self, action: str, params: dict) -> dict:
|
||
"""统一入口:根据 action 路由到对应方法"""
|
||
router = {
|
||
# 消息
|
||
"send_message": lambda p: self.send_message(
|
||
p.get("to_id", ""), p.get("content", ""),
|
||
p.get("msg_type", "text"), p.get("media_url"),
|
||
p.get("at_list")),
|
||
"get_messages": lambda p: self.get_messages(
|
||
p.get("conversation_id"), p.get("limit", 20)),
|
||
"forward_message": lambda p: self.forward_message(
|
||
p.get("to_id", ""), p.get("content", "")),
|
||
"recall_message": lambda p: self.recall_message(),
|
||
"send_card": lambda p: self.send_card(
|
||
p.get("to_id", ""), p.get("card_wxid", "")),
|
||
# 好友
|
||
"add_friend": lambda p: self.add_friend(
|
||
p.get("user_id", ""), p.get("message", ""), p.get("source")),
|
||
"accept_friend": lambda p: self.accept_friend(p.get("user_id")),
|
||
"set_remark": lambda p: self.set_remark(
|
||
p.get("user_id", ""), p.get("remark", "")),
|
||
"delete_friend": lambda p: self.delete_friend(p.get("user_id", "")),
|
||
"get_contacts": lambda p: self.get_contacts(p.get("limit", 100)),
|
||
"search_contact": lambda p: self.search_contact(p.get("keyword", "")),
|
||
"batch_add_friend": lambda p: self.batch_add_friend(
|
||
p.get("user_ids", []), p.get("message", ""), p.get("interval", 5)),
|
||
"get_friend_info": lambda p: self.get_friend_info(p.get("user_id", "")),
|
||
# 群聊
|
||
"create_group": lambda p: self.create_group(
|
||
p.get("group_name", ""), p.get("member_ids", [])),
|
||
"invite_to_group": lambda p: self.invite_to_group(
|
||
p.get("group_id", ""), p.get("member_ids", [])),
|
||
"remove_from_group": lambda p: self.remove_from_group(
|
||
p.get("group_id", ""), p.get("member_ids", [])),
|
||
"set_group_notice": lambda p: self.set_group_notice(
|
||
p.get("group_id", ""), p.get("notice", "")),
|
||
"set_group_name": lambda p: self.set_group_name(
|
||
p.get("group_id", ""), p.get("group_name", "")),
|
||
"send_group_message": lambda p: self.send_group_message(
|
||
p.get("group_id", ""), p.get("content", ""),
|
||
p.get("msg_type", "text"), p.get("media_url"),
|
||
p.get("at_all", False), p.get("at_list")),
|
||
"set_group_welcome": lambda p: self.set_group_welcome(
|
||
p.get("group_id", ""), p.get("welcome_text", ""),
|
||
p.get("welcome_image")),
|
||
"get_groups": lambda p: self.get_groups(p.get("limit", 100)),
|
||
"get_group_members": lambda p: self.get_group_members(p.get("group_id", "")),
|
||
"quit_group": lambda p: self.quit_group(p.get("group_id", "")),
|
||
# 标签
|
||
"create_tag": lambda p: self.create_tag(p.get("tag_name", "")),
|
||
"delete_tag": lambda p: self.delete_tag(p.get("tag_name", "")),
|
||
"get_tags": lambda p: self.get_tags(),
|
||
"add_tag": lambda p: self.add_tag(
|
||
p.get("user_id", ""), p.get("tags", [])),
|
||
"remove_tag": lambda p: self.remove_tag(
|
||
p.get("user_id", ""), p.get("tags", [])),
|
||
"get_users_by_tag": lambda p: self.get_users_by_tag(
|
||
p.get("tag_name", ""), p.get("limit", 100)),
|
||
# 朋友圈
|
||
"post_moments": lambda p: self.post_moments(
|
||
p.get("content", ""), p.get("images"), p.get("video_url"),
|
||
p.get("location"), p.get("visible_list"), p.get("invisible_list")),
|
||
"like_moments": lambda p: self.like_moments(
|
||
p.get("user_id"), p.get("post_index", 0)),
|
||
"comment_moments": lambda p: self.comment_moments(
|
||
p.get("user_id"), p.get("post_index", 0),
|
||
p.get("comment", ""), p.get("reply_to")),
|
||
"get_moments": lambda p: self.get_moments(
|
||
p.get("user_id"), p.get("limit", 10)),
|
||
"delete_moments": lambda p: self.delete_moments(p.get("post_index", 0)),
|
||
# 个人设置
|
||
"set_nickname": lambda p: self.set_nickname(p.get("nickname", "")),
|
||
"set_signature": lambda p: self.set_signature(p.get("signature", "")),
|
||
"set_avatar": lambda p: self.set_avatar(p.get("image_path")),
|
||
"set_gender": lambda p: self.set_gender(p.get("gender", "")),
|
||
"set_region": lambda p: self.set_region(p.get("region", "")),
|
||
"get_profile": lambda p: self.get_profile(),
|
||
# 账号安全
|
||
"check_account_status": lambda p: self.check_account_status(),
|
||
"unblock_account": lambda p: self.unblock_account(p.get("helper_wxid")),
|
||
"safety_center": lambda p: self.safety_center(),
|
||
"change_password": lambda p: self.change_password(
|
||
p.get("old_pwd", ""), p.get("new_pwd", "")),
|
||
# 收藏
|
||
"add_to_favorites": lambda p: self.add_to_favorites(p.get("content_desc")),
|
||
"get_favorites": lambda p: self.get_favorites(p.get("limit", 20)),
|
||
# 支付
|
||
"send_red_packet": lambda p: self.send_red_packet(
|
||
p.get("to_id", ""), p.get("amount", ""), p.get("message", "恭喜发财")),
|
||
"transfer": lambda p: self.transfer(
|
||
p.get("to_id", ""), p.get("amount", ""), p.get("message", "")),
|
||
# 聊天设置
|
||
"set_chat_top": lambda p: self.set_chat_top(
|
||
p.get("user_id", ""), p.get("enable", True)),
|
||
"set_mute_chat": lambda p: self.set_mute_chat(
|
||
p.get("user_id", ""), p.get("enable", True)),
|
||
"clear_chat_history": lambda p: self.clear_chat_history(p.get("user_id", "")),
|
||
# 小程序
|
||
"open_mini_program": lambda p: self.open_mini_program(p.get("name", "")),
|
||
# 公众号
|
||
"follow_official_account": lambda p: self.follow_official_account(
|
||
p.get("account_name", "")),
|
||
# 解封完善
|
||
"unblock_self": lambda p: self.unblock_self(),
|
||
"unblock_appeal": lambda p: self.unblock_appeal(),
|
||
"check_restrictions": lambda p: self.check_restrictions(),
|
||
"appeal_restriction": lambda p: self.appeal_restriction(),
|
||
"unblock_with_sms": lambda p: self.unblock_with_sms(p.get("phone", "")),
|
||
# 视频号
|
||
"open_video_channel": lambda p: self.open_video_channel(),
|
||
"like_video": lambda p: self.like_video(p.get("index", 0)),
|
||
"comment_video": lambda p: self.comment_video(
|
||
p.get("index", 0), p.get("comment", "")),
|
||
"follow_video_creator": lambda p: self.follow_video_creator(p.get("index", 0)),
|
||
"share_video": lambda p: self.share_video(
|
||
p.get("index", 0), p.get("to_id", "")),
|
||
"get_video_list": lambda p: self.get_video_list(p.get("limit", 10)),
|
||
# 扫一扫
|
||
"scan_qr_code": lambda p: self.scan_qr_code(),
|
||
"scan_add_friend": lambda p: self.scan_add_friend(),
|
||
"show_my_qr": lambda p: self.show_my_qr(),
|
||
"extract_qr_from_image": lambda p: self.extract_qr_from_image(),
|
||
# 支付完善
|
||
"show_payment_code": lambda p: self.show_payment_code(),
|
||
"receive_payment": lambda p: self.receive_payment(
|
||
p.get("amount", ""), p.get("desc", "")),
|
||
"view_wallet": lambda p: self.view_wallet(),
|
||
"view_transactions": lambda p: self.view_transactions(p.get("limit", 20)),
|
||
"receive_red_packet": lambda p: self.receive_red_packet(p.get("from_id", "")),
|
||
# 通话
|
||
"voice_call": lambda p: self.voice_call(p.get("user_id", "")),
|
||
"video_call": lambda p: self.video_call(p.get("user_id", "")),
|
||
# 群发助手
|
||
"mass_send": lambda p: self.mass_send(
|
||
p.get("content", ""), p.get("user_ids")),
|
||
# 搜一搜/看一看
|
||
"wechat_search": lambda p: self.wechat_search(p.get("keyword", "")),
|
||
"top_stories": lambda p: self.top_stories(),
|
||
# 微信运动
|
||
"get_steps": lambda p: self.get_steps(),
|
||
"like_steps": lambda p: self.like_steps(p.get("user_id", "")),
|
||
# 位置分享
|
||
"send_location": lambda p: self.send_location(p.get("user_id", "")),
|
||
"share_real_time_location": lambda p: self.share_real_time_location(
|
||
p.get("user_id", "")),
|
||
# 表情管理
|
||
"send_emoji": lambda p: self.send_emoji(
|
||
p.get("user_id", ""), p.get("emoji_name", "")),
|
||
"get_sticker_list": lambda p: self.get_sticker_list(),
|
||
# 朋友圈增强
|
||
"set_moments_cover": lambda p: self.set_moments_cover(),
|
||
"set_moments_privacy": lambda p: self.set_moments_privacy(p.get("days", 180)),
|
||
"forward_moments_link": lambda p: self.forward_moments_link(
|
||
p.get("url", ""), p.get("to_id", "")),
|
||
# 语音消息
|
||
"send_voice_message": lambda p: self.send_voice_message(
|
||
p.get("user_id", ""), p.get("duration", 3)),
|
||
# 文件管理
|
||
"send_file_from_chat": lambda p: self.send_file_from_chat(p.get("user_id", "")),
|
||
"download_file": lambda p: self.download_file(p.get("user_id", "")),
|
||
# 设置管理
|
||
"toggle_do_not_disturb": lambda p: self.toggle_do_not_disturb(
|
||
p.get("enable", True)),
|
||
"clear_cache": lambda p: self.clear_cache(),
|
||
"check_for_update": lambda p: self.check_for_update(),
|
||
"logout": lambda p: self.logout(),
|
||
"switch_account": lambda p: self.switch_account(),
|
||
# 截图
|
||
"screenshot": lambda p: self.d.screenshot(),
|
||
}
|
||
|
||
handler = router.get(action)
|
||
if not handler:
|
||
logger.warning(f"WeChatADBEngine: 未知 action={action}")
|
||
return self._fail(f"不支持的操作: {action}")
|
||
|
||
if self.op_log:
|
||
self.op_log.start_operation(action, params)
|
||
|
||
try:
|
||
result = handler(params)
|
||
success = isinstance(result, dict) and result.get("success", False)
|
||
if self.op_log:
|
||
self.op_log.end_operation(
|
||
success, result,
|
||
result.get("error") if not success else None)
|
||
return result
|
||
except Exception as e:
|
||
if self.op_log:
|
||
self.op_log.end_operation(False, error=str(e))
|
||
return self._fail(str(e))
|