qwen-code 会话附件引用(Session Attachment References)设计解析:从 base64 内联到文件名引用的存储与生命周期
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
导读
本文以 qwen-code 设计文档 docs/design/session-attachment-references.md 为骨架,系统讲解 daemon 如何将图片与任意文件字节落盘到会话附件目录、以文件名引用(attachment reference)替代 base64/字节内联,从而避免在 daemon 请求、队列、事件与回放(replay)数据中重复携带大体积负载的设计。读完本文,你将掌握附件引用的数据结构与命名规则、存储位置与所有权模型、8 MiB 单文件上限等边界约束,以及session_attachments能力与/session/:id/attachmentsHTTP 路由在 TypeScript SDK 与 web-shell 中的真实调用形态。
背景问题:内联负载带来的重复膨胀
在引入附件引用之前,图片 base64 与文件字节会被直接嵌入 daemon 的请求体、消息队列、事件流和会话回放数据中。同一份附件在「上传 → 排队 → 转发给 ACP 子进程 → 会话记录回放」的每一跳都会被复制一份,产生以下问题:
- 负载重复膨胀:同一字节在多份请求/事件中反复出现,内存与网络开销随链路长度线性放大;
- 回放困难:附件需要在 daemon 重启后仍可预览,内联字节若只存在于进程内存中,重启即丢失;
- 无统一出处:不同客户端各自携带字节,缺少单一可信来源(single source of truth)。
设计文档给出的解法非常直接:daemon 将图片和任意文件字节写入 workspace runtime 的附件目录,只返回一个基于文件名的引用,后续所有链路传递的都是这个轻量引用。
核心数据结构:基于文件名的附件引用
daemon 在存储附件后返回如下引用对象:
{ type: 'image' | 'resource'; attachmentId: string; mimeType: string; size: number; }各字段含义:
| 字段 | 说明 |
|---|---|
type | image(图片)或resource(任意文件资源) |
attachmentId | 附件在存储目录中的文件名,即引用本身 |
mimeType | 附件 MIME 类型(如image/png、application/pdf) |
size | 附件字节大小 |
这一结构在 TypeScript SDK 中有完全对应的类型定义,见 packages/sdk-typescript/src/daemon/types.ts#L4676-L4681:
export type DaemonSessionAttachmentReference = Record<string, unknown> & { type: 'image' | 'resource'; attachmentId: string; mimeType: string; size: number; };配套的读取结果类型为DaemonSessionAttachmentData({ data: string; mimeType: string },data 为 base64 字符串),见 packages/sdk-typescript/src/daemon/types.ts#L4683-L4686。
命名规则与去重
- attachmentId 就是存储文件名,没有独立的 ID 体系;
- 重名文件采用平台通用约定追加序号:
name (1).ext、name (2).ext,以此类推; - 不存在内存中的附件索引,也没有 sidecar 元数据;MIME 类型与大小在读取时直接从存储文件推导,从而避免了索引与实际文件之间的一致性维护成本。
引用在链路中的流转
Prompt 与 mid-turn API 将引用对象随队列、事件、会话记录元数据(transcript metadata)一起传递,但只有真正向 ACP 子进程派发(dispatch)时才由 bridge 解析引用——即「引用轻量传递、字节延迟物化」。
- TypeScript 会话客户端通过带鉴权的附件路由(authenticated attachment route)读取同一份引用,用于预览与回放渲染;
- 文本类资源解析为 ACP text;其他文件格式解析为 ACP blob,原始字节不会在浏览器中被解码或改写。
对应实现中,上传与解析的桥接逻辑位于 packages/channels/base/src/DaemonChannelBridge.ts(含removeAttachment钩子、session_attachments能力预检与按批次扇出上传、以及不支持该能力时的图片内联降级路径);浏览器侧的预览/回放水合逻辑见 packages/web-shell/client/daemon/session/actions.ts 与 packages/web-shell/client/daemon/session/types.ts。
存储位置与所有权模型
存储路径
附件落盘于 workspace runtime 的附件目录:
~/.qwen/tmp/<workspace-hash>/attachments/session-<id>/其中<workspace-hash>是 workspace 的哈希标识,session-<id>是会话 ID;使用自定义 runtime 目录时,路径等价替换。
所有权与鉴权
- resolved live-session owner 与客户端授权保护每一次上传(upload)、读取(read)与移除(remove)操作;
- 关闭 daemon 或客户端断开连接只关闭文件句柄,不会删除文件——附件是持久化资源,不随连接生命周期消失;
- 永久删除会话时,其附件目录一并移除,避免孤儿文件残留。
生命周期策略:刻意从简
设计上刻意排除了常见的复杂回收机制:
- 无 TTL:附件不设过期时间;
- 无 sweeper:没有后台清扫任务;
- 无 retained-media cache:不维护媒体缓存;
- 无重启重建索引:daemon 重启不需要重建任何附件索引。
这与「无内存索引、无 sidecar 元数据」的设计一脉相承:附件就是普通文件,文件名即 ID,状态完全可由文件系统自身表达。
大小限制与配额
- 单个附件上限 8 MiB(
8 * 1024 * 1024字节); - 会话没有累计附件大小或数量上限。
在源码中,单文件上限常量定义为SESSION_ATTACHMENT_MAX_ITEM_BYTES = 8 * 1024 * 1024,另有SESSION_ATTACHMENT_MAX_NAME_BYTES = 255限制文件名长度,见 packages/acp-bridge/src/sessionAttachments.ts#L13-L14。同一文件中还定义了受支持的图片 MIME 类型集合(image/bmp、image/gif、image/jpeg、image/png、image/webp)。
能力声明与统一 HTTP 接口
能力:session_attachments
统一能力标识为session_attachments,在 serve 能力表中声明为v1起可用,见 packages/cli/src/serve/capabilities.ts#L57-L58。
HTTP 表面:/session/:id/attachments
统一 HTTP 路由为/session/:id/attachments,不存在session_media、/media或mediaId之类的兼容路径——设计上明确只保留一套接口。
TypeScript SDK 在 packages/sdk-typescript/src/daemon/DaemonClient.ts 中提供了四个对应的客户端方法:
| 方法 | HTTP 路由 | 作用 |
|---|---|---|
uploadSessionAttachment(sessionId, data, name, mimeType, opts?) | POST /session/:id/attachments?name=<name> | 上传字节,请求体为原始字节流,Content-Type即附件 MIME,返回引用对象 |
readSessionAttachment(sessionId, attachmentId, opts?) | GET /session/:id/attachments/:attachmentId | 读取附件内容,返回{ data: base64, mimeType } |
listSessionAttachments(sessionId, opts?) | GET /session/:id/attachments | 按上传顺序列出该会话当前存储的全部附件引用 |
removeSessionAttachment(sessionId, attachmentId, opts?) | DELETE /session/:id/attachments/:attachmentId | 删除附件,返回{ removed: boolean } |
从实现细节可以印证设计中的几个要点:
- 引用即文件名:上传 URL 以 query 参数
name携带原始文件名,daemon 据此落盘并返回attachmentId(即存储文件名),见 DaemonClient.ts#L4080-L4102; - 读取时推导 MIME 与大小:读取响应的
mimeType直接取自响应头content-type,缺失时回退为application/octet-stream,见 DaemonClient.ts#L4128-L4163; - 浏览器兼容:读取实现将
Uint8Array分块(每块0x8000字节)再btoa编码,避免超出引擎参数上限,注释明确说明该包同时面向 Node 与浏览器环境; - mid-turn 预检:
enqueueMidTurnMessage的文档注释提示调用方应先预检session_attachments能力,旧 daemon 会忽略 media 字段并丢弃图片内容,见 DaemonClient.ts#L4212-L4229。
web-shell 会话层将这四个方法进一步封装为uploadAttachment/readAttachment/listAttachments/removeAttachment(含read_attachment、remove_attachment等权限动作),并会在附加附件块前预检能力,见 packages/web-shell/client/daemon/session/types.ts#L565-L581 与 packages/web-shell/client/daemon/session/actions.ts#L2546-L2625。
实现层面的安全加固:目录防替换校验
虽然设计文档保持简洁,但从实现可以看到一层额外的持久化安全措施:附件存储目录被包装为DurableAttachmentDirectory,通过持有目录句柄并校验dev(设备号)与ino(inode 号)来检测目录是否被替换(如符号链接攻击或目录被删除重建),校验失败时抛出'Session attachment parent directory changed.',见 packages/acp-bridge/src/sessionAttachments.ts#L23-L71。这层校验与文档「resolved live-session owner 和客户端授权保护每一次上传、读取、移除」的所有权要求互相配合,共同构成附件读写的安全边界。
小结:引用式附件设计的取舍
会话附件引用方案的核心取舍可以概括为:
- 以文件名替代字节内联,让队列、事件与回放数据只携带轻量引用,字节只存在于磁盘与最终物化环节;
- 文件系统即状态,无索引、无 sidecar、无 TTL、无重建,生命周期完全由会话删除驱动,换来极低的维护复杂度;
- 边界清晰:单文件 8 MiB、文件名 255 字节上限、统一
session_attachments能力与/session/:id/attachments路由,且不保留任何旧式/media兼容路径。
对需要集成 qwen-code daemon 的客户端而言,接入路径非常明确:预检session_attachments能力 → 通过POST /session/:id/attachments上传并拿到attachmentId→ 在 prompt/mid-turn 内容块中携带引用 → 需要预览或回放时通过GET路由按 ID 读取、按需DELETE。相关类型定义、客户端方法与桥接实现均可在 packages/sdk-typescript/src/daemon、packages/acp-bridge/src/sessionAttachments.ts 与 packages/channels/base/src/DaemonChannelBridge.ts 中进一步查阅。
【免费下载链接】qwen-codeAn open-source AI coding agent that lives in your terminal.项目地址: https://gitcode.com/GitHub_Trending/qw/qwen-code
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考