- `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:
parent
71f9ae221c
commit
c5d835ea09
Binary file not shown.
Binary file not shown.
26
CHANGELOG.md
26
CHANGELOG.md
|
|
@ -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
|
||||
|
||||
### Geändert
|
||||
|
|
@ -50,6 +69,7 @@ Alle Änderungen am Projekt. Format: [Keep a Changelog](https://keepachangelog.c
|
|||
|
||||
**Docker & Infrastruktur**
|
||||
- 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)
|
||||
- `libportaudio2` in Bridge Dockerfile hinzugefügt — `sounddevice` braucht PortAudio
|
||||
- `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)
|
||||
- Verbindungslog im Settings-Tab — zeigt jeden Verbindungsversuch, Fehler, Fallback (scrollbar, max 200px)
|
||||
- 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`**
|
||||
- Lädt Piper Stimmen (Ramona + Thorsten) von HuggingFace herunter
|
||||
|
|
|
|||
|
|
@ -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
|
||||
24
README.md
24
README.md
|
|
@ -215,7 +215,8 @@ services:
|
|||
- proxy
|
||||
environment:
|
||||
- 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_BASE_URL=http://proxy:3456/v1
|
||||
- DEFAULT_MODEL=openai/claude-sonnet-4-6
|
||||
|
|
@ -249,6 +250,7 @@ services:
|
|||
- /dev/snd
|
||||
environment:
|
||||
- PULSE_SERVER=unix:/run/user/1000/pulse/native
|
||||
- ARIA_AUTH_TOKEN=${ARIA_AUTH_TOKEN:-}
|
||||
- RVS_HOST=${RVS_HOST:-}
|
||||
- RVS_PORT=${RVS_PORT:-443}
|
||||
- RVS_TLS=${RVS_TLS:-true}
|
||||
|
|
@ -406,7 +408,19 @@ cp .env.example .env
|
|||
# → 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
|
||||
# Voice Bridge Konfiguration anlegen
|
||||
|
|
@ -417,7 +431,7 @@ cp aria-data/config/aria.env.example aria-data/config/aria.env
|
|||
./get-voices.sh
|
||||
```
|
||||
|
||||
### 4. Token generieren & starten
|
||||
### 5. Token generieren & starten
|
||||
|
||||
```bash
|
||||
# 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
|
||||
```
|
||||
|
||||
### 5. App verbinden
|
||||
### 6. App verbinden
|
||||
|
||||
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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -224,10 +224,17 @@ class RVSConnection {
|
|||
// TLS-Fallback: Wenn wss:// fehlschlaegt, auf ws:// wechseln
|
||||
if (this.config?.useTLS && !this.usingTLSFallback) {
|
||||
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.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.shouldReconnect = true;
|
||||
this.reconnectDelay = INITIAL_RECONNECT_DELAY_MS;
|
||||
this.establishConnection();
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -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_THORSTEN=/voices/de_DE-thorsten-high.onnx
|
||||
WAKE_WORD=aria
|
||||
|
||||
# Wake-Word
|
||||
WAKE_WORD=aria
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import signal
|
|||
import ssl
|
||||
import sys
|
||||
import tempfile
|
||||
import uuid
|
||||
import wave
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
|
@ -51,7 +52,8 @@ 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: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_PORT = os.getenv("RVS_PORT", "443") # Port des RVS
|
||||
RVS_TLS = os.getenv("RVS_TLS", "true") # true = wss://, false = ws://
|
||||
|
|
@ -405,6 +407,9 @@ class ARIABridge:
|
|||
def __init__(self) -> None:
|
||||
self.config = load_config()
|
||||
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_host = self.config.get("RVS_HOST", RVS_HOST)
|
||||
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.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:
|
||||
"""Persistente WebSocket-Verbindung zu aria-core mit Auto-Reconnect."""
|
||||
"""Persistente WebSocket-Verbindung zu aria-core (OpenClaw Gateway)."""
|
||||
retry_delay = 2
|
||||
|
||||
while self.running:
|
||||
try:
|
||||
logger.info("[core] Verbinde: %s", self.ws_url)
|
||||
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
|
||||
retry_delay = 2
|
||||
logger.info("[core] Verbunden")
|
||||
logger.info("[core] Verbunden und authentifiziert")
|
||||
|
||||
async for message in ws:
|
||||
await self._handle_core_message(message)
|
||||
|
|
@ -493,7 +578,7 @@ class ARIABridge:
|
|||
except websockets.ConnectionClosed:
|
||||
logger.warning("[core] Verbindung verloren")
|
||||
except ConnectionRefusedError:
|
||||
logger.warning("[core] Nicht erreichbar")
|
||||
logger.warning("[core] Nicht erreichbar (%s)", self.ws_url)
|
||||
except Exception:
|
||||
logger.exception("[core] WebSocket-Fehler")
|
||||
finally:
|
||||
|
|
@ -505,10 +590,11 @@ class ARIABridge:
|
|||
retry_delay = min(retry_delay * 2, 30)
|
||||
|
||||
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)
|
||||
- Sprachausgabe ueber TTS (wenn Modus erlaubt)
|
||||
Unterstuetzte Frame-Typen:
|
||||
- event: chat:delta (Streaming-Tokens), chat:final (fertige Antwort)
|
||||
- res: Antworten auf Requests (chat.send Acknowledgment)
|
||||
"""
|
||||
try:
|
||||
message = json.loads(raw_message)
|
||||
|
|
@ -516,13 +602,71 @@ class ARIABridge:
|
|||
logger.error("[core] Ungueltige JSON: %s", raw_message[:100])
|
||||
return
|
||||
|
||||
text = message.get("text", "")
|
||||
metadata = message.get("metadata", {})
|
||||
frame_type = message.get("type", "")
|
||||
|
||||
# ── 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)
|
||||
requested_voice = metadata.get("voice")
|
||||
|
||||
logger.info("[core] Nachricht: '%s'", text[:80])
|
||||
|
||||
# Modus-Wechsel pruefen
|
||||
new_mode = detect_mode_switch(text)
|
||||
if new_mode is not None:
|
||||
|
|
@ -532,7 +676,6 @@ class ARIABridge:
|
|||
self.current_mode.config.emoji,
|
||||
self.current_mode.config.name,
|
||||
)
|
||||
# Modus-Aenderung auch an die App senden
|
||||
await self._send_to_rvs({
|
||||
"type": "mode",
|
||||
"payload": {"mode": self.current_mode.name},
|
||||
|
|
@ -576,21 +719,26 @@ class ARIABridge:
|
|||
logger.info("[core] TTS unterdrueckt (Modus: %s)", self.current_mode.config.name)
|
||||
|
||||
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:
|
||||
logger.error("[core] Nicht verbunden — Nachricht verworfen: '%s'", text[:60])
|
||||
return
|
||||
|
||||
req_id = self._next_req_id()
|
||||
message = json.dumps({
|
||||
"type": "voice_input" if source == "bridge" else "chat_input",
|
||||
"text": text,
|
||||
"mode": self.current_mode.name,
|
||||
"source": source,
|
||||
"type": "req",
|
||||
"id": req_id,
|
||||
"method": "chat.send",
|
||||
"params": {
|
||||
"sessionKey": self._session_key,
|
||||
"text": text,
|
||||
"idempotencyKey": str(uuid.uuid4()),
|
||||
},
|
||||
})
|
||||
|
||||
try:
|
||||
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:
|
||||
logger.exception("[core] Sendefehler")
|
||||
|
||||
|
|
|
|||
|
|
@ -20,7 +20,8 @@ services:
|
|||
- proxy
|
||||
environment:
|
||||
- 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_BASE_URL=http://proxy:3456/v1
|
||||
- DEFAULT_MODEL=openai/claude-sonnet-4-6
|
||||
|
|
@ -54,6 +55,7 @@ services:
|
|||
- /dev/snd
|
||||
environment:
|
||||
- PULSE_SERVER=unix:/run/user/1000/pulse/native
|
||||
- ARIA_AUTH_TOKEN=${ARIA_AUTH_TOKEN:-}
|
||||
- RVS_HOST=${RVS_HOST:-}
|
||||
- RVS_PORT=${RVS_PORT:-443}
|
||||
- RVS_TLS=${RVS_TLS:-true}
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS || "10", 10);
|
|||
|
||||
// Erlaubte Nachrichtentypen — alles andere wird verworfen
|
||||
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> }
|
||||
|
|
@ -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(() => {
|
||||
for (const client of wss.clients) {
|
||||
if (client.isAlive === false) {
|
||||
log(`Toter Client entfernt (kein Pong)`);
|
||||
client.terminate();
|
||||
continue;
|
||||
}
|
||||
|
|
@ -147,10 +148,18 @@ const heartbeat = setInterval(() => {
|
|||
wss.on("connection", (ws) => {
|
||||
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
|
||||
const cleanup = setInterval(cleanupRooms, 60_000);
|
||||
// Aufräumen alle 30 Sekunden (statt 60)
|
||||
const cleanup = setInterval(cleanupRooms, 30_000);
|
||||
|
||||
wss.on("close", () => {
|
||||
clearInterval(heartbeat);
|
||||
|
|
|
|||
Loading…
Reference in New Issue