读、写、改、搜
五个文件工具的实现:Read 怎样分流文本、图片、PDF 与视频并计入预算,read-file-state 怎样落实“先读后改”,Edit 的八级匹配与唯一性要求,文件系统 adapter 的原子写与编码,以及默认搜索为何改走 Bash 里的 bfs 与 ugrep、这些原生工具怎样分发到各平台。
模型读写代码靠五个内置工具:Read、Write、Edit、Glob、Grep,实现在 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。
有一点先说在前面:默认配置下模型看不到 Glob 和 Grep。只要 Bash 可用,运行时就把这两个工具从注册表里摘掉,让模型直接在 Bash 里写 find 和 grep,再由注入的 Shell 函数把它们换成 bfs 和 ugrep。工具怎样注册与过滤见工具契约、注册表与可见性,调用流水线见执行器:调度、审批、超时与结果,Bash 本身见Bash:解析、只读判定与后台任务。
怎么用
| 工具 | 参数 | 只读 | 默认审批 | 要点 |
|---|---|---|---|---|
Read | file_path、offset、limit,模型支持 PDF 时加 pages | 是 | 否 | 文本带行号;图片、视频、PDF 各有分支 |
Write | file_path、content | 否 | 是 | 整文件覆盖;覆盖已有文件前必须完整读过 |
Edit | file_path、old_string、new_string、replace_all | 否 | 是 | old_string 须唯一;精确匹配失败再试七种宽松策略 |
Glob | pattern、path | 是 | 否 | 按修改时间倒序,最多 100 条;默认不可见 |
Grep | pattern、path、glob、type、output_mode、-A、-B、-C、-n、-i、-o、head_limit、offset、multiline | 是 | 否 | 默认只列文件,最多 250 条;默认不可见 |
参数定义在契约包(如 apps/zcode-cli/packages/contracts/src/tools/read.ts:65、apps/zcode-cli/packages/contracts/src/tools/edit.ts:18、apps/zcode-cli/packages/contracts/src/tools/grep.ts:22),只读、并发安全、超时与结果预算写在各 handler 的 ToolEntry 里(如 apps/zcode-cli/packages/core/src/tool/handlers/read.ts:464、apps/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 里的find、grep不再被替换,但Glob、Grep也不会因此回来。
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_BYTES,apps/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)。
解码在 detectTextEncoding(apps/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 也只在这时多出 pages(apps/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、:7,apps/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:9、packages/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 持有一张 ReadFileStateMap(apps/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:339、handlers/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_write(apps/zcode-cli/packages/adapters/src/fs/index.ts:473),挡住“检查之后、写入之前”被改的窗口。
另外三个地方会动这张表:
- Bash:命令里带
--fix、--write、black、cargo fmt等格式化标记时,跑完扫一遍已读文件,mtime 晚于命令开始的,在结果后追加 “This command modified … Call Read before editing.”(apps/zcode-cli/packages/core/src/tool/handlers/bash-read-file-state.ts:20、:189);反过来,cat、head、tail、sed -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_string 与 new_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: true 与 atomic: true(write.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)。 - 原子写:
atomicWrite先lstat,目标是符号链接就拒绝;在同目录创建<原名>.tmp.<pid>.<随机串>,以O_EXCL与O_NOFOLLOW打开,复制原文件权限位(保住脚本的执行位),fsync后rename覆盖;任何一步失败就删掉临时文件,退回以O_TRUNC与O_NOFOLLOW原地覆盖(fs/index.ts:700、:717、:740)。 - 边界:端口只接受绝对路径,Node 错误码被映射成
not_found、permission_denied、is_directory等稳定类别,取消单独记为cancelled(fs/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:11,apps/zcode-cli/packages/cli/src/arguments.ts:125)。满足时 Glob、Grep 在首次注册就被跳过(apps/zcode-cli/packages/core/src/tool/handlers/index.ts:206),会话 Shell 选定、刷新工具面时再注销一次(embedded-search-branch.ts:21),给模型的清单也再过滤一遍(embedded-search-branch.ts:48)。提示词跟着变:Bash 描述里“不要用 Bash 跑这些命令”的名单去掉了 find 和 grep(apps/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:395、apps/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=bfs、ARGV0=ugrep、ARGV0=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)。
后端与 __internal-search
后端在 bootstrap 里决定(apps/zcode-cli/packages/bootstrap/src/app/embedded-search-backend.ts:7)。默认是 native-binaries:三个命令分别取 ZCODE_BFS_BINARY、ZCODE_UGREP_BINARY、ZCODE_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)。它只认 find 和 grep,其余返回 2;实际做的是 spawn 系统同名命令,grep 前面补 -G -I 与六个 --exclude-dir,但没有 ugrep 的 --ignore-files 与 --hidden(apps/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 递归遍历,只跳过六种版本库目录,不读
.gitignore,node_modules也会被扫到(apps/zcode-cli/packages/adapters/src/fs/index.ts:1708、:54);模式转成正则,支持*、**、?与花括号多选,不含斜杠的模式同时匹配文件名(fs/index.ts:1733、:1746);按 mtime 倒序截取前 100 条,截断时附一句提示(fs/index.ts:430,apps/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:11,scripts/native-search-tools-unix.mjs:144)。
| 目标 | bfs | ugrep | ripgrep |
|---|---|---|---|
| macOS arm64、x64 | 4.1.1-1 | 7.8.4-1 | 14.1.1-1 |
| Linux arm64、x64 | 4.1.1-2 | 7.8.4-1 | 14.1.1-1(musl 版) |
| Windows arm64、x64 | 无 | 7.8.4-1 | 14.1.1-1 |
| 远端 macOS | 无 | 无 | 13.0.0-10 |
| 远端 Linux | 4.1.1-2 | 7.8.4-1 | 14.1.1-1 |
18 个归档直接提交在 apps/zcode-cli/dependencies/native-search,每个都记在 SHA256SUMS 里,许可证与来源清单放在 third-party/native-search(apps/zcode-cli/dependencies/README.md:9、:15)。准备脚本先校验全部归档的哈希再动缓存,然后检查解出的可执行文件架构,不下载、也不走镜像(scripts/prepare-native-search-tools.mjs:40,apps/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.md(apps/zcode-cli/dependencies/README.md:20),但仓库里没有这个文件。
下一篇:WebFetch 与 WebSearch——抓网页与搜网页:出站护栏、预批准域名、缓存,以及为什么搜索要借模型提供方的原生能力。