跳转到内容

领域事件

领域事件是一种解耦机制,让各功能无需直接依赖即可相互通信。功能 A 不再调用功能 B, 而是由功能 A 发布事件、功能 B 订阅该事件。

DomainEventBus 是一个进程内的广播式发布/订阅总线。发布方调用 publish(event), 订阅方消费带类型的 on<T>() 流。每个事件都实现 DomainEvent,并携带 occurredAt。

总线运行在 cc_server 内部。 每个发布方和每个存活的订阅方都在其中。没有 任何客户端能看到这条总线。客户端看到的是服务器重新以 notifications/* JSON-RPC 传输帧发出的一个经过筛选的子集。

没有事件,每一个跨功能的响应都会变成一次导入。通知链路为了知道何时弹出提示, 就不得不导入智能体功能、PR 功能、工单和会议——而这些功能反过来又都必须知道 通知的存在。工单还要导入流水线,好让已完成的工单推进一次运行。依赖图就此缠成 一个死结,共享内核也不再共享。

发布事件打破了这一点。通知链路订阅二十一个事件类,却不导入任何一个引发这些 事件的功能。流水线触发器调度器订阅整条总线,对 PR、工单或会议一无所知。 可观测性汇总运行结果,却不介入派发。

代价是常见的那种:发布方无法知道有没有人在监听,而一个从未被构造出来的监听者 会悄无声息地处于惰性状态。TicketRemoteSyncHandler 就是常被提及的例子:它已声明、 有测试覆盖,却从未接入生产环境。

有一点总线_并不_负责,值得把话说清楚。

审计记录并不是事件流的投影。 默认情况下,每个变更型 RPC 操作都会被审计—— RepoOp.audited 默认为 true,操作只有显式声明 audited: false 才能退出审计, 而这一个刻意之举就写在操作本身上,一目了然。调度器会在调用成功后直接追加一条 “谁在何处做了什么”的记录。并不存在 DomainEventAuditBridge。 ActivityLogPersister 会从 ActivityLogged 事件写入 activity_log 记录;那是与 RPC 审计相互独立的一条数据流。

事件总线_确实_是通知和自动化的传输通道,但它不是负责记账的权威记录系统。

  • 通知。 服务器会把二十一个事件类转成 notifications/* 传输帧,写入每个 工作区各自持久的推送流,并广播给已连接的会话。如果某个帧的 workspace_id 指向的会话用户并非其成员的工作区,该帧会在服务器上被丢弃;客户端再把剩余帧 过滤为它当前查看的那个工作区。
  • 事件驱动自动化。 流水线触发器订阅总线,并自动启动匹配的流水线(见下文)。
  • 生命周期响应。 创建工作区会为其初始化 CEO 与专家智能体、内置流水线模板 以及入门评测套件。智能体运行完成后,会恢复挂起的流水线步骤、关闭任务、向目标 监督者提供输入、唤醒指定的检查者,还可能重新触发团队负责人。PR 合并或空间被 删除会触发工作树垃圾回收。
  • 跨厂商同步。 一个监听五种工单事件的协调器会把本地工单变更推送到已配置的 厂商。

事件覆盖整个产品面。概括来说:

  • 工作区、智能体与仓库——工作区创建、运行完成、仓库注册、技能更新
  • PR——发布、状态变更、合并、审查请求、提及、自己创作的 PR 的跟踪、外部检测到的 PR
  • 消息——消息、空间与空间预配进度
  • 工单——创建、分配、改派、详情、状态以及三种终态结果
  • 任务——针对单次派发运行的有序生命周期帧,便于客户端排序和去重
  • 流水线——仅有运行的终态事件(已完成、已取消、已失败)
  • 可观测性——活动日志条目与预算阈值
  • 身份与成员关系——成员添加、移除、角色变更
  • 日历与会议——认证过期、会议开始与录制结束
  • 机台——接管、回收与意外关闭

记忆、编排、计划文档、工件、审批、工单同步接收、日历刷新、工作树合并、用户创建、 邀请与设备吊销都不是领域事件;参见参考页。

完整的目录——全部 53 个具体事件类、它们的载荷以及消费这些事件的 24 个订阅方—— 参见领域事件参考。那一页是清单,这一页是模型。

成员关系和设备事件看起来是对称的,其实并不是,这一点在你推敲“吊销多快生效”时 很关键。

吊销设备确实会终止它的会话。 服务器监视已配对设备表,只要某台设备离开了 活跃集合,它对应的任何存活会话都会在数秒内被断开。

移除成员并不会关闭套接字。 WorkspaceMemberRemoved 会被消费:LocalRpcServer 和 RemoteRelayHost 会移除该用户已挂载的工作区订阅,RemoteEventForwarder 会使其 成员关系缓存失效,因此不会再推送该工作区的后续帧。真正拦下下一次 RPC 的仍然是 角色门槛,它会在每一次调用时重新解析成员关系——于是他们的下一个请求会被拒绝, 提示 “Not a member of this workspace”。对他们尝试做的任何事,效果都是即时的; 只是这并非靠挂断连接实现。

流水线触发器订阅领域事件,并自动启动匹配的流水线。PipelineTriggerDispatcher 监听整条总线,然后:

  1. 如果事件类型不是它知道如何映射为载荷的类型,就直接短路返回
  2. 在每一个工作区中查找与该事件类型匹配的已启用触发器
  3. 将每个触发器过滤到它自己的工作区,并应用其载荷匹配过滤器
  4. 为每一个通过筛选的触发器启动一次运行

关键约束在第 1 步。总线承载 53 种事件类型;添加触发器选择器只会为以下十四种 启动运行(EventPayloadMapper 的载荷分支): ExternalPrDetected、PullRequestPublished、PullRequestStatusChanged、 PrMerged、MessageReceived、TicketAssigned、 TicketCompleted、TicketFailed、TicketCancelled、 BudgetThresholdCrossed、RepoAdded、MeetingRecordingStopped、SkillUpdated 和 SpaceDeleted。除此之外的任何事件都无法启动流水线。

RemoteEventForwarder 把通知类事件转成传输帧,并通过 RPC 推送到已连接的会话, 在那里由帧映射器将它们转成应用内通知。因此,瘦客户端看到的是服务器事件流的一个 投影,自身并不承担任何执行——这正是瘦客户端拆分的全部 意义所在。

不过,并非每个推送的帧都是通知。任务生命周期流和工单改派会实时转发以驱动界面, 但它们不会被记入通知信息流,也不会弹出任何提示。