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
@@ -86,11 +86,11 @@ public static class HtmlExtraktor
|
||||
bereich = doc.QuerySelectorAll(selector).ToList();
|
||||
bereichsName = "selector";
|
||||
if (bereich.Count == 0)
|
||||
hinweis = $"Selektor '{selector}' traf nichts - Hauptinhalt verwendet.";
|
||||
hinweis = $"Selector '{selector}' matched nothing - main content used.";
|
||||
}
|
||||
catch (Exception ex) when (ex is DomException or ArgumentException)
|
||||
{
|
||||
hinweis = $"Selektor '{selector}' ist ungueltig ({ex.Message}) - Hauptinhalt verwendet.";
|
||||
hinweis = $"Selector '{selector}' is invalid ({ex.Message}) - main content used.";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -271,7 +271,7 @@ public static class HtmlExtraktor
|
||||
case "img":
|
||||
{
|
||||
var alt = Normalisiere(e.GetAttribute("alt") ?? "");
|
||||
if (alt.Length > 0) sb.Append("[Bild: ").Append(alt).Append(']');
|
||||
if (alt.Length > 0) sb.Append("[Image: ").Append(alt).Append(']');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -37,7 +37,7 @@ public static class JsonFilter
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return (body.Trim(), "roh", "filter nicht angewendet: Der Inhalt ist kein gueltiges JSON - ungefiltert geliefert.");
|
||||
return (body.Trim(), "roh", "filter not applied: Content is not valid JSON - returned unfiltered.");
|
||||
}
|
||||
|
||||
var treffer = new List<object>();
|
||||
@@ -48,10 +48,10 @@ public static class JsonFilter
|
||||
var text = JsonSerializer.Serialize(ergebnis, WebInhalt.JsonOptionen);
|
||||
|
||||
var hinweis = gesamt == 0
|
||||
? $"Keine Eintraege mit '{filter}' gefunden (Antwort insgesamt {roh.Length} Zeichen)."
|
||||
? $"No entries matching '{filter}' found (response total {roh.Length} characters)."
|
||||
: gesamt > MaxTreffer
|
||||
? $"{gesamt} Treffer fuer '{filter}', die ersten {MaxTreffer} geliefert - grenze den filter ein."
|
||||
: $"{gesamt} Treffer fuer '{filter}' aus {roh.Length} Zeichen Gesamtantwort.";
|
||||
? $"{gesamt} matches for '{filter}', first {MaxTreffer} returned - narrow down the filter."
|
||||
: $"{gesamt} matches for '{filter}' from {roh.Length} characters total response.";
|
||||
|
||||
return (text, "gefiltert", hinweis);
|
||||
}
|
||||
|
||||
@@ -37,31 +37,31 @@ public static class UrlGuard
|
||||
|
||||
if (string.IsNullOrWhiteSpace(url))
|
||||
{
|
||||
error = "Leere Adresse.";
|
||||
error = "Empty address.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!Uri.TryCreate(url.Trim(), UriKind.Absolute, out var parsed))
|
||||
{
|
||||
error = "Keine gültige absolute URL.";
|
||||
error = "Not a valid absolute URL.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (parsed.Scheme != Uri.UriSchemeHttp && parsed.Scheme != Uri.UriSchemeHttps)
|
||||
{
|
||||
error = $"Schema '{parsed.Scheme}' ist nicht erlaubt — nur http und https.";
|
||||
error = $"Scheme '{parsed.Scheme}' is not allowed — only http and https.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (IsPrivateOrLocal(parsed.Host))
|
||||
{
|
||||
error = $"Adresse '{parsed.Host}' liegt im lokalen oder privaten Netz und ist gesperrt.";
|
||||
error = $"Address '{parsed.Host}' is in the local or private network and is blocked.";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!IsDomainAllowed(parsed.Host, allowedDomains))
|
||||
{
|
||||
error = $"Domain '{parsed.Host}' steht nicht auf der Whitelist dieses Agenten.";
|
||||
error = $"Domain '{parsed.Host}' is not on this agent's whitelist.";
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -213,14 +213,14 @@ public static class UrlGuard
|
||||
: await resolve(value, ct);
|
||||
|
||||
if (addresses.Length == 0)
|
||||
throw new HttpRequestException($"Adresse '{host}' ließ sich nicht auflösen.");
|
||||
throw new HttpRequestException($"Address '{host}' could not be resolved.");
|
||||
|
||||
foreach (var address in addresses)
|
||||
{
|
||||
if (IsPrivateOrLocal(address))
|
||||
{
|
||||
throw new HttpRequestException(
|
||||
$"Adresse '{host}' löst auf {address} im lokalen oder privaten Netz auf und ist gesperrt.");
|
||||
$"Address '{host}' resolves to {address} in the local or private network and is blocked.");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -94,28 +94,28 @@ public static class WebInhalt
|
||||
{
|
||||
text = voll;
|
||||
bereich = "vollstaendig";
|
||||
hinweis = "Hauptinhalt zu kurz - vollstaendiger Seitentext geliefert. Baut die Seite ihren "
|
||||
+ "Inhalt per JavaScript auf, fehlt er auch hier.";
|
||||
hinweis = "Main content too short - full page text returned. If the page builds its "
|
||||
+ "content via JavaScript, it is missing here as well.";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (art == "html" && !string.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
const string nurJson = "filter gilt nur fuer JSON - fuer HTML nimm selector.";
|
||||
const string nurJson = "filter only applies to JSON - for HTML use selector.";
|
||||
hinweis = hinweis is null ? nurJson : $"{hinweis} {nurJson}";
|
||||
}
|
||||
|
||||
var (teil, start, naechster) = Abschnitt(text, offset, maxZeichen);
|
||||
if (naechster is not null)
|
||||
{
|
||||
var fortsetzung = $"Inhalt gekuerzt ({teil.Length} von {text.Length} Zeichen) - mit offset={naechster} weiterlesen, nur falls noetig.";
|
||||
var fortsetzung = $"Content truncated ({teil.Length} of {text.Length} characters) - continue with offset={naechster} only if necessary.";
|
||||
|
||||
// #632: Jeder weitere Abschnitt bleibt im Kontext und wird bei jedem folgenden Schritt
|
||||
// erneut bezahlt. Fuer die DWD-Warnungen las ein Agent fuenf Abschnitte - 160.000 Tokens
|
||||
// fuer eine Frage nach einer einzigen Stadt.
|
||||
if (art == "json" && bereich != "gefiltert")
|
||||
fortsetzung += " Suchst du bestimmte Eintraege, hole sie mit 'filter' (z.B. filter=\"Berlin\") statt weiterzublaettern.";
|
||||
fortsetzung += " If looking for specific entries, retrieve them with 'filter' (e.g. filter=\"Berlin\") instead of paginating.";
|
||||
|
||||
hinweis = hinweis is null ? fortsetzung : $"{hinweis} {fortsetzung}";
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user