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
@@ -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);
|
||||
|
||||
@@ -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 =>
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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
@@ -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