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

377 lines
12 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

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

"""
微信自动登录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