feat: Thumbnail-Varianten (256px) für Agent-Avatare und Session-Images

- ImageService: ResizeDataUrlSmall() für 256px WebP-Thumbnails (Quality 70)
- Neue DB-Spalten: agents.avatar_small, sessions.image_small
- Migration generiert Thumbnails für bestehende Bilder automatisch
- Listen-Endpoints (/api/agents, /api/sessions) liefern kleine Variante
- Detail-Endpoints liefern weiterhin das volle 512px-Bild
- Frontend unverändert (Cards nutzen automatisch die kleineren Bilder)
This commit is contained in:
Meleki committed 2026-03-07 20:09:01 +01:00
1 parent 8efce5f2d5
commit 2a2e98ca68
6 files changed
+116 -15

No files matched your search

+84 -7
View File
@@ -8,6 +8,7 @@ using Gott.Models;
using Microsoft.Data.Sqlite;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Processing;
namespace Gott.Services;
@@ -262,8 +263,14 @@ public class DatabaseService
CREATE INDEX IF NOT EXISTS idx_shared_messages_message ON shared_messages(message_id);
");
TryAddColumn(conn, "agents", "avatar_small", "TEXT");
TryAddColumn(conn, "sessions", "image_small", "TEXT");
// Migration: Bestehende PNG/JPEG-Avatare/Images zu WebP konvertieren
MigrateImagesToWebP(conn);
// Migration: Small-Thumbnails für bestehende Bilder generieren
MigrateImageThumbnails(conn);
}
private void MigrateImagesToWebP(SqliteConnection conn)
@@ -298,6 +305,70 @@ public class DatabaseService
Console.WriteLine($"[Migration] Converted {agentImages.Count} agent avatars and {sessionImages.Count} session images to WebP");
}
private void MigrateImageThumbnails(SqliteConnection conn)
{
const int smallSize = 256;
const int smallQuality = 70;
// Agent Avatare ohne Thumbnail
var agentsWithoutThumb = conn.Query<(string AgentId, string Avatar)>(
"SELECT agent_id, avatar FROM agents WHERE avatar IS NOT NULL AND avatar != '' AND (avatar_small IS NULL OR avatar_small = '')")
.ToList();
foreach (var (agentId, avatar) in agentsWithoutThumb)
{
var small = ResizeDataUrlToWebP(avatar, smallSize, smallQuality);
if (small != null)
conn.Execute("UPDATE agents SET avatar_small = @small WHERE agent_id = @agentId", new { small, agentId });
}
// Session Images ohne Thumbnail
var sessionsWithoutThumb = conn.Query<(string SessionId, string Image)>(
"SELECT session_id, image FROM sessions WHERE image IS NOT NULL AND image != '' AND (image_small IS NULL OR image_small = '')")
.ToList();
foreach (var (sessionId, image) in sessionsWithoutThumb)
{
var small = ResizeDataUrlToWebP(image, smallSize, smallQuality);
if (small != null)
conn.Execute("UPDATE sessions SET image_small = @small WHERE session_id = @sessionId", new { small, sessionId });
}
if (agentsWithoutThumb.Count > 0 || sessionsWithoutThumb.Count > 0)
Console.WriteLine($"[Migration] Generated {agentsWithoutThumb.Count} agent thumbnails and {sessionsWithoutThumb.Count} session thumbnails");
}
private static string ResizeDataUrlToWebP(string dataUrl, int maxSide, int quality)
{
try
{
var commaIndex = dataUrl.IndexOf(',');
if (commaIndex < 0) return null;
var base64 = dataUrl.Substring(commaIndex + 1);
var bytes = Convert.FromBase64String(base64);
using var image = Image.Load(bytes);
if (image.Width > maxSide || image.Height > maxSide)
{
image.Mutate(x => x.Resize(new ResizeOptions
{
Size = new Size(maxSide, maxSide),
Mode = ResizeMode.Max
}));
}
using var ms = new MemoryStream();
image.Save(ms, new WebpEncoder { Quality = quality });
return "data:image/webp;base64," + Convert.ToBase64String(ms.ToArray());
}
catch
{
return null;
}
}
private static string ConvertDataUrlToWebP(string dataUrl, int quality)
{
try
@@ -395,8 +466,8 @@ public class DatabaseService
agent.CreatedAt = now;
agent.UpdatedAt = now;
await conn.ExecuteAsync(@"
INSERT INTO agents (agent_id, user_id, name, avatar, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, use_web_search, web_search_version, allow_user_choice, task_instruction, avatar_scale, avatar_offset_x, avatar_offset_y, is_deleted, created_at, updated_at)
VALUES (@AgentId, @UserId, @Name, @Avatar, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, @UseWebSearch, @WebSearchVersion, @AllowUserChoice, @TaskInstruction, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, 0, @CreatedAt, @UpdatedAt)
INSERT INTO agents (agent_id, user_id, name, avatar, avatar_small, description, model, tool_choice, custom_commands, auto_commit, use_soul, use_identity, use_user_info, use_memory, use_web_search, web_search_version, allow_user_choice, task_instruction, avatar_scale, avatar_offset_x, avatar_offset_y, is_deleted, created_at, updated_at)
VALUES (@AgentId, @UserId, @Name, @Avatar, @AvatarSmall, @Description, @Model, @ToolChoice, @CustomCommands, @AutoCommit, @UseSoul, @UseIdentity, @UseUserInfo, @UseMemory, @UseWebSearch, @WebSearchVersion, @AllowUserChoice, @TaskInstruction, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, 0, @CreatedAt, @UpdatedAt)
", agent);
return agent;
}
@@ -420,7 +491,7 @@ public class DatabaseService
using var conn = CreateConnection();
agent.UpdatedAt = DateTime.UtcNow.ToString("o");
await conn.ExecuteAsync(@"
UPDATE agents SET name = @Name, avatar = @Avatar, description = @Description,
UPDATE agents SET name = @Name, avatar = @Avatar, avatar_small = @AvatarSmall, description = @Description,
model = @Model, tool_choice = @ToolChoice,
custom_commands = @CustomCommands, auto_commit = @AutoCommit,
use_soul = @UseSoul, use_identity = @UseIdentity, use_user_info = @UseUserInfo, use_memory = @UseMemory, use_web_search = @UseWebSearch, web_search_version = @WebSearchVersion,
@@ -440,7 +511,7 @@ public class DatabaseService
// ── Sessions ──
public async Task<SessionRecord> CreateSession(string userId, string name = null, string description = null, string image = null, string agentId = null, string workspace = null, string context = null, string customCommands = null, double avatarScale = 1.0, double avatarOffsetX = 0, double avatarOffsetY = 0, string reviewAgentId = null, int maxReviewIterations = 3, string finalizerAgentId = null, string preparerAgentId = null)
public async Task<SessionRecord> CreateSession(string userId, string name = null, string description = null, string image = null, string imageSmall = null, string agentId = null, string workspace = null, string context = null, string customCommands = null, double avatarScale = 1.0, double avatarOffsetX = 0, double avatarOffsetY = 0, string reviewAgentId = null, int maxReviewIterations = 3, string finalizerAgentId = null, string preparerAgentId = null)
{
using var conn = CreateConnection();
var now = DateTime.UtcNow.ToString("o");
@@ -451,6 +522,7 @@ public class DatabaseService
Name = name,
Description = description,
Image = image,
ImageSmall = imageSmall,
AgentId = agentId,
Workspace = workspace,
Context = context,
@@ -466,8 +538,8 @@ public class DatabaseService
UpdatedAt = now
};
await conn.ExecuteAsync(@"
INSERT INTO sessions (session_id, user_id, name, description, image, agent_id, workspace, context, custom_commands, avatar_scale, avatar_offset_x, avatar_offset_y, review_agent_id, max_review_iterations, finalizer_agent_id, preparer_agent_id, created_at, updated_at)
VALUES (@SessionId, @UserId, @Name, @Description, @Image, @AgentId, @Workspace, @Context, @CustomCommands, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, @ReviewAgentId, @MaxReviewIterations, @FinalizerAgentId, @PreparerAgentId, @CreatedAt, @UpdatedAt)
INSERT INTO sessions (session_id, user_id, name, description, image, image_small, agent_id, workspace, context, custom_commands, avatar_scale, avatar_offset_x, avatar_offset_y, review_agent_id, max_review_iterations, finalizer_agent_id, preparer_agent_id, created_at, updated_at)
VALUES (@SessionId, @UserId, @Name, @Description, @Image, @ImageSmall, @AgentId, @Workspace, @Context, @CustomCommands, @AvatarScale, @AvatarOffsetX, @AvatarOffsetY, @ReviewAgentId, @MaxReviewIterations, @FinalizerAgentId, @PreparerAgentId, @CreatedAt, @UpdatedAt)
", session);
return session;
}
@@ -541,7 +613,12 @@ public class DatabaseService
if (body.TryGetProperty("image", out var img))
{
var imageData = img.GetString();
if (imageService != null) imageData = imageService.ResizeDataUrl(imageData);
if (imageService != null)
{
sets.Add("image_small = @imageSmall");
p.Add("imageSmall", imageService.ResizeDataUrlSmall(imageData));
imageData = imageService.ResizeDataUrl(imageData);
}
sets.Add("image = @image");
p.Add("image", imageData);
}