176 lines
8.1 KiB
Python
Executable File
176 lines
8.1 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
# -*- coding: utf-8 -*-
|
|
"""WP-HL-06 微信转账无界面续跑脚本。
|
|
|
|
只走 HTTP API → WebSocket Agent → Frida RPC → 回读证据。
|
|
密码仅从环境变量读取,响应和日志不写明文。
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import datetime as dt
|
|
import json
|
|
import os
|
|
import sys
|
|
import time
|
|
import urllib.error
|
|
import urllib.request
|
|
from pathlib import Path
|
|
|
|
DEFAULT_REPORT = Path("开发文档/10、项目管理/02-测试报告/20260724_WP-HL-06_转账接口_xd2214_爱笑的阿猫")
|
|
|
|
|
|
def now() -> str:
|
|
return dt.datetime.now().strftime("%Y%m%d_%H%M%S")
|
|
|
|
|
|
def request_json(method: str, url: str, body: dict | None = None, timeout: int = 75) -> tuple[int, dict]:
|
|
data = None
|
|
headers = {}
|
|
if body is not None:
|
|
safe_body = dict(body)
|
|
if "payment_password" in safe_body:
|
|
safe_body["payment_password"] = "******"
|
|
data = json.dumps(body, ensure_ascii=False).encode("utf-8")
|
|
headers["Content-Type"] = "application/json"
|
|
req = urllib.request.Request(url, data=data, headers=headers, method=method)
|
|
try:
|
|
with urllib.request.urlopen(req, timeout=timeout) as resp:
|
|
raw = resp.read().decode("utf-8", "replace")
|
|
return resp.status, json.loads(raw) if raw else {}
|
|
except urllib.error.HTTPError as e:
|
|
raw = e.read().decode("utf-8", "replace")
|
|
try:
|
|
return e.code, json.loads(raw) if raw else {"error": raw}
|
|
except Exception:
|
|
return e.code, {"error": raw}
|
|
except Exception as e:
|
|
return 0, {"success": False, "error_code": "request_failed", "error_message": str(e)}
|
|
|
|
|
|
def save(report: Path, name: str, payload: dict) -> Path:
|
|
report.mkdir(parents=True, exist_ok=True)
|
|
path = report / f"{name}_{now()}.json"
|
|
text = json.dumps(payload, ensure_ascii=False, indent=2)
|
|
# 二次兜底:不让六位支付口令形态落盘。
|
|
import re
|
|
text = re.sub(r'("payment_password"\s*:\s*")([^"]+)(")', r'\1******\3', text)
|
|
path.write_text(text, encoding="utf-8")
|
|
return path
|
|
|
|
|
|
def success_of(payload: dict) -> bool:
|
|
if payload.get("success") is True:
|
|
data = payload.get("data") if isinstance(payload.get("data"), dict) else payload
|
|
readback = data.get("readback") or data.get("db_readback") or data.get("ledger_readback") or {}
|
|
return bool(data.get("settlement_verified") or data.get("ledger_verified") or (isinstance(readback, dict) and readback.get("found")))
|
|
return False
|
|
|
|
|
|
def main() -> int:
|
|
ap = argparse.ArgumentParser(description="WP-HL-06 微信转账直到回读成功")
|
|
ap.add_argument("--base", default=os.getenv("WP_BASE", "http://127.0.0.1:8899"))
|
|
ap.add_argument("--device", default=os.getenv("WP_DEVICE", "xgfe65eimrrofyws"))
|
|
ap.add_argument("--to", default=os.getenv("WP_TRANSFER_TO", "wxid_xbhui4yjlw9a22"))
|
|
ap.add_argument("--amount", default=os.getenv("WP_TRANSFER_AMOUNT", "1.00"))
|
|
ap.add_argument("--message", default=os.getenv("WP_TRANSFER_MESSAGE", "WP-HL-06无界面转账测试"))
|
|
ap.add_argument("--report", default=os.getenv("WP_REPORT", str(DEFAULT_REPORT)))
|
|
ap.add_argument("--wait-seconds", type=int, default=int(os.getenv("WP_WAIT_SECONDS", "900")))
|
|
ap.add_argument("--poll", type=int, default=int(os.getenv("WP_POLL_SECONDS", "5")))
|
|
ap.add_argument("--execute", action="store_true", default=os.getenv("WP_PAYMENT_CONFIRM") == "1")
|
|
args = ap.parse_args()
|
|
|
|
report = Path(args.report)
|
|
password = os.getenv("WP_PAYMENT_PASSWORD", "")
|
|
if args.execute and not password:
|
|
payload = {"success": False, "status": "ready_waiting_password_env", "error_code": "payment_password_env_required", "message": "设置 WP_PAYMENT_PASSWORD 后续跑;脚本不保存明文。"}
|
|
print(json.dumps(payload, ensure_ascii=False))
|
|
save(report, "transfer_until_success_password_env_required", payload)
|
|
return 2
|
|
|
|
deadline = time.time() + args.wait_seconds
|
|
last_health = {}
|
|
while time.time() < deadline:
|
|
_, health = request_json("GET", f"{args.base}/health", timeout=10)
|
|
last_health = health
|
|
save(report, "transfer_until_success_health", health)
|
|
online = int(health.get("devices_online") or 0) > 0 and (not health.get("device_ids") or args.device in health.get("device_ids", []))
|
|
if online:
|
|
break
|
|
print(json.dumps({"stage": "wait_agent", "devices_online": health.get("devices_online"), "device_ids": health.get("device_ids", [])}, ensure_ascii=False), flush=True)
|
|
time.sleep(args.poll)
|
|
else:
|
|
payload = {"success": False, "status": "agent_offline_timeout", "health": last_health}
|
|
print(json.dumps(payload, ensure_ascii=False))
|
|
save(report, "transfer_until_success_agent_offline_timeout", payload)
|
|
return 3
|
|
|
|
for name, url in [
|
|
("hook_status", f"{args.base}/api/v3/wechat/hook/status?device_id={args.device}"),
|
|
("hook_probe", f"{args.base}/api/v3/hook/probe/{args.device}"),
|
|
("hook_actions", f"{args.base}/api/v3/hook/actions"),
|
|
]:
|
|
_, payload = request_json("GET", url, timeout=30)
|
|
save(report, f"transfer_until_success_{name}", payload)
|
|
print(json.dumps({"stage": name, "success": payload.get("success"), "code": payload.get("code")}, ensure_ascii=False), flush=True)
|
|
|
|
if not args.execute:
|
|
payload = {"success": True, "status": "ready_for_execute", "execute_env": "WP_PAYMENT_CONFIRM=1 + WP_PAYMENT_PASSWORD", "target": args.to, "amount": args.amount}
|
|
print(json.dumps(payload, ensure_ascii=False))
|
|
save(report, "transfer_until_success_ready", payload)
|
|
return 0
|
|
|
|
body = {
|
|
"device_id": args.device,
|
|
"platform": "wechat",
|
|
"to_id": args.to,
|
|
"amount": args.amount,
|
|
"message": args.message,
|
|
"payment_password": password,
|
|
"confirm": True,
|
|
"dry_run": False,
|
|
}
|
|
_, place = request_json("POST", f"{args.base}/api/v3/payment/transfer", body, timeout=90)
|
|
save(report, "transfer_until_success_placeorder", place)
|
|
print(json.dumps({"stage": "placeorder", "success": place.get("success"), "code": place.get("code")}, ensure_ascii=False), flush=True)
|
|
if success_of(place):
|
|
save(report, "transfer_until_success_final", place)
|
|
print(json.dumps({"success": True, "status": "settlement_verified", "source": "placeorder"}, ensure_ascii=False))
|
|
return 0
|
|
|
|
data = place.get("data") if isinstance(place.get("data"), dict) else place
|
|
req_key = data.get("req_key") or data.get("reqkey")
|
|
if not req_key:
|
|
payload = {"success": False, "status": "placeorder_not_ready", "placeorder_code": place.get("code"), "error_code": data.get("error_code") or data.get("error")}
|
|
save(report, "transfer_until_success_placeorder_not_ready", payload)
|
|
print(json.dumps(payload, ensure_ascii=False))
|
|
return 4
|
|
|
|
confirm_body = {
|
|
"device_id": args.device,
|
|
"platform": "wechat",
|
|
"req_key": req_key,
|
|
"to_id": args.to,
|
|
"transfer_id": data.get("transfer_id", ""),
|
|
"transaction_id": data.get("transaction_id", ""),
|
|
"payment_password": password,
|
|
"confirm": True,
|
|
"dry_run": False,
|
|
}
|
|
_, confirm = request_json("POST", f"{args.base}/api/v3/payment/transfer/confirm", confirm_body, timeout=100)
|
|
save(report, "transfer_until_success_confirm", confirm)
|
|
print(json.dumps({"stage": "confirm", "success": confirm.get("success"), "code": confirm.get("code")}, ensure_ascii=False), flush=True)
|
|
if success_of(confirm):
|
|
save(report, "transfer_until_success_final", confirm)
|
|
print(json.dumps({"success": True, "status": "settlement_verified", "source": "confirm"}, ensure_ascii=False))
|
|
return 0
|
|
|
|
payload = {"success": False, "status": "waiting_readback_or_confirm_required", "confirm_code": confirm.get("code"), "error_code": (confirm.get("data") or {}).get("error_code") if isinstance(confirm.get("data"), dict) else confirm.get("error_code")}
|
|
save(report, "transfer_until_success_waiting_readback", payload)
|
|
print(json.dumps(payload, ensure_ascii=False))
|
|
return 5
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|