@epoch-agent/plugin-file

中文文档
6.2.0发布于 2026/11/13Apache-2.0TypeScriptES Module近 30 天下载 4340

epoch-agent 文件系统操作插件

原始使用文档
文档目录6 节
  1. 工具
  2. 处理的几类脏输入
  3. 编辑的匹配判据:三个工具一份(2026-10-12)
  4. 四个读类工具自己收进 ctx.outputBudget (2026-09-08)
  5. file_diff_preview 的 operation 是故意填错的
  6. 开发

@epoch-agent/plugin-file

文件工具插件。8 个工具。

  • ✅ 做:读 / 写 / 精确编辑(单处、多处、跨文件)/ 搜索 / 列目录
  • ❌ 不做:git 操作、跨机同步、任何引擎逻辑
  • 依赖:diff。protocol 和 infra 都是 dependencies (本包没有 peerDependencies —— 2026-11-03 核)

工具

只读五个:file_read file_multi_read file_search file_list file_diff_preview; 破坏性三个:file_write file_patch file_multi_patch。

每个工具干什么、分页与 output_mode 那些边界在 docs/TOOLS.md —— 那份是工具清单的唯一真源, 这里只写这个包自己的实现取舍。

file_search 用 ripgrep,但不要求用户自己装:rg 从哪来由 infra 的 resolveRipgrep() 三级决定 —— 系统 PATH 上有就用系统的, 没有就用随包带的 vendor 二进制,两个都没有才退回「工具在、一调才报错 + 给安装提示」 的老行为(启动诊断一条 warn,其余七个工具不受影响)。

命中顺序由我们钉死(--sort path):rg 默认并行遍历,多文件命中按到达顺序打印, 同一条查询两次可能给出不同顺序。这不只是「看着别扭」—— 模型会把顺序变化读成结果变化, 而 evals/ 的指纹把工具结果原文算进哈希,于是它会变成偶发误报(2026-11-13 实测 200 遍翻 1 遍,现场是一条 eval 的第 1、2 发漂了一次)。代价是 rg 退化成单线程遍历; 搜索输出本来就有 limit 和截断兜着。

本插件不再导出 ripgrep 的解析与安装提示,它们搬去 infra 了(今天叫 resolveRipgrep() / ripgrepInstallHint(),见 infra/src/ripgrep.ts): file_list 和将来的 @ 文件补全也要用同一份解析,而 plugin 之间不许互相依赖。 这里只管参数怎么拼和输出怎么收。

只读的四个(file_read / file_multi_read / file_search / file_list)标了 supportsParallel: true,引擎可以并发调。⚠️ file_diff_preview 没有这一格 (2026-11-03 核:supportsParallel 全包只有四处 true,它不在内)。

处理的几类脏输入

  • 二进制:扩展名黑名单拦截(.exe / .zip / .pdf 等),不把乱码喂给模型
  • 图片(2026-10-15):file_read 读到 png / jpeg / gif / webp 时交一张图(kind: 'image' 的 artifact),不再回「二进制,无法读取」。进不进模型上下文不归这个包判 —— 模型认不认图、 字节 / 边长 / 像素上限是 core 的 artifact-gate.ts 那四道闸门(MCP 截图走的也是那一道), 过不了的模型拿到的是闸门那句「为什么没给你图」。OpenAI Chat 协议的部分接口(openai-compatible / groq 等) 的 tool 消息收不了图,图会被挪进下一条 user 消息;deepseek / xai 自 2026-10-19 升级后的 SDK 版本起原生收图,不再挪 —— 逐家判据在 core 的 registry 的 TOOL_RESULT_MEDIA、 挪法在 sdk-adapter.ts 的 hoistToolMedia 上。 超过 32 MB 的「图」不读进内存。工具说明没改,eval 指纹不动
  • BOM:剥掉 Windows 编辑器留的 UTF-8 BOM,否则第一行的 patch 匹配永远不中
  • 行尾:纯 CRLF 文件归一成 LF 去匹配、写回时整份还原;混合行尾的文件原样不动(归一再还原会改掉用户没让动的那些行)
  • 路径:~ 和 ~user 展开
  • 工作区边界:resolveSafePath 防路径穿越——resolve 之后用 relative 判, 不是 startsWith。判定本身是 infra 的 isInWorkspace()(全仓唯一一份,权限层和 plugin-terminal 共用),这里只管 ~ 展开和抛 PathEscapeError。 ctx.extraRoots(epoch --add-dir)一并透传:落在额外根里的绝对路径 不算越界,但相对路径仍然只按 ctx.workDir 解析——否则 src/a.ts 会在每个 根里各找一遍,同一个相对路径有多个答案,而模型无从知道命中的是哪一个
  • ctx.grantedPaths:权限层这一次放行的路径(2026-10-08)。它和 extraRoots 并列当根用,但来源完全不同:那个是你事先 --add-dir 声明的地盘,这个是 权限层这次判定现算的(档位放行 / 你刚在审批框里点了「允许」)。 ⚠️ 工具不许再自己看 ctx.permissionLevel 判一次档位 —— 档位表只有权限层 那一张,这里多判一次的症状是「界面说放行了、工具说越界」,而那正是这一格 要修的东西。判据全文在 protocol 的 ToolContext.grantedPaths 上
  • 读类工具多认一格:本会话的 artifact 目录(方案 47)。file_read / file_search / file_list / file_multi_read 走的是 resolveReadablePath, 它在上面那套之外再认 artifactsDir(ctx.sessionId)。理由是大输出落盘之后 取回提示会告诉模型「用 file_search 在这个路径里搜」,而那个路径在工作区外—— 不放开这一格,那句提示就是一句必定失败的指令。 口子收窄到三条:只放开读(file_write / file_patch 仍走 resolveSafePath, 模型没有理由改写「命令当时输出了什么」这份证据)、只放开本会话那一个子目录 (不是 ~/.epoch/artifacts,更不是 ~/.epoch——后者等于把 .env 里的 API key 和 sessions.db 一起交出去)、sessionId 由引擎给且 artifactsDir() 内部再洗一次。 这三条各有一条否定用例钉着,见 __tests__/artifact-readable.test.ts

编辑的匹配判据:三个工具一份(2026-10-12)

file_patch、file_multi_patch、file_diff_preview 都走 edit-match.ts:先精确子串,不中再退到「按行、忽略 行尾空白」「按行、忽略行首尾空白」(后者命中时新内容按文件的缩进平移)。每一档都 要求唯一命中,多处而没给 replace_all 就报歧义。

file_multi_patch 的参数是 edits: [{ path, old_string, new_string, replace_all? }], 同一文件的多条按顺序作用。先在内存里全部算完、任何一条不中就一个文件都不写, 回执点名第几条、哪个文件;file_diff_preview 吃同一份参数、跑同一个计算,只是不写盘。 只改已有文件 —— 新建用 file_write。

这一版之前那两条路各有一种静默写坏文件的方式(file_patch 的模糊匹配把命中点 之后整个文件换掉、file_multi_patch 的 V4A 解析剥掉缩进且没命中也报成功),取证和 「为什么不修 V4A 而是换成结构化参数」写在两个文件的文件头。

四个读类工具自己收进 ctx.outputBudget(2026-09-08)

预算从 ctx.outputBudget 现拿 —— 那是引擎将要用的同一个数,所以自己截完不会 再被引擎切一次,工具说的那句「还有多少、怎么接着读」也就是真的。 MAX_OUTPUT_CHARS(50_000)退化成兜底,只在宿主自己拼 ToolContext 时用(那条路上也没有引擎在截)。

改之前是两处各量一遍、而且两个数不一样:这四个按 50_000 截并明说截了多少, 引擎再按自己那个数切一刀 —— 模型看到的既不是全文,也不是我们说的那句话。

file_read 那一格更具体:它的分页单位是行(offset / limit),预算的单位是 字符。两个单位不换算的后果是那条工具描述里的假话「一次读 500 行」—— 500 行长行早就越过预算被引擎从中间切掉,而它报的 nextOffset 是按行算的, 模型照它翻页正好跳过被切掉的那一段,两侧都不报错。现在按行收进预算 (fitLines),报的是真正读到的那一行。

⚠️ 有一档翻不动,而它是有意留着的(2026-10-21 修):第一行自己就长过整页预算 (minified 产物 / 单行 JSON / 导出的 CSV)时,这一页交的是截断的那一行并明说 「只给了前 N 字符」。余下部分没有下一页可翻 —— offset 是按行走的,一行之内 不存在下一个偏移量。工具会直接指去 run_code 按字符切,而不是给一个翻回原地 的 offset(那样模型会原地打转,而两侧都不报错)。file_multi_read 也截在同一个 字符预算上,它腾不出第二个偏移量,所以那一档只有 run_code 答得了。

file_diff_preview 的 operation 是故意填错的

它的真实语义是只读(只算 diff 不落盘),annotations 照实写。但 operation 仍填 file_write:老白名单把它归在 FILE_WRITE_TOOLS 里,只改一边会让 recordApproval 那条路径与这里分叉。往严的方向偏,代价只是多一次确认。

开发

npx vitest run --project=plugin-file

path-boundary.test.ts 记的是具体的穿越手法,改 resolveSafePath 之前先读它。

版本与发布记录

@epoch-agent/plugin-file 共发布 57 个版本,首次发布于 2026/08/19;过去 12 个月发布了 57 个版本。

版本发布日期距今
6.2.0latest2026/11/13今天
6.1.02026/11/112 天前
6.0.02026/11/112 天前
5.0.02026/11/103 天前
4.1.12026/11/067 天前
4.1.02026/11/067 天前

关于 @epoch-agent/plugin-file 的常见问题

@epoch-agent/plugin-file 是什么?

epoch-agent 文件系统操作插件

如何安装 @epoch-agent/plugin-file?

在项目目录运行 npm install @epoch-agent/plugin-file,或使用 pnpm add / yarn add / bun add。

@epoch-agent/plugin-file 支持 TypeScript 吗?

支持。包内自带 TypeScript 类型声明(package.json 声明了 types 入口),安装后即可获得类型提示,无需再安装 @types 包。

@epoch-agent/plugin-file 支持 ESM 还是 CommonJS?

以 ES Module 形式发布,推荐使用 import 引入。在 CommonJS 项目中,Node.js 20.19+ / 22.12+ 可以直接 require() 不含顶层 await 的 ES Module,更早的版本需要改用动态 import()。

@epoch-agent/plugin-file 需要什么版本的 Node.js?

package.json 的 engines 字段要求 Node.js >=22.0.0。版本不符时 npm 默认只给出警告,开启 engine-strict 后会拒绝安装。

@epoch-agent/plugin-file 可以商用吗?

@epoch-agent/plugin-file 使用 Apache-2.0 许可。宽松许可:允许商业使用、修改和再分发,并附带明确的专利授权;分发时需保留许可与 NOTICE 文件,修改过的文件要注明改动。以上为概括,不构成法律意见,请以许可原文为准。

@epoch-agent/plugin-file 的下载量有多少?

根据 npm 官方下载统计,@epoch-agent/plugin-file 在 2026/10/14 – 2026/11/12 这 30 天内被下载 4,340 次,日均约 145 次。下载量会把 CI 构建、镜像同步和作为其他包依赖的安装一并计入,适合横向比较热度,不能直接等同于使用人数。

@epoch-agent/plugin-file 还在维护吗?

最新版本 6.2.0 发布于 2026/11/13(今天);至今共发布 57 个版本,其中过去 12 个月发布了 57 个;首次发布于 2026/08/19。发布频率只是参考信号:功能稳定的工具可能很少需要更新,建议结合源码仓库的 issue 和提交记录判断。