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:
1 parent
04c7c2e13d
commit
bca5e5145a
11 files changed
+145
-10
No files matched your search
+120
-2
@@ -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}";
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user