""" 微信自动登录(u2)— 检测未登录 → 账号密码登录 适配:微信 8.0.56+ / Redmi Note 13 (1080x2400) """ from __future__ import annotations import logging import re from typing import Any, Dict, List logger = logging.getLogger(__name__) WX_PKG = "com.tencent.mm" LOGIN_PAGE_MARKERS = [ "登录", "注册", "用手机号登录", "用微信号", "QQ号", "邮箱", "Sign in", "Log in", "Log In", ] MAIN_MARKERS = ["通讯录", "发现", "我", "微信"] # 红米 Note 13 1080x2400 近似坐标(降级点击) COORD_LOGIN_BTN = (540, 2050) COORD_WECHAT_ID_LINK = (540, 1680) COORD_FORM_LOGIN = (540, 2180) class WeChatAutoLogin: """微信登录状态检测 + 密码登录""" def __init__(self, skill): self.s = skill def _xml(self) -> str: try: return self.s.d.dump_hierarchy() or "" except Exception as exc: logger.warning(f"[auto_login] dump_hierarchy 失败: {exc}") return "" def _activity(self) -> str: try: cur = self.s.d.app_current() return str(cur.get("activity") or "") except Exception: return "" def _ui_sample(self, xml: str = "", limit: int = 24) -> List[str]: src = xml or self._xml() texts = re.findall(r'text="([^"]{1,48})"', src) descs = re.findall(r'content-desc="([^"]{1,48})"', src) seen: set[str] = set() out: List[str] = [] for t in texts + descs: t = t.strip() if not t or t in seen: continue seen.add(t) out.append(t) if len(out) >= limit: break return out def _tap(self, x: int, y: int) -> None: try: self.s.d.click(x, y) except Exception: self.s.click(x, y) def _cold_start_wechat(self) -> None: try: self.s.d.app_stop(WX_PKG) self.s.sleep(0.8) except Exception: pass self.s.launch() self.s.sleep(2.5) def _dismiss_dialogs(self) -> None: for label in ( "允许", "同意", "同意并继续", "同意并登录", "我知道了", "确定", "继续", "Allow", "OK", ): if self.s.click_text(label, timeout=1) or self.s.click_contains(label, timeout=1): self.s.sleep(0.8) def check_login_state(self) -> Dict[str, Any]: """检测是否已登录微信主界面""" try: if not self.s.is_running(): self.s.launch() else: self.s.d.app_start(WX_PKG) self.s.sleep(2.0) self._dismiss_dialogs() xml = self._xml() activity = self._activity() main_hits = sum(1 for m in ("通讯录", "发现", "我") if m in xml) if main_hits >= 2 or ( "LauncherUI" in activity and "通讯录" in xml ): return { "logged_in": True, "state": "logged_in", "activity": activity, } if any(m in xml for m in LOGIN_PAGE_MARKERS) or any( k in activity for k in ("Login", "Welcome", "MobileLogin", "LoginUI") ): return { "logged_in": False, "state": "login_page", "activity": activity, "ui_sample": self._ui_sample(xml), } if self.s.click_text("通讯录", timeout=2): return { "logged_in": True, "state": "logged_in", "activity": activity, } return { "logged_in": False, "state": "unknown", "activity": activity, "ui_sample": self._ui_sample(xml), } except Exception as exc: logger.exception(f"[auto_login] check_login_state: {exc}") return {"logged_in": False, "state": "error", "error": str(exc)} def _goto_password_login(self) -> None: """从欢迎页/登录页进入账号密码输入界面""" self._cold_start_wechat() self._dismiss_dialogs() xml = self._xml() if xml.count("EditText") >= 2: return clicked = False for label in ("登录", "Log in", "Log In", "Sign in"): if self.s.click_text(label, timeout=2) or self.s.click_contains(label, timeout=2): clicked = True self.s.sleep(1.2) break if not clicked: self._tap(*COORD_LOGIN_BTN) self.s.sleep(1.2) self._dismiss_dialogs() for label in ( "用微信号/QQ号/邮箱登录", "用微信号/QQ号/邮箱", "用微信号/QQ/邮箱登录", "用微信号", "QQ号/邮箱", "密码登录", "用密码登录", "更多登录方式", "其他登录方式", ): if self.s.click_contains(label, timeout=2): logger.info(f"[auto_login] 进入登录: {label}") self.s.sleep(1.2) break else: if "手机号" in self._xml() and "EditText" in self._xml(): self._tap(*COORD_WECHAT_ID_LINK) self.s.sleep(1.2) for label in ( "用微信号/QQ号/邮箱登录", "用微信号/QQ号/邮箱", "密码登录", ): if self.s.click_contains(label, timeout=1): self.s.sleep(1.0) break def _edit_count(self) -> int: try: edits = self.s.d(className="android.widget.EditText") return edits.count if hasattr(edits, "count") else len(list(edits)) except Exception: return 0 def _fill_edittexts(self, account: str, password: str) -> bool: """填写账号/密码(优先 EditText 顺序,降级 hint/坐标)""" d = self.s.d count = self._edit_count() if count >= 2: try: edits = d(className="android.widget.EditText") edits[0].click() self.s.sleep(0.3) edits[0].clear_text() edits[0].set_text(account) self.s.sleep(0.4) edits[1].click() self.s.sleep(0.3) edits[1].clear_text() edits[1].set_text(password) return True except Exception as exc: logger.warning(f"[auto_login] EditText[0,1] 失败: {exc}") if count == 1: try: edits = d(className="android.widget.EditText") edits[0].click() self.s.sleep(0.3) edits[0].clear_text() edits[0].set_text(account) self.s.sleep(0.5) if self.s.click_text("下一步", timeout=2) or self.s.click_contains("下一步", timeout=2): self.s.sleep(1.5) pwd = d(className="android.widget.EditText") if pwd.count >= 1: pwd[0].click() pwd[0].clear_text() pwd[0].set_text(password) return True except Exception as exc: logger.warning(f"[auto_login] 分步登录失败: {exc}") filled_account = False for hint in ("微信号", "QQ号", "邮箱", "账号", "手机号", "WeChat ID"): if self.s.click_contains(hint, timeout=1): self.s.input_text(account, clear=True) filled_account = True self.s.sleep(0.5) break if not filled_account and count >= 1: try: d(className="android.widget.EditText")[0].set_text(account) filled_account = True except Exception: pass for hint in ("密码", "请输入密码", "Password"): if self.s.click_contains(hint, timeout=1): self.s.input_text(password, clear=True) return True if count >= 2: try: edits = d(className="android.widget.EditText") edits[1].set_text(password) return bool(account) except Exception: pass return False def _submit_login(self) -> bool: """提交登录(避免误点欢迎页「登录」)""" if self._edit_count() >= 1: for label in ("同意并登录", "登录", "Log In", "Sign in"): elems = self.s.d(textContains=label) try: n = elems.count if hasattr(elems, "count") else len(list(elems)) except Exception: n = 0 if n == 1 and self.s.click_contains(label, timeout=2): return True if n > 1: try: elems[n - 1].click() return True except Exception: pass self._tap(*COORD_FORM_LOGIN) return True return ( self.s.click_text("登录", timeout=2) or self.s.click_contains("登录", timeout=2) ) def login_by_password(self, account: str, password: str) -> Dict[str, Any]: """ 账号密码登录微信。 account: 微信号 / 手机号 / 邮箱(如 Lytiao1) password: 登录密码 """ account = (account or "").strip() password = password or "" steps: List[str] = [] if not account or not password: return { "success": False, "error": "缺少 account 或 password", "steps": steps, } state = self.check_login_state() steps.append(f"check:{state.get('state')}") if state.get("logged_in"): return { "success": True, "already_logged_in": True, "state": state, "account": account, "steps": steps, } try: self._goto_password_login() steps.append("goto_password_login") steps.append(f"edits:{self._edit_count()}") if not self._fill_edittexts(account, password): steps.append("fill_edittexts_partial") else: steps.append("fill_edittexts_ok") submitted = self._submit_login() steps.append(f"submit:{submitted}") self.s.sleep(4.0) self._dismiss_dialogs() self.s.sleep(2.0) final = self.check_login_state() steps.append(f"final:{final.get('state')}") if final.get("logged_in"): return { "success": True, "already_logged_in": False, "state": final, "account": account, "steps": steps, } xml = self._xml() need_verify = any( k in xml for k in ("验证", "安全验证", "滑块", "验证码", "人脸识别", "新设备") ) return { "success": False, "error": "登录未完成,可能需要安全验证" if need_verify else "未能进入微信主界面", "need_verify": need_verify, "state": final, "account": account, "steps": steps, "activity": final.get("activity", ""), "ui_sample": self._ui_sample(xml), } except Exception as exc: logger.exception(f"[auto_login] login_by_password: {exc}") return { "success": False, "error": str(exc), "account": account, "steps": steps, "ui_sample": self._ui_sample(), } def ensure_logged_in(self, account: str, password: str) -> Dict[str, Any]: """未登录则自动密码登录""" state = self.check_login_state() if state.get("logged_in"): return {"success": True, "action": "skip", "state": state} result = self.login_by_password(account, password) result["action"] = "login" return result