added brain and session viewer
This commit is contained in:
@@ -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", () => {
|
||||
|
||||
Reference in New Issue
Block a user