qwen-code WebShell 非主工作区会话归档加固:能力门控、身份对账与部分失败处理
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
本文基于 qwen-code 仓库的设计文档 web-shell-non-primary-session-archive-hardening.md,讲解 WebShell 在多工作区(daemon 多 workspace)场景下如何安全地暴露"归档/取消归档"操作:哪些能力(capability)与信任条件决定 UI 是否可用、跨工作区如何以(workspaceCwd, sessionId)二元组隔离同名会话,以及 HTTP 成功响应中携带的逐会话失败如何被前端对账(reconcile)。读完后,你能理解 qwen-code 会话归档链路从 UI 门控到 daemon 执行、再到状态收敛的完整契约。
一、变更定位:补齐既有 UI 路径,不动 API 契约
设计文档的 Summary 明确了这次加固的边界:
- WebShell 已经在从已注册的 secondary(非主)工作区列出 active 与 archived 会话;
- daemon 已经暴露了workspace-qualified 的 archive/unarchive 路由;
- 本次变更只是完成既有的 UI 路径,不修改:归档 REST API、SDK 类型定义、持久化格式、以及删除(delete)行为。
也就是说,这是一次典型的"契约不变、行为补齐"的加固:API 形状、协议字段、落盘格式全部保持稳定,变更集中在能力门控、身份隔离与错误对账三个层面。相关协议背景可参考 qwen-serve-protocol.md 与 会话生命周期说明。
二、能力与信任边界:三条件门控归档 UI
文档规定的门控规则是:
| 条件 | 适用范围 | 说明 |
|---|---|---|
session_archive能力 | 所有会话归档 UI | 缺失时不暴露归档入口,也不查询 archived 目录 |
workspace_qualified_rest_core能力 | secondary 工作区 | 非主工作区额外要求 |
| 受信任的运行时(trusted runtime) | secondary 工作区 | 非信任工作区不开放写操作 |
并且:受信任 secondary 工作区的 active 行只暴露 Archive 操作;这些行对 pin(置顶)、group(分组)、rename(重命名)、export(导出)、delete(删除)保持既有的load-only(仅加载)处理,本次加固不改变这一点。最后一个关键细节是:当所需能力缺失时,archived 目录根本不发起查询——门控发生在数据请求之前,而不是渲染阶段。
从源码可以印证这套门控。能力声明在 capabilities.ts 中:
session_archive: { since: 'v1' },即session_archive自 v1 能力集起可用;workspace_qualified_rest_core是另一条用于工作区限定 REST 通道的能力位,两者在 WebShell 客户端分别被消费。侧边栏 WebShellSidebar.tsx 中对能力的判断形如:
connection.capabilities?.features?.includes('session_archive'),能力版本的演进规则可参考 能力与版本化文档。
三、身份模型:以 (workspaceCwd, sessionId) 作为会话唯一键
文档的 Identity and Reconciliation 部分规定:WebShell 中合并后的会话集合与瞬态行状态一律用(workspaceCwd, sessionId)二元组识别一个会话。这一规则覆盖:
- 去重(deduplication);
- React key 生成;
- 当前选中态(current selection);
- 忙碌态(busy state);
- 未读完成标记(unread completion);
- 导出进行中状态(export-in-flight state)。
目的是保证不同工作区中 sessionId 相同的两个会话完全独立——选中一个工作区的sess-abc,绝不能误伤另一个工作区同 id 的会话。
WebShellSidebar.tsx 中的会话键生成正是这个契约的落地:
return `${workspaceCwd ?? ''}\0${sessionId}`;用\0分隔 workspace 路径与 session id,避免纯字符串拼接产生歧义碰撞。secondary 工作区会话的查询与合并逻辑集中在 useOtherWorkspaceSessions.ts,其中每个查询以workspaceCwd为单位维护archiveState: 'active'等状态。
四、部分失败与响应后对账:成功 HTTP 响应里的逐会话错误
文档对错误语义的定义是核心设计点之一:
workspace-qualified 的 archive / unarchive 响应可能在 HTTP 2xx 成功响应中报告逐会话失败。WebShell 会把对应的
errors[]条目呈现给用户,并且在操作落定后,总是重新对账(reconcile)主工作区的 active/archived 目录以及所选工作区的目录。幂等的alreadyArchived与alreadyActive结果被视为成功。
这段契约与 daemon 侧的结果结构严格对应。session-archive.ts 定义了批量结果类型:
export interface DaemonArchiveSessionsResult { archived: string[]; alreadyArchived: string[]; resolvedConflicts: string[]; notFound: string[]; errors: Array<{ sessionId: string; error: unknown }>; } export interface DaemonUnarchiveSessionsResult { unarchived: string[]; alreadyActive: string[]; resolvedConflicts: string[]; notFound: string[]; errors: Array<{ sessionId: string; error: unknown }>; }几个实现细节值得注意:
- 批量部分成功是常态而非异常。
archiveDaemonSessions/unarchiveDaemonSessions对每个 session 独立执行并归桶(archived / alreadyArchived / notFound / error),整体请求仍是 200,失败信息走errors[]字段。因此客户端不能只看状态码,必须消费errors[]——这正是文档要求 WebShell "surface a matchingerrors[]entry" 的原因。 - 幂等语义内置。目标会话已处于目标状态时(已在归档目录 / 已在活跃目录),结果落入
alreadyArchived/alreadyActive桶,不计入errors[],对调用方而言是成功。 - id 规范化后再去重。批量入口先
sessionIds.map(normalizeSessionIdForLookup)再Set去重(见 session-archive.ts),保证不同大小写写法不会重复执行。 - 冲突处理。当会话同时出现在 active 与 archived 目录时,默认拒绝并提示以
resolveConflicts: true重试(见 sessionLocationError),显式开启后冲突结果会记入resolvedConflicts桶。 - 每次批量操作后写 stderr 审计行。
logSessionArchiveResult输出requested/archived(alreadyArchived)/notFound/errors的完整计数与 id 列表(见 session-archive.ts),便于运维侧核对前端呈现与后端实际变更是否一致。
WebShell 侧的写操作入口在 WebShellSidebar.tsx,分别调用archiveSessionsData([sessionId])与unarchiveSessionsData([sessionId]);操作完成后触发对主工作区与会话目录的重查,即文档所说的 "always reconciles ... after the operation settles"。
五、daemon 侧的执行安全:协调锁与写入租约
归档/取消归档最终通过 SessionArchiveCoordinator 的互斥锁串行化到同一会话的并发维护操作:
runExclusiveMany对批内所有(规范化后的)session id 加互斥标记,若某 id 正处于排他迁移中则抛SessionArchivingError;- 锁键经过
normalizeSessionIdForLookup规范化——源码注释说明这是为了防止大小写不敏感文件系统中,不同拼写的调用者 id 绕过锁、在 restore 进行中误删 transcript 文件(session-archive.ts); sealMaintenanceAndWait支持 daemon drain 阶段封存维护操作,封存后新请求抛DaemonDrainingError;- 实际的存储变更在
runWithDaemonWriterLease内完成:先获取 daemon 写入租约(processKind: 'daemon'、takeoverPolicy: 'certified'),执行变更前用assertOwnedAndUnchanged断言存储未被外部修改,变更后再更新关联的 scheduled task 生命周期(归档会disableTasksForSessions,取消归档会enableTasksForSessions,见 updateScheduledTaskForMaintenance)。
这套机制保证了文档承诺的"删除行为不变":archive / unarchive / delete 共用同一套协调与租约原语,互不干扰。
六、验证矩阵:文档声明的测试覆盖
文档 Verification 一节声明了两层回归:
WebShell 层覆盖:成功与部分失败响应、能力缺失、非信任工作区、幂等结果、equal-id(同 id 不同工作区)场景下的 current / busy 状态隔离、以及操作后对账。对应的测试文件包括 SessionOverviewPanel.test.tsx、WebShellSidebar.collapse-persist.test.tsx、WebShellSidebar.workspace-removal.test.tsx 与 useOtherWorkspaceSessions.test.tsx。
daemon 层回归:归档并取消归档一个与主工作区 id 相同的 secondary 会话,同时断言主工作区的会话文件与 bridge 状态保持未变。daemon 侧归档逻辑的单测见 session-archive.test.ts,多工作区路由行为见 multi-workspace-sessions.test.ts 与 qwen-serve-routes.test.ts。
七、小结
这次加固可以用三句话概括其设计契约:
- 门控前置:没有
session_archive(以及 secondary 工作区所需的workspace_qualified_rest_core+ 受信任运行时),归档 UI 不出现、archived 目录不查询;secondary 行仅开放 Archive,其余操作维持 load-only。 - 身份隔离:所有前端会话状态以
(workspaceCwd, sessionId)为键,同名会话跨工作区互不可见。 - 失败可感知、状态最终一致:HTTP 200 中的
errors[]逐条呈现,alreadyArchived/alreadyActive幂等成功,操作落定后强制对账所有相关目录。
对二次开发者的启示在于:多租户/多作用区的写操作 UI,能力位应同时作为"数据请求开关"而非仅"按钮开关";批量接口应以逐目标结果桶(changed / already / notFound / errors)为契约核心,客户端对账逻辑与结果桶一一对应,才能既容忍部分失败又不丢失状态一致性。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考