Backend: WAL+busy_timeout, DB↔Datei-Reconcile beim Start, zentraler JSON-Parser (jsonio)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
team3
2026-06-12 07:54:57 +02:00
parent 38db80296c
commit c0b7d236bb
4 changed files with 69 additions and 40 deletions

View File

@@ -17,8 +17,9 @@ from config import (
TIMEOUTS,
MAX_CONCURRENT_GENERATIONS,
)
from database import update_guide
from database import list_guides, update_guide
from fsutil import atomic_write_json, atomic_write_text
from jsonio import parse_json_text as _parse_json_text, read_json_file as _json_datei
from paths import arbeit_dir, bausteine_path, guide_content_path, project_dir
_semaphore = asyncio.Semaphore(MAX_CONCURRENT_GENERATIONS)
@@ -109,19 +110,6 @@ def _titel_index(entries: dict[int, str]) -> dict[str, int]:
return {_norm_titel(_titel(text)): num for num, text in entries.items()}
def _json_datei(path: Path):
"""Liest eine JSON-Datei (Code-Fences tolerant); None bei fehlend/ungültig."""
if not path.exists():
return None
try:
text = path.read_text(encoding="utf-8").strip()
text = re.sub(r"^```(?:json)?\s*|\s*```$", "", text)
return json.loads(text)
except Exception as e:
log.debug("JSON-Datei ungültig: %s (%s)", path, e)
return None
def _timeout(step: str, n: int = 0) -> int:
base, per = TIMEOUTS[step]
return base + per * n
@@ -1267,6 +1255,19 @@ async def _generate_sections(
return chapters
async def reconcile_guides() -> None:
"""DB↔Dateisystem abgleichen: status=done ohne Content-Datei → error.
Läuft beim Server-Start (nach init_db) — fängt Crashes zwischen
Datei-Write und Status-Update ab.
"""
for g in await list_guides():
if g["status"] == "done" and not guide_content_path(g["topic"], g["format"]).exists():
log.warning("[%s] Guide %s: done ohne Content-Datei — auf error gesetzt", g["topic"], g["id"])
now = datetime.now(timezone.utc).isoformat()
await update_guide(g["id"], status="error", error_msg="Inhalt fehlt — neu generieren", updated_at=now)
async def generate_guide(guide_id: str, topic: str, format_name: str, instructions: str = "", provider: str = DEFAULT_PROVIDER) -> None:
async with _semaphore:
now = datetime.now(timezone.utc).isoformat()
@@ -1354,32 +1355,6 @@ async def chat_with_guide(topic: str, format_name: str, section: str, outline: s
# --- Elemente (persönliche Zusammenfassung) ---
def _parse_json_text(text: str):
"""Parst JSON aus KI-Output (Code-Fences und Drumherum-Text tolerant).
Repariert unescapte Anführungszeichen in Strings (z. B. MiniMax: "Titel „p" geändert"):
das letzte `"` vor der Fehlerstelle escapen und erneut parsen.
"""
text = re.sub(r"^```(?:json)?\s*|\s*```$", "", (text or "").strip())
start, end = text.find("{"), text.rfind("}")
if start == -1 or end <= start:
return None
candidate = text[start:end + 1]
for _ in range(20):
try:
return json.loads(candidate)
except json.JSONDecodeError as e:
if not e.msg.startswith(("Expecting ',' delimiter", "Expecting ':' delimiter")):
return None
q = candidate.rfind('"', 0, e.pos)
if q <= 0:
return None
candidate = candidate[:q] + '\\"' + candidate[q + 1:]
except Exception:
return None
return None
def _element_fields(data: dict) -> dict | None:
"""Validiert KI-Element-JSON und normalisiert auf die DB-Felder."""
if not isinstance(data, dict):