إزالة العناصر المكررة من RSS وReddit عبر التشغيلات المجدولة قبل التلخيص بواسطة LLM

أقوم بتشغيل سير عمل مجدول يسحب العناصر من عدة موجزات RSS وبحث Reddit، ثم يرسل العناصر الجديدة إلى عقدة LLM لملخص قصير. المشكلة هي أن نفس القصة غالباً ما تظهر في عدة موجزات برعناوين وعناوين URL مختلفة قليلاً، لذا يتم تلخيصها مرتين.

حالياً أقوم بتخزين عناوين URL التي شاهدتها في جدول بيانات والتصفية على مطابقة دقيقة، مما يفتقد التكرارات القريبة. هل وجد أحد نمطاً نظيفاً لهذا داخل n8n، على سبيل المثال تطبيع عناوين URL (إزالة معاملات utm، الشرطة المائلة العكسية) بالإضافة إلى فحص تشابه العنوان، دون إضافة قاعدة بيانات خارجية؟

وصف المشكلة/الخطأ/السؤال

ما رسالة الخطأ (إن وجدت)؟

يرجى مشاركة سير العمل الخاص بك

(Select the nodes on your canvas and use the keyboard shortcuts CMD+C/CTRL+C and CMD+V/CTRL+V to copy and paste the workflow.)

شارك الناتج الذي أرجعته العقدة الأخيرة

معلومات حول إعداد n8n الخاص بك

  • إصدار n8n:
  • قاعدة البيانات (الافتراضي: SQLite):
  • إعداد n8n EXECUTIONS_PROCESS (الافتراضي: own, main):
  • تشغيل n8n عبر (Docker, npm, n8n cloud, desktop app):
  • نظام التشغيل:

مرحباً @dana_d7545a أهلاً بك في مجتمع N8n.

أقترح عليك الاحتفاظ بـ Data Table، لكن قم بتخزين canonicalUrl و normalizedTitle و processedAt. بالنسبة لمنشورات روابط Reddit، استخدم عنوان URL للمقالة المرتبطة. أزل معاملات التتبع والأجزاء، لكن احتفظ بمعاملات الاستعلام التي تحدد المقالة.

حمّل الصفوف المخزنة الأخيرة مرة واحدة لكل تشغيل. قارن كل عنصر وارد مقابل تلك الصفوف والعناصر المقبولة بالفعل في هذه الدفعة: تخطَّ المطابقات الدقيقة للعنوان الأساسي، ثم تحقق من التشابه في العناوين للعناصر المتبقية. حول العناوين إلى أحرف صغيرة، أزل علامات الترقيم، وقارن التداخل بين الكلمات؛ اضبط الحد الأدنى مقابل أمثلة حقيقية، لأن العناوين المتشابهة يمكن أن تصف قصصاً مختلفة.

مرّر العناصر المقبولة فقط إلى LLM، واحفظها بعد التلخيص الناجح. منع التشغيلات المجدولة من التداخل لتجنب معالجة تشغيلين لنفس العنصر غير المرئي.

إضافة عقدة كود جاهزة للنمط الذي وصفه @Deepankar، حيث أن الجزء القريب المكرر هو المكان الذي عادة ما يصبح فيه الأمر معقدًا.

الإعداد: جدول بيانات “الحصول على الصفوف” باسم Seen (آخر 7 أيام، الأعمدة canonicalUrl و normTitle، إخراج البيانات دائمًا مفعل)، وعناصر RSS/Reddit المدمجة في عقدة باسم New Items. وضع عقدة الكود “التشغيل مرة واحدة لجميع العناصر”:

// عقدة كود n8n، "التشغيل مرة واحدة لجميع العناصر".
// حلقة على العقدة "New Items" (عناصر الخلاصة التي تحتوي على `title` و `link`).
// العقدة السابقة "Seen" = صفوف جدول البيانات التي تحتوي على canonicalUrl و normTitle (آخر ~7 أيام).
const TRACKING = /^(utm_|fbclid$|gclid$|mc_|ref$|ref_src$|igshid$|si$)/i;
const STOP = new Set(['the','a','an','of','to','in','on','for','and','is','are','with','at','by','from','as','its','new']);

function canonicalUrl(u) {
  try {
    const x = new URL(u);
    x.hash = '';
    x.hostname = x.hostname.replace(/^(www|m|amp)\./, '');
    for (const k of [...x.searchParams.keys()]) if (TRACKING.test(k)) x.searchParams.delete(k);
    x.pathname = x.pathname.replace(/\/amp\/?$/, '/').replace(/\/+$/, '') || '/';
    return (x.hostname + x.pathname + (x.search || '')).toLowerCase();
  } catch { return (u || '').toLowerCase(); }
}
function normTitle(t) {
  return (t || '').toLowerCase().replace(/\s[-|:–—]\s[^-|:–—]{2,40}$/, '') // إسقاط " - اسم الموقع"
    .replace(/[^\p{L}\p{N}\s]/gu, ' ').split(/\s+/).filter(w => w && !STOP.has(w)).join(' ');
}
function sameStory(a, b) { // Jaccard على مجموعات الكلمات + حد أدنى على الكلمات المشتركة
  const A = new Set(a.split(' ')), B = new Set(b.split(' '));
  let inter = 0; for (const w of A) if (B.has(w)) inter++;
  return inter >= MIN_SHARED && inter / (A.size + B.size - inter) >= THRESHOLD;
}
const THRESHOLD = 0.6, MIN_SHARED = 4; // اضبط على خلاصاتك الخاصة

const seen = $('Seen').all().map(i => i.json);
const urls = new Set(seen.map(r => r.canonicalUrl));
const titles = seen.map(r => r.normTitle).filter(Boolean);
const out = [];
for (const item of $('New Items').all()) {
  const cu = canonicalUrl(item.json.link), nt = normTitle(item.json.title);
  if (urls.has(cu)) continue;
  if (nt && titles.some(t => sameStory(t, nt))) continue;
  urls.add(cu); titles.push(nt); // يقوم أيضًا بإلغاء التكرار ضمن هذه الدفعة
  out.push({ json: { ...item.json, canonicalUrl: cu, normTitle: nt } });
}
return out;

قمت بتشغيله على عينة صغيرة من الاختراع: تنهار نسخ The Verge و TechCrunch من نفس قصة DevDay إلى واحدة، وتنهار قصة Shopify من TechCrunch و Reuters إلى واحدة، وكلا منشوري “Show HN” المختلفين ينجوان. الحالة الأخيرة هي السبب وراء وجود MIN_SHARED، والعناوين القصيرة تشارك “show hn” و Jaccard البسيط يدمجها.

التعديل: الحلقة الآن تقرأ $('New Items')، شكراً @Deepankar. مع Seen مباشرة في المنحة، كان $input سيكون صفوف الجدول.

لمنشورات روابط Reddit، اربط حقل url للمنشور (المقالة المرتبطة) بـ link، وليس permalink، وإلا فإن نسخة Reddit لن تطابق نسخة RSS أبدًا.

وأعد كتابة canonicalUrl و normTitle إلى جدول البيانات فقط بعد نجاح عقدة LLM، لذلك ستحاول التشغيل الفاشل العنصر مرة أخرى بدلاً من وضع علامة صامتة عليه كما تم عرضه.

يا @dana_d7545a
إذا استخدمت عقدة Code مباشرة بعد Seen، غيّر for (const item of $input.all()) إلى for (const item of $('New Items').all())، مع استبدال New Items بعقدتك السابقة التي تحتوي على عناصر RSS/Reddit المدمجة. وإلا، فسيتم التكرار على صفوف جدول البيانات المخزنة بدلاً من القصص الجديدة.

احتفظ بـ $('Seen').all() للبحث في السجل، وفعّل Always Output Data على Seen حتى لا يوقف جدول فارغ التشغيل الأول، واستخدم normTitle بشكل متسق عند حفظ وقراءة العناوين.