ARIA sitzt woanders und kommt nicht ins Buero-Netz. Der Satellit kann beides: er sieht das LAN und haelt eine ausgehende Verbindung zum RVS. Also reicht er eine TCP-Verbindung durch — wie der VNC-Tunnel, nur fuer beliebige Ports. Damit erreicht ARIA einen FreeCAD-MCP, eine Web-App, ein NAS-Interface. Entscheidend fuer die Brauchbarkeit: die Bridge macht daraus einen GANZ NORMALEN lokalen Port (aria-bridge:19000...). Dadurch funktioniert alles, was sowieso TCP spricht — MCP-Client, curl, Browser — ohne Sonder-API. Jede eingehende Verbindung bekommt einen eigenen Tunnel, parallele Verbindungen sind also kein Problem (ein Browser oeffnet gern sechs). Geraete-Namen: "duffy-desktop" ist ansprechbar, "192.168.1.47" nicht. Stefan kann jedem gefundenen Geraet einen Namen geben, ARIA adressiert danach. Schluessel ist die MAC, nicht die IP — per DHCP wandert die IP, die MAC bleibt. Absicherung, weil das ein Weg in ein fremdes Netz ist: - TUNNEL_ENABLED default AUS, pro Satellit. - Ohne TUNNEL_ALLOW nur Ziele, die bei der Suche GEFUNDEN wurden. Der Tunnel kann damit nichts ansteuern, was nicht ohnehin im LAN sichtbar ist — und nichts im Internet. - Obergrenze gleichzeitiger Tunnel + Reaper fuer ungenutzte (sonst bleibt eine vergessene Verbindung offen, bis der Satellit neu startet). - Faehigkeit 'tunnel' wird im sat_hello gemeldet, damit Diagnostic und ARIA sehen, ob der Satellit das ueberhaupt erlaubt. Neue RVS-Typen eingetragen (sat_tunnel_*, sat_name_*) — ohne das verwirft die Allow-List sie still, und nichts funktioniert ohne Fehlermeldung. 16 Pruefungen gegen BEIDE echten Implementierungen (nur der RVS dazwischen ist kurzgeschlossen): Namensaufloesung, Freigabe-Regeln, fremder Satellit ignoriert, abgeschaltet blockt, echter Byte-Durchlauf (PONG:HALLO), unbekanntes Ziel schliesst sauber statt zu haengen, Liste/Schliessen/Doppel-Schliessen. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
666 lines
26 KiB
JavaScript
666 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",
|
|
// Port-Tunnel: ARIA erreicht ueber den Satelliten einen beliebigen Port im
|
|
// fremden LAN (FreeCAD-MCP, Web-App ...). Die Bridge macht daraus einen
|
|
// lokalen Port. sat_tunnel_data laeuft in BEIDE Richtungen — unterschieden
|
|
// wird ueber die tunnelId. Plus sat_name_* fuer vergebene Geraetenamen,
|
|
// damit ARIA "duffy-desktop" sagen kann statt einer IP.
|
|
"sat_tunnel_open", "sat_tunnel_opened", "sat_tunnel_data",
|
|
"sat_tunnel_close", "sat_tunnel_closed",
|
|
"sat_name_set", "sat_name_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} |