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:
1 parent
c8859a89d0
commit
542a031172
15 files changed
+614
-90
No files matched your search
@@ -192,6 +192,9 @@
|
||||
<option value="opus-4-5">opus-4-5</option>
|
||||
<option value="opus-4-6" selected>opus-4-6</option>
|
||||
<option value="haiku-4-5">haiku-4-5</option>
|
||||
<optgroup label="OpenAI" id="openaiOptgroup">
|
||||
<option value="" disabled>Wird geladen...</option>
|
||||
</optgroup>
|
||||
<optgroup label="Google Gemini" id="geminiOptgroup">
|
||||
<option value="" disabled>Wird geladen...</option>
|
||||
</optgroup>
|
||||
@@ -572,6 +575,8 @@
|
||||
<label>Provider</label>
|
||||
<select id="apiKeysProvider">
|
||||
<option value="anthropic">Anthropic</option>
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="google">Google / Gemini</option>
|
||||
</select>
|
||||
<label>Name</label>
|
||||
<input type="text" id="apiKeysName" placeholder="z.B. Mein Pro Key" autocomplete="off">
|
||||
|
||||
@@ -21,6 +21,7 @@ registerPage('agent', {
|
||||
const nvidiaOptgroup = document.getElementById('nvidiaOptgroup');
|
||||
const pollinationsOptgroup = document.getElementById('pollinationsOptgroup');
|
||||
const geminiOptgroup = document.getElementById('geminiOptgroup');
|
||||
const openaiOptgroup = document.getElementById('openaiOptgroup');
|
||||
|
||||
// Modelle dynamisch laden
|
||||
const loadProviderModels = async (url, prefix, optgroup) => {
|
||||
@@ -35,13 +36,16 @@ registerPage('agent', {
|
||||
opt.textContent = m.displayName;
|
||||
optgroup.appendChild(opt);
|
||||
}
|
||||
} else if (optgroup) {
|
||||
optgroup.remove();
|
||||
}
|
||||
} catch (e) {
|
||||
if (optgroup) optgroup.innerHTML = '<option value="" disabled>Fehler beim Laden</option>';
|
||||
if (optgroup) optgroup.remove();
|
||||
}
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
loadProviderModels('/api/models/openai', 'openai', openaiOptgroup),
|
||||
loadProviderModels('/api/models/nvidia', 'nvidia', nvidiaOptgroup),
|
||||
loadProviderModels('/api/models/pollinations', 'pollinations', pollinationsOptgroup),
|
||||
loadProviderModels('/api/models/gemini', 'gemini', geminiOptgroup)
|
||||
|
||||
@@ -137,8 +137,8 @@ registerPage('api-keys', {
|
||||
statusLine.className = 'admin-user-meta';
|
||||
statusLine.style.cssText = 'padding:0 var(--box-padding) .5rem;display:none;font-size:.85em';
|
||||
|
||||
// Check (nur Anthropic) – rechts neben Usage-Badge
|
||||
if (key.provider === 'anthropic') {
|
||||
// Check – rechts neben Usage-Badge
|
||||
if (key.provider === 'anthropic' || key.provider === 'openai' || key.provider === 'google') {
|
||||
var checkBtn = document.createElement('button');
|
||||
checkBtn.className = 'icon-btn';
|
||||
checkBtn.title = 'Key prüfen';
|
||||
@@ -324,6 +324,7 @@ registerPage('api-keys', {
|
||||
var scannedList = document.getElementById('scannedKeysList');
|
||||
var scannedKeys = [];
|
||||
var selectedType = null;
|
||||
var checkUncheckedAbort = false;
|
||||
var pollTimer = null;
|
||||
|
||||
var typeNames = {
|
||||
@@ -405,10 +406,71 @@ registerPage('api-keys', {
|
||||
backName.innerHTML = '<i class="fa fa-arrow-left"></i> Zurück zur Übersicht';
|
||||
backInfo.appendChild(backName);
|
||||
backRow.appendChild(backInfo);
|
||||
backRow.addEventListener('click', function() { selectedType = null; loadSummary(); });
|
||||
backRow.addEventListener('click', function() { selectedType = null; checkUncheckedAbort = true; loadSummary(); });
|
||||
scannedList.appendChild(backRow);
|
||||
|
||||
// Toolbar: "Ungeprüfte prüfen"-Button
|
||||
var visibleKeys = scannedKeys.filter(function(sk) { return sk.adoptedApiKeyId == null; });
|
||||
var uncheckedKeys = visibleKeys.filter(function(sk) { return sk.status === 'unchecked'; });
|
||||
if (uncheckedKeys.length > 0) {
|
||||
var toolbar = document.createElement('div');
|
||||
toolbar.style.cssText = 'display:flex;align-items:center;gap:8px;padding:8px var(--box-padding);border-bottom:1px solid var(--border)';
|
||||
|
||||
var checkUncheckedBtn = document.createElement('button');
|
||||
checkUncheckedBtn.className = 'section-add-btn';
|
||||
checkUncheckedBtn.innerHTML = '<i class="fa fa-search"></i> ' + uncheckedKeys.length + ' Ungeprüfte prüfen';
|
||||
|
||||
var progressSpan = document.createElement('span');
|
||||
progressSpan.className = 'admin-user-meta';
|
||||
progressSpan.style.marginLeft = '8px';
|
||||
|
||||
toolbar.appendChild(checkUncheckedBtn);
|
||||
toolbar.appendChild(progressSpan);
|
||||
scannedList.appendChild(toolbar);
|
||||
|
||||
checkUncheckedBtn.addEventListener('click', async function() {
|
||||
checkUncheckedAbort = false;
|
||||
checkUncheckedBtn.disabled = true;
|
||||
|
||||
var stopBtn = document.createElement('button');
|
||||
stopBtn.className = 'icon-btn';
|
||||
stopBtn.title = 'Abbrechen';
|
||||
stopBtn.innerHTML = '<i class="fa fa-stop" style="color:var(--danger)"></i>';
|
||||
stopBtn.addEventListener('click', function() { checkUncheckedAbort = true; });
|
||||
toolbar.appendChild(stopBtn);
|
||||
|
||||
var checked = 0;
|
||||
var found = 0;
|
||||
for (var i = 0; i < uncheckedKeys.length; i++) {
|
||||
if (checkUncheckedAbort) {
|
||||
progressSpan.textContent = 'Abgebrochen bei ' + checked + '/' + uncheckedKeys.length;
|
||||
break;
|
||||
}
|
||||
var sk = uncheckedKeys[i];
|
||||
progressSpan.textContent = (i + 1) + '/' + uncheckedKeys.length + ' prüfe…';
|
||||
checkUncheckedBtn.innerHTML = '<i class="fa fa-spinner fa-spin"></i> ' + (i + 1) + '/' + uncheckedKeys.length;
|
||||
try {
|
||||
var result = await apiRecheckScannedKey(sk.scannedKeyId);
|
||||
checked++;
|
||||
if (result && result.status === 'valid') {
|
||||
found++;
|
||||
progressSpan.textContent = '✅ Gültigen Key gefunden! Stoppe.';
|
||||
showToast('Gültigen Key gefunden!', 'success');
|
||||
break;
|
||||
}
|
||||
} catch (err) {
|
||||
checked++;
|
||||
}
|
||||
}
|
||||
if (!checkUncheckedAbort && found === 0) {
|
||||
progressSpan.textContent = checked + ' geprüft, keinen gültigen gefunden.';
|
||||
}
|
||||
checkUncheckedBtn.disabled = false;
|
||||
checkUncheckedBtn.innerHTML = '<i class="fa fa-search"></i> Ungeprüfte prüfen';
|
||||
if (stopBtn.parentNode) stopBtn.parentNode.removeChild(stopBtn);
|
||||
await loadScannedKeysByType(selectedType);
|
||||
});
|
||||
}
|
||||
if (visibleKeys.length === 0) {
|
||||
var empty = document.createElement('div');
|
||||
empty.className = 'admin-user-meta';
|
||||
|
||||
@@ -168,6 +168,8 @@ async function _showKeySwitchModal() {
|
||||
var meta = document.createElement('div');
|
||||
meta.className = 'key-switch-meta';
|
||||
var parts = [];
|
||||
var providerLabels = { anthropic: '🟤 Anthropic', openai: '🟢 OpenAI', google: '🔵 Google' };
|
||||
parts.push(providerLabels[key.provider] || key.provider || 'Anthropic');
|
||||
if (key.plan) parts.push(key.plan);
|
||||
if (key.status) parts.push(key.status === 'valid' ? '✅' : '⚠️');
|
||||
meta.textContent = parts.join(' · ');
|
||||
|
||||
Reference in new issue
Block a user