feat: publish workphone SDK deployment and API docs
This commit is contained in:
303
sdk/scripts/ws_full_test.py
Executable file
303
sdk/scripts/ws_full_test.py
Executable file
@@ -0,0 +1,303 @@
|
||||
#!/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()
|
||||
Reference in New Issue
Block a user