feat: publish workphone SDK deployment and API docs
This commit is contained in:
181
sdk/scripts/wechat_interface_audit.py
Normal file
181
sdk/scripts/wechat_interface_audit.py
Normal file
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
微信 Hook 接口完整性审计 — 禁止空接口/占位冒充
|
||||
|
||||
检查项:
|
||||
1. WECHAT_ACTIONS ↔ ACTION_TO_RPC ↔ wechat_hook_v2.js rpc.exports 三方对齐
|
||||
2. ACTION_ALIASES(173 catalog)→ canonical 均在 WECHAT_ACTIONS
|
||||
3. (可选)真机 hook/execute 探针 / 全量 128 探针
|
||||
|
||||
用法:
|
||||
python3 sdk/scripts/wechat_interface_audit.py
|
||||
python3 sdk/scripts/wechat_interface_audit.py --probe -d xgfe65eimrrofyws
|
||||
python3 sdk/scripts/wechat_interface_audit.py --probe-full -d xgfe65eimrrofyws
|
||||
"""
|
||||
|
||||
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[2]
|
||||
JS = ROOT / "sdk/agent/hook/wechat_hook_v2.js"
|
||||
HE = ROOT / "sdk/agent/hook/hook_executor.py"
|
||||
SKILL = ROOT / "sdk/app/skills/wechat/skill_v2.py"
|
||||
OUT = ROOT / "sdk/tmp"
|
||||
|
||||
|
||||
def _load_rpc_keys() -> set[str]:
|
||||
text = JS.read_text(encoding="utf-8", errors="ignore")
|
||||
return set(re.findall(r"^\s+(\w+)\s*:\s*function", text, re.M))
|
||||
|
||||
|
||||
def _load_action_rpc() -> dict[str, str]:
|
||||
text = HE.read_text(encoding="utf-8", errors="ignore")
|
||||
block = text.split("ACTION_TO_RPC:", 1)[1].split("ACTION_ALIASES:", 1)[0]
|
||||
return dict(re.findall(r'"([a-z_]+)"\s*:\s*"(\w+)"', block))
|
||||
|
||||
|
||||
def _load_action_aliases() -> dict[str, str]:
|
||||
text = HE.read_text(encoding="utf-8", errors="ignore")
|
||||
block = text.split("ACTION_ALIASES:", 1)[1].split("\n\n", 1)[0]
|
||||
return dict(re.findall(r'"([a-z_]+)"\s*:\s*"([a-z_]+)"', block))
|
||||
|
||||
|
||||
def _load_wechat_actions() -> dict[str, str]:
|
||||
text = SKILL.read_text(encoding="utf-8", errors="ignore")
|
||||
block = text.split("WECHAT_ACTIONS = {", 1)[1].split("\n}\n", 1)[0]
|
||||
return dict(re.findall(r'"([a-z_]+)"\s*:\s*"(\w+)"', block))
|
||||
|
||||
|
||||
def audit() -> dict:
|
||||
rpc = _load_rpc_keys()
|
||||
action_rpc = _load_action_rpc()
|
||||
aliases = _load_action_aliases()
|
||||
skill_actions = _load_wechat_actions()
|
||||
catalog = sorted(set(action_rpc) | set(aliases))
|
||||
|
||||
missing_rpc = sorted(set(action_rpc.values()) - rpc)
|
||||
skill_not_in_executor = sorted(set(skill_actions) - set(action_rpc))
|
||||
# diag_* 为 Hook 内部联调诊断项(JS 有真实现),不进对外 130 action 清单,豁免对齐
|
||||
executor_not_in_skill = sorted(
|
||||
a for a in set(action_rpc) - set(skill_actions)
|
||||
if not a.startswith("diag_")
|
||||
)
|
||||
skill_rpc_mismatch = [
|
||||
a for a, r in skill_actions.items()
|
||||
if a in action_rpc and action_rpc[a] != r
|
||||
]
|
||||
alias_unresolved = sorted(
|
||||
a for a, target in aliases.items()
|
||||
if target not in skill_actions
|
||||
)
|
||||
|
||||
ok = not (
|
||||
missing_rpc
|
||||
or skill_not_in_executor
|
||||
or skill_rpc_mismatch
|
||||
or executor_not_in_skill
|
||||
or alias_unresolved
|
||||
)
|
||||
|
||||
return {
|
||||
"timestamp": datetime.now().isoformat(),
|
||||
"rpc_exports": len(rpc),
|
||||
"action_to_rpc": len(action_rpc),
|
||||
"wechat_actions": len(skill_actions),
|
||||
"action_aliases": len(aliases),
|
||||
"catalog_actions": len(catalog),
|
||||
"missing_rpc_in_js": missing_rpc,
|
||||
"skill_not_in_executor": skill_not_in_executor,
|
||||
"executor_not_in_skill": executor_not_in_skill,
|
||||
"skill_rpc_mismatch": skill_rpc_mismatch,
|
||||
"alias_unresolved": alias_unresolved,
|
||||
"complete": ok,
|
||||
}
|
||||
|
||||
|
||||
def _probe_actions(device_id: str, base: str, actions: list[str], params_map: dict) -> list[dict]:
|
||||
import httpx
|
||||
|
||||
rows = []
|
||||
with httpx.Client(timeout=60) as c:
|
||||
for action in actions:
|
||||
params = dict(params_map.get(action, {}))
|
||||
r = c.post(
|
||||
f"{base}/api/v3/hook/execute",
|
||||
json={
|
||||
"device_id": device_id,
|
||||
"platform": "wechat",
|
||||
"action": action,
|
||||
"params": params,
|
||||
"hook_only": True,
|
||||
},
|
||||
)
|
||||
data = r.json()
|
||||
ch = data.get("channel_used") or data.get("_channel_used") or data.get("channel") or ""
|
||||
inner = data.get("data") if isinstance(data.get("data"), dict) else {}
|
||||
ok = bool(inner.get("success") or data.get("success")) and "offline" not in str(ch).lower()
|
||||
if inner.get("success") is False:
|
||||
ok = False
|
||||
rows.append({"action": action, "ok": ok, "channel": ch, "error": inner.get("error") or inner.get("message")})
|
||||
return rows
|
||||
|
||||
|
||||
def probe(device_id: str, base: str, limit: int = 20) -> list[dict]:
|
||||
action_rpc = _load_action_rpc()
|
||||
safe = ["ping", "get_wechat_version", "get_hook_status", "get_contacts", "get_profile", "get_messages"]
|
||||
actions = [a for a in safe if a in action_rpc][:limit]
|
||||
params = {
|
||||
"get_messages": {"conversation_id": "filehelper", "limit": 3},
|
||||
"get_contacts": {"limit": 5},
|
||||
}
|
||||
return _probe_actions(device_id, base, actions, params)
|
||||
|
||||
|
||||
def probe_full(device_id: str, base: str) -> list[dict]:
|
||||
sys.path.insert(0, str(ROOT / "sdk" / "scripts"))
|
||||
from matrix_hook_catalog_verify import SAFE_PARAMS # type: ignore
|
||||
|
||||
actions = sorted(_load_wechat_actions().keys())
|
||||
return _probe_actions(device_id, base, actions, SAFE_PARAMS)
|
||||
|
||||
|
||||
def main() -> int:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--probe", action="store_true", help="6 项安全探针")
|
||||
parser.add_argument("--probe-full", action="store_true", help="128 canonical 全量探针")
|
||||
parser.add_argument("-d", "--device-id", default="xgfe65eimrrofyws")
|
||||
parser.add_argument("--base", default="http://127.0.0.1:8899")
|
||||
args = parser.parse_args()
|
||||
|
||||
report = audit()
|
||||
OUT.mkdir(parents=True, exist_ok=True)
|
||||
out = OUT / f"wechat_interface_audit_{int(datetime.now().timestamp())}.json"
|
||||
if args.probe or args.probe_full:
|
||||
rows = probe_full(args.device_id, args.base) if args.probe_full else probe(args.device_id, args.base)
|
||||
report["probe"] = rows
|
||||
report["probe_pass"] = sum(1 for x in rows if x["ok"])
|
||||
report["probe_total"] = len(rows)
|
||||
out.write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
|
||||
print(f"rpc.exports: {report['rpc_exports']}")
|
||||
print(f"ACTION_TO_RPC: {report['action_to_rpc']}")
|
||||
print(f"WECHAT_ACTIONS: {report['wechat_actions']}")
|
||||
print(f"Catalog(含alias): {report['catalog_actions']}")
|
||||
print(f"缺失 RPC: {len(report['missing_rpc_in_js'])}")
|
||||
print(f"Skill 未映射 executor: {len(report['skill_not_in_executor'])}")
|
||||
print(f"Alias 未解析: {len(report.get('alias_unresolved', []))}")
|
||||
print(f"完整对齐: {'✅' if report['complete'] else '❌'}")
|
||||
print(f"报告: {out}")
|
||||
if args.probe or args.probe_full:
|
||||
print(f"真机探针: {report.get('probe_pass', 0)}/{report.get('probe_total', 0)}")
|
||||
return 0 if report["complete"] else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user