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.MapGet("/api/workspace/file", HandleFile);
|
||||||
app.MapPut("/api/workspace/file", HandleFileSave);
|
app.MapPut("/api/workspace/file", HandleFileSave);
|
||||||
app.MapGet("/api/workspace/browse", HandleBrowse);
|
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)
|
private static async Task HandleTree(HttpContext ctx)
|
||||||
@@ -253,4 +256,169 @@ public static class WorkspaceEndpoint
|
|||||||
or "procfile" or "vagrantfile" or "license" or "readme"
|
or "procfile" or "vagrantfile" or "license" or "readme"
|
||||||
or "changelog" or "authors" or "contributors";
|
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>
|
<input type="checkbox" id="agentUseMemory" checked>
|
||||||
Auf Erinnerungen zugreifen <span class="checkbox-hint">(Memory-Tools verfügbar)</span>
|
Auf Erinnerungen zugreifen <span class="checkbox-hint">(Memory-Tools verfügbar)</span>
|
||||||
</label>
|
</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>
|
<label>Benutzerdefinierte Befehle</label>
|
||||||
<div class="custom-commands" id="agentCommandsList"></div>
|
<div class="custom-commands" id="agentCommandsList"></div>
|
||||||
<button type="button" id="agentAddCommandBtn" class="add-cmd-btn">+ Befehl hinzufügen</button>
|
<button type="button" id="agentAddCommandBtn" class="add-cmd-btn">+ Befehl hinzufügen</button>
|
||||||
|
|||||||
@@ -4,11 +4,19 @@
|
|||||||
|
|
||||||
let _feState = null;
|
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 = {}) {
|
function openFileExplorer(opts = {}) {
|
||||||
if (_feState) closeFileExplorer();
|
if (_feState) closeFileExplorer();
|
||||||
|
|
||||||
_feState = {
|
_feState = {
|
||||||
sessionId: opts.sessionId || getCurrentSessionId(),
|
sessionId: opts.sessionId || (!opts.agentId ? getCurrentSessionId() : null),
|
||||||
|
agentId: opts.agentId || null,
|
||||||
onSelect: opts.onSelect || null,
|
onSelect: opts.onSelect || null,
|
||||||
currentPath: '',
|
currentPath: '',
|
||||||
history: [],
|
history: [],
|
||||||
@@ -102,7 +110,7 @@ async function feLoadDir(path) {
|
|||||||
document.getElementById('feFooter').hidden = true;
|
document.getElementById('feFooter').hidden = true;
|
||||||
|
|
||||||
try {
|
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) {
|
if (!resp.ok) {
|
||||||
const err = await resp.json().catch(() => ({}));
|
const err = await resp.json().catch(() => ({}));
|
||||||
sidebar.innerHTML = `<div class="fe-error">${err.error || 'Fehler'}</div>`;
|
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>';
|
main.innerHTML = '<div class="fe-loading">Laden...</div>';
|
||||||
|
|
||||||
try {
|
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) {
|
if (!resp.ok) {
|
||||||
main.innerHTML = '<div class="fe-error">Fehler beim Laden</div>';
|
main.innerHTML = '<div class="fe-error">Fehler beim Laden</div>';
|
||||||
return;
|
return;
|
||||||
@@ -431,7 +439,7 @@ async function feDoSave() {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await fetch(
|
const resp = await fetch(
|
||||||
`/api/workspace/file?sessionId=${_feState.sessionId}&path=${encodeURIComponent(path)}`,
|
_feApiUrl('file', `path=${encodeURIComponent(path)}`),
|
||||||
{ method: 'PUT', body: content }
|
{ method: 'PUT', body: content }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@@ -33,6 +33,8 @@ registerPage('agent', {
|
|||||||
const deleteBtn = document.getElementById('agentDeleteBtn');
|
const deleteBtn = document.getElementById('agentDeleteBtn');
|
||||||
const commandsList = document.getElementById('agentCommandsList');
|
const commandsList = document.getElementById('agentCommandsList');
|
||||||
const addCommandBtn = document.getElementById('agentAddCommandBtn');
|
const addCommandBtn = document.getElementById('agentAddCommandBtn');
|
||||||
|
const filesSection = document.getElementById('agentFilesSection');
|
||||||
|
const filesBtn = document.getElementById('agentFilesBtn');
|
||||||
|
|
||||||
_agentAvatarData = null;
|
_agentAvatarData = null;
|
||||||
|
|
||||||
@@ -49,6 +51,7 @@ registerPage('agent', {
|
|||||||
useMemoryEl.checked = agent.useMemory !== false;
|
useMemoryEl.checked = agent.useMemory !== false;
|
||||||
_agentAvatarData = agent.avatar || null;
|
_agentAvatarData = agent.avatar || null;
|
||||||
deleteBtn.hidden = false;
|
deleteBtn.hidden = false;
|
||||||
|
filesSection.hidden = false;
|
||||||
|
|
||||||
if (agent.avatar) {
|
if (agent.avatar) {
|
||||||
avatarPreview.innerHTML = `<img src="${agent.avatar}" alt="Avatar">`;
|
avatarPreview.innerHTML = `<img src="${agent.avatar}" alt="Avatar">`;
|
||||||
@@ -58,6 +61,11 @@ registerPage('agent', {
|
|||||||
loadCommandsIntoUI(commandsList, agent.customCommands);
|
loadCommandsIntoUI(commandsList, agent.customCommands);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Agent-Dateien öffnen
|
||||||
|
filesBtn.addEventListener('click', () => {
|
||||||
|
openFileExplorer({ agentId: agentId });
|
||||||
|
});
|
||||||
|
|
||||||
// Befehl hinzufügen
|
// Befehl hinzufügen
|
||||||
addCommandBtn.addEventListener('click', () => createCommandRow(commandsList));
|
addCommandBtn.addEventListener('click', () => createCommandRow(commandsList));
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user