Fruehjahrsputz: WinForms-Altlast entfernt, Dokumentation nachgezogen

Die Avalonia-Portierung ist abgeschlossen, damit ist die in ClawdDotNet.slnx
angekuendigte Aufgabe "WinForms-Oberflaeche entfernen" faellig. Der Stand
davor liegt unter dem Tag vor-fruehjahrsputz-2026-08.

Entfernt (56 Dateien, seit dem Herausloesen der Anwendungsschicht nicht mehr
Teil des Builds): ClawdDotNet.csproj, Program.cs, sieben frm_*-Formulare,
UI/, Models/, EmbeddedUI/, Properties/, Resources/, Services/, das alte
Anwendungssymbol und Deploy-Build.ps1 (ersetzt durch deploy/publish.py).
Dazu configs/*.json - Beispielkonfigurationen aus der Zeit vor dem
Instanzverzeichnis, auf die nur noch die alten Prompts verwiesen.

Die vier Entwicklungs-Prompts der Anfangszeit ziehen nach docs/archiv/ um,
mit README, das ihren Stand einordnet. Eine Regel darin gilt weiter - die
Pflichtfelder fetchedAt/dataAsOf/source der Internet-Tools -, deshalb
Archiv statt Loeschen; der WebSearch-Plan verweist auf den neuen Pfad.

Toter Code
- PlaceholderPageViewModel samt Ansicht: Es gibt keinen Platzhalter-Bereich
  mehr, seit alle neun Seiten portiert sind.
- Snappier als direkter Paketverweis: MongoDB.Driver loest es ohnehin auf
  dieselbe Fassung auf, der Verweis hob nichts an.

Zwei Fehler, die dabei sichtbar wurden
- Die taegliche Sicherung lief ins Leere. Die Oberflaeche bot sie an und
  schrieb Uhrzeit, Zielordner und Anzahl in die Einstellungen, aber der
  BackupScheduler wurde nirgends erzeugt. Jetzt am AppHost verdrahtet und
  in den geordneten Abbau aufgenommen.
- SettingsPageViewModel hielt die vier Sicherungs-Einstellungen doppelt.
  Aus der Ansicht waren sie laengst verschwunden, gelesen und beim
  Speichern zurueckgeschrieben wurden sie weiter: Wer die Uhrzeit auf der
  Sicherungs-Seite aenderte und danach die Einstellungen speicherte, bekam
  den alten Wert zurueck.

Pakete: keine bekannten Sicherheitsluecken mehr
- SQLitePCLRaw.bundle_e_sqlite3 auf 2.1.13 angehoben. Microsoft.Data.Sqlite
  bringt 2.1.11 mit, darin steckt GHSA-2m69-gcr7-jv3q (NU1903, hoch).
- SharpCompress bleibt als direkter Verweis stehen. Beim Aufraeumen erst
  als ungenutzt entfernt - dabei kam die von MongoDB.Driver gezogene
  Fassung 0.30.1 mit GHSA-6c8g-7p36-r338 zurueck. Der Verweis ist eine
  Anhebung, kein Ballast; das steht jetzt als Kommentar dabei.

Dokumentation
- Roadmap mit Statusblock: A1 und A3 erledigt, A2 nur zur Haelfte - Gate,
  Policy und Dienst greifen, aber keine Ansicht ruft ApproveAsync auf, ein
  gestagter Aufruf liegt unbeantwortet. Das ist jetzt Punkt 1 der Reihung.
  Rocket.Chat steht und kollidiert mit A5 (Matrix) - Entscheidung faellig.
- Avalonia-Portierungsleitfaden -> Oberflaechen-Leitfaden: kein Auftrag mehr,
  sondern Beschreibung des Stands.
- Bestandsaufnahme und Linux-Analyse als datierte Befunde gekennzeichnet;
  der teure Teil der Linux-Analyse (8.900 Zeilen WinForms) ist hinfaellig.
- Verweise auf frm_*, WebView2 und ClawdDotNet.csproj in den lebenden
  Dokumenten richtiggestellt.

Build fehlerfrei, 585 Tests gruen (6 uebersprungen).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Richard
2026-08-23 12:26:14 +02:00
co-authored by Claude Opus 5
parent 33d95a6c3f
commit 2853541629
79 changed files with 186 additions and 18381 deletions
@@ -0,0 +1,695 @@
# ClawdDotNet Prompt-Anhang: Internet-Tools
Dieser Abschnitt ergänzt die bestehenden Prompt-Anhänge und definiert drei
unabhängige Internet-Tools sowie ein spezialisiertes Monitoring-Tool für
strukturierte Webseiten (z.B. Capitol Trades).
Alle Tools folgen den bekannten Prinzipien: IAgentTool implementiert,
Konfiguration ausschließlich aus AgentToolContext, keine Tool-zu-Tool-Abhängigkeiten.
---
## Pflicht-Regel: Timestamp in jedem ToolResult
**Diese Regel gilt für ALLE Internet-Tools ohne Ausnahme.**
Jedes `ToolResult.Content` das Finanzdaten, Nachrichten oder externe Daten enthält,
muss ein JSON-Objekt zurückgeben das mindestens enthält:
```json
{
"fetchedAt": "2026-05-13T07:42:00Z",
"dataAsOf": "2026-05-13T07:40:00Z",
"source": "https://...",
"data": { ... }
}
```
- `fetchedAt` = Zeitpunkt des HTTP-Requests (UTC, immer `DateTime.UtcNow`)
- `dataAsOf` = Zeitpunkt der Daten laut Quelle (aus Response-Header, HTML oder API-Feld)
Falls nicht ermittelbar: `null` — NIEMALS schätzen oder weglassen
- `source` = exakte URL die abgerufen wurde
Der System-Prompt jedes Agenten der Internet-Tools nutzt MUSS enthalten:
> "Verwende niemals Daten ohne `fetchedAt`-Feld. Wenn `dataAsOf` null ist,
> teile dem Nutzer mit dass der Datenzeitpunkt unbekannt ist.
> Erfinde niemals Kurse, Preise oder Daten aus dem Gedächtnis."
---
## Tool 1: DirectAPI — Echtzeit-Finanzdaten
**Datei: `ClawdDotNet.Tools.DirectAPI/DirectApiTool.cs`**
Direkter HTTP-Zugriff auf Finanz-APIs. Kein HTML-Parsing.
Strukturiertes JSON mit verifizierbaren Timestamps.
### AgentConfig-Beispiel
```json
"DirectAPI": {
"providers": {
"twelvedata": {
"apiKey": "your-key-here",
"baseUrl": "https://api.twelvedata.com"
},
"alphavantage": {
"apiKey": "your-key-here",
"baseUrl": "https://www.alphavantage.co"
},
"coingecko": {
"baseUrl": "https://api.coingecko.com/api/v3"
},
"yahoo": {
"baseUrl": "https://query1.finance.yahoo.com"
}
},
"defaultProvider": "twelvedata",
"cacheTtlSeconds": 60
}
```
### Tool-Implementierung
```csharp
namespace ClawdDotNet.Tools.DirectAPI;
public sealed class DirectApiTool : IAgentTool
{
public string Name => "DirectAPI";
public string Description => """
Ruft Echtzeit-Finanzdaten von verifizierten APIs ab.
Alle Antworten enthalten fetchedAt und dataAsOf Timestamps.
Aktionen: quote, history, crypto, forex, search
""";
public JsonElement InputSchema => JsonDocument.Parse("""
{
"type": "object",
"required": ["action", "symbol"],
"properties": {
"action": {
"type": "string",
"enum": ["quote", "history", "crypto", "forex", "search"],
"description": "quote=aktueller Kurs, history=Kursverlauf, crypto=Krypto, forex=Wechselkurs, search=Symbol suchen"
},
"symbol": { "type": "string", "description": "z.B. NVDA, BTC, EUR/USD" },
"provider": { "type": "string", "description": "optional: twelvedata|alphavantage|coingecko|yahoo" },
"interval": { "type": "string", "description": "für history: 1min|5min|1h|1day" },
"outputsize":{ "type": "integer","description": "für history: Anzahl Datenpunkte, max 500" }
}
}
""").RootElement;
public async Task<ToolResult> ExecuteAsync(
JsonElement input, AgentToolContext ctx, CancellationToken ct)
{
// Config aus Context lesen — nie aus statischen Feldern
var config = ctx.ToolConfig["DirectAPI"] as Dictionary<string, object?>
?? throw new InvalidOperationException("DirectAPI config missing");
var providers = config["providers"] as Dictionary<string, object?> ?? new();
var cacheTtl = Convert.ToInt32(config.GetValueOrDefault("cacheTtlSeconds") ?? 60);
var action = input.GetProperty("action").GetString()!;
var symbol = input.GetProperty("symbol").GetString()!;
var provider = input.TryGetProperty("provider", out var p)
? p.GetString()
: config.GetValueOrDefault("defaultProvider")?.ToString()
?? "twelvedata";
// Cache-Check: agentId + symbol + action als Key
var cacheKey = $"directapi:{ctx.AgentId}:{provider}:{action}:{symbol}";
// (Cache-Implementierung über IMemoryCache oder Redis aus Core)
return action switch
{
"quote" => await FetchQuoteAsync(symbol, provider, providers, ct),
"history" => await FetchHistoryAsync(input, symbol, provider, providers, ct),
"crypto" => await FetchCryptoAsync(symbol, providers, ct),
"forex" => await FetchForexAsync(symbol, provider, providers, ct),
"search" => await SearchSymbolAsync(symbol, provider, providers, ct),
_ => new ToolResult(false, "", $"Unknown action: {action}")
};
}
private async Task<ToolResult> FetchQuoteAsync(
string symbol, string provider,
Dictionary<string, object?> providers, CancellationToken ct)
{
// Jeder Provider hat eigene URL-Struktur
// Gemeinsam: immer Cache-Control: no-cache Header setzen
// Gemeinsam: dataAsOf aus Response extrahieren (nicht schätzen)
// Twelve Data Quote:
// GET https://api.twelvedata.com/quote?symbol={symbol}&apikey={key}
// Response enthält: "datetime" → das ist dataAsOf
// Response enthält: "timestamp" (Unix) → ebenfalls verwertbar
// Yahoo Finance Quote (kein Key nötig):
// GET https://query1.finance.yahoo.com/v8/finance/chart/{symbol}?interval=1m&range=1d
// Response: result[0].meta.regularMarketTime (Unix timestamp) → dataAsOf
// Alpha Vantage Quote:
// GET https://www.alphavantage.co/query?function=GLOBAL_QUOTE&symbol={symbol}&apikey={key}
// Response: "Global Quote"."07. latest trading day" → dataAsOf (nur Datum, keine Zeit)
// IMPLEMENTIERUNGSREGEL: dataAsOf IMMER aus der API-Antwort lesen.
// Wenn das Feld fehlt oder leer ist → dataAsOf = null, NICHT DateTime.UtcNow.
throw new NotImplementedException("Implement per provider");
}
// history, crypto, forex, search analog implementieren
}
```
### NuGet
Keine externen HTTP-Bibliotheken. Nur `System.Net.Http.HttpClient` via `IHttpClientFactory`.
---
## Tool 2: WebFetch — Nachrichten & strukturiertes HTML
**Datei: `ClawdDotNet.Tools.WebFetch/WebFetchTool.cs`**
HTTP-Abruf mit Whitelist, Timestamp-Extraktion und HTML-zu-Text-Konvertierung.
Kein JavaScript-Rendering (statisches HTML only).
### AgentConfig-Beispiel
```json
"WebFetch": {
"allowedDomains": [
"reuters.com",
"bloomberg.com",
"sec.gov",
"feeds.finance.yahoo.com",
"reddit.com",
"capitoltrades.com"
],
"maxResponseKb": 512,
"timeoutSeconds": 15,
"userAgent": "ClawdDotNet-Agent/1.0 (Research Bot)"
}
```
### Tool-Implementierung
```csharp
namespace ClawdDotNet.Tools.WebFetch;
public sealed class WebFetchTool : IAgentTool
{
public string Name => "WebFetch";
public string Description => """
Ruft statische Webseiten oder RSS-Feeds ab und extrahiert Text + Timestamps.
Nur Domains aus der Whitelist erlaubt. Kein JavaScript-Rendering.
Aktionen: fetch, rss
""";
public JsonElement InputSchema => JsonDocument.Parse("""
{
"type": "object",
"required": ["action", "url"],
"properties": {
"action": {
"type": "string",
"enum": ["fetch", "rss"],
"description": "fetch=HTML-Seite abrufen und zu Text konvertieren, rss=RSS/Atom-Feed parsen"
},
"url": { "type": "string" },
"selector":{ "type": "string",
"description": "optional: CSS-ähnlicher Hint welcher Teil relevant ist, z.B. 'table', 'article'" }
}
}
""").RootElement;
public async Task<ToolResult> ExecuteAsync(
JsonElement input, AgentToolContext ctx, CancellationToken ct)
{
var config = ctx.ToolConfig["WebFetch"] as Dictionary<string, object?> ?? new();
var allowedDomains = (config.GetValueOrDefault("allowedDomains")
as List<string>) ?? new List<string>();
var url = input.GetProperty("url").GetString()!;
var action = input.GetProperty("action").GetString()!;
// Domain-Whitelist prüfen
var host = new Uri(url).Host.Replace("www.", "");
if (!allowedDomains.Any(d => host == d || host.EndsWith("." + d)))
return new ToolResult(false, "",
$"Domain '{host}' nicht in der Whitelist dieses Agenten.");
return action switch
{
"fetch" => await FetchPageAsync(url, input, config, ct),
"rss" => await FetchRssAsync(url, ct),
_ => new ToolResult(false, "", $"Unknown action: {action}")
};
}
private async Task<ToolResult> FetchPageAsync(
string url, JsonElement input,
Dictionary<string, object?> config, CancellationToken ct)
{
using var http = CreateHttpClient(config);
using var request = new HttpRequestMessage(HttpMethod.Get, url);
// Kein Cache — immer frische Daten anfordern
request.Headers.CacheControl = new System.Net.Http.Headers.CacheControlHeaderValue
{ NoCache = true, NoStore = true };
using var response = await http.SendAsync(request, ct);
response.EnsureSuccessStatusCode();
// dataAsOf aus HTTP-Headern extrahieren (Reihenfolge: Last-Modified > Date)
DateTimeOffset? dataAsOf = response.Content.Headers.LastModified
?? response.Headers.Date;
var html = await response.Content.ReadAsStringAsync(ct);
var maxKb = Convert.ToInt32(config.GetValueOrDefault("maxResponseKb") ?? 512);
if (html.Length > maxKb * 1024)
html = html[..(maxKb * 1024)];
// HTML → lesbarer Text (einfache Implementierung ohne externe Libs)
var text = StripHtml(html);
// Versuche dataAsOf aus HTML-Meta-Tags zu verfeinern falls Header fehlt
if (dataAsOf == null)
dataAsOf = ExtractDateFromHtml(html);
var result = new
{
fetchedAt = DateTime.UtcNow,
dataAsOf = dataAsOf?.UtcDateTime,
source = url,
data = new { text }
};
return new ToolResult(true, JsonSerializer.Serialize(result));
}
private async Task<ToolResult> FetchRssAsync(string url, CancellationToken ct)
{
// XML parsen mit System.Xml.Linq
// Einträge: title, link, pubDate (→ dataAsOf), description
// Neueste Einträge zuerst, max 20
throw new NotImplementedException();
}
private static string StripHtml(string html)
{
// Einfaches Regex-basiertes Stripping
// Script- und Style-Tags zuerst entfernen, dann alle anderen Tags
// Anschließend HTML-Entities dekodieren (System.Net.WebUtility.HtmlDecode)
// Mehrfache Leerzeilen auf max. 2 reduzieren
throw new NotImplementedException();
}
private static DateTime? ExtractDateFromHtml(string html)
{
// Suche nach: <time datetime="...">, og:article:published_time,
// datePublished JSON-LD, <meta name="date" content="...">
throw new NotImplementedException();
}
private static HttpClient CreateHttpClient(Dictionary<string, object?> config)
{
var client = new HttpClient();
var timeout = Convert.ToInt32(config.GetValueOrDefault("timeoutSeconds") ?? 15);
client.Timeout = TimeSpan.FromSeconds(timeout);
client.DefaultRequestHeaders.UserAgent.ParseAdd(
config.GetValueOrDefault("userAgent")?.ToString()
?? "ClawdDotNet-Agent/1.0");
return client;
}
}
```
### NuGet
`System.Xml.Linq` (im SDK enthalten) für RSS-Parsing. Kein externes HTML-Parser-Paket nötig.
---
## Tool 3: WebMonitor — Strukturiertes Seiten-Monitoring
**Datei: `ClawdDotNet.Tools.WebMonitor/WebMonitorTool.cs`**
Spezialisiert auf wiederkehrende Überprüfung von Seiten auf **neue Einträge**.
Speichert den zuletzt gesehenen Stand in der Datenbank und liefert nur Deltas.
Primärer Anwendungsfall: Capitol Trades, SEC-Filings, jede tabellarische Seite
mit eindeutigen IDs oder fortlaufenden Einträgen.
### Wie Capitol Trades funktioniert
Die Seite `https://www.capitoltrades.com/trades?pageSize=96` liefert:
- Sauber strukturiertes HTML mit einer Tabelle
- Jeder Trade hat eine eindeutige Trade-ID in der Detail-URL: `/trades/20003797558`
- Trade-IDs sind fortlaufend und numerisch aufsteigend
- Kein JavaScript-Rendering nötig — Daten sind im initialen HTML
Strategie: Höchste bekannte Trade-ID als Anker speichern. Bei jedem Check:
alle IDs auf Seite 1 extrahieren, mit gespeicherter Max-ID vergleichen,
nur neue Einträge melden.
### AgentConfig-Beispiel
```json
"WebMonitor": {
"monitors": {
"capitol_trades": {
"url": "https://www.capitoltrades.com/trades?pageSize=96",
"checkIntervalMinutes": 30,
"idPattern": "/trades/(\\d+)",
"idField": "tradeId",
"parser": "capitol_trades",
"alertOnNew": true,
"storeHistory": true
},
"sec_filings": {
"url": "https://www.sec.gov/cgi-bin/browse-edgar?action=getcurrent&type=4&dateb=&owner=include&count=40",
"checkIntervalMinutes": 60,
"idPattern": "CIK=(\\d+)",
"parser": "sec_form4",
"alertOnNew": true,
"storeHistory": false
}
}
}
```
### Tool-Implementierung
```csharp
namespace ClawdDotNet.Tools.WebMonitor;
public sealed class WebMonitorTool : IAgentTool
{
public string Name => "WebMonitor";
public string Description => """
Überwacht Webseiten auf neue Einträge und liefert nur die Deltas seit dem letzten Check.
Speichert den Stand in der Datenbank. Ideal für Capitol Trades, SEC-Filings, etc.
Aktionen: check, history, status
""";
public JsonElement InputSchema => JsonDocument.Parse("""
{
"type": "object",
"required": ["action", "monitorId"],
"properties": {
"action": {
"type": "string",
"enum": ["check", "history", "status"],
"description": "check=jetzt prüfen und Deltas liefern, history=bisherige Einträge, status=letzter Check-Zeitpunkt"
},
"monitorId": {
"type": "string",
"description": "z.B. capitol_trades, sec_filings — muss in Config definiert sein"
},
"limit": {
"type": "integer",
"description": "max. Anzahl Einträge für history, default 50"
}
}
}
""").RootElement;
public async Task<ToolResult> ExecuteAsync(
JsonElement input, AgentToolContext ctx, CancellationToken ct)
{
var config = ctx.ToolConfig["WebMonitor"] as Dictionary<string, object?> ?? new();
var monitors = config["monitors"] as Dictionary<string, object?> ?? new();
var action = input.GetProperty("action").GetString()!;
var monitorId = input.GetProperty("monitorId").GetString()!;
if (!monitors.ContainsKey(monitorId))
return new ToolResult(false, "",
$"Monitor '{monitorId}' nicht in der Config dieses Agenten definiert.");
var monitorConfig = monitors[monitorId] as Dictionary<string, object?> ?? new();
return action switch
{
"check" => await CheckForNewEntriesAsync(monitorId, monitorConfig, ctx, ct),
"history" => await GetHistoryAsync(monitorId, input, ctx, ct),
"status" => await GetStatusAsync(monitorId, ctx, ct),
_ => new ToolResult(false, "", $"Unknown action: {action}")
};
}
private async Task<ToolResult> CheckForNewEntriesAsync(
string monitorId, Dictionary<string, object?> monitorConfig,
AgentToolContext ctx, CancellationToken ct)
{
var url = monitorConfig["url"]?.ToString()!;
var parser = monitorConfig["parser"]?.ToString() ?? "generic";
var idPattern = monitorConfig["idPattern"]?.ToString();
// 1. Seite abrufen
using var http = new HttpClient();
http.DefaultRequestHeaders.CacheControl =
new System.Net.Http.Headers.CacheControlHeaderValue { NoCache = true };
var html = await http.GetStringAsync(url, ct);
var fetchedAt = DateTime.UtcNow;
// 2. Einträge parsen (parser-spezifisch)
var entries = parser switch
{
"capitol_trades" => ParseCapitolTrades(html),
"sec_form4" => ParseSecForm4(html),
_ => ParseGeneric(html, idPattern)
};
// 3. Letzte bekannte Max-ID aus State laden
// State-Key: "webmonitor:{agentId}:{monitorId}:maxId"
// State-Speicher: über den StateManager aus dem Core
// (wird als Dependency über AgentToolContext injiziert — siehe Core-Erweiterung unten)
var stateKey = $"webmonitor:{ctx.AgentId}:{monitorId}:maxId";
var lastMaxId = await ctx.StateStore.GetAsync(stateKey, ct); // string? → long?
var lastKnown = long.TryParse(lastMaxId, out var l) ? l : 0L;
// 4. Neue Einträge = alle mit ID > lastKnown
var newEntries = entries
.Where(e => e.NumericId > lastKnown)
.OrderBy(e => e.NumericId)
.ToList();
// 5. Neue Max-ID persistieren
if (newEntries.Count > 0)
{
var newMax = newEntries.Max(e => e.NumericId).ToString();
await ctx.StateStore.SetAsync(stateKey, newMax, ct);
// Optional: Einträge in History-Tabelle speichern
if (monitorConfig.GetValueOrDefault("storeHistory") is true)
await StoreHistoryAsync(monitorId, newEntries, ctx, ct);
}
// 6. Letzten Check-Zeitpunkt aktualisieren
await ctx.StateStore.SetAsync(
$"webmonitor:{ctx.AgentId}:{monitorId}:lastCheck",
fetchedAt.ToString("O"), ct);
// 7. Ergebnis
var result = new
{
fetchedAt = fetchedAt,
dataAsOf = fetchedAt, // Capitol Trades: Seite ist immer aktuell
source = url,
monitorId = monitorId,
newCount = newEntries.Count,
data = new
{
newEntries = newEntries,
message = newEntries.Count == 0
? "Keine neuen Einträge seit dem letzten Check."
: $"{newEntries.Count} neue Einträge gefunden."
}
};
return new ToolResult(true, JsonSerializer.Serialize(result));
}
private static List<MonitorEntry> ParseCapitolTrades(string html)
{
// HTML-Tabelle parsen mit System.Text.RegularExpressions + String-Operationen
//
// Zu extrahieren pro Zeile:
// tradeId: aus /trades/(\d+) in der Detail-URL → NumericId
// politician: Linktext des Politiker-Links
// party: "Republican" | "Democrat" aus dem Text
// chamber: "House" | "Senate"
// state: 2-Buchstaben-Code
// issuer: Unternehmensname
// ticker: z.B. "AVGO:US" → nur "AVGO"
// published: Datum "8 May 2026" → DateTime
// traded: Datum "27 Apr 2026" → DateTime
// filedAfterDays: Zahl aus "days N"
// owner: "Undisclosed" | "Spouse" | "Joint" | etc.
// tradeType: "buy" | "sell"
// size: "1K15K" | "15K50K" | etc.
// price: "$418.20" → decimal
// detailUrl: vollständige URL
// WICHTIG: Duplikate durch pageSize=96 möglich (selbe Transaktion, 2 IDs).
// Beide IDs einliefern — der Agent entscheidet ob relevant.
throw new NotImplementedException();
}
private static List<MonitorEntry> ParseSecForm4(string html)
=> throw new NotImplementedException();
private static List<MonitorEntry> ParseGeneric(string html, string? idPattern)
=> throw new NotImplementedException();
private Task StoreHistoryAsync(string monitorId,
List<MonitorEntry> entries, AgentToolContext ctx, CancellationToken ct)
=> throw new NotImplementedException();
private Task<ToolResult> GetHistoryAsync(string monitorId,
JsonElement input, AgentToolContext ctx, CancellationToken ct)
=> throw new NotImplementedException();
private Task<ToolResult> GetStatusAsync(string monitorId,
AgentToolContext ctx, CancellationToken ct)
=> throw new NotImplementedException();
}
public sealed record MonitorEntry(
long NumericId,
string RawId,
string DetailUrl,
DateTime? PublishedAt,
DateTime? TradedAt,
Dictionary<string, string> Fields // flexible Felder je nach Parser
);
```
### Core-Erweiterung: IStateStore
`WebMonitor` braucht persistenten State zwischen Runs (die letzte bekannte Trade-ID).
Dafür muss `AgentToolContext` um ein `IStateStore` erweitert werden:
```csharp
// Core/Tools/AgentToolContext.cs — erweitern:
public sealed record AgentToolContext(
string AgentId,
string InstanceId,
IReadOnlyDictionary<string, object?> ToolConfig,
IStateStore StateStore, // NEU
ILogger Logger,
CancellationToken CancellationToken
);
// Core/State/IStateStore.cs — neues Interface:
namespace ClawdDotNet.Core.State;
public interface IStateStore
{
Task<string?> GetAsync(string key, CancellationToken ct);
Task SetAsync(string key, string value, CancellationToken ct);
Task DeleteAsync(string key, CancellationToken ct);
}
// Implementierungen (im Core oder als separates Projekt):
// - JsonFileStateStore → speichert in ./data/{instanceId}/state.json
// - SqliteStateStore → speichert in ./data/{instanceId}/state.db (empfohlen)
// Beide implementieren IStateStore.
// SqliteStateStore ist bevorzugt: atomic writes, kein Datenverlust bei Absturz.
// NuGet: Microsoft.Data.Sqlite
```
---
## Automatisches Monitoring via Scheduler
Das `WebMonitor`-Tool wird typischerweise nicht interaktiv genutzt, sondern
vom Scheduler getriggert. Konfiguration in der AgentConfig:
```json
{
"agentId": "capitol-watcher",
"displayName": "Capitol Trades Monitor",
"model": "google/gemini-flash-1.5",
"systemPrompt": "Du überwachst Politiker-Trades auf Capitol Trades. Bei neuen Trades analysierst du: Welcher Sektor? Auffälliges Timing? Cluster mehrerer Politiker beim selben Wert? Fasse neue Trades prägnant zusammen. Verwende niemals Daten ohne fetchedAt-Feld.",
"tools": {
"WebMonitor": {
"monitors": {
"capitol_trades": {
"url": "https://www.capitoltrades.com/trades?pageSize=96",
"checkIntervalMinutes": 30,
"idPattern": "/trades/(\\d+)",
"parser": "capitol_trades",
"alertOnNew": true,
"storeHistory": true
}
}
},
"Database": {
"connectionString": "...",
"allowedTables": ["capitol_trades_history", "trade_alerts"]
},
"Mail": {
"smtpHost": "...",
"allowedRecipients": ["owner@example.com"]
}
},
"scheduler": {
"cron": "*/30 * * * *",
"runOnStart": true
},
"loopGuard": {
"maxSteps": 10,
"maxTokens": 30000
}
}
```
Ablauf eines automatischen Runs:
1. Scheduler feuert alle 30 Minuten
2. Agent ruft `WebMonitor.check(capitol_trades)` auf
3. Tool liefert neue Trades als Delta
4. Agent analysiert: Cluster? Insider-Timing? Sektor-Häufung?
5. Bei relevanten Funden: `Mail.send` oder `Database.insert` zur Archivierung
6. Run beendet — kein manueller Eingriff nötig
---
## Implementierungsreihenfolge (für Claude Code)
Bearbeite diesen Abschnitt nach Abschluss der Core- und WinForms-Phase:
1. `IStateStore` Interface + `SqliteStateStore` Implementierung in Core anlegen
2. `AgentToolContext` um `IStateStore` erweitern, alle bestehenden Tool-Calls anpassen
3. `DirectApiTool` implementieren:
- Twelve Data quote + history
- Yahoo Finance quote (kein Key nötig, als Fallback)
- CoinGecko crypto
- Timestamp-Extraktion aus jeweiligem Response-Format
4. `WebFetchTool` implementieren:
- Domain-Whitelist-Check
- HttpClient mit no-cache Headers
- Einfaches HTML-Stripping (ohne externe Libs)
- RSS/Atom-Parser mit System.Xml.Linq
- Timestamp-Extraktion aus HTML-Meta-Tags
5. `WebMonitorTool` implementieren:
- `ParseCapitolTrades` als ersten Parser (Regex auf HTML-Tabelle)
- `CheckForNewEntriesAsync` mit IStateStore-Integration
- `GetHistory` und `GetStatus` Aktionen
- `ParseSecForm4` als zweiten Parser
6. xUnit-Tests:
- `ParseCapitolTrades` gegen gespeichertes HTML-Sample testen
- Delta-Logik: lastKnownId=X, neue IDs=[X-1, X, X+1, X+2] → nur X+1 und X+2
- Domain-Whitelist: erlaubte und gesperrte Domain testen
- Timestamp-Extraktion: Last-Modified Header, og:article:published_time, kein Header
7. Beispiel-Config `capitol-team.json` anlegen
8. In `Program.cs`: WebMonitorTool registrieren, SqliteStateStore als IStateStore in DI
**Beginne mit Schritt 1 dieses Abschnitts.**
@@ -0,0 +1,718 @@
# ClawdDotNet Prompt-Anhang: Tool "TelegramClient"
Dieser Abschnitt ergänzt die bestehenden Prompt-Anhänge und definiert das Tool
`TelegramClient`, das über die Telegram Client API (MTProto) auf den persönlichen
Telegram-Account des Nutzers zugreift. Dieses Tool ist NICHT der bereits vorhandene
Telegram Bot — es nutzt die User-API und kann damit auch Nachrichten aus privaten
Gruppen lesen, in denen der Nutzer Mitglied ist.
**Nur Lese-Zugriff. Kein Senden von Nachrichten.**
---
## Voraussetzungen
### Telegram API Credentials
Der Nutzer muss einmalig auf https://my.telegram.org/apps eine App registrieren.
Ergebnis: `api_id` (Integer) und `api_hash` (String). Diese Werte repräsentieren
die Anwendung (nicht den User) und werden in der InstanceConfig gespeichert.
### Erstmalige Authentifizierung
Beim allerersten Start muss der Nutzer sich interaktiv authentifizieren:
1. Telefonnummer eingeben
2. Verifizierungscode eingeben (kommt per Telegram-App, SMS oder Anruf)
3. Optional: 2FA-Passwort eingeben
Danach wird eine Session-Datei gespeichert. Alle weiteren Starts verwenden
diese Session automatisch — kein erneuter Login nötig.
### NuGet
```xml
<PackageReference Include="WTelegramClient" Version="4.*" />
```
---
## Architektur: Shared Client, Read-Only Access
### Warum ein Shared Client?
Die Telegram Client API erlaubt pro Telefonnummer nur EINE aktive MTProto-Verbindung.
Mehrere Agent-Runs dürfen NICHT jeweils einen eigenen WTelegram.Client instanziieren —
das würde die Session invalidieren und den Login auf dem echten Telegram-Client killen.
Lösung: Ein einziger `WTelegram.Client` wird im Host instanziiert und als Singleton
an alle Agenten weitergegeben. Das Tool selbst ist stateless und greift über den
Shared Client auf Telegram zu.
```
Host (Program.cs)
└─ TelegramClientManager (Singleton)
└─ WTelegram.Client (eine Instanz pro Prozess)
├─ Agent A: TelegramClient-Tool → liest Gruppe "Aktien-Chat"
├─ Agent B: TelegramClient-Tool → liest Gruppe "Krypto-Signals"
└─ Agent C: TelegramClient-Tool → liest DMs
```
### Concurrency
WTelegram.Client ist NICHT thread-safe für gleichzeitige API-Calls.
Der `TelegramClientManager` muss alle Aufrufe über einen `SemaphoreSlim(1,1)`
serialisieren. Da wir nur lesen und die Calls schnell sind (<500ms), ist
die Serialisierung kein Bottleneck.
---
## TelegramClientManager
**Datei: `Host/Services/TelegramClientManager.cs`**
Verwaltet die einzige WTelegram.Client-Instanz. Wird im Host als Singleton registriert.
```csharp
namespace ClawdDotNet.Host.Services;
using WTelegram;
using TL;
public sealed class TelegramClientManager : IAsyncDisposable
{
private Client? _client;
private User? _self;
private readonly SemaphoreSlim _gate = new(1, 1);
private readonly ILogger<TelegramClientManager> _logger;
// Config-Werte aus InstanceConfig
private readonly int _apiId;
private readonly string _apiHash;
private readonly string _phoneNumber;
private readonly string _sessionPath;
private readonly string? _2faPassword;
// Event für interaktive Login-Aufforderung (Code-Eingabe via UI)
public event Func<string, Task<string>>? OnLoginCodeRequired;
public event Func<Task<string>>? On2FAPasswordRequired;
public bool IsConnected => _client?.User != null;
public User? Self => _self;
public TelegramClientManager(
Config.InstanceConfig config,
ILogger<TelegramClientManager> logger)
{
_logger = logger;
var tgConfig = config.TelegramClient
?? throw new InvalidOperationException("TelegramClient config missing in InstanceConfig");
_apiId = tgConfig.ApiId;
_apiHash = tgConfig.ApiHash;
_phoneNumber = tgConfig.PhoneNumber;
_sessionPath = Path.Combine(config.WorkingDirectory, $"telegram_{config.InstanceId}.session");
_2faPassword = tgConfig.Password2FA;
}
public async Task ConnectAsync(CancellationToken ct)
{
// WTelegram.Client mit Config-Callback instanziieren
_client = new Client(ConfigCallback, _sessionPath);
// Logging an ILogger umleiten
Helpers.Log = (lvl, msg) =>
_logger.Log((Microsoft.Extensions.Logging.LogLevel)lvl, "WTelegram: {Message}", msg);
_self = await _client.LoginUserIfNeeded();
_logger.LogInformation(
"Telegram: logged in as {Name} (id {Id})",
_self.first_name, _self.id);
}
private string? ConfigCallback(string what) => what switch
{
"api_id" => _apiId.ToString(),
"api_hash" => _apiHash,
"phone_number" => _phoneNumber,
"session_pathname" => _sessionPath,
// Interaktiver Code — wird über Event an die UI weitergeleitet
"verification_code" => OnLoginCodeRequired != null
? OnLoginCodeRequired("Bitte Telegram-Verifizierungscode eingeben:").Result
: throw new InvalidOperationException(
"Verification code required but no UI handler registered. " +
"Connect OnLoginCodeRequired to prompt the user."),
// 2FA-Passwort — aus Config oder interaktiv
"password" => _2faPassword
?? (On2FAPasswordRequired != null
? On2FAPasswordRequired().Result
: throw new InvalidOperationException(
"2FA password required but not configured.")),
_ => null // Defaults für alles andere
};
/// Alle Dialoge (Chats, Gruppen, Kanäle, DMs) auflisten
public async Task<Messages_Dialogs> GetAllDialogsAsync(CancellationToken ct)
{
await _gate.WaitAsync(ct);
try { return await _client!.Messages_GetAllDialogs(); }
finally { _gate.Release(); }
}
/// Alle Gruppen/Kanäle auflisten (ohne DMs)
public async Task<Messages_Chats> GetAllChatsAsync(CancellationToken ct)
{
await _gate.WaitAsync(ct);
try { return await _client!.Messages_GetAllChats(); }
finally { _gate.Release(); }
}
/// Nachrichten aus einem Chat/Kanal/Gruppe lesen
/// peer: Chat-ID oder Username
/// minId: nur Nachrichten neuer als diese ID (für Delta-Abfragen)
/// limit: max. Anzahl Nachrichten
public async Task<Messages_MessagesBase> GetMessagesAsync(
InputPeer peer, int minId = 0, int limit = 50, CancellationToken ct = default)
{
await _gate.WaitAsync(ct);
try
{
return await _client!.Messages_GetHistory(
peer, offset_id: 0, offset_date: default,
add_offset: 0, limit: limit, max_id: 0, min_id: minId, hash: 0);
}
finally { _gate.Release(); }
}
/// Peer über Username oder Chat-ID auflösen
public async Task<IPeerInfo> ResolveUsernameAsync(string username, CancellationToken ct)
{
await _gate.WaitAsync(ct);
try { return await _client!.Contacts_ResolveUsername(username.TrimStart('@')); }
finally { _gate.Release(); }
}
/// Peer über bekannte Chat-ID auflösen (benötigt vorherigen GetAllChats/Dialogs Aufruf)
public InputPeer? GetInputPeerFromCache(long chatId)
=> _client!.GetInputPeerID(chatId);
public async ValueTask DisposeAsync()
{
_client?.Dispose();
_gate.Dispose();
}
}
```
---
## TelegramClientTool — das IAgentTool
**Datei: `ClawdDotNet.Tools.TelegramClient/TelegramClientTool.cs`**
```csharp
namespace ClawdDotNet.Tools.TelegramClient;
using ClawdDotNet.Core.Tools;
using ClawdDotNet.Host.Services; // TelegramClientManager
using TL;
using System.Text.Json;
public sealed class TelegramClientTool : IAgentTool
{
// Manager wird per DI injiziert (Singleton im Host)
private readonly TelegramClientManager _tg;
public TelegramClientTool(TelegramClientManager tg) => _tg = tg;
public string Name => "TelegramClient";
public string Description => """
Liest Nachrichten aus dem persönlichen Telegram-Account des Nutzers.
Zugriff auf alle Chats, Gruppen und Kanäle in denen der Nutzer Mitglied ist.
NUR LESEN kein Senden, kein Löschen, kein Bearbeiten.
Aktionen: list_chats, read_messages, read_new
""";
public JsonElement InputSchema => JsonDocument.Parse("""
{
"type": "object",
"required": ["action"],
"properties": {
"action": {
"type": "string",
"enum": ["list_chats", "read_messages", "read_new"],
"description": "list_chats: alle Chats/Gruppen/Kanäle auflisten. read_messages: letzte N Nachrichten aus einem Chat lesen. read_new: nur neue Nachrichten seit letztem Abruf."
},
"chatId": {
"type": "integer",
"description": "Chat-ID aus list_chats Ergebnis. Erforderlich für read_messages und read_new."
},
"username": {
"type": "string",
"description": "Alternativ zu chatId: @username einer Gruppe/Person auflösen."
},
"limit": {
"type": "integer",
"description": "Max. Anzahl Nachrichten (default: 30, max: 100)"
}
}
}
""").RootElement;
public async Task<ToolResult> ExecuteAsync(
JsonElement input, AgentToolContext ctx, CancellationToken ct)
{
// ---- Permission-Check: Welche Chats darf dieser Agent lesen? ----
var config = ctx.ToolConfig.GetValueOrDefault("TelegramClient")
as Dictionary<string, object?> ?? new();
var allowedChats = config.GetValueOrDefault("allowedChatIds")
as List<long>; // null = alle erlaubt
var allowedUsernames = config.GetValueOrDefault("allowedUsernames")
as List<string>;
if (!_tg.IsConnected)
return new ToolResult(false, "",
"Telegram-Client ist nicht verbunden. Bitte zuerst authentifizieren.");
var action = input.GetProperty("action").GetString()!;
return action switch
{
"list_chats" => await ListChatsAsync(allowedChats, ct),
"read_messages" => await ReadMessagesAsync(input, allowedChats, config, ctx, ct),
"read_new" => await ReadNewAsync(input, allowedChats, config, ctx, ct),
_ => new ToolResult(false, "", $"Unknown action: {action}")
};
}
private async Task<ToolResult> ListChatsAsync(
List<long>? allowedChats, CancellationToken ct)
{
var dialogs = await _tg.GetAllDialogsAsync(ct);
var chatList = new List<object>();
foreach (Dialog dialog in dialogs.dialogs)
{
var peer = dialogs.UserOrChat(dialog);
if (peer == null) continue;
var chatId = dialog.Peer.ID;
// Filter: nur erlaubte Chats anzeigen (wenn Whitelist definiert)
if (allowedChats != null && !allowedChats.Contains(chatId))
continue;
var info = peer switch
{
User user when user.IsActive => new
{
chatId = chatId,
type = "user",
name = $"{user.first_name} {user.last_name}".Trim(),
username = user.MainUsername,
unread = dialog.UnreadCount,
lastMsgId = dialog.TopMessage
} as object,
ChatBase chat when chat.IsActive => new
{
chatId = chatId,
type = chat is Channel ch
? (ch.IsGroup ? "supergroup" : "channel")
: "group",
name = chat.Title,
username = (chat as Channel)?.MainUsername,
unread = dialog.UnreadCount,
lastMsgId = dialog.TopMessage
} as object,
_ => null
};
if (info != null) chatList.Add(info);
}
var result = new
{
fetchedAt = DateTime.UtcNow,
dataAsOf = DateTime.UtcNow,
source = "telegram_client_api",
data = new
{
totalChats = chatList.Count,
chats = chatList
}
};
return new ToolResult(true, JsonSerializer.Serialize(result));
}
private async Task<ToolResult> ReadMessagesAsync(
JsonElement input, List<long>? allowedChats,
Dictionary<string, object?> config,
AgentToolContext ctx, CancellationToken ct)
{
var (peer, chatId, error) = await ResolvePeerAsync(input, allowedChats, ct);
if (error != null) return new ToolResult(false, "", error);
var limit = input.TryGetProperty("limit", out var l)
? Math.Clamp(l.GetInt32(), 1, 100)
: 30;
var messages = await _tg.GetMessagesAsync(peer!, minId: 0, limit: limit, ct: ct);
var msgList = FormatMessages(messages);
var result = new
{
fetchedAt = DateTime.UtcNow,
dataAsOf = DateTime.UtcNow,
source = $"telegram_chat_{chatId}",
data = new
{
chatId = chatId,
count = msgList.Count,
messages = msgList
}
};
return new ToolResult(true, JsonSerializer.Serialize(result));
}
private async Task<ToolResult> ReadNewAsync(
JsonElement input, List<long>? allowedChats,
Dictionary<string, object?> config,
AgentToolContext ctx, CancellationToken ct)
{
var (peer, chatId, error) = await ResolvePeerAsync(input, allowedChats, ct);
if (error != null) return new ToolResult(false, "", error);
// Letzte bekannte Message-ID aus StateStore laden
var stateKey = $"tgclient:{ctx.AgentId}:chat_{chatId}:lastMsgId";
var lastIdStr = await ctx.StateStore.GetAsync(stateKey, ct);
var lastId = int.TryParse(lastIdStr, out var id) ? id : 0;
var limit = input.TryGetProperty("limit", out var l)
? Math.Clamp(l.GetInt32(), 1, 100)
: 50;
// min_id = lastId → nur Nachrichten neuer als lastId
var messages = await _tg.GetMessagesAsync(peer!, minId: lastId, limit: limit, ct: ct);
var msgList = FormatMessages(messages);
// Neue Max-ID persistieren
if (msgList.Count > 0)
{
var newMaxId = msgList.Max(m => m.messageId);
await ctx.StateStore.SetAsync(stateKey, newMaxId.ToString(), ct);
}
var result = new
{
fetchedAt = DateTime.UtcNow,
dataAsOf = DateTime.UtcNow,
source = $"telegram_chat_{chatId}",
data = new
{
chatId = chatId,
sinceId = lastId,
newCount = msgList.Count,
messages = msgList
}
};
return new ToolResult(true, JsonSerializer.Serialize(result));
}
private async Task<(InputPeer? peer, long chatId, string? error)> ResolvePeerAsync(
JsonElement input, List<long>? allowedChats, CancellationToken ct)
{
long chatId = 0;
InputPeer? peer = null;
if (input.TryGetProperty("chatId", out var cid))
{
chatId = cid.GetInt64();
if (allowedChats != null && !allowedChats.Contains(chatId))
return (null, chatId,
$"Agent hat keinen Zugriff auf Chat {chatId}.");
peer = _tg.GetInputPeerFromCache(chatId);
if (peer == null)
{
// Cache befüllen durch einmaligen GetAllDialogs-Aufruf
await _tg.GetAllDialogsAsync(ct);
peer = _tg.GetInputPeerFromCache(chatId);
}
}
else if (input.TryGetProperty("username", out var uname))
{
var resolved = await _tg.ResolveUsernameAsync(uname.GetString()!, ct);
peer = resolved?.ToInputPeer();
chatId = peer?.ID ?? 0;
if (allowedChats != null && !allowedChats.Contains(chatId))
return (null, chatId,
$"Agent hat keinen Zugriff auf Chat @{uname.GetString()}.");
}
if (peer == null)
return (null, 0, "chatId oder username muss angegeben werden.");
return (peer, chatId, null);
}
private static List<FormattedMessage> FormatMessages(Messages_MessagesBase messages)
{
var result = new List<FormattedMessage>();
foreach (var msgBase in messages.Messages)
{
var from = messages.UserOrChat(msgBase.From ?? msgBase.Peer);
var fromName = from switch
{
User u => $"{u.first_name} {u.last_name}".Trim(),
ChatBase c => c.Title,
_ => "Unknown"
};
if (msgBase is Message msg)
{
result.Add(new FormattedMessage(
messageId: msg.ID,
date: msg.Date,
from: fromName,
fromId: msgBase.From?.ID ?? 0,
text: msg.message,
hasMedia: msg.media != null,
mediaType: msg.media?.GetType().Name,
replyToId: (msg.reply_to as MessageReplyHeader)?.reply_to_msg_id,
forwardFrom: msg.fwd_from != null
? msg.fwd_from.from_name ?? "forwarded"
: null,
views: msg.views
));
}
}
return result.OrderBy(m => m.messageId).ToList();
}
private sealed record FormattedMessage(
int messageId,
DateTime date,
string from,
long fromId,
string? text,
bool hasMedia,
string? mediaType,
int? replyToId,
string? forwardFrom,
int? views
);
}
```
---
## AgentConfig-Beispiel
```json
{
"agentId": "telegram-scout",
"displayName": "Telegram News-Scout",
"model": "google/gemini-flash-1.5",
"systemPrompt": "Du überwachst Telegram-Gruppen auf relevante Finanznachrichten und Trading-Signale. Fasse neue Nachrichten zusammen und bewerte ihre Relevanz. Verwende niemals Daten ohne fetchedAt-Feld.",
"tools": {
"TelegramClient": {
"allowedChatIds": [1001234567890, 1009876543210],
"allowedUsernames": ["aktien_chat", "crypto_signals_de"]
},
"Database": {
"connectionString": "...",
"allowedTables": ["telegram_messages", "signal_archive"]
}
},
"scheduler": {
"cron": "*/15 * * * *",
"runOnStart": true
},
"loopGuard": {
"maxSteps": 10,
"maxTokens": 30000
}
}
```
Ein Agent ohne `TelegramClient`-Eintrag in seiner Config bekommt das Tool
gar nicht erst in seinem LLM-Tool-Set angezeigt (normales Permission-Verhalten).
Ein Agent MIT Config aber ohne `allowedChatIds` (= null) darf alle Chats lesen.
---
## InstanceConfig-Erweiterung
```csharp
// Config/InstanceConfig.cs — neues optionales Feld:
public sealed class InstanceConfig
{
// ... bestehende Felder ...
public TelegramClientConfig? TelegramClient { get; set; }
}
public sealed class TelegramClientConfig
{
public int ApiId { get; set; } // von https://my.telegram.org/apps
public string ApiHash { get; set; } = ""; // von https://my.telegram.org/apps
public string PhoneNumber { get; set; } = ""; // z.B. "+491701234567"
public string? Password2FA { get; set; } // optional, nur bei aktivierter 2FA
}
```
```json
// In stock-team.json:
{
"instanceId": "stock-01",
"instanceName": "Aktien-Team",
"openRouterApiKey": "sk-or-...",
"workingDirectory": "./data/stock/",
"webServerPort": 8081,
"telegramClient": {
"apiId": 12345678,
"apiHash": "abcdef1234567890abcdef1234567890",
"phoneNumber": "+491701234567"
},
"agents": [ ... ]
}
```
---
## Interaktiver Login in WinForms
Der erste Login erfordert einen Verifizierungscode. Dieser wird über das
bestehende Chat-UI in `frm_main` abgefragt — nicht über die Konsole.
**In `frm_main` oder `Program.cs` beim Start:**
```csharp
var tgManager = provider.GetRequiredService<TelegramClientManager>();
// UI-Handler für Code-Eingabe registrieren
tgManager.OnLoginCodeRequired = async (prompt) =>
{
// Auf UI-Thread: InputBox oder Chat-Nachricht anzeigen
string? code = null;
mainForm.Invoke(() =>
{
code = Microsoft.VisualBasic.Interaction.InputBox(
prompt, "Telegram Verifizierung", "");
});
return code ?? "";
};
tgManager.On2FAPasswordRequired = async () =>
{
string? pw = null;
mainForm.Invoke(() =>
{
pw = Microsoft.VisualBasic.Interaction.InputBox(
"Bitte 2FA-Passwort eingeben:", "Telegram 2FA", "");
});
return pw ?? "";
};
// Verbindung herstellen (nutzt Session-Datei wenn vorhanden)
try
{
await tgManager.ConnectAsync(CancellationToken.None);
}
catch (Exception ex)
{
_logger.LogError(ex, "Telegram: Login fehlgeschlagen");
// App startet trotzdem — TelegramClient-Tool meldet "nicht verbunden"
}
```
Nach erfolgreichem Login wird die Session-Datei
`./data/{instanceId}/telegram_{instanceId}.session` gespeichert.
Alle weiteren Starts loggen automatisch ein — kein Code mehr nötig.
---
## Sicherheitsregeln
1. **NUR LESEN** — Das Tool implementiert keine Sende-Funktionen.
Es gibt keine `send_message`-Action. Der `TelegramClientManager`
exponiert bewusst keine `SendMessageAsync`-Methode.
2. **Session-Datei ist sensibel** — Sie enthält die Auth-Keys für den
Telegram-Account. Die Datei liegt im `WorkingDirectory` und darf
NICHT vom FileRW-Tool erreichbar sein. In der AgentConfig für
FileRW darf der `rootPath` NIEMALS auf das WorkingDirectory zeigen
wenn dort die Session-Datei liegt. Empfehlung: Session-Datei in
einem Unterordner `./data/{instanceId}/sessions/` speichern, der
für kein FileRW-Tool als rootPath konfiguriert ist.
3. **Chat-Whitelist pro Agent** — Über `allowedChatIds` kann eingeschränkt
werden welche Chats ein Agent lesen darf. Ein Finanzmarkt-Agent hat
keinen Zugriff auf private DMs. Ein SEO-Agent hat keinen Zugriff auf
Trading-Gruppen.
4. **Rate Limiting** — Die Telegram Client API hat undokumentierte Rate-Limits.
Bei zu vielen Requests kommt ein `FLOOD_WAIT_X` Error. Der
`TelegramClientManager` muss `FloodException` abfangen und
`await Task.Delay(ex.X * 1000)` warten bevor er den Call wiederholt.
Empfehlung: mindestens 1 Sekunde Pause zwischen aufeinanderfolgenden
API-Calls (der SemaphoreSlim allein reicht nicht).
---
## Besonderheiten von WTelegramClient
### Terminology-Mapping
In der Telegram Client API unterscheiden sich die Begriffe von der Benutzeroberfläche:
| Telegram-App | API-Bezeichnung | C#-Typ |
|---|---|---|
| Gruppe (klein) | Chat | `Chat` |
| Gruppe (groß) | Channel mit IsGroup | `Channel` (IsGroup) |
| Kanal | Channel ohne IsGroup | `Channel` (!IsGroup) |
| Privatnachricht | User | `User` |
### access_hash-Problem
Telegram-API-Calls benötigen für die meisten Peers einen `access_hash`.
Dieser wird automatisch gecacht wenn vorher `Messages_GetAllDialogs()`
oder `Messages_GetAllChats()` aufgerufen wurde. Deshalb MUSS bei jedem
Start (nach Login) einmalig `GetAllDialogsAsync()` aufgerufen werden,
bevor `GetMessagesAsync()` funktioniert.
### Session-Datei
- Pfad konfigurierbar über `session_pathname` in der Config-Callback
- Verschlüsselt (Standard-Verschlüsselung von WTelegramClient)
- NICHT zwischen Rechnern portierbar (an Hardware gebunden)
- Bei Session-Problemen: Datei löschen → neuer Login erforderlich
---
## Implementierungsreihenfolge (für Claude Code)
1. `TelegramClientConfig` zu `InstanceConfig` hinzufügen
2. `TelegramClientManager` implementieren (Singleton, SemaphoreSlim, Rate-Limit-Schutz)
3. `TelegramClientTool` implementieren (list_chats, read_messages, read_new)
4. Host: Login-Flow in `Program.cs` / `frm_main` integrieren (InputBox für Code)
5. Sicherheits-Check: Session-Pfad darf nicht in FileRW-rootPath liegen
6. xUnit-Tests: FormatMessages-Serialisierung, Chat-Whitelist-Filter, Rate-Limit-Handling
7. Beispiel-Config ergänzen: `stock-team.json` mit TelegramClient-Eintrag
**Beginne mit Schritt 1 dieses Abschnitts.**
@@ -0,0 +1,792 @@
# ClawdDotNet Prompt-Anhang: WinForms & WebView2 Integration
Dieser Abschnitt ergänzt den Haupt-Entwicklungsprompt und behandelt ausschließlich
die WinForms-UI-Schicht mit WebView2. Er baut auf den bereits definierten Core-Typen
(AgentConfig, InstanceConfig, AgentEngine, IAgentTool etc.) auf.
---
## Übersicht: Zwei WebView2-Kontexte
Es gibt genau zwei WebView2-Kontexte im Host. Sie sind vollständig getrennt
und haben unterschiedliche Sicherheits-Scopes:
| Kontext | Control | Form | Zweck |
|---|---|---|---|
| `webView_chat` | `WebView2` in `frm_main` | Hauptfenster | Agentenübersicht + Auswahl + Chat mit einem Agenten |
| `webView_chat2` | `WebView2` in `frm_chat` | Einzelchat-Fenster | Chat mit genau einem Agenten, mehrfach öffenbar |
`frm_chat` ist bewusst ein eigenständiges, nicht-modales Fenster — es kann mehrfach
instanziiert werden, sodass der Nutzer mehrere Agenten-Chats nebeneinander
auf dem Bildschirm überwachen kann. Jede `frm_chat`-Instanz kennt genau einen `AgentId`.
---
## Sicherheitsarchitektur: Physische Trennung der WebRoots
### Zwei Hostnamen, zwei Quellen — niemals überlappend
```
Assembly (Embedded Resources) Disk (vom FileRW-Tool beschreibbar)
────────────────────────────── ──────────────────────────────────
Host/EmbeddedUI/ data/{instanceId}/
overview.html ← frm_main wwwroot/ ← Kestrel-Root
overview.css webView_chat index.html
overview.js styles/
chat.html ← frm_chat data/
chat.css webView_chat2 assets/
bridge.js
```
**Kernregel:** `EmbeddedUI/` existiert nur als Assembly-Resource.
Sie hat keinen Dateisystempfad, auf den ein Tool zeigen könnte.
Kein `FileRW`-Tool bekommt jemals einen `rootPath`, der auf `EmbeddedUI/` zeigt.
### WebView2 Virtual Host Mapping
```csharp
// Beide Mappings werden in InitWebViewAsync() jeder Form gesetzt:
// Intern aus Assembly-Stream (temporär extrahiert beim Start)
webView.CoreWebView2.SetVirtualHostNameToFolderMapping(
"ui.clwd.internal",
EmbeddedUiManager.GetExtractedPath(), // einmalig beim App-Start nach temp/
CoreWebView2HostResourceAccessKind.DenyCors);
// Extern Agent-generierte Inhalte auf Disk
webView.CoreWebView2.SetVirtualHostNameToFolderMapping(
"dash.clwd.local",
_instanceConfig.WwwRootPath,
CoreWebView2HostResourceAccessKind.Allow);
```
`ui.clwd.internal` → nur lesbar, kein Cross-Origin-Zugriff von außen
`dash.clwd.local` → lesbar für den WebView, schreibbar nur durch FileRW-Tool
### Startvalidierung (Pflicht, einmalig in Program.cs)
```csharp
// Sicherheitscheck beim App-Start Exception wenn verletzt:
var wwwAbs = Path.GetFullPath(instanceConfig.WwwRootPath);
var uiAbs = Path.GetFullPath(EmbeddedUiManager.GetExtractedPath());
if (wwwAbs.StartsWith(uiAbs) || uiAbs.StartsWith(wwwAbs))
throw new InvalidOperationException(
"SECURITY: WwwRootPath and EmbeddedUI path must never overlap.");
```
---
## EmbeddedUiManager
**Datei: `Host/UI/EmbeddedUiManager.cs`**
Aufgabe: HTML/CSS/JS-Dateien aus den Assembly Embedded Resources einmalig beim
Programmstart in einen temporären Ordner extrahieren. WebView2 kann nur auf
Dateisystempfade mappen, nicht direkt auf Streams.
```csharp
namespace ClawdDotNet.Host.UI;
public static class EmbeddedUiManager
{
private static string? _extractedPath;
// Einmalig beim App-Start aufrufen (vor Application.Run)
public static string ExtractToTemp()
{
if (_extractedPath != null) return _extractedPath;
var tempDir = Path.Combine(Path.GetTempPath(), "ClawdDotNet_UI",
Assembly.GetExecutingAssembly()
.GetName().Version?.ToString() ?? "dev");
Directory.CreateDirectory(tempDir);
var asm = Assembly.GetExecutingAssembly();
// Alle Embedded Resources im Namespace "ClawdDotNet.Host.EmbeddedUI"
foreach (var name in asm.GetManifestResourceNames()
.Where(n => n.Contains(".EmbeddedUI.")))
{
// "ClawdDotNet.Host.EmbeddedUI.chat.css" → "chat.css"
var fileName = name.Split(".EmbeddedUI.").Last();
var dest = Path.Combine(tempDir, fileName);
using var stream = asm.GetManifestResourceStream(name)!;
using var file = File.Create(dest);
stream.CopyTo(file);
}
_extractedPath = tempDir;
return tempDir;
}
public static string GetExtractedPath()
=> _extractedPath ?? throw new InvalidOperationException(
"EmbeddedUiManager.ExtractToTemp() must be called first.");
}
```
Embedded Resources werden in der `.csproj` so eingebunden:
```xml
<ItemGroup>
<EmbeddedResource Include="EmbeddedUI\**\*" />
</ItemGroup>
```
---
## C#JavaScript Bridge
**Datei: `Host/UI/WebViewBridge.cs`**
Eine Bridge-Instanz pro WebView2-Control. Kapselt die gesamte
bidirektionale Kommunikation. Keine rohen `ExecuteScriptAsync`-Aufrufe
außerhalb dieser Klasse.
```csharp
namespace ClawdDotNet.Host.UI;
public sealed class WebViewBridge : IDisposable
{
private readonly Microsoft.Web.WebView2.WinForms.WebView2 _wv;
private readonly ILogger<WebViewBridge> _logger;
// Eingehende Nachrichten vom Browser → C#
public event Action<BridgeMessage>? MessageReceived;
public WebViewBridge(
Microsoft.Web.WebView2.WinForms.WebView2 webView,
ILogger<WebViewBridge> logger)
{
_wv = webView;
_logger = logger;
_wv.CoreWebView2.WebMessageReceived += OnWebMessageReceived;
}
// C# → Browser: typisiert, immer als JSON
public async Task SendAsync(BridgeMessage message, CancellationToken ct = default)
{
var json = JsonSerializer.Serialize(message, BridgeJsonOptions.Default);
// Muss auf dem UI-Thread ausgeführt werden
await _wv.InvokeAsync(async () =>
await _wv.CoreWebView2.ExecuteScriptAsync(
$"window.__bridge?.receive({json})"));
}
private void OnWebMessageReceived(object? sender,
CoreWebView2WebMessageReceivedEventArgs e)
{
try
{
var msg = JsonSerializer.Deserialize<BridgeMessage>(
e.WebMessageAsJson, BridgeJsonOptions.Default);
if (msg != null) MessageReceived?.Invoke(msg);
}
catch (Exception ex)
{
_logger.LogError(ex, "Bridge: failed to deserialize incoming message");
}
}
public void Dispose()
=> _wv.CoreWebView2.WebMessageReceived -= OnWebMessageReceived;
}
```
### BridgeMessage Nachrichtenformat
**Datei: `Host/UI/BridgeMessage.cs`**
Alle Nachrichten in beide Richtungen verwenden diesen Typ.
Das `Type`-Feld bestimmt, was in `Payload` steckt.
```csharp
namespace ClawdDotNet.Host.UI;
public sealed record BridgeMessage(
string Type, // siehe Konstanten unten
string? AgentId = null,
string? Content = null, // Chat-Text, HTML-Snippet
string? Status = null, // "running" | "idle" | "error"
int? StepCount = null,
int? TokenCount = null,
string? Error = null,
object? Extra = null // type-spezifische Zusatzdaten
);
// Typ-Konstanten (C# → Browser)
public static class BridgeTypes
{
// frm_main: overview.html
public const string AgentListUpdate = "agent_list_update"; // Alle Agenten initial laden
public const string AgentStatusUpdate = "agent_status"; // Statusänderung eines Agenten
public const string SelectAgent = "select_agent"; // Agenten im Chat auswählen
// frm_main + frm_chat: chat.html
public const string ChatMessage = "chat_message"; // Neue Nachricht anzeigen
public const string ChatTyping = "chat_typing"; // Tipp-Indikator an/aus
public const string ChatHistory = "chat_history"; // Verlauf beim Öffnen laden
public const string RunStarted = "run_started"; // Agent-Run begann
public const string RunFinished = "run_finished"; // Agent-Run beendet
// Browser → C# (eingehend)
public const string UserMessage = "user_message"; // Nutzer hat Enter gedrückt
public const string OpenAgentChat = "open_agent_chat"; // "Eigenes Fenster öffnen"
public const string RunNow = "run_now"; // Manueller Run-Trigger
public const string AbortRun = "abort_run"; // Run abbrechen
}
```
---
## frm_main Hauptfenster
**Datei: `Host/Forms/frm_main.cs`**
`frm_main` enthält `webView_chat` (bereits angelegt). Dieses WebView zeigt
`overview.html`: eine Seitenleiste mit allen Agenten und einen Chat-Bereich
für den aktuell ausgewählten Agenten.
```csharp
namespace ClawdDotNet.Host.Forms;
public partial class frm_main : Form
{
private readonly InstanceConfig _instance;
private readonly AgentEngine _engine;
private readonly AgentScheduler _scheduler;
private readonly ILogger<frm_main> _logger;
private WebViewBridge? _bridge;
private string? _selectedAgentId;
// Offene Einzelchat-Fenster: AgentId → frm_chat
private readonly Dictionary<string, frm_chat> _chatWindows = new();
public frm_main(InstanceConfig instance, AgentEngine engine,
AgentScheduler scheduler, ILogger<frm_main> logger)
{
InitializeComponent();
_instance = instance;
_engine = engine;
_scheduler = scheduler;
_logger = logger;
}
private async void frm_main_Load(object sender, EventArgs e)
{
await InitWebViewAsync();
_scheduler.RunStatusChanged += OnRunStatusChanged; // Event aus Core
}
private async Task InitWebViewAsync()
{
await webView_chat.EnsureCoreWebView2Async();
// Virtual Host Mappings
webView_chat.CoreWebView2.SetVirtualHostNameToFolderMapping(
"ui.clwd.internal",
EmbeddedUiManager.GetExtractedPath(),
CoreWebView2HostResourceAccessKind.DenyCors);
webView_chat.CoreWebView2.SetVirtualHostNameToFolderMapping(
"dash.clwd.local",
_instance.WwwRootPath,
CoreWebView2HostResourceAccessKind.Allow);
_bridge = new WebViewBridge(webView_chat, /* logger */);
_bridge.MessageReceived += OnBridgeMessage;
webView_chat.CoreWebView2.Navigate(
"https://ui.clwd.internal/overview.html");
// Kurz warten bis DOM bereit, dann Agentenliste senden
await Task.Delay(300);
await PushAgentListAsync();
}
private async Task PushAgentListAsync()
{
// Alle AgentConfigs als Liste → overview.html baut die Sidebar auf
var agents = _instance.Agents.Select(a => new
{
agentId = a.AgentId,
displayName = a.DisplayName,
model = a.Model,
status = _engine.GetStatus(a.AgentId) // "idle"|"running"|"error"
});
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.AgentListUpdate,
Extra: agents));
}
private async void OnBridgeMessage(BridgeMessage msg)
{
// Immer auf UI-Thread
if (InvokeRequired) { Invoke(() => OnBridgeMessage(msg)); return; }
switch (msg.Type)
{
case BridgeTypes.UserMessage:
// Nutzer hat im Chat Enter gedrückt
if (_selectedAgentId is null || msg.Content is null) break;
await HandleUserMessageAsync(_selectedAgentId, msg.Content);
break;
case BridgeTypes.SelectAgent:
// Agenten in der Sidebar angeklickt → Chat-Verlauf laden
_selectedAgentId = msg.AgentId;
await LoadChatHistoryAsync(msg.AgentId!);
break;
case BridgeTypes.OpenAgentChat:
// "Eigenes Fenster" Button → frm_chat öffnen oder fokussieren
OpenChatWindow(msg.AgentId!);
break;
case BridgeTypes.RunNow:
_ = _engine.RunAsync(msg.AgentId!, CancellationToken.None);
break;
case BridgeTypes.AbortRun:
_engine.Abort(msg.AgentId!);
break;
}
}
private void OpenChatWindow(string agentId)
{
if (_chatWindows.TryGetValue(agentId, out var existing)
&& !existing.IsDisposed)
{
existing.BringToFront();
return;
}
var agentConfig = _instance.Agents.First(a => a.AgentId == agentId);
var frm = new frm_chat(agentConfig, _engine, /* logger */);
frm.FormClosed += (_, _) => _chatWindows.Remove(agentId);
_chatWindows[agentId] = frm;
frm.Show(this); // nicht-modal, Elternfenster = frm_main
}
private void OnRunStatusChanged(string agentId, AgentRunStatus status)
{
// Vom Scheduler/Engine gefeuert auf UI-Thread pushen
this.InvokeAsync(async () =>
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.AgentStatusUpdate,
AgentId: agentId,
Status: status.ToString().ToLower(),
StepCount: status.StepCount,
TokenCount: status.TokensUsed)));
}
private async Task HandleUserMessageAsync(string agentId, string text)
{
// Eigene Nachricht sofort anzeigen
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatMessage,
AgentId: agentId,
Content: text,
Extra: new { role = "user", timestamp = DateTime.Now }));
// Tipp-Indikator an
await _bridge.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatTyping, AgentId: agentId));
// Chat-Run starten (non-blocking)
_ = Task.Run(async () =>
{
var result = await _engine.ChatAsync(agentId, text, CancellationToken.None);
await _bridge.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatMessage,
AgentId: agentId,
Content: result.FinalMessage,
Extra: new { role = "agent", timestamp = DateTime.Now }));
});
}
private async Task LoadChatHistoryAsync(string agentId)
{
var history = await _engine.GetChatHistoryAsync(agentId);
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatHistory,
AgentId: agentId,
Extra: history));
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
_bridge?.Dispose();
_scheduler.RunStatusChanged -= OnRunStatusChanged;
base.OnFormClosed(e);
}
}
```
---
## frm_chat Einzelchat-Fenster
**Datei: `Host/Forms/frm_chat.cs`**
`frm_chat` enthält `webView_chat2` (bereits angelegt). Dieses Fenster zeigt
den Chat mit genau einem Agenten. Es kann beliebig oft gleichzeitig geöffnet
sein — jede Instanz ist vollständig unabhängig.
```csharp
namespace ClawdDotNet.Host.Forms;
public partial class frm_chat : Form
{
private readonly AgentConfig _agentConfig;
private readonly AgentEngine _engine;
private readonly ILogger<frm_chat> _logger;
private WebViewBridge? _bridge;
public frm_chat(AgentConfig agentConfig, AgentEngine engine,
ILogger<frm_chat> logger)
{
InitializeComponent();
_agentConfig = agentConfig;
_engine = engine;
_logger = logger;
// Fenstertitel = Agent-Name
Text = $"Chat {agentConfig.DisplayName}";
}
private async void frm_chat_Load(object sender, EventArgs e)
=> await InitWebViewAsync();
private async Task InitWebViewAsync()
{
await webView_chat2.EnsureCoreWebView2Async();
// Identische Virtual Host Mappings wie frm_main
webView_chat2.CoreWebView2.SetVirtualHostNameToFolderMapping(
"ui.clwd.internal",
EmbeddedUiManager.GetExtractedPath(),
CoreWebView2HostResourceAccessKind.DenyCors);
webView_chat2.CoreWebView2.SetVirtualHostNameToFolderMapping(
"dash.clwd.local",
// WwwRootPath kommt vom InstanceConfig über DI/Singleton
ServiceLocator.Get<InstanceConfig>().WwwRootPath,
CoreWebView2HostResourceAccessKind.Allow);
_bridge = new WebViewBridge(webView_chat2, /* logger */);
_bridge.MessageReceived += OnBridgeMessage;
// chat.html lädt für einen bestimmten Agenten
webView_chat2.CoreWebView2.Navigate(
$"https://ui.clwd.internal/chat.html?agent={_agentConfig.AgentId}");
await Task.Delay(300);
// AgentInfo und Verlauf initial senden
await _bridge.SendAsync(new BridgeMessage(
Type: BridgeTypes.AgentListUpdate,
AgentId: _agentConfig.AgentId,
Extra: new { agents = new[] { new {
agentId = _agentConfig.AgentId,
displayName = _agentConfig.DisplayName,
model = _agentConfig.Model
}}}));
await LoadChatHistoryAsync();
}
private async void OnBridgeMessage(BridgeMessage msg)
{
if (InvokeRequired) { Invoke(() => OnBridgeMessage(msg)); return; }
switch (msg.Type)
{
case BridgeTypes.UserMessage:
await HandleUserMessageAsync(msg.Content ?? "");
break;
case BridgeTypes.RunNow:
_ = _engine.RunAsync(_agentConfig.AgentId, CancellationToken.None);
break;
case BridgeTypes.AbortRun:
_engine.Abort(_agentConfig.AgentId);
break;
}
}
private async Task HandleUserMessageAsync(string text)
{
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatMessage,
AgentId: _agentConfig.AgentId,
Content: text,
Extra: new { role = "user", timestamp = DateTime.Now }));
await _bridge.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatTyping, AgentId: _agentConfig.AgentId));
_ = Task.Run(async () =>
{
var result = await _engine.ChatAsync(
_agentConfig.AgentId, text, CancellationToken.None);
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatMessage,
AgentId: _agentConfig.AgentId,
Content: result.FinalMessage,
Extra: new { role = "agent", timestamp = DateTime.Now }));
});
}
private async Task LoadChatHistoryAsync()
{
var history = await _engine.GetChatHistoryAsync(_agentConfig.AgentId);
await _bridge!.SendAsync(new BridgeMessage(
Type: BridgeTypes.ChatHistory,
AgentId: _agentConfig.AgentId,
Extra: history));
}
protected override void OnFormClosed(FormClosedEventArgs e)
{
_bridge?.Dispose();
base.OnFormClosed(e);
}
}
```
---
## Embedded HTML/JS/CSS Dateistruktur
Alle Dateien liegen in `Host/EmbeddedUI/`. Build Action: `Embedded Resource`.
### overview.html (für webView_chat in frm_main)
Dieses HTML baut die komplette Ansicht aus dem Mockup auf:
- Linke Sidebar: Agentenliste (wird via Bridge befüllt)
- Rechter Bereich: Chat mit dem aktuell ausgewählten Agenten
- "Eigenes Fenster"-Button pro Agent → sendet `open_agent_chat`-Nachricht
Kommunikationsprotokoll (JavaScript-Seite):
```javascript
// bridge.js wird von beiden HTML-Seiten eingebunden
window.__bridge = {
// Eingehend von C#
receive(msg) {
document.dispatchEvent(
new CustomEvent('bridge:' + msg.type, { detail: msg }));
},
// Ausgehend zu C#
send(msg) {
window.chrome.webview.postMessage(JSON.stringify(msg));
}
};
// Beispiel: auf Agentenliste reagieren
document.addEventListener('bridge:agent_list_update', e => {
renderSidebar(e.detail.extra.agents);
});
// Beispiel: Nachricht senden
function sendUserMessage(agentId, text) {
window.__bridge.send({
type: 'user_message',
agentId: agentId,
content: text
});
}
```
### chat.html (für webView_chat2 in frm_chat)
Vereinfachte Version ohne Sidebar — nur der Chat-Bereich.
Liest den `?agent=`-URL-Parameter beim Laden und stellt sich
damit auf den entsprechenden Agenten ein.
```javascript
// chat.html Init
const agentId = new URLSearchParams(location.search).get('agent');
document.addEventListener('bridge:chat_history', e => {
if (e.detail.agentId !== agentId) return;
renderHistory(e.detail.extra);
});
document.addEventListener('bridge:chat_message', e => {
if (e.detail.agentId !== agentId) return;
appendBubble(e.detail.extra.role, e.detail.content,
e.detail.extra.timestamp);
});
document.addEventListener('bridge:chat_typing', e => {
if (e.detail.agentId !== agentId) return;
showTypingIndicator();
});
```
---
## Program.cs Startup-Reihenfolge
```csharp
// Host/Program.cs
[STAThread]
static async Task Main(string[] args)
{
Application.EnableVisualStyles();
Application.SetCompatibleTextRenderingDefault(false);
// 1. Config laden (--config Argument oder default)
var configPath = GetConfigPath(args);
var instance = InstanceConfig.LoadFromFile(configPath);
// 2. Sicherheitscheck: Pfade dürfen sich nicht überlappen
var uiPath = EmbeddedUiManager.ExtractToTemp(); // extrahiert EmbeddedUI
var wwwPath = Path.GetFullPath(instance.WwwRootPath);
if (wwwPath.StartsWith(uiPath) || uiPath.StartsWith(wwwPath))
throw new InvalidOperationException(
"SECURITY: WwwRootPath and EmbeddedUI path must never overlap.");
// 3. DI-Container aufbauen
var services = new ServiceCollection();
services.AddSingleton(instance);
services.AddSingleton<ToolRegistry>();
services.AddSingleton<PermissionGate>();
services.AddSingleton<AgentEngine>();
services.AddSingleton<AgentScheduler>();
services.AddSingleton<OpenRouterClient>();
services.AddLogging(b => b.AddConsole());
// 4. Tools registrieren (Host ist der einzige Ort, der Tool-Typen kennt)
var provider = services.BuildServiceProvider();
var registry = provider.GetRequiredService<ToolRegistry>();
registry.Register(new DatabaseTool());
registry.Register(new FileRwTool());
registry.Register(new MailTool());
// 5. Scheduler starten
var scheduler = provider.GetRequiredService<AgentScheduler>();
await scheduler.StartAsync(CancellationToken.None);
// 6. WinForms starten
var mainForm = provider.GetRequiredService<frm_main>();
Application.Run(mainForm);
// 7. Cleanup
await scheduler.StopAsync(CancellationToken.None);
}
```
---
## Core-Erweiterungen für Chat-Support
Der `AgentEngine` im Core benötigt zwei zusätzliche Methoden für
den interaktiven Chat-Modus (ergänze Phase 1.7):
```csharp
// AgentEngine zusätzliche Methoden
// Interaktiver Chat: eine Nutzer-Nachricht → Agent-Antwort
// Unterschied zum autonomen Run: kein Scheduler-Trigger,
// Verlauf wird an bestehende Konversation angehängt
Task<AgentRunResult> ChatAsync(string agentId, string userMessage, CancellationToken ct);
// Chat-Verlauf aus dem StateManager laden
// Rückgabe: Liste von { role, content, timestamp }
Task<IReadOnlyList<ChatEntry>> GetChatHistoryAsync(string agentId);
// Aktuellen Run-Status abrufen (für Statusanzeige in der Sidebar)
AgentRunStatus GetStatus(string agentId);
// Laufenden Run abbrechen
void Abort(string agentId);
```
```csharp
// Neue Typen in Core/Engine/
public sealed record ChatEntry(
string Role, // "user" | "agent" | "tool"
string Content,
DateTime Timestamp
);
public sealed record AgentRunStatus(
string State, // "idle" | "running" | "error"
int StepCount,
int TokensUsed,
string? LastError
);
```
Der StateManager (Phase 1.2) speichert den Chat-Verlauf pro AgentId
persistent in der konfigurierten Datenbank oder als JSON-Datei,
sodass Verläufe auch nach Programm-Neustart verfügbar sind.
---
## Entwicklungsregeln: WinForms-spezifisch
- **Kein UI-Thread-Blocking:** Alle `await`-Aufrufe in Forms immer mit
`ConfigureAwait(false)` oder explizitem `InvokeAsync`. Bridge-Callbacks
kommen auf beliebigen Threads — immer per `InvokeRequired` prüfen.
- **WebView2 ist async:** `EnsureCoreWebView2Async()` muss abgewartet sein
bevor `CoreWebView2`-Eigenschaften gesetzt werden.
- **Keine direkte Form-zu-Form-Kommunikation:** `frm_chat` kommuniziert
ausschließlich über `AgentEngine` und `WebViewBridge`. Kein direkter
Methodenaufruf zwischen Form-Instanzen.
- **frm_chat ist nicht-modal:** Immer `frm.Show(owner)` statt
`frm.ShowDialog()`. Mehrere Instanzen mit demselben AgentId: nur
eine öffnen, fokussieren wenn vorhanden (Dictionary-Check in frm_main).
- **Bridge-Nachrichten immer typisiert:** Kein rohes JSON-String-Bauen
außerhalb von `WebViewBridge` und `BridgeMessage`.
- **EmbeddedUI ist readonly:** Keine dynamischen Schreibzugriffe auf
die extrahierten UI-Dateien zur Laufzeit. UI-Änderungen erfordern
Neu-Kompilierung.
---
## NuGet-Pakete (Host-Projekt)
```xml
<PackageReference Include="Microsoft.Web.WebView2" Version="*" />
<PackageReference Include="Microsoft.Extensions.DependencyInjection" Version="*" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="*" />
```
---
## Implementierungsreihenfolge (für Claude Code)
Bearbeite diesen Abschnitt nach Abschluss der Core-Phase (Phasen 12):
1. `EmbeddedUiManager` implementieren und Sicherheitscheck in `Program.cs` einbauen
2. `BridgeMessage` + `BridgeTypes` + `WebViewBridge` implementieren
3. Core: `ChatAsync`, `GetChatHistoryAsync`, `GetStatus`, `Abort` zu `AgentEngine` hinzufügen
4. Core: `ChatEntry` und `AgentRunStatus` Records anlegen
5. `frm_main`: `InitWebViewAsync`, `PushAgentListAsync`, Bridge-Handler implementieren
6. `frm_chat`: vollständig implementieren
7. `EmbeddedUI/bridge.js` erstellen (gemeinsame Bridge-Logik für beide HTML-Seiten)
8. `EmbeddedUI/overview.html` + `overview.css` erstellen (Sidebar + Chat-Bereich)
9. `EmbeddedUI/chat.html` + `chat.css` erstellen (Einzelchat, agentId aus URL-Parameter)
10. `Program.cs` Startup-Reihenfolge implementieren
11. xUnit-Tests: Bridge-Serialisierung, Pfad-Overlap-Check, frm_chat Isolation
**Beginne mit Schritt 1 dieses Abschnitts.**
+470
View File
@@ -0,0 +1,470 @@
# ClawdDotNet Claude Code Entwicklungs-Prompt
## Projektkontext
Wir entwickeln **ClawdDotNet** einen modularen "Coworking Space" für AI-Agenten in **C# .NET 10 / WinForms**.
Das Projekt ist bereits angelegt und hat erste UI-Steuerelemente.
Das System ermöglicht es, mehrere spezialisierte AI-Agenten parallel laufen zu lassen, die gemeinsam
strukturierte Aufgaben erledigen (z.B. Finanzmarktanalyse, Trading-Empfehlungen, SEO, Programmierung).
Als LLM-Backend wird **OpenRouter** verwendet, damit jeder Agent flexibel ein anderes Modell nutzen kann.
---
## Kernprinzipien diese gelten für JEDE Zeile Code
1. **Agenten blockieren sich niemals gegenseitig.**
Alle Agent-Runs laufen vollständig async/await mit eigenem CancellationToken.
Kein shared mutable state ohne explizites Locking. Kein Agent wartet synchron auf einen anderen.
2. **Mehrinstanzfähigkeit von Anfang an.**
Die Anwendung kann mehrfach gleichzeitig gestartet werden (z.B. ein Prozess für Aktien-Team,
ein Prozess für Krypto-Team). Jede Instanz ist vollständig isoliert:
- Eigene Konfigurationsdatei (per Instanz wählbar beim Start, z.B. `--config stock-team.json`)
- Eigene Datenbankverbindungen (keine Shared-DB-Locks ohne explizites Design dafür)
- Eigener Arbeitsordner und wwwroot-Ordner
- Eigener Netzwerk-Port für den integrierten Webserver (konfigurierbar)
- Keine globalen Singletons, keine statischen Felder mit Zustand
3. **Core ist niemals von einem Tool abhängig.**
Der Core kompiliert und läuft vollständig ohne jedes Tool. Fehlt ein Tool, bleibt der Core
funktionsfähig. Tools werden zur Laufzeit registriert.
4. **Tools sind niemals voneinander abhängig.**
Tool A darf Tool B weder referenzieren noch aufrufen. Jedes Tool ist ein eigenständiges Projekt/Assembly.
5. **Jedes Tool ist pro Agent konfiguriert.**
Agent A kann auf eine andere Datenbank zugreifen als Agent B. Agent A darf in `./wwwroot/` schreiben,
Agent B nicht. Die Konfiguration liegt in der AgentConfig, nicht im Tool-Code.
---
## Architektur-Übersicht
```
ClawdDotNet.sln
├── src/
│ ├── ClawdDotNet.Core/ ← .NET 10 Klassenbibliothek, KEIN Tool-Verweis
│ ├── ClawdDotNet.Tools.Database/ ← Tool-Plugin, nur Core-Verweis
│ ├── ClawdDotNet.Tools.FileRW/ ← Tool-Plugin, nur Core-Verweis
│ ├── ClawdDotNet.Tools.Mail/ ← Tool-Plugin, nur Core-Verweis
│ └── ClawdDotNet.Host/ ← WinForms .NET 10, verweist auf Core + alle Tools
└── configs/
├── stock-team.json
└── crypto-team.json
```
---
## Phase 1: Core implementieren
### 1.1 Kern-Interfaces und Datentypen
**Datei: `Core/Tools/IAgentTool.cs`**
```csharp
namespace ClawdDotNet.Core.Tools;
public interface IAgentTool
{
/// Eindeutiger Name, den das LLM in tool_calls verwendet
string Name { get; }
/// Natürlichsprachige Beschreibung für das LLM (geht in den System-Prompt)
string Description { get; }
/// JSON Schema des Input-Objekts (OpenAI Function Calling Format)
System.Text.Json.JsonElement InputSchema { get; }
/// Ausführung bekommt NUR seinen eigenen Kontext, nie andere Tools
Task<ToolResult> ExecuteAsync(
System.Text.Json.JsonElement input,
AgentToolContext context,
CancellationToken ct);
}
```
**Datei: `Core/Tools/AgentToolContext.cs`**
```csharp
namespace ClawdDotNet.Core.Tools;
/// Wird vom Core befüllt und an das Tool übergeben.
/// Das Tool liest seine Konfiguration NUR aus ToolConfig[tool.Name].
public sealed record AgentToolContext(
string AgentId,
string InstanceId, // Mehrinstanz-Isolation
IReadOnlyDictionary<string, object?> ToolConfig, // tool-spezifische Config aus AgentConfig
Microsoft.Extensions.Logging.ILogger Logger,
CancellationToken CancellationToken
);
```
**Datei: `Core/Tools/ToolResult.cs`**
```csharp
namespace ClawdDotNet.Core.Tools;
public sealed record ToolResult(
bool Success,
string Content, // JSON oder Plaintext, geht zurück ans LLM
string? ErrorMessage = null
);
```
### 1.2 AgentConfig (Konfigurationsmodell)
**Datei: `Core/Config/AgentConfig.cs`**
```csharp
namespace ClawdDotNet.Core.Config;
public sealed class AgentConfig
{
public string AgentId { get; set; } = "";
public string DisplayName { get; set; } = "";
public string Model { get; set; } = "anthropic/claude-sonnet-4-5";
public string SystemPrompt { get; set; } = "";
/// Welche Tools darf dieser Agent nutzen?
/// Key = Tool.Name, Value = tool-spezifische Konfiguration (frei definierbar je Tool)
public Dictionary<string, Dictionary<string, object?>> Tools { get; set; } = new();
public SchedulerConfig? Scheduler { get; set; }
public LoopGuardConfig LoopGuard { get; set; } = new();
}
public sealed class SchedulerConfig
{
public string Cron { get; set; } = ""; // z.B. "0 7 * * 1-5"
public bool RunOnStart { get; set; } = false;
}
public sealed class LoopGuardConfig
{
public int MaxSteps { get; set; } = 20;
public int MaxTokens { get; set; } = 80_000;
public TimeSpan Timeout { get; set; } = TimeSpan.FromMinutes(10);
}
```
**Datei: `Core/Config/InstanceConfig.cs`**
```csharp
namespace ClawdDotNet.Core.Config;
/// Instanz-weite Konfiguration (eine pro laufendem Prozess)
public sealed class InstanceConfig
{
public string InstanceId { get; set; } = Guid.NewGuid().ToString("N")[..8];
public string InstanceName { get; set; } = "Default";
public string OpenRouterApiKey { get; set; } = "";
public string WorkingDirectory { get; set; } = "./data/";
public int WebServerPort { get; set; } = 8080;
public List<AgentConfig> Agents { get; set; } = new();
}
```
### 1.3 Tool Registry
**Datei: `Core/Tools/ToolRegistry.cs`**
```csharp
namespace ClawdDotNet.Core.Tools;
/// Thread-safe Registry. Wird beim Start im Host befüllt.
/// Der Core kennt keine konkreten Tool-Typen.
public sealed class ToolRegistry
{
private readonly Dictionary<string, IAgentTool> _tools = new();
private readonly Lock _lock = new();
public void Register(IAgentTool tool)
{
lock (_lock)
_tools[tool.Name] = tool;
}
public IAgentTool? Get(string name)
{
lock (_lock)
return _tools.GetValueOrDefault(name);
}
/// Gibt nur die Tools zurück, für die der Agent eine Config hat
public IReadOnlyList<IAgentTool> GetForAgent(AgentConfig agent)
{
lock (_lock)
return _tools.Values
.Where(t => agent.Tools.ContainsKey(t.Name))
.ToList();
}
}
```
### 1.4 Permission Gate
**Datei: `Core/Security/PermissionGate.cs`**
```csharp
namespace ClawdDotNet.Core.Security;
public sealed class PermissionGate
{
public bool IsAllowed(string agentId, string toolName,
Config.AgentConfig agentConfig)
=> agentConfig.Tools.ContainsKey(toolName);
public void Enforce(string agentId, string toolName,
Config.AgentConfig agentConfig)
{
if (!IsAllowed(agentId, toolName, agentConfig))
throw new ToolAccessDeniedException(agentId, toolName);
}
}
public sealed class ToolAccessDeniedException(string agentId, string toolName)
: Exception($"Agent '{agentId}' has no access to tool '{toolName}'.");
```
### 1.5 Loop Guard
**Datei: `Core/Engine/LoopGuard.cs`**
```csharp
namespace ClawdDotNet.Core.Engine;
/// Pro Agent-Run instanziieren, nicht wiederverwenden.
public sealed class LoopGuard
{
private readonly Config.LoopGuardConfig _cfg;
private int _steps;
private int _tokens;
public LoopGuard(Config.LoopGuardConfig cfg) => _cfg = cfg;
public void RecordStep()
{
if (Interlocked.Increment(ref _steps) > _cfg.MaxSteps)
throw new LoopLimitExceededException($"Max steps ({_cfg.MaxSteps}) exceeded.");
}
public void RecordTokens(int count)
{
if (Interlocked.Add(ref _tokens, count) > _cfg.MaxTokens)
throw new LoopLimitExceededException($"Max tokens ({_cfg.MaxTokens}) exceeded.");
}
}
public sealed class LoopLimitExceededException(string message) : Exception(message);
```
### 1.6 OpenRouter Client
**Datei: `Core/Api/OpenRouterClient.cs`**
Implementiere einen schlanken HTTP-Client gegen `https://openrouter.ai/api/v1/chat/completions`.
Format ist OpenAI-kompatibel (JSON).
```csharp
namespace ClawdDotNet.Core.Api;
public sealed class OpenRouterClient : IDisposable
{
// Basis-URL: https://openrouter.ai/api/v1/
// Header: Authorization: Bearer {ApiKey}
// Header: HTTP-Referer: ClawdDotNet
// Format: OpenAI Chat Completions JSON
// Methoden:
// Task<ChatResponse> CompleteAsync(ChatRequest request, CancellationToken ct)
// IAsyncEnumerable<ChatChunk> StreamAsync(ChatRequest request, CancellationToken ct) [optional]
// ChatRequest enthält: model, messages[], tools[] (optional), tool_choice
// ChatResponse enthält: choices[0].message (content + tool_calls), usage (prompt_tokens, completion_tokens)
}
```
Nutze `System.Net.Http.HttpClient` mit `IHttpClientFactory`-Muster.
Keine externen HTTP-Bibliotheken. Serialisierung mit `System.Text.Json`.
### 1.7 Agent Engine
**Datei: `Core/Engine/AgentEngine.cs`**
Der Kern des Agentenablaufs. Pro Agent-Run wird eine neue Instanz erzeugt.
```
Ablauf eines Agent-Runs:
1. AgentConfig laden → erlaubte Tools aus Registry holen → Tool-Beschreibungen für LLM bauen
2. System-Prompt + User-Message an OpenRouter senden (mit Tool-Definitionen)
3. Antwort prüfen:
a. Enthält tool_calls → PermissionGate.Enforce → Tool.ExecuteAsync → Ergebnis zurück ans LLM
b. Kein tool_call → Antwort ist final → Run beendet
4. Nach jedem Schritt: LoopGuard.RecordStep() + LoopGuard.RecordTokens(usage)
5. Bei Exception: sauber abbrechen, Status = Failed, Exception loggen
```
Wichtig:
- Jeder Run bekommt seinen eigenen `CancellationToken` (kombiniert aus Timeout + externer Abbruch)
- Kein `await` ohne CancellationToken
- Rückgabe: `AgentRunResult` mit Status, FinalMessage, StepCount, TokensUsed, Duration
### 1.8 Scheduler
**Datei: `Core/Scheduling/AgentScheduler.cs`**
- Nutze `System.Threading.PeriodicTimer` oder Cron-Parsing (einfaches eigenes Parsing oder NCrontab NuGet)
- Pro AgentConfig mit `Scheduler != null` wird ein eigener Timer gestartet
- Scheduled Runs werden als `Task` gestartet (fire-and-forget mit Exception-Handling)
- `RunOnStart = true` → erster Run sofort beim Registrieren
- Scheduler ist instanzweit (ein Scheduler pro Prozess, verwaltet alle Agenten)
---
## Phase 2: Tools implementieren
### Tool: Database (`ClawdDotNet.Tools.Database`)
AgentConfig-Beispiel:
```json
"Database": {
"connectionString": "Server=localhost;Database=markets;User=agent_a;Password=...;",
"allowedTables": ["quotes", "indicators", "news"]
}
```
Implementiere `IAgentTool` mit diesen Operationen (via `action`-Feld im Input):
- `query` SELECT, nur auf `allowedTables`, SQL-Injection-Schutz via Parameterized Queries
- `insert` INSERT, nur auf `allowedTables`
- `upsert` INSERT ... ON DUPLICATE KEY UPDATE
Verbindungsstring kommt IMMER aus `context.ToolConfig`, nie aus statischen Feldern.
NuGet: `MySqlConnector` (für MySQL) und/oder `MongoDB.Driver` (für MongoDB), je nach Config-Eintrag `"type": "mysql"` oder `"type": "mongodb"`.
### Tool: FileRW (`ClawdDotNet.Tools.FileRW`)
AgentConfig-Beispiel:
```json
"FileRW": {
"rootPath": "./data/analyst/",
"allowWrite": true,
"allowedExtensions": [".txt", ".json", ".html", ".md"]
}
```
Operationen: `read`, `write`, `append`, `list`, `delete`
**Sicherheit (Pflicht):**
- Alle Pfade werden mit `Path.GetFullPath` aufgelöst
- Prüfe: `resolvedPath.StartsWith(Path.GetFullPath(rootPath))` — sonst `PathTraversalException`
- Nur Dateien mit erlaubter Extension (aus `allowedExtensions`) dürfen gelesen/geschrieben werden
### Tool: Mail (`ClawdDotNet.Tools.Mail`)
AgentConfig-Beispiel:
```json
"Mail": {
"imapHost": "imap.example.com",
"imapPort": 993,
"smtpHost": "smtp.example.com",
"smtpPort": 587,
"username": "agent@example.com",
"password": "...",
"allowedRecipients": ["owner@example.com"]
}
```
Operationen: `send`, `read_inbox`, `read_message`, `mark_read`
NuGet: `MailKit`
Empfänger müssen in `allowedRecipients` stehen, sonst Exception.
---
## Phase 3: Host (WinForms)
**Datei: `Host/Program.cs`**
Startparameter: `--config <pfad>` (optional, default: `./config.json`)
```csharp
// Startup-Ablauf:
// 1. InstanceConfig aus JSON laden (Pfad aus --config Argument)
// 2. ToolRegistry befüllen (Database, FileRW, Mail registrieren)
// 3. PermissionGate, AgentScheduler, OpenRouterClient instanziieren
// 4. AgentScheduler starten (alle Agenten aus InstanceConfig)
// 5. WinForms Application.Run(new MainForm(...))
```
**MainForm:** Zeigt pro Agent eine Statuszeile (AgentId, letzter Run, Status, Token-Verbrauch).
Manueller "Run now"-Button pro Agent. Log-Output in einer ListBox oder RichTextBox.
---
## Konfigurationsbeispiel: `stock-team.json`
```json
{
"instanceId": "stock-01",
"instanceName": "Aktien-Team",
"openRouterApiKey": "sk-or-...",
"workingDirectory": "./data/stock/",
"webServerPort": 8081,
"agents": [
{
"agentId": "market-analyst",
"displayName": "Marktanalyse",
"model": "anthropic/claude-sonnet-4-5",
"systemPrompt": "Du bist ein erfahrener Marktanalyst...",
"tools": {
"Database": {
"connectionString": "Server=db1;Database=stocks;...",
"allowedTables": ["quotes", "indicators"]
},
"FileRW": {
"rootPath": "./data/stock/analyst/",
"allowWrite": true,
"allowedExtensions": [".json", ".txt"]
}
},
"scheduler": { "cron": "0 7 * * 1-5", "runOnStart": false },
"loopGuard": { "maxSteps": 25, "maxTokens": 100000 }
},
{
"agentId": "webdev",
"displayName": "Web-Entwickler",
"model": "google/gemini-flash-1.5",
"systemPrompt": "Du erstellst HTML-Dashboards...",
"tools": {
"FileRW": {
"rootPath": "./data/stock/wwwroot/",
"allowWrite": true,
"allowedExtensions": [".html", ".css", ".js", ".json"]
}
},
"loopGuard": { "maxSteps": 10, "maxTokens": 40000 }
}
]
}
```
---
## Entwicklungsregeln (für Claude Code)
- **Keine externen Frameworks** außer: `MailKit`, `MySqlConnector`, `MongoDB.Driver`, optional `NCrontab`
- **Keine statischen Zustände** in Tools oder Engine-Komponenten
- **Jeder await-Aufruf** bekommt einen CancellationToken
- **Exceptions** in Agent-Runs niemals schlucken — loggen und als `AgentRunResult` mit Status=Failed zurückgeben
- **Alle Pfadoperationen** in FileRW mit Path-Traversal-Check
- **Connection Strings** kommen immer aus `AgentToolContext.ToolConfig`, nie hardcoded
- **Tests**: Für Core-Komponenten (PermissionGate, LoopGuard, FileRW-Pfadprüfung) xUnit-Unit-Tests anlegen
- **Logging**: `Microsoft.Extensions.Logging.ILogger` überall, kein Console.WriteLine in Produktionscode
---
## Startreihenfolge für Claude Code
1. Solution-Struktur und .csproj-Dateien anlegen (Projekt-Verweise korrekt setzen)
2. Core vollständig implementieren (Interfaces, Config, Registry, Gate, Guard, Client, Engine, Scheduler)
3. Tool: FileRW (einfachstes Tool, gut testbar)
4. Tool: Database
5. Tool: Mail
6. Host: Program.cs Startup, MainForm UI
7. xUnit Tests für Core + FileRW
8. Beispiel-Configs erstellen
**Beginne mit Schritt 1.**
+27
View File
@@ -0,0 +1,27 @@
# Archiv
Die vier Dokumente hier sind die **Entwicklungs-Prompts aus der Anfangszeit** des
Projekts (MaiJuli 2026). Sie haben ClawdDotNet aufgebaut und beschreiben deshalb
den Stand von damals — unter anderem eine WinForms-Oberfläche mit WebView2, die es
seit dem Frühjahrsputz vom 2026-08-23 nicht mehr gibt.
**Sie werden nicht mehr fortgeschrieben.** Wer den heutigen Stand sucht, findet ihn
in [Roadmap](../Roadmap.md), [Bestandsaufnahme](../Bestandsaufnahme-2026-07.md) und
den Konzept-Dokumenten daneben.
Aufgehoben werden sie aus zwei Gründen:
1. **Herkunft.** Sie erklären, warum das System so geschnitten ist, wie es
geschnitten ist — der Schichtschnitt Core/Tools/App stammt von hier.
2. **Eine Regel gilt weiter.** `ClawdDotNet_Prompt_InternetTools.md` enthält die
Pflichtfelder `fetchedAt` / `dataAsOf` / `source`, an die sich alle Internet-Tools
halten. Der [WebSearch-Umsetzungsplan](../umsetzungsplaene/UMSETZUNGSPLAN-WebSearch-Tool.md)
verweist darauf. Zieht diese Regel eines Tages in ein eigenes Dokument um, kann
die Datei ganz weg.
| Datei | Was darin steht | Was davon noch gilt |
|---|---|---|
| `ClawdDotNet_StartPrompt.md` | Gesamtentwurf, Kernklassen, Beispielkonfiguration | Schichtschnitt und Tool-Vertrag; Oberfläche und `configs/*.json` überholt |
| `ClawdDotNet_Prompt_WebviewChatWinForms.md` | WinForms-Oberfläche mit WebView2-Chat | nichts — ersetzt durch `src/ClawdDotNet.Desktop` |
| `ClawdDotNet_Prompt_TelegramClient.md` | Entwurf des TelegramClient-Tools | umgesetzt in `src/ClawdDotNet.Tools.TelegramClient` |
| `ClawdDotNet_Prompt_InternetTools.md` | WebFetch, DirectAPI, WebMonitor | die Pflichtfelder-Regel (siehe oben) |