"use strict"; /** * ARIA Diagnostic Server * * Leichtgewichtiges Diagnose-Tool: * - Verbindet sich direkt mit OpenClaw Gateway (ws://127.0.0.1:18789) * - Fuehrt den vollstaendigen Handshake durch * - Sendet Testnachrichten und zeigt Antworten * - Zeigt Verbindungsstatus aller Komponenten * * Laeuft im selben Netzwerk wie aria-core (network_mode: service:aria) */ const http = require("http"); const { WebSocket, WebSocketServer } = require("ws"); const crypto = require("crypto"); const fs = require("fs"); const path = require("path"); // ── Konfiguration ─────────────────────────────────────── const HTTP_PORT = parseInt(process.env.DIAG_PORT || "3001", 10); const GATEWAY_URL = process.env.ARIA_CORE_WS || "ws://127.0.0.1:18789"; const GATEWAY_TOKEN = process.env.ARIA_AUTH_TOKEN || ""; const RVS_HOST = process.env.RVS_HOST || ""; const RVS_PORT = process.env.RVS_PORT || "443"; const RVS_TLS = process.env.RVS_TLS || "true"; const RVS_TLS_FALLBACK = process.env.RVS_TLS_FALLBACK || "true"; const RVS_TOKEN = process.env.RVS_TOKEN || ""; const PROXY_URL = process.env.PROXY_URL || "http://proxy:3456"; // ── State ─────────────────────────────────────────────── const state = { gateway: { status: "disconnected", lastError: null, handshakeOk: false }, rvs: { status: "disconnected", lastError: null }, proxy: { status: "unknown", lastError: null }, }; const logs = []; let gatewayWs = null; let rvsWs = null; let reqIdCounter = 0; const browserClients = new Set(); // ── Pipeline Tracking ────────────────────────────────── let pipelineActive = false; let pipelineStartTime = 0; function plog(message, level) { const elapsed = pipelineActive ? `+${Date.now() - pipelineStartTime}ms` : ""; const entry = { ts: new Date().toISOString(), level: level || "info", source: "pipeline", message: `${elapsed ? `[${elapsed}] ` : ""}${message}` }; logs.push(entry); if (logs.length > 500) logs.shift(); console.log(`[PIPELINE] ${entry.message}`); broadcast({ type: "log", entry }); } let pipelineTimeout = null; function pipelineStart(method, text) { // Falls noch eine Pipeline laeuft, beenden if (pipelineActive) pipelineEnd(false, "Abgebrochen (neue Nachricht)"); pipelineActive = true; pipelineStartTime = Date.now(); if (pipelineTimeout) clearTimeout(pipelineTimeout); pipelineTimeout = setTimeout(() => { if (pipelineActive) pipelineEnd(false, "Timeout — keine Antwort nach 60s"); }, 60000); plog(`━━━ Pipeline Start: ${method} ━━━`); plog(`Nachricht: "${text}"`); } function pipelineEnd(ok, detail) { if (!pipelineActive) return; if (pipelineTimeout) { clearTimeout(pipelineTimeout); pipelineTimeout = null; } const elapsed = Date.now() - pipelineStartTime; if (ok) { plog(`>>> Fertig (${elapsed}ms): ${detail}`); } else { plog(`>>> FEHLER (${elapsed}ms): ${detail}`, "error"); } plog(`━━━ Pipeline Ende ━━━`); pipelineActive = false; } function nextReqId() { return `diag-${++reqIdCounter}`; } function log(level, source, message) { const entry = { ts: new Date().toISOString(), level, source, message }; logs.push(entry); if (logs.length > 500) logs.shift(); console.log(`[${entry.ts}] [${level}] [${source}] ${message}`); // An alle Browser-Clients senden broadcast({ type: "log", entry }); } function broadcast(msg) { const data = JSON.stringify(msg); for (const client of browserClients) { if (client.readyState === WebSocket.OPEN) { client.send(data); } } } function broadcastState() { broadcast({ type: "state", state }); } // ── OpenClaw Gateway Verbindung ───────────────────────── async function connectGateway() { if (gatewayWs) { try { gatewayWs.close(); } catch (_) {} gatewayWs = null; } state.gateway.status = "connecting"; state.gateway.handshakeOk = false; broadcastState(); log("info", "gateway", `Verbinde: ${GATEWAY_URL}`); try { const ws = new WebSocket(GATEWAY_URL); await new Promise((resolve, reject) => { const timeout = setTimeout(() => { ws.close(); reject(new Error("Verbindungs-Timeout (10s)")); }, 10000); ws.on("open", () => { clearTimeout(timeout); resolve(); }); ws.on("error", (err) => { clearTimeout(timeout); reject(err); }); }); log("info", "gateway", "TCP-Verbindung hergestellt — warte auf Challenge"); // Schritt 1: Auf Challenge warten const challengeRaw = await waitForMessage(ws, 10000); const challenge = JSON.parse(challengeRaw); if (challenge.type !== "event" || challenge.event !== "connect.challenge") { throw new Error(`Unerwartete erste Nachricht: ${challengeRaw.slice(0, 200)}`); } const nonce = challenge.payload?.nonce || ""; log("info", "gateway", `Challenge empfangen (nonce: ${nonce.slice(0, 8)}...)`); // Schritt 2: Connect Request senden const connectReq = { type: "req", id: nextReqId(), method: "connect", params: { minProtocol: 3, maxProtocol: 3, client: { id: "cli", version: "0.0.1", platform: "linux", mode: "cli", }, role: "operator", scopes: ["operator.read", "operator.write"], caps: [], commands: [], permissions: {}, auth: GATEWAY_TOKEN ? { token: GATEWAY_TOKEN } : {}, locale: "de-DE", userAgent: "aria-diagnostic/0.0.1", }, }; ws.send(JSON.stringify(connectReq)); log("info", "gateway", "Connect-Request gesendet"); // Schritt 3: hello-ok warten const responseRaw = await waitForMessage(ws, 10000); const response = JSON.parse(responseRaw); if (response.type === "res" && response.ok) { log("info", "gateway", "Handshake erfolgreich — hello-ok!"); state.gateway.status = "connected"; state.gateway.handshakeOk = true; state.gateway.lastError = null; } else { const error = typeof response.error === "string" ? response.error : JSON.stringify(response.error || response).slice(0, 300); throw new Error(`Handshake fehlgeschlagen: ${error}`); } gatewayWs = ws; broadcastState(); // Nachrichten-Loop — RAW logging fuer Debugging ws.on("message", (raw) => { try { const rawStr = raw.toString(); log("debug", "gateway", `RAW <<< ${rawStr.slice(0, 300)}`); const msg = JSON.parse(rawStr); handleGatewayMessage(msg); } catch (err) { log("error", "gateway", `Parse-Fehler: ${err.message}`); } }); ws.on("close", (code, reason) => { log("warn", "gateway", `Verbindung geschlossen (Code: ${code}, Reason: ${reason || "-"})`); state.gateway.status = "disconnected"; state.gateway.handshakeOk = false; gatewayWs = null; broadcastState(); // Auto-Reconnect nach 5s setTimeout(connectGateway, 5000); }); ws.on("error", (err) => { log("error", "gateway", `WebSocket-Fehler: ${err.message}`); state.gateway.lastError = err.message; broadcastState(); }); } catch (err) { log("error", "gateway", `Fehler: ${err.message}`); state.gateway.status = "error"; state.gateway.lastError = err.message; state.gateway.handshakeOk = false; gatewayWs = null; broadcastState(); // Retry nach 5s setTimeout(connectGateway, 5000); } } // Extrahiert Text aus OpenClaw chat-Event message.content Array function extractChatText(payload) { try { const content = payload.message?.content; if (Array.isArray(content)) { return content .filter(c => c.type === "text") .map(c => c.text || "") .join(""); } if (typeof payload.message === "string") return payload.message; return payload.text || ""; } catch { return ""; } } function handleGatewayMessage(msg) { if (msg.type === "res") { const status = msg.ok ? "OK" : `FEHLER: ${JSON.stringify(msg.error).slice(0, 100)}`; log("info", "gateway", `Response [${msg.id}]: ${status}`); if (pipelineActive) { if (msg.ok) plog(`Gateway ACK [${msg.id}] — Nachricht angenommen`); else plog(`Gateway NACK [${msg.id}] — ${JSON.stringify(msg.error).slice(0, 100)}`, "error"); } broadcast({ type: "response", msg }); return; } if (msg.type === "event") { const event = msg.event || "?"; const payload = msg.payload || {}; // ── agent Events: Streaming-Deltas vom LLM ── if (event === "agent") { const data = payload.data || {}; const delta = data.delta || ""; if (delta && payload.stream === "assistant") { broadcast({ type: "chat_delta", delta, payload }); } // agent Events nicht einzeln loggen (zu viele) return; } // ── chat Events: Snapshots mit state=delta|final ── if (event === "chat") { const state = payload.state || ""; const text = extractChatText(payload); if (state === "final") { log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`); if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`); broadcast({ type: "chat_final", text, payload }); return; } if (state === "delta") { // Periodischer Snapshot — nicht einzeln loggen return; } if (state === "error") { const error = payload.error || text || "Unbekannt"; log("error", "gateway", `Chat-Fehler: ${error}`); if (pipelineActive) pipelineEnd(false, error); broadcast({ type: "chat_error", error, payload }); return; } log("debug", "gateway", `chat state=${state}`); return; } // ── Legacy event names (chat:delta, chat:final, chat:error) ── if (event === "chat:delta") { const delta = payload.delta || payload.text || ""; if (delta) broadcast({ type: "chat_delta", delta, payload }); return; } if (event === "chat:final") { const text = extractChatText(payload) || payload.text || ""; log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`); if (pipelineActive) pipelineEnd(true, `"${text.slice(0, 120)}"`); broadcast({ type: "chat_final", text, payload }); return; } if (event === "chat:error") { const error = payload.error || payload.message || "Unbekannt"; log("error", "gateway", `Chat-Fehler: ${error}`); if (pipelineActive) pipelineEnd(false, error); broadcast({ type: "chat_error", error, payload }); return; } // ── Andere Events (tick, health, presence) ── if (event === "tick" || event === "health") return; // Noise log("debug", "gateway", `Event: ${event}`); } } function sendToGateway(text, isPipeline) { if (!gatewayWs || gatewayWs.readyState !== WebSocket.OPEN) { log("error", "gateway", "Nicht verbunden — kann nicht senden"); if (isPipeline) pipelineEnd(false, "Gateway nicht verbunden"); return false; } const reqId = nextReqId(); const msg = { type: "req", id: reqId, method: "chat.send", params: { sessionKey: "aria-diagnostic", message: text, idempotencyKey: crypto.randomUUID(), }, }; const payload = JSON.stringify(msg); log("debug", "gateway", `RAW >>> ${payload}`); gatewayWs.send(payload); log("info", "gateway", `chat.send [${reqId}]: "${text}"`); if (isPipeline) plog(`chat.send [${reqId}] an Gateway gesendet — warte auf ACK...`); return true; } // ── RVS Verbindung (optional) ─────────────────────────── function connectRVS(forcePlain) { if (!RVS_HOST || !RVS_TOKEN) { log("info", "rvs", "Nicht konfiguriert — ueberspringe"); state.rvs.status = "not_configured"; broadcastState(); return; } // TLS-Logik: wss zuerst, bei Fehler Fallback auf ws (wenn erlaubt) const useTls = RVS_TLS === "true" && !forcePlain; const proto = useTls ? "wss" : "ws"; const url = `${proto}://${RVS_HOST}:${RVS_PORT}?token=${RVS_TOKEN}`; state.rvs.status = "connecting"; broadcastState(); log("info", "rvs", `Verbinde: ${proto}://${RVS_HOST}:${RVS_PORT}`); const ws = new WebSocket(url); ws.on("open", () => { log("info", "rvs", `Verbunden (${proto})`); state.rvs.status = "connected"; state.rvs.lastError = null; rvsWs = ws; broadcastState(); }); ws.on("message", (raw) => { try { const msg = JSON.parse(raw.toString()); if (msg.type === "chat" && msg.payload) { const sender = msg.payload.sender || "?"; log("info", "rvs", `Chat von ${sender}: "${(msg.payload.text || "").slice(0, 100)}"`); if (pipelineActive && sender !== "diagnostic") { pipelineEnd(true, `Antwort via RVS von ${sender}: "${(msg.payload.text || "").slice(0, 120)}"`); } broadcast({ type: "rvs_chat", msg }); } else if (msg.type === "heartbeat") { // ignorieren } else { log("debug", "rvs", `Nachricht: ${JSON.stringify(msg).slice(0, 150)}`); } } catch {} }); ws.on("close", () => { log("warn", "rvs", "Verbindung geschlossen"); state.rvs.status = "disconnected"; rvsWs = null; broadcastState(); setTimeout(() => connectRVS(), 5000); }); ws.on("error", (err) => { log("error", "rvs", `Fehler (${proto}): ${err.message}`); state.rvs.lastError = err.message; broadcastState(); // TLS Fallback: wenn wss fehlschlaegt und Fallback erlaubt → ws versuchen if (useTls && RVS_TLS_FALLBACK === "true") { log("warn", "rvs", "TLS fehlgeschlagen — Fallback auf ws://"); ws.removeAllListeners(); try { ws.close(); } catch (_) {} connectRVS(true); } }); } function sendToRVS(text, isPipeline) { if (!rvsWs || rvsWs.readyState !== WebSocket.OPEN) { log("error", "rvs", "Nicht verbunden"); if (isPipeline) pipelineEnd(false, "RVS nicht verbunden"); return false; } rvsWs.send(JSON.stringify({ type: "chat", payload: { text, sender: "diagnostic" }, timestamp: Date.now(), })); log("info", "rvs", `Gesendet via RVS: "${text}"`); if (isPipeline) plog(`Nachricht an RVS gesendet — warte auf Antwort via RVS...`); return true; } // ── Claude Proxy Test ──────────────────────────────────── async function testProxy(prompt) { state.proxy.status = "testing"; state.proxy.lastError = null; broadcastState(); log("info", "proxy", `Teste Proxy: ${PROXY_URL}`); try { // Schritt 1: Erreichbarkeit pruefen const healthUrl = `${PROXY_URL}/v1/models`; log("info", "proxy", `Rufe ab: ${healthUrl}`); const modelsRes = await fetch(healthUrl, { headers: { "Authorization": "Bearer not-needed" }, signal: AbortSignal.timeout(10000), }); if (!modelsRes.ok) { throw new Error(`Models-Endpoint: HTTP ${modelsRes.status} ${modelsRes.statusText}`); } const modelsData = await modelsRes.json(); const models = (modelsData.data || []).map(m => m.id).filter(Boolean); log("info", "proxy", `Proxy erreichbar — ${models.length} Model(s) verfuegbar`); // Modellnamen loggen + OpenClaw-Config Hinweis if (models.length > 0) { log("info", "proxy", `Modelle: ${models.join(", ")}`); log("info", "proxy", `Fuer docker-compose.yml (DEFAULT_MODEL): ${models.map(m => m.replace("openai/", "")).join(" | ")}`); } // Schritt 1b: Auth-Dateien im Proxy-Container pruefen try { const authInfo = await dockerExec("aria-proxy", "echo '--- /root/.config/claude/ ---' && ls -la /root/.config/claude/ 2>&1 && echo '--- /root/.claude/ ---' && ls -la /root/.claude/ 2>&1 && echo '--- Credential-Dateien ---' && find /root/.config/claude /root/.claude -name '*.json' -o -name '*credential*' -o -name '*auth*' -o -name '*token*' 2>/dev/null | head -20"); log("info", "proxy", `Auth-Dateien im Container:\n${authInfo}`); broadcast({ type: "proxy_auth", info: authInfo }); } catch (authErr) { log("warn", "proxy", `Auth-Check fehlgeschlagen: ${authErr.message}`); } // Schritt 2: Chat Completion testen (kurzer Prompt) const testPrompt = prompt || "Antworte mit genau einem Wort: Ping"; log("info", "proxy", `Sende Test-Prompt: "${testPrompt}"`); const chatRes = await fetch(`${PROXY_URL}/v1/chat/completions`, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer not-needed", }, body: JSON.stringify({ model: "claude-sonnet-4-6", messages: [{ role: "user", content: testPrompt }], max_tokens: 200, }), signal: AbortSignal.timeout(30000), }); if (!chatRes.ok) { const errBody = await chatRes.text().catch(() => ""); throw new Error(`Chat-Completion: HTTP ${chatRes.status} — ${errBody.slice(0, 300)}`); } const chatData = await chatRes.json(); const reply = chatData.choices?.[0]?.message?.content || "(leer)"; log("info", "proxy", `Antwort: "${reply.slice(0, 200)}"`); state.proxy.status = "connected"; state.proxy.lastError = null; state.proxy.models = models; broadcastState(); broadcast({ type: "proxy_result", ok: true, reply, models }); } catch (err) { log("error", "proxy", `Fehler: ${err.message}`); state.proxy.status = "error"; state.proxy.lastError = err.message; broadcastState(); broadcast({ type: "proxy_result", ok: false, error: err.message }); } } // ── Claude Login im Proxy-Container ───────────────────── // ── Interaktives Terminal (xterm.js ↔ Docker Exec) ────── const net = require("net"); function attachTerminal(clientWs, containerName, cmd) { const createBody = JSON.stringify({ AttachStdin: true, AttachStdout: true, AttachStderr: true, Tty: true, Cmd: Array.isArray(cmd) ? cmd : ["sh", "-c", cmd], }); const createReq = http.request({ socketPath: "/var/run/docker.sock", path: `/containers/${containerName}/exec`, method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(createBody) }, }, (res) => { let data = ""; res.on("data", (c) => data += c); res.on("end", () => { if (res.statusCode !== 201) { clientWs.send(JSON.stringify({ type: "term_error", error: `Exec create failed: HTTP ${res.statusCode}` })); return; } const execId = JSON.parse(data).Id; // Exec starten — raw TCP socket fuer bidirektionalen Stream // Docker Exec mit Tty macht ein HTTP Upgrade auf raw stream const startBody = JSON.stringify({ Detach: false, Tty: true }); const sock = net.connect({ path: "/var/run/docker.sock" }, () => { // Raw HTTP Request senden (Upgrade-artig) const req = `POST /exec/${execId}/start HTTP/1.1\r\nHost: localhost\r\nContent-Type: application/json\r\nConnection: Upgrade\r\nUpgrade: tcp\r\nContent-Length: ${Buffer.byteLength(startBody)}\r\n\r\n${startBody}`; sock.write(req); }); let headersParsed = false; let headerBuf = ""; sock.on("data", (chunk) => { if (!headersParsed) { // HTTP Response Header parsen headerBuf += chunk.toString("utf-8"); const headerEnd = headerBuf.indexOf("\r\n\r\n"); if (headerEnd === -1) return; // Noch nicht komplett const headers = headerBuf.slice(0, headerEnd); const remaining = chunk.slice(chunk.length - (headerBuf.length - headerEnd - 4)); headersParsed = true; if (!headers.includes("200") && !headers.includes("101")) { clientWs.send(JSON.stringify({ type: "term_error", error: `Exec start failed: ${headers.split("\r\n")[0]}` })); sock.end(); return; } log("info", "proxy", "Terminal-Session gestartet"); clientWs.send(JSON.stringify({ type: "term_ready" })); clientWs._termSock = sock; // Verbleibende Daten nach dem Header if (remaining.length > 0) { clientWs.send(JSON.stringify({ type: "term_data", data: remaining.toString("base64") })); } return; } // Tty=true: Rohdaten direkt durchreichen if (clientWs.readyState === WebSocket.OPEN) { clientWs.send(JSON.stringify({ type: "term_data", data: chunk.toString("base64") })); } }); sock.on("end", () => { log("info", "proxy", "Terminal-Session beendet"); if (clientWs.readyState === WebSocket.OPEN) { clientWs.send(JSON.stringify({ type: "term_exit" })); } clientWs._termSock = null; setTimeout(() => checkProxyAuth(), 1000); }); sock.on("error", (err) => { log("error", "proxy", `Terminal-Socket-Fehler: ${err.message}`); if (clientWs.readyState === WebSocket.OPEN) { clientWs.send(JSON.stringify({ type: "term_error", error: err.message })); } }); // Wenn Browser-Client disconnected, Socket schliessen clientWs.on("close", () => { if (sock && !sock.destroyed) sock.end(); }); }); }); createReq.on("error", (err) => { clientWs.send(JSON.stringify({ type: "term_error", error: err.message })); }); createReq.write(createBody); createReq.end(); } function handleTermInput(clientWs, data) { if (clientWs._termSock && !clientWs._termSock.destroyed) { clientWs._termSock.write(Buffer.from(data, "base64")); } } // Credentials manuell einfuegen (von einem Rechner wo Claude eingeloggt ist) async function writeProxyCredentials(credentialsJson) { try { // Validieren const parsed = JSON.parse(credentialsJson); if (!parsed.claudeAiOauth && !parsed.oauth) { throw new Error("Ungueltig: Kein OAuth-Objekt gefunden. Erwartet 'claudeAiOauth' oder 'oauth' Key."); } log("info", "proxy", "Schreibe Credentials in Proxy-Container..."); // Escaped fuer Shell — Einfache Anfuehrungszeichen im JSON escapen const escaped = credentialsJson.replace(/'/g, "'\\''"); // In beide moegliche Speicherorte schreiben await dockerExec("aria-proxy", `mkdir -p /root/.config/claude && echo '${escaped}' > /root/.config/claude/.credentials.json && mkdir -p /root/.claude && echo '${escaped}' > /root/.claude/credentials.json`); log("info", "proxy", "Credentials geschrieben!"); broadcast({ type: "login_status", status: "done" }); broadcast({ type: "login_output", text: "Credentials erfolgreich geschrieben! Proxy muss neu gestartet werden." }); // Auth pruefen setTimeout(() => checkProxyAuth(), 500); } catch (err) { log("error", "proxy", `Credentials schreiben fehlgeschlagen: ${err.message}`); broadcast({ type: "login_status", status: "error", error: err.message }); } } async function checkProxyAuth() { try { log("info", "proxy", "Pruefe Auth-Dateien im Proxy-Container..."); // Breit suchen: Claude Code speichert Credentials je nach Version an verschiedenen Orten const authInfo = await dockerExec("aria-proxy", ` echo '=== /root/.config/claude/ ===' && ls -la /root/.config/claude/ 2>&1 && echo '' && echo '=== /root/.claude/ ===' && ls -la /root/.claude/ 2>&1 && echo '' && echo '=== /root/.claude/auth/ ===' && ls -la /root/.claude/auth/ 2>&1 && echo '' && echo '=== Credentials-Dateien (rekursiv) ===' && find /root/.config/claude /root/.claude -name '*.json' -o -name '*credential*' -o -name '*auth*' -o -name '*token*' -o -name '*oauth*' -o -name '*session*' 2>/dev/null | head -20 && echo '' && echo '=== .credentials.json ===' && cat /root/.config/claude/.credentials.json 2>/dev/null || echo '(nicht in .config/claude/)' && echo '' && echo '=== /root/.claude/credentials.json ===' && cat /root/.claude/credentials.json 2>/dev/null || echo '(nicht in .claude/)' && echo '' && echo '=== /root/.claude/auth/*.json ===' && cat /root/.claude/auth/*.json 2>/dev/null || echo '(keine auth/*.json)' `.trim()); log("info", "proxy", `Auth-Dateien:\n${authInfo}`); broadcast({ type: "proxy_auth", info: authInfo }); } catch (err) { log("error", "proxy", `Auth-Check fehlgeschlagen: ${err.message}`); broadcast({ type: "proxy_auth", info: null, error: err.message }); } } // ── OpenClaw Agent-Auth pruefen ────────────────────────── async function checkCoreAuth() { try { log("info", "gateway", "Pruefe OpenClaw Agent-Konfiguration..."); const info = await dockerExec("aria-core", ` echo '=== Agent-Verzeichnis ===' && ls -la /home/node/.openclaw/agents/main/agent/ 2>&1 && echo '' && echo '=== auth-profiles.json ===' && cat /home/node/.openclaw/agents/main/agent/auth-profiles.json 2>/dev/null || echo '(nicht vorhanden)' && echo '' && echo '=== Umgebungsvariablen ===' && echo "OPENAI_BASE_URL=$OPENAI_BASE_URL" && echo "OPENAI_API_KEY=$(echo $OPENAI_API_KEY | head -c 15)..." && echo "DEFAULT_MODEL=$DEFAULT_MODEL" && echo '' && echo '=== OpenClaw Version ===' && openclaw --version 2>/dev/null || echo '(openclaw CLI nicht gefunden)' && echo '' && echo '=== Agents Liste ===' && openclaw agents list 2>/dev/null || echo '(Befehl fehlgeschlagen)' `.trim()); log("info", "gateway", `OpenClaw Config:\n${info}`); broadcast({ type: "core_auth", info }); } catch (err) { log("error", "gateway", `Core-Auth-Check fehlgeschlagen: ${err.message}`); broadcast({ type: "core_auth", info: null, error: err.message }); } } // ── Docker Container Logs ──────────────────────────────── const CONTAINER_MAP = { gateway: "aria-core", proxy: "aria-proxy", bridge: "aria-bridge", }; function fetchDockerLogs(tab, tail) { const containerName = CONTAINER_MAP[tab]; if (!containerName) return; const lines = parseInt(tail, 10) || 100; const dockerPath = `/containers/${containerName}/logs?stdout=true&stderr=true&tail=${lines}×tamps=true`; return new Promise((resolve, reject) => { const req = http.request( { socketPath: "/var/run/docker.sock", path: dockerPath, method: "GET" }, (res) => { const chunks = []; res.on("data", (chunk) => chunks.push(chunk)); res.on("end", () => { // Docker log stream hat 8-byte Header pro Frame (stream type + size) const raw = Buffer.concat(chunks); const logLines = []; let offset = 0; while (offset < raw.length) { if (offset + 8 > raw.length) break; const size = raw.readUInt32BE(offset + 4); if (offset + 8 + size > raw.length) break; const line = raw.slice(offset + 8, offset + 8 + size).toString("utf-8").trimEnd(); if (line) logLines.push(line); offset += 8 + size; } resolve(logLines); }); } ); req.on("error", (err) => reject(err)); req.end(); }); } async function handleDockerLogs(ws, tab, tail) { const containerName = CONTAINER_MAP[tab]; if (!containerName) { ws.send(JSON.stringify({ type: "docker_logs", tab, error: `Unbekannter Tab: ${tab}` })); return; } try { log("info", "server", `Lade Docker-Logs: ${containerName} (tail ${tail || 100})`); const lines = await fetchDockerLogs(tab, tail); ws.send(JSON.stringify({ type: "docker_logs", tab, container: containerName, lines })); } catch (err) { log("error", "server", `Docker-Logs Fehler (${containerName}): ${err.message}`); ws.send(JSON.stringify({ type: "docker_logs", tab, error: err.message })); } } // ── Docker Exec (Befehl in Container ausfuehren) ──────── function dockerExec(containerName, cmd) { return new Promise((resolve, reject) => { const createBody = JSON.stringify({ AttachStdout: true, AttachStderr: true, Cmd: Array.isArray(cmd) ? cmd : ["sh", "-c", cmd], }); const createReq = http.request({ socketPath: "/var/run/docker.sock", path: `/containers/${containerName}/exec`, method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(createBody) }, }, (res) => { let data = ""; res.on("data", (c) => data += c); res.on("end", () => { if (res.statusCode !== 201) return reject(new Error(`Exec create: HTTP ${res.statusCode} — ${data.slice(0, 200)}`)); const execId = JSON.parse(data).Id; // Exec starten const startBody = JSON.stringify({ Detach: false }); const startReq = http.request({ socketPath: "/var/run/docker.sock", path: `/exec/${execId}/start`, method: "POST", headers: { "Content-Type": "application/json", "Content-Length": Buffer.byteLength(startBody) }, }, (sRes) => { const chunks = []; sRes.on("data", (c) => chunks.push(c)); sRes.on("end", () => { // Docker multiplexed stream: 8-byte header pro Frame const raw = Buffer.concat(chunks); const lines = []; let offset = 0; while (offset < raw.length) { if (offset + 8 > raw.length) { lines.push(raw.slice(offset).toString("utf-8").trim()); break; } const size = raw.readUInt32BE(offset + 4); if (size === 0 || offset + 8 + size > raw.length) { lines.push(raw.slice(offset).toString("utf-8").trim()); break; } const line = raw.slice(offset + 8, offset + 8 + size).toString("utf-8").trimEnd(); if (line) lines.push(line); offset += 8 + size; } resolve(lines.join("\n")); }); }); startReq.on("error", reject); startReq.write(startBody); startReq.end(); }); }); createReq.on("error", reject); createReq.write(createBody); createReq.end(); }); } // ── Hilfsfunktionen ───────────────────────────────────── function waitForMessage(ws, timeoutMs) { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error(`Timeout (${timeoutMs}ms)`)); }, timeoutMs); ws.once("message", (data) => { clearTimeout(timeout); resolve(data.toString()); }); }); } // ── HTTP Server + WebSocket fuer Browser ──────────────── const htmlPath = path.join(__dirname, "index.html"); const server = http.createServer((req, res) => { if (req.url === "/" || req.url === "/index.html") { res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" }); res.end(fs.readFileSync(htmlPath, "utf-8")); } else if (req.url === "/api/state") { res.writeHead(200, { "Content-Type": "application/json" }); res.end(JSON.stringify({ state, logs: logs.slice(-100) })); } else { res.writeHead(404); res.end("Not Found"); } }); const wss = new WebSocketServer({ server }); wss.on("connection", (ws) => { browserClients.add(ws); // Initialen State + letzte Logs senden ws.send(JSON.stringify({ type: "init", state, logs: logs.slice(-100) })); ws.on("message", (raw) => { try { const msg = JSON.parse(raw.toString()); if (msg.action === "test_gateway") { pipelineStart("Gateway", msg.text || "aria lebst du noch?"); sendToGateway(msg.text || "aria lebst du noch?", true); } else if (msg.action === "test_rvs") { pipelineStart("RVS", msg.text || "aria lebst du noch?"); sendToRVS(msg.text || "aria lebst du noch?", true); } else if (msg.action === "reconnect_gateway") { connectGateway(); } else if (msg.action === "reconnect_rvs") { connectRVS(); } else if (msg.action === "test_proxy") { testProxy(msg.text); } else if (msg.action === "check_proxy_auth") { checkProxyAuth(); } else if (msg.action === "proxy_login") { attachTerminal(ws, "aria-proxy", "claude login"); } else if (msg.action === "core_terminal") { // Interaktive Shell in aria-core (fuer openclaw agents, etc.) attachTerminal(ws, "aria-core", msg.cmd || "sh"); } else if (msg.action === "check_core_auth") { checkCoreAuth(); } else if (msg.action === "term_input") { handleTermInput(ws, msg.data); } else if (msg.action === "write_credentials") { writeProxyCredentials(msg.credentials); } else if (msg.action === "docker_logs") { handleDockerLogs(ws, msg.tab, msg.tail); } } catch {} }); ws.on("close", () => { browserClients.delete(ws); }); }); // ── 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}`); // Verbindungen aufbauen connectGateway(); connectRVS(); });