Appearance
功能规格说明:文件系统工具
规格文件:docs/specs/core/fs-tools.md创建日期:2024-12-19
用户场景与测试 (必填)
用户故事:读取和分析文件(优先级:P1)
作为 AI 代理,我希望读取本地文件系统中文件的内容,以便理解代码库、分析日志或查看图片和笔记本。
优先级原因:这是任何代码相关任务所需的基本能力。
独立测试:可以通过在各种文件类型(文本、图片、.ipynb)上调用 Read 工具并验证输出格式和内容来测试。
验收场景:
- 假设 文本文件存在,当 代理调用
Read时,则 它必须接收到带有行号和结构化元数据的内容。 - 假设 文件较大,当 代理使用
offset和limit时,则 它必须仅接收到指定的块。 - 假设 是图片文件,当 代理调用
Read时,则 它必须接收到适合多模态分析的图像数据和结构化元数据。 - 假设 文件自上次读取以来未发生变化,当 代理调用
Read时,则 它必须接收到表示文件未变的最小响应。
用户故事:精确代码修改(优先级:P1)
作为 AI 代理,我希望使用精确的字符串替换来修改文件,以便在不破坏文件结构的情况下安全地应用更改。
优先级原因:对于可靠地执行重构和错误修复至关重要。
独立测试:可以通过对文件应用 Edit 并验证内容是否正确更新以及缩进是否保留来测试。
验收场景:
- 假设 文件已被读取,当 代理使用唯一的
old_string调用Edit时,则 文件必须使用new_string更新。 - 假设
old_string不唯一,当 代理不使用replace_all调用Edit时,则 操作必须失败。 - 假设 一个
Edit工具调用,当 显示差异时,则 当删除和添加的行数匹配时,它必须显示已更改部分的词级高亮。 - 假设 文件使用 CRLF 换行符,当 代理使用 LF 的
old_string调用Edit时,则 通过在比较之前规范化换行符,匹配必须成功。 - 假设 文件尚未被读取,当 代理调用
Edit时,则 操作必须失败,并返回指示代理先读取文件的消息。 - 假设 一个
Edit工具调用中old_string未找到,当 返回错误时,则 它必须包含尝试的字符串以帮助模型自我纠正。 - 假设 文件在上次完整读取后被外部进程(git checkout、编辑器原地保存、云同步、杀毒软件等)触碰导致 mtime 变化但内容未变,当 代理调用
Edit时,则 操作必须成功,不得误报"自上次读取后被修改"。 - 假设 文件自上次读取后确实被修改(内容变化),当 代理调用
Edit时,则 操作必须失败并返回指示重新读取文件的消息。 - 假设 文件是分页读取(提供了
offset或limit)后 mtime 变新,当 代理调用Edit时,则 不进行内容兜底,必须失败并要求重新读取(分页读取只缓存了部分内容,无法可靠比较)。
用户故事:原子写文件(优先级:P2)
作为用户,我希望代理写文件时先写同目录临时文件再原子重命名,以便其他并发读者(后台提取 fork、编辑器、其他会话)不会读到写了一半的文件。
优先级原因:Write/Edit 直接覆写目标文件时,写入过程中目标文件会短暂处于被截断状态;后台自动记忆提取 fork 与主会话可能并发读写同一记忆文件,读到半写内容会把坏数据带进上下文。对齐 Claude Code 的临时文件 + 重命名写入。
独立测试:调用 Write/Edit 写文件,验证写入经过同目录临时文件并重命名到目标路径;模拟写入失败,验证目标路径不残留部分内容、临时文件被清理。
验收场景:
- 假设 代理调用
Write或Edit写入文件,当 执行写入时,则 必须先把内容写入目标路径同目录下的临时文件、再重命名到目标路径 - 假设 写入过程中失败(临时文件写入失败或重命名失败),当 工具返回失败时,则 目标路径不得残留部分写入的内容,且必须清理临时文件
- 假设 目标文件已存在且已按读取前写入/陈旧检测(OCC)通过校验,当 以原子方式替换它时,则 写入前读取与陈旧检测的语义必须保持不变
用户故事:高效代码探索(优先级:P2)
作为 AI 代理,我希望使用 glob 模式或正则表达式搜索模式,以便在大型项目中快速定位相关代码或资源。
优先级原因:通过避免手动遍历目录树来提高效率。
独立测试:可以通过使用特定模式运行 Glob 或 Grep 并验证返回了正确的文件和行来测试。
验收场景:
- 假设 有搜索模式,当 代理调用
Grep时,则 它必须接收到匹配的行或文件路径,遵循.gitignore和常见忽略模式,以及包含匹配计数的结构化元数据。 - 假设 有类似
**/*.ts的 glob 模式,当 代理调用Glob时,则 它必须接收到匹配的 TypeScript 文件列表,包括被.gitignore忽略的文件(但排除.git目录),最多到指定的limit(默认 100),以及结构化元数据。 - 假设 搜索模式没有匹配结果,当 代理调用
Grep时,则 它必须接收到建议指定path字段以在忽略或其他目录中搜索的消息。
边界情况
- 大文件:读取超出内存限制或令牌窗口的文件。文件大小超过 256KB 且未显式提供
limit参数时,工具直接返回错误。通过offset和limit分页读取。如果估计令牌超过maxTokens,工具返回错误并建议使用 offset/limit 或 grep/jq。 - 二进制文档:尝试读取 PDF、DOCX 或其他不支持的二进制格式。工具必须阻止此操作并返回错误。
- 不匹配分析:当未找到
old_string时,Edit工具必须提供详细的不匹配报告,包括尝试的字符串(截断为 200 个字符)以帮助模型自我纠正。 - 编辑前读取:
Edit工具必须拒绝编辑在当前对话中尚未读取的文件,防止盲目编辑。Grep工具不会将文件登记为已读取,因此仅Grep后直接Edit仍会被拒绝。 - 写入前读取:
Write工具必须拒绝覆盖在当前对话中尚未读取的已存在文件;新建文件不受此限制。Grep不登记为已读取,因此仅Grep后直接覆盖Write仍会被拒绝。 - 陈旧检测:
Edit与Write工具在写入前比较文件当前 mtime 与readFileState中记录的 mtime。仅当文件变新(当前 mtime 晚于记录值)时判定为可能被修改;此时若该次读取为完整读取(未提供offset和limit)且文件当前内容哈希与记录值一致,则视为内容未变、允许继续,避免 git checkout / 编辑器原地保存 / 云同步 / 杀毒软件等导致的误报。分页读取不享受此兜底,mtime 变新即拒绝。Write仅对已存在文件进行该检测,新建文件不检测。 - CRLF 规范化:
Edit工具必须在匹配前将\r\n规范化为\n,这样模型可以在old_string中使用 LF 换行符,无论文件的实际换行符如何。 - 最小 old_string:
Edit工具提示应引导模型使用明显唯一的最小old_string(通常为 2-4 行相邻文本),减少在长模板字面量文本上的复现错误。 - 文件权限:尝试写入没有适当权限的只读文件或目录。
- 原子写:
Write/Edit及记忆文件写入必须走「同目录临时文件 + 重命名」的原子路径,避免并发读者观察到被截断的内容;写失败时必须清理临时文件且不破坏目标文件的既有内容。
假设
- 代理具有访问工作区目录所需的系统级权限。
ripgrep(rg)二进制由@vscode/ripgrep依赖提供(通过optionalDependencies按平台分发),供Grep工具使用。npm 安装的wave-code自带该依赖;宿主自带的那份 CLI 不带node_modules,由 CLI 在启动期按需从 npmmirror 下载到共享~/.wave/cli/node_modules/@vscode/(机制见 stdio-transport.md「边界情况 · 运行时依赖自装与缓存」)。二进制路径每次使用时再解析(不在模块求值期定死——依赖是 CLI 自己在启动期装进来的,晚于模块求值):平台二进制包缺失时Grep返回"ripgrep is not available"(文件搜索同理报错),既不使宿主进程在加载时崩溃,也不因下载失败阻断 CLI 启动——桌面端与 IDE 插件宿主的 grep 运行在 CLI 子进程中,宿主自身不消费该路径。PermissionManager已正确配置以处理文件系统访问级别。