Files
workphone-sdk/sdk/scripts/workphone_execution_preflight.py

195 lines
7.2 KiB
Python
Executable File

#!/usr/bin/env python3
"""工作手机执行前只读总门禁:一次检查高频重复问题。"""
from __future__ import annotations
import argparse
import hashlib
import json
import subprocess
import sys
from datetime import datetime
from pathlib import Path
from typing import Any
from urllib.error import URLError
from urllib.request import ProxyHandler, build_opener
ROOT = Path(__file__).resolve().parents[1]
HOOK_MIRRORS = [
ROOT / "agent/hook/wechat_hook_v2.js",
ROOT / "app/agent/hook/wechat_hook_v2.js",
ROOT / "android-app/app/src/main/assets/wechat_hook_v2.js",
]
WRITE_CONTRACTS = {
"/api/v3/message/send": {"dry_run", "confirm", "idempotency_key", "trace_id"},
"/api/v3/moments/post": {"dry_run", "confirm", "idempotency_key", "trace_id"},
"/api/v3/wechat/friend-group/execute": {
"dry_run", "confirm", "idempotency_key", "trace_id",
},
"/api/v3/payment/transfer": {
"dry_run", "confirm", "idempotency_key", "trace_id",
},
}
def sha256(path: Path) -> str:
digest = hashlib.sha256()
with path.open("rb") as handle:
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
digest.update(chunk)
return digest.hexdigest()
def schema_properties(openapi: dict[str, Any], path: str) -> set[str]:
schema = (
openapi.get("paths", {})
.get(path, {})
.get("post", {})
.get("requestBody", {})
.get("content", {})
.get("application/json", {})
.get("schema", {})
)
ref = str(schema.get("$ref") or "")
if ref:
schema = openapi.get("components", {}).get("schemas", {}).get(ref.rsplit("/", 1)[-1], {})
return set((schema.get("properties") or {}).keys())
def main() -> int:
parser = argparse.ArgumentParser()
parser.add_argument("--base", default="http://127.0.0.1:8899")
parser.add_argument("--device-id", default="")
parser.add_argument("--timeout", type=float, default=5)
parser.add_argument("--output", type=Path)
parser.add_argument("--skip-interface-audit", action="store_true")
args = parser.parse_args()
opener = build_opener(ProxyHandler({}))
def get_json(path: str) -> dict[str, Any]:
with opener.open(args.base.rstrip("/") + path, timeout=args.timeout) as response:
return json.load(response)
checks: list[dict[str, Any]] = []
def add(name: str, ok: bool, *, blocker: str = "", detail: Any = None) -> None:
checks.append({"name": name, "ok": ok, "blocker": blocker if not ok else "", "detail": detail})
health: dict[str, Any] = {}
devices: list[dict[str, Any]] = []
try:
health = get_json("/health")
add(
"sdk_health",
health.get("status") == "healthy",
blocker="environment/sdk_unhealthy",
detail={
"version": health.get("version"),
"devices_online": health.get("devices_online"),
"device_ids": health.get("device_ids"),
},
)
except (OSError, URLError, ValueError, json.JSONDecodeError) as exc:
add("sdk_health", False, blocker="environment/sdk_unreachable", detail=type(exc).__name__)
try:
payload = get_json("/api/v3/devices")
devices = payload.get("data") if isinstance(payload.get("data"), list) else []
online = [item for item in devices if item.get("status") == "online"]
add(
"single_online_device",
len(online) == 1,
blocker="connection/device_identity_not_unique",
detail=[item.get("device_id") for item in online],
)
except (OSError, URLError, ValueError, json.JSONDecodeError) as exc:
online = []
add("single_online_device", False, blocker="connection/device_list_failed", detail=type(exc).__name__)
device_id = args.device_id or (str(online[0].get("device_id")) if len(online) == 1 else "")
if device_id:
try:
hook = get_json(f"/api/v3/hook/probe/{device_id}")
hook_ok = bool(hook.get("supports_hook") and hook.get("hook_available_reported"))
add(
"hook_rpc_ready",
hook_ok,
blocker="capability/frida_hook_not_attached",
detail={
"device_id": device_id,
"supports_hook": hook.get("supports_hook"),
"hook_available_reported": hook.get("hook_available_reported"),
"frida_available_reported": hook.get("frida_available_reported"),
"transport": hook.get("transport"),
"probe_detail": hook.get("probe_detail"),
},
)
except (OSError, URLError, ValueError, json.JSONDecodeError) as exc:
add("hook_rpc_ready", False, blocker="capability/hook_probe_failed", detail=type(exc).__name__)
else:
add("hook_rpc_ready", False, blocker="connection/no_unique_device", detail={})
hashes = {str(path.relative_to(ROOT)): sha256(path) for path in HOOK_MIRRORS if path.exists()}
add(
"hook_source_sha_consistent",
len(hashes) == len(HOOK_MIRRORS) and len(set(hashes.values())) == 1,
blocker="runtime/hook_mirror_sha_drift",
detail=hashes,
)
try:
openapi = get_json("/openapi.json")
missing: dict[str, list[str]] = {}
for path, required in WRITE_CONTRACTS.items():
absent = sorted(required - schema_properties(openapi, path))
if absent:
missing[path] = absent
add(
"write_contract_fields",
not missing,
blocker="contract/openapi_write_gate_fields_missing",
detail=missing,
)
except (OSError, URLError, ValueError, json.JSONDecodeError) as exc:
add("write_contract_fields", False, blocker="contract/openapi_unavailable", detail=type(exc).__name__)
if not args.skip_interface_audit:
audit = subprocess.run(
[sys.executable, str(ROOT / "scripts/wechat_interface_audit.py")],
cwd=ROOT.parent,
capture_output=True,
text=True,
timeout=30,
)
add(
"action_mapping_audit",
audit.returncode == 0,
blocker="capability/action_mapping_drift",
detail=(audit.stdout + audit.stderr).strip().splitlines()[-8:],
)
hard_read_checks = {"sdk_health", "single_online_device"}
ready_for_read = all(item["ok"] for item in checks if item["name"] in hard_read_checks)
ready_for_write = all(item["ok"] for item in checks)
result = {
"checked_at": datetime.now().astimezone().isoformat(),
"sdk_base": args.base,
"device_id": device_id,
"ready_for_read": ready_for_read,
"ready_for_write": ready_for_write,
"blockers": [item["blocker"] for item in checks if not item["ok"]],
"checks": checks,
}
rendered = json.dumps(result, ensure_ascii=False, indent=2)
print(rendered)
if args.output:
args.output.parent.mkdir(parents=True, exist_ok=True)
args.output.write_text(rendered + "\n", encoding="utf-8")
return 0 if ready_for_write else 2
if __name__ == "__main__":
raise SystemExit(main())