Files
workphone-sdk/tools/extract_wechat_sdk_control_interfaces_1838.py

350 lines
17 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
# -*- coding: utf-8 -*-
"""只追加:扫描 SDK 服务器中能控制手机端微信的真实接口。
输出:
- 开发文档/6、测试/110项功能逐项确认/2026-05-18/微信SDK服务器控制接口扫描_追加版_*.md
- 开发文档/6、测试/110项功能逐项确认/2026-05-18/微信SDK服务器控制接口矩阵_追加版_*.csv
判定原则:
1. 微信业务接口:接口语义指向好友、消息、群、朋友圈、搜索、资料、二维码、小程序、支付/钱包等微信功能。
2. 微信支撑接口亮屏、解锁、打开微信、截图、OCR、前台恢复、Frida/ADB 连接等服务于微信控制链路的接口。
3. 非核心接口:泛设备、非微信、项目管理、注册中心、经验库等不直接控制手机微信端的接口。
"""
from __future__ import annotations
import ast
import csv
import json
import os
import re
from dataclasses import dataclass, asdict
from datetime import datetime
from pathlib import Path
from typing import Iterable
ROOT = Path.cwd()
OUT_DIR = ROOT / "开发文档" / "6、测试" / "110项功能逐项确认" / "2026-05-18"
OUT_DIR.mkdir(parents=True, exist_ok=True)
TS = datetime.now().strftime("%H%M%S")
PY_SOURCES = [
ROOT / "sdk" / "app" / "routers",
ROOT / "sdk" / "app" / "services",
ROOT / "sdk" / "agent" / "hook",
ROOT / "sdk" / "tests",
ROOT / "tools",
ROOT / "工作手机SDK补全包_微信Frida",
ROOT / "补全包" / "微信Frida_SDK_20260518",
]
JS_SOURCES = [
ROOT / "sdk" / "agent" / "hook",
ROOT / "工作手机SDK补全包_微信Frida",
ROOT / "补全包" / "微信Frida_SDK_20260518",
]
BUSINESS_KEYWORDS = {
"好友与通讯录": ["friend", "contact", "contacts", "address", "通讯录", "好友", "添加好友", "验证", "label", "tag", "标签"],
"消息与会话": ["message", "msg", "chat", "conversation", "session", "send_text", "sendtext", "send_message", "voice_message", "file", "emoji", "消息", "会话", "聊天", "群发", "撤回"],
"群与社群": ["group", "room", "chatroom", "member", "", "社群", "群聊", "群成员", "invite", "kick"],
"朋友圈": ["moment", "moments", "timeline", "sns", "朋友圈", "动态", "like", "comment", "cover", "privacy"],
"搜索与发现": ["search", "discover", "scan", "qr", "qrcode", "mini", "program", "", "发现", "扫一扫", "二维码", "小程序", "公众号"],
"资料与账号": ["profile", "account", "me", "self", "user_info", "userinfo", "version", "login", "资料", "账号", "名片", "二维码名片", "微信版本"],
"钱包与交易安全闸": ["wallet", "payment", "pay", "red_packet", "redpacket", "transfer", "transaction", "收款", "支付", "钱包", "红包", "转账", "账单"],
}
SUPPORT_KEYWORDS = [
"frida", "adb", "device", "screen", "screenshot", "capture", "ocr", "unlock", "wake", "foreground", "launch", "start", "connect", "wireless", "uiautomator", "dump", "window", "前台", "截图", "识别", "亮屏", "解锁", "启动", "无线", "连接", "黑屏"
]
NON_CORE_KEYWORDS = [
"project", "registry", "experience", "gateway", "discovery", "server", "health", "metrics", "storage", "network", "cpu", "memory", "browser", "doubao", "soul", "项目", "注册中心", "经验库", "网关", "服务发现", "存储", "网络"
]
RISK_KEYWORDS = ["send", "post", "publish", "delete", "remove", "kick", "transfer", "pay", "red", "privacy", "cover", "voice_call", "video_call", "发送", "发布", "删除", "", "转账", "支付", "红包", "群发", "修改", "评论", "点赞"]
WECHAT_HINTS = ["wechat", "微信", "com.tencent.mm", "weixin", "frida"]
@dataclass
class InterfaceItem:
source_type: str
file: str
line: int
method: str
route_or_symbol: str
function: str
tag: str
scene: str
core_level: str
risk_level: str
reason: str
evidence_required: str
full_key: str
def rel(path: Path) -> str:
try:
return str(path.relative_to(ROOT))
except Exception:
return str(path)
def contains_any(text: str, words: Iterable[str]) -> bool:
t = text.lower()
return any(w.lower() in t for w in words)
def classify(text: str, file: str) -> tuple[str, str, str, str]:
combined = f"{text} {file}"
scene = "未分类"
for group, words in BUSINESS_KEYWORDS.items():
if contains_any(combined, words):
scene = group
break
has_wechat = contains_any(combined, WECHAT_HINTS)
has_business = scene != "未分类"
has_support = contains_any(combined, SUPPORT_KEYWORDS)
has_non_core = contains_any(combined, NON_CORE_KEYWORDS)
if has_business and (has_wechat or "unified" in file.lower() or "wechat_full" in file.lower()):
core_level = "核心微信业务接口"
reason = "接口语义指向微信业务控制,需验证服务器能控制手机端微信完成对应页面或数据动作。"
elif has_business:
core_level = "待人工归类的微信疑似接口"
reason = "接口语义像微信业务,但文件或上下文未明确绑定微信,需要人工确认是否落到手机端微信。"
elif has_support and (has_wechat or "adb" in file.lower() or "frida" in file.lower() or "capture" in file.lower() or "device" in file.lower()):
scene = "设备支撑"
core_level = "微信控制支撑接口"
reason = "接口用于连接、亮屏、解锁、截图、识别或恢复微信前台,作为微信控制链路支撑能力。"
elif has_non_core:
scene = "非核心"
core_level = "非核心接口"
reason = "接口偏项目、注册、网关、泛设备或非微信能力,不计入核心微信控制完成率。"
else:
core_level = "待确认非核心或遗漏接口"
reason = "未命中微信业务或支撑关键词,需要结合代码调用链复核。"
risk = "高风险需安全闸" if contains_any(combined, RISK_KEYWORDS) else "普通/读取/导航"
if "钱包" in scene or "交易" in scene:
risk = "高风险需安全闸"
evidence = "服务器路由/RPC、参数、控制通道、微信前台截图、返回字段摘要、黑屏检测、失败原因"
if risk == "高风险需安全闸":
evidence += "、确认前页面、人工确认状态"
return scene, core_level, risk, reason, evidence
def parse_py_routes(path: Path) -> list[InterfaceItem]:
items: list[InterfaceItem] = []
try:
text = path.read_text(encoding="utf-8")
except UnicodeDecodeError:
text = path.read_text(encoding="utf-8", errors="ignore")
lines = text.splitlines()
route_decorator_re = re.compile(r"@(?P<obj>router|app)\.(?P<method>get|post|put|delete|patch)\((?P<args>.*)")
def_re = re.compile(r"^(async\s+def|def)\s+(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*\(")
pending: list[tuple[int, str, str, str]] = []
for idx, line in enumerate(lines, 1):
m = route_decorator_re.search(line.strip())
if m:
args = m.group("args")
path_m = re.search(r"[\"']([^\"']+)[\"']", args)
tag_m = re.search(r"tags\s*=\s*\[\s*[\"']([^\"']+)[\"']", args)
route = path_m.group(1) if path_m else ""
tag = tag_m.group(1) if tag_m else ""
pending.append((idx, m.group("method").upper(), route, tag))
continue
dm = def_re.search(line.strip())
if dm and pending:
dec_line, method, route, tag = pending[-1]
function = dm.group("name")
context = "\n".join(lines[max(0, idx-4):min(len(lines), idx+12)])
scene, core_level, risk, reason, evidence = classify(f"{method} {route} {tag} {function} {context}", rel(path))
items.append(InterfaceItem("HTTP路由", rel(path), dec_line, method, route, function, tag, scene, core_level, risk, reason, evidence, f"{method} {route} -> {function}"))
pending.clear()
return items
def parse_py_symbols(path: Path) -> list[InterfaceItem]:
items: list[InterfaceItem] = []
try:
text = path.read_text(encoding="utf-8")
tree = ast.parse(text)
except Exception:
return items
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
name = node.name
if name.startswith("_"):
continue
src = ast.get_source_segment(text, node) or name
scene, core_level, risk, reason, evidence = classify(f"{name} {src[:1200]}", rel(path))
if core_level in {"核心微信业务接口", "微信控制支撑接口", "待人工归类的微信疑似接口"}:
items.append(InterfaceItem("Python符号", rel(path), getattr(node, "lineno", 0), "SYMBOL", name, name, "", scene, core_level, risk, reason, evidence, f"SYMBOL {name}"))
return items
def parse_js_exports(path: Path) -> list[InterfaceItem]:
items: list[InterfaceItem] = []
try:
text = path.read_text(encoding="utf-8")
except UnicodeDecodeError:
text = path.read_text(encoding="utf-8", errors="ignore")
patterns = [
re.compile(r"(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*:\s*function\s*\("),
re.compile(r"(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*\([^)]*\)\s*\{"),
re.compile(r"rpc\.exports\.(?P<name>[A-Za-z_][A-Za-z0-9_]*)\s*="),
]
lines = text.splitlines()
for i, line in enumerate(lines, 1):
for pat in patterns:
m = pat.search(line)
if not m:
continue
name = m.group("name")
if name in {"if", "for", "while", "switch", "function"}:
continue
context = "\n".join(lines[max(0, i-5):min(len(lines), i+15)])
scene, core_level, risk, reason, evidence = classify(f"{name} {context}", rel(path))
if core_level in {"核心微信业务接口", "微信控制支撑接口", "待人工归类的微信疑似接口"}:
items.append(InterfaceItem("Frida/JS符号", rel(path), i, "RPC/JS", name, name, "", scene, core_level, risk, reason, evidence, f"RPC/JS {name}"))
break
return items
def unique_items(items: list[InterfaceItem]) -> list[InterfaceItem]:
seen = set()
out = []
for it in items:
key = (it.source_type, it.file, it.method, it.route_or_symbol, it.function)
if key not in seen:
seen.add(key)
out.append(it)
return out
def scan() -> list[InterfaceItem]:
items: list[InterfaceItem] = []
for base in PY_SOURCES:
if not base.exists():
continue
for path in base.rglob("*.py"):
if "__pycache__" in path.parts:
continue
items.extend(parse_py_routes(path))
items.extend(parse_py_symbols(path))
for base in JS_SOURCES:
if not base.exists():
continue
for path in base.rglob("*.js"):
if "node_modules" in path.parts:
continue
items.extend(parse_js_exports(path))
return unique_items(items)
def write_outputs(items: list[InterfaceItem]) -> tuple[Path, Path, Path]:
csv_path = OUT_DIR / f"微信SDK服务器控制接口矩阵_追加版_{TS}.csv"
json_path = OUT_DIR / f"微信SDK服务器控制接口矩阵_追加版_{TS}.json"
md_path = OUT_DIR / f"微信SDK服务器控制接口扫描_追加版_{TS}.md"
fields = list(asdict(items[0]).keys()) if items else ["source_type", "file", "line", "method", "route_or_symbol", "function", "tag", "scene", "core_level", "risk_level", "reason", "evidence_required", "full_key"]
with csv_path.open("w", newline="", encoding="utf-8-sig") as f:
writer = csv.DictWriter(f, fieldnames=fields)
writer.writeheader()
for it in items:
writer.writerow(asdict(it))
json_path.write_text(json.dumps([asdict(it) for it in items], ensure_ascii=False, indent=2), encoding="utf-8")
counts: dict[str, int] = {}
scene_counts: dict[str, int] = {}
for it in items:
counts[it.core_level] = counts.get(it.core_level, 0) + 1
scene_counts[it.scene] = scene_counts.get(it.scene, 0) + 1
core_items = [it for it in items if it.core_level in {"核心微信业务接口", "微信控制支撑接口", "待人工归类的微信疑似接口"}]
business_items = [it for it in items if it.core_level == "核心微信业务接口"]
support_items = [it for it in items if it.core_level == "微信控制支撑接口"]
suspicious_items = [it for it in items if it.core_level == "待人工归类的微信疑似接口"]
non_core_items = [it for it in items if it.core_level in {"非核心接口", "待确认非核心或遗漏接口"}]
def table(rows: list[InterfaceItem], limit: int = 220) -> str:
header = "| 序号 | 来源 | 文件:行 | 方法/符号 | 接口/函数 | 场景 | 核心级别 | 风险 | 证据要求 |\n|---:|---|---|---|---|---|---|---|---|"
lines = [header]
for n, it in enumerate(rows[:limit], 1):
file_line = f"{it.file}:{it.line}"
iface = it.route_or_symbol.replace("|", "\\|")
func = it.function.replace("|", "\\|")
lines.append(f"| {n} | {it.source_type} | `{file_line}` | `{it.method}` | `{iface}` / `{func}` | {it.scene} | {it.core_level} | {it.risk_level} | {it.evidence_required} |")
if len(rows) > limit:
lines.append(f"| | | | | | | | | 另有 {len(rows)-limit} 条详见 CSV/JSON。 |")
return "\n".join(lines)
md = f"""# 微信 SDK 服务器控制接口扫描(追加版)
> 作者Manus AI
> 生成时间2026-05-18 {datetime.now().strftime('%H:%M:%S')}
> 原则:**只追加,不覆盖旧文档**。本报告用于纠偏:核心验收对象不是泛化 110 项,而是 SDK 服务器能否真实控制手机端微信。
## 一、扫描结论
本次从 `sdk/app/routers`、`sdk/app/services`、`sdk/agent/hook`、`sdk/tests`、`tools`、`工作手机SDK补全包_微信Frida`、`补全包/微信Frida_SDK_20260518` 中提取服务器路由、Python 符号和 Frida/JS 符号。扫描结果仅作为接口矩阵初稿,下一步还必须结合真实调用链和真机证据复核。
| 指标 | 数量 |
|---|---:|
| 扫描出的候选接口/符号总数 | {len(items)} |
| 核心微信业务接口 | {len(business_items)} |
| 微信控制支撑接口 | {len(support_items)} |
| 待人工归类的微信疑似接口 | {len(suspicious_items)} |
| 非核心或待排除接口 | {len(non_core_items)} |
| 核心级别 | 数量 |
|---|---:|
"""
for k, v in sorted(counts.items(), key=lambda kv: (-kv[1], kv[0])):
md += f"| {k} | {v} |\n"
md += "\n| 场景 | 数量 |\n|---|---:|\n"
for k, v in sorted(scene_counts.items(), key=lambda kv: (-kv[1], kv[0])):
md += f"| {k} | {v} |\n"
md += f"""
## 二、核心微信业务接口初稿
下面这些接口/符号将优先纳入核心验收。每项都必须补齐“服务器入口、控制通道、微信端页面/数据、可见截图、返回字段摘要、黑屏检测、失败原因”。高风险动作只能验收到确认前或必须等待人工确认。
{table(business_items, 260)}
## 三、微信控制支撑接口初稿
这些接口不等于微信业务本身,但它们决定 SDK 服务器是否能稳定控制手机微信包括无线连接、Frida、ADB、亮屏、解锁、截图、识别、恢复前台等。支撑接口通过后才允许进行微信业务接口真机验收。
{table(support_items, 180)}
## 四、待人工归类的微信疑似接口
这些接口命中了好友、消息、群、朋友圈等语义,但代码上下文未明确绑定微信端控制链路。下一步需要人工归类和调用链复核,确认是否能落到 `com.tencent.mm`。
{table(suspicious_items, 120)}
## 五、非核心或待排除接口说明
非微信、泛设备、项目管理、网关、注册中心、经验库、服务发现、Mock 或只有契约没有真机证据的接口,不能计入核心微信控制完成率。若其中某个泛设备接口是微信控制必需能力,应移入“微信控制支撑接口”,但仍不应伪装成微信业务功能。
## 六、下一步
下一步将基于本扫描结果重建“微信控制接口矩阵”,按好友、消息、群、朋友圈、搜索、资料、钱包安全闸、设备支撑八类重新归档。旧 110 项只作为历史记录,不再作为核心完成率依据。所有新结论将继续追加写入开发文档和项目落地执行表,不覆盖旧文件。
## 七、关联文件
| 类型 | 路径 |
|---|---|
| CSV 矩阵 | `{csv_path}` |
| JSON 明细 | `{json_path}` |
| 本报告 | `{md_path}` |
"""
md_path.write_text(md, encoding="utf-8")
return md_path, csv_path, json_path
def main() -> None:
items = scan()
items.sort(key=lambda x: (x.core_level, x.scene, x.file, x.line, x.function))
md, csvp, jsonp = write_outputs(items)
print(json.dumps({"items": len(items), "md": str(md), "csv": str(csvp), "json": str(jsonp)}, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()