413 lines
12 KiB
JavaScript
413 lines
12 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 || "";
|
|
|
|
// ── State ───────────────────────────────────────────────
|
|
const state = {
|
|
gateway: { status: "disconnected", lastError: null, handshakeOk: false },
|
|
rvs: { status: "disconnected", lastError: null },
|
|
};
|
|
const logs = [];
|
|
let gatewayWs = null;
|
|
let rvsWs = null;
|
|
let reqIdCounter = 0;
|
|
const browserClients = new Set();
|
|
|
|
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: "aria-diagnostic",
|
|
version: "0.0.1",
|
|
platform: "linux",
|
|
mode: "operator",
|
|
},
|
|
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 = response.error || JSON.stringify(response).slice(0, 200);
|
|
throw new Error(`Handshake fehlgeschlagen: ${error}`);
|
|
}
|
|
|
|
gatewayWs = ws;
|
|
broadcastState();
|
|
|
|
// Nachrichten-Loop
|
|
ws.on("message", (raw) => {
|
|
try {
|
|
const msg = JSON.parse(raw.toString());
|
|
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);
|
|
}
|
|
}
|
|
|
|
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}`);
|
|
broadcast({ type: "response", msg });
|
|
return;
|
|
}
|
|
|
|
if (msg.type === "event") {
|
|
const event = msg.event || "?";
|
|
const payload = msg.payload || {};
|
|
|
|
if (event === "chat:delta") {
|
|
const delta = payload.delta || payload.text || "";
|
|
if (delta) {
|
|
log("info", "gateway", `Delta: "${delta.slice(0, 60)}"`);
|
|
broadcast({ type: "chat_delta", delta, payload });
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (event === "chat:final") {
|
|
const text = payload.text || payload.message || "";
|
|
log("info", "gateway", `ANTWORT: "${text.slice(0, 200)}"`);
|
|
broadcast({ type: "chat_final", text, payload });
|
|
return;
|
|
}
|
|
|
|
if (event === "chat:error") {
|
|
const error = payload.error || payload.message || "Unbekannt";
|
|
log("error", "gateway", `Chat-Fehler: ${error}`);
|
|
broadcast({ type: "chat_error", error, payload });
|
|
return;
|
|
}
|
|
|
|
// Andere Events (presence, tick, etc.)
|
|
log("debug", "gateway", `Event: ${event}`);
|
|
}
|
|
}
|
|
|
|
function sendToGateway(text) {
|
|
if (!gatewayWs || gatewayWs.readyState !== WebSocket.OPEN) {
|
|
log("error", "gateway", "Nicht verbunden — kann nicht senden");
|
|
return false;
|
|
}
|
|
|
|
const reqId = nextReqId();
|
|
const msg = {
|
|
type: "req",
|
|
id: reqId,
|
|
method: "chat.send",
|
|
params: {
|
|
sessionKey: "aria-diagnostic",
|
|
text,
|
|
idempotencyKey: crypto.randomUUID(),
|
|
},
|
|
};
|
|
|
|
gatewayWs.send(JSON.stringify(msg));
|
|
log("info", "gateway", `chat.send [${reqId}]: "${text}"`);
|
|
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) {
|
|
log("info", "rvs", `Chat von ${msg.payload.sender || "?"}: "${(msg.payload.text || "").slice(0, 100)}"`);
|
|
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) {
|
|
if (!rvsWs || rvsWs.readyState !== WebSocket.OPEN) {
|
|
log("error", "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}"`);
|
|
return true;
|
|
}
|
|
|
|
// ── 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") {
|
|
sendToGateway(msg.text || "aria lebst du noch?");
|
|
} else if (msg.action === "test_rvs") {
|
|
sendToRVS(msg.text || "aria lebst du noch?");
|
|
} else if (msg.action === "reconnect_gateway") {
|
|
connectGateway();
|
|
} else if (msg.action === "reconnect_rvs") {
|
|
connectRVS();
|
|
}
|
|
} 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)"}`);
|
|
|
|
// Verbindungen aufbauen
|
|
connectGateway();
|
|
connectRVS();
|
|
});
|