Web Shell 后端权威队列显示:Qwen Code 待处理提示的重复条目消除与失败恢复策略
2026/9/13 23:57:55 网站建设 项目流程

Web Shell 后端权威队列显示:Qwen Code 待处理提示的重复条目消除与失败恢复策略

【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code

导读

在 Qwen Code 的 Web Shell 界面中,用户提交的提示词(prompt)会先进入一个本地队列,再通过 daemon 的 admission(准入)流程转交给后端会话。由于网络抖动或连接中断,admission 请求可能已经到达 daemon、但响应在回传途中丢失,此时界面会同时出现"本地未知行"与"daemon 权威行"两套表现,造成重复条目、"delivery unknown(送达未知)"、"local copy discarded(本地副本已丢弃)"等混乱 UI。本文基于仓库中的设计文档 web-shell-backend-authoritative-queue-display.md,结合 useQueuedPrompts.ts 等源码实现与测试用例,讲解这套"后端权威"队列显示方案的边界规则:本地行何时存在、失败后何时恢复草稿、何时彻底移除,以及如何通过 daemon 快照消除重复条目。读完本文,你将掌握 Qwen Code Web Shell 待处理队列的状态机与失败恢复的完整设计。

问题背景:响应丢失引发的重复条目

Web Shell 在两种场景下会发起"准入请求":

  • pending-prompt(待处理提示):普通的下一次会话提示词提交;
  • mid-turn admission(回合中准入):当前回合进行中插入的提示词。

当这样的请求可能已经到达 daemon、但其响应在传输中丢失时(例如连接在onAdmissionStarted之后、响应返回之前断开),Web Shell 无法确定请求是否被 daemon 接受。旧实现会保留一个本地的unknown队列行;随后一次 daemon 刷新(refresh)又会在本地副本旁追加一条权威行,于是界面上出现看起来重复的条目,并伴随 "delivery unknown"(送达状态未知)或 "local copy discarded"(本地副本被丢弃)之类的 UI 文案。

这个问题的本质是:本地乐观状态与 daemon 权威状态之间缺乏单一事实来源(single source of truth)。本地行与服务器行各自独立存活,谁也无法裁决哪一条才是"真的",最终只能靠用户手动选择恢复或丢弃。

设计核心:本地行只存在于请求在途期间

设计文档给出的核心原则非常简洁:本地行(local row)仅在 admission 请求"在途(in flight)"期间存在。一旦请求结束(无论成败),两条 admission 路径都必须查询 daemon 并渲染其队列快照,而不是继续相信本地记忆。具体规则如下:

  1. 请求成功:立即以 daemon 返回的权威数据(promptId、state)渲染队列行,本地行与权威行按 id 合并,绝不出现两行并存。
  2. 请求失败或后续跟随的查询失败:移除本地行。之后的重连(reconnect)或队列事件(queue event)只有在 daemon 主动报告该提示时才可能重新恢复它——本地不再单方面"复活"未知条目。
  3. 失败发生在请求派发(dispatch)之后:同样遵循上述规则,不把 payload 恢复回编辑器,因为该内容可能已被 daemon 接受,恢复回去会造成用户重复发送。
  4. 失败发生在请求派发之前(例如媒体上传失败):此时 daemon 绝无可能接受该内容,因此安全地将草稿恢复回编辑器,用户可以直接重新编辑提交。

源码印证:权威快照同步机制

在 useQueuedPrompts.ts 中,refreshPendingPrompts(L704-L736)实现了"查询 daemon 并渲染其队列快照"这一核心动作:

const refreshPendingPrompts = useCallback( async (targetSessionId = sessionId): Promise<RefreshPendingPromptsResult> => { if (!connected || !targetSessionId) return 'skipped'; // ... const result = await sessionActions.getPendingPrompts({ sessionId: targetSessionId, }); // 通过 requestSeq 序列号与 ownerToken 双重校验,防止过期响应覆盖新状态 if (requestSeq !== refreshRequestSeqRef.current) return 'superseded'; // ... syncServerQueuedPrompts( result.pendingPrompts.filter( (p) => p.state === 'queued' || p.state === 'running', ), targetSessionId, ); return 'refreshed'; }, [connected, sessionActions, sessionId, syncServerQueuedPrompts], );

需要注意两个细节:

  • 结果类型'refreshed' | 'skipped' | 'superseded' | 'failed'中,'superseded'表示本次请求已被更新的刷新请求取代(stale-response fence),'failed'表示查询失败——对应文档中"请求或后续查询失败则移除本地行"的判定。
  • 过滤条件只保留queuedrunning状态的提示,已 settle 的提示不再出现在队列快照中,从而被同步逻辑自然清除。

真正执行"合并去重"的是syncServerQueuedPrompts(L566-L702)。它以 daemon 快照为基准重建本地列表:

  • 本地行若持有serverPromptId但快照中已不存在 → 从列表移除;
  • 快照中的提示若本地已有同 id 行 → 就地更新其serverState不新增重复行
  • 快照中的提示若本地完全没有 → 以serverPromptId重建新行(含从content提取的图片/文件附件);
  • 最终通过areQueuedPromptsEqual(L238-L266)做深度比较,只有列表真正变化时才触发setQueuedPrompts重渲染。

这套"以服务器为准、按 id 合并、整体替换"的逻辑,正是消除"权威行与本地副本并列"这一问题的实现基础。

mid-turn 场景:快照协调与缺失行清理

mid-turn admission 走的是另一条路径。reconcileMidTurnMessages(L935-L977)会依次完成:

  1. 调用sessionActions.getMidTurnMessages获取回合中消息快照;
  2. 调用refreshPendingPrompts获取待处理提示快照;
  3. 调用applyMidTurnSnapshot(L738-L910)合并快照:settledMessageIds中的消息触发完成回调并删除本地 pending 记录,promotedMessageIds提升为服务器行并抢救其图片附件;
  4. 调用pruneMissingMidTurnRows(L912-L933)清理"快照中已不存在"的 mid-turn 行,并触发其onComplete回调。

其中applyMidTurnSnapshot的过滤逻辑与文档"本地行只在请求在途期间存在"完全对应:

let next = current.filter( (prompt) => !( prompt.midTurnState !== undefined && prompt.midTurnMessageId !== undefined && !prompt.isEditing && !prompt.isRemoving && (displayedServerPromptIdsRef.current.has(prompt.midTurnMessageId) || settledServerPromptIdsRef.current.has(prompt.midTurnMessageId) || settledIds.has(prompt.midTurnMessageId) || (applyPromoted && promotedIds.has(prompt.midTurnMessageId))) ), );

即:一旦消息在服务器端被 settle 或 promoted,本地 mid-turn 行立即移除;只有当快照中确实还"等待中(waiting)"的消息才保留为queued状态行。

失败恢复的边界:dispatch 前后行为不同

文档中最关键的一条实战规则是按失败发生的时间点决定是否恢复草稿,其实现位于submitPendingPrompt.catch分支(L1518-L1544):

.catch((error: unknown) => { // ... if (!queuedPromptsRef.current.some((p) => p.id === localId)) return; const next = queuedPromptsRef.current.filter( (prompt) => prompt.id !== localId, ); queuedPromptsRef.current = next; setQueuedPrompts(next); if (!admissionStarted) { restoreQueuedPromptsToEditor([prompt], targetSessionId); } reportError(error, t('queue.queueFailed')); })

这里的admissionStarted标志由onAdmissionStarted回调置位(L1403-L1405),它标记请求是否已经真正派发给 daemon:

  • admissionStarted === false(dispatch 前失败):例如媒体上传失败、session 创建失败——daemon 不可能接受该内容,因此调用restoreQueuedPromptsToEditor把文本、图片、文件、输入注解完整恢复回编辑器,用户可安全地再次编辑提交;
  • admissionStarted === true(dispatch 后失败):内容可能已被 daemon 接受,不恢复,只移除本地行并报错。之后仅当 daemon 在后续快照或队列事件中报告该提示时,它才会以权威行的形式重新出现。

restoreQueuedPromptsToEditor(L998-L1062)还配合mergeRestoredPromptText(L226-L230)做去重保护:若编辑器顶部已经存在相同文本,则不再叠加,避免刷新/重连多次触发恢复时把同一段输入拼接多次(即 issue #7128 报告的 "inputs concatenated after refresh")。

过时 UI 的移除与保留的防护机制

设计文档明确指出:过时的 unknown-admission 行状态及其 restore/discard(恢复/丢弃)UI 被移除。旧界面中,当送达状态未知时,用户会看到类似这样的提示与操作:

  • "Delivery is uncertain. Check the session before trying again."(消息是否送达尚不确定,请先检查会话再重试)
  • "Restore local copy" / "Discard local copy"(恢复本地副本 / 丢弃本地副本)
  • "Continue editing"(继续编辑,带二次确认:"The prompt may already be running. Continue editing only if you accept the risk of sending it twice.")

这些文案仍可在 i18n.tsx 的queue.admissionUnknownqueue.restoreUnknownqueue.discardUnknownqueue.continueEditing等键中找到(L1765-L1771)。在新方案下,未知状态不再以"本地未知队列行"的形式长期存在,而是收敛为受限的会话级防护:测试用例App.test.tsx[data-testid="prompt-admission-unknown"]promptAdmissionUncertain的语义表明,保留的是"锁定重复提交"级别的保护,而非可恢复/可丢弃的本地行副本。

从源码结构看,ChatPane.tsx 中仍保留着 admission-unknown 的状态条渲染(含continueEditingUnknownPrompt/discardUnknownPromptPayload两个动作),但其行为已被测试约束为"锁定重复提交 + 限定到分配的 session",与旧的"本地 unknown 队列行 + 手动恢复/丢弃"模型已是两套东西——这印证了设计文档描述的正是这一演进方向。

测试验证:边界行为被测试固化

仓库中的测试用例 App.test.tsx 从三个角度固化了上述边界行为(见 "App prompt send failure retry" 描述组,L33355 起):

  1. does not mark delivery unknown when lazy session creation fails before prompt admission(L33356):当createSession在 admission 派发前失败时,sendPrompt不应被调用,编辑器不被清空(editorCommit不触发),也不出现 admission-unknown 标记——与"dispatch 前失败恢复草稿"规则一致;
  2. keeps an unknown lazy-session admission scoped to its allocated session(L33383):onAdmissionStarted之后连接断开,unknown 状态保留但限定在已分配的 session 内,并调用promptAdmissionUncertain上报;
  3. locks duplicate submission when prompt admission is unknown(L33426):admission 结果未知时编辑器被禁用(disabled === true),不出现failed-prompt-retry按钮,且再次点击操作不会重复调用sendPrompt——杜绝了"未知状态下重发导致重复执行"的风险。

这三个用例分别对应设计文档的三条规则:dispatch 前失败安全恢复、unknown 状态不扩散、以及绝不因本地猜测造成重复提交。

总结

"后端权威队列显示"方案用一个简单的不变式解决了复杂的重复条目问题:本地行只在请求在途期间存活,最终状态一律以 daemon 快照为准。由此派生出三条清晰的失败恢复规则:

失败时机daemon 是否可能已接受处理方式
dispatch 前(如媒体上传失败)不可能恢复草稿回编辑器
dispatch 后(响应丢失)可能移除本地行,不恢复;等 daemon 快照/事件决定去留
后续刷新/查询失败——移除本地行,不单方面复活

实现上,useQueuedPrompts.ts 通过refreshPendingPrompts → syncServerQueuedPromptsreconcileMidTurnMessages → applyMidTurnSnapshot → pruneMissingMidTurnRows两条链路保证了权威快照的同步,通过admissionStarted标志划分了失败恢复的边界,并通过 App.test.tsx 的用例将行为固化。这套设计保证了 Web Shell 在弱网、断连、重连等不确定环境下,既不会向用户展示重复条目,也不会因本地猜测造成重复提交。

【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询