feat: File-Explorer für Agent-Ordner im Agent-Formular
This commit is contained in:
1 parent
8e585443e2
commit
58c5ccb79b
4 files changed
+192
-4
No files matched your search
@@ -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
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
);
|
||||
|
||||
|
||||
@@ -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));
|
||||
|
||||
|
||||
Reference in new issue
Block a user