- 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:
+167
-19
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user