500 lines
15 KiB
Python
500 lines
15 KiB
Python
#!/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()
|