Files
llm_wiki/tools/scripts/manage-relations.py
hehaiguang1123 a6f05ab2d5 Phase 0-2: Schema cleanup, typed relations, event-driven automation
- Phase 0: AGENTS.md cleanup (dedup quotes, renumber sections, merge qmd)
- Phase 1: typed relations (manage-relations.py, graph-search.py, check-staleness.py, detect-conflicts.py)
- Phase 2: frontmatter validator, weekly lint, knowledge promotion, git hooks
- Fix .gitignore to track tools/ and .githooks/
- Fix git remote URL (remove plaintext token)
- New wiki pages: 504 pages, 34 raw sources
2026-07-01 08:05:43 +08:00

211 lines
6.6 KiB
Python

#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
manage-relations.py — 管理 wiki 页面的 typed relationships
用法:
python manage-relations.py list <page> # 查看页面 relations
python manage-relations.py add <page> --type <type> --target "页面" [--desc "说明"] [--conf 3]
python manage-relations.py remove <page> --type <type> --target "页面"
python manage-relations.py graph <page> # 输出 DOT 格式
python manage-relations.py stats # 统计关系网络
关系类型:
depends_on | conflicts_with | supersedes | caused_by | supports | extends | part_of | example_of
"""
import argparse
import re
import sys
from pathlib import Path
try:
import yaml
except ImportError:
yaml = None
VAULT = Path(r"D:\Applications\app\kepano-obsidian-main")
WIKI = VAULT / "wiki"
VALID_TYPES = [
"depends_on", "conflicts_with", "supersedes", "caused_by",
"supports", "extends", "part_of", "example_of"
]
def read_page(page: str) -> tuple[dict | None, str | None, str | None]:
"""返回 (frontmatter_dict, body_text, error_msg)"""
fp = WIKI / f"{page}.md"
if not fp.exists():
return None, None, f"ERROR: {page}.md not found"
content = fp.read_text(encoding="utf-8")
m = re.match(r"^---\n(.*?)\n---", content, re.DOTALL)
if not m:
return None, None, f"ERROR: {page}.md has no frontmatter"
body = content[m.end():]
if yaml:
try:
front = yaml.safe_load(m.group(1)) or {}
except yaml.YAMLError as e:
return None, None, f"ERROR: YAML parse error: {e}"
else:
return None, None, "ERROR: PyYAML not installed (pip install pyyaml)"
return front, body, None
def write_page(page: str, front: dict, body: str) -> str | None:
"""写入页面,返回 error_msg"""
fp = WIKI / f"{page}.md"
new_yaml = yaml.dump(front, allow_unicode=True, default_flow_style=False, sort_keys=False)
fp.write_text(f"---\n{new_yaml}---\n{body.lstrip()}", encoding="utf-8")
return None
def cmd_list(page: str):
front, body, err = read_page(page)
if err:
print(err); return
rels = front.get("relations", [])
if not rels:
print(f" [[{page}]]: no relations")
return
print(f"[[{page}]] ({len(rels)} relations):")
for r in rels:
desc = f"{r.get('description', '')}" if r.get("description") else ""
conf = f" [conf={r.get('confidence', 3)}]" if r.get("confidence", 3) != 3 else ""
target_name = r['target'].strip('[]')
print(f" {r['type']} --> [[{target_name}]]{desc}{conf}")
def cmd_add(page: str, rel_type: str, target: str, desc: str | None, conf: int):
front, body, err = read_page(page)
if err:
print(err); return
if "relations" not in front:
front["relations"] = []
# Dedup
for r in front["relations"]:
if r.get("type") == rel_type and r.get("target") == target:
print(f" Already exists: {rel_type} [[{target}]]")
return
entry = {"type": rel_type, "target": f"[[{target}]]"}
if desc:
entry["description"] = desc
if conf < 5:
entry["confidence"] = conf
front["relations"].append(entry)
err = write_page(page, front, body)
if err:
print(err)
else:
print(f" Added: [[{page}]] --{rel_type}--> [[{target}]]")
def cmd_remove(page: str, rel_type: str, target: str):
front, body, err = read_page(page)
if err:
print(err); return
rels = front.get("relations", [])
before = len(rels)
front["relations"] = [
r for r in rels
if not (r.get("type") == rel_type and r.get("target", "").strip("[]") == target)
]
if len(front["relations"]) == before:
print(f" Not found: {rel_type} [[{target}]]")
return
if not front["relations"]:
del front["relations"]
err = write_page(page, front, body)
if err:
print(err)
else:
print(f" Removed: [[{page}]] --{rel_type}--> [[{target}]]")
def cmd_graph(page: str):
"""输出 DOT 格式"""
front, body, err = read_page(page)
if err:
print(err); return
rels = front.get("relations", [])
print(f"digraph {page} {{")
print(f' "{page}" [style=filled, fillcolor=lightblue];')
for r in rels:
target = r["target"].strip("[]")
label = r["type"]
print(f' "{page}" -> "{target}" [label="{label}"];')
print("}")
def cmd_stats():
"""统计整个 wiki 的关系网络"""
all_pages = list(WIKI.glob("*.md"))
total = 0
type_counts = {}
pages_with_rels = 0
for fp in all_pages:
if fp.name in ("index.md", "log.md"):
continue
front, _, err = read_page(fp.stem)
if err or not front:
continue
rels = front.get("relations", [])
if rels:
pages_with_rels += 1
total += len(rels)
for r in rels:
t = r["type"]
type_counts[t] = type_counts.get(t, 0) + 1
print(f"=== Relations Stats ===")
print(f" Total wiki pages: {len(all_pages) - 2}")
print(f" Pages with relations: {pages_with_rels}")
print(f" Total relations: {total}")
print(f" By type:")
for t, c in sorted(type_counts.items(), key=lambda x: -x[1]):
print(f" {t}: {c}")
def main():
parser = argparse.ArgumentParser(description="Manage typed relationships in wiki pages")
sub = parser.add_subparsers(dest="cmd")
p_list = sub.add_parser("list", help="List relations of a page")
p_list.add_argument("page")
p_add = sub.add_parser("add", help="Add a relation")
p_add.add_argument("page")
p_add.add_argument("--type", required=True, choices=VALID_TYPES)
p_add.add_argument("--target", required=True)
p_add.add_argument("--desc")
p_add.add_argument("--conf", type=int, default=3)
p_rm = sub.add_parser("remove", help="Remove a relation")
p_rm.add_argument("page")
p_rm.add_argument("--type", required=True, choices=VALID_TYPES)
p_rm.add_argument("--target", required=True)
p_g = sub.add_parser("graph", help="Output DOT graph")
p_g.add_argument("page")
p_s = sub.add_parser("stats", help="Stats of the relation network")
args = parser.parse_args()
if not args.cmd:
parser.print_help(); return
if args.cmd == "list":
cmd_list(args.page)
elif args.cmd == "add":
cmd_add(args.page, args.type, args.target, args.desc, args.conf)
elif args.cmd == "remove":
cmd_remove(args.page, args.type, args.target)
elif args.cmd == "graph":
cmd_graph(args.page)
elif args.cmd == "stats":
cmd_stats()
if __name__ == "__main__":
main()