Files
workphone-sdk/tools/inventory_routes.py

107 lines
5.0 KiB
Python

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""盘点工作手机 SDK 的 FastAPI 路由,并按真机验证优先级分类。"""
from __future__ import annotations
import ast
import json
import re
from pathlib import Path
ROOT = Path(__file__).resolve().parents[1]
SCAN_DIRS = [ROOT / "sdk" / "app"]
OUT_DIR = ROOT / "开发文档" / "6、测试" / "live_verify_20260518"
OUT_DIR.mkdir(parents=True, exist_ok=True)
HTTP_METHODS = {"get", "post", "put", "delete", "patch"}
HIGH_RISK_WORDS = [
"friend/add", "add-friend", "phone", "mobile", "手机号", "feedback", "report",
"moments/publish", "moments/create", "mass", "broadcast", "群发", "unblock",
"red-packet", "transfer", "payment", "change-password", "delete", "clear-history",
"send", "set-avatar", "set-nickname", "set-signature", "set-gender", "set-region",
"quit", "recall", "remove", "ban", "blacklist", "解封", "发朋友圈", "添加好友",
]
READ_SAFE_WORDS = [
"get", "list", "search", "status", "info", "detail", "qr", "device", "health",
"screenshot", "current", "battery", "network", "contacts/search", "profile/get",
"group/list", "moments/list", "favorites/list", "safety-center",
]
def literal_str(node: ast.AST) -> str:
if isinstance(node, ast.Constant) and isinstance(node.value, str):
return node.value
if isinstance(node, ast.JoinedStr):
return "".join(v.value for v in node.values if isinstance(v, ast.Constant) and isinstance(v.value, str))
return ""
def decorator_route(dec: ast.AST):
if not isinstance(dec, ast.Call):
return None
func = dec.func
method = None
if isinstance(func, ast.Attribute) and func.attr in HTTP_METHODS:
method = func.attr.upper()
if not method:
return None
path = literal_str(dec.args[0]) if dec.args else ""
tags = []
for kw in dec.keywords:
if kw.arg == "tags" and isinstance(kw.value, ast.List):
for item in kw.value.elts:
val = literal_str(item)
if val:
tags.append(val)
return method, path, tags
def classify(method: str, path: str, func: str, tags: list[str]) -> tuple[str, str]:
blob = f"{method} {path} {func} {' '.join(tags)}".lower()
if any(w.lower() in blob for w in HIGH_RISK_WORDS):
return "暂不真机执行", "涉及添加好友、手机号、反馈、朋友圈、群发、解封、支付、删除或资料改写等高风险动作"
if method == "GET" or any(w in blob for w in READ_SAFE_WORDS):
return "可真机验证", "读操作或设备状态类接口,可优先连接 Type-C 真机验证"
return "需沙箱/空数据验证", "写操作但未命中高风险词,先做契约与空数据验证,避免误触真实微信对象"
def main():
routes = []
for base in SCAN_DIRS:
for py in sorted(base.rglob("*.py")):
try:
tree = ast.parse(py.read_text(encoding="utf-8"), filename=str(py))
except Exception as e:
routes.append({"file": str(py.relative_to(ROOT)), "error": str(e)})
continue
for node in ast.walk(tree):
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
for dec in node.decorator_list:
found = decorator_route(dec)
if found:
method, path, tags = found
status, reason = classify(method, path, node.name, tags)
routes.append({
"method": method,
"path": path,
"function": node.name,
"tags": tags,
"file": str(py.relative_to(ROOT)),
"line": node.lineno,
"verify_class": status,
"reason": reason,
})
routes = [r for r in routes if "method" in r]
md = ["# 110接口真机验证清单", "", "本清单由 `tools/inventory_routes.py` 自动扫描 SDK 路由生成,优先服务于 Type-C 连接真机验证。", "", "| 序号 | 方法 | 路径 | 函数 | 文件 | 分类 | 原因 |", "|---:|---|---|---|---|---|---|"]
for i, r in enumerate(routes, 1):
md.append(f"| {i} | {r['method']} | `{r['path']}` | `{r['function']}` | `{r['file']}:{r['line']}` | {r['verify_class']} | {r['reason']} |")
summary = {}
for r in routes:
summary[r["verify_class"]] = summary.get(r["verify_class"], 0) + 1
(OUT_DIR / "接口清单.json").write_text(json.dumps({"total": len(routes), "summary": summary, "routes": routes}, ensure_ascii=False, indent=2), encoding="utf-8")
(OUT_DIR / "110接口真机验证清单.md").write_text("\n".join(md) + "\n", encoding="utf-8")
print(json.dumps({"total": len(routes), "summary": summary, "out": str(OUT_DIR)}, ensure_ascii=False, indent=2))
if __name__ == "__main__":
main()