refactor: Backend deduplizieren – 13 Dateien, -528 Zeilen netto
ChatContext: 3 identische Context-Klassen (PreparerContext, ReviewContext, FinalizerContext) zu einer SubAgentContext-Klasse zusammengefasst. ChatService: 3x40 Zeilen Sub-Agent-Setup durch BuildSubAgentContext() ersetzt. AnthropicService: 5 Hilfsmethoden extrahiert – ExtractWebSearchSources(), TruncateToolResult(), AddToolsToPayload(), ParseServerToolUse(), TrackUsage(). WorkspaceEndpoint: 5 Session/Agent-Endpoint-Paare durch generische Core-Handler mit basePath-Parameter ersetzt. Ungenutzte rmdir-Endpoints entfernt. AgentEndpoint: Doppeltes Agent-DTO-Mapping durch MapAgentList() ersetzt. SessionEndpoint: Doppelte Session/Agent-Auflösung durch ResolvePromptContext() ersetzt. DatabaseService: Toten Code entfernt (SetSessionTelegramChatId, ClearSessionTelegramChatId, GetSessions, GetUserSessions). ConvertDataUrlToWebP und ResizeDataUrlToWebP zu einer Methode zusammengefasst. TelegramEndpoint: Bot-DTO-Mapping durch MapBotResponse() dedupliziert. KreaEndpoint + SkillEndpoint: Thumbnail-Generierung in ImageService.GenerateThumbnail() extrahiert, SixLabors-Imports aus Endpoints entfernt. CronjobEndpoint: 7x ctx.Items[UserId] durch ctx.GetUserId() ersetzt.
This commit is contained in:
1 parent
0097ce69db
commit
dcedbaa074
13 files changed
+447
-975
No files matched your search
+10
-45
@@ -27,57 +27,22 @@ public static class AgentEndpoint
|
||||
{
|
||||
var userId = ctx.GetUserId();
|
||||
var agents = await db.GetAgents(userId);
|
||||
var allSkills = skillService.GetAllSkills();
|
||||
var skillMap = allSkills.ToDictionary(s => s.Slug);
|
||||
|
||||
var results = new List<object>();
|
||||
foreach (var a in agents)
|
||||
{
|
||||
var agentSkillSlugs = await db.GetAgentSkills(a.AgentId);
|
||||
var skills = agentSkillSlugs
|
||||
.Where(slug => skillMap.ContainsKey(slug))
|
||||
.Select(slug => new { slug, name = skillMap[slug].Name, description = skillMap[slug].Description })
|
||||
.ToList();
|
||||
|
||||
results.Add(new
|
||||
{
|
||||
a.AgentId,
|
||||
a.Name,
|
||||
Avatar = a.AvatarSmall ?? a.Avatar,
|
||||
a.Description,
|
||||
a.Model,
|
||||
a.CustomCommands,
|
||||
a.AutoCommit,
|
||||
a.UseSoul,
|
||||
a.UseIdentity,
|
||||
a.UseUserInfo,
|
||||
a.UseMemory,
|
||||
a.UseWebSearch,
|
||||
a.WebSearchVersion,
|
||||
a.AllowUserChoice,
|
||||
a.TaskInstruction,
|
||||
a.AvatarScale,
|
||||
a.AvatarOffsetX,
|
||||
a.AvatarOffsetY,
|
||||
a.IsPublic,
|
||||
a.IsDeleted,
|
||||
a.CreatedAt,
|
||||
a.UpdatedAt,
|
||||
Skills = skills
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(results);
|
||||
await ctx.Response.WriteAsJsonAsync(await MapAgentList(agents, userId, db, skillService));
|
||||
}
|
||||
|
||||
private static async Task HandleListAvailable(HttpContext ctx, DatabaseService db, SkillService skillService)
|
||||
{
|
||||
var userId = ctx.GetUserId();
|
||||
var agents = await db.GetAvailableAgents(userId);
|
||||
var allSkills = skillService.GetAllSkills();
|
||||
var skillMap = allSkills.ToDictionary(s => s.Slug);
|
||||
await ctx.Response.WriteAsJsonAsync(await MapAgentList(agents, userId, db, skillService, includeOwner: true));
|
||||
}
|
||||
|
||||
private static async Task<List<object>> MapAgentList(
|
||||
IEnumerable<AgentRecord> agents, string userId, DatabaseService db, SkillService skillService, bool includeOwner = false)
|
||||
{
|
||||
var skillMap = skillService.GetAllSkills().ToDictionary(s => s.Slug);
|
||||
var results = new List<object>();
|
||||
|
||||
foreach (var a in agents)
|
||||
{
|
||||
var agentSkillSlugs = await db.GetAgentSkills(a.AgentId);
|
||||
@@ -110,12 +75,12 @@ public static class AgentEndpoint
|
||||
a.IsDeleted,
|
||||
a.CreatedAt,
|
||||
a.UpdatedAt,
|
||||
IsOwner = a.UserId == userId,
|
||||
IsOwner = includeOwner ? (bool?)(a.UserId == userId) : null,
|
||||
Skills = skills
|
||||
});
|
||||
}
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(results);
|
||||
return results;
|
||||
}
|
||||
|
||||
private static async Task HandleGet(HttpContext ctx, DatabaseService db)
|
||||
|
||||
@@ -2,6 +2,7 @@ using System;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Gott.Models;
|
||||
using Gott.Extensions;
|
||||
using Gott.Services;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
@@ -15,14 +16,14 @@ public static class CronjobEndpoint
|
||||
{
|
||||
app.MapGet("/api/cronjobs", async (HttpContext ctx, DatabaseService db) =>
|
||||
{
|
||||
var userId = ctx.Items["UserId"]?.ToString();
|
||||
var userId = ctx.GetUserId();
|
||||
var cronjobs = await db.GetCronjobs(userId);
|
||||
return Results.Ok(cronjobs);
|
||||
});
|
||||
|
||||
app.MapGet("/api/cronjobs/{id}", async (string id, HttpContext ctx, DatabaseService db) =>
|
||||
{
|
||||
var userId = ctx.Items["UserId"]?.ToString();
|
||||
var userId = ctx.GetUserId();
|
||||
var cronjob = await db.GetCronjob(id);
|
||||
if (cronjob == null || cronjob.UserId != userId) return Results.NotFound();
|
||||
return Results.Ok(cronjob);
|
||||
@@ -30,7 +31,7 @@ public static class CronjobEndpoint
|
||||
|
||||
app.MapPost("/api/cronjobs", async (HttpContext ctx, DatabaseService db) =>
|
||||
{
|
||||
var userId = ctx.Items["UserId"]?.ToString();
|
||||
var userId = ctx.GetUserId();
|
||||
var body = await JsonSerializer.DeserializeAsync<JsonElement>(ctx.Request.Body);
|
||||
|
||||
var name = body.TryGetProperty("name", out var n) ? n.GetString() ?? "" : "";
|
||||
@@ -79,7 +80,7 @@ public static class CronjobEndpoint
|
||||
|
||||
app.MapPut("/api/cronjobs/{id}", async (string id, HttpContext ctx, DatabaseService db) =>
|
||||
{
|
||||
var userId = ctx.Items["UserId"]?.ToString();
|
||||
var userId = ctx.GetUserId();
|
||||
var existing = await db.GetCronjob(id);
|
||||
if (existing == null || existing.UserId != userId) return Results.NotFound();
|
||||
|
||||
@@ -134,7 +135,7 @@ public static class CronjobEndpoint
|
||||
|
||||
app.MapDelete("/api/cronjobs/{id}", async (string id, HttpContext ctx, DatabaseService db) =>
|
||||
{
|
||||
var userId = ctx.Items["UserId"]?.ToString();
|
||||
var userId = ctx.GetUserId();
|
||||
var existing = await db.GetCronjob(id);
|
||||
if (existing == null || existing.UserId != userId) return Results.NotFound();
|
||||
|
||||
@@ -144,7 +145,7 @@ public static class CronjobEndpoint
|
||||
|
||||
app.MapPost("/api/cronjobs/{id}/toggle", async (string id, HttpContext ctx, DatabaseService db) =>
|
||||
{
|
||||
var userId = ctx.Items["UserId"]?.ToString();
|
||||
var userId = ctx.GetUserId();
|
||||
var existing = await db.GetCronjob(id);
|
||||
if (existing == null || existing.UserId != userId) return Results.NotFound();
|
||||
|
||||
@@ -158,7 +159,7 @@ public static class CronjobEndpoint
|
||||
|
||||
app.MapPost("/api/cronjobs/{id}/run", async (string id, HttpContext ctx, DatabaseService db, CronjobService cronjobService) =>
|
||||
{
|
||||
var userId = ctx.Items["UserId"]?.ToString();
|
||||
var userId = ctx.GetUserId();
|
||||
var existing = await db.GetCronjob(id);
|
||||
if (existing == null || existing.UserId != userId) return Results.NotFound();
|
||||
|
||||
|
||||
+18
-40
@@ -7,9 +7,6 @@ using Gott.Services;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using SixLabors.ImageSharp.Formats.Webp;
|
||||
|
||||
namespace Gott.Endpoints
|
||||
{
|
||||
@@ -142,50 +139,31 @@ namespace Gott.Endpoints
|
||||
return;
|
||||
}
|
||||
|
||||
// Cache-Verzeichnis: data/cache/images/
|
||||
var imagesDir = Path.GetDirectoryName(originalPath);
|
||||
var dataDir = Path.GetDirectoryName(imagesDir);
|
||||
var cacheDir = Path.Combine(dataDir, "cache", "images");
|
||||
Directory.CreateDirectory(cacheDir);
|
||||
|
||||
var thumbFilename = Path.GetFileNameWithoutExtension(filename) + ".webp";
|
||||
var thumbPath = Path.Combine(cacheDir, thumbFilename);
|
||||
|
||||
if (!File.Exists(thumbPath) || File.GetLastWriteTimeUtc(originalPath) > File.GetLastWriteTimeUtc(thumbPath))
|
||||
var thumbPath = await ImageService.GenerateThumbnail(originalPath, cacheDir);
|
||||
if (thumbPath != null)
|
||||
{
|
||||
try
|
||||
res.ContentType = "image/webp";
|
||||
res.Headers["Cache-Control"] = "private, max-age=86400";
|
||||
await res.SendFileAsync(thumbPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: Original ausliefern
|
||||
res.ContentType = ext switch
|
||||
{
|
||||
using var image = await Image.LoadAsync(originalPath);
|
||||
var maxSize = 320;
|
||||
image.Mutate(x => x.Resize(new ResizeOptions
|
||||
{
|
||||
Size = new Size(maxSize, maxSize),
|
||||
Mode = ResizeMode.Max
|
||||
}));
|
||||
var encoder = new WebpEncoder { Quality = 75 };
|
||||
await image.SaveAsync(thumbPath, encoder);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback: Original ausliefern
|
||||
var contentType = ext switch
|
||||
{
|
||||
".png" => "image/png",
|
||||
".jpg" => "image/jpeg",
|
||||
".jpeg" => "image/jpeg",
|
||||
".gif" => "image/gif",
|
||||
_ => "image/webp",
|
||||
};
|
||||
res.ContentType = contentType;
|
||||
res.Headers["Cache-Control"] = "private, max-age=3600";
|
||||
await res.SendFileAsync(originalPath);
|
||||
return;
|
||||
}
|
||||
".png" => "image/png",
|
||||
".jpg" => "image/jpeg",
|
||||
".jpeg" => "image/jpeg",
|
||||
".gif" => "image/gif",
|
||||
_ => "image/webp",
|
||||
};
|
||||
res.Headers["Cache-Control"] = "private, max-age=3600";
|
||||
await res.SendFileAsync(originalPath);
|
||||
}
|
||||
|
||||
res.ContentType = "image/webp";
|
||||
res.Headers["Cache-Control"] = "private, max-age=86400";
|
||||
await res.SendFileAsync(thumbPath);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -198,28 +198,26 @@ public static class SessionEndpoint
|
||||
ctx.Response.StatusCode = 204;
|
||||
}
|
||||
|
||||
private static async Task HandleSystemPrompt(HttpContext ctx, DatabaseService db, MemoryService memoryService, SkillService skillService)
|
||||
/// <summary>
|
||||
/// Löst Session, Agent, Workspace und Prompt-Abhängigkeiten auf.
|
||||
/// Gibt null zurück und setzt StatusCode bei Fehler.
|
||||
/// </summary>
|
||||
private static async Task<(SessionRecord session, AgentRecord agent, IEnumerable<MessageRecord> marked,
|
||||
string memDir, IEnumerable<string> skills, UserRecord user, string workspace, bool planMode)?>
|
||||
ResolvePromptContext(HttpContext ctx, DatabaseService db, MemoryService memoryService)
|
||||
{
|
||||
var userId = ctx.GetUserId();
|
||||
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
||||
if (string.IsNullOrEmpty(sessionId))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
return;
|
||||
}
|
||||
if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return null; }
|
||||
|
||||
var session = await db.GetSession(sessionId);
|
||||
if (session == null || session.UserId != userId)
|
||||
{
|
||||
ctx.Response.StatusCode = 404;
|
||||
return;
|
||||
}
|
||||
if (session == null || session.UserId != userId) { ctx.Response.StatusCode = 404; return null; }
|
||||
|
||||
if (string.IsNullOrEmpty(session.AgentId))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Agent ausgewählt" });
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
var agent = await db.GetAgent(session.AgentId);
|
||||
@@ -227,18 +225,29 @@ public static class SessionEndpoint
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Agent nicht gefunden" });
|
||||
return;
|
||||
return null;
|
||||
}
|
||||
|
||||
var markedMessages = await db.GetMarkedMessages(sessionId);
|
||||
var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId);
|
||||
var agentSkills = await db.GetAgentSkills(agent.AgentId);
|
||||
var marked = await db.GetMarkedMessages(sessionId);
|
||||
var memDir = memoryService.GetAgentMemoryDir(agent.AgentId);
|
||||
var skills = await db.GetAgentSkills(agent.AgentId);
|
||||
var user = await db.GetUser(userId);
|
||||
var resolvedWorkspace = WorkspaceHelper.ResolveWorkspace(session, user);
|
||||
if (string.IsNullOrEmpty(resolvedWorkspace) && agentSkills.Any())
|
||||
resolvedWorkspace = "/tmp";
|
||||
var workspace = WorkspaceHelper.ResolveWorkspace(session, user);
|
||||
if (string.IsNullOrEmpty(workspace) && skills.Any())
|
||||
workspace = "/tmp";
|
||||
var planMode = ctx.Request.Query.ContainsKey("planMode") && ctx.Request.Query["planMode"] == "true";
|
||||
var systemPrompt = SystemPromptBuilder.Build(agent, session, markedMessages, memoryService, agentMemoryDir, agentSkills, skillService, userPersona: user?.GetEffectivePersona(), resolvedWorkspace: resolvedWorkspace, planMode: planMode);
|
||||
|
||||
return (session, agent, marked, memDir, skills, user, workspace, planMode);
|
||||
}
|
||||
|
||||
private static async Task HandleSystemPrompt(HttpContext ctx, DatabaseService db, MemoryService memoryService, SkillService skillService)
|
||||
{
|
||||
var result = await ResolvePromptContext(ctx, db, memoryService);
|
||||
if (result == null) return;
|
||||
var (session, agent, marked, memDir, skills, user, workspace, planMode) = result.Value;
|
||||
|
||||
var systemPrompt = SystemPromptBuilder.Build(agent, session, marked, memoryService, memDir,
|
||||
skills, skillService, userPersona: user?.GetEffectivePersona(), resolvedWorkspace: workspace, planMode: planMode);
|
||||
|
||||
ctx.Response.ContentType = "text/plain; charset=utf-8";
|
||||
await ctx.Response.WriteAsync(systemPrompt);
|
||||
@@ -246,37 +255,12 @@ public static class SessionEndpoint
|
||||
|
||||
private static async Task HandleSystemPromptBlocks(HttpContext ctx, DatabaseService db, MemoryService memoryService, SkillService skillService)
|
||||
{
|
||||
var userId = ctx.GetUserId();
|
||||
var sessionId = ctx.Request.RouteValues["sessionId"]?.ToString();
|
||||
if (string.IsNullOrEmpty(sessionId)) { ctx.Response.StatusCode = 400; return; }
|
||||
var result = await ResolvePromptContext(ctx, db, memoryService);
|
||||
if (result == null) return;
|
||||
var (session, agent, marked, memDir, skills, user, workspace, planMode) = result.Value;
|
||||
|
||||
var session = await db.GetSession(sessionId);
|
||||
if (session == null || session.UserId != userId) { ctx.Response.StatusCode = 404; return; }
|
||||
|
||||
if (string.IsNullOrEmpty(session.AgentId))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Kein Agent ausgewählt" });
|
||||
return;
|
||||
}
|
||||
|
||||
var agent = await db.GetAgent(session.AgentId);
|
||||
if (agent == null)
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Agent nicht gefunden" });
|
||||
return;
|
||||
}
|
||||
|
||||
var markedMessages = await db.GetMarkedMessages(sessionId);
|
||||
var agentMemoryDir = memoryService.GetAgentMemoryDir(agent.AgentId);
|
||||
var agentSkills = await db.GetAgentSkills(agent.AgentId);
|
||||
var user = await db.GetUser(userId);
|
||||
var resolvedWorkspace = WorkspaceHelper.ResolveWorkspace(session, user);
|
||||
if (string.IsNullOrEmpty(resolvedWorkspace) && agentSkills.Any())
|
||||
resolvedWorkspace = "/tmp";
|
||||
var planMode = ctx.Request.Query.ContainsKey("planMode") && ctx.Request.Query["planMode"] == "true";
|
||||
var blocks = SystemPromptBuilder.BuildBlocks(agent, session, markedMessages, memoryService, agentMemoryDir, agentSkills, skillService, userPersona: user?.GetEffectivePersona(), resolvedWorkspace: resolvedWorkspace, planMode: planMode);
|
||||
var blocks = SystemPromptBuilder.BuildBlocks(agent, session, marked, memoryService, memDir,
|
||||
skills, skillService, userPersona: user?.GetEffectivePersona(), resolvedWorkspace: workspace, planMode: planMode);
|
||||
|
||||
await ctx.Response.WriteAsJsonAsync(blocks.Select(b => new { b.Icon, b.Label, b.Content }));
|
||||
}
|
||||
|
||||
+12
-37
@@ -9,9 +9,6 @@ using Gott.Services;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
using SixLabors.ImageSharp;
|
||||
using SixLabors.ImageSharp.Processing;
|
||||
using SixLabors.ImageSharp.Formats.Webp;
|
||||
|
||||
namespace Gott.Endpoints;
|
||||
|
||||
@@ -186,44 +183,22 @@ public static class SkillEndpoint
|
||||
var originalPath = Path.Combine(skill.Path, "output", filename);
|
||||
if (!File.Exists(originalPath)) { ctx.Response.StatusCode = 404; return; }
|
||||
|
||||
// Cache-Verzeichnis: data/cache/skills/{slug}/
|
||||
var dataDir = Path.GetDirectoryName(skillService.SkillsDir);
|
||||
var cacheDir = Path.Combine(dataDir, "cache", "skills", slug);
|
||||
Directory.CreateDirectory(cacheDir);
|
||||
|
||||
var thumbFilename = Path.GetFileNameWithoutExtension(filename) + ".webp";
|
||||
var thumbPath = Path.Combine(cacheDir, thumbFilename);
|
||||
|
||||
// Thumbnail generieren wenn nicht gecacht oder Original neuer
|
||||
if (!File.Exists(thumbPath) || File.GetLastWriteTimeUtc(originalPath) > File.GetLastWriteTimeUtc(thumbPath))
|
||||
var thumbPath = await ImageService.GenerateThumbnail(originalPath, cacheDir);
|
||||
if (thumbPath != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var image = await Image.LoadAsync(originalPath);
|
||||
|
||||
// Max 320px auf der längsten Seite
|
||||
var maxSize = 320;
|
||||
image.Mutate(x => x.Resize(new ResizeOptions
|
||||
{
|
||||
Size = new Size(maxSize, maxSize),
|
||||
Mode = ResizeMode.Max
|
||||
}));
|
||||
|
||||
var encoder = new WebpEncoder { Quality = 75 };
|
||||
await image.SaveAsync(thumbPath, encoder);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Fallback: Original ausliefern wenn Thumbnail-Generierung fehlschlägt
|
||||
ctx.Response.ContentType = MimeTypes.GetValueOrDefault(ext, "application/octet-stream");
|
||||
ctx.Response.Headers["Cache-Control"] = "private, max-age=3600";
|
||||
await ctx.Response.SendFileAsync(originalPath);
|
||||
return;
|
||||
}
|
||||
ctx.Response.ContentType = "image/webp";
|
||||
ctx.Response.Headers["Cache-Control"] = "private, max-age=86400";
|
||||
await ctx.Response.SendFileAsync(thumbPath);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback: Original ausliefern
|
||||
ctx.Response.ContentType = MimeTypes.GetValueOrDefault(ext, "application/octet-stream");
|
||||
ctx.Response.Headers["Cache-Control"] = "private, max-age=3600";
|
||||
await ctx.Response.SendFileAsync(originalPath);
|
||||
}
|
||||
|
||||
ctx.Response.ContentType = "image/webp";
|
||||
ctx.Response.Headers["Cache-Control"] = "private, max-age=86400";
|
||||
await ctx.Response.SendFileAsync(thumbPath);
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Gott.Extensions;
|
||||
using Gott.Services;
|
||||
using Gott.Models;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
@@ -72,26 +73,29 @@ public static class TelegramEndpoint
|
||||
|
||||
// ── Bot CRUD ──
|
||||
|
||||
private static async Task<object> MapBotResponse(TelegramBotRecord bot, DatabaseService db)
|
||||
{
|
||||
var sessionCount = await db.GetTelegramBotSessionCount(bot.BotId);
|
||||
return new
|
||||
{
|
||||
botId = bot.BotId,
|
||||
name = bot.Name,
|
||||
botToken = bot.BotToken,
|
||||
webhookSecret = bot.WebhookSecret,
|
||||
isPublic = bot.IsPublic,
|
||||
sessionCount,
|
||||
createdAt = bot.CreatedAt,
|
||||
updatedAt = bot.UpdatedAt
|
||||
};
|
||||
}
|
||||
|
||||
private static async Task HandleGetBots(HttpRequest req, HttpResponse res, DatabaseService db)
|
||||
{
|
||||
var userId = req.HttpContext.GetUserId();
|
||||
var bots = await db.GetTelegramBots(userId);
|
||||
var result = new System.Collections.Generic.List<object>();
|
||||
foreach (var bot in bots)
|
||||
{
|
||||
var sessionCount = await db.GetTelegramBotSessionCount(bot.BotId);
|
||||
result.Add(new
|
||||
{
|
||||
botId = bot.BotId,
|
||||
name = bot.Name,
|
||||
botToken = bot.BotToken,
|
||||
webhookSecret = bot.WebhookSecret,
|
||||
isPublic = bot.IsPublic,
|
||||
sessionCount,
|
||||
createdAt = bot.CreatedAt,
|
||||
updatedAt = bot.UpdatedAt
|
||||
});
|
||||
}
|
||||
result.Add(await MapBotResponse(bot, db));
|
||||
await res.WriteAsJsonAsync(result);
|
||||
}
|
||||
|
||||
@@ -106,18 +110,7 @@ public static class TelegramEndpoint
|
||||
return;
|
||||
}
|
||||
|
||||
var sessionCount = await db.GetTelegramBotSessionCount(bot.BotId);
|
||||
await res.WriteAsJsonAsync(new
|
||||
{
|
||||
botId = bot.BotId,
|
||||
name = bot.Name,
|
||||
botToken = bot.BotToken,
|
||||
webhookSecret = bot.WebhookSecret,
|
||||
isPublic = bot.IsPublic,
|
||||
sessionCount,
|
||||
createdAt = bot.CreatedAt,
|
||||
updatedAt = bot.UpdatedAt
|
||||
});
|
||||
await res.WriteAsJsonAsync(await MapBotResponse(bot, db));
|
||||
}
|
||||
|
||||
private static async Task HandleCreateBot(HttpRequest req, HttpResponse res, DatabaseService db)
|
||||
|
||||
+135
-394
@@ -38,27 +38,40 @@ public static class WorkspaceEndpoint
|
||||
|
||||
public static void MapWorkspaceEndpoint(this IEndpointRouteBuilder app)
|
||||
{
|
||||
app.MapGet("/api/workspace/tree", HandleTree);
|
||||
app.MapGet("/api/workspace/file", HandleFile);
|
||||
app.MapPut("/api/workspace/file", HandleFileSave);
|
||||
// Session-Workspace-Endpunkte
|
||||
app.MapGet("/api/workspace/tree", async ctx =>
|
||||
await HandleTreeCore(ctx, await ResolveSessionBasePath(ctx)));
|
||||
app.MapGet("/api/workspace/file", async ctx =>
|
||||
await HandleFileCore(ctx, await ResolveSessionBasePath(ctx)));
|
||||
app.MapPut("/api/workspace/file", async ctx =>
|
||||
await HandleFileSaveCore(ctx, await ResolveSessionBasePath(ctx), allowCreate: false));
|
||||
app.MapPost("/api/workspace/mkdir", async ctx =>
|
||||
await HandleMkdirCore(ctx, await ResolveSessionBasePath(ctx)));
|
||||
|
||||
// Agent-Memory-Endpunkte (gleiche Logik, anderer basePath)
|
||||
app.MapGet("/api/workspace/agent/tree", async ctx =>
|
||||
await HandleTreeCore(ctx, ResolveAgentBasePath(ctx)));
|
||||
app.MapGet("/api/workspace/agent/file", async ctx =>
|
||||
await HandleFileCore(ctx, ResolveAgentBasePath(ctx)));
|
||||
app.MapPut("/api/workspace/agent/file", async ctx =>
|
||||
await HandleFileSaveCore(ctx, ResolveAgentBasePath(ctx), allowCreate: true));
|
||||
app.MapPost("/api/workspace/agent/mkdir", async ctx =>
|
||||
await HandleMkdirCore(ctx, ResolveAgentBasePath(ctx)));
|
||||
|
||||
// Sonstige
|
||||
app.MapGet("/api/workspace/browse", HandleBrowse);
|
||||
app.MapPost("/api/workspace/browse/mkdir", HandleBrowseMkdir);
|
||||
app.MapGet("/api/workspace/agent/tree", HandleAgentTree);
|
||||
app.MapGet("/api/workspace/agent/file", HandleAgentFile);
|
||||
app.MapPut("/api/workspace/agent/file", HandleAgentFileSave);
|
||||
app.MapGet("/api/workspace/raw", HandleRawFile);
|
||||
app.MapPost("/api/workspace/mkdir", HandleMkdir);
|
||||
app.MapDelete("/api/workspace/dir", HandleRmdir);
|
||||
app.MapPost("/api/workspace/agent/mkdir", HandleAgentMkdir);
|
||||
app.MapDelete("/api/workspace/agent/dir", HandleAgentRmdir);
|
||||
}
|
||||
|
||||
// ── BasePath-Auflösung ──────────────────────────────────────────────
|
||||
|
||||
/// <summary>
|
||||
/// Lädt Session + User und löst den Workspace-Pfad auf (relativ → absolut via DefaultPath).
|
||||
/// Gibt null zurück und setzt StatusCode 400, wenn kein gültiger Workspace vorhanden ist.
|
||||
/// Löst den Workspace-Pfad einer Session auf. Gibt null zurück + setzt 400 bei Fehler.
|
||||
/// </summary>
|
||||
private static async Task<(SessionRecord session, string basePath)?> ResolveSessionWorkspace(HttpContext ctx, string sessionId)
|
||||
private static async Task<string> ResolveSessionBasePath(HttpContext ctx)
|
||||
{
|
||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
||||
var db = ctx.RequestServices.GetRequiredService<Gott.Services.DatabaseService>();
|
||||
var session = await db.GetSession(sessionId ?? "");
|
||||
if (session == null || session.UserId != ctx.GetUserId() || string.IsNullOrEmpty(session.Workspace))
|
||||
@@ -78,20 +91,40 @@ public static class WorkspaceEndpoint
|
||||
return null;
|
||||
}
|
||||
|
||||
return (session, Path.GetFullPath(resolved));
|
||||
return Path.GetFullPath(resolved);
|
||||
}
|
||||
|
||||
private static async Task HandleTree(HttpContext ctx)
|
||||
/// <summary>
|
||||
/// Löst den Memory-Pfad eines Agents auf. Gibt null zurück + setzt 400 bei Fehler.
|
||||
/// </summary>
|
||||
private static string ResolveAgentBasePath(HttpContext ctx)
|
||||
{
|
||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
||||
var subPath = ctx.Request.Query["path"].FirstOrDefault() ?? "";
|
||||
var agentId = ctx.Request.Query["agentId"].FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(agentId))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
ctx.Response.WriteAsJsonAsync(new { error = "agentId fehlt" }).Wait();
|
||||
return null;
|
||||
}
|
||||
|
||||
var result = await ResolveSessionWorkspace(ctx, sessionId);
|
||||
if (result == null) return;
|
||||
var (session, basePath) = result.Value;
|
||||
var memoryService = ctx.RequestServices.GetRequiredService<Gott.Services.MemoryService>();
|
||||
var basePath = memoryService.GetAgentMemoryDir(agentId);
|
||||
|
||||
if (!Directory.Exists(basePath))
|
||||
Directory.CreateDirectory(basePath);
|
||||
|
||||
return basePath;
|
||||
}
|
||||
|
||||
// ── Generische Handler ──────────────────────────────────────────────
|
||||
|
||||
private static async Task HandleTreeCore(HttpContext ctx, string basePath)
|
||||
{
|
||||
if (basePath == null) return;
|
||||
|
||||
var subPath = ctx.Request.Query["path"].FirstOrDefault() ?? "";
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, subPath));
|
||||
|
||||
// Sicherheit: nicht aus dem Workspace ausbrechen
|
||||
if (!targetPath.StartsWith(basePath))
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
@@ -107,17 +140,14 @@ public static class WorkspaceEndpoint
|
||||
|
||||
var entries = new List<object>();
|
||||
|
||||
// Ordner
|
||||
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" });
|
||||
}
|
||||
|
||||
// Dateien
|
||||
foreach (var file in Directory.GetFiles(targetPath).OrderBy(f => Path.GetFileName(f), StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
var name = Path.GetFileName(file);
|
||||
@@ -125,30 +155,24 @@ public static class WorkspaceEndpoint
|
||||
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 HandleFile(HttpContext ctx)
|
||||
private static async Task HandleFileCore(HttpContext ctx, string basePath)
|
||||
{
|
||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
||||
var filePath = ctx.Request.Query["path"].FirstOrDefault();
|
||||
if (basePath == null) return;
|
||||
|
||||
var filePath = ctx.Request.Query["path"].FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await ResolveSessionWorkspace(ctx, sessionId);
|
||||
if (result == null) return;
|
||||
var (session, basePath) = result.Value;
|
||||
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
|
||||
|
||||
if (!targetPath.StartsWith(basePath))
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
@@ -162,8 +186,6 @@ public static class WorkspaceEndpoint
|
||||
}
|
||||
|
||||
var info = new FileInfo(targetPath);
|
||||
|
||||
// Max 2MB für Textdateien
|
||||
if (info.Length > 2 * 1024 * 1024)
|
||||
{
|
||||
ctx.Response.StatusCode = 413;
|
||||
@@ -184,33 +206,39 @@ public static class WorkspaceEndpoint
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task HandleFileSave(HttpContext ctx)
|
||||
private static async Task HandleFileSaveCore(HttpContext ctx, string basePath, bool allowCreate)
|
||||
{
|
||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
||||
var filePath = ctx.Request.Query["path"].FirstOrDefault();
|
||||
if (basePath == null) return;
|
||||
|
||||
var filePath = ctx.Request.Query["path"].FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(filePath))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await ResolveSessionWorkspace(ctx, sessionId);
|
||||
if (result == null) return;
|
||||
var (session, basePath) = result.Value;
|
||||
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
|
||||
|
||||
if (!targetPath.StartsWith(basePath))
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!File.Exists(targetPath))
|
||||
if (allowCreate)
|
||||
{
|
||||
ctx.Response.StatusCode = 404;
|
||||
return;
|
||||
// Agent-Memory: Verzeichnis erstellen falls nötig
|
||||
var dir = Path.GetDirectoryName(targetPath);
|
||||
if (!string.IsNullOrEmpty(dir) && !Directory.Exists(dir))
|
||||
Directory.CreateDirectory(dir);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Session-Workspace: Datei muss existieren
|
||||
if (!File.Exists(targetPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 404;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
using var reader = new StreamReader(ctx.Request.Body);
|
||||
@@ -230,6 +258,42 @@ public static class WorkspaceEndpoint
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task HandleMkdirCore(HttpContext ctx, string basePath)
|
||||
{
|
||||
if (basePath == null) return;
|
||||
|
||||
var dirPath = ctx.Request.Query["path"].FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(dirPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Pfad fehlt" });
|
||||
return;
|
||||
}
|
||||
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, dirPath));
|
||||
if (!targetPath.StartsWith(basePath))
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Directory.Exists(targetPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 409;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner existiert bereits" });
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(targetPath);
|
||||
await ctx.Response.WriteAsJsonAsync(new
|
||||
{
|
||||
path = Path.GetRelativePath(basePath, targetPath).Replace('\\', '/'),
|
||||
created = true
|
||||
});
|
||||
}
|
||||
|
||||
// ── Sonstige Endpunkte ──────────────────────────────────────────────
|
||||
|
||||
private static async Task HandleBrowse(HttpContext ctx)
|
||||
{
|
||||
var path = ctx.Request.Query["path"].FirstOrDefault() ?? "/";
|
||||
@@ -277,7 +341,6 @@ public static class WorkspaceEndpoint
|
||||
return;
|
||||
}
|
||||
|
||||
// Parent muss existieren
|
||||
var parent = Path.GetDirectoryName(targetPath);
|
||||
if (string.IsNullOrEmpty(parent) || !Directory.Exists(parent))
|
||||
{
|
||||
@@ -290,345 +353,6 @@ public static class WorkspaceEndpoint
|
||||
await ctx.Response.WriteAsJsonAsync(new { path = targetPath.Replace('\\', '/'), created = true });
|
||||
}
|
||||
|
||||
private static bool IsLikelyTextFile(string name)
|
||||
{
|
||||
var lower = name.ToLowerInvariant();
|
||||
return lower is "makefile" or "dockerfile" or "rakefile" or "gemfile"
|
||||
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
|
||||
});
|
||||
}
|
||||
|
||||
private static async Task HandleMkdir(HttpContext ctx)
|
||||
{
|
||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
||||
var dirPath = ctx.Request.Query["path"].FirstOrDefault();
|
||||
|
||||
if (string.IsNullOrEmpty(dirPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Pfad fehlt" });
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await ResolveSessionWorkspace(ctx, sessionId);
|
||||
if (result == null) return;
|
||||
var (session, basePath) = result.Value;
|
||||
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, dirPath));
|
||||
|
||||
if (!targetPath.StartsWith(basePath))
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Directory.Exists(targetPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 409;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner existiert bereits" });
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(targetPath);
|
||||
await ctx.Response.WriteAsJsonAsync(new { path = dirPath, created = true });
|
||||
}
|
||||
|
||||
private static async Task HandleRmdir(HttpContext ctx)
|
||||
{
|
||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
||||
var dirPath = ctx.Request.Query["path"].FirstOrDefault();
|
||||
|
||||
if (string.IsNullOrEmpty(dirPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Pfad fehlt" });
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await ResolveSessionWorkspace(ctx, sessionId);
|
||||
if (result == null) return;
|
||||
var (session, basePath) = result.Value;
|
||||
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, dirPath));
|
||||
|
||||
if (!targetPath.StartsWith(basePath) || targetPath == basePath)
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(targetPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 404;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner nicht gefunden" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (Directory.GetFileSystemEntries(targetPath).Length > 0)
|
||||
{
|
||||
ctx.Response.StatusCode = 409;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner ist nicht leer" });
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.Delete(targetPath);
|
||||
await ctx.Response.WriteAsJsonAsync(new { path = dirPath, deleted = true });
|
||||
}
|
||||
|
||||
private static async Task HandleAgentMkdir(HttpContext ctx)
|
||||
{
|
||||
var basePath = GetAgentBasePath(ctx, out var error);
|
||||
if (basePath == null)
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error });
|
||||
return;
|
||||
}
|
||||
|
||||
var dirPath = ctx.Request.Query["path"].FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(dirPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Pfad fehlt" });
|
||||
return;
|
||||
}
|
||||
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, dirPath));
|
||||
if (!targetPath.StartsWith(basePath))
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
return;
|
||||
}
|
||||
|
||||
if (Directory.Exists(targetPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 409;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner existiert bereits" });
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(targetPath);
|
||||
await ctx.Response.WriteAsJsonAsync(new { path = dirPath, created = true });
|
||||
}
|
||||
|
||||
private static async Task HandleAgentRmdir(HttpContext ctx)
|
||||
{
|
||||
var basePath = GetAgentBasePath(ctx, out var error);
|
||||
if (basePath == null)
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error });
|
||||
return;
|
||||
}
|
||||
|
||||
var dirPath = ctx.Request.Query["path"].FirstOrDefault();
|
||||
if (string.IsNullOrEmpty(dirPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 400;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Pfad fehlt" });
|
||||
return;
|
||||
}
|
||||
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, dirPath));
|
||||
if (!targetPath.StartsWith(basePath) || targetPath == basePath)
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
return;
|
||||
}
|
||||
|
||||
if (!Directory.Exists(targetPath))
|
||||
{
|
||||
ctx.Response.StatusCode = 404;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner nicht gefunden" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (Directory.GetFileSystemEntries(targetPath).Length > 0)
|
||||
{
|
||||
ctx.Response.StatusCode = 409;
|
||||
await ctx.Response.WriteAsJsonAsync(new { error = "Ordner ist nicht leer" });
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.Delete(targetPath);
|
||||
await ctx.Response.WriteAsJsonAsync(new { path = dirPath, deleted = true });
|
||||
}
|
||||
|
||||
private static readonly Dictionary<string, string> MimeTypes = new(StringComparer.OrdinalIgnoreCase)
|
||||
{
|
||||
{ ".png", "image/png" }, { ".jpg", "image/jpeg" }, { ".jpeg", "image/jpeg" },
|
||||
{ ".gif", "image/gif" }, { ".webp", "image/webp" }, { ".svg", "image/svg+xml" },
|
||||
{ ".bmp", "image/bmp" }, { ".ico", "image/x-icon" },
|
||||
{ ".pdf", "application/pdf" },
|
||||
{ ".mp4", "video/mp4" }, { ".webm", "video/webm" },
|
||||
{ ".mp3", "audio/mpeg" }, { ".wav", "audio/wav" }, { ".ogg", "audio/ogg" },
|
||||
};
|
||||
|
||||
private static async Task HandleRawFile(HttpContext ctx)
|
||||
{
|
||||
var sessionId = ctx.Request.Query["sessionId"].FirstOrDefault();
|
||||
@@ -640,12 +364,10 @@ public static class WorkspaceEndpoint
|
||||
return;
|
||||
}
|
||||
|
||||
var result = await ResolveSessionWorkspace(ctx, sessionId);
|
||||
if (result == null) return;
|
||||
var basePath = result.Value.basePath;
|
||||
var basePath = await ResolveSessionBasePath(ctx);
|
||||
if (basePath == null) return;
|
||||
|
||||
var targetPath = Path.GetFullPath(Path.Combine(basePath, filePath));
|
||||
|
||||
if (!targetPath.StartsWith(basePath))
|
||||
{
|
||||
ctx.Response.StatusCode = 403;
|
||||
@@ -658,11 +380,30 @@ public static class WorkspaceEndpoint
|
||||
return;
|
||||
}
|
||||
|
||||
var ext = Path.GetExtension(targetPath);
|
||||
var contentType = MimeTypes.GetValueOrDefault(ext, "application/octet-stream");
|
||||
var ext = Path.GetExtension(targetPath).ToLowerInvariant();
|
||||
ctx.Response.ContentType = ext switch
|
||||
{
|
||||
".png" => "image/png",
|
||||
".jpg" or ".jpeg" => "image/jpeg",
|
||||
".gif" => "image/gif",
|
||||
".webp" => "image/webp",
|
||||
".svg" => "image/svg+xml",
|
||||
".pdf" => "application/pdf",
|
||||
".mp4" => "video/mp4",
|
||||
".webm" => "video/webm",
|
||||
".mp3" => "audio/mpeg",
|
||||
".wav" => "audio/wav",
|
||||
_ => "application/octet-stream"
|
||||
};
|
||||
|
||||
ctx.Response.ContentType = contentType;
|
||||
ctx.Response.Headers["Cache-Control"] = "private, max-age=3600";
|
||||
await ctx.Response.SendFileAsync(targetPath);
|
||||
}
|
||||
|
||||
private static bool IsLikelyTextFile(string name)
|
||||
{
|
||||
var lower = name.ToLowerInvariant();
|
||||
return lower is "makefile" or "dockerfile" or "rakefile" or "gemfile"
|
||||
or "procfile" or "vagrantfile" or "license" or "readme"
|
||||
or "changelog" or "authors" or "contributors";
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user