Files
workphone-sdk/sdk/tests/step_verify.py

500 lines
15 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
"""
工作手机SDK - 逐步操作验证器
每个功能导航到对应UI页面 -> 执行Frida RPC -> 等待UI响应 -> 截图 -> 返回数据
确保每张截图与功能一一对应
"""
import frida
import json
import time
import os
import subprocess
from datetime import datetime
DEVICE_IP = "192.168.0.12"
FRIDA_PORT = 27042
WECHAT_PID = 7239
DEVICE_SERIAL = "xgfe65eimrrofyws"
WECHAT_PKG = "com.tencent.mm"
SCRIPT_PATH = os.path.expanduser("~/Documents/GitHub/workphone-sdk/sdk/agent/hook/wechat_hook_v2.js")
OUTPUT_DIR = os.path.expanduser("~/Documents/GitHub/workphone-sdk/verification_steps")
os.makedirs(OUTPUT_DIR, exist_ok=True)
LOG_FILE = os.path.join(OUTPUT_DIR, "verification_log.md")
class PhoneController:
"""通过ADB控制手机UI导航"""
def __init__(self, serial):
self.serial = serial
def adb(self, cmd):
full = f"adb -s {self.serial} {cmd}"
r = subprocess.run(full, shell=True, capture_output=True, timeout=10)
return r.stdout.decode("utf-8", errors="ignore").strip()
def shell(self, cmd):
return self.adb(f'shell "{cmd}"')
def screenshot(self, name):
"""截图并保存到本地,返回文件路径"""
ts = int(time.time())
fname = f"{name}_{ts}.png"
fpath = os.path.join(OUTPUT_DIR, fname)
self.shell("screencap -p /sdcard/verify_sc.png")
time.sleep(0.5)
self.adb(f"pull /sdcard/verify_sc.png {fpath}")
if os.path.exists(fpath) and os.path.getsize(fpath) > 1000:
return fpath
return ""
def tap(self, x, y):
"""点击屏幕坐标"""
self.shell(f"input tap {x} {y}")
time.sleep(0.8)
def swipe(self, x1, y1, x2, y2, duration=300):
"""滑动"""
self.shell(f"input swipe {x1} {y1} {x2} {y2} {duration}")
time.sleep(0.5)
def back(self):
"""返回键"""
self.shell("input keyevent 4")
time.sleep(0.5)
def home(self):
"""Home键"""
self.shell("input keyevent 3")
time.sleep(0.5)
def open_wechat(self):
"""打开微信主页"""
self.shell(f"am start -n {WECHAT_PKG}/.ui.LauncherUI")
time.sleep(2)
def open_wechat_contacts(self):
"""打开微信通讯录Tab"""
self.open_wechat()
time.sleep(1)
# 点击底部通讯录Tab (通常在底部第2个位置)
# 小米手机分辨率通常是1080x2400
self.tap(360, 2340) # 通讯录tab
time.sleep(1)
def open_wechat_discover(self):
"""打开微信发现Tab"""
self.open_wechat()
time.sleep(1)
self.tap(720, 2340) # 发现tab
time.sleep(1)
def open_wechat_me(self):
"""打开微信我Tab"""
self.open_wechat()
time.sleep(1)
self.tap(980, 2340) # 我tab
time.sleep(1)
def open_chat(self, name="filehelper"):
"""打开指定聊天窗口"""
self.open_wechat()
time.sleep(1)
# 点击搜索
self.tap(950, 140)
time.sleep(1)
# 输入搜索内容
self.shell(f"input text {name}")
time.sleep(1)
# 点击第一个结果
self.tap(540, 400)
time.sleep(1)
def get_screen_size(self):
"""获取屏幕分辨率"""
r = self.shell("wm size")
# Physical size: 1080x2400
if "x" in r:
parts = r.split(":")[-1].strip().split("x")
return int(parts[0]), int(parts[1])
return 1080, 2400
class StepVerifier:
"""逐步验证器"""
def __init__(self):
self.phone = PhoneController(DEVICE_SERIAL)
self.exports = None
self.script = None
self.session = None
self.log_lines = []
self.results = []
def connect(self):
"""连接Frida"""
print("[CONNECT] Connecting to Frida...")
device = frida.get_device_manager().add_remote_device(f"{DEVICE_IP}:{FRIDA_PORT}")
self.session = device.attach(WECHAT_PID)
with open(SCRIPT_PATH, "r", encoding="utf-8") as f:
source = f.read()
self.script = self.session.create_script(source)
self.script.on("message", lambda m, d: None)
self.script.load()
time.sleep(2)
self.exports = self.script.exports_sync
avail = [x for x in dir(self.exports) if not x.startswith("_")]
print(f" Loaded {len(avail)} methods")
pong = self.exports.ping()
print(f" ping: {pong}")
# 获取屏幕尺寸
w, h = self.phone.get_screen_size()
print(f" Screen: {w}x{h}")
return True
def log(self, text):
print(text)
self.log_lines.append(text)
def verify_step(self, module, method, description, nav_action, params=None, wait_after=1):
"""
执行一个验证步骤:
1. 导航到对应页面
2. 截图(操作前)
3. 执行RPC
4. 等待UI响应
5. 截图(操作后)
6. 记录结果
"""
self.log(f"\n{'='*60}")
self.log(f" [{module}] {method}")
self.log(f" 描述: {description}")
self.log(f"{'='*60}")
# Step 1: 导航
self.log(f" [NAV] {nav_action.__doc__ or 'navigating...'}")
nav_action()
time.sleep(1)
# Step 2: 操作前截图
before_img = self.phone.screenshot(f"{module}__{method}__before")
self.log(f" [BEFORE] 截图: {os.path.basename(before_img) if before_img else 'FAILED'}")
# Step 3: 执行RPC
fn = getattr(self.exports, method, None)
if fn is None:
self.log(f" [ERROR] 方法不存在: {method}")
self.results.append({
"module": module, "method": method, "status": "MISSING",
"description": description, "data": None,
"before_img": before_img, "after_img": "",
})
return
start = time.time()
try:
if params:
result = fn(params)
else:
result = fn()
latency = int((time.time() - start) * 1000)
except Exception as e:
latency = int((time.time() - start) * 1000)
result = {"success": False, "error": str(e)[:200]}
self.log(f" [EXEC] {method}({json.dumps(params, ensure_ascii=False)[:50] if params else ''})")
self.log(f" [TIME] {latency}ms")
# 格式化结果
if isinstance(result, str):
self.log(f" [DATA] {result[:100]}")
data_preview = result
elif isinstance(result, dict):
data_preview = json.dumps(result, ensure_ascii=False, indent=2)[:300]
self.log(f" [DATA] {data_preview[:150]}")
elif isinstance(result, list):
data_preview = f"[{len(result)} items] " + json.dumps(result[:2], ensure_ascii=False)[:200]
self.log(f" [DATA] {data_preview[:150]}")
else:
data_preview = str(result)[:100]
self.log(f" [DATA] {data_preview}")
# Step 4: 等待UI响应
time.sleep(wait_after)
# Step 5: 操作后截图
after_img = self.phone.screenshot(f"{module}__{method}__after")
self.log(f" [AFTER] 截图: {os.path.basename(after_img) if after_img else 'FAILED'}")
# 判断状态
if isinstance(result, dict) and result.get("success") == False:
status = "EXEC_ERR"
else:
status = "PASS"
self.log(f" [STATUS] {status}")
self.results.append({
"module": module, "method": method, "status": status,
"description": description, "latency_ms": latency,
"data": result if isinstance(result, (dict, list, str, bool, int, float)) else str(result),
"before_img": before_img, "after_img": after_img,
})
def run_all(self):
"""运行所有验证步骤"""
self.log("# 工作手机SDK - 逐步真机验证")
self.log(f"> 时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
self.log(f"> 设备: {DEVICE_IP}:{FRIDA_PORT}")
self.log("")
# ===== 模块1: 系统状态 =====
self.log("\n## 模块1: 系统状态\n")
self.verify_step(
"SYS", "getProcessInfo",
"获取微信进程信息(PID/架构/模块数)",
lambda: self.phone.open_wechat(),
)
self.verify_step(
"SYS", "getWechatVersion",
"获取微信版本号",
lambda: None, # 不需要导航,直接在当前页面
)
self.verify_step(
"SYS", "getHookStatus",
"检查Hook是否已激活",
lambda: None,
)
# ===== 模块2: 联系人 =====
self.log("\n## 模块2: 联系人\n")
self.verify_step(
"H16_联系人", "getContacts",
"打开通讯录 -> 获取联系人列表",
lambda: self.phone.open_wechat_contacts(),
params={"limit": 10},
)
self.verify_step(
"H16_联系人", "getContactInfo",
"获取文件传输助手的详细信息",
lambda: None, # 保持在通讯录页面
params={"wxid": "filehelper"},
)
self.verify_step(
"H16_联系人", "searchContacts",
"搜索包含'文件'的联系人",
lambda: None,
params={"keyword": "文件", "limit": 5},
)
# ===== 模块3: 消息发送 =====
self.log("\n## 模块3: 消息发送\n")
msg_content = f"[SDK验证] {datetime.now().strftime('%H:%M:%S')} 测试消息"
self.verify_step(
"H17_消息发送", "sendMessage",
f"打开文件传输助手对话 -> 发送文本消息: '{msg_content}'",
lambda: self.phone.open_chat("filehelper"),
params={"to_id": "filehelper", "content": msg_content, "msg_type": "text"},
wait_after=2,
)
# ===== 模块4: 消息接收 =====
self.log("\n## 模块4: 消息接收\n")
self.verify_step(
"H15_消息接收", "getRecentMessages",
"获取最近消息列表",
lambda: self.phone.open_wechat(),
params={"limit": 5},
)
self.verify_step(
"H15_消息接收", "getMessages",
"获取filehelper的消息记录",
lambda: None,
params={"conversation_id": "filehelper", "limit": 5},
)
# ===== 模块5: 好友管理 =====
self.log("\n## 模块5: 好友管理\n")
self.verify_step(
"H19_好友管理", "setFriendRemark",
"修改文件传输助手的备注名为'SDK文件助手'",
lambda: self.phone.open_wechat_contacts(),
params={"wxid": "filehelper", "remark": "SDK文件助手"},
wait_after=2,
)
self.verify_step(
"H18_好友请求", "getFriendRequests",
"获取好友请求列表",
lambda: None,
params={"limit": 5},
)
# ===== 模块6: 群管理 =====
self.log("\n## 模块6: 群管理\n")
self.verify_step(
"H22_群管理", "getGroups",
"获取群聊列表",
lambda: self.phone.open_wechat(),
params={"limit": 10},
)
# ===== 模块7: 个人信息 =====
self.log("\n## 模块7: 个人信息\n")
self.verify_step(
"H23_账号管理", "getProfile",
"打开''页面 -> 获取个人资料",
lambda: self.phone.open_wechat_me(),
params={},
)
self.verify_step(
"H23_账号管理", "checkAccountStatus",
"检查账号状态",
lambda: None,
params={},
)
# ===== 模块8: 标签 =====
self.log("\n## 模块8: 标签\n")
self.verify_step(
"H28_标签", "getLabels",
"获取标签列表",
lambda: None,
params={},
)
# ===== 模块9: 朋友圈 =====
self.log("\n## 模块9: 朋友圈\n")
self.verify_step(
"H21_朋友圈", "getMoments",
"打开发现页 -> 获取朋友圈动态",
lambda: self.phone.open_wechat_discover(),
params={"wxid": "", "limit": 3},
)
# ===== 模块10: 搜索 =====
self.log("\n## 模块10: 搜索\n")
self.verify_step(
"H31_搜索", "globalSearch",
"全局搜索'微信'",
lambda: self.phone.open_wechat(),
params={"keyword": "微信", "limit": 5},
)
# ===== 模块11: 设备信息 =====
self.log("\n## 模块11: 设备信息\n")
self.verify_step(
"H39_设备信息", "getDeviceInfo",
"获取设备信息",
lambda: None,
params={},
)
self.verify_step(
"H39_设备信息", "getNetworkInfo",
"获取网络信息",
lambda: None,
params={},
)
self.verify_step(
"H39_设备信息", "getStorageInfo",
"获取存储信息",
lambda: None,
params={},
)
# ===== 模块12: 账号安全 =====
self.log("\n## 模块12: 账号安全\n")
self.verify_step(
"H24_账号安全", "getLoginDevices",
"获取登录设备列表",
lambda: self.phone.open_wechat_me(),
params={},
)
self.verify_step(
"H35_注册登录", "checkLoginState",
"检查登录状态",
lambda: None,
params={},
)
# ===== 汇总 =====
self.summarize()
def summarize(self):
"""汇总结果"""
total = len(self.results)
passed = sum(1 for r in self.results if r["status"] in ("PASS", "EXEC_ERR"))
missing = sum(1 for r in self.results if r["status"] == "MISSING")
self.log(f"\n{'='*60}")
self.log(f" 验证完成!")
self.log(f" 总计: {total}")
self.log(f" 通过: {passed}")
self.log(f" 缺失: {missing}")
self.log(f" 通过率: {passed/total*100:.1f}%")
self.log(f"{'='*60}")
# 保存日志
with open(LOG_FILE, "w", encoding="utf-8") as f:
f.write("\n".join(self.log_lines))
print(f"\n日志: {LOG_FILE}")
# 保存JSON
result_file = os.path.join(OUTPUT_DIR, "step_results.json")
with open(result_file, "w", encoding="utf-8") as f:
json.dump({
"time": datetime.now().isoformat(),
"total": total,
"passed": passed,
"missing": missing,
"results": self.results,
}, f, ensure_ascii=False, indent=2, default=str)
print(f"结果: {result_file}")
def cleanup(self):
try:
self.script.unload()
self.session.detach()
except:
pass
def main():
v = StepVerifier()
try:
if v.connect():
v.run_all()
finally:
v.cleanup()
if __name__ == "__main__":
main()