feat: publish workphone SDK deployment and API docs

This commit is contained in:
Manus AI
2026-07-14 18:10:52 +08:00
commit 021d633cc1
534 changed files with 122391 additions and 0 deletions

View File

@@ -0,0 +1,181 @@
#!/usr/bin/env python3
"""
微信 Hook 接口完整性审计 — 禁止空接口/占位冒充
检查项:
1. WECHAT_ACTIONS ↔ ACTION_TO_RPC ↔ wechat_hook_v2.js rpc.exports 三方对齐
2. ACTION_ALIASES173 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())