190 lines
9.1 KiB
Python
190 lines
9.1 KiB
Python
#!/usr/bin/env python3
|
||
# -*- coding: utf-8 -*-
|
||
"""
|
||
微信 174 action 能力矩阵 生成器
|
||
|
||
把「Hook catalog 探针报告(probe 假参数)」+ ACTION_TO_RPC 映射 + JS intent 分类
|
||
合成一张**诚实的能力矩阵**:每个 action → 模块 / 通道 / 可调用条件,
|
||
让存客宝 / 触客宝 / AI数智员工 / SuperAdmin 知道「这个动作怎么调、需要什么」。
|
||
|
||
诚实口径(真机铁律 #3/#14):
|
||
- probe 用占位参数,写操作 success:False 是**正常**(需真实参数/上下文),非代码缺陷;
|
||
- 本矩阵据此区分「已验可直调 / 需真实参数 / 需UI或测试号 / companion模块」。
|
||
|
||
用法:
|
||
python3 sdk/scripts/gen_wechat_capability_matrix.py \
|
||
--report sdk/tmp/matrix_hook_catalog_xgfe65eimrrofyws_1780127048.json
|
||
输出:开发文档/5、接口/03-Hook与微信/微信174动作能力矩阵.md
|
||
"""
|
||
from __future__ import annotations
|
||
import argparse, datetime, glob, json, os, re
|
||
|
||
ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..", ".."))
|
||
HOOK_EXEC = os.path.join(ROOT, "sdk", "agent", "hook", "hook_executor.py")
|
||
HOOK_JS = os.path.join(ROOT, "sdk", "agent", "hook", "wechat_hook_v2.js")
|
||
OUT = os.path.join(ROOT, "开发文档", "5、接口", "03-Hook与微信", "微信174动作能力矩阵.md")
|
||
|
||
# 资金/隐私敏感(需测试号 + 小额 + 审计)
|
||
MONEY = {"send_red_packet", "receive_red_packet", "transfer", "send_transfer",
|
||
"receive_transfer", "payment_receive", "receive_payment"}
|
||
# 需相机/系统 UI(纯 Frida 难覆盖,u2/companion)
|
||
UI_HEAVY = {"set_avatar", "scan_qr_code", "scan_add_friend", "scan_extract_qr",
|
||
"extract_qr_from_image", "add_friend_by_qr", "show_payment_code",
|
||
"voice_call", "video_call", "send_voice", "send_voice_message"}
|
||
|
||
|
||
def parse_action_to_rpc() -> dict:
|
||
pairs, inb = {}, False
|
||
for ln in open(HOOK_EXEC, encoding="utf-8"):
|
||
if re.match(r"\s*ACTION_TO_RPC\s*[:=]", ln):
|
||
inb = True; continue
|
||
if inb:
|
||
if re.match(r"^\}", ln):
|
||
break
|
||
m = re.search(r'["\']([a-zA-Z0-9_]+)["\']\s*:\s*["\']([a-zA-Z0-9_]+)["\']', ln)
|
||
if m:
|
||
pairs[m.group(1)] = m.group(2)
|
||
return pairs
|
||
|
||
|
||
def _extract_func_body(js: str, brace_start: int) -> str:
|
||
"""从函数体起始 `{`(brace_start 指向该 `{`)做花括号配平,返回函数体文本。
|
||
|
||
旧实现用固定 1400 字符窗口,会越界吃到相邻函数体,导致
|
||
sendVideo/sendFile/sendLink(实为 _shareMediaToWechat 真实现)被误判为 companion。
|
||
"""
|
||
depth = 0
|
||
for i in range(brace_start, len(js)):
|
||
c = js[i]
|
||
if c == "{":
|
||
depth += 1
|
||
elif c == "}":
|
||
depth -= 1
|
||
if depth == 0:
|
||
return js[brace_start:i + 1]
|
||
return js[brace_start:]
|
||
|
||
|
||
def parse_intent_funcs() -> set:
|
||
"""只在每个函数**自身**体内判定是否为 _intentAction 占位(花括号配平,杜绝越界)。"""
|
||
js = open(HOOK_JS, encoding="utf-8").read()
|
||
intent = set()
|
||
for mm in re.finditer(r"([a-zA-Z_][a-zA-Z0-9_]*)\s*:\s*function[^{]*\{", js):
|
||
brace_start = js.rfind("{", mm.start(), mm.end())
|
||
body = _extract_func_body(js, brace_start)
|
||
# 仅当函数体内**唯一**返回路径是 _intentAction 才算占位;
|
||
# 若同时存在 _shareMediaToWechat/_shareTextToWechat/_sendMessageInternal/performNow 等真实现,则不算占位。
|
||
has_intent = "_intentAction(" in body
|
||
has_real = any(k in body for k in (
|
||
"_shareMediaToWechat", "_shareTextToWechat", "_sendMessageInternal",
|
||
"_execSQLWrite", "Java.performNow", "_sendMessageBySendMsgEvent",
|
||
))
|
||
if has_intent and not has_real:
|
||
intent.add(mm.group(1))
|
||
return intent
|
||
|
||
|
||
def latest_report() -> str:
|
||
files = sorted(glob.glob(os.path.join(ROOT, "sdk", "tmp", "matrix_hook_catalog_*.json")),
|
||
key=os.path.getmtime, reverse=True)
|
||
return files[0] if files else ""
|
||
|
||
|
||
def classify(action, passed, a2r, intent):
|
||
rpc = a2r.get(action, "")
|
||
if passed:
|
||
return "✅ 已验可直调", "Frida RPC", "probe 即通(读类/无副作用)"
|
||
if action in MONEY:
|
||
return "🟡 需测试号", "Frida RPC + 风控", "资金动作:测试号 + 小额 + 审计,真实参数"
|
||
if action in UI_HEAVY:
|
||
return "🟠 需UI/u2", "u2/companion", "相机/系统UI/录音,纯 Frida 难覆盖"
|
||
if rpc and rpc in intent:
|
||
return "🟠 companion", "companion intent", "设备端无障碍/广播模块;无模块诚实返 false"
|
||
if rpc:
|
||
return "🟢 需真实参数", "Frida RPC", "probe 假参数 success:false;传真实 to_id/sns_id/msg_id 即可用"
|
||
return "⬜ 未映射", "u2 兜底", "无 ACTION_TO_RPC;走 u2 或经 hook/execute 别名解析"
|
||
|
||
|
||
def main():
|
||
ap = argparse.ArgumentParser()
|
||
ap.add_argument("--report", default="")
|
||
args = ap.parse_args()
|
||
report = args.report or latest_report()
|
||
rep = json.load(open(report, encoding="utf-8")) if report else {"results": []}
|
||
results = {r["action"]: bool(r.get("success")) for r in rep.get("results", [])}
|
||
a2r = parse_action_to_rpc()
|
||
intent = parse_intent_funcs()
|
||
now = datetime.datetime.now().strftime("%Y-%m-%d %H:%M")
|
||
|
||
# 全 action 集合:catalog 结果 ∪ ACTION_TO_RPC
|
||
all_actions = sorted(set(results) | set(a2r))
|
||
rows = []
|
||
for a in all_actions:
|
||
passed = results.get(a, None)
|
||
status, channel, cond = classify(a, passed is True, a2r, intent)
|
||
rows.append((a, a2r.get(a, "—"), status, channel, cond))
|
||
|
||
from collections import Counter
|
||
dist = Counter(r[2] for r in rows)
|
||
|
||
L = []
|
||
L.append("---")
|
||
L.append("tags: [工作手机, 接口, Hook, 微信, 能力矩阵, 自动生成]")
|
||
L.append("doc-type: 验收")
|
||
L.append("layer: 5、接口/03-Hook与微信")
|
||
L.append('parent: "[[5、接口/03-Hook与微信/README|03-Hook与微信]]"')
|
||
L.append('obsidian-color: "#EF6C00"')
|
||
L.append("---\n")
|
||
L.append("# 微信 Action 能力矩阵(诚实口径)")
|
||
L.append("")
|
||
L.append("> **自动生成**:`python3 sdk/scripts/gen_wechat_capability_matrix.py` ")
|
||
L.append(f"> **数据源**:catalog 探针 `{os.path.basename(report) or '—'}` + `ACTION_TO_RPC` + JS intent 分类 · **生成**:{now} ")
|
||
L.append("> **诚实口径**:catalog 用**占位参数**探针,写操作 `success:false` 属**正常**(需真实参数/上下文),非代码缺陷;本矩阵据此分级。")
|
||
L.append("")
|
||
L.append("## 〇、能力分级总览")
|
||
L.append("")
|
||
L.append("| 分级 | 含义 | 数量 |")
|
||
L.append("|:---|:---|:---:|")
|
||
grade_desc = {
|
||
"✅ 已验可直调": "probe 即通,读类/无副作用,可直接调用",
|
||
"🟢 需真实参数": "有 Frida RPC,传真实参数即可用(probe 假参数才失败)",
|
||
"🟡 需测试号": "资金类,须测试号+小额+审计",
|
||
"🟠 需UI/u2": "相机/系统UI/录音,纯 Frida 难覆盖,走 u2",
|
||
"🟠 companion": "需设备端无障碍/广播模块,无模块诚实返 false",
|
||
"⬜ 未映射": "无直接 RPC,经 hook/execute 别名解析或 u2",
|
||
}
|
||
for g, desc in grade_desc.items():
|
||
L.append(f"| {g} | {desc} | {dist.get(g, 0)} |")
|
||
L.append(f"| **合计** | | **{len(rows)}** |")
|
||
L.append("")
|
||
L.append("**结论**:✅+🟢 = **真实可用**(读类即调、写类传真实参数即调);🟡🟠 需对应前置(测试号/UI/模块);四端按本表选择调用方式,缺口走 `sdk/proxy` 或 COORD。")
|
||
L.append("")
|
||
L.append("## 一、动作明细")
|
||
L.append("")
|
||
L.append("| action | RPC | 分级 | 通道 | 可调用条件 |")
|
||
L.append("|:---|:---|:---|:---|:---|")
|
||
for a, rpc, status, channel, cond in rows:
|
||
L.append(f"| `{a}` | `{rpc}` | {status} | {channel} | {cond} |")
|
||
L.append("")
|
||
L.append("## 二、四端用法")
|
||
L.append("")
|
||
L.append("- **读类(✅)**:`GET /api/v3/{module}/*` 或 `POST /api/v3/hook/execute`(hook_only:1)直接调。")
|
||
L.append("- **写类(🟢)**:传**真实** to_id/sns_id/msg_id/group_id;经 BFF `/v1/workphone/*` 或 `hook/execute`。")
|
||
L.append("- **资金(🟡)**:仅测试号;存客宝侧加二次确认;SDK 已入 `rate_limiter`。")
|
||
L.append("- **UI/companion(🟠)**:需设备端 u2 在线或无障碍模块;无则诚实返回 false(不假成功)。")
|
||
L.append("")
|
||
L.append("## 三、关联")
|
||
L.append("- 需求真源:`1、需求/修改/工作手机_微信全量控机与私域_20260529.md`")
|
||
L.append("- 接口契约:`5、接口/03-Hook与微信/微信Frida_API契约.md`")
|
||
L.append("- 四端可直调清单:`5、接口/01-规范与统一层/四端可直调接口能力清单.md`")
|
||
L.append("- 静态对齐:`python3 sdk/scripts/wechat_interface_audit.py`(0 缺失)")
|
||
L.append("")
|
||
os.makedirs(os.path.dirname(OUT), exist_ok=True)
|
||
open(OUT, "w", encoding="utf-8").write("\n".join(L))
|
||
print(f"[ok] 写入 {OUT}")
|
||
print(f" 动作 {len(rows)},分级分布:{dict(dist)}")
|
||
|
||
|
||
if __name__ == "__main__":
|
||
main()
|