mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-22 07:03:53 +08:00
169 lines
5.5 KiB
Python
169 lines
5.5 KiB
Python
#!/usr/bin/env python3
|
|
"""Extract the Spring MVC contract without starting the Java application."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import argparse
|
|
import fnmatch
|
|
import json
|
|
import re
|
|
from dataclasses import asdict, dataclass
|
|
from pathlib import Path
|
|
|
|
MAPPING_RE = re.compile(r"@(Get|Post|Put|Delete|Patch)Mapping(?:\((.*)\))?")
|
|
CLASS_MAPPING_RE = re.compile(r"@RequestMapping\(\s*(?:value\s*=\s*)?[\"']([^\"']*)[\"']")
|
|
PATH_RE = re.compile(r"[\"']([^\"']*)[\"']")
|
|
METHOD_RE = re.compile(r"\bpublic\s+(?:<[^>]+>\s+)?[^=(;]+?\s+(\w+)\s*\(")
|
|
PERMISSION_RE = re.compile(r'@RequiresPermissions\(\s*"([^"]+)"')
|
|
|
|
PUBLIC_PATTERNS = (
|
|
"/ota/**",
|
|
"/otaMag/download/**",
|
|
"/webjars/**",
|
|
"/druid/**",
|
|
"/v3/api-docs/**",
|
|
"/doc.html",
|
|
"/favicon.ico",
|
|
"/user/captcha",
|
|
"/user/smsVerification",
|
|
"/user/login",
|
|
"/user/pub-config",
|
|
"/user/register",
|
|
"/user/retrieve-password",
|
|
"/agent/chat-history/download/**",
|
|
"/agent/play/**",
|
|
"/voiceClone/play/**",
|
|
)
|
|
SERVER_PATTERNS = (
|
|
"/config/**",
|
|
"/device/address-book/call",
|
|
"/agent/chat-history/report",
|
|
"/agent/chat-summary/**",
|
|
"/agent/chat-title/**",
|
|
)
|
|
|
|
|
|
@dataclass(frozen=True, slots=True)
|
|
class Route:
|
|
method: str
|
|
path: str
|
|
controller: str
|
|
handler: str
|
|
auth: str
|
|
permission: str | None
|
|
source: str
|
|
line: int
|
|
java_signature: str
|
|
|
|
|
|
def _spring_match(path: str, pattern: str) -> bool:
|
|
return fnmatch.fnmatchcase(path, pattern.replace("**", "*"))
|
|
|
|
|
|
def classify_auth(path: str) -> str:
|
|
if any(_spring_match(path, pattern) for pattern in PUBLIC_PATTERNS):
|
|
return "anonymous"
|
|
if any(_spring_match(path, pattern) for pattern in SERVER_PATTERNS):
|
|
return "server-secret"
|
|
return "database-token"
|
|
|
|
|
|
def join_paths(base: str, child: str) -> str:
|
|
if not base:
|
|
base = "/"
|
|
if not base.startswith("/"):
|
|
base = "/" + base
|
|
if not child:
|
|
return base
|
|
if not child.startswith("/"):
|
|
child = "/" + child
|
|
if base == "/":
|
|
return child
|
|
return base.rstrip("/") + child
|
|
|
|
|
|
def extract_controller(path: Path, root: Path) -> list[Route]:
|
|
source = path.read_text(encoding="utf-8")
|
|
class_position = source.find(" class ")
|
|
class_header = source[:class_position] if class_position >= 0 else source
|
|
class_match = list(CLASS_MAPPING_RE.finditer(class_header))
|
|
base = class_match[-1].group(1) if class_match else ""
|
|
controller_match = re.search(r"public\s+class\s+(\w+)", source)
|
|
controller = controller_match.group(1) if controller_match else path.stem
|
|
lines = source.splitlines()
|
|
routes: list[Route] = []
|
|
for index, line in enumerate(lines):
|
|
mapping = MAPPING_RE.search(line)
|
|
if not mapping:
|
|
continue
|
|
method = mapping.group(1).upper()
|
|
arguments = mapping.group(2) or ""
|
|
path_match = PATH_RE.search(arguments)
|
|
child = path_match.group(1) if path_match else ""
|
|
decorator_block: list[str] = [line]
|
|
signature_lines: list[str] = []
|
|
handler = "unknown"
|
|
for cursor in range(index + 1, min(index + 40, len(lines))):
|
|
candidate = lines[cursor]
|
|
if not signature_lines and candidate.lstrip().startswith("@"):
|
|
decorator_block.append(candidate)
|
|
continue
|
|
signature_lines.append(candidate.strip())
|
|
signature = " ".join(signature_lines)
|
|
handler_match = METHOD_RE.search(signature)
|
|
if handler_match:
|
|
handler = handler_match.group(1)
|
|
break
|
|
if "{" in candidate and "public " not in signature:
|
|
break
|
|
permission_match = PERMISSION_RE.search("\n".join(decorator_block))
|
|
route_path = join_paths(base, child)
|
|
routes.append(
|
|
Route(
|
|
method=method,
|
|
path=route_path,
|
|
controller=controller,
|
|
handler=handler,
|
|
auth=classify_auth(route_path),
|
|
permission=permission_match.group(1) if permission_match else None,
|
|
source=str(path.relative_to(root)),
|
|
line=index + 1,
|
|
java_signature=" ".join(signature_lines),
|
|
)
|
|
)
|
|
return routes
|
|
|
|
|
|
def extract_routes(java_root: Path, repository_root: Path) -> list[Route]:
|
|
routes: list[Route] = []
|
|
for controller in sorted(java_root.rglob("*Controller.java")):
|
|
routes.extend(extract_controller(controller, repository_root))
|
|
return sorted(routes, key=lambda route: (route.path, route.method, route.controller, route.handler))
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser()
|
|
parser.add_argument("--repository-root", type=Path, default=Path(__file__).resolve().parents[3])
|
|
parser.add_argument("--output", type=Path)
|
|
args = parser.parse_args()
|
|
repository_root = args.repository_root.resolve()
|
|
java_root = repository_root / "main" / "manager-api" / "src" / "main" / "java"
|
|
routes = extract_routes(java_root, repository_root)
|
|
payload = {
|
|
"source": "main/manager-api",
|
|
"contextPath": "/xiaozhi",
|
|
"count": len(routes),
|
|
"routes": [asdict(route) for route in routes],
|
|
}
|
|
serialized = json.dumps(payload, ensure_ascii=False, indent=2) + "\n"
|
|
if args.output:
|
|
args.output.parent.mkdir(parents=True, exist_ok=True)
|
|
args.output.write_text(serialized, encoding="utf-8")
|
|
else:
|
|
print(serialized, end="")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|