乐于分享
好东西不私藏

OpenClaw工具拆解之apply_patch+sandboxed_read

OpenClaw工具拆解之apply_patch+sandboxed_read

一、apply_patch 工具

1.1 工具概述

功能:应用补丁到多个文件
核心特性

  • • 支持添加/修改/删除文件
  • • 使用 *** Begin Patch/End Patch 格式
  • • 支持文件移动
  • • 仅 OpenAI 模型启用
  • • 支持沙盒模式

1.2 Schema 定义

位置:第 12935 行

const applyPatchSchema = Type.Object({ 
inputType.String({ 
description"Patch content using the *** Begin Patch/End Patch format."
    }) 
});

1.3 完整执行代码

位置:第 12936 行

functioncreateApplyPatchTool(options = {}) {
// 1. 解析配置
const cwd = options.cwd ?? process.cwd();
const sandbox = options.sandbox;
const workspaceOnly = options.workspaceOnly !== false;

return {
name"apply_patch",
label"apply_patch",
description"Apply a patch to one or more files using the apply_patch format. The input should include *** Begin Patch and *** End Patch markers.",
parameters: applyPatchSchema,
executeasync (_toolCallId, args, signal) => {
const params = args;

// 2. 解析输入
const input = typeof params.input === "string" ? params.input : "";

if (!input.trim()) {
thrownewError("Provide a patch input.");
            }

// 3. 检查中止信号
if (signal?.aborted) {
const err = newError("Aborted");
                err.name = "AbortError";
throw err;
            }

// 4. 应用补丁
const result = awaitapplyPatch(input, {
                cwd,
                sandbox,
                workspaceOnly,
                signal
            });

// 5. 返回结果
return {
content: [{
type"text",
text: result.text
                }],
details: { summary: result.summary }
            };
        }
    };
}

// 补丁应用核心函数
asyncfunctionapplyPatch(input, options) {
// 1. 解析补丁文本
const parsed = parsePatchText(input);

if (parsed.hunks.length === 0) {
thrownewError("No files were modified.");
    }

// 2. 构建摘要
const summary = {
added: [],
modified: [],
deleted: []
    };

const seen = {
addednewSet(),
modifiednewSet(),
deletednewSet()
    };

// 3. 解析文件操作
const fileOps = resolvePatchFileOps(options);

// 4. 处理每个补丁块
for (const hunk of parsed.hunks) {
if (options.signal?.aborted) {
const err = newError("Aborted");
            err.name = "AbortError";
throw err;
        }

// 4.1 添加文件
if (hunk.kind === "add") {
const target = awaitresolvePatchPath(hunk.path, options);
awaitensureDir(target.resolved, fileOps);
await fileOps.writeFile(target.resolved, hunk.contents);
recordSummary(summary, seen, "added", target.display);
continue;
        }

// 4.2 删除文件
if (hunk.kind === "delete") {
const target = awaitresolvePatchPath(hunk.path, options, PATH_ALIAS_POLICIES.unlinkTarget);
await fileOps.remove(target.resolved);
recordSummary(summary, seen, "deleted", target.display);
continue;
        }

// 4.3 修改文件
const target = awaitresolvePatchPath(hunk.path, options);
const applied = awaitapplyUpdateHunk(target.resolved, hunk.chunks, { 
readFile(path) => fileOps.readFile(path) 
        });

// 4.4 移动文件
if (hunk.movePath) {
const moveTarget = awaitresolvePatchPath(hunk.movePath, options);
awaitensureDir(moveTarget.resolved, fileOps);
await fileOps.writeFile(moveTarget.resolved, applied);
await fileOps.remove(target.resolved);
recordSummary(summary, seen, "modified", moveTarget.display);
        } else {
await fileOps.writeFile(target.resolved, applied);
recordSummary(summary, seen, "modified", target.display);
        }
    }

// 5. 返回结果
return {
        summary,
textformatSummary(summary)
    };
}

// 记录摘要
functionrecordSummary(summary, seen, bucket, value) {
if (seen[bucket].has(value)) return;
    seen[bucket].add(value);
    summary[bucket].push(value);
}

// 格式化摘要
functionformatSummary(summary) {
const lines = ["Success. Updated the following files:"];
for (const file of summary.added) lines.push(`A ${file}`);
for (const file of summary.modified) lines.push(`M ${file}`);
for (const file of summary.deleted) lines.push(`D ${file}`);
return lines.join("\n");
}

1.4 补丁格式

*** Begin Patch
*** Add File: src/new_file.js
console.log("Hello, World!");
*** End Patch

*** Begin Patch
*** Modify File: src/existing_file.js
@@ -1,3 +1,4 @@
 function hello() {
-  console.log("Hi");
+  console.log("Hello, World!");
 }
*** End Patch

*** Begin Patch
*** Delete File: src/old_file.js
*** End Patch

*** Begin Patch
*** Move File: src/old_path.js -> src/new_path.js
*** End Patch

1.5 补丁操作类型

操作
说明
格式
Add
添加新文件
*** Add File: path
Modify
修改文件
*** Modify File: path

 + 差异内容
Delete
删除文件
*** Delete File: path
Move
移动文件
*** Move File: old -> new

1.6 执行流程图

apply_patch 工具调用
    ↓
1. 解析输入
    ↓
2. 检查中止信号
    ↓
3. 解析补丁文本
    ↓
4. 处理每个补丁块
    ├─ Add → 创建文件
    ├─ Delete → 删除文件
    ├─ Modify → 应用差异
    └─ Move → 移动文件
    ↓
5. 记录摘要
    ↓
6. 返回结果

1.7 返回结果格式

成功

{
"content":[{
"type":"text",
"text":"Success. Updated the following files:\nA src/new_file.js\nM src/existing_file.js\nD src/old_file.js"
}],
"details":{
"summary":{
"added":["src/new_file.js"],
"modified":["src/existing_file.js"],
"deleted":["src/old_file.js"]
}
}
}

失败

{
"error":"No files were modified."
}

二、sandboxed_read 工具

2.1 工具概述

功能:沙盒中读取文件
核心特性

  • • 通过 bridge 访问隔离文件系统
  • • 支持图片 MIME 检测
  • • 支持模型上下文窗口限制
  • • 图片清理

2.2 创建函数

位置:第 115000 行

functioncreateSandboxedReadTool(params) {
returncreateOpenClawReadTool(createReadTool(params.root, { 
operationscreateSandboxReadOperations(params) 
    }), {
modelContextWindowTokens: params.modelContextWindowTokens,
imageSanitization: params.imageSanitization
    });
}

2.3 沙盒读取操作

位置:第 115034 行

functioncreateSandboxReadOperations(params) {
return {
// 1. 读取文件
readFile(absolutePath) => params.bridge.readFile({
filePath: absolutePath,
cwd: params.root
        }),

// 2. 访问检查
accessasync (absolutePath) => {
if (!await params.bridge.stat({
filePath: absolutePath,
cwd: params.root
            })) {
throwcreateFsAccessError("ENOENT", absolutePath);
            }
        },

// 3. 检测图片 MIME 类型
detectImageMimeTypeasync (absolutePath) => {
const mime = awaitdetectMime({
bufferawait params.bridge.readFile({
filePath: absolutePath,
cwd: params.root
                }),
filePath: absolutePath
            });
return mime && mime.startsWith("image/") ? mime : void0;
        }
    };
}

2.4 包装链

createReadTool (外部库)
    ↓ (使用沙盒操作)
createOpenClawReadTool (OpenClaw 包装)
    ↓ (模型上下文限制)
wrapToolWorkspaceRootGuard (工作目录限制)
    ↓
最终工具

2.5 执行流程图

sandboxed_read 工具调用
    ↓
1. 参数标准化
    ↓
2. 验证必填参数
    ↓
3. 沙盒桥接读取
    ├─ bridge.readFile
    ├─ bridge.stat
    └─ detectMime
    ↓
4. 自适应分页(如果需要)
    ↓
5. 图片处理(如果是图片)
    ↓
6. 返回结果

三、关键机制对比

3.1 功能定位

特性
apply_patch
sandboxed_read
用途
批量文件修改
沙盒文件读取
写操作
Add/Modify/Delete
只读
沙盒支持
支持
必需

3.2 启用条件

特性
apply_patch
sandboxed_read
模型限制
仅 OpenAI
沙盒必需
可选
必需
配置启用
需要
自动

3.3 安全限制

限制类型
apply_patch
sandboxed_read
工作目录
workspaceOnly
root 限制
桥接访问
支持
必需
路径解析
resolvePatchPath
bridge 路径

四、使用示例

4.1 apply_patch 工具调用

用户添加一个新文件并修改现有文件

大模型返回

{
"tool_call":{
"name":"apply_patch",
"arguments":{
"input":"*** Begin Patch\n*** Add File: src/hello.js\nconsole.log(\"Hello\");\n*** End Patch\n\n*** Begin Patch\n*** Modify File: src/main.js\n@@ -1,2 +1,3 @@\n function main() {\n+  console.log(\"Main\");\n }\n*** End Patch"
}
}
}

执行结果

{
"content":[{
"type":"text",
"text":"Success. Updated the following files:\nA src/hello.js\nM src/main.js"
}],
"details":{
"summary":{
"added":["src/hello.js"],
"modified":["src/main.js"],
"deleted":[]
}
}
}

4.2 sandboxed_read 工具调用

用户读取沙盒中的文件

大模型返回

{
"tool_call":{
"name":"sandboxed_read",
"arguments":{
"path":"src/main.js"
}
}
}

执行结果

{
"content":[{
"type":"text",
"text":"function main() {\n  console.log(\"Main\");\n}"
}],
"details":{
"path":"src/main.js",
"sandbox":true
}
}