第一阶段 · 模块一 · 第一节:Claude Code 全局架构
核心问题
Claude Code 的三层架构是什么?cli.tsx、main.tsx、QueryEngine、query.ts 各自负责什么?它们如何协作?
◇ 本节位置
Claude Code 全局架构
┌─────────────────────────────────────────────────────────────────────┐
│ 入口层(entrypoints/) │
│ │
│ cli.tsx ──> main.tsx ──> REPL.tsx (交互模式) │
│ └──> QueryEngine.ts (SDK/headless) │
└──────────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 查询引擎层 │
│ │
│ QueryEngine.ts ──> query.ts (核心循环) │
│ ├── callModel() 调用 Claude API │
│ ├── StreamingToolExecutor 并行工具执行 │
│ └── yield SDKMessage 流式输出 │
└──────────────────────────────┬──────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────┐
│ 工具/服务/状态层 │
│ │
│ tools.ts (40+工具) / commands.ts (~80命令) / services/ │
│ context.ts (上下文) / cost-tracker.ts (成本追踪) │
└─────────────────────────────────────────────────────────────────────┘
本节内容:三层架构概览与入口层分析
一、入口层:cli.tsx 的 bootstrap 模式
1.1 cli.tsx 的核心职责
源码位置 :src/entrypoints/cli.tsx 第 28-45 行
cli.tsx 是整个 Claude Code 的程序入口 ,它的核心职责是:在加载完整 CLI 之前,先检查特殊标志。
typescript
/**
* Bootstrap entrypoint - checks for special flags before loading the full CLI.
* All imports are dynamic to minimize module evaluation for fast paths.
* Fast-path for --version has zero imports beyond this file.
*/
async function main(): Promise<void> {
const args = process.argv.slice(2);
// Fast-path for --version/-v: zero module loading needed
if (args.length === 1 && (args[0] === '--version' || args[0] === '-v' || args[0] === '-V')) {
// MACRO.VERSION is inlined at build time
console.log(`${MACRO.VERSION} (Claude Code)`);
return; // ⚠️ 关键:完全不加载任何模块
}
// 其他快速路径:--dump-system-prompt, --daemon, --bg 等
// 最后:加载完整 CLI
const { main: cliMain } = await import('../main.js');
await cliMain();
}
1.2 为什么需要 bootstrap?
问 1:为什么 --version 需要零模块加载?
用户期望 --version 立即响应(毫秒级)。如果加载完整模块:
claude --version (静态导入)
│
├── 加载 main.tsx(4683行)
├── 加载 QueryEngine.ts(1295行)
├── 加载 query.ts(1729行)
├── 加载 tools.ts(40+工具)
├── 加载 commands.ts(~80命令)
├── 加载所有 services/
└── 打印版本
│
└── 耗时:数百毫秒
零模块加载则:
claude --version (cli.tsx bootstrap)
│
├── 读取 process.argv
├── 匹配 --version
├── 打印 MACRO.VERSION
└── 耗时:<10毫秒
问 2:MACRO.VERSION 是什么?
typescript
// MACRO 是 Bun 的编译时内建变量
// 在构建时从 package.json + git describe 注入
console.log(`${MACRO.VERSION} (Claude Code)`);
// 输出:claude-code 2.1.88
问 3:动态导入的本质是什么?
typescript
// 静态导入:编译时确定,运行时立即加载
import { main } from '../main.js'; // 立即加载所有模块
// 动态导入:运行时确定,按需加载
const { main } = await import('../main.js'); // 执行到这里才加载
1.3 cli.tsx 的完整快速路径
源码位置 :src/entrypoints/cli.tsx 第 28-302 行
cli.tsx main()
│
├── --version / -v / -V
│ └── console.log(MACRO.VERSION) ← 零模块加载
│
├── --dump-system-prompt
│ └── 加载 config.js + prompts.js
│
├── --daemon
│ └── 加载 daemon/main.js
│
├── --bg / ps / logs / attach / kill
│ └── 加载 cli/bg.js
│
├── --new / --list / --reply
│ └── 加载 cli/handlers/templateJobs.js
│
└── [其他命令]
└── 加载 main.js
└── 启动完整 CLI
二、主程序层:main.tsx
2.1 main.tsx 的角色
源码位置 :src/main.tsx
main.tsx 是一个 4683 行的大文件,包含:
- Commander.js 程序定义
- 所有 action handler(
claude [prompt]、claude --resume等) - 子命令处理(
claude mcp install) - 初始化逻辑(
init())
2.2 preAction 钩子:惰性初始化
问 1:为什么 --help 不触发初始化?
typescript
async function run(): Promise<CommanderCommand> {
const program = new CommanderCommand();
// preAction 钩子:在真正执行命令之前触发
program.hook('preAction', async () => {
await Promise.all([
ensureMdmSettingsLoaded(),
ensureKeychainPrefetchCompleted()
]);
await init(); // 只有执行实际命令才触发
});
// 注册 CLI 选项
program
.name('claude')
.option('-p, --print', '...')
.option('--model <model>', '...')
// ... 80+ 个选项
.action(async (prompt, options) => {
await launchRepl(...); // 实际执行业务逻辑
});
return program.parse();
}
| 用户输入 | Commander.js 行为 | preAction 触发? |
|---|---|---|
claude --help |
直接显示帮助 | ✗ 不触发 |
claude "hello" |
执行 action | ✓ 触发 |
claude --version |
cli.tsx 已处理 | ✗ 不触发 |
问 2:init() 函数的职责是什么?
init() 是 Claude Code 的核心初始化函数,负责任务:
- 加载用户配置(
~/.claude/) - 检查 API key 和认证状态
- 初始化 MCP 客户端
- 加载插件和技能
三、查询引擎层:QueryEngine vs query()
3.1 两层分离的设计
┌─────────────────────────────────────────────────────────────────────┐
│ QueryEngine.ts (SDK/headless 封装) │
│ │
│ class QueryEngine { │
│ private mutableMessages: Message[] // 会话状态 │
│ private permissionDenials: SDKPermissionDenial[] // 权限记录 │
│ private totalUsage: NonNullableUsage // 使用量追踪 │
│ │
│ async *submitMessage(prompt, options) { │
│ // 1. 包装 canUseTool,记录权限拒绝 │
│ // 2. 调用 query() 核心循环 │
│ // 3. session 持久化 │
│ // 4. yield SDK 格式消息 │
│ } │
│ } │
└──────────────────────────────┬──────────────────────────────────────┘
│
│ 调用
▼
┌─────────────────────────────────────────────────────────────────────┐
│ query.ts (核心循环) │
│ │
│ while (true) { // 第 307 行 │
│ // 1. 状态解构 │
│ // 2. 调用 Claude API(流式) │
│ // 3. 检查是否需要工具调用 │
│ // 4. 执行工具(StreamingToolExecutor 并行) │
│ // 5. 状态转移,继续循环 │
│ } │
└─────────────────────────────────────────────────────────────────────┘
3.2 QueryEngine 的职责
源码位置 :src/QueryEngine.ts 第 184-220 行
typescript
export class QueryEngine {
// 会话状态
private mutableMessages: Message[] = []
private permissionDenials: SDKPermissionDenial[] = []
private totalUsage: NonNullableUsage = EMPTY_USAGE
async *submitMessage(
prompt: string | ContentBlockParam[],
options?: { uuid?: string; isMeta?: boolean },
): AsyncGenerator<SDKMessage, void, unknown> {
// 1. 包装 canUseTool,记录权限拒绝
const wrappedCanUseTool: CanUseToolFn = async (tool, input, toolUseContext) => {
const result = await canUseTool(tool, input, toolUseContext)
if (!result.allowed) {
this.permissionDenials.push({ tool, input, toolUseID })
}
return result
}
// 2. 调用核心循环
for await (const message of query({
messages: this.mutableMessages,
canUseTool: wrappedCanUseTool,
// ...
})) {
// 3. session 持久化
if (options?.persistSession && message.type === 'assistant') {
await recordTranscript(this.mutableMessages)
}
// 4. yield SDK 格式消息
yield toSDKMessage(message)
}
}
}
问 1:为什么需要 QueryEngine 封装?
| 问题 | 原因 |
|---|---|
| query() 是纯函数 | 状态通过参数传递,不能自己管理 session |
| SDK 需要持久化 | session 跨调用持久化 |
| 权限拒绝需要记录 | SDK 用户可能需要查询被拒绝的工具 |
问 2:wrappedCanUseTool 的作用?
typescript
const wrappedCanUseTool: CanUseToolFn = async (tool, input, toolUseContext) => {
const result = await canUseTool(tool, input, toolUseContext) // 原始权限检查
if (!result.allowed) {
this.permissionDenials.push({ tool, input, toolUseID }) // 记录拒绝
}
return result
}
- 包装原始的
canUseTool - 添加记录权限拒绝的副作用
- SDK 可以通过
permissionDenials查询哪些工具被拒绝
3.3 query.ts 的核心循环
源码位置 :src/query.ts 第 307 行
typescript
// eslint-disable-next-line no-constant-condition
while (true) {
// 1. 状态解构:从 state 中读取最新状态
let { toolUseContext } = state
const {
messages,
turnCount,
autoCompactTracking,
// ...
} = state
// 2. 初始化:每轮循环的临时状态
const assistantMessages: AssistantMessage[] = []
const toolResults: (UserMessage | AttachmentMessage)[] = []
let needsFollowUp = false
// 3. 调用 Claude API(流式)
for await (const message of deps.callModel({
messages: prependUserContext(messagesForQuery, userContext),
systemPrompt: fullSystemPrompt,
tools: toolUseContext.options.tools,
signal: toolUseContext.abortController.signal,
// ...
})) {
// 处理每个流式消息
if (message.type === 'assistant') {
assistantMessages.push(message)
// 检查是否有 tool_use blocks
if (hasToolUseBlocks(message)) {
needsFollowUp = true
}
}
}
// 4. 检查退出条件
if (!needsFollowUp) {
return { reason: 'completed' } // 没有工具调用,正常完成
}
// 5. 执行工具
const toolUpdates = streamingToolExecutor
? streamingToolExecutor.getRemainingResults()
: runTools(toolUseBlocks, ...)
for await (const update of toolUpdates) {
if (update.message) {
toolResults.push(update.message)
}
}
// 6. 状态转移:更新 state,继续循环
state = {
messages: [...messagesForQuery, ...assistantMessages, ...toolResults],
turnCount: nextTurnCount,
// ...
}
} // while (true) 结束
问:为什么用 while(true) 而不是递归?
| 方案 | 问题 |
|---|---|
| 递归 | 长时间会话会栈溢出(每轮调用累积栈帧) |
| while(true) | 状态在堆上,无栈溢出风险 |
typescript
// 递归方案
async function query(state) {
const nextState = computeNext(state)
return query(nextState) // ⚠️ 每次递归累积一个栈帧
}
// while(true) 方案
while (true) {
state = computeNext(state) // ✅ 状态在堆上
}
四、工具/服务/状态层
4.1 工具系统
源码位置 :src/tools.ts
Claude Code 内置 40+ 工具:
| 工具 | 作用 |
|---|---|
Read |
读取文件 |
Write |
写入文件 |
Bash |
执行 shell 命令 |
Glob |
文件模式匹配 |
Grep |
文本搜索 |
WebSearch |
网络搜索 |
| ... | ... |
4.2 命令系统
源码位置 :src/commands.ts
Claude Code 支持 ~80 个斜杠命令:
| 命令 | 作用 |
|---|---|
/help |
显示帮助 |
/clear |
清空会话 |
/compact |
压缩上下文 |
/model |
切换模型 |
| ... | ... |
五、设计模式总结
5.1 责任链模式
cli.tsx (bootstrap)
↓
main.tsx (程序入口)
↓
QueryEngine (SDK 封装)
↓
query (核心循环)
每层只负责自己的职责,下一层不知道上一层的存在。
5.2 工厂模式
typescript
const streamingToolExecutor = config.gates.streamingToolExecution
? new StreamingToolExecutor(...)
: null
根据条件创建不同的执行器。
5.3 状态机模式
typescript
while (true) {
state = computeNext(state) // 状态转移
}
六、思考题
思考题 1:动态导入的风险
问题 :如果 main.js 加载失败,用户的体验是什么?如何改进?
答案:
用户会看到难以理解的错误:
Error: Cannot find module '../main.js'
SyntaxError: /path/to/main.js:123
改进方案:
typescript
try {
const { main: cliMain } = await import('../main.js');
await cliMain();
} catch (error) {
exitWithError(
`Failed to load Claude Code. This may be caused by a corrupted installation.\n` +
`Try reinstalling: npm install -g @anthropic-ai/claude-code\n` +
`Error: ${error.message}`
);
}
思考题 2:三层 vs 两层
问题:如果只有 cli.tsx → query(两层),有什么问题?
答案:
- session 无法持久化:query() 每次调用都是新的会话
- SDK 模式无法实现:SDK 需要管理会话状态
- 权限拒绝无法追踪:无法记录用户拒绝了哪些工具
- 代码耦合:CLI 逻辑和核心循环混在一起
思考题 3:while(true) 的退出条件
问题:query.ts 的 while(true) 循环有哪些退出条件?
答案:
| 退出条件 | 触发场景 |
|---|---|
{ reason: 'completed' } |
没有工具调用,正常完成 |
{ reason: 'max_turns' } |
达到最大轮数限制 |
{ reason: 'aborted_streaming' } |
用户中断 |
{ reason: 'model_error' } |
API 调用错误 |
{ reason: 'stop_hook_prevented' } |
stop hook 阻止执行 |
七、延伸阅读
| 文件 | 行数 | 核心内容 |
|---|---|---|
src/entrypoints/cli.tsx |
302 | Bootstrap 入口 |
src/main.tsx |
4683 | 主程序 |
src/QueryEngine.ts |
1295 | SDK 封装 |
src/query.ts |
1729 | 核心循环 |
八、下节预告
下一节我们将深入 cli.tsx 的 bootstrap 机制:
- 动态导入的详细分析
- feature() Bun DCE 的作用
- 快速路径的扩展规则