用设备 Cookie 和授权表记住本机已验证账号,登录页按确认样图做成门户,不再把密码写进浏览器。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
177 lines
6.2 KiB
Python
177 lines
6.2 KiB
Python
from __future__ import annotations
|
|
|
|
import argparse
|
|
import ast
|
|
import json
|
|
import re
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
|
|
ROOT = Path(__file__).resolve().parents[1]
|
|
OUTPUT = ROOT / "config" / "api.config.json"
|
|
if str(ROOT) not in sys.path:
|
|
sys.path.insert(0, str(ROOT))
|
|
|
|
|
|
def _owner(path: str) -> str:
|
|
ordered = (
|
|
("/api/health", "health"),
|
|
("/api/auth", "auth"),
|
|
("/api/admin", "admin"),
|
|
("/api/account", "account"),
|
|
("/api/sentiment", "sentiment"),
|
|
("/api/dashboard", "market"),
|
|
("/api/realtime-aggregate", "market"),
|
|
("/api/rotation", "rotation"),
|
|
("/api/auction", "auction"),
|
|
("/api/themes", "themes"),
|
|
("/api/popularity", "popularity"),
|
|
("/api/dragon-tiger", "dragon_tiger"),
|
|
("/api/search", "search"),
|
|
("/api/chart", "charts"),
|
|
("/api/stock", "market"),
|
|
("/api/screener", "screener"),
|
|
("/api/mentors", "mentor"),
|
|
("/api/heaven", "heaven"),
|
|
("/api/alerts", "alerts"),
|
|
("/api/trades", "review"),
|
|
("/api/assistant", "review"),
|
|
("/api/watchlist", "review"),
|
|
("/api/notes", "review"),
|
|
("/api/reasons", "admin"),
|
|
("/api/seat-aliases", "admin"),
|
|
("/api/backfill", "admin"),
|
|
)
|
|
for prefix, owner in ordered:
|
|
if path.startswith(prefix):
|
|
return owner
|
|
raise ValueError(f"API owner is not registered: {path}")
|
|
|
|
|
|
def _role(method: str, path: str) -> str:
|
|
if path == "/api/health" or path in {
|
|
"/api/auth/register",
|
|
"/api/auth/login",
|
|
"/api/auth/accounts",
|
|
"/api/auth/switch",
|
|
"/api/auth/forget",
|
|
}:
|
|
return "public"
|
|
from api_access import required_role
|
|
|
|
sample = re.sub(r"\\d\{6\}", "000001", path)
|
|
sample = re.sub(r"\\d\+", "1", sample)
|
|
sample = sample.replace("(.+)", "sample").replace("(", "").replace(")", "")
|
|
return required_role(method, sample)
|
|
|
|
|
|
def _mapped_paths(text: str) -> dict[str, set[str]]:
|
|
paths = {method: set() for method in ("GET", "POST", "DELETE")}
|
|
tree = ast.parse(text)
|
|
for node in tree.body:
|
|
if not isinstance(node, ast.Assign) or len(node.targets) != 1:
|
|
continue
|
|
target = node.targets[0]
|
|
if not isinstance(target, ast.Name):
|
|
continue
|
|
match = re.fullmatch(
|
|
r"(?:PUBLIC_|AUTHENTICATED_)?(GET|POST|DELETE)_HANDLERS", target.id
|
|
)
|
|
if not match:
|
|
continue
|
|
mapping = ast.literal_eval(node.value)
|
|
if not isinstance(mapping, dict) or not all(
|
|
isinstance(path, str) and path.startswith("/api/") for path in mapping
|
|
):
|
|
raise ValueError(f"Invalid route handler map: {target.id}")
|
|
paths[match.group(1)].update(mapping)
|
|
return paths
|
|
|
|
|
|
def _route_sources() -> list[tuple[Path, str]]:
|
|
paths = [ROOT / "backend" / "http" / "dispatch.py"]
|
|
paths.extend(sorted((ROOT / "backend" / "features").glob("*/routes.py")))
|
|
return [(path, path.read_text(encoding="utf-8")) for path in paths]
|
|
|
|
|
|
def _method_blocks(path: Path, text: str) -> list[tuple[str, str]]:
|
|
tree = ast.parse(text, filename=str(path))
|
|
lines = text.splitlines()
|
|
blocks: list[tuple[str, str]] = []
|
|
for node in ast.walk(tree):
|
|
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
|
continue
|
|
method = ""
|
|
if node.name.startswith("do_") and node.name[3:] in {"GET", "POST", "DELETE"}:
|
|
method = node.name[3:]
|
|
else:
|
|
suffix = node.name.rsplit("_", 1)[-1].upper()
|
|
if node.name.startswith("_handle_") and suffix in {"GET", "POST", "DELETE"}:
|
|
method = suffix
|
|
if method:
|
|
blocks.append((method, "\n".join(lines[node.lineno - 1 : node.end_lineno])))
|
|
return blocks
|
|
|
|
|
|
def build() -> dict:
|
|
sources = _route_sources()
|
|
mapped_paths = {method: set() for method in ("GET", "POST", "DELETE")}
|
|
for _, text in sources:
|
|
discovered = _mapped_paths(text)
|
|
for method in mapped_paths:
|
|
mapped_paths[method].update(discovered[method])
|
|
|
|
discovered_routes = {method: {"exact": set(), "patterns": set()} for method in mapped_paths}
|
|
for path, text in sources:
|
|
for method, block in _method_blocks(path, text):
|
|
discovered_routes[method]["exact"].update(
|
|
re.findall(r'parsed\.path\s*==\s*"(/api/[^"]+)"', block)
|
|
)
|
|
discovered_routes[method]["patterns"].update(
|
|
re.findall(
|
|
r're\.(?:fullmatch|match)\(\s*r?["\']([^"\']*?/api/[^"\']+)["\']\s*,\s*parsed\.path',
|
|
block,
|
|
)
|
|
)
|
|
|
|
routes = []
|
|
for method in ("GET", "POST", "DELETE"):
|
|
exact_paths = discovered_routes[method]["exact"] | mapped_paths[method]
|
|
patterns = discovered_routes[method]["patterns"]
|
|
for path in sorted(exact_paths):
|
|
routes.append(
|
|
{"method": method, "path": path, "match": "exact", "feature": _owner(path), "access": _role(method, path)}
|
|
)
|
|
for path in sorted(patterns):
|
|
normalized = path.replace("^", "").replace("$", "")
|
|
routes.append(
|
|
{"method": method, "path": normalized, "match": "regex", "feature": _owner(normalized), "access": _role(method, normalized)}
|
|
)
|
|
routes.sort(key=lambda item: (item["path"], item["method"], item["match"]))
|
|
return {
|
|
"schema_version": 1,
|
|
"generated_from": "server.py",
|
|
"routes": routes,
|
|
}
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Build the transitional API ownership registry")
|
|
parser.add_argument("--check", action="store_true")
|
|
args = parser.parse_args()
|
|
rendered = json.dumps(build(), ensure_ascii=False, indent=2) + "\n"
|
|
if args.check:
|
|
if not OUTPUT.exists() or OUTPUT.read_text(encoding="utf-8") != rendered:
|
|
raise SystemExit("API registry is stale; run tools/build_api_registry.py")
|
|
print("API registry is current.")
|
|
return 0
|
|
OUTPUT.parent.mkdir(parents=True, exist_ok=True)
|
|
OUTPUT.write_text(rendered, encoding="utf-8")
|
|
print(OUTPUT.relative_to(ROOT).as_posix())
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|