저장된 AI 아티팩트 데이터가 업데이트될 때마다 AI 아티팩트가 웹훅을 보낼 수 있도록 허용하는 기능을 제안하고 싶습니다. 이 기능이 유용한 이유는 많지만, 제 논리를 간결하게 정리해 보겠습니다.
Discourse를 사용하는 많은 조직은 다양한 자동화 도구도 함께 사용하고 있으며, 자동화 도구는 점점 더 인기 있고, 설정이 쉬우며, 초보자 친화적인 경향을 보이고 있습니다. AI 아티팩트에는 이러한 자동화에 매우 효과적으로 활용될 수 있는 귀중한 데이터가 포함될 수 있으며, 이미 JSON 형식이라는 추가적인 장점도 있습니다.
저의 경우, Discourse 컨테이너 옆에 Docker Compose 환경에서 n8n을 사용하고 있으며, Docker 네트워크를 통해 Discourse 인스턴스에서의 다양한 작업을 자동화하는 데 이미 활용하고 있습니다. 그러나 제가 관리하는 Discourse 인스턴스 중 하나는 교육 기관/비즈니스를 위한 것이며, 여기서 AI 아티팩트는 학습자 노트, 수업 일지 등을 추적하는 데 사용됩니다. 이러한 유형의 데이터는 n8n과 같은 자동화 도구를 통해 교사의 워크플로우 개선, 요약 생성 등에 매우 유용할 것입니다.
아티팩트의 업데이트를 폴링(polling)하여 확인하는 것은 기술적으로 가능하지만, 이는 시스템 리소스에 부담을 줄 수 있으며, 적어도 자원 낭비가 될 수 있고, 많은 관리자가 갖추고 있지 않은 상당한 기술적 숙련도가 설정에 필요합니다.
말할 필요도 없이, 이는 Discourse의 엔터프라이즈 고객에게도 매우 유익할 것입니다.
대안으로, 아티팩트의 JavaScript가 window.discourseArtifact.set(...)를 호출한 후 외부 웹훅을 호출하도록 하는 방법이 있을 수 있습니다.
그러나 이 방식에는 몇 가지 한계가 있습니다:
-
브라우저 측에서 이루어지므로 권위적(authoritative)이지 않습니다.
-
아티팩트 JavaScript가 사용자의 브라우저에서 성공적으로 실행될 때만 트리거됩니다.
-
CORS, 브라우저 개인정보 보호 도구, 네트워크 차단기 또는 샌드박스 제약에 영향을 받을 수 있습니다.
-
추가적인 서버 측 검증이 구축되지 않는 한, 페이로드가 Discourse에서 온 것으로 신뢰할 수 없습니다.
-
시크릿(Secrets)을 아티팩트 JavaScript에 안전하게 포함할 수 없습니다.
서버 측 이벤트/웹훅은 훨씬 더 신뢰할 수 있고 안전합니다.
저는 Ruby 개발자가 아니므로, GPT-5.5와 이에 대해 대화해 왔고, 흥미로운 통찰력도 제공받았습니다…
Discourse의 현재 웹훅 아키텍처를 기반으로 볼 때, 깨끗한 구현은 AI 아티팩트 내부에 있는 맞춤형 “이 URL을 호출하세요” 기능이 아닐 것입니다. 이는 정상적인 Discourse 웹훅 이벤트 유형으로 구현되어야 하며, 정상적인 내부 DiscourseEvent를 기반으로 해야 합니다.
최적의 구현 형태
Discourse 개발자들이 두 층위로 구현하는 것을 제안합니다:
-
아티팩트 KV(키-값) 레코드가 변경될 때 방출되는 내부 이벤트.
-
이러한 내부 이벤트에 구독하고 기존 웹훅 전달 시스템을 사용하는 웹훅 이벤트 유형.
이것은 Discourse의 나머지 부분과 일관성을 유지합니다. 기존 웹훅은 이미 config/initializers/012-web_hook_events.rb에서 내부 DiscourseEvent를 WebHook.enqueue_* 호출로 매핑하여 작동합니다. 예를 들어, 토픽, 게시물, 사용자, 카테고리, 태그, 리뷰어블(reviewable), 알림, 좋아요 이벤트 등이 이 방식으로 연결되어 있습니다.
아티팩트 저장 경로는 단순합니다: ArtifactKeyValuesController#set은 키-값 레코드를 찾거나 초기화하고, key/value/public을 할당하여 저장합니다; destroy는 현재 사용자의 키 기반 레코드를 찾아 삭제합니다. 모델 자체는 AiArtifactKeyValue이며, 아티팩트와 사용자에게 속하고, key, value, public을 가지며, 아티팩트/사용자/키에 대한 고유성을 강제합니다.
제안된 이벤트 이름
아마도 세 가지 구체적인 웹훅 이벤트를 사용할 것입니다:
ai_artifact_key_value_created
ai_artifact_key_value_updated
ai_artifact_key_value_deleted
대안으로, 단일 이벤트도 작동할 수 있습니다:
ai_artifact_key_value_changed
…하지만 세 가지 이벤트가 Discourse의 기존 스타일에 더 잘 부합합니다. Discourse에는 이미 post_created, post_edited, post_destroyed, calendar_event_created, calendar_event_updated와 같은 별도의 웹훅 이벤트 이름이 있습니다.
가능성이 높은 파일/클래스
1. 새로운 웹훅 이벤트 유형 추가
WebHookEventType는 현재 숫자 상수, group 열거형(enum), 이벤트 이름에서 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 유지보수자의 판단에 달려 있으며, 충돌을 피하기만 하면 됩니다.
기존 웹훅 이벤트 유형은 ID, 이름, 그룹과 함께 시드되므로 db/fixtures/007_web_hook_event_types.rb에도 시드 엔트리를 추가해야 합니다.
예시:
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
관리자 웹훅 UI는 Admin::WebHooksController#index가 이미 UI를 위해 그룹화된 활성 이벤트 유형을 직렬화하므로 자동으로 이를 가져올 것입니다. 이벤트 유형 직렬화기는 이미 id, name, group을 노출합니다.
2. Discourse AI가 비활성화되어 있을 때 이벤트 숨기기
WebHookEventType.active는 기능이 비활성화되어 있을 때 플러그인 의존 웹훅 이벤트를 이미 숨깁니다. 예를 들어, 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 부분은 데이터베이스 변경이 안전하게 커밋된 후에만 웹훅 전달이 큐에 추가되어야 하므로 중요합니다.
한 가지 세부 사항: 아티팩트가 동일한 값으로 set()을 호출하여 실제로 아무것도 변경되지 않을 때 “updated” 이벤트를 트리거하지 않는 것이 좋습니다. previous_changes를 확인함으로써 노이즈가 많은 웹훅을 방지할 수 있습니다.
4. 웹훅 페이로드 직렬화기 추가
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. 내부 이벤트를 웹훅 전달에 연결
기존 패턴을 반영하여 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.enqueue_hooks는 이벤트에 대한 활성 웹훅을 찾아 Jobs::EmitWebHookEvent를 큐에 추가합니다. 이 작업은 이미 전달, 재시도, 로깅, 헤더, 서명, 관리자 가시성을 처리합니다.
category_id와 tag_ids를 포함하는 것은 기존 웹훅 작업이 카테고리와 태그로 필터링할 수 있기 때문에 좋은 디테일입니다. 웹훅 작업은 전송 전에 이미 카테고리 및 태그 제약 사항을 확인합니다. AI 아티팩트가 게시물에 속하고, 모델에서 아티팩트가 게시물에 속하므로, 카테고리/토픽 컨텍스트를 파생하는 것이 가능해야 합니다.
전달된 웹훅의 모습
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"
}
}
이벤트 이름은 기존 웹훅 전달과 일관되게 헤더에 포함될 것입니다:
X-Discourse-Event-Type: ai_artifact_key_value
X-Discourse-Event: ai_artifact_key_value_updated
X-Discourse-Event-Signature: sha256=...
이러한 헤더는 EmitWebHookEvent가 현재 웹훅 헤더를 구성하는 방식과 일치합니다.
개발자들이 결정해야 할 사항
값을 포함해야 할까요?
저의 투표: 기본적으로 아니요. 아마도 공개 값만 허용하거나, 값 포함을 관리자 설정으로 만드는 것이 좋습니다. 사용자별 비공개 아티팩트 데이터는 사이트에서 조용히 빠져나가지 않아야 합니다.
이벤트가 하나여야 할까요, 셋이여야 할까요?
웹훅 구독자에게는 세 개가 더 깔끔합니다. action 필드가 있는 단일 이벤트는 내부적으로 더 단순합니다. 기존 Discourse 스타일은 여러 이벤트 이름을 선호합니다.
카테고리/태그 필터링이 적용되어야 할까요?
예라고 생각합니다. 아티팩트는 게시물/토픽에 첨부되므로, 카테고리 및 태그 필터는 의미 있습니다.
이것은 Discourse 코어에서 구현해야 할까요, Discourse AI 플러그인에서 구현해야 할까요?
데이터 모델은 Discourse AI 플러그인에 있지만, 웹훅 이벤트 레지스트리는 코어에 있습니다. solved/chat/calendar와 같은 번들 플러그인이 이미 공유된 WebHookEventType 목록에 웹훅 이벤트 유형을 가지고 있으므로, Discourse는 AI 아티팩트 이벤트를 거기에 추가하는 것에 편안할 수 있습니다. active 메서드는 AI가 비활성화되어 있을 때 이를 숨길 수 있습니다.
웹훅이 없더라도 내부 DiscourseEvent를 방출해야 할까요?
예. 이는 웹훅과 별개로 플러그인 개발자에게 깔끔한 후크 포인트를 제공합니다.
복잡성
중간 정도이지만 매우 제한적이라고 표현하겠습니다.
예상되는 작업은 다음과 같습니다:
-
3개의 이벤트 유형 상수 추가.
-
1개의 웹훅 그룹 추가.
-
3개의 시드 레코드 추가.
-
1개의 직렬화기 추가.
-
3개의 모델 콜백 또는 서비스 수준의 이벤트 트리거 추가.
-
웹훅 초기화기 핸들러 추가.
-
생성/업데이트/삭제에 대한 스펙 추가.
-
value가 포함될 것인지에 대한 개인정보 보호 결정 추가.
전달 시스템, 재시도, 서명, 이벤트 로그, ping/재전달 UI, 관리자 웹훅 구성은 이미 존재합니다. 이 기능은 주로 아티팩트 KV 변경 사항을 기존 시스템에 가시적으로 만드는 것에 관한 것입니다.