Day 20 / 共 20 天 · 收官 · 加深版

构建/测试/native + 收官串讲

最后一天。前 19 天从源码逐层拆到了循环、工具、扩展、平台化;今天补上"怎么把这一切打包成能跑的产物"(构建/测试/原生包,贴真实代码),然后把 20 天串成一张完整地图、提炼贯穿全书的设计哲学、给你继续深入的路线。

📍 你在整门课的位置 · 收官(20 天走到终点,今天回望整条路)
W1 心智 W2 核心引擎 W3 扩展能力 W4 平台化 D20 收官串讲
L01

构建双管线回顾

🤔 痛点:源码是 TS + Bun 专属语法,用户机器上只有 node 怎么办? 你开发时用 Bun(快),代码里还用了 import.meta.requireglobalThis.Bun 这类 Bun 专有写法。但装你 CLI 的用户机器上多半只有 node——直接把源码丢过去必然跑不起来。
💡 本质:构建 = 把家常菜翻译成到处能点的标准菜单 构建就像餐厅把"后厨土话菜谱"整理成"任何门店都看得懂的标准菜单":打包成一份产物,再用正则把 Bun 专有语法改写成 node/bun 两边都认的写法。一份产物、两种运行时都能跑。

Day 02 加深版讲过。两条管线(package.json:44):Bun.build(默认,build.tssplitting:true 拆几百 chunk)和 Vite/Rollupbuild:vite,发布用)。产物双兼容靠 build.ts 正则打补丁:把 Bun 专有的 import.meta.require 换成 Node 版、给裸 globalThis.Buntypeof 守卫。两个 bin 就是一行 import "./cli.js" 的壳(Day 02 贴过真实代码)。

为什么同一份产物要 node/bun 都能跑? 用户环境不一定有 Bun。开发用 Bun(快),但发布的 ccb 默认走 node(普及)。与其打两份产物,不如打一份、用正则把"Bun 专有语法"改成"两边都认的写法"。这是"最大化兼容性"的务实工程——逆向重写的目标之一就是能在尽量多的环境跑起来。
L02

feature flags:编译期开关(真实来源)

贯穿全教程你反复看到 feature('BRIDGE_MODE')feature('DAEMON')feature('COORDINATOR_MODE')feature('TOKEN_BUDGET')……这些是编译期 feature flagsscripts/defines.tsDEFAULT_BUILD_FEATURES,约 40 个)。

  • 构建时通过"宏(MACRO)"注入为常量,运行时用 import { feature } from 'bun:bundle' 消费。
  • 关掉某 feature,相关代码被死代码消除——直接不进产物。
  • dev 模式(bun run dev)默认全开。
📝 举个例子:关掉一个 feature 会发生什么 构建时把 DAEMON 设为 false → 源码里 feature('DAEMON') && daemonCmd(Day 11 见过)中的 feature('DAEMON') 变成编译期常量 falsefalse && daemonCmd 恒为假 → 打包器把 daemon 相关整块代码当"死代码"直接删掉,不进产物 → 精简版体积更小、也无法被逆向出该功能。
feature flag 的好处:一套代码,多种"发行版"。想要精简版就关掉 workflow/remote/voice 等 feature 重新构建,产物更小、启动更快;想要全功能就全开。而且"关掉的功能代码根本不进产物"(死代码消除),不是运行时判断——所以关掉的功能零开销、也不会被逆向出来。你在教程里遇到 feature('X') 门控的东西(远程/workflow/voice/buddy/coordinator…)基本都是可选特性;核心(循环/工具/UI/权限)不受 flag 控制。这也是 Day 01/17/19 讲的"判断官方 vs 扩展"窍门之一。
L03

测试与依赖注入

测试用 bun test。回顾 Day 06 加深版讲的依赖注入——query()const deps = params.deps ?? productionDeps():能注入假的 callModel,于是能测整个 agent 循环而不真调 API

  • bun test:单元/集成测试。
  • bun run test:production:产物级测试,支持 --offline(不联网)。
  • bun run precheck:提交前三合一(typecheck + biome check + test)。
  • check:bundle(产物完整性)、check:unused(knip 查未用代码)、health(健康检查)。
可测试性是设计出来的:能"不联网测 agent 循环",靠的是 Day 06/07 讲的依赖注入 + 工具协议层解耦。如果代码里到处直接 new Anthropic()、直接调 API,就没法测。这跟 gov-agents 教程的 FakeLLM 是同一个道理:把"外部依赖"抽成可替换的接口,测试时换成假的。好的可测试性不是事后补的,是架构时就留好注入点。
L04

native 原生包(*-napi)

packages/ 里几个 *-napi原生能力——JS 干不了或干得慢的活,交给原生代码:

干什么
audio-capture-napi麦克风音频捕获(Voice 模式)——真带预编译 .node
image-processor-napi图像处理
color-diff-napi颜色差异计算
modifiers-napi键盘修饰键检测(macOS FFI,bun:ffi)——就是 Day 18 换行判定里 isModifierPressed('shift') 用的
url-handler-napiURL scheme 处理
N-API / FFI 是什么? N-API 是 Node 提供的"用 C/Rust 等原生代码写扩展、给 JS 调用"的接口。FFI(外部函数接口)是"直接调用系统原生库函数"。为什么需要?像"捕获麦克风音频""检测键盘修饰键"这类涉及操作系统底层的事,JS 做不到或很别扭,得用原生代码。有意思的是——多数 *-napi 包的 main 直接指向 ./src/index.ts,即靠 Bun 直接跑 TS + bun:ffi,而非预编译二进制;只有 audio-capture 真带 vendored .node。构建时这些原生资源被复制到 dist/vendor/(Day 02 的 build.ts)。
L05

20 天全景回顾

W1 心智:项目全景 → 启动链 → Agent 循环 → Ink UI → 完整旅程
W2 核心引擎:QueryEngine → Tool 接口 → 内置工具 → 权限 → 上下文管理
W3 扩展能力:Slash 命令 → MCP → Skills → 事件 Hooks → 子代理
W4 平台化:会话记忆 → 配置模型成本 → TUI 深入 → 远程/workflow → 构建收官
query() Agent 主循环 W2 · 灵魂 W1 启动链 + Ink UI(脸) 工具 + 权限(手脚) 上下文/成本管理 W3 扩展:命令/MCP/Skill/子代理 会话/记忆(存档) W4 配置/远程/构建
图注:全书一张图——query() 循环是轴心,其余一切(UI/工具/权限/上下文/扩展/平台化)都围着它转。

一次对话的完整机理,现在你能对着真实源码完整讲出来:

ccb 启动 → cli.tsx fast-path → main/init → Ink 挂载 REPL       [Day 02/04]
你输入 → PromptInput 提交 → QueryEngine 起 turn               [Day 05/06/18]
  queryLoop while(true):                                      [Day 03/06]
    分级压缩上下文(microcompact/autocompact)                   [Day 10]
    → 调 Anthropic API 流式返回                                [Day 03/17]
    → 检测 tool_use → 权限检查(5模式/判定顺序) → 执行工具       [Day 07/08/09]
    → 结果回填 → 下一轮 … 直到不再要工具(+Stop hook/token预算)  [Day 03/10]
  可派子代理并行(递归 query)、可用 MCP/Skill/命令扩展工具      [Day 11-15]
→ 结果渲染回 Ink 界面 · 成本自动记账 · transcript 落盘         [Day 04/16/17]
L06

贯穿全书的设计哲学

如果 20 天只带走 7 句话,就这些——它们比任何单个 API 都重要:

1. 统一抽象覆盖多样需求:一切工具归约到 Tool(协议层,Day 07/12)、一切命令/技能/MCP prompt 归约到 Command(Day 11/13)、多代理只是递归 query(Day 15)。不堆砌 N 套机制。
2. 循环是核心,其余是外围:QueryEngine/query 那个 while 循环是灵魂(Day 06),UI/命令/MCP 都围着它转。抓住循环就抓住全局。
3. fail-closed 安全默认 + 多道不可绕过的闸:工具默认当"危险"(Day 07 buildTool)、权限 deny/ask/safetyCheck 连 bypass 也拦(Day 08/09)。安全靠机制不靠自觉。
4. 上下文/成本经济:分级压缩、模型分档(主/小快)、结果落盘、Read 去重、prompt 缓存——处处省 token 省钱(Day 08/10/17)。
5. 不可变数据避免状态错乱:queryLoop 整体替换 State(Day 06)、上下文瘦身浅拷贝(Day 10)、编辑用不可变 Cursor(Day 18)——一以贯之。
6. 依赖注入换可测试 + 隔离不受信:query 的 deps 让测试不联网(Day 06);子代理/远程任务独立进程、hook 沙箱(Day 15/19)。
7. 冷启动优化到极致:fast-path 动态 import(Day 02)、命令懒加载(Day 11)、条件技能(Day 13)、code splitting(Day 02)——只加载此刻需要的。
L07

那些坑再提醒一次

  • 文档滞后于代码:CLAUDE.md 说 2.2.1,实际 2.8.3——以源码为准。
  • CLAUDE.md ≈ AGENTS.md:同一份的两个副本。
  • 两种 hooksrc/hooks 是 React UI;src/utils/hooks 是事件系统(Day 14)。
  • prompt() ≠ description():发给模型的工具说明用 prompt(Day 07/12)。
  • "Task" 实为 "Agent":Task 只是别名(Day 08)。
  • 区分官方 vs 扩展:远程/workflow/voice/多 Provider 是本 fork 扩展(feature() 门控、@claude-code-best/* 包名、stub 文件);核心循环/工具/权限/UI 是官方真实机制(Day 01/17/19/20)。
  • 此版本只有 !(bash)前导模式,没有 #(memory)独立输入模式(Day 18)。
这些"坑"本身是最好的一课:真实工程从不完美。学会以代码为准、识别扩展与核心、区分同名不同物,比记住任何 API 都更能让你读懂真实项目。
L08

继续深入的路线

1

亲手 dev 跑 + 打断点观察

query.ts:1092(检测 tool_use)、:1647(completed)、toolExecution.ts:1256(call)打 log,真的发几句需求看循环怎么转。

2

精读 query.ts 全文

它是灵魂(2057 行)。今天你懂了骨架,逐行读一遍 queryLoop,把 7 种 continue、各种停止/恢复分支吃透。

3

写一个自定义工具

在 builtin-tools 里仿 Read 写个新工具,走一遍 Day 07 的接口 + buildTool,注册进 getAllBaseTools,看模型怎么调它。

4

配 hook / 写 skill / 接 MCP

实操 Day 12/13/14——配个 PreToolUse hook 拦 Bash、写个 SKILL.md、接个 MCP server。

5

对比官方 Claude Code

你现在用的这个 Claude Code 就是官方版。对照本 fork,体会"逆向重写"和"原版"的异同——最好的验证。

L09

结业 🎉

你已经对着真实源码完整走过了 Claude Code CLI 的 20 天。现在的你应该能——

  • ✅ 讲清"一个终端 AI 编码 Agent"从启动到干完活的完整机理;
  • ✅ 打开 query.ts 读懂那个 agent 循环的真实代码;
  • ✅ 说清工具系统、权限、上下文管理各自怎么实现(能引用到具体函数/行号);
  • ✅ 理解 Slash 命令/MCP/Skills/Hooks/子代理这套扩展机制的统一底层;
  • ✅ 分清哪些是官方核心、哪些是本 fork 扩展;
  • ✅ 带走 7 条设计哲学,迁移到你自己的 AI 应用开发。

这套"Agentic CLI"的工程范式——循环 + 工具 + 权限 + 上下文管理 + 可扩展——是当下 AI 应用的核心骨架。你读懂的不只是一个 CLI,而是一整类 AI Agent 系统的造法

"一个 Agent CLI 的复杂度,不在任何单个功能,而在让循环、工具、权限、上下文、UI、扩展这六件事协同工作。读懂它们如何咬合,你就有了造任何 AI Agent 产品的地基。"
← Day 19 远程/workflow 🎉 返回 20 天总目录