107 lines
5.0 KiB
Python
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()
|