- aria-data/config/AGENT.md — ARIAs Persönlichkeit und Sicherheitsregeln

- `aria-data/config/USER.md` — Stefans Präferenzen
- `aria-data/config/TOOLING.md` — VM-Tooling Liste
- `aria-data/skills/README.md` — Skill-Bauanleitung

### Bekannte Probleme
- Android Release-Build: `EMFILE: too many open files` — Fix: `CI=true` in `build.sh`
- JDK 21 inkompatibel mit AGP 8.1 — Fix: Automatischer Fallback auf JDK 17
- `react-native-screens` > 3.27.0 inkompatibel mit RN 0.73.4 — Fix: Version gepinnt
This commit is contained in:
2026-03-11 23:13:28 +01:00
parent 71f9ae221c
commit c5d835ea09
11 changed files with 245 additions and 44 deletions
Binary file not shown.
Binary file not shown.
+26
View File
@@ -4,6 +4,25 @@ Alle Änderungen am Projekt. Format: [Keep a Changelog](https://keepachangelog.c
--- ---
## [0.0.0.4] — 2026-03-11
### Geändert
**Bridge → aria-core: OpenClaw Gateway Protokoll**
- Bridge nutzt jetzt das echte OpenClaw Gateway WebSocket-Protokoll (Port 18789 statt 8080)
- Vollständiger Handshake: `connect.challenge``connect` Request (mit Auth-Token) → `hello-ok`
- Nachrichten über `chat.send` Method mit `sessionKey` und `idempotencyKey`
- Antworten über `chat:final` Events (statt custom JSON)
- Streaming-Support vorbereitet (`chat:delta` Events werden empfangen)
- Fehlerbehandlung für `chat:error` Events — werden an die App weitergeleitet
**Docker-Compose Fixes**
- `OPENCLAW_GATEWAY_BIND=0.0.0.0` — Gateway bindet auf alle Interfaces (sonst nur 127.0.0.1, Bridge kann nicht zugreifen)
- `OPENCLAW_GATEWAY_TOKEN` statt `AUTH_TOKEN` — korrekter Env-Var-Name fuer OpenClaw Gateway Auth
- `ARIA_AUTH_TOKEN` an Bridge-Container durchgereicht — Bridge authentifiziert sich am Gateway
---
## [0.0.0.3] — 2026-03-09 ## [0.0.0.3] — 2026-03-09
### Geändert ### Geändert
@@ -50,6 +69,7 @@ Alle Änderungen am Projekt. Format: [Keep a Changelog](https://keepachangelog.c
**Docker & Infrastruktur** **Docker & Infrastruktur**
- OpenClaw Image fix: `openclaw/openclaw:latest``ghcr.io/openclaw/openclaw:latest` - OpenClaw Image fix: `openclaw/openclaw:latest``ghcr.io/openclaw/openclaw:latest`
- Proxy fix: Binary heißt `claude-max-api`, braucht `@anthropic-ai/claude-code` als Peer-Dependency
- Proxy Binary-Name fix: `claude-max-api-proxy``claude-max-api` (npm-Paket heißt anders als die Binary) - Proxy Binary-Name fix: `claude-max-api-proxy``claude-max-api` (npm-Paket heißt anders als die Binary)
- `libportaudio2` in Bridge Dockerfile hinzugefügt — `sounddevice` braucht PortAudio - `libportaudio2` in Bridge Dockerfile hinzugefügt — `sounddevice` braucht PortAudio
- `aria-data/config/aria.env.example` hinzugefügt — Voice Bridge Konfigurationsvorlage - `aria-data/config/aria.env.example` hinzugefügt — Voice Bridge Konfigurationsvorlage
@@ -80,6 +100,12 @@ Alle Änderungen am Projekt. Format: [Keep a Changelog](https://keepachangelog.c
- `network_security_config.xml` hinzugefuegt — Android 9+ blockiert sonst `ws://` (Cleartext) - `network_security_config.xml` hinzugefuegt — Android 9+ blockiert sonst `ws://` (Cleartext)
- Verbindungslog im Settings-Tab — zeigt jeden Verbindungsversuch, Fehler, Fallback (scrollbar, max 200px) - Verbindungslog im Settings-Tab — zeigt jeden Verbindungsversuch, Fehler, Fallback (scrollbar, max 200px)
- Gespeicherte Config wird beim Start in die Einstellungsfelder geladen - Gespeicherte Config wird beim Start in die Einstellungsfelder geladen
- Fix: TLS-Fallback erzeugte Doppel-Verbindungen (onerror + onclose beide reconnected)
**RVS — Ghost-Client Fix**
- Heartbeat-Intervall 30s → 15s, Cleanup 60s → 30s — tote Clients werden schneller entfernt
- `heartbeat` als erlaubter Nachrichtentyp hinzugefuegt — App-Heartbeats halten Verbindung lebendig
- App-seitiger JSON-Heartbeat zaehlt als Lebenszeichen (zusaetzlich zu WebSocket Ping/Pong)
**Neues Script: `get-voices.sh`** **Neues Script: `get-voices.sh`**
- Lädt Piper Stimmen (Ramona + Thorsten) von HuggingFace herunter - Lädt Piper Stimmen (Ramona + Thorsten) von HuggingFace herunter
-11
View File
@@ -1,11 +0,0 @@
[Interface]
Address = 10.252.1.21/32
PrivateKey = 2JmAeJQ1wL+nfaAVp32RiEsPFcaoXVtZh/p7pqHGCl4=
MTU = 1450
[Peer]
PublicKey = IHBroF1ChESXWQQ+2RC4DmrNoHQl54Hc/xhH+iYLTBA=
PresharedKey = A1i59KCEjvwtx9J03pkcqDdGP7Jhr4PcbA5Um32iMoY=
AllowedIPs = 192.168.0.0/24
Endpoint = stb-er.selfhost.eu:51820
PersistentKeepalive = 15
+19 -5
View File
@@ -215,7 +215,8 @@ services:
- proxy - proxy
environment: environment:
- CANVAS_HOST=127.0.0.1 - CANVAS_HOST=127.0.0.1
- AUTH_TOKEN=${ARIA_AUTH_TOKEN} - OPENCLAW_GATEWAY_BIND=0.0.0.0 # Bridge muss von Docker-Netz zugreifen
- OPENCLAW_GATEWAY_TOKEN=${ARIA_AUTH_TOKEN}
- OPENAI_API_KEY=not-needed - OPENAI_API_KEY=not-needed
- OPENAI_BASE_URL=http://proxy:3456/v1 - OPENAI_BASE_URL=http://proxy:3456/v1
- DEFAULT_MODEL=openai/claude-sonnet-4-6 - DEFAULT_MODEL=openai/claude-sonnet-4-6
@@ -249,6 +250,7 @@ services:
- /dev/snd - /dev/snd
environment: environment:
- PULSE_SERVER=unix:/run/user/1000/pulse/native - PULSE_SERVER=unix:/run/user/1000/pulse/native
- ARIA_AUTH_TOKEN=${ARIA_AUTH_TOKEN:-}
- RVS_HOST=${RVS_HOST:-} - RVS_HOST=${RVS_HOST:-}
- RVS_PORT=${RVS_PORT:-443} - RVS_PORT=${RVS_PORT:-443}
- RVS_TLS=${RVS_TLS:-true} - RVS_TLS=${RVS_TLS:-true}
@@ -406,7 +408,19 @@ cp .env.example .env
# → RVS_HOST + RVS_PORT eintragen (z.B. rvs.hackersoft.de / 443) # → RVS_HOST + RVS_PORT eintragen (z.B. rvs.hackersoft.de / 443)
``` ```
### 3. Konfiguration & Stimmen ### 3. Claude CLI verbinden (Proxy-Auth)
Der Proxy-Container leitet API-Calls über deine Claude Max Subscription.
Dafür muss die Claude CLI einmalig auf der VM eingeloggt sein:
```bash
npm install -g @anthropic-ai/claude-code
claude login
# → Zeigt einen Link + Code — im Browser öffnen und bestätigen
# → Credentials landen in ~/.config/claude/ (wird read-only in den Container gemounted)
```
### 4. Konfiguration & Stimmen
```bash ```bash
# Voice Bridge Konfiguration anlegen # Voice Bridge Konfiguration anlegen
@@ -417,7 +431,7 @@ cp aria-data/config/aria.env.example aria-data/config/aria.env
./get-voices.sh ./get-voices.sh
``` ```
### 4. Token generieren & starten ### 5. Token generieren & starten
```bash ```bash
# Token erzeugen — schreibt RVS_TOKEN automatisch in .env, zeigt QR-Code # Token erzeugen — schreibt RVS_TOKEN automatisch in .env, zeigt QR-Code
@@ -427,11 +441,11 @@ cp aria-data/config/aria.env.example aria-data/config/aria.env
docker compose up -d docker compose up -d
``` ```
### 5. App verbinden ### 6. App verbinden
App öffnen → QR-Code scannen → "ARIA, hörst du mich?" 🎙️ App öffnen → QR-Code scannen → "ARIA, hörst du mich?" 🎙️
> Alles was über diese fünf Schritte hinausgeht macht ARIA selbst. > Alles was über diese sechs Schritte hinausgeht macht ARIA selbst.
--- ---
+8 -1
View File
@@ -224,10 +224,17 @@ class RVSConnection {
// TLS-Fallback: Wenn wss:// fehlschlaegt, auf ws:// wechseln // TLS-Fallback: Wenn wss:// fehlschlaegt, auf ws:// wechseln
if (this.config?.useTLS && !this.usingTLSFallback) { if (this.config?.useTLS && !this.usingTLSFallback) {
this.usingTLSFallback = true; this.usingTLSFallback = true;
// shouldReconnect kurz deaktivieren damit onclose keinen
// parallelen Reconnect ausloest — wir machen das selbst
this.shouldReconnect = false;
this.log('warn', 'TLS fehlgeschlagen — Fallback auf ws:// (ohne TLS)'); this.log('warn', 'TLS fehlgeschlagen — Fallback auf ws:// (ohne TLS)');
this.clearTimers(); this.clearTimers();
this.ws?.close(); if (this.ws) {
this.ws.onclose = null; // onclose-Handler entfernen um Doppel-Reconnect zu verhindern
try { this.ws.close(); } catch (_) {}
}
this.ws = null; this.ws = null;
this.shouldReconnect = true;
this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS; this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
this.establishConnection(); this.establishConnection();
return; return;
+7 -1
View File
@@ -1,4 +1,10 @@
OPENCLAW_URL=http://aria-core:18789 # Bridge → aria-core (OpenClaw Gateway)
# Standard: ws://aria-core:18789 (internes Docker-Netz)
ARIA_CORE_WS=ws://aria-core:18789
# Piper TTS Stimmen
PIPER_RAMONA=/voices/de_DE-ramona-low.onnx PIPER_RAMONA=/voices/de_DE-ramona-low.onnx
PIPER_THORSTEN=/voices/de_DE-thorsten-high.onnx PIPER_THORSTEN=/voices/de_DE-thorsten-high.onnx
# Wake-Word
WAKE_WORD=aria WAKE_WORD=aria
+167 -19
View File
@@ -25,6 +25,7 @@ import signal
import ssl import ssl
import sys import sys
import tempfile import tempfile
import uuid
import wave import wave
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
@@ -51,7 +52,8 @@ logger = logging.getLogger("aria-bridge")
CONFIG_PATH = Path("/config/aria.env") CONFIG_PATH = Path("/config/aria.env")
VOICES_DIR = Path("/voices") VOICES_DIR = Path("/voices")
CORE_WS_URL = os.getenv("ARIA_CORE_WS", "ws://aria:8080") CORE_WS_URL = os.getenv("ARIA_CORE_WS", "ws://aria-core:18789")
CORE_AUTH_TOKEN = os.getenv("ARIA_AUTH_TOKEN", "") # OpenClaw Gateway Token
RVS_HOST = os.getenv("RVS_HOST", "") # z.B. rvs.hackersoft.de RVS_HOST = os.getenv("RVS_HOST", "") # z.B. rvs.hackersoft.de
RVS_PORT = os.getenv("RVS_PORT", "443") # Port des RVS RVS_PORT = os.getenv("RVS_PORT", "443") # Port des RVS
RVS_TLS = os.getenv("RVS_TLS", "true") # true = wss://, false = ws:// RVS_TLS = os.getenv("RVS_TLS", "true") # true = wss://, false = ws://
@@ -405,6 +407,9 @@ class ARIABridge:
def __init__(self) -> None: def __init__(self) -> None:
self.config = load_config() self.config = load_config()
self.ws_url = self.config.get("ARIA_CORE_WS", CORE_WS_URL) self.ws_url = self.config.get("ARIA_CORE_WS", CORE_WS_URL)
self.core_auth_token = self.config.get("ARIA_AUTH_TOKEN", CORE_AUTH_TOKEN)
self._req_id_counter = 0
self._session_key = "aria-bridge" # Feste Session fuer die Bridge
# RVS-Verbindungsinfo aus Config oder Env # RVS-Verbindungsinfo aus Config oder Env
rvs_host = self.config.get("RVS_HOST", RVS_HOST) rvs_host = self.config.get("RVS_HOST", RVS_HOST)
rvs_port = self.config.get("RVS_PORT", RVS_PORT) rvs_port = self.config.get("RVS_PORT", RVS_PORT)
@@ -473,19 +478,99 @@ class ARIABridge:
logger.warning(" Setze RVS_HOST, RVS_PORT, RVS_TOKEN in /config/aria.env") logger.warning(" Setze RVS_HOST, RVS_PORT, RVS_TOKEN in /config/aria.env")
logger.info("Modus: %s %s", self.current_mode.config.emoji, self.current_mode.config.name) logger.info("Modus: %s %s", self.current_mode.config.emoji, self.current_mode.config.name)
# ── aria-core Verbindung ───────────────────────────────── # ── aria-core Verbindung (OpenClaw Gateway Protokoll) ───
def _next_req_id(self) -> str:
"""Erzeugt eine eindeutige Request-ID fuer das OpenClaw-Protokoll."""
self._req_id_counter += 1
return f"bridge-{self._req_id_counter}"
async def _openclaw_handshake(self, ws: websockets.WebSocketClientProtocol) -> bool:
"""Fuehrt den OpenClaw Gateway Handshake durch.
1. Wartet auf connect.challenge Event vom Gateway
2. Sendet connect Request mit Auth-Token
3. Wartet auf hello-ok Response
Returns:
True wenn Handshake erfolgreich, sonst False.
"""
try:
# Schritt 1: Auf Challenge warten (max 10s)
raw = await asyncio.wait_for(ws.recv(), timeout=10.0)
challenge = json.loads(raw)
if challenge.get("type") != "event" or challenge.get("event") != "connect.challenge":
logger.error("[core] Unerwartete erste Nachricht: %s", raw[:200])
return False
nonce = challenge.get("payload", {}).get("nonce", "")
logger.info("[core] Challenge empfangen (nonce: %s...)", nonce[:8] if nonce else "?")
# Schritt 2: Connect Request senden
connect_req = {
"type": "req",
"id": self._next_req_id(),
"method": "connect",
"params": {
"minProtocol": 3,
"maxProtocol": 3,
"client": {
"id": "aria-bridge",
"version": "0.0.3",
"platform": "linux",
"mode": "operator",
},
"role": "operator",
"scopes": ["operator.read", "operator.write"],
"caps": ["voice"],
"commands": [],
"permissions": {},
"auth": {"token": self.core_auth_token} if self.core_auth_token else {},
"locale": "de-DE",
"userAgent": "aria-bridge/0.0.3",
},
}
await ws.send(json.dumps(connect_req))
logger.info("[core] Connect-Request gesendet")
# Schritt 3: Auf hello-ok warten (max 10s)
raw = await asyncio.wait_for(ws.recv(), timeout=10.0)
response = json.loads(raw)
if response.get("type") == "res" and response.get("ok"):
logger.info("[core] Handshake erfolgreich — hello-ok empfangen")
return True
else:
error = response.get("error", response)
logger.error("[core] Handshake fehlgeschlagen: %s", json.dumps(error)[:200])
return False
except asyncio.TimeoutError:
logger.error("[core] Handshake-Timeout (10s)")
return False
except Exception:
logger.exception("[core] Handshake-Fehler")
return False
async def connect_to_core(self) -> None: async def connect_to_core(self) -> None:
"""Persistente WebSocket-Verbindung zu aria-core mit Auto-Reconnect.""" """Persistente WebSocket-Verbindung zu aria-core (OpenClaw Gateway)."""
retry_delay = 2 retry_delay = 2
while self.running: while self.running:
try: try:
logger.info("[core] Verbinde: %s", self.ws_url) logger.info("[core] Verbinde: %s", self.ws_url)
async with websockets.connect(self.ws_url) as ws: async with websockets.connect(self.ws_url) as ws:
# OpenClaw Handshake durchfuehren
if not await self._openclaw_handshake(ws):
logger.error("[core] Handshake fehlgeschlagen — Reconnect")
await asyncio.sleep(retry_delay)
retry_delay = min(retry_delay * 2, 30)
continue
self.ws_core = ws self.ws_core = ws
retry_delay = 2 retry_delay = 2
logger.info("[core] Verbunden") logger.info("[core] Verbunden und authentifiziert")
async for message in ws: async for message in ws:
await self._handle_core_message(message) await self._handle_core_message(message)
@@ -493,7 +578,7 @@ class ARIABridge:
except websockets.ConnectionClosed: except websockets.ConnectionClosed:
logger.warning("[core] Verbindung verloren") logger.warning("[core] Verbindung verloren")
except ConnectionRefusedError: except ConnectionRefusedError:
logger.warning("[core] Nicht erreichbar") logger.warning("[core] Nicht erreichbar (%s)", self.ws_url)
except Exception: except Exception:
logger.exception("[core] WebSocket-Fehler") logger.exception("[core] WebSocket-Fehler")
finally: finally:
@@ -505,10 +590,11 @@ class ARIABridge:
retry_delay = min(retry_delay * 2, 30) retry_delay = min(retry_delay * 2, 30)
async def _handle_core_message(self, raw_message: str) -> None: async def _handle_core_message(self, raw_message: str) -> None:
"""Verarbeitet Nachrichten von aria-core. """Verarbeitet Nachrichten von aria-core (OpenClaw Gateway Protokoll).
- Leitet Antworten an die App weiter (via RVS) Unterstuetzte Frame-Typen:
- Sprachausgabe ueber TTS (wenn Modus erlaubt) - event: chat:delta (Streaming-Tokens), chat:final (fertige Antwort)
- res: Antworten auf Requests (chat.send Acknowledgment)
""" """
try: try:
message = json.loads(raw_message) message = json.loads(raw_message)
@@ -516,13 +602,71 @@ class ARIABridge:
logger.error("[core] Ungueltige JSON: %s", raw_message[:100]) logger.error("[core] Ungueltige JSON: %s", raw_message[:100])
return return
text = message.get("text", "") frame_type = message.get("type", "")
metadata = message.get("metadata", {})
# ── Response auf unsere Requests (z.B. chat.send Ack) ──
if frame_type == "res":
req_id = message.get("id", "")
if message.get("ok"):
logger.debug("[core] Request %s bestätigt", req_id)
else:
error = message.get("error", "Unbekannt")
logger.error("[core] Request %s fehlgeschlagen: %s", req_id, error)
return
# ── Events vom Gateway ──
if frame_type != "event":
logger.debug("[core] Unbekannter Frame-Typ: %s", frame_type)
return
event_name = message.get("event", "")
payload = message.get("payload", {})
if event_name == "chat:delta":
# Streaming-Delta — fuer spaeter (Live-Typing in der App)
delta = payload.get("delta", payload.get("text", ""))
if delta:
logger.debug("[core] Delta: '%s'", delta[:40])
return
if event_name == "chat:final":
# Fertige Antwort von aria-core
text = payload.get("text", payload.get("message", ""))
if not text:
logger.warning("[core] chat:final ohne Text: %s", json.dumps(payload)[:200])
return
logger.info("[core] Antwort: '%s'", text[:80])
await self._process_core_response(text, payload)
return
if event_name == "chat:error":
error = payload.get("error", payload.get("message", "Unbekannt"))
logger.error("[core] Chat-Fehler: %s", error)
# Fehler auch an die App melden
await self._send_to_rvs({
"type": "chat",
"payload": {
"text": f"[Fehler] {error}",
"sender": "aria",
},
"timestamp": int(asyncio.get_event_loop().time() * 1000),
})
return
# Andere Events loggen (presence, tick, etc.)
logger.debug("[core] Event: %s", event_name)
async def _process_core_response(self, text: str, payload: dict) -> None:
"""Verarbeitet eine fertige Antwort von aria-core.
- Leitet Antwort an die App weiter (via RVS)
- Sprachausgabe ueber TTS (wenn Modus erlaubt)
"""
metadata = payload.get("metadata", {})
is_critical = metadata.get("critical", False) is_critical = metadata.get("critical", False)
requested_voice = metadata.get("voice") requested_voice = metadata.get("voice")
logger.info("[core] Nachricht: '%s'", text[:80])
# Modus-Wechsel pruefen # Modus-Wechsel pruefen
new_mode = detect_mode_switch(text) new_mode = detect_mode_switch(text)
if new_mode is not None: if new_mode is not None:
@@ -532,7 +676,6 @@ class ARIABridge:
self.current_mode.config.emoji, self.current_mode.config.emoji,
self.current_mode.config.name, self.current_mode.config.name,
) )
# Modus-Aenderung auch an die App senden
await self._send_to_rvs({ await self._send_to_rvs({
"type": "mode", "type": "mode",
"payload": {"mode": self.current_mode.name}, "payload": {"mode": self.current_mode.name},
@@ -576,21 +719,26 @@ class ARIABridge:
logger.info("[core] TTS unterdrueckt (Modus: %s)", self.current_mode.config.name) logger.info("[core] TTS unterdrueckt (Modus: %s)", self.current_mode.config.name)
async def send_to_core(self, text: str, source: str = "bridge") -> None: async def send_to_core(self, text: str, source: str = "bridge") -> None:
"""Sendet Text an aria-core.""" """Sendet Text an aria-core (OpenClaw chat.send Protokoll)."""
if self.ws_core is None: if self.ws_core is None:
logger.error("[core] Nicht verbunden — Nachricht verworfen: '%s'", text[:60]) logger.error("[core] Nicht verbunden — Nachricht verworfen: '%s'", text[:60])
return return
req_id = self._next_req_id()
message = json.dumps({ message = json.dumps({
"type": "voice_input" if source == "bridge" else "chat_input", "type": "req",
"text": text, "id": req_id,
"mode": self.current_mode.name, "method": "chat.send",
"source": source, "params": {
"sessionKey": self._session_key,
"text": text,
"idempotencyKey": str(uuid.uuid4()),
},
}) })
try: try:
await self.ws_core.send(message) await self.ws_core.send(message)
logger.info("[core] Gesendet (%s): '%s'", source, text[:80]) logger.info("[core] chat.send (%s, id=%s): '%s'", source, req_id, text[:80])
except Exception: except Exception:
logger.exception("[core] Sendefehler") logger.exception("[core] Sendefehler")
+3 -1
View File
@@ -20,7 +20,8 @@ services:
- proxy - proxy
environment: environment:
- CANVAS_HOST=127.0.0.1 - CANVAS_HOST=127.0.0.1
- AUTH_TOKEN=${ARIA_AUTH_TOKEN} - 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_API_KEY=not-needed
- OPENAI_BASE_URL=http://proxy:3456/v1 - OPENAI_BASE_URL=http://proxy:3456/v1
- DEFAULT_MODEL=openai/claude-sonnet-4-6 - DEFAULT_MODEL=openai/claude-sonnet-4-6
@@ -54,6 +55,7 @@ services:
- /dev/snd - /dev/snd
environment: environment:
- PULSE_SERVER=unix:/run/user/1000/pulse/native - PULSE_SERVER=unix:/run/user/1000/pulse/native
- ARIA_AUTH_TOKEN=${ARIA_AUTH_TOKEN:-}
- RVS_HOST=${RVS_HOST:-} - RVS_HOST=${RVS_HOST:-}
- RVS_PORT=${RVS_PORT:-443} - RVS_PORT=${RVS_PORT:-443}
- RVS_TLS=${RVS_TLS:-true} - RVS_TLS=${RVS_TLS:-true}
+14 -5
View File
@@ -8,7 +8,7 @@ const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS || "10", 10);
// Erlaubte Nachrichtentypen — alles andere wird verworfen // Erlaubte Nachrichtentypen — alles andere wird verworfen
const ALLOWED_TYPES = new Set([ const ALLOWED_TYPES = new Set([
"chat", "audio", "file", "location", "mode", "log", "event", "chat", "audio", "file", "location", "mode", "log", "event", "heartbeat",
]); ]);
// Token-Raum: token -> { clients: Set<ws> } // Token-Raum: token -> { clients: Set<ws> }
@@ -129,13 +129,14 @@ function registerClient(ws, token) {
}); });
} }
// ── Heartbeat — hält Verbindungen am Leben ────────────────────────── // ── Heartbeat — hält Verbindungen am Leben, räumt tote auf ──────────
const HEARTBEAT_INTERVAL = 30_000; const HEARTBEAT_INTERVAL = 15_000;
const heartbeat = setInterval(() => { const heartbeat = setInterval(() => {
for (const client of wss.clients) { for (const client of wss.clients) {
if (client.isAlive === false) { if (client.isAlive === false) {
log(`Toter Client entfernt (kein Pong)`);
client.terminate(); client.terminate();
continue; continue;
} }
@@ -147,10 +148,18 @@ const heartbeat = setInterval(() => {
wss.on("connection", (ws) => { wss.on("connection", (ws) => {
ws.isAlive = true; ws.isAlive = true;
ws.on("pong", () => { ws.isAlive = true; }); ws.on("pong", () => { ws.isAlive = true; });
// App-seitiger Heartbeat (JSON) zaehlt auch als lebendig
const origOnMessage = ws._events?.message;
ws.on("message", (raw) => {
try {
const msg = JSON.parse(raw);
if (msg.type === "heartbeat") ws.isAlive = true;
} catch {}
});
}); });
// Aufräumen alle 60 Sekunden // Aufräumen alle 30 Sekunden (statt 60)
const cleanup = setInterval(cleanupRooms, 60_000); const cleanup = setInterval(cleanupRooms, 30_000);
wss.on("close", () => { wss.on("close", () => {
clearInterval(heartbeat); clearInterval(heartbeat);