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

+47 -5
View File
@@ -1,11 +1,53 @@
using System;
using System.IO;
using System.Runtime.InteropServices;
using Gott.Models;
namespace Gott.Services;
public static class WorkspaceHelper
{
/// <summary>True wenn die Plattform Windows ist.</summary>
internal static bool IsWindows => RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
/// <summary>Plattformunabhängiges Temp-Verzeichnis ohne Trailing-Separator.</summary>
internal static string TempDir => Path.GetTempPath().TrimEnd(Path.DirectorySeparatorChar);
/// <summary>
/// Prüft ob ein Pfad ein Sub-Pfad eines Basis-Pfads ist (oder gleich).
/// Auf Windows case-insensitive, auf Linux case-sensitive.
/// </summary>
internal static bool IsSubPathOf(string path, string basePath)
{
var comparison = IsWindows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
var normalizedPath = Path.GetFullPath(path);
var normalizedBase = Path.GetFullPath(basePath);
return normalizedPath.Equals(normalizedBase, comparison)
|| normalizedPath.StartsWith(normalizedBase + Path.DirectorySeparatorChar, comparison);
}
/// <summary>
/// Gibt den relativen Pfad zurück – immer mit Forward-Slashes (für Agent-Output/API).
/// </summary>
internal static string GetRelativePath(string relativeTo, string path)
{
var rel = Path.GetRelativePath(relativeTo, path);
return IsWindows ? rel.Replace('\\', '/') : rel;
}
/// <summary>
/// Gibt den Shell-Dateinamen zurück (cmd.exe auf Windows, /bin/bash auf Linux).
/// </summary>
internal static string ShellExecutable => IsWindows ? "cmd.exe" : "/bin/bash";
/// <summary>
/// Gibt die Shell-Argumente zurück um einen Befehl auszuführen.
/// Windows: /c "command", Linux: via stdin.
/// </summary>
internal static string ShellArguments(string command)
=> IsWindows ? $"/c \"{command.Replace("\"", "\\\"")}\"" : "";
/// <summary>
/// Löst den Workspace-Pfad einer Session auf.
/// session.Workspace ist relativ zum user.DefaultPath.
@@ -30,8 +72,7 @@ public static class WorkspaceHelper
var resolved = Path.GetFullPath(Path.Combine(basePath, workspace));
// Sicherheit: darf nicht aus dem DefaultPath ausbrechen
var normalizedBase = Path.GetFullPath(basePath);
if (!resolved.StartsWith(normalizedBase + Path.DirectorySeparatorChar) && resolved != normalizedBase)
if (!IsSubPathOf(resolved, Path.GetFullPath(basePath)))
return null;
return resolved;
@@ -47,7 +88,7 @@ public static class WorkspaceHelper
if (string.IsNullOrWhiteSpace(workspace))
return null;
var trimmed = workspace.Trim().TrimStart('/').TrimEnd('/');
var trimmed = workspace.Trim().TrimStart('/', '\\').TrimEnd('/', '\\');
if (string.IsNullOrEmpty(trimmed))
return null;
@@ -76,11 +117,12 @@ public static class WorkspaceHelper
var normalizedBase = Path.GetFullPath(defaultPath.Trim());
var normalizedWorkspace = Path.GetFullPath(absoluteWorkspace.Trim());
var comparison = IsWindows ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal;
if (normalizedWorkspace.StartsWith(normalizedBase + Path.DirectorySeparatorChar))
if (normalizedWorkspace.StartsWith(normalizedBase + Path.DirectorySeparatorChar, comparison))
return normalizedWorkspace.Substring(normalizedBase.Length + 1);
if (normalizedWorkspace == normalizedBase)
if (normalizedWorkspace.Equals(normalizedBase, comparison))
return "";
return absoluteWorkspace; // Außerhalb des DefaultPath → absoluten Pfad beibehalten (Legacy)