إضافة دعم للويب هوك / الأحداث لتحديثات مفاتيح القيم المزدوجة في أدوات الذكاء الاصطناعي لـ Discourse (أو السماح للمسؤولين بتعطيل العزل)

أود اقتراح السماح لقطع الذكاء الاصطناعي (AI artifacts) بإرسال Webhooks في كل مرة يتم فيها تحديث البيانات المخزنة في قطعة ذكاء اصطناعي. هناك أسباب عديدة تجعل هذا الأمر مفيدًا، لكنني سأحاول اختصار حجتي.

العديد من المنظمات التي تستخدم Discourse تستخدم أيضًا أتمتة متنوعة، وتصبح أدوات الأتمتة شائعة بشكل متزايد، وسهلة الإعداد، ومُصممة لتتناسب مع المبتدئين. يمكن أن تحتوي قطع الذكاء الاصطناعي على بيانات ثمينة يمكن استخدامها بفعالية كبيرة في مثل هذه الأتمتة، مع الميزة الإضافية أنها بالفعل بصيغة JSON!

في حالتي، أستخدم n8n في إعداد Docker Compose بجانب حاوية Discourse الخاصة بي، وأستخدمه بالفعل لأتمتة مجموعة واسعة من الأمور، بما في ذلك أشياء على مثيلات Discourse الخاصة بي عبر شبكة Docker. ومع ذلك، فإن إحدى مثيلات Discourse التي أديرها مخصصة لمنظمة تعليمية/عمل، وتُستخدم قطع الذكاء الاصطناعي لتتبع ملاحظات المتعلمين، ومذكرات الدروس، وما إلى ذلك. سيكون هذا النوع من البيانات مفيدًا جدًا في تحسين سير عمل المعلمين، وإنشاء ملخصات، وما إلى ذلك عبر أداة أتمتة مثل n8n.

سيكون من الممكن تقنيًا استجواب القطع بحثًا عن تحديثات، لكن ذلك قد يبدأ بسهولة في إجهاد موارد النظام، وسيكون في أحسن الأحوال هدرًا للموارد، بالإضافة إلى أنه يتطلب مهارات تقنية متقدمة للإعداد، وهو ما لا يمتلكه العديد من المسؤولين.

وبطبيعة الحال، سيكون هذا مفيدًا للغاية أيضًا لعملاء Discourse من المؤسسات.

قد تكون هناك حيلة بديلة تتمثل في استدعاء كود JavaScript الخاص بالقطعة لـ Webhook خارجي بعد استدعاء window.discourseArtifact.set(...).

ومع ذلك، سيكون لهذا بعض القيود:

  1. إنه يعمل على جانب المتصفح وبالتالي ليس مرجعيًا (غير موثوق به كمصدر وحيد للحقيقة).

  2. يعمل فقط إذا تم تنفيذ كود JavaScript الخاص بالقطعة بنجاح في متصفح المستخدم.

  3. يمكن أن يتأثر بـ CORS، وأدوات خصوصية المتصفح، وحواجز الشبكة، أو قيود الصناديق الرملية (sandbox).

  4. لا يمكن الوثوق في الحمولة (payload) بأنها قادمة من Discourse ما لم يتم بناء تحقق إضافي على جانب الخادم.

  5. لا يمكن تضمين الأسرار (Secrets) بشكل آمن في كود JavaScript الخاص بالقطعة.

سيكون حدث Webhook على جانب الخادم أكثر موثوقية وأمانًا بكثير.

[details=“لست مطور Ruby، لذا كنت أتحدث مع GPT-5.5 حول هذا الأمر وقد قدم بعض الأفكار المثيرة للاهتمام أيضًا…”]
بناءً على بنية Webhook الحالية في Discourse، فإن التنفيذ النظيف هو على الأرجح ليس ميزة مخصصة “استدعي هذا الرابط” داخل قطع الذكاء الاصطناعي. يجب تنفيذه كنوع حدث Webhook عادي في Discourse، مدعومًا بـ DiscourseEvent داخلي عادي.

الشكل الأمثل للتنفيذ

أقترح أن يقوم مطورو Discourse بتنفيذه في طبقتين:

  1. أحداث داخلية يتم بثها عند تغيير سجلات KV الخاصة بالقطعة.

  2. أنواع أحداث Webhook تشترك في هذه الأحداث الداخلية وتستخدم نظام تسليم Webhook الحالي.

هذا يبقي الأمر متسقًا مع بقية Discourse. تعمل أحداث Webhook الحالية بالفعل عن طريق ربط الأحداث الداخلية DiscourseEvent باستدعاءات WebHook.enqueue_* في config/initializers/012-web_hook_events.rb؛ على سبيل المثال، يتم توصيل أحداث الموضوعات، والمنشورات، والمستخدمين، والفئات، والوسوم، والقابل للمراجعة، والإشعارات، والإعجابات بهذه الطريقة.

مسار تخزين القطعة بسيط بما يكفي: ArtifactKeyValuesController#set يجد أو يهيئ سجل مفتاح-قيمة، ويعين key/value/public، ويحفظه؛ destroy يجد سجل المستخدم الحالي حسب المفتاح ويحذفه. النموذج نفسه هو AiArtifactKeyValue، ينتمي إلى قطعة ذكاء اصطناعي ومستخدم، ويحتوي على key، value، وpublic، ويفرض التفرد حسب القطعة/المستخدم/المفتاح.

أسماء الأحداث المقترحة

ربما سأستخدم ثلاثة أحداث Webhook محددة:

ai_artifact_key_value_created
ai_artifact_key_value_updated
ai_artifact_key_value_deleted

بدلاً من ذلك، قد يعمل حدث واحد:

ai_artifact_key_value_changed

…لكن ثلاثة أحداث تناسب أسلوب Discourse الحالي بشكل أفضل. لدى Discourse بالفعل أسماء أحداث Webhook منفصلة مثل post_created، post_edited، post_destroyed، calendar_event_created، calendar_event_updated، وما إلى ذلك.

الملفات/الأصناف التي من المرجح أن يلمسها المطورون

1. إضافة أنواع أحداث Webhook جديدة

يحدد WebHookEventType حاليًا ثوابت رقمية، وقيمة group من نوع enum، وقيمة TYPES وهي عبارة عن هاش لاسم الحدث إلى المعرف (ID).

يمكنهم إضافة شيء مثل:

AI_ARTIFACT = 20

enum :group,
  {
    # existing groups...
    ai_artifact: 18,
  },
  scopes: false

TYPES = {
  # existing types...
  ai_artifact_key_value_created: 2001,
  ai_artifact_key_value_updated: 2002,
  ai_artifact_key_value_deleted: 2003,
}

سيكون المعرفات الدقيقة متروكة لمُشرفي Discourse؛ فهم فقط بحاجة إلى تجنب التعارضات.

سيضيفون أيضًا مدخلات في db/fixtures/007_web_hook_event_types.rb، لأن أنواع أحداث Webhook الحالية يتم زرعها هناك مع المعرفات والأسماء والمجموعات.

مثال:

WebHookEventType.seed do |b|
  b.id = WebHookEventType::TYPES[:ai_artifact_key_value_created]
  b.name = "ai_artifact_key_value_created"
  b.group = WebHookEventType.groups[:ai_artifact]
end

WebHookEventType.seed do |b|
  b.id = WebHookEventType::TYPES[:ai_artifact_key_value_updated]
  b.name = "ai_artifact_key_value_updated"
  b.group = WebHookEventType.groups[:ai_artifact]
end

WebHookEventType.seed do |b|
  b.id = WebHookEventType::TYPES[:ai_artifact_key_value_deleted]
  b.name = "ai_artifact_key_value_deleted"
  b.group = WebHookEventType.groups[:ai_artifact]
end

يجب أن يلتقط واجهة مستخدم Webhook الخاصة بالمسؤول هذه تلقائيًا، لأن Admin::WebHooksController#index يقوم بالفعل بتسلسل أنواع الأحداث النشطة المجمعة للواجهة. مُسلسل نوع الحدث يعرض بالفعل id، name، وgroup.

2. إخفاء الأحداث عند تعطيل Discourse AI

WebHookEventType.active يخفي بالفعل أحداث Webhook المعتمدة على الإضافات عندما تكون ميزاتها معطلة، مثل أحداث solved، وassign، والتصويت على الموضوعات، والدردشة، والتقويم.

لذا يمكنهم إضافة:

unless defined?(SiteSetting.discourse_ai_enabled) && SiteSetting.discourse_ai_enabled
  ids_to_exclude.concat(
    [
      TYPES[:ai_artifact_key_value_created],
      TYPES[:ai_artifact_key_value_updated],
      TYPES[:ai_artifact_key_value_deleted],
    ],
  )
end

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

3. بث الأحداث الداخلية من النموذج، وليس فقط من المتحكم

على الرغم من أن مسار الكتابة الحالي يعتمد على المتحكم، إلا أن المكان الأكثر قوة هو على الأرجح النموذج، باستخدام استدعاءات الالتزام (commit callbacks):

class AiArtifactKeyValue < ActiveRecord::Base
  after_create_commit :trigger_created_event
  after_update_commit :trigger_updated_event
  after_destroy_commit :trigger_deleted_event

  private

  def trigger_created_event
    DiscourseEvent.trigger(:ai_artifact_key_value_created, self)
  end

  def trigger_updated_event
    return if previous_changes.slice("value", "public").blank?

    DiscourseEvent.trigger(:ai_artifact_key_value_updated, self)
  end

  def trigger_deleted_event
    DiscourseEvent.trigger(:ai_artifact_key_value_deleted, self)
  end
end

ستلتقط استدعاءات مستوى النموذج مسارات الكتابة المستقبلية أيضًا، وليس فقط ArtifactKeyValuesController#set. جزء after_commit مهم لأن تسليم Webhook يجب أن يتم وضعه في قائمة الانتظار فقط بعد تأكيد تغيير قاعدة البيانات بأمان.

دقة واحدة: يجب عليهم على الأرجح عدم بث حدث “مُحدّث” عندما تستدعي القطعة set() بنفس القيمة ولا يتغير شيء فعليًا. التحقق من previous_changes يتجنب أحداث Webhook الصاخبة.

4. إضافة مُسلسل حمولة Webhook

تولد أحداث Webhook في Discourse حمولات من خلال المسلسلات. يمكن للطريقة العامة WebHook.enqueue_object_hooks أن تأخذ مُسلسلًا، وتقوم WebHook.generate_payload بتسلسل الكائن مع حارس مستخدم النظام.

يمكنهم إضافة شيء مثل:

class WebHookAiArtifactKeyValueSerializer < ApplicationSerializer
  attributes :id,
             :ai_artifact_id,
             :post_id,
             :topic_id,
             :user_id,
             :key,
             :public,
             :value_included,
             :created_at,
             :updated_at

  def post_id
    object.ai_artifact.post_id
  end

  def topic_id
    object.ai_artifact.post.topic_id
  end

  def value_included
    false
  end
end

أوصي بشدة ** بعدم تضمين value بشكل افتراضي**. يمكن أن تكون بيانات KV الخاصة بالقطعة خاصة لكل مستخدم، ولدى النموذج علم public. إذا أراد Discourse دعم القيم، فيمكنهم جعل ذلك صريحًا:

include_value: false
include_public_values: true
include_private_values: false

لكن الإصدار الآمن v1 يجب على الأرجح أن يستثني القيم تمامًا.

5. ربط الأحداث الداخلية بتسليم Webhook

يمكنهم إضافة معالجات إلى config/initializers/012-web_hook_events.rb، مع عكس الأنماط الحالية.

شيء مثل:

%i[
  ai_artifact_key_value_created
  ai_artifact_key_value_updated
  ai_artifact_key_value_deleted
].each do |event|
  DiscourseEvent.on(event) do |key_value|
    artifact = key_value.ai_artifact
    post = artifact.post
    topic = post.topic

    payload =
      WebHook.generate_payload(
        :ai_artifact_key_value,
        key_value,
        WebHookAiArtifactKeyValueSerializer
      )

    WebHook.enqueue_hooks(
      :ai_artifact_key_value,
      event,
      id: key_value.id,
      category_id: topic&.category_id,
      tag_ids: topic&.tags&.pluck(:id),
      payload: payload
    )
  end
end

سيؤدي ذلك إلى إعادة استخدام خط أنابيب وظيفة Webhook الحالية. تجد WebHook.enqueue_hooks أحداث Webhook النشطة للحدث وتضع Jobs::EmitWebHookEvent في قائمة الانتظار. تتعامل الوظيفة بالفعل مع التسليم، وإعادة المحاولة، والتسجيل، والترويسات، والتوقيعات، وإمكانية رؤية المسؤول.

يُعد تضمين category_id وtag_ids لمسة لطيفة لأن وظائف Webhook الحالية يمكنها التصفية حسب الفئة والوسم. تتحقق وظيفة Webhook بالفعل من قيود الفئة والوسم قبل الإرسال. نظرًا لأن قطعة الذكاء الاصطناعي تنتمي إلى منشور، وتنتمي القطعة إلى منشور في النموذج، فيجب أن يكون من الممكن اشتقاق سياق الفئة/الموضوع.

كيف قد تبدو حمولة Webhook المُسلّمة

نظرًا لأن جسم Webhook في Discourse يستخدم event_type كجذر من أعلى مستوى، فسيبدو هذا على الأرجح كالتالي:

{
  "ai_artifact_key_value": {
    "id": 456,
    "ai_artifact_id": 123,
    "post_id": 789,
    "topic_id": 321,
    "user_id": 42,
    "key": "score",
    "public": true,
    "value_included": false,
    "created_at": "2026-05-26T12:00:00Z",
    "updated_at": "2026-05-26T12:05:00Z"
  }
}

سيكون اسم الحدث في الترويسات، متسقًا مع تسليم Webhook الحالي:

X-Discourse-Event-Type: ai_artifact_key_value
X-Discourse-Event: ai_artifact_key_value_updated
X-Discourse-Event-Signature: sha256=...

تتوافق هذه الترويسات مع الطريقة التي يبني بها EmitWebHookEvent ترويسات Webhook اليوم.

ما الذي سيحتاج المطورون على الأرجح إلى تحديده:

هل يجب تضمين القيم؟
صوتي: لا بشكل افتراضي. ربما السماح فقط بالقيم العامة، أو جعل تضمين القيمة إعدادًا للمسؤول. لا يجب أن تغادر بيانات القطعة الخاصة لكل مستخدم الموقع بصمت.

هل يجب أن يكون هناك حدث واحد أم ثلاثة؟
ثلاثة أنظف للمشتركين في Webhook. حدث واحد مع حقل action أبسط داخليًا. يميل أسلوب Discourse الحالي نحو أسماء أحداث متعددة.

هل يجب أن تنطبق التصفية حسب الفئة/الوسم؟
أعتقد نعم. القطعة مرفقة بمنشور/موضوع، لذا فإن مرشحات الفئة والوسم ذات معنى.

هل يجب تنفيذ هذا في جوهر Discourse أو في إضافة Discourse AI؟
نموذج البيانات يعيش في إضافة Discourse AI، لكن سجل أحداث Webhook يعيش في الجوهر. نظرًا لأن الإضافات المرفقة مثل solved/chat/calendar لديها بالفعل أنواع أحداث Webhook في قائمة WebHookEventType المشتركة، فقد يكون Discourse مرتاحًا لإضافة أحداث قطع الذكاء الاصطناعي هناك أيضًا. يمكن للطريقة active إخفائها عند تعطيل الذكاء الاصطناعي.

هل يجب أن يبث هذا أيضًا حدث DiscourseEvent داخلي حتى لو لم يكن هناك Webhook؟
نعم. هذا يمنح مؤلفي الإضافات نقطة ربط نظيفة حتى بغض النظر عن أحداث Webhook.

التعقيد

سأصنفه على أنه متوسط ولكنه محدود للغاية.

العمل المحتمل هو:

  1. إضافة 3 ثوابت لنوع الحدث.

  2. إضافة 1 مجموعة Webhook.

  3. إضافة 3 سجلات للزرع (seed).

  4. إضافة 1 مُسلسل.

  5. إضافة 3 استدعاءات نموذجية أو محفزات أحداث على مستوى الخدمة.

  6. إضافة معالجات مبدئية لـ Webhook.

  7. إضافة اختبارات لـ create/update/delete.

  8. إضافة قرار خصوصي حول ما إذا كان سيتم تضمين value.

نظام التسليم، وإعادة المحاولة، والتوقيعات، وسجل الأحداث، وواجهة المستخدم الخاصة بـ ping/إعادة التسليم، وإعدادات Webhook الخاصة بالمسؤول موجودة بالفعل. الميزة تتعلق في الغالب بجعل طفرات KV الخاصة بالقطعة مرئية لهذا النظام الحالي.

شيء نعود إليه بعد إطلاق Workflows، النهج الجديد للأتمتة في Discourse.

إعجابَين (2)

واو، هذا يبدو مثيرًا للاهتمام حقًا. هل فاتني أي معلومات حول هذا الأمر هنا على ميتا، وهل هناك جدول زمني محدد؟