Construindo um agregador de feed RSS com tecnologia de IA
Como MVP da Microsoft e entusiasta de tecnologia, constantemente me afogo no oceano de conteúdo incrível publicado nos DevBlogs da Microsoft. Dos anúncios do .NET às atualizações do Visual Studio, das inovações do Azure aos aprofundamentos do Kernel Semântico – sempre há algo novo e interessante acontecendo no ecossistema da Microsoft.
O problema? Acompanhar tudo isso é quase impossível.
Eu queria ficar por dentro dos anúncios mais recentes e compartilhá-los com minha rede, mas verificar manualmente sete feeds RSS diferentes, ler artigos, criar postagens envolventes nas redes sociais e acompanhar o que já compartilhei estava se tornando um trabalho de tempo integral por si só. Todas as manhãs eu abria várias abas do navegador, examinava dezenas de artigos, tentava lembrar quais já havia compartilhado e então gastava um tempo precioso escrevendo posts sobre aqueles que chamavam minha atenção.
Então fiz o que qualquer desenvolvedor faria: automatizei.
Neste guia abrangente, mostrarei como construí um agregador de feeds RSS com tecnologia de IA que monitora vários feeds RSS do Microsoft DevBlogs para novos conteúdos, usa Azure OpenAI e Kernel Semântico para analisar artigos e gerar postagens envolventes, cria documentação detalhada de markdown para cada artigo analisado, envia notificações via Telegram para que eu possa revisar e compartilhar o conteúdo, rastreia tudo para evitar postagens duplicadas e é executado automaticamente por meio do GitHub Actions.
Vamos nos aprofundar em cada aspecto desta solução.
A história por trás deste projeto
Vivendo com sobrecarga de informações
Deixe-me pintar um retrato da minha manhã típica antes de construir esta ferramenta. Eu acordava, pegava meu café e abria meu laptop para conferir o que há de novo no ecossistema de desenvolvedores da Microsoft. Primeiro, eu navegaria até o site principal do DevBlogs para ver se havia algum anúncio importante. Então eu verificaria o blog do .NET especificamente porque essa é minha principal pilha de tecnologia. Depois disso, eu iria para o blog do Kernel Semântico, já que a IA está se tornando cada vez mais importante. O blog do Visual Studio foi o próximo da lista porque as atualizações do IDE podem impactar significativamente meu fluxo de trabalho diário. Em seguida, veio o blog DevOps para notícias relacionadas a CI/CD e GitHub, seguido pelo blog All Things Azure para atualizações de infraestrutura em nuvem e, finalmente, o blog Azure SQL para inovações de banco de dados.
São sete feeds diferentes para verificar. Cada um desses blogs publica vários artigos por semana, às vezes vários por dia durante períodos de anúncios importantes, como .NET Conf ou Build. São potencialmente dezenas de artigos para rastrear, ler e compartilhar. E o problema é o seguinte: como alguém que valoriza o compartilhamento de conhecimento com a comunidade, eu não queria apenas ler esses artigos. Eu queria compartilhar os mais valiosos com minha rede no LinkedIn, ajudando outros desenvolvedores a se manterem informados também.Mas criar uma boa postagem no LinkedIn leva tempo. Você precisa ler o artigo completamente, entender os pontos-chave, pensar por que ele é importante para o seu público, escrever um gancho envolvente e formatar tudo de maneira adequada. Multiplique isso por vários artigos por semana e você terá horas de trabalho.
O que eu realmente queria
Depois de lidar com isso durante meses, sentei-me e pensei em como seria a solução ideal. Em primeiro lugar, nunca mais queria perder anúncios importantes. O sistema deve capturar automaticamente novos artigos assim que forem publicados. Eu também queria economizar tempo na criação de conteúdo, permitindo que a IA ajudasse a criar postagens envolventes – não para substituir totalmente minha voz, mas para me dar um ponto de partida sólido que eu pudesse personalizar.
A consistência foi outro grande fator. Eu queria compartilhar conteúdo regularmente, sem ter que me lembrar de fazer isso manualmente todos os dias. O aspecto do rastreamento também foi crucial – eu precisava de uma maneira de saber o que já compartilhei para evitar postar duplicatas e irritar meus seguidores. Por fim, queria me manter organizado com um registro permanente de tudo o que processei, para poder olhar para trás e ver quais tópicos abordei.
A solução toma forma
A solução que imaginei seria executada de acordo com um cronograma usando GitHub Actions, totalmente sem usar as mãos. Ele buscaria todos os sete feeds automaticamente, sem que eu precisasse abrir uma única guia do navegador. O componente de IA realmente leria e compreenderia o conteúdo e depois o resumiria de uma forma que fosse útil para meu público. Em vez de eu ter que escrever postagens do zero, isso criaria conteúdo de mídia social pronto para compartilhar que eu poderia ajustar se necessário. Tudo seria enviado ao meu Telegram para revisão, para que eu pudesse rapidamente olhar para o meu telefone e decidir o que compartilhar. E, claro, manteria um registro permanente de tudo para referência futura.
Antes de começarmos a construir
O que você precisa em sua máquina
Para acompanhar este tutorial, você precisará de algumas coisas instaladas em sua máquina de desenvolvimento. O mais importante é o .NET SDK versão 9.0 ou posterior. Este é o nosso tempo de execução e fornece todas as ferramentas de construção de que precisamos. Se você não o instalou, acesse dot.net e baixe a versão mais recente. A instalação é simples no Windows, macOS ou Linux.
Você também desejará que o Git seja instalado para controle de versão. Enviaremos nosso código para o GitHub e usaremos GitHub Actions para automação, portanto, ter o Git configurado localmente é essencial. Qualquer versão recente funcionará bem.
Para o seu ambiente de desenvolvimento, recomendo Visual Studio ou VS Code. Pessoalmente, eu uso o VS Code na maior parte do meu trabalho atualmente porque é leve e tem excelente suporte a C# por meio da extensão C# Dev Kit. Mas se você estiver mais confortável com o Visual Studio completo, isso também funciona perfeitamente.
Serviços e contas que você precisaAlém das ferramentas locais, você precisará de contas com alguns serviços. O mais importante é o Azure OpenAI, que alimenta nossa análise de IA. Este é um serviço pré-pago, mas os custos são mínimos para este caso de uso – estamos falando de centavos por artigo analisado. Se não tiver uma conta do Azure, você poderá se inscrever para uma avaliação gratuita que inclui alguns créditos para começar.
Para notificações, usaremos um Telegram Bot. A grande vantagem do Telegram é que sua API de bot é totalmente gratuita. Você pode criar quantos bots quiser e enviar mensagens ilimitadas. Orientarei você no processo de configuração posteriormente neste guia.
Por fim, você precisará de uma conta GitHub para hospedar seu código e executar GitHub Actions. O nível gratuito é mais que suficiente para este projeto. O GitHub oferece 2.000 minutos de execução de Actions por mês em repositórios privados e minutos ilimitados em repositórios públicos.
As bibliotecas que tornam isso possível
Nosso projeto depende de três pacotes NuGet principais, cada um servindo a um propósito específico.
O primeiro é o HtmlAgilityPack, que é o padrão ouro para análise de HTML no .NET. Quando buscamos um artigo em um blog, recuperamos o HTML completo da página – incluindo menus de navegação, rodapés, anúncios e todo tipo de elementos com os quais não nos importamos. HtmlAgilityPack nos permite analisar esse HTML e extrair apenas o conteúdo do artigo que precisamos.
O segundo pacote é Microsoft.SemanticKernel, que é o SDK da Microsoft para integração de modelos de IA em aplicativos. Pense nisso como uma ponte entre seu código .NET e grandes modelos de linguagem como GPT-4. Ele lida com toda a complexidade de chamadas de API, gerenciamento de tokens e análise de respostas, permitindo que você se concentre no que deseja que a IA realmente faça.
O terceiro pacote é System.ServiceModel.Syndication, que fornece suporte integrado para análise de feeds RSS e Atom. RSS pode parecer uma tecnologia antiga, mas ainda é a melhor maneira de obter atualizações estruturadas de blogs e sites de notícias. Este pacote transforma feeds XML brutos em objetos C# fortemente tipados e fáceis de trabalhar.
Compreendendo a arquitetura
Como as peças se encaixam
Antes de mergulharmos no código, deixe-me explicar como todos os componentes funcionam juntos. Compreender o panorama geral tornará os detalhes da implementação muito mais claros.
No nível mais alto, temos nosso arquivo principal Program.cs que atua como orquestrador. Este é o ponto de entrada da nossa aplicação e coordena todos os outros componentes. Quando o aplicativo é executado, ele primeiro carrega a configuração das variáveis de ambiente – coisas como chaves de API e credenciais do Telegram. Em seguida, ele busca feeds RSS de todas as sete fontes do Microsoft DevBlogs. À medida que processa esses feeds, ele desduplica artigos para lidar com casos em que o mesmo artigo aparece em vários feeds. Ele verifica cada artigo em nosso arquivo de rastreamento para ver se já o processamos. Para novos artigos, ele os entrega ao analisador de IA para processamento.A classe ArticleAnalyzer é onde a mágica da IA acontece. Este componente recebe um artigo e faz diversas coisas com ele. Primeiro, ele busca o conteúdo HTML completo da URL do artigo. Em seguida, ele extrai texto limpo desse HTML, removendo todos os elementos de navegação, scripts e estilos desnecessários. Assim que tiver texto limpo, ele o envia para o Azure OpenAI por meio do Kernel Semântico com um prompt cuidadosamente elaborado. A IA analisa o artigo e retorna uma resposta estruturada que inclui um resumo, tópicos principais, explicação de relevância e, o mais importante, uma postagem no LinkedIn pronta para uso. O analisador analisa esta resposta e retorna um objeto ArticleAnalysis contendo todas essas informações.
A classe MarkdownGenerator pega esse objeto ArticleAnalysis e cria um registro permanente dele. Ele gera um arquivo markdown bem formatado que inclui todos os metadados do artigo, a análise da IA e a postagem gerada. Esses arquivos são armazenados em um diretório de postagens geradas, fornecendo um arquivo pesquisável de tudo o que você processou.
Por fim, a integração do Telegram envia o conteúdo do post gerado para o seu telefone. Este é o ponto onde você, como ser humano, analisa o trabalho da IA e decide se deseja compartilhá-lo. O bot envia uma mensagem com o conteúdo da postagem e você pode copiá-la diretamente para o LinkedIn ou modificá-la primeiro.
O Fluxo de Dados
Deixe-me explicar o que acontece quando um novo artigo é publicado no blog .NET. O fluxo de trabalho começa quando o GitHub Actions aciona nosso aplicativo de acordo com sua programação – digamos, a cada seis horas. O aplicativo é ativado e começa a buscar todos os sete feeds RSS. Cada feed retorna um documento XML contendo os artigos mais recentes daquele blog.
À medida que analisamos cada feed, extraímos artigos individuais e os armazenamos em uma lista. Mas aqui está uma parte complicada: o feed principal do DevBlogs geralmente inclui artigos que também aparecem nos feeds de categorias individuais. Portanto, um artigo sobre “.NET 10” pode aparecer tanto no feed principal quanto no feed específico do .NET. Lidamos com isso rastreando URLs em um HashSet, que evita automaticamente duplicatas.
Assim que tivermos nossa lista desduplicada de artigos, nós a filtraremos apenas para os mais recentes – normalmente artigos publicados no último dia ou depois. Não queremos processar artigos antigos que já tratamos em execuções anteriores. Em seguida, verificamos cada artigo recente em nosso arquivo de rastreamento. Se já processamos e postamos sobre um artigo, nós o ignoramos.
Para cada novo artigo, iniciamos o pipeline de análise de IA. O analisador busca o HTML completo do artigo, limpa-o e envia-o para o GPT-4 com nosso prompt. A IA lê o artigo e gera uma análise abrangente junto com uma postagem no LinkedIn. Salvamos essa análise em um arquivo markdown para nossos registros.Com a análise concluída, formatamos uma mensagem e enviamos via Telegram. A mensagem inclui o conteúdo da postagem gerado com o URL e as hashtags anexadas. No meu celular recebo uma notificação, reviso a postagem e, se gostar, posso copiá-la e compartilhá-la no LinkedIn com apenas alguns toques.
Por fim, atualizamos nosso arquivo de rastreamento para marcar este artigo como processado, para que não o tratemos novamente em execuções futuras. Se algum arquivo foi criado ou modificado, o GitHub Actions envia essas alterações de volta ao repositório, mantendo tudo sincronizado.
Configurando o projeto do zero
Criando a estrutura da solução
Vamos começar a construir. Abra seu terminal e navegue até onde deseja criar o projeto. Gosto de manter meus projetos organizados em uma pasta Desenvolvimento, mas você pode colocá-los onde fizer sentido para você.
Primeiro, criaremos um novo arquivo de solução. No .NET, uma solução é um contêiner que pode conter vários projetos. Embora tenhamos apenas um projeto no momento, começar com uma solução torna mais fácil adicionar mais projetos posteriormente, se necessário. Execute o comando dotnet new sln -n vs-feed-linkedin para criar uma solução chamada vs-feed-linkedin.
A seguir, precisamos criar nosso projeto de aplicativo de console. Colocaremos isso em um subdiretório src para manter as coisas organizadas. Execute dotnet new console -n VsFeedLinkedin -o src para criar um projeto de console chamado VsFeedLinkedin na pasta src. Em seguida, adicione este projeto à nossa solução com dotnet sln add src/VsFeedLinkedin.csproj.
Agora navegue até o diretório src com cd src. É aqui que adicionaremos nossos pacotes NuGet e faremos a maior parte do nosso desenvolvimento.
Adicionando os pacotes necessários
Com nosso projeto criado, precisamos adicionar os três pacotes NuGet que mencionei anteriormente. Execute cada um destes comandos em sequência:
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
Depois de executar esses comandos, seu arquivo de projeto deverá ficar parecido com isto:
<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>
O arquivo do projeto informa ao .NET que estamos construindo um executável (OutputType é Exe), direcionado ao .NET 9.0 e usando recursos modernos do C#, como usos implícitos e tipos de referência anuláveis. A seção ItemGroup lista nossas três dependências de pacotes com suas versões exatas.
Aprofunde-se nos feeds RSS
O que exatamente é RSS?
Antes de começarmos a escrever código para buscar feeds, vamos ter certeza de que entendemos com o que estamos trabalhando. RSS significa Really Simple Syndication e é um formato XML padronizado para distribuição de atualizações de conteúdo. A ideia é simples: em vez de exigir que os usuários visitem seu site para ver se há conteúdo novo, você publica um arquivo legível por máquina que lista seu conteúdo recente. Os aplicativos podem então pesquisar esse arquivo periodicamente para descobrir novos artigos.
O RSS existe desde o final dos anos 1990 e início dos anos 2000. Você pode pensar que é uma tecnologia ultrapassada, mas na verdade ainda é amplamente utilizada – especialmente por blogs, sites de notícias e podcasts. A beleza do RSS é a sua simplicidade. É apenas XML com uma estrutura definida e qualquer aplicativo pode analisá-lo.
A estrutura de um feed de DevBlogsAo buscar um feed RSS do Microsoft DevBlogs, você recebe de volta um documento XML que segue uma estrutura específica. No nível superior, há um elemento rss que contém um único elemento de canal. O canal representa o próprio blog e inclui metadados como título, URL e descrição do blog.
Dentro do canal, você encontrará vários elementos de item, cada um representando uma postagem individual do blog. Cada item inclui um título (o título do artigo), um link (a URL onde você pode ler o artigo completo), um pubDate (quando o artigo foi publicado), um elemento dc:creator (o nome do autor), um ou mais elementos de categoria (tags para o artigo) e uma descrição (geralmente um resumo ou trecho do artigo).
Aqui está um exemplo simplificado de como isso se parece:
<?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>
A grande vantagem do pacote System.ServiceModel.Syndication do .NET é que ele analisa tudo isso para nós. Não precisamos navegar manualmente nos nós XML ou nos preocupar com diferentes versões de RSS. Apenas carregamos o feed e recuperamos objetos fortemente digitados.
Os sete feeds que monitoramos
Na minha implementação, monitoro sete feeds diferentes do Microsoft DevBlogs. O feed principal do DevBlogs em devblogs.microsoft.com/feed nos dá uma visão ampla de tudo o que a Microsoft está publicando em todos os seus blogs de desenvolvedores. O feed específico do .NET em devblogs.microsoft.com/dotnet/feed concentra-se especificamente em versões, recursos e práticas recomendadas do .NET. O feed do Kernel Semântico em devblogs.microsoft.com/semantic-kernel/feed abrange a orquestração e integração de IA – cada vez mais importante à medida que a IA se torna central para o desenvolvimento moderno.
O feed do Visual Studio em devblogs.microsoft.com/visualstudio/feed me mantém atualizado sobre as melhorias do IDE e os recursos de produtividade. O feed DevOps em devblogs.microsoft.com/devops/feed abrange tópicos de Azure DevOps, GitHub e CI/CD. O feed All Things Azure em devblogs.microsoft.com/all-things-azure/feed concentra-se em serviços de nuvem e padrões de arquitetura. Por fim, o feed SQL do Azure em devblogs.microsoft.com/azure-sql/feed abrange inovações e recursos de banco de dados.
Você pode estar se perguntando por que verifico o feed principal e os feeds de categorias individuais. O feed principal me dá amplitude – verei artigos de qualquer blog de desenvolvedor da Microsoft, incluindo aqueles que talvez eu não conheça. Os feeds de categoria me dão profundidade – eles garantem que eu não perca nada importante nas minhas principais áreas de interesse, mesmo que esses artigos sejam empurrados para fora do feed principal por causa de conteúdos mais recentes.
Construindo a lógica de busca de RSS
A função principal de busca
Agora vamos escrever algum código. A base do nosso aplicativo é a capacidade de buscar e analisar feeds RSS. Aqui está a função que lida com isso:
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);
}
Deixe-me explicar o que esse código faz. Começamos criando um HttpClient, que é a classe integrada do .NET para fazer solicitações HTTP. Definimos um cabeçalho User-Agent porque alguns servidores bloqueiam solicitações que não se identificam. É uma boa prática definir isso mesmo quando os servidores não exigem isso.Em seguida, fazemos uma solicitação GET para a URL do feed e recebemos a resposta como uma string. Esta string contém o XML bruto do feed RSS.
Para analisar esse XML, criamos um StringReader para agrupar nossa string de resposta e, em seguida, configuramos alguns XmlReaderSettings. A configuração DtdProcessing é importante – os feeds RSS às vezes incluem declarações DTD (Document Type Definition) que precisam ser processadas. A configuração MaxCharactersFromEntities é uma medida de segurança que evita ataques de bomba XML, limitando a quantidade de expansão da entidade que pode ocorrer.
Por fim, criamos um XmlReader com essas configurações e usamos SyndicationFeed.Load para analisar o XML em um objeto SyndicationFeed fortemente tipado. Isso nos dá acesso aos metadados do feed e a todos os seus itens por meio de boas propriedades C# em vez de navegação XML bruta.
Buscando vários feeds com tratamento de erros
No mundo real, as solicitações de rede falham. Os servidores ficam inativos, as conexões atingem o tempo limite e o XML pode ficar malformado. Precisamos lidar com esses casos com elegância. Veja como buscamos todos os nossos feeds e ao mesmo tempo somos resilientes a falhas:
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}");
}
}
Mantemos duas coleções aqui. A lista allArticles conterá todos os artigos que encontrarmos, juntamente com o feed de onde eles vieram. O HashSet vistoUrls rastreia quais URLs de artigos já vimos, ajudando-nos a evitar duplicatas.
Percorremos cada URL de feed e envolvemos a operação de busca em um bloco try-catch. Se a busca de um feed específico falhar – talvez o servidor esteja temporariamente inativo – registramos um aviso e continuamos com o próximo feed. Dessa forma, um problema com um feed não nos impede de processar os outros.
Para cada feed obtido com sucesso, iteramos seus itens. Extraímos o URL do artigo da coleção Links do item. O método HashSet.Add retorna false se a URL já estiver no conjunto, o que é perfeito para nossa lógica de desduplicação. Só adicionamos o artigo à nossa lista se for novo.
Armazenamos o URL do feed junto com cada artigo porque essas informações podem ser úteis posteriormente – por exemplo, podemos querer saber de qual feed específico veio um artigo para fins de depuração ou registro.
Tratamento de duplicatas e estado de rastreamento
O desafio da desduplicação
Como mencionei anteriormente, o Microsoft DevBlogs possui uma estrutura de feed hierárquica que cria um desafio interessante. Quando um membro da equipe .NET publica um artigo sobre, digamos, melhorias de desempenho no .NET 10, esse artigo provavelmente aparecerá no feed principal do DevBlogs e no feed específico do .NET. Às vezes, pode até aparecer no feed do Visual Studio se estiver relacionado aos recursos do IDE.
Se processássemos ingenuamente cada artigo de cada feed, acabaríamos analisando e postando o mesmo artigo diversas vezes. Isso desperdiçaria chamadas de API para o Azure OpenAI, enviaria spam para nosso Telegram com notificações duplicadas e potencialmente irritaria nossos seguidores se postássemos duplicatas.A solução é a desduplicação baseada em URL. Cada artigo possui um URL exclusivo, então podemos usá-lo como identificador. A estrutura de dados HashSet é perfeita para isso porque fornece tempo de pesquisa O(1) e evita automaticamente duplicatas. Quando tentamos adicionar uma URL que já está no conjunto, o método Add simplesmente retorna false, informando que devemos pular esse artigo.
Estado persistente com Markdown
A desduplicação lida com duplicatas em uma única execução, mas e entre execuções? Quando nosso aplicativo é executado a cada seis horas, precisamos lembrar quais artigos já processamos para não manipulá-los novamente.
Optei por armazenar esse estado em um arquivo markdown chamado postado-artigos.md. Por que redução? Algumas razões. Primeiro, é legível por humanos. Posso abrir o arquivo e ver imediatamente quais artigos compartilhei. Em segundo lugar, é controlado por versão. Como esse arquivo está em nosso repositório Git, tenho um histórico completo de quando os artigos foram processados. Terceiro, serve como documentação. Qualquer pessoa que olhar o repositório pode ver o que o aplicativo fez.
O formato deste arquivo é simples. Ele tem um cabeçalho, um carimbo de data e hora mostrando quando o aplicativo foi executado pela última vez e, em seguida, uma lista de artigos em formato de link de desconto:
# 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)
Carregando e analisando o arquivo de rastreamento
Para verificar se já processamos um artigo, precisamos carregar este arquivo e extrair as URLs. Aqui está a função que faz isso:
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;
}
Esta função retorna um HashSet contendo todas as URLs que já processamos. Começamos verificando se o arquivo existe – na primeira execução, não existirá, então retornamos um conjunto vazio.
Para cada linha do arquivo, usamos uma regex para extrair a URL do formato do link markdown. A regex \(([^)]+)\) corresponde a qualquer coisa entre parênteses, que é onde os links markdown armazenam seus URLs.
Depois vem uma etapa importante: normalização de URL. Os URLs do mesmo artigo podem variar em formato. O feed RSS pode nos fornecer https://devblogs.microsoft.com/dotnet/article, mas nossa versão salva tem um parâmetro de rastreamento anexado: https://devblogs.microsoft.com/dotnet/article?wt.mc_id=DT-MVP-5004972. Alguns URLs possuem barras finais, outros não.
Para lidar com isso, eliminamos todos os parâmetros de consulta (tudo após ?) e removemos as barras finais. Essa normalização garante que reconhecemos os artigos como duplicados, mesmo que seus URLs sejam diferentes nessas formas superficiais.
Salvando novos artigos
Quando processamos um artigo com sucesso, precisamos adicioná-lo ao nosso arquivo de rastreamento:
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);
}
Esta função cria uma entrada formatada em markdown com o título do artigo como um link, seguido por carimbos de data e hora mostrando quando o postamos e quando foi publicado originalmente. Se o arquivo ainda não existir, primeiro o criamos com um cabeçalho.
O mecanismo de análise de IA
Compreendendo o kernel semânticoAgora chegamos à parte mais interessante da nossa aplicação – a análise de IA. O Kernel Semântico é o SDK de código aberto da Microsoft para integração de grandes modelos de linguagem em aplicativos. É mais do que apenas um wrapper para chamadas de API. Ele fornece uma estrutura para a construção de aplicativos sofisticados de IA com recursos como plug-ins, planejadores e memória.
Para nosso caso de uso, estamos usando os recursos de conclusão de bate-papo do Semantic Kernel. Enviaremos um prompt ao Azure OpenAI e o modelo analisará nosso artigo e gerará uma resposta. O Kernel Semântico lida com toda a complexidade de autenticação de API, formatação de solicitação e análise de resposta.
Configurando o analisador de artigos
Vejamos como configuramos nossa classe de analisador:
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>();
}
O Kernel Semântico usa um padrão de construtor para configuração. Criamos um KernelBuilder, adicionamos nosso serviço de conclusão de chat Azure OpenAI com as credenciais necessárias e, em seguida, construímos o kernel. Do kernel compilado, recuperamos a interface IChatCompletionService, que usaremos para enviar prompts e receber respostas.
O construtor usa três parâmetros: o ponto de extremidade do Azure OpenAI (algo como https://your-resource.openai.azure.com/), sua chave de API e o nome da implantação (como gpt-4o). Eles são transmitidos a partir de variáveis de ambiente, mantendo nossas credenciais seguras.
Elaborando o prompt perfeito
O aviso que enviamos para a IA é crucial. Um prompt bem elaborado produz resultados consistentes e de alta qualidade. Uma solicitação vaga ou mal estruturada produz resultados inconsistentes e medíocres. Passei um tempo considerável iterando esse prompt para obter resultados que me agradam:
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]
""";
Deixe-me explicar as decisões de design aqui. Começamos dando à IA um papel claro: “Você é um analista de conteúdo técnico profissional e criador de conteúdo do LinkedIn”. Isso prepara o modelo para responder com estilo e voz apropriados.
Fornecemos todo o contexto que a IA precisa: título do artigo, autor, URL, tags do feed RSS e o conteúdo completo do artigo. Quanto mais contexto dermos, melhor será a análise.
Então especificamos exatamente o que queremos de volta. Peço quatro coisas: um resumo, tópicos principais, explicação de relevância e uma postagem no LinkedIn. Especificamente para a postagem no LinkedIn, dou instruções detalhadas sobre o que constitui uma boa postagem - ela deve ter um gancho, destacar o valor, incluir uma frase de chamariz, usar emojis de maneira adequada e manter um tom profissional.
As instruções negativas são igualmente importantes. Eu digo explicitamente à IA para NÃO incluir hashtags ou URL na postagem. Por que? Porque eu os adiciono separadamente e, se a IA os incluísse, teria duplicatas. Este tipo de instrução explícita evita erros comuns.
Finalmente, especifico o formato de saída exato. Ao solicitar seções marcadas com ## cabeçalhos, facilito a análise programática da resposta. A IA é muito boa em seguir instruções de formatação e essa consistência torna nosso código de análise mais simples e confiável.
Executando a Análise
Veja como juntamos tudo:
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);
}
```Primeiro extraímos o texto limpo do conteúdo HTML (explicarei isso na próxima seção). Então truncamos o conteúdo se for muito longo. Modelos de linguagem grandes têm limites de tokens e artigos muito longos podem excedê-los. Ao limitar o limite de 8.000 caracteres, garantimos que permaneceremos dentro dos limites e, ao mesmo tempo, forneceremos um contexto substancial.
Criamos um objeto ChatHistory e adicionamos nosso prompt como uma mensagem do usuário. Esta é a abstração do Semantic Kernel para interações baseadas em chat. Enviamos isso para o serviço de conclusão de chat e recebemos uma resposta. Finalmente, analisamos a resposta para extrair as seções individuais.
### Analisando a resposta da IA
A IA retorna sua resposta como texto formatado com nossa estrutura solicitada. Precisamos analisar isso em campos individuais:
```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;
}
Dividimos a resposta pelos marcadores ##, o que nos dá cada seção. Para cada seção, dividimos por nova linha para separar o cabeçalho do conteúdo. Em seguida, usamos uma instrução switch para atribuir o conteúdo de cada seção à propriedade apropriada.
Também armazenamos a resposta bruta e não analisada. Isso é útil para depuração – se algo der errado com a análise, podemos ver o que a IA realmente retornou.
Extraindo conteúdo de HTML
Por que precisamos limpar o HTML
Quando buscamos um artigo em um blog, obtemos o HTML completo da página. Isso inclui muito mais do que apenas o conteúdo do artigo – há menus de navegação, cabeçalhos, rodapés, barras laterais, widgets de artigos relacionados, seções de comentários, scripts para análise e rastreamento, folhas de estilo e todos os tipos de outros elementos.
Se enviássemos tudo isso para a nossa IA, várias coisas ruins aconteceriam. A IA teria que processar muitos textos irrelevantes, desperdiçando tokens e potencialmente confundindo a análise. O texto de navegação e rodapé pode ser incluído no resumo. Scripts e CSS seriam tratados como conteúdo, poluindo ainda mais a análise.
Precisamos extrair apenas o conteúdo do artigo – a parte que um leitor humano realmente leria.
Usando HtmlAgilityPack
HtmlAgilityPack é uma biblioteca robusta de análise de HTML para .NET. Ao contrário do XML, o HTML geralmente é malformado – as tags podem não ser fechadas corretamente, os atributos podem não ser citados corretamente. HtmlAgilityPack lida com tudo isso com elegância, fornecendo-nos uma estrutura semelhante a DOM que podemos consultar e manipular.
Aqui está nossa função de extração:
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();
}
Carregamos o HTML em um HtmlDocument, que o analisa em uma estrutura de árvore. Em seguida, usamos XPath para selecionar todos os nós que queremos remover. A expressão XPath //script|//style|//nav|//footer|//header seleciona todos os elementos de script (código JavaScript que não precisamos), elementos de estilo (CSS que não precisamos), elementos de navegação (menus de navegação), elementos de rodapé e elementos de cabeçalho.
Após remover esses nós, obtemos a propriedade InnerText, que extrai todo o conteúdo do texto enquanto remove as tags HTML. Isso nos dá o texto simples do artigo.Finalmente, limpamos os espaços em branco. O HTML geralmente tem muitos espaços em branco extras para fins de formatação – vários espaços, tabulações, novas linhas. Usamos um regex para substituir qualquer sequência de caracteres de espaço em branco por um único espaço e, em seguida, cortamos o resultado.
Buscando o artigo completo
O feed RSS fornece apenas resumos, não o conteúdo completo do artigo. Para obter o texto completo, precisamos buscar a página web do artigo:
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;
}
}
Isso é simples – fazemos uma solicitação HTTP GET para o URL do artigo e retornamos a resposta HTML. Nós o envolvemos em um try-catch porque as solicitações de rede podem falhar e preferimos retornar uma string vazia do que travar todo o aplicativo.
Criando Documentação Permanente
Por que gerar arquivos Markdown
Cada vez que analisamos um artigo, geramos um arquivo markdown detalhado que documenta essa análise. Isso serve a vários propósitos.
Primeiro, ele cria um arquivo pesquisável. Com o tempo, você construirá uma coleção de artigos analisados. Você pode pesquisar nesses arquivos para encontrar conteúdo anterior sobre tópicos específicos.
Em segundo lugar, proporciona transparência. Você pode ver exatamente o que a IA gerou para cada artigo, incluindo a análise completa e a postagem no LinkedIn.
Terceiro, é útil para depuração. Se algo der errado com uma postagem, você pode consultar o arquivo markdown para entender o que aconteceu.
A classe geradora de Markdown
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(análise.LinkedInPost);
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;
}
O construtor pega um caminho de diretório de saída e o cria se ele não existir. O método GenerateMarkdownFile pega um objeto ArticleAnalysis e produz um documento de redução bem formatado.
O nome do arquivo inclui a data e uma versão higienizada do título. Isso torna os arquivos fáceis de classificar cronologicamente e identificar rapidamente.
Lidando com nomes de arquivos inseguros
Os títulos dos artigos podem conter caracteres que não são permitidos em nomes de arquivos – coisas como dois pontos, barras, pontos de interrogação e aspas. Precisamos higienizar estes:
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();
}
Usamos Path.GetInvalidFileNameChars() para obter uma lista de caracteres que não podem aparecer em nomes de arquivos no sistema operacional atual. Nós os filtramos, substituímos os espaços por hífens para facilitar a leitura, limitamos o comprimento a 50 caracteres e convertemos para letras minúsculas para maior consistência.
Configurando notificações de telegrama
Por que escolhi o Telegram
Para o componente de notificação, considerei várias opções – email, SMS, Slack, Discord e Telegram. No final das contas, escolhi o Telegram por vários motivos.
A API é totalmente gratuita, sem limites de taxa para uso razoável. Muitos serviços de notificação têm limites de quantas mensagens você pode enviar gratuitamente, mas o Telegram não restringe mensagens de bot a usuários individuais.
A API do bot é incrivelmente simples. São apenas solicitações HTTP com cargas JSON. Não há fluxos de autenticação complexos nem webhooks necessários para funcionalidades básicas.O Telegram funciona em qualquer lugar – no meu telefone, no meu desktop, no meu navegador. Posso receber notificações onde quer que esteja e responder imediatamente.
As mensagens suportam formatação avançada. Posso usar texto em negrito, itálico e até blocos de código para tornar minhas notificações mais legíveis.
Criando seu bot de telegrama
Configurar um bot do Telegram é surpreendentemente fácil. Abra o Telegram e pesquise @BotFather – este é o bot oficial do Telegram para criar e gerenciar bots. Inicie uma conversa com BotFather e envie o comando /newbot. O BotFather solicitará um nome para o seu bot (este é o nome de exibição) e um nome de usuário (deve ser exclusivo e terminar em “bot”). Depois de fornecer isso, o BotFather criará seu bot e fornecerá um token de API. Este token é como uma senha – mantenha-o em segredo e não o envie para repositórios públicos.
Para encontrar seu ID de bate-papo para que o bot saiba para onde enviar mensagens, inicie uma conversa com seu novo bot procurando por ele e pressionando Iniciar. Em seguida, acesse a URL https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates no seu navegador ou usando curl. Procure o objeto chat na resposta – o campo id é o seu ID de bate-papo.
Envio de mensagens via API
Esta é a nossa função para enviar mensagens do Telegram:
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}");
}
}
A API Telegram Bot é baseada em REST. Fazemos uma solicitação POST para o endpoint sendMessage com um corpo JSON contendo o ID do chat (para onde enviar), o texto da mensagem (o que enviar) e, opcionalmente, um modo de análise (para formatação).
Definir parse_mode como “HTML” nos permite usar tags HTML básicas em nossas mensagens – coisas como <b>bold</b> e <i>italic</i>. Isso pode tornar as notificações mais legíveis, embora, para nosso caso de uso atual, enviemos texto simples.
Se a solicitação falhar, lançamos uma exceção com detalhes sobre o que deu errado. Isso ajuda na depuração se algo não estiver funcionando.
Configurando o aplicativo
Variáveis de ambiente
Nosso aplicativo precisa de várias informações confidenciais – chaves de API, tokens de bot e URLs de endpoint. Nunca devemos codificá-los ou submetê-los ao controle de versão. Em vez disso, usamos variáveis de ambiente, que podem ser definidas com segurança em cada ambiente onde o aplicativo é executado.
Para o Telegram, precisamos de TELEGRAM_BOT_TOKEN (o token que o BotFather lhe deu) e TELEGRAM_CHAT_ID (seu ID do chat para onde as mensagens devem ser enviadas).
Para o Azure OpenAI, precisamos de AZURE_OPENAI_ENDPOINT (a URL do seu recurso), AZURE_OPENAI_API_KEY (sua chave de API) e AZURE_OPENAI_DEPLOYMENT (o nome do seu modelo implantado, como “gpt-4o”).
Carregando configuração no código
Veja como carregamos esses valores na inicialização do aplicativo:
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);
Usamos Environment.GetEnvironmentVariable para ler cada valor. Para o nome da implantação, fornecemos um padrão de “gpt-4o” se nenhum valor for definido.
Em seguida, verificamos se a análise de IA deve ser habilitada, verificando se temos um endpoint e uma chave de API. Isso permite que o aplicativo seja executado em modo degradado se o Azure OpenAI não estiver configurado – ele ainda buscará feeds e rastreará artigos, mas sem a análise de IA.### Degradação Graciosa
Este conceito de degradação graciosa é importante. Não queremos que o aplicativo trave só porque um recurso opcional não está configurado:
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");
}
Se a IA estiver habilitada, criamos o analisador e o gerador de descontos. Caso contrário, os deixamos nulos e pulamos as etapas relacionadas à IA durante o processamento. O aplicativo ainda agrega valor ao buscar feeds e enviar notificações básicas, mesmo sem o aprimoramento de IA.
Automatizando com ações do GitHub
Por que as ações do GitHub
O verdadeiro poder desta solução vem da automação. Não queremos executar manualmente o aplicativo a cada poucas horas – queremos que ele seja executado automaticamente em segundo plano.
GitHub Actions é perfeito para isso. Ele está integrado ao GitHub, portanto não há serviço adicional para configurar. É gratuito para repositórios públicos e inclui minutos gratuitos generosos para repositórios privados. Ele pode ser executado de acordo com uma programação, acionando nosso aplicativo em intervalos regulares. Possui gerenciamento de segredos integrado para armazenar nossas chaves de API com segurança. E pode enviar alterações de volta ao repositório, mantendo nosso arquivo de rastreamento atualizado.
O arquivo de fluxo de trabalho
Crie um arquivo em .github/workflows/fetch-and-notify.yml com o seguinte conteúdo:
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
Deixe-me explicar cada parte. A seção on define quando o fluxo de trabalho é executado. O gatilho de agendamento usa sintaxe cron – 0 */6 * * * significa “no minuto 0 de cada 6 horas.” Portanto, o fluxo de trabalho é executado à meia-noite, 6h, meio-dia e 18h UTC. O gatilho workflow_dispatch permite execuções manuais na UI do GitHub, o que é útil para testes.
O trabalho é executado no ubuntu-latest, que é uma máquina virtual Linux. Verificamos nosso repositório, configuramos o .NET 9, restauramos os pacotes NuGet e construímos o projeto.
A etapa Executar aplicativo é onde a mágica acontece. Passamos nossos segredos como variáveis de ambiente usando a sintaxe ${{ secrets.SECRET_NAME }}. Esses segredos são armazenados com segurança no GitHub e nunca expostos em logs.
Finalmente, enviamos todas as alterações de volta ao repositório. Configuramos o Git com uma identidade de bot, verificamos se há alguma alteração em nosso arquivo de rastreamento ou no diretório de postagens gerado e, em caso afirmativo, criamos um commit e enviamos por push.
Configurando segredos
Para adicionar segredos ao seu repositório GitHub, vá para Configurações do seu repositório, depois Segredos e variáveis e, em seguida, Ações. Clique em “Novo segredo do repositório” e adicione cada uma de suas variáveis de ambiente. Os nomes devem corresponder exatamente ao que referenciamos no arquivo de fluxo de trabalho.
Concluindo
O que construímos
Analisando tudo o que abordamos, construímos um agregador de feed RSS abrangente e baseado em IA que automatiza o que costumava ser um processo manual tedioso. O aplicativo monitora automaticamente sete feeds do Microsoft DevBlogs, capturando cada novo artigo assim que é publicado. Ele lida com as complexidades da desduplicação, reconhecendo quando o mesmo artigo aparece em vários feeds.A análise de IA alimentada pelo Semantic Kernel e Azure OpenAI lê e entende o conteúdo do artigo, gerando resumos, identificando os principais tópicos e explicando a relevância – tudo automaticamente. Mais importante ainda, ele cria postagens envolventes no LinkedIn que posso compartilhar com o mínimo de edição.
A integração do Telegram significa que sou notificado no meu telefone sempre que há novo conteúdo para revisar. Posso dar uma olhada na mensagem, decidir se quero compartilhá-la e agir imediatamente.
E como ele é executado no GitHub Actions de acordo com uma programação, não preciso me lembrar de fazer nada. O sistema funciona em segundo plano e só envolvo quando há algo que vale a pena compartilhar.
As tecnologias que tornaram isso possível
Este projeto reuniu diversas tecnologias e cada uma desempenhou um papel crucial. O .NET 9 forneceu uma base sólida com recursos de linguagem modernos e excelente desempenho. O Semantic Kernel simplificou a integração da IA, lidando com toda a complexidade das chamadas de API e gerenciamento de respostas. O Azure OpenAI forneceu a inteligência – a capacidade de realmente compreender e analisar conteúdo técnico. HtmlAgilityPack resolveu o complicado problema de extrair texto limpo de páginas da web. System.ServiceModel.Syndication facilitou muito a análise de RSS. A API do Telegram Bot nos forneceu notificações gratuitas e confiáveis. E o GitHub Actions uniu tudo isso com uma execução automatizada e programada.
Pensando em custos
Uma pergunta que você pode ter é: quanto custa para funcionar? A resposta é: não muito.
O Telegram é totalmente gratuito – sem custos para enviar mensagens através do seu bot.
GitHub Actions é gratuito para repositórios públicos. Para repositórios privados, você recebe 2.000 minutos por mês no nível gratuito, o que é mais que suficiente para nosso caso de uso.
O Azure OpenAI é o único componente pago e os custos são mínimos. Usando o GPT-4o, analisar um artigo típico de blog custa algo entre um e três centavos. Mesmo que você processe dezenas de artigos por mês, terá menos de um dólar em custos de IA.
Onde você pode levar isso a seguir
Embora esta solução funcione muito bem para minhas necessidades, há muitas maneiras de estendê-la. Você pode adicionar suporte para múltiplas plataformas sociais – talvez postar no Twitter/X, Mastodon ou Bluesky, além do LinkedIn. Você pode implementar a análise de sentimento para rastrear o tom dos artigos ao longo do tempo e identificar tendências. Você pode permitir diferentes modelos de prompt para diferentes feeds, gerando diferentes estilos de postagens para diferentes tópicos. Você poderia construir um painel web para revisar e gerenciar postagens em vez de usar o Telegram. Você pode acompanhar as métricas de engajamento do conteúdo postado para ver quais tópicos têm maior repercussão em seu público.
Considerações FinaisO que mais adoro neste projeto é que ele incorpora uma filosofia na qual acredito fortemente: a automação deve cuidar das partes tediosas, deixando as partes criativas e de tomada de decisão para os humanos. O sistema faz todo o trabalho pesado – buscar, analisar, analisar, gerar – mas eu ainda reviso tudo antes de compartilhar. As postagens geradas por IA são pontos de partida que posso customizar e personalizar.
Ao combinar o poder do .NET, do Kernel Semântico e do Azure OpenAI, criamos uma ferramenta que economiza horas de trabalho manual todas as semanas, mantendo a qualidade e a consistência. É o tipo de automação prática que faz uma diferença real na vida diária.
Se você construir algo semelhante ou tiver ideias para melhorias, adoraria ouvir sobre isso. Sinta-se à vontade para entrar em contato no LinkedIn!
Boa codificação e feliz Natal! 🎄