#!/usr/bin/env python3 """ 工作手机 · WebSocket + 连接态全量自检 用法(项目根或 sdk 目录): python3 scripts/ws_full_test.py python3 scripts/ws_full_test.py -d xgfe65eimrrofyws --base http://127.0.0.1:8899 检查项:health / connection/status / protocol / modes / hook/probe / hook/execute(只读) / 指定设备是否在线 / 手机 Agent 包是否安装(需 ADB) """ from __future__ import annotations import argparse import asyncio import json import subprocess import sys import time from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional from urllib.error import URLError from urllib.request import Request, urlopen DEFAULT_DEVICE = "xgfe65eimrrofyws" DEFAULT_BASE = "http://127.0.0.1:8899" AGENT_PKG = "com.system.cloudservice" WECHAT_PKG = "com.tencent.mm" OUT_DIR = Path(__file__).resolve().parent.parent / "tmp" def _get(url: str, timeout: float = 15) -> Dict[str, Any]: req = Request(url, headers={"X-API-Key": "workphone-secret-key"}) with urlopen(req, timeout=timeout) as resp: return json.loads(resp.read().decode()) def _post(url: str, body: dict, timeout: float = 90) -> Dict[str, Any]: data = json.dumps(body).encode() req = Request( url, data=data, headers={"Content-Type": "application/json", "X-API-Key": "workphone-secret-key"}, method="POST", ) with urlopen(req, timeout=timeout) as resp: return json.loads(resp.read().decode()) def _adb(*args: str) -> str: try: r = subprocess.run(["adb"] + list(args), capture_output=True, text=True, timeout=12) return (r.stdout or r.stderr or "").strip() except Exception as e: return f"adb error: {e}" def _check_adb_install(serial: str) -> Dict[str, Any]: out = { "adb_serials": [], "adb_serial_resolved": None, "expected_connected": False, "agent_pkg_installed": None, "agent_pkg_enabled": None, "wechat_installed": None, "launcher_activity": None, } lines = _adb("devices").splitlines()[1:] serials = [ln.split()[0] for ln in lines if ln.strip() and "device" in ln and "offline" not in ln] out["adb_serials"] = serials resolved = None for s in serials: if s == serial: resolved = s break ro = _adb("-s", s, "shell", "getprop", "ro.serialno") if ro.strip() == serial: resolved = s break out["adb_serial_resolved"] = resolved out["expected_connected"] = resolved is not None if not out["expected_connected"]: return out prefix = ["-s", resolved] pm = _adb(*prefix, "shell", "pm", "path", AGENT_PKG) out["agent_pkg_installed"] = AGENT_PKG in pm and "package:" in pm dump = _adb(*prefix, "shell", "pm", "dump", AGENT_PKG) out["agent_pkg_enabled"] = "enabled=1" in dump or "enabled=0" not in dump wc = _adb(*prefix, "shell", "pm", "path", WECHAT_PKG) out["wechat_installed"] = WECHAT_PKG in wc launcher = _adb( *prefix, "shell", "cmd", "package", "resolve-activity", "--brief", "-c", "android.intent.category.LAUNCHER", AGENT_PKG, ) out["launcher_activity"] = launcher if launcher and "No activity" not in launcher else None return out async def _ws_protocol_smoke(base_ws: str, device_id: str) -> Dict[str, Any]: """用 websockets 客户端走 register → heartbeat 协议(不替代真机 Agent)。""" try: import websockets except ImportError: return {"ok": False, "error": "pip install websockets"} url = f"{base_ws.rstrip('/')}/{device_id}_ws_smoke" result: Dict[str, Any] = {"ok": False, "url": url, "steps": []} try: async with websockets.connect(url, open_timeout=8) as ws: await ws.send( json.dumps( { "type": "register", "data": { "project_id": "cunkebao", "model": "WS-Smoke-Test", "capabilities": ["event", "device_request"], "source": "ws_full_test.py", }, } ) ) msg = json.loads(await asyncio.wait_for(ws.recv(), timeout=5)) result["steps"].append({"register_ack": msg.get("type")}) await ws.send(json.dumps({"type": "heartbeat", "device_id": f"{device_id}_ws_smoke"})) msg2 = json.loads(await asyncio.wait_for(ws.recv(), timeout=5)) result["steps"].append({"heartbeat_ack": msg2.get("type")}) result["ok"] = msg.get("type") == "registered" and msg2.get("type") in ("pong", "heartbeat_ack", "registered") except Exception as e: result["error"] = str(e)[:300] return result def run_tests(base: str, device_id: str) -> Dict[str, Any]: base = base.rstrip("/") ws_base = base.replace("http://", "ws://").replace("https://", "wss://") + "/ws/device" report: Dict[str, Any] = { "at": datetime.now().isoformat(), "base": base, "expected_device_id": device_id, "checks": [], "summary": {"pass": 0, "fail": 0, "warn": 0}, } def add(name: str, ok: bool, detail: Any = None, level: str = "pass"): if not ok and level == "pass": level = "fail" report["checks"].append({"name": name, "ok": ok, "level": level, "detail": detail}) key = level if level in ("pass", "fail", "warn") else ("pass" if ok else "fail") report["summary"][key] += 1 # 1 health try: h = _get(f"{base}/health") add("SDK /health", h.get("status") == "healthy", h) except URLError as e: add("SDK /health", False, str(e)) # 2 connection status conn = {} try: conn = _get(f"{base}/api/v3/connection/status").get("data", {}) ws_ids = conn.get("online_device_ids") or [] adb_serials = conn.get("adb_serials") or [] on_ws = device_id in ws_ids on_adb = device_id in adb_serials or any( _adb("-s", s, "shell", "getprop", "ro.serialno").strip() == device_id for s in adb_serials ) add( "指定设备 WS 在线", on_ws, {"online_ws_count": conn.get("online_ws_count"), "ids": ws_ids}, "warn" if not on_ws else "pass", ) add( "指定设备 ADB 在线", on_adb, {"adb_count": conn.get("adb_count"), "serials": adb_serials}, "warn" if not on_adb else "pass", ) report["connection"] = conn except URLError as e: add("connection/status", False, str(e)) # 3 protocol doc try: p = _get(f"{base}/api/v3/connection/protocol") add("connection/protocol", p.get("code") == 200, {"version": p.get("data", {}).get("version")}) except URLError as e: add("connection/protocol", False, str(e)) # 4 connection modes try: m = _get(f"{base}/api/v3/connection/modes/{device_id}") modes = m.get("data", {}).get("modes") or [] best = m.get("data", {}).get("best_mode") add("connection/modes", bool(modes), {"best_mode": best, "count": len(modes)}) report["modes"] = m.get("data") except URLError as e: add("connection/modes", False, str(e)) # 5 WS 协议冒烟(服务端 WS 通路) try: ws_smoke = asyncio.run(_ws_protocol_smoke(ws_base, device_id)) add("WebSocket 协议冒烟 register+heartbeat", ws_smoke.get("ok"), ws_smoke) except Exception as e: add("WebSocket 协议冒烟", False, str(e)) # 6 hook probe probe = {} try: probe = _get(f"{base}/api/v3/hook/probe/{device_id}", timeout=60) ok = probe.get("supports_hook") is True or probe.get("hook_tests", {}).get("connect") == "ok" add("hook/probe Frida", ok, probe, "warn" if not ok else "pass") except URLError as e: add("hook/probe", False, str(e), "warn") # 7 hook execute 只读(设备+Frida 就绪时) for action, params in [ ("get_profile", {}), ("get_contacts", {"limit": 3}), ]: try: r = _post( f"{base}/api/v3/hook/execute", { "device_id": device_id, "platform": "wechat", "action": action, "params": params, "hook_only": True, }, timeout=120, ) data = r.get("data") or {} ok = data.get("success") is True or r.get("code") == 200 and data.get("success") is not False ch = r.get("channel_used") or data.get("_channel_used") add(f"hook/execute {action}", ok, {"channel": ch, "keys": list(data.keys())[:8]}, "warn" if not ok else "pass") except URLError as e: add(f"hook/execute {action}", False, str(e), "warn") # 8 ADB 安装与桌面入口 adb_info = _check_adb_install(device_id) report["adb"] = adb_info if adb_info["expected_connected"]: add("Agent APK 已安装", adb_info.get("agent_pkg_installed") is True, AGENT_PKG) add( "桌面 Launcher 入口", bool(adb_info.get("launcher_activity")), adb_info.get("launcher_activity") or "无 LAUNCHER,需重新安装带桌面图标的 APK", "warn" if not adb_info.get("launcher_activity") else "pass", ) add("微信已安装", adb_info.get("wechat_installed") is True, WECHAT_PKG) else: add("ADB 指定设备已连接", False, adb_info["adb_serials"], "fail") # 9 workbench try: wb = _get(f"{base}/api/v3/workbench/overview", timeout=20) add("workbench/overview", wb.get("code") == 200, {"keys": list((wb.get("data") or {}).keys())[:6]}) except URLError as e: add("workbench/overview", False, str(e), "warn") return report def main(): ap = argparse.ArgumentParser(description="工作手机 WS+连接全量自检") ap.add_argument("-d", "--device", default=DEFAULT_DEVICE, help="指定设备序列号") ap.add_argument("--base", default=DEFAULT_BASE, help="SDK 地址") ap.add_argument("-o", "--output", default="", help="报告 JSON 路径") args = ap.parse_args() report = run_tests(args.base, args.device) OUT_DIR.mkdir(parents=True, exist_ok=True) out = Path(args.output) if args.output else OUT_DIR / f"ws_full_test_{args.device}_{int(time.time())}.json" out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") s = report["summary"] print("=" * 60) print(f"工作手机全量自检 · 设备 {args.device}") print(f"通过 {s['pass']} · 警告 {s['warn']} · 失败 {s['fail']}") print("-" * 60) for c in report["checks"]: icon = "✅" if c["ok"] else ("⚠️" if c["level"] == "warn" else "❌") print(f"{icon} {c['name']}") print("-" * 60) print(f"报告: {out}") print("=" * 60) sys.exit(0 if s["fail"] == 0 else 1) if __name__ == "__main__": main()