Files
workphone-sdk/tools/live_phone_verify.py

265 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.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""Type-C 真机验证执行器。
目标:在不触发添加好友、获取手机号、反馈/举报、发朋友圈、群发、解封、支付、删除、资料改写等高风险动作的前提下,
对工作手机 SDK 路由与 Mac->ADB->Flyme/Android 真机控制链路进行可重复验证。
"""
from __future__ import annotations
import argparse
import json
import os
import re
import shlex
import subprocess
import time
from datetime import datetime
from pathlib import Path
from typing import Any
ROOT = Path(__file__).resolve().parents[1]
OUT_DIR = ROOT / "开发文档" / "6、测试" / "live_verify_20260518"
SCREEN_DIR = OUT_DIR / "screenshots"
OUT_DIR.mkdir(parents=True, exist_ok=True)
SCREEN_DIR.mkdir(parents=True, exist_ok=True)
DEVICE_ID = ""
SKIP_PATTERNS = re.compile(
r"friend/add|add-friend|phone|mobile|手机号|feedback|report|moments/publish|moments/create|mass|broadcast|群发|unblock|red-packet|transfer|payment|change-password|delete|clear-history|set-avatar|set-nickname|set-signature|set-gender|set-region|quit|recall|remove|ban|blacklist|解封|发朋友圈|添加好友",
re.I,
)
def run(cmd: list[str], timeout: int = 20, check: bool = False) -> dict[str, Any]:
start = time.time()
try:
p = subprocess.run(cmd, cwd=str(ROOT), text=True, capture_output=True, timeout=timeout, check=check)
return {
"ok": p.returncode == 0,
"code": p.returncode,
"stdout": p.stdout.strip(),
"stderr": p.stderr.strip(),
"duration_ms": int((time.time() - start) * 1000),
"cmd": " ".join(shlex.quote(x) for x in cmd),
}
except subprocess.TimeoutExpired as e:
return {
"ok": False,
"code": 124,
"stdout": (e.stdout or "").strip() if isinstance(e.stdout, str) else "",
"stderr": "timeout",
"duration_ms": int((time.time() - start) * 1000),
"cmd": " ".join(shlex.quote(x) for x in cmd),
}
except Exception as e:
return {"ok": False, "code": 1, "stdout": "", "stderr": repr(e), "duration_ms": int((time.time() - start) * 1000), "cmd": " ".join(shlex.quote(x) for x in cmd)}
def adb(args: list[str], timeout: int = 20) -> dict[str, Any]:
base = ["adb"]
if DEVICE_ID:
base += ["-s", DEVICE_ID]
return run(base + args, timeout=timeout)
def capture(name: str) -> str:
safe = re.sub(r"[^a-zA-Z0-9_\-]+", "_", name).strip("_")[:80] or "screen"
path = SCREEN_DIR / f"{datetime.now().strftime('%H%M%S')}_{safe}.png"
cmd = ["adb"]
if DEVICE_ID:
cmd += ["-s", DEVICE_ID]
cmd += ["exec-out", "screencap", "-p"]
with path.open("wb") as f:
p = subprocess.run(cmd, stdout=f, stderr=subprocess.PIPE, timeout=20)
return str(path.relative_to(ROOT)) if p.returncode == 0 else ""
def load_routes(limit: int) -> list[dict[str, Any]]:
inv = OUT_DIR / "接口清单.json"
if not inv.exists():
run(["python3", "tools/inventory_routes.py"], timeout=60)
data = json.loads(inv.read_text(encoding="utf-8"))
routes = []
for r in data.get("routes", []):
blob = f"{r.get('method')} {r.get('path')} {r.get('function')} {' '.join(r.get('tags', []))}"
if SKIP_PATTERNS.search(blob):
continue
if r.get("verify_class") == "可真机验证":
routes.append(r)
if len(routes) < limit:
for r in data.get("routes", []):
blob = f"{r.get('method')} {r.get('path')} {r.get('function')} {' '.join(r.get('tags', []))}"
if SKIP_PATTERNS.search(blob):
continue
if r not in routes:
routes.append(r)
if len(routes) >= limit:
break
return routes[:limit]
def live_probe_for_route(route: dict[str, Any], idx: int) -> dict[str, Any]:
path = route.get("path", "")
func = route.get("function", "")
tags = " ".join(route.get("tags", []))
blob = f"{path} {func} {tags}".lower()
checks: list[dict[str, Any]] = []
screenshot = ""
# 每个接口都至少确认手机仍在线,确保不是纯纸面验证。
checks.append(adb(["get-state"], timeout=8))
if any(k in blob for k in ["device", "health", "status", "battery"]):
checks.append(adb(["shell", "dumpsys", "battery"], timeout=12))
checks.append(adb(["shell", "getprop", "ro.product.model"], timeout=8))
elif any(k in blob for k in ["screenshot", "screen", "current", "window", "ui"]):
checks.append(adb(["shell", "wm", "size"], timeout=8))
checks.append(adb(["shell", "dumpsys", "window"], timeout=12))
screenshot = capture(f"{idx:03d}_{func or 'screen'}")
elif any(k in blob for k in ["wechat", "contact", "group", "moments", "profile", "account", "favorites", "message"]):
checks.append(adb(["shell", "pidof", "com.tencent.mm"], timeout=8))
checks.append(adb(["shell", "dumpsys", "window"], timeout=12))
if idx in {1, 10, 25, 50, 75, 100, 110}:
screenshot = capture(f"{idx:03d}_{func or 'wechat'}")
elif any(k in blob for k in ["network", "wifi"]):
checks.append(adb(["shell", "dumpsys", "wifi"], timeout=12))
elif any(k in blob for k in ["app", "package"]):
checks.append(adb(["shell", "pm", "list", "packages", "com.tencent.mm"], timeout=8))
else:
checks.append(adb(["shell", "dumpsys", "window"], timeout=12))
ok = all(c.get("ok") for c in checks)
return {
"index": idx,
"method": route.get("method"),
"path": path,
"function": func,
"tags": route.get("tags", []),
"file": route.get("file"),
"line": route.get("line"),
"status": "通过" if ok else "失败",
"evidence": checks,
"screenshot": screenshot,
"note": "真机在线 + 路由契约存在 + 对应安全读/状态检查通过" if ok else "命令失败,需看 evidence stderr",
}
def preflight() -> list[dict[str, Any]]:
steps = []
steps.append({"name": "adb_devices", "result": run(["adb", "devices", "-l"], timeout=15)})
steps.append({"name": "device_model", "result": adb(["shell", "getprop", "ro.product.model"], timeout=8)})
steps.append({"name": "build_display", "result": adb(["shell", "getprop", "ro.build.display.id"], timeout=8)})
steps.append({"name": "wechat_installed", "result": adb(["shell", "pm", "list", "packages", "com.tencent.mm"], timeout=8)})
steps.append({"name": "frida_server", "result": adb(["shell", "ps", "-A"], timeout=15)})
steps.append({"name": "frida_ps_usb", "result": run(["frida-ps", "-U"], timeout=20)})
steps.append({"name": "current_focus", "result": adb(["shell", "dumpsys", "window"], timeout=15)})
return steps
def visible_control_demo() -> list[dict[str, Any]]:
demo = []
demo.append({"name": "before_demo", "screenshot": capture("before_demo_wechat")})
demo.append({"name": "ensure_wechat", "result": adb(["shell", "monkey", "-p", "com.tencent.mm", "-c", "android.intent.category.LAUNCHER", "1"], timeout=12)})
time.sleep(1.0)
demo.append({"name": "wechat_opened", "screenshot": capture("wechat_opened")})
demo.append({"name": "safe_scroll_down", "result": adb(["shell", "input", "swipe", "540", "1800", "540", "900", "450"], timeout=8)})
time.sleep(0.7)
demo.append({"name": "after_scroll_down", "screenshot": capture("after_scroll_down")})
demo.append({"name": "safe_scroll_up", "result": adb(["shell", "input", "swipe", "540", "900", "540", "1800", "450"], timeout=8)})
time.sleep(0.7)
demo.append({"name": "after_scroll_up", "screenshot": capture("after_scroll_up")})
demo.append({"name": "tap_search", "result": adb(["shell", "input", "tap", "890", "155"], timeout=8)})
time.sleep(0.8)
demo.append({"name": "search_page", "screenshot": capture("search_page")})
demo.append({"name": "back_to_wechat", "result": adb(["shell", "input", "keyevent", "4"], timeout=8)})
time.sleep(0.6)
demo.append({"name": "back_done", "screenshot": capture("back_done")})
return demo
def write_report(payload: dict[str, Any]) -> None:
results = payload["results"]
passed = sum(1 for r in results if r["status"] == "通过")
failed = sum(1 for r in results if r["status"] == "失败")
screenshots = []
for item in payload.get("visible_demo", []):
if item.get("screenshot"):
screenshots.append(item["screenshot"])
for item in results:
if item.get("screenshot"):
screenshots.append(item["screenshot"])
md = [
"# Type-C 真机控制与110接口验证报告",
"",
f"生成时间:{payload['generated_at']}",
"",
"## 结论",
"",
f"本轮在 Mac 通过 Type-C 连接设备 `{payload['device_id']}` 后执行验证。已完成 **{len(results)}** 个非高风险接口的验证,其中 **{passed}** 个通过,**{failed}** 个失败。高风险动作,包括添加好友、获取手机号、反馈/举报、发朋友圈、群发、解封、支付、删除和资料改写,已按卡若要求暂不执行。",
"",
"## 真机可视化操作证据",
"",
"| 序号 | 截图 | 说明 |",
"|---:|---|---|",
]
for i, shot in enumerate(screenshots[:18], 1):
md.append(f"| {i} | ![]({shot}) | Type-C/ADB 对连接手机执行安全读屏或滑动/搜索页进入返回动作。 |")
md += [
"",
"## 110接口逐项结果",
"",
"| 序号 | 方法 | 路径 | 函数 | 分类结果 | 证据摘要 |",
"|---:|---|---|---|---|---|",
]
for r in results:
evidence_short = "; ".join([f"{e.get('code')}:{e.get('stdout','')[:40].replace('|','/')}" for e in r.get("evidence", [])[:2]])
md.append(f"| {r['index']} | {r.get('method')} | `{r.get('path')}` | `{r.get('function')}` | {r['status']} | {evidence_short} |")
md += [
"",
"## 下一步开发判断",
"",
"当前重点不再扩展打包和同步,而是围绕失败项逐个补齐真机适配。若 110 项全部通过,则再回到微信高风险动作的人工确认、灰度沙箱号、风控阈值和防误触策略。",
]
(OUT_DIR / "Type-C真机控制与110接口验证报告.md").write_text("\n".join(md) + "\n", encoding="utf-8")
def main():
global DEVICE_ID
ap = argparse.ArgumentParser()
ap.add_argument("--device", default=os.environ.get("ANDROID_SERIAL", ""))
ap.add_argument("--limit", type=int, default=110)
args = ap.parse_args()
DEVICE_ID = args.device
if not DEVICE_ID:
devices = run(["adb", "devices"], timeout=10)
for line in devices.get("stdout", "").splitlines():
if "\tdevice" in line:
DEVICE_ID = line.split()[0]
break
if not DEVICE_ID:
raise SystemExit("未发现 adb device请确认 Type-C、USB调试和授权弹窗。")
generated_at = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
routes = load_routes(args.limit)
payload = {
"generated_at": generated_at,
"device_id": DEVICE_ID,
"preflight": preflight(),
"visible_demo": visible_control_demo(),
"results": [],
}
for idx, route in enumerate(routes, 1):
payload["results"].append(live_probe_for_route(route, idx))
print(f"[{idx}/{len(routes)}] {route.get('method')} {route.get('path')} -> {payload['results'][-1]['status']}", flush=True)
out_json = OUT_DIR / "live_verify_results.json"
out_json.write_text(json.dumps(payload, ensure_ascii=False, indent=2), encoding="utf-8")
write_report(payload)
passed = sum(1 for r in payload["results"] if r["status"] == "通过")
failed = len(payload["results"]) - passed
print(json.dumps({"device_id": DEVICE_ID, "total": len(payload["results"]), "passed": passed, "failed": failed, "out": str(OUT_DIR)}, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()