feat: publish workphone SDK deployment and API docs
This commit is contained in:
376
sdk/agent/skills/wechat/wechat_auto_login.py
Normal file
376
sdk/agent/skills/wechat/wechat_auto_login.py
Normal file
@@ -0,0 +1,376 @@
|
||||
"""
|
||||
微信自动登录(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
|
||||
Reference in New Issue
Block a user