i18n: Englisch als Hauptsprache, Sprachpakete fuer Desktop und WebUI (#661)
Umbau durch Gemini Flash, Durchsicht und Nacharbeit durch Claude. Umfang: - Desktop: LocalizationManager (JSON-Pakete, Rueckfall auf Englisch, Umschalten zur Laufzeit), Localize-Markup-Erweiterung, Sprachwahl in den Einstellungen (AppSettings.Language, Vorgabe "en"); 317 Schluessel je Sprache - WebUI: alle Ansichten auf t()/data-i18n, 656 Schluessel je Sprache - Agenten-Werkzeuge: Beschreibungen, Schemata und Meldungen auf Englisch - Tests an die englischen Meldungen angepasst, keine Pruefung abgeschwaecht Nacharbeit bei der Durchsicht: - ~25 Kommentarbloecke mit Begruendungen waren ersatzlos entfallen, darunter sicherheitsrelevante (NTFS-Datenstrom beim shared:-Praefix, Bremse gegen Agentenschleifen, doppelter POST beim Anlegen einer Instanz). Wieder eingesetzt: auf Englisch, wo die Datei umgestellt ist, sonst im Wortlaut. - Drei WebUI-Schluessel fehlten (logs.paused, instances.instanceOfflineError, instances.noInstanceActiveError). Weil t() bei fehlendem Schluessel den Schluessel selbst liefert, griff der "|| err.message"-Rueckfall nie — die Oberflaeche haette den Schluesselnamen angezeigt. Geprueft: Schluesselgleichheit en/de, Platzhalter je Schluessel, jeder im Code benutzte Schluessel vorhanden, node --check fuer alle Module, 1.220 Tests gruen. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
69718122f3
commit
ad71682c2d
@@ -12,23 +12,22 @@ public sealed class WebFetchTool : IAgentTool
|
||||
public string Name => "WebFetch";
|
||||
|
||||
public string Description => """
|
||||
Ruft Webseiten, JSON-/Text-Schnittstellen und RSS/Atom-Feeds ab. Nur Domains aus der
|
||||
Whitelist, kein JavaScript-Rendering.
|
||||
Fetches web pages, JSON/text APIs, and RSS/Atom feeds. Only whitelist domains, no JavaScript rendering.
|
||||
|
||||
fetch: Bei HTML kommt standardmaessig nur der Hauptinhalt als kompaktes Markdown
|
||||
(Ueberschriften, Absaetze, Listen, Tabellen, Links) - der guenstigste Weg. Mit
|
||||
"selector" (CSS, z.B. "table", "#kurse", "article h2") holst du gezielt nur den Teil,
|
||||
den du brauchst - noch guenstiger. Linkziele (URLs) kommen nur mit links=true - nur
|
||||
setzen, wenn du die Adressen wirklich brauchst. mode="full" liefert den ganzen Seitentext und kostet
|
||||
ein Vielfaches: nur verwenden, wenn der Hauptinhalt fehlt oder unvollstaendig ist.
|
||||
JSON, Text und XML kommen unveraendert. Brauchst du aus einer grossen JSON-Antwort nur
|
||||
bestimmte Eintraege (z.B. eine Stadt), nimm "filter": dann kommen nur die Objekte, die den
|
||||
Text enthalten - viel guenstiger als Blaettern. Lange Inhalte kommen in Abschnitten: die Antwort
|
||||
nennt nextOffset - nur weiterlesen, wenn der Rest fuer den Auftrag wirklich gebraucht wird.
|
||||
fetch: For HTML, by default only the main content is returned as compact Markdown
|
||||
(headings, paragraphs, lists, tables, links) - the most cost-effective way. With
|
||||
"selector" (CSS, e.g. "table", "#content", "article h2") you selectively retrieve only the part
|
||||
you need - even more economical. Link targets (URLs) are only included with links=true - only
|
||||
set if you really need the URLs. mode="full" returns the full page text and costs
|
||||
much more: only use when main content is missing or incomplete.
|
||||
JSON, text and XML are returned unchanged. If you only need specific entries from a large JSON response
|
||||
(e.g., a city), use "filter": only objects containing the text will be returned - much cheaper
|
||||
than pagination. Long content is returned in chunks: the response includes nextOffset - only continue
|
||||
reading if the remainder is really needed for the task.
|
||||
|
||||
rss: Titel, Link, Datum und kurze Beschreibung je Eintrag ("limit" begrenzt die Anzahl).
|
||||
rss: Title, link, date, and short description per item ("limit" caps the count).
|
||||
|
||||
Waehle immer den fuer den Zweck guenstigsten Weg.
|
||||
Always choose the most efficient path for your purpose.
|
||||
""";
|
||||
|
||||
/// <summary>
|
||||
@@ -58,21 +57,21 @@ public sealed class WebFetchTool : IAgentTool
|
||||
"action": {
|
||||
"type": "string",
|
||||
"enum": ["fetch", "rss"],
|
||||
"description": "fetch=Seite oder Schnittstelle abrufen, rss=RSS/Atom-Feed lesen"
|
||||
"description": "fetch=fetch webpage or API, rss=read RSS/Atom feed"
|
||||
},
|
||||
"url": { "type": "string" },
|
||||
"selector":{ "type": "string",
|
||||
"description": "fetch, optional: CSS-Selektor fuer den benoetigten Teil, z.B. 'table', 'article', '#inhalt'" },
|
||||
"description": "fetch, optional: CSS selector for the required section, e.g. 'table', 'article', '#content'" },
|
||||
"mode": { "type": "string", "enum": ["auto", "full"],
|
||||
"description": "fetch: auto (Standard) = Hauptinhalt als Markdown; full = ganzer Seitentext, nur wenn auto nicht reicht" },
|
||||
"description": "fetch: auto (default) = main content as Markdown; full = full page text, only if auto is insufficient" },
|
||||
"links": { "type": "boolean",
|
||||
"description": "fetch: Linkziele als [Text](URL) mitliefern (Standard false - kostet auf linkreichen Seiten viel)" },
|
||||
"description": "fetch: include link targets as [Text](URL) (default false - expensive on link-heavy pages)" },
|
||||
"filter": { "type": "string",
|
||||
"description": "fetch, nur JSON/JSONP: nur Eintraege liefern, die diesen Text enthalten (ohne Gross-/Kleinschreibung), z.B. 'Berlin'" },
|
||||
"description": "fetch, JSON/JSONP only: only return entries containing this text (case-insensitive), e.g. 'Berlin'" },
|
||||
"offset": { "type": "integer",
|
||||
"description": "fetch: Fortsetzung eines langen Inhalts - Wert aus nextOffset der vorigen Antwort" },
|
||||
"description": "fetch: continuation of long content - value from nextOffset of previous response" },
|
||||
"limit": { "type": "integer",
|
||||
"description": "rss: hoechstens so viele Eintraege (Standard 20, maximal 50)" }
|
||||
"description": "rss: maximum number of items (default 20, max 50)" }
|
||||
}
|
||||
}
|
||||
""").RootElement.Clone();
|
||||
@@ -91,7 +90,7 @@ public sealed class WebFetchTool : IAgentTool
|
||||
? JsonSerializer.Deserialize<Dictionary<string, object?>>(je.GetRawText())
|
||||
: context.ToolConfig;
|
||||
|
||||
if (config == null) return ToolResult.Fail("WebFetch Konfiguration fehlt.");
|
||||
if (config == null) return ToolResult.Fail("WebFetch configuration missing.");
|
||||
|
||||
var allowedDomains = config.GetValueOrDefault("allowedDomains") is JsonElement ad
|
||||
? ad.EnumerateArray().Select(x => x.GetString()!).ToList()
|
||||
@@ -105,13 +104,13 @@ public sealed class WebFetchTool : IAgentTool
|
||||
{
|
||||
"fetch" => await FetchPageAsync(url, allowedDomains, config, input, ct),
|
||||
"rss" => await FetchRssAsync(url, allowedDomains, input, ct),
|
||||
_ => ToolResult.Fail($"Unbekannte Aktion: {action}")
|
||||
_ => ToolResult.Fail($"Unknown action: {action}")
|
||||
};
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
context.Logger.LogError(ex, "Fehler in WebFetch");
|
||||
return ToolResult.Fail($"Fehler: {ex.Message}");
|
||||
context.Logger.LogError(ex, "Error in WebFetch");
|
||||
return ToolResult.Fail($"Error: {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -130,12 +129,12 @@ public sealed class WebFetchTool : IAgentTool
|
||||
// Compiler das nicht an. Die Pruefung sagt dasselbe nachvollziehbar und ersetzt
|
||||
// das Ausrufezeichen, das hier stand.
|
||||
if (response is null)
|
||||
return ToolResult.Fail($"Keine Antwort von {url}.");
|
||||
return ToolResult.Fail($"No response from {url}.");
|
||||
|
||||
using var _ = response;
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return ToolResult.Fail($"Seite nicht erreichbar: {url} → HTTP {(int)response.StatusCode} {response.ReasonPhrase}");
|
||||
return ToolResult.Fail($"Page unreachable: {url} → HTTP {(int)response.StatusCode} {response.ReasonPhrase}");
|
||||
|
||||
var body = await response.Content.ReadAsStringAsync(ct);
|
||||
var maxKb = GetConfigInt(config, "maxResponseKb", 512);
|
||||
@@ -214,12 +213,12 @@ public sealed class WebFetchTool : IAgentTool
|
||||
// Compiler das nicht an. Die Pruefung sagt dasselbe nachvollziehbar und ersetzt
|
||||
// das Ausrufezeichen, das hier stand.
|
||||
if (response is null)
|
||||
return ToolResult.Fail($"Keine Antwort von {url}.");
|
||||
return ToolResult.Fail($"No response from {url}.");
|
||||
|
||||
using var _ = response;
|
||||
|
||||
if (!response.IsSuccessStatusCode)
|
||||
return ToolResult.Fail($"RSS-Feed nicht erreichbar: {url} → HTTP {(int)response.StatusCode} {response.ReasonPhrase}");
|
||||
return ToolResult.Fail($"RSS feed unreachable: {url} → HTTP {(int)response.StatusCode} {response.ReasonPhrase}");
|
||||
|
||||
var xml = await response.Content.ReadAsStringAsync(ct);
|
||||
var doc = XDocument.Parse(xml);
|
||||
@@ -305,7 +304,7 @@ public sealed class WebFetchTool : IAgentTool
|
||||
{
|
||||
if (!UrlGuard.TryValidate(current, allowedDomains, out var uri, out var error))
|
||||
{
|
||||
var context = hop == 0 ? "" : $" (Weiterleitung {hop} von {url})";
|
||||
var context = hop == 0 ? "" : $" (Redirect {hop} from {url})";
|
||||
return (null, ToolResult.Fail($"{error}{context}"));
|
||||
}
|
||||
|
||||
@@ -318,7 +317,7 @@ public sealed class WebFetchTool : IAgentTool
|
||||
response.Dispose();
|
||||
|
||||
if (location is null)
|
||||
return (null, ToolResult.Fail($"Weiterleitung ohne Zieladresse bei {current}."));
|
||||
return (null, ToolResult.Fail($"Redirect without target address at {current}."));
|
||||
|
||||
// Relative Weiterleitungen gegen die aktuelle Adresse auflösen.
|
||||
current = location.IsAbsoluteUri
|
||||
@@ -327,7 +326,7 @@ public sealed class WebFetchTool : IAgentTool
|
||||
}
|
||||
|
||||
return (null, ToolResult.Fail(
|
||||
$"Mehr als {MaxRedirects} Weiterleitungen ausgehend von {url} — abgebrochen."));
|
||||
$"More than {MaxRedirects} redirects starting from {url} — aborted."));
|
||||
}
|
||||
|
||||
private static bool IsRedirect(HttpStatusCode status) => status is
|
||||
|
||||
Reference in New Issue
Block a user