投影不变量服务端事件与快照是事实输入,Reducer 是唯一解释器,组件只是状态视图。连接断开不等于 Run 失败,收到 Delta 不等于事实已经持久化。

流式 UI 消费事件,不是拼接一个字符串

同一 Run 中至少存在 Run、Message、Content、Tool、Approval 与 Artifact 生命周期。事件应带 event ID、run ID、entity ID、sequence 或 version、类型和 payload;文本 Delta 关联具体 Content,Tool 状态关联 Tool Call,不能靠数组位置或“最后一条消息”猜测归属。

Delta 表示相对更新,Snapshot 表示某版本完整状态,二者采用不同处理路径。未知事件记录类型、版本和关联 ID,并安全忽略或触发能力降级;直接抛错会让一次前向兼容升级摧毁整个对话。事件 Schema 在入口校验,错误 payload 进入诊断队列而不是污染视图状态。

type UiEvent =
  | { id: string; seq: number; type: 'content.delta'; contentId: string; text: string }
  | { id: string; seq: number; type: 'tool.state'; callId: string; state: ToolState }
  | { id: string; seq: number; type: 'run.snapshot'; version: number; run: RunSnapshot };

type Projection = { seen: Set<string>; lastSeq: number; run: RunView };

Reducer 从事件推导可预测状态

Reducer 以旧投影和已验证事件为输入,返回新投影与可选诊断,不执行网络请求、滚动或通知。纯函数让一段录制事件在开发机、CI 和故障复盘中得到相同结果,也避免每个 Vue 组件各自解释协议。

状态集合

至少把传输、Run、Tool 和消息拆成正交状态。一个 Run 可以在网络重连时仍处于 waiting-approval,也可以在连接正常时已经 failed。

连接状态

idle、connecting、open、reconnecting 和 closed描述浏览器到事件源的可达性。只有服务端快照或终态事件能决定 Run 结果;网络错误仅触发恢复与“状态可能过期”提示。

运行状态

streaming、waiting-tool、waiting-approval表达进行中责任,completed、failed、cancelled为终态。Tool、消息和 Artifact 仍有自己的部分或失败状态,Run 完成不等于每个可选 Artifact 都成功。

终态不可被晚到 Delta 改写

按 event ID 去重;同一实体按 sequence/version 拒绝旧更新;终态只接受协议明确允许的补充元数据。Snapshot 到达时按 server version 替换事实投影,再重放其后的已确认事件。若发现 sequence 缺口,不自行猜测,应请求缺失区间或新快照。

事件到视图的单向投影链
校验事件Schema、版本、实体和顺序。
纯 Reducer去重、状态转换与不变量。
派生视图选择器生成可呈现状态。
执行效果通知、滚动和恢复命令。

SSE、Fetch Stream 与 WebSocket 的选择

Server-Sent Events(SSE)适合服务端到浏览器的有序文本事件,WHATWG HTML 标准定义了重连和 Last-Event-ID;Fetch Stream 适合 POST 后直接读取响应体并自行解析;WebSocket 适合低延迟双向控制。选择取决于命令方向、鉴权、代理缓冲、连接上限和基础设施支持,而不是流行度。

恢复游标必须代表服务端持久位置,心跳只证明通道仍活跃。断线后携带 Last Event ID 或游标请求续传;若保留窗口已过、游标未知或发现缺口,回拉 Snapshot 并对账。浏览器自动重连不能代替业务恢复,因为它不知道 Run 是否在断线期间完成或取消。

渲染节奏与 Token 节奏解耦

事件可以每几毫秒到达,但 DOM 不应每个 Token 更新。先在内存累积,再按 requestAnimationFrame 或短时间窗提交;同时记录排队长度和事件到可见渲染延迟。压力升高时优先合并连续文本 Delta,而不是丢失状态事件。

Markdown 增量阶段可能存在未闭合代码块或列表,应使用支持增量的解析策略,或把未完成内容当纯文本,完成后再做最终解析和净化。长对话虚拟化、Artifact 懒加载,自动滚动只在用户仍贴近底部时启用;用户向上阅读后显示“有新内容”按钮,不抢走位置与焦点。

取消和重试必须表达真实后端状态

点击取消后可立即禁用重复点击并显示 cancelling,但只有服务端确认后才能显示 cancelled。关闭连接或停止渲染只是本地行为,后台 Run 和 Tool 可能仍在执行。超时则显示“终态未知”,提供刷新事实和人工核对,而不是允许直接重试。

重试引用失败步骤、检查点和幂等键。已成功的付款、发信或文件写入不能随整段生成重放;服务端应决定从检查点继续、补偿后重做或创建新 Run。UI 展示这个决定以及会复用哪些结果。

动态内容也要可访问

WAI-ARIA 1.2把 status 与 alert定义为不同紧迫程度的 live region。逐 Token 宣读会让读屏持续被打断;更稳妥的是把阶段变化、完整句子或 Tool 结果批量写入低优先级状态区,只有需要立即处理的失败才使用 alert,且不要自动抢焦点。

暂停、取消、跳到最新消息和恢复都能用键盘完成。新内容不移动用户焦点,错误把焦点引导到可恢复动作时要有明确原因。尊重 Reduced Motion,动画不是唯一状态表达,连接和 Run 状态同时用文字与语义标记。

用确定性事件回放测试概率 UI

录制协议夹具覆盖正常文本、并行 Tool、审批、Artifact 与恢复快照。变异器注入重复、乱序、晚到、截断、未知类型、sequence 缺口和连接切换;每条序列断言状态不变量、可用动作、可见文案和诊断事件,而不是截图恰好相同。

回放验收矩阵
注入条件必须保持证据
重复 Delta内容不重复Reducer 幂等断言
终态后晚到 Delta终态与最终内容不被改写状态转换测试
断线跨过审批Snapshot 后仍显示待审批游标与快照对账日志
高频文本流状态事件不丢,DOM 有界增长渲染延迟与节点数曲线

性能验收记录首个可见响应、P95 事件到渲染延迟、长 Run 内存和 DOM 数;恢复验收证明同一事件日志在刷新前后得到等价投影。只有网络 Happy Path 的 Demo 不构成流式 UI 证据。

主动提取:为什么 EventSource 自动重连后仍要拉 Snapshot?
传输重连只说明连接恢复。服务端可能已丢弃旧事件、Run 可能已结束,或浏览器存在 sequence 缺口;Snapshot 才能重新建立事实基线。
故障判断:用户点击取消后连接立即关闭,可以显示“已取消”吗?
不能。关闭连接只证明浏览器停止接收;应显示取消请求中或终态未知,直到服务端确认 Run 终止及副作用状态。

资料与事实边界

以下以官方文档、标准、论文和原始工程资料为主。厂商 API、价格、限额与模型能力会变化,落地时仍需按目标版本做能力探测和回归。

  1. WHATWG HTML:Server-sent events — EventSource、重连时间与 Last-Event-ID 的标准语义
  2. WHATWG Streams Standard — ReadableStream、背压和流消费基础
  3. WAI-ARIA 1.2 — status、alert 与 live region 语义
  4. W3C APG:Alert Pattern — 动态提醒的频率、焦点与可访问边界