Aufbau eines KI-gestützten RSS-Feed-Aggregators
Als Microsoft MVP und Technikbegeisterter ertrinke ich ständig im Meer erstaunlicher Inhalte, die in den DevBlogs von Microsoft veröffentlicht werden. Von .NET-Ankündigungen bis zu Visual Studio-Updates, von Azure-Innovationen bis hin zu ausführlichen Einblicken in den Semantic Kernel – im Microsoft-Ökosystem passiert immer etwas Neues und Aufregendes.
Das Problem? Es ist fast unmöglich, mit allem Schritt zu halten.
Ich wollte über die neuesten Ankündigungen auf dem Laufenden bleiben und sie mit meinem Netzwerk teilen, aber das manuelle Überprüfen von sieben verschiedenen RSS-Feeds, das Lesen von Artikeln, das Verfassen ansprechender Social-Media-Beiträge und das Verfolgen dessen, was ich bereits geteilt habe, wurde zu einem Vollzeitjob für sich. Jeden Morgen öffnete ich mehrere Browser-Tabs, überflog Dutzende von Artikeln, versuchte mich daran zu erinnern, welche ich bereits geteilt hatte, und verbrachte dann wertvolle Zeit damit, Beiträge über diejenigen zu schreiben, die meine Aufmerksamkeit erregten.
Also habe ich getan, was jeder Entwickler tun würde: Ich habe es automatisiert.
In diesem umfassenden Leitfaden erkläre ich Ihnen, wie ich einen KI-gestützten RSS-Feed-Aggregator erstellt habe, der mehrere RSS-Feeds von Microsoft DevBlogs auf neue Inhalte überwacht, Azure OpenAI und Semantic Kernel verwendet, um Artikel zu analysieren und ansprechende Beiträge zu generieren, eine detaillierte Markdown-Dokumentation für jeden analysierten Artikel erstellt, Benachrichtigungen per Telegram sendet, damit ich den Inhalt überprüfen und teilen kann, alles verfolgt, um doppelte Beiträge zu vermeiden, und der automatisch über GitHub-Aktionen ausgeführt wird.
Lassen Sie uns tief in jeden Aspekt dieser Lösung eintauchen.
Die Geschichte hinter diesem Projekt
Leben mit Informationsüberflutung
Lassen Sie mich Ihnen ein Bild von meinem typischen Morgen zeichnen, bevor ich dieses Tool gebaut habe. Ich wachte auf, holte mir meinen Kaffee und öffnete meinen Laptop, um nachzusehen, was es Neues im Microsoft-Entwickler-Ökosystem gibt. Zuerst würde ich zur Hauptseite von DevBlogs navigieren, um zu sehen, ob es dort wichtige Ankündigungen gibt. Dann würde ich mir speziell den .NET-Blog ansehen, da dies mein primärer Technologie-Stack ist. Danach würde ich zum Semantic Kernel-Blog vorbeischauen, da KI immer wichtiger wird. Der Visual Studio-Blog stand als nächstes auf der Liste, da IDE-Updates meinen täglichen Arbeitsablauf erheblich beeinträchtigen können. Dann folgte der DevOps-Blog für CI/CD- und GitHub-bezogene Neuigkeiten, gefolgt vom All Things Azure-Blog für Cloud-Infrastruktur-Updates und schließlich der Azure SQL-Blog für Datenbankinnovationen.
Das sind sieben verschiedene Feeds, die überprüft werden müssen. Jeder dieser Blogs veröffentlicht mehrere Artikel pro Woche, manchmal mehrere pro Tag während wichtiger Ankündigungszeiträume wie .NET Conf oder Build. Das sind potenziell Dutzende Artikel, die Sie verfolgen, lesen und teilen können. Und hier ist die Sache: Als jemand, der Wert darauf legt, Wissen mit der Community zu teilen, wollte ich diese Artikel nicht nur lesen. Ich wollte die wertvollsten mit meinem Netzwerk auf LinkedIn teilen und so auch anderen Entwicklern helfen, auf dem Laufenden zu bleiben.Aber die Erstellung eines guten LinkedIn-Beitrags braucht Zeit. Sie müssen den Artikel gründlich lesen, die wichtigsten Punkte verstehen, darüber nachdenken, warum er für Ihr Publikum wichtig ist, einen ansprechenden Hook schreiben und alles gut formatieren. Multiplizieren Sie das mit mehreren Artikeln pro Woche, und Sie haben Stunden an Arbeit vor sich.
Was ich wirklich wollte
Nachdem ich mich monatelang damit beschäftigt hatte, habe ich mich hingesetzt und darüber nachgedacht, wie eine ideale Lösung aussehen könnte. In erster Linie wollte ich nie wieder wichtige Ankündigungen verpassen. Das System sollte neue Artikel automatisch erkennen, sobald sie veröffentlicht werden. Außerdem wollte ich bei der Inhaltserstellung Zeit sparen, indem ich die KI bei der Erstellung ansprechender Beiträge unterstützen ließ – nicht um meine Stimme vollständig zu ersetzen, sondern um mir einen soliden Ausgangspunkt zu geben, den ich anpassen kann.
Konsistenz war ein weiterer wichtiger Faktor. Ich wollte Inhalte regelmäßig teilen, ohne jeden Tag daran denken zu müssen, dies manuell zu tun. Auch der Tracking-Aspekt war entscheidend – ich brauchte eine Möglichkeit, herauszufinden, was ich bereits geteilt habe, um zu vermeiden, dass Du Duplikate postest und meine Follower verärgerst. Schließlich wollte ich mit einer permanenten Aufzeichnung aller von mir verarbeiteten Daten den Überblick behalten, damit ich zurückblicken und sehen kann, welche Themen ich behandelt habe.
Die Lösung nimmt Gestalt an
Die Lösung, die ich mir vorgestellt hatte, würde nach einem Zeitplan mithilfe von GitHub-Aktionen laufen, völlig freihändig. Es würde alle sieben Feeds automatisch abrufen, ohne dass ich einen einzigen Browser-Tab öffnen müsste. Die KI-Komponente würde den Inhalt tatsächlich lesen und verstehen und ihn dann auf eine Weise zusammenfassen, die für mein Publikum nützlich ist. Anstatt Beiträge von Grund auf neu schreiben zu müssen, würde es fertige Social-Media-Inhalte zum Teilen erstellen, die ich bei Bedarf anpassen könnte. Alles wurde zur Überprüfung an mein Telegram gesendet, sodass ich schnell einen Blick auf mein Telefon werfen und entscheiden konnte, was ich teilen wollte. Und natürlich würde alles dauerhaft aufgezeichnet werden, um später darauf zurückgreifen zu können.
Bevor wir mit dem Bau beginnen
Was Sie auf Ihrer Maschine benötigen
Um diesem Tutorial folgen zu können, müssen einige Dinge auf Ihrem Entwicklungscomputer installiert sein. Das wichtigste ist die .NET SDK-Version 9.0 oder höher. Dies ist unsere Laufzeit und stellt alle Build-Tools bereit, die wir benötigen. Wenn Sie es nicht installiert haben, gehen Sie zu dot.net und laden Sie die neueste Version herunter. Die Installation ist unkompliziert unter Windows, macOS oder Linux.
Sie möchten außerdem, dass Git zur Versionskontrolle installiert wird. Wir werden unseren Code auf GitHub übertragen und GitHub-Aktionen zur Automatisierung verwenden, daher ist es wichtig, dass Git lokal eingerichtet ist. Jede neuere Version wird einwandfrei funktionieren.
Für Ihre Entwicklungsumgebung empfehle ich entweder Visual Studio oder VS Code. Persönlich verwende ich heutzutage für den Großteil meiner Arbeit VS-Code, weil er leichtgewichtig ist und über die C#-Dev-Kit-Erweiterung hervorragende C#-Unterstützung bietet. Aber wenn Sie mit dem vollständigen Visual Studio vertrauter sind, funktioniert das auch perfekt.
Dienste und Konten, die Sie benötigenÜber die lokalen Tools hinaus benötigen Sie Konten bei einigen Diensten. Das wichtigste ist Azure OpenAI, das unsere KI-Analyse unterstützt. Hierbei handelt es sich um einen Pay-as-you-go-Dienst, die Kosten sind für diesen Anwendungsfall jedoch minimal – wir sprechen von Cent pro analysiertem Artikel. Wenn Sie kein Azure-Konto haben, können Sie sich für eine kostenlose Testversion anmelden, die einige Credits für den Einstieg enthält.
Für Benachrichtigungen verwenden wir einen Telegram Bot. Das Tolle an Telegram ist, dass die Nutzung der Bot-API völlig kostenlos ist. Sie können so viele Bots erstellen, wie Sie möchten, und unbegrenzt Nachrichten senden. Ich werde Sie später in diesem Handbuch durch den Einrichtungsprozess führen.
Schließlich benötigen Sie ein GitHub-Konto zum Hosten Ihres Codes und zum Ausführen von GitHub-Aktionen. Das kostenlose Kontingent ist für dieses Projekt mehr als ausreichend. GitHub bietet Ihnen 2.000 Minuten Actions-Laufzeit pro Monat für private Repositorys und unbegrenzte Minuten für öffentliche Repositorys.
Die Bibliotheken, die dies ermöglichen
Unser Projekt basiert auf drei Haupt-NuGet-Paketen, die jeweils einem bestimmten Zweck dienen.
Das erste ist HtmlAgilityPack, der Goldstandard für die HTML-Analyse in .NET. Wenn wir einen Artikel aus einem Blog abrufen, erhalten wir den vollständigen HTML-Code der Seite zurück – einschließlich Navigationsmenüs, Fußzeilen, Anzeigen und allen möglichen Elementen, die uns nicht interessieren. Mit HtmlAgilityPack können wir diesen HTML-Code analysieren und genau den Artikelinhalt extrahieren, den wir benötigen.
Das zweite Paket ist Microsoft.SemanticKernel, das SDK von Microsoft zur Integration von KI-Modellen in Anwendungen. Betrachten Sie es als Brücke zwischen Ihrem .NET-Code und großen Sprachmodellen wie GPT-4. Es bewältigt die gesamte Komplexität von API-Aufrufen, Token-Management und Antwortanalyse, sodass Sie sich auf das konzentrieren können, was die KI tatsächlich tun soll.
Das dritte Paket ist System.ServiceModel.Syndication, das integrierte Unterstützung für das Parsen von RSS- und Atom-Feeds bietet. RSS mag wie eine veraltete Technologie erscheinen, ist aber immer noch die beste Möglichkeit, strukturierte Updates von Blogs und Nachrichtenseiten zu erhalten. Dieses Paket wandelt rohe XML-Feeds in stark typisierte C#-Objekte um, mit denen man einfach arbeiten kann.
Die Architektur verstehen
Wie die Teile zusammenpassen
Bevor wir uns mit dem Code befassen, möchte ich erklären, wie alle Komponenten zusammenarbeiten. Wenn Sie das Gesamtbild verstehen, werden die Implementierungsdetails viel klarer.
Auf der höchsten Ebene haben wir unsere Hauptdatei Program.cs, die als Orchestrator fungiert. Dies ist der Einstiegspunkt unserer Anwendung und koordiniert alle anderen Komponenten. Wenn die Anwendung ausgeführt wird, lädt sie zunächst die Konfiguration aus Umgebungsvariablen – Dinge wie API-Schlüssel und Telegram-Anmeldeinformationen. Dann geht es los und ruft RSS-Feeds von allen sieben Microsoft DevBlogs-Quellen ab. Bei der Verarbeitung dieser Feeds werden Artikel dedupliziert, um Fälle zu bearbeiten, in denen derselbe Artikel in mehreren Feeds erscheint. Es prüft jeden Artikel anhand unserer Tracking-Datei, um festzustellen, ob wir ihn bereits verarbeitet haben. Neue Artikel werden zur Verarbeitung an den KI-Analysator übergeben.In der ArticleAnalyzer-Klasse geschieht die KI-Magie. Diese Komponente empfängt einen Artikel und macht mehrere Dinge damit. Zunächst wird der vollständige HTML-Inhalt von der URL des Artikels abgerufen. Dann extrahiert es sauberen Text aus diesem HTML und entfernt alle Navigationselemente, Skripte und Stile, die wir nicht benötigen. Sobald sauberer Text vorliegt, wird dieser über den Semantic Kernel mit einer sorgfältig ausgearbeiteten Eingabeaufforderung an Azure OpenAI gesendet. Die KI analysiert den Artikel und gibt eine strukturierte Antwort zurück, die eine Zusammenfassung, Schlüsselthemen, eine Erklärung zur Relevanz und vor allem einen gebrauchsfertigen LinkedIn-Beitrag enthält. Der Analysator analysiert diese Antwort und gibt ein ArticleAnalysis-Objekt zurück, das alle diese Informationen enthält.
Die MarkdownGenerator-Klasse nimmt dieses ArticleAnalysis-Objekt und erstellt eine permanente Aufzeichnung davon. Es generiert eine schön formatierte Markdown-Datei, die alle Artikelmetadaten, die KI-Analyse und den generierten Beitrag enthält. Diese Dateien werden in einem Verzeichnis „generated-posts“ gespeichert und bieten Ihnen ein durchsuchbares Archiv aller von Ihnen verarbeiteten Daten.
Schließlich sendet die Telegram-Integration den generierten Beitragsinhalt an Ihr Telefon. Dies ist der Punkt, an dem Sie als Mensch die Arbeit der KI überprüfen und entscheiden können, ob Sie sie teilen möchten. Der Bot sendet Ihnen eine Nachricht mit dem Inhalt des Beitrags, den Sie entweder direkt auf LinkedIn kopieren oder zunächst ändern können.
Der Datenfluss
Lassen Sie mich erklären, was passiert, wenn ein neuer Artikel im .NET-Blog veröffentlicht wird. Der Workflow beginnt, wenn GitHub Actions unsere Anwendung nach ihrem Zeitplan auslöst – sagen wir alle sechs Stunden. Die Anwendung wird aktiviert und beginnt mit dem Abrufen aller sieben RSS-Feeds. Jeder Feed gibt ein XML-Dokument zurück, das die neuesten Artikel aus diesem Blog enthält.
Während wir jeden Feed analysieren, extrahieren wir einzelne Artikel und speichern sie in einer Liste. Aber hier ist ein kniffliger Teil: Der Haupt-DevBlogs-Feed enthält oft Artikel, die auch in den einzelnen Kategorie-Feeds erscheinen. Daher könnte ein Artikel über „.NET 10“ sowohl im Haupt-Feed als auch im .NET-spezifischen Feed erscheinen. Wir handhaben dies, indem wir URLs in einem HashSet verfolgen, was automatisch Duplikate verhindert.
Sobald wir unsere deduplizierte Artikelliste haben, filtern wir sie auf die neuesten Artikel herunter – typischerweise Artikel, die am letzten Tag oder so veröffentlicht wurden. Wir möchten keine alten Artikel verarbeiten, die wir bereits in früheren Durchläufen bearbeitet haben. Dann vergleichen wir jeden aktuellen Artikel mit unserer Tracking-Datei. Wenn wir einen Artikel bereits bearbeitet und veröffentlicht haben, überspringen wir ihn.
Für jeden neuen Artikel starten wir die KI-Analysepipeline. Der Analysator ruft den vollständigen Artikel-HTML-Code ab, bereinigt ihn und sendet ihn mit unserer Eingabeaufforderung an GPT-4. Die KI liest den Artikel und generiert eine umfassende Analyse zusammen mit einem LinkedIn-Beitrag. Wir speichern diese Analyse für unsere Unterlagen in einer Markdown-Datei.Nach Abschluss der Analyse formatieren wir eine Nachricht und senden sie per Telegram. Die Nachricht enthält den generierten Beitragsinhalt mit angehängter URL und Hashtags. Auf meinem Telefon erhalte ich eine Benachrichtigung, überprüfe den Beitrag und wenn er mir gefällt, kann ich ihn mit nur wenigen Fingertipps kopieren und auf LinkedIn teilen.
Abschließend aktualisieren wir unsere Tracking-Datei, um diesen Artikel als verarbeitet zu markieren, sodass wir ihn in zukünftigen Läufen nicht erneut bearbeiten. Wenn Dateien erstellt oder geändert wurden, schreibt GitHub Actions diese Änderungen zurück in das Repository und hält alles synchron.
Einrichten des Projekts von Grund auf
Erstellen der Lösungsstruktur
Beginnen wir mit dem Bau. Öffnen Sie Ihr Terminal und navigieren Sie zu der Stelle, an der Sie das Projekt erstellen möchten. Ich organisiere meine Projekte gerne in einem Entwicklungsordner, aber Sie können ihn auch dort ablegen, wo es für Sie sinnvoll ist.
Zuerst erstellen wir eine neue Lösungsdatei. In .NET ist eine Lösung ein Container, der mehrere Projekte enthalten kann. Auch wenn wir derzeit nur ein Projekt haben, ist es einfacher, später bei Bedarf weitere Projekte hinzuzufügen, wenn wir mit einer Lösung beginnen. Führen Sie den Befehl dotnet new sln -n vs-feed-linkedin aus, um eine Lösung mit dem Namen vs-feed-linkedin zu erstellen.
Als nächstes müssen wir unser Konsolenanwendungsprojekt erstellen. Wir legen dies in einem src-Unterverzeichnis ab, um die Organisation zu gewährleisten. Führen Sie dotnet new console -n VsFeedLinkedin -o src aus, um ein Konsolenprojekt mit dem Namen VsFeedLinkedin im Ordner src zu erstellen. Fügen Sie dann dieses Projekt mit dotnet sln add src/VsFeedLinkedin.csproj zu unserer Lösung hinzu.
Navigieren Sie nun mit cd src in das src-Verzeichnis. Hier fügen wir unsere NuGet-Pakete hinzu und erledigen den Großteil unserer Entwicklung.
Hinzufügen der erforderlichen Pakete
Nachdem unser Projekt erstellt wurde, müssen wir die drei zuvor erwähnten NuGet-Pakete hinzufügen. Führen Sie jeden dieser Befehle nacheinander aus:
dotnet add package System.ServiceModel.Syndication --version 9.0.9
dotnet add package Microsoft.SemanticKernel --version 1.30.0
dotnet add package HtmlAgilityPack --version 1.11.72
Nachdem Sie diese Befehle ausgeführt haben, sollte Ihre Projektdatei etwa so aussehen:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="HtmlAgilityPack" Version="1.11.72" />
<PackageReference Include="Microsoft.SemanticKernel" Version="1.30.0" />
<PackageReference Include="System.ServiceModel.Syndication" Version="9.0.9" />
</ItemGroup>
</Project>
Die Projektdatei teilt .NET mit, dass wir eine ausführbare Datei (OutputType ist Exe) erstellen, die auf .NET 9.0 abzielt und moderne C#-Funktionen wie implizite Verwendungen und nullfähige Referenztypen verwendet. Der Abschnitt „ItemGroup“ listet unsere drei Paketabhängigkeiten mit ihren genauen Versionen auf.
Tauchen Sie tief in RSS-Feeds ein
Was genau ist RSS?
Bevor wir mit dem Schreiben von Code zum Abrufen von Feeds beginnen, stellen wir sicher, dass wir verstehen, womit wir arbeiten. RSS steht für Really Simple Syndication und ist ein standardisiertes XML-Format zur Verteilung von Inhaltsaktualisierungen. Die Idee ist einfach: Anstatt zu verlangen, dass Benutzer Ihre Website besuchen, um zu sehen, ob es neue Inhalte gibt, veröffentlichen Sie eine maschinenlesbare Datei, die Ihre neuesten Inhalte auflistet. Anwendungen können diese Datei dann regelmäßig abfragen, um neue Artikel zu entdecken.
RSS gibt es seit Ende der 1990er und Anfang der 2000er Jahre. Man könnte meinen, es handele sich um eine veraltete Technologie, aber tatsächlich wird sie immer noch häufig verwendet – insbesondere von Blogs, Nachrichtenseiten und Podcasts. Das Schöne an RSS ist seine Einfachheit. Es handelt sich lediglich um XML mit einer definierten Struktur, und jede Anwendung kann es analysieren.
Die Struktur eines DevBlogs-FeedsWenn Sie einen RSS-Feed von Microsoft DevBlogs abrufen, erhalten Sie ein XML-Dokument zurück, das einer bestimmten Struktur folgt. Auf der obersten Ebene gibt es ein RSS-Element, das ein einzelnes Kanalelement enthält. Der Kanal stellt den Blog selbst dar und enthält Metadaten wie den Titel, die URL und die Beschreibung des Blogs.
Innerhalb des Kanals finden Sie mehrere Elementelemente, die jeweils einen einzelnen Blogbeitrag darstellen. Jedes Element enthält einen Titel (die Überschrift des Artikels), einen Link (die URL, unter der Sie den vollständigen Artikel lesen können), ein pubDate (Veröffentlichungsdatum des Artikels), ein dc:creator-Element (den Namen des Autors), ein oder mehrere Kategorieelemente (Tags für den Artikel) und eine Beschreibung (normalerweise eine Zusammenfassung oder einen Auszug des Artikels).
Hier ist ein vereinfachtes Beispiel, wie das aussieht:
<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0">
<channel>
<title>.NET Blog</title>
<link>https://devblogs.microsoft.com/dotnet</link>
<description>The latest news about .NET</description>
<item>
<title>Announcing .NET 10</title>
<link>https://devblogs.microsoft.com/dotnet/announcing-dotnet-10</link>
<pubDate>Mon, 10 Dec 2025 12:00:00 GMT</pubDate>
<dc:creator>Microsoft</dc:creator>
<category>Announcements</category>
<category>.NET</category>
<description>Article summary...</description>
</item>
</channel>
</rss>
Das Tolle am System.ServiceModel.Syndication-Paket von .NET ist, dass es all dies für uns analysiert. Wir müssen nicht manuell durch XML-Knoten navigieren oder uns um unterschiedliche RSS-Versionen kümmern. Wir laden einfach den Feed und erhalten stark typisierte Objekte zurück.
Die sieben Feeds, die wir überwachen
In meiner Implementierung überwache ich sieben verschiedene Microsoft DevBlogs-Feeds. Der Haupt-DevBlogs-Feed unter devblogs.microsoft.com/feed bietet uns einen umfassenden Überblick über alles, was Microsoft in all seinen Entwicklerblogs veröffentlicht. Der .NET-spezifische Feed unter devblogs.microsoft.com/dotnet/feed konzentriert sich speziell auf .NET-Versionen, -Funktionen und Best Practices. Der Semantic-Kernel-Feed unter devblogs.microsoft.com/semantic-kernel/feed befasst sich mit der Orchestrierung und Integration von KI – was immer wichtiger wird, da KI für die moderne Entwicklung von zentraler Bedeutung ist.
Der Visual Studio-Feed unter devblogs.microsoft.com/visualstudio/feed hält mich über IDE-Verbesserungen und Produktivitätsfunktionen auf dem Laufenden. Der DevOps-Feed unter devblogs.microsoft.com/devops/feed behandelt Azure DevOps-, GitHub- und CI/CD-Themen. Der All Things Azure-Feed unter devblogs.microsoft.com/all-things-azure/feed konzentriert sich auf Cloud-Dienste und Architekturmuster. Schließlich behandelt der Azure SQL-Feed unter devblogs.microsoft.com/azure-sql/feed Datenbankinnovationen und -funktionen.
Sie fragen sich vielleicht, warum ich sowohl den Haupt-Feed als auch die einzelnen Kategorie-Feeds überprüfe. Der Haupt-Feed gibt mir Abwechslung – ich sehe Artikel aus jedem Microsoft-Entwicklerblog, auch solche, von denen ich vielleicht nichts weiß. Die Kategorie-Feeds geben mir Tiefe – sie stellen sicher, dass ich nichts Wichtiges in meinen Kerninteressengebieten verpasse, selbst wenn diese Artikel durch neuere Inhalte aus dem Haupt-Feed verdrängt werden.
Aufbau der RSS-Abruflogik
Die Kernabruffunktion
Jetzt schreiben wir etwas Code. Die Grundlage unserer Anwendung ist die Möglichkeit, RSS-Feeds abzurufen und zu analysieren. Hier ist die Funktion, die das erledigt:
static async Task<SyndicationFeed?> FetchRssFeedAsync(string url)
{
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("User-Agent", "VsFeedLinkedin/1.0");
var response = await httpClient.GetStringAsync(url);
using var stringReader = new StringReader(response);
var settings = new XmlReaderSettings
{
DtdProcessing = DtdProcessing.Parse,
MaxCharactersFromEntities = 1024
};
using var xmlReader = XmlReader.Create(stringReader, settings);
return SyndicationFeed.Load(xmlReader);
}
Lassen Sie mich durchgehen, was dieser Code bewirkt. Wir beginnen mit der Erstellung eines HttpClient, der in .NET integrierten Klasse zum Senden von HTTP-Anfragen. Wir setzen einen User-Agent-Header, da einige Server Anfragen blockieren, die sich nicht identifizieren. Es empfiehlt sich, dies auch dann festzulegen, wenn Server dies nicht benötigen.Anschließend stellen wir eine GET-Anfrage an die Feed-URL und erhalten die Antwort als String. Diese Zeichenfolge enthält das Roh-XML des RSS-Feeds.
Um dieses XML zu analysieren, erstellen wir einen StringReader, um unsere Antwortzeichenfolge zu umschließen, und konfigurieren dann einige XmlReaderSettings. Die Einstellung „DtdProcessing“ ist wichtig – RSS-Feeds enthalten manchmal DTD-Deklarationen (Document Type Definition), die verarbeitet werden müssen. Die Einstellung „MaxCharactersFromEntities“ ist eine Sicherheitsmaßnahme, die XML-Bombenangriffe verhindert, indem sie begrenzt, wie viel Entitätserweiterung stattfinden kann.
Schließlich erstellen wir einen XmlReader mit diesen Einstellungen und verwenden SyndicationFeed.Load, um das XML in ein stark typisiertes SyndicationFeed-Objekt zu analysieren. Dies gibt uns Zugriff auf die Metadaten des Feeds und alle seine Elemente über nette C#-Eigenschaften anstelle der reinen XML-Navigation.
Mehrere Feeds mit Fehlerbehandlung abrufen
In der realen Welt schlagen Netzwerkanfragen fehl. Server fallen aus, es kommt zu Zeitüberschreitungen bei Verbindungen und XML kann fehlerhaft sein. Wir müssen diese Fälle mit Würde behandeln. So rufen wir alle unsere Feeds ab und bleiben gleichzeitig ausfallsicher:
var allArticles = new List<(SyndicationItem item, string feedUrl)>();
var seenUrls = new HashSet<string>();
foreach (var feedUrl in feedUrls)
{
try
{
Console.WriteLine($" 📡 Fetching {feedUrl}...");
var feed = await FetchRssFeedAsync(feedUrl);
if (feed?.Items != null && feed.Items.Any())
{
foreach (var item in feed.Items)
{
var itemUrl = item.Links.FirstOrDefault()?.Uri.ToString() ?? "";
if (!string.IsNullOrEmpty(itemUrl) && seenUrls.Add(itemUrl))
{
allArticles.Add((item, feedUrl));
}
}
}
}
catch (Exception ex)
{
Console.WriteLine($" ⚠️ Failed to fetch {feedUrl}: {ex.Message}");
}
}
Wir unterhalten hier zwei Sammlungen. Die allArticles-Liste enthält alle von uns gefundenen Artikel sowie den Feed, aus dem sie stammen. Das seenUrls HashSet verfolgt, welche Artikel-URLs wir bereits gesehen haben, und hilft uns, Duplikate zu vermeiden.
Wir durchlaufen jede Feed-URL und schließen den Abrufvorgang in einen Try-Catch-Block ein. Wenn das Abrufen eines bestimmten Feeds fehlschlägt – möglicherweise ist der Server vorübergehend ausgefallen – protokollieren wir eine Warnung und fahren mit dem nächsten Feed fort. Auf diese Weise hindert uns ein Problem mit einem Feed nicht daran, die anderen zu verarbeiten.
Für jeden erfolgreich abgerufenen Feed durchlaufen wir seine Elemente. Wir extrahieren die Artikel-URL aus der Link-Sammlung des Artikels. Die HashSet.Add-Methode gibt „false“ zurück, wenn die URL bereits im Satz vorhanden ist, was perfekt für unsere Deduplizierungslogik ist. Wir nehmen den Artikel nur dann in unsere Liste auf, wenn er neu ist.
Wir speichern die Feed-URL zusammen mit jedem Artikel, da diese Informationen später nützlich sein könnten – beispielsweise möchten wir zu Debug- oder Protokollierungszwecken wissen, aus welchem spezifischen Feed ein Artikel stammt.
Umgang mit Duplikaten und Verfolgungsstatus
Die Herausforderung der Deduplizierung
Wie ich bereits erwähnt habe, verfügt Microsoft DevBlogs über eine hierarchische Feed-Struktur, die eine interessante Herausforderung darstellt. Wenn ein .NET-Teammitglied einen Artikel beispielsweise über Leistungsverbesserungen in .NET 10 veröffentlicht, wird dieser Artikel wahrscheinlich sowohl im Haupt-DevBlogs-Feed als auch im .NET-spezifischen Feed erscheinen. Manchmal erscheint es sogar im Visual Studio-Feed, wenn es sich auf IDE-Funktionen bezieht.
Wenn wir naiv jeden Artikel aus jedem Feed verarbeiten würden, würden wir am Ende denselben Artikel mehrmals analysieren und veröffentlichen. Das würde API-Aufrufe an Azure OpenAI verschwenden, unser Telegram mit doppelten Benachrichtigungen spammen und möglicherweise unsere Follower verärgern, wenn wir Duplikate posten.Die Lösung ist die URL-basierte Deduplizierung. Jeder Artikel hat eine eindeutige URL, sodass wir diese als Kennung verwenden können. Die HashSet-Datenstruktur ist dafür perfekt, da sie O(1)-Suchzeit bietet und Duplikate automatisch verhindert. Wenn wir versuchen, eine URL hinzuzufügen, die bereits im Satz enthalten ist, gibt die Add-Methode einfach „false“ zurück und weist uns so darauf hin, dass wir diesen Artikel überspringen sollten.
Persistenter Zustand mit Markdown
Die Deduplizierung verarbeitet Duplikate innerhalb eines einzigen Durchlaufs, aber wie sieht es mit mehreren Durchläufen aus? Wenn unsere Anwendung alle sechs Stunden ausgeführt wird, müssen wir uns merken, welche Artikel wir bereits verarbeitet haben, damit wir sie nicht erneut bearbeiten.
Ich habe mich entschieden, diesen Status in einer Markdown-Datei namens „posted-articles.md“ zu speichern. Warum Abschlag? Ein paar Gründe. Erstens ist es für Menschen lesbar. Ich kann die Datei öffnen und sofort sehen, welche Artikel ich geteilt habe. Zweitens ist es versioniert. Da sich diese Datei in unserem Git-Repository befindet, verfüge ich über einen vollständigen Verlauf der Artikelverarbeitung. Drittens dient es der Dokumentation. Jeder, der sich das Repository ansieht, kann sehen, was die Anwendung getan hat.
Das Format dieser Datei ist einfach. Es enthält einen Header, einen Zeitstempel, der angibt, wann die Anwendung zuletzt ausgeführt wurde, und dann eine Liste von Artikeln im Markdown-Link-Format:
# Posted Articles
*Last run: 2025-12-10 15:30:00*
List of articles posted to LinkedIn:
- [Announcing .NET 10](https://devblogs.microsoft.com/dotnet/announcing-dotnet-10?wt.mc_id=DT-MVP-5004972) - Posted on 2025-12-10 15:30:00 (Published: 2025-12-10)
- [Visual Studio 2026 Preview](https://devblogs.microsoft.com/visualstudio/vs-2026-preview?wt.mc_id=DT-MVP-5004972) - Posted on 2025-12-09 10:15:00 (Published: 2025-12-09)
Laden und Analysieren der Tracking-Datei
Um zu überprüfen, ob wir einen Artikel bereits verarbeitet haben, müssen wir diese Datei laden und die URLs extrahieren. Hier ist die Funktion, die dies tut:
static HashSet<string> LoadPostedArticles(string filePath)
{
var postedUrls = new HashSet<string>();
if (!File.Exists(filePath))
{
return postedUrls;
}
var lines = File.ReadAllLines(filePath);
foreach (var line in lines)
{
var match = System.Text.RegularExpressions.Regex.Match(line, @"\(([^)]+)\)");
if (match.Success)
{
var url = match.Groups[1].Value;
if (url.Contains("?wt.mc_id="))
{
url = url.Substring(0, url.IndexOf("?wt.mc_id="));
}
else if (url.Contains("?"))
{
url = url.Substring(0, url.IndexOf("?"));
}
url = url.TrimEnd('/');
postedUrls.Add(url);
}
}
return postedUrls;
}
Diese Funktion gibt ein HashSet zurück, das alle URLs enthält, die wir bereits verarbeitet haben. Wir prüfen zunächst, ob die Datei vorhanden ist. Beim ersten Start ist dies nicht der Fall, sodass wir einen leeren Satz zurückgeben.
Für jede Zeile in der Datei verwenden wir einen regulären Ausdruck, um die URL aus dem Markdown-Link-Format zu extrahieren. Der reguläre Ausdruck \(([^)]+)\) stimmt mit allem überein, was in Klammern steht. Dort speichern Markdown-Links ihre URLs.
Dann kommt ein wichtiger Schritt: die URL-Normalisierung. URLs für denselben Artikel können im Format variieren. Der RSS-Feed gibt uns möglicherweise https://devblogs.microsoft.com/dotnet/article, aber unserer gespeicherten Version ist ein Tracking-Parameter angehängt: https://devblogs.microsoft.com/dotnet/article?wt.mc_id=DT-MVP-5004972. Einige URLs haben abschließende Schrägstriche, andere nicht.
Um dies zu bewältigen, entfernen wir alle Abfrageparameter (alles nach dem ?) und entfernen abschließende Schrägstriche. Durch diese Normalisierung wird sichergestellt, dass wir Artikel auch dann als Duplikate erkennen, wenn sich ihre URLs auf diese oberflächliche Weise unterscheiden.
Neue Artikel speichern
Wenn wir einen Artikel erfolgreich verarbeiten, müssen wir ihn zu unserer Tracking-Datei hinzufügen:
static void SavePostedArticle(string filePath, string url, string title, DateTimeOffset publishDate)
{
var markdownEntry = $"- [{title}]({url}) - Posted on {DateTime.Now:yyyy-MM-dd HH:mm:ss} (Published: {publishDate:yyyy-MM-dd})\n";
if (!File.Exists(filePath))
{
File.WriteAllText(filePath, "# Posted Articles\n\n*Last run: {DateTime.Now:yyyy-MM-dd HH:mm:ss}*\n\nList of articles posted:\n\n");
}
File.AppendAllText(filePath, markdownEntry);
}
Diese Funktion erstellt einen Eintrag im Markdown-Format mit dem Titel des Artikels als Link, gefolgt von Zeitstempeln, die angeben, wann wir ihn gepostet haben und wann er ursprünglich veröffentlicht wurde. Wenn die Datei noch nicht existiert, erstellen wir sie zunächst mit einem Header.
Die KI-Analyse-Engine
Den semantischen Kernel verstehenNun kommen wir zum spannendsten Teil unserer Anwendung – der KI-Analyse. Semantic Kernel ist das Open-Source-SDK von Microsoft zur Integration großer Sprachmodelle in Anwendungen. Es ist mehr als nur ein Wrapper für API-Aufrufe. Es bietet einen Rahmen für die Erstellung anspruchsvoller KI-Anwendungen mit Funktionen wie Plugins, Planern und Speicher.
Für unseren Anwendungsfall verwenden wir die Chat-Vervollständigungsfunktionen von Semantic Kernel. Wir senden eine Eingabeaufforderung an Azure OpenAI und das Modell analysiert unseren Artikel und generiert eine Antwort. Der semantische Kernel übernimmt die gesamte Komplexität der API-Authentifizierung, Anforderungsformatierung und Antwortanalyse.
Einrichten des Artikelanalysators
Schauen wir uns an, wie wir unsere Analyseklasse einrichten:
using Microsoft.SemanticKernel;
using Microsoft.SemanticKernel.ChatCompletion;
using HtmlAgilityPack;
namespace VsFeedLinkedin.Services;
public class ArticleAnalyzer
{
private readonly Kernel _kernel;
private readonly IChatCompletionService _chatService;
public ArticleAnalyzer(string endpoint, string apiKey, string deploymentName)
{
var builder = Kernel.CreateBuilder();
builder.AddAzureOpenAIChatCompletion(
deploymentName: deploymentName,
endpoint: endpoint,
apiKey: apiKey
);
_kernel = builder.Build();
_chatService = _kernel.GetRequiredService<IChatCompletionService>();
}
Semantic Kernel verwendet ein Builder-Muster für die Konfiguration. Wir erstellen einen KernelBuilder, fügen unseren Azure OpenAI-Chat-Abschlussdienst mit den erforderlichen Anmeldeinformationen hinzu und erstellen dann den Kernel. Aus dem erstellten Kernel rufen wir die IChatCompletionService-Schnittstelle ab, die wir zum Senden von Eingabeaufforderungen und zum Empfangen von Antworten verwenden.
Der Konstruktor benötigt drei Parameter: den Azure OpenAI-Endpunkt (etwa https://your-resource.openai.azure.com/), Ihren API-Schlüssel und den Bereitstellungsnamen (etwa gpt-4o). Diese werden von Umgebungsvariablen übergeben, um unsere Anmeldeinformationen zu schützen.
Die perfekte Eingabeaufforderung erstellen
Die Eingabeaufforderung, die wir an die KI senden, ist entscheidend. Eine gut gestaltete Eingabeaufforderung führt zu konsistenten, qualitativ hochwertigen Ergebnissen. Eine vage oder schlecht strukturierte Eingabeaufforderung führt zu inkonsistenten, mittelmäßigen Ergebnissen. Ich habe viel Zeit damit verbracht, diese Eingabeaufforderung zu durchlaufen, um Ergebnisse zu erhalten, mit denen ich zufrieden bin:
var prompt = $"""
You are a professional tech content analyst and LinkedIn content creator.
Analyze the following Microsoft DevBlogs article and create an engaging LinkedIn post.
Article Title: {title}
Author: {author}
URL: {url}
Tags: {string.Join(", ", tags)}
Article Content:
{cleanContent}
Please provide:
1. A brief summary (2-3 sentences) of the key points
2. The main technologies or topics covered
3. Why this is relevant for developers/tech professionals
4. An engaging LinkedIn post (max 1300 characters) that:
- Starts with a hook or attention-grabbing statement
- Highlights the key value for readers
- Includes a call to action
- Uses appropriate emojis (but not too many)
- Maintains a professional yet approachable tone
- DO NOT include hashtags in the post (they will be added separately)
- DO NOT include the URL in the post (it will be added separately)
Format your response as follows:
## Summary
[Your summary here]
## Key Topics
[List of main topics/technologies]
## Relevance
[Why this matters]
## LinkedIn Post
[Your engaging LinkedIn post here]
""";
Lassen Sie mich hier die Designentscheidungen erläutern. Wir beginnen damit, der KI eine klare Rolle zuzuweisen: „Sie sind ein professioneller Tech-Content-Analyst und LinkedIn-Content-Ersteller.“ Dadurch wird das Modell darauf vorbereitet, mit dem passenden Stil und der passenden Stimme zu reagieren.
Wir stellen den gesamten Kontext bereit, den die KI benötigt: Artikeltitel, Autor, URL, Tags aus dem RSS-Feed und den vollständigen Artikelinhalt. Je mehr Kontext wir angeben, desto besser wird die Analyse sein.
Dann legen wir genau fest, was wir zurückhaben wollen. Ich bitte um vier Dinge: eine Zusammenfassung, Schlüsselthemen, eine Erklärung zur Relevanz und einen LinkedIn-Beitrag. Speziell für den LinkedIn-Beitrag gebe ich detaillierte Anweisungen dazu, was einen guten Beitrag ausmacht – er sollte einen Haken haben, den Wert hervorheben, einen Call-to-Action enthalten, Emojis angemessen verwenden und einen professionellen Ton beibehalten.
Ebenso wichtig sind die negativen Anweisungen. Ich sage der KI ausdrücklich, dass sie KEINE Hashtags oder die URL in den Beitrag einfügen soll. Warum? Weil ich diese separat hinzufüge und wenn die KI sie einbeziehen würde, hätte ich Duplikate. Diese Art der expliziten Anweisung verhindert häufige Fehler.
Abschließend lege ich das genaue Ausgabeformat fest. Indem ich nach Abschnitten frage, die mit ##-Überschriften gekennzeichnet sind, erleichtere ich die programmgesteuerte Analyse der Antwort. Die KI ist sehr gut darin, Formatierungsanweisungen zu befolgen, und diese Konsistenz macht unseren Parsing-Code einfacher und zuverlässiger.
Ausführen der Analyse
So haben wir alles zusammengestellt:
public async Task<ArticleAnalysis> AnalyzeArticleAsync(
string title,
string url,
string htmlContent,
string author,
List<string> tags)
{
var cleanContent = ExtractTextFromHtml(htmlContent);
if (cleanContent.Length > 8000)
{
cleanContent = cleanContent.Substring(0, 8000) + "...";
}
var chatHistory = new ChatHistory();
chatHistory.AddUserMessage(prompt);
var response = await _chatService.GetChatMessageContentAsync(chatHistory);
var responseText = response.Content ?? "";
return ParseAnalysisResponse(responseText, title, url, author, tags);
}
```Wir extrahieren zunächst sauberen Text aus dem HTML-Inhalt (ich erkläre dies im nächsten Abschnitt). Dann kürzen wir den Inhalt, wenn er zu lang ist. Für große Sprachmodelle gelten Token-Grenzwerte, die bei sehr langen Artikeln möglicherweise überschritten werden. Durch die Begrenzung auf 8000 Zeichen stellen wir sicher, dass wir innerhalb der Grenzen bleiben und dennoch einen umfassenden Kontext bieten.
Wir erstellen ein ChatHistory-Objekt und fügen unsere Eingabeaufforderung als Benutzernachricht hinzu. Dies ist die Abstraktion des Semantic Kernels für chatbasierte Interaktionen. Wir senden dies an den Chat-Abschlussdienst und erhalten eine Antwort zurück. Abschließend analysieren wir die Antwort, um die einzelnen Abschnitte zu extrahieren.
### Analysieren der KI-Antwort
Die KI gibt ihre Antwort als Text zurück, der mit unserer angeforderten Struktur formatiert ist. Wir müssen dies in einzelne Felder analysieren:
```csharp
private static ArticleAnalysis ParseAnalysisResponse(
string response,
string title,
string url,
string author,
List<string> tags)
{
var analysis = new ArticleAnalysis
{
Title = title,
Url = url,
Author = author,
Tags = tags,
RawAnalysis = response
};
var sections = response.Split("##", StringSplitOptions.RemoveEmptyEntries);
foreach (var section in sections)
{
var lines = section.Trim().Split('\n', 2);
if (lines.Length < 2) continue;
var sectionTitle = lines[0].Trim().ToLower();
var sectionContent = lines[1].Trim();
switch (sectionTitle)
{
case "summary":
analysis.Summary = sectionContent;
break;
case "key topics":
analysis.KeyTopics = sectionContent;
break;
case "relevance":
analysis.Relevance = sectionContent;
break;
case "linkedin post":
analysis.LinkedInPost = sectionContent;
break;
}
}
return analysis;
}
Wir teilen die Antwort durch die ##-Markierungen auf, wodurch wir jeden Abschnitt erhalten. Für jeden Abschnitt teilen wir ihn durch eine neue Zeile auf, um die Kopfzeile vom Inhalt zu trennen. Anschließend verwenden wir eine Switch-Anweisung, um den Inhalt jedes Abschnitts der entsprechenden Eigenschaft zuzuweisen.
Wir speichern auch die rohe, ungeparste Antwort. Dies ist beim Debuggen nützlich – wenn beim Parsen etwas schief geht, können wir sehen, was die KI tatsächlich zurückgegeben hat.
Inhalte aus HTML extrahieren
Warum wir HTML bereinigen müssen
Wenn wir einen Artikel aus einem Blog abrufen, erhalten wir den vollständigen HTML-Code der Seite. Dazu gehört viel mehr als nur der Artikelinhalt – es gibt Navigationsmenüs, Kopf- und Fußzeilen, Seitenleisten, Widgets für verwandte Artikel, Kommentarabschnitte, Skripte für Analyse und Nachverfolgung, Stylesheets und alle möglichen anderen Elemente.
Wenn wir das alles an unsere KI schicken würden, würden mehrere schlimme Dinge passieren. Die KI müsste eine Menge irrelevanten Text verarbeiten, was Token verschwendet und die Analyse möglicherweise verwirren würde. Der Navigations- und Fußzeilentext wird möglicherweise in die Zusammenfassung einbezogen. Skripte und CSS würden als Inhalte behandelt, was die Analyse weiter verunreinigt.
Wir müssen nur den Artikelinhalt extrahieren – den Teil, den ein menschlicher Leser tatsächlich lesen würde.
Verwenden von HtmlAgilityPack
HtmlAgilityPack ist eine robuste HTML-Parsing-Bibliothek für .NET. Im Gegensatz zu XML ist HTML häufig fehlerhaft – Tags werden möglicherweise nicht richtig geschlossen, Attribute werden möglicherweise nicht richtig in Anführungszeichen gesetzt. HtmlAgilityPack handhabt all dies elegant und gibt uns eine DOM-ähnliche Struktur, die wir abfragen und bearbeiten können.
Hier ist unsere Extraktionsfunktion:
private static string ExtractTextFromHtml(string html)
{
if (string.IsNullOrWhiteSpace(html))
return string.Empty;
var doc = new HtmlDocument();
doc.LoadHtml(html);
var nodesToRemove = doc.DocumentNode.SelectNodes("//script|//style|//nav|//footer|//header");
if (nodesToRemove != null)
{
foreach (var node in nodesToRemove)
{
node.Remove();
}
}
var text = doc.DocumentNode.InnerText;
text = System.Text.RegularExpressions.Regex.Replace(text, @"\s+", " ");
return text.Trim();
}
Wir laden den HTML-Code in ein HtmlDocument, das ihn in eine Baumstruktur analysiert. Dann verwenden wir XPath, um alle Knoten auszuwählen, die wir entfernen möchten. Der XPath-Ausdruck //script|//style|//nav|//footer|//header wählt alle Skriptelemente (JavaScript-Code, den wir nicht benötigen), Stilelemente (CSS, die wir nicht benötigen), Navigationselemente (Navigationsmenüs), Fußzeilenelemente und Kopfzeilenelemente aus.
Nachdem wir diese Knoten entfernt haben, erhalten wir die InnerText-Eigenschaft, die den gesamten Textinhalt extrahiert und gleichzeitig HTML-Tags entfernt. Dies gibt uns den Klartext des Artikels.Zum Schluss bereinigen wir Leerzeichen. HTML enthält oft viele zusätzliche Leerzeichen für Formatierungszwecke – mehrere Leerzeichen, Tabulatoren, Zeilenumbrüche. Wir verwenden einen regulären Ausdruck, um eine beliebige Folge von Leerzeichen durch ein einzelnes Leerzeichen zu ersetzen, und kürzen dann das Ergebnis.
Den vollständigen Artikel abrufen
Der RSS-Feed liefert uns nur Zusammenfassungen, keinen vollständigen Artikelinhalt. Um den vollständigen Text zu erhalten, müssen wir die Webseite des Artikels abrufen:
public static async Task<string> FetchArticleContentAsync(string url)
{
using var httpClient = new HttpClient();
httpClient.DefaultRequestHeaders.Add("User-Agent", "VsFeedLinkedin/1.0");
try
{
return await httpClient.GetStringAsync(url);
}
catch (Exception ex)
{
Console.WriteLine($"⚠️ Failed to fetch article content: {ex.Message}");
return string.Empty;
}
}
Das ist ganz einfach: Wir stellen eine HTTP-GET-Anfrage an die Artikel-URL und geben die HTML-Antwort zurück. Wir verpacken es in einen Try-Catch, da Netzwerkanforderungen fehlschlagen können und wir lieber eine leere Zeichenfolge zurückgeben, als die gesamte Anwendung zum Absturz zu bringen.
Permanente Dokumentation erstellen
Warum Markdown-Dateien generieren?
Jedes Mal, wenn wir einen Artikel analysieren, erstellen wir eine detaillierte Markdown-Datei, die diese Analyse dokumentiert. Dies dient mehreren Zwecken.
Zunächst wird ein durchsuchbares Archiv erstellt. Mit der Zeit bauen Sie eine Sammlung analysierter Artikel auf. Sie können diese Dateien durchsuchen, um frühere Inhalte zu bestimmten Themen zu finden.
Zweitens sorgt es für Transparenz. Sie können genau sehen, was die KI für jeden Artikel generiert hat, einschließlich der vollständigen Analyse und des LinkedIn-Beitrags.
Drittens ist es nützlich zum Debuggen. Wenn bei einem Beitrag etwas schief geht, können Sie sich die Markdown-Datei ansehen, um zu verstehen, was passiert ist.
Die Markdown-Generator-Klasse
public class MarkdownGenerator
{
private readonly string _outputDirectory;
public MarkdownGenerator(string outputDirectory)
{
_outputDirectory = outputDirectory;
if (!Directory.Exists(_outputDirectory))
{
Directory.CreateDirectory(_outputDirectory);
}
}
public string GenerateMarkdownFile(ArticleAnalysis analysis)
{
var sb = new StringBuilder();
var safeTitle = GenerateSafeFileName(analysis.Title);
var fileName = $"{analysis.AnalyzedAt:yyyy-MM-dd}_{safeTitle}.md";
var filePath = Path.Combine(_outputDirectory, fileName);
sb.AppendLine($"# {analysis.Title}");
sb.AppendLine();
sb.AppendLine("## Article Information");
sb.AppendLine();
sb.AppendLine($"- **Author:** {analysis.Author}");
sb.AppendLine($"- **URL:** [{analysis.Url}]({analysis.Url})");
sb.AppendLine($"- **Published:** {analysis.PublishDate:yyyy-MM-dd}");
sb.AppendLine($"- **Analyzed:** {analysis.AnalyzedAt:yyyy-MM-dd HH:mm:ss}");
sb.AppendLine($"- **Tags:** {string.Join(", ", analysis.Tags)}");
sb.AppendLine();
sb.AppendLine("");
sb.AppendLine();
sb.AppendLine("## AI Analysis");
sb.AppendLine();
sb.AppendLine("### Summary");
sb.AppendLine();
sb.AppendLine(analysis.Summary);
sb.AppendLine();
sb.AppendLine("### Key Topics");
sb.AppendLine();
sb.AppendLine(analysis.KeyTopics);
sb.AppendLine();
sb.AppendLine("### Relevance for Developers");
sb.AppendLine();
sb.AppendLine(analysis.Relevance);
sb.AppendLine();
sb.AppendLine("");
sb.AppendLine();
sb.AppendLine("## Generated LinkedIn Post");
sb.AppendLine();
sb.AppendLine("```");
sb.AppendLine(analysis.LinkedInPost);
sb.AppendLine("```");
sb.AppendLine();
sb.AppendLine("");
sb.AppendLine();
sb.AppendLine("*This analysis was generated using AI (Semantic Kernel with Azure OpenAI)*");
File.WriteAllText(filePath, sb.ToString());
return filePath;
}
Der Konstruktor nimmt einen Ausgabeverzeichnispfad und erstellt ihn, falls er nicht vorhanden ist. Die GenerateMarkdownFile-Methode nimmt ein ArticleAnalysis-Objekt und erstellt ein schön formatiertes Markdown-Dokument.
Der Dateiname enthält das Datum und eine bereinigte Version des Titels. Dadurch lassen sich Dateien einfach chronologisch sortieren und auf einen Blick identifizieren.
Umgang mit unsicheren Dateinamen
Artikeltitel können Zeichen enthalten, die in Dateinamen nicht zulässig sind – beispielsweise Doppelpunkte, Schrägstriche, Fragezeichen und Anführungszeichen. Wir müssen diese desinfizieren:
private static string GenerateSafeFileName(string title)
{
var invalidChars = Path.GetInvalidFileNameChars();
var safeTitle = new string(title
.Where(c => !invalidChars.Contains(c))
.ToArray());
safeTitle = safeTitle.Replace(" ", "-").Replace("--", "-");
if (safeTitle.Length > 50)
{
safeTitle = safeTitle.Substring(0, 50);
}
return safeTitle.TrimEnd('-').ToLowerInvariant();
}
Wir verwenden Path.GetInvalidFileNameChars(), um eine Liste von Zeichen abzurufen, die auf dem aktuellen Betriebssystem nicht in Dateinamen vorkommen dürfen. Wir filtern diese heraus, ersetzen Leerzeichen zur besseren Lesbarkeit durch Bindestriche, begrenzen die Länge auf 50 Zeichen und wandeln sie aus Konsistenzgründen in Kleinbuchstaben um.
Einrichten von Telegrammbenachrichtigungen
Warum ich Telegram gewählt habe
Für die Benachrichtigungskomponente habe ich mehrere Optionen in Betracht gezogen – E-Mail, SMS, Slack, Discord und Telegram. Letztendlich habe ich mich aus mehreren Gründen für Telegram entschieden.
Die API ist völlig kostenlos und weist bei angemessener Nutzung keine Ratenbeschränkungen auf. Bei vielen Benachrichtigungsdiensten ist die Anzahl der Nachrichten, die Sie kostenlos senden können, begrenzt, aber Telegram beschränkt Bot-Nachrichten nicht auf einzelne Benutzer.
Die Bot-API ist unglaublich einfach. Es handelt sich lediglich um HTTP-Anfragen mit JSON-Nutzlasten. Keine komplexen Authentifizierungsabläufe, keine Webhooks für grundlegende Funktionen erforderlich.Telegram funktioniert überall – auf meinem Telefon, auf meinem Desktop, in meinem Webbrowser. Ich kann Benachrichtigungen empfangen, wo immer ich bin, und sofort reagieren.
Nachrichten unterstützen umfangreiche Formatierung. Ich kann Fettdruck, Kursivschrift und sogar Codeblöcke verwenden, um meine Benachrichtigungen besser lesbar zu machen.
Erstellen Sie Ihren Telegram-Bot
Das Einrichten eines Telegram-Bots ist überraschend einfach. Öffnen Sie Telegram und suchen Sie nach @BotFather – dies ist der offizielle Bot von Telegram zum Erstellen und Verwalten von Bots. Starten Sie ein Gespräch mit BotFather und senden Sie den Befehl /newbot. BotFather fragt Sie nach einem Namen für Ihren Bot (dies ist der Anzeigename) und einem Benutzernamen (dieser muss eindeutig sein und auf „bot“ enden). Sobald Sie diese bereitgestellt haben, erstellt BotFather Ihren Bot und gibt Ihnen ein API-Token. Dieses Token ist wie ein Passwort – halten Sie es geheim und geben Sie es nicht an öffentliche Repositorys weiter.
Um Ihre Chat-ID zu finden, damit der Bot weiß, wohin er Nachrichten senden soll, beginnen Sie eine Konversation mit Ihrem neuen Bot, indem Sie danach suchen und auf Start klicken. Greifen Sie dann in Ihrem Browser oder mit Curl auf die URL https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates zu. Suchen Sie in der Antwort nach dem Objekt chat – das Feld id ist Ihre Chat-ID.
Senden von Nachrichten über die API
Hier ist unsere Funktion zum Versenden von Telegram-Nachrichten:
static async Task SendToTelegramAsync(string botToken, string chatId, string message)
{
using var httpClient = new HttpClient();
var telegramApiUrl = $"https://api.telegram.org/bot{botToken}/sendMessage";
var payload = new
{
chat_id = chatId,
text = message,
parse_mode = "HTML"
};
var jsonContent = JsonSerializer.Serialize(payload);
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(telegramApiUrl, content);
if (!response.IsSuccessStatusCode)
{
var errorContent = await response.Content.ReadAsStringAsync();
throw new Exception($"Telegram API error: {response.StatusCode} - {errorContent}");
}
}
Die Telegram Bot API ist REST-basiert. Wir stellen eine POST-Anfrage an den sendMessage-Endpunkt mit einem JSON-Body, der die Chat-ID (wohin gesendet werden soll), den Nachrichtentext (was gesendet werden soll) und optional einen Analysemodus (zur Formatierung) enthält.
Wenn wir parse_mode auf „HTML“ setzen, können wir in unseren Nachrichten grundlegende HTML-Tags verwenden – Dinge wie <b>bold</b> und <i>italic</i>. Dies kann die Lesbarkeit von Benachrichtigungen verbessern, obwohl wir für unseren aktuellen Anwendungsfall Klartext senden.
Wenn die Anfrage fehlschlägt, lösen wir eine Ausnahme mit Details zum Fehler aus. Dies hilft beim Debuggen, wenn etwas nicht funktioniert.
Konfigurieren der Anwendung
Umgebungsvariablen
Unsere Anwendung benötigt mehrere vertrauliche Informationen – API-Schlüssel, Bot-Tokens und Endpunkt-URLs. Wir sollten diese niemals fest codieren oder der Versionskontrolle unterwerfen. Stattdessen verwenden wir Umgebungsvariablen, die in jeder Umgebung, in der die Anwendung ausgeführt wird, sicher festgelegt werden können.
Für Telegram benötigen wir TELEGRAM_BOT_TOKEN (das Token, das BotFather Ihnen gegeben hat) und TELEGRAM_CHAT_ID (Ihre Chat-ID, an die Nachrichten gesendet werden sollen).
Für Azure OpenAI benötigen wir AZURE_OPENAI_ENDPOINT (die URL Ihrer Ressource), AZURE_OPENAI_API_KEY (Ihren API-Schlüssel) und AZURE_OPENAI_DEPLOYMENT (den Namen Ihres bereitgestellten Modells, z. B. „gpt-4o“).
Konfiguration im Code laden
So laden wir diese Werte beim Anwendungsstart:
var telegramBotToken = Environment.GetEnvironmentVariable("TELEGRAM_BOT_TOKEN");
var telegramChatId = Environment.GetEnvironmentVariable("TELEGRAM_CHAT_ID");
var azureOpenAiEndpoint = Environment.GetEnvironmentVariable("AZURE_OPENAI_ENDPOINT");
var azureOpenAiKey = Environment.GetEnvironmentVariable("AZURE_OPENAI_API_KEY");
var azureOpenAiDeployment = Environment.GetEnvironmentVariable("AZURE_OPENAI_DEPLOYMENT") ?? "gpt-4o";
var aiAnalysisEnabled = !string.IsNullOrWhiteSpace(azureOpenAiEndpoint) &&
!string.IsNullOrWhiteSpace(azureOpenAiKey);
Wir verwenden Environment.GetEnvironmentVariable, um jeden Wert zu lesen. Für den Bereitstellungsnamen stellen wir den Standardwert „gpt-4o“ bereit, wenn kein Wert festgelegt ist.
Anschließend prüfen wir, ob die KI-Analyse aktiviert werden sollte, indem wir überprüfen, ob wir sowohl einen Endpunkt als auch einen API-Schlüssel haben. Dadurch kann die Anwendung in einem herabgesetzten Modus ausgeführt werden, wenn Azure OpenAI nicht konfiguriert ist – sie ruft weiterhin Feeds ab und verfolgt Artikel, nur ohne die KI-Analyse.###Anmutige Erniedrigung
Dieses Konzept der würdevollen Degradierung ist wichtig. Wir möchten nicht, dass die Anwendung abstürzt, nur weil eine optionale Funktion nicht konfiguriert ist:
ArticleAnalyzer? articleAnalyzer = null;
MarkdownGenerator? markdownGenerator = null;
if (aiAnalysisEnabled)
{
Console.WriteLine("🤖 AI Analysis enabled - Using Azure OpenAI with Semantic Kernel");
articleAnalyzer = new ArticleAnalyzer(azureOpenAiEndpoint!, azureOpenAiKey!, azureOpenAiDeployment);
markdownGenerator = new MarkdownGenerator(articlesOutputDir);
}
else
{
Console.WriteLine("ℹ️ AI Analysis disabled - Set AZURE_OPENAI_ENDPOINT and AZURE_OPENAI_API_KEY to enable");
}
Wenn KI aktiviert ist, erstellen wir den Analysator und Markdown-Generator. Wenn nicht, lassen wir sie null und überspringen die KI-bezogenen Schritte während der Verarbeitung. Auch ohne die KI-Verbesserung bietet die Anwendung weiterhin einen Mehrwert durch das Abrufen von Feeds und das Senden grundlegender Benachrichtigungen.
Automatisieren mit GitHub-Aktionen
Warum GitHub-Aktionen
Die wahre Stärke dieser Lösung liegt in der Automatisierung. Wir möchten die Anwendung nicht alle paar Stunden manuell ausführen, sondern automatisch im Hintergrund ausführen.
GitHub Actions ist dafür perfekt. Es ist in GitHub integriert, sodass kein zusätzlicher Dienst eingerichtet werden muss. Es ist für öffentliche Repositories kostenlos und beinhaltet großzügige Freiminuten für private Repositories. Es kann nach einem Zeitplan ausgeführt werden und unsere Anwendung in regelmäßigen Abständen auslösen. Es verfügt über eine integrierte Geheimnisverwaltung zur sicheren Speicherung unserer API-Schlüssel. Und es kann Änderungen zurück in das Repository übertragen und so unsere Tracking-Datei auf dem neuesten Stand halten.
Die Workflow-Datei
Erstellen Sie unter .github/workflows/fetch-and-notify.yml eine Datei mit folgendem Inhalt:
name: Fetch DevBlogs and Notify
on:
schedule:
- cron: '0 */6 * * *'
workflow_dispatch:
jobs:
fetch-and-notify:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: '9.0.x'
- name: Restore dependencies
run: dotnet restore src/VsFeedLinkedin.csproj
- name: Build
run: dotnet build src/VsFeedLinkedin.csproj --no-restore
- name: Run application
env:
TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }}
TELEGRAM_CHAT_ID: ${{ secrets.TELEGRAM_CHAT_ID }}
AZURE_OPENAI_ENDPOINT: ${{ secrets.AZURE_OPENAI_ENDPOINT }}
AZURE_OPENAI_API_KEY: ${{ secrets.AZURE_OPENAI_API_KEY }}
AZURE_OPENAI_DEPLOYMENT: ${{ secrets.AZURE_OPENAI_DEPLOYMENT }}
run: dotnet run --project src/VsFeedLinkedin.csproj
- name: Commit and push changes
run: |
git config user.name "GitHub Actions Bot"
git config user.email "[email protected]"
if [[ -n $(git status --porcelain posted-articles.md generated-posts/) ]]; then
TIMESTAMP=$(date +%Y%m%d_%H%M%S)
git add posted-articles.md generated-posts/
git commit -m "chore($TIMESTAMP): processed new articles"
git push
else
echo "No changes to commit"
fi
Lassen Sie mich jeden Teil erklären. Der Abschnitt „on“ definiert, wann der Workflow ausgeführt wird. Der Zeitplanauslöser verwendet Cron-Syntax – 0 */6 * * * bedeutet „zu Minute 0 jeder 6. Stunde“. Der Workflow wird also um Mitternacht, 6:00 Uhr, 12:00 Uhr und 18:00 Uhr UTC ausgeführt. Der Workflow_dispatch-Trigger ermöglicht manuelle Ausführungen über die GitHub-Benutzeroberfläche, was zum Testen nützlich ist.
Der Job läuft auf ubuntu-latest, einer virtuellen Linux-Maschine. Wir überprüfen unser Repository, richten .NET 9 ein, stellen NuGet-Pakete wieder her und erstellen das Projekt.
Im Schritt „Anwendung ausführen“ geschieht die Magie. Wir übergeben unsere Geheimnisse als Umgebungsvariablen mithilfe der Syntax ${{ Secrets.SECRET_NAME }}. Diese Geheimnisse werden sicher in GitHub gespeichert und niemals in Protokollen offengelegt.
Abschließend übertragen wir alle Änderungen zurück in das Repository. Wir konfigurieren Git mit einer Bot-Identität, prüfen, ob es Änderungen an unserer Tracking-Datei oder dem Verzeichnis der generierten Beiträge gibt, und wenn ja, erstellen wir einen Commit und pushen ihn.
Geheimnisse einrichten
Um Geheimnisse zu Ihrem GitHub-Repository hinzuzufügen, gehen Sie zu den Einstellungen Ihres Repositorys, dann zu Geheimnissen und Variablen und dann zu Aktionen. Klicken Sie auf „Neues Repository-Geheimnis“ und fügen Sie alle Ihre Umgebungsvariablen hinzu. Die Namen müssen genau mit denen übereinstimmen, auf die wir in der Workflow-Datei verweisen.
Zusammenfassung
Was wir gebaut haben
Wenn wir auf alles zurückblicken, was wir besprochen haben, haben wir einen umfassenden, KI-gestützten RSS-Feed-Aggregator entwickelt, der einen früher mühsamen manuellen Prozess automatisiert. Die Anwendung überwacht automatisch sieben Microsoft DevBlogs-Feeds und erfasst jeden neuen Artikel, sobald er veröffentlicht wird. Es bewältigt die Komplexität der Deduplizierung und erkennt, wenn derselbe Artikel in mehreren Feeds erscheint.Die auf Semantic Kernel und Azure OpenAI basierende KI-Analyse liest und versteht Artikelinhalte, erstellt Zusammenfassungen, identifiziert Schlüsselthemen und erläutert die Relevanz – alles automatisch. Am wichtigsten ist, dass dadurch ansprechende LinkedIn-Beiträge erstellt werden, die ich mit minimalem Bearbeitungsaufwand teilen kann.
Durch die Telegram-Integration werde ich auf meinem Telefon benachrichtigt, wenn es neue Inhalte zu überprüfen gibt. Ich kann einen Blick auf die Nachricht werfen, entscheiden, ob ich sie teilen möchte, und sofort handeln.
Und da es auf GitHub Actions nach einem Zeitplan ausgeführt wird, muss ich nicht daran denken, etwas zu tun. Das System arbeitet im Hintergrund und ich greife nur dann zu, wenn es etwas gibt, das es wert ist, geteilt zu werden.
Die Technologien, die es möglich gemacht haben
Dieses Projekt vereinte mehrere Technologien, die jeweils eine entscheidende Rolle spielten. .NET 9 bot mit seinen modernen Sprachfunktionen und seiner hervorragenden Leistung eine solide Grundlage. Semantic Kernel machte die KI-Integration unkompliziert und bewältigte die gesamte Komplexität von API-Aufrufen und Antwortmanagement. Azure OpenAI lieferte die Intelligenz – die Fähigkeit, technische Inhalte tatsächlich zu verstehen und zu analysieren. HtmlAgilityPack löste das komplizierte Problem, sauberen Text aus Webseiten zu extrahieren. System.ServiceModel.Syndication machte das Parsen von RSS zum Kinderspiel. Die Telegram Bot API lieferte uns kostenlose und zuverlässige Benachrichtigungen. Und GitHub Actions verknüpfte alles mit einer automatisierten, geplanten Ausführung.
Über Kosten nachdenken
Eine Frage, die Sie möglicherweise haben, ist: Wie viel kostet der Betrieb? Die Antwort lautet: Gar nicht viel.
Telegram ist völlig kostenlos – es fallen keine Gebühren für das Versenden von Nachrichten über Ihren Bot an.
GitHub Actions ist für öffentliche Repositories kostenlos. Für private Repositories erhalten Sie im kostenlosen Kontingent 2.000 Minuten pro Monat, was für unseren Anwendungsfall mehr als ausreichend ist.
Azure OpenAI ist die einzige kostenpflichtige Komponente und die Kosten sind minimal. Mit GPT-4o kostet die Analyse eines typischen Blogartikels zwischen einem und drei Cent. Selbst wenn Sie Dutzende Artikel pro Monat verarbeiten, fallen für Sie KI-Kosten von weniger als einem Dollar an.
Wohin Sie das als nächstes bringen könnten
Obwohl diese Lösung für meine Anforderungen hervorragend funktioniert, gibt es viele Möglichkeiten, sie zu erweitern. Sie könnten Unterstützung für mehrere soziale Plattformen hinzufügen – vielleicht zusätzlich zu LinkedIn auch auf Twitter/X, Mastodon oder Bluesky posten. Sie könnten eine Stimmungsanalyse implementieren, um den Ton von Artikeln im Laufe der Zeit zu verfolgen und Trends zu erkennen. Sie können unterschiedliche Eingabeaufforderungsvorlagen für unterschiedliche Feeds zulassen und so unterschiedliche Beitragsstile für unterschiedliche Themen generieren. Sie könnten ein Web-Dashboard zum Überprüfen und Verwalten von Beiträgen erstellen, anstatt Telegram zu verwenden. Sie können Engagement-Metriken für veröffentlichte Inhalte verfolgen, um zu sehen, welche Themen bei Ihrem Publikum am meisten Anklang finden.
Abschließende GedankenWas ich an diesem Projekt am meisten liebe, ist, dass es eine Philosophie verkörpert, an die ich fest glaube: Die Automatisierung sollte die mühsamen Teile erledigen, während die kreativen und entscheidungsrelevanten Teile den Menschen überlassen werden. Das System übernimmt die ganze Arbeit – Abrufen, Parsen, Analysieren, Generieren –, aber ich überprüfe trotzdem alles, bevor ich es teile. Die von der KI generierten Beiträge sind Ausgangspunkte, die ich anpassen und personalisieren kann.
Durch die Kombination der Leistungsfähigkeit von .NET, Semantic Kernel und Azure OpenAI haben wir ein Tool geschaffen, das jede Woche Stunden manueller Arbeit einspart und gleichzeitig Qualität und Konsistenz beibehält. Es ist die Art praktischer Automatisierung, die im täglichen Leben einen echten Unterschied macht.
Wenn Sie etwas Ähnliches bauen oder Ideen für Verbesserungen haben, würde ich gerne davon hören. Kontaktieren Sie uns gerne auf LinkedIn!
Viel Spaß beim Programmieren und frohe Weihnachten! 🎄