Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/01-项目全景与四种入口形态.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# 第 1 章:项目全景与四种入口形态 — 一个 AI CLI 产品的技术蓝图

> 本篇是《深入 Claude Code 源码》系列的开篇。我们将从技术栈选型、启动链路、模块依赖三个维度,建立对整个项目的全局认知。
> 本章是《深入 Claude Code 源码》系列的开篇。我们将从技术栈选型、启动链路、模块依赖三个维度,建立对整个项目的全局认知。

## 为什么要从全景开始?

当你面对一个约 1900 个源码文件(其中 TypeScript 文件约 1884 个)、核心文件超过 4000 行的大型项目时,最大的挑战不是读懂某个函数,而是**不知道从哪里开始读**。

Claude Code 是 Anthropic 公司开发的 AI 驱动命令行编程助手。它不是一个简单的 API wrapper —— 它是一个完整的 AI Agent 运行时,包含了从终端 UI 渲染、多 Agent 编排、工具系统、权限安全到 Prompt 工程的全技术栈。理解它的全貌,等于理解了一个生产级 AI 产品的完整架构。

本篇将回答四个核心问题
本章将回答四个核心问题
1. **同一份源码是怎么对外露出的?** — `entrypoints/` 下四种入口形态(CLI / SDK / MCP server / Sandbox runner)共用一份代码
2. **为什么选择这些技术?** — Bun + TypeScript + Ink + Commander.js 的技术栈选型逻辑
3. **程序是怎么启动的?** — 从 `cli.tsx` 到 REPL 的启动链路
Expand Down
4 changes: 2 additions & 2 deletions docs/02-启动链路与冷启动优化.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
# 第 2 章:启动链路与冷启动优化 — 毫秒级 CLI 启动的工程艺术

> 本篇是《深入 Claude Code 源码》系列的第 2 。我们将深入 `cli.tsx` 和 `main.tsx` 的启动路径,揭示 Claude Code 团队如何将 CLI 启动时间优化到毫秒级别。
> 本章是《深入 Claude Code 源码》系列第 2 。我们将深入 `cli.tsx` 和 `main.tsx` 的启动路径,揭示 Claude Code 团队如何将 CLI 启动时间优化到毫秒级别。

## 为什么启动速度如此重要?

CLI 工具的第一印象就是**启动速度**。用户在终端输入 `claude` 并回车的那一刻,心理预期是"即时响应"。如果启动需要 2 秒,用户会觉得卡顿;如果需要 5 秒,用户会开始怀疑是不是命令输错了。

对于 Claude Code 这样一个重量级应用——近 1900 个 TypeScript 文件、依赖 React/Ink/Yoga 渲染引擎、需要连接 MCP 服务器和 Anthropic API——要做到毫秒级启动,绝非易事。

本篇将揭示 Claude Code 采用的 **6 大启动优化策略**:
本章将揭示 Claude Code 采用的 **6 大启动优化策略**:

1. **快速路径(Fast Path)**:对简单命令实现零 import 返回
2. **侧效果前置(Side-Effect Hoisting)**:利用模块求值时间并行执行 I/O
Expand Down
6 changes: 3 additions & 3 deletions docs/03-配置体系与企业MDM.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 第 3 章:配置体系与企业 MDM — 多层配置的合并之道

> 本篇是《深入 Claude Code 源码》系列第 17 篇。我们将剖析 Settings 系统如何从 5 个正式配置源(加 1 个 Plugin 基底层)中读取、验证、合并配置,以及如何在运行时监听变更并热更新 —— 一个面向企业级部署的多层配置合并架构。除了文件层的合并管线之外,Claude Code 还有两条独立运行的组织级服务管线 —— `services/policyLimits/` 与 `services/settingsSync/` —— 它们不参与设置合并,但同样在「企业管控」与「跨设备一致性」两个维度上塑造了用户最终看到的运行时配置面貌。
> 本章是《深入 Claude Code 源码》系列第 3 章。我们将剖析 Settings 系统如何从 5 个正式配置源(加 1 个 Plugin 基底层)中读取、验证、合并配置,以及如何在运行时监听变更并热更新 —— 一个面向企业级部署的多层配置合并架构。除了文件层的合并管线之外,Claude Code 还有两条独立运行的组织级服务管线 —— `services/policyLimits/` 与 `services/settingsSync/` —— 它们不参与设置合并,但同样在「企业管控」与「跨设备一致性」两个维度上塑造了用户最终看到的运行时配置面貌。

## 为什么需要多层配置?

Expand Down Expand Up @@ -234,7 +234,7 @@ function parseSettingsFileUncached(path: string): {

### 3.2 SettingsSchema 的向后兼容设计

`SettingsSchema` 定义在 `utils/settings/types.ts` 中,使用 `lazySchema()` 延迟构造(与工具系统一致的模式,见第 9 篇)。它的设计严格遵循向后兼容原则:
`SettingsSchema` 定义在 `utils/settings/types.ts` 中,使用 `lazySchema()` 延迟构造(与工具系统一致的模式,见第 10 章)。它的设计严格遵循向后兼容原则:

```typescript
// utils/settings/types.ts:210-241(注释节选)
Expand Down Expand Up @@ -321,7 +321,7 @@ mdm/
└── settings.ts — 解析、缓存、first-source-wins 逻辑
```

**为什么要这样拆?** 因为 `rawRead.ts` 在 `main.tsx` 模块求值阶段就被调用(第 2 篇提到的侧效果前置),此时不能引入任何重量级模块。MDM 读取分为**两个阶段**:
**为什么要这样拆?** 因为 `rawRead.ts` 在 `main.tsx` 模块求值阶段就被调用(第 2 章提到的侧效果前置),此时不能引入任何重量级模块。MDM 读取分为**两个阶段**:

**阶段一:预启动子进程**(`main.tsx:3-4`,模块求值期)

Expand Down
6 changes: 3 additions & 3 deletions docs/04-配置迁移即代码.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 第 4 章:配置迁移即代码 — 把每一次破坏性配置改动写成一段小函数

> 本篇是《深入 Claude Code 源码》系列第 26 篇。我们将剖析 `migrations/` 目录下 11 个迁移文件与 `main.tsx` 的 `runMigrations()` 调度,看 Claude Code 如何把"产品在用户身后偷偷换零件"这件事写成一组幂等、可独立 review、由版本号守护串行执行的小函数。
> 本章是《深入 Claude Code 源码》系列第 4 章。我们将剖析 `migrations/` 目录下 11 个迁移文件与 `main.tsx` 的 `runMigrations()` 调度,看 Claude Code 如何把"产品在用户身后偷偷换零件"这件事写成一组幂等、可独立 review、由版本号守护串行执行的小函数。

## 为什么配置迁移值得单独一篇?

Expand Down Expand Up @@ -32,7 +32,7 @@ resetProToOpusDefault.ts

11 个文件全部是顶层文件——没有子目录、没有公共基类、没有"框架",每个文件导出一个同名的无参函数,函数名就是它做的那件事。这种克制的组织方式背后有一个很清楚的意图:**每一次破坏性的配置改动,都是一段可以被独立 review、独立删除、独立写测试的小代码片段**。你不需要去理解一个"迁移引擎"——只需要打开你感兴趣的那个文件,从头读到尾,三五十行内它就讲完了自己的故事。

本篇按下面的顺序拆这 11 个函数:先看 `main.tsx` 里那条把它们串起来的流水线(§1),再分三组挨个看 —— 把字段从一处搬到另一处的 3 个(§2)、围绕模型字符串做重命名的 6 个(§3)、围绕"旧键名 / 旧弹窗选项"做清理的 2 个(§4),最后把贯穿全章的几条纪律收束起来(§5)。
本章按下面的顺序拆这 11 个函数:先看 `main.tsx` 里那条把它们串起来的流水线(§1),再分三组挨个看 —— 把字段从一处搬到另一处的 3 个(§2)、围绕模型字符串做重命名的 6 个(§3)、围绕"旧键名 / 旧弹窗选项"做清理的 2 个(§4),最后把贯穿全章的几条纪律收束起来(§5)。

---

Expand Down Expand Up @@ -69,7 +69,7 @@ function runMigrations(): void {

**接着是一段固定顺序。** 9 个无条件迁移 + 2 个带条件的,**顺序写死在源码里**,没有依赖图、没有拓扑排序。这个顺序不是随便排的,它隐含了几条事实关系:`resetProToOpusDefault` 要先于所有 Sonnet/Opus 模型字符串改名跑,因为它会基于"用户当时有没有自定义模型"决定要不要弹横幅;`migrateSonnet1mToSonnet45` 要先于 `migrateSonnet45ToSonnet46`,因为它产出的是 `sonnet-4-5-20250929[1m]` 这种显式串、正好是后者要往回卷成 `sonnet[1m]` 别名的输入;`migrateLegacyOpusToCurrent` 和 `migrateOpusToOpus1m` 一前一后,前者把 `claude-opus-4-0` 这种远古串收敛到 `opus` 别名,后者再把 `opus` 升级成 `opus[1m]`。顺序就是事实顺序,不需要装出 DAG 的样子。

**然后是两段条件分支。** `feature('TRANSCRIPT_CLASSIFIER')` 是一道编译期 / 运行期同款的开关,命中才跑 `resetAutoModeOptInForDefaultOffer`;写得最有意思的是那段 `if ("external" === 'ant')`,这是 bundler 留下的字符串折叠点——发到外部用户的 bundle 里这一支永远是死代码、`migrateFennecToOpus` 被裁掉,而内部 `ant` 用户的 bundle 里这一支永远进入、迁移会跑。这是 Claude Code 把"内部独有逻辑"与"外发产物"用同一份源码一起维护的典型手法(详见第 19 篇 Feature Flag 与编译期优化)。
**然后是两段条件分支。** `feature('TRANSCRIPT_CLASSIFIER')` 是一道编译期 / 运行期同款的开关,命中才跑 `resetAutoModeOptInForDefaultOffer`;写得最有意思的是那段 `if ("external" === 'ant')`,这是 bundler 留下的字符串折叠点——发到外部用户的 bundle 里这一支永远是死代码、`migrateFennecToOpus` 被裁掉,而内部 `ant` 用户的 bundle 里这一支永远进入、迁移会跑。这是 Claude Code 把"内部独有逻辑"与"外发产物"用同一份源码一起维护的典型手法(详见第 22 章 Feature Flag 与编译期优化)。

**最后是落版本号。** `saveGlobalConfig(prev => prev.migrationVersion === CURRENT_MIGRATION_VERSION ? prev : { ...prev, migrationVersion: CURRENT_MIGRATION_VERSION })` 这一行也有讲究:内层先比一次再决定要不要返回新对象,避免一次空写——`saveGlobalConfig` 的 updater 如果返回的是同一个对象引用,下游能省一次磁盘 IO 与一次内存中"已经一致就别落盘"的判断。这种"防御性同等性"在 11 个迁移内部到处都是。

Expand Down
6 changes: 3 additions & 3 deletions docs/05-QueryEngine与对话主循环.md
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
# 第 5 章:QueryEngine 与对话主循环 — 一次完整 AI 交互的心跳

> 本篇是《深入 Claude Code 源码》系列的第 5 。我们将深入 `query.ts` 这个 1729 行的核心文件,揭示一次完整的 AI 对话是如何被驱动的——从消息组装、API 调用、工具执行到错误恢复,理解这个 Agent 运行时的"心跳"。
> 本章是《深入 Claude Code 源码》系列第 5 。我们将深入 `query.ts` 这个 1729 行的核心文件,揭示一次完整的 AI 对话是如何被驱动的——从消息组装、API 调用、工具执行到错误恢复,理解这个 Agent 运行时的"心跳"。

## 为什么需要理解对话循环?

如果把 Claude Code 比作一个人体,那 `query.ts` 就是它的**心脏**——对话循环的编排入口。当然,心脏需要血管系统才能工作:重试逻辑在 `services/api/withRetry.ts`,工具执行在 `services/tools/`,停止钩子在 `query/stopHooks.ts`,环境配置在 `query/config.ts`。本篇会覆盖这个完整的"循环系统",而不仅仅是 `query.ts` 一个文件。每一次用户提问,都会触发这个循环:
如果把 Claude Code 比作一个人体,那 `query.ts` 就是它的**心脏**——对话循环的编排入口。当然,心脏需要血管系统才能工作:重试逻辑在 `services/api/withRetry.ts`,工具执行在 `services/tools/`,停止钩子在 `query/stopHooks.ts`,环境配置在 `query/config.ts`。本章会覆盖这个完整的"循环系统",而不仅仅是 `query.ts` 一个文件。每一次用户提问,都会触发这个循环:

```
用户输入 → 组装消息 → 调用 API → 模型返回 → 执行工具 → 结果回传 → 模型继续...
Expand All @@ -18,7 +18,7 @@
- **模型降级**:主模型不可用时,自动切换到 fallback 模型
- **并发工具执行**:只读工具可以并行,写入工具必须串行

本篇将从宏观到微观,层层展开这个循环的设计。
本章将从宏观到微观,层层展开这个循环的设计。

---

Expand Down
4 changes: 2 additions & 2 deletions docs/06-SystemPrompt与OutputStyle注入.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# 第 6 章:System Prompt 与 Output Style 注入 — 精密控制模型行为的提示词体系

> 本篇是《深入 Claude Code 源码》系列的第 4 篇。我们将深入 `constants/prompts.ts`(914 行,commit `290fdc94`)这个核心文件,揭示 Claude Code 如何通过精心设计的 System Prompt 架构,在「精确控制模型行为」和「最大化 Prompt Cache 命中率」之间取得平衡。
> 本章是《深入 Claude Code 源码》系列第 6 章。我们将深入 `constants/prompts.ts`(914 行,commit `290fdc94`)这个核心文件,揭示 Claude Code 如何通过精心设计的 System Prompt 架构,在「精确控制模型行为」和「最大化 Prompt Cache 命中率」之间取得平衡。

## 为什么 System Prompt 值得单独一篇?

Expand All @@ -11,7 +11,7 @@
3. **它是模型行为的根基** — 代码风格约束、安全指令、工具使用优先级、输出格式,全部编码在这里
4. **它有内外版差异** — 通过 `process.env.USER_TYPE === 'ant'` 区分内部版(Anthropic 员工)和外部版(公开用户),同一套代码产出不同的行为指引

本篇将回答三个核心问题
本章将回答三个核心问题
1. **System Prompt 由哪些模块组成,如何组装?** — `getSystemPrompt()` 的完整流程
2. **静态与动态内容如何分离以优化缓存?** — `SYSTEM_PROMPT_DYNAMIC_BOUNDARY` 机制
3. **提示词中编码了哪些关键的行为引导技巧?** — 从安全指令到代码风格约束
Expand Down
12 changes: 6 additions & 6 deletions docs/07-上下文压缩家族.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# 第 7 章:上下文压缩家族 — 无限对话的秘密

> 本篇是《深入 Claude Code 源码》系列的第 6 篇。我们将深入分析 Claude Code 如何在有限的 context window 内支撑无限长度的对话——从 token 预算管理、多级压缩策略,到文件状态缓存与 compact 后恢复的设计。
> 本章是《深入 Claude Code 源码》系列第 7 章。我们将深入分析 Claude Code 如何在有限的 context window 内支撑无限长度的对话——从 token 预算管理、多级压缩策略,到文件状态缓存与 compact 后恢复的设计。
>
> **范围说明**:上下文的"构建"(System Prompt 组装、CLAUDE.md 注入、git status 获取)已在第 4 篇中覆盖,缓存策略的横切视角将在第 7 篇中展开。本篇聚焦于上下文构建完成后的**压缩、清理与恢复**——即当 context window 不够用时,Claude Code 如何在不中断对话的情况下释放空间并重建必要的工作上下文。
> **范围说明**:上下文的"构建"(System Prompt 组装、CLAUDE.md 注入、git status 获取)已在第 6 章中覆盖,缓存策略的横切视角将在第 8 章中展开。本章聚焦于上下文构建完成后的**压缩、清理与恢复**——即当 context window 不够用时,Claude Code 如何在不中断对话的情况下释放空间并重建必要的工作上下文。

## 为什么上下文管理如此重要?

Expand Down Expand Up @@ -350,7 +350,7 @@ export async function autoCompactIfNeeded(
除了这两条最直接的递归保护,源码在 `shouldAutoCompact()` 里还排布了几道针对新一代上下文机制的护栏(`services/compact/autoCompact.ts:174-223`),它们都属于"看似与 compact 无关、一旦同时点火就会互相拆台"那一类隐患:

- **`marble_origami` 这个 ctx-agent**:开启了 `CONTEXT_COLLAPSE` 之后,contextCollapse 自己也是一个 forked agent(querySource = `marble_origami`)。如果它在工作过程中又触发 autocompact,`runPostCompactCleanup` 会顺手调用 `resetContextCollapse()`,把主线程那一份模块级 collapse log 全部清掉。源码注释把这个连锁反应讲得很直白——所以这条 querySource 直接被列入"绝不再触发 autocompact"的黑名单。
- **`REACTIVE_COMPACT` 反应式模式**:当 `tengu_cobalt_raccoon` gate 打开时,全局策略是"前置不再 proactive 压缩,等 API 真的返回 prompt-too-long 再由 reactive compact 兜底",因此 `shouldAutoCompact()` 直接返回 `false`,把决策权完全交给第 5 篇里讲过的 413 恢复链。
- **`REACTIVE_COMPACT` 反应式模式**:当 `tengu_cobalt_raccoon` gate 打开时,全局策略是"前置不再 proactive 压缩,等 API 真的返回 prompt-too-long 再由 reactive compact 兜底",因此 `shouldAutoCompact()` 直接返回 `false`,把决策权完全交给第 5 章里讲过的 413 恢复链。
- **`CONTEXT_COLLAPSE` 启用时的让位**:collapse 自己在 90% 触发承诺、95% 触发阻塞生成,而 autocompact 的阈值落在 effective 减去 13K 的位置(200K 模型上即 167K,相当于 93%)——恰好夹在这两条线中间。如果不让位,autocompact 通常会抢在 collapse 之前点火,把 collapse 即将保住的细粒度上下文一刀切掉。这里通过 `isContextCollapseEnabled()` 二次判定(而不是直接看 feature flag),是为了让 `CLAUDE_CONTEXT_COLLAPSE` 环境变量这种本地覆盖也能生效。

这一串护栏背后的共同主题是:**当一个进程里同时活着多种上下文管理子系统时,谁先点火谁就赢,而点火顺序错了会发生静默的状态破坏**。把所有"我现在不该跑"的判定收拢到 `shouldAutoCompact()` 一个入口,比让每个子系统各自试探安全得多。
Expand All @@ -361,7 +361,7 @@ export async function autoCompactIfNeeded(

这是 compact 的一个创新路径:不调用模型生成总结,而是**直接使用 Session Memory 系统已有的对话记忆**作为压缩后的总结。

Session Memory 是一个独立的后台系统(将在第 23 篇详述),它在对话过程中持续异步提取关键信息到磁盘文件。当 compact 触发时,如果 Session Memory 已经有内容,就直接用它作为总结,跳过昂贵的 API 调用:
Session Memory 是一个独立的后台系统(将在第 31 章详述),它在对话过程中持续异步提取关键信息到磁盘文件。当 compact 触发时,如果 Session Memory 已经有内容,就直接用它作为总结,跳过昂贵的 API 调用:

```typescript
export async function trySessionMemoryCompaction(
Expand Down Expand Up @@ -542,7 +542,7 @@ export class FileStateCache {

**文件**:`services/compact/compactWarningState.ts`

这是一个有趣的小模块——它复用了第 3 篇介绍的极简 Store 来管理 compact 告警的抑制状态:
这是一个有趣的小模块——它复用了第 33 章介绍的极简 Store 来管理 compact 告警的抑制状态:

```typescript
import { createStore } from '../../state/store.js'
Expand Down Expand Up @@ -573,7 +573,7 @@ export function useCompactWarningSuppression(): boolean {
}
```

这个 hook 只有 7 行有效代码,单独成文件是为了**让 `compactWarningState.ts` 保持 React-free**——`microCompact.ts` 在每个 turn 的预处理阶段都会读写这个 store,如果状态文件 import 了 React,那么整条 print-mode 启动路径都会被迫把 React 拖进模块图,这是源码注释里直接写明的考量。这种"状态文件零依赖、订阅 hook 拆出去"的拆法在第 3 篇 Store 模式之外又添了一个小的样本。
这个 hook 只有 7 行有效代码,单独成文件是为了**让 `compactWarningState.ts` 保持 React-free**——`microCompact.ts` 在每个 turn 的预处理阶段都会读写这个 store,如果状态文件 import 了 React,那么整条 print-mode 启动路径都会被迫把 React 拖进模块图,这是源码注释里直接写明的考量。这种"状态文件零依赖、订阅 hook 拆出去"的拆法在第 33 章 Store 模式之外又添了一个小的样本。

---

Expand Down
Loading
Loading