From 122000487ce5e40980115ea28c1eb9d0e23d09d7 Mon Sep 17 00:00:00 2001 From: Meleki Date: Sat, 14 Mar 2026 13:06:44 +0100 Subject: [PATCH] Refactor: Server-Settings von appsettings.json in DB-Tabelle (server_settings) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Neuer ServerSettingsService: zentrale Settings in SQLite mit In-Memory-Cache - Auto-Migration: bestehende appsettings.Production.json wird einmalig in DB übernommen - Alle Services umgestellt: IConfiguration → ServerSettingsService (dynamisch, kein Neustart) - Port bleibt als einziges Setting in appsettings.Production.json (erfordert Neustart) - ServerSettingsEndpoint: liest/schreibt DB statt JSON-Datei, Port als Sonder-Setting - IConfiguration komplett entfernt aus allen Services (ChatService, ToolService, AnthropicService, OpenAiService, KreaService, TelegramService, KeyScannerService, GitHubEventScannerService) - AuthMiddleware: KreaApiKey dynamisch aus DB - appsettings.json bereinigt: nur noch Port und Logging - Dokumentation aktualisiert (ARCHITECTURE.md, ARCHITECTURE_QUICK.md, README.md) --- ARCHITECTURE.md | 11 +- ARCHITECTURE_QUICK.md | 11 +- Endpoints/ChatEndpoint.cs | 5 +- Endpoints/ServerSettingsEndpoint.cs | 182 +++++++------------- Middleware/AuthMiddleware.cs | 12 +- Program.cs | 35 ++-- README.md | 11 +- Services/AnthropicService.cs | 12 +- Services/ChatService.cs | 19 ++- Services/GitHubEventScannerService.cs | 33 ++-- Services/KeyScannerService.cs | 29 +--- Services/KreaService.cs | 8 +- Services/OpenAiService.cs | 13 +- Services/ServerSettingsService.cs | 236 ++++++++++++++++++++++++++ Services/TelegramService.cs | 8 +- Services/ToolService.cs | 17 +- appsettings.json | 15 +- wwwroot/index.html | 2 - 18 files changed, 389 insertions(+), 270 deletions(-) create mode 100644 Services/ServerSettingsService.cs diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index e79bfcc..4e28dbe 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1812,16 +1812,7 @@ Die Anwendung ist als systemd-Service konfiguriert und läuft standardmäßig au ```json { - "Port": 19000, // Server-Port (Default 19000, erfordert Neustart) - "MaxIterations": 100, // Max Tool-Loop Iterationen pro Chat - "MaxHistory": 120, // Max Messages in Chat-History - "ReminderInterval": 30, // System-Prompt-Reminder alle N Messages - "ExecuteTimeout": 180, // Timeout für Shell-Befehle in Sekunden - "PayloadLogging": false, // API-Payloads loggen (Debug) - "PayloadLoggingGzip": false, // Payload-Logs als .gz komprimieren - "KreaToken": "", // Krea API Token (optional) - "KreaApiKey": "", // API-Key für Krea-Endpoints ohne User-Auth (optional) - "GeminiApiKey": "" // Google Gemini API Key (kostenlos via aistudio.google.com/apikey) + "Port": 19000 // Server-Port (Default 19000, erfordert Neustart) } ``` diff --git a/ARCHITECTURE_QUICK.md b/ARCHITECTURE_QUICK.md index b88294d..6a48234 100644 --- a/ARCHITECTURE_QUICK.md +++ b/ARCHITECTURE_QUICK.md @@ -275,16 +275,7 @@ User-Input → POST /api/chat (SSE) ```json { - "Port": 19000, // Server-Port (erfordert Neustart) - "MaxIterations": 100, // Max Tool-Loop Iterationen pro Chat - "MaxHistory": 120, // Max Messages in Chat-History - "ReminderInterval": 30, // System-Prompt-Reminder alle N Messages - "ExecuteTimeout": 180, // Timeout für Shell-Befehle in Sekunden - "PayloadLogging": false, // API-Payloads loggen (Debug) - "PayloadLoggingGzip": false, // Payload-Logs als .gz komprimieren - "KreaToken": "", // Krea API Token (optional) - "KreaApiKey": "", // API-Key für Krea-Endpoints ohne User-Auth (optional) - "GeminiApiKey": "" // Google Gemini API Key (optional, kostenlos) + "Port": 19000 // Server-Port (Default 19000, erfordert Neustart) } ``` diff --git a/Endpoints/ChatEndpoint.cs b/Endpoints/ChatEndpoint.cs index 302109a..b51c058 100644 --- a/Endpoints/ChatEndpoint.cs +++ b/Endpoints/ChatEndpoint.cs @@ -10,7 +10,6 @@ using Gott.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Routing; -using Microsoft.Extensions.Configuration; namespace Gott.Endpoints; @@ -22,7 +21,7 @@ public static class ChatEndpoint app.MapPost("/api/chat/stop", HandleStop); } - private static async Task HandleChat(HttpRequest req, HttpResponse res, IConfiguration config, ChatService chatService, DatabaseService db, StreamingTracker streamingTracker, AnthropicService anthropic, OpenAiService openAi, RemoteConnectionManager remoteMgr, EmbeddingBackgroundService embeddingBg, CancellationToken cancellationToken) + private static async Task HandleChat(HttpRequest req, HttpResponse res, ServerSettingsService settings, ChatService chatService, DatabaseService db, StreamingTracker streamingTracker, AnthropicService anthropic, OpenAiService openAi, RemoteConnectionManager remoteMgr, EmbeddingBackgroundService embeddingBg, CancellationToken cancellationToken) { var body = await JsonSerializer.DeserializeAsync(req.Body); if (body == null) @@ -61,7 +60,7 @@ public static class ChatEndpoint return; } - var maxIterations = config.GetValue("MaxIterations", 100); + var maxIterations = settings.GetInt("MaxIterations", 100); var hasRemote = remoteMgr.GetBySessionId(sessionId) != null; var ctx = await chatService.PrepareContext(sessionId, userId, maxIterations, planMode: body.PlanMode, hasRemoteConnection: hasRemote); diff --git a/Endpoints/ServerSettingsEndpoint.cs b/Endpoints/ServerSettingsEndpoint.cs index f45b153..bc165c7 100644 --- a/Endpoints/ServerSettingsEndpoint.cs +++ b/Endpoints/ServerSettingsEndpoint.cs @@ -5,6 +5,7 @@ using System.Text.Json; using System.Text.Json.Nodes; using System.Threading.Tasks; using Gott.Extensions; +using Gott.Services; using Microsoft.AspNetCore.Builder; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Configuration; @@ -13,42 +14,13 @@ namespace Gott.Endpoints; public static class ServerSettingsEndpoint { - // Pfade werden beim MapEndpoints gesetzt – basierend auf ContentRootPath, - // da .NET die Config-Dateien vom ContentRoot lädt (= WorkingDirectory), - // nicht von AppContext.BaseDirectory (= Executable-Verzeichnis). - private static string ProductionConfigPath; - private static string FallbackConfigPath; - - // Definiert alle konfigurierbaren Properties mit Typ und Standardwert - private static readonly List Definitions = new List - { - new SettingDefinition("Port", "int", 19000, "Server-Port (erfordert Neustart)", "number"), - new SettingDefinition("MaxIterations", "int", 100, "Max Tool-Loop Iterationen pro Chat", "number"), - new SettingDefinition("MaxHistory", "int", 120, "Max History-Messages pro Request", "number"), - new SettingDefinition("ReminderInterval", "int", 30, "System-Prompt Reminder alle N Messages (0 = deaktiviert)", "number"), - new SettingDefinition("ExecuteTimeout", "int", 180, "Timeout für Shell-Befehle in Sekunden", "number"), - new SettingDefinition("PayloadLogging", "bool", false, "API-Payloads loggen (Debug)", "toggle"), - new SettingDefinition("PayloadLoggingGzip", "bool", false, "Payload-Logs als .gz komprimieren", "toggle"), - new SettingDefinition("KreaToken", "string", "", "Krea API Token", "password"), - new SettingDefinition("KreaApiKey", "string", "", "API-Key für Krea-Endpoints (Automation)", "password"), - new SettingDefinition("GitHubToken", "string", "", "GitHub PAT für Key-Scanner", "password"), - new SettingDefinition("SerperApiKey", "string", "", "Serper.dev API-Key für Google-Suche (Key-Scanner)", "password"), - new SettingDefinition("ProxyUrl", "string", "", "HTTP/SOCKS Proxy für alle Requests (z.B. socks5://127.0.0.1:1080)", "text"), - new SettingDefinition("ScanIntervalHours", "int", 0, "Key-Scanner Intervall in Stunden (0 = nur manuell)", "number"), - new SettingDefinition("GitHubEventScanner", "bool", false, "GitHub Event Scanner (Echtzeit-Monitoring öffentlicher Pushes)", "toggle"), - }; - public static void MapServerSettingsEndpoints(this WebApplication app) { - var contentRoot = app.Environment.ContentRootPath; - ProductionConfigPath = Path.Combine(contentRoot, "appsettings.Production.json"); - FallbackConfigPath = Path.Combine(contentRoot, "appsettings.json"); - app.MapGet("/api/admin/server-settings", HandleGet); app.MapPut("/api/admin/server-settings", HandleUpdate); } - private static async Task HandleGet(HttpContext ctx, IConfiguration config) + private static async Task HandleGet(HttpContext ctx, ServerSettingsService settings, IConfiguration config) { if (!ctx.IsAdmin()) { @@ -57,54 +29,25 @@ public static class ServerSettingsEndpoint return; } - // Production-Config lesen (falls vorhanden) - JsonObject prodConfig = null; - if (File.Exists(ProductionConfigPath)) + // Port aus appsettings (nicht DB) – als erstes Feld + var port = config.GetValue("Port", 19000); + var allSettings = new List { - var json = await File.ReadAllTextAsync(ProductionConfigPath); - prodConfig = JsonNode.Parse(json) as JsonObject; - } - - // Fallback-Config lesen - JsonObject fallbackConfig = null; - if (File.Exists(FallbackConfigPath)) - { - var json = await File.ReadAllTextAsync(FallbackConfigPath); - fallbackConfig = JsonNode.Parse(json) as JsonObject; - } - - var settings = new List(); - - foreach (var def in Definitions) - { - object value = def.DefaultValue; - - // Fallback: appsettings.json - if (fallbackConfig != null && fallbackConfig.ContainsKey(def.Key)) + new { - value = ExtractValue(fallbackConfig[def.Key], def.Type); + key = "Port", + type = "int", + value = (object)port, + label = "Server-Port (erfordert Neustart)", + inputType = "number", } + }; + allSettings.AddRange(settings.GetAll()); - // Override: appsettings.Production.json - if (prodConfig != null && prodConfig.ContainsKey(def.Key)) - { - value = ExtractValue(prodConfig[def.Key], def.Type); - } - - settings.Add(new - { - key = def.Key, - type = def.Type, - value = value, - label = def.Label, - inputType = def.InputType, - }); - } - - await ctx.Response.WriteAsJsonAsync(new { settings }); + await ctx.Response.WriteAsJsonAsync(new { settings = allSettings }); } - private static async Task HandleUpdate(HttpContext ctx) + private static async Task HandleUpdate(HttpContext ctx, ServerSettingsService settings, IConfiguration config) { if (!ctx.IsAdmin()) { @@ -114,75 +57,68 @@ public static class ServerSettingsEndpoint } var body = await JsonSerializer.DeserializeAsync(ctx.Request.Body); + var hint = ""; - // Bestehende Production-Config laden oder neue erstellen - JsonObject prodConfig; - if (File.Exists(ProductionConfigPath)) + // Port separat behandeln – in appsettings.Production.json speichern + if (body.TryGetProperty("Port", out var portProp) && portProp.TryGetInt32(out var newPort)) { - var existing = await File.ReadAllTextAsync(ProductionConfigPath); - prodConfig = JsonNode.Parse(existing) as JsonObject ?? new JsonObject(); - } - else - { - prodConfig = new JsonObject(); + var currentPort = config.GetValue("Port", 19000); + if (newPort != currentPort && newPort > 0 && newPort <= 65535) + { + SavePortToAppSettings(ctx, newPort); + hint = "Port geändert – Neustart erforderlich!"; + } } - // Nur definierte Keys akzeptieren - foreach (var def in Definitions) + // Alle anderen Settings in DB speichern + foreach (var def in ServerSettingsService.Definitions) { if (!body.TryGetProperty(def.Key, out var prop)) continue; - // Alten Wert entfernen falls vorhanden - prodConfig.Remove(def.Key); - switch (def.Type) { case "string": - prodConfig.Add(def.Key, prop.GetString() ?? ""); + settings.Set(def.Key, prop.GetString() ?? ""); break; case "int": - prodConfig.Add(def.Key, prop.TryGetInt32(out var i) ? i : (int)def.DefaultValue); + settings.SetInt(def.Key, prop.TryGetInt32(out var i) ? i : 0); break; case "bool": - prodConfig.Add(def.Key, prop.ValueKind == JsonValueKind.True); + settings.SetBool(def.Key, prop.ValueKind == JsonValueKind.True); break; } } + await ctx.Response.WriteAsJsonAsync(new { ok = true, hint = string.IsNullOrEmpty(hint) ? "Einstellungen gespeichert" : hint }); + } + + private static void SavePortToAppSettings(HttpContext ctx, int port) + { + // appsettings.Production.json im ContentRootPath (= publish-Verzeichnis) + var contentRoot = ctx.RequestServices.GetService(typeof(Microsoft.AspNetCore.Hosting.IWebHostEnvironment)) as Microsoft.AspNetCore.Hosting.IWebHostEnvironment; + var path = Path.Combine(contentRoot?.ContentRootPath ?? AppContext.BaseDirectory, "appsettings.Production.json"); + + JsonObject obj; + if (File.Exists(path)) + { + try + { + var existing = File.ReadAllText(path); + obj = JsonNode.Parse(existing) as JsonObject ?? new JsonObject(); + } + catch + { + obj = new JsonObject(); + } + } + else + { + obj = new JsonObject(); + } + + obj["Port"] = port; + var options = new JsonSerializerOptions { WriteIndented = true }; - var json = prodConfig.ToJsonString(options); - await File.WriteAllTextAsync(ProductionConfigPath, json); - - await ctx.Response.WriteAsJsonAsync(new { ok = true, hint = "Die Änderungen erfordern einen Neustart des Servers." }); - } - - private static object ExtractValue(JsonNode node, string type) - { - if (node == null) return null; - switch (type) - { - case "string": return node.GetValue(); - case "int": return node.GetValue(); - case "bool": return node.GetValue(); - default: return node.ToString(); - } - } - - private class SettingDefinition - { - public string Key { get; } - public string Type { get; } - public object DefaultValue { get; } - public string Label { get; } - public string InputType { get; } - - public SettingDefinition(string key, string type, object defaultValue, string label, string inputType) - { - Key = key; - Type = type; - DefaultValue = defaultValue; - Label = label; - InputType = inputType; - } + File.WriteAllText(path, obj.ToJsonString(options)); } } diff --git a/Middleware/AuthMiddleware.cs b/Middleware/AuthMiddleware.cs index 0716ce0..a0f5c23 100644 --- a/Middleware/AuthMiddleware.cs +++ b/Middleware/AuthMiddleware.cs @@ -1,19 +1,18 @@ using System.Threading.Tasks; using Gott.Services; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Configuration; namespace Gott.Middleware; public class AuthMiddleware { private readonly RequestDelegate _next; - private readonly string _kreaApiKey; + private readonly ServerSettingsService _settings; - public AuthMiddleware(RequestDelegate next, IConfiguration config) + public AuthMiddleware(RequestDelegate next, ServerSettingsService settings) { _next = next; - _kreaApiKey = config["KreaApiKey"] ?? ""; + _settings = settings; } public async Task InvokeAsync(HttpContext ctx, AuthService auth, DatabaseService db) @@ -34,10 +33,11 @@ public class AuthMiddleware } // Krea API-Key auth (for automation without user context) - if (path.StartsWith("/api/images/") && !string.IsNullOrEmpty(_kreaApiKey)) + var kreaApiKey = _settings.Get("KreaApiKey"); + if (path.StartsWith("/api/images/") && !string.IsNullOrEmpty(kreaApiKey)) { var apiKey = ctx.Request.Headers["X-Api-Key"].ToString(); - if (!string.IsNullOrEmpty(apiKey) && apiKey == _kreaApiKey) + if (!string.IsNullOrEmpty(apiKey) && apiKey == kreaApiKey) { await _next(ctx); return; diff --git a/Program.cs b/Program.cs index 26dc018..ecb1a69 100644 --- a/Program.cs +++ b/Program.cs @@ -16,7 +16,18 @@ var builder = WebApplication.CreateBuilder(args); if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) builder.Services.AddWindowsService(); -var proxyUrl = builder.Configuration["ProxyUrl"]; +var dataDir = Path.Combine(AppContext.BaseDirectory, "data"); +Directory.CreateDirectory(dataDir); + +var dbPath = Path.Combine(dataDir, "gott.db"); + +// ServerSettingsService – zentrale Settings aus DB +var serverSettings = new ServerSettingsService(dbPath); +serverSettings.MigrateFromAppSettings(builder.Environment.ContentRootPath); +builder.Services.AddSingleton(serverSettings); + +// Proxy (erfordert Neustart – wird nur beim Start gelesen) +var proxyUrl = serverSettings.Get("ProxyUrl"); if (!string.IsNullOrWhiteSpace(proxyUrl)) { builder.Services.AddHttpClient().ConfigureHttpClientDefaults(b => @@ -31,10 +42,6 @@ else builder.Services.AddHttpClient(); } -var dataDir = Path.Combine(AppContext.BaseDirectory, "data"); -Directory.CreateDirectory(dataDir); - -var dbPath = Path.Combine(dataDir, "gott.db"); var authService = new AuthService(dbPath); var dbService = new DatabaseService(dbPath); dbService.EnsureDefaultUser(authService); @@ -50,11 +57,9 @@ builder.Services.AddSingleton(memoryService); builder.Services.AddSingleton(skillService); // Shared Services -var toolService = new ToolService(builder.Configuration); +var toolService = new ToolService(serverSettings); builder.Services.AddSingleton(toolService); -var maxHistory = builder.Configuration.GetValue("MaxHistory", 120); -var reminderInterval = builder.Configuration.GetValue("ReminderInterval", 30); -var chatService = new ChatService(dbService, toolService, memoryService, skillService, maxHistory, reminderInterval); +var chatService = new ChatService(dbService, toolService, memoryService, skillService, serverSettings); builder.Services.AddSingleton(chatService); // Embedding-Services (lokal, ONNX) @@ -67,14 +72,12 @@ builder.Services.AddSingleton(embeddingBgService); builder.Services.AddHostedService(sp => sp.GetRequiredService()); // AnthropicService (Singleton – Token wird pro Request vom User aufgelöst) -var payloadLogging = builder.Configuration.GetValue("PayloadLogging", false); -var payloadLoggingGzip = builder.Configuration.GetValue("PayloadLoggingGzip", false); builder.Services.AddSingleton(sp => { var svc = new AnthropicService( sp.GetRequiredService(), toolService, dbService, memoryService, - payloadLogging, payloadLoggingGzip, + serverSettings, vectorSearch: vectorSearchService, embeddingBg: embeddingBgService); svc.SetChatService(chatService); @@ -83,21 +86,19 @@ builder.Services.AddSingleton(sp => // OpenAI-kompatibler Service (Nvidia etc.) builder.Services.AddSingleton(sp => - new OpenAiService(sp.GetRequiredService(), dbService, sp.GetRequiredService(), builder.Configuration, dataDir)); + new OpenAiService(sp.GetRequiredService(), dbService, sp.GetRequiredService(), serverSettings, dataDir)); // Telegram Bot (pro Session – Token aus Session-Config) -var maxIterations = builder.Configuration.GetValue("MaxIterations", 100); builder.Services.AddSingleton(sp => new TelegramService( sp.GetRequiredService(), dbService, chatService, sp.GetRequiredService(), - maxIterations)); + serverSettings)); // Krea Image Generation -var kreaToken = builder.Configuration["KreaToken"] ?? ""; builder.Services.AddSingleton(sp => - new KreaService(sp.GetRequiredService(), kreaToken, dataDir)); + new KreaService(sp.GetRequiredService(), serverSettings, dataDir)); // Cronjob-Service (BackgroundService) builder.Services.AddSingleton(); diff --git a/README.md b/README.md index 69b2d84..5fe0f3b 100644 --- a/README.md +++ b/README.md @@ -66,16 +66,7 @@ Beim ersten Start wird `appsettings.json` mit Standardwerten verwendet. Für eig ```json { - "Port": 19000, - "MaxIterations": 100, - "MaxHistory": 120, - "ReminderInterval": 30, - "ExecuteTimeout": 180, - "PayloadLogging": false, - "PayloadLoggingGzip": false, - "KreaToken": "", - "KreaApiKey": "", - "GeminiApiKey": "" + "Port": 19000 } ``` diff --git a/Services/AnthropicService.cs b/Services/AnthropicService.cs index 96d3855..8aaa9ab 100644 --- a/Services/AnthropicService.cs +++ b/Services/AnthropicService.cs @@ -19,21 +19,19 @@ public class AnthropicService private readonly ToolService _toolService; private readonly DatabaseService _db; private readonly MemoryService _memoryService; - private readonly bool _payloadLogging; - private readonly bool _payloadLoggingGzip; + private readonly ServerSettingsService _settings; private readonly VectorSearchService _vectorSearch; private readonly EmbeddingBackgroundService _embeddingBg; private ChatService _chatService; private TelegramService _telegramService; - public AnthropicService(IHttpClientFactory httpFactory, ToolService toolService, DatabaseService db, MemoryService memoryService = null, bool payloadLogging = false, bool payloadLoggingGzip = false, VectorSearchService vectorSearch = null, EmbeddingBackgroundService embeddingBg = null) + public AnthropicService(IHttpClientFactory httpFactory, ToolService toolService, DatabaseService db, MemoryService memoryService = null, ServerSettingsService settings = null, VectorSearchService vectorSearch = null, EmbeddingBackgroundService embeddingBg = null) { _httpFactory = httpFactory; _toolService = toolService; _db = db; _memoryService = memoryService; - _payloadLogging = payloadLogging; - _payloadLoggingGzip = payloadLoggingGzip; + _settings = settings; _vectorSearch = vectorSearch; _embeddingBg = embeddingBg; } @@ -1201,7 +1199,7 @@ public class AnthropicService private void LogPayload(string json, string sessionId, string chainId, int iteration) { - if (!_payloadLogging) return; + if (_settings == null || !_settings.GetBool("PayloadLogging", false)) return; try { var logsDir = Path.Combine(AppContext.BaseDirectory, "data", "logs"); @@ -1209,7 +1207,7 @@ public class AnthropicService var timestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var baseName = $"{timestamp}-{sessionId}-{chainId}-iter{iteration}-payload"; - if (_payloadLoggingGzip) + if (_settings?.GetBool("PayloadLoggingGzip", false) ?? false) { var filePath = Path.Combine(logsDir, baseName + ".json.gz"); using var fs = File.Create(filePath); diff --git a/Services/ChatService.cs b/Services/ChatService.cs index 80bf8d7..bfcdefc 100644 --- a/Services/ChatService.cs +++ b/Services/ChatService.cs @@ -14,20 +14,21 @@ public class ChatService private readonly ToolService _toolService; private readonly MemoryService _memoryService; private readonly SkillService _skillService; - private readonly int _maxHistory; - private readonly int _reminderInterval; + private readonly ServerSettingsService _settings; public ChatService(DatabaseService db, ToolService toolService, MemoryService memoryService, SkillService skillService, - int maxHistory = 120, int reminderInterval = 30) + ServerSettingsService settings) { _db = db; _toolService = toolService; _memoryService = memoryService; _skillService = skillService; - _maxHistory = maxHistory; - _reminderInterval = reminderInterval; + _settings = settings; } + private int MaxHistory => _settings.GetInt("MaxHistory", 120); + private int ReminderInterval => _settings.GetInt("ReminderInterval", 30); + public static string ResolveModelId(string model) { // OpenAI-kompatible Provider werden unverändert durchgereicht (Format: "provider:model") @@ -487,9 +488,9 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop? // History auf maximal N Messages begrenzen (älteste entfernen) // WICHTIG: Nicht mitten in einem tool_use/tool_result-Paar abschneiden! - if (history.Count > _maxHistory) + if (history.Count > MaxHistory) { - int removeCount = history.Count - _maxHistory; + int removeCount = history.Count - MaxHistory; // Nach vorne korrigieren bis wir an einer sicheren Stelle sind: // Nicht abschneiden wenn die erste verbleibende Message ein tool_result ist // oder die letzte entfernte Message ein assistant mit tool_use ist @@ -550,7 +551,7 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop? // Reminder einfügen: alle ~30 user/assistant-Paare ein Reminder-Paar einfügen var reminder = SystemPromptBuilder.BuildReminder(promptBlocks); - if (!string.IsNullOrWhiteSpace(reminder) && _reminderInterval > 0 && history.Count > _reminderInterval) + if (!string.IsNullOrWhiteSpace(reminder) && ReminderInterval > 0 && history.Count > ReminderInterval) { var reminderUser = new Dictionary { @@ -569,7 +570,7 @@ Für welche Plattform soll die App entwickelt werden? Web, Mobile oder Desktop? for (int i = 0; i < history.Count; i++) { messageCount++; - if (messageCount > 0 && messageCount % _reminderInterval == 0) + if (messageCount > 0 && messageCount % ReminderInterval == 0) { // Position nach dem aktuellen Element – aber nur einfügen wenn es eine // assistant-Message ist (damit user/assistant-Alternierung nicht bricht) diff --git a/Services/GitHubEventScannerService.cs b/Services/GitHubEventScannerService.cs index eb00c1b..a0dd679 100644 --- a/Services/GitHubEventScannerService.cs +++ b/Services/GitHubEventScannerService.cs @@ -8,7 +8,6 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Gott.Models; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -23,8 +22,10 @@ public class GitHubEventScannerService : BackgroundService private readonly DatabaseService _db; private readonly IHttpClientFactory _httpFactory; private readonly ILogger _logger; - private readonly string _githubToken; - private readonly bool _enabled; + private readonly ServerSettingsService _settings; + + private string _githubToken => _settings.Get("GitHubToken"); + private bool _enabled => _settings.GetBool("GitHubEventScanner", false); private const int PollIntervalMs = 30_000; // 30 Sekunden (häufiger pollen, weniger verpassen) private const int MaxCommitsPerPoll = 30; // Max Commit-Diffs pro Poll-Zyklus @@ -90,31 +91,18 @@ public class GitHubEventScannerService : BackgroundService public GitHubEventScannerService( DatabaseService db, IHttpClientFactory httpFactory, - IConfiguration config, + ServerSettingsService settings, ILogger logger) { _db = db; _httpFactory = httpFactory; + _settings = settings; _logger = logger; - _githubToken = config["GitHubToken"] ?? ""; - _enabled = config.GetValue("GitHubEventScanner", false); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - if (!_enabled) - { - _logger.LogInformation("GitHubEventScannerService deaktiviert (GitHubEventScanner=false)"); - return; - } - - if (string.IsNullOrWhiteSpace(_githubToken)) - { - _logger.LogWarning("GitHubEventScannerService deaktiviert (kein GitHubToken konfiguriert)"); - return; - } - - _logger.LogInformation("GitHubEventScannerService gestartet (Poll-Intervall: {Interval}s)", PollIntervalMs / 1000); + _logger.LogInformation("GitHubEventScannerService bereit (Enabled wird dynamisch aus Settings gelesen)"); // Kurze Verzögerung nach App-Start await Task.Delay(5_000, stoppingToken); @@ -123,6 +111,13 @@ public class GitHubEventScannerService : BackgroundService while (!stoppingToken.IsCancellationRequested) { + // Dynamisch prüfen ob aktiviert + if (!_enabled || string.IsNullOrWhiteSpace(_githubToken)) + { + lock (_lock) _isRunning = false; + await Task.Delay(10_000, stoppingToken); + continue; + } try { lock (_lock) _isRunning = true; diff --git a/Services/KeyScannerService.cs b/Services/KeyScannerService.cs index aaa9d2f..2ff192d 100644 --- a/Services/KeyScannerService.cs +++ b/Services/KeyScannerService.cs @@ -9,7 +9,6 @@ using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Gott.Models; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; @@ -20,9 +19,11 @@ public class KeyScannerService : BackgroundService private readonly DatabaseService _db; private readonly IHttpClientFactory _httpFactory; private readonly ILogger _logger; - private readonly string _githubToken; - private readonly string _serperApiKey; - private readonly int _intervalHours; + private readonly ServerSettingsService _settings; + + private string _githubToken => _settings.Get("GitHubToken"); + private string _serperApiKey => _settings.Get("SerperApiKey"); + private int _intervalHours => _settings.GetInt("ScanIntervalHours", 0); // Status-Tracking private readonly object _lock = new(); @@ -75,15 +76,13 @@ public class KeyScannerService : BackgroundService public KeyScannerService( DatabaseService db, IHttpClientFactory httpFactory, - IConfiguration config, + ServerSettingsService settings, ILogger logger) { _db = db; _httpFactory = httpFactory; + _settings = settings; _logger = logger; - _githubToken = config["GitHubToken"] ?? ""; - _serperApiKey = config["SerperApiKey"] ?? ""; - _intervalHours = config.GetValue("ScanIntervalHours", 0); } protected override async Task ExecuteAsync(CancellationToken stoppingToken) @@ -163,7 +162,6 @@ public class KeyScannerService : BackgroundService var modeLabel = modeLabels.ContainsKey(mode) ? modeLabels[mode] : "Full"; AddLog("scan", modeLabel + "-Scan gestartet"); var newKeysFound = 0; - var checkedCount = 0; var totalFound = 0; try @@ -230,21 +228,10 @@ public class KeyScannerService : BackgroundService } AddLog("scan", newKeysFound + " neue Key(s) gespeichert"); - // Phase 3: Alle unchecked Keys prüfen - var allScanned = (await _db.GetScannedKeys()).ToList(); - var toCheck = allScanned.Where(k => k.Status == "unchecked").ToList(); - foreach (var sk in toCheck) - { - ct.ThrowIfCancellationRequested(); - await CheckAndUpdateKey(sk); - checkedCount++; - await Task.Delay(500, ct); // Rate-Limit schonen - } - lock (_lock) { _lastRun = DateTime.UtcNow.ToString("o"); - _lastResult = $"{modeLabel}: {totalFound} gefunden, {newKeysFound} neu, {checkedCount} geprüft"; + _lastResult = $"{modeLabel}: {totalFound} gefunden, {newKeysFound} neu"; } AddLog("scan", "Scan abgeschlossen – " + _lastResult); diff --git a/Services/KreaService.cs b/Services/KreaService.cs index 84c68c9..b76fd61 100644 --- a/Services/KreaService.cs +++ b/Services/KreaService.cs @@ -14,9 +14,11 @@ namespace Gott.Services public class KreaService { private readonly IHttpClientFactory _httpFactory; - private readonly string _token; + private readonly ServerSettingsService _settings; private readonly string _imagesDir; + private string _token => _settings.Get("KreaToken"); + private const string BASE_URL = "https://api.krea.ai"; public static readonly Dictionary IMAGE_MODELS = new Dictionary @@ -47,10 +49,10 @@ namespace Gott.Services public bool IsConfigured => !string.IsNullOrWhiteSpace(_token); - public KreaService(IHttpClientFactory httpFactory, string token, string dataDir) + public KreaService(IHttpClientFactory httpFactory, ServerSettingsService settings, string dataDir) { _httpFactory = httpFactory; - _token = token; + _settings = settings; _imagesDir = Path.Combine(dataDir, "images"); Directory.CreateDirectory(_imagesDir); } diff --git a/Services/OpenAiService.cs b/Services/OpenAiService.cs index b5eeb6d..909f637 100644 --- a/Services/OpenAiService.cs +++ b/Services/OpenAiService.cs @@ -156,17 +156,20 @@ public class OpenAiService private List _openaiModels = new List(); private readonly string _cacheFile; - public OpenAiService(IHttpClientFactory httpFactory, DatabaseService db, ToolService toolService, Microsoft.Extensions.Configuration.IConfiguration configuration, string dataDir) + private readonly ServerSettingsService _settings; + + public OpenAiService(IHttpClientFactory httpFactory, DatabaseService db, ToolService toolService, ServerSettingsService settings, string dataDir) { _httpFactory = httpFactory; _db = db; _toolService = toolService; + _settings = settings; _cacheFile = Path.Combine(dataDir, "models_cache.json"); Providers = new Dictionary { ["nvidia"] = ("https://g4f.space/api/nvidia/v1", ""), ["pollinations"] = ("https://text.pollinations.ai/openai", ""), - ["gemini"] = ("https://generativelanguage.googleapis.com/v1beta/openai", configuration["GeminiApiKey"] ?? ""), + ["gemini"] = ("https://generativelanguage.googleapis.com/v1beta/openai", ""), // Key kommt dynamisch aus Settings ["openai"] = ("https://api.openai.com/v1", "") // Key kommt dynamisch aus DB }; } @@ -209,7 +212,7 @@ public class OpenAiService _nvidiaModels = await FetchModels("https://g4f.space/api/nvidia/models", "Nvidia"); _pollinationsModels = await FetchModels("https://text.pollinations.ai/models", "Pollinations"); - var geminiApiKey = Providers["gemini"].ApiKey; + var geminiApiKey = _settings.Get("GeminiApiKey"); if (!string.IsNullOrEmpty(geminiApiKey)) _geminiModels = await FetchModels("https://generativelanguage.googleapis.com/v1beta/openai/models", "Gemini", geminiApiKey); else @@ -363,6 +366,10 @@ public class OpenAiService if (!string.IsNullOrEmpty(apiKeyOverride)) providerConfig = (providerConfig.BaseUrl, apiKeyOverride); + // Gemini: Key dynamisch aus ServerSettings holen (falls nicht per Override gesetzt) + if (provider == "gemini" && string.IsNullOrEmpty(providerConfig.ApiKey)) + providerConfig = (providerConfig.BaseUrl, _settings.Get("GeminiApiKey")); + try { // OpenAI-kompatibles Messages-Format aufbauen diff --git a/Services/ServerSettingsService.cs b/Services/ServerSettingsService.cs new file mode 100644 index 0000000..c67c386 --- /dev/null +++ b/Services/ServerSettingsService.cs @@ -0,0 +1,236 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.Data.Sqlite; + +namespace Gott.Services; + +/// +/// Zentraler Settings-Service. Liest/schreibt aus der DB-Tabelle 'server_settings'. +/// In-Memory-Cache für schnellen Zugriff. Kein Neustart bei Änderungen nötig. +/// +public class ServerSettingsService +{ + private readonly string _dbPath; + private readonly Dictionary _cache = new Dictionary(); + private readonly object _lock = new object(); + + // Definiert alle Settings mit Typ, Default und Beschreibung + public static readonly List Definitions = new List + { + new SettingDefinition( "ProxyUrl", "string", "", "HTTP/SOCKS Proxy (erfordert Neustart)", "text" ), + new SettingDefinition( "MaxIterations", "int", "100", "Max Tool-Loop Iterationen pro Chat", "number" ), + new SettingDefinition( "MaxHistory", "int", "120", "Max History-Messages pro Request", "number" ), + new SettingDefinition( "ReminderInterval", "int", "30", "System-Prompt Reminder alle N Messages (0 = deaktiviert)", "number" ), + new SettingDefinition( "ExecuteTimeout", "int", "180", "Timeout für Shell-Befehle in Sekunden", "number" ), + new SettingDefinition( "PayloadLogging", "bool", "false", "API-Payloads loggen (Debug)", "toggle" ), + new SettingDefinition( "PayloadLoggingGzip", "bool", "false", "Payload-Logs als .gz komprimieren", "toggle" ), + new SettingDefinition( "KreaToken", "string", "", "Krea API Token", "password" ), + new SettingDefinition( "KreaApiKey", "string", "", "API-Key für Krea-Endpoints (Automation)", "password" ), + new SettingDefinition( "GeminiApiKey", "string", "", "Gemini API-Key (für Gemini-Provider im Chat)", "password" ), + new SettingDefinition( "GitHubToken", "string", "", "GitHub PAT für Key-Scanner und Code-Search Tool", "password" ), + new SettingDefinition( "SerperApiKey", "string", "", "Serper.dev API-Key für Google-Suche (Key-Scanner + Tool)", "password" ), + new SettingDefinition( "ScanIntervalHours", "int", "0", "Key-Scanner Intervall in Stunden (0 = nur manuell)", "number" ), + new SettingDefinition( "GitHubEventScanner", "bool", "false", "GitHub Event Scanner (Echtzeit-Monitoring öffentlicher Pushes)", "toggle" ), + }; + + public ServerSettingsService(string dbPath) + { + _dbPath = dbPath; + EnsureTable(); + LoadCache(); + } + + private void EnsureTable() + { + using var conn = new SqliteConnection($"Data Source={_dbPath}"); + conn.Open(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = @" + CREATE TABLE IF NOT EXISTS server_settings ( + key TEXT PRIMARY KEY, + value TEXT NOT NULL DEFAULT '', + type TEXT NOT NULL DEFAULT 'string' + )"; + cmd.ExecuteNonQuery(); + } + + private void LoadCache() + { + lock (_lock) + { + _cache.Clear(); + using var conn = new SqliteConnection($"Data Source={_dbPath}"); + conn.Open(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = "SELECT key, value FROM server_settings"; + using var reader = cmd.ExecuteReader(); + while (reader.Read()) + { + _cache[reader.GetString(0)] = reader.GetString(1); + } + } + } + + /// + /// Migriert bestehende Werte aus appsettings.Production.json in die DB (einmalig). + /// Nur Settings die noch NICHT in der DB sind werden übernommen. + /// + public void MigrateFromAppSettings(string contentRootPath) + { + var prodPath = Path.Combine(contentRootPath, "appsettings.Production.json"); + if (!File.Exists(prodPath)) return; + + try + { + var json = File.ReadAllText(prodPath); + var obj = JsonNode.Parse(json) as JsonObject; + if (obj == null) return; + + // Port bleibt in appsettings – nicht migrieren + var skipKeys = new HashSet { "Port", "Logging", "AllowedHosts" }; + + foreach (var def in Definitions) + { + if (skipKeys.Contains(def.Key)) continue; + if (!obj.ContainsKey(def.Key)) continue; + + // Nur migrieren wenn nicht schon in DB + lock (_lock) + { + if (_cache.ContainsKey(def.Key)) continue; + } + + var node = obj[def.Key]; + string value; + switch (def.Type) + { + case "bool": + value = (node?.GetValue() ?? false) ? "true" : "false"; + break; + case "int": + value = (node?.GetValue() ?? 0).ToString(); + break; + default: + value = node?.GetValue() ?? ""; + break; + } + + // Default-Werte nicht migrieren (kein Müll in DB) + if (value == def.DefaultValue) continue; + + Set(def.Key, value); + } + } + catch + { + // Fehler bei Migration ignorieren – Settings sind halt Default + } + } + + // ─── Getter ──────────────────────────────────────────────── + + public string Get(string key, string defaultValue = "") + { + lock (_lock) + { + return _cache.TryGetValue(key, out var val) ? val : defaultValue; + } + } + + public int GetInt(string key, int defaultValue = 0) + { + var raw = Get(key, null); + return raw != null && int.TryParse(raw, out var v) ? v : defaultValue; + } + + public bool GetBool(string key, bool defaultValue = false) + { + var raw = Get(key, null); + if (raw == null) return defaultValue; + return raw.Equals("true", StringComparison.OrdinalIgnoreCase) || raw == "1"; + } + + // ─── Setter ──────────────────────────────────────────────── + + public void Set(string key, string value) + { + // Typ aus Definitions holen + var def = Definitions.Find(d => d.Key == key); + var type = def?.Type ?? "string"; + + using var conn = new SqliteConnection($"Data Source={_dbPath}"); + conn.Open(); + using var cmd = conn.CreateCommand(); + cmd.CommandText = @" + INSERT INTO server_settings (key, value, type) VALUES (@key, @value, @type) + ON CONFLICT(key) DO UPDATE SET value = @value, type = @type"; + cmd.Parameters.AddWithValue("@key", key); + cmd.Parameters.AddWithValue("@value", value ?? ""); + cmd.Parameters.AddWithValue("@type", type); + cmd.ExecuteNonQuery(); + + lock (_lock) + { + _cache[key] = value ?? ""; + } + } + + public void SetInt(string key, int value) => Set(key, value.ToString()); + public void SetBool(string key, bool value) => Set(key, value ? "true" : "false"); + + // ─── Bulk-Operationen (für Endpoint) ─────────────────────── + + public List GetAll() + { + var result = new List(); + foreach (var def in Definitions) + { + object value; + switch (def.Type) + { + case "int": + value = GetInt(def.Key, int.TryParse(def.DefaultValue, out var dv) ? dv : 0); + break; + case "bool": + value = GetBool(def.Key, def.DefaultValue.Equals("true", StringComparison.OrdinalIgnoreCase)); + break; + default: + value = Get(def.Key, def.DefaultValue); + break; + } + + result.Add(new + { + key = def.Key, + type = def.Type, + value = value, + label = def.Label, + inputType = def.InputType, + }); + } + return result; + } + + // ─── Definition ──────────────────────────────────────────── + + public class SettingDefinition + { + public string Key { get; } + public string Type { get; } + public string DefaultValue { get; } + public string Label { get; } + public string InputType { get; } + + public SettingDefinition(string key, string type, string defaultValue, string label, string inputType) + { + Key = key; + Type = type; + DefaultValue = defaultValue; + Label = label; + InputType = inputType; + } + } +} diff --git a/Services/TelegramService.cs b/Services/TelegramService.cs index 8626041..3618c0a 100644 --- a/Services/TelegramService.cs +++ b/Services/TelegramService.cs @@ -15,20 +15,20 @@ public class TelegramService private readonly DatabaseService _db; private readonly ChatService _chatService; private readonly AnthropicService _anthropic; - private readonly int _maxIterations; + private readonly ServerSettingsService _settings; public TelegramService( IHttpClientFactory httpFactory, DatabaseService db, ChatService chatService, AnthropicService anthropic, - int maxIterations = 100) + ServerSettingsService settings) { _httpFactory = httpFactory; _db = db; _chatService = chatService; _anthropic = anthropic; - _maxIterations = maxIterations; + _settings = settings; } // ── Webhook-Update verarbeiten ── @@ -161,7 +161,7 @@ public class TelegramService private async Task HandleMessage(long chatId, string userText, string botToken, string botUserId, SessionRecord session) { - var ctx = await _chatService.PrepareContext(session.SessionId, botUserId, _maxIterations); + var ctx = await _chatService.PrepareContext(session.SessionId, botUserId, _settings.GetInt("MaxIterations", 100)); if (ctx == null) { await SendMessage(chatId, botToken, "❌ Kein Agent konfiguriert. Bitte weise der Session im Web-UI einen Agent zu."); diff --git a/Services/ToolService.cs b/Services/ToolService.cs index de38ee2..001da3d 100644 --- a/Services/ToolService.cs +++ b/Services/ToolService.cs @@ -10,7 +10,6 @@ using System.Text.Json; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.FileSystemGlobbing; using Microsoft.Extensions.FileSystemGlobbing.Abstractions; @@ -18,12 +17,12 @@ namespace Gott.Services; public class ToolService { - private readonly IConfiguration _config; + private readonly ServerSettingsService _settings; private IHttpClientFactory _httpFactory; - public ToolService(IConfiguration config) + public ToolService(ServerSettingsService settings) { - _config = config; + _settings = settings; } public void SetHttpClientFactory(IHttpClientFactory httpFactory) @@ -31,9 +30,9 @@ public class ToolService _httpFactory = httpFactory; } - private int ExecuteTimeoutSeconds => _config.GetValue("ExecuteTimeout", 180); - public bool HasGitHubToken => !string.IsNullOrWhiteSpace(_config["GitHubToken"]); - public bool HasSerperKey => !string.IsNullOrWhiteSpace(_config["SerperApiKey"]); + private int ExecuteTimeoutSeconds => _settings.GetInt("ExecuteTimeout", 180); + public bool HasGitHubToken => !string.IsNullOrWhiteSpace(_settings.Get("GitHubToken")); + public bool HasSerperKey => !string.IsNullOrWhiteSpace(_settings.Get("SerperApiKey")); public const string AskUserChoiceToolName = "ask_user_choice"; /// @@ -858,7 +857,7 @@ public class ToolService private async Task ExecuteGitHubCodeSearch(JsonElement input, CancellationToken ct) { - var token = _config["GitHubToken"] ?? ""; + var token = _settings.Get("GitHubToken"); if (string.IsNullOrWhiteSpace(token)) return "Fehler: Kein GitHubToken konfiguriert. Bitte in den Server-Einstellungen hinterlegen."; @@ -961,7 +960,7 @@ public class ToolService private async Task ExecuteSerperWebSearch(JsonElement input, CancellationToken ct) { - var apiKey = _config["SerperApiKey"] ?? ""; + var apiKey = _settings.Get("SerperApiKey"); if (string.IsNullOrWhiteSpace(apiKey)) return "Fehler: Kein SerperApiKey konfiguriert. Bitte in den Server-Einstellungen hinterlegen."; diff --git a/appsettings.json b/appsettings.json index 2e7f0d5..9f466c8 100755 --- a/appsettings.json +++ b/appsettings.json @@ -6,18 +6,5 @@ } }, "AllowedHosts": "*", - "Port": 19000, - "MaxIterations": 100, - "MaxHistory": 120, - "ReminderInterval": 30, - "ExecuteTimeout": 180, - "PayloadLogging": false, - "PayloadLoggingGzip": false, - "KreaToken": "", - "KreaApiKey": "", - "GeminiApiKey": "", - "GitHubToken": "", - "SerperApiKey": "", - "ScanIntervalHours": 0, - "ProxyUrl": "" + "Port": 19000 } diff --git a/wwwroot/index.html b/wwwroot/index.html index 91d80b7..dad3276 100644 --- a/wwwroot/index.html +++ b/wwwroot/index.html @@ -541,12 +541,10 @@
-

Änderungen werden in appsettings.Production.json gespeichert. Danach ist ein Neustart erforderlich.

-