Bugtracker-Durchgang: 13 Bugs behoben, Build ohne Warnungen
Alle Behebungen auf der Linux-Test-VM gegengeprueft (Ubuntu, .NET 10.0.111): Build 0 Warnungen / 0 Fehler, 637 Tests gruen, 1 uebersprungen (WindowsFact). Sicherheit und Zielplattform #373 PathBoundary.Canonicalize fiel offen aus. File.ResolveLinkTarget wirft fuer einen fehlenden Abschnitt FileNotFoundException; der catch(IOException) verliess daraufhin die GANZE Aufloesung mit dem unaufgeloesten Pfad - und der besteht jede Praefixpruefung. Der Symlink-Schutz war damit bei jedem Schreibvorgang wirkungslos. Jetzt ueber FileSystemInfo.LinkTarget, und TryCanonicalize meldet "nicht bestimmbar", statt fail-open durchzureichen. #374 WorkspacePath weist den Backslash rundheraus ab, wie den Doppelpunkt. Eine Schreibweise statt einer Sonderregel je Windows-Form. #375 Log-Verzeichnis: leer heisst "Anwendungseinstellung"; ein relativer Wert zaehlt gegen das Instanzverzeichnis, nicht gegen das Arbeitsverzeichnis des Prozesses. Ein systemd-Dienst landet nicht mehr auf /Logs. Agentenkommunikation #206 Delegationskette gegen A->B->A (Test A13). Zyklusfrei per Bauart, dazu MaxDelegationDepth gegen die Staffel, die niemand beauftragt hat. #461 Zwei unabhaengige Ketten ueber Kreuz - beim Beheben von #206 gefunden und reproduziert. Ein delegierter Lauf wartet nur noch begrenzt auf ein fremdes Gate; ein Chat vom Menschen reiht sich weiter unbegrenzt ein. #234 Instanz-Id wird durchgereicht statt aus einem nur beilaeufig gesetzten Feld gelesen. Betrieb und Oberflaeche #376 Log-Ansicht chronologisch statt gruppiert je Moduldatei, mit Stacktraces an ihrer Meldung - auch ueber Taktgrenzen hinweg. #377 Info-Tabelle: feste Spaltenbreiten, damit ein langer Aenderungstext den anderen Spalten nicht den Platz nimmt. #378 Sicherung: Mindestabstand zwischen Ueberschrift und Schaltflaechenleiste. #379 Hellmodus: drei von vier gedaempften Textpinseln lagen unter 4,5:1. Die helle Palette ist als Ganzes verschoben, damit die Staffelung sichtbar bleibt statt auf einen Wert zusammenzufallen. Hygiene #233 stock_add: der Indexstand wird innerhalb der Sperre festgehalten. #235 Zeitumstellung: eine nicht existierende Ortszeit wird auf den ersten Zeitpunkt nach der Luecke verschoben statt uebersprungen (R4, R5). #236 45 Nullable-Warnungen und eine CA1416 - nicht unterdrueckt, sondern die Stellen so umgebaut, dass die Zusage nachvollziehbar wird. Mail meldet jetzt, WELCHER Zugangsschluessel fehlt, statt an einer KeyNotFoundException zu scheitern. #203 Kompaktiert wird vor dem API-Aufruf statt danach; die gemessenen Prompt-Groessen kalibrieren die Schaetzung je Modell. Neu: tests/ClawdDotNet.App.Tests - fuer #375 und #376 gab es keinen Ort fuer Tests. Dazu Regressionstests fuer jede Behebung; jeder wurde gegen den alten Stand geprueft, damit belegt ist, dass er den Fehler faengt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -81,49 +81,45 @@ public sealed class DirectApiTool : IAgentTool
|
||||
{
|
||||
var apiKey = GetApiKey(providers, "twelvedata");
|
||||
var url = $"https://api.twelvedata.com/quote?symbol={symbol}&apikey={apiKey}";
|
||||
var (json, error) = await SafeGetJsonAsync(http, url, ct);
|
||||
if (error != null) return error;
|
||||
if (!(await SafeGetJsonAsync(http, url, ct)).TryGet(out var json, out var error)) return error;
|
||||
|
||||
if (json.Value.TryGetProperty("code", out var code) && code.GetInt32() != 200)
|
||||
return ToolResult.Fail(json.Value.GetProperty("message").GetString() ?? "API Fehler");
|
||||
if (json.TryGetProperty("code", out var code) && code.GetInt32() != 200)
|
||||
return ToolResult.Fail(json.GetProperty("message").GetString() ?? "API Fehler");
|
||||
|
||||
DateTime? dataAsOf = null;
|
||||
if (json.Value.TryGetProperty("timestamp", out var ts))
|
||||
if (json.TryGetProperty("timestamp", out var ts))
|
||||
dataAsOf = DateTimeOffset.FromUnixTimeSeconds(ts.GetInt64()).UtcDateTime;
|
||||
|
||||
return CreateSuccessResult(fetchedAt, dataAsOf, url, json.Value);
|
||||
return CreateSuccessResult(fetchedAt, dataAsOf, url, json);
|
||||
}
|
||||
else if (provider == "alphavantage")
|
||||
{
|
||||
var apiKey = GetApiKey(providers, "alphavantage");
|
||||
var url = $"https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={symbol}&apikey={apiKey}";
|
||||
var (json, error) = await SafeGetJsonAsync(http, url, ct);
|
||||
if (error != null) return error;
|
||||
if (!(await SafeGetJsonAsync(http, url, ct)).TryGet(out var json, out var error)) return error;
|
||||
|
||||
if (json.Value.TryGetProperty("Note", out var note))
|
||||
if (json.TryGetProperty("Note", out var note))
|
||||
return ToolResult.Fail($"Alpha Vantage Rate Limit: {note.GetString()}");
|
||||
if (json.Value.TryGetProperty("Error Message", out var err))
|
||||
if (json.TryGetProperty("Error Message", out var err))
|
||||
return ToolResult.Fail($"Alpha Vantage Fehler: {err.GetString()}");
|
||||
|
||||
return CreateSuccessResult(fetchedAt, fetchedAt, url, json.Value);
|
||||
return CreateSuccessResult(fetchedAt, fetchedAt, url, json);
|
||||
}
|
||||
else if (provider == "massive")
|
||||
{
|
||||
var apiKey = GetApiKey(providers, "massive");
|
||||
var url = $"https://api.massive.com/v3/snapshot?ticker={symbol}";
|
||||
http.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
|
||||
var (json, error) = await SafeGetJsonAsync(http, url, ct);
|
||||
if (error != null) return error;
|
||||
if (!(await SafeGetJsonAsync(http, url, ct)).TryGet(out var json, out var error)) return error;
|
||||
|
||||
return CreateSuccessResult(fetchedAt, fetchedAt, url, json.Value);
|
||||
return CreateSuccessResult(fetchedAt, fetchedAt, url, json);
|
||||
}
|
||||
else if (provider == "yahoo")
|
||||
{
|
||||
var url = $"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}?interval=1m&range=1d";
|
||||
var (json, error) = await SafeGetJsonAsync(http, url, ct);
|
||||
if (error != null) return error;
|
||||
if (!(await SafeGetJsonAsync(http, url, ct)).TryGet(out var json, out var error)) return error;
|
||||
|
||||
if (!json.Value.TryGetProperty("chart", out var chartProp) ||
|
||||
if (!json.TryGetProperty("chart", out var chartProp) ||
|
||||
!chartProp.TryGetProperty("result", out var resultArr) ||
|
||||
resultArr.GetArrayLength() == 0)
|
||||
return ToolResult.Fail($"Yahoo Finance: Keine Daten für '{symbol}' erhalten.");
|
||||
@@ -153,13 +149,12 @@ public sealed class DirectApiTool : IAgentTool
|
||||
var apiKey = GetApiKey(providers, "twelvedata");
|
||||
var url = $"https://api.twelvedata.com/time_series?symbol={symbol}&interval={interval}&outputsize={outputSize}&apikey={apiKey}";
|
||||
|
||||
var (json, error) = await SafeGetJsonAsync(http, url, ct);
|
||||
if (error != null) return error;
|
||||
if (!(await SafeGetJsonAsync(http, url, ct)).TryGet(out var json, out var error)) return error;
|
||||
|
||||
if (json.Value.TryGetProperty("status", out var status) && status.GetString() != "ok")
|
||||
return ToolResult.Fail(json.Value.GetProperty("message").GetString() ?? "API Fehler");
|
||||
if (json.TryGetProperty("status", out var status) && status.GetString() != "ok")
|
||||
return ToolResult.Fail(json.GetProperty("message").GetString() ?? "API Fehler");
|
||||
|
||||
return CreateSuccessResult(fetchedAt, null, url, json.Value);
|
||||
return CreateSuccessResult(fetchedAt, null, url, json);
|
||||
}
|
||||
else if (provider == "alphavantage")
|
||||
{
|
||||
@@ -177,15 +172,14 @@ public sealed class DirectApiTool : IAgentTool
|
||||
var urlBase = $"https://www.alphavantage.co/query?function={function_}&symbol={symbol}&apikey={apiKey}&outputsize=compact";
|
||||
if (!string.IsNullOrEmpty(avInterval)) urlBase += $"&interval={avInterval}";
|
||||
|
||||
var (json, error) = await SafeGetJsonAsync(http, urlBase, ct);
|
||||
if (error != null) return error;
|
||||
if (!(await SafeGetJsonAsync(http, urlBase, ct)).TryGet(out var json, out var error)) return error;
|
||||
|
||||
if (json.Value.TryGetProperty("Note", out var note))
|
||||
if (json.TryGetProperty("Note", out var note))
|
||||
return ToolResult.Fail($"Alpha Vantage Rate Limit: {note.GetString()}");
|
||||
if (json.Value.TryGetProperty("Error Message", out var err))
|
||||
if (json.TryGetProperty("Error Message", out var err))
|
||||
return ToolResult.Fail($"Alpha Vantage Fehler: {err.GetString()}");
|
||||
|
||||
return CreateSuccessResult(fetchedAt, null, urlBase, json.Value);
|
||||
return CreateSuccessResult(fetchedAt, null, urlBase, json);
|
||||
}
|
||||
else if (provider == "massive")
|
||||
{
|
||||
@@ -204,10 +198,9 @@ public sealed class DirectApiTool : IAgentTool
|
||||
var url = $"https://api.massive.com/v2/aggs/ticker/{symbol}/range/{multiplier}/{timespan}/{from}/{to}?limit={outputSize}";
|
||||
http.DefaultRequestHeaders.Add("Authorization", $"Bearer {apiKey}");
|
||||
|
||||
var (json, error) = await SafeGetJsonAsync(http, url, ct);
|
||||
if (error != null) return error;
|
||||
if (!(await SafeGetJsonAsync(http, url, ct)).TryGet(out var json, out var error)) return error;
|
||||
|
||||
return CreateSuccessResult(fetchedAt, null, url, json.Value);
|
||||
return CreateSuccessResult(fetchedAt, null, url, json);
|
||||
}
|
||||
else if (provider == "yahoo")
|
||||
{
|
||||
@@ -219,10 +212,9 @@ public sealed class DirectApiTool : IAgentTool
|
||||
var range = interval is "1min" or "5min" or "15min" ? "1d" : $"{outputSize}d";
|
||||
var url = $"https://query1.finance.yahoo.com/v8/finance/chart/{symbol}?interval={yahooInterval}&range={range}";
|
||||
|
||||
var (json, error) = await SafeGetJsonAsync(http, url, ct);
|
||||
if (error != null) return error;
|
||||
if (!(await SafeGetJsonAsync(http, url, ct)).TryGet(out var json, out var error)) return error;
|
||||
|
||||
if (!json.Value.TryGetProperty("chart", out var chartProp) ||
|
||||
if (!json.TryGetProperty("chart", out var chartProp) ||
|
||||
!chartProp.TryGetProperty("result", out var resultArr) ||
|
||||
resultArr.GetArrayLength() == 0)
|
||||
return ToolResult.Fail($"Yahoo Finance: Keine Historien-Daten für '{symbol}' erhalten.");
|
||||
@@ -240,10 +232,9 @@ public sealed class DirectApiTool : IAgentTool
|
||||
var fetchedAt = DateTime.UtcNow;
|
||||
var url = $"https://api.coingecko.com/api/v3/simple/price?ids={symbol.ToLowerInvariant()}&vs_currencies=usd&include_last_updated_at=true";
|
||||
|
||||
var (json, error) = await SafeGetJsonAsync(http, url, ct);
|
||||
if (error != null) return error;
|
||||
if (!(await SafeGetJsonAsync(http, url, ct)).TryGet(out var json, out var error)) return error;
|
||||
|
||||
if (!json.Value.TryGetProperty(symbol.ToLowerInvariant(), out var data))
|
||||
if (!json.TryGetProperty(symbol.ToLowerInvariant(), out var data))
|
||||
return ToolResult.Fail($"Symbol '{symbol}' nicht gefunden.");
|
||||
|
||||
DateTime? dataAsOf = null;
|
||||
@@ -262,14 +253,13 @@ public sealed class DirectApiTool : IAgentTool
|
||||
var fetchedAt = DateTime.UtcNow;
|
||||
var url = $"https://api.twelvedata.com/exchange_rate?symbol={symbol}&apikey={apiKey}";
|
||||
|
||||
var (json, error) = await SafeGetJsonAsync(http, url, ct);
|
||||
if (error != null) return error;
|
||||
if (!(await SafeGetJsonAsync(http, url, ct)).TryGet(out var json, out var error)) return error;
|
||||
|
||||
DateTime? dataAsOf = null;
|
||||
if (json.Value.TryGetProperty("timestamp", out var ts))
|
||||
if (json.TryGetProperty("timestamp", out var ts))
|
||||
dataAsOf = DateTimeOffset.FromUnixTimeSeconds(ts.GetInt64()).UtcDateTime;
|
||||
|
||||
return CreateSuccessResult(fetchedAt, dataAsOf, url, json.Value);
|
||||
return CreateSuccessResult(fetchedAt, dataAsOf, url, json);
|
||||
}
|
||||
|
||||
private async Task<ToolResult> SearchSymbolAsync(string symbol, string provider, JsonElement? providers, AgentToolContext context, CancellationToken ct)
|
||||
@@ -281,16 +271,54 @@ public sealed class DirectApiTool : IAgentTool
|
||||
var fetchedAt = DateTime.UtcNow;
|
||||
var url = $"https://api.twelvedata.com/symbol_search?symbol={symbol}"; // search often doesn't need key or uses same
|
||||
|
||||
var (json, error) = await SafeGetJsonAsync(http, url, ct);
|
||||
if (error != null) return error;
|
||||
return CreateSuccessResult(fetchedAt, null, url, json.Value);
|
||||
if (!(await SafeGetJsonAsync(http, url, ct)).TryGet(out var json, out var error)) return error;
|
||||
return CreateSuccessResult(fetchedAt, null, url, json);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Das Ergebnis eines API-Aufrufs: <b>entweder</b> Daten <b>oder</b> eine Absage —
|
||||
/// nie beides, nie keines von beiden.
|
||||
///
|
||||
/// <para>Vorher war das ein Tupel <c>(JsonElement? json, ToolResult? error)</c>, und
|
||||
/// jede der elf Aufrufstellen las danach <c>json.Value</c>. Dass der Wert nach
|
||||
/// <c>if (error != null) return error;</c> immer da ist, wusste nur der Mensch —
|
||||
/// der Compiler wies mit elf CS8629 darauf hin, dass er es nicht nachvollziehen
|
||||
/// kann. Elf Warnungen, die man wegzusehen lernt, verdecken die zwölfte, die
|
||||
/// stimmt.</para>
|
||||
///
|
||||
/// <para><see cref="TryGet"/> sagt dasselbe so, dass es nachvollziehbar ist: Wer
|
||||
/// <c>true</c> bekommt, hat die Daten; wer <c>false</c> bekommt, hat die Absage,
|
||||
/// die er zurückgeben soll.</para>
|
||||
/// </summary>
|
||||
private readonly struct JsonFetch
|
||||
{
|
||||
private readonly JsonElement _json;
|
||||
private readonly ToolResult? _error;
|
||||
|
||||
private JsonFetch(JsonElement json, ToolResult? error)
|
||||
{
|
||||
_json = json;
|
||||
_error = error;
|
||||
}
|
||||
|
||||
public static JsonFetch Ok(JsonElement json) => new(json, null);
|
||||
public static JsonFetch Fail(ToolResult error) => new(default, error);
|
||||
|
||||
public bool TryGet(out JsonElement json, out ToolResult error)
|
||||
{
|
||||
json = _json;
|
||||
// Der Ersatz greift nie: Fail verlangt eine Absage, Ok setzt keine. Er steht
|
||||
// hier, damit die Zusage ohne Ausrufezeichen auskommt.
|
||||
error = _error ?? ToolResult.Fail("Unbekannter Fehler beim API-Aufruf.");
|
||||
return _error is null;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sichere JSON-Antwort von einer API abrufen.
|
||||
/// Prüft HTTP-Status und ob die Antwort tatsächlich JSON ist, bevor geparst wird.
|
||||
/// </summary>
|
||||
private static async Task<(JsonElement? json, ToolResult? error)> SafeGetJsonAsync(
|
||||
private static async Task<JsonFetch> SafeGetJsonAsync(
|
||||
HttpClient http, string url, CancellationToken ct)
|
||||
{
|
||||
// Auch Fehlermeldungen gehen an das Modell — der Schlüssel darf darin nicht auftauchen.
|
||||
@@ -303,7 +331,7 @@ public sealed class DirectApiTool : IAgentTool
|
||||
}
|
||||
catch (HttpRequestException ex)
|
||||
{
|
||||
return (null, ToolResult.Fail($"API nicht erreichbar: {safeUrl} → {ex.Message}"));
|
||||
return JsonFetch.Fail(ToolResult.Fail($"API nicht erreichbar: {safeUrl} → {ex.Message}"));
|
||||
}
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync(ct);
|
||||
@@ -311,7 +339,7 @@ public sealed class DirectApiTool : IAgentTool
|
||||
if (!response.IsSuccessStatusCode)
|
||||
{
|
||||
var preview = body.Length > 300 ? body[..300] + "…" : body;
|
||||
return (null, ToolResult.Fail(
|
||||
return JsonFetch.Fail(ToolResult.Fail(
|
||||
$"API Fehler: HTTP {(int)response.StatusCode} {response.ReasonPhrase} von {safeUrl}\nAntwort: {preview}"));
|
||||
}
|
||||
|
||||
@@ -320,19 +348,19 @@ public sealed class DirectApiTool : IAgentTool
|
||||
if (trimmed.Length == 0 || (trimmed[0] != '{' && trimmed[0] != '['))
|
||||
{
|
||||
var preview = body.Length > 300 ? body[..300] + "…" : body;
|
||||
return (null, ToolResult.Fail(
|
||||
return JsonFetch.Fail(ToolResult.Fail(
|
||||
$"API hat kein JSON zurückgegeben ({safeUrl}). Antwort: {preview}"));
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var json = JsonDocument.Parse(body).RootElement.Clone();
|
||||
return (json, null);
|
||||
return JsonFetch.Ok(json);
|
||||
}
|
||||
catch (JsonException ex)
|
||||
{
|
||||
var preview = body.Length > 300 ? body[..300] + "…" : body;
|
||||
return (null, ToolResult.Fail(
|
||||
return JsonFetch.Fail(ToolResult.Fail(
|
||||
$"Ungültiges JSON von {safeUrl}: {ex.Message}\nAntwort: {preview}"));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user