Verdrahtet die neuen Kanaele "dunkel" (noch ohne UI):
- rvs/server.js: ALLOWED_TYPES um code_file/code_file_edit, check_desktop/
desktop_status und vnc_open/close/data/input erweitert (Base64-in-JSON-Relay
wie audio_pcm, kein Binaer-Handling noetig).
- brainApi.ts: Project.kind ('code'|'chat') + desktop_url; ChatScreen publiziert
die Kinds in den projectFocus-Spiegel.
- services/codeFile.ts: Live-Spiegel der Code-Dateien (content + Deltas) mit
Ruckkanal code_file_edit fuer eigene Edits.
- services/desktop.ts: Desktop-Status + VNC-RFB-Tunnel (vnc_open/close/input/
data) durch RVS, Session = Projekt-ID.
tsc clean, rvs/server.js syntaktisch OK.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
103 lines
3.4 KiB
TypeScript
103 lines
3.4 KiB
TypeScript
/**
|
|
* desktop — Desktop-/VNC-Anbindung fuer Code-Projekte.
|
|
*
|
|
* Zwei Aufgaben:
|
|
* 1. Verfuegbarkeit: `check_desktop` triggert die Bridge, `desktop_status`
|
|
* meldet zurueck ob eine QEMU-VNC laeuft (und ggf. eine direkte URL).
|
|
* 2. VNC-Tunnel: der noVNC-Client in der App-WebView spricht kein eigenes
|
|
* WebSocket, sondern schickt RFB-Bytes als `vnc_input` (Base64) ueber RVS;
|
|
* die Bridge oeffnet die TCP-Verbindung zu QEMU (host:5901) und streamt die
|
|
* Antwort als `vnc_data` zurueck. Base64-in-JSON wie audio_pcm.
|
|
*
|
|
* Eine Session = ein Desktop; wir nutzen die Projekt-ID als Session-Key (leer =
|
|
* 'main'). Muster wie services/rvs.ts (Singleton mit Listener-Listen).
|
|
*/
|
|
|
|
import rvs, { RVSMessage } from './rvs';
|
|
|
|
export interface DesktopStatus {
|
|
available: boolean;
|
|
session: string;
|
|
/** optionale direkte noVNC-URL (falls Host direkt erreichbar) */
|
|
url?: string;
|
|
message?: string;
|
|
}
|
|
|
|
type StatusSub = (s: DesktopStatus) => void;
|
|
type VncDataSub = (b64: string) => void;
|
|
|
|
const DEFAULT_VNC_PORT = 5901;
|
|
const sessionOf = (projectId: string) => projectId || 'main';
|
|
|
|
class DesktopService {
|
|
private status: DesktopStatus = { available: false, session: '' };
|
|
private statusSubs: StatusSub[] = [];
|
|
private vncDataSubs: VncDataSub[] = [];
|
|
private currentSession = '';
|
|
|
|
constructor() {
|
|
rvs.onMessage((m) => this.onMessage(m));
|
|
}
|
|
|
|
private onMessage(m: RVSMessage): void {
|
|
const p = (m.payload || {}) as any;
|
|
if (m.type === 'desktop_status') {
|
|
this.status = {
|
|
available: !!p.available,
|
|
session: p.session || '',
|
|
url: typeof p.url === 'string' ? p.url : undefined,
|
|
message: p.message,
|
|
};
|
|
const s = this.status;
|
|
this.statusSubs.forEach((cb) => cb(s));
|
|
} else if (m.type === 'vnc_data') {
|
|
if (this.currentSession && p.session && p.session !== this.currentSession) return;
|
|
const b64 = typeof p.b64 === 'string' ? p.b64 : '';
|
|
if (b64) this.vncDataSubs.forEach((cb) => cb(b64));
|
|
}
|
|
}
|
|
|
|
getStatus(): DesktopStatus {
|
|
return this.status;
|
|
}
|
|
|
|
subscribeStatus(cb: StatusSub): () => void {
|
|
this.statusSubs.push(cb);
|
|
cb(this.status);
|
|
return () => { this.statusSubs = this.statusSubs.filter((s) => s !== cb); };
|
|
}
|
|
|
|
/** Bridge fragen, ob fuer dieses Projekt ein QEMU-Desktop laeuft. */
|
|
requestCheck(projectId: string, port: number = DEFAULT_VNC_PORT): void {
|
|
rvs.send('check_desktop', { projectId: projectId || '', session: sessionOf(projectId), port });
|
|
}
|
|
|
|
/** VNC-Tunnel oeffnen — Bridge verbindet TCP zu QEMU. */
|
|
openVnc(projectId: string, port: number = DEFAULT_VNC_PORT): string {
|
|
const session = sessionOf(projectId);
|
|
this.currentSession = session;
|
|
rvs.send('vnc_open', { session, port });
|
|
return session;
|
|
}
|
|
|
|
closeVnc(): void {
|
|
if (this.currentSession) rvs.send('vnc_close', { session: this.currentSession });
|
|
this.currentSession = '';
|
|
}
|
|
|
|
/** RFB-Bytes (Base64) aus der noVNC-WebView an die Bridge weiterreichen. */
|
|
sendInput(b64: string): void {
|
|
if (!this.currentSession) return;
|
|
rvs.send('vnc_input', { session: this.currentSession, b64 });
|
|
}
|
|
|
|
/** Listener fuer eingehende RFB-Bytes (Base64) — die noVNC-WebView. */
|
|
onVncData(cb: VncDataSub): () => void {
|
|
this.vncDataSubs.push(cb);
|
|
return () => { this.vncDataSubs = this.vncDataSubs.filter((s) => s !== cb); };
|
|
}
|
|
}
|
|
|
|
const desktop = new DesktopService();
|
|
export default desktop;
|