构建/测试/native + 收官串讲
最后一天。前 19 天从源码逐层拆到了循环、工具、扩展、平台化;今天补上"怎么把这一切打包成能跑的产物"(构建/测试/原生包,贴真实代码),然后把 20 天串成一张完整地图、提炼贯穿全书的设计哲学、给你继续深入的路线。
构建双管线回顾
import.meta.require、globalThis.Bun 这类 Bun 专有写法。但装你 CLI 的用户机器上多半只有 node——直接把源码丢过去必然跑不起来。Day 02 加深版讲过。两条管线(package.json:44):Bun.build(默认,build.ts,splitting:true 拆几百 chunk)和 Vite/Rollup(build:vite,发布用)。产物双兼容靠 build.ts 正则打补丁:把 Bun 专有的 import.meta.require 换成 Node 版、给裸 globalThis.Bun 加 typeof 守卫。两个 bin 就是一行 import "./cli.js" 的壳(Day 02 贴过真实代码)。
ccb 默认走 node(普及)。与其打两份产物,不如打一份、用正则把"Bun 专有语法"改成"两边都认的写法"。这是"最大化兼容性"的务实工程——逆向重写的目标之一就是能在尽量多的环境跑起来。feature flags:编译期开关(真实来源)
贯穿全教程你反复看到 feature('BRIDGE_MODE')、feature('DAEMON')、feature('COORDINATOR_MODE')、feature('TOKEN_BUDGET')……这些是编译期 feature flags(scripts/defines.ts 的 DEFAULT_BUILD_FEATURES,约 40 个)。
- 构建时通过"宏(MACRO)"注入为常量,运行时用
import { feature } from 'bun:bundle'消费。 - 关掉某 feature,相关代码被死代码消除——直接不进产物。
- dev 模式(
bun run dev)默认全开。
DAEMON 设为 false → 源码里 feature('DAEMON') && daemonCmd(Day 11 见过)中的 feature('DAEMON') 变成编译期常量 false → false && daemonCmd 恒为假 → 打包器把 daemon 相关整块代码当"死代码"直接删掉,不进产物 → 精简版体积更小、也无法被逆向出该功能。feature('X') 门控的东西(远程/workflow/voice/buddy/coordinator…)基本都是可选特性;核心(循环/工具/UI/权限)不受 flag 控制。这也是 Day 01/17/19 讲的"判断官方 vs 扩展"窍门之一。测试与依赖注入
测试用 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(健康检查)。
new Anthropic()、直接调 API,就没法测。这跟 gov-agents 教程的 FakeLLM 是同一个道理:把"外部依赖"抽成可替换的接口,测试时换成假的。好的可测试性不是事后补的,是架构时就留好注入点。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-napi | URL scheme 处理 |
*-napi 包的 main 直接指向 ./src/index.ts,即靠 Bun 直接跑 TS + bun:ffi,而非预编译二进制;只有 audio-capture 真带 vendored .node。构建时这些原生资源被复制到 dist/vendor/(Day 02 的 build.ts)。20 天全景回顾
一次对话的完整机理,现在你能对着真实源码完整讲出来:
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]
贯穿全书的设计哲学
如果 20 天只带走 7 句话,就这些——它们比任何单个 API 都重要:
那些坑再提醒一次
- 文档滞后于代码:CLAUDE.md 说 2.2.1,实际 2.8.3——以源码为准。
- CLAUDE.md ≈ AGENTS.md:同一份的两个副本。
- 两种 hook:
src/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)。
继续深入的路线
亲手 dev 跑 + 打断点观察
在 query.ts:1092(检测 tool_use)、:1647(completed)、toolExecution.ts:1256(call)打 log,真的发几句需求看循环怎么转。
精读 query.ts 全文
它是灵魂(2057 行)。今天你懂了骨架,逐行读一遍 queryLoop,把 7 种 continue、各种停止/恢复分支吃透。
写一个自定义工具
在 builtin-tools 里仿 Read 写个新工具,走一遍 Day 07 的接口 + buildTool,注册进 getAllBaseTools,看模型怎么调它。
配 hook / 写 skill / 接 MCP
实操 Day 12/13/14——配个 PreToolUse hook 拦 Bash、写个 SKILL.md、接个 MCP server。
对比官方 Claude Code
你现在用的这个 Claude Code 就是官方版。对照本 fork,体会"逆向重写"和"原版"的异同——最好的验证。
结业 🎉
你已经对着真实源码完整走过了 Claude Code CLI 的 20 天。现在的你应该能——
- ✅ 讲清"一个终端 AI 编码 Agent"从启动到干完活的完整机理;
- ✅ 打开
query.ts读懂那个 agent 循环的真实代码; - ✅ 说清工具系统、权限、上下文管理各自怎么实现(能引用到具体函数/行号);
- ✅ 理解 Slash 命令/MCP/Skills/Hooks/子代理这套扩展机制的统一底层;
- ✅ 分清哪些是官方核心、哪些是本 fork 扩展;
- ✅ 带走 7 条设计哲学,迁移到你自己的 AI 应用开发。
这套"Agentic CLI"的工程范式——循环 + 工具 + 权限 + 上下文管理 + 可扩展——是当下 AI 应用的核心骨架。你读懂的不只是一个 CLI,而是一整类 AI Agent 系统的造法。