我想提议允许 AI 工件在存储的数据更新时发送 Webhook。这样做有很多好处,但我会尽量简明扼要地阐述我的理由。
许多使用 Discourse 的组织也同时使用各种自动化工具,这些工具正变得越来越流行、易于设置且对新手友好。AI 工件可以包含宝贵数据,这些数据可以非常有效地用于此类自动化,而且额外的优势是它们已经是 JSON 格式!
就我而言,我在 Docker Compose 环境中使用 n8n,并将其与 Discourse 容器并列部署。我已经利用它自动化了各种事务,包括通过 Docker 网络对 Discourse 实例进行操作。然而,我维护的其中一个 Discourse 实例是为一个教育组织/企业服务的,其中使用 AI 工件来追踪学习者的笔记、课程日志等。这类数据对于通过 n8n 等自动化工具改善教师工作流程、生成摘要等将非常有益。
从技术上讲,轮询工件以获取更新是可行的,但这很容易开始消耗系统资源,充其量也是一种资源浪费,而且需要 extensive 的技术技能来设置,而许多管理员并不具备这些技能。
不用说,这对 Discourse 的企业客户也将非常有益。
一种变通方法可能是让工件的 JavaScript 在调用 window.discourseArtifact.set(...) 后调用外部 Webhook。
然而,这种方法存在一些局限性:
-
它运行在浏览器端,因此不具备权威性。
-
仅当工件 JavaScript 在用户浏览器中成功执行时才会触发。
-
可能受到 CORS、浏览器隐私工具、网络拦截器或沙箱限制的影响。
-
除非构建额外的服务器端验证,否则无法信任载荷确实来自 Discourse。
-
无法安全地将密钥嵌入到工件的 JavaScript 中。
服务器端事件/Webhook 将更加可靠和安全。
[details=“我不是 Ruby 开发者,所以我一直与 GPT-5.5 讨论此事,它也提供了一些有趣的见解……”]
基于 Discourse 当前的 Webhook 架构,最干净的实现可能不是在 AI 工件内部定制一个“调用此 URL”的功能。它应该被实现为标准的 Discourse Webhook 事件类型,并由标准的内部 DiscourseEvent 提供支持。
最佳实现形态
我建议 Discourse 开发人员分两层来实现:
-
内部事件:在工件 KV 记录变更时发出。
-
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 事件:
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 枚举以及一个从事件名称到 ID 的 TYPES 哈希。
他们可以添加类似以下内容:
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、名称和组进行种子的。
示例:
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. 从模型而非仅仅是控制器发出内部事件
虽然当前的写入路径是基于控制器的,但更稳健的位置可能是模型,使用提交回调:
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 使用系统用户守卫对对象进行序列化。
他们可以添加类似以下内容:
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 作业在发送前已经检查了分类和标签约束。由于 AI 工件属于一个帖子,而模型中工件也属于一个帖子,因此推导分类/主题上下文应该是可行的。
投递的 Webhook 可能长什么样
由于 Discourse 的 Webhook 正文使用 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)已经在共享的 WebHookEventType 列表中拥有 Webhook 事件类型,Discourse 可能也愿意在此处添加 AI 工件事件。active 方法可以在 AI 禁用时隐藏它们。
即使不存在 Webhook,是否也应发出内部 DiscourseEvent?
是的。这为插件作者提供了一个干净的钩子点,即使与 Webhook 无关。
复杂度
我会将其描述为中等但非常集中。
可能的工作包括:
-
添加 3 个事件类型常量。
-
添加 1 个 Webhook 组。
-
添加 3 条种子记录。
-
添加 1 个序列化器。
-
添加 3 个模型回调或服务级事件触发器。
-
添加 Webhook 初始化器处理程序。
-
为创建/更新/删除添加测试。
-
围绕是否包含
value做出隐私决策。
投递系统、重试、签名、事件日志、ping/重投界面以及管理员 Webhook 配置已经存在。该功能主要是让工件 KV 变更对现有系统可见。