2026-09-27·by Sijie Wang#standmeet#architecture#design#events

event-model

事件模型:字符串类型 + JSON,只声明一次

上级:events

状态: 已在 v0.1.76 发布(2026-09-27)—— 设计与落地记录见 StandMeet 仓库的 docs/design/event-bus-outbox-webhooks.md。

事件 = 类型字符串 + JSON。底座没有“每种事件一个 Go 类型”,每种事件类型都由拥有它的域以数据形式只声明一次。

event = { id (uuid), seq, type (string), owner_id, subject (string URI or id),
          occurred_at, data (json) }
  • id 是 gen_random_uuid()。seq 是自增列,只用来给领取排序,没有游标读它(relay-claims-rows-not-cursor)。
  • outbox 行还带着 relay 的记账字段:fanned_out_at、fanout(每个订阅方拿到了哪个任务)、relay_failures、poisoned_at、last_error。

类型命名

  • type 是点分字符串:名词(本身可以带点)+ 动词。例:corpus.note.changed、access_request.created、microsite.build.settled。
  • 底座只认字符串和 JSON。这和 seam 是同一条规则:seam 是名字 + verb + JSON,从不是 typed 契约。
  • 事件从哪来(触发器还是 Record)不体现在形状上:见 two-sources-of-events。

声明是数据

字段含义
Type点分类型字符串
Description给人看的说明
Subject主体的模式,如 <genre>://<note path> 或 webhook://<endpoint id>
Exposure谁能收到:Internal 或 Webhook
  • 各域声明自己的类型,经 facade 暴露;组合根在 cmd/server/wire/periodic.go 里每个来源一行收集。
  • 后台 UI 和 MCP 的 webhooks.event_types 读的就是收集出的列表,没有手工维护的清单。
  • Record 一个没声明的类型 → 返回 ErrUndeclaredType,不是静默写进去。订阅的通配匹配不到任何已声明类型 → 启动失败。
  • 注册表 UT(cmd/server/wire/event_types_test.go)逐条检查声明:只声明一次、点分的名词加动词命名、有描述、有主体模式、exposure 是有意选的。

Exposure 默认 Internal

取值谁能收到
Internal (零值)只有进程内订阅方
Webhook还可以经 webhooks 出实例

新事件类型不会意外出实例。这回应了生成式界面的误暴露风险:忘了归类,绝不默认公开。类型停在零值却没被有意列为 internal,注册表 UT 就失败。

已声明的 39 个类型

全部 thin,全部 Webhook 可见,默认都不订阅。

来源类型
corpus(触发器)corpus.note.changed
corpus(writings)writing.published、writing.unpublished
owner:webhookwebhook.test
accessaccess_request.created、.approved、.status_changed;code.issued、.revoked、.redeemed;api_key.issued、.revoked
conversationconversation.started、.message、.pruned;ghost.accepted
owner:预约booking.created、.cancelled、.rescheduled
owner:微站microsite.build.settled、page.promoted_live、.rolled_back、.unpublished、microsite.store.doc_inserted
owner:账户与 vaultowner.login、.email_changed、.recovery_requested;gas.exhausted、.refilled;vault.imported
statsinstance.upgrade_requested
securityip_ban.added
block 与 supplier(block 模型)block.installed、.failed;supplier.connected、.disconnected、.activated
求职闭环application.committed、jobs.fetched
  • instance.upgrade_requested 在自己的事务里提交,而且只在 updater 存在时才记。
  • owner.login 在自己的事务里提交(登录不写行,会话在 Redis 里)。用恢复短语登录也会记它,和被用掉的短语一起提交。
  • jobs.fetched 每个抓取成功的源记一条。

payload:thin

payload 只带类型、主体和 id(Stripe thin events 的形状)。消费方回它本来就用的 API 取资源。出实例的数据最少,事实只在生产方一处。

核心类图

  • 底座只认 type 字符串和 JSON。
  • Exposure 默认 Internal,忘了归类的事件出不了实例。
  • embed 范围复用唯一的 ACL 判定 entity.AllowsCorpusEntry。组合根把 access 域的回答交给 owner 域的扇出,所以 infra 不 import 任何域。

相关:relay-claims-rows-not-cursor(行怎么变成任务)、queue-behind-ports(接口)、webhooks。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →