diff --git a/aria-data/config/aria.env.example b/aria-data/config/aria.env.example
index 25d67b7..eccfa90 100644
--- a/aria-data/config/aria.env.example
+++ b/aria-data/config/aria.env.example
@@ -1,6 +1,7 @@
# Bridge → aria-core (OpenClaw Gateway)
-# Standard: ws://aria-core:18789 (internes Docker-Netz)
-ARIA_CORE_WS=ws://aria-core:18789
+# Bridge teilt Netzwerk mit aria-core (network_mode: service:aria)
+# → localhost ist aria-core
+ARIA_CORE_WS=ws://127.0.0.1:18789
# Piper TTS Stimmen
PIPER_RAMONA=/voices/de_DE-ramona-low.onnx
diff --git a/bridge/aria_bridge.py b/bridge/aria_bridge.py
index bf6e764..6edfdfb 100644
--- a/bridge/aria_bridge.py
+++ b/bridge/aria_bridge.py
@@ -52,7 +52,7 @@ logger = logging.getLogger("aria-bridge")
CONFIG_PATH = Path("/config/aria.env")
VOICES_DIR = Path("/voices")
-CORE_WS_URL = os.getenv("ARIA_CORE_WS", "ws://aria-core:18789")
+CORE_WS_URL = os.getenv("ARIA_CORE_WS", "ws://127.0.0.1:18789")
CORE_AUTH_TOKEN = os.getenv("ARIA_AUTH_TOKEN", "") # OpenClaw Gateway Token
RVS_HOST = os.getenv("RVS_HOST", "") # z.B. rvs.hackersoft.de
RVS_PORT = os.getenv("RVS_PORT", "443") # Port des RVS
diff --git a/diagnostic/Dockerfile b/diagnostic/Dockerfile
new file mode 100644
index 0000000..d65f05e
--- /dev/null
+++ b/diagnostic/Dockerfile
@@ -0,0 +1,7 @@
+FROM node:22-alpine
+WORKDIR /app
+COPY package.json ./
+RUN npm install --production
+COPY . .
+EXPOSE 3001
+CMD ["node", "server.js"]
diff --git a/diagnostic/index.html b/diagnostic/index.html
new file mode 100644
index 0000000..c76da67
--- /dev/null
+++ b/diagnostic/index.html
@@ -0,0 +1,228 @@
+
+
+
+
+
+ ARIA Diagnostic
+
+
+
+ ARIA Diagnostic
+
+
+
+
+
OpenClaw Gateway
+
+
+
+
+
+
+
RVS (Rendezvous)
+
+
+
+
+
+
+
+
+
+
Chat Test
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/diagnostic/package.json b/diagnostic/package.json
new file mode 100644
index 0000000..6840176
--- /dev/null
+++ b/diagnostic/package.json
@@ -0,0 +1,12 @@
+{
+ "name": "aria-diagnostic",
+ "version": "0.0.1",
+ "description": "ARIA Diagnostic — Verbindungstest und Chat-Test",
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js"
+ },
+ "dependencies": {
+ "ws": "^8.18.0"
+ }
+}
diff --git a/diagnostic/server.js b/diagnostic/server.js
new file mode 100644
index 0000000..23993c5
--- /dev/null
+++ b/diagnostic/server.js
@@ -0,0 +1,401 @@
+"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_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() {
+ if (!RVS_HOST || !RVS_TOKEN) {
+ log("info", "rvs", "Nicht konfiguriert — ueberspringe");
+ state.rvs.status = "not_configured";
+ broadcastState();
+ return;
+ }
+
+ const proto = RVS_TLS === "true" ? "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");
+ 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: ${err.message}`);
+ state.rvs.lastError = err.message;
+ broadcastState();
+ });
+}
+
+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();
+});
diff --git a/docker-compose.yml b/docker-compose.yml
index 3e2059a..a9aea8a 100644
--- a/docker-compose.yml
+++ b/docker-compose.yml
@@ -18,9 +18,10 @@ services:
privileged: true # ARIAs Wohnung — sie hat die Schlüssel
depends_on:
- proxy
+ ports:
+ - "3001:3001" # Diagnostic Web-UI (laeuft im shared network)
environment:
- CANVAS_HOST=127.0.0.1
- - OPENCLAW_GATEWAY_BIND=0.0.0.0 # Bridge muss von Docker-Netz zugreifen (kein Port-Mapping nach aussen)
- OPENCLAW_GATEWAY_TOKEN=${ARIA_AUTH_TOKEN}
- OPENAI_API_KEY=not-needed
- OPENAI_BASE_URL=http://proxy:3456/v1
@@ -45,6 +46,7 @@ services:
container_name: aria-bridge
depends_on:
- aria
+ network_mode: "service:aria" # Teilt Netzwerk mit aria-core → localhost:18789 erreichbar
volumes:
- ./aria-data/voices:/voices:ro # TTS Stimmen
- ./aria-data/config/aria.env:/config/aria.env
@@ -62,8 +64,21 @@ services:
- RVS_TLS_FALLBACK=${RVS_TLS_FALLBACK:-true}
- RVS_TOKEN=${RVS_TOKEN:-}
restart: unless-stopped
- networks:
- - aria-net
+
+ # ─── Diagnostic (Selbstcheck-UI) ──────────────────────
+ diagnostic:
+ build: ./diagnostic
+ container_name: aria-diagnostic
+ depends_on:
+ - aria
+ network_mode: "service:aria" # Teilt Netzwerk mit aria-core → localhost:18789
+ environment:
+ - ARIA_AUTH_TOKEN=${ARIA_AUTH_TOKEN:-}
+ - RVS_HOST=${RVS_HOST:-}
+ - RVS_PORT=${RVS_PORT:-443}
+ - RVS_TLS=${RVS_TLS:-true}
+ - RVS_TOKEN=${RVS_TOKEN:-}
+ restart: unless-stopped
networks:
aria-net: