- 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:
Binary file not shown.
Binary file not shown.
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
@@ -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.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||||
|
|||||||
Reference in New Issue
Block a user