Files
gott/Services/DatabaseService.cs
T
MelekiandClaude Fable 5 81892d6e65 Agent: Max Output Tokens + Thinking deaktivieren einstellbar
Zwei neue Agent-Einstellungen (Frontend, DB-Migration additiv):
- MaxOutputTokens: hartes Ausgabelimit. CLI: CLAUDE_CODE_MAX_OUTPUT_TOKENS,
  API: max_tokens (statt hart 16384). 0 = aus.
- DisableThinking: CLI: CLAUDE_CODE_DISABLE_THINKING=1 + MAX_THINKING_TOKENS=0;
  entfernt CLAUDE_CODE_EFFORT_LEVEL (Explizit-Aus schlaegt Effort).
  API-Pfad ohne Aequivalent (Hint im UI).

Die einzigen zwei Parameter, die z.ai/GLM nachweislich honoriert
(Sampling-Parameter wie temperature/top_p werden dort still verworfen).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-09-03 17:54:11 +02:00

557 lines
25 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Threading.Tasks;
using Dapper;
using Gott.Models;
using Microsoft.Data.Sqlite;
using SixLabors.ImageSharp;
using SixLabors.ImageSharp.Formats.Webp;
using SixLabors.ImageSharp.Processing;
namespace Gott.Services;
public partial class DatabaseService
{
private readonly string _connectionString;
public DatabaseService(string dbPath)
{
_connectionString = $"Data Source={dbPath}";
InitializeDatabase();
}
private SqliteConnection CreateConnection()
{
var conn = new SqliteConnection(_connectionString);
conn.Open();
return conn;
}
public SqliteConnection GetConnection() => CreateConnection();
private void InitializeDatabase()
{
using var conn = CreateConnection();
Dapper.DefaultTypeMap.MatchNamesWithUnderscores = true;
conn.Execute(@"
CREATE TABLE IF NOT EXISTS sessions (
id INTEGER PRIMARY KEY AUTOINCREMENT,
session_id TEXT NOT NULL UNIQUE,
name TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS archives (
id INTEGER PRIMARY KEY AUTOINCREMENT,
archive_id TEXT NOT NULL UNIQUE,
session_id TEXT NOT NULL,
name TEXT,
chain_count INTEGER NOT NULL DEFAULT 0,
message_count INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS chains (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chain_id TEXT NOT NULL UNIQUE,
session_id TEXT,
archive_id TEXT,
model TEXT NOT NULL,
system_prompt TEXT,
input_tokens INTEGER NOT NULL DEFAULT 0,
output_tokens INTEGER NOT NULL DEFAULT 0,
iterations INTEGER NOT NULL DEFAULT 0,
stop_reason TEXT,
duration_ms INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
chain_id TEXT NOT NULL,
role TEXT NOT NULL,
type TEXT NOT NULL,
content TEXT,
tool_name TEXT,
tool_call_id TEXT,
is_error INTEGER NOT NULL DEFAULT 0,
iteration INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
FOREIGN KEY (chain_id) REFERENCES chains(chain_id)
);
CREATE TABLE IF NOT EXISTS agents (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_id TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
avatar TEXT,
description TEXT,
model TEXT NOT NULL DEFAULT 'sonnet',
persona TEXT,
instructions TEXT,
is_deleted INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_chains_session ON chains(session_id);
CREATE INDEX IF NOT EXISTS idx_chains_archive ON chains(archive_id);
CREATE INDEX IF NOT EXISTS idx_messages_chain ON messages(chain_id);
");
conn.Execute(@"
CREATE TABLE IF NOT EXISTS attachments (
id INTEGER PRIMARY KEY AUTOINCREMENT,
message_id INTEGER NOT NULL,
type TEXT NOT NULL,
media_type TEXT NOT NULL,
file_name TEXT,
data TEXT NOT NULL,
created_at TEXT NOT NULL,
FOREIGN KEY (message_id) REFERENCES messages(id)
);
CREATE INDEX IF NOT EXISTS idx_attachments_message ON attachments(message_id);
");
// Migrations
TryAddColumn(conn, "sessions", "agent_id", "TEXT");
TryAddColumn(conn, "sessions", "description", "TEXT");
TryAddColumn(conn, "sessions", "image", "TEXT");
TryAddColumn(conn, "sessions", "workspace", "TEXT");
TryAddColumn(conn, "sessions", "context", "TEXT");
TryAddColumn(conn, "sessions", "is_deleted", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "sessions", "custom_commands", "TEXT");
TryAddColumn(conn, "chains", "agent_id", "TEXT");
TryAddColumn(conn, "chains", "cache_creation_tokens", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "chains", "cache_read_tokens", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "chains", "tool_choice", "TEXT");
TryAddColumn(conn, "chains", "tool_names", "TEXT");
TryAddColumn(conn, "messages", "attachment_count", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "messages", "attachment_names", "TEXT");
TryAddColumn(conn, "agents", "tool_choice", "TEXT DEFAULT 'any'");
TryAddColumn(conn, "agents", "custom_commands", "TEXT");
TryAddColumn(conn, "messages", "is_marked", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "messages", "thought_signature", "TEXT");
TryAddColumn(conn, "agents", "auto_commit", "INTEGER NOT NULL DEFAULT 0");
// Migration: auto_commit von INTEGER (0/1) zu TEXT ("", "mention", "forbid")
try
{
conn.Execute("UPDATE agents SET auto_commit = '' WHERE auto_commit = '0' OR auto_commit = 0");
conn.Execute("UPDATE agents SET auto_commit = 'enforce' WHERE auto_commit = '1' OR auto_commit = 1");
}
catch { /* already migrated */ }
TryAddColumn(conn, "chains", "stream_error_info", "TEXT");
TryAddColumn(conn, "chains", "workflow_info", "TEXT");
TryAddColumn(conn, "chains", "workflow_run_id", "TEXT");
TryAddColumn(conn, "agents", "use_soul", "INTEGER NOT NULL DEFAULT 1");
TryAddColumn(conn, "agents", "use_identity", "INTEGER NOT NULL DEFAULT 1");
TryAddColumn(conn, "agents", "use_user_info", "INTEGER NOT NULL DEFAULT 1");
TryAddColumn(conn, "agents", "use_memory", "INTEGER NOT NULL DEFAULT 1");
TryAddColumn(conn, "agents", "use_auto_context", "INTEGER NOT NULL DEFAULT 1");
TryAddColumn(conn, "agents", "use_github_search", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "agents", "use_serper_search", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "messages", "deleted_on", "TEXT");
TryAddColumn(conn, "agents", "use_web_search", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "agents", "web_search_version", "TEXT DEFAULT ''");
// Migration: use_web_search (bool) → web_search_version (text)
try
{
conn.Execute("UPDATE agents SET web_search_version = 'web_search_20260209' WHERE use_web_search = 1 AND (web_search_version IS NULL OR web_search_version = '')");
}
catch { /* already migrated */ }
// User-Zuordnung
TryAddColumn(conn, "sessions", "user_id", "TEXT");
TryAddColumn(conn, "sessions", "telegram_bot_token", "TEXT");
TryAddColumn(conn, "sessions", "telegram_webhook_secret", "TEXT");
TryAddColumn(conn, "sessions", "telegram_chat_id", "TEXT");
TryAddColumn(conn, "sessions", "telegram_allowed_chat_ids", "TEXT");
TryAddColumn(conn, "sessions", "telegram_bot_id", "TEXT");
TryAddColumn(conn, "agents", "user_id", "TEXT");
TryAddColumn(conn, "users", "is_admin", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "users", "persona", "TEXT");
TryAddColumn(conn, "users", "settings", "TEXT");
TryAddColumn(conn, "users", "default_path", "TEXT");
TryAddColumn(conn, "sessions", "avatar_scale", "REAL NOT NULL DEFAULT 1.0");
TryAddColumn(conn, "agents", "avatar_scale", "REAL NOT NULL DEFAULT 1.0");
TryAddColumn(conn, "cronjobs", "notify_session_id", "TEXT");
TryAddColumn(conn, "sessions", "review_agent_id", "TEXT");
TryAddColumn(conn, "sessions", "max_review_iterations", "INTEGER NOT NULL DEFAULT 3");
TryAddColumn(conn, "sessions", "avatar_offset_x", "REAL NOT NULL DEFAULT 0");
TryAddColumn(conn, "sessions", "avatar_offset_y", "REAL NOT NULL DEFAULT 0");
TryAddColumn(conn, "agents", "avatar_offset_x", "REAL NOT NULL DEFAULT 0");
TryAddColumn(conn, "agents", "avatar_offset_y", "REAL NOT NULL DEFAULT 0");
TryAddColumn(conn, "sessions", "finalizer_agent_id", "TEXT");
TryAddColumn(conn, "agents", "allow_user_choice", "INTEGER NOT NULL DEFAULT 1");
TryAddColumn(conn, "agents", "task_instruction", "TEXT");
TryAddColumn(conn, "chains", "plan_mode", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "sessions", "preparer_agent_id", "TEXT");
TryAddColumn(conn, "sessions", "unity_mcp_url", "TEXT");
TryAddColumn(conn, "sessions", "unity_mcp_token", "TEXT");
TryAddColumn(conn, "sessions", "mcp_servers", "TEXT");
TryAddColumn(conn, "messages", "tag", "TEXT");
TryAddColumn(conn, "agents", "effort", "TEXT NOT NULL DEFAULT ''");
TryAddColumn(conn, "chains", "effort", "TEXT NOT NULL DEFAULT ''");
TryAddColumn(conn, "agents", "max_output_tokens", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "agents", "disable_thinking", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "sessions", "multi_model_mode", "TEXT");
TryAddColumn(conn, "sessions", "panel_agent_ids", "TEXT");
TryAddColumn(conn, "sessions", "moa_layers", "INTEGER NOT NULL DEFAULT 2");
// Migration: 1M-Suffix entfernen (Mai 2026). Seit März 2026 ist 1M-Context Default
// bei Opus 4.6/4.7/4.8 + Sonnet 4.6 – kein "[1m]"-Suffix mehr nötig.
try
{
conn.Execute("UPDATE agents SET model = REPLACE(model, '[1m]', '') WHERE model LIKE '%[1m]%'");
conn.Execute("UPDATE chains SET model = REPLACE(model, '[1m]', '') WHERE model LIKE '%[1m]%'");
}
catch { /* idempotent */ }
conn.Execute(@"
CREATE TABLE IF NOT EXISTS agent_skills (
id INTEGER PRIMARY KEY AUTOINCREMENT,
agent_id TEXT NOT NULL,
skill_slug TEXT NOT NULL,
created_at TEXT NOT NULL,
UNIQUE(agent_id, skill_slug)
);
CREATE INDEX IF NOT EXISTS idx_agent_skills_agent ON agent_skills(agent_id);
");
conn.Execute(@"
CREATE TABLE IF NOT EXISTS users (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL UNIQUE,
username TEXT NOT NULL UNIQUE COLLATE NOCASE,
password_hash TEXT NOT NULL,
display_name TEXT,
is_admin INTEGER NOT NULL DEFAULT 0,
persona TEXT,
settings TEXT,
default_path TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
");
conn.Execute(@"
CREATE TABLE IF NOT EXISTS telegram_links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
telegram_chat_id INTEGER NOT NULL,
user_id TEXT NOT NULL,
session_id TEXT NOT NULL,
agent_id TEXT,
link_code TEXT,
expires_at TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL,
UNIQUE(telegram_chat_id)
);
CREATE INDEX IF NOT EXISTS idx_telegram_links_code ON telegram_links(link_code);
");
conn.Execute(@"
CREATE TABLE IF NOT EXISTS telegram_bots (
id INTEGER PRIMARY KEY AUTOINCREMENT,
bot_id TEXT NOT NULL UNIQUE,
user_id TEXT NOT NULL,
name TEXT NOT NULL,
bot_token TEXT NOT NULL,
webhook_secret TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_telegram_bots_user ON telegram_bots(user_id);
");
TryAddColumn(conn, "telegram_bots", "is_public", "INTEGER NOT NULL DEFAULT 0");
// Migration: Bestehende Session-Telegram-Daten in telegram_bots überführen
MigrateTelegramBotsFromSessions(conn);
conn.Execute(@"
CREATE TABLE IF NOT EXISTS cronjobs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
cronjob_id TEXT NOT NULL UNIQUE,
user_id TEXT NOT NULL,
session_id TEXT NOT NULL,
name TEXT NOT NULL,
cron_expression TEXT NOT NULL,
prompt TEXT NOT NULL,
notify_session_id TEXT,
is_enabled INTEGER NOT NULL DEFAULT 1,
last_run_at TEXT,
next_run_at TEXT,
last_run_status TEXT,
last_run_error TEXT,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_cronjobs_user ON cronjobs(user_id);
CREATE INDEX IF NOT EXISTS idx_cronjobs_next_run ON cronjobs(next_run_at);
");
conn.Execute(@"
CREATE TABLE IF NOT EXISTS shared_messages (
id INTEGER PRIMARY KEY AUTOINCREMENT,
share_token TEXT NOT NULL UNIQUE,
message_id INTEGER NOT NULL,
user_id TEXT NOT NULL,
include_context INTEGER NOT NULL DEFAULT 0,
expires_at TEXT,
created_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_shared_messages_token ON shared_messages(share_token);
CREATE INDEX IF NOT EXISTS idx_shared_messages_message ON shared_messages(message_id);
");
conn.Execute(@"
CREATE TABLE IF NOT EXISTS api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
key_id TEXT NOT NULL UNIQUE,
provider TEXT NOT NULL,
name TEXT NOT NULL,
api_key TEXT NOT NULL,
is_active INTEGER NOT NULL DEFAULT 0,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE INDEX IF NOT EXISTS idx_api_keys_provider ON api_keys(provider);
");
// Migration: AnthropicToken aus appsettings.Production.json → api_keys Tabelle
MigrateAnthropicTokenFromConfig(conn);
conn.Execute(@"
CREATE TABLE IF NOT EXISTS scanned_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
scanned_key_id TEXT NOT NULL UNIQUE,
api_key TEXT NOT NULL,
source_url TEXT,
status TEXT NOT NULL DEFAULT 'unchecked',
plan TEXT NOT NULL DEFAULT 'Unknown',
usage_7d REAL,
usage_5h REAL,
scanned_at TEXT NOT NULL,
checked_at TEXT,
deleted INTEGER NOT NULL DEFAULT 0
);
CREATE INDEX IF NOT EXISTS idx_scanned_keys_api_key ON scanned_keys(api_key);
CREATE INDEX IF NOT EXISTS idx_scanned_keys_status ON scanned_keys(status);
");
conn.Execute(@"
CREATE TABLE IF NOT EXISTS scanner_logs (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL,
type TEXT NOT NULL,
message TEXT NOT NULL,
scanner TEXT NOT NULL DEFAULT 'event'
);
CREATE INDEX IF NOT EXISTS idx_scanner_logs_timestamp ON scanner_logs(timestamp);
CREATE INDEX IF NOT EXISTS idx_scanner_logs_type ON scanner_logs(type);
");
TryAddColumn(conn, "agents", "avatar_small", "TEXT");
TryAddColumn(conn, "agents", "is_public", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "sessions", "image_small", "TEXT");
TryAddColumn(conn, "scanned_keys", "deleted", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "scanned_keys", "adopted_api_key_id", "INTEGER");
TryAddColumn(conn, "scanned_keys", "source", "TEXT");
TryAddColumn(conn, "scanned_keys", "type", "TEXT NOT NULL DEFAULT 'anthropic_oauth'");
// api_keys Metadaten-Spalten
TryAddColumn(conn, "api_keys", "status", "TEXT");
TryAddColumn(conn, "api_keys", "plan", "TEXT");
TryAddColumn(conn, "api_keys", "usage_7d", "REAL");
TryAddColumn(conn, "api_keys", "usage_5h", "REAL");
TryAddColumn(conn, "api_keys", "checked_at", "TEXT");
TryAddColumn(conn, "api_keys", "source_url", "TEXT");
TryAddColumn(conn, "api_keys", "is_public", "INTEGER NOT NULL DEFAULT 0");
// OAuth-spezifische Spalten
TryAddColumn(conn, "api_keys", "refresh_token", "TEXT");
TryAddColumn(conn, "api_keys", "access_token_expires_at", "TEXT");
TryAddColumn(conn, "api_keys", "oauth_provider", "TEXT");
// users: ausgewählter API-Key (legacy, wird durch agent_api_keys ersetzt)
TryAddColumn(conn, "users", "selected_api_key_id", "TEXT");
// users: Berechtigungen (Default: true für Abwärtskompatibilität)
TryAddColumn(conn, "users", "can_create_agents", "INTEGER NOT NULL DEFAULT 1");
TryAddColumn(conn, "users", "can_use_cli", "INTEGER NOT NULL DEFAULT 1");
conn.Execute(@"
CREATE TABLE IF NOT EXISTS agent_api_keys (
id INTEGER PRIMARY KEY AUTOINCREMENT,
user_id TEXT NOT NULL,
agent_id TEXT NOT NULL,
key_id TEXT NOT NULL,
UNIQUE(user_id, agent_id)
);
CREATE INDEX IF NOT EXISTS idx_agent_api_keys_user ON agent_api_keys(user_id);
");
// Migration: selected_api_key_id → agent_api_keys (einmalig)
MigrateSelectedApiKeysToAgentKeys(conn);
// Migration: Bestehende PNG/JPEG-Avatare/Images zu WebP konvertieren
MigrateImagesToWebP(conn);
// Migration: Small-Thumbnails für bestehende Bilder generieren
MigrateImageThumbnails(conn);
}
private void MigrateSelectedApiKeysToAgentKeys(SqliteConnection conn)
{
// Nur migrieren wenn agent_api_keys leer ist und users mit selected_api_key_id existieren
var count = conn.ExecuteScalar<int>("SELECT COUNT(*) FROM agent_api_keys");
if (count > 0) return;
var usersWithKeys = conn.Query<(string UserId, string SelectedApiKeyId)>(
"SELECT user_id, selected_api_key_id FROM users WHERE selected_api_key_id IS NOT NULL AND selected_api_key_id != ''")
.ToList();
foreach (var (userId, keyId) in usersWithKeys)
{
// Alle Agents des Users + öffentliche Agents
var agentIds = conn.Query<string>(
"SELECT agent_id FROM agents WHERE user_id = @userId OR is_public = 1",
new { userId }).ToList();
// Alle Sessions des Users mit Agent → deren Agents auch berücksichtigen
var sessionAgentIds = conn.Query<string>(
"SELECT DISTINCT agent_id FROM sessions WHERE user_id = @userId AND agent_id IS NOT NULL AND agent_id != ''",
new { userId }).ToList();
var allAgentIds = agentIds.Union(sessionAgentIds).Distinct().ToList();
foreach (var agentId in allAgentIds)
{
try
{
conn.Execute(
"INSERT OR IGNORE INTO agent_api_keys (user_id, agent_id, key_id) VALUES (@userId, @agentId, @keyId)",
new { userId, agentId, keyId });
}
catch { /* UNIQUE constraint – already exists */ }
}
}
}
private void MigrateImagesToWebP(SqliteConnection conn)
{
const int webPQuality = 80;
// Agent Avatare
var agentImages = conn.Query<(string AgentId, string Avatar)>(
"SELECT agent_id, avatar FROM agents WHERE avatar IS NOT NULL AND avatar != '' AND avatar NOT LIKE 'data:image/webp%'")
.ToList();
foreach (var (agentId, avatar) in agentImages)
{
var converted = ConvertDataUrlToWebP(avatar, webPQuality);
if (converted != null)
conn.Execute("UPDATE agents SET avatar = @converted WHERE agent_id = @agentId", new { converted, agentId });
}
// Session Images
var sessionImages = conn.Query<(string SessionId, string Image)>(
"SELECT session_id, image FROM sessions WHERE image IS NOT NULL AND image != '' AND image NOT LIKE 'data:image/webp%'")
.ToList();
foreach (var (sessionId, image) in sessionImages)
{
var converted = ConvertDataUrlToWebP(image, webPQuality);
if (converted != null)
conn.Execute("UPDATE sessions SET image = @converted WHERE session_id = @sessionId", new { converted, sessionId });
}
if (agentImages.Count > 0 || sessionImages.Count > 0)
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 = ConvertDataUrlToWebP(avatar, smallQuality, maxSide: smallSize);
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 = ConvertDataUrlToWebP(image, smallQuality, maxSide: smallSize);
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");
}
/// <summary>
/// Konvertiert ein DataUrl-Bild zu WebP. Optional Resize wenn maxSide > 0.
/// </summary>
private static string ConvertDataUrlToWebP(string dataUrl, int quality, int maxSide = 0)
{
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 (maxSide > 0 && (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 void TryAddColumn(SqliteConnection conn, string table, string column, string type)
{
try { conn.Execute($"ALTER TABLE {table} ADD COLUMN {column} {type}"); }
catch { /* column already exists */ }
}
}