Skip to content

功能规格说明:文件系统工具 ​

规格文件:docs/specs/core/fs-tools.md创建日期:2024-12-19

用户场景与测试 (必填) ​

用户故事:读取和分析文件(优先级:P1) ​

作为 AI 代理,我希望读取本地文件系统中文件的内容,以便理解代码库、分析日志或查看图片和笔记本。

优先级原因:这是任何代码相关任务所需的基本能力。

独立测试:可以通过在各种文件类型(文本、图片、.ipynb)上调用 Read 工具并验证输出格式和内容来测试。

验收场景:

  1. 假设 文本文件存在,当 代理调用 Read 时,则 它必须接收到带有行号和结构化元数据的内容。
  2. 假设 文件较大,当 代理使用 offset 和 limit 时,则 它必须仅接收到指定的块。
  3. 假设 是图片文件,当 代理调用 Read 时,则 它必须接收到适合多模态分析的图像数据和结构化元数据。
  4. 假设 文件自上次读取以来未发生变化,当 代理调用 Read 时,则 它必须接收到表示文件未变的最小响应。

用户故事:精确代码修改(优先级:P1) ​

作为 AI 代理,我希望使用精确的字符串替换来修改文件,以便在不破坏文件结构的情况下安全地应用更改。

优先级原因:对于可靠地执行重构和错误修复至关重要。

独立测试:可以通过对文件应用 Edit 并验证内容是否正确更新以及缩进是否保留来测试。

验收场景:

  1. 假设 文件已被读取,当 代理使用唯一的 old_string 调用 Edit 时,则 文件必须使用 new_string 更新。
  2. 假设 old_string 不唯一,当 代理不使用 replace_all 调用 Edit 时,则 操作必须失败。
  3. 假设 一个 Edit 工具调用,当 显示差异时,则 当删除和添加的行数匹配时,它必须显示已更改部分的词级高亮。
  4. 假设 文件使用 CRLF 换行符,当 代理使用 LF 的 old_string 调用 Edit 时,则 通过在比较之前规范化换行符,匹配必须成功。
  5. 假设 文件尚未被读取,当 代理调用 Edit 时,则 操作必须失败,并返回指示代理先读取文件的消息。
  6. 假设 一个 Edit 工具调用中 old_string 未找到,当 返回错误时,则 它必须包含尝试的字符串以帮助模型自我纠正。
  7. 假设 文件在上次完整读取后被外部进程(git checkout、编辑器原地保存、云同步、杀毒软件等)触碰导致 mtime 变化但内容未变,当 代理调用 Edit 时,则 操作必须成功,不得误报"自上次读取后被修改"。
  8. 假设 文件自上次读取后确实被修改(内容变化),当 代理调用 Edit 时,则 操作必须失败并返回指示重新读取文件的消息。
  9. 假设 文件是分页读取(提供了 offset 或 limit)后 mtime 变新,当 代理调用 Edit 时,则 不进行内容兜底,必须失败并要求重新读取(分页读取只缓存了部分内容,无法可靠比较)。

用户故事:原子写文件(优先级:P2) ​

作为用户,我希望代理写文件时先写同目录临时文件再原子重命名,以便其他并发读者(后台提取 fork、编辑器、其他会话)不会读到写了一半的文件。

优先级原因:Write/Edit 直接覆写目标文件时,写入过程中目标文件会短暂处于被截断状态;后台自动记忆提取 fork 与主会话可能并发读写同一记忆文件,读到半写内容会把坏数据带进上下文。对齐 Claude Code 的临时文件 + 重命名写入。

独立测试:调用 Write/Edit 写文件,验证写入经过同目录临时文件并重命名到目标路径;模拟写入失败,验证目标路径不残留部分内容、临时文件被清理。

验收场景:

  1. 假设 代理调用 Write 或 Edit 写入文件,当 执行写入时,则 必须先把内容写入目标路径同目录下的临时文件、再重命名到目标路径
  2. 假设 写入过程中失败(临时文件写入失败或重命名失败),当 工具返回失败时,则 目标路径不得残留部分写入的内容,且必须清理临时文件
  3. 假设 目标文件已存在且已按读取前写入/陈旧检测(OCC)通过校验,当 以原子方式替换它时,则 写入前读取与陈旧检测的语义必须保持不变

用户故事:高效代码探索(优先级:P2) ​

作为 AI 代理,我希望使用 glob 模式或正则表达式搜索模式,以便在大型项目中快速定位相关代码或资源。

优先级原因:通过避免手动遍历目录树来提高效率。

独立测试:可以通过使用特定模式运行 Glob 或 Grep 并验证返回了正确的文件和行来测试。

验收场景:

  1. 假设 有搜索模式,当 代理调用 Grep 时,则 它必须接收到匹配的行或文件路径,遵循 .gitignore 和常见忽略模式,以及包含匹配计数的结构化元数据。
  2. 假设 有类似 **/*.ts 的 glob 模式,当 代理调用 Glob 时,则 它必须接收到匹配的 TypeScript 文件列表,包括被 .gitignore 忽略的文件(但排除 .git 目录),最多到指定的 limit(默认 100),以及结构化元数据。
  3. 假设 搜索模式没有匹配结果,当 代理调用 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 已正确配置以处理文件系统访问级别。