Import des bestehenden Projektstands in Git. - .NET 10 WinForms Anwendung (Multi-Agent / Tool-System) - .gitignore fuer Build-Artefakte, Secrets und Runtime-Daten ergaenzt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
25 KiB
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:
{
"fetchedAt": "2026-05-13T07:42:00Z",
"dataAsOf": "2026-05-13T07:40:00Z",
"source": "https://...",
"data": { ... }
}
fetchedAt= Zeitpunkt des HTTP-Requests (UTC, immerDateTime.UtcNow)dataAsOf= Zeitpunkt der Daten laut Quelle (aus Response-Header, HTML oder API-Feld) Falls nicht ermittelbar:null— NIEMALS schätzen oder weglassensource= exakte URL die abgerufen wurde
Der System-Prompt jedes Agenten der Internet-Tools nutzt MUSS enthalten:
"Verwende niemals Daten ohne
fetchedAt-Feld. WenndataAsOfnull 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
"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
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
"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
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
"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
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: "1K–15K" | "15K–50K" | 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:
// 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:
{
"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:
- Scheduler feuert alle 30 Minuten
- Agent ruft
WebMonitor.check(capitol_trades)auf - Tool liefert neue Trades als Delta
- Agent analysiert: Cluster? Insider-Timing? Sektor-Häufung?
- Bei relevanten Funden:
Mail.sendoderDatabase.insertzur Archivierung - Run beendet — kein manueller Eingriff nötig
Implementierungsreihenfolge (für Claude Code)
Bearbeite diesen Abschnitt nach Abschluss der Core- und WinForms-Phase:
IStateStoreInterface +SqliteStateStoreImplementierung in Core anlegenAgentToolContextumIStateStoreerweitern, alle bestehenden Tool-Calls anpassenDirectApiToolimplementieren:- Twelve Data quote + history
- Yahoo Finance quote (kein Key nötig, als Fallback)
- CoinGecko crypto
- Timestamp-Extraktion aus jeweiligem Response-Format
WebFetchToolimplementieren:- 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
WebMonitorToolimplementieren:ParseCapitolTradesals ersten Parser (Regex auf HTML-Tabelle)CheckForNewEntriesAsyncmit IStateStore-IntegrationGetHistoryundGetStatusAktionenParseSecForm4als zweiten Parser
- xUnit-Tests:
ParseCapitolTradesgegen 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
- Beispiel-Config
capitol-team.jsonanlegen - In
Program.cs: WebMonitorTool registrieren, SqliteStateStore als IStateStore in DI
Beginne mit Schritt 1 dieses Abschnitts.