ARIA-AGENT/diagnostic/server.js

954 lines
33 KiB
JavaScript

"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}&timestamps=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();
});