This commit is contained in:
@@ -1,151 +0,0 @@
|
||||
"""Wissens-Vault-Reader: die nächtlichen Traum-Notizen (~/wissens-vault) als klickbares
|
||||
Wiki in der Zentrale. Bewusst READ-ONLY — geschrieben wird der Vault nur vom Traum-Cron
|
||||
(und via Auftragsbuch-Entscheidungen); hier wird nur gelesen und navigiert."""
|
||||
|
||||
import os
|
||||
import re
|
||||
import time
|
||||
from pathlib import Path
|
||||
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
|
||||
router = APIRouter(prefix="/api")
|
||||
|
||||
VAULT = Path(os.environ.get("MC_VAULT_DIR", "~/wissens-vault")).expanduser()
|
||||
|
||||
|
||||
def _available() -> bool:
|
||||
return VAULT.is_dir()
|
||||
|
||||
|
||||
def _title_of(p: Path) -> str:
|
||||
"""Erste nicht-leere Zeile (ohne Markdown-#) als Anzeigename."""
|
||||
try:
|
||||
with p.open(encoding="utf-8", errors="replace") as fh:
|
||||
for line in fh:
|
||||
s = line.strip()
|
||||
if s:
|
||||
return s.lstrip("# ").strip()[:160]
|
||||
except OSError:
|
||||
pass
|
||||
return p.stem
|
||||
|
||||
|
||||
class VaultFile(BaseModel):
|
||||
path: str
|
||||
name: str
|
||||
dir: str
|
||||
title: str
|
||||
mtime: float
|
||||
neu: bool
|
||||
|
||||
|
||||
class WissenListResponse(BaseModel):
|
||||
available: bool
|
||||
files: list[VaultFile]
|
||||
|
||||
|
||||
class WissenFileResponse(BaseModel):
|
||||
path: str
|
||||
content: str
|
||||
mtime: float
|
||||
|
||||
|
||||
@router.get("/wissen", response_model=WissenListResponse)
|
||||
def list_vault() -> dict:
|
||||
"""Alle Markdown-Notizen des Vaults (relativer Pfad, Titel, Ordner, Alter)."""
|
||||
if not _available():
|
||||
return {"available": False, "files": []}
|
||||
files = []
|
||||
now = time.time()
|
||||
for p in sorted(VAULT.rglob("*.md")):
|
||||
if ".git" in p.parts:
|
||||
continue
|
||||
rel = p.relative_to(VAULT).as_posix()
|
||||
st = p.stat()
|
||||
files.append({
|
||||
"path": rel,
|
||||
"name": p.stem,
|
||||
"dir": p.parent.relative_to(VAULT).as_posix() if p.parent != VAULT else "",
|
||||
"title": _title_of(p),
|
||||
"mtime": st.st_mtime,
|
||||
"neu": (now - st.st_mtime) < 36 * 3600, # „neu seit gestern Nacht"
|
||||
})
|
||||
files.sort(key=lambda f: f["mtime"], reverse=True)
|
||||
return {"available": True, "files": files}
|
||||
|
||||
|
||||
@router.get("/wissen/datei", response_model=WissenFileResponse)
|
||||
def read_file(pfad: str) -> dict:
|
||||
"""Inhalt einer Vault-Notiz — Traversal hart geblockt (resolve + is_relative_to)."""
|
||||
if not _available():
|
||||
raise HTTPException(404, "Wissens-Vault liegt auf der Box (hier nicht verfügbar).")
|
||||
try:
|
||||
target = (VAULT / pfad).resolve()
|
||||
if not target.is_relative_to(VAULT.resolve()) or target.suffix != ".md" or not target.is_file():
|
||||
raise HTTPException(404, "Notiz nicht gefunden.")
|
||||
return {"path": pfad, "content": target.read_text(encoding="utf-8", errors="replace")[:400_000],
|
||||
"mtime": target.stat().st_mtime}
|
||||
except HTTPException:
|
||||
raise
|
||||
except (ValueError, OSError):
|
||||
raise HTTPException(404, "Notiz nicht lesbar.")
|
||||
|
||||
|
||||
@router.get("/wissen/graph")
|
||||
def graph_vault() -> dict:
|
||||
"""Obsidian-artiger Graph des Vaults (Nodes = Dateien, Edges = Wiki-Links)."""
|
||||
if not _available():
|
||||
return {"nodes": [], "edges": []}
|
||||
|
||||
nodes = []
|
||||
edges = []
|
||||
# Für schnelle Link-Auflösung (case-insensitive Name -> relativer Pfad als ID)
|
||||
name_to_id = {}
|
||||
|
||||
# 1. Alle Nodes sammeln
|
||||
for p in VAULT.rglob("*.md"):
|
||||
if ".git" in p.parts:
|
||||
continue
|
||||
rel = p.relative_to(VAULT).as_posix()
|
||||
name_to_id[p.stem.lower()] = rel
|
||||
|
||||
# Kategorie aus dem Ordner ableiten
|
||||
cat = "knowledge"
|
||||
parent = p.parent.name if p.parent != VAULT else ""
|
||||
if parent == "traeume":
|
||||
cat = "events"
|
||||
elif parent == "muster":
|
||||
cat = "rules"
|
||||
elif parent == "skill-kandidaten":
|
||||
cat = "identity"
|
||||
|
||||
nodes.append({
|
||||
"id": rel,
|
||||
"content": p.stem, # stem ist oft kürzer/prägnanter als der Titel
|
||||
"category": cat,
|
||||
"source": "wiki"
|
||||
})
|
||||
|
||||
# 2. Edges extrahieren (Wiki-Links [[Name]])
|
||||
link_rx = re.compile(r"\[\[([^\]]+)\]\]")
|
||||
for n in nodes:
|
||||
try:
|
||||
target = VAULT / n["id"]
|
||||
content = target.read_text(encoding="utf-8", errors="replace")
|
||||
# Set, um mehrfache Links auf dieselbe Datei zu entduplizieren
|
||||
found_links = {m.group(1).strip().lower() for m in link_rx.finditer(content)}
|
||||
|
||||
for link in found_links:
|
||||
target_id = name_to_id.get(link)
|
||||
if target_id and target_id != n["id"]:
|
||||
edges.append({
|
||||
"source": n["id"],
|
||||
"target": target_id,
|
||||
"weight": 1.0
|
||||
})
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
return {"nodes": nodes, "edges": edges}
|
||||
Reference in New Issue
Block a user