بناء مجمع تغذية RSS مدعوم بالذكاء الاصطناعي
باعتباري أحد أفضل اللاعبين في Microsoft وعشاقًا للتكنولوجيا، أجد نفسي دائمًا غارقًا في محيط من المحتوى المذهل المنشور عبر DevBlogs من Microsoft. بدءًا من إعلانات .NET وحتى تحديثات Visual Studio، ومن ابتكارات Azure إلى الأبحاث العميقة حول Semantic Kernel - هناك دائمًا شيء جديد ومثير يحدث في نظام Microsoft البيئي.
المشكلة؟ مواكبة كل ذلك يكاد يكون من المستحيل.
كنت أرغب في البقاء على اطلاع بأحدث الإعلانات ومشاركتها مع شبكتي، ولكن التحقق يدويًا من سبعة خلاصات RSS مختلفة، وقراءة المقالات، وصياغة منشورات جذابة على وسائل التواصل الاجتماعي، وتتبع ما قمت بمشاركته بالفعل، أصبحت وظيفة بدوام كامل في حد ذاتها. كل صباح، كنت أفتح عدة علامات تبويب في المتصفح، وأتصفح عشرات المقالات، وأحاول أن أتذكر المقالات التي قمت بمشاركتها بالفعل، ثم أقضي وقتًا ثمينًا في كتابة منشورات حول المقالات التي لفتت انتباهي.
لذلك فعلت ما كان سيفعله أي مطور – لقد قمت بتشغيله تلقائيًا.
في هذا الدليل الشامل، سأرشدك عبر كيفية إنشاء مجمع موجزات RSS مدعوم بالذكاء الاصطناعي والذي يراقب خلاصات RSS المتعددة لـ Microsoft DevBlogs للمحتوى الجديد، ويستخدم Azure OpenAI وSemantic Kernel لتحليل المقالات وإنشاء منشورات جذابة، وإنشاء وثائق تخفيض السعر التفصيلية لكل مقالة تم تحليلها، وإرسال إشعارات عبر Telegram حتى أتمكن من مراجعة المحتوى ومشاركته، وتتبع كل شيء لتجنب المنشورات المكررة، وتشغيله تلقائيًا عبر GitHub Actions.
دعونا نتعمق في كل جانب من جوانب هذا الحل.
القصة وراء هذا المشروع
العيش مع الحمل الزائد للمعلومات
اسمحوا لي أن أرسم لكم صورة لصباحي النموذجي قبل أن أقوم ببناء هذه الأداة. كنت أستيقظ وأتناول قهوتي وأفتح جهاز الكمبيوتر المحمول الخاص بي للتحقق من الجديد في النظام البيئي لمطوري Microsoft. أولاً، سأنتقل إلى موقع DevBlogs الرئيسي لمعرفة ما إذا كان هناك أي إعلانات رئيسية. ثم سأقوم بمراجعة مدونة .NET على وجه التحديد لأن هذه هي مجموعتي التكنولوجية الأساسية. بعد ذلك، سأنتقل إلى مدونة Semantic Kernel نظرًا لأن الذكاء الاصطناعي أصبح ذا أهمية متزايدة. كانت مدونة Visual Studio هي التالية في القائمة لأن تحديثات IDE يمكن أن تؤثر بشكل كبير على سير العمل اليومي. ثم جاءت مدونة DevOps للأخبار المتعلقة بـ CI/CD وGitHub، تليها مدونة All Things Azure لتحديثات البنية التحتية السحابية، وأخيرًا مدونة Azure SQL لابتكارات قواعد البيانات.
هذه سبعة خلاصات مختلفة للتحقق منها. تنشر كل من هذه المدونات عدة مقالات أسبوعيًا، وأحيانًا عدة مقالات يوميًا خلال فترات الإعلان الرئيسية مثل .NET Conf أو Build. من المحتمل أن يكون هذا عشرات المقالات لتتبعها وقراءتها ومشاركتها. وهنا الأمر - كشخص يقدر مشاركة المعرفة مع المجتمع، لم أرغب في قراءة هذه المقالات فقط. أردت مشاركة أهم الأشياء مع شبكتي على LinkedIn، لمساعدة المطورين الآخرين على البقاء على اطلاع أيضًا.لكن إنشاء منشور جيد على LinkedIn يستغرق وقتًا. تحتاج إلى قراءة المقالة جيدًا، وفهم النقاط الرئيسية، والتفكير في سبب أهميتها لجمهورك، وكتابة رابط جذاب، وتنسيق كل شيء بشكل جيد. اضرب ذلك بعدة مقالات في الأسبوع، وستحصل على ساعات العمل.
ما أردته حقًا
بعد التعامل مع هذا لعدة أشهر، جلست وفكرت في الشكل الذي سيبدو عليه الحل الأمثل. أولاً وقبل كل شيء، لم أرغب أبدًا في تفويت أي إعلانات مهمة مرة أخرى. يجب أن يلتقط النظام المقالات الجديدة تلقائيًا بمجرد نشرها. وأردت أيضًا توفير الوقت في إنشاء المحتوى من خلال السماح للذكاء الاصطناعي بالمساعدة في صياغة منشورات جذابة - ليس لاستبدال صوتي بالكامل، ولكن لإعطائي نقطة بداية قوية يمكنني تخصيصها.
وكان الاتساق عاملا كبيرا آخر. كنت أرغب في مشاركة المحتوى بانتظام دون الحاجة إلى تذكر القيام بذلك يدويًا كل يوم. كان جانب التتبع حاسمًا أيضًا - كنت بحاجة إلى طريقة لمعرفة ما قمت بمشاركته بالفعل لتجنب نشر التكرارات وإزعاج متابعي. أخيرًا، أردت أن أبقى منظمًا من خلال سجل دائم لكل ما قمت بمعالجته، حتى أتمكن من الرجوع إلى الوراء ومعرفة الموضوعات التي قمت بتغطيتها.
الحل يتبلور
سيتم تشغيل الحل الذي تصورته وفقًا لجدول زمني باستخدام GitHub Actions، بدون استخدام اليدين تمامًا. سيجلب جميع الخلاصات السبعة تلقائيًا دون الحاجة إلى فتح علامة تبويب متصفح واحدة. سيقرأ مكون الذكاء الاصطناعي المحتوى ويفهمه، ثم يلخصه بطريقة مفيدة لجمهوري. بدلاً من أن أضطر إلى كتابة منشورات من الصفر، فإنه سينشئ محتوى جاهزًا للمشاركة على وسائل التواصل الاجتماعي ويمكنني تعديله إذا لزم الأمر. سيتم إرسال كل شيء إلى Telegram الخاص بي للمراجعة، حتى أتمكن من إلقاء نظرة سريعة على هاتفي وتحديد ما سأشاركه. وبطبيعة الحال، فإنه سيحتفظ بسجل دائم لكل شيء للرجوع إليه في المستقبل.
قبل أن نبدأ البناء
ما ستحتاجه على جهازك
لمتابعة هذا البرنامج التعليمي، ستحتاج إلى تثبيت بعض الأشياء على جهاز التطوير الخاص بك. أهمها هو الإصدار 9.0 من .NET SDK أو الأحدث. هذا هو وقت التشغيل الخاص بنا ويوفر جميع أدوات البناء التي نحتاجها. إذا لم يكن مثبتًا لديك، فانتقل إلى dot.net وقم بتنزيل أحدث إصدار. التثبيت سهل ومباشر على أنظمة التشغيل Windows أو macOS أو Linux.
ستحتاج أيضًا إلى تثبيت Git للتحكم في الإصدار. سنقوم بإرسال الكود الخاص بنا إلى GitHub واستخدام إجراءات GitHub للأتمتة، لذا يعد إعداد Git محليًا أمرًا ضروريًا. أي نسخة حديثة سوف تعمل بشكل جيد.
بالنسبة لبيئة التطوير الخاصة بك، أوصي باستخدام Visual Studio أو VS Code. أنا شخصياً أستخدم VS Code في معظم أعمالي هذه الأيام لأنه خفيف الوزن ويتمتع بدعم ممتاز لـ C# من خلال ملحق C# Dev Kit. ولكن إذا كنت أكثر راحة مع Visual Studio الكامل، فهذا يعمل بشكل مثالي أيضًا.
الخدمات والحسابات التي ستحتاج إليهابالإضافة إلى الأدوات المحلية، ستحتاج إلى حسابات تحتوي على عدد قليل من الخدمات. وأهمها هو Azure OpenAI، الذي يدعم تحليل الذكاء الاصطناعي لدينا. هذه خدمة الدفع أولاً بأول، ولكن التكاليف ضئيلة بالنسبة لحالة الاستخدام هذه - فنحن نتحدث عن سنتات لكل مقالة يتم تحليلها. إذا لم يكن لديك حساب Azure، فيمكنك التسجيل للحصول على نسخة تجريبية مجانية تتضمن بعض الاعتمادات للبدء.
للإشعارات، سنستخدم Telegram Bot. إن الشيء العظيم في Telegram هو أن واجهة برمجة التطبيقات الخاصة بهم مجانية تمامًا للاستخدام. يمكنك إنشاء العدد الذي تريده من الروبوتات وإرسال رسائل غير محدودة. سأرشدك خلال عملية الإعداد لاحقًا في هذا الدليل.
وأخيرًا، ستحتاج إلى حساب GitHub لاستضافة التعليمات البرمجية الخاصة بك وتشغيل إجراءات GitHub. الطبقة المجانية أكثر من كافية لهذا المشروع. يمنحك GitHub 2000 دقيقة من وقت تشغيل الإجراءات شهريًا على المستودعات الخاصة، ودقائق غير محدودة على المستودعات العامة.
المكتبات التي تجعل هذا ممكنًا
يعتمد مشروعنا على ثلاث حزم NuGet رئيسية، تخدم كل منها غرضًا محددًا.
الأول هو HtmlAgilityPack، وهو المعيار الذهبي لتحليل HTML في .NET. عندما نجلب مقالة من إحدى المدونات، فإننا نستعيد HTML الكامل للصفحة - بما في ذلك قوائم التنقل والتذييلات والإعلانات وجميع أنواع العناصر التي لا نهتم بها. يتيح لنا HtmlAgilityPack تحليل HTML واستخراج محتوى المقالة الذي نحتاجه فقط.
الحزمة الثانية هي Microsoft.SemanticKernel، وهي حزمة SDK من Microsoft لدمج نماذج الذكاء الاصطناعي في التطبيقات. فكر في الأمر كجسر بين كود .NET الخاص بك ونماذج اللغات الكبيرة مثل GPT-4. فهو يتعامل مع كل تعقيدات استدعاءات واجهة برمجة التطبيقات (API)، وإدارة الرموز المميزة، وتحليل الاستجابة، مما يتيح لك التركيز على ما تريد أن يفعله الذكاء الاصطناعي بالفعل.
الحزمة الثالثة هي System.ServiceModel.Synvation، والتي توفر دعمًا مدمجًا لتحليل خلاصات RSS وAtom. قد تبدو خدمة RSS بمثابة تقنية قديمة، ولكنها لا تزال أفضل طريقة للحصول على تحديثات منظمة من المدونات والمواقع الإخبارية. تعمل هذه الحزمة على تحويل موجزات XML الأولية إلى كائنات C# مكتوبة بقوة والتي يسهل التعامل معها.
فهم العمارة
كيف تتناسب القطع مع بعضها البعض
قبل أن نتعمق في الكود، اسمحوا لي أن أشرح كيفية عمل جميع المكونات معًا. إن فهم الصورة الكبيرة سيجعل تفاصيل التنفيذ أكثر وضوحًا.
على أعلى مستوى، لدينا ملف Program.cs الرئيسي الذي يعمل كمنسق. هذه هي نقطة الدخول لتطبيقنا، وهي تنسق جميع المكونات الأخرى. عند تشغيل التطبيق، يقوم أولاً بتحميل التكوين من متغيرات البيئة – أشياء مثل مفاتيح API وبيانات اعتماد Telegram. ثم يخرج ويجلب موجزات RSS من جميع مصادر Microsoft DevBlogs السبعة. أثناء معالجة هذه الخلاصات، فإنها تعمل على إلغاء تكرار المقالات للتعامل مع الحالات التي تظهر فيها نفس المقالة في خلاصات متعددة. يقوم بفحص كل مقالة مقابل ملف التتبع الخاص بنا لمعرفة ما إذا كنا قد قمنا بمعالجتها بالفعل. بالنسبة للمقالات الجديدة، يتم تسليمها إلى محلل الذكاء الاصطناعي لمعالجتها.فئة ArticleAnalyzer هي المكان الذي يحدث فيه سحر الذكاء الاصطناعي. يتلقى هذا المكون مقالًا ويقوم بعدة أشياء به. أولاً، يقوم بجلب محتوى HTML الكامل من عنوان URL الخاص بالمقالة. ثم يقوم باستخراج نص نظيف من HTML، وإزالة كافة عناصر التنقل والبرامج النصية والتصميمات التي لا نحتاج إليها. بمجرد حصوله على نص نظيف، فإنه يرسل ذلك إلى Azure OpenAI من خلال Semantic Kernel بمطالبة مصممة بعناية. يقوم الذكاء الاصطناعي بتحليل المقالة وإرجاع استجابة منظمة تتضمن ملخصًا وموضوعات رئيسية وشرحًا لأهميتها، والأهم من ذلك، منشورًا جاهزًا للاستخدام على LinkedIn. يقوم المحلل بتحليل هذه الاستجابة وإرجاع كائن ArticleAnalogy الذي يحتوي على كل هذه المعلومات.
تأخذ فئة MarkdownGenerator كائن ArticleAna Analysis وتقوم بإنشاء سجل دائم له. يقوم بإنشاء ملف تخفيض السعر منسق بشكل جيد يتضمن جميع البيانات التعريفية للمقالة، وتحليل الذكاء الاصطناعي، والمنشور الذي تم إنشاؤه. يتم تخزين هذه الملفات في دليل المنشورات التي تم إنشاؤها، مما يتيح لك أرشيفًا قابلاً للبحث لكل ما قمت بمعالجته.
أخيرًا، يرسل تكامل Telegram محتوى المنشور الذي تم إنشاؤه إلى هاتفك. هذه هي النقطة التي يمكنك فيها، كإنسان، مراجعة عمل الذكاء الاصطناعي وتقرر ما إذا كنت تريد مشاركته أم لا. يرسل لك الروبوت رسالة تحتوي على محتوى المنشور، ويمكنك إما نسخها مباشرةً إلى LinkedIn أو تعديلها أولاً.
تدفق البيانات
اسمح لي أن أطلعك على ما يحدث عند نشر مقالة جديدة على مدونة .NET. يبدأ سير العمل عندما تقوم GitHub Actions بتشغيل تطبيقنا وفقًا لجدوله الزمني - لنفترض كل ست ساعات. ينشط التطبيق ويبدأ في جلب جميع خلاصات RSS السبعة. يعرض كل موجز مستند XML يحتوي على أحدث المقالات من تلك المدونة.
أثناء تحليل كل موجز، نقوم باستخراج المقالات الفردية وتخزينها في القائمة. ولكن هنا جزء صعب - غالبًا ما تتضمن خلاصة DevBlogs الرئيسية مقالات تظهر أيضًا في خلاصات الفئات الفردية. لذلك قد تظهر مقالة حول “.NET 10” في كل من الخلاصة الرئيسية والخلاصة الخاصة بـ .NET. نحن نتعامل مع هذا من خلال تتبع عناوين URL في HashSet، مما يمنع التكرارات تلقائيًا.
بمجرد حصولنا على قائمة المقالات المكررة، نقوم بتصفيتها وصولاً إلى المقالات الحديثة فقط - عادةً المقالات المنشورة في اليوم الأخير أو نحو ذلك. لا نريد معالجة المقالات القديمة التي تعاملنا معها بالفعل في عمليات التشغيل السابقة. ثم نتحقق من كل مقالة حديثة مقابل ملف التتبع الخاص بنا. إذا قمنا بالفعل بمعالجة مقالة ونشرها، فإننا نتخطى ذلك.
لكل مقال جديد، نبدأ مسار تحليل الذكاء الاصطناعي. يقوم المحلل بإحضار HTML للمقالة الكاملة، وينظفها، ويرسلها إلى GPT-4 مع مطالبتنا. يقرأ الذكاء الاصطناعي المقالة ويقوم بإنشاء تحليل شامل بالإضافة إلى منشور على LinkedIn. نحفظ هذا التحليل في ملف تخفيض السعر لسجلاتنا.بعد اكتمال التحليل، نقوم بتنسيق رسالة وإرسالها عبر Telegram. تتضمن الرسالة محتوى المنشور الذي تم إنشاؤه مع عنوان URL وعلامات التصنيف الملحقة. على هاتفي، أتلقى إشعارًا، وأراجع المنشور، وإذا أعجبتني، يمكنني نسخه ومشاركته على LinkedIn ببضع نقرات فقط.
أخيرًا، نقوم بتحديث ملف التتبع الخاص بنا لوضع علامة على هذه المقالة على أنها تمت معالجتها، لذلك لن نتعامل معها مرة أخرى في عمليات التشغيل المستقبلية. إذا تم إنشاء أي ملفات أو تعديلها، فإن GitHub Actions يُلزم هذه التغييرات مرة أخرى بالمستودع، مع الحفاظ على مزامنة كل شيء.
إعداد المشروع من الصفر
إنشاء هيكل الحل
لنبدأ بالبناء. افتح المحطة الطرفية الخاصة بك وانتقل إلى المكان الذي تريد إنشاء المشروع فيه. أحب أن أبقي مشاريعي منظمة في مجلد التطوير، ولكن يمكنك وضعها في أي مكان يناسبك.
أولاً، سنقوم بإنشاء ملف حل جديد. في .NET، الحل عبارة عن حاوية يمكنها الاحتفاظ بمشاريع متعددة. على الرغم من أنه لدينا مشروع واحد فقط في الوقت الحالي، فإن البدء بالحل يجعل من السهل إضافة المزيد من المشاريع لاحقًا إذا لزم الأمر. قم بتشغيل الأمر dotnet new sln -n vs-feed-linkedin لإنشاء حل باسم vs-feed-linkedin.
بعد ذلك، نحتاج إلى إنشاء مشروع تطبيق وحدة التحكم الخاص بنا. سنضع هذا في دليل فرعي src للحفاظ على تنظيم الأمور. قم بتشغيل dotnet new console -n VsFeedLinkedin -o src لإنشاء مشروع وحدة تحكم باسم VsFeedLinkedin في مجلد src. ثم أضف هذا المشروع إلى حلنا باستخدام dotnet sln add src/VsFeedLinkedin.csproj.
انتقل الآن إلى دليل src باستخدام cd src. هذا هو المكان الذي سنضيف فيه حزم NuGet الخاصة بنا ونقوم بمعظم عمليات التطوير لدينا.
إضافة الحزم المطلوبة
بعد إنشاء مشروعنا، نحتاج إلى إضافة حزم NuGet الثلاث التي ذكرتها سابقًا. قم بتشغيل كل من هذه الأوامر بالتسلسل:
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
بعد تشغيل هذه الأوامر، يجب أن يبدو ملف مشروعك كما يلي:
<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>
يخبر ملف المشروع .NET أننا نبني ملفًا قابلاً للتنفيذ (OutputType هو Exe)، ونستهدف .NET 9.0، ونستخدم ميزات C# الحديثة مثل الاستخدامات الضمنية وأنواع المراجع الخالية. يسرد قسم ItemGroup تبعيات الحزمة الثلاثة مع إصداراتها الدقيقة.
الغوص العميق في خلاصات RSS
ما هو نظام RSS بالضبط؟
قبل أن نبدأ في كتابة التعليمات البرمجية لجلب الخلاصات، دعونا نتأكد من أننا نفهم ما نعمل معه. يرمز RSS إلى “Really Simple Syndication”، وهو تنسيق XML موحد لتوزيع تحديثات المحتوى. الفكرة بسيطة: بدلاً من مطالبة المستخدمين بزيارة موقع الويب الخاص بك لمعرفة ما إذا كان هناك محتوى جديد، يمكنك نشر ملف يمكن قراءته آليًا يسرد المحتوى الحديث الخاص بك. يمكن للتطبيقات بعد ذلك استقصاء هذا الملف بشكل دوري لاكتشاف مقالات جديدة.
كانت خدمة RSS موجودة منذ أواخر التسعينيات وأوائل العقد الأول من القرن الحادي والعشرين. قد تعتقد أنها تقنية قديمة، لكنها في الواقع لا تزال مستخدمة على نطاق واسع - خاصة من خلال المدونات والمواقع الإخبارية والبودكاست. جمال RSS هو بساطته. إنه مجرد ملف XML ذو بنية محددة، ويمكن لأي تطبيق تحليله.
بنية موجز مدونات المطورينعندما تقوم بإحضار موجز RSS من Microsoft DevBlogs، يمكنك استرجاع مستند XML يتبع بنية محددة. في المستوى الأعلى، يوجد عنصر RSS يحتوي على عنصر قناة واحد. تمثل القناة المدونة نفسها وتتضمن بيانات وصفية مثل عنوان المدونة وعنوان URL والوصف.
ستجد داخل القناة عناصر متعددة، يمثل كل منها منشور مدونة فرديًا. يتضمن كل عنصر عنوانًا (عنوان المقالة)، ورابطًا (عنوان URL الذي يمكنك من خلاله قراءة المقالة كاملة)، وتاريخ النشر (تاريخ نشر المقالة)، وعنصر dc:creator (اسم المؤلف)، وعنصر فئة واحد أو أكثر (علامات للمقالة)، ووصف (عادةً ملخص أو مقتطف من المقالة).
فيما يلي مثال مبسط لما يبدو عليه هذا:
<?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>
إن الشيء العظيم في حزمة System.ServiceModel.Synvation الخاصة بـ .NET هو أنها تقوم بتحليل كل هذا لنا. لا يتعين علينا التنقل يدويًا في عقد XML أو القلق بشأن إصدارات RSS المختلفة. نحن فقط نقوم بتحميل الخلاصة ونستعيد الكائنات المكتوبة بقوة.
الخلاصات السبعة التي نراقبها
أثناء تنفيذي، أراقب سبعة خلاصات مختلفة لـ Microsoft DevBlogs. يمنحنا موجز DevBlogs الرئيسي على devblogs.microsoft.com/feed رؤية واسعة لكل ما تنشره Microsoft عبر جميع مدونات المطورين الخاصة بها. يركز الموجز الخاص بـ .NET الموجود على devblogs.microsoft.com/dotnet/feed بشكل خاص على إصدارات .NET وميزاته وأفضل الممارسات. يغطي موجز Semantic Kernel الموجود على devblogs.microsoft.com/semantic-kernel/feed تنسيق الذكاء الاصطناعي وتكامله - وهو أمر ذو أهمية متزايدة حيث أصبح الذكاء الاصطناعي محوريًا في التطوير الحديث.
يبقيني موجز Visual Studio على devblogs.microsoft.com/visualstudio/feed على اطلاع دائم بتحسينات IDE وميزات الإنتاجية. يغطي موجز DevOps الموجود على devblogs.microsoft.com/devops/feed موضوعات Azure DevOps وGitHub وCI/CD. يركز موجز All Things Azure على devblogs.microsoft.com/all-things-azure/feed على الخدمات السحابية وأنماط الهندسة المعمارية. وأخيرًا، يغطي موجز Azure SQL الموجود على devblogs.microsoft.com/azure-sql/feed ابتكارات قواعد البيانات وميزاتها.
قد تتساءل لماذا أتحقق من كل من الخلاصات الرئيسية وخلاصات الفئات الفردية. يمنحني الموجز الرئيسي اتساعًا - سأرى مقالات من أي مدونة لمطوري Microsoft، بما في ذلك تلك التي قد لا أعرف عنها شيئًا. تمنحني خلاصات الفئات عمقًا - فهي تضمن عدم تفويت أي شيء مهم في مجالات اهتماماتي الأساسية، حتى لو تم إخراج هذه المقالات من الخلاصة الرئيسية بواسطة محتوى أحدث.
بناء منطق جلب RSS
وظيفة الجلب الأساسية
الآن دعونا نكتب بعض التعليمات البرمجية. أساس تطبيقنا هو القدرة على جلب وتحليل خلاصات RSS. إليك الوظيفة التي تتعامل مع هذا:
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);
}
اسمحوا لي أن أتعرف على ما يفعله هذا الرمز. نبدأ بإنشاء HttpClient، وهو فئة .NET المضمنة لتقديم طلبات HTTP. لقد قمنا بتعيين رأس وكيل المستخدم لأن بعض الخوادم تحظر الطلبات التي لا تحدد هويتها. من الممارسات الجيدة ضبط هذا حتى عندما لا تتطلب الخوادم ذلك.نقوم بعد ذلك بتقديم طلب GET إلى عنوان URL للخلاصة ونتلقى الرد كسلسلة. تحتوي هذه السلسلة على ملف XML الأولي لخلاصة RSS.
لتحليل ملف XML هذا، نقوم بإنشاء StringReader لتغليف سلسلة الاستجابة الخاصة بنا، ثم نقوم بتكوين بعض إعدادات XmlReaderSettings. يعد إعداد DtdProcessing مهمًا - تتضمن خلاصات RSS أحيانًا إعلانات DTD (تعريف نوع المستند) التي تحتاج إلى معالجة. يعد إعداد MaxCharactersFromEntities بمثابة إجراء أمني يمنع هجمات XML بالقنابل عن طريق الحد من مقدار توسيع الكيان الذي يمكن أن يحدث.
وأخيرًا، نقوم بإنشاء XmlReader بهذه الإعدادات واستخدام SyndicationFeed.Load لتحليل XML إلى كائن SyndicationFeed مكتوب بقوة. يمنحنا هذا إمكانية الوصول إلى البيانات التعريفية للخلاصة وجميع عناصرها من خلال خصائص C# الرائعة بدلاً من التنقل بتنسيق XML الأولي.
جلب موجزات متعددة مع معالجة الأخطاء
في العالم الحقيقي، تفشل طلبات الشبكة. تتعطل الخوادم، وتنتهي مهلة الاتصالات، ويمكن أن يكون XML مشوهًا. نحن بحاجة للتعامل مع هذه الحالات بأمان. إليك كيفية جلب جميع خلاصاتنا مع المرونة في مواجهة حالات الفشل:
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}");
}
}
نحن نحتفظ بمجموعتين هنا. ستحتوي قائمة allArticles على جميع المقالات التي نجدها، بالإضافة إلى الخلاصة التي أتت منها. يتتبع SeenUrls HashSet عناوين URL للمقالات التي رأيناها بالفعل، مما يساعدنا على تجنب التكرارات.
نقوم بالتكرار خلال كل عنوان URL للخلاصة ونقوم بتغليف عملية الجلب في كتلة محاولة الالتقاط. إذا فشل جلب موجز معين - ربما يكون الخادم معطلاً مؤقتًا - فإننا نسجل تحذيرًا ونستمر في البث التالي. بهذه الطريقة، لا تمنعنا مشكلة في إحدى الخلاصات من معالجة الخلاصات الأخرى.
لكل خلاصة تم جلبها بنجاح، نقوم بالتكرار من خلال عناصرها. نقوم باستخراج عنوان URL للمقالة من مجموعة الروابط الخاصة بالعنصر. تقوم طريقة HashSet.Add بإرجاع خطأ إذا كان عنوان URL موجودًا بالفعل في المجموعة، وهو مثالي لمنطق إلغاء البيانات المكررة لدينا. نضيف المقالة إلى قائمتنا فقط إذا كانت جديدة.
نقوم بتخزين عنوان URL للخلاصة بجانب كل مقالة لأن هذه المعلومات قد تكون مفيدة لاحقًا - على سبيل المثال، قد نرغب في معرفة الخلاصة المحددة التي جاءت منها المقالة لأغراض تصحيح الأخطاء أو التسجيل.
التعامل مع التكرارات وحالة التتبع
تحدي إلغاء البيانات المكررة
كما ذكرت سابقًا، تمتلك Microsoft DevBlogs بنية موجزة هرمية تخلق تحديًا مثيرًا للاهتمام. عندما ينشر أحد أعضاء فريق .NET مقالًا حول، على سبيل المثال، تحسينات الأداء في .NET 10، فمن المحتمل أن تظهر هذه المقالة في كل من خلاصة DevBlogs الرئيسية والخلاصة الخاصة بـ .NET. في بعض الأحيان قد يظهر في موجز Visual Studio إذا كان يتعلق بميزات IDE.
إذا قمنا بسذاجة بمعالجة كل مقالة من كل خلاصة، فسينتهي بنا الأمر إلى تحليل ونشر نفس المقالة عدة مرات. سيؤدي ذلك إلى إهدار مكالمات API إلى Azure OpenAI، وإرسال بريد عشوائي إلى Telegram بإشعارات مكررة، وربما إزعاج متابعينا إذا قمنا بنشر نسخ مكررة.الحل هو إلغاء البيانات المكررة المستندة إلى عنوان URL. تحتوي كل مقالة على عنوان URL فريد، لذا يمكننا استخدامه كمعرف. تعتبر بنية بيانات HashSet مثالية لهذا لأنها توفر وقت بحث O(1) وتمنع التكرارات تلقائيًا. عندما نحاول إضافة عنوان URL موجود بالفعل في المجموعة، فإن طريقة الإضافة ترجع ببساطة خطأ، مما يتيح لنا معرفة أنه يجب علينا تخطي هذه المقالة.
الحالة المستمرة مع تخفيض السعر
يعالج إلغاء البيانات المكررة التكرارات في عملية تشغيل واحدة، ولكن ماذا عن عمليات التشغيل المختلفة؟ عندما يتم تشغيل تطبيقنا كل ست ساعات، نحتاج إلى تذكر المقالات التي قمنا بمعالجتها بالفعل حتى لا نتعامل معها مرة أخرى.
لقد اخترت تخزين هذه الحالة في ملف تخفيض السعر المسمىpost-articles.md. لماذا تخفيض السعر؟ عدة أسباب. أولاً، إنها قابلة للقراءة من قبل الإنسان. يمكنني فتح الملف ورؤية المقالات التي قمت بمشاركتها على الفور. ثانيًا، يتم التحكم في الإصدار. وبما أن هذا الملف موجود في مستودع Git الخاص بنا، فلدي سجل كامل بمواعيد معالجة المقالات. ثالثًا، إنه بمثابة توثيق. يمكن لأي شخص ينظر إلى المستودع أن يرى ما فعله التطبيق.
تنسيق هذا الملف بسيط. يحتوي على رأس، وطابع زمني يوضح آخر مرة تم فيها تشغيل التطبيق، ثم قائمة بالمقالات بتنسيق رابط تخفيض السعر:
# 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)
تحميل ملف التتبع وتحليله
للتحقق مما إذا كنا قد قمنا بالفعل بمعالجة مقالة، نحتاج إلى تحميل هذا الملف واستخراج عناوين URL. إليك الوظيفة التي تقوم بذلك:
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;
}
تقوم هذه الوظيفة بإرجاع HashSet الذي يحتوي على جميع عناوين URL التي قمنا بمعالجتها بالفعل. نبدأ بالتحقق من وجود الملف - عند التشغيل لأول مرة، لن يكون موجودًا، لذلك نعيد مجموعة فارغة.
لكل سطر في الملف، نستخدم regex لاستخراج عنوان URL من تنسيق رابط تخفيض السعر. يتطابق التعبير العادي \(([^)]+)\) مع أي شيء داخل الأقواس، وهو المكان الذي تخزن فيه روابط تخفيض السعر عناوين URL الخاصة بها.
ثم تأتي خطوة مهمة: تطبيع عنوان URL. يمكن أن تختلف عناوين URL لنفس المقالة في التنسيق. قد يقدم لنا موجز RSS https://devblogs.microsoft.com/dotnet/article، ولكن نسختنا المحفوظة تحتوي على معلمة تتبع ملحقة: https://devblogs.microsoft.com/dotnet/article?wt.mc_id=DT-MVP-5004972. تحتوي بعض عناوين URL على خطوط مائلة زائدة، والبعض الآخر لا تحتوي عليها.
للتعامل مع هذا الأمر، نقوم بإزالة أي معلمات استعلام (كل شيء بعد ?) وإزالة الخطوط المائلة اللاحقة. يضمن هذا التطبيع أننا نتعرف على المقالات على أنها مكررة حتى لو كانت عناوين URL الخاصة بها تختلف بهذه الطرق السطحية.
حفظ المقالات الجديدة
عندما نعالج مقالة بنجاح، نحتاج إلى إضافتها إلى ملف التتبع الخاص بنا:
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);
}
تقوم هذه الوظيفة بإنشاء إدخال بتنسيق تخفيض السعر مع عنوان المقالة كرابط، متبوعًا بطوابع زمنية توضح متى قمنا بنشره ومتى تم نشره في الأصل. إذا لم يكن الملف موجودًا بعد، فإننا نقوم بإنشائه برأس أولاً.
محرك تحليل الذكاء الاصطناعي
فهم النواة الدلاليةنصل الآن إلى الجزء الأكثر إثارة من تطبيقنا - تحليل الذكاء الاصطناعي. Semantic Kernel هو SDK مفتوح المصدر من Microsoft لدمج نماذج اللغات الكبيرة في التطبيقات. إنه أكثر من مجرد غلاف حول مكالمات API. فهو يوفر إطارًا لبناء تطبيقات الذكاء الاصطناعي المتطورة مع ميزات مثل المكونات الإضافية والمخططات والذاكرة.
بالنسبة لحالة الاستخدام الخاصة بنا، فإننا نستخدم إمكانات إكمال الدردشة الخاصة بـ Semantic Kernel. سنرسل مطالبة إلى Azure OpenAI، وسيقوم النموذج بتحليل مقالتنا وإنشاء استجابة. يتعامل Semantic Kernel مع كل تعقيدات مصادقة واجهة برمجة التطبيقات (API)، وتنسيق الطلب، وتحليل الاستجابة.
إعداد محلل المقالات
دعونا نلقي نظرة على كيفية إعداد فئة المحلل لدينا:
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 نمط البناء للتكوين. نقوم بإنشاء KernelBuilder، ونضيف خدمة إكمال الدردشة Azure OpenAI الخاصة بنا مع بيانات الاعتماد اللازمة، ثم نبني النواة. من النواة المبنية، نقوم باسترداد واجهة IChatCompletionService، والتي سنستخدمها لإرسال المطالبات وتلقي الاستجابات.
يأخذ المُنشئ ثلاث معلمات: نقطة نهاية Azure OpenAI (شيء مثل https://your-resource.openai.azure.com/)، ومفتاح واجهة برمجة التطبيقات (API)، واسم النشر (مثل gpt-4o). ويتم تمريرها من متغيرات البيئة، مما يحافظ على أمان بيانات الاعتماد الخاصة بنا.
صياغة الموجه المثالي
تعد المطالبة التي نرسلها إلى الذكاء الاصطناعي أمرًا بالغ الأهمية. تنتج المطالبة المصممة جيدًا مخرجات متسقة وعالية الجودة. تؤدي المطالبة الغامضة أو سيئة التنظيم إلى نتائج غير متسقة ومتواضعة. لقد أمضيت وقتًا طويلاً في تكرار هذه المطالبة للحصول على النتائج التي أسعدني:
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]
""";
اسمحوا لي أن أشرح قرارات التصميم هنا. نبدأ بإعطاء الذكاء الاصطناعي دورًا واضحًا: “أنت محلل محتوى تقني محترف ومنشئ محتوى على LinkedIn”. يؤدي هذا إلى إعداد النموذج للاستجابة بالأسلوب والصوت المناسبين.
نحن نقدم كل السياق الذي يحتاجه الذكاء الاصطناعي: عنوان المقالة، والمؤلف، وعنوان URL، والعلامات من موجز RSS، ومحتوى المقالة بالكامل. كلما زاد السياق الذي نقدمه، كلما كان التحليل أفضل.
ثم نحدد بالضبط ما نريده مرة أخرى. أطلب أربعة أشياء: الملخص، والموضوعات الرئيسية، وشرح الصلة، ومنشور LinkedIn. بالنسبة لمنشور LinkedIn على وجه التحديد، أقدم تعليمات تفصيلية حول ما يجعل المنشور جيدًا - يجب أن يكون له تأثير، وقيمة مميزة، ويتضمن عبارة تحث المستخدم على اتخاذ إجراء، ويستخدم الرموز التعبيرية بشكل مناسب، ويحافظ على نبرة احترافية.
التعليمات السلبية لها نفس القدر من الأهمية. أخبر الذكاء الاصطناعي صراحةً بعدم تضمين علامات التصنيف أو عنوان URL في المنشور. لماذا؟ لأنني أضفتها بشكل منفصل، وإذا شملها الذكاء الاصطناعي، فسيكون لدي نسخ مكررة. هذا النوع من التعليمات الصريحة يمنع الأخطاء الشائعة.
وأخيرا، أحدد تنسيق الإخراج الدقيق. من خلال السؤال عن الأقسام المميزة برؤوس ##، أجعل من السهل تحليل الاستجابة برمجيًا. الذكاء الاصطناعي جيد جدًا في اتباع تعليمات التنسيق، وهذا الاتساق يجعل كود التحليل الخاص بنا أبسط وأكثر موثوقية.
تنفيذ التحليل
إليك كيفية تجميع كل ذلك معًا:
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);
}
```نقوم أولاً باستخراج النص النظيف من محتوى HTML (سأشرح ذلك في القسم التالي). ثم نقوم باقتطاع المحتوى إذا كان طويلاً جدًا. نماذج اللغات الكبيرة لها حدود رمزية، وقد تتجاوزها المقالات الطويلة جدًا. من خلال الحد الأقصى لعدد الأحرف وهو 8000 حرف، فإننا نضمن البقاء ضمن الحدود مع الاستمرار في توفير سياق جوهري.
نقوم بإنشاء كائن ChatHistory ونضيف مطالبتنا كرسالة مستخدم. هذا هو مفهوم Semantic Kernel للتفاعلات القائمة على الدردشة. نرسل هذا إلى خدمة إكمال الدردشة ونحصل على الرد. وأخيرا، نقوم بتحليل الاستجابة لاستخراج الأقسام الفردية.
### تحليل استجابة الذكاء الاصطناعي
يقوم الذكاء الاصطناعي بإرجاع استجابته كنص منسق بالبنية المطلوبة. نحن بحاجة إلى تحليل هذا في الحقول الفردية:
```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;
}
قمنا بتقسيم الاستجابة حسب علامات ##، مما يعطينا كل قسم. لكل قسم، نقوم بتقسيمه بواسطة سطر جديد لفصل الرأس عن المحتوى. نستخدم بعد ذلك عبارة التبديل لتعيين محتوى كل قسم إلى الخاصية المناسبة.
نقوم أيضًا بتخزين الاستجابة الأولية غير المحللة. وهذا مفيد لتصحيح الأخطاء – إذا حدث خطأ ما في التحليل، فيمكننا أن ننظر إلى ما أعاده الذكاء الاصطناعي بالفعل.
استخراج المحتوى من HTML
لماذا نحتاج إلى تنظيف HTML
عندما نجلب مقالة من إحدى المدونات، نحصل على HTML الكامل للصفحة. يتضمن ذلك أكثر من مجرد محتوى المقالة - فهناك قوائم التنقل، والترويسات، والتذييلات، والأشرطة الجانبية، وعناصر واجهة المقالة ذات الصلة، وأقسام التعليقات، والبرامج النصية للتحليلات والتتبع، وأوراق الأنماط، وجميع أنواع العناصر الأخرى.
إذا أرسلنا كل هذا إلى الذكاء الاصطناعي لدينا، فسوف تحدث العديد من الأشياء السيئة. سيتعين على الذكاء الاصطناعي معالجة الكثير من النصوص غير ذات الصلة، مما يؤدي إلى إهدار الرموز المميزة وربما إرباك التحليل. قد يتم تضمين نص التنقل والتذييل في الملخص. سيتم التعامل مع البرامج النصية وCSS كمحتوى، مما يزيد من تلويث التحليل.
نحن بحاجة إلى استخراج محتوى المقالة فقط – الجزء الذي سيقرأه القارئ البشري بالفعل.
استخدام HtmlAgilityPack
HtmlAgilityPack عبارة عن مكتبة تحليل HTML قوية لـ .NET. على عكس XML، غالبًا ما يكون HTML مشوهًا - فقد لا يتم إغلاق العلامات بشكل صحيح، وقد لا يتم اقتباس السمات بشكل صحيح. يتعامل HtmlAgilityPack مع كل هذا بأمان، مما يمنحنا بنية تشبه DOM يمكننا الاستعلام عنها ومعالجتها.
ها هي وظيفة الاستخراج لدينا:
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();
}
نقوم بتحميل HTML إلى مستند HtmlDocument، والذي يقوم بتوزيعه إلى بنية شجرة. ثم نستخدم XPath لتحديد جميع العقد التي نريد إزالتها. يحدد تعبير XPath //script|//style|//nav|//footer|//header جميع عناصر البرنامج النصي (لا نحتاج إلى كود JavaScript)، وعناصر النمط (لا نحتاج إلى CSS)، وعناصر التنقل (قوائم التنقل)، وعناصر التذييل، وعناصر الرأس.
بعد إزالة هذه العقد، نحصل على خاصية InnerText، التي تستخرج كل محتوى النص أثناء تجريد علامات HTML. وهذا يعطينا النص الواضح للمقالة.وأخيرا، نقوم بتنظيف المساحة البيضاء. غالبًا ما يحتوي HTML على الكثير من المسافات البيضاء الإضافية لأغراض التنسيق - مسافات متعددة وعلامات تبويب وأسطر جديدة. نستخدم التعبير العادي لاستبدال أي تسلسل من أحرف المسافات البيضاء بمسافة واحدة، ثم نقوم بقص النتيجة.
جلب المقالة كاملة
يقدم لنا موجز RSS ملخصات فقط، وليس محتوى المقالة بالكامل. للحصول على النص الكامل، نحتاج إلى جلب صفحة الويب الخاصة بالمقالة:
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;
}
}
هذا أمر واضح ومباشر – نقوم بإرسال طلب HTTP GET إلى عنوان URL للمقالة ونعيد استجابة HTML. نقوم بلفها في محاولة التقاط لأن طلبات الشبكة يمكن أن تفشل، ونفضل إرجاع سلسلة فارغة بدلاً من تعطل التطبيق بأكمله.
إنشاء وثائق دائمة
لماذا يتم إنشاء ملفات Markdown
في كل مرة نقوم بتحليل مقال ما، نقوم بإنشاء ملف تخفيض تفصيلي يوثق هذا التحليل. وهذا يخدم عدة أغراض.
أولاً، يقوم بإنشاء أرشيف قابل للبحث. وبمرور الوقت، ستتمكن من إنشاء مجموعة من المقالات التي تم تحليلها. يمكنك البحث في هذه الملفات للعثور على المحتوى السابق حول موضوعات محددة.
ثانياً، توفر الشفافية. يمكنك أن ترى بالضبط ما تم إنشاؤه بواسطة الذكاء الاصطناعي لكل مقالة، بما في ذلك التحليل الكامل ومنشور LinkedIn.
ثالثًا، إنه مفيد لتصحيح الأخطاء. إذا حدث خطأ ما في إحدى المشاركات، فيمكنك إلقاء نظرة على ملف تخفيض السعر لفهم ما حدث.
فئة مولد تخفيض السعر
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(ana 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;
}
يأخذ المنشئ مسار دليل الإخراج ويقوم بإنشائه إذا لم يكن موجودًا. تأخذ طريقة GenerateMarkdownFile كائن ArticleAnalogy وتنتج مستند تخفيض السعر منسق بشكل جيد.
يتضمن اسم الملف التاريخ ونسخة منقحة من العنوان. وهذا يجعل من السهل فرز الملفات بترتيب زمني والتعرف عليها في لمحة.
التعامل مع أسماء الملفات غير الآمنة
يمكن أن تحتوي عناوين المقالات على أحرف غير مسموح بها في أسماء الملفات - أشياء مثل النقطتين والشرطات المائلة وعلامات الاستفهام وعلامات الاقتباس. نحن بحاجة إلى تعقيم هذه:
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();
}
نستخدم Path.GetInvalidFileNameChars() للحصول على قائمة بالأحرف التي لا يمكن أن تظهر في أسماء الملفات على نظام التشغيل الحالي. نقوم بتصفية هذه العناصر، واستبدال المسافات بواصلات لتسهيل القراءة، وتحديد الطول بـ 50 حرفًا، وتحويلها إلى أحرف صغيرة لتحقيق الاتساق.
إعداد إشعارات تيليجرام
لماذا اخترت تيليجرام
بالنسبة لعنصر الإشعارات، فكرت في عدة خيارات – البريد الإلكتروني، والرسائل النصية القصيرة، وSlack، وDiscord، وTelegram. لقد اخترت Telegram في النهاية لعدة أسباب.
واجهة برمجة التطبيقات (API) مجانية تمامًا ولا توجد حدود للمعدلات للاستخدام المعقول. تفرض العديد من خدمات الإشعارات حدودًا على عدد الرسائل التي يمكنك إرسالها مجانًا، لكن Telegram لا يقيد رسائل الروبوت للمستخدمين الفرديين.
واجهة برمجة تطبيقات الروبوت بسيطة بشكل لا يصدق. إنها مجرد طلبات HTTP مع حمولات JSON. لا توجد تدفقات مصادقة معقدة، ولا يلزم وجود خطافات ويب للوظائف الأساسية.يعمل Telegram في كل مكان – على هاتفي، وعلى سطح المكتب، وفي متصفح الويب. يمكنني تلقي الإخطارات أينما كنت والرد عليها على الفور.
تدعم الرسائل التنسيق الغني. يمكنني استخدام النص الغامق والخط المائل وحتى كتل التعليمات البرمجية لجعل إشعاراتي أكثر قابلية للقراءة.
إنشاء بوت Telegram الخاص بك
يعد إعداد روبوت Telegram أمرًا سهلاً بشكل مدهش. افتح Telegram وابحث عن @BotFather – هذا هو روبوت Telegram الرسمي لإنشاء الروبوتات وإدارتها. ابدأ محادثة مع BotFather وأرسل الأمر /newbot. سيطلب منك BotFather اسمًا لروبوتك (هذا هو اسم العرض) واسم مستخدم (يجب أن يكون فريدًا وينتهي بـ “bot”). بمجرد تقديم هذه العناصر، سيقوم BotFather بإنشاء الروبوت الخاص بك ويمنحك رمزًا مميزًا لواجهة برمجة التطبيقات. هذا الرمز المميز يشبه كلمة المرور - احتفظ به سرًا ولا تضعه في المستودعات العامة.
للعثور على معرف الدردشة الخاص بك حتى يعرف الروبوت مكان إرسال الرسائل، ابدأ محادثة مع الروبوت الجديد الخاص بك عن طريق البحث عنه والضغط على ابدأ. ثم قم بالوصول إلى عنوان URL https://api.telegram.org/bot<YOUR_TOKEN>/getUpdates في متصفحك أو باستخدام حليقة. ابحث عن كائن chat في الرد - الحقل id هو معرف الدردشة الخاص بك.
إرسال الرسائل عبر API
هذه هي وظيفتنا لإرسال رسائل 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}");
}
}
تعتمد Telegram Bot API على REST. نقوم بإجراء طلب POST إلى نقطة نهاية sendMessage باستخدام نص JSON يحتوي على معرف الدردشة (مكان الإرسال)، ونص الرسالة (ما يجب إرساله)، ووضع التحليل اختياريًا (للتنسيق).
يتيح لنا تعيين parse_mode على “HTML” استخدام علامات HTML الأساسية في رسائلنا - أشياء مثل <b>bold</b> و<i>italic</i>. وهذا يمكن أن يجعل الإشعارات أكثر قابلية للقراءة، على الرغم من أننا نرسل نصًا عاديًا في حالة الاستخدام الحالية لدينا.
إذا فشل الطلب، فإننا نطرح استثناءً يتضمن تفاصيل حول الخطأ الذي حدث. يساعد هذا في تصحيح الأخطاء إذا كان هناك شيء لا يعمل.
تكوين التطبيق
متغيرات البيئة
يحتاج تطبيقنا إلى عدة أجزاء من المعلومات الحساسة - مفاتيح واجهة برمجة التطبيقات، ورموز الروبوت، وعناوين URL لنقطة النهاية. لا ينبغي لنا أبدًا أن نبرمجها أو نلزمها بالتحكم في الإصدار. وبدلاً من ذلك، نستخدم متغيرات البيئة، والتي يمكن تعيينها بشكل آمن في كل بيئة يتم فيها تشغيل التطبيق.
بالنسبة إلى Telegram، نحتاج إلى TELEGRAM_BOT_TOKEN (الرمز المميز الذي قدمه لك BotFather) وTELEGRAM_CHAT_ID (معرف الدردشة الخاص بك حيث يجب إرسال الرسائل).
بالنسبة إلى Azure OpenAI، نحتاج إلى AZURE_OPENAI_ENDPOINT (عنوان URL لموردك)، وAZURE_OPENAI_API_KEY (مفتاح API الخاص بك)، وAZURE_OPENAI_DEPLOYMENT (اسم النموذج الذي تم نشره، مثل “gpt-4o”).
تحميل التكوين في التعليمات البرمجية
إليك كيفية تحميل هذه القيم عند بدء تشغيل التطبيق:
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);
نحن نستخدم Environment.GetEnvironmentVariable لقراءة كل قيمة. بالنسبة لاسم النشر، نقدم الإعداد الافتراضي “gpt-4o” إذا لم يتم تعيين أي قيمة.
نتحقق بعد ذلك مما إذا كان ينبغي تمكين تحليل الذكاء الاصطناعي من خلال التحقق من أن لدينا نقطة نهاية ومفتاح واجهة برمجة التطبيقات. يسمح هذا للتطبيق بالعمل في وضع منخفض إذا لم يتم تكوين Azure OpenAI - فسيستمر في جلب الخلاصات وتتبع المقالات، فقط بدون تحليل الذكاء الاصطناعي.### تدهور رشيق
مفهوم التدهور الرشيق هذا مهم. لا نريد أن يتعطل التطبيق لمجرد عدم تكوين ميزة اختيارية واحدة:
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");
}
إذا تم تمكين الذكاء الاصطناعي، فإننا نقوم بإنشاء المحلل ومولد تخفيض السعر. إذا لم يكن الأمر كذلك، فإننا نتركها فارغة ونتخطى الخطوات المتعلقة بالذكاء الاصطناعي أثناء المعالجة. لا يزال التطبيق يوفر قيمة من خلال جلب الخلاصات وإرسال الإشعارات الأساسية، حتى بدون تحسين الذكاء الاصطناعي.
الأتمتة باستخدام إجراءات GitHub
لماذا إجراءات جيثب
القوة الحقيقية لهذا الحل تأتي من الأتمتة. لا نريد تشغيل التطبيق يدويًا كل بضع ساعات، بل نريد تشغيله تلقائيًا في الخلفية.
إجراءات GitHub مثالية لهذا الغرض. إنه مدمج في GitHub، لذا لا توجد خدمة إضافية لإعدادها. إنه مجاني للمستودعات العامة ويتضمن دقائق مجانية سخية للمستودعات الخاصة. ويمكن تشغيله وفقًا لجدول زمني، مما يؤدي إلى تشغيل تطبيقنا على فترات منتظمة. يحتوي على إدارة أسرار مدمجة لتخزين مفاتيح API الخاصة بنا بشكل آمن. ويمكنه إجراء التغييرات مرة أخرى على المستودع، مما يحافظ على تحديث ملف التتبع الخاص بنا.
ملف سير العمل
قم بإنشاء ملف على .github/workflows/fetch-and-notify.yml بالمحتوى التالي:
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
اسمحوا لي أن أشرح كل جزء. يحدد القسم “وقت تشغيل سير العمل”. يستخدم مشغل الجدول الزمني صيغة cron - 0 */6 * * * تعني “في الدقيقة 0 من كل 6 ساعات.” لذلك يتم تشغيل سير العمل في منتصف الليل، والساعة 6 صباحًا، والظهيرة، والساعة 6 مساءً بالتوقيت العالمي المنسق. يسمح مشغل Workflow_dispatch بالتشغيل اليدوي من واجهة مستخدم GitHub، وهو أمر مفيد للاختبار.
تعمل المهمة على نظام التشغيل ubuntu-latest، وهو جهاز Linux افتراضي. نحن نتحقق من مستودعنا، ونقوم بإعداد .NET 9، ونستعيد حزم NuGet، ونبني المشروع.
خطوة تشغيل التطبيق هي المكان الذي يحدث فيه السحر. نقوم بتمرير أسرارنا كمتغيرات بيئة باستخدام بناء الجملة ${{ Secrets.SECRET_NAME }}. يتم تخزين هذه الأسرار بشكل آمن في GitHub ولا يتم كشفها أبدًا في السجلات.
وأخيرًا، نلتزم بأي تغييرات مرة أخرى في المستودع. نقوم بتكوين Git بهوية الروبوت، والتحقق مما إذا كانت هناك أي تغييرات على ملف التتبع الخاص بنا أو دليل المنشورات التي تم إنشاؤها، وإذا كان الأمر كذلك، فقم بإنشاء التزام ودفعه.
إعداد الأسرار
لإضافة أسرار إلى مستودع GitHub الخاص بك، انتقل إلى إعدادات المستودع الخاص بك، ثم الأسرار والمتغيرات، ثم الإجراءات. انقر فوق “سر المستودع الجديد” وأضف كل متغير من متغيرات البيئة الخاصة بك. يجب أن تتطابق الأسماء تمامًا مع ما نشير إليه في ملف سير العمل.
الختام
ما قمنا ببنائه
بالنظر إلى كل ما قمنا بتغطيته، قمنا ببناء مجمع موجز RSS شامل ومدعوم بالذكاء الاصطناعي والذي يعمل على أتمتة ما كان في السابق عملية يدوية شاقة. يراقب التطبيق سبعة خلاصات لـ Microsoft DevBlogs تلقائيًا، ويلتقط كل مقالة جديدة بمجرد نشرها. فهو يتعامل مع تعقيدات إلغاء البيانات المكررة، ويتعرف على وقت ظهور المقالة نفسها في خلاصات متعددة.يقوم تحليل الذكاء الاصطناعي المدعوم من Semantic Kernel وAzure OpenAI بقراءة محتوى المقالة وفهمه، وإنشاء ملخصات، وتحديد الموضوعات الرئيسية، وشرح مدى صلتها بالموضوع - كل ذلك تلقائيًا. والأهم من ذلك، أنه ينشئ منشورات جذابة على LinkedIn يمكنني مشاركتها مع الحد الأدنى من التحرير.
يعني تكامل Telegram أنني سأتلقى إشعارًا على هاتفي عندما يكون هناك محتوى جديد لمراجعته. يمكنني إلقاء نظرة على الرسالة، وتحديد ما إذا كنت أرغب في مشاركتها، والتصرف على الفور.
ولأنه يعمل على GitHub Actions وفقًا لجدول زمني، فلا يتعين علي أن أتذكر القيام بأي شيء. يعمل النظام في الخلفية، ولا أشارك إلا عندما يكون هناك شيء يستحق المشاركة.
التقنيات التي جعلت ذلك ممكنًا
جمع هذا المشروع بين العديد من التقنيات التي لعبت كل منها دورًا حاسمًا. قدم .NET 9 أساسًا متينًا بميزات اللغة الحديثة والأداء الممتاز. لقد جعل Semantic Kernel تكامل الذكاء الاصطناعي أمرًا مباشرًا، حيث تعامل مع كل تعقيدات استدعاءات واجهة برمجة التطبيقات وإدارة الاستجابة. لقد وفر Azure OpenAI الذكاء – القدرة على فهم المحتوى التقني وتحليله فعليًا. حل HtmlAgilityPack المشكلة الفوضوية المتمثلة في استخراج النص النظيف من صفحات الويب. System.ServiceModel.Syndicator جعل تحليل RSS أمرًا سهلاً. أعطتنا واجهة برمجة تطبيقات Telegram Bot إشعارات مجانية وموثوقة. وربطت إجراءات GitHub كل ذلك معًا من خلال التنفيذ الآلي المجدول.
التفكير في التكاليف
سؤال واحد قد يكون لديك: ما هي تكلفة تشغيل هذا؟ الجواب هو: ليس كثيرا على الاطلاق.
Telegram مجاني تمامًا – لا توجد رسوم لإرسال الرسائل من خلال برنامج الروبوت الخاص بك.
إجراءات GitHub مجانية للمستودعات العامة. بالنسبة للمستودعات الخاصة، تحصل على 2000 دقيقة شهريًا على المستوى المجاني، وهو أكثر من كافٍ لحالة الاستخدام الخاصة بنا.
Azure OpenAI هو المكون الوحيد المدفوع، والتكاليف ضئيلة. باستخدام GPT-4o، يتكلف تحليل مقال مدونة نموذجي ما بين سنت وثلاثة سنتات. حتى لو كنت تقوم بمعالجة عشرات المقالات شهريًا، فإنك تتوقع أقل من دولار واحد من تكاليف الذكاء الاصطناعي.
أين يمكنك أن تأخذ هذا بعد ذلك
على الرغم من أن هذا الحل يناسب احتياجاتي بشكل رائع، إلا أن هناك العديد من الطرق التي يمكنك من خلالها توسيع نطاقه. يمكنك إضافة دعم لمنصات اجتماعية متعددة - ربما النشر على Twitter/X أو Mastodon أو Bluesky بالإضافة إلى LinkedIn. يمكنك تنفيذ تحليل المشاعر لتتبع أسلوب المقالات بمرور الوقت وتحديد الاتجاهات. يمكنك السماح بنماذج مطالبات مختلفة لخلاصات مختلفة، وإنشاء أنماط مختلفة من المنشورات لموضوعات مختلفة. يمكنك إنشاء لوحة تحكم ويب لمراجعة المنشورات وإدارتها بدلاً من استخدام Telegram. يمكنك تتبع مقاييس التفاعل للمحتوى المنشور لمعرفة الموضوعات التي تلقى صدى أكبر لدى جمهورك.
الأفكار النهائيةأكثر ما أحبه في هذا المشروع هو أنه يجسد فلسفة أؤمن بها بشدة: يجب أن تتعامل الأتمتة مع الأجزاء المملة مع ترك الأجزاء الإبداعية وأجزاء صنع القرار للبشر. يقوم النظام بكل الأعمال الشاقة - الجلب والتحليل والتحليل والتوليد - ولكنني ما زلت أراجع كل شيء قبل المشاركة. تعد المنشورات التي يتم إنشاؤها بواسطة الذكاء الاصطناعي بمثابة نقاط بداية يمكنني تخصيصها وتخصيصها.
من خلال الجمع بين قوة .NET وSemantic Kernel وAzure OpenAI، قمنا بإنشاء أداة توفر ساعات من العمل اليدوي كل أسبوع مع الحفاظ على الجودة والاتساق. إنه نوع من الأتمتة العملية التي تُحدث فرقًا حقيقيًا في الحياة اليومية.
إذا قمت ببناء شيء مماثل أو لديك أفكار للتحسينات، فأنا أحب أن أسمع عنها. لا تتردد في التواصل على LinkedIn!
برمجة سعيدة، وعيد ميلاد سعيد! 🎄