Add image preview support: Krea-tag parsing with modal, file-explorer preview, skill output endpoint, increase execute timeout to 180s

- SkillEndpoint: GET /api/skills/{slug}/output/{filename} for skill output files
- WorkspaceEndpoint: GET /api/workspace/raw for raw file serving (images)
- File-Explorer: Image preview for png/jpg/gif/webp/svg/bmp/ico
- Chat UI: Parse [Krea: filename] tags into clickable chips with image modal
- ToolService: Increase execute timeout from 30s to 180s for long-running skills
This commit is contained in:
Meleki committed 2026-03-01 20:00:35 +01:00
1 parent cf6caea2dc
commit 9e4ed6dada
7 files changed
+253 -3

No files matched your search

+56
View File
@@ -1,3 +1,6 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Threading.Tasks;
@@ -10,11 +13,24 @@ namespace Gott.Endpoints;
public static class SkillEndpoint
{
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" },
};
public static void MapSkillEndpoints(this IEndpointRouteBuilder app)
{
// Skills (Dateisystem-basiert)
app.MapGet("/api/skills", HandleListSkills);
// Skill-Output-Dateien (Bilder etc.)
app.MapGet("/api/skills/{slug}/output/{filename}", HandleSkillOutput);
// Agent ↔ Skill Zuordnung
app.MapGet("/api/agents/{agentId}/skills", HandleGetAgentSkills);
app.MapPut("/api/agents/{agentId}/skills", HandleSetAgentSkills);
@@ -79,4 +95,44 @@ public static class SkillEndpoint
await db.RemoveAgentSkill(agentId, slug);
ctx.Response.StatusCode = 204;
}
private static async Task HandleSkillOutput(HttpContext ctx, SkillService skillService)
{
var slug = ctx.Request.RouteValues["slug"]?.ToString();
var filename = ctx.Request.RouteValues["filename"]?.ToString();
if (string.IsNullOrEmpty(slug) || string.IsNullOrEmpty(filename))
{
ctx.Response.StatusCode = 400;
return;
}
// Sicherheit: Keine Pfad-Traversals erlauben
if (filename.Contains("..") || filename.Contains('/') || filename.Contains('\\'))
{
ctx.Response.StatusCode = 400;
return;
}
var skill = skillService.GetSkill(slug);
if (skill == null)
{
ctx.Response.StatusCode = 404;
return;
}
var filePath = Path.Combine(skill.Path, "output", filename);
if (!File.Exists(filePath))
{
ctx.Response.StatusCode = 404;
return;
}
var ext = Path.GetExtension(filename);
var contentType = MimeTypes.GetValueOrDefault(ext, "application/octet-stream");
ctx.Response.ContentType = contentType;
ctx.Response.Headers["Cache-Control"] = "private, max-age=3600";
await ctx.Response.SendFileAsync(filePath);
}
}