Files
gott/Services/ServerSettingsService.cs
T
MelekiandClaude Opus 5 7305a5a50f OpenAI: Server-Key-Setting, Filter-Fix fuer o-Reihe, ehrlicher Modell-Refresh
Der Modell-Refresh war fuer OpenAI faktisch ein No-Op: der Key wurde nur aus
der api_keys-Tabelle geholt, und ohne Key blieb die alte Liste im Speicher
stehen und wurde direkt wieder in models_cache.json geschrieben - waehrend der
Endpoint pauschal Erfolg meldete.

- ServerSettingsService: neues Setting OpenAiApiKey (OpenAI war der einzige
  Provider ohne Server-Key, analog Gemini/MiniMax/z.ai)
- OpenAiService: ResolveOpenAiKey() (Setting -> api_keys), HasServerOpenAiKey(),
  ResolveProviderConfig zieht den Setting-Key auch im Chat
- OpenAiService: IsOpenAiChatModel() erfasst die o-Reihe inkl. bare IDs
  (o1, o3, kuenftig o5) - StartsWith("o3-") hat die verschluckt
- RefreshModelsFromApis() liefert ProviderRefreshResult[] pro Provider;
  ModelsEndpoint gibt providers[] zurueck, agent-page.js meldet uebersprungene
  Provider als Fehler-Toast statt pauschalem Erfolg
- ChatEndpoint/TelegramService/CronjobService: OpenAI ohne Agent-Key faellt auf
  den Server-Key zurueck statt hart abzubrechen
- ARCHITECTURE.md nachgezogen

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-15 11:55:22 +02:00

294 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( "OpenAiApiKey", "string", "", "OpenAI API-Key (für openai-Provider im Chat + Modell-Liste)", "password" ),
new SettingDefinition( "ZaiApiKey", "string", "", "z.ai API-Key (für GLM-Modelle über die Claude CLI)", "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;
}
}
}