读、写、改、搜

五个文件工具的实现:Read 怎样分流文本、图片、PDF 与视频并计入预算,read-file-state 怎样落实“先读后改”,Edit 的八级匹配与唯一性要求,文件系统 adapter 的原子写与编码,以及默认搜索为何改走 Bash 里的 bfs 与 ugrep、这些原生工具怎样分发到各平台。

作者 David更新于 16 篇(共 47 篇)

模型读写代码靠五个内置工具:ReadWriteEditGlobGrep,实现在 apps/zcode-cli/packages/core/src/tool/handlers。它们不直接调用 node:fs,而是经 FileSystemPort 交给 apps/zcode-cli/packages/adapters/src/fs 里的 NodeFileSystemAdapter;图片交给基于 Jimp 的 ImageProcessorPort,PDF 交给调用 Poppler 命令行的 PdfDocumentPort。改文件之前的新鲜度检查靠一张 read-file-state 表,搜索则依赖随发行物分发的三个原生工具:bfs、ugrep、ripgrep。

有一点先说在前面:默认配置下模型看不到 GlobGrep。只要 Bash 可用,运行时就把这两个工具从注册表里摘掉,让模型直接在 Bash 里写 findgrep,再由注入的 Shell 函数把它们换成 bfs 和 ugrep。工具怎样注册与过滤见工具契约、注册表与可见性,调用流水线见执行器:调度、审批、超时与结果,Bash 本身见Bash:解析、只读判定与后台任务

怎么用

工具参数只读默认审批要点
Readfile_pathoffsetlimit,模型支持 PDF 时加 pages文本带行号;图片、视频、PDF 各有分支
Writefile_pathcontent整文件覆盖;覆盖已有文件前必须完整读过
Editfile_pathold_stringnew_stringreplace_allold_string 须唯一;精确匹配失败再试七种宽松策略
Globpatternpath按修改时间倒序,最多 100 条;默认不可见
Greppatternpathglobtypeoutput_mode-A-B-C-n-i-ohead_limitoffsetmultiline默认只列文件,最多 250 条;默认不可见

参数定义在契约包(如 apps/zcode-cli/packages/contracts/src/tools/read.ts:65apps/zcode-cli/packages/contracts/src/tools/edit.ts:18apps/zcode-cli/packages/contracts/src/tools/grep.ts:22),只读、并发安全、超时与结果预算写在各 handler 的 ToolEntry 里(如 apps/zcode-cli/packages/core/src/tool/handlers/read.ts:464apps/zcode-cli/packages/core/src/tool/handlers/edit.ts:254)。Read、Glob、Grep 并发安全,Write、Edit 不是;五个工具的超时都是 30 秒且不许调用方改,唯一例外是 Read 的 PDF 分页(150 秒,handlers/read.ts:510)。

  • 审批:Write 与 Edit 的权限类别是 edit,build 模式下逐次询问,edit 模式直接放行(apps/zcode-cli/packages/core/src/permission/service.ts:518)。细节见权限模式与规则
  • 路径:描述要求绝对路径,实际上相对路径会按当前工作目录解析,工作区之外的路径也不拦,注释给的理由是子 Agent 可能要查看用户指定的兄弟仓库(apps/zcode-cli/packages/core/src/tool/path-policy.ts:32:36)。
  • 设置:桌面端“增强 Find 和 Grep”默认打开(packages/shared/src/validationAppSettings.ts:461,文案见 packages/ui/src/i18n/locales/zh-CN.ts:1713)。关掉后 Bash 里的 findgrep 不再被替换,但 GlobGrep 也不会因此回来。

Read:按类型分流

图表加载中…

预检在 schema 层完成:12 个会阻塞或无限输出的设备路径(/dev/zero/dev/stdin 等)与 19 种二进制扩展名(.zip.so.wasm 等)直接拒绝(apps/zcode-cli/packages/contracts/src/tools/read.ts:29:50:129),错误文本以 <tool_use_error> 包裹返回给模型(apps/zcode-cli/packages/core/src/tool/handlers/read.ts:266)。之后按图片、视频、PDF、文本的顺序分流(handlers/read.ts:167)。

文本的几个数字:

  • 不给 limit 时文件不能超过 256 KiB(READ_MAX_FILE_SIZE_BYTESapps/zcode-cli/packages/contracts/src/tools/read.ts:15),超了报错并提示改用 offset、limit(apps/zcode-cli/packages/adapters/src/fs/text-range-reader.ts:37)。给了 limit 就不看文件大小:10 MiB 以内整读再切行,更大的流式逐行读(text-range-reader.ts:19:30)。
  • 输出上限约 25000 token(apps/zcode-cli/packages/contracts/src/tools/read.ts:16),按字符数除以 3 估算,中文字符按两个计(apps/zcode-cli/packages/core/src/context/utils.ts:12,除数在 packages/shared/src/usage-stats.ts:9)。超限时看是不是“首次整文件读”:是,就二分出不超过 85% 预算(21250 token)的最长行前缀作为 partial view 返回,并提示从哪一行续读;不是,直接报错(apps/zcode-cli/packages/core/src/tool/handlers/read-text.ts:18:161:238)。
  • 行号格式是“行号 + 制表符”(read-text.ts:85);空文件或 offset 越界时返回一段 <system-reminder> 提醒(read-text.ts:62)。
  • 工具描述写着“Reads up to 2000 lines by default”(apps/zcode-cli/packages/core/src/tool/handlers/read.ts:61),但 Read 并不按行截断:没有 limit 时只传字节上限(read-text.ts:47)。2000 行只出现在 partial view 的续读建议里,以及用户 @ 引用超过 256 KiB 的文本附件时(apps/zcode-cli/packages/core/src/runtime/helpers/attachments.ts:257)。
  • 同一范围再读、文件的整数毫秒 mtime 与大小都没变时,不再返回正文,只回一句“Wasted call — file unchanged since your last Read.”(apps/zcode-cli/packages/core/src/tool/handlers/read.ts:55:190:335)。文件不存在时会在父目录里找同名不同扩展名、或编辑距离不超过 3 的文件名,附一句“Did you mean”(handlers/read.ts:412)。
  • .ipynb 没有专门分支,按 JSON 文本读;契约里的 notebook 输出类型没有生产方(handlers/read.ts:95)。

解码在 detectTextEncodingapps/zcode-cli/packages/adapters/src/fs/text-metadata.ts:29):先认 UTF-8 与 UTF-16LE 的 BOM;出现 NUL 字节或控制字节超过 30% 判为二进制;合法 UTF-8 就按 UTF-8;否则依次试 GB2312、GBK、GB18030,用 iconv-lite 解码再编码,字节一致才采用(允许末尾丢掉至多 3 个字节,text-metadata.ts:131)。CRLF 读入时统一成 LF,原文件的换行风格另行记下(text-metadata.ts:189)。

图片限 jpg、jpeg、png、gif、webp(apps/zcode-cli/packages/core/src/tool/handlers/read-image.ts:134),读入上限 20 MiB,预算四项同时生效:base64 不超过 5 MiB、原始字节不超过其四分之三、长边不超过 2000 像素、每个 base64 字符折 0.125 个 token 且总数不超过 25000(apps/zcode-cli/packages/contracts/src/tools/read.ts:18,判定在 apps/zcode-cli/packages/adapters/src/image/image-budget.ts:43)。最紧的是 token 一项:25000 ÷ 0.125 = 200000 个 base64 字符,折合原图约 150000 字节,所以多数截图都会被重新编码。压缩阶梯在 apps/zcode-cli/packages/adapters/src/image/jimp-compression.ts:118:原图合格就原样;否则先在原尺寸内保持格式(PNG 以 deflate 9 无损优化一次,JPEG 依次试 80、60、40、20 的质量);再缩到 2000 像素长边;再按 0.75、0.5、0.25 逐级缩小;最后以质量 20 的 JPEG 把长边压到 1000 至 200 像素(jimp-compression.ts:32)。WebP 不能转码,超预算直接报错(apps/zcode-cli/packages/adapters/src/image/webp-passthrough.ts:13)。交给模型的只有图片块,尺寸只留在结构化输出里,注释说这样 provider 可见内容不会随是否缩放而变(apps/zcode-cli/packages/core/src/tool/handlers/read.ts:113)。

PDF 只在模型的 inputFormat.supportsPdf 为真时才有分支,描述与 schema 也只在这时多出 pagesapps/zcode-cli/packages/core/src/tool/handlers/read-pdf.ts:57:65);否则 .pdf 落回文本分支。不带 pages 时整份 PDF 作为 file 块交给模型,限 20 MiB、10 页,页数用 pdfinfo 查(apps/zcode-cli/packages/contracts/src/tools/read-pdf.ts:5:7apps/zcode-cli/packages/adapters/src/pdf/index.ts:37)。带 pages 时每次最多 20 页、文件不超过 100 MiB,渲染超时 120 秒(同一契约文件 tools/read-pdf.ts:6:8:11),由 pdftoppm -jpeg -r 100 渲染(apps/zcode-cli/packages/adapters/src/pdf/index.ts:76),每页再过一遍图片预算,工具超时因此放宽到 150 秒(apps/zcode-cli/packages/core/src/tool/handlers/read-pdf.ts:230:51)。Poppler 不随包分发,没装时报 “pdftoppm is not installed” 并给出安装命令(apps/zcode-cli/packages/adapters/src/pdf/index.ts:139)。

视频限 mp4、m4v、mov、webm、mkv、avi,不超过 30 MiB(apps/zcode-cli/packages/core/src/runtime/helpers/attachment-video.ts:9packages/shared/src/zcode-media-policy.ts:2),不转码,文件头注释说 CLI 不引入 ffmpeg(apps/zcode-cli/packages/core/src/tool/handlers/read-video.ts:1)。

媒体结果真正进模型请求前还有两道投影(apps/zcode-cli/packages/core/src/runtime/helpers/media-budget.ts:51):模型不支持的类型换成一段文字占位(apps/zcode-cli/packages/core/src/runtime/helpers/media-capability.ts:30);所有媒体按编码后体积合计不超过 40 MiB,最近一条真实用户消息里的附件受保护,其余从新到旧保留,放不下的换成 “Media omitted” 占位(media-budget.ts:28:203:257)。

先读后改:read-file-state

每个 AgentRuntime 持有一张 ReadFileStateMapapps/zcode-cli/packages/core/src/runtime/agent-runtime.ts:278),键是“规范化路径 + offset + limit”(apps/zcode-cli/packages/core/src/tool/read-file-state.ts:18),值记着模型看到的内容、读取时间、来源工具,以及 revision、整数毫秒 mtime 与大小(apps/zcode-cli/packages/core/src/tool/types.ts:203)。isPartialView 只在正文被 token 上限截断时为真,offset、limit 的范围读不算(apps/zcode-cli/packages/core/src/tool/handlers/read.ts:371)。Write 写之前的检查(apps/zcode-cli/packages/core/src/tool/handlers/write.ts:278):

function assertWritableExistingFileIsFresh(
  filePath: string,
  currentRead: FileSystemReadTextResult,
  readFileState: ReadFileStateMap | undefined,
): void {
  const lastRead = findLatestReadFileState(readFileState, filePath);
  if (!lastRead || lastRead.isPartialView) {
    throw createCoreError(CoreErrorType.ToolExecutionFailed, WRITE_NOT_READ_MESSAGE, {
      context: {
        code: "write_file_not_read",
        filePath,
      },
      recoverable: true,
    });
  }

  if (!hasReadStateChanged(lastRead, currentRead)) return;
  if (isStrictFullRead(lastRead) && lastRead.content === currentRead.content) return;

  throw createCoreError(CoreErrorType.ToolExecutionFailed, WRITE_STALE_MESSAGE, {
    context: {
      code: "write_file_stale",
      filePath,
    },
    recoverable: true,
  });
}
  • 取哪条记录:同一路径下读取时间最新的一条,不论整读还是范围读。注释解释了原因:若优先整读记录,格式化器改完文件、模型按提示做了范围重读之后,Edit 仍拿旧记录比对,会一直误报 stale(read-file-state.ts:46)。
  • 没读过:没有记录,或最新记录是 partial view,报 “File has not been read yet”。被 token 上限截断的整读因此不能直接改,要先用 offset、limit 读到目标段落。
  • 读后变了:Write 先比 revision(形如 mtime:<整数毫秒>:size:<字节数>),再比 mtime 是否前进、大小是否变化(write.ts:306);Edit 顺序相反,先 mtime 与大小、后 revision(handlers/edit.ts:444)。变了但上次是完整整读、内容逐字未变,照样放行,只被 touch 过的文件不会误判。其余情况报 “File has been modified since read, either by the user or by a linter”。
  • 写后更新:成功后用新内容覆盖该路径的整读记录,来源标成 Write 或 Edit(write.ts:339handlers/edit.ts:572),回执末尾是 “file state is current in your context — no need to Read it back”(write.ts:42)。此时再完整 Read 一遍同一文件,只会得到上面那句 file_unchanged。

写盘时还有第二道闸:读到的 revision 作为 expectedRevision 传给 adapter,写前重新 stat 比对,不一致抛 stale_writeapps/zcode-cli/packages/adapters/src/fs/index.ts:473),挡住“检查之后、写入之前”被改的窗口。

另外三个地方会动这张表:

  • Bash:命令里带 --fix--writeblackcargo fmt 等格式化标记时,跑完扫一遍已读文件,mtime 晚于命令开始的,在结果后追加 “This command modified … Call Read before editing.”(apps/zcode-cli/packages/core/src/tool/handlers/bash-read-file-state.ts:20:189);反过来,catheadtailsed -n、单条 grep 读过且输出没被截断的文件会回填成已读(bash-read-file-state.ts:95:99)。
  • 压缩:上下文压缩后整张表清空(apps/zcode-cli/packages/core/src/runtime/methods/compact-active.ts:625),同时把最近读过的至多 5 个文件以提醒形式放回上下文,单个超过约 5000 token 或合计超过 50000 token 的只留一句“需要时再读”的提示(apps/zcode-cli/packages/core/src/runtime/helpers/compact-post-reminders.ts:20),见上下文压缩
  • 恢复会话:每次成功的 Read、Write、Edit 把一份带 revision、mtime、大小的快照写进 tool part 的 metadata(apps/zcode-cli/packages/core/src/tool/read-file-state-metadata.ts:22)。恢复时只重建整文件读,范围读不跨恢复保留,也不会去读当前磁盘“补全”状态,免得把用户手动保存的内容当成 Agent 已读(apps/zcode-cli/packages/core/src/agent/read-file-state-hydrator.ts:90:125),见SQLite 会话库

Edit:八级匹配

Edit 的检查顺序(handlers/edit.ts:104):old_stringnew_string 相同直接失败;文件不存在时,old_string 为空视为新建,否则报不存在;文件超过 1 GiB 拒绝;old_string 为空但文件有内容拒绝;.ipynb 让模型改用 NotebookEdit,但内置工具里并没有这个工具,名字只出现在排序表里(apps/zcode-cli/packages/core/src/tool/provider-visible-order.ts:17);然后是上一节的读状态检查。失败以错误码返回,定义在 apps/zcode-cli/packages/contracts/src/tools/edit.ts:179。匹配本身在 apps/zcode-cli/packages/core/src/tool/edit-matchers.ts:41

export function findEditMatch(input: {
  content: string;
  search: string;
  replaceAll: boolean;
}): EditMatchResult {
  const exact = collectExactCandidates(input.content, input.search);
  if (exact.length > 0) {
    return toMatchResult("exact", exact);
  }

  const strategies: EditMatchStrategy[] = [
    "quote_normalized",
    "line_number_prefix_stripped",
    "escape_normalized",
    "unicode_escape_normalized",
    "line_trimmed",
    "indentation_flexible",
    "block_anchor",
  ];

  for (const strategy of strategies) {
    if (input.replaceAll && BROAD_MATCHERS.has(strategy)) continue;
    const candidates = collectCandidates(strategy, input.content, input.search);
    if (candidates.length === 0) continue;
    return toMatchResult(strategy, candidates);
  }

  return { status: "not_found" };
}
策略做法
exact原样子串
quote_normalized弯引号折成直引号后比对,取回文件里的原片段
line_number_prefix_stripped每一行都带 Read 的行号前缀(数字加制表符,或数字加冒号空格)时去掉再找
escape_normalized\n\t\" 这类转义还原成真实字符
unicode_escape_normalized\uXXXX 还原成字符
line_trimmed逐行去掉首尾空白后整块相等
indentation_flexible至少两行,去掉公共缩进后相等
block_anchor至少三行,首尾两行去空白后相等,中间各行按编辑距离算的平均相似度不低于 0.8

每一级找到候选就停。候选片段不止一种即为歧义(edit-matchers.ts:132);选定片段后还要数它在文件里出现几次,超过一次且没开 replace_all 就报 “Found N matches of the string to replace”(apps/zcode-cli/packages/core/src/tool/handlers/edit.ts:225)。replace_all 会跳过后三种“宽”策略(edit-matchers.ts:25:62),免得一口气替换掉一批只是长得像的代码块。替换文本也有配套处理:按转义匹配上的,new_string 做同样的反转义(edit-matchers.ts:75);按引号匹配上而原文是弯引号的,new_string 里的直引号按上下文换成左右弯引号(edit-matchers.ts:82);new_string 为空且被删片段后紧跟换行时,连换行一起删;替换用回调函数,避免 $&$$ 被当成替换模式(handlers/edit.ts:608:631)。比对前 CRLF 统一成 LF,写回时按原文件风格还原(handlers/edit.ts:164:513)。

写完用 jsdiff 的 structuredPatch 生成带 3 行上下文的 hunk,最多计算 5 秒(apps/zcode-cli/packages/core/src/tool/diff.ts:4:5)。hunk 只进结构化输出供界面渲染,模型看到的是一句 “The file … has been updated successfully.”(apps/zcode-cli/packages/core/src/tool/handlers/edit.ts:67)。Write 覆盖已有文件时同样附 patch,新建时为空数组(write.ts:148)。

Write 与文件系统 adapter

Write 先尝试读原文件,既为了新鲜度检查,也为了沿用原编码与换行风格;读不到就当新建(write.ts:98)。写入固定带 createParents: trueatomic: truewrite.ts:125);写进记忆目录的 Markdown 会被补上来源会话 ID(write.ts:118),见项目记忆

adapter 这一侧(apps/zcode-cli/packages/adapters/src/fs/index.ts:291):

  • 编码:GB 系编码用 iconv-lite 写回,编码后再解码必须与原文一致,否则拒绝写入,免得把 GBK 文件里存不下的字符悄悄换掉(text-metadata.ts:62:115)。
  • 原子写atomicWritelstat,目标是符号链接就拒绝;在同目录创建 <原名>.tmp.<pid>.<随机串>,以 O_EXCLO_NOFOLLOW 打开,复制原文件权限位(保住脚本的执行位),fsyncrename 覆盖;任何一步失败就删掉临时文件,退回以 O_TRUNCO_NOFOLLOW 原地覆盖(fs/index.ts:700:717:740)。
  • 边界:端口只接受绝对路径,Node 错误码被映射成 not_foundpermission_deniedis_directory 等稳定类别,取消单独记为 cancelledfs/index.ts:779:804)。

搜索:默认交给 Bash

默认分支由一个常量打开(apps/zcode-cli/packages/core/src/embedded-search/capability.ts:4),唯一的前提是 Bash 在工具面里:没被会话白名单排除,也没被 --disallowedTools 禁用(apps/zcode-cli/packages/core/src/runtime/methods/embedded-search-branch.ts:11apps/zcode-cli/packages/cli/src/arguments.ts:125)。满足时 GlobGrep 在首次注册就被跳过(apps/zcode-cli/packages/core/src/tool/handlers/index.ts:206),会话 Shell 选定、刷新工具面时再注销一次(embedded-search-branch.ts:21),给模型的清单也再过滤一遍(embedded-search-branch.ts:48)。提示词跟着变:Bash 描述里“不要用 Bash 跑这些命令”的名单去掉了 findgrepapps/zcode-cli/packages/core/src/tool/handlers/bash-prompt.ts:6),Explore 子 Agent 的指引改成 “Use find via Bash”(apps/zcode-cli/packages/core/src/subagent/explore.ts:21)。

图表加载中…

find 与 grep 被换成了什么

Bash 起进程前带上一段 embedded-search prelude,条件是分支打开、有搜索后端、会话 Shell 是 POSIX 或 Git Bash(apps/zcode-cli/packages/core/src/tool/handlers/bash.ts:395apps/zcode-cli/packages/core/src/embedded-search/shell.ts:14)。prelude 由 apps/zcode-cli/packages/adapters/src/exec/embedded-search-prelude.ts:34 拼出,核心是三组常量(embedded-search-prelude.ts:15):

// ugrep 的 -z/-Z 与 GNU grep 的 null-data 语义不同;这些参数必须绕回系统 grep。
const GREP_BYPASS_CASE_PATTERN =
  "-*-filter*|-*-pager*|-*-view*|-*-format-open*|-*-config*|---*|-@*|-*-save-config*|-[Zz]*|-[!-]*[Zz]*|--null|--null-data";

const BFS_DEFAULT_ARGS = ["-S", "dfs", "-regextype", "findutils-default"] as const;

const UGREP_DEFAULT_ARGS = [
  "-G",
  "--ignore-files",
  "--hidden",
  "-I",
  "--exclude-dir=.git",
  "--exclude-dir=.svn",
  "--exclude-dir=.hg",
  "--exclude-dir=.bzr",
  "--exclude-dir=.jj",
  "--exclude-dir=.sl",
] as const;
  • find 变成 bfs -S dfs -regextype findutils-default:深度优先遍历,正则方言对齐 GNU findutils。
  • grep 变成 ugrep:基本正则、读取忽略文件、包含隐藏文件、跳过二进制、排除六种版本库目录。参数里出现 -z-Z--null,或 ugrep 特有的 --filter--pager--view--config 一类时,整条命令原样交给系统 grep。
  • 每个函数先用 command -v 确认后端存在,不在就退回同名系统命令(embedded-search-prelude.ts:179)。Git Bash 下不覆盖 find,因为 Windows 不分发 bfs;cmd 与旧式 Shell 不支持函数,不注入(embedded-search-prelude.ts:42:58)。PATH 上没有 rg 而后端带着一个 rg 路径时,再补一个 rg 函数(embedded-search-prelude.ts:142)。
  • “增强 Find 和 Grep”关掉时只剩这个 rg 兜底(embedded-search-prelude.ts:44,开关经 apps/zcode-cli/packages/core/src/tool/executor/call-runner.ts:401 传入)。

契约里还定义了第三种后端 argv0-dispatch:同一个可执行文件以 ARGV0=bfsARGV0=ugrepARGV0=rg 的身份被调起,再按 argv0 分派成三个工具(apps/zcode-cli/packages/adapters/src/exec/embedded-search-prelude.ts:108:131:152)。prelude 能生成这种函数,但仓库里没有任何地方产生这种后端(apps/zcode-cli/packages/contracts/src/interfaces/execution.port.ts:85)。

后端在 bootstrap 里决定(apps/zcode-cli/packages/bootstrap/src/app/embedded-search-backend.ts:7)。默认是 native-binaries:三个命令分别取 ZCODE_BFS_BINARYZCODE_UGREP_BINARYZCODE_RG_BINARY,没设就用裸命令名靠 PATH 查找(packages/shared/src/runtime-tool-runtime.ts:23)。设了 ZCODE_EMBEDDED_SEARCH_COMMAND 则改用 internal-cli:函数调用 <该命令> __internal-search find …… grep …embedded-search-backend.ts:11)。

__internal-search 是 CLI 的隐藏入口,在参数解析之前分流(apps/zcode-cli/packages/cli/src/run.ts:287)。它只认 findgrep,其余返回 2;实际做的是 spawn 系统同名命令,grep 前面补 -G -I 与六个 --exclude-dir,但没有 ugrep 的 --ignore-files--hiddenapps/zcode-cli/packages/cli/src/internal-search/embedded-search-cli.ts:12:102)。这层转发替原生命令处理三件事:下游 head 读够后关闭管道时吞掉 EPIPE 并停掉子进程;取消时先 SIGTERM、750 毫秒后 SIGKILL,Windows 用 taskkill /T /F;收到 SIGINT、SIGTERM、SIGHUP 时按 130、143、129 返回退出码(embedded-search-cli.ts:39:139:266)。这种后端没有 rg 兜底(embedded-search-prelude.ts:147)。

Glob 与 Grep 工具

Bash 不可用时才登场的这两个工具走的是另一套实现:

  • Glob:纯 JavaScript 递归遍历,只跳过六种版本库目录,不读 .gitignorenode_modules 也会被扫到(apps/zcode-cli/packages/adapters/src/fs/index.ts:1708:54);模式转成正则,支持 ***? 与花括号多选,不含斜杠的模式同时匹配文件名(fs/index.ts:1733:1746);按 mtime 倒序截取前 100 条,截断时附一句提示(fs/index.ts:430apps/zcode-cli/packages/core/src/tool/handlers/glob.ts:20:152)。
  • Grep:用 npm 包 ripgrep(0.3.1,见 apps/zcode-cli/packages/adapters/package.json)提供的 WASI 版 ripgrep,放在 Worker 线程里跑,注释说在主线程跑会占住事件循环,用户停止无从响应(fs/index.ts:96:1056);只把搜索根目录预开放给 WASI(fs/index.ts:977);固定参数含 --no-config--hidden--max-columns 500,同样排除版本库目录(fs/index.ts:933);30 秒超时后终止 Worker(fs/index.ts:53:1095);WASM 跑不起来时退回 JavaScript 正则逐文件扫(fs/index.ts:466:576)。
  • type 不是 ripgrep 的 --type,而是按一张 20 项的扩展名表换成 --glob,表外的值当扩展名用(fs/index.ts:1018:1850)。head_limit 默认 250、传 0 不限(fs/index.ts:52:1811),命中文件按 mtime 倒序(fs/index.ts:1272),给模型的内容上限 20000 字节(apps/zcode-cli/packages/core/src/tool/handlers/grep.ts:23)。

原生工具怎样分发

三件工具的版本钉在 scripts/native-search-tools-config.mjs:25:bfs 4.1.1、ugrep 7.8.4、ripgrep 14.1.1。ripgrep 用官方预编译包(配置里标为 official,缺包时报 “missing Microsoft ripgrep asset”,native-search-tools-config.mjs:111:370);bfs 与 ugrep 由 ZCode 自己从钉死版本与 SHA-256 的源码包构建(native-search-tools-config.mjs:43),Unix 构建脚本把依赖的 Oniguruma、PCRE2、zlib、bzip2、zstd、Brotli 都编成静态库,Linux 以 glibc 2.28 为基线,macOS 部署目标 12.0(native-search-tools-config.mjs:11scripts/native-search-tools-unix.mjs:144)。

目标bfsugrepripgrep
macOS arm64、x644.1.1-17.8.4-114.1.1-1
Linux arm64、x644.1.1-27.8.4-114.1.1-1(musl 版)
Windows arm64、x647.8.4-114.1.1-1
远端 macOS13.0.0-10
远端 Linux4.1.1-27.8.4-114.1.1-1

18 个归档直接提交在 apps/zcode-cli/dependencies/native-search,每个都记在 SHA256SUMS 里,许可证与来源清单放在 third-party/native-searchapps/zcode-cli/dependencies/README.md:9:15)。准备脚本先校验全部归档的哈希再动缓存,然后检查解出的可执行文件架构,不下载、也不走镜像(scripts/prepare-native-search-tools.mjs:40apps/zcode-cli/dependencies/README.md:40)。之后分三路:

  • 桌面端:electron-builder 把它们放进 resources/tools/<工具>packages/desktop/electron-builder.config.js:632:639);服务层找到后设置 ZCODE_*_BINARY,并把目录追加到 PATH 末尾,用户自己装的 rg 优先(packages/services/src/runtime-tools/runtimeToolResolver.ts:144:163)。
  • CLI 单文件包:构建时作为 SEA 资产嵌入,键为 zcode-runtime-tools/<工具>/<sha256>/<文件名>apps/zcode-cli/packages/cli/scripts/sea-runtime-tool-assets.mjs:11:49);启动时逐个校验大小与哈希,解到 ~/.zcode/cache/runtime_tools/<目标>/<工具>/<版本>-<sha256>/(根目录可由 ZCODE_STORAGE_DIR 改),再写回环境变量(apps/zcode-cli/packages/cli/src/sea-runtime-tools.ts:52:96,调用在 apps/zcode-cli/packages/cli/src/main.ts:52)。
  • 远端工作区:macOS 远端只部署 rg 13,Linux 远端部署三件工具(packages/shared/src/runtime-tool-runtime.ts:41:64),见远程工作区与手机远控

依赖目录的 README 把维护指南链接到 third-party/README.mdapps/zcode-cli/dependencies/README.md:20),但仓库里没有这个文件。

下一篇:WebFetch 与 WebSearch——抓网页与搜网页:出站护栏、预批准域名、缓存,以及为什么搜索要借模型提供方的原生能力。

本页目录