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:
1 parent
9f4746c2e7
commit
10da2bd964
14 files changed
+706
-18
No files matched your search
@@ -22,7 +22,7 @@ public static class ChatEndpoint
|
|||||||
app.MapPost("/api/chat/stop", HandleStop);
|
app.MapPost("/api/chat/stop", HandleStop);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task HandleChat(HttpRequest req, HttpResponse res, IConfiguration config, ChatService chatService, DatabaseService db, StreamingTracker streamingTracker, AnthropicService anthropic, CancellationToken cancellationToken)
|
private static async Task HandleChat(HttpRequest req, HttpResponse res, IConfiguration config, ChatService chatService, DatabaseService db, StreamingTracker streamingTracker, AnthropicService anthropic, RemoteConnectionManager remoteMgr, CancellationToken cancellationToken)
|
||||||
{
|
{
|
||||||
var body = await JsonSerializer.DeserializeAsync<ChatRequest>(req.Body);
|
var body = await JsonSerializer.DeserializeAsync<ChatRequest>(req.Body);
|
||||||
if (body == null)
|
if (body == null)
|
||||||
@@ -53,7 +53,8 @@ public static class ChatEndpoint
|
|||||||
var userId = req.HttpContext.GetUserId();
|
var userId = req.HttpContext.GetUserId();
|
||||||
var maxIterations = config.GetValue<int>("MaxIterations", 100);
|
var maxIterations = config.GetValue<int>("MaxIterations", 100);
|
||||||
|
|
||||||
var ctx = await chatService.PrepareContext(sessionId, userId, maxIterations, planMode: body.PlanMode);
|
var hasRemote = remoteMgr.GetBySessionId(sessionId) != null;
|
||||||
|
var ctx = await chatService.PrepareContext(sessionId, userId, maxIterations, planMode: body.PlanMode, hasRemoteConnection: hasRemote);
|
||||||
if (ctx == null)
|
if (ctx == null)
|
||||||
{
|
{
|
||||||
res.StatusCode = 400;
|
res.StatusCode = 400;
|
||||||
@@ -206,7 +207,7 @@ public static class ChatEndpoint
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, workCt: workCt, hardCt: hardCt, sseCt: sseCt);
|
await anthropic.StreamChat(res, ctx.ModelId, ctx.SystemPrompt, messages, ctx.ChainId, ctx.Workspace, sessionId, agentId: ctx.AgentId, useMemory: ctx.UseMemory, webSearchVersion: ctx.WebSearchVersion, maxIterations: ctx.MaxIterations, toolChoice: effectiveToolChoice, review: ctx.Review, finalizer: ctx.Finalizer, allowUserChoice: ctx.Agent?.AllowUserChoice ?? true, planMode: ctx.PlanMode, workCt: workCt, hardCt: hardCt, sseCt: sseCt, remoteMgr: remoteMgr);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,193 @@
|
|||||||
|
using System;
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.IO;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Gott.Services;
|
||||||
|
using Microsoft.AspNetCore.Builder;
|
||||||
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using Microsoft.AspNetCore.Routing;
|
||||||
|
|
||||||
|
namespace Gott.Endpoints;
|
||||||
|
|
||||||
|
public static class RemoteEndpoint
|
||||||
|
{
|
||||||
|
public static void MapRemoteEndpoints(this IEndpointRouteBuilder app)
|
||||||
|
{
|
||||||
|
// WebSocket-Endpoint für Remote-Clients (kein Auth)
|
||||||
|
app.Map("/api/remote", HandleWebSocket);
|
||||||
|
|
||||||
|
// REST-APIs für Frontend (Auth required)
|
||||||
|
app.MapGet("/api/remote/connections", GetConnections);
|
||||||
|
app.MapPost("/api/remote/assign", AssignConnection);
|
||||||
|
app.MapPost("/api/remote/unassign", UnassignConnection);
|
||||||
|
|
||||||
|
// PowerShell-Script ausliefern
|
||||||
|
app.MapGet("/remote.ps1", (Delegate)ServeScript);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task HandleWebSocket(HttpContext ctx, RemoteConnectionManager mgr)
|
||||||
|
{
|
||||||
|
if (!ctx.WebSockets.IsWebSocketRequest)
|
||||||
|
{
|
||||||
|
ctx.Response.StatusCode = 400;
|
||||||
|
await ctx.Response.WriteAsync("WebSocket expected");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var ws = await ctx.WebSockets.AcceptWebSocketAsync();
|
||||||
|
var conn = mgr.Register(ws);
|
||||||
|
Console.WriteLine($"[Remote] Client connected: {conn.Id}");
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await ReceiveLoop(ws, conn, mgr);
|
||||||
|
}
|
||||||
|
catch (WebSocketException)
|
||||||
|
{
|
||||||
|
// Client disconnected
|
||||||
|
}
|
||||||
|
catch (OperationCanceledException)
|
||||||
|
{
|
||||||
|
// Shutdown
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
mgr.Unregister(conn.Id);
|
||||||
|
Console.WriteLine($"[Remote] Client disconnected: {conn.Id} ({conn.Hostname})");
|
||||||
|
|
||||||
|
if (ws.State == WebSocketState.Open || ws.State == WebSocketState.CloseReceived)
|
||||||
|
{
|
||||||
|
try { await ws.CloseAsync(WebSocketCloseStatus.NormalClosure, "bye", CancellationToken.None); }
|
||||||
|
catch { }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task ReceiveLoop(WebSocket ws, RemoteConnection conn, RemoteConnectionManager mgr)
|
||||||
|
{
|
||||||
|
var buffer = new byte[64 * 1024]; // 64 KB Buffer
|
||||||
|
var messageBuffer = new MemoryStream();
|
||||||
|
|
||||||
|
while (ws.State == WebSocketState.Open)
|
||||||
|
{
|
||||||
|
WebSocketReceiveResult result;
|
||||||
|
messageBuffer.SetLength(0);
|
||||||
|
|
||||||
|
do
|
||||||
|
{
|
||||||
|
result = await ws.ReceiveAsync(buffer, CancellationToken.None);
|
||||||
|
if (result.MessageType == WebSocketMessageType.Close) return;
|
||||||
|
messageBuffer.Write(buffer, 0, result.Count);
|
||||||
|
} while (!result.EndOfMessage);
|
||||||
|
|
||||||
|
if (result.MessageType != WebSocketMessageType.Text) continue;
|
||||||
|
|
||||||
|
var json = Encoding.UTF8.GetString(messageBuffer.GetBuffer(), 0, (int)messageBuffer.Length);
|
||||||
|
JsonElement msg;
|
||||||
|
try { msg = JsonSerializer.Deserialize<JsonElement>(json); }
|
||||||
|
catch { continue; }
|
||||||
|
|
||||||
|
var type = msg.TryGetProperty("type", out var tp) ? tp.GetString() : "";
|
||||||
|
|
||||||
|
switch (type)
|
||||||
|
{
|
||||||
|
case "hello":
|
||||||
|
// Client meldet sich mit Hostname + OS-Info
|
||||||
|
conn.Hostname = msg.TryGetProperty("hostname", out var h) ? h.GetString() ?? "" : "";
|
||||||
|
conn.Username = msg.TryGetProperty("username", out var u) ? u.GetString() ?? "" : "";
|
||||||
|
conn.OsVersion = msg.TryGetProperty("os", out var o) ? o.GetString() ?? "" : "";
|
||||||
|
Console.WriteLine($"[Remote] Client identified: {conn.Id} = {conn.Username}@{conn.Hostname} ({conn.OsVersion})");
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "result":
|
||||||
|
// Ergebnis eines Befehls
|
||||||
|
var cmdResult = new RemoteCommandResult
|
||||||
|
{
|
||||||
|
ExitCode = msg.TryGetProperty("exitCode", out var ec) ? ec.GetInt32() : -1,
|
||||||
|
Output = msg.TryGetProperty("output", out var ot) ? ot.GetString() ?? "" : "",
|
||||||
|
Error = msg.TryGetProperty("error", out var er) ? er.GetString() ?? "" : ""
|
||||||
|
};
|
||||||
|
mgr.HandleResult(conn.Id, cmdResult);
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "ping":
|
||||||
|
// Heartbeat – antworte mit pong
|
||||||
|
var pong = Encoding.UTF8.GetBytes("{\"type\":\"pong\"}");
|
||||||
|
await ws.SendAsync(pong, WebSocketMessageType.Text, true, CancellationToken.None);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IResult GetConnections(RemoteConnectionManager mgr)
|
||||||
|
{
|
||||||
|
var connections = mgr.GetAll().Select(c => new
|
||||||
|
{
|
||||||
|
id = c.Id,
|
||||||
|
hostname = c.Hostname,
|
||||||
|
username = c.Username,
|
||||||
|
os = c.OsVersion,
|
||||||
|
connectedAt = c.ConnectedAt.ToString("o"),
|
||||||
|
assignedSessionId = c.AssignedSessionId
|
||||||
|
});
|
||||||
|
return Results.Ok(connections);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IResult AssignConnection(HttpContext ctx, RemoteConnectionManager mgr)
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(ctx.Request.Body);
|
||||||
|
var body = reader.ReadToEndAsync().Result;
|
||||||
|
var json = JsonSerializer.Deserialize<JsonElement>(body);
|
||||||
|
|
||||||
|
var connectionId = json.TryGetProperty("connectionId", out var cid) ? cid.GetString() : "";
|
||||||
|
var sessionId = json.TryGetProperty("sessionId", out var sid) ? sid.GetString() : "";
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(connectionId) || string.IsNullOrEmpty(sessionId))
|
||||||
|
return Results.BadRequest(new { error = "connectionId und sessionId erforderlich" });
|
||||||
|
|
||||||
|
if (!mgr.Assign(connectionId, sessionId))
|
||||||
|
return Results.NotFound(new { error = "Verbindung nicht gefunden" });
|
||||||
|
|
||||||
|
return Results.Ok(new { ok = true });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static IResult UnassignConnection(HttpContext ctx, RemoteConnectionManager mgr)
|
||||||
|
{
|
||||||
|
using var reader = new StreamReader(ctx.Request.Body);
|
||||||
|
var body = reader.ReadToEndAsync().Result;
|
||||||
|
var json = JsonSerializer.Deserialize<JsonElement>(body);
|
||||||
|
|
||||||
|
var connectionId = json.TryGetProperty("connectionId", out var cid) ? cid.GetString() : "";
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(connectionId))
|
||||||
|
return Results.BadRequest(new { error = "connectionId erforderlich" });
|
||||||
|
|
||||||
|
if (!mgr.Unassign(connectionId))
|
||||||
|
return Results.NotFound(new { error = "Verbindung nicht gefunden" });
|
||||||
|
|
||||||
|
return Results.Ok(new { ok = true });
|
||||||
|
}
|
||||||
|
|
||||||
|
private static async Task<IResult> ServeScript(HttpContext ctx)
|
||||||
|
{
|
||||||
|
var scriptPath = Path.Combine(AppContext.BaseDirectory, "wwwroot", "remote.ps1");
|
||||||
|
if (!File.Exists(scriptPath))
|
||||||
|
return Results.NotFound("Script nicht gefunden");
|
||||||
|
|
||||||
|
// Host aus dem Request ermitteln für die WebSocket-URL
|
||||||
|
var host = ctx.Request.Host.ToString();
|
||||||
|
var scheme = ctx.Request.Scheme == "https" ? "wss" : "ws";
|
||||||
|
var wsUrl = $"{scheme}://{host}/api/remote";
|
||||||
|
|
||||||
|
var script = await File.ReadAllTextAsync(scriptPath);
|
||||||
|
script = script.Replace("{{WS_URL}}", wsUrl);
|
||||||
|
|
||||||
|
return Results.Content(script, "text/plain; charset=utf-8");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -25,7 +25,9 @@ public class AuthMiddleware
|
|||||||
var isSharedGet = path.StartsWith("/api/shared/") && ctx.Request.Method == "GET";
|
var isSharedGet = path.StartsWith("/api/shared/") && ctx.Request.Method == "GET";
|
||||||
var isAutologin = path == "/api/auth/autologin" && ctx.Request.Method == "GET";
|
var isAutologin = path == "/api/auth/autologin" && ctx.Request.Method == "GET";
|
||||||
var isImageFile = path.StartsWith("/api/images/file/") && ctx.Request.Method == "GET";
|
var isImageFile = path.StartsWith("/api/images/file/") && ctx.Request.Method == "GET";
|
||||||
if (!path.StartsWith("/api/") || path == "/api/auth/login" || isTelegramWebhook || isSharedGet || isAutologin || isImageFile)
|
var isRemoteWs = path == "/api/remote";
|
||||||
|
var isRemoteScript = path == "/remote.ps1";
|
||||||
|
if (!path.StartsWith("/api/") || path == "/api/auth/login" || isTelegramWebhook || isSharedGet || isAutologin || isImageFile || isRemoteWs || isRemoteScript)
|
||||||
{
|
{
|
||||||
await _next(ctx);
|
await _next(ctx);
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ builder.Services.AddSingleton(authService);
|
|||||||
builder.Services.AddSingleton(dbService);
|
builder.Services.AddSingleton(dbService);
|
||||||
builder.Services.AddSingleton(new ImageService());
|
builder.Services.AddSingleton(new ImageService());
|
||||||
builder.Services.AddSingleton(new StreamingTracker());
|
builder.Services.AddSingleton(new StreamingTracker());
|
||||||
|
builder.Services.AddSingleton(new RemoteConnectionManager());
|
||||||
var memoryService = new MemoryService(dataDir);
|
var memoryService = new MemoryService(dataDir);
|
||||||
var skillService = new SkillService(dataDir);
|
var skillService = new SkillService(dataDir);
|
||||||
builder.Services.AddSingleton(memoryService);
|
builder.Services.AddSingleton(memoryService);
|
||||||
@@ -71,6 +72,7 @@ builder.Services.AddSingleton<CronjobService>();
|
|||||||
builder.Services.AddHostedService<CronjobService>(sp => sp.GetRequiredService<CronjobService>());
|
builder.Services.AddHostedService<CronjobService>(sp => sp.GetRequiredService<CronjobService>());
|
||||||
|
|
||||||
var app = builder.Build();
|
var app = builder.Build();
|
||||||
|
app.UseWebSockets();
|
||||||
app.UseDefaultFiles();
|
app.UseDefaultFiles();
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
app.UseMiddleware<AuthMiddleware>();
|
app.UseMiddleware<AuthMiddleware>();
|
||||||
@@ -93,6 +95,7 @@ app.MapSharedEndpoints();
|
|||||||
app.MapAdminEndpoints();
|
app.MapAdminEndpoints();
|
||||||
app.MapServerSettingsEndpoints();
|
app.MapServerSettingsEndpoints();
|
||||||
app.MapOpenClawEndpoints();
|
app.MapOpenClawEndpoints();
|
||||||
|
app.MapRemoteEndpoints();
|
||||||
|
|
||||||
var port = app.Configuration.GetValue<int>("Port", 19000);
|
var port = app.Configuration.GetValue<int>("Port", 19000);
|
||||||
app.Run($"http://0.0.0.0:{port}");
|
app.Run($"http://0.0.0.0:{port}");
|
||||||
@@ -53,7 +53,8 @@ public class AnthropicService
|
|||||||
bool planMode = false,
|
bool planMode = false,
|
||||||
CancellationToken workCt = default,
|
CancellationToken workCt = default,
|
||||||
CancellationToken hardCt = default,
|
CancellationToken hardCt = default,
|
||||||
CancellationToken sseCt = default)
|
CancellationToken sseCt = default,
|
||||||
|
RemoteConnectionManager remoteMgr = null)
|
||||||
{
|
{
|
||||||
var sw = System.Diagnostics.Stopwatch.StartNew();
|
var sw = System.Diagnostics.Stopwatch.StartNew();
|
||||||
var usage = new TokenUsage();
|
var usage = new TokenUsage();
|
||||||
@@ -90,7 +91,8 @@ public class AnthropicService
|
|||||||
if (systemPrompt != null && systemPrompt.Count > 0)
|
if (systemPrompt != null && systemPrompt.Count > 0)
|
||||||
payload["system"] = systemPrompt;
|
payload["system"] = systemPrompt;
|
||||||
|
|
||||||
AddToolsToPayload(payload, workspace, agentId, useMemory, webSearchVersion, toolChoice, iterations, allowUserChoice, planMode);
|
var hasRemote = remoteMgr?.GetBySessionId(sessionId) != null;
|
||||||
|
AddToolsToPayload(payload, workspace, agentId, useMemory, webSearchVersion, toolChoice, iterations, allowUserChoice, planMode, includeRemote: hasRemote);
|
||||||
|
|
||||||
var client = CreateClient();
|
var client = CreateClient();
|
||||||
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true });
|
var json = JsonSerializer.Serialize(payload, new JsonSerializerOptions { WriteIndented = true });
|
||||||
@@ -136,7 +138,7 @@ public class AnthropicService
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var (stopReason, assistantContent, executedTools, streamErrorInfo) = await ProcessStream(response, httpResp, usage, workspace, agentId, workCt, sseCt);
|
var (stopReason, assistantContent, executedTools, streamErrorInfo) = await ProcessStream(response, httpResp, usage, workspace, agentId, workCt, sseCt, remoteMgr, sessionId);
|
||||||
lastStopReason = stopReason;
|
lastStopReason = stopReason;
|
||||||
promptTrimRetries = 0; // Erfolgreicher API-Call → Retry-Counter resetten
|
promptTrimRetries = 0; // Erfolgreicher API-Call → Retry-Counter resetten
|
||||||
|
|
||||||
@@ -1153,7 +1155,9 @@ public class AnthropicService
|
|||||||
string workspace,
|
string workspace,
|
||||||
string agentId = null,
|
string agentId = null,
|
||||||
CancellationToken workCt = default,
|
CancellationToken workCt = default,
|
||||||
CancellationToken sseCt = default)
|
CancellationToken sseCt = default,
|
||||||
|
RemoteConnectionManager remoteMgr = null,
|
||||||
|
string sessionId = null)
|
||||||
{
|
{
|
||||||
var stream = await httpResp.Content.ReadAsStreamAsync(workCt);
|
var stream = await httpResp.Content.ReadAsStreamAsync(workCt);
|
||||||
// Bei Cancellation den Stream disposen, damit ReadLineAsync sofort abbricht.
|
// Bei Cancellation den Stream disposen, damit ReadLineAsync sofort abbricht.
|
||||||
@@ -1335,7 +1339,7 @@ public class AnthropicService
|
|||||||
|
|
||||||
string toolResult;
|
string toolResult;
|
||||||
bool isError = false;
|
bool isError = false;
|
||||||
try { toolResult = await _toolService.ExecuteTool(currentToolName, inputEl, workspace, _memoryService, agentId, workCt); }
|
try { toolResult = await _toolService.ExecuteTool(currentToolName, inputEl, workspace, _memoryService, agentId, workCt, remoteMgr: remoteMgr, sessionId: sessionId); }
|
||||||
catch (OperationCanceledException) { throw; }
|
catch (OperationCanceledException) { throw; }
|
||||||
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
catch (Exception ex) { toolResult = $"Fehler: {ex.Message}"; isError = true; }
|
||||||
|
|
||||||
@@ -1508,7 +1512,7 @@ public class AnthropicService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fügt Tool-Definitionen und WebSearch zum Payload hinzu.
|
/// Fügt Tool-Definitionen und WebSearch zum Payload hinzu.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
private void AddToolsToPayload(Dictionary<string, object> payload, string workspace, string agentId, bool useMemory, string webSearchVersion, string toolChoice, int iteration, bool allowUserChoice = false, bool planMode = false)
|
private void AddToolsToPayload(Dictionary<string, object> payload, string workspace, string agentId, bool useMemory, string webSearchVersion, string toolChoice, int iteration, bool allowUserChoice = false, bool planMode = false, bool includeRemote = false)
|
||||||
{
|
{
|
||||||
var hasWorkspace = !string.IsNullOrEmpty(workspace);
|
var hasWorkspace = !string.IsNullOrEmpty(workspace);
|
||||||
var includeMemory = useMemory && _memoryService != null && !string.IsNullOrEmpty(agentId);
|
var includeMemory = useMemory && _memoryService != null && !string.IsNullOrEmpty(agentId);
|
||||||
@@ -1520,7 +1524,7 @@ public class AnthropicService
|
|||||||
var tools = new List<object>();
|
var tools = new List<object>();
|
||||||
if (hasTools)
|
if (hasTools)
|
||||||
{
|
{
|
||||||
var allTools = _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: allowUserChoice);
|
var allTools = _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: allowUserChoice, includeRemote: includeRemote);
|
||||||
if (planMode)
|
if (planMode)
|
||||||
allTools = _toolService.FilterReadOnlyTools(allTools);
|
allTools = _toolService.FilterReadOnlyTools(allTools);
|
||||||
tools.AddRange(allTools);
|
tools.AddRange(allTools);
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ public class ChatService
|
|||||||
/// <summary>
|
/// <summary>
|
||||||
/// Baut den vollständigen ChatContext auf: Agent, Model, SystemPrompt, Chain, Workspace etc.
|
/// Baut den vollständigen ChatContext auf: Agent, Model, SystemPrompt, Chain, Workspace etc.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public async Task<ChatContext> PrepareContext(string sessionId, string userId, int maxIterations = 100, bool planMode = false)
|
public async Task<ChatContext> PrepareContext(string sessionId, string userId, int maxIterations = 100, bool planMode = false, bool hasRemoteConnection = false)
|
||||||
{
|
{
|
||||||
var session = await _db.GetSession(sessionId);
|
var session = await _db.GetSession(sessionId);
|
||||||
if (session == null || (userId != null && session.UserId != userId) || string.IsNullOrEmpty(session.AgentId))
|
if (session == null || (userId != null && session.UserId != userId) || string.IsNullOrEmpty(session.AgentId))
|
||||||
@@ -80,7 +80,7 @@ public class ChatService
|
|||||||
var toolChoice = hasTools ? "any" : null;
|
var toolChoice = hasTools ? "any" : null;
|
||||||
|
|
||||||
var toolDefs = hasTools
|
var toolDefs = hasTools
|
||||||
? _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: agent.AllowUserChoice)
|
? _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: agent.AllowUserChoice, includeRemote: hasRemoteConnection)
|
||||||
: Array.Empty<object>();
|
: Array.Empty<object>();
|
||||||
var toolNames = toolDefs.Length > 0
|
var toolNames = toolDefs.Length > 0
|
||||||
? JsonSerializer.Serialize(toolDefs.Select(td =>
|
? JsonSerializer.Serialize(toolDefs.Select(td =>
|
||||||
|
|||||||
@@ -0,0 +1,145 @@
|
|||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Linq;
|
||||||
|
using System.Net.WebSockets;
|
||||||
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
|
namespace Gott.Services;
|
||||||
|
|
||||||
|
public class RemoteConnection
|
||||||
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
|
public string Hostname { get; set; }
|
||||||
|
public string Username { get; set; }
|
||||||
|
public string OsVersion { get; set; }
|
||||||
|
public DateTime ConnectedAt { get; set; }
|
||||||
|
public string AssignedSessionId { get; set; }
|
||||||
|
public WebSocket Socket { get; set; }
|
||||||
|
|
||||||
|
// Für synchrone Command-Execution: ein Befehl wird gesendet, Ergebnis abgewartet
|
||||||
|
public SemaphoreSlim CommandLock { get; } = new SemaphoreSlim(1, 1);
|
||||||
|
public TaskCompletionSource<RemoteCommandResult> PendingResult { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RemoteCommandResult
|
||||||
|
{
|
||||||
|
public int ExitCode { get; set; }
|
||||||
|
public string Output { get; set; }
|
||||||
|
public string Error { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
public class RemoteConnectionManager
|
||||||
|
{
|
||||||
|
private readonly ConcurrentDictionary<string, RemoteConnection> _connections = new();
|
||||||
|
|
||||||
|
public RemoteConnection Register(WebSocket socket)
|
||||||
|
{
|
||||||
|
var conn = new RemoteConnection
|
||||||
|
{
|
||||||
|
Id = Guid.NewGuid().ToString("N")[..12],
|
||||||
|
ConnectedAt = DateTime.UtcNow,
|
||||||
|
Socket = socket
|
||||||
|
};
|
||||||
|
_connections[conn.Id] = conn;
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Unregister(string connectionId)
|
||||||
|
{
|
||||||
|
if (_connections.TryRemove(connectionId, out var conn))
|
||||||
|
{
|
||||||
|
// Falls ein Befehl auf Antwort wartet, abbrechen
|
||||||
|
conn.PendingResult?.TrySetCanceled();
|
||||||
|
conn.AssignedSessionId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<RemoteConnection> GetAll()
|
||||||
|
{
|
||||||
|
return _connections.Values.ToList();
|
||||||
|
}
|
||||||
|
|
||||||
|
public RemoteConnection GetById(string connectionId)
|
||||||
|
{
|
||||||
|
_connections.TryGetValue(connectionId, out var conn);
|
||||||
|
return conn;
|
||||||
|
}
|
||||||
|
|
||||||
|
public RemoteConnection GetBySessionId(string sessionId)
|
||||||
|
{
|
||||||
|
return _connections.Values.FirstOrDefault(c => c.AssignedSessionId == sessionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Assign(string connectionId, string sessionId)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(connectionId, out var conn)) return false;
|
||||||
|
conn.AssignedSessionId = sessionId;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool Unassign(string connectionId)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(connectionId, out var conn)) return false;
|
||||||
|
conn.AssignedSessionId = null;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Sendet einen Befehl an den Remote-Client und wartet auf das Ergebnis.
|
||||||
|
/// </summary>
|
||||||
|
public async Task<RemoteCommandResult> ExecuteCommand(string connectionId, string command, int timeoutSeconds = 180, CancellationToken ct = default)
|
||||||
|
{
|
||||||
|
if (!_connections.TryGetValue(connectionId, out var conn))
|
||||||
|
return new RemoteCommandResult { ExitCode = -1, Error = "Verbindung nicht gefunden" };
|
||||||
|
|
||||||
|
if (conn.Socket.State != WebSocketState.Open)
|
||||||
|
return new RemoteCommandResult { ExitCode = -1, Error = "WebSocket nicht verbunden" };
|
||||||
|
|
||||||
|
await conn.CommandLock.WaitAsync(ct);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
conn.PendingResult = new TaskCompletionSource<RemoteCommandResult>();
|
||||||
|
|
||||||
|
// Befehl senden
|
||||||
|
var msg = JsonSerializer.Serialize(new { type = "execute", command });
|
||||||
|
var bytes = Encoding.UTF8.GetBytes(msg);
|
||||||
|
await conn.Socket.SendAsync(bytes, WebSocketMessageType.Text, true, ct);
|
||||||
|
|
||||||
|
// Auf Ergebnis warten (mit Timeout)
|
||||||
|
using var cts = CancellationTokenSource.CreateLinkedTokenSource(ct);
|
||||||
|
cts.CancelAfter(TimeSpan.FromSeconds(timeoutSeconds));
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var registration = cts.Token.Register(() => conn.PendingResult.TrySetCanceled());
|
||||||
|
var result = await conn.PendingResult.Task;
|
||||||
|
registration.Dispose();
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
catch (TaskCanceledException)
|
||||||
|
{
|
||||||
|
return new RemoteCommandResult { ExitCode = -1, Error = $"Timeout nach {timeoutSeconds} Sekunden" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
conn.PendingResult = null;
|
||||||
|
conn.CommandLock.Release();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Wird vom WebSocket-Receiver aufgerufen wenn ein Ergebnis vom Client kommt.
|
||||||
|
/// </summary>
|
||||||
|
public void HandleResult(string connectionId, RemoteCommandResult result)
|
||||||
|
{
|
||||||
|
if (_connections.TryGetValue(connectionId, out var conn))
|
||||||
|
{
|
||||||
|
conn.PendingResult?.TrySetResult(result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+47
-3
@@ -34,7 +34,7 @@ public class ToolService
|
|||||||
{
|
{
|
||||||
"read_file", "list_files", "glob", "grep", "execute", "todo_write",
|
"read_file", "list_files", "glob", "grep", "execute", "todo_write",
|
||||||
"memory_read", "memory_write", "memory_list",
|
"memory_read", "memory_write", "memory_list",
|
||||||
AskUserChoiceToolName
|
"remote_execute", AskUserChoiceToolName
|
||||||
};
|
};
|
||||||
|
|
||||||
public object[] FilterReadOnlyTools(object[] tools)
|
public object[] FilterReadOnlyTools(object[] tools)
|
||||||
@@ -54,7 +54,7 @@ public class ToolService
|
|||||||
return filtered.ToArray();
|
return filtered.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
public object[] GetToolDefinitions(bool includeFileTools = false, bool includeMemory = false, bool includeUserChoice = true)
|
public object[] GetToolDefinitions(bool includeFileTools = false, bool includeMemory = false, bool includeUserChoice = true, bool includeRemote = false)
|
||||||
{
|
{
|
||||||
var allTools = new List<object>();
|
var allTools = new List<object>();
|
||||||
if (includeUserChoice)
|
if (includeUserChoice)
|
||||||
@@ -63,6 +63,8 @@ public class ToolService
|
|||||||
allTools.AddRange(GetFileTools());
|
allTools.AddRange(GetFileTools());
|
||||||
if (includeMemory)
|
if (includeMemory)
|
||||||
allTools.AddRange(GetMemoryToolDefinitions());
|
allTools.AddRange(GetMemoryToolDefinitions());
|
||||||
|
if (includeRemote)
|
||||||
|
allTools.Add(GetRemoteExecuteTool());
|
||||||
return allTools.ToArray();
|
return allTools.ToArray();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -262,6 +264,24 @@ public class ToolService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private object GetRemoteExecuteTool()
|
||||||
|
{
|
||||||
|
return new
|
||||||
|
{
|
||||||
|
name = "remote_execute",
|
||||||
|
description = "Führt einen PowerShell-Befehl auf der verbundenen Remote-Windows-Workstation aus. Gibt stdout und stderr zurück. Nutze dies für Diagnose und Administration des Remote-PCs (z.B. Druckerprobleme, Netzwerk, Services, Registry). Timeout: 180 Sekunden.",
|
||||||
|
input_schema = new
|
||||||
|
{
|
||||||
|
type = "object",
|
||||||
|
properties = new Dictionary<string, object>
|
||||||
|
{
|
||||||
|
["command"] = new { type = "string", description = "PowerShell-Befehl zum Ausführen auf dem Remote-PC" }
|
||||||
|
},
|
||||||
|
required = new[] { "command" }
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private object[] GetMemoryToolDefinitions()
|
private object[] GetMemoryToolDefinitions()
|
||||||
{
|
{
|
||||||
return new object[]
|
return new object[]
|
||||||
@@ -313,7 +333,7 @@ public class ToolService
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<string> ExecuteTool(string name, JsonElement input, string workspace, MemoryService memoryService = null, string agentId = null, CancellationToken cancellationToken = default)
|
public async Task<string> ExecuteTool(string name, JsonElement input, string workspace, MemoryService memoryService = null, string agentId = null, CancellationToken cancellationToken = default, RemoteConnectionManager remoteMgr = null, string sessionId = null)
|
||||||
{
|
{
|
||||||
switch (name)
|
switch (name)
|
||||||
{
|
{
|
||||||
@@ -597,6 +617,30 @@ public class ToolService
|
|||||||
var files = memoryService.ListMemoryFiles(agentId, subPath);
|
var files = memoryService.ListMemoryFiles(agentId, subPath);
|
||||||
return files.Count == 0 ? "(keine Memory-Dateien vorhanden)" : string.Join("\n", files);
|
return files.Count == 0 ? "(keine Memory-Dateien vorhanden)" : string.Join("\n", files);
|
||||||
}
|
}
|
||||||
|
case "remote_execute":
|
||||||
|
{
|
||||||
|
if (remoteMgr == null || string.IsNullOrEmpty(sessionId))
|
||||||
|
return "Fehler: Keine Remote-Verbindung verfügbar";
|
||||||
|
var conn = remoteMgr.GetBySessionId(sessionId);
|
||||||
|
if (conn == null)
|
||||||
|
return "Fehler: Keine Remote-Verbindung für diese Session zugeordnet";
|
||||||
|
var command = input.GetProperty("command").GetString() ?? "";
|
||||||
|
var result = await remoteMgr.ExecuteCommand(conn.Id, command, ExecuteTimeoutSeconds, cancellationToken);
|
||||||
|
var sb = new StringBuilder();
|
||||||
|
if (!string.IsNullOrEmpty(result.Output))
|
||||||
|
sb.Append(result.Output);
|
||||||
|
if (!string.IsNullOrEmpty(result.Error))
|
||||||
|
{
|
||||||
|
if (sb.Length > 0) sb.AppendLine();
|
||||||
|
sb.Append("[stderr] ").Append(result.Error);
|
||||||
|
}
|
||||||
|
if (result.ExitCode != 0)
|
||||||
|
{
|
||||||
|
if (sb.Length > 0) sb.AppendLine();
|
||||||
|
sb.Append($"[Exit Code: {result.ExitCode}]");
|
||||||
|
}
|
||||||
|
return sb.Length > 0 ? sb.ToString() : "(keine Ausgabe)";
|
||||||
|
}
|
||||||
default:
|
default:
|
||||||
return $"Fehler: Unbekanntes Tool: {name}";
|
return $"Fehler: Unbekanntes Tool: {name}";
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -358,6 +358,7 @@
|
|||||||
<div class="header-menu">
|
<div class="header-menu">
|
||||||
<button class="header-menu-trigger icon-btn" id="chatMenuBtn"><i class="fa fa-ellipsis-v"></i></button>
|
<button class="header-menu-trigger icon-btn" id="chatMenuBtn"><i class="fa fa-ellipsis-v"></i></button>
|
||||||
<div class="header-menu-dropdown" id="chatMenuDropdown" hidden>
|
<div class="header-menu-dropdown" id="chatMenuDropdown" hidden>
|
||||||
|
<button class="header-menu-item" id="remoteBtn"><i class="fa fa-desktop"></i> Remote-Verbindungen</button>
|
||||||
<button class="header-menu-item" id="systemPromptBtn">System-Prompt anzeigen</button>
|
<button class="header-menu-item" id="systemPromptBtn">System-Prompt anzeigen</button>
|
||||||
<button class="header-menu-item" id="archiveBtn">Archivieren</button>
|
<button class="header-menu-item" id="archiveBtn">Archivieren</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -629,6 +629,31 @@ async function apiImportOpenclawSession(openclawSessionId, sessionId) {
|
|||||||
return resp.json();
|
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 ──
|
// ── Backup API ──
|
||||||
|
|
||||||
async function apiBackupDb() {
|
async function apiBackupDb() {
|
||||||
|
|||||||
@@ -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
|
// Event-Delegation für Krea-Thumbnails im Chat
|
||||||
document.addEventListener('click', function(e) {
|
document.addEventListener('click', function(e) {
|
||||||
var ref = e.target.closest('.krea-ref');
|
var ref = e.target.closest('.krea-ref');
|
||||||
|
|||||||
@@ -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
|
// System-Prompt anzeigen
|
||||||
const systemPromptBtn = document.getElementById('systemPromptBtn');
|
const systemPromptBtn = document.getElementById('systemPromptBtn');
|
||||||
if (systemPromptBtn) systemPromptBtn.addEventListener('click', () => {
|
if (systemPromptBtn) systemPromptBtn.addEventListener('click', () => {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
// ── Tool Call Visibility ──
|
// ── 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 = {
|
const TOOL_DISPLAY_NAMES = {
|
||||||
write_file: 'Datei schreiben',
|
write_file: 'Datei schreiben',
|
||||||
edit_file: 'Datei bearbeiten',
|
edit_file: 'Datei bearbeiten',
|
||||||
delete_file: 'Datei löschen',
|
delete_file: 'Datei löschen',
|
||||||
memory_write: 'Erinnerungen festhalten',
|
memory_write: 'Erinnerungen festhalten',
|
||||||
web_search: 'Web-Suche'
|
web_search: 'Web-Suche',
|
||||||
|
remote_execute: 'Remote-Befehl'
|
||||||
};
|
};
|
||||||
|
|
||||||
function isToolVisible(toolName) {
|
function isToolVisible(toolName) {
|
||||||
|
|||||||
@@ -0,0 +1,164 @@
|
|||||||
|
# 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 ""
|
||||||
|
|
||||||
|
try {
|
||||||
|
$ws = New-Object System.Net.WebSockets.ClientWebSocket
|
||||||
|
$ct = New-Object System.Threading.CancellationToken
|
||||||
|
|
||||||
|
# Verbindung aufbauen
|
||||||
|
$ws.ConnectAsync([Uri]$Url, $ct).GetAwaiter().GetResult()
|
||||||
|
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
|
||||||
|
|
||||||
|
$helloBytes = [System.Text.Encoding]::UTF8.GetBytes($hello)
|
||||||
|
$segment = New-Object System.ArraySegment[byte] -ArgumentList @(,$helloBytes)
|
||||||
|
$ws.SendAsync($segment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, $ct).GetAwaiter().GetResult()
|
||||||
|
|
||||||
|
Write-Host " Host: $username@$hostname ($osVersion)" -ForegroundColor Gray
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Warte auf Befehle... (Strg+C zum Beenden)" -ForegroundColor Yellow
|
||||||
|
Write-Host ""
|
||||||
|
|
||||||
|
# Empfangs-Loop
|
||||||
|
$buffer = New-Object byte[] 65536
|
||||||
|
$messageBuffer = New-Object System.IO.MemoryStream
|
||||||
|
|
||||||
|
# Heartbeat-Timer (alle 30 Sekunden)
|
||||||
|
$lastPing = [DateTime]::UtcNow
|
||||||
|
|
||||||
|
while ($ws.State -eq [System.Net.WebSockets.WebSocketState]::Open) {
|
||||||
|
# Heartbeat senden
|
||||||
|
if (([DateTime]::UtcNow - $lastPing).TotalSeconds -gt 30) {
|
||||||
|
$pingBytes = [System.Text.Encoding]::UTF8.GetBytes('{"type":"ping"}')
|
||||||
|
$pingSegment = New-Object System.ArraySegment[byte] -ArgumentList @(,$pingBytes)
|
||||||
|
try {
|
||||||
|
$ws.SendAsync($pingSegment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, $ct).GetAwaiter().GetResult()
|
||||||
|
} catch {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
$lastPing = [DateTime]::UtcNow
|
||||||
|
}
|
||||||
|
|
||||||
|
# Auf Nachricht warten (mit Timeout fuer Heartbeat)
|
||||||
|
$receiveTask = $ws.ReceiveAsync((New-Object System.ArraySegment[byte] -ArgumentList @(,$buffer)), $ct)
|
||||||
|
if (-not $receiveTask.Wait(5000)) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
$result = $receiveTask.Result
|
||||||
|
|
||||||
|
if ($result.MessageType -eq [System.Net.WebSockets.WebSocketMessageType]::Close) {
|
||||||
|
Write-Host " Server hat Verbindung geschlossen." -ForegroundColor Yellow
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
$messageBuffer.SetLength(0)
|
||||||
|
$messageBuffer.Write($buffer, 0, $result.Count)
|
||||||
|
|
||||||
|
# Mehrteilige Nachrichten zusammenbauen
|
||||||
|
while (-not $result.EndOfMessage) {
|
||||||
|
$result = $ws.ReceiveAsync((New-Object System.ArraySegment[byte] -ArgumentList @(,$buffer)), $ct).GetAwaiter().GetResult()
|
||||||
|
$messageBuffer.Write($buffer, 0, $result.Count)
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = [System.Text.Encoding]::UTF8.GetString($messageBuffer.ToArray())
|
||||||
|
$msg = $json | ConvertFrom-Json
|
||||||
|
|
||||||
|
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 {
|
||||||
|
# PowerShell-Befehl ausfuehren
|
||||||
|
$output = & powershell.exe -NoProfile -NonInteractive -Command $command 2>&1
|
||||||
|
$stdout = ($output | Where-Object { $_ -isnot [System.Management.Automation.ErrorRecord] }) -join "`n"
|
||||||
|
$stderrLines = ($output | Where-Object { $_ -is [System.Management.Automation.ErrorRecord] }) | ForEach-Object { $_.ToString() }
|
||||||
|
$stderr = $stderrLines -join "`n"
|
||||||
|
$exitCode = $LASTEXITCODE
|
||||||
|
if ($null -eq $exitCode) { $exitCode = 0 }
|
||||||
|
} catch {
|
||||||
|
$stderr = $_.Exception.Message
|
||||||
|
$exitCode = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
# Ergebnis senden
|
||||||
|
$response = @{
|
||||||
|
type = "result"
|
||||||
|
exitCode = $exitCode
|
||||||
|
output = if ($stdout) { $stdout } else { "" }
|
||||||
|
error = if ($stderr) { $stderr } else { "" }
|
||||||
|
} | ConvertTo-Json -Compress
|
||||||
|
|
||||||
|
$responseBytes = [System.Text.Encoding]::UTF8.GetBytes($response)
|
||||||
|
$responseSegment = New-Object System.ArraySegment[byte] -ArgumentList @(,$responseBytes)
|
||||||
|
$ws.SendAsync($responseSegment, [System.Net.WebSockets.WebSocketMessageType]::Text, $true, $ct).GetAwaiter().GetResult()
|
||||||
|
|
||||||
|
# Kurzausgabe
|
||||||
|
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 ""
|
||||||
|
}
|
||||||
|
"pong" {
|
||||||
|
# Heartbeat-Antwort – ignorieren
|
||||||
|
}
|
||||||
|
default {
|
||||||
|
Write-Host " Unbekannte Nachricht: $($msg.type)" -ForegroundColor Yellow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch [System.Net.WebSockets.WebSocketException] {
|
||||||
|
Write-Host " Verbindung verloren." -ForegroundColor Red
|
||||||
|
} catch {
|
||||||
|
Write-Host " Fehler: $_" -ForegroundColor Red
|
||||||
|
} finally {
|
||||||
|
if ($ws -and $ws.State -eq [System.Net.WebSockets.WebSocketState]::Open) {
|
||||||
|
try {
|
||||||
|
$ws.CloseAsync([System.Net.WebSockets.WebSocketCloseStatus]::NormalClosure, "bye", $ct).GetAwaiter().GetResult()
|
||||||
|
} catch { }
|
||||||
|
}
|
||||||
|
if ($ws) { $ws.Dispose() }
|
||||||
|
Write-Host ""
|
||||||
|
Write-Host " Verbindung beendet." -ForegroundColor Yellow
|
||||||
|
}
|
||||||
Reference in new issue
Block a user