Files
gott/wwwroot/remote.ps1
T
MelekiandClaude Opus 4.8 ace5036f00 Remote-Keep-Alive gegen 180s-Timeout + System-Prompt via Datei
Task 1 – Remote-Befehle laufen nicht mehr in einen starren 180s-Timeout:
- RemoteConnectionManager.ExecuteCommand nutzt jetzt einen INAKTIVITÄTS-
  Timeout (Watchdog auf LastProgressAt) statt hartem Wall-Clock-Limit; ein
  Befehl darf beliebig lange laufen, solange der Client Progress-Frames sendet.
- RemoteEndpoint: neuer "progress"-Case setzt die Frist zurück.
- InternalEndpoint (MCP-Pfad remote_execute): hartes CancellationTokenSource(180s)
  entfernt → nutzt konfigurierbares ExecuteTimeout-Setting + RequestAborted.
- remote.ps1 / remote.sh: senden während laufender Befehle alle 15s ein
  {"type":"progress"}-Keep-Alive; async stdout/stderr-Lesen gegen Pipe-Deadlocks.
- ToolService: Tool-Beschreibungen erklären den Inaktivitäts-Timeout.

Task 2 – System-Prompt über --system-prompt-file statt CLI-Argument:
- ClaudeCliService schreibt den vollen System-Prompt in eine Temp-Datei und
  übergibt --system-prompt-file, um das Windows-Kommandozeilen-Längenlimit zu
  umgehen. Cleanup im finally. Agent-Verhalten unverändert (MEMORY.md bleibt inline).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 09:21:55 +02:00

223 lines
8.8 KiB
PowerShell

# GOTT Remote Agent - Windows PowerShell Client
# Verbindet sich per WebSocket zum GOTT-Server und fuehrt Befehle aus.
#
# Verwendung:
# irm https://gott.example.com/remote.ps1 | iex
# oder:
# .\remote.ps1 -Url "wss://gott.example.com/api/remote"
param(
[string]$Url = "{{WS_URL}}"
)
$ErrorActionPreference = "Stop"
Write-Host ""
Write-Host " ============================" -ForegroundColor Cyan
Write-Host " GOTT Remote Agent" -ForegroundColor Cyan
Write-Host " ============================" -ForegroundColor Cyan
Write-Host ""
Write-Host " Verbinde zu: $Url" -ForegroundColor Gray
Write-Host ""
function Send-WsMessage {
param([System.Net.WebSockets.ClientWebSocket]$ws, [string]$msg, [System.Threading.CancellationToken]$ct)
$bytes = [System.Text.Encoding]::UTF8.GetBytes($msg)
$ws.SendAsync((New-Object System.ArraySegment[byte] -ArgumentList @(,$bytes)),
[System.Net.WebSockets.WebSocketMessageType]::Text, $true, $ct).GetAwaiter().GetResult() | Out-Null
}
function Receive-WsMessage {
param([System.Net.WebSockets.ClientWebSocket]$ws, [System.Threading.CancellationToken]$ct)
$buf = New-Object byte[] 65536
$ms = New-Object System.IO.MemoryStream
try {
do {
$seg = New-Object System.ArraySegment[byte] -ArgumentList @(,$buf)
$result = $ws.ReceiveAsync($seg, $ct).GetAwaiter().GetResult()
if ($result.MessageType -eq [System.Net.WebSockets.WebSocketMessageType]::Close) {
return $null
}
$ms.Write($buf, 0, $result.Count)
} while (-not $result.EndOfMessage)
return [System.Text.Encoding]::UTF8.GetString($ms.ToArray())
} finally {
$ms.Dispose()
}
}
try {
$ws = New-Object System.Net.WebSockets.ClientWebSocket
$cts = New-Object System.Threading.CancellationTokenSource
$ct = $cts.Token
# Ctrl+C abfangen → CancellationToken ausloesen
[Console]::TreatControlCAsInput = $false
$cancelHandler = [System.ConsoleCancelEventHandler]{
param($s, $e)
$e.Cancel = $true
$cts.Cancel()
}
[Console]::add_CancelKeyPress($cancelHandler)
# TLS 1.2 erzwingen (fuer aeltere Windows-Versionen)
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.SecurityProtocolType]::Tls12
# Verbindung aufbauen
try {
$ws.ConnectAsync([Uri]$Url, $ct).GetAwaiter().GetResult() | Out-Null
} catch {
Write-Host " [FEHLER] Verbindung fehlgeschlagen: $_" -ForegroundColor Red
Write-Host " URL: $Url" -ForegroundColor Gray
return
}
Write-Host " [OK] Verbunden!" -ForegroundColor Green
# Hello-Message senden
$hostname = $env:COMPUTERNAME
$username = $env:USERNAME
$osVersion = [System.Environment]::OSVersion.VersionString
$hello = @{
type = "hello"
hostname = $hostname
username = $username
os = $osVersion
} | ConvertTo-Json -Compress
Send-WsMessage $ws $hello $ct
Write-Host " Host: $username@$hostname ($osVersion)" -ForegroundColor Gray
Write-Host ""
Write-Host " Warte auf Befehle... (Strg+C zum Beenden)" -ForegroundColor Yellow
Write-Host ""
# Synchroner Empfangs-Loop
while ($ws.State -eq [System.Net.WebSockets.WebSocketState]::Open) {
# Blockierend auf naechste Nachricht warten
$json = Receive-WsMessage $ws $ct
if ($null -eq $json) {
Write-Host " Server hat Verbindung geschlossen." -ForegroundColor Yellow
break
}
try {
$msg = $json | ConvertFrom-Json
} catch {
continue
}
switch ($msg.type) {
"execute" {
$command = $msg.command
$timestamp = Get-Date -Format "HH:mm:ss"
Write-Host " [$timestamp] Ausfuehren: " -NoNewline -ForegroundColor Cyan
Write-Host $command -ForegroundColor White
$exitCode = 0
$stdout = ""
$stderr = ""
try {
# Process direkt starten um CLIXML-Parsing-Fehler bei stderr zu vermeiden
$encodedCmd = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($command))
$proc = New-Object System.Diagnostics.Process
$proc.StartInfo.FileName = "powershell.exe"
$proc.StartInfo.Arguments = "-NoProfile -NonInteractive -EncodedCommand $encodedCmd"
$proc.StartInfo.RedirectStandardOutput = $true
$proc.StartInfo.RedirectStandardError = $true
$proc.StartInfo.UseShellExecute = $false
$proc.StartInfo.CreateNoWindow = $true
$proc.Start() | Out-Null
# stdout+stderr async lesen um Pipe-Deadlocks zu vermeiden
$stdoutTask = $proc.StandardOutput.ReadToEndAsync()
$stderrTask = $proc.StandardError.ReadToEndAsync()
# Auf Prozessende warten und dabei periodisch ein Keep-Alive senden,
# damit lange Befehle nicht in den Server-Inaktivitaets-Timeout laufen.
$lastProgress = [DateTime]::UtcNow
while (-not $proc.HasExited) {
Start-Sleep -Milliseconds 500
if (([DateTime]::UtcNow - $lastProgress).TotalSeconds -ge 15) {
try { Send-WsMessage $ws '{"type":"progress"}' $ct } catch { }
$lastProgress = [DateTime]::UtcNow
}
}
$stdoutTask.Wait()
$stderrTask.Wait()
$stdout = $stdoutTask.Result
$stderr = $stderrTask.Result
$proc.WaitForExit()
$exitCode = $proc.ExitCode
$proc.Dispose()
} catch {
$stderr = $_.Exception.Message
$exitCode = 1
}
# CLIXML-Progress aus stderr filtern (PowerShell schreibt Modul-Lade-Fortschritt als CLIXML)
if ($stderr -and $stderr.StartsWith("#< CLIXML")) {
$cleanLines = @()
foreach ($line in ($stderr -split "`n")) {
$trimmed = $line.Trim()
if ($trimmed -eq "#< CLIXML") { continue }
if ($trimmed -match '^\<Objs .+\<PR N="Record"\>.*\<T\>Completed\</T\>') { continue }
if ($trimmed -ne "") { $cleanLines += $line }
}
$stderr = ($cleanLines -join "`n").Trim()
}
# Ergebnis senden
$response = @{
type = "result"
exitCode = $exitCode
output = if ($stdout) { $stdout } else { "" }
error = if ($stderr) { $stderr } else { "" }
} | ConvertTo-Json -Compress -Depth 3
Send-WsMessage $ws $response $ct
# Kurzausgabe lokal
if ($stdout) {
$preview = if ($stdout.Length -gt 200) { $stdout.Substring(0, 200) + "..." } else { $stdout }
Write-Host " -> $preview" -ForegroundColor Gray
}
if ($stderr) {
Write-Host " [stderr] $stderr" -ForegroundColor Red
}
Write-Host " [Exit: $exitCode]" -ForegroundColor $(if ($exitCode -eq 0) { "Green" } else { "Red" })
Write-Host ""
}
"ping" {
# Server-Heartbeat → mit pong antworten
Send-WsMessage $ws '{"type":"pong"}' $ct
}
"pong" {
# Heartbeat-Antwort - ignorieren
}
default {
Write-Host " Unbekannte Nachricht: $($msg.type)" -ForegroundColor Yellow
}
}
}
} catch [System.OperationCanceledException] {
# Ctrl+C
} catch [System.AggregateException] {
# Kann durch CancellationToken in .GetAwaiter().GetResult() kommen
} catch [System.Net.WebSockets.WebSocketException] {
Write-Host " Verbindung verloren." -ForegroundColor Red
} catch {
Write-Host " Fehler: $_" -ForegroundColor Red
} finally {
# Event-Handler entfernen
try { [Console]::remove_CancelKeyPress($cancelHandler) } catch { }
if ($ws -and $ws.State -eq [System.Net.WebSockets.WebSocketState]::Open) {
try {
$closeCt = (New-Object System.Threading.CancellationTokenSource(3000)).Token
$ws.CloseAsync([System.Net.WebSockets.WebSocketCloseStatus]::NormalClosure, "bye", $closeCt).GetAwaiter().GetResult() | Out-Null
} catch { }
}
if ($cts) { $cts.Dispose() }
if ($ws) { $ws.Dispose() }
Write-Host ""
Write-Host " Verbindung beendet." -ForegroundColor Yellow
}