"""Dump 5 棵作用域分类树(PG public.global_category)→ 本地 JSON。 产物(scope_trees/): - trees.json 整个分类树,按 source_type 分组(含已退役节点,live 标记) - trees_index.json 仅 live 节点(scope-link 定位目标),与 trees_embeddings.npy 行对齐 须在能连 RDS 的环境跑(云端)。用法:python scripts/dump_trees.py [env_file] """ from __future__ import annotations import json import sys from collections import Counter from pathlib import Path from core.config import PgConfig from core.db import fetch_all OUT = Path("scope_trees") def fetch_nodes(cfg: PgConfig) -> list[dict]: return fetch_all( cfg, "SELECT stable_id, name, source_type, path, level, parent_stable_id, " "(retired_at_execution_id IS NULL) AS live " "FROM global_category ORDER BY source_type, level, stable_id", schema="public", ) def main(env_file: str = ".env") -> None: cfg = PgConfig.from_env(env_file) rows = fetch_nodes(cfg) OUT.mkdir(exist_ok=True) trees: dict[str, list[dict]] = {} for r in rows: trees.setdefault(r["source_type"], []).append({ "stable_id": r["stable_id"], "name": r["name"], "path": r["path"], "level": r["level"], "parent_stable_id": r["parent_stable_id"], "live": r["live"], }) (OUT / "trees.json").write_text( json.dumps(trees, ensure_ascii=False, indent=2), encoding="utf-8") live_rows = [r for r in rows if r["live"]] index = [{ "row": i, "stable_id": r["stable_id"], "source_type": r["source_type"], "name": r["name"], "path": r["path"], } for i, r in enumerate(live_rows)] (OUT / "trees_index.json").write_text( json.dumps(index, ensure_ascii=False, indent=2), encoding="utf-8") total = Counter(r["source_type"] for r in rows) live = Counter(r["source_type"] for r in live_rows) print("trees.json (全部):", dict(total), "总计", sum(total.values())) print("trees_index (live):", dict(live), "总计", len(index)) if __name__ == "__main__": main(sys.argv[1] if len(sys.argv) > 1 else ".env")