feat: File-Explorer für Agent-Ordner im Agent-Formular

This commit is contained in:
Meleki committed 2026-03-01 15:48:13 +01:00
1 parent 8e585443e2
commit 58c5ccb79b
4 files changed
+192 -4

No files matched your search

+168
View File
@@ -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<Gott.Services.MemoryService>();
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<object>();
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
});
}
}
+4
View File
@@ -141,6 +141,10 @@
<input type="checkbox" id="agentUseMemory" checked>
Auf Erinnerungen zugreifen <span class="checkbox-hint">(Memory-Tools verfügbar)</span>
</label>
<div id="agentFilesSection" hidden>
<label>Agent-Dateien</label>
<button type="button" id="agentFilesBtn" class="add-cmd-btn"><i class="fa fa-folder"></i> Dateien verwalten</button>
</div>
<label>Benutzerdefinierte Befehle</label>
<div class="custom-commands" id="agentCommandsList"></div>
<button type="button" id="agentAddCommandBtn" class="add-cmd-btn">+ Befehl hinzufügen</button>
+12 -4
View File
@@ -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 = `<div class="fe-error">${err.error || 'Fehler'}</div>`;
@@ -206,7 +214,7 @@ async function feSelectFile(entry, itemEl) {
main.innerHTML = '<div class="fe-loading">Laden...</div>';
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 = '<div class="fe-error">Fehler beim Laden</div>';
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 }
);
+8
View File
@@ -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 = `<img src="${agent.avatar}" alt="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));