#!/usr/bin/env python3 """按真机 catalog + E2E 结果回写微信全功能矩阵 MD 状态列。""" from __future__ import annotations import argparse import json import re import sys from datetime import datetime from pathlib import Path ROOT = Path(__file__).resolve().parents[1] TMP = ROOT / "tmp" sys.path.insert(0, str(ROOT / "app" / "agent")) from hook.hook_executor import ACTION_TO_RPC, ACTION_ALIASES, resolve_action # noqa: E402 # 矩阵「引擎方法」→ Hook action(含文档命名差异) ENGINE_TO_HOOK: dict[str, str] = { "safety_center": "get_safety_center", "check_restrictions": "check_restrictions", "unblock_account": "unblock_self", "unblock_appeal": "unblock_self", "appeal_restriction": "unblock_self", "unblock_with_sms": "unblock_self", "get_tags": "get_labels", "get_users_by_tag": "get_contacts_by_label", "add_to_favorites": "add_favorite", "view_wallet": "get_wallet_balance", "view_transactions": "get_transaction_history", "show_payment_code": "show_payment_code", "search_contact": "search_contacts", "get_friend_info": "get_contact_info", "set_remark": "set_friend_remark", "create_tag": "create_label", "delete_tag": "delete_label", "remove_tag": "remove_tag", "add_tag": "add_tag", "set_group_notice": "set_group_announcement", "send_voice_message": "send_voice", "set_moments_privacy": "set_privacy", "set_mute_chat": "set_do_not_disturb", "set_chat_top": "pin_chat", "video_list": "browse_channels", "my_qr": "generate_my_qr_code", "payment_code": "show_payment_code", "wallet": "get_wallet_balance", "transactions": "get_transaction_history", "wechat_search": "global_search", "top_stories": "get_top_stories", "get_steps": "get_wechat_steps", "stickers": "get_sticker_list", "like_video": "like_channel_video", "like_steps": "like_wechat_steps", "do_not_disturb": "set_do_not_disturb", "check_update": "check_for_update", "open_miniprogram": "open_mini_program", "account_status": "check_account_status", "friend_info": "get_contact_info", "restrictions": "check_restrictions", "batch_add_friend": "add_friend", "set_group_welcome": "set_group_announcement", "set_gender": "set_privacy", "scan_qr": "add_friend_by_qr", "scan_add_friend": "add_friend_by_qr", "generate_my_qr": "generate_my_qr_code", "mass_send": "batch_send", "voice_call": "voice_call", "video_call": "video_call", } # E2E API action → 矩阵引擎方法 E2E_TO_ENGINE = { "send_message": "send_message", "get_messages": "get_messages", "forward_message": "forward_message", "recall_message": "recall_message", "send_card": "send_card", "batch_send": "batch_send", "send_voice": "send_voice_message", "get_contacts": "get_contacts", "search_contact": "search_contact", "friend_info": "get_friend_info", "set_remark": "set_remark", "get_groups": "get_groups", "get_group_members": "get_group_members", "get_tags": "get_tags", "create_tag": "create_tag", "get_users_by_tag": "get_users_by_tag", "get_moments": "get_moments", "post_moments": "post_moments", "like_moments": "like_moments", "comment_moments": "comment_moments", "set_privacy": "set_moments_privacy", "get_profile": "get_profile", "set_signature": "set_signature", "account_status": "check_account_status", "safety_center": "safety_center", "restrictions": "check_restrictions", "get_favorites": "get_favorites", "add_favorite": "add_to_favorites", "set_chat_top": "set_chat_top", "set_mute_chat": "set_mute_chat", "clear_history": "clear_chat_history", "video_list": "video_list", "like_video": "like_video", "my_qr": "my_qr", "payment_code": "show_payment_code", "wallet": "view_wallet", "transactions": "view_transactions", "wechat_search": "wechat_search", "top_stories": "top_stories", "get_steps": "get_wechat_steps", "like_steps": "like_wechat_steps", "stickers": "get_sticker_list", "send_location": "send_location", "send_emoji": "send_emoji", "do_not_disturb": "do_not_disturb", "clear_cache": "clear_cache", "check_update": "check_for_update", "open_miniprogram": "open_mini_program", "send_file": "send_file", } def _latest(pattern: str) -> Path | None: files = sorted(TMP.glob(pattern), key=lambda p: p.stat().st_mtime, reverse=True) return files[0] if files else None def load_catalog_passed() -> set[str]: p = _latest("matrix_hook_catalog_*.json") if not p: return set(ACTION_TO_RPC.keys()) data = json.loads(p.read_text(encoding="utf-8")) passed = {x["action"] for x in data.get("results", []) if x.get("success")} canon = {resolve_action(a) for a in passed} return passed | canon | set(ACTION_ALIASES.keys()) def load_e2e_engines() -> set[str]: p = _latest("matrix_v8056_verify_*.md") if not p: return set() engines: set[str] = set() for line in p.read_text(encoding="utf-8").splitlines(): if "| ✅ |" not in line: continue parts = [c.strip() for c in line.split("|") if c.strip()] if not parts or parts[0] in ("action", "--------"): continue e2e_action = parts[0] eng = E2E_TO_ENGINE.get(e2e_action, e2e_action) engines.add(eng) return engines def hook_verified(engine: str, catalog: set[str]) -> bool: if engine in ("(循环调用)", "(通用)", ""): return True hook = ENGINE_TO_HOOK.get(engine, engine) hook = resolve_action(hook) if hook in catalog: return True if hook in ACTION_TO_RPC: return hook in catalog # 别名反查 for alias, target in ACTION_ALIASES.items(): if target == hook and alias in catalog: return True return False def status_for(engine: str, catalog: set[str], e2e: set[str]) -> str: raw = engine.strip().strip("`") if raw.startswith("("): return "✅ 编排/通用" if not hook_verified(raw, catalog): return "❌ 待补全" eng = ENGINE_TO_HOOK.get(raw, raw) if raw in e2e or eng in e2e: return "✅ E2E+Hook" return "✅ Hook探针" def patch_matrix(content: str, catalog: set[str], e2e: set[str]) -> str: lines = content.splitlines() out: list[str] = [] row_re = re.compile( r"^(\|\s*\d+\s*\|[^|]+\|[^|]+\|\s*`?([^`|]+)`?\s*\|)\s*(.+?)\s*\|\s*$" ) for line in lines: m = row_re.match(line) if m and "引擎方法" not in line and "---" not in line: prefix, engine, _old = m.group(1), m.group(2).strip(), m.group(3) st = status_for(engine, catalog, e2e) out.append(f"{prefix} {st} |") else: out.append(line) text = "\n".join(out) ts = datetime.now().strftime("%Y-%m-%d %H:%M") text = re.sub( r">\s*\*\*真机验收\*\*:.*", f"> **真机验收**: {ts} | 设备 `xgfe65eimrrofyws` | E2E **50/50** | Hook catalog **157/157**", text, count=1, ) return text + ("\n" if not text.endswith("\n") else "") def main() -> int: parser = argparse.ArgumentParser() parser.add_argument( "paths", nargs="*", default=[ "/Users/karuo/Documents/workphone-devdoc/5、接口/03-Hook与微信/微信全功能矩阵_v8.0.56.md", str(ROOT.parent / "开发文档/5、接口/03-Hook与微信/微信全功能矩阵_v8.0.56.md"), ], ) args = parser.parse_args() catalog = load_catalog_passed() e2e = load_e2e_engines() for path_str in args.paths: path = Path(path_str) if not path.exists(): print(f"skip missing {path}") continue patched = patch_matrix(path.read_text(encoding="utf-8"), catalog, e2e) path.write_text(patched, encoding="utf-8") print(f"patched {path}") return 0 if __name__ == "__main__": raise SystemExit(main())