# Step 12: Bash 工具
一句话导读:往 step11 的插槽里插入第一个「真」工具——用
execSync加三个关键参数(超时、缓冲上限、隐藏窗口)把 shell 命令执行封装成一个ToolDef。它是能力最强也最危险的工具,而这一步的重点正是「怎么给一头猛兽套上笼子」。
# 一、这一步做了什么(What)
step12 实现 tools/bash.ts,第一个能真正影响外部世界的工具:
BashTool:实现ToolDef形状,execute用execSync同步执行命令;- 三个安全参数:
timeout: 30000、maxBuffer: 10MB、windowsHide: true; - 成败统一为 ToolResult:成功返回输出,失败捕获
err.stderr并置isError: true; /run <cmd>命令:不等 AI,手动测试工具是否工作。
它像 Echo 一样 register(BashTool) 进注册表——接口的威力在此显现:一个改变世界的工具和一个玩具工具,注册方式一模一样。
# 二、面试官视角:为什么要做?(Why)
面试题:
execSync(command)一行就能执行命令了,为什么还要加 timeout、maxBuffer、windowsHide 这三个参数?不加会怎样?
因为子进程是把双刃剑——它能干任何事,也意味着任何事都可能出错,且默认行为对「给 AI 用」很不安全。三个参数各堵一个洞:
| 参数 | 不加会怎样 | 加了保证 |
|---|---|---|
timeout: 30000 | 命令挂起(如等输入、死循环)→ 整个 CLI 永久卡死 | 30 秒强制终止,进程不被拖死 |
maxBuffer: 10MB | 大输出(如 cat 大文件)撑爆默认 1MB 缓冲 → 抛错崩溃 | 上限内不炸内存 |
windowsHide: true | Windows 下每条命令弹一个黑色控制台窗 | 静默执行,不打扰用户 |
核心洞察:给 AI 用的工具,边界情况不是「可能发生」而是「一定发生」。AI 会生成你意想不到的命令,超时、巨量输出这些极端场景是常态。所以「加固」不是锦上添花,是 Bash 工具的主体工作——execSync 那一行只是骨架,三个参数才是它能安全交给 AI 的原因。
# 三、原理:它是怎么工作的(How)
# execSync + 三参数
import { execSync } from 'child_process';
const output = execSync(command, {
encoding: 'utf-8', // 直接拿字符串而非 Buffer
timeout: 30000, // 30 秒超时 → 防挂死
maxBuffer: 10 * 1024 * 1024, // 10MB → 防大输出爆内存
windowsHide: true, // Windows 下不弹黑窗
});
2
3
4
5
6
7
8
# 空命令先挡一道
const command = input.command as string;
if (!command || !command.trim()) {
return { content: [{ type: 'text', text: '错误: 命令不能为空' }], isError: true };
}
2
3
4
不把空命令扔给 execSync,而是提前返回带 isError 的结果——防御式编程,把可预期的坏输入挡在系统调用之前。
# 成功和失败都返回 ToolResult
关键在 catch 里对 execSync 抛错的处理。命令返回非 0 退出码时 execSync 会抛异常,而真正的错误信息在 err.stderr 里,不在 err.message:
try {
const output = execSync(command, { /* ...三参数... */ });
const text = output || '(无输出)'; // 空输出也给个占位,别返回空串
return { content: [{ type: 'text', text }] };
} catch (err: any) {
// execSync 失败时把 stderr/stdout 挂在 err 上
const msg = err.stderr || err.stdout || err.message || String(err);
return { content: [{ type: 'text', text: msg }], isError: true };
}
2
3
4
5
6
7
8
9
err.stderr || err.stdout || err.message 这个降级链是经验之谈:优先拿 stderr(命令自己的报错),退而求其次 stdout,再不行才用 JS 层的 message。目的是把命令失败的真实原因回传给 AI,让它能据此纠错,而不是收到一句无用的「Command failed」。
# 数据流
/run ls -la 或 AI 未来调用 Bash({command})
↓
空命令检查 → 挡掉空输入
↓
execSync(command, {timeout, maxBuffer, windowsHide})
↓成功 ↓失败(非0退出/超时/爆缓冲)
输出 or '(无输出)' catch → err.stderr||stdout||message
↓ ↓
{content:[{text}]} {content:[{text}], isError:true}
↓
统一 ToolResult 回给上层
2
3
4
5
6
7
8
9
10
11
# /run:脱离 AI 的测试通道
if (input.startsWith('/run ')) {
const r = await BashTool.execute({ command: cmd });
// ...直接展示结果
}
2
3
4
/run 让开发者不必花 API token、不必等 AI「决定」调用,就能直接验证工具本身对不对。这是「工具逻辑」和「AI 决策」解耦——工具能独立测试,是好设计的标志。
注意:这一步 Bash 工具已注册进 registry,但还没接进 AI 的对话循环(
streamAI尚未传registry.toAPI(),也没处理tool_use)。也就是说此刻 AI 还不能自己调 Bash,只能靠/run手动触发。让 AI 自主调用工具是后续步骤的事——本步先把工具本身打磨好。
# 四、深入追问(面试常见 follow-up)
Q:为什么用 execSync(同步)而不是 exec(异步)?同步不会阻塞吗?
A:会阻塞,但对这个学习版本是可接受的取舍——同步代码线性、易读,工具逻辑一目了然。代价是执行期间整个 CLI 卡住,所以才必须配 timeout 兜底。真实 BashTool 用异步 + 支持 run_in_background,命令在后台跑、对话不阻塞——那是生产级的正确做法,但复杂得多。
Q:命令失败时错误为什么要从 err.stderr 取,用 err.message 不行吗?
A:err.message 通常只是笼统的「Command failed: xxx」,真正有用的报错(如「文件不存在」「权限拒绝」)在子进程的 stderr 里。把 stderr 回传给 AI,它才能读懂失败原因并自我纠正。取错地方,AI 就等于收到一句废话,没法 debug。
Q:这个 Bash 工具有明显的安全隐患,是什么?
A:没有任何权限检查和命令过滤。AI 生成 rm -rf / 它就会照执行。真实 BashTool(18 个文件)有沙箱、权限确认、命令注入检测、PowerShell 安全分析等一整套防线。我们这版只有「不炸自己」的稳定性加固(超时/缓冲),没有「不作恶」的安全加固——这是 mini 版和生产版最大的鸿沟,也是后续权限系统步骤要补的。
Q:maxBuffer 超了会发生什么?为什么不是截断而是报错?
A:execSync 的 maxBuffer 超限会直接抛 ENOBUFS 错误、丢弃输出。它是保护机制不是截断器。真实版会做智能截断(保留头尾、中间省略)而非整个失败——因为「给 AI 半截输出」通常比「给它一个 buffer 错误」有用。我们这版接住这个错、置 isError 回传,至少不崩。
# 五、踩坑 / 设计权衡
- 同步阻塞 vs 简单:
execSync期间 CLI 完全冻结,长命令体验差。选它纯为学习期可读性,timeout: 30000是它的安全绳。 - 稳定性加固 ≠ 安全加固:三个参数解决的是「工具别把自己搞崩」,完全没碰「工具别把用户搞崩」。别把这个 mini Bash 直接用在不可信输入上。
# 六、与真实源码的对照
| 我们的实现 | Claude Code 源码 |
|---|---|
bash.ts 单文件 execSync | tools/BashTool/ 18 个文件 |
timeout: 30000 固定 | 可配置超时 + run_in_background 后台执行 |
| 无权限检查 | 沙箱 + 权限确认 + 命令注入检测 + PowerShell 安全分析 |
| maxBuffer 超了报错 | 大输出智能截断(留头尾) |
/run 手动测试 | 集成进对话循环,AI 自主调用 + 权限门控 |
# 七、一句话总结
step12 = 给猛兽套笼子:execSync 一行是骨架,timeout / maxBuffer / windowsHide 三参数才是让它敢交给 AI 的稳定性防线,catch 里从 err.stderr 取真实报错让 AI 能纠错,/run 提供脱离 AI 的独立测试通道——但要清醒:这版只做了「别搞崩自己」的加固,「别作恶」的安全防线要等权限系统。
# 下一节预告
Bash 太危险,下一个工具温和许多。step13 实现 Read 工具——readFileSync 的简单封装,却是 AI 编程助手最常用的能力:看代码才能改代码。