Files
gott/Services/ServerSettingsService.cs
T
Meleki 122000487c 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)
2026-03-14 13:06:44 +01:00

237 lines
9.4 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.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;
}
}
}