feat: add A-card operations frontend and backend foundation

This commit is contained in:
郑龙捷
2026-09-02 09:54:03 +08:00
parent 9badd3597f
commit ad71259ba5
106 changed files with 12461 additions and 1796 deletions
+148
View File
@@ -0,0 +1,148 @@
"""Render the versioned A-card architecture YAML files to PNG and SVG.
Setup once:
python3 -m pip install diagrams pyyaml
Usage:
python3 docs/architecture/render_diagrams.py
python3 docs/architecture/render_diagrams.py path/to/one.yaml
"""
from __future__ import annotations
import argparse
import importlib
from pathlib import Path
from typing import Any
import yaml
from diagrams import Cluster, Diagram, Edge
ICONS = {
"users": "diagrams.onprem.client:Users",
"nginx": "diagrams.onprem.network:Nginx",
"server": "diagrams.onprem.compute:Server",
"storage": "diagrams.generic.storage:Storage",
"process": "diagrams.programming.flowchart:PredefinedProcess",
"flowdb": "diagrams.programming.flowchart:Database",
"fastapi": "diagrams.programming.framework:Fastapi",
"react": "diagrams.programming.framework:React",
"mysql": "diagrams.onprem.database:MySQL",
}
EDGE_TYPES = {
"flow": {"color": "#2f6fe0"},
"write": {"color": "#3f9ad0"},
"async": {"color": "#e0902f", "style": "dashed"},
"obs": {"color": "#4f9d78", "style": "dashed"},
"dep": {"color": "#8b6fc4", "style": "dashed"},
"plain": {"color": "#9aa2ad"},
}
def resolve_icon(name: str) -> Any:
module_path, class_name = ICONS[name].split(":")
return getattr(importlib.import_module(module_path), class_name)
def cluster_attr(style: dict[str, Any], font: str) -> dict[str, str]:
return {
"bgcolor": style.get("bg", "#F5F6F8"),
"pencolor": style.get("pen", "#B8C0CC"),
"style": "rounded,dashed" if style.get("dashed") else "rounded",
"fontname": font,
"fontsize": str(style.get("fontsize", 15)),
"margin": str(style.get("margin", 18)),
}
def build_nodes(definitions: list[dict], registry: dict[str, Any]) -> None:
for definition in definitions:
label = str(definition.get("label", definition["id"])).replace("\\n", "\n")
registry[definition["id"]] = resolve_icon(definition.get("icon", "server"))(label)
def build_clusters(
definitions: list[dict], registry: dict[str, Any], font: str
) -> None:
for definition in definitions:
with Cluster(
definition["name"],
graph_attr=cluster_attr(definition.get("style", {}), font),
):
build_nodes(definition.get("nodes", []), registry)
build_clusters(definition.get("clusters", []), registry, font)
def draw_edges(definitions: list[dict], registry: dict[str, Any]) -> None:
for definition in definitions:
attributes = dict(
EDGE_TYPES.get(definition.get("type", "plain"), EDGE_TYPES["plain"])
)
for key, output_key in (
("label", "label"),
("style", "style"),
("width", "penwidth"),
("color", "color"),
("dir", "dir"),
):
if key in definition:
value = definition[key]
attributes[output_key] = (
str(value).replace("\\n", "\n") if key == "label" else str(value)
)
if definition.get("free"):
attributes["constraint"] = "false"
registry[definition["from"]] >> Edge(**attributes) >> registry[definition["to"]]
def render(path: Path) -> None:
spec = yaml.safe_load(path.read_text(encoding="utf-8"))
theme = spec.get("theme", {})
font = theme.get("font", "PingFang SC")
graph_attr = {
"fontname": font,
"fontsize": str(theme.get("title_size", 28)),
"bgcolor": "white",
"pad": str(theme.get("pad", 0.7)),
"splines": theme.get("splines", "ortho"),
"nodesep": str(theme.get("nodesep", 0.75)),
"ranksep": str(theme.get("ranksep", 1.0)),
"compound": "true",
"newrank": "true",
"label": str(spec.get("title", "")).replace("\\n", "\n"),
"labelloc": "t",
}
for optional in ("dpi", "size"):
if theme.get(optional):
graph_attr[optional] = str(theme[optional])
registry: dict[str, Any] = {}
with Diagram(
str(spec.get("title", path.stem)).replace("\\n", "\n"),
filename=str(path.with_suffix("")),
show=False,
direction=spec.get("direction", "TB"),
graph_attr=graph_attr,
node_attr={"fontname": font, "fontsize": str(theme.get("node_size", 12))},
edge_attr={"fontname": font, "fontsize": str(theme.get("edge_size", 10))},
outformat=spec.get("formats", ["png", "svg"]),
):
build_nodes(spec.get("nodes", []), registry)
build_clusters(spec.get("clusters", []), registry, font)
draw_edges(spec.get("edges", []), registry)
print(f"rendered {path.name}: {len(registry)} nodes")
def main() -> None:
parser = argparse.ArgumentParser()
parser.add_argument("paths", nargs="*", type=Path)
args = parser.parse_args()
paths = args.paths or sorted(Path(__file__).parent.glob("*.yaml"))
for path in paths:
render(path.resolve())
if __name__ == "__main__":
main()