#!/usr/bin/env python3 """矩阵补全:Hook catalog 全 action 真机 hook/execute 探针。""" from __future__ import annotations import argparse import asyncio import json import os import sys import time from datetime import datetime from pathlib import Path import httpx BASE = os.environ.get("SDK_BASE_URL", "http://127.0.0.1:8899") DEVICE = os.environ.get("SDK_DEVICE_ID", "xgfe65eimrrofyws") TO = "filehelper" OUT = Path(__file__).resolve().parents[1] / "tmp" SAFE_PARAMS: dict[str, dict] = { "ping": {}, "send_message": {"to_id": TO, "content": "[catalog-probe]", "msg_type": "text"}, "batch_send": {"user_ids": [TO], "content": "[catalog-probe]"}, "mass_send": {"user_ids": [TO], "content": "[catalog-probe]"}, "get_messages": {"conversation_id": TO, "limit": 3}, "get_contacts": {"limit": 5}, "get_contact_info": {"wxid": TO}, "get_friend_info": {"user_id": TO}, "search_contacts": {"keyword": "文件", "limit": 5}, "search_contact": {"keyword": "文件", "limit": 5}, "search_messages": {"keyword": "test", "limit": 5}, "wechat_search": {"keyword": "微信", "limit": 5}, "get_groups": {"limit": 5}, "get_group_info": {"group_id": "test@chatroom"}, "get_group_members": {"group_id": "test@chatroom"}, "get_profile": {}, "check_account_status": {}, "get_favorites": {"limit": 5}, "get_labels": {}, "get_moments": {"limit": 3}, "browse_channels": {"limit": 3}, "global_search": {"keyword": "微信", "limit": 5}, "generate_my_qr_code": {}, "generate_group_qr_code": {"group_id": "test@chatroom"}, "get_wallet_balance": {}, "get_transaction_history": {"limit": 5}, "get_wechat_steps": {}, "get_sticker_list": {}, "get_top_stories": {}, "get_safety_center": {}, "check_restrictions": {}, "show_payment_code": {}, "pin_chat": {"wxid": TO, "pin": False}, "set_chat_top": {"user_id": TO, "enable": False}, "set_do_not_disturb": {"wxid": TO, "enable": False}, "add_favorite": {"content": "catalog-probe", "type": "text"}, "add_to_favorites": {"content": "catalog-probe", "type": "text"}, "set_privacy": {"setting": "moments_privacy", "value": "all"}, "set_moments_privacy": {"privacy_type": "all", "days": 180}, "set_moments_cover": {"image_path": "/sdcard/test.jpg"}, "open_mini_program": {"name": "微信支付"}, "open_miniprogram": {"name": "微信支付"}, "send_location": {"to_id": TO, "latitude": 24.48, "longitude": 118.08, "label": "probe"}, "send_emoji": {"to_id": TO, "emoji_name": "微笑"}, "send_voice": {"to_id": TO, "duration": 1}, "send_voice_message": {"to_id": TO, "duration": 1}, "send_file": {"to_id": TO, "file_path": "/sdcard/test.txt"}, "send_file_from_chat": {"to_id": TO, "file_path": "/sdcard/test.txt"}, "send_image": {"to_id": TO, "image_path": "/sdcard/test.jpg"}, "send_video": {"to_id": TO, "video_path": "/sdcard/test.mp4"}, "send_link": {"to_id": TO, "url": "https://example.com", "title": "probe"}, "send_card": {"to_id": TO, "card_wxid": TO}, "send_group_message": {"group_id": "test@chatroom", "content": "probe"}, "like_channel_video": {"index": 0}, "like_video": {"index": 0}, "like_wechat_steps": {"user_id": TO}, "like_moments": {"sns_id": "probe_sns"}, "comment_moments": {"sns_id": "probe_sns", "comment": "probe"}, "comment_channel_video": {"video_id": "index_0", "comment": "probe"}, "clear_cache": {}, "check_for_update": {}, "post_moments": {"content": "[catalog-probe]"}, "forward_moments_link": {"content": "https://example.com probe"}, "set_signature": {"signature": "catalog-probe"}, "set_nickname": {"nickname": "probe"}, "set_region": {"region": "福建 厦门"}, "set_avatar": {"image_path": "/sdcard/test.jpg"}, "set_remark": {"user_id": TO, "remark": "probe"}, "set_friend_remark": {"wxid": TO, "remark": "probe"}, "create_label": {"name": "catalog_probe"}, "create_tag": {"tag_name": "catalog_probe"}, "get_contacts_by_label": {"label_id": "1"}, "get_users_by_tag": {"tag_name": "catalog_probe"}, "add_friend": {"user_id": "28533368", "message": "catalog-probe"}, "accept_friend": {"encrypt_username": "probe", "ticket": "probe"}, "delete_friend": {"wxid": "invalid_wxid_probe"}, "invite_to_group": {"group_id": "test@chatroom", "member_ids": [TO]}, "remove_from_group": {"group_id": "test@chatroom", "member_ids": [TO]}, "create_group": {"member_ids": [TO, TO], "group_name": "catalog-probe"}, "set_group_announcement": {"group_id": "test@chatroom", "announcement": "probe"}, "set_group_notice": {"group_id": "test@chatroom", "notice": "probe"}, "set_group_name": {"group_id": "test@chatroom", "name": "probe"}, "quit_group": {"group_id": "test@chatroom"}, "set_contact_label": {"wxid": TO, "label_ids": []}, "add_tag": {"wxid": TO, "tag_name": "catalog_probe"}, "remove_tag": {"wxid": TO, "tag_name": "catalog_probe"}, "delete_label": {"label_id": "1"}, "delete_tag": {"label_id": "1"}, "delete_moments": {"sns_id": "1"}, "delete_favorite": {"local_id": "1"}, "forward_message": {"to_id": TO, "msg_svr_id": "1779123054724"}, "forward_multiple": {"to_id": TO, "msg_svr_ids": ["1779123054724"]}, "recall_message": {"msg_svr_id": "1779123054724"}, "revoke_message": {"msg_svr_id": "1779123054724"}, "send_red_packet": {"to_id": TO, "amount": "0.01", "message": "probe"}, "send_transfer": {"to_id": TO, "amount": "0.01", "description": "probe"}, "transfer": {"to_id": TO, "amount": "0.01", "description": "probe"}, "receive_red_packet": {"msg_svr_id": "1"}, "receive_transfer": {"msg_svr_id": "1"}, "receive_payment": {"msg_svr_id": "1"}, "follow_channel": {"channel_id": "probe"}, "unfollow_channel": {"channel_id": "probe"}, "follow_official_account": {"account_id": "probe"}, "unfollow_official_account": {"account_id": "probe"}, "get_official_account_articles": {"account_id": "probe", "limit": 5}, "share_channel_video": {"video_id": "index_0", "to_id": TO}, "share_mini_program": {"app_id": "wxprobe", "to_id": TO, "title": "probe"}, "add_friend_by_qr": {"qr_content": "probe"}, "add_custom_emoji": {"image_path": "/sdcard/test.jpg"}, "add_to_float": {"wxid": TO}, "remove_from_float": {"wxid": TO}, "clear_chat_history": {"wxid": TO}, "clear_history": {"user_id": TO}, "bind_phone": {"phone": "13800138000"}, "change_password": {"old_password": "old", "new_password": "new"}, "remove_login_device": {"device_id": "probe"}, "register_account": {"phone": "13800138000", "nickname": "probe"}, "login_by_password": {"phone": "13800138000", "password": "probe"}, "login_by_sms": {"phone": "13800138000"}, "switch_account": {"wxid": TO}, "batch_execute": {"actions": [{"action": "getProfile", "params": {}}]}, "get_hook_status": {}, "get_wechat_version": {}, "get_process_info": {}, "voice_call": {"to_id": TO}, "video_call": {"to_id": TO}, "share_real_time_location": {"to_id": TO}, "download_file": {"url": "https://example.com/probe.txt", "save_path": "/sdcard/Download/wp_probe.txt"}, "reply_comment": {"sns_id": "probe_sns", "comment": "catalog-probe"}, "set_what_up": {"content": "catalog-probe"}, "logout": {}, "get_device_info": {}, "get_storage_info": {}, "get_network_info": {}, "get_recent_messages": {"limit": 5}, "get_recent_mini_programs": {}, "get_friend_requests": {}, "get_official_accounts": {"limit": 5}, "unblock_self": {}, "unbind_phone": {}, "enable_fingerprint": {}, "set_account_protection": {"enable": True}, "scan_qr_code": {}, "set_sex": {"sex": 1}, "set_notification": {"enable": True}, "set_chat_background": {"wxid": TO}, "auto_register": {}, "check_login_state": {}, "get_sim_phone": {}, # ── 奥创协议补全 29 action 真机探针参数(读类空参安全 / 写类给最小 probe,真机验收才标 ✅)── "get_conversations": {"limit": 5}, "get_message_by_id": {"msg_svr_id": "1779123054724"}, "get_msg_ids_by_time": {"start_time": 0}, "get_message_raw_xml": {"msg_svr_id": "1779123054724"}, "add_friend_in_room": {"group_id": "test@chatroom", "wxid": TO, "verify_message": "catalog-probe"}, "add_friend_from_phonebook": {"phone": "13800138000", "verify_message": "catalog-probe"}, "add_friend_with_scene": {"wxid": TO, "scene": "30", "verify_message": "catalog-probe"}, "add_friend_by_card": {"card_wxid": TO, "verify_message": "catalog-probe"}, "join_group_by_qr": {"qr_content": "probe"}, "approve_group_invite": {"group_id": "test@chatroom", "wxid": TO}, "agree_join_group": {"group_id": "test@chatroom"}, "mark_conversation_read": {"conversation_id": TO}, "send_multi_image": {"to_id": TO, "images": ["/sdcard/test.jpg"]}, "send_jielong": {"to_id": TO, "content": "catalog-probe 接龙"}, "send_friend_verify": {"wxid": TO, "verify_message": "catalog-probe"}, "voice_to_text": {"msg_svr_id": "1779123054724"}, "get_friend_moments": {"wxid": TO}, "get_moment_detail": {"sns_id": "probe_sns"}, "delete_moment_comment": {"sns_id": "probe_sns", "comment_id": "1"}, "reply_moment_comment": {"sns_id": "probe_sns", "content": "catalog-probe"}, "stop_moments_praise": {}, "start_nurture": {"plan": "default"}, "stop_nurture": {}, "detect_zombie_fans": {}, "stop_detect_zombie_fans": {}, "query_red_packet": {"red_packet_id": "1"}, "get_wechat_location": {}, "search_official_account": {"keyword": "微信"}, "phone_action": {"op": "screenshot"}, } def _load_actions(): sys.path.insert(0, str(Path(__file__).resolve().parents[1] / "app" / "agent")) from hook.hook_executor import ACTION_TO_RPC, ACTION_ALIASES, resolve_action, normalize_params # type: ignore actions = sorted(set(ACTION_TO_RPC.keys()) | set(ACTION_ALIASES.keys())) return actions, resolve_action, normalize_params async def probe_one(client: httpx.AsyncClient, device_id: str, action: str, resolve_action, normalize_params) -> dict: canonical = resolve_action(action) params = dict(SAFE_PARAMS.get(action) or SAFE_PARAMS.get(canonical, {})) params = normalize_params(action, params) t0 = time.time() try: resp = await client.post( f"{BASE}/api/v3/hook/execute", json={"device_id": device_id, "platform": "wechat", "action": action, "params": params, "hook_only": True}, timeout=60, ) body = resp.json() data = body.get("data") if isinstance(body.get("data"), dict) else body ok = bool(data.get("success")) or body.get("code") == 200 and data.get("success") is not False and "不支持" not in str(data.get("message", "")) if data.get("success") is False: ok = False err = "" if ok else str(data.get("message") or data.get("error") or body)[:120] ch = body.get("channel_used") or data.get("_channel_used") or data.get("channel") or "hook" except Exception as e: ok, err, ch = False, str(e)[:160], "error" # 间歇性 503 / frida_connected:False / WS 未连接 → 多次重试(frida session 繁忙或瞬断) retries = 0 while not ok and retries < 3 and any( k in err for k in ("Frida", "frida_connected", "503", "未连接", "RPC 未成功") ): retries += 1 await asyncio.sleep(2.0 * retries) try: resp = await client.post( f"{BASE}/api/v3/hook/execute", json={"device_id": device_id, "platform": "wechat", "action": action, "params": params, "hook_only": True}, timeout=60, ) body = resp.json() data = body.get("data") if isinstance(body.get("data"), dict) else body ok = bool(data.get("success")) or (body.get("code") == 200 and data.get("success") is not False and "不支持" not in str(data.get("message", ""))) if data.get("success") is False: ok = False err = "" if ok else str(data.get("message") or data.get("error") or body)[:160] ch = body.get("channel_used") or data.get("_channel_used") or "hook" except Exception as e2: err = str(e2)[:160] return {"action": action, "success": ok, "channel": str(ch), "elapsed_ms": int((time.time() - t0) * 1000), "error": err, "retries": retries} async def prefetch_context(client: httpx.AsyncClient, device_id: str) -> None: """从真机拉取 group_id / label_id 等,写入 SAFE_PARAMS。""" try: r = await client.post( f"{BASE}/api/v3/hook/execute", json={"device_id": device_id, "platform": "wechat", "action": "get_groups", "params": {"limit": 5}, "hook_only": True}, timeout=60, ) groups = (r.json().get("data") or {}).get("groups") or [] gid = None for g in groups: candidate = g.get("group_id") or g.get("wxid") or g.get("username") if not candidate: continue chk = await client.post( f"{BASE}/api/v3/hook/execute", json={"device_id": device_id, "platform": "wechat", "action": "get_group_info", "params": {"group_id": candidate}, "hook_only": True}, timeout=60, ) if (chk.json().get("data") or {}).get("success"): gid = candidate break if gid: for k in ( "get_group_info", "get_group_members", "send_group_message", "generate_group_qr_code", "invite_to_group", "remove_from_group", "set_group_announcement", "set_group_notice", "set_group_name", "quit_group", ): if k in SAFE_PARAMS: SAFE_PARAMS[k]["group_id"] = gid except Exception: pass try: r = await client.post( f"{BASE}/api/v3/hook/execute", json={"device_id": device_id, "platform": "wechat", "action": "get_labels", "params": {}, "hook_only": True}, timeout=60, ) labels = (r.json().get("data") or {}).get("labels") or [] if labels: lid = str(labels[0].get("label_id") or labels[0].get("id") or "1") SAFE_PARAMS["get_contacts_by_label"]["label_id"] = lid SAFE_PARAMS["delete_tag"]["label_id"] = lid except Exception: pass async def main() -> int: parser = argparse.ArgumentParser() parser.add_argument("-d", "--device-id", default=DEVICE) args = parser.parse_args() actions, resolve_action, normalize_params = _load_actions() results = [] async with httpx.AsyncClient(trust_env=False) as client: await prefetch_context(client, args.device_id) for action in actions: row = await probe_one(client, args.device_id, action, resolve_action, normalize_params) results.append(row) print(f"{'✓' if row['success'] else '✗'} {action:<32} {row['elapsed_ms']:>5}ms{' (retry %d)' % row['retries'] if row.get('retries') else ''}") await asyncio.sleep(0.6) passed = sum(1 for x in results if x["success"]) report = { "timestamp": datetime.now().isoformat(), "device_id": args.device_id, "total": len(results), "passed": passed, "failed": len(results) - passed, "success_rate": f"{100 * passed / max(len(results), 1):.1f}%", "results": results, } OUT.mkdir(parents=True, exist_ok=True) path = OUT / f"matrix_hook_catalog_{args.device_id}_{int(time.time())}.json" path.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") print(f"\nHook catalog: {passed}/{len(results)} → {path}") return 0 if passed == len(results) else 1 if __name__ == "__main__": raise SystemExit(asyncio.run(main()))