added brain and session viewer

This commit is contained in:
2026-03-13 16:45:56 +01:00
parent 6a04d861bd
commit 706005d7f5
3 changed files with 438 additions and 0 deletions
+191
View File
@@ -970,6 +970,16 @@ wss.on("connection", (ws) => {
if (ws._sshSock) { ws._sshSock.end(); ws._sshSock = null; }
} else if (msg.action === "check_desktop") {
checkDesktopAvailable(ws);
} else if (msg.action === "list_sessions") {
handleListSessions(ws);
} else if (msg.action === "read_session") {
handleReadSession(ws, msg.sessionPath);
} else if (msg.action === "delete_session") {
handleDeleteSession(ws, msg.sessionPath);
} else if (msg.action === "list_brain") {
handleListBrain(ws);
} else if (msg.action === "read_brain_file") {
handleReadBrainFile(ws, msg.filename);
}
} catch {}
});
@@ -1094,6 +1104,187 @@ function checkDesktopAvailable(clientWs) {
});
}
// ── Session Viewer ──────────────────────────────────────
async function handleListSessions(clientWs) {
try {
log("info", "server", "Lade Sessions aus aria-core...");
// OpenClaw/Claude Code speichert Sessions in projects/<hash>/sessions/
const raw = await dockerExec("aria-core", `
echo '=== SESSIONS ===' &&
find /home/node/.openclaw/projects -name '*.jsonl' -type f 2>/dev/null | sort &&
echo '=== DETAILS ===' &&
for f in $(find /home/node/.openclaw/projects -name '*.jsonl' -type f 2>/dev/null | sort); do
lines=$(wc -l < "$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)
# Erste Zeile fuer Session-Info
first=$(head -1 "$f" 2>/dev/null | head -c 500)
echo "FILE:$f|LINES:$lines|SIZE:$size|MODIFIED:$modified|FIRST:$first"
done
`.trim());
// Parse Output
const sessions = [];
const lines = raw.split("\n");
for (const line of lines) {
if (!line.startsWith("FILE:")) continue;
const parts = {};
for (const seg of line.split("|")) {
const idx = seg.indexOf(":");
if (idx > 0) parts[seg.slice(0, idx)] = seg.slice(idx + 1);
}
if (!parts.FILE) continue;
// Session-Key aus Pfad oder erster Zeile extrahieren
let sessionKey = "";
try {
const firstObj = JSON.parse(parts.FIRST || "{}");
sessionKey = firstObj.sessionKey || firstObj.key || "";
} catch {}
if (!sessionKey) {
// Dateiname als Fallback
sessionKey = parts.FILE.split("/").pop().replace(".jsonl", "");
}
sessions.push({
path: parts.FILE,
sessionKey,
lines: parseInt(parts.LINES) || 0,
size: parts.SIZE || "?",
modified: parseInt(parts.MODIFIED) || 0,
});
}
// Nach Aenderungsdatum sortieren (neueste zuerst)
sessions.sort((a, b) => b.modified - a.modified);
clientWs.send(JSON.stringify({ type: "sessions_list", sessions, raw: sessions.length === 0 ? raw : undefined }));
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 handleDeleteSession(clientWs, sessionPath) {
if (!sessionPath || sessionPath.includes("..") || !sessionPath.startsWith("/home/node/.openclaw/")) {
clientWs.send(JSON.stringify({ type: "session_deleted", ok: false, error: "Ungueltiger Pfad" }));
return;
}
try {
log("warn", "server", `Loesche Session: ${sessionPath}`);
await dockerExec("aria-core", `rm '${sessionPath.replace(/'/g, "")}'`);
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 }));
}
}
// ── 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 }));
}
}
// ── Start ───────────────────────────────────────────────
server.listen(HTTP_PORT, "0.0.0.0", () => {