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:
@@ -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):
|
||||
|
||||
Reference in New Issue
Block a user