diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py index b30b368..7d0c985 100644 --- a/bridge/aria_bridge.py +++ b/bridge/aria_bridge.py @@ -105,7 +105,14 @@ EPIC_TRIGGERS = load_epic_triggers() def load_config() -> dict[str, str]: - """Laedt Konfiguration aus /config/aria.env.""" + """Laedt Konfiguration. + + Reihenfolge (hoechste Prioritaet zuletzt): + 1. /config/aria.env (bind-mount) + 2. /shared/config/runtime.json (zentral gepflegt ueber Diagnostic UI) + + Werte aus runtime.json ueberschreiben die env-Datei. + """ config: dict[str, str] = {} if CONFIG_PATH.exists(): for line in CONFIG_PATH.read_text().splitlines(): @@ -118,6 +125,18 @@ def load_config() -> dict[str, str]: logger.info("Konfiguration geladen aus %s", CONFIG_PATH) else: logger.warning("Keine Konfiguration gefunden: %s", CONFIG_PATH) + + # Runtime-Overrides aus zentralem Shared-Volume (Diagnostic UI) + runtime_path = Path("/shared/config/runtime.json") + if runtime_path.exists(): + try: + runtime = json.loads(runtime_path.read_text()) + overrides = {k: str(v) for k, v in runtime.items() if v not in (None, "")} + if overrides: + config.update(overrides) + logger.info("Runtime-Overrides geladen: %s", sorted(overrides.keys())) + except Exception as e: + logger.warning("runtime.json konnte nicht gelesen werden: %s", e) return config diff --git a/diagnostic/index.html b/diagnostic/index.html index b01e22b..d36ea11 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -523,6 +523,39 @@ + +
+

Runtime-Konfiguration

+
+ Werte werden in /shared/config/runtime.json persistiert und + ueberschreiben die ENV-Variablen aus aria.env. Bridge liest + sie beim naechsten Start — nach Aenderung Bridge-Container neu starten + (Diagnostic-Container bleibt auf ENV). +
+
+
+ + + + + + + + + + +
+
+ + +
+
+
+
+

App-Onboarding (QR-Code)

@@ -1465,6 +1498,57 @@ send({ action: 'send_voice_config', defaultVoice, highlightVoice, ttsEnabled, speedRamona, speedThorsten, ttsEngine, xttsVoice, whisperModel }); } + // ── Runtime-Konfiguration ───────────────────── + async function loadRuntimeConfig() { + const statusEl = document.getElementById('rc-status'); + statusEl.textContent = 'Lade...'; + try { + const resp = await fetch('/api/runtime-config'); + const cfg = await resp.json(); + document.getElementById('rc-rvs-host').value = cfg.RVS_HOST || ''; + document.getElementById('rc-rvs-port').value = cfg.RVS_PORT || '443'; + document.getElementById('rc-rvs-tls').value = String(cfg.RVS_TLS) === 'false' ? 'false' : 'true'; + document.getElementById('rc-rvs-token').value = cfg.RVS_TOKEN || ''; + document.getElementById('rc-auth-token').value = cfg.ARIA_AUTH_TOKEN || ''; + statusEl.textContent = 'Geladen.'; + statusEl.style.color = '#34C759'; + loadOnboardingQR(); // QR bei Config-Wechsel neu generieren + } catch (e) { + statusEl.textContent = 'Fehler: ' + e.message; + statusEl.style.color = '#FF6B6B'; + } + } + + async function saveRuntimeConfig() { + const statusEl = document.getElementById('rc-status'); + statusEl.textContent = 'Speichere...'; + const patch = { + RVS_HOST: document.getElementById('rc-rvs-host').value.trim(), + RVS_PORT: document.getElementById('rc-rvs-port').value.trim(), + RVS_TLS: document.getElementById('rc-rvs-tls').value, + RVS_TOKEN: document.getElementById('rc-rvs-token').value.trim(), + ARIA_AUTH_TOKEN: document.getElementById('rc-auth-token').value.trim(), + }; + try { + const resp = await fetch('/api/runtime-config', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(patch), + }); + const data = await resp.json(); + if (data.ok) { + statusEl.textContent = 'Gespeichert — Bridge-Container fuer Uebernahme neu starten.'; + statusEl.style.color = '#FFD60A'; + loadOnboardingQR(); // QR mit neuem Token + } else { + throw new Error(data.error || 'Unbekannt'); + } + } catch (e) { + statusEl.textContent = 'Fehler: ' + e.message; + statusEl.style.color = '#FF6B6B'; + } + } + // ── App-Onboarding QR-Code ──────────────────── let qrLibReady = false; function ensureQRLib() { @@ -1991,6 +2075,7 @@ if (tab === 'settings') { loadHighlightTriggers(); send({ action: 'get_voice_config' }); + loadRuntimeConfig(); loadOnboardingQR(); } } diff --git a/diagnostic/server.js b/diagnostic/server.js index 57ae6c8..1ce5ee6 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -58,6 +58,41 @@ let activeSessionKey = (() => { return "main"; })(); +// ── Runtime-Config: /shared/config/runtime.json ───────────── +// ENV-Werte sind Defaults; Werte aus runtime.json haben Vorrang. +// Bridge und ggf. andere Komponenten lesen dieselbe Datei. +const RUNTIME_CONFIG_FILE = "/shared/config/runtime.json"; +const RUNTIME_CONFIG_FIELDS = [ + "RVS_HOST", "RVS_PORT", "RVS_TLS", "RVS_TOKEN", + "ARIA_AUTH_TOKEN", "WHISPER_MODEL", "WHISPER_LANGUAGE", +]; +function readRuntimeConfig() { + const envDefaults = { + RVS_HOST, RVS_PORT, RVS_TLS, RVS_TOKEN, + ARIA_AUTH_TOKEN: process.env.ARIA_AUTH_TOKEN || "", + WHISPER_MODEL: process.env.WHISPER_MODEL || "medium", + WHISPER_LANGUAGE: process.env.WHISPER_LANGUAGE || "de", + }; + try { + const raw = fs.readFileSync(RUNTIME_CONFIG_FILE, "utf-8"); + const parsed = JSON.parse(raw); + return { ...envDefaults, ...parsed }; + } catch { + return envDefaults; + } +} +function writeRuntimeConfig(patch) { + let current = {}; + try { current = JSON.parse(fs.readFileSync(RUNTIME_CONFIG_FILE, "utf-8")); } catch {} + for (const key of Object.keys(patch)) { + if (RUNTIME_CONFIG_FIELDS.includes(key)) current[key] = patch[key]; + } + fs.mkdirSync("/shared/config", { recursive: true }); + const tmp = RUNTIME_CONFIG_FILE + ".tmp"; + fs.writeFileSync(tmp, JSON.stringify(current, null, 2)); + fs.renameSync(tmp, RUNTIME_CONFIG_FILE); +} + // Atomic write: temp-file + rename, laute Logs bei Fehler. function persistActiveSession(key) { try { @@ -1169,6 +1204,26 @@ const server = http.createServer((req, res) => { } else if (req.url === "/api/session") { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ sessionKey: activeSessionKey })); + } else if (req.url === "/api/runtime-config" && req.method === "GET") { + // Zentrale Runtime-Config (ENV + Override aus /shared/config/runtime.json) + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify(readRuntimeConfig())); + } else if (req.url === "/api/runtime-config" && req.method === "POST") { + let body = ""; + req.on("data", chunk => { body += chunk; if (body.length > 32768) req.destroy(); }); + req.on("end", () => { + try { + const patch = JSON.parse(body); + writeRuntimeConfig(patch); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: true, config: readRuntimeConfig() })); + log("info", "server", `Runtime-Config aktualisiert: ${Object.keys(patch).join(", ")}`); + } catch (err) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: false, error: err.message })); + } + }); + return; } else if (req.url === "/api/onboarding") { // RVS-Credentials fuer QR-Code App-Onboarding res.writeHead(200, { "Content-Type": "application/json" }); diff --git a/issue.md b/issue.md index abd3150..314fe92 100644 --- a/issue.md +++ b/issue.md @@ -41,6 +41,12 @@ - [x] Gespraechsmodus: Max-Dauer 30s pro Aufnahme, Cache-Cleanup alter Files, Messages-Array gekappt (500) - [x] Diagnostic: Archivierte Session-Versionen (.reset.*) werden angezeigt + exportierbar — OpenClaw resettet Sessions bei erster Nutzung nach Container-Restart, Inhalt ist aber in .reset. Dateien gesichert - [x] tools/export-jsonl-to-md.js: CLI-Konverter fuer beliebige Session-JSONL zu Markdown +- [x] NO_REPLY-Filter in Bridge + Diagnostic — still verworfen (kein Chat, kein TTS) +- [x] Audio-Ducking + Exklusiv-Focus (Kotlin AudioFocusModule): andere Apps leiser bei TTS, pausiert bei Aufnahme +- [x] TTS-Cleanup serverseitig: Code-Bloecke raus, Einheiten ausgeschrieben (22GB → Gigabyte), Abkuerzungen buchstabiert (CPU), URLs zu "ein Link". `` Tag wird bevorzugt wenn ARIA ihn liefert. +- [x] QR-Code Onboarding: Diagnostic generiert QR, App scannt (bestehender QRScanner funktioniert out of the box) +- [x] TTS-Audio-Cache im Filesystem: Piper-Audio wird mit messageId verknuepft, als WAV in DocumentDirectory/tts_cache gespeichert, Play-Button spielt aus Cache statt regenerieren +- [x] Config via Diagnostic: RVS-Credentials + Aria-Auth-Token via /api/runtime-config, persistiert in /shared/config/runtime.json, Bridge liest beim Start (Overrides der ENV) ## Offen