feat: publish workphone SDK deployment and API docs

This commit is contained in:
Manus AI
2026-07-14 18:10:52 +08:00
commit 021d633cc1
534 changed files with 122391 additions and 0 deletions

View 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