#!/usr/bin/env python3 """工作手机 SDK v9-final 完整验证脚本""" import frida, json, time, subprocess, re, sys, os PHONE_IP = '192.168.110.80' JS_FILE = '/Users/karuo/Documents/开发/2、私域银行/工作手机/sdk/agent/hook/wechat_api_v9_final.js' SCREENSHOT_DIR = '/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/6、测试' def camel_to_snake(name): s1 = re.sub('(.)([A-Z][a-z]+)', r'\1_\2', name) return re.sub('([a-z0-9])([A-Z])', r'\1_\2', s1).lower() def call(ex, method, *args): snake = camel_to_snake(method) return getattr(ex, snake)(*args) def screenshot(name): ts = int(time.time()) remote = f'/sdcard/{name}_{ts}.png' local = f'{SCREENSHOT_DIR}/{name}_{ts}.png' subprocess.run(['adb', '-s', f'{PHONE_IP}:5555', 'shell', f'screencap -p {remote}'], timeout=5, capture_output=True) time.sleep(0.5) subprocess.run(['adb', '-s', f'{PHONE_IP}:5555', 'pull', remote, local], timeout=10, capture_output=True) return local def p(label, data, show_rows=3): print(f"\n{'='*50}") print(f" {label}") print(f"{'='*50}") if isinstance(data, dict): if 'error' in data: print(f" ❌ 错误: {data['error']}") elif 'rows' in data: print(f" ✅ 返回 {data.get('count', len(data['rows']))} 条") for row in data['rows'][:show_rows]: print(f" {json.dumps(row, ensure_ascii=False)}") else: for k, v in data.items(): if isinstance(v, (dict, list)): print(f" {k}: {json.dumps(v, ensure_ascii=False)[:100]}") else: print(f" {k}: {v}") else: print(f" {data}") print("\n" + "="*60) print(" 工作手机 SDK v9-final 完整验证") print("="*60) # 设置ADB端口转发 subprocess.run(['adb', '-s', f'{PHONE_IP}:5555', 'forward', 'tcp:27042', 'tcp:27042'], timeout=5, capture_output=True) # 获取微信PID result = subprocess.run(['adb', '-s', f'{PHONE_IP}:5555', 'shell', 'ps -A'], capture_output=True, text=True, timeout=5) pid = None for line in result.stdout.split('\n'): if 'com.tencent.mm' in line and ':push' not in line and ':sandbox' not in line and ':tools' not in line and ':appbrand' not in line and ':xweb' not in line: parts = line.split() if len(parts) >= 2: try: pid = int(parts[1]) print(f"\n 微信主进程 PID: {pid}") break except: pass if not pid: print("❌ 微信主进程未运行,正在启动...") subprocess.run(['adb', '-s', f'{PHONE_IP}:5555', 'shell', 'monkey -p com.tencent.mm -c android.intent.category.LAUNCHER 1'], timeout=10, capture_output=True) time.sleep(8) result = subprocess.run(['adb', '-s', f'{PHONE_IP}:5555', 'shell', 'ps -A'], capture_output=True, text=True, timeout=5) for line in result.stdout.split('\n'): if 'com.tencent.mm' in line and ':push' not in line and ':sandbox' not in line: parts = line.split() if len(parts) >= 2: try: pid = int(parts[1]); break except: pass # 连接Frida dm = frida.get_device_manager() device = dm.add_remote_device('localhost:27042') session = device.attach(pid) with open(JS_FILE, 'r', encoding='utf-8') as f: js_code = f.read() messages = [] def on_message(msg, data): if msg.get('type') == 'send': messages.append(msg['payload']) print(f" [Frida] {msg['payload']}") script = session.create_script(js_code) script.on('message', on_message) script.load() print(" 等待DB初始化...") time.sleep(4) # 等待Java.choose完成 ex = script.exports_sync # ===== 测试开始 ===== results = {} passed = 0 failed = 0 def test(name, func): global passed, failed try: r = func() if isinstance(r, dict) and 'error' in r: print(f" ❌ [{name}] {r['error'][:80]}") failed += 1 results[name] = {'status': 'FAIL', 'error': r['error'][:80]} else: count = r.get('count', '-') if isinstance(r, dict) else '-' print(f" ✅ [{name}] count={count}") passed += 1 results[name] = {'status': 'PASS', 'data': r} return r except Exception as e: print(f" ❌ [{name}] Exception: {str(e)[:80]}") failed += 1 results[name] = {'status': 'FAIL', 'error': str(e)[:80]} return None print("\n--- 1. 系统初始化 ---") r = test('ping', lambda: call(ex, 'ping')) if r: print(f" DB状态: {json.dumps(r.get('dbs', {}), ensure_ascii=False)}") r = test('initDbs', lambda: call(ex, 'initDbs')) if r: print(f" DB状态: {json.dumps(r.get('status', {}), ensure_ascii=False)}") print("\n--- 2. 账号信息 ---") r = test('getAccountInfo', lambda: call(ex, 'getAccountInfo')) if r and not r.get('error'): print(f" 账号: {r.get('wxid')} | 昵称: {r.get('nickname')} | 手机: {r.get('phone')}") print("\n--- 3. 联系人(修复版)---") r = test('getContactCount', lambda: call(ex, 'getContactCount')) if r: print(f" 好友: {r.get('friends')} | 总计: {r.get('total')}") r = test('getContacts(100)', lambda: call(ex, 'getContacts', 100, 0)) if r and r.get('count', 0) > 0: print(f" 前3条: {json.dumps(r['rows'][:3], ensure_ascii=False)[:200]}") r = test('searchContacts', lambda: call(ex, 'searchContacts', '游')) if r: print(f" 搜索'游': {r.get('count')} 条") print("\n--- 4. 标签(修复版)---") r = test('getLabels', lambda: call(ex, 'getLabels')) if r and r.get('count', 0) > 0: print(f" 前5个标签: {json.dumps(r['rows'][:5], ensure_ascii=False)[:200]}") print("\n--- 5. 统计 ---") r = test('getAllStats', lambda: call(ex, 'getAllStats')) if r and not r.get('error'): print(f" 好友:{r.get('friends')} 群:{r.get('groups')} 消息:{r.get('messages')} 图片:{r.get('images')} 视频:{r.get('videos')} 标签:{r.get('labels')}") print("\n--- 6. 消息 ---") r = test('getRecentMessages(24h)', lambda: call(ex, 'getRecentMessages', 24)) if r: print(f" 24小时消息: {r.get('count')} 条") if r.get('rows'): print(f" 最新: {json.dumps(r['rows'][0], ensure_ascii=False)[:150]}") # 截图(发送前) subprocess.run(['adb', '-s', f'{PHONE_IP}:5555', 'shell', 'input keyevent 26'], timeout=3, capture_output=True) time.sleep(0.5) subprocess.run(['adb', '-s', f'{PHONE_IP}:5555', 'shell', 'input swipe 540 1200 540 600'], timeout=3, capture_output=True) time.sleep(0.5) sc_before = screenshot('v9_before_send') print(f"\n 📸 发送前截图: {sc_before}") # 发送消息 print("\n--- 7. 发送消息 ---") r = test('sendMessage', lambda: call(ex, 'sendMessage', 'filehelper', f'[工作手机SDK v9-final] 测试消息 {time.strftime("%H:%M:%S")}', 1)) if r: print(f" msgId: {r.get('msgId')} | DB验证: {r.get('db_verified')}") print(f" 说明: {r.get('note', '')}") # 截图(发送后) time.sleep(1) sc_after = screenshot('v9_after_send') print(f" 📸 发送后截图: {sc_after}") print("\n--- 8. 群发消息 ---") r = test('massMessage', lambda: call(ex, 'massMessage', ['filehelper', 'wxid_xvlpukwd3i8r12'], f'[群发测试] {time.strftime("%H:%M:%S")}')) if r: print(f" 总计:{r.get('total')} 成功:{r.get('success')}") print("\n--- 9. 群组(修复版)---") r = test('getGroupCount', lambda: call(ex, 'getGroupCount')) if r: print(f" 群组总数: {r.get('rows', [{}])[0].get('total', '?')}") r = test('getGroups(10)', lambda: call(ex, 'getGroups', 10)) if r and r.get('count', 0) > 0: print(f" 前3个群: {json.dumps(r['rows'][:3], ensure_ascii=False)[:300]}") # 获取第一个群的成员 first_group = r['rows'][0].get('chatroomname') if first_group: r2 = test('getGroupMembers', lambda: call(ex, 'getGroupMembers', first_group)) if r2: print(f" 群{first_group}: {r2.get('memberCount')}人, 群主:{r2.get('owner')}") print("\n--- 10. 朋友圈 ---") # 先触发朋友圈DB加载 subprocess.run(['adb', '-s', f'{PHONE_IP}:5555', 'shell', 'am start -n com.tencent.mm/.plugin.sns.ui.SnsTimeLineUI'], timeout=5, capture_output=True) time.sleep(3) r = test('getSnsDbInfo', lambda: call(ex, 'getSnsDbInfo')) if r: print(f" SNS DB: {json.dumps(r, ensure_ascii=False)}") r = test('getMoments(5)', lambda: call(ex, 'getMoments', 5)) if r and r.get('count', 0) > 0: print(f" 朋友圈: {r.get('count')} 条") print(f" 最新: {json.dumps(r['rows'][0], ensure_ascii=False)[:200]}") # 截图朋友圈 time.sleep(1) sc_sns = screenshot('v9_moments') print(f" 📸 朋友圈截图: {sc_sns}") print("\n--- 11. 视频号 ---") r = test('getFinderInfo', lambda: call(ex, 'getFinderInfo')) if r: print(f" 视频号表: {r.get('tables', [])[:5]}") r = test('getFinderContacts', lambda: call(ex, 'getFinderContacts', 5)) if r: print(f" 视频号联系人: {r.get('count')} 条") r = test('getFinderAccounts', lambda: call(ex, 'getFinderAccounts')) if r: print(f" 视频号账号: {json.dumps(r, ensure_ascii=False)[:200]}") print("\n--- 12. 收藏 ---") r = test('getFavorites(10)', lambda: call(ex, 'getFavorites', 10)) if r: if r.get('count', 0) > 0: print(f" 收藏: {r.get('count')} 条") elif 'available_tables' in r: print(f" 可用表: {r.get('available_tables', [])[:5]}") print("\n--- 13. 钱包 ---") r = test('getWalletInfo', lambda: call(ex, 'getWalletInfo')) if r: print(f" 钱包表: {r.get('tables', [])[:5]}") r = test('getWalletLedger(5)', lambda: call(ex, 'getWalletLedger', 5)) if r and r.get('count', 0) > 0: print(f" 钱包流水: {r.get('count')} 条") print(f" 最新: {json.dumps(r['rows'][0], ensure_ascii=False)[:200]}") r = test('getLuckyMoney', lambda: call(ex, 'getLuckyMoney', 5)) if r: print(f" 红包记录: {r.get('count')} 条") print("\n--- 14. 媒体 ---") r = test('getImageCount', lambda: call(ex, 'getImageCount')) if r: print(f" 图片总数: {r.get('rows', [{}])[0].get('total', '?')}") r = test('getVideoList(3)', lambda: call(ex, 'getVideoList', 3)) if r: print(f" 视频列表: {r.get('count')} 条") r = test('getFileList(3)', lambda: call(ex, 'getFileList', 3)) if r: print(f" 文件列表: {r.get('count')} 条") print("\n--- 15. 小程序 ---") r = test('getAppBrandList', lambda: call(ex, 'getAppBrandList')) if r: print(f" 小程序表: {r.get('tables', [])[:5]}") if r.get('common_apps'): print(f" 常用小程序: {r['common_apps'].get('count')} 条") print("\n--- 16. 全局搜索 ---") r = test('globalSearch', lambda: call(ex, 'globalSearch', '游')) if r: print(f" 联系人:{r.get('contacts',{}).get('count')} 群:{r.get('groups',{}).get('count')} 消息:{r.get('messages',{}).get('count')}") print("\n--- 17. 添加好友 ---") r = test('addFriend', lambda: call(ex, 'addFriend', 'wxid_test123456', '你好,我是游条姐')) if r: print(f" 结果: {json.dumps(r, ensure_ascii=False)[:200]}") print("\n--- 18. 导航 ---") r = test('navigate_main', lambda: call(ex, 'navigate', 'main')) if r: print(f" 导航到主界面: {r}") # 最终截图 time.sleep(2) sc_final = screenshot('v9_final') print(f"\n 📸 最终截图: {sc_final}") # ===== 汇总 ===== total = passed + failed print(f"\n{'='*60}") print(f" 验证结果: {passed}/{total} 通过 ({100*passed//total if total>0 else 0}%)") print(f"{'='*60}") # 保存结果 result_file = f'/Users/karuo/Documents/开发/2、私域银行/工作手机/开发文档/6、测试/v9_verify_result_{int(time.time())}.json' with open(result_file, 'w', encoding='utf-8') as f: json.dump({ 'timestamp': time.strftime('%Y-%m-%d %H:%M:%S'), 'version': 'v9-final', 'passed': passed, 'failed': failed, 'total': total, 'pass_rate': f'{100*passed//total if total>0 else 0}%', 'results': results }, f, ensure_ascii=False, indent=2) print(f"\n 结果已保存: {result_file}") print(f" 截图: {sc_before}, {sc_after}, {sc_sns}, {sc_final}")