تخطَّ إلى المحتوى
NerdLevelTech

مراقب صفحة المنافس بالذكاء الاصطناعي مع diff وتنبيهات Slack في n8n

سير n8n مجدول يومياً يجلب صفحات تسعير وميزات منافسيك، يُجري hash مقابل snapshot الأمس في static data لـn8n، يحسب diff على مستوى الجمل، ويُرسل التغييرات المهمة فقط إلى Slack — مدعوم بـgpt-5-mini. سير متفرع كامل بمنطق IF.

Nerd Level Techحُدّث في الأدوات والإنتاجيةEnglish

بُني واستُورد مباشرة على nerdleveltech.app.n8n.cloud · gpt-5-mini

إحدى عشرة عقدة، If متفرع واحد، مُقارن على مستوى الجمل، مُلخِّص AI — كلها وُصلت وحُفظت على n8n cloud. أكثر العقد إثارة هي Code التي تُجري hash مقابل $getWorkflowStaticData لتحصل على تنبيهات فقط عندما يهم الأمر. تنفيذ حقيقي مُلتقط في الأسفل.

⚠️ قبل التفعيل: عقدة Post to Slack تأتي برابط webhook عام (https://hooks.slack.com/services/REPLACE/WITH/YOUR_WEBHOOK). حتى تستبدله، diff وتلخيص AI يعملان — لكن خطوة التنبيه تفشل بـ ٤٠٤. انظر الخطوة ٥ لإجراء الاستبدال.

ما الذي ستبنيه

سير n8n يومي:

  • يشتغل الساعة ٩ صباحاً كل يوم
  • يجلب قائمة صفحات منافسين تهتم بها (التسعير، الميزات، changelogs)
  • يحسب SHA-1 hash لمحتوى كل صفحة بعد تجريدها
  • يقارن مع hash السابق المخزن في workflow static data لـn8n
  • إذا لم يتغير: no-op صامت (بدون تكلفة AI)
  • إذا تغير: يحسب diff على مستوى الجمل (جمل جديدة، جمل محذوفة) ← gpt-5-mini يُلخص التغيير المادي ← تنبيه Slack Block Kit
سير مراقب المنافسين: Daily Check 9AM ← Competitor URLs ← Fan Out URLs ← Fetch Page ← Diff vs Last Snapshot ← If Changed (فرع إلى Summarize ← Format ← Slack، أو No Change Log)

تخطَّ البناء — استورد السير


المتطلبات المسبقة

المتطلبالتفاصيل
حساب n8nتجربة مجانية
أرصدة OpenAI١٠٠ مجانية من n8n
مساحة عمل Slackصلاحية admin لتثبيت Incoming Webhook
بعض روابط المنافسينالتسعير، الميزات، changelog — اختيارك
الوقتحوالي ١٥ دقيقة

الخطوة ١ — استيراد السير

أنشئ سير عمل جديد. ألصق JSON على اللوحة الفارغة. تُحمَّل ١١ عقدة بتخطيط متفرع — خط مستقيم حتى عقدة If Changed، ثم ينقسم إلى مسار "Summarize & Alert" ومسار "No Change Log".

افتح عقدة OpenAI Chat Model الفرعية وتأكد من gpt-5-mini بدرجة حرارة 0.3.


الخطوة ٢ — قائمة الصفحات التي تراقبها

انقر نقراً مزدوجاً على Competitor URLs. عقدة Set بتعيين واحد:

json
"urls": "[\"https://openai.com/pricing\", \"https://www.anthropic.com/pricing\"]"

مُحوَّلة إلى JSON-string لأن حقول تعبير n8n لا تعرض المصفوفات الخام بسلاسة. العقدة التالية تحللها.

لإضافة روابط، عدّل السلسلة:

json
"urls": "[\"https://openai.com/pricing\", \"https://www.anthropic.com/pricing\", \"https://docs.n8n.io/changelog/\", \"https://www.zapier.com/pricing\"]"

بدون حد أقصى — عقدة Fan Out الأسفل توازيها.


الخطوة ٣ — الجلب و diff مقابل static data

عقدة Fan Out URLs Code تُجري JSON.parse + map لإنتاج عنصر واحد لكل رابط. ثم Fetch Page تشتغل مرة لكل عنصر بالتوازي (توازي n8n التلقائي).

عقدة Diff هي النجمة

انقر نقراً مزدوجاً على Diff vs Last Snapshot. الأجزاء الرئيسية:

١. إزالة الضوضاء، hash ما تبقى:

javascript
const stripped = html
  .replace(/<script[^>]*>[\s\S]*?<\/script>/gi, ' ')
  .replace(/<style[^>]*>[\s\S]*?<\/style>/gi, ' ')
  .replace(/<nav[^>]*>[\s\S]*?<\/nav>/gi, ' ')
  .replace(/<footer[^>]*>[\s\S]*?<\/footer>/gi, ' ')
  .replace(/<header[^>]*>[\s\S]*?<\/header>/gi, ' ')
  .replace(/<[^>]+>/g, ' ')
  .replace(/\s+/g, ' ')
  .trim();

const currentHash = crypto.createHash('sha1').update(stripped).digest('hex');

إزالة nav/footer/header تقتل ٩٥٪ من الإيجابيات الكاذبة. تلك المناطق حيث تسكن معرفات بنر الكوكيز والأختام الزمنية وbuild hashes.

٢. قراءة snapshot السابق من static data:

javascript
const wfStatic = $getWorkflowStaticData('global');
wfStatic.snapshots = wfStatic.snapshots || {};
const prev = wfStatic.snapshots[url] || null;
const prevHash = prev?.hash;
const prevText = prev?.text || '';

$getWorkflowStaticData('global') يُعيد كائناً يُحفظ عبر تشغيلات السير. القراءات مجانية، الكتابات تُحفظ تلقائياً عندما يكتمل تشغيل السير بنجاح.

٣. حساب diff على مستوى الجمل:

javascript
const toLines = t => t.split(/(?<=[.!?])\s+/).filter(l => l.length > 30);
const newLines = new Set(toLines(stripped));
const oldLines = new Set(toLines(prevText));
const added = [...newLines].filter(l => !oldLines.has(l)).slice(0, 12);
const removed = [...oldLines].filter(l => !newLines.has(l)).slice(0, 12);

يُقسِّم على علامات ترقيم الجمل (نقطة/تعجب/استفهام)، يُرشح الأجزاء الصغيرة (< ٣٠ حرف = روابط nav، تواريخ)، يأخذ فرق Set. محدود بـ١٢ مضافة + ١٢ محذوفة لإبقاء مطالبة AI مُحدودة.

٤. حفظ snapshot الجديد:

javascript
wfStatic.snapshots[url] = {
  hash: currentHash,
  text: stripped.slice(0, 20000),
  capturedAt: new Date().toISOString()
};

يخزن الإصدار الحالي ليكون لتشغيل الغد شيء للـdiff ضده. محدود بـ٢٠ ألف حرف لتجنب التضخم.

٥. إرجاع كل ما يحتاجه الفرع التالي:

javascript
return [{ json: {
  url,
  changed: !prevHash || prevHash !== currentHash,
  isFirstRun: !prevHash,
  addedLines: added,
  removedLines: removed
}}];

isFirstRun حاسمة — لا نريد التنبيه في التشغيل الأول (كل URL سينطلق لأن لا شيء للمقارنة ضده).


الخطوة ٤ — التفريع: التلخيص أو التخطي

انقر نقراً مزدوجاً على If Changed. الشرط:

{{ $json.changed && !$json.isFirstRun }}

يجب أن يكون true وليس تشغيلاً أولاً. فروع المخرجات:

  • True ← Summarize the Change ← Format Alert ← Post to Slack
  • False ← No Change Log (عقدة noOp — لا تفعل شيئاً، مجرد terminator بصري)
سير مراقب المنافسين يعرض عقدة If Changed تتفرع إلى مسارين — فرع True يذهب إلى Summarize the Change ← OpenAI Chat Model، فرع False يذهب إلى No Change Log

مطالبة Summarize the Change

انقر نقراً مزدوجاً على Summarize the Change. المطالبة حذرة عن قصد:

You are a competitive-intelligence analyst. A competitor's page changed
overnight. Summarize what is materially different, based strictly on the
added/removed sentences below. Do not speculate beyond what is shown.

PAGE: {{ $json.url }}

ADDED SENTENCES:
{{ ($json.addedLines || []).map(l => '+ ' + l).join('\n') }}

REMOVED SENTENCES:
{{ ($json.removedLines || []).map(l => '- ' + l).join('\n') }}

RULES
- Output a punchy 3-5 bullet Slack message.
- Lead with the most commercially-interesting change (pricing, feature,
  tier, customer, policy).
- Call out specific numbers, tier names, or feature names from the diff.
- If the change is cosmetic/copy-only, say that in one line and stop.
- No preamble. No "As an AI...". No hashtags.

القاعدة "If the change is cosmetic/copy-only, say that in one line and stop" تمنع AI من تضخيم تبديلات كلمات تافهة إلى قوائم bullet مُثيرة للذعر.


الخطوة ٥ — التنسيق والإرسال إلى Slack

Format Alert عقدة Code تبني JSON Slack Block Kit برأس، قسم بحقلين (URL + أعداد diff)، ملخص AI، وتذييل سياق.

Post to Slack عقدة HTTP Request. استبدل placeholder الرابط:

https://hooks.slack.com/services/REPLACE/WITH/YOUR_WEBHOOK

…برابط Incoming Webhook الحقيقي. انظر دليل ملخص الأخبار متعدد المصادر للخطوات الدقيقة لإنشاء واحد.

تنبيه Slack المُعروض يبدو هكذا:

🚨 Competitor page changed
─────────────────────────
Page: openai.com/pricing     Diff: +3 new, -2 removed
─────────────────────────
• Flex pricing tier now public: $1/M input tokens for gpt-5-nano
• Enterprise tier removed minimum commitment language
• "Priority processing" replaced with "Batch processing" as tier label

رابط قابل للنقر، أعداد diff واضحة، bullets محددة. بالضبط ما يريده تسويق on-call.


الخطوة ٦ — تشغيل جدول الإنتاج

Schedule Trigger يُطلق عند ٩ صباحاً بمجرد تبديل السير إلى Active. افتراضياً، السير الجديد غير نشط.

تفعيل السير

اضغط زر Publish أعلى يمين اللوحة، ثم بدّل السير إلى Active. Schedule Trigger يبدأ العد. n8n يتعامل مع الطابور؛ سيرك يشتغل عند ٩ صباحاً في منطقة وقتك المضبوطة في بروفايلك (UTC افتراضياً).

التشغيل الإنتاجي الأول

في التشغيل الأول بعد التفعيل، كل رابط سيُصنَّف كـisFirstRun: true — فلا تنبيه Slack. اليوم الثاني، كل رابط يُقارن مع snapshot الأول — التغييرات تُطلق تنبيهات.

اختبر خط الأنابيب قبل الانتظار: اضغط Execute workflow لتشغيله يدوياً. التشغيل الأول يلتقط الأساس. عدّل إحدى صفحات المنافسين (ليس حقاً — لكن يمكن محاكاة بتحرير النص المخزن عبر كتابة يدوية في static data)، ثم شغّل مرة أخرى للتحقق من إطلاق تنبيه Slack.

تنفيذ n8n حقيقي: 2 رابط منافس جُلبا بالتوازي عبر Fan Out URLs ← Fetch Page ← Diff vs Last Snapshot، ثم If Changed وجَّه كلا العنصرين إلى الفرع False (No Change Log) — Summarize the Change و Format Alert و Post to Slack وعقدة OpenAI Chat الفرعية ظلَّت رماديَّة (لم تُنفَّذ) بشكل صحيح لأن الرابطين isFirstRun=true بدون أساس للمقارنة

في اختبارنا الحقيقي مقابل openai.com/pricing و anthropic.com/pricing، السير جلب الاثنين بشكل صحيح، حسب SHA-1 hashes، ووجّه كلا الرابطين عبر فرع No Change Log لأن isFirstRun: true (لم يكن لهما snapshot سابق). في التشغيل المجدول التالي، أي تغيير في صفحة التسعير سيُطلق مُلخص AI + تنبيه Slack.

⚠️ ملاحظة المواقع المحمية بـ Cloudflare: خلال اختبارنا الحقيقي، أعادت كلتا صفحتي التسعير شاشة "Enable JavaScript and cookies to continue" بدلاً من المحتوى الكامل. هذا يعني diff يلتقط الشاشة الفاصلة، ليس الأسعار الحقيقية. للمواقع ذات حماية البوتات، استبدل عقدة Fetch Page باستدعاء Browserless أو ScrapingBee (انظر التوسعات) — تعيد HTML ما بعد JS.


التوسعات: لقطات شاشة، Teams، Notion

تضمين لقطة شاشة للصفحة

أضف استدعاء HTTP Browserless أو ScreenshotOne بالتوازي مع Fetch Page. غذِّ رابط الصورة المُعاد إلى عقدة Format Alert لـSlack كبلوك image:

json
{ "type": "image", "image_url": "{{ $json.screenshotUrl }}", "alt_text": "Before / after preview" }

يتيح لفريقك رؤية التغيير البصري بدون نقر.

النشر إلى Teams بدلاً من ذلك

استبدل عقدة Post to Slack Webhook بـHTTP Request إلى رابط Incoming Webhook لـTeams. حوّل Block Kit إلى JSON Adaptive Card في عقدة Format Alert. انظر adaptivecards.io للمخطط.

أرشفة كل snapshot إلى Notion

بعد Diff vs Last Snapshot، أضف عقدة Notion Append Database Row. أنشئ قاعدة بيانات بأعمدة: URL و Captured At و Changed (bool) و Diff Summary و Full Text. تحصل على أرشيف مُصدَّر لكل صفحة منافس — رائع للمراجعات التنافسية ربع السنوية.

التلخيص الشرطي بالأهمية

أضف عقدة If أخرى بين Fetch Page و Diff: إذا كان URL يحتوي على "/pricing" أو "/changelog"، نبه دائماً. للصفحات الأخرى، نبه فقط عندما تتغير > ٥ جمل. يُقلل الضوضاء في صفحات تسويق المحتوى الثقيلة التي تُغير النص يومياً.


ما التالي

ازدوج مع إثراء العملاء المحتملين بالـ AI لتأهيل العملاء تلقائياً من الشركات التي تغيرت تسعيرتها، أو ملخص أخبار متعدد المصادر لدمج التغييرات التنافسية مع أخبار الصناعة في ملخص صباحي واحد.

الأسئلة الشائعة

لماذا نُجري hash للصفحة بدلاً من diff كامل في كل مرة؟

مقارنة Hash تُجيب 'هل تغير شيء؟' في O(١). إذا طابق hash الأمس، نتخطى diff واستدعاء AI — يوفر ٩٨٪ من التشغيلات. عندما يختلف hash، نحسب diff على مستوى الجمل (O(n)) ونُرسل فقط الجمل المضافة/المحذوفة إلى AI. هذا النهج ذو المرحلتين يعني التشغيلات اليومية لا تُكلف شيئاً تقريباً عندما تكون الصفحات مستقرة، ولا ندفع لـAI إلا في أيام فيها شيء للتلخيص.

أين يخزن السير snapshot السابق؟

في **static data للسير** في n8n، يُوصل إليه عبر `$getWorkflowStaticData('global')`. هذا مخزن key-value محصور بالسير، مُحفوظ عبر التشغيلات، ومجاني على كل خطط n8n. السير يكتب `{ hash, text, capturedAt }` لكل رابط. Static data ينجو من تحرير السير لكن يُمسح إذا حذفت وأعدت استيراد السير — بعد إعادة الاستيراد، التشغيل الأول يُعامل كل رابط كـ'تشغيل أول' ولا ينبه.

هل يعمل هذا لتطبيقات الصفحة الواحدة (مواقع Vercel و Next.js)؟

جزئياً. Next.js ومعظم مواقع SaaS تُجسد من الخادم التحميل الأولي مع المحتوى الحرج (العنوان الرئيسي، التسعير، الميزات) — هذا ما يلتقطه السير. المحتوى الديناميكي المُحمَّل بـJS العميل بعد hydration (CTAs مخصصة، متغيرات A/B، حالة carousel) لن يُلتقط. لالتقاط SPA كامل، استبدل عقدة Fetch Page باستدعاء لخدمة Browserless/Playwright/Puppeteer تُعيد HTML مُجسَّداً.

كيف أتجنب إيجابيات كاذبة من بنرات الكوكيز والأختام الزمنية؟

السير يُزيل كتل `<nav>` و `<header>` و `<footer>` و `<script>` قبل hash — يزيل معظم مصادر الإيجابيات الكاذبة الشائعة (أختام nav الزمنية، سنة حقوق النشر في footer، معرفات بنر الكوكيز). إذا كنت لا تزال ترى إنذارات كاذبة، أضف استبدالات regex في عقدة Diff: استبدل الأختام الزمنية ومعرفات الجلسة ورموز CSRF بـplaceholder ثابت قبل hash. مثال: `.replace(/\b\d{٤}-\d{٢}-\d{٢}T[\d:.]+Z\b/g, 'TIMESTAMP')`.

هل أستطيع مراقبة ٥٠+ صفحة منافس؟

نعم. أضف روابطك إلى مصفوفة `urls` في عقدة Competitor URLs. عقدة Fan Out تُوزعها إلى fetches متوازية، وكل منها يُجري diff مستقلاً وينبه شرطياً. عند ٥٠ رابطاً تشتغل يومياً، تستخدم ~٥٠ تنفيذ/يوم — لا يزال تحت حد 1000/يوم المجاني في n8n. لـ٥٠٠+ رابط، فكر في تقسيم إلى عدة سير مجدولة في ساعات مختلفة لتوزيع الحمل والبقاء تحت حدود معدل الناشرين الصغار.

المزيد من الأدلّة