From 58c5ccb79b757da35109702cbd962b8476fec29c Mon Sep 17 00:00:00 2001 From: Meleki Date: Sun, 1 Mar 2026 15:48:13 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20File-Explorer=20f=C3=BCr=20Agent-Ordner?= =?UTF-8?q?=20im=20Agent-Formular?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Endpoints/WorkspaceEndpoint.cs | 168 +++++++++++++++++++++++++++++++++ wwwroot/index.html | 4 + wwwroot/js/file-explorer.js | 16 +++- wwwroot/js/pages/agent-page.js | 8 ++ 4 files changed, 192 insertions(+), 4 deletions(-) diff --git a/Endpoints/WorkspaceEndpoint.cs b/Endpoints/WorkspaceEndpoint.cs index 72375ed..afa9281 100644 --- a/Endpoints/WorkspaceEndpoint.cs +++ b/Endpoints/WorkspaceEndpoint.cs @@ -40,6 +40,9 @@ public static class WorkspaceEndpoint app.MapGet("/api/workspace/file", HandleFile); app.MapPut("/api/workspace/file", HandleFileSave); app.MapGet("/api/workspace/browse", HandleBrowse); + app.MapGet("/api/workspace/agent/tree", HandleAgentTree); + app.MapGet("/api/workspace/agent/file", HandleAgentFile); + app.MapPut("/api/workspace/agent/file", HandleAgentFileSave); } private static async Task HandleTree(HttpContext ctx) @@ -253,4 +256,169 @@ public static class WorkspaceEndpoint or "procfile" or "vagrantfile" or "license" or "readme" or "changelog" or "authors" or "contributors"; } + + private static string GetAgentBasePath(HttpContext ctx, out string error) + { + error = null; + var agentId = ctx.Request.Query["agentId"].FirstOrDefault(); + if (string.IsNullOrEmpty(agentId)) + { + error = "agentId fehlt"; + return null; + } + + var memoryService = ctx.RequestServices.GetRequiredService(); + var basePath = memoryService.GetAgentMemoryDir(agentId); + + if (!Directory.Exists(basePath)) + Directory.CreateDirectory(basePath); + + return basePath; + } + + private static async Task HandleAgentTree(HttpContext ctx) + { + var basePath = GetAgentBasePath(ctx, out var error); + if (basePath == null) + { + ctx.Response.StatusCode = 400; + await ctx.Response.WriteAsJsonAsync(new { error }); + return; + } + + var subPath = ctx.Request.Query["path"].FirstOrDefault() ?? ""; + var targetPath = Path.GetFullPath(Path.Combine(basePath, subPath)); + + if (!targetPath.StartsWith(basePath)) + { + ctx.Response.StatusCode = 403; + return; + } + + if (!Directory.Exists(targetPath)) + { + ctx.Response.StatusCode = 404; + await ctx.Response.WriteAsJsonAsync(new { error = "Verzeichnis nicht gefunden" }); + return; + } + + var entries = new List(); + + foreach (var dir in Directory.GetDirectories(targetPath).OrderBy(d => Path.GetFileName(d), StringComparer.OrdinalIgnoreCase)) + { + var name = Path.GetFileName(dir); + if (IgnoredDirs.Contains(name)) continue; + var relPath = Path.GetRelativePath(basePath, dir).Replace('\\', '/'); + entries.Add(new { name, path = relPath, type = "dir" }); + } + + foreach (var file in Directory.GetFiles(targetPath).OrderBy(f => Path.GetFileName(f), StringComparer.OrdinalIgnoreCase)) + { + var name = Path.GetFileName(file); + var ext = Path.GetExtension(file); + var relPath = Path.GetRelativePath(basePath, file).Replace('\\', '/'); + var size = new FileInfo(file).Length; + var isText = TextExtensions.Contains(ext) || IsLikelyTextFile(name); + entries.Add(new { name, path = relPath, type = "file", ext, size, isText }); + } + + await ctx.Response.WriteAsJsonAsync(entries); + } + + private static async Task HandleAgentFile(HttpContext ctx) + { + var basePath = GetAgentBasePath(ctx, out var error); + if (basePath == null) + { + ctx.Response.StatusCode = 400; + await ctx.Response.WriteAsJsonAsync(new { error }); + return; + } + + var filePath = ctx.Request.Query["path"].FirstOrDefault(); + if (string.IsNullOrEmpty(filePath)) + { + ctx.Response.StatusCode = 400; + return; + } + + var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath)); + if (!targetPath.StartsWith(basePath)) + { + ctx.Response.StatusCode = 403; + return; + } + + if (!File.Exists(targetPath)) + { + ctx.Response.StatusCode = 404; + return; + } + + var info = new FileInfo(targetPath); + if (info.Length > 2 * 1024 * 1024) + { + ctx.Response.StatusCode = 413; + await ctx.Response.WriteAsJsonAsync(new { error = "Datei zu groß (max. 2MB)" }); + return; + } + + var content = await File.ReadAllTextAsync(targetPath); + var lineCount = content.Split('\n').Length; + + await ctx.Response.WriteAsJsonAsync(new + { + path = filePath, + name = info.Name, + size = info.Length, + lineCount, + content + }); + } + + private static async Task HandleAgentFileSave(HttpContext ctx) + { + var basePath = GetAgentBasePath(ctx, out var error); + if (basePath == null) + { + ctx.Response.StatusCode = 400; + await ctx.Response.WriteAsJsonAsync(new { error }); + return; + } + + var filePath = ctx.Request.Query["path"].FirstOrDefault(); + if (string.IsNullOrEmpty(filePath)) + { + ctx.Response.StatusCode = 400; + return; + } + + var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath)); + if (!targetPath.StartsWith(basePath)) + { + ctx.Response.StatusCode = 403; + return; + } + + // Verzeichnis erstellen falls nötig + var dir = Path.GetDirectoryName(targetPath); + if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir)) + Directory.CreateDirectory(dir); + + using var reader = new StreamReader(ctx.Request.Body); + var content = await reader.ReadToEndAsync(); + + await File.WriteAllTextAsync(targetPath, content); + + var info = new FileInfo(targetPath); + var lineCount = content.Split('\n').Length; + + await ctx.Response.WriteAsJsonAsync(new + { + path = filePath, + name = info.Name, + size = info.Length, + lineCount + }); + } } diff --git a/wwwroot/index.html b/wwwroot/index.html index 9dbea89..ac1bd43 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -141,6 +141,10 @@ Auf Erinnerungen zugreifen (Memory-Tools verfügbar) +
diff --git a/wwwroot/js/file-explorer.js b/wwwroot/js/file-explorer.js index 2907e84..cf10ce9 100644 --- a/wwwroot/js/file-explorer.js +++ b/wwwroot/js/file-explorer.js @@ -4,11 +4,19 @@ let _feState = null; +function _feApiUrl(action, params = '') { + if (_feState && _feState.agentId) { + return `/api/workspace/agent/${action}?agentId=${_feState.agentId}${params ? '&' + params : ''}`; + } + return `/api/workspace/${action}?sessionId=${_feState.sessionId}${params ? '&' + params : ''}`; +} + function openFileExplorer(opts = {}) { if (_feState) closeFileExplorer(); _feState = { - sessionId: opts.sessionId || getCurrentSessionId(), + sessionId: opts.sessionId || (!opts.agentId ? getCurrentSessionId() : null), + agentId: opts.agentId || null, onSelect: opts.onSelect || null, currentPath: '', history: [], @@ -102,7 +110,7 @@ async function feLoadDir(path) { document.getElementById('feFooter').hidden = true; try { - const resp = await fetch(`/api/workspace/tree?sessionId=${_feState.sessionId}&path=${encodeURIComponent(path)}`); + const resp = await fetch(_feApiUrl('tree', `path=${encodeURIComponent(path)}`)); if (!resp.ok) { const err = await resp.json().catch(() => ({})); sidebar.innerHTML = `
${err.error || 'Fehler'}
`; @@ -206,7 +214,7 @@ async function feSelectFile(entry, itemEl) { main.innerHTML = '
Laden...
'; try { - const resp = await fetch(`/api/workspace/file?sessionId=${_feState.sessionId}&path=${encodeURIComponent(entry.path)}`); + const resp = await fetch(_feApiUrl('file', `path=${encodeURIComponent(entry.path)}`)); if (!resp.ok) { main.innerHTML = '
Fehler beim Laden
'; return; @@ -431,7 +439,7 @@ async function feDoSave() { try { const resp = await fetch( - `/api/workspace/file?sessionId=${_feState.sessionId}&path=${encodeURIComponent(path)}`, + _feApiUrl('file', `path=${encodeURIComponent(path)}`), { method: 'PUT', body: content } ); diff --git a/wwwroot/js/pages/agent-page.js b/wwwroot/js/pages/agent-page.js index 0867532..1b23d4f 100644 --- a/wwwroot/js/pages/agent-page.js +++ b/wwwroot/js/pages/agent-page.js @@ -33,6 +33,8 @@ registerPage('agent', { const deleteBtn = document.getElementById('agentDeleteBtn'); const commandsList = document.getElementById('agentCommandsList'); const addCommandBtn = document.getElementById('agentAddCommandBtn'); + const filesSection = document.getElementById('agentFilesSection'); + const filesBtn = document.getElementById('agentFilesBtn'); _agentAvatarData = null; @@ -49,6 +51,7 @@ registerPage('agent', { useMemoryEl.checked = agent.useMemory !== false; _agentAvatarData = agent.avatar || null; deleteBtn.hidden = false; + filesSection.hidden = false; if (agent.avatar) { avatarPreview.innerHTML = `Avatar`; @@ -58,6 +61,11 @@ registerPage('agent', { loadCommandsIntoUI(commandsList, agent.customCommands); } + // Agent-Dateien öffnen + filesBtn.addEventListener('click', () => { + openFileExplorer({ agentId: agentId }); + }); + // Befehl hinzufügen addCommandBtn.addEventListener('click', () => createCommandRow(commandsList));