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

+
+
+ + + +
+
+
+ + +
+

Verbindungslog

+
+
+ + + + 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: