Files
gott/Services/ServerSettingsService.cs
T
Meleki d66f8d2e73 feat(minimax): Thinking-Parsing, 1h-Cache-TTL, Null-Safety, Provider-Integration
- Thinking-Streaming: Parse 'reasoning_details' (MiniMax mit reasoning_split),
  'reasoning' (Nvidia), 'reasoning_content' (DeepSeek/OpenAI-Compat),
  und inline <think>...</think>-Tags aus delta.content (Fallback)
- MiniMax: max_tokens=32768, stream_options={include_usage}, reasoning_split=true
- 1h-Prompt-Cache-TTL für API (extended-cache-ttl-2025-04-11 Header,
  Cache-Breakpoints auf System-Prompt + letztes Tool) und CLI (ENABLE_PROMPT_CACHING_1H=1)
- Null-Safety: choices/delta/tool_calls/usage null-Checks in Streaming + Non-Streaming
- finish_reason='length' → warning-Event + stopReason='max_tokens'
- MiniMax-Provider: BaseUrl, MinimaxApiKey-Setting, /api/models/minimax-Endpoint,
  Model-Dropdown, statische Fallback-Modelle, Cache-Laden mit Fallback
- Safety-Net: InvalidOperationException-Catch um SSE-Zeilen-Block
2026-05-14 00:55:32 +02:00

292 lines
12 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.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", "220", "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( "ScanDailyHour", "int", "-1", "Key-Scanner Full-Scan Uhrzeit (0-23, -1 = deaktiviert)", "number" ),
new SettingDefinition( "GitHubEventScanner", "bool", "false", "GitHub Event Scanner (Echtzeit-Monitoring öffentlicher Pushes)", "toggle" ),
new SettingDefinition( "HuggingFaceToken", "string", "", "HuggingFace Token (optional, erhöht Rate-Limits)", "password" ),
new SettingDefinition( "NvidiaApiKey", "string", "", "Nvidia API-Key (direkt statt g4f.space Proxy)", "password" ),
new SettingDefinition( "MinimaxApiKey", "string", "", "MiniMax API-Key (für minimax-Provider im Chat)", "password" ),
new SettingDefinition( "ScannerKeyTypes", "string_array", "anthropic_oauth,krea_ai", "Key-Scanner: Aktive Key-Typen", "checkboxes" ),
};
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>
/// Entfernt veraltete Settings aus der DB die nicht mehr in Definitions existieren.
/// </summary>
private void CleanupObsoleteSettings()
{
var obsoleteKeys = new[] { "ScanIntervalHours" };
using var conn = new SqliteConnection($"Data Source={_dbPath}");
conn.Open();
foreach (var key in obsoleteKeys)
{
using var cmd = conn.CreateCommand();
cmd.CommandText = "DELETE FROM server_settings WHERE key = @key";
cmd.Parameters.AddWithValue("@key", key);
cmd.ExecuteNonQuery();
}
lock (_lock)
{
foreach (var key in obsoleteKeys) _cache.Remove(key);
}
}
/// <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)
{
CleanupObsoleteSettings();
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");
public string[] GetStringArray(string key, string defaultCsv = "")
{
var raw = Get(key, null);
if (raw == null)
{
var def = Definitions.Find(d => d.Key == key);
raw = def?.DefaultValue ?? defaultCsv;
}
if (string.IsNullOrWhiteSpace(raw)) return Array.Empty<string>();
return raw.Split(',', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries);
}
public void SetStringArray(string key, string[] values)
{
Set(key, string.Join(",", values ?? Array.Empty<string>()));
}
// ─── 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;
case "string_array":
value = GetStringArray(def.Key, def.DefaultValue);
break;
default:
value = Get(def.Key, def.DefaultValue);
break;
}
// Zusatz-Metadaten für bestimmte Settings
object options = null;
if (def.Key == "ScannerKeyTypes")
{
options = KeyPatterns.AllTypes.Select(t => new { id = t, name = KeyPatterns.DisplayName(t) }).ToArray();
}
result.Add(new
{
key = def.Key,
type = def.Type,
value = value,
label = def.Label,
inputType = def.InputType,
options = options,
});
}
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;
}
}
}