Refactor: Server-Settings von appsettings.json in DB-Tabelle (server_settings)

- 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)
This commit is contained in:
Meleki committed 2026-03-14 13:06:44 +01:00
1 parent 542a031172
commit 122000487c
18 files changed
+389 -270

No files matched your search

+1 -10
View File
@@ -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)
}
```
+1 -10
View File
@@ -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)
}
```
+2 -3
View File
@@ -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<ChatRequest>(req.Body);
if (body == null)
@@ -61,7 +60,7 @@ public static class ChatEndpoint
return;
}
var maxIterations = config.GetValue<int>("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);
+59 -123
View File
@@ -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<SettingDefinition> Definitions = new List<SettingDefinition>
{
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<int>("Port", 19000);
var allSettings = new List<object>
{
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<object>();
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<JsonElement>(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<int>("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<string>();
case "int": return node.GetValue<int>();
case "bool": return node.GetValue<bool>();
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));
}
}
+6 -6
View File
@@ -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;
+18 -17
View File
@@ -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<int>("MaxHistory", 120);
var reminderInterval = builder.Configuration.GetValue<int>("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<Gott.Services.EmbeddingBackgroundService>());
// AnthropicService (Singleton – Token wird pro Request vom User aufgelöst)
var payloadLogging = builder.Configuration.GetValue<bool>("PayloadLogging", false);
var payloadLoggingGzip = builder.Configuration.GetValue<bool>("PayloadLoggingGzip", false);
builder.Services.AddSingleton<AnthropicService>(sp =>
{
var svc = new AnthropicService(
sp.GetRequiredService<IHttpClientFactory>(),
toolService, dbService, memoryService,
payloadLogging, payloadLoggingGzip,
serverSettings,
vectorSearch: vectorSearchService,
embeddingBg: embeddingBgService);
svc.SetChatService(chatService);
@@ -83,21 +86,19 @@ builder.Services.AddSingleton<AnthropicService>(sp =>
// OpenAI-kompatibler Service (Nvidia etc.)
builder.Services.AddSingleton<OpenAiService>(sp =>
new OpenAiService(sp.GetRequiredService<IHttpClientFactory>(), dbService, sp.GetRequiredService<ToolService>(), builder.Configuration, dataDir));
new OpenAiService(sp.GetRequiredService<IHttpClientFactory>(), dbService, sp.GetRequiredService<ToolService>(), serverSettings, dataDir));
// Telegram Bot (pro Session – Token aus Session-Config)
var maxIterations = builder.Configuration.GetValue<int>("MaxIterations", 100);
builder.Services.AddSingleton<TelegramService>(sp =>
new TelegramService(
sp.GetRequiredService<IHttpClientFactory>(),
dbService, chatService,
sp.GetRequiredService<AnthropicService>(),
maxIterations));
serverSettings));
// Krea Image Generation
var kreaToken = builder.Configuration["KreaToken"] ?? "";
builder.Services.AddSingleton(sp =>
new KreaService(sp.GetRequiredService<IHttpClientFactory>(), kreaToken, dataDir));
new KreaService(sp.GetRequiredService<IHttpClientFactory>(), serverSettings, dataDir));
// Cronjob-Service (BackgroundService)
builder.Services.AddSingleton<CronjobService>();
+1 -10
View File
@@ -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
}
```
+5 -7
View File
@@ -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);
+10 -9
View File
@@ -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<string, object>
{
@@ -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)
+14 -19
View File
@@ -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<GitHubEventScannerService> _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<GitHubEventScannerService> logger)
{
_db = db;
_httpFactory = httpFactory;
_settings = settings;
_logger = logger;
_githubToken = config["GitHubToken"] ?? "";
_enabled = config.GetValue<bool>("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;
+8 -21
View File
@@ -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<KeyScannerService> _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<KeyScannerService> logger)
{
_db = db;
_httpFactory = httpFactory;
_settings = settings;
_logger = logger;
_githubToken = config["GitHubToken"] ?? "";
_serperApiKey = config["SerperApiKey"] ?? "";
_intervalHours = config.GetValue<int>("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);
+5 -3
View File
@@ -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<string, string> IMAGE_MODELS = new Dictionary<string, string>
@@ -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);
}
+10 -3
View File
@@ -156,17 +156,20 @@ public class OpenAiService
private List<ProviderModel> _openaiModels = new List<ProviderModel>();
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<string, (string BaseUrl, string ApiKey)>
{
["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
+236
View File
@@ -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;
/// <summary>
/// Zentraler Settings-Service. Liest/schreibt aus der DB-Tabelle 'server_settings'.
/// In-Memory-Cache für schnellen Zugriff. Kein Neustart bei Änderungen nötig.
/// </summary>
public class ServerSettingsService
{
private readonly string _dbPath;
private readonly Dictionary<string, string> _cache = new Dictionary<string, string>();
private readonly object _lock = new object();
// Definiert alle Settings mit Typ, Default und Beschreibung
public static readonly List<SettingDefinition> Definitions = new List<SettingDefinition>
{
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);
}
}
}
/// <summary>
/// Migriert bestehende Werte aus appsettings.Production.json in die DB (einmalig).
/// Nur Settings die noch NICHT in der DB sind werden übernommen.
/// </summary>
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<string> { "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<bool>() ?? false) ? "true" : "false";
break;
case "int":
value = (node?.GetValue<int>() ?? 0).ToString();
break;
default:
value = node?.GetValue<string>() ?? "";
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<object> GetAll()
{
var result = new List<object>();
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;
}
}
}
+4 -4
View File
@@ -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.");
+8 -9
View File
@@ -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<int>("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";
/// <summary>
@@ -858,7 +857,7 @@ public class ToolService
private async Task<string> 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<string> 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.";
+1 -14
View File
@@ -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
}
-2
View File
@@ -541,12 +541,10 @@
</header>
<div class="page-content">
<div class="form">
<p class="form-hint"><i class="fa fa-info-circle"></i> Änderungen werden in <code>appsettings.Production.json</code> gespeichert. Danach ist ein Neustart erforderlich.</p>
<div id="serverSettingsFields"></div>
<div class="form-actions">
<button id="serverSettingsSaveBtn"><i class="fa fa-save"></i> Speichern</button>
</div>
</div>
</div>
</div>