跳转到内容

领域事件

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

总线运行在 cc_server 内部。 每个发布方和每个存活的订阅方都在其中。没有任何客户端能看到这条总线——服务器会把一个经过筛选的子集作为 notifications/* JSON-RPC 帧转发出去,并按订阅方所属的工作区成员关系逐帧过滤。

指标 数量
具体事件类 54
源文件(packages/cc_domain/lib/core/domain/events/) 14(外加 domain_event_bus.dart)
从不直接发布的抽象基类 1(TaskLifecycleEvent,一个密封层级)

下文目录中的每个事件都位于那一个目录里;其他任何地方都没有声明领域事件。

事件 载荷 触发时机
WorkspaceCreated workspaceId 创建工作区
AgentRunCompleted agentId, runId?, workspaceId?, conversationId? 智能体完成一次运行(无论成功还是失败)
AgentAwaitingInput workspaceId, kind, summary, spaceId?, conversationId?, agentId?, agentName? 智能体开始等待人工处理:一项批准、一个 ask_user 问题,或一次因凭据而挂起的运行(kind 为 approval、question 或 credential)
RepoAdded repoId, path, workspaceId 注册了一个仓库;会触发后台代码索引
SkillUpdated workspaceId, slug, origin, computedHash, scanVerdict? 安装或更新了一个技能,并携带其供应链扫描结论

第一块是 PR 自身的生命周期。第二块是自己创作的 PR 的跟踪:关于你打开的 PR 的信号,每条都携带 forUserId,好让轮询把它们路由给相关的人,而不是广播出去。

事件 载荷 触发时机
PullRequestPublished prId, workspaceId, repoOwner, repoName 智能体打开一个 PR
PullRequestStatusChanged status, workspaceId, prId?, repoFullName?, prNumber? PR 被合并/关闭/打开/重新打开/批准
PrMerged prId, workspaceId, agentId PR 被合并——一个仅针对合并的窄信号,保留用于分析/通知
PrReviewRequested workspaceId, repoOwner, repoName, prNumber, prTitle GitHub 请求服务器用户进行审查(通知轮询,review_requested)
PrMentioned workspaceId, repoOwner, repoName, prNumber, prTitle 服务器用户在某个 PR 上被提及(通知轮询,mention)
ExternalPrMerged workspaceId, repoOwner, repoName, prNumber, prTitle, mergedByLogin? GitHub 通知轮询检测到外部跟踪的(非智能体)PR 被合并;映射到 prMerged 通知类别
ExternalPrDetected repoOwner, repoName, prNumber, prTitle, author, workspaceId? 轮询发现非智能体 PR——仅用于流水线(驱动 external_pr_welcome),不会产生通知
PrHeadChanged repoOwner, repoName, prNumber, prTitle, previousHeadSha, headSha, workspaceId PR 的 head SHA 发生变化——作者进行了推送
ReviewBecameStale workspaceId, spaceId, repoOwner, repoName, prNumber, prTitle, reviewedHeadSha, headSha 已完成的 AI 审查不再描述它所审查的那个 PR。仅当被替换的提交上存在审查时才会触发,因此它始终是一个信号,而不是每次推送都来一次提醒

这六个事件都携带 workspaceId、repoOwner、repoName、prNumber、prTitle,以及可选的 forUserId。

事件 附加载荷 触发时机
PrMergeReadinessChanged ready, reason 你创作的 PR 变为可合并,或不再可合并
PrReviewDecisionChanged decision, reviewersRemaining, approverLogin? 审查者批准、请求修改,或某次批准被撤销
PrChecksStatusChanged failing, failingCheckName?, failingCheckUrl? CI 变红,或恢复
PrCommentMentioned commentId, authorLogin, bodyPreview, isReviewComment, threadId?, path?, line? 有人在 PR 评论或审查评论中提到了你
PrThreadReplied commentId, authorLogin, bodyPreview, threadId?, path?, line? 有人在你参与的审查线程中回复
PrThreadResolved threadId, commentId?, path?, line? 有人解决了你参与的审查线程
事件 载荷 触发时机
MessageReceived spaceId, messageId, senderName, contentPreview, isAgentMessage, workspaceId?, mentions, requestedByUserId?, senderUserId? 收到新消息
SpaceCreated spaceId, workspaceId 创建了空间
SpaceDeleted spaceId, workspaceId 删除了空间
SpaceProvisioningChanged workspaceId, spaceId, status, step? 空间的工作区进入预配、到达某个步骤(克隆仓库、检出 PR、设置智能体),或转为就绪/失败——聊天桥接会在其任务卡上描述这一过程
事件 载荷 触发时机
TicketCreated ticketId, workspaceId 创建了工单
TicketAssigned ticketId, ticketTitle, ticketBody?, ticketUrl?, assignedAgentId?, assigneeType, assignedTeamId?, workspaceId 工单分配给某个主体或团队
TicketCompleted ticketId 工作完成(终态)
TicketFailed ticketId, errorMessage 智能体失败(终态)
TicketCancelled ticketId 工单被取消(终态)
TicketStatusChanged ticketId, workspaceId, from, to 任何状态变更
TicketReassigned ticketId, workspaceId, fromAgentId?, toAgentId? 工单被改派
TicketDetailsUpdated ticketId, workspaceId 标题、描述或优先级发生变化

统一的任务生命周期流——一次派发的智能体运行,以其运行日志 id 为键。每个事件都携带 taskId、按任务单调递增的 seq、workspaceId?、agentId? 和一个 phase;下表只列出附加字段。

事件 载荷 触发时机
TaskDispatched — 任务被交给某个后端执行
TaskRunning — 任务开始产生输出
TaskWaitingLocalDirectory lockedPath, holderTaskId? 任务被停驻——它需要的磁盘路径锁由另一个任务持有
TaskProgress note? 运行中途的进度检查点
TaskMessage messageType, content 流式消息(text/thinking/tool_use/tool_result/error);在 progress 阶段下报告
TaskCompleted summary? 任务成功完成
TaskFailed errorMessage 任务以错误结束
TaskCancelled — 任务在完成前被取消

TaskPhase 枚举还有一个 queued 成员,但没有任何东西发布排队事件——不存在 TaskQueued 类。一个任务首个可观察到的帧是 TaskDispatched。

事件 载荷 触发时机
PipelineRunCompleted workspaceId, pipelineRunId, templateId 运行完成
PipelineRunCancelled workspaceId, pipelineRunId, templateId 运行被取消(由用户或系统)——一次刻意的停止,而不是错误
PipelineRunFailed workspaceId, pipelineRunId, templateId, errorMessage 运行失败

只有终态转换会被发布。不存在 PipelineRunStarted,也没有按步骤的事件:某个步骤的进度通过该次运行自己的记录行,以及它派发的那个智能体的任务生命周期流到达客户端,而不是经由总线。

事件 载荷 触发时机
ActivityLogged id, actorType, actorId?, action, entityType, entityId?, details?, workspaceId?, runId? 创建了审计跟踪条目
BudgetThresholdCrossed scopeType, scopeId, spentCents, budgetCents, isHardStop 支出超过阈值
事件 载荷 触发时机
WorkspaceMemberAdded workspaceId, userId, role 用户加入工作区(兑换邀请或由管理员添加)
WorkspaceMemberRemoved workspaceId, userId 成员被移除——该用户以该工作区为作用域的存活会话必须立即重新检查访问权限
WorkspaceMemberRoleChanged workspaceId, userId, role 成员的角色发生变化

成员关系是总线上身份相关的全部内容。用户创建和设备吊销确实会发生,但它们不作为领域事件发布——吊销是直接针对存活会话强制执行的,而不是经由总线。

事件 载荷 触发时机
CalendarAuthExpired workspaceId, accountEmail 已连接账号的 OAuth 刷新令牌永久失效(每次断开过程只发布一次)
MeetingStartingSoon workspaceId, eventId, title, startTime, meetingUrl? 某个日历事件将在配置的提前量窗口内开始
MeetingRecordingStopped workspaceId, meetingId, title, userNotes, transcript, summaryInstructions? 会议录制结束并触发内置的 meeting_summary 流水线
事件 载荷 触发时机
RigClosedEvent workspaceId, rigId, reason 机台消失,并带上其关闭原因;消费方只对 backendFailure 采取动作——其他任何原因要么是被请求的,要么伴随一次 RigReaped
RigControlChanged workspaceId, rigId, controller? 人类取得机台的独占控制权,或把控制权交还(释放时 controller 为 null——事件不记录是谁放手的)
RigReaped workspaceId, rigId, reason, agentId? 系统回收了一台机器:空闲、TTL 或内存压力淘汰(报告为 idleTimeout),并带上驱动它的智能体

机台的打开不是事件。启动完成后,机台所在的记录行翻转为就绪;没有任何东西被发布,因此没有 RigOpened 可供订阅。

有几个子系统从外部看像是事件驱动的,其实并不是。这里把它们列出来,是因为在你去寻找某个钩子之前,值得先知道它们的缺席本身就是一个设计决策:

子域 状态改由什么方式推进
记忆 record_memory_fact 及同类调用写入并返回;召回则读取。事实或冲突产生时不会发布任何事件
编排 提案、其批准以及生成的流水线运行都是记录行和 RPC 调用
计划文档 submit_plan 写入一个修订;批准会实体化一次编排。两者都不会被宣告
工件 publish_artifact / revise_artifact 直接写入修订版本
审批 升级处理发生在审批工作流服务内部
工单同步 厂商协调器订阅上文的工单事件;传入的 webhook 被直接应用
日历刷新 同步会 upsert 事件,客户端再重新读取;不存在刷新广播
工作树合并 记录在 worktree_merge_log 中,并从那里读取

下文的每个订阅方都在 runCcServer(或它构建的某个服务)内部构造。不存在客户端侧的订阅方。

订阅方 它消费的事件
WorkspaceSeeder WorkspaceCreated
入门评测套件初始化(内联) WorkspaceCreated
聊天空间预配(内联) SpaceCreated
PipelineTriggerDispatcher 每个 DomainEvent;每个启用的触发器会按自己的类型过滤
SubPipelineResumeListener PipelineRunCompleted, PipelineRunFailed
PipelineCostRollupListener AgentRunCompleted
BudgetEvaluationListener AgentRunCompleted
MeetingSummaryReconciler PipelineRunCompleted, PipelineRunFailed, PipelineRunCancelled(仅 meeting_summary 运行)
PipelineStepResumeListener AgentRunCompleted
AgentRunTaskCompleter AgentRunCompleted
GoalSupervisor AgentRunCompleted
CheckerDispatchListener AgentRunCompleted
TeamRoutingService AgentRunCompleted, TicketAssigned
OrchestrationRunListener PipelineRunCancelled, PipelineRunFailed
MultiVendorTicketSyncCoordinator TicketCreated, TicketAssigned, TicketReassigned, TicketStatusChanged, TicketDetailsUpdated
WorktreeGcListener TicketCompleted, TicketCancelled, SpaceDeleted, PrMerged, PullRequestStatusChanged
ChatBridgeService MessageReceived, SpaceProvisioningChanged
ActivityLogPersister ActivityLogged
RigEventListener RigControlChanged, RigReaped, RigClosedEvent——向驱动中的智能体的引导通道注入一条通俗的提示
StaleReviewWatcher PrHeadChanged——仅当被替换的提交上存在已完成的审查时才发布 ReviewBecameStale
GitHubPrConversationBridge AgentRunCompleted——外发的 PR 对话回复
NotificationFeedRecorder 下文二十二个通知类事件
RemoteEventForwarder 那二十二个,外加 TicketReassigned 和每个 TaskLifecycleEvent。三个成员关系事件只会使其按工作区划分的成员关系缓存失效;它们不会作为帧被转发
LocalRpcServer / RemoteRelayHost WorkspaceMemberRemoved——丢弃该用户已挂载的工作区订阅;套接字保持打开

TicketAssigned 既是审计与通知信号,也是流水线触发器。分配给智能体会记录归属,但不会启动任何运行——不存在工单派发器。分配给团队(设置了 assignedTeamId)是例外:TeamRoutingService 会派发团队负责人。

TicketRemoteSyncHandler 已在生产代码中声明,却从未被构造,因此它如今不订阅任何东西。客户端侧的 NotificationEventMapper 只在它自己的测试中被构造——它是客户端/服务器分离之前那条进程内路径的残留。实时提示来自下文的传输帧。

在服务器上,NotificationFeedRecorder 和 RemoteEventForwarder 把这些事件转成 notifications/* 传输帧(notification_wire.dart)。在客户端,RpcNotificationMapper → mapNotificationFrame 把帧转成 AppNotification。

通知类别恰好有 十七 个。若干事件会映射到多个传输方法,因为帧承载的是边沿(失败还是恢复),而类别承载的是问题(“CI 正在做什么?”)。

类别 源事件 传输方法
newMessage MessageReceived notifications/message_received(提及你)
agentRunCompleted MessageReceived notifications/message_received(is_agent_message,未提及你)
agentAwaitingInput AgentAwaitingInput notifications/agent_awaiting_input
ticketAssigned TicketAssigned notifications/ticket_assigned
ticketStatusChanged TicketStatusChanged notifications/ticket_status_changed
pullRequestPublished PullRequestPublished notifications/pr_published
prMerged PrMerged notifications/pr_merged
prMerged ExternalPrMerged notifications/external_pr_merged
prMentioned PrMentioned notifications/pr_mentioned
prMentioned PrCommentMentioned notifications/pr_comment_mentioned
reviewRequested PrReviewRequested notifications/pr_review_requested
reviewStale ReviewBecameStale notifications/review_stale
prMergeReadiness PrMergeReadinessChanged notifications/pr_ready_to_merge / pr_merge_blocked
prReviewDecision PrReviewDecisionChanged notifications/pr_approved / pr_changes_requested / pr_review_dismissed
prChecksStatus PrChecksStatusChanged notifications/pr_checks_failed / pr_checks_recovered
prThreadActivity PrThreadReplied notifications/pr_thread_replied
prThreadActivity PrThreadResolved notifications/pr_thread_resolved
meetingStartsSoon MeetingStartingSoon notifications/meeting_starting_soon
calendarAuthExpired CalendarAuthExpired notifications/calendar_auth_expired
rigStatusChanged RigControlChanged notifications/rig_control_changed
rigStatusChanged RigReaped notifications/rig_reaped
rigStatusChanged RigClosedEvent notifications/rig_closed

机台接管帧不会回送给接过操控权的那个人,释放则不加过滤(事件不记录是谁放手的),而且只有 backendFailure 关闭会产生 rig_closed 帧——其他原因要么是被请求的,要么已由回收帧涵盖。

AgentAwaitingInput 由 AgentAwaitingInputPublisher 发布:它监视批准注册表和凭据阻塞注册表中的新条目,并由 AgentQuestionService 告知每个 ask_user 问题。一次等待对应一个事件;等待结束时不发布任何事件。

五个自己创作的 PR 类别携带 forUserId,因此它们到达作者本人,而不是工作区里的所有人。

RemoteEventForwarder 还会把 TicketReassigned 作为 notifications/ticket_reassigned 转发,并把每个 TaskLifecycleEvent 作为 notifications/task_<phase> 转发。两者都不映射到通知类别——它们驱动实时界面,而不是通知中心。成员关系事件不会作为帧被转发:它们使转发器的缓存失效,而会话宿主机则丢弃被移除成员的工作区订阅。