Files
workphone-sdk/sdk/scripts/matrix_hook_catalog_verify.py

336 lines
16 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
"""矩阵补全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()))