- 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
+167 -19
View File
@@ -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")