feat: Remote-Verbindungen – Windows-Workstations per WebSocket steuern

- RemoteConnectionManager: Singleton-Service verwaltet WebSocket-Verbindungen
  mit Command-Execution (Send/Receive mit Timeout + SemaphoreSlim-Locking)
- RemoteEndpoint: WebSocket-Endpoint /api/remote (auth-frei), REST-APIs für
  Connections/Assign/Unassign, PowerShell-Script-Auslieferung mit dynamischer URL
- remote.ps1: PowerShell-Client für Windows – verbindet per WebSocket,
  sendet Hostname/User/OS, empfängt und führt Befehle aus, Heartbeat
- remote_execute Tool: Neues Agent-Tool, nur verfügbar wenn Session eine
  zugeordnete Remote-Verbindung hat, führt PowerShell-Befehle remote aus
- Frontend: Menüpunkt 'Remote-Verbindungen' im Chat-Dropdown, Modal zeigt
  alle aktiven WebSocket-Verbindungen (Hostname, User, OS, Verbindungszeit),
  Verbinden/Trennen-Buttons pro Verbindung und Session
- Auth: /api/remote und /remote.ps1 von Authentifizierung ausgenommen

Architektur: Windows-Client → WebSocket → GOTT-Server → Agent-Tool
Session-gebunden: Eine Verbindung wird einer Session zugeordnet
This commit is contained in:
Meleki committed 2026-03-11 07:04:24 +01:00
1 parent 9f4746c2e7
commit 10da2bd964
14 files changed
+706 -18

No files matched your search

+25
View File
@@ -629,6 +629,31 @@ async function apiImportOpenclawSession(openclawSessionId, sessionId) {
return resp.json();
}
// ── Remote API ──
async function apiGetRemoteConnections() {
const resp = await fetch('/api/remote/connections');
return resp.json();
}
async function apiAssignRemote(connectionId, sessionId) {
const resp = await fetch('/api/remote/assign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ connectionId, sessionId })
});
return resp.json();
}
async function apiUnassignRemote(connectionId) {
const resp = await fetch('/api/remote/unassign', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ connectionId })
});
return resp.json();
}
// ── Backup API ──
async function apiBackupDb() {
+98
View File
@@ -292,6 +292,104 @@ async function showOpenClawImportModal(sessionId) {
}
}
// ── Remote-Verbindungen Modal ──
async function showRemoteModal(sessionId) {
const { body, close } = _createModal('Remote-Verbindungen');
body.innerHTML = '<div class="sp-loading">Laden...</div>';
async function renderConnections() {
try {
const connections = await apiGetRemoteConnections();
body.innerHTML = '';
if (!connections || connections.length === 0) {
var empty = document.createElement('div');
empty.className = 'sp-loading';
empty.innerHTML = 'Keine Remote-Verbindungen aktiv.<br><br>' +
'<span style="font-size:var(--font-size);opacity:var(--semi-transparent)">Auf dem Windows-PC ausführen:<br>' +
'<code>irm ' + esc(location.origin) + '/remote.ps1 | iex</code></span>';
body.appendChild(empty);
return;
}
var hint = document.createElement('div');
hint.style.cssText = 'font-size:var(--font-size);opacity:var(--semi-transparent);margin-bottom:var(--box-spacing)';
hint.textContent = 'Verbindung auswählen um Remote-Befehle in dieser Session zu ermöglichen:';
body.appendChild(hint);
for (var i = 0; i < connections.length; i++) {
var conn = connections[i];
var card = document.createElement('div');
card.className = 'sp-block';
card.style.cursor = 'pointer';
var isAssigned = conn.assignedSessionId === sessionId;
var isAssignedOther = conn.assignedSessionId && conn.assignedSessionId !== sessionId;
var label = document.createElement('div');
label.className = 'sp-block-label';
var statusIcon = isAssigned ? 'fa-check-circle' : (isAssignedOther ? 'fa-link' : 'fa-desktop');
var statusColor = isAssigned ? 'color:var(--primary)' : '';
label.innerHTML = '<i class="fa ' + statusIcon + '" style="' + statusColor + '"></i> ' +
esc(conn.username || '?') + '@' + esc(conn.hostname || '?');
card.appendChild(label);
var content = document.createElement('div');
content.className = 'sp-block-content';
content.style.cssText = 'font-size:var(--font-size);display:flex;align-items:center;gap:var(--box-spacing)';
var info = document.createElement('span');
info.style.cssText = 'flex:1;opacity:var(--semi-transparent)';
var connDate = new Date(conn.connectedAt);
info.textContent = conn.os + ' • Verbunden seit ' + connDate.toLocaleTimeString('de');
content.appendChild(info);
var btn = document.createElement('button');
if (isAssigned) {
btn.textContent = 'Trennen';
btn.style.cssText = 'color:var(--dangerous)';
} else if (isAssignedOther) {
btn.textContent = 'Andere Session';
btn.disabled = true;
btn.style.cssText = 'opacity:var(--semi-transparent)';
} else {
btn.textContent = 'Verbinden';
btn.style.cssText = 'color:var(--primary)';
}
content.appendChild(btn);
card.appendChild(content);
body.appendChild(card);
// Event-Handler
(function(c, assigned) {
btn.addEventListener('click', async function(e) {
e.stopPropagation();
btn.disabled = true;
try {
if (assigned) {
await apiUnassignRemote(c.id);
} else {
await apiAssignRemote(c.id, sessionId);
}
await renderConnections();
} catch(err) {
showToast('Fehler: ' + err.message, 'error');
btn.disabled = false;
}
});
})(conn, isAssigned);
}
} catch (e) {
body.innerHTML = '<div class="sp-loading" style="color:var(--dangerous)">Fehler beim Laden: ' + esc(e.message) + '</div>';
}
}
await renderConnections();
}
// Event-Delegation für Krea-Thumbnails im Chat
document.addEventListener('click', function(e) {
var ref = e.target.closest('.krea-ref');
+7
View File
@@ -169,6 +169,13 @@ registerPage('chat', {
});
}
// Remote-Verbindungen
const remoteBtn = document.getElementById('remoteBtn');
if (remoteBtn) remoteBtn.addEventListener('click', () => {
if (chatMenuDropdown) chatMenuDropdown.hidden = true;
showRemoteModal(sessionId);
});
// System-Prompt anzeigen
const systemPromptBtn = document.getElementById('systemPromptBtn');
if (systemPromptBtn) systemPromptBtn.addEventListener('click', () => {
+3 -2
View File
@@ -1,12 +1,13 @@
// ── Tool Call Visibility ──
const MODIFICATION_TOOLS = ['write_file', 'edit_file', 'delete_file', 'memory_write'];
const MODIFICATION_TOOLS = ['write_file', 'edit_file', 'delete_file', 'memory_write', 'remote_execute'];
const TOOL_DISPLAY_NAMES = {
write_file: 'Datei schreiben',
edit_file: 'Datei bearbeiten',
delete_file: 'Datei löschen',
memory_write: 'Erinnerungen festhalten',
web_search: 'Web-Suche'
web_search: 'Web-Suche',
remote_execute: 'Remote-Befehl'
};
function isToolVisible(toolName) {