Webhook-/Event-Unterstützung für Discourse AI-Artifact-Key-Value-Updates hinzufügen (oder Sandboxierung für Administratoren deaktivieren)

Ich möchte vorschlagen, AI-Artefakte zu ermöglichen, Webhooks zu senden, whenever die gespeicherten Daten in einem AI-Artefakt aktualisiert werden. Es gibt viele Gründe, warum dies vorteilhaft wäre, aber ich werde versuchen, meinen Fall kurz zu halten.

Viele Organisationen, die Discourse nutzen, setzen auch verschiedene Automatisierungen ein, und die Tools für Automatisierungen werden zunehmend beliebter, einfacher einzurichten und benutzerfreundlich. AI-Artefakte können wertvolle Daten enthalten, die in solchen Automatisierungen sehr effektiv genutzt werden können, mit dem zusätzlichen Vorteil, dass sie bereits im JSON-Format vorliegen!

In meinem Fall verwende ich n8n in einer Docker Compose-Umgebung neben meinem Discourse-Container, und ich nutze es bereits, um eine Vielzahl von Dingen zu automatisieren, einschließlich Vorgängen auf meinen Discourse-Instanzen über ein Docker-Netzwerk. Allerdings ist eine der Discourse-Instanzen, die ich betreue, für eine Bildungsorganisation/ein Bildungsunternehmen gedacht, und AI-Artefakte werden verwendet, um Lernernotizen, Lektionstagebücher und dergleichen zu verfolgen. Solche Daten wären wirklich nützlich, um die Arbeitsabläufe der Lehrkräfte zu verbessern, Zusammenfassungen zu erstellen und so weiter, und zwar über ein Automatisierungstool wie n8n.

Es wäre technisch möglich, Artefakte auf Updates zu prüfen, aber dies könnte leicht zu einer Belastung der Systemressourcen führen und wäre bestenfalls eine Verschwendung von Ressourcen, zudem wäre eine umfangreiche technische Expertise für die Einrichtung erforderlich, die viele Administratoren nicht besitzen.

Unnötig zu erwähnen, wäre dies auch für die Unternehmenskunden von Discourse höchst vorteilhaft.

Eine Workaround-Lösung könnte darin bestehen, dass das JavaScript des Artefakts nach dem Aufruf von window.discourseArtifact.set(...) einen externen Webhook aufruft.

Dies hätte jedoch einige Einschränkungen:

  1. Es ist browserseitig und daher nicht autoritativ.

  2. Es wird nur ausgelöst, wenn das Artefakt-JavaScript im Browser des Benutzers erfolgreich ausgeführt wird.

  3. Es kann durch CORS, Browser-Privatsphäre-Tools, Netzwerk-Blocker oder Sandbox-Einschränkungen beeinflusst werden.

  4. Die Nutzlast kann nicht als von Discourse stammend vertraut werden, es sei denn, es wird eine zusätzliche serverseitige Verifizierung implementiert.

  5. Geheimnisse können nicht sicher in das Artefakt-JavaScript eingebettet werden.

Ein serverseitiges Event/Webhook wäre viel zuverlässiger und sicherer.

Ich bin kein Ruby-Entwickler, daher habe ich mit GPT-5.5 darüber gesprochen, und es hat auch einige interessante Einblicke geliefert…

Basierend auf der aktuellen Webhook-Architektur von Discourse ist die saubere Implementierung wahrscheinlich keine maßgeschneiderte „Rufe diese URL auf“-Funktion innerhalb von AI-Artefakten. Sie sollte als normaler Discourse-Webhook-Event-Typ implementiert werden, unterstützt durch ein normales internes DiscourseEvent.

Optimale Implementierungsform

Ich schlage vor, dass die Discourse-Entwickler dies in zwei Schichten implementieren:

  1. Interne Events, die ausgelöst werden, wenn sich KV-Einträge von Artefakten ändern.

  2. Webhook-Event-Typen, die diese internen Events abonnieren und das bestehende Webhook-Zustellungssystem verwenden.

Dies hält es konsistent mit dem Rest von Discourse. Bestehende Webhooks funktionieren bereits, indem sie interne DiscourseEvents auf WebHook.enqueue_*-Aufrufe in config/initializers/012-web_hook_events.rb abbilden; beispielsweise werden Topic-, Post-, User-, Category-, Tag-, Reviewable-, Notification- und Like-Events auf diese Weise verkabelt.

Der Speicherpfad für Artefakte ist einfach genug: ArtifactKeyValuesController#set findet oder initialisiert einen Key-Value-Eintrag, weist key/value/public zu und speichert ihn; destroy findet den aktuellen Eintrag des Benutzers nach Key und löscht ihn. Das Modell selbst ist AiArtifactKeyValue, gehört zu einem Artefakt und einem Benutzer, hat key, value und public und erzwingt die Eindeutigkeit nach Artefakt/Benutzer/Key.

Vorgeschlagene Event-Namen

Ich würde wahrscheinlich drei spezifische Webhook-Events verwenden:

ai_artifact_key_value_created
ai_artifact_key_value_updated
ai_artifact_key_value_deleted

Alternativ könnte ein einzelnes Event funktionieren:

ai_artifact_key_value_changed

…aber drei Events passen besser zum bestehenden Stil von Discourse. Discourse hat bereits separate Webhook-Event-Namen wie post_created, post_edited, post_destroyed, calendar_event_created, calendar_event_updated und so weiter.

Dateien/Klassen, die sie wahrscheinlich berühren würden

1. Neue Webhook-Event-Typen hinzufügen

WebHookEventType definiert derzeit numerische Konstanten, ein group-Enum und einen TYPES-Hash von Event-Namen zu IDs.

Sie könnten etwas wie Folgendes hinzufügen:

AI_ARTIFACT = 20

enum :group,
  {
    # bestehende Gruppen…
    ai_artifact: 18,
  },
  scopes: false

TYPES = {
  # bestehende Typen…
  ai_artifact_key_value_created: 2001,
  ai_artifact_key_value_updated: 2002,
  ai_artifact_key_value_deleted: 2003,
}

Die genauen IDs liegen bei den Discourse-Maintainern; sie müssen nur Konflikte vermeiden.

Sie würden auch Seed-Einträge in db/fixtures/007_web_hook_event_types.rb hinzufügen, da bestehende Webhook-Event-Typen dort mit IDs, Namen und Gruppen geseedet werden.

Beispiel:

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

Die Admin-Webhook-Oberfläche sollte diese dann automatisch aufnehmen, da Admin::WebHooksController#index bereits gruppierte aktive Event-Typen für die UI serialisiert. Der Event-Typ-Serializer stellt bereits id, name und group zur Verfügung.

2. Die Events ausblenden, wenn Discourse AI deaktiviert ist

WebHookEventType.active blendet bereits plugin-abhängige Webhook-Events aus, wenn deren Funktionen deaktiviert sind, wie z. B. solved, assign, Topic-Voting, Chat und Calendar-Events.

Sie könnten also Folgendes hinzufügen:

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

Sie möchten möglicherweise auch eine artefaktspezifische Einstellung prüfen, falls eine existiert oder hinzugefügt wird.

3. Interne Events vom Modell aus auslösen, nicht nur vom Controller

Obwohl der aktuelle Schreibpfad controllerbasiert ist, ist der robuste Ort wahrscheinlich das Modell, unter Verwendung von 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

Callbacks auf Modellebene würden auch zukünftige Schreibpfade abfangen, nicht nur ArtifactKeyValuesController#set. Der after_commit-Teil ist wichtig, da die Webhook-Zustellung erst nach dem sicheren Commit der Datenbankänderung in die Warteschlange gestellt werden sollte.

Eine Feinheit: Sie sollten wahrscheinlich kein „updated“-Event auslösen, wenn das Artefakt set() mit demselben Wert aufruft und sich nichts tatsächlich ändert. Die Prüfung von previous_changes vermeidet laute Webhooks.

4. Einen Webhook-Payload-Serializer hinzufügen

Discourse-Webhooks generieren Payloads durch Serializer. Die generische Methode WebHook.enqueue_object_hooks kann einen Serializer übernehmen, und WebHook.generate_payload serialisiert das Objekt mit einem System-Benutzer-Guardian.

Sie könnten etwas wie Folgendes hinzufügen:

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

Ich empfehle dringend, den value-Wert standardmäßig nicht einzuschließen. Artefakt-KV-Daten können pro Benutzer privat sein, und das Modell hat eine public-Flagge. Wenn Discourse Werte unterstützen möchte, könnten sie dies explizit machen:

include_value: false
include_public_values: true
include_private_values: false

Aber die sichere v1 sollte Werte wahrscheinlich vollständig weglassen.

5. Interne Events mit der Webhook-Zustellung verknüpfen

Sie könnten Handler zu config/initializers/012-web_hook_events.rb hinzufügen, die bestehenden Mustern entsprechen.

Etwas wie:

%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

Dies würde die bestehende Webhook-Job-Pipeline wiederverwenden. WebHook.enqueue_hooks findet aktive Webhooks für das Event und stellt Jobs::EmitWebHookEvent in die Warteschlange. Der Job übernimmt bereits Zustellung, Wiederholungen, Protokollierung, Header, Signaturen und Admin-Sichtbarkeit.

Die Einbeziehung von category_id und tag_ids ist ein netter Touch, da bestehende Webhook-Jobs nach Kategorie und Tag filtern können. Der Webhook-Job prüft bereits Kategorie- und Tag-Einschränkungen, bevor er sendet. Da ein AI-Artefakt zu einem Post gehört und das Artefakt im Modell zu einem Post gehört, sollte es möglich sein, den Kategorie/Topic-Kontext abzuleiten.

Wie der ausgelieferte Webhook aussehen könnte

Da der Webhook-Body von Discourse event_type als oberste Wurzel verwendet, würde dies wahrscheinlich so aussehen:

{
  "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"
  }
}

Der Event-Name würde in den Headern stehen, konsistent mit der bestehenden Webhook-Zustellung:

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

Diese Header entsprechen der Art und Weise, wie EmitWebHookEvent heute Webhook-Header erstellt.

Die Entwickler müssten wahrscheinlich folgende Entscheidungen treffen:

Sollen Werte eingeschlossen werden?
Meine Stimme: nein, standardmäßig. Vielleicht nur öffentliche Werte zulassen oder die Einschließung von Werten zu einer Admin-Einstellung machen. Private pro-Benutzer-Artefaktdaten sollten die Seite nicht stillschweigend verlassen.

Soll es ein Event oder drei geben?
Drei sind für Webhook-Abonnenten sauberer. Ein Event mit einem action-Feld ist intern einfacher. Der bestehende Discourse-Stil neigt zu mehreren Event-Namen.

Sollten Kategorie/Tag-Filterung gelten?
Ich denke ja. Das Artefakt ist an einen Post/Topic angehängt, daher sind Kategorie- und Tag-Filter sinnvoll.

Soll dies im Discourse-Kern oder im Discourse AI-Plugin implementiert werden?
Das Datenmodell lebt im Discourse AI-Plugin, aber die Webhook-Event-Registrierung lebt im Kern. Da gebündelte Plugins wie solved/chat/calendar bereits Webhook-Event-Typen in der gemeinsamen WebHookEventType-Liste haben, könnte Discourse damit einverstanden sein, AI-Artefakt-Events dort ebenfalls hinzuzufügen. Die active-Methode kann sie deaktivieren, wenn AI deaktiviert ist.

Sollte dies auch ein internes DiscourseEvent auslösen, auch wenn kein Webhook existiert?
Ja. Das gibt Plugin-Autoren einen sauberen Hook-Punkt, unabhängig von Webhooks.

Komplexität

Ich würde dies als mäßig, aber sehr begrenzt einstufen.

Die wahrscheinliche Arbeit besteht darin:

  1. 3 Event-Typ-Konstanten hinzufügen.

  2. 1 Webhook-Gruppe hinzufügen.

  3. 3 Seed-Einträge hinzufügen.

  4. 1 Serializer hinzufügen.

  5. 3 Modell-Callbacks oder servicelevel-Event-Trigger hinzufügen.

  6. Webhook-Initialisierer-Handler hinzufügen.

  7. Spezifikationen für Erstellen/Aktualisieren/Löschen hinzufügen.

  8. Eine Datenschutzentscheidung treffen, ob value eingeschlossen wird.

Das Zustellungssystem, Wiederholungen, Signaturen, Event-Protokoll, Ping/Wiederzustellungs-UI und Admin-Webhook-Konfiguration sind bereits vorhanden. Das Feature dreht sich hauptsächlich darum, Artefakt-KV-Änderungen für dieses bestehende System sichtbar zu machen.

Etwas, das wir nach der Einführung von Workflows, dem neuen Ansatz für Automatisierung in Discourse, erneut aufgreifen werden.

2 „Gefällt mir“

Ooooh, das klingt tatsächlich ziemlich spannend. Habe ich hier auf Meta irgendwelche Informationen dazu verpasst und gibt es einen Zeitplan?