3.9 KiB
3.9 KiB
失败插入卡片链路与实现契约
现状链路
sendMessage
→ 创建本地用户卡片 + clientMessageId
→ WebSocket type=message
→ handleMessage
→ 活动 Codex App turn 时进入 handleCodexAppSteerMessage
→ codex_app_steer_status(pending / inserted / failed)
→ handleServerMessage
→ updateCodexAppSteerMessage
→ setCodexAppSteerStatusElement
关键位置:
public/app.js:sendMessage、setCodexAppSteerStatusElement、updateCodexAppSteerMessage、createMsgElement、clearUserMessageIndex。public/style.css:.codex-steer-*状态样式。server.js:handleMessage、handleCodexAppSteerMessage。scripts/regression.js:runCodexAppRuntimeImageSteerRegression、runCodexAppStaleRunningRegression。
已确认事实
- 本地失败卡片以
clientMessageId/data-message-id稳定定位,不应按文件名或 DOM 顺序匹配。 - 当前
userMessageIndex只保存内容与时间;重试还需要保存附件、会话、模式和 agent。 - 当前状态渲染只有文本,没有操作容器、忙碌语义和并发保护。
- 当前服务端在调用
turn/steer前持久化用户消息,最终失败时不回滚,会留下模型未接收的幽灵消息。 - 同步前置失败不会持久化;两类失败必须统一为“失败卡片仅存在于当前客户端”的语义。
- 本地临时卡片当前提前增加
currentSessionMessageCount,失败后会造成消息索引漂移。
推荐实现
前端
- 新增独立的运行中插入记录 Map,以
clientMessageId保存:元素、文本、附件克隆、sessionId、mode、agent、pending/committed 状态。 clearUserMessageIndex同时清理该 Map,防止切换会话后重试旧请求。setCodexAppSteerStatusElement统一维护:role=status、aria-live=polite;- 卡片
aria-busy; - 失败态的“重试”“删除”按钮;
- 非失败态移除操作容器;
- 按钮使用
type=button、明确aria-label,并阻止重复绑定。
retryCodexAppSteerMessage只在记录存在、仍属于当前会话、当前为 Codex App 运行中且 WebSocket 可用时发送;先切到 pending 并禁用操作,发送失败则恢复 failed。deleteCodexAppSteerMessage只允许删除 failed 临时项,移除 DOM、两个索引记录并刷新轮廓/滚动条。- 临时插入不提前递增
currentSessionMessageCount;收到inserted后才标记 session message 并递增,失败/删除不改变持久化索引。
服务端
- 给待持久化的 steer 用户消息写入稳定
id(优先使用clientMessageId)。 - 最终
turn/steer失败时,在发送 failed 状态前,从最新会话中按该 id 精确移除本次用户消息并保存。 - stale no-active-turn 自动恢复成功分支不得回滚;同步前置失败和 ready 超时本就没有持久化项。
- 异步错误消息补带
clientMessageId,便于前端和回归关联。
测试契约
- 前端独立单测或可执行契约测试:
- failed 创建两个按钮,pending/inserted 不展示;
- 快速双击重试只发送一次,载荷复用原文本与附件;
- retry 时
aria-busy=true且操作禁用/移除; - failed 恢复操作;
- delete 只移除目标项及对应索引;
- turn 已结束或 WebSocket 断开时不发送并保留可恢复失败态。
- 服务端定向回归:
- 现有同步附件失败仍不持久化;
- mismatch 异步失败后同名用户消息为 0;
- stale recovery / 成功 steer 用户消息仍恰好为 1。
- 静态契约:按钮文本、CSS 作用域和 status handler 保持存在。
已知限制
- 附件已经从服务端过期时,单纯重试仍会失败;按钮主要恢复临时 turn/server 错误。再次失败后必须恢复可操作状态和明确错误提示。
- 失败项按现有持久化边界保持客户端临时态,刷新后会消失;不新增任意历史消息删除协议。