13 · React UI 与实时数据链路¶
一句话结论: Board UI 以 REST + TanStack Query 维护权威读模型,再用每 Company 单一 WebSocket 对高频 Run 做局部 cache patch、对业务变更做批量失效;断线不回放,而是重连后主动对账。
1. UI 的三层数据路径¶
flowchart TB
COMP["Pages / Components"] --> HOOK["useQuery / useMutation"]
HOOK --> API["ui/src/api/*.ts"]
API --> REST["Express REST API"]
REST --> DB["PostgreSQL"]
DB --> EVENT["Server live events"]
EVENT --> WS["Company WebSocket"]
WS --> LIVE["LiveUpdatesProvider"]
LIVE --> CACHE["TanStack Query Cache"]
CACHE --> COMP
REST 返回可重建的事实,WebSocket 提供低延迟提示。WebSocket 事件不是唯一真相,因为当前事件总线不持久化也不回放。
2. App 组合¶
UI 根部组合了:
- Router;
- QueryClient;
- auth/bootstrap;
- CompanyContext;
- theme/toast/dialog 等 UI context;
- LiveUpdatesProvider;
- feature/experimental gates;
- route pages。
Provider 顺序有依赖:实时连接需要 session 与 selected Company,toast 需要其 context,company-aware router 又依赖选中 Company。
3. Company-prefixed Routing¶
URL 使用 Company issue prefix,例如:
自定义 Link、NavLink、Navigate、useNavigate 会自动应用 active Company prefix,并为 Issue link 接入 quicklook/prefetch。
好处:
- URL 自带 Company 上下文;
- 多 Company 切换不只依赖内存 selected ID;
- Issue identifier 可读;
- 深链接与浏览器历史稳定。
Router 仍需处理无 prefix 旧路径和 canonical redirect。
4. API client 与 query key¶
API 按资源拆分,query key 统一在 queryKeys.ts。以 Issue 为例:
issues.list(company)
issues.detail(ref)
issues.comments(ref)
issues.interactions(ref)
issues.documents(ref)
issues.activity(ref)
issues.runs(ref)
issues.liveRuns(ref)
issues.activeRun(ref)
issues.workProducts(ref)
issues.fileResources(ref, options)
同一实体有多个读模型,因为刷新频率、大小和用户可见面不同。把所有数据放进 issue.detail 会让每条 log 都重新下载整条线程和文档。
5. Mutation 的三种更新策略¶
5.1 服务端成功后 invalidate¶
适合低频、结果复杂的 mutation。成功后失效相关 key,由下一次 fetch 获取权威视图。
5.2 Optimistic update¶
评论、Issue 局部状态、运行占位等需要即时反馈的操作,会先 setQueryData,失败时回滚或 reconciliation。
5.3 Live event patch¶
后台 Agent 更新不经当前浏览器 mutation,WebSocket 根据 event patch cache 或 invalidate。
三者必须可收敛到服务端事实,不能各自维护独立永久状态。
6. Server 事件总线¶
services/live-events.ts 使用 Node EventEmitter:
还有 companyId="*" 的 global event channel。事件包含 ID、company、type、createdAt、payload。
优点是简单低延迟;限制是:
- ID 只在当前进程递增;
- Server restart 后重置;
- 没有 durable replay;
- 多 Server 实例不会自动共享 EventEmitter。
7. WebSocket Upgrade 鉴权¶
路径形如:
Upgrade 时:
- local trusted 无 token可建立 local Board;
- authenticated 浏览器用 session cookie,并检查 instance admin 或 active Company membership;
- bearer/query token 走 agent API key hash,且 key.companyId 必须匹配; -失败返回 403 并关闭 raw socket。
连接建立后只订阅该 Company channel。
8. Ping/Pong 与连接清理¶
Server 每 30 秒遍历 client:
- 上一轮未收到 pong → terminate;
- 标记为 not alive;
- 发送 ping;
- pong 时恢复 alive;
- close 时 unsubscribe EventEmitter listener。
没有 unsubscribe 会让每次重连累积 listener 和重复消息。
9. 每个 Company 一个共享 Socket¶
LiveUpdatesProvider 在 session 成功、selected Company 稳定后建立一个 socket。组件通过 useCompanyLiveEvent(handler) 订阅 Provider 的本地 fan-out,不自行再开连接。
flowchart LR
WS["1 Company WebSocket"] --> P["LiveUpdatesProvider"]
P --> CACHE["Cache patch / invalidate"]
P --> TOAST["Toast gate"]
P --> S1["Run Transcript subscriber"]
P --> S2["Summary stream subscriber"]
P --> S3["Other component subscribers"]
这样组件挂载/卸载不会造成连接风暴。
10. Event handling 分层¶
高频 heartbeat.run.log¶
只更新必要的 last-event/transcript 状态,然后尽早 return,不做大范围 query invalidation。
heartbeat.run.progress/event¶
把 current tool、status message、assistant snippet、last event time patch 到:
- Company live runs;
- 当前 Issue active/live run;
- 可见页面相关 cache。
heartbeat.run.queued/status¶
- 更新 live-runs lifecycle;
- 失效 heartbeats、Agent、dashboard、cost/sidebar badge 等相关读模型;
- terminal 时从 live cache 移除并清 Issue execution run;
- 必要时显示 toast。
agent.status¶
失效 Agent list/detail、org、dashboard。
activity.logged¶
根据 entity type/action 精准失效 Issue、Project、Goal、Approval、Cost、Routine、Pipeline 等 query。
11. 为什么先 patch,再 invalidate¶
Patch 给用户即时反馈,invalidate 最终与 Server 对账:
只 patch 的风险是漏字段或事件丢失;只 invalidate 的风险是每秒几十次网络请求和闪烁。
12. Invalidation Batcher¶
实时事件可能在短时间触发大量相同失效。Provider 包装 QueryClient:
setQueryData仍即时执行;invalidateQueries进入 batch;- 同 key 合并;
- 每秒只 flush 少数几次;
- Provider unmount 时 dispose。
这是一种“热路径写 cache、冷路径重新取权威数据”的背压机制。
13. 当前可见 Issue 的特殊处理¶
Provider 会从 pathname 解析当前 Issue,收集:
- UUID 与 identifier;
- assignee;
- active/live/linked run IDs。
若事件与当前可见 Issue 重合:
- 更精确更新详情、活动、run;
- 当前用户正在看评论时可先 hydrate 新 comment;
- 抑制重复 toast,避免页面已经显示变化又弹一遍;
- terminal run 立即清理 optimistic placeholder。
页面焦点/visibility 也参与 toast 抑制。
14. Toast Gate¶
系统按类别维护 10 秒窗口和最大次数,并在 WebSocket 重连后的短时间抑制 toast。否则断线重连/批量 activity 可能一次弹出大量历史感知通知。
Toast 是注意力产品,不是事件日志;完整历史仍在 activity/run 页面。
15. 重连策略¶
浏览器端指数退避:
特殊处理:
- StrictMode 首次连接延迟到 0ms timer,避免 double-effect 产生 noisy close;
- CONNECTING 状态不强行 close,等 open 后安静关闭 stale connection;
- error 不主动重复 close,让 onclose 统一驱动重连;
- Company/session 变化关闭旧 socket;
- 重连成功后 invalidate
liveRuns(company)对账。
16. 为什么重连后必须 refetch¶
当前 server 不保存 event replay log,断线期间的消息不可补。因此 event ID 不能当可靠 cursor。重连后 UI 至少重新取:
- live runs;
- 后续事件触发的相关资源;
- 页面本身仍 stale 的 query(按 React Query 策略)。
这是一种明确的“事件是提示、REST 是真相”设计。
17. Optimistic 运行与最终对账¶
用户点击 invoke/assignment 后,UI 可以先显示 queued/live placeholder。真正 WebSocket status 到来时:
- 以 run ID merge;
- 避免重复条目;
- terminal 时移除 live placeholder;
- 失败/响应丢失时通过 active/live run API 对账。
辅助模块 optimistic-issue-runs、live-runs-cache、recovery-reconcile 等把复杂 merge 从组件中抽出,并有专门测试。
18. Adapter Transcript 的展示边界¶
UI adapter registry 为不同 runtime 提供 parser,结构化 event 转成统一 transcript item。Parser 需要处理:
- 不完整 chunk;
- unknown event;
- 重复/乱序;
- tool call/result 配对;
- secret redaction;
- 大 payload 截断;
- parser worker 隔离。
展示层不应改变 run 业务状态;解析失败最多影响可读性,权威 run status 来自 Server。
19. 当前架构的横向扩展边界¶
若部署多个 Server 实例:
- 每个有自己的 EventEmitter 和 event ID;
- WebSocket 连到 A,mutation 可能在 B;
- 没有 Redis/Postgres pubsub 就收不到事件;
- plugin worker/local process ownership 也有单主机假设。
因此当前实现更适合 single-host/persistent server。要横向扩展,需要 durable/shared event bus、sticky/ownership 协议和 replay cursor。
20. 可复用经验¶
- REST 保存可重建事实,WebSocket 只做低延迟提示。
- 每租户/Company 共享一个 socket,组件在本地订阅分发。
- 高频事件做局部 cache patch,低频业务变更做精准失效。
- Invalidation 要 batch/coalesce,不能让事件速率等于请求速率。
- 无 replay 的系统重连后必须主动对账。
- Toast 是注意力通道,需要可见页面和冷却抑制。
- 进程内 EventEmitter 要明确标为单实例边界。
源码入口¶
ui/src/App.tsxui/src/context/LiveUpdatesProvider.tsxui/src/lib/queryKeys.tsui/src/lib/query-invalidation-batcher.tsui/src/lib/live-runs-cache.tsui/src/lib/optimistic-issue-runs.tsserver/src/services/live-events.tsserver/src/realtime/live-events-ws.tspackages/shared/src/types/live.ts
下一章看系统怎样扩展而不把核心完全打开:14 · 插件系统与进程边界。