Der RVS ist ein Broadcast-Relay — bisher bekam jeder Client im Raum jede Nachricht. Bei einem Datei-Re-Download war das fatal: ein 150-MB-Video wird als base64 ~200 MB gross und ging an ALLE Clients, auch an die STT/TTS-Worker. Die sind daran mit Code 1009 (message too big) gestorben und haben die gerade laufende Sprachaufnahme mitgerissen — die Ursache von "erste Sprachnachricht kommt nicht durch". Wer eine Anfrage mit requestId stellt, wird jetzt gemerkt; die Antwort geht nur an ihn zurueck (file_response, file_zip_response, file_version_download_response, brain_response). Unbekannte requestId wird wie bisher gebroadcastet, ein weggefallener Fragender verworfen statt an alle geschickt. Verifiziert mit drei WS-Clients gegen einen lokalen RVS: der Fragende bekommt seine Antwort, der Unbeteiligte nicht, chat bleibt Broadcast. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
658 lines
26 KiB
JavaScript
658 lines
26 KiB
JavaScript
"use strict";
|
|
|
|
const { WebSocketServer } = require("ws");
|
|
const http = require("http");
|
|
const fs = require("fs");
|
|
const path = require("path");
|
|
|
|
// ── Konfiguration aus Umgebungsvariablen ────────────────────────────
|
|
const PORT = parseInt(process.env.PORT || "3000", 10);
|
|
const MAX_SESSIONS = parseInt(process.env.MAX_SESSIONS || "10", 10);
|
|
const UPDATES_DIR = process.env.UPDATES_DIR || "/updates";
|
|
// Kein Polling — APK wird manuell per git pull bereitgestellt
|
|
|
|
// Erlaubte Nachrichtentypen — alles andere wird verworfen
|
|
const ALLOWED_TYPES = new Set([
|
|
"chat", "audio", "file", "location", "mode", "log", "event", "heartbeat",
|
|
"file_request", "file_response", "file_saved", "stt_result", "config", "tts_request",
|
|
"xtts_request", "xtts_response", "xtts_list_voices", "xtts_voices_list", "voice_upload", "xtts_voice_saved",
|
|
"update_check", "update_available", "update_download", "update_data",
|
|
"agent_activity", "cancel_request", "interject",
|
|
"audio_pcm",
|
|
"file_from_aria",
|
|
"container_restart",
|
|
"file_list_request", "file_list_response",
|
|
"file_delete_request", "file_deleted",
|
|
"xtts_export_voice", "xtts_voice_exported",
|
|
"xtts_import_voice", "xtts_voice_imported",
|
|
"skill_created",
|
|
"trigger_created",
|
|
"memory_saved",
|
|
"location_update", "location_tracking",
|
|
"chat_history_request", "chat_history_response", "chat_cleared",
|
|
"delete_message_request", "chat_message_deleted",
|
|
"brain_request", "brain_response",
|
|
"app_log",
|
|
"file_delete_batch_request", "file_delete_batch_response",
|
|
"file_zip_request", "file_zip_response",
|
|
"xtts_delete_voice",
|
|
"voice_preload", "voice_ready",
|
|
"stt_request", "stt_response",
|
|
// Streaming-STT (Phase 1+2): App schickt PCM live an whisper-bridge,
|
|
// die feuert stt_endpoint mit dem finalen Text — kein Audio-Roundtrip.
|
|
"stt_stream_start", "stt_audio_chunk", "stt_stream_end",
|
|
"stt_partial", "stt_endpoint", "stt_stream_done",
|
|
// Speaker-ID / Voice-Enrollment (Phase 1+2): App schickt 5-10 Samples zur
|
|
// whisper-bridge, die berechnet einen Voice-Fingerprint (Embedding-Vektor)
|
|
// und nutzt ihn um nur Stefans Stimme an Whisper STT durchzulassen.
|
|
"voice_id_status_request", "voice_id_status_response",
|
|
"voice_id_enroll_request", "voice_id_enroll_response",
|
|
"voice_id_delete_request", "voice_id_delete_response",
|
|
// Projekte (Stefan-Konzept: Threads im Hauptchat verankert) — Side-Channel-
|
|
// Event vom Brain → Bridge → App/Diagnostic, damit beide Clients ihren
|
|
// aktiven-Projekt-Banner refreshen wenn ARIA via Tool was aendert.
|
|
"project_changed",
|
|
// File-Versioning (Datei-Manager in App): Versionen pro Datei listen,
|
|
// alte Versionen herunterladen, Restore = non-destructive neuer Commit.
|
|
"file_version_list_request", "file_version_list_response",
|
|
"file_version_download_request", "file_version_download_response",
|
|
"file_version_restore_request", "file_version_restore_response",
|
|
"service_status",
|
|
"config_request",
|
|
"flux_request", "flux_response",
|
|
"agent_stream",
|
|
"oauth_callback",
|
|
// Lokales LLM (Plan B) — Router im Brain schickt einfache Turns an das
|
|
// Qwen3 auf der AI-Box (via Bridge → RVS → llm-adapter → llama.cpp).
|
|
// llm_partial ist fuer B2 (Token-Streaming) reserviert, noch ungenutzt.
|
|
"llm_request", "llm_response", "llm_partial",
|
|
// Workspace-Desktop (Code-Projekte): Live-Code-Editor (CodeMirror in der App)
|
|
// spiegelt ARIAs Datei-Writes, und QEMU-VNC wird als RFB-Bytes durch RVS
|
|
// getunnelt (Base64-in-JSON wie audio_pcm — kein Binaer-Handling noetig).
|
|
"code_file", "code_file_edit",
|
|
// M1 Generatives Cockpit: ARIA komponiert via present_view eine View-Spec
|
|
// (Orb + Karten), die App/Web/Diagnostic mit ihrem jeweiligen Renderer
|
|
// materialisieren. Brain → Bridge → RVS → Clients.
|
|
"aria_view",
|
|
"check_desktop", "desktop_status",
|
|
"vnc_open", "vnc_close", "vnc_data", "vnc_input",
|
|
// Satelliten (Info-/Gateway-Aussenposten in fremden Netzen): melden sich mit
|
|
// sat_hello, liefern Geraete-Inventar (sat_devices) auf sat_discover und
|
|
// fuehren Aktionen aus (sat_command → sat_result).
|
|
"sat_hello", "sat_discover", "sat_devices", "sat_command", "sat_result",
|
|
// Satelliten-Credential-Store: Diagnostic legt pro Geraet Zugangsdaten ab
|
|
// (SNMP/HTTP/FritzBox), der Satellit speichert sie verschluesselt.
|
|
"sat_creds_set", "sat_creds_delete", "sat_creds_list",
|
|
"sat_creds_result", "sat_creds_list_result",
|
|
// Host-Agenten: ein Agent laeuft direkt auf einem Rechner, meldet sich mit
|
|
// host_hello/host_ping und fuehrt host_command aus (exec/read/write/info/
|
|
// screenshot) -> host_result. ARIA steuert so Rechner auch hinter NAT.
|
|
"host_hello", "host_ping", "host_command", "host_result",
|
|
// Agent-Historie: Brain schickt pro Host-Aktion ein agent_history-Event (via
|
|
// Bridge), Diagnostic fragt die gespeicherte Historie per agent_history_query
|
|
// ab, die Bridge antwortet mit agent_history_list. OHNE diese Typen verwirft
|
|
// der RVS sie still an der Allow-List und die Historie bleibt leer.
|
|
"agent_history", "agent_history_query", "agent_history_list",
|
|
// STT-Lease: die App fragt vor einer Aufnahme, WELCHE STT-Instanz sie nehmen
|
|
// soll (stt_lease_request -> stt_lease). Fehlten diese Typen hier, lief die
|
|
// Anfrage IMMER in den 250ms-Timeout: jede Aufnahme startete verspaetet (erste
|
|
// Silben weg) und ohne targetInstance nahmen ALLE STT-Worker dieselbe Session
|
|
// an -> doppelte stt_endpoint. chat_ack quittiert getippte Nachrichten; ohne
|
|
// ihn liefen sie in der App nach 3 Versuchen auf "fehlgeschlagen", obwohl sie
|
|
// laengst verarbeitet waren. stt_transcribe_* ist die Wake-Wort-Bestaetigung.
|
|
"stt_lease", "stt_lease_request", "stt_transcribe_blob", "stt_transcribe_result",
|
|
"chat_ack",
|
|
// Raum-Diagnose: Diagnostic fragt die aktuellen RVS-Raeume ab (rooms_query),
|
|
// RVS antwortet direkt mit rooms_info (Fingerprint + Laenge + Client-Zahl je
|
|
// Raum). Deckt Token-Prefix-Kollisionen auf (2 Raeume, gleicher 8-Zeichen-Log).
|
|
"rooms_query", "rooms_info",
|
|
// Compute-Flotte (AI-Boxen): Worker (f5tts/whisper/voxtral/llm-adapter) melden
|
|
// sich per worker_hello an und pingen per worker_ping; der Diagnostic-Server
|
|
// aggregiert das und broadcastet worker_update/worker_list an die Browser-UI.
|
|
// node_stats_* speisen den Auslastungs-Monitor (live nvidia-smi + Historie).
|
|
// OHNE diese Typen verwirft der RVS die Meldungen an der Allow-List (Z. 312),
|
|
// und die Box bleibt in der Flotte unsichtbar, obwohl sie sendet.
|
|
"worker_hello", "worker_ping", "worker_update", "worker_list",
|
|
// OpenAI-Endpunkt einer LLM-Box: Diagnostic setzt den API-Key (der PORT kommt
|
|
// immer aus der .env der Box — er ist ein Docker-Port-Mapping).
|
|
"llm_openai_config", "llm_openai_config_result",
|
|
"node_stats", "node_stats_stream_start", "node_stats_stream_stop",
|
|
"node_stats_history_request", "node_stats_history",
|
|
"node_stats_reset", "node_stats_reset_done",
|
|
]);
|
|
|
|
// Token-Raum: token -> { clients: Set<ws> }
|
|
const rooms = new Map();
|
|
|
|
// ── Punkt-zu-Punkt-Antworten ────────────────────────────────────────
|
|
//
|
|
// Der RVS ist ein Broadcast-Relay: normalerweise bekommt JEDER Client im Raum
|
|
// jede Nachricht. Bei Datei-Antworten ist das fatal. Ein 150-MB-Video wird als
|
|
// base64 ~200 MB gross und ging bisher an ALLE Clients im Raum — auch an die
|
|
// STT/TTS-Worker, die daran mit Code 1009 (message too big) gestorben sind und
|
|
// die gerade laufende Sprachaufnahme mitgerissen haben ("erste Sprachnachricht
|
|
// kommt nicht durch"). Die Worker-max_size-Angleichung verhindert den Absturz,
|
|
// aber 200 MB an zehn Clients zu schicken bleibt Unsinn: nur EINER hat gefragt.
|
|
//
|
|
// Darum: wer eine Anfrage mit requestId stellt, wird gemerkt — die Antwort mit
|
|
// derselben requestId geht nur an ihn zurueck. Unbekannte requestId → wie
|
|
// bisher broadcasten (nichts geht verloren, falls ein Client mithoeren muss,
|
|
// dessen Anfrage der RVS nie gesehen hat).
|
|
const TARGETED = new Map([
|
|
["file_request", "file_response"],
|
|
["file_zip_request", "file_zip_response"],
|
|
["file_version_download_request", "file_version_download_response"],
|
|
["brain_request", "brain_response"],
|
|
]);
|
|
const TARGETED_REPLIES = new Set(TARGETED.values());
|
|
|
|
// `antwortTyp \0 requestId` -> { clients: Set<ws>, at }
|
|
// Mehrere Clients koennen dieselbe requestId benutzen (die App nimmt die
|
|
// Nachrichten-ID) — darum ein Set, sonst bekaeme nur der letzte die Antwort.
|
|
const pendingReplies = new Map();
|
|
const PENDING_TTL_MS = 180_000;
|
|
|
|
function pendingKey(replyType, requestId) {
|
|
return `${replyType} |