feat: Windows-Kompatibilität

- WorkspaceHelper: Zentrale Hilfsmethoden (IsWindows, TempDir, IsSubPathOf,
  GetRelativePath, ShellExecutable, ShellArguments)
- ToolService: execute nutzt cmd.exe auf Windows, GetRelativePath mit
  Forward-Slashes, IsSubPathOf für case-insensitive Pfadprüfung
- SessionEndpoint: /tmp → Path.GetTempPath(), bash → cmd.exe,
  Fire-and-forget via .bat statt systemd-run auf Windows
- ChatService: /tmp → WorkspaceHelper.TempDir
- MemoryService: IsSubPathOf + GetRelativePath
- WorkspaceEndpoint: HandleBrowse zeigt Laufwerke auf Windows,
  IsSubPathOf statt StartsWith
- InfoEndpoint: gott.exe auf Windows, isWindows-Flag in API-Response
- Frontend: File-Explorer mit Laufwerks-Navigation auf Windows
This commit is contained in:
Meleki committed 2026-03-09 11:35:04 +01:00
1 parent 79899a6981
commit 5d090e160b
9 files changed
+155 -61

No files matched your search

+4 -2
View File
@@ -29,7 +29,8 @@ public static class InfoEndpoint
else
{
var dir = AppContext.BaseDirectory;
var exePath = Path.Combine(dir, "gott");
var exeName = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows) ? "gott.exe" : "gott";
var exePath = Path.Combine(dir, exeName);
if (File.Exists(exePath))
{
var lastWrite = File.GetLastWriteTimeUtc(exePath);
@@ -37,6 +38,7 @@ public static class InfoEndpoint
}
}
return Results.Json(new { buildTime });
var isWindows = System.Runtime.InteropServices.RuntimeInformation.IsOSPlatform(System.Runtime.InteropServices.OSPlatform.Windows);
return Results.Json(new { buildTime, isWindows });
}
}
+43 -19
View File
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading;
@@ -234,7 +235,7 @@ public static class SessionEndpoint
var user = await db.GetUser(userId);
var workspace = WorkspaceHelper.ResolveWorkspace(session, user);
if (string.IsNullOrEmpty(workspace) && skills.Any())
workspace = "/tmp";
workspace = WorkspaceHelper.TempDir;
var planMode = ctx.Request.Query.ContainsKey("planMode") && ctx.Request.Query["planMode"] == "true";
return (session, agent, marked, memDir, skills, user, workspace, planMode);
@@ -317,7 +318,7 @@ public static class SessionEndpoint
var user = await db.GetUser(userId);
var resolvedWs = WorkspaceHelper.ResolveWorkspace(session, user);
var workingDir = !string.IsNullOrWhiteSpace(resolvedWs) ? resolvedWs : "/tmp";
var workingDir = !string.IsNullOrWhiteSpace(resolvedWs) ? resolvedWs : WorkspaceHelper.TempDir;
if (mode == "foreground")
{
@@ -326,14 +327,16 @@ public static class SessionEndpoint
{
var psi = new ProcessStartInfo
{
FileName = "/bin/bash",
FileName = WorkspaceHelper.ShellExecutable,
WorkingDirectory = workingDir,
RedirectStandardInput = true,
RedirectStandardInput = !WorkspaceHelper.IsWindows,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
};
if (WorkspaceHelper.IsWindows)
psi.Arguments = WorkspaceHelper.ShellArguments(command);
using var proc = System.Diagnostics.Process.Start(psi);
if (proc == null)
@@ -343,8 +346,11 @@ public static class SessionEndpoint
return;
}
await proc.StandardInput.WriteAsync(command);
proc.StandardInput.Close();
if (!WorkspaceHelper.IsWindows)
{
await proc.StandardInput.WriteAsync(command);
proc.StandardInput.Close();
}
using var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(30));
try
@@ -379,22 +385,40 @@ public static class SessionEndpoint
}
else
{
// Fire-and-forget via systemd-run
// Fire-and-forget
try
{
var scriptPath = $"/tmp/gott-exec-{Guid.NewGuid():N}.sh";
await System.IO.File.WriteAllTextAsync(scriptPath, $"#!/bin/bash\ncd {workingDir}\n{command}\nrm -f {scriptPath}\n");
var psi = new ProcessStartInfo
ProcessStartInfo psi;
if (WorkspaceHelper.IsWindows)
{
FileName = "systemd-run",
Arguments = $"--scope --quiet bash {scriptPath}",
WorkingDirectory = workingDir,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
var scriptPath = Path.Combine(WorkspaceHelper.TempDir, $"gott-exec-{Guid.NewGuid():N}.bat");
await System.IO.File.WriteAllTextAsync(scriptPath, $"@echo off\r\ncd /d \"{workingDir}\"\r\n{command}\r\ndel \"%~f0\"\r\n");
psi = new ProcessStartInfo
{
FileName = "cmd.exe",
Arguments = $"/c \"{scriptPath}\"",
WorkingDirectory = workingDir,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
}
else
{
var scriptPath = Path.Combine(WorkspaceHelper.TempDir, $"gott-exec-{Guid.NewGuid():N}.sh");
await System.IO.File.WriteAllTextAsync(scriptPath, $"#!/bin/bash\ncd {workingDir}\n{command}\nrm -f {scriptPath}\n");
psi = new ProcessStartInfo
{
FileName = "systemd-run",
Arguments = $"--scope --quiet bash {scriptPath}",
WorkingDirectory = workingDir,
UseShellExecute = false,
RedirectStandardOutput = true,
RedirectStandardError = true,
CreateNoWindow = true
};
}
var process = Process.Start(psi);
if (process != null)
+22 -10
View File
@@ -9,6 +9,7 @@ using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Routing;
using Microsoft.Extensions.DependencyInjection;
using Gott.Services;
namespace Gott.Endpoints;
@@ -83,7 +84,7 @@ public static class WorkspaceEndpoint
var userId = ctx.GetUserId();
var user = await db.GetUser(userId);
var resolved = Gott.Services.WorkspaceHelper.ResolveWorkspace(session, user);
var resolved = WorkspaceHelper.ResolveWorkspace(session, user);
if (string.IsNullOrEmpty(resolved))
{
ctx.Response.StatusCode = 400;
@@ -125,7 +126,7 @@ public static class WorkspaceEndpoint
var subPath = ctx.Request.Query["path"].FirstOrDefault() ?? "";
var targetPath = Path.GetFullPath(Path.Combine(basePath, subPath));
if (!targetPath.StartsWith(basePath))
if (!WorkspaceHelper.IsSubPathOf(targetPath, basePath))
{
ctx.Response.StatusCode = 403;
return;
@@ -144,7 +145,7 @@ public static class WorkspaceEndpoint
{
var name = Path.GetFileName(dir);
if (IgnoredDirs.Contains(name)) continue;
var relPath = Path.GetRelativePath(basePath, dir).Replace('\\', '/');
var relPath = WorkspaceHelper.GetRelativePath(basePath, dir);
entries.Add(new { name, path = relPath, type = "dir" });
}
@@ -152,7 +153,7 @@ public static class WorkspaceEndpoint
{
var name = Path.GetFileName(file);
var ext = Path.GetExtension(file);
var relPath = Path.GetRelativePath(basePath, file).Replace('\\', '/');
var relPath = WorkspaceHelper.GetRelativePath(basePath, file);
var size = new FileInfo(file).Length;
var isText = TextExtensions.Contains(ext) || IsLikelyTextFile(name);
entries.Add(new { name, path = relPath, type = "file", ext, size, isText });
@@ -173,7 +174,7 @@ public static class WorkspaceEndpoint
}
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
if (!targetPath.StartsWith(basePath))
if (!WorkspaceHelper.IsSubPathOf(targetPath, basePath))
{
ctx.Response.StatusCode = 403;
return;
@@ -218,7 +219,7 @@ public static class WorkspaceEndpoint
}
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
if (!targetPath.StartsWith(basePath))
if (!WorkspaceHelper.IsSubPathOf(targetPath, basePath))
{
ctx.Response.StatusCode = 403;
return;
@@ -271,7 +272,7 @@ public static class WorkspaceEndpoint
}
var targetPath = Path.GetFullPath(Path.Combine(basePath, dirPath));
if (!targetPath.StartsWith(basePath))
if (!WorkspaceHelper.IsSubPathOf(targetPath, basePath))
{
ctx.Response.StatusCode = 403;
return;
@@ -287,7 +288,7 @@ public static class WorkspaceEndpoint
Directory.CreateDirectory(targetPath);
await ctx.Response.WriteAsJsonAsync(new
{
path = Path.GetRelativePath(basePath, targetPath).Replace('\\', '/'),
path = WorkspaceHelper.GetRelativePath(basePath, targetPath),
created = true
});
}
@@ -296,7 +297,18 @@ public static class WorkspaceEndpoint
private static async Task HandleBrowse(HttpContext ctx)
{
var path = ctx.Request.Query["path"].FirstOrDefault() ?? "/";
var path = ctx.Request.Query["path"].FirstOrDefault() ?? (WorkspaceHelper.IsWindows ? "" : "/");
// Windows: Leerer Pfad oder Root → Laufwerke anzeigen
if (WorkspaceHelper.IsWindows && string.IsNullOrEmpty(path))
{
var drives = System.IO.DriveInfo.GetDrives()
.Where(d => d.IsReady)
.Select(d => new { name = d.Name.TrimEnd('\\'), path = d.Name.Replace('\\', '/'), type = "dir" })
.ToList<object>();
await ctx.Response.WriteAsJsonAsync(new { current = "", entries = drives });
return;
}
var targetPath = Path.GetFullPath(path);
if (!Directory.Exists(targetPath))
@@ -368,7 +380,7 @@ public static class WorkspaceEndpoint
if (basePath == null) return;
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
if (!targetPath.StartsWith(basePath))
if (!WorkspaceHelper.IsSubPathOf(targetPath, basePath))
{
ctx.Response.StatusCode = 403;
return;