Qwen Code Daemon 多工作区会话导出:Workspace-Qualified Session Export 设计与实现解析
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
导读
本篇文章围绕 qwen-code 仓库中docs/design/daemon-multi-workspace-session-export.md设计方案展开,深入剖析 daemon 如何让客户端从**显式选定的已注册工作区(workspace)**导出持久化会话。你将掌握新的GET /workspaces/:workspace/session/:id/export路由契约、workspace 选择与信任检查规则、workspace_session_export能力通告机制,以及 SDK 层与遥测层对应的实现细节。文中所有结论均以当前仓库源码为据,可直接对照 路由实现 与 SDK 客户端 进行验证。
背景与动机:为什么单一导出路由不够用了
qwen-code 的 daemon 在设计上支持多工作区运行时(multi-workspace runtime)。在引入 workspace-qualified 导出之前,客户端只能通过GET /session/:id/export导出会话,而该路由**有意绑定在主工作区(primary workspace)**上。这带来两个实际问题:
- 当某个会话持久化在次工作区(secondary workspace)时,直接复用主绑定路由会返回
404; - 更危险的是,当同一个 session id 在多个工作区中都存在时,旧路由可能选中错误的 transcript,导致导出的内容张冠李戴。
Issue #6378 因此要求客户端能够"从一个显式选定的已注册工作区导出持久化会话"。围绕这个目标,该设计新增了四样东西:
- 新的复数路由
GET /workspaces/:workspace/session/:id/export?format=html|md|json|jsonl; - 对应的能力标签
workspace_session_export; - 匹配的
WorkspaceDaemonClientSDK 方法; - 配套的使用文档。
与此同时,旧路由GET /session/:id/export继续保留且仍为主工作区绑定,保证既有客户端的兼容性不被破坏。
路由契约:workspace 选择、格式参数与错误语义
新增路由与选择规则
新路由的完整形态为:
GET /workspaces/:workspace/session/:id/export?format=html|md|json|jsonl其中:workspace选择器遵循仓库中既有的复数路由规则(plural-route rule),解析顺序为:
- 先按精确的已注册 workspace id 匹配;
- 再按 URL 编码的绝对 cwd(canonicalization 之后)匹配。
这一步在源码中由resolveQualifiedSessionTarget实现(见 packages/cli/src/serve/routes/session.ts#L1295-L1328):先用workspaceRegistry.getManagedEntryByWorkspaceId(selector)精确查找 id,若 selector 是绝对路径则继续尝试getManagedEntryByWorkspaceCwd(selector),最后通过resolveWorkspaceRuntimeFromParam解析出目标运行时。选中的运行时必须受信任(trusted)——对于普通(非 primary)runtime,若runtime.trusted为假,会直接返回 untrusted workspace 响应。值得注意的执行顺序是:workspace 解析与信任检查先于 session 存在性与格式校验,也就是说,即使 session id 或 format 不合法,也先保证不会把请求路由到错误的工作区。
导出行为边界
该路由只做一件事:读取所选 workspace 的活动(active)持久化 JSONL 并导出。设计文档明确列出它"不做"的事情,这些约束也对应了实现中handleSessionExport与resolveQualifiedSessionRuntime的代码路径(见 packages/cli/src/serve/routes/session.ts#L1376-L1424 与 #L1508-L1603):
- 不搜索其他工作区,也不回退到主工作区;
- 不解析 live owner、不启动 ACP、不附加客户端;
- 不加载 workspace 设置;
- 归档(archived)会话不可用(归档导出由单独的
/workspaces/:workspace/session/:id/archive/export路由承载,见 session.ts#L5534-L5552)。
在成功路径上,新路由复用与旧路由完全相同的 formatter、文件名清理、MIME 类型、缓存策略与附件头。从实现可以看到具体行为(session.ts#L1577-L1585):
res .status(200) .set('Cache-Control', 'no-store') .set('X-Content-Type-Options', 'nosniff') .set('Content-Type', result.mimeType) .set('Content-Disposition', `attachment; filename="${filename}"`) .send(result.content);其中文件名会先经过filename.replace(/["\\\r\n]/g, '_')清理,防止响应头注入。
错误契约
新路由的错误响应复用既有的 export/storage 错误形状,覆盖以下错误码:
| HTTP 状态码 | 错误码 | 触发场景 |
|---|---|---|
| 400 | workspace_mismatch | workspace 选择器无法解析为已注册工作区 |
| 403 | untrusted_workspace | 选中的次工作区 runtime 不受信任 |
| 400 | invalid_export_format | format参数不在html/md/json/jsonl内,响应体附带allowedFormats |
| 404 | session_not_found | 目标工作区中不存在该 session id |
| 409 | session_archived/session_archiving/session_conflict | 会话归档状态与导出目标冲突 |
实现中parseSessionExportFormat对非法格式直接返回400 invalid_export_format并携带允许值列表;session_not_found的 404 响应还会回传sessionId字段(见 session.ts#L1587-L1593)。
能力通告与兼容性:workspace_session_export
新能力标签workspace_session_export在 packages/cli/src/serve/capabilities.ts#L442-L445 中声明为无条件(unconditional)v1 能力:
// Workspace-qualified full session export from active persisted storage. // This is separate from `session_export` so clients do not infer the plural // route from the legacy primary-workspace export capability. workspace_session_export: { since: 'v1' },设计上做"无条件"的原因很务实:复数路由对于受信任的单工作区主工作区同样有用(可以通过 id 或 cwd 显式选中主工作区)。信任仍然在每个请求上独立评估,并不因为能力被通告就放行。
该标签的独立性体现在三处关键约束:
- 与
multi_workspace_sessions相互独立,互不推断; - 不能从
session_export或workspace_qualified_rest_core推断出来——已发布的旧 daemon 会同时通告这两个旧标签,但并不实现这条新路由,因此客户端必须显式 pre-flight 检查workspace_session_export; - 对应的归档导出能力是单独的
workspace_archived_session_export(capabilities.ts#L446-L449),避免旧 daemon 把归档意图悄悄降级成活动 transcript。
在兼容性层面,当 SDK 直接调用者把新方法发往旧 daemon 时,只会收到正常的 HTTP 错误,不会产生任何意外行为。Web Shell 集成不在此变更范围内,其既有的仅主工作区导出行为保持不变——这也意味着该能力的消费方目前主要是 SDK / REST 客户端。
并发与安全设计
归档协调器共享锁
导出复用了既有的共享归档协调器锁(shared archive-coordinator lock),锁按 session id 键控(见 session.ts#L1543 中archiveCoordinator.runSharedMany([sessionId], ...))。这样做的目的是:在导出重放(replay)期间,归档(archive)与删除(delete)操作不能移动或移除正在读取的文件。
设计文档同时坦承该协调器保守地保持全局性:不同工作区中相同 id 的会话,即便文件彼此独立,也可能发生串行化。把所有 archive/delete 锁键改为带 workspace 限定(keying by workspace + session id)被明确列为超出本次变更范围的工作。
全量导出与受信任边界
与有界(bounded)的持久化 transcript 分页器不同,全量导出会物化完整 transcript,因此绝不提供给不受信任的次工作区。这也正是上述403 untrusted_workspace存在的原因:不受信任的次工作区只能通过受控的分页接口读取有界 transcript,而不能拿到全量导出。
响应大小预算方面,既有受信任导出没有新增的响应大小上限。设计文档解释:若给 workspace 专属导出加一个限制,会让复数路由与旧路由的 format 契约产生分歧(diverge)。安全边界仍由以下既有机制兜底:
- daemon bearer 认证;
- 默认的 GET 读速率层级(read-rate tier);
- 按请求执行的 workspace 信任检查。
运行时移除竞态
运行时移除(runtime removal)竞态使用请求解析时选中的那个 runtime,而移除操作并不会删除 transcript 存储。因此导出不需要 runtime 租约(lease),也不会维持 ACP 子进程存活——导出的生命周期只依赖持久的 transcript 文件,不依赖运行时进程。
SDK 与可观测性:WorkspaceDaemonClient.exportSession
SDK 方法
SDK 层提供WorkspaceDaemonClient.exportSession,其设计要点(源码见 packages/sdk-typescript/src/daemon/DaemonClient.ts#L3283-L3295):
- 复用既有导出结果类型与格式类型(
DaemonSessionExportResult、DaemonSessionExportFormat),不新增类型族; - 总是使用原生 REST(
mode: 'rest'),即使父客户端配置了 ACP transport 也不例外; - 通过共享请求助手
sessionExportRequest保留 token、客户端身份、超时、错误解析、Content-Type 与附件文件名行为,保证与旧路由 SDK 调用体验一致。
调用示例(workspace-qualified 形态):
const result = await client.exportSessionFromWorkspace( workspaceSelector, // 注册的 workspace id 或 URL 编码的绝对 cwd sessionId, { format: 'md' }, );SDK 内部会将该调用归一为GET /workspaces/:workspace/session/:id/export请求;若服务端 daemon 不支持,则按普通 HTTP 错误处理。
遥测规范化
Daemon 遥测层将新路径规范化为GET /workspaces/:workspace/session/:id/export(见 packages/cli/src/serve/server/telemetry.ts#L630),并:
- 解码 session id;
- 使用中间件层完成的 workspace 解析结果,记录所选 workspace 的 hash作为遥测属性。
这样做的价值在于:同一会话 id 在不同工作区中导出时,遥测数据仍能准确归属到对应 workspace,避免跨工作区的指标串扰。
被否决的备选方案
设计文档记录了四个被明确否决的替代方案,理解它们有助于把握本方案的边界取舍:
- 按 live owner 路由单数导出:对非活动(inactive)的持久化会话无法工作,且重启后 owner 归属变得模糊;
- 给旧路由增加
cwd查询参数:会改变主工作区专属的兼容性契约,也不如既有复数 workspace 路由风格一致; - miss 时回退主工作区:当 id 冲突时,可能导出另一个工作区的会话,正是本设计要消除的错误;
- 允许不受信任的全量导出:会绕过为持久化 transcript 分页器设计的有界读取策略,破坏安全边界。
验证:测试矩阵与端到端手段
该设计文档要求覆盖的验证面非常广,结合仓库中的测试布局(packages/cli/src/serve/multi-workspace-sessions.test.ts 等),可以归纳为以下维度:
- 能力通告:
workspace_session_export是否按 v1 无条件通告,且不与session_export/workspace_qualified_rest_core互相推断; - 选择器:id 与 cwd 两种选择路径的正确解析,以及 unknown/missing 目标的错误响应;
- 同 id 隔离:相同 session id 在不同 workspace 中存在时,导出严格命中目标 workspace 的 transcript;
- 格式矩阵:
html/md/json/jsonl每种格式的导出内容与 MIME 类型; - 响应头:
Cache-Control: no-store、X-Content-Type-Options: nosniff、Content-Disposition附件头与清理后的文件名; - 信任与归档边界:不受信任次工作区返回
403、归档会话不可通过 active 路由导出、session_conflict等 409 语义; - 无桥接活动:导出过程不启动 ACP、不附加客户端、不加载 workspace 设置;
- 遥测归属:规范化路径、session id 解码、workspace hash 属性;
- SDK 传输与编码:原生 REST 强制、URL 编码的 workspace cwd 与 session id、错误解析复用;
- 归档/删除协调:与
archiveCoordinator共享锁的并发行为。
端到端验证采用隔离的 runtime 与 workspace 目录,配合确定性的持久化 transcript,从而保证导出结果可复现、可断言。
小结
daemon-multi-workspace-session-export设计为 qwen-code daemon 补齐了多工作区场景下的会话导出能力:以GET /workspaces/:workspace/session/:id/export复数路由替代主绑定的单数路由,通过"精确 id → 规范化 cwd"的选择规则与逐请求信任检查杜绝跨工作区串台,同时保持旧路由、旧能力标签与既有错误契约的完全兼容。对于希望基于 REST/SDK 构建多工作区工具链的开发者,这条路由与workspace_session_export能力标签是当前仓库中直接可用的标准入口;如需进一步深入,建议继续阅读 路由实现、能力声明 与 SDK 客户端 中的对应代码,并结合 多工作区会话测试 验证行为边界。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考