# Step 12: Bash 工具

一句话导读:往 step11 的插槽里插入第一个「真」工具——用 execSync 加三个关键参数(超时、缓冲上限、隐藏窗口)把 shell 命令执行封装成一个 ToolDef。它是能力最强也最危险的工具,而这一步的重点正是「怎么给一头猛兽套上笼子」。


# 一、这一步做了什么(What)

step12 实现 tools/bash.ts,第一个能真正影响外部世界的工具:

  1. BashTool:实现 ToolDef 形状,execute 用 execSync 同步执行命令;
  2. 三个安全参数:timeout: 30000、maxBuffer: 10MB、windowsHide: true;
  3. 成败统一为 ToolResult:成功返回输出,失败捕获 err.stderr 并置 isError: true;
  4. /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 下不弹黑窗
});
1
2
3
4
5
6
7
8

# 空命令先挡一道

const command = input.command as string;
if (!command || !command.trim()) {
  return { content: [{ type: 'text', text: '错误: 命令不能为空' }], isError: true };
}
1
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 };
}
1
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 回给上层
1
2
3
4
5
6
7
8
9
10
11

# /run:脱离 AI 的测试通道

if (input.startsWith('/run ')) {
  const r = await BashTool.execute({ command: cmd });
  // ...直接展示结果
}
1
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 编程助手最常用的能力:看代码才能改代码。