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:
1 parent
542a031172
commit
122000487c
18 files changed
+389
-270
No files matched your search
@@ -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);
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user