# 为 Discourse AI 的 artifact 键值更新添加 webhook/事件支持（或允许管理员禁用沙箱）

**URL:** https://meta.discourse.org/t/add-webhook-event-support-for-discourse-ai-artifact-key-value-updates-or-allow-admins-to-disable-sandboxing/403865
**Category:** Feature
**Tags:** webhooks, ai, ai-artifacts
**Created:** [2026 年5 月 26 日 18:32 UTC](https://meta.discourse.org/t/add-webhook-event-support-for-discourse-ai-artifact-key-value-updates-or-allow-admins-to-disable-sandboxing/403865 "2026-05-26T18:32:38Z")
**Posts on this page:** 1
**Showing post:** 1

<div class="post-metadata">

### Author: ![MachineScholar](https://sea3.discourse-cdn.com/meta/user_avatar/meta.discourse.org/machinescholar/32/424719_2.png) [@MachineScholar](https://meta.discourse.org/u/MachineScholar)
#### Post date: [2026 年5 月 26 日 18:32 UTC](https://meta.discourse.org/t/add-webhook-event-support-for-discourse-ai-artifact-key-value-updates-or-allow-admins-to-disable-sandboxing/403865/1 "2026-05-26T18:32:38Z")

</div>

我想提议允许 AI 工件在存储的数据更新时发送 Webhook。这样做有很多好处，但我会尽量简明扼要地阐述我的理由。

许多使用 Discourse 的组织也同时使用各种自动化工具，这些工具正变得越来越流行、易于设置且对新手友好。AI 工件可以包含宝贵数据，这些数据可以非常有效地用于此类自动化，而且额外的优势是它们已经是 JSON 格式！

就我而言，我在 Docker Compose 环境中使用 n8n，并将其与 Discourse 容器并列部署。我已经利用它自动化了各种事务，包括通过 Docker 网络对 Discourse 实例进行操作。然而，我维护的其中一个 Discourse 实例是为一个教育组织/企业服务的，其中使用 AI 工件来追踪学习者的笔记、课程日志等。这类数据对于通过 n8n 等自动化工具改善教师工作流程、生成摘要等将非常有益。

从技术上讲，轮询工件以获取更新是可行的，但这很容易开始消耗系统资源，充其量也是一种资源浪费，而且需要 extensive 的技术技能来设置，而许多管理员并不具备这些技能。

**不用说，这对 Discourse 的企业客户也将非常有益。**

一种变通方法可能是让工件的 JavaScript 在调用 `window.discourseArtifact.set(...)` 后调用外部 Webhook。

然而，这种方法存在一些局限性：

1. 它运行在浏览器端，因此不具备权威性。

2. 仅当工件 JavaScript 在用户浏览器中成功执行时才会触发。

3. 可能受到 CORS、浏览器隐私工具、网络拦截器或沙箱限制的影响。

4. 除非构建额外的服务器端验证，否则无法信任载荷确实来自 Discourse。

5. 无法安全地将密钥嵌入到工件的 JavaScript 中。

服务器端事件/Webhook 将更加可靠和安全。

[details=“我不是 Ruby 开发者，所以我一直与 GPT-5.5 讨论此事，它也提供了一些有趣的见解……”]  
基于 Discourse 当前的 Webhook 架构，最干净的实现可能 **不是** 在 AI 工件内部定制一个“调用此 URL”的功能。它应该被实现为标准的 Discourse Webhook 事件类型，并由标准的内部 `DiscourseEvent` 提供支持。

## 最佳实现形态

我建议 Discourse 开发人员分两层来实现：

1. **内部事件** ：在工件 KV 记录变更时发出。

2. **Webhook 事件类型** ：订阅这些内部事件，并使用现有的 Webhook 投递系统。

这样可以保持与 Discourse 其余部分的一致性。现有的 Webhook 通过将内部 `DiscourseEvent` 映射到 `config/initializers/012-web_hook_events.rb` 中的 `WebHook.enqueue_*` 调用来工作；例如，主题、帖子、用户、分类、标签、可审核项、通知和点赞事件都是这样连接的。

工件存储路径非常简单：`ArtifactKeyValuesController#set` 查找或初始化键值记录，分配 `key/value/public` 并保存；`destroy` 根据键查找当前用户的记录并销毁它。模型本身是 `AiArtifactKeyValue`，属于一个工件和一个用户，包含 `key`、`value` 和 `public` 字段，并强制执行按工件/用户/键的唯一性。

## 建议的事件名称

我可能会使用三个特定的 Webhook 事件：

```ruby
ai_artifact_key_value_created
ai_artifact_key_value_updated
ai_artifact_key_value_deleted

```

或者，单个事件也可以工作：

```ruby
ai_artifact_key_value_changed

```

……但三个事件更符合 Discourse 现有的风格。Discourse 已经有单独的 Webhook 事件名称，如 `post_created`、`post_edited`、`post_destroyed`、`calendar_event_created`、`calendar_event_updated` 等等。

## 它们可能涉及的文件/类

### 1. 添加新的 Webhook 事件类型

`WebHookEventType` 目前定义数值常量、一个 `group` 枚举以及一个从事件名称到 ID 的 `TYPES` 哈希。

他们可以添加类似以下内容：

```ruby
AI_ARTIFACT = 20

enum :group,
  {
    # 现有组...
    ai_artifact: 18,
  },
  scopes: false

TYPES = {
  # 现有类型...
  ai_artifact_key_value_created: 2001,
  ai_artifact_key_value_updated: 2002,
  ai_artifact_key_value_deleted: 2003,
}

```

具体的 ID 由 Discourse 维护者决定；他们只需要避免冲突即可。

他们还需要在 `db/fixtures/007_web_hook_event_types.rb` 中添加种子条目，因为现有的 Webhook 事件类型是在那里使用 ID、名称和组进行种子的。

示例：

```ruby
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、主题投票、聊天和日历事件。

因此，他们可以添加：

```ruby
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. 从模型而非仅仅是控制器发出内部事件

虽然当前的写入路径是基于控制器的，但更稳健的位置可能是模型，使用提交回调：

```ruby
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 载荷序列化器

Discourse Webhook 通过序列化器生成载荷。通用的 `WebHook.enqueue_object_hooks` 方法可以接受一个序列化器，而 `WebHook.generate_payload` 使用系统用户守卫对对象进行序列化。

他们可以添加类似以下内容：

```ruby
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 希望支持值，他们可以明确指定：

```ruby
include_value: false
include_public_values: true
include_private_values: false

```

但安全的 v1 版本可能应该完全省略值。

### 5. 将内部事件连接到 Webhook 投递

他们可以在 `config/initializers/012-web_hook_events.rb` 中添加处理程序，模仿现有模式。

类似以下内容：

```ruby
%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 作业在发送前已经检查了分类和标签约束。由于 AI 工件属于一个帖子，而模型中工件也属于一个帖子，因此推导分类/主题上下文应该是可行的。

## 投递的 Webhook 可能长什么样

由于 Discourse 的 Webhook 正文使用 `event_type` 作为顶层根，这可能看起来像：

```json
{
  "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 投递一致：

```plaintext
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）已经在共享的 `WebHookEventType` 列表中拥有 Webhook 事件类型，Discourse 可能也愿意在此处添加 AI 工件事件。`active` 方法可以在 AI 禁用时隐藏它们。

**即使不存在 Webhook，是否也应发出内部 `DiscourseEvent`？**  
是的。这为插件作者提供了一个干净的钩子点，即使与 Webhook 无关。

## 复杂度

我会将其描述为 **中等但非常集中** 。

可能的工作包括：

1. 添加 3 个事件类型常量。

2. 添加 1 个 Webhook 组。

3. 添加 3 条种子记录。

4. 添加 1 个序列化器。

5. 添加 3 个模型回调或服务级事件触发器。

6. 添加 Webhook 初始化器处理程序。

7. 为创建/更新/删除添加测试。

8. 围绕是否包含 `value` 做出隐私决策。

投递系统、重试、签名、事件日志、ping/重投界面以及管理员 Webhook 配置已经存在。该功能主要是让工件 KV 变更对现有系统可见。

---

_[View the full topic](https://meta.discourse.org/t/add-webhook-event-support-for-discourse-ai-artifact-key-value-updates-or-allow-admins-to-disable-sandboxing/403865)._
