@dreamtof/dsh-apply-patch
v3.7.0
Published
One-call multi-file, multi-span editing with exact matching, original-file coordinates, byte fidelity and conditional failure restoration.
Maintainers
Readme
apply_patch
版本 3.7.0。只做文件内容编辑:让 Agent 在一次调用中,准确修改多个文件的多个片段。
工具名就是 apply_patch。本包是唯一需要安装的编辑工具,v1 / v2 已由它取代;包名仍保留 -v3 只是为了不使已安装 Profile 的配置失效。
批量优先
一次调用改多个文件、多个片段才是这个工具的默认形态。逐文件单独调用是亏的:每次调用都占一个完整往返,而整批会在写入任何一个文件之前校验全部目标,任何一处不匹配就零写入。
只有一种情况值得拆开:后一步需要先看到前一步的结果(比如要看命令输出,或上一处匹配失败必须重读)。仅仅因为「改的是不同文件」而拆分,是把批量优势白白丢掉。
{
"operations": [
{ "op": "replace_text", "path": "src/a.ts", "search": "return 1", "replace": "return 2" },
{ "op": "replace_range", "path": "src/a.ts", "startLine": 20, "endLine": 20, "oldText": "const old = true", "text": "const old = false" },
{ "op": "set_json", "path": "config.jsonc", "pointer": "/enabled", "value": true },
{ "op": "insert_json", "path": "config.jsonc", "pointer": "/hosts", "index": 1, "value": "beta" },
{ "op": "append_text", "path": "notes.txt", "text": "\n- one more note" },
{ "op": "create_file", "path": "notes.txt", "text": "new content\n" }
]
}所有操作都以每个文件修改前的内容为基准。模型不用计算前面修改造成的行号偏移;重复匹配、原文不符或操作重叠时,整批在写入前拒绝。可选 dryRun: true 只返回预览。
真实调用长这样:五个文件、六处不相交编辑、五种原语混用,一次往返完成。
Success: 5 file(s), 6 span(s), +30 -4 lines
M src/a.ts
M src/b.ts
M config.jsonc
M docs/d.md
M notes.txt同一次调用中,同一个文件可以接收多处编辑,但它们必须在原文里互不重叠。JSON 写操作会各自解析出最小差异跨度,所以相邻键(例如 /name 与紧跟其后的 /enabled)同批修改会被判为重叠并整批拒绝——这是刻意的:报告重叠比猜测应用顺序安全。
replace_text:精确原文替换,默认必须恰好匹配一次;可用expect声明多次匹配。同一操作内的多处匹配若互相重叠,报MATCH_COUNT。可选fuzzy处理「读文件之后被格式化改动」的情况,见下节。replace_range:按行替换,必须提供oldText校验。可用绝对行号(startLine/endLine),也可用一个精确search锚点加offset与lines相对定位。text内部的换行会改写成该文件自己的行分隔符;text为空串时删除这些整行。set_json:按 JSON Pointer 修改 JSON/JSONC 值,保留注释和无关格式;不相交的目标可以同批修改,重叠拒绝。insert_json:在数组的index位置之前插入元素,保留注释与排版。index必须小于数组长度(追加请用set_json的/-);空数组同样拒绝。append_text:追加到文件末尾,不改变原有末字节的换行状态;只作用于已存在文件。create_file:只新建,不覆盖;父目录默认须事先存在,加parents: true则由本工具创建缺失的祖先目录。
保留 UTF-8 BOM、原有换行、尾换行状态和未指定的文本。拒绝不支持的编码和损坏的 Unicode。只做内容定位与写入,不评判项目代码的类型正确性。
insert_json 的注释归属是刻意固定的:插入点取目标元素的起始位置,新元素落在该元素前导注释之后,注释仍属于原元素。四个边界(注释归属、index === length 拒绝、空数组拒绝、与另一操作重叠拒绝)见 PROTOCOL.md。
不提供复制、移动、删除、历史管理、撤销、快照会话或计划提交。无辅助模型调用、Shell 依赖和工作区状态目录。v1、v2 目录及工具保持独立。
新建模块的首个文件(parents)
批量新建模块时,src/feature/x.ts 的父目录通常还不存在。默认行为仍是拒绝(父目录须已存在),需要时显式加 parents: true:
{ "op": "create_file", "path": "src/feature/x.ts", "text": "export {}\n", "parents": true }Success: 1 file(s), 1 span(s), +1 -0 lines
A src/feature/x.ts
(created directory src/)
(created directory src/feature/)目录在任何写入之前创建,因此某个目标校验失败时不会留下空目录;创建失败也不会有文件被改。目录是这个工具唯一能完整撤销的东西:只删除本轮自己创建、且仍然为空的目录(深处优先),新建文件所在目录因文件保留而保留,并在 error.details.recovery.createdDirs 中报告。
DSH 的文件系统服务没有 mkdir 原语,因此这两个系统调用是 node:fs。范围被收得很紧:仍然先经 ctx.fs.resolve 判目录级沙箱包含与符号链接,且 verify:source 断言只有 src/v3/backend.ts 能 import node:fs/promises 的 mkdir/rmdir。
读文件之后被格式化(fuzzy)
replace_text 默认逐字节匹配。但读到的是旧内容、目标是 prettier / go fmt / IDE 保存时格式化过的新内容,这时原文对不上。加 fuzzy 显式放宽:
| 值 | 容忍 |
| --- | --- |
| trim | 每行行尾空白不同 |
| indent | 整块统一平移了缩进(每行移动量相同) |
三条边界是刻意的:
- 永不归一字符。 弯引号、破折号、全角空格都是不同的文本,不当作空白放行。
- 多处候选就拒绝。 容忍出两个以上位置时报
MATCH_COUNT并列出candidates,不按位置挑一个。 - 每次命中都上报。 结果里带
fuzzy.line,模型能看到「我按容忍匹配到了第 12 行」并自行核对:
Success: 1 file(s), 1 span(s), +1 -1 lines
M drift.ts
(fuzzy=indent matched line 1; verify this is the intended site)匹配顺序是精确优先:search 在原文里逐字节出现过,就按精确命中处理,不会去走容忍路径;fuzzy 与 expect > 1 不能同时用,因为容忍匹配只对唯一位置才可辩护。替换区间仍是文件里被匹配到的那些行,replace 自带缩进,不会叠加。
不做任何符号级 / AST 级编辑。 重命名、改函数体、插成员、补导入这些需要绑定分析的操作不属于文本编辑工具:它们要解析语言、判断作用域与名字冲突、跨项目索引引用,且每种语言各需一套实现与测试。混合在一个工具里只会让它既不是精确的文本编辑器,也不是合格的语义工具。这类需求交给专门的 AST / LSP 工具。
因此本插件没有运行时编译依赖:typescript 只作为构建工具链存在于 devDependencies,不会随包进入你的 Profile。
隐藏原生编辑工具
本包安装后默认隐藏原生 edit 和 str_replace_editor(包括前者的提示词段落与 PTC tools.sdk 声明),所以精确文本修改只有 apply_patch 一条路。
write 不在隐藏集里:本工具没有整文件替换操作(create_file 拒绝覆盖),隐藏 write 会让「重写整个文件」无处可去。确实需要时把它列进 hiddenToolNames。
| 配置 | 默认 | 作用 |
| --- | --- | --- |
| hideNativeWriteTools | true | 是否隐藏 edit 与 str_replace_editor。开关为 volatile,翻转后下一个请求即生效,不需重新注册工具。 |
| hiddenToolNames | [] | 额外要隐藏的工具名,可与上述开关同时用。与自身注册名相同的项会被忽略——工具永远不会隐藏自己。 |
仅在本工具注册成功时隐藏生效。若因名字冲突按 conflictPolicy: skip 未注册,或注册抛错,隐藏层完全不动——否则 Profile 会失去全部编辑能力。
隐藏走请求装配面(system-prompt/assemble,即 agent-loop 真正发送的工具表)加 ctx.tools.guard 兜底。后者覆盖装配过滤到不了的路径:旧会话残留的 schema,或在 PTC 里从 registry.schemas() 绑定名字的程序。被拦截的调用得到一条普通工具错误,明确指出改用 apply_patch 的 operations。
失败语义
先读、校验整批,再写入;每次写入均携带宿主文件版本约束。多文件写入不具备操作系统级原子性。
中途失败时,尝试按本轮写入回执恢复旧文件;外部后续修改不会被覆盖。error.details.recovery 明确列出 restored、remaining 和 uncertain。先更新旧文件,再创建新文件;宿主没有条件删除能力,因此失败前已创建的新文件会保留并报告。进程被强制终止后不提供持久恢复。
安装与验证
包名:@dreamtof/dsh-apply-patch(Loader ID dsh-apply-patch);工具名:apply_patch。目标 DSH:>=0.2.1-rc.2 <0.2.2-0。
安装见 INSTALL.zh.md,参数细则见 PROTOCOL.md,验证记录见 VALIDATION.zh.md。
开发检查:npm run typecheck、npm run lint、npm run build、npm test、npm run verify:source、npm run smoke。发布包检查:npm pack --ignore-scripts 后运行 npm run smoke:packed。
