领域事件
什么是领域事件
标题为“什么是领域事件”的部分领域事件是一种解耦机制,让各功能无需直接依赖即可相互通信。功能 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 步。总线承载 53 种事件类型;添加触发器选择器只会为以下十四种
启动运行(EventPayloadMapper 的载荷分支):
ExternalPrDetected、PullRequestPublished、PullRequestStatusChanged、
PrMerged、MessageReceived、TicketAssigned、
TicketCompleted、TicketFailed、TicketCancelled、
BudgetThresholdCrossed、RepoAdded、MeetingRecordingStopped、SkillUpdated
和 SpaceDeleted。除此之外的任何事件都无法启动流水线。
客户端如何看到这些
标题为“客户端如何看到这些”的部分RemoteEventForwarder 把通知类事件转成传输帧,并通过 RPC 推送到已连接的会话,
在那里由帧映射器将它们转成应用内通知。因此,瘦客户端看到的是服务器事件流的一个
投影,自身并不承担任何执行——这正是瘦客户端拆分的全部
意义所在。
不过,并非每个推送的帧都是通知。任务生命周期流和工单改派会实时转发以驱动界面, 但它们不会被记入通知信息流,也不会弹出任何提示。