Add github_code_search tool + scanned key type field

- New tool: github_code_search (GitHub Code Search API with text-match fragments)
- Tool available when GitHubToken is configured in server settings
- Registered in AnthropicService + ChatService tool pipelines
- ToolService: SetHttpClientFactory() pattern for lazy DI resolution
- ScannedKeyRecord: type field (anthropic_oauth, anthropic_api, anthropic_unknown, unknown)
- DetectKeyType() based on key prefix
- DB migration: type column with default 'anthropic_oauth'
- Frontend: type label in scanned keys list
This commit is contained in:
Meleki committed 2026-03-13 08:12:55 +01:00
1 parent 04c7c2e13d
commit bca5e5145a
11 files changed
+145 -10

No files matched your search

+1
View File
@@ -263,6 +263,7 @@ public static class ApiKeysEndpoint
{
k.ScannedKeyId,
k.Source,
k.Type,
k.SourceUrl,
k.Status,
k.Plan,
+1
View File
@@ -6,6 +6,7 @@ public class ScannedKeyRecord
public string ScannedKeyId { get; set; }
public string ApiKey { get; set; }
public string Source { get; set; } // Herkunft: github, serper, gist, pastebin
public string Type { get; set; } // Key-Typ: anthropic_oauth, etc.
public string SourceUrl { get; set; } // URL wo der Key gefunden wurde
public string Status { get; set; } // valid, invalid, auth_error, rate_limited, quota_exceeded, error
public string Plan { get; set; } // Pro, Max, Pro/Max, API, Unknown
+1
View File
@@ -101,6 +101,7 @@ builder.Services.AddSingleton<KeyScannerService>();
builder.Services.AddHostedService<KeyScannerService>(sp => sp.GetRequiredService<KeyScannerService>());
var app = builder.Build();
toolService.SetHttpClientFactory(app.Services.GetRequiredService<IHttpClientFactory>());
app.Services.GetRequiredService<AnthropicService>().SetTelegramService(app.Services.GetRequiredService<TelegramService>());
await app.Services.GetRequiredService<OpenAiService>().LoadModelsAsync();
app.UseWebSockets();
+3 -2
View File
@@ -1625,7 +1625,8 @@ public class AnthropicService
var includeMemory = useMemory && _memoryService != null && !string.IsNullOrEmpty(agentId);
var includeMemorySearch = _vectorSearch != null && _vectorSearch.GetEmbeddingCount() > 0;
var includeSendToSession = _db != null && _chatService != null;
var hasTools = hasWorkspace || includeMemory || includeRemote || includeMemorySearch || includeSendToSession;
var includeGitHubSearch = _toolService.HasGitHubToken;
var hasTools = hasWorkspace || includeMemory || includeRemote || includeMemorySearch || includeSendToSession || includeGitHubSearch;
var hasWebSearch = !string.IsNullOrEmpty(webSearchVersion);
if (!hasTools && !hasWebSearch) return;
@@ -1633,7 +1634,7 @@ public class AnthropicService
var tools = new List<object>();
if (hasTools)
{
var allTools = _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: allowUserChoice, includeRemote: includeRemote, includeMemorySearch: includeMemorySearch, includeSendToSession: includeSendToSession);
var allTools = _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: allowUserChoice, includeRemote: includeRemote, includeMemorySearch: includeMemorySearch, includeSendToSession: includeSendToSession, includeGitHubSearch: includeGitHubSearch);
if (planMode)
allTools = _toolService.FilterReadOnlyTools(allTools);
tools.AddRange(allTools);
+3 -2
View File
@@ -79,11 +79,12 @@ public class ChatService
var hasWorkspace = !string.IsNullOrEmpty(workspace);
var includeMemory = agent.UseMemory && !string.IsNullOrEmpty(agent.AgentId);
var hasTools = hasWorkspace || includeMemory;
var includeGitHubSearch = _toolService.HasGitHubToken;
var hasTools = hasWorkspace || includeMemory || includeGitHubSearch;
var toolChoice = hasTools ? "any" : null;
var toolDefs = hasTools
? _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: agent.AllowUserChoice, includeRemote: hasRemoteConnection)
? _toolService.GetToolDefinitions(includeFileTools: hasWorkspace, includeMemory: includeMemory, includeUserChoice: agent.AllowUserChoice, includeRemote: hasRemoteConnection, includeGitHubSearch: includeGitHubSearch)
: Array.Empty<object>();
var toolNames = toolDefs.Length > 0
? JsonSerializer.Serialize(toolDefs.Select(td =>
+2 -2
View File
@@ -460,8 +460,8 @@ public partial class DatabaseService
{
using var conn = CreateConnection();
await conn.ExecuteAsync(@"
INSERT OR IGNORE INTO scanned_keys (scanned_key_id, api_key, source, source_url, status, plan, usage_7d, usage_5h, scanned_at, checked_at)
VALUES (@ScannedKeyId, @ApiKey, @Source, @SourceUrl, @Status, @Plan, @Usage7d, @Usage5h, @ScannedAt, @CheckedAt)",
INSERT OR IGNORE INTO scanned_keys (scanned_key_id, api_key, source, source_url, type, status, plan, usage_7d, usage_5h, scanned_at, checked_at)
VALUES (@ScannedKeyId, @ApiKey, @Source, @SourceUrl, @Type, @Status, @Plan, @Usage7d, @Usage5h, @ScannedAt, @CheckedAt)",
record);
}
+1
View File
@@ -330,6 +330,7 @@ public partial class DatabaseService
TryAddColumn(conn, "scanned_keys", "deleted", "INTEGER NOT NULL DEFAULT 0");
TryAddColumn(conn, "scanned_keys", "adopted_api_key_id", "INTEGER");
TryAddColumn(conn, "scanned_keys", "source", "TEXT");
TryAddColumn(conn, "scanned_keys", "type", "TEXT NOT NULL DEFAULT 'anthropic_oauth'");
// api_keys Metadaten-Spalten
TryAddColumn(conn, "api_keys", "status", "TEXT");
+9
View File
@@ -185,6 +185,7 @@ public class KeyScannerService : BackgroundService
ApiKey = key,
Source = source,
SourceUrl = sourceUrl,
Type = DetectKeyType(key),
Status = "unchecked",
Plan = "Unknown",
ScannedAt = DateTime.UtcNow.ToString("o")
@@ -471,4 +472,12 @@ public class KeyScannerService : BackgroundService
await _db.UpdateScannedKeyStatus(sk.ScannedKeyId, "error", "Unknown", null, null);
}
}
private static string DetectKeyType(string key)
{
if (key.StartsWith("sk-ant-oat01-", StringComparison.Ordinal)) return "anthropic_oauth";
if (key.StartsWith("sk-ant-api03-", StringComparison.Ordinal)) return "anthropic_api";
if (key.StartsWith("sk-ant-", StringComparison.Ordinal)) return "anthropic_unknown";
return "unknown";
}
}
+120 -2
View File
@@ -3,6 +3,8 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
using System.Text.RegularExpressions;
@@ -17,13 +19,20 @@ namespace Gott.Services;
public class ToolService
{
private readonly IConfiguration _config;
private IHttpClientFactory _httpFactory;
public ToolService(IConfiguration config)
{
_config = config;
}
public void SetHttpClientFactory(IHttpClientFactory httpFactory)
{
_httpFactory = httpFactory;
}
private int ExecuteTimeoutSeconds => _config.GetValue<int>("ExecuteTimeout", 180);
public bool HasGitHubToken => !string.IsNullOrWhiteSpace(_config["GitHubToken"]);
public const string AskUserChoiceToolName = "ask_user_choice";
/// <summary>
@@ -34,7 +43,7 @@ public class ToolService
{
"read_file", "list_files", "glob", "grep", "execute", "todo_write",
"memory_read", "memory_write", "memory_list", "memory_search",
"remote_execute", AskUserChoiceToolName
"remote_execute", "github_code_search", AskUserChoiceToolName
};
public object[] FilterReadOnlyTools(object[] tools)
@@ -54,7 +63,7 @@ public class ToolService
return filtered.ToArray();
}
public object[] GetToolDefinitions(bool includeFileTools = false, bool includeMemory = false, bool includeUserChoice = true, bool includeRemote = false, bool includeMemorySearch = false, bool includeSendToSession = false)
public object[] GetToolDefinitions(bool includeFileTools = false, bool includeMemory = false, bool includeUserChoice = true, bool includeRemote = false, bool includeMemorySearch = false, bool includeSendToSession = false, bool includeGitHubSearch = false)
{
var allTools = new List<object>();
if (includeUserChoice)
@@ -69,6 +78,8 @@ public class ToolService
allTools.Add(GetRemoteExecuteTool());
if (includeSendToSession)
allTools.Add(GetSendToSessionTool());
if (includeGitHubSearch)
allTools.Add(GetGitHubCodeSearchTool());
return allTools.ToArray();
}
@@ -799,6 +810,10 @@ public class ToolService
return $"Nachricht wurde erfolgreich an Session \"{targetSession.Name}\" gesendet. Der Agent dort hat geantwortet.";
}
case "github_code_search":
{
return await ExecuteGitHubCodeSearch(input, cancellationToken);
}
default:
return $"Fehler: Unbekanntes Tool: {name}";
}
@@ -811,4 +826,107 @@ public class ToolService
throw new Exception("Zugriff außerhalb des Workspace nicht erlaubt");
return full;
}
// ── GitHub Code Search ──
private object GetGitHubCodeSearchTool()
{
return new
{
name = "github_code_search",
description = "Durchsucht öffentliche GitHub-Repositories nach Code. Nutzt die GitHub Code Search API. Gibt Dateinamen, Repositories und Code-Ausschnitte zurück. Unterstützt GitHub-Suchsyntax (z.B. language:python, path:*.js, org:microsoft).",
input_schema = new
{
type = "object",
properties = new Dictionary<string, object>
{
["query"] = new { type = "string", description = "Suchbegriff mit optionaler GitHub-Suchsyntax (z.B. 'useState language:typescript', 'OPENAI_API_KEY path:.env')" },
["per_page"] = new { type = "integer", description = "Anzahl Ergebnisse (1-50, Default: 10)" }
},
required = new[] { "query" }
}
};
}
private async Task<string> ExecuteGitHubCodeSearch(JsonElement input, CancellationToken ct)
{
var token = _config["GitHubToken"] ?? "";
if (string.IsNullOrWhiteSpace(token))
return "Fehler: Kein GitHubToken konfiguriert. Bitte in den Server-Einstellungen hinterlegen.";
var query = input.GetProperty("query").GetString() ?? "";
if (string.IsNullOrWhiteSpace(query))
return "Fehler: Suchbegriff (query) darf nicht leer sein.";
var perPage = input.TryGetProperty("per_page", out var ppProp) ? Math.Clamp(ppProp.GetInt32(), 1, 50) : 10;
try
{
var client = _httpFactory.CreateClient();
client.DefaultRequestHeaders.UserAgent.ParseAdd("GottApp/1.0");
client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token);
client.DefaultRequestHeaders.Accept.Clear();
client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/vnd.github.text-match+json"));
var url = $"https://api.github.com/search/code?q={Uri.EscapeDataString(query)}&per_page={perPage}";
var response = await client.GetAsync(url, ct);
if (!response.IsSuccessStatusCode)
{
var errorBody = await response.Content.ReadAsStringAsync(ct);
return $"GitHub API Fehler {(int)response.StatusCode}: {errorBody}";
}
var json = await response.Content.ReadAsStringAsync(ct);
var doc = JsonDocument.Parse(json);
var root = doc.RootElement;
var totalCount = root.GetProperty("total_count").GetInt32();
var items = root.GetProperty("items");
var sb = new StringBuilder();
sb.AppendLine($"**{totalCount} Treffer gefunden** (zeige {Math.Min(perPage, items.GetArrayLength())})");
sb.AppendLine();
var index = 0;
foreach (var item in items.EnumerateArray())
{
index++;
var name = item.GetProperty("name").GetString() ?? "";
var path = item.GetProperty("path").GetString() ?? "";
var repo = item.GetProperty("repository").GetProperty("full_name").GetString() ?? "";
var htmlUrl = item.GetProperty("html_url").GetString() ?? "";
sb.AppendLine($"### {index}. {repo}");
sb.AppendLine($"📄 `{path}`");
sb.AppendLine($"🔗 {htmlUrl}");
// Text-Match-Fragmente (wenn vorhanden)
if (item.TryGetProperty("text_matches", out var textMatches))
{
foreach (var match in textMatches.EnumerateArray())
{
if (match.TryGetProperty("fragment", out var fragment))
{
var frag = fragment.GetString() ?? "";
if (frag.Length > 500) frag = frag.Substring(0, 500) + "...";
sb.AppendLine("```");
sb.AppendLine(frag);
sb.AppendLine("```");
}
}
}
sb.AppendLine();
}
return sb.ToString();
}
catch (TaskCanceledException)
{
return "GitHub-Suche abgebrochen (Timeout).";
}
catch (Exception ex)
{
return $"GitHub-Suche fehlgeschlagen: {ex.Message}";
}
}
}
+2 -2
View File
@@ -580,8 +580,8 @@
</div>
</div>
<div id="scannedKeysList" class="admin-user-list"></div>
<div id="scannedKeysActions" style="padding:.5rem 0;display:none">
<button id="scannedDeleteAllBtn" class="icon-btn" title="Alle gescannten Keys löschen" style="font-size:.85em;color:var(--danger)"><i class="fa fa-trash"></i> Alle löschen</button>
<div class="form-actions">
<button hidden>&nbsp;</button>
</div>
</div>
</div>
+2
View File
@@ -362,6 +362,8 @@ registerPage('api-keys', {
var metaParts = [];
var sourceIcons = { github: '🐙 GitHub', serper: '🔍 Google', gist: '📋 Gist', pastebin: '📄 Pastebin' };
metaParts.push(sourceIcons[sk.source] || sk.source || 'github');
var typeLabels = { anthropic_oauth: 'OAuth', anthropic_api: 'API', anthropic_unknown: 'Anthropic', unknown: '?' };
metaParts.push(typeLabels[sk.type] || sk.type || '?');
metaParts.push(formatKeyStatus(sk.status));
if (sk.plan && sk.plan !== 'Unknown') metaParts.push(sk.plan);
if (sk.checkedAt) metaParts.push(new Date(sk.checkedAt).toLocaleString());