From 0f9a0292690d0706322373b4514a53c0e447ede8 Mon Sep 17 00:00:00 2001 From: duffyduck Date: Mon, 11 May 2026 22:23:42 +0200 Subject: [PATCH] feat(diagnostic): Gehirn-Tab, Skills-Tab, Datei-Manager, Wipe-All MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Komplett-Umbau der Diagnostic auf die neue Brain-Architektur. OpenClaw-spezifische Sektionen raus (Gateway-Card, Watchdog, OpenClaw-Config, doctor-fix/Restart-Buttons, Sessions-Verwaltung, Compact-After-Messages), dafuer drei neue Tabs. Neue Tabs - Gehirn Memory-CRUD (Add/Edit/Delete + Such-Feld + Type/Pinned-Filter), Conversation-Status mit Distill-Trigger + Reset, Bootstrap & Migration: aus brain-import/ migrieren, Bootstrap-Export/Import als JSON (nur pinned), komplettes Gehirn als tar.gz exportieren/importieren. - Skills Aufklappbare Liste mit Run-Logs, Aktivieren/Deaktivieren, Export pro Skill (tar.gz), globaler Import-Button, "von ARIA"-Badge wenn vom Agent selbst angelegt. - Dateien Browser fuer /shared/uploads/ β€” User+ARIA-Dateien herunterladen oder loeschen. Beim Delete: Live-Update der Chat-Bubbles (durchgestrichener Pfad, kein Download-Link mehr). Einstellungen - Neue "Reparatur & Restart"-Section mit Container-Restart-Buttons (Bridge/Brain/Qdrant) ueber generischen /api/container-restart - Komplett-Reset (πŸ—‘ ALLES loeschen) β€” Brain + Qdrant stoppen, /shared/config + /shared/voices + /brain/data + /brain/qdrant leeren - Sprachausgabe-Header: Voice-Config-Bundle Export/Import (JSON) - Voice-Liste: ⬇ pro Stimme + ⬆ Stimme importieren (tar.gz via XTTS-Bridge) Backend (server.js) - connectGateway/Watchdog/checkGatewayHealth: No-Op (aria-core ist raus) - handleLoadChatHistory neu β€” liest jetzt chat_backup.jsonl statt OpenClaw-Sessions; respektiert file_deleted-Marker - Neue Endpoints: /api/container-restart, /api/wipe-all, /api/files-list, /api/files-download, /api/files-delete, /api/voice-config-export, /api/voice-config-import, /api/brain/* (Reverse-Proxy zum aria-brain:8080) - Entfernt: /api/doctor-fix, /api/aria-restart, /api/aria-session-reset, /api/sessions-snapshot, handleGetOpenClawConfig Co-Authored-By: Claude Opus 4.7 (1M context) --- diagnostic/index.html | 1563 +++++++++++++++++++++++++++++++---------- diagnostic/server.js | 1182 ++++++++++++------------------- 2 files changed, 1637 insertions(+), 1108 deletions(-) diff --git a/diagnostic/index.html b/diagnostic/index.html index 32196f0..4360bd5 100644 --- a/diagnostic/index.html +++ b/diagnostic/index.html @@ -207,6 +207,9 @@ @@ -216,16 +219,13 @@
-

OpenClaw Gateway

+

ARIA Brain

-
- - +
+ Lade...
-
- - - - +
+
@@ -289,9 +289,6 @@ @@ -326,48 +323,8 @@
- -
-
-
-

Sessions

-
- - -
-
- -
- -
-
-
-

Brain / Memory

- -
-
- - -
-
+
@@ -445,6 +402,43 @@
+ +
+

Reparatur & Restart

+
+

+ Wenn ein Container haengt oder ARIA nicht mehr antwortet β€” hier kann man gezielt eingreifen. +

+
+ + + +
+
+
+
+ + +
+

Komplett-Reset

+
+

+ ⚠ WIPE ALL β€” alle Einstellungen, Stimmen und das komplette + GedΓ€chtnis (Vector-DB) werden gelΓΆscht. ARIA wird leer wie nach + Erstinstallation. Bleiben tun nur: +

+
    +
  • .env (Tokens + RVS-Config)
  • +
  • aria-data/ssh/ (SSH-Keys fΓΌr aria-wohnung)
  • +
+

+ Vorher exportieren wenn etwas erhalten bleiben soll (Gehirn-Tab + Voice-Liste). +

+ +
+
+
+

Betriebsmodus

@@ -472,7 +466,14 @@
-

Sprachausgabe

+
+

Sprachausgabe

+
+ + + +
+
@@ -628,10 +629,6 @@
-
- - -
@@ -700,16 +697,210 @@
- + + +
+ + +
+
-

OpenClaw Config

+

Gehirn β€” Status

- -
(Noch nicht geladen)
+
(Lade...)
+
+
+ + + +
-
+
+

Bootstrap & Migration

+
+

+ Drei Wege ARIA mit "Grundregeln" zu fΓΌttern β€” von leichtgewichtig bis Voll-Wiederherstellung. +

+ + +
+
1. Aus brain-import/ migrieren
+
+ Parst AGENT.md/USER.md/TOOLING.md aus dem Repo und schreibt sie + als atomare pinned Memory-Punkte (identity / rule / preference / tool / skill). Idempotent β€” Re-Run + ersetzt nur die Migration-Punkte, eigene Memories bleiben. +
+
(Brain-Status laden fΓΌr Datei-Liste)
+ +
+ + +
+
2. Bootstrap-Snapshot (nur pinned)
+
+ Klein und schnell: nur die pinned Memories (IdentitΓ€t, Regeln, PrΓ€ferenzen, Tools, Skills) als JSON. + Use-Case: Wipe β†’ Bootstrap-Import β†’ ARIA hat PersΓΆnlichkeit zurΓΌck, sonst leer. + Cold Memory (Konversations-Fakten) bleibt beim Import unangetastet. +
+ + + +
+
+ + +
+
3. Komplettes Gehirn (alles)
+
+ Tar.gz mit Memories + Skills + Qdrant-DB. Brain + Qdrant werden kurz angehalten. + Import ΓΌberschreibt ALLES β€” vorher exportieren wenn etwas erhalten bleiben soll. +
+ + + +
+
+
+
+ + + + +
+
+

Memories

+
+ + +
+
+
+
+ + + + + +
+ +
+
+
(Brain-Container nicht erreichbar oder leer)
+
+
+ + + + +
+
+
+

Dateien

+ +
+
+
+ + +
+
+
+
+
(Lade...)
+
+
+
+ + +
+
+
+

Skills

+
+ + + +
+
+
+

+ Skills sind ARIAs Faehigkeiten. ARIA legt sie selbst an wenn sie ein wiederkehrendes + Problem geloest hat. Du kannst sie hier verwalten β€” ansehen, ausfuehren, deaktivieren, + exportieren, importieren. Ein deaktivierter Skill bleibt ARIA bekannt (sie baut ihn + nicht doppelt), kann aber nicht aufgerufen werden. +

+
+
+
(Lade...)
+
+
+
+ + + `; } html += ''; + html += '
'; + html += ''; + html += ''; + html += '
'; box.innerHTML = html; } + // ── Voice Export/Import ──────────────────────────── + function exportXttsVoice(name) { + const status = document.getElementById('voice-status'); + if (status) status.textContent = '⏳ Exportiere ' + name + ' ...'; + // RVS request/response β€” Antwort kommt via 'xtts_voice_exported' + send({ action: 'xtts_export_voice', name }); + } + + async function importXttsVoice(event) { + const file = event.target.files[0]; + if (!file) return; + const status = document.getElementById('voice-status'); + try { + // Datei als base64 lesen (kann mehrere MB sein) + const buf = await file.arrayBuffer(); + const bytes = new Uint8Array(buf); + let bin = ''; + const chunk = 8192; + for (let i = 0; i < bytes.length; i += chunk) { + bin += String.fromCharCode.apply(null, bytes.subarray(i, i + chunk)); + } + const b64 = btoa(bin); + // Name aus Dateinamen ableiten β€” z.B. "maia.tar.gz" β†’ "maia" + const baseName = file.name.replace(/\.tar\.gz$|\.tgz$/i, ''); + if (!confirm(`Stimme "${baseName}" importieren?\n\nFalls schon vorhanden, wird sie ΓΌberschrieben.`)) { + event.target.value = ''; + return; + } + if (status) status.textContent = '⏳ Lade hoch (' + (file.size/1024).toFixed(0) + ' KB)...'; + send({ action: 'xtts_import_voice', name: baseName, data: b64 }); + } catch (e) { + if (status) status.textContent = 'βœ— ' + e.message; + } finally { + event.target.value = ''; + } + } + // ── Voice Preview Modal ───────────────────────── const VOICE_PREVIEW_DEFAULT = 'Hallo, ich bin ARIA. Das hier ist ein kleiner Test damit du meine Stimme beurteilen kannst.'; const PREVIEW_SPEED_DEFAULT = 1.0; @@ -1843,47 +2153,90 @@ renderDiagPending(); } - // ── Reparieren β€” openclaw doctor --fix ────── - function doctorFix() { - fetch('/api/doctor-fix', { method: 'POST' }) - .then(r => r.json()) - .then(data => { - if (data.ok) { - addLog('info', 'server', 'Reparatur ausgefuehrt: ' + (data.output || 'OK').slice(0, 200)); - } else { - addLog('error', 'server', 'Reparatur fehlgeschlagen: ' + (data.error || '')); - } - }) - .catch(err => addLog('error', 'server', 'Reparatur Request fehlgeschlagen: ' + err.message)); + // doctorFix + ariaRestart entfernt β€” aria-core ist raus. + // Fuer Container-Restarts: restartContainer('aria-bridge'|'aria-brain'|'aria-qdrant'). + + // ── Voice Settings Export/Import ────────────────────── + function exportVoiceSettings() { + window.location.href = '/api/voice-config-export'; } - // ── Hard-Restart β€” docker restart aria-core ────── - function ariaRestart() { - if (!confirm('ARIA wird hart neu gestartet (Container-Restart, ~15s).\n\nLaufende Anfragen gehen verloren. Sicher?')) return; - fetch('/api/aria-restart', { method: 'POST' }) - .then(r => r.json()) - .then(data => { - if (data.ok) { - addLog('info', 'server', 'ARIA neu gestartet β€” wartet auf Reconnect'); - } else { - addLog('error', 'server', 'Restart fehlgeschlagen: ' + (data.error || '')); - } - }) - .catch(err => addLog('error', 'server', 'Restart Request fehlgeschlagen: ' + err.message)); + async function importVoiceSettings(event) { + const file = event.target.files[0]; + if (!file) return; + try { + const text = await file.text(); + // Sanity-Check: muss JSON sein + JSON.parse(text); + if (!confirm(`Voice-Settings aus "${file.name}" importieren?\n\nAktuelle voice_config + highlight_triggers werden ΓΌberschrieben.`)) { + event.target.value = ''; + return; + } + const r = await fetch('/api/voice-config-import', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: text, + }); + const d = await r.json(); + if (d.ok) { + alert('βœ“ ' + d.message); + send({ action: 'get_voice_config' }); + } else { + alert('Import fehlgeschlagen: ' + d.error); + } + } catch (e) { + alert('UngΓΌltige Datei: ' + e.message); + } finally { + event.target.value = ''; + } } - // ── Compact / Session-Reset ────── - function ariaSessionReset() { - if (!confirm('Konversation komprimieren: alle Nachrichten in ARIAs aktueller Session werden geloescht und der Container neu gestartet. ARIA vergisst den bisherigen Gespraechsverlauf. Sicher?')) return; - fetch('/api/aria-session-reset', { method: 'POST' }) - .then(r => r.json()) - .then(data => { - if (data.ok) addLog('info', 'server', 'Session geleert, ARIA neu gestartet'); - else addLog('error', 'server', 'Reset fehlgeschlagen: ' + (data.error || '')); - }) - .catch(err => addLog('error', 'server', 'Reset Request fehlgeschlagen: ' + err.message)); + // ── Wipe All β€” Komplett-Reset ────────────────────────── + async function wipeAll() { + const statusEl = document.getElementById('wipe-status'); + if (!confirm('WIRKLICH alles lΓΆschen?\n\nGedΓ€chtnis + Stimmen + Settings β†’ komplett weg.\n.env + SSH-Keys bleiben.\n\nVorher exportieren wenn was erhalten bleiben soll.')) return; + if (!confirm('Letzte Warnung: Das KANN NICHT rΓΌckgΓ€ngig gemacht werden.\n\nFortfahren?')) return; + statusEl.innerHTML = '⏳ Stoppe Container, lΓΆsche Daten...'; + try { + const r = await fetch('/api/wipe-all', { method: 'POST' }); + const d = await r.json(); + if (d.ok) { + statusEl.innerHTML = `βœ“ ${d.message}`; + setTimeout(() => location.reload(), 3000); + } else { + statusEl.innerHTML = `βœ— ${d.error}`; + } + } catch (e) { + statusEl.innerHTML = `βœ— ${e.message}`; + } } + // ── Generischer Container-Restart (Bridge/Brain/Qdrant) ────── + async function restartContainer(name) { + const statusEl = document.getElementById('restart-status'); + if (!confirm(`Container "${name}" wirklich neu starten?\n\nLaufende Anfragen gehen verloren.`)) return; + if (statusEl) statusEl.innerHTML = `⏳ Restart ${name}...`; + try { + const r = await fetch('/api/container-restart', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name }), + }); + const d = await r.json(); + if (d.ok) { + if (statusEl) statusEl.innerHTML = `βœ“ ${name} neu gestartet`; + addLog('info', 'server', `${name} neu gestartet`); + } else { + if (statusEl) statusEl.innerHTML = `βœ— ${d.error}`; + addLog('error', 'server', `${name} Restart: ${d.error}`); + } + } catch (e) { + if (statusEl) statusEl.innerHTML = `βœ— ${e.message}`; + } + } + + // ariaSessionReset entfernt β€” aria-core ist raus. + // ── Abbrechen ────────────────────────────── function cancelRequest() { send({ action: 'cancel_request' }); @@ -2256,233 +2609,8 @@ } } - // ── Session Viewer ──────────────────────────────────────── - - function loadSessions() { - document.getElementById('sessions-list').innerHTML = '
Lade...
'; - send({ action: 'list_sessions' }); - } - - let currentActiveSession = ''; - - function renderSessions(data) { - const container = document.getElementById('sessions-list'); - if (data.error) { - container.innerHTML = `
Fehler: ${escapeHtml(data.error)}
`; - return; - } - if (!data.sessions || data.sessions.length === 0) { - container.innerHTML = data.raw - ? `
${escapeHtml(data.raw)}
` - : '
Keine Sessions gefunden
'; - return; - } - - const active = data.sessions.filter(s => !s.archived); - const archives = data.sessions.filter(s => s.archived); - - const headerRow = '' - + 'Session' - + 'Msgs' - + 'Zuletzt' - + ''; - - const rowFor = (s, opts) => { - const date = s.modified ? new Date(s.modified * 1000).toLocaleString('de-DE', {day:'2-digit',month:'2-digit',hour:'2-digit',minute:'2-digit'}) : '?'; - const key = escapeHtml(s.sessionKey || s.path.split('/').pop()); - const orphanBadge = s.orphan ? ' verwaist' : ''; - const archivedBadge = s.archived ? ' archiv' : ''; - const modelBadge = s.model ? `
${escapeHtml(s.model)}
` : ''; - const isActive = (s.sessionKey === currentActiveSession) && !s.archived; - const keyColor = isActive ? '#34C759' : (s.archived || s.orphan ? '#8888AA' : '#E0E0F0'); - const activeBadge = isActive ? ' aktiv' : ''; - const rowBg = isActive ? 'background:rgba(52,199,89,0.08);' : (s.archived ? 'background:rgba(136,136,170,0.04);' : ''); - - let actions = ''; - if (s.archived) { - // Archive: nur Export + Loeschen (kein Aktivieren β€” wuerde aktive Session ueberschreiben) - actions = `` - + ``; - } else { - actions = (isActive ? '' : ``) - + `` - + ``; - } - - return `` - + `` - + `
${key}${activeBadge}${orphanBadge}${archivedBadge}
${modelBadge}` - + `${s.lines}` - + `${date}` - + `${actions}`; - }; - - let html = '' + headerRow; - for (const s of active) html += rowFor(s); - html += '
'; - - if (archives.length > 0) { - html += `
` - + `` - + `Archivierte Versionen (${archives.length}) β€” von OpenClaw beim Session-Reset gesichert` - + `` - + `` + headerRow; - for (const s of archives) html += rowFor(s); - html += '
'; - } - - container.innerHTML = html; - } - - function viewSession(path) { - const detail = document.getElementById('session-detail'); - const title = document.getElementById('session-detail-title'); - const content = document.getElementById('session-detail-content'); - detail.style.display = 'block'; - title.textContent = path.split('/').pop(); - content.innerHTML = '
Lade...
'; - send({ action: 'read_session', sessionPath: path }); - } - - function renderSessionDetail(data) { - const content = document.getElementById('session-detail-content'); - if (data.error) { - content.innerHTML = `
${escapeHtml(data.error)}
`; - return; - } - if (data.raw) { - content.innerHTML = `
${escapeHtml(data.raw)}
`; - return; - } - if (!data.messages || data.messages.length === 0) { - content.innerHTML = '
Keine Nachrichten
'; - return; - } - let html = ''; - for (const msg of data.messages) { - const role = msg.role || msg.type || '?'; - const text = extractMessageText(msg); - const roleColor = role === 'user' ? '#0096FF' : role === 'assistant' ? '#34C759' : '#8888AA'; - html += `
` - + `${escapeHtml(role)} ` - + `${escapeHtml(text.slice(0, 500))}${text.length > 500 ? '...' : ''}` - + '
'; - } - content.innerHTML = html; - } - - function extractMessageText(msg) { - if (typeof msg.content === 'string') return msg.content; - if (Array.isArray(msg.content)) { - return msg.content.filter(c => c.type === 'text').map(c => c.text || '').join(''); - } - if (msg.text) return msg.text; - if (msg.message) return typeof msg.message === 'string' ? msg.message : JSON.stringify(msg.message); - return JSON.stringify(msg).slice(0, 200); - } - - function closeSessionDetail() { - document.getElementById('session-detail').style.display = 'none'; - } - - function deleteSession(path) { - const name = path.split('/').pop(); - if (!confirm(`Session "${name}" wirklich loeschen?`)) return; - send({ action: 'delete_session', sessionPath: path }); - } - - function exportSession(path, sessionKey) { - send({ action: 'export_session', sessionPath: path, sessionKey }); - } - - function activateSession(sessionKey) { - send({ action: 'set_active_session', sessionKey }); - } - - function createSession() { - const name = prompt('Name fuer neue Session (a-z, 0-9, -, _):'); - if (!name) return; - send({ action: 'create_session', sessionName: name.trim() }); - } - - function updateActiveSessionBar(sessionKey) { - currentActiveSession = sessionKey || ''; - const bar = document.getElementById('active-session-bar'); - const nameEl = document.getElementById('active-session-name'); - if (currentActiveSession) { - bar.style.display = 'block'; - nameEl.textContent = currentActiveSession; - } else { - bar.style.display = 'none'; - } - } - - // ── Brain Viewer ──────────────────────────────────────── - - function loadBrain() { - document.getElementById('brain-list').innerHTML = '
Lade...
'; - document.getElementById('brain-empty').style.display = 'none'; - send({ action: 'list_brain' }); - } - - function renderBrainList(data) { - const container = document.getElementById('brain-list'); - const emptyEl = document.getElementById('brain-empty'); - - if (data.error) { - container.innerHTML = `
Fehler: ${escapeHtml(data.error)}
`; - emptyEl.style.display = 'none'; - return; - } - if (!data.files || data.files.length === 0) { - container.innerHTML = ''; - emptyEl.style.display = 'block'; - return; - } - emptyEl.style.display = 'none'; - - const TYPE_COLORS = { user: '#0096FF', feedback: '#FFD60A', project: '#34C759', reference: '#FF9500' }; - let html = ''; - for (const f of data.files) { - if (f.name === '.gitkeep') continue; - const color = TYPE_COLORS[f.memType] || '#8888AA'; - const date = f.modified ? new Date(f.modified * 1000).toLocaleString('de-DE') : '?'; - html += `
` - + `` - + `
` - + `
${escapeHtml(f.name)}
` - + (f.description ? `
${escapeHtml(f.description)}
` : '') - + `
` - + `
${escapeHtml(f.size)}
` - + '
'; - } - container.innerHTML = html || '
Nur .gitkeep gefunden
'; - } - - function viewBrainFile(name) { - const panel = document.getElementById('brain-content'); - const title = document.getElementById('brain-content-title'); - const text = document.getElementById('brain-content-text'); - panel.style.display = 'block'; - title.textContent = name; - text.textContent = 'Lade...'; - send({ action: 'read_brain_file', filename: name }); - } - - function renderBrainContent(data) { - const text = document.getElementById('brain-content-text'); - if (data.error) { - text.textContent = `Fehler: ${data.error}`; - text.style.color = '#FF6B6B'; - return; - } - text.style.color = '#E0E0F0'; - text.textContent = data.content || '(leer)'; - } - - function closeBrainContent() { - document.getElementById('brain-content').style.display = 'none'; - } + // Sessions- und alter Brain-Viewer wurden entfernt β€” Memories laufen + // jetzt komplett ueber den Gehirn-Tab + Vector-DB im aria-brain. // ── Haupt-Tab Navigation ────────────────────────────────── @@ -2491,15 +2619,691 @@ document.querySelectorAll('.main-nav-btn').forEach(b => b.classList.remove('active')); const target = document.getElementById('tab-' + tab); if (target) target.classList.add('visible'); - // Button aktivieren + // Button aktivieren β€” Match per onclick-Attribut (robust gegen Beschriftungs-Aenderungen) document.querySelectorAll('.main-nav-btn').forEach(b => { - if (b.textContent.trim().toLowerCase().includes(tab === 'main' ? 'main' : 'einstellung')) b.classList.add('active'); + const oc = b.getAttribute('onclick') || ''; + if (oc.includes(`'${tab}'`)) b.classList.add('active'); }); // Einstellungen: Config + QR laden if (tab === 'settings') { send({ action: 'get_voice_config' }); loadRuntimeConfig(); loadOnboardingQR(); + } else if (tab === 'brain') { + loadBrainStatus(); + loadBrainMemoryList(); + refreshImportFiles(); + } else if (tab === 'files') { + loadFiles(); + } else if (tab === 'skills') { + loadSkills(); + } + } + + // ── Skills-Verwaltung ──────────────────────────── + let skillsCache = []; + const skillExpanded = new Set(); + + async function loadSkills() { + const el = document.getElementById('skills-list'); + if (!el) return; + try { + const r = await fetch('/api/brain/skills/list'); + if (!r.ok) throw new Error('HTTP ' + r.status); + const d = await r.json(); + skillsCache = d.skills || []; + renderSkillsList(); + } catch (e) { + el.innerHTML = `πŸ”΄ Brain nicht erreichbar (${e.message})`; + } + } + + function renderSkillsList() { + const el = document.getElementById('skills-list'); + if (!el) return; + if (!skillsCache.length) { + el.innerHTML = '
Keine Skills vorhanden. ARIA legt welche an wenn sie ein wiederkehrendes Problem lΓΆst β€” oder importiere einen.
'; + return; + } + const fmtDate = (iso) => iso ? new Date(iso).toLocaleString('de-DE') : '–'; + el.innerHTML = skillsCache.map(s => { + const active = s.active !== false; + const expanded = skillExpanded.has(s.name); + const statusBadge = active + ? 'aktiv' + : 'DEAKTIVIERT'; + const execBadge = `${escapeHtml(s.execution || 'bash')}`; + const authorBadge = s.author === 'aria' + ? 'von ARIA' + : ''; + const setupErr = s.setup_error + ? `
⚠ Setup-Fehler: ${escapeHtml(s.setup_error.slice(0,200))}
` + : ''; + let expandedSection = ''; + if (expanded) { + expandedSection = ` +
+
(README lΓ€dt...)
+
+ + + + +
+
Logs (letzte 20)
+
(Logs lΓ€dt...)
+
+ `; + } + return ` +
+
+ ${expanded ? 'β–Ό' : 'β–Ά'} + ${escapeHtml(s.name)} + ${statusBadge} ${execBadge} ${authorBadge} + ${s.use_count || 0}Γ— Β· zuletzt ${fmtDate(s.last_used)} +
+
${escapeHtml(s.description || '(ohne Beschreibung)')}
+ ${setupErr} + ${expandedSection} +
+ `; + }).join(''); + } + + async function toggleSkillExpand(name) { + if (skillExpanded.has(name)) skillExpanded.delete(name); + else skillExpanded.add(name); + renderSkillsList(); + if (skillExpanded.has(name)) { + // README + Logs lazy laden + try { + const r = await fetch('/api/brain/skills/' + encodeURIComponent(name)); + const d = await r.json(); + const el = document.getElementById('skill-readme-' + name); + if (el && d.readme) el.innerHTML = '
' + escapeHtml(d.readme) + '
'; + } catch {} + try { + const r2 = await fetch('/api/brain/skills/' + encodeURIComponent(name) + '/logs'); + const d2 = await r2.json(); + const el2 = document.getElementById('skill-logs-' + name); + if (!el2) return; + if (!d2.logs.length) { el2.innerHTML = '(keine Runs)'; return; } + el2.innerHTML = d2.logs.map(l => { + const okBadge = l.exit_code === 0 ? 'OK' : `FEHLER (${l.exit_code})`; + return `
+
${escapeHtml(l.ts)} Β· ${l.duration_sec}s Β· ${okBadge}
+ ${l.stdout ? `
${escapeHtml(l.stdout)}
` : ''} + ${l.stderr ? `
${escapeHtml(l.stderr)}
` : ''} +
`; + }).join(''); + } catch {} + } + } + + async function toggleSkillActive(name, newActive) { + try { + await fetch('/api/brain/skills/' + encodeURIComponent(name), { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ active: newActive }), + }); + loadSkills(); + } catch (e) { + alert('Toggle fehlgeschlagen: ' + e.message); + } + } + + async function deleteSkill(name) { + if (!confirm(`Skill "${name}" wirklich lΓΆschen?\n\nFolder, venv, Logs β€” alles weg.`)) return; + try { + await fetch('/api/brain/skills/' + encodeURIComponent(name), { method: 'DELETE' }); + skillExpanded.delete(name); + loadSkills(); + } catch (e) { + alert('LΓΆschen fehlgeschlagen: ' + e.message); + } + } + + async function runSkillPrompt(name) { + const argsStr = prompt(`Skill "${name}" ausfΓΌhren.\n\nArgs als JSON (oder leer):`, '{}'); + if (argsStr === null) return; + let args = {}; + try { + args = JSON.parse(argsStr || '{}'); + } catch (e) { + alert('UngΓΌltiges JSON: ' + e.message); + return; + } + try { + const r = await fetch('/api/brain/skills/run', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ name, args }), + }); + const d = await r.json(); + const summary = `${d.ok ? 'βœ“ OK' : 'βœ— FEHLER'} Β· ${d.duration_sec}s Β· exit=${d.exit_code}\n\nstdout:\n${(d.stdout||'').slice(0,1500)}\n\nstderr:\n${(d.stderr||'').slice(0,500)}`; + alert(summary); + if (skillExpanded.has(name)) toggleSkillExpand(name); // refresh logs by collapse+expand + loadSkills(); + } catch (e) { + alert('Run fehlgeschlagen: ' + e.message); + } + } + + function exportSkill(name) { + window.location.href = '/api/brain/skills/' + encodeURIComponent(name) + '/export'; + } + + async function importSkillFile(event) { + const file = event.target.files[0]; + if (!file) return; + if (!confirm(`Skill aus "${file.name}" importieren?\n\nFalls schon vorhanden mit gleichem Namen, wird er ΓΌberschrieben.`)) { + event.target.value = ''; return; + } + try { + const r = await fetch('/api/brain/skills/import?overwrite=true', { + method: 'POST', + headers: { 'Content-Type': 'application/gzip' }, + body: file, + }); + const d = await r.json(); + if (r.ok) { + alert(`βœ“ Skill "${d.imported?.name}" importiert.`); + loadSkills(); + } else { + alert('Import fehlgeschlagen: ' + (d.detail || JSON.stringify(d))); + } + } catch (e) { + alert('Import fehlgeschlagen: ' + e.message); + } finally { + event.target.value = ''; + } + } + + // ── Datei-Manager ────────────────────────────────────── + let filesCache = []; + + async function loadFiles() { + const listEl = document.getElementById('files-list'); + if (listEl) listEl.innerHTML = '(Lade...)'; + try { + const r = await fetch('/api/files-list'); + const d = await r.json(); + if (!d.ok) throw new Error(d.error || 'Unbekannter Fehler'); + filesCache = d.files || []; + renderFilesList(); + } catch (e) { + if (listEl) listEl.innerHTML = `πŸ”΄ ${e.message}`; + } + } + + function renderFilesList() { + const listEl = document.getElementById('files-list'); + const infoEl = document.getElementById('files-info'); + if (!listEl) return; + const q = (document.getElementById('files-search').value || '').toLowerCase(); + const filter = document.getElementById('files-filter').value; + let files = filesCache.slice(); + if (filter === 'aria') files = files.filter(f => f.fromAria); + else if (filter === 'user') files = files.filter(f => !f.fromAria); + if (q) files = files.filter(f => f.name.toLowerCase().includes(q)); + if (infoEl) infoEl.textContent = `${files.length} von ${filesCache.length} Dateien`; + if (!files.length) { + listEl.innerHTML = '(Keine Dateien gefunden)'; + return; + } + const fmtSize = (b) => b < 1024 ? `${b} B` : b < 1024*1024 ? `${(b/1024).toFixed(1)} KB` : `${(b/1024/1024).toFixed(1)} MB`; + const fmtDate = (ms) => new Date(ms).toLocaleString('de-DE', { day: '2-digit', month: '2-digit', year: '2-digit', hour: '2-digit', minute: '2-digit' }); + listEl.innerHTML = files.map(f => { + const badge = f.fromAria + ? 'ARIA' + : 'User'; + return `
+
+
${badge}${escapeHtml(f.name)}
+
${fmtSize(f.size)} Β· ${fmtDate(f.mtime)}
+
+ + +
`; + }).join(''); + } + + function downloadFile(encPath) { + window.location.href = '/api/files-download?path=' + encPath; + } + + async function deleteFile(p, name) { + if (!confirm(`Datei "${name}" wirklich lΓΆschen?\n\nIn allen Chat-Bubbles wird sie als gelΓΆscht markiert.`)) return; + try { + const r = await fetch('/api/files-delete', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ path: p }), + }); + const d = await r.json(); + if (d.ok) { + loadFiles(); + // Server broadcastet file_deleted Event β†’ markFileDeletedInChat() updated bubbles + } else { + alert('LΓΆschen fehlgeschlagen: ' + d.error); + } + } catch (e) { + alert('LΓΆschen fehlgeschlagen: ' + e.message); + } + } + + // ── Gehirn-Tab ──────────────────────────────────────────── + + async function loadBrainStatus() { + const el = document.getElementById('brain-status'); + if (!el) return; + try { + const r = await fetch('/api/brain/health'); + if (!r.ok) throw new Error('HTTP ' + r.status); + const d = await r.json(); + const st = d.status === 'ok' ? '🟒 online' : '🟑 ' + (d.status || 'unknown'); + el.innerHTML = `${st} Β· ${d.memory_count ?? '?'} Memories Β· Qdrant: ${d.qdrant || '-'}`; + } catch (e) { + el.innerHTML = `πŸ”΄ Brain nicht erreichbar (${e.message})`; + } + // Conversation-Stats (separater Endpoint) + const conv = document.getElementById('conversation-status'); + if (!conv) return; + try { + const r2 = await fetch('/api/brain/conversation/stats'); + if (!r2.ok) throw new Error('HTTP ' + r2.status); + const d2 = await r2.json(); + const distillIcon = d2.needs_distill ? ' ⚠ Destillat bald fΓ€llig' : ''; + conv.innerHTML = `Konversation: ${d2.turns} Turns Β· Window: ${d2.max_window} Β· Schwelle: ${d2.distill_threshold}${distillIcon}`; + } catch (e) { + conv.innerHTML = `Konversation: ${e.message}`; + } + } + + async function distillNow() { + if (!confirm('Destillat manuell auslΓΆsen?\n\nDie Γ€ltesten Turns werden zu fact-Memories verdichtet β€” kostet einen Claude-Call.')) return; + try { + const r = await fetch('/api/brain/conversation/distill', { method: 'POST' }); + const d = await r.json(); + alert(`Destillat: ${d.distilled || 0} Facts geschrieben, ${d.removed_turns || 0} Turns entfernt.${d.error ? '\nFehler: ' + d.error : ''}`); + loadBrainStatus(); + loadBrainMemoryList(); + } catch (e) { + alert('Destillat fehlgeschlagen: ' + e.message); + } + } + + async function resetConversation() { + if (!confirm('Konversation leeren?\n\nDer Rolling-Window-Verlauf wird komplett verworfen. Destillierte Facts bleiben in der DB.')) return; + try { + const r = await fetch('/api/brain/conversation/reset', { method: 'POST' }); + const d = await r.json(); + if (d.ok) { + loadBrainStatus(); + } else { + alert('Reset fehlgeschlagen'); + } + } catch (e) { + alert('Reset fehlgeschlagen: ' + e.message); + } + } + + // Cache aller geladenen Memories β€” fuer Edit-Lookup + let brainMemoryCache = {}; + // Aktuelle Search-Treffer (IDs in Reihenfolge); leer = normale Liste + let brainSearchIds = null; + + function resetBrainFilters() { + const s = document.getElementById('brain-search'); if (s) s.value = ''; + const t = document.getElementById('brain-filter-type'); if (t) t.value = ''; + const p = document.getElementById('brain-filter-pinned'); if (p) p.value = 'all'; + const info = document.getElementById('brain-search-info'); if (info) info.style.display = 'none'; + brainSearchIds = null; + } + + async function runBrainSearch() { + const q = (document.getElementById('brain-search').value || '').trim(); + const info = document.getElementById('brain-search-info'); + if (!q) { + brainSearchIds = null; + if (info) info.style.display = 'none'; + loadBrainMemoryList(); + return; + } + const typeFilter = document.getElementById('brain-filter-type').value; + const params = new URLSearchParams({ q, k: '20', include_pinned: 'true' }); + if (typeFilter) params.set('type', typeFilter); + try { + const r = await fetch('/api/brain/memory/search?' + params.toString()); + if (!r.ok) throw new Error('HTTP ' + r.status); + const hits = await r.json(); + hits.forEach(m => { brainMemoryCache[m.id] = m; }); + brainSearchIds = hits.map(m => m.id); + if (info) { + info.style.display = 'block'; + info.innerHTML = `πŸ” ${hits.length} Treffer fΓΌr "${escapeHtml(q)}"` + + (typeFilter ? ` Β· Typ=${escapeHtml(typeFilter)}` : '') + + ` Β· sortiert nach Aehnlichkeit`; + } + renderBrainList(hits, true); + } catch (e) { + if (info) { + info.style.display = 'block'; + info.innerHTML = `πŸ”΄ Suche fehlgeschlagen: ${escapeHtml(e.message)}`; + } + } + } + + async function loadBrainMemoryList() { + const el = document.getElementById('brain-memory-list'); + if (!el) return; + // Wenn aktive Search-Treffer da sind: die anzeigen, nicht neu laden + if (brainSearchIds && brainSearchIds.length) { + const items = brainSearchIds.map(id => brainMemoryCache[id]).filter(Boolean); + renderBrainList(items, true); + return; + } + try { + const typeFilter = document.getElementById('brain-filter-type').value; + const pinnedFilter = document.getElementById('brain-filter-pinned').value; + const params = new URLSearchParams({ limit: '500' }); + if (typeFilter) params.set('type', typeFilter); + const r = await fetch('/api/brain/memory/list?' + params.toString()); + if (!r.ok) throw new Error('HTTP ' + r.status); + let items = await r.json(); + brainMemoryCache = {}; + items.forEach(m => { brainMemoryCache[m.id] = m; }); + if (pinnedFilter === 'pinned') items = items.filter(m => m.pinned); + else if (pinnedFilter === 'cold') items = items.filter(m => !m.pinned); + if (!items.length) { + el.innerHTML = '(Keine Memories β€” leere DB oder Filter zu eng)'; + return; + } + renderBrainList(items, false); + } catch (e) { + el.innerHTML = `πŸ”΄ Brain nicht erreichbar (${e.message})`; + } + } + + const BRAIN_TYPE_LABELS = { + identity: 'IdentitΓ€t', rule: 'Regeln / Werte', preference: 'Praeferenzen', + tool: 'Tools', skill: 'Skills', fact: 'Fakten', + conversation: 'Konversation', reminder: 'Reminder' + }; + const BRAIN_TYPE_ORDER = ['identity','rule','preference','tool','skill','fact','conversation','reminder']; + + function renderMemoryRow(m, withScore) { + const pin = m.pinned ? 'πŸ“Œ ' : ''; + const preview = (m.content || '').slice(0, 140).replace(/\n/g, ' '); + const score = withScore && typeof m.score === 'number' ? `${m.score.toFixed(2)}` : ''; + const typeBadge = withScore ? `${escapeHtml(BRAIN_TYPE_LABELS[m.type] || m.type)}` : ''; + return `
+
+
${typeBadge}${pin}${escapeHtml(m.title || '(ohne Titel)')}${score} + ${m.category ? `[${escapeHtml(m.category)}]` : ''} +
+
${escapeHtml(preview)}${m.content && m.content.length > 140 ? '...' : ''}
+
+ + +
`; + } + + function renderBrainList(items, isSearchResult) { + const el = document.getElementById('brain-memory-list'); + if (!el) return; + if (isSearchResult) { + // Such-Treffer: in Aehnlichkeits-Reihenfolge, kein Type-Gruppieren + const html = items.map(m => renderMemoryRow(m, true)).join(''); + el.innerHTML = html || '(Keine Treffer)'; + return; + } + // Normale Liste: nach Type gruppieren + const byType = {}; + items.forEach(m => { (byType[m.type] = byType[m.type] || []).push(m); }); + const html = BRAIN_TYPE_ORDER.flatMap(t => { + if (!byType[t]) return []; + const heading = `
${BRAIN_TYPE_LABELS[t] || t} (${byType[t].length})
`; + const rows = byType[t].map(m => renderMemoryRow(m, false)).join(''); + return [heading, rows]; + }).join(''); + // Falls Items mit unbekannten Types existieren, hinten dranhaengen + const extraTypes = Object.keys(byType).filter(t => !BRAIN_TYPE_ORDER.includes(t)); + let extra = ''; + for (const t of extraTypes) { + extra += `
${escapeHtml(t)} (${byType[t].length})
`; + extra += byType[t].map(m => renderMemoryRow(m, false)).join(''); + } + el.innerHTML = (html + extra) || '(Keine bekannten Typen gefunden)'; + } + + // ── Memory CRUD ─────────────────────────────────── + + function openMemoryModal(id) { + const modal = document.getElementById('memory-modal'); + const titleEl = document.getElementById('memory-modal-title'); + const idEl = document.getElementById('memory-edit-id'); + const errEl = document.getElementById('memory-modal-error'); + errEl.style.display = 'none'; + + if (id && brainMemoryCache[id]) { + const m = brainMemoryCache[id]; + titleEl.textContent = 'Memory bearbeiten'; + idEl.value = id; + document.getElementById('memory-type').value = m.type || 'fact'; + document.getElementById('memory-title').value = m.title || ''; + document.getElementById('memory-content').value = m.content || ''; + document.getElementById('memory-category').value = m.category || ''; + document.getElementById('memory-tags').value = (m.tags || []).join(', '); + document.getElementById('memory-pinned').checked = !!m.pinned; + } else { + titleEl.textContent = 'Neue Memory'; + idEl.value = ''; + document.getElementById('memory-type').value = 'fact'; + document.getElementById('memory-title').value = ''; + document.getElementById('memory-content').value = ''; + document.getElementById('memory-category').value = ''; + document.getElementById('memory-tags').value = ''; + document.getElementById('memory-pinned').checked = false; + } + modal.classList.add('open'); + } + + function closeMemoryModal() { + document.getElementById('memory-modal').classList.remove('open'); + } + + async function saveMemory() { + const errEl = document.getElementById('memory-modal-error'); + errEl.style.display = 'none'; + const id = document.getElementById('memory-edit-id').value; + const type = document.getElementById('memory-type').value; + const title = document.getElementById('memory-title').value.trim(); + const content = document.getElementById('memory-content').value.trim(); + const category = document.getElementById('memory-category').value.trim(); + const tags = document.getElementById('memory-tags').value.split(',').map(t => t.trim()).filter(Boolean); + const pinned = document.getElementById('memory-pinned').checked; + + if (!title) { errEl.textContent = 'Titel fehlt.'; errEl.style.display = 'block'; return; } + if (!content) { errEl.textContent = 'Inhalt fehlt.'; errEl.style.display = 'block'; return; } + + try { + let r; + if (id) { + r = await fetch('/api/brain/memory/update/' + encodeURIComponent(id), { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ title, content, pinned, category, tags }), + }); + } else { + r = await fetch('/api/brain/memory/save', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ type, title, content, pinned, category, tags, source: 'manual' }), + }); + } + if (!r.ok) { + const txt = await r.text(); + throw new Error('HTTP ' + r.status + ': ' + txt.slice(0, 200)); + } + closeMemoryModal(); + loadBrainMemoryList(); + loadBrainStatus(); + } catch (e) { + errEl.textContent = e.message; + errEl.style.display = 'block'; + } + } + + async function deleteMemory(id) { + const m = brainMemoryCache[id]; + const label = m ? `"${m.title}" (${m.type})` : id; + if (!confirm(`Memory ${label} wirklich lΓΆschen?`)) return; + try { + const r = await fetch('/api/brain/memory/delete/' + encodeURIComponent(id), { method: 'DELETE' }); + if (!r.ok) throw new Error('HTTP ' + r.status); + loadBrainMemoryList(); + loadBrainStatus(); + } catch (e) { + alert('LΓΆschen fehlgeschlagen: ' + e.message); + } + } + + // snapshotSessions entfernt β€” aria-core ist raus. + + function escapeHtml(s) { + return String(s).replace(/[&<>"']/g, c => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[c])); + } + + function brainExport() { + // Browser folgt der Download-Header-Antwort automatisch + window.location.href = '/api/brain-export'; + } + + // ── Migration aus brain-import/ ──────────────────────── + async function runMigration() { + const status = document.getElementById('brain-import-files-status'); + if (!confirm('Migration starten?\n\nParst die Markdown-Dateien aus brain-import/ und schreibt sie als pinned Memory-Punkte in die DB. Bei Re-Run werden nur die Migration-Punkte ersetzt β€” eigene Memories bleiben.')) return; + if (status) status.innerHTML = '⏳ Migriere...'; + try { + const r = await fetch('/api/brain/memory/migrate', { method: 'POST' }); + if (!r.ok) throw new Error('HTTP ' + r.status); + const d = await r.json(); + if (d.error) throw new Error(d.error); + const files = (d.files || []).join(', '); + if (status) status.innerHTML = `βœ“ ${d.created} Memories erstellt aus: ${files}`; + loadBrainMemoryList(); + loadBrainStatus(); + } catch (e) { + if (status) status.innerHTML = `βœ— ${e.message}`; + } + } + + async function refreshImportFiles() { + const el = document.getElementById('brain-import-files-status'); + if (!el) return; + try { + const r = await fetch('/api/brain/memory/import-files'); + if (!r.ok) throw new Error('HTTP ' + r.status); + const d = await r.json(); + if (!d.exists) { + el.innerHTML = `brain-import/ nicht gemountet (${d.import_dir})`; + return; + } + if (!d.files.length) { + el.innerHTML = 'Keine .md-Dateien in brain-import/'; + return; + } + const fmt = d.files.map(f => `${f.name} (${(f.size/1024).toFixed(1)}KB)`).join(', '); + el.innerHTML = `VerfΓΌgbar: ${escapeHtml(fmt)}`; + } catch (e) { + el.innerHTML = `Brain nicht erreichbar`; + } + } + + // ── Bootstrap Export / Import ────────────────────────── + async function exportBootstrap() { + const status = document.getElementById('bootstrap-status'); + if (status) status.innerHTML = '⏳ Lade...'; + try { + const r = await fetch('/api/brain/memory/export-bootstrap'); + if (!r.ok) throw new Error('HTTP ' + r.status); + const data = await r.json(); + const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const ts = new Date().toISOString().replace(/[:.]/g, '-').slice(0, 19); + const a = document.createElement('a'); + a.href = url; + a.download = `aria-bootstrap-${ts}.json`; + document.body.appendChild(a); a.click(); + setTimeout(() => { URL.revokeObjectURL(url); a.remove(); }, 100); + if (status) status.innerHTML = `βœ“ ${data.count} pinned Memories exportiert`; + } catch (e) { + if (status) status.innerHTML = `βœ— ${e.message}`; + } + } + + async function importBootstrap(event) { + const file = event.target.files[0]; + if (!file) return; + const status = document.getElementById('bootstrap-status'); + try { + const text = await file.text(); + const bundle = JSON.parse(text); + if (!Array.isArray(bundle.memories)) throw new Error('Datei hat kein "memories"-Array'); + if (!confirm(`Bootstrap importieren?\n\n${bundle.memories.length} pinned Memories aus "${file.name}".\n\nALLE aktuell pinned Memories werden ΓΌberschrieben. Cold Memory bleibt unverΓ€ndert.`)) { + event.target.value = ''; + return; + } + if (status) status.innerHTML = '⏳ Importiere...'; + const r = await fetch('/api/brain/memory/import-bootstrap', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: text, + }); + if (!r.ok) { + const errText = await r.text(); + throw new Error('HTTP ' + r.status + ': ' + errText.slice(0, 200)); + } + const d = await r.json(); + if (status) status.innerHTML = `βœ“ ${d.created} Memories importiert`; + loadBrainMemoryList(); + loadBrainStatus(); + } catch (e) { + if (status) status.innerHTML = `βœ— ${e.message}`; + } finally { + event.target.value = ''; + } + } + + async function brainImport(event) { + const file = event.target.files[0]; + if (!file) return; + const statusEl = document.getElementById('brain-import-status'); + if (!confirm(`Wirklich importieren? ALLE aktuellen Memories + Skills gehen verloren.\nDatei: ${file.name} (${(file.size/1024/1024).toFixed(1)} MB)`)) { + event.target.value = ''; + return; + } + statusEl.innerHTML = `⏳ Lade hoch (${(file.size/1024/1024).toFixed(1)} MB) β€” Container werden gestoppt...`; + try { + const r = await fetch('/api/brain-import', { + method: 'POST', + headers: { 'Content-Type': 'application/gzip' }, + body: file, + }); + const d = await r.json(); + if (d.ok) { + statusEl.innerHTML = `βœ“ ${d.message}`; + setTimeout(() => { loadBrainStatus(); loadBrainMemoryList(); }, 3000); + } else { + statusEl.innerHTML = `βœ— Fehler: ${d.error}`; + } + } catch (e) { + statusEl.innerHTML = `βœ— Upload fehlgeschlagen: ${e.message}`; + } finally { + event.target.value = ''; } } @@ -2522,10 +3326,7 @@ // ── Einstellungen: OpenClaw Config ────────────────────── - function loadOpenClawConfig() { - document.getElementById('openclaw-config').textContent = 'Lade...'; - send({ action: 'get_openclaw_config' }); - } + // loadOpenClawConfig entfernt β€” aria-core ist raus. // Toggle-Checkbox initial korrekt setzen const ttsToggleEl = document.getElementById('tts-debug-toggle'); diff --git a/diagnostic/server.js b/diagnostic/server.js index 067321a..cca1022 100644 --- a/diagnostic/server.js +++ b/diagnostic/server.js @@ -163,26 +163,10 @@ function traceEnd(ok, detail) { pendingMessageTime = 0; } -// ── Auto-Restart bei Netzwerk-Namespace-Verlust ────── -// Bei network_mode: "service:aria" verliert dieser Container -// den Netzwerkzugriff wenn aria-core neustartet. -// Nach MAX_GATEWAY_FAILURES aufeinanderfolgenden Fehlern β†’ process.exit -// Docker restart: unless-stopped startet uns mit neuem Namespace neu. -const MAX_GATEWAY_FAILURES = 6; // 6 Γ— 5s = 30s +// Auto-Restart-Heuristik entfernt (war an aria-core-Network gebunden). +// connectGateway ist ein No-Op solange der Brain-Loop noch nicht steht. let gatewayFailCount = 0; - -function checkGatewayHealth() { - if (state.gateway.status === "connected") { - gatewayFailCount = 0; - return; - } - gatewayFailCount++; - if (gatewayFailCount >= MAX_GATEWAY_FAILURES) { - log("error", "server", `Gateway ${MAX_GATEWAY_FAILURES}x nicht erreichbar β€” Neustart (Netzwerk-Namespace veraltet?)`); - // Kurze Verzoegerung damit die Log-Nachricht noch gesendet wird - setTimeout(() => process.exit(1), 500); - } -} +function checkGatewayHealth() { /* No-Op */ } function nextReqId() { return `diag-${++reqIdCounter}`; @@ -213,14 +197,15 @@ function broadcastState() { // ── OpenClaw Gateway Verbindung ───────────────────────── async function connectGateway() { - if (gatewayWs) { - try { gatewayWs.close(); } catch (_) {} - gatewayWs = null; - } - - state.gateway.status = "connecting"; - state.gateway.handshakeOk = false; + // aria-core/OpenClaw-Gateway abgerissen β€” diese Funktion ist ein No-Op, + // bis der neue Brain-Loop angedockt ist. Wir setzen den Status nur einmal. + state.gateway.status = "disabled"; + state.gateway.lastError = "aria-core entfernt β€” Brain-Loop in Arbeit"; broadcastState(); + return; + + // Originaler Connect-Code unten ist toter Code, bleibt zur Referenz. + // eslint-disable-next-line no-unreachable log("info", "gateway", `Verbinde: ${GATEWAY_URL}`); try { @@ -1113,6 +1098,44 @@ async function handleDockerLogs(ws, tab, tail) { // ── Docker Exec (Befehl in Container ausfuehren) ──────── +function dockerContainerStop(name, timeoutSec = 10) { + return new Promise((resolve, reject) => { + const req = http.request({ + socketPath: "/var/run/docker.sock", + path: `/containers/${name}/stop?t=${timeoutSec}`, + method: "POST", + headers: { "Content-Length": 0 }, + timeout: (timeoutSec + 5) * 1000, + }, (dRes) => { + // 204 = OK, 304 = already stopped, 404 = nicht da + if (dRes.statusCode === 204 || dRes.statusCode === 304) resolve(); + else if (dRes.statusCode === 404) resolve(); // Container fehlt: nicht fatal + else reject(new Error(`docker stop ${name}: HTTP ${dRes.statusCode}`)); + dRes.resume(); + }); + req.on("error", reject); + req.end(); + }); +} + +function dockerContainerStart(name) { + return new Promise((resolve, reject) => { + const req = http.request({ + socketPath: "/var/run/docker.sock", + path: `/containers/${name}/start`, + method: "POST", + headers: { "Content-Length": 0 }, + timeout: 30000, + }, (dRes) => { + if (dRes.statusCode === 204 || dRes.statusCode === 304) resolve(); + else reject(new Error(`docker start ${name}: HTTP ${dRes.statusCode}`)); + dRes.resume(); + }); + req.on("error", reject); + req.end(); + }); +} + function dockerExec(containerName, cmd) { return new Promise((resolve, reject) => { const createBody = JSON.stringify({ @@ -1184,17 +1207,9 @@ function waitForMessage(ws, timeoutMs) { }); } -// ── Watchdog: Stuck Run Erkennung ──────────────────────── - -let lastAgentActivity = Date.now(); -let watchdogWarned = false; -let watchdogFixAttempted = false; -let pendingMessageTime = 0; // Wann wurde die letzte Nachricht gesendet - -function updateAgentActivity() { - lastAgentActivity = Date.now(); - watchdogWarned = false; -} +// ── Watchdog entfernt β€” pendingMessageTime bleibt fuer /api/cancel +let pendingMessageTime = 0; +function updateAgentActivity() { /* No-Op nach Watchdog-Ausbau */ } // ── Disk-Space Monitor ─────────────────────────────── // Prueft regelmaessig die Host-Disk (via gemountetem /shared) und @@ -1243,51 +1258,8 @@ function checkDiskSpace() { setTimeout(checkDiskSpace, 2000); setInterval(checkDiskSpace, 30000); -// Watchdog prΓΌft alle 30s ob ARIA nach einer gesendeten Nachricht reagiert -setInterval(async () => { - if (pendingMessageTime === 0) return; // Keine Nachricht gesendet - const waitingMs = Date.now() - pendingMessageTime; - - // Nach 2min ohne Agent-Activity: Warnung - if (waitingMs > 120000 && !watchdogWarned) { - watchdogWarned = true; - log("warn", "server", `Watchdog: Keine ARIA-Aktivitaet seit ${Math.round(waitingMs / 1000)}s β€” moeglicherweise stuck`); - broadcast({ type: "watchdog", status: "warning", waitingMs, message: "ARIA reagiert nicht β€” moeglicherweise stuck Run" }); - } - - // Nach 5min: doctor --fix - if (waitingMs > 300000 && watchdogWarned && !watchdogFixAttempted) { - watchdogFixAttempted = true; - log("error", "server", "Watchdog: 5min ohne Antwort β€” fuehre openclaw doctor --fix aus"); - broadcast({ type: "watchdog", status: "fixing", message: "Auto-Fix: openclaw doctor --fix" }); - try { - await dockerExec("aria-core", "openclaw doctor --fix 2>/dev/null || true"); - log("info", "server", "Watchdog: doctor --fix ausgefuehrt"); - broadcast({ type: "watchdog", status: "fixed", message: "doctor --fix ausgefuehrt β€” warte auf Antwort..." }); - } catch (err) { - log("error", "server", `Watchdog: doctor --fix fehlgeschlagen: ${err.message}`); - } - } - - // Nach 8min: Container neustarten - if (waitingMs > 480000 && watchdogFixAttempted) { - log("error", "server", "Watchdog: 8min ohne Antwort β€” starte aria-core + aria-proxy neu"); - broadcast({ type: "watchdog", status: "restarting", message: "Container-Restart: aria-core + aria-proxy" }); - try { - const { execSync } = require("child_process"); - execSync("docker restart aria-core aria-proxy", { timeout: 60000 }); - log("info", "server", "Watchdog: Container neugestartet"); - broadcast({ type: "watchdog", status: "restarted", message: "Container neugestartet β€” warte auf Gateway-Reconnect..." }); - // Gateway wird sich automatisch neu verbinden - } catch (err) { - log("error", "server", `Watchdog: Container-Restart fehlgeschlagen: ${err.message}`); - broadcast({ type: "watchdog", status: "error", message: `Restart fehlgeschlagen: ${err.message}` }); - } - pendingMessageTime = 0; - watchdogWarned = false; - watchdogFixAttempted = false; - } -}, 30000); +// Watchdog entfernt β€” war auf aria-core/OpenClaw zugeschnitten (doctor --fix, +// docker restart aria-core). Der Brain-Loop bekommt seinen eigenen Health-Check. // ── HTTP Server + WebSocket fuer Browser ──────────────── @@ -1335,103 +1307,329 @@ const server = http.createServer((req, res) => { } else if (req.url === "/api/cancel" && req.method === "POST") { log("warn", "server", "HTTP /api/cancel β€” Cancel-Request (von Bridge)"); pendingMessageTime = 0; - watchdogWarned = false; - watchdogFixAttempted = false; if (traceActive) traceEnd(false, "Vom Benutzer abgebrochen (App)"); else broadcast({ type: "agent_activity", activity: "idle" }); - dockerExec("aria-core", "openclaw doctor --fix 2>/dev/null || true").catch(() => {}); res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: true })); - } else if (req.url === "/api/doctor-fix" && req.method === "POST") { - // Manueller "ARIA reparieren"-Button β€” stuck OpenClaw-Runs aufloesen. - log("info", "server", "HTTP /api/doctor-fix β€” manueller Reparatur-Trigger"); - dockerExec("aria-core", "openclaw doctor --fix 2>&1") - .then(out => { - const summary = (out || "").split("\n").filter(l => l.trim()).slice(-3).join(" | "); - broadcast({ type: "watchdog", status: "fixed", message: `Reparatur ausgefuehrt: ${summary || "OK"}` }); - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ ok: true, output: out })); - }) - .catch(err => { - broadcast({ type: "watchdog", status: "error", message: `Reparatur fehlgeschlagen: ${err.message}` }); - res.writeHead(500, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ ok: false, error: err.message })); - }); - return; - } else if (req.url === "/api/aria-session-reset" && req.method === "POST") { - // Sessions weg + Container neu β€” fuer Compact-After-N-Messages. - // E2BIG bei zu langen Sessions: argv beim Subprocess-spawn ueberschritten. - log("warn", "server", "HTTP /api/aria-session-reset β€” Sessions loeschen + Restart"); - broadcast({ type: "watchdog", status: "fixing", message: "Sessions werden geleert β€” ARIA bekommt frischen Start" }); - dockerExec("aria-core", "rm -f /home/node/.openclaw/agents/main/sessions/*.jsonl /home/node/.openclaw/agents/main/sessions/*.lock 2>&1 && echo '{}' > /home/node/.openclaw/agents/main/sessions/sessions.json") - .then(() => { - // Restart via Docker-API (gleicher Pfad wie /api/aria-restart) - const restartReq = http.request({ - socketPath: "/var/run/docker.sock", - path: "/containers/aria-core/restart?t=10", - method: "POST", - headers: { "Content-Length": 0 }, - timeout: 30000, - }, (dRes) => { - if (dRes.statusCode === 204) { - log("info", "server", "aria-session-reset OK"); - broadcast({ type: "watchdog", status: "fixed", message: "Sessions geleert, ARIA neu gestartet" }); - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ ok: true })); - } else { - res.writeHead(500, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ ok: false, error: `Docker-API ${dRes.statusCode}` })); - } - }); - restartReq.on("error", (err) => { - res.writeHead(500, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ ok: false, error: err.message })); - }); - restartReq.end(); - }) - .catch((err) => { - log("error", "server", `aria-session-reset Cleanup fehlgeschlagen: ${err.message}`); - res.writeHead(500, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ ok: false, error: err.message })); - }); - return; - } else if (req.url === "/api/aria-restart" && req.method === "POST") { - // Harter Restart β€” fuer Faelle wo doctor --fix nicht reicht (alive aber - // haengender Run). Geht ueber Docker-API (Socket), kein CLI noetig. - // POST /containers/aria-core/restart?t=10 β†’ SIGTERM, dann nach 10s SIGKILL. - log("warn", "server", "HTTP /api/aria-restart β€” harter Container-Restart"); - broadcast({ type: "watchdog", status: "fixing", message: "ARIA wird hart neu gestartet (~15s)" }); - const restartReq = http.request({ - socketPath: "/var/run/docker.sock", - path: "/containers/aria-core/restart?t=10", - method: "POST", - headers: { "Content-Length": 0 }, - timeout: 30000, - }, (dRes) => { - let body = ""; - dRes.on("data", (c) => body += c); - dRes.on("end", () => { - // Docker-API: 204 = OK, 404 = container nicht da, 500 = anderer Fehler - if (dRes.statusCode === 204) { - log("info", "server", "aria-restart OK (Docker-API)"); - broadcast({ type: "watchdog", status: "fixed", message: "ARIA wurde neu gestartet β€” sollte gleich antworten" }); - res.writeHead(200, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ ok: true })); - } else { - log("error", "server", `aria-restart Docker-API ${dRes.statusCode}: ${body.slice(0, 200)}`); - broadcast({ type: "watchdog", status: "error", message: `Restart fehlgeschlagen: HTTP ${dRes.statusCode}` }); - res.writeHead(500, { "Content-Type": "application/json" }); - res.end(JSON.stringify({ ok: false, error: `Docker-API ${dRes.statusCode}: ${body}` })); - } - }); - }); - restartReq.on("error", (err) => { - log("error", "server", `aria-restart Socket-Fehler: ${err.message}`); - broadcast({ type: "watchdog", status: "error", message: `Restart fehlgeschlagen: ${err.message}` }); + } else if (req.url === "/api/files-list" && req.method === "GET") { + // Liste alle Dateien in /shared/uploads/ β€” die kommen entweder vom User + // (Upload aus App/Diagnostic) oder von ARIA (aria_. Pattern). + try { + const dir = "/shared/uploads"; + let entries = []; + try { entries = fs.readdirSync(dir); } catch { entries = []; } + const files = entries + .map(name => { + try { + const full = path.join(dir, name); + const st = fs.statSync(full); + if (!st.isFile()) return null; + return { + name, + path: full, + size: st.size, + mtime: Math.floor(st.mtimeMs), + fromAria: name.startsWith("aria_"), + }; + } catch { return null; } + }) + .filter(Boolean) + .sort((a, b) => b.mtime - a.mtime); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: true, files })); + } catch (err) { res.writeHead(500, { "Content-Type": "application/json" }); res.end(JSON.stringify({ ok: false, error: err.message })); + } + return; + } else if (req.url.startsWith("/api/files-download?") && req.method === "GET") { + const u = new URL("http://x" + req.url); + const p = u.searchParams.get("path") || ""; + const safe = path.resolve(p); + if (!safe.startsWith("/shared/uploads/") || !fs.existsSync(safe)) { + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: false, error: "Datei nicht gefunden" })); + return; + } + const stat = fs.statSync(safe); + const fname = path.basename(safe); + res.writeHead(200, { + "Content-Type": "application/octet-stream", + "Content-Length": stat.size, + "Content-Disposition": `attachment; filename="${fname}"`, }); - restartReq.end(); + fs.createReadStream(safe).pipe(res); + return; + } else if (req.url === "/api/files-delete" && req.method === "POST") { + let body = ""; + req.on("data", c => { body += c; if (body.length > 4096) req.destroy(); }); + req.on("end", () => { + try { + const { path: p } = JSON.parse(body || "{}"); + const safe = path.resolve(p || ""); + if (!safe.startsWith("/shared/uploads/")) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: false, error: "Pfad nicht erlaubt" })); + return; + } + if (!fs.existsSync(safe)) { + res.writeHead(404, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: false, error: "Datei nicht vorhanden" })); + return; + } + fs.unlinkSync(safe); + log("info", "server", `Datei geloescht: ${safe}`); + // Live-Event an alle Browser-Clients + broadcast({ type: "file_deleted", path: safe }); + // Auch an die Bridge weiterleiten, damit die App-Bubbles aktualisiert + sendToRVS_raw({ + type: "file_deleted", + payload: { path: safe }, + timestamp: Date.now(), + }); + // Im chat_backup.jsonl markieren β€” beim Reload sehen Clients dass weg + try { + const backupFile = "/shared/config/chat_backup.jsonl"; + const line = JSON.stringify({ + type: "file_deleted", + path: safe, + ts: Date.now(), + by: "user", + }) + "\n"; + fs.appendFileSync(backupFile, line); + } catch {} + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: true })); + } catch (err) { + res.writeHead(500, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: false, error: err.message })); + } + }); + return; + } else if (req.url === "/api/voice-config-export" && req.method === "GET") { + // voice_config.json + highlight_triggers.json als JSON-Bundle exportieren + try { + const bundle = {}; + try { bundle.voice_config = JSON.parse(fs.readFileSync("/shared/config/voice_config.json", "utf-8")); } catch {} + try { bundle.highlight_triggers = JSON.parse(fs.readFileSync("/shared/config/highlight_triggers.json", "utf-8")); } catch {} + const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); + res.writeHead(200, { + "Content-Type": "application/json", + "Content-Disposition": `attachment; filename="aria-voice-settings-${ts}.json"`, + }); + res.end(JSON.stringify(bundle, null, 2)); + } catch (err) { + res.writeHead(500, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: false, error: err.message })); + } + return; + } else if (req.url === "/api/voice-config-import" && req.method === "POST") { + let body = ""; + req.on("data", c => { body += c; if (body.length > 1024 * 1024) req.destroy(); }); + req.on("end", () => { + try { + const bundle = JSON.parse(body || "{}"); + fs.mkdirSync("/shared/config", { recursive: true }); + if (bundle.voice_config && typeof bundle.voice_config === "object") { + fs.writeFileSync("/shared/config/voice_config.json", JSON.stringify(bundle.voice_config, null, 2)); + } + if (bundle.highlight_triggers && typeof bundle.highlight_triggers === "object") { + fs.writeFileSync("/shared/config/highlight_triggers.json", JSON.stringify(bundle.highlight_triggers, null, 2)); + } + log("info", "server", "Voice-Settings importiert"); + // Bridge bekommt die neue Config via RVS (bei naechstem Reload), aber + // ein Restart ist sauberer damit Whisper/F5 sofort neu laden. + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: true, message: "Voice-Settings importiert β€” Bridge-Restart empfohlen." })); + } catch (err) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: false, error: err.message })); + } + }); + return; + } else if (req.url === "/api/wipe-all" && req.method === "POST") { + // Komplett-Reset β€” Gedaechtnis, Stimmen, Config alle weg. SSH-Keys + // und .env bleiben, RVS-Anbindung bleibt. Brain + Qdrant werden + // gestoppt, Filesystem geleert, dann neu gestartet. + log("warn", "server", "HTTP /api/wipe-all β€” Gesamt-Reset"); + const { spawn } = require("child_process"); + + Promise.resolve() + .then(() => dockerContainerStop("aria-brain").catch(() => {})) + .then(() => dockerContainerStop("aria-qdrant").catch(() => {})) + .then(() => new Promise((resolve, reject) => { + // Liste der Pfade die innerhalb des Diagnostic-Containers gemountet + // sind: /shared (config + voices) und /brain (Memory + Skills). + const cmd = [ + "rm -rf /shared/config /shared/voices /shared/conversations-archive /shared/chat_backup.jsonl", + "rm -rf /brain/data /brain/qdrant /brain/.[!.]* 2>/dev/null || true", + "mkdir -p /brain/data /brain/qdrant /shared/config /shared/voices", + ].join(" && "); + const sh = spawn("sh", ["-c", cmd]); + let stderr = ""; + sh.stderr.on("data", d => stderr += d.toString()); + sh.on("close", c => c === 0 ? resolve() : reject(new Error(`wipe exit ${c}: ${stderr}`))); + })) + .then(async () => { + // Bridge auch neustarten, damit Whisper-Config + Voice-Config frisch + await dockerContainerStop("aria-bridge").catch(() => {}); + await dockerContainerStart("aria-qdrant"); + await new Promise(r => setTimeout(r, 2000)); + await dockerContainerStart("aria-brain"); + await dockerContainerStart("aria-bridge"); + log("info", "server", "wipe-all OK β€” Brain + Qdrant + Bridge neu gestartet"); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: true, message: "Komplett-Reset durchgefuehrt β€” ARIA ist leer." })); + }) + .catch(async err => { + log("error", "server", `wipe-all: ${err.message}`); + // Container trotzdem hochfahren + try { await dockerContainerStart("aria-qdrant"); } catch {} + try { await dockerContainerStart("aria-brain"); } catch {} + try { await dockerContainerStart("aria-bridge"); } catch {} + res.writeHead(500, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: false, error: err.message })); + }); + return; + } else if (req.url === "/api/container-restart" && req.method === "POST") { + // Generischer Restart fuer ARIAs Container β€” Whitelist verhindert + // dass jemand aria-proxy oder das Diagnostic selbst kickt. + let body = ""; + req.on("data", c => { body += c; if (body.length > 1024) req.destroy(); }); + req.on("end", async () => { + try { + const { name } = JSON.parse(body || "{}"); + const ALLOWED = ["aria-bridge", "aria-brain", "aria-qdrant"]; + if (!ALLOWED.includes(name)) { + res.writeHead(400, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: false, error: `Container '${name}' nicht erlaubt (Whitelist: ${ALLOWED.join(", ")})` })); + return; + } + log("info", "server", `HTTP /api/container-restart β€” ${name}`); + await dockerContainerStop(name).catch(() => {}); + await new Promise(r => setTimeout(r, 500)); + await dockerContainerStart(name); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: true, container: name })); + } catch (err) { + log("error", "server", `container-restart: ${err.message}`); + res.writeHead(500, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: false, error: err.message })); + } + }); + return; + } else if (req.url.startsWith("/api/brain/")) { + // Reverse-Proxy zum aria-brain Container (intern auf 8080, nicht expose'd). + // Frontend ruft z.B. /api/brain/health β†’ http://aria-brain:8080/health + const targetPath = req.url.replace(/^\/api\/brain/, ""); + const proxyReq = http.request({ + host: "aria-brain", + port: 8080, + path: targetPath, + method: req.method, + headers: req.headers, + timeout: 30000, + }, (proxyRes) => { + res.writeHead(proxyRes.statusCode, proxyRes.headers); + proxyRes.pipe(res); + }); + proxyReq.on("error", (err) => { + res.writeHead(503, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ status: "unreachable", error: err.message })); + }); + req.pipe(proxyReq); + return; + } else if (req.url === "/api/brain-export" && req.method === "GET") { + // Komplettes Gehirn als tar.gz streamen. + // Schritte: Brain + Qdrant stoppen (saubere Bytes) β†’ tar streamen β†’ wieder starten. + log("info", "server", "HTTP /api/brain-export β€” Gehirn-Export gestartet"); + const { spawn } = require("child_process"); + + dockerContainerStop("aria-brain").catch(() => {}) + .then(() => dockerContainerStop("aria-qdrant").catch(() => {})) + .then(() => { + const ts = new Date().toISOString().replace(/[:.]/g, "-").slice(0, 19); + res.writeHead(200, { + "Content-Type": "application/gzip", + "Content-Disposition": `attachment; filename="aria-brain-${ts}.tar.gz"`, + "Cache-Control": "no-store", + }); + const tar = spawn("tar", ["czf", "-", "-C", "/brain", "."]); + tar.stdout.pipe(res); + let stderr = ""; + tar.stderr.on("data", d => stderr += d.toString()); + const restartAll = async () => { + try { + await dockerContainerStart("aria-qdrant"); + await new Promise(r => setTimeout(r, 1500)); + await dockerContainerStart("aria-brain"); + log("info", "server", "brain-export: Container wieder gestartet"); + } catch (e) { + log("error", "server", `brain-export Restart fehlgeschlagen: ${e.message}`); + } + }; + tar.on("close", (code) => { + if (code !== 0) log("error", "server", `brain-export tar exit ${code}: ${stderr.slice(0, 200)}`); + restartAll(); + }); + // Client-Disconnect β†’ tar abbrechen + Container wieder hoch + req.on("close", () => { if (!tar.killed) { tar.kill("SIGTERM"); restartAll(); } }); + }) + .catch(err => { + log("error", "server", `brain-export: ${err.message}`); + if (!res.headersSent) { + res.writeHead(500, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: false, error: err.message })); + } + }); + return; + } else if (req.url === "/api/brain-import" && req.method === "POST") { + // Body ist die rohe tar.gz-Datei (kein multipart). Beliebig grosse Uploads + // werden direkt in tar -xz gepiped β†’ kein Memory-Bloat. + log("warn", "server", "HTTP /api/brain-import β€” Gehirn-Import gestartet"); + const { spawn } = require("child_process"); + + dockerContainerStop("aria-brain").catch(() => {}) + .then(() => dockerContainerStop("aria-qdrant").catch(() => {})) + .then(() => new Promise((resolve, reject) => { + // Brain-Verzeichnis leeren (Mount-Point selbst nicht entfernen) + const rm = spawn("sh", ["-c", "rm -rf /brain/data /brain/qdrant /brain/.[!.]* 2>/dev/null; mkdir -p /brain/data /brain/qdrant"]); + rm.on("close", (code) => code === 0 ? resolve() : reject(new Error(`cleanup exit ${code}`))); + rm.on("error", reject); + })) + .then(() => new Promise((resolve, reject) => { + const tar = spawn("tar", ["xzf", "-", "-C", "/brain"]); + req.pipe(tar.stdin); + let stderr = ""; + tar.stderr.on("data", d => stderr += d.toString()); + tar.on("close", (code) => { + if (code === 0) resolve(); + else reject(new Error(`tar exit ${code}: ${stderr.slice(0, 300)}`)); + }); + tar.on("error", reject); + // Falls Client mittendrin abbricht + req.on("close", () => { if (!tar.killed && !req.complete) tar.kill("SIGTERM"); }); + })) + .then(async () => { + // Subdirs sicherstellen (falls Archiv unsauber war) + await new Promise(r => spawn("mkdir", ["-p", "/brain/data", "/brain/qdrant"]).on("close", r)); + await dockerContainerStart("aria-qdrant"); + await new Promise(r => setTimeout(r, 2000)); + await dockerContainerStart("aria-brain"); + log("info", "server", "brain-import OK β€” Container neu gestartet"); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: true, message: "Gehirn importiert, Container neu gestartet" })); + }) + .catch(async (err) => { + log("error", "server", `brain-import fehlgeschlagen: ${err.message}`); + // Container trotzdem wieder hochfahren, sonst steht alles + try { await dockerContainerStart("aria-qdrant"); } catch {} + try { await dockerContainerStart("aria-brain"); } catch {} + if (!res.headersSent) { + res.writeHead(500, { "Content-Type": "application/json" }); + res.end(JSON.stringify({ ok: false, error: err.message })); + } + }); return; } else if (req.url.startsWith("/shared/")) { // Dateien aus Shared Volume ausliefern (Bilder, Uploads) @@ -1531,6 +1729,12 @@ wss.on("connection", (ws) => { } else if (msg.action === "xtts_list_voices") { // Frische Verbindung die auf Antwort wartet sendToRVS_withResponse("xtts_list_voices", {}, "xtts_voices_list", ws); + } else if (msg.action === "xtts_export_voice") { + // Anfrage an XTTS-Bridge β€” gibt die Stimme als base64 tar.gz zurueck + sendToRVS_withResponse("xtts_export_voice", { name: msg.name }, "xtts_voice_exported", ws); + } else if (msg.action === "xtts_import_voice") { + // tar.gz (base64) an XTTS-Bridge schicken β€” die packt aus + sendToRVS_withResponse("xtts_import_voice", { name: msg.name, data: msg.data }, "xtts_voice_imported", ws); } else if (msg.action === "xtts_delete_voice") { // Weiterleiten an XTTS-Bridge, die antwortet mit neuer Liste sendToRVS_raw({ type: "xtts_delete_voice", payload: { name: msg.name }, timestamp: Date.now() }); @@ -1577,35 +1781,19 @@ wss.on("connection", (ws) => { checkDesktopAvailable(ws); } else if (msg.action === "load_chat_history") { handleLoadChatHistory(ws); - } else if (msg.action === "list_sessions") { - handleListSessions(ws); - } else if (msg.action === "read_session") { - handleReadSession(ws, msg.sessionPath); - } else if (msg.action === "export_session") { - handleExportSession(ws, msg.sessionPath, msg.sessionKey); - } else if (msg.action === "delete_session") { - handleDeleteSession(ws, msg.sessionPath); - } else if (msg.action === "set_active_session") { - handleSetActiveSession(ws, msg.sessionKey); - } else if (msg.action === "get_active_session") { - ws.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey })); - } else if (msg.action === "create_session") { - handleCreateSession(ws, msg.sessionName); + // Sessions- und Brain-File-Viewer entfernt β€” Sessions sind raus, Memory + // laeuft jetzt komplett ueber die Vector-DB im aria-brain (siehe Gehirn-Tab). + // restart_session kommt weiter rein, weil der Watchdog ihn manchmal triggert. } else if (msg.action === "restart_session") { handleRestartSession(ws); - } else if (msg.action === "list_brain") { - handleListBrain(ws); - } else if (msg.action === "read_brain_file") { - handleReadBrainFile(ws, msg.filename); // ── Einstellungen ── // list_permissions / save_permissions entfernt β€” Alles-oder-Nichts via --dangerously-skip-permissions } else if (msg.action === "get_model") { handleGetModel(ws); } else if (msg.action === "set_model") { handleSetModel(ws, msg.model); - } else if (msg.action === "get_openclaw_config") { - handleGetOpenClawConfig(ws); } + // get_openclaw_config entfernt β€” aria-core ist raus. } catch {} }); @@ -1838,446 +2026,89 @@ function checkDesktopAvailable(clientWs) { }); } -// ── Session Viewer ────────────────────────────────────── +// ── Session-Viewer + Brain-File-Viewer entfernt ───────── +// OpenClaw-Sessions sind raus. Wer alte jsonl-Konversationen sichern +// will: POST /api/sessions-snapshot kopiert sie nach +// /shared/conversations-archive/ β†’ spaeter Brain-Migration. +// Memory laeuft jetzt komplett ueber den aria-brain Container +// (Vector-DB, siehe /api/brain/* Proxy). -const SESSIONS_DIR = "/home/node/.openclaw/agents/main/sessions"; - -async function handleListSessions(clientWs) { - try { - log("info", "server", "Lade Sessions aus aria-core..."); - - // sessions.json als Index lesen + Datei-Details holen (inkl. .reset.* Archive) - const raw = await dockerExec("aria-core", ` - cat ${SESSIONS_DIR}/sessions.json 2>/dev/null || echo '{}' && - echo '===FILE_DETAILS===' && - for f in ${SESSIONS_DIR}/*.jsonl ${SESSIONS_DIR}/*.jsonl.reset.*; do - [ -f "$f" ] || continue - name=$(basename "$f") - msgs=$(grep -cE '"role":"(user|assistant)"' "$f" 2>/dev/null || echo 0) - size=$(du -h "$f" 2>/dev/null | cut -f1) - modified=$(stat -c '%Y' "$f" 2>/dev/null || echo 0) - echo "FILE:$name|LINES:$msgs|SIZE:$size|MODIFIED:$modified" - done - `.trim()); - - const parts = raw.split("===FILE_DETAILS==="); - let sessionsIndex = {}; - try { sessionsIndex = JSON.parse(parts[0].trim()); } catch {} - - // Datei-Details parsen - const fileDetails = {}; - if (parts[1]) { - for (const line of parts[1].trim().split("\n")) { - if (!line.startsWith("FILE:")) continue; - const segs = {}; - for (const seg of line.split("|")) { - const idx = seg.indexOf(":"); - if (idx > 0) segs[seg.slice(0, idx)] = seg.slice(idx + 1); - } - if (segs.FILE) fileDetails[segs.FILE] = segs; - } - } - - // Sessions zusammenbauen: Index + Datei-Info kombinieren - const sessions = []; - - // Aus sessions.json die Session-Keys und IDs - const indexEntries = Array.isArray(sessionsIndex) ? sessionsIndex - : Array.isArray(sessionsIndex.sessions) ? sessionsIndex.sessions - : Object.entries(sessionsIndex).map(([k, v]) => ({ key: k, ...(typeof v === "object" ? v : { id: v }) })); - - for (const entry of indexEntries) { - const id = entry.id || entry.sessionId || ""; - const rawKey = entry.key || entry.sessionKey || entry.name || id; - // "agent:main:aria-diagnostic" β†’ "aria-diagnostic" - const key = rawKey.replace(/^agent:main:/, ""); - const filename = `${id}.jsonl`; - const details = fileDetails[filename] || {}; - // updatedAt aus sessions.json (ms) ist genauer als stat - const updatedAt = entry.updatedAt || 0; - const model = entry.model || ""; - - sessions.push({ - path: `${SESSIONS_DIR}/${filename}`, - sessionKey: key, - sessionId: id, - lines: parseInt(details.LINES) || 0, - size: details.SIZE || "?", - modified: updatedAt ? Math.floor(updatedAt / 1000) : (parseInt(details.MODIFIED) || 0), - model, - }); - - // Aus fileDetails entfernen (fuer Waisen-Check) - delete fileDetails[filename]; - } - - // Dateien die nicht im Index stehen (Waisen ODER Reset-Archive) - for (const [filename, details] of Object.entries(fileDetails)) { - // .jsonl.reset.Z β†’ archivierte Session (OpenClaw-Reset) - // Format: 528f4d70-...jsonl.reset.2026-04-18T09-49-44.814Z - const resetMatch = filename.match(/^([a-f0-9-]+)\.jsonl\.reset\.(.+Z)$/); - if (resetMatch) { - const id = resetMatch[1]; - // Timestamp ISO-8601 parsen: 2026-04-18T09-49-44.814Z β†’ 2026-04-18T09:49:44.814Z - const tsStr = resetMatch[2].replace(/T(\d{2})-(\d{2})-(\d{2})/, "T$1:$2:$3"); - const resetAt = Math.floor(new Date(tsStr).getTime() / 1000) || parseInt(details.MODIFIED) || 0; - sessions.push({ - path: `${SESSIONS_DIR}/${filename}`, - sessionKey: id.slice(0, 8) + "… (archiv)", - sessionId: id, - lines: parseInt(details.LINES) || 0, - size: details.SIZE || "?", - modified: resetAt, - archived: true, - resetAt, - }); - continue; - } - // Echte Waisen (UUID.jsonl ohne Eintrag in sessions.json) - const id = filename.replace(".jsonl", ""); - sessions.push({ - path: `${SESSIONS_DIR}/${filename}`, - sessionKey: id.slice(0, 12) + "...", - sessionId: id, - lines: parseInt(details.LINES) || 0, - size: details.SIZE || "?", - modified: parseInt(details.MODIFIED) || 0, - orphan: true, - }); - } - - sessions.sort((a, b) => b.modified - a.modified); - - clientWs.send(JSON.stringify({ type: "sessions_list", sessions })); - log("info", "server", `${sessions.length} Session(s) gefunden`); - } catch (err) { - log("error", "server", `Sessions laden fehlgeschlagen: ${err.message}`); - clientWs.send(JSON.stringify({ type: "sessions_list", sessions: [], error: err.message })); - } -} - -async function handleReadSession(clientWs, sessionPath) { - if (!sessionPath || sessionPath.includes("..")) { - clientWs.send(JSON.stringify({ type: "session_detail", error: "Ungueltiger Pfad" })); - return; - } - try { - // Letzte 100 Zeilen der Session (JSONL) - const raw = await dockerExec("aria-core", `tail -100 '${sessionPath.replace(/'/g, "")}'`); - const messages = []; - for (const line of raw.split("\n")) { - if (!line.trim()) continue; - try { - const obj = JSON.parse(line); - messages.push(obj); - } catch {} - } - clientWs.send(JSON.stringify({ type: "session_detail", path: sessionPath, messages, raw: messages.length === 0 ? raw : undefined })); - } catch (err) { - clientWs.send(JSON.stringify({ type: "session_detail", error: err.message })); - } -} - -async function handleExportSession(clientWs, sessionPath, sessionKey) { - if (!sessionPath || sessionPath.includes("..") || !sessionPath.startsWith(SESSIONS_DIR)) { - clientWs.send(JSON.stringify({ type: "session_export", ok: false, error: "Ungueltiger Pfad" })); - return; - } - try { - const safePath = sessionPath.replace(/'/g, ""); - const raw = await dockerExec("aria-core", `cat '${safePath}'`); - const lines = raw.split("\n").filter(l => l.trim()); - - const blocks = []; - for (const line of lines) { - let obj; - try { obj = JSON.parse(line); } catch { continue; } - if (obj.type !== "message" || !obj.message) continue; - const role = obj.message.role; - if (role !== "user" && role !== "assistant") continue; - - let text = ""; - const content = obj.message.content; - if (typeof content === "string") text = content; - else if (Array.isArray(content)) text = content.filter(c => c.type === "text").map(c => c.text || "").join("\n"); - if (!text) continue; - - if (role === "user") { - text = text.replace(/^Sender \(untrusted metadata\):[\s\S]*?```[\s\S]*?```\s*\n*/m, "").trim(); - text = text.replace(/^\[.*?\]\s*/, "").trim(); - } else { - text = text.replace(/^\[\[reply_to_\w+\]\]\s*/g, "").trim(); - } - if (!text) continue; - - const ts = obj.message.timestamp || obj.timestamp || 0; - const when = ts ? new Date(ts).toISOString().replace("T", " ").slice(0, 19) : ""; - const heading = role === "user" ? "## πŸ§‘ User" : "## πŸ€– ARIA"; - blocks.push(`${heading}${when ? ` β€” ${when}` : ""}\n\n${text}`); - } - - const exportedAt = new Date().toISOString().replace("T", " ").slice(0, 19); - const title = sessionKey || sessionPath.split("/").pop().replace(".jsonl", ""); - const markdown = [ - `# Session: ${title}`, - ``, - `Exportiert: ${exportedAt} `, - `Quelle: ${sessionPath}`, - ``, - `---`, - ``, - blocks.join("\n\n---\n\n"), - ``, - ].join("\n"); - - const safeKey = (sessionKey || "session").replace(/[^a-zA-Z0-9_-]/g, "_"); - const filename = `${exportedAt.slice(0, 10)}_${safeKey}.md`; - clientWs.send(JSON.stringify({ type: "session_export", ok: true, filename, markdown })); - log("info", "server", `Session exportiert: ${filename} (${blocks.length} Nachrichten)`); - } catch (err) { - log("error", "server", `Session-Export fehlgeschlagen: ${err.message}`); - clientWs.send(JSON.stringify({ type: "session_export", ok: false, error: err.message })); - } -} - -async function handleDeleteSession(clientWs, sessionPath) { - if (!sessionPath || sessionPath.includes("..") || !sessionPath.startsWith(SESSIONS_DIR)) { - clientWs.send(JSON.stringify({ type: "session_deleted", ok: false, error: "Ungueltiger Pfad" })); - return; - } - try { - log("warn", "server", `Loesche Session: ${sessionPath}`); - const safePath = sessionPath.replace(/'/g, ""); - // Session-ID aus Pfad extrahieren (UUID.jsonl) - const filename = safePath.split("/").pop(); - const sessionId = filename.replace(".jsonl", ""); - - // 1. JSONL-Datei loeschen - await dockerExec("aria-core", `rm -f '${safePath}'`); - - // 2. Eintrag aus sessions.json entfernen - try { - const sFile = `${SESSIONS_DIR}/sessions.json`; - const script = [ - 'const fs=require("fs");', - `const f="${sFile}",sid="${sessionId}";`, - 'try{const d=JSON.parse(fs.readFileSync(f,"utf8"));', - 'for(const k of Object.keys(d)){const v=d[k];', - 'if(v&&(v.sessionId===sid||v.id===sid))delete d[k];}', - 'fs.writeFileSync(f,JSON.stringify(d,null,2));}catch(e){}', - ].join(""); - const b64 = Buffer.from(script).toString("base64"); - await dockerExec("aria-core", `echo ${b64} | base64 -d | node`); - } catch (e) { - log("warn", "server", `sessions.json Update fehlgeschlagen: ${e.message}`); - } - - clientWs.send(JSON.stringify({ type: "session_deleted", ok: true, path: sessionPath })); - log("info", "server", "Session geloescht"); - } catch (err) { - clientWs.send(JSON.stringify({ type: "session_deleted", ok: false, error: err.message })); - } -} - -// ── Session-Aufloesung: letzte aktive Session finden ──── -// Wird nach Gateway-(Re-)Connect aufgerufen. Darf die explizit gewaehlte -// Session NIE ueberschreiben β€” nur beim absoluten Erststart auto-picken. -async function resolveActiveSession() { - if (sessionFromFile) { - log("info", "server", `Session '${activeSessionKey}' aus /data β€” keine Auto-Wahl`); - return; - } - - const indexRaw = await dockerExec("aria-core", `cat ${SESSIONS_DIR}/sessions.json 2>/dev/null || echo '{}'`); - log("debug", "server", `sessions.json: ${indexRaw.slice(0, 500)}`); - let sessionsIndex = {}; - try { sessionsIndex = JSON.parse(indexRaw.trim()); } catch { return; } - - const entries = Array.isArray(sessionsIndex) ? sessionsIndex - : Array.isArray(sessionsIndex.sessions) ? sessionsIndex.sessions - : Object.entries(sessionsIndex).map(([k, v]) => ({ key: k, ...(typeof v === "object" ? v : { id: v }) })); - - if (entries.length === 0) return; - - // Vorhandene Keys loggen - const keys = entries.map(e => (e.key || e.sessionKey || e.name || "?").replace(/^agent:main:/, "")); - log("info", "server", `Verfuegbare Sessions: [${keys.join(", ")}]`); - - // Neueste Session nehmen β€” aber user-definierte bevorzugen. - // aria-bridge / aria-diagnostic werden von den Services auto-erstellt; - // bei erstem Start soll lieber eine "echte" Session gewaehlt werden, - // falls vorhanden. - const AUTO_KEYS = new Set(["aria-bridge", "aria-diagnostic"]); - const normalise = (e) => (e.key || e.sessionKey || e.name || "").replace(/^agent:main:/, ""); - - const userEntries = entries.filter(e => !AUTO_KEYS.has(normalise(e))); - const pool = userEntries.length > 0 ? userEntries : entries; - - let newest = null; - let newestTime = 0; - for (const entry of pool) { - const t = entry.updatedAt || entry.createdAt || 0; - if (t >= newestTime) { - newestTime = t; - newest = entry; - } - } - - if (newest) { - const key = normalise(newest); - if (key) { - activeSessionKey = key; - persistActiveSession(activeSessionKey); - log("info", "server", `Auto-Wahl Erststart: '${activeSessionKey}'`); - for (const c of browserClients) { - c.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey })); - } - } - } -} - -// ── Chat-History aus aktiver Session laden (Display-Only) ── +// ── Chat-History: liest aus chat_backup.jsonl ──────────── +// Ablage: jede Zeile ein JSON-Eintrag. +// {ts, role: "user"|"assistant", text, session} +// {ts, type: "file_deleted", path, by} +// Marker [FILE: /shared/uploads/...] werden zu aria_file-Bubbles aufgeloest, +// wenn die Datei noch existiert; geloeschte Dateien zu deleted-Markern. async function handleLoadChatHistory(clientWs) { try { - // Session-ID fuer den aktiven sessionKey finden - const indexRaw = await dockerExec("aria-core", `cat ${SESSIONS_DIR}/sessions.json 2>/dev/null || echo '{}'`); - let sessionsIndex = {}; - try { sessionsIndex = JSON.parse(indexRaw.trim()); } catch {} - - const entries = Array.isArray(sessionsIndex) ? sessionsIndex - : Array.isArray(sessionsIndex.sessions) ? sessionsIndex.sessions - : Object.entries(sessionsIndex).map(([k, v]) => ({ key: k, ...(typeof v === "object" ? v : { id: v }) })); - - let sessionId = null; - const availableKeys = []; - for (const entry of entries) { - const rawKey = entry.key || entry.sessionKey || entry.name || ""; - const key = rawKey.replace(/^agent:main:/, ""); - availableKeys.push(key); - if (key === activeSessionKey) { - sessionId = entry.id || entry.sessionId || ""; - break; - } - } - - if (!sessionId) { - log("warn", "server", `Chat-History: Session '${activeSessionKey}' nicht in sessions.json gefunden. Verfuegbar: [${availableKeys.join(", ")}]`); + const file = "/shared/config/chat_backup.jsonl"; + let raw = ""; + try { raw = fs.readFileSync(file, "utf-8"); } catch { clientWs.send(JSON.stringify({ type: "chat_history", messages: [] })); return; } + const lines = raw.split("\n").filter(l => l.trim()); + const deletedPaths = new Set(); + const messages = []; + const mimeMap = { + ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif", + ".webp": "image/webp", ".svg": "image/svg+xml", ".pdf": "application/pdf", + ".mp3": "audio/mpeg", ".wav": "audio/wav", ".txt": "text/plain", + ".md": "text/markdown", ".json": "application/json", ".zip": "application/zip", + }; - log("info", "server", `Chat-History: Session '${activeSessionKey}' -> ID '${sessionId}'`); - - const sessionFile = `${SESSIONS_DIR}/${sessionId}.jsonl`; - const raw = await dockerExec("aria-core", `cat '${sessionFile}' 2>/dev/null || echo ''`); - const chatMessages = []; - - for (const line of raw.split("\n")) { - if (!line.trim()) continue; + // Erst Durchlauf: deleted-Marker sammeln + for (const line of lines) { try { const obj = JSON.parse(line); - // OpenClaw Format: {"type":"message","message":{"role":"user|assistant","content":[...]}} - if (obj.type !== "message" || !obj.message) continue; - const msg = obj.message; - const role = msg.role; - if (!role) continue; - - // Text aus content-Array extrahieren - let text = ""; - if (typeof msg.content === "string") text = msg.content; - else if (Array.isArray(msg.content)) text = msg.content.filter(c => c.type === "text").map(c => c.text || "").join("\n"); - if (!text) continue; - - if (role === "user") { - // Metadata-Prefix entfernen: "Sender (untrusted metadata):\n```json\n{...}\n```\n\n[timestamp] Text" - text = text.replace(/^Sender \(untrusted metadata\):[\s\S]*?```[\s\S]*?```\s*\n*/m, "").trim(); - // Timestamp-Prefix entfernen: "[Sat 2026-03-28 14:51 UTC] " - text = text.replace(/^\[.*?\]\s*/, "").trim(); - chatMessages.push({ type: "sent", text, meta: "Gateway direkt", ts: msg.timestamp || obj.timestamp || 0 }); - } else if (role === "assistant") { - // Reply-Prefix entfernen: "[[reply_to_current]] " - text = text.replace(/^\[\[reply_to_\w+\]\]\s*/g, "").trim(); - const ts = msg.timestamp || obj.timestamp || 0; - // ARIA-File-Marker aus dem Text parsen β€” pro existierender Datei - // eine separate file_from_aria-aehnliche Message einfuegen damit die - // Anhang-Bubble nach Browser-Refresh wieder erscheint. - const fileRe = /\[FILE:\s*(\/shared\/uploads\/[^\]]+?)\s*\]/gi; - let m; - while ((m = fileRe.exec(text)) !== null) { - const p = m[1].trim(); - try { - if (fs.existsSync(p)) { - const st = fs.statSync(p); - const ext = path.extname(p).toLowerCase(); - const mimeMap = { ".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".png": "image/png", ".gif": "image/gif", - ".webp": "image/webp", ".svg": "image/svg+xml", ".pdf": "application/pdf", - ".mp3": "audio/mpeg", ".mid": "audio/midi", ".midi": "audio/midi", - ".wav": "audio/wav", ".txt": "text/plain", ".md": "text/markdown", - ".json": "application/json", ".zip": "application/zip" }; - chatMessages.push({ - type: "aria_file", - serverPath: p, - name: path.basename(p), - mimeType: mimeMap[ext] || "application/octet-stream", - size: st.size, - ts, - }); - } - } catch {} - } - if (text) chatMessages.push({ type: "received", text, meta: "chat:final", ts }); - } + if (obj.type === "file_deleted" && obj.path) deletedPaths.add(obj.path); } catch {} } - clientWs.send(JSON.stringify({ type: "chat_history", messages: chatMessages })); - log("info", "server", `Chat-History geladen: ${chatMessages.length} Nachrichten`); + for (const line of lines) { + let obj; + try { obj = JSON.parse(line); } catch { continue; } + if (obj.type === "file_deleted") continue; // Marker selbst nicht anzeigen + if (obj.role !== "user" && obj.role !== "assistant") continue; + const ts = obj.ts || 0; + const text = String(obj.text || ""); + if (obj.role === "user") { + if (text) messages.push({ type: "sent", text, meta: "Gateway direkt", ts }); + continue; + } + // assistant: nach FILE-Markern scannen, eigene aria_file-Eintraege pro Datei + const fileRe = /\[FILE:\s*(\/shared\/uploads\/[^\]]+?)\s*\]/gi; + let m; + while ((m = fileRe.exec(text)) !== null) { + const p = m[1].trim(); + const wasDeleted = deletedPaths.has(p); + let size = 0; + let exists = false; + try { const st = fs.statSync(p); size = st.size; exists = true; } catch {} + if (!exists && !wasDeleted) continue; // war vielleicht nie da + const ext = path.extname(p).toLowerCase(); + messages.push({ + type: "aria_file", + serverPath: p, + name: path.basename(p), + mimeType: mimeMap[ext] || "application/octet-stream", + size, + ts, + deleted: wasDeleted || !exists, + }); + } + if (text) messages.push({ type: "received", text, meta: "chat:final", ts }); + } + + clientWs.send(JSON.stringify({ type: "chat_history", messages })); + log("info", "server", `Chat-History geladen: ${messages.length} Eintraege (${deletedPaths.size} geloescht)`); } catch (err) { - log("error", "server", `Chat-History laden fehlgeschlagen: ${err.message}`); + log("error", "server", `Chat-History: ${err.message}`); clientWs.send(JSON.stringify({ type: "chat_history", messages: [], error: err.message })); } } -// ── Session aktivieren ───────────────────────────────── -function handleSetActiveSession(clientWs, sessionKey) { - if (!sessionKey || typeof sessionKey !== "string") { - clientWs.send(JSON.stringify({ type: "active_session", ok: false, error: "Kein sessionKey" })); - return; - } - activeSessionKey = sessionKey; - const ok = persistActiveSession(activeSessionKey); - log("info", "server", `Aktive Session: ${activeSessionKey}${ok ? "" : " (WARN: nicht persistiert!)"}`); - if (!ok) { - clientWs.send(JSON.stringify({ type: "active_session", ok: false, sessionKey: activeSessionKey, error: "Persistierung fehlgeschlagen β€” /data Volume pruefen" })); - } - // Allen Clients mitteilen - for (const c of browserClients) { - c.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey })); - } -} - -// ── Session erstellen ────────────────────────────────── -async function handleCreateSession(clientWs, sessionName) { - if (!sessionName || typeof sessionName !== "string" || !/^[a-zA-Z0-9_-]+$/.test(sessionName)) { - clientWs.send(JSON.stringify({ type: "session_created", ok: false, error: "Ungueltiger Name (nur a-z, 0-9, -, _)" })); - return; - } - try { - // Session wird automatisch erstellt wenn man die erste Nachricht sendet - activeSessionKey = sessionName; - persistActiveSession(activeSessionKey); - log("info", "server", `Neue Session erstellt und aktiviert: ${sessionName}`); - // Allen Clients mitteilen - for (const c of browserClients) { - c.send(JSON.stringify({ type: "active_session", sessionKey: activeSessionKey })); - } - clientWs.send(JSON.stringify({ type: "session_created", ok: true, sessionKey: sessionName })); - } catch (err) { - clientWs.send(JSON.stringify({ type: "session_created", ok: false, error: err.message })); - } -} - -// ── Session neu starten (Container Restart) ──────────── +// ── Session neu starten (Container Restart) β€” toter Code fuer aria-core async function handleRestartSession(clientWs) { try { log("info", "server", "Starte aria-core Container neu (Session Restart)..."); @@ -2327,86 +2158,6 @@ async function handleRestartSession(clientWs) { } } -// ── Brain Viewer ──────────────────────────────────────── - -async function handleListBrain(clientWs) { - try { - log("info", "server", "Lade Brain-Dateien..."); - const raw = await dockerExec("aria-core", ` - for f in /home/node/.openclaw/workspace/memory/*; do - [ -f "$f" ] || continue - name=$(basename "$f") - size=$(du -h "$f" 2>/dev/null | cut -f1) - lines=$(wc -l < "$f" 2>/dev/null || echo 0) - modified=$(stat -c '%Y' "$f" 2>/dev/null || echo 0) - # Frontmatter extrahieren (erste 10 Zeilen) - head10=$(head -10 "$f" 2>/dev/null | tr '\\n' '|') - echo "FILE:$name|SIZE:$size|LINES:$lines|MODIFIED:$modified|HEAD:$head10" - done - `.trim()); - - const files = []; - for (const line of raw.split("\n")) { - if (!line.startsWith("FILE:")) continue; - const parts = {}; - for (const seg of line.split("|")) { - const idx = seg.indexOf(":"); - if (idx > 0) { - const key = seg.slice(0, idx); - const val = seg.slice(idx + 1); - // HEAD hat mehrere |, also nur die bekannten Keys parsen - if (["FILE", "SIZE", "LINES", "MODIFIED"].includes(key)) { - parts[key] = val; - } - } - } - if (!parts.FILE || parts.FILE === "*") continue; - - // Frontmatter-Info aus HEAD extrahieren - let description = ""; - let memType = ""; - const headPart = line.slice(line.indexOf("|HEAD:") + 6); - if (headPart) { - const headLines = headPart.split("|"); - for (const hl of headLines) { - if (hl.startsWith("description:")) description = hl.replace("description:", "").trim(); - if (hl.startsWith("type:")) memType = hl.replace("type:", "").trim(); - } - } - - files.push({ - name: parts.FILE, - size: parts.SIZE || "?", - lines: parseInt(parts.LINES) || 0, - modified: parseInt(parts.MODIFIED) || 0, - description, - memType, - }); - } - - files.sort((a, b) => b.modified - a.modified); - clientWs.send(JSON.stringify({ type: "brain_list", files })); - log("info", "server", `${files.length} Brain-Datei(en) gefunden`); - } catch (err) { - log("error", "server", `Brain laden fehlgeschlagen: ${err.message}`); - clientWs.send(JSON.stringify({ type: "brain_list", files: [], error: err.message })); - } -} - -async function handleReadBrainFile(clientWs, filename) { - // Path Traversal verhindern - if (!filename || filename.includes("..") || filename.includes("/")) { - clientWs.send(JSON.stringify({ type: "brain_content", error: "Ungueltiger Dateiname" })); - return; - } - try { - const content = await dockerExec("aria-core", - `cat '/home/node/.openclaw/workspace/memory/${filename.replace(/'/g, "")}'`); - clientWs.send(JSON.stringify({ type: "brain_content", filename, content })); - } catch (err) { - clientWs.send(JSON.stringify({ type: "brain_content", filename, error: err.message })); - } -} // ── Einstellungen: Tool-Berechtigungen ────────────────── // ENTFERNT: Granulare Permissions haben nie funktioniert. @@ -2451,45 +2202,22 @@ async function handleSetModel(clientWs, model) { } } -// ── Einstellungen: OpenClaw Config ────────────────────── - -async function handleGetOpenClawConfig(clientWs) { - try { - const raw = await dockerExec("aria-core", ` - echo '=== Umgebungsvariablen ===' - echo "DEFAULT_MODEL=$DEFAULT_MODEL" - echo "RATE_LIMIT_PER_USER=$RATE_LIMIT_PER_USER" - echo "OPENCLAW_GATEWAY_TOKEN=$(echo $OPENCLAW_GATEWAY_TOKEN | head -c 8)..." - echo "OPENCLAW_GATEWAY_BIND=$OPENCLAW_GATEWAY_BIND" - echo "" - echo '=== openclaw.json ===' - cat /home/node/.openclaw/openclaw.json 2>/dev/null || echo "(nicht vorhanden)" - echo "" - echo '=== exec-approvals.json ===' - cat /home/node/.openclaw/exec-approvals.json 2>/dev/null || echo "(nicht vorhanden)" - echo "" - echo '=== Agent-Verzeichnis ===' - ls -la /home/node/.openclaw/agents/main/agent/ 2>&1 - echo "" - echo '=== Workspace ===' - ls -la /home/node/.openclaw/workspace/ 2>&1 - `.trim()); - clientWs.send(JSON.stringify({ type: "openclaw_config", config: raw })); - } catch (err) { - clientWs.send(JSON.stringify({ type: "openclaw_config", error: err.message })); - } -} +// OpenClaw-Config-Handler entfernt β€” aria-core ist raus. // ── Start ─────────────────────────────────────────────── server.listen(HTTP_PORT, "0.0.0.0", () => { log("info", "server", `Diagnostic Server laeuft auf http://0.0.0.0:${HTTP_PORT}`); - log("info", "server", `Gateway: ${GATEWAY_URL}`); - log("info", "server", `Token: ${GATEWAY_TOKEN ? GATEWAY_TOKEN.slice(0, 8) + "..." : "(keiner)"}`); log("info", "server", `RVS: ${RVS_HOST ? `${RVS_HOST}:${RVS_PORT}` : "(nicht konfiguriert)"}`); log("info", "server", `Proxy: ${PROXY_URL}`); + log("info", "server", `Brain: ${process.env.BRAIN_URL || "http://aria-brain:8080"}`); - // Verbindungen aufbauen - connectGateway(); + // RVS-Verbindung β€” aria-core/OpenClaw-Gateway entfaellt. connectRVS(); + + // gateway-State bleibt "disconnected" stehen; State-Card im UI + // wurde mit ausgebaut. + state.gateway.status = "disabled"; + state.gateway.lastError = "aria-core entfernt β€” Brain-Loop in Arbeit"; + broadcastState(); });