模块二:启动与状态 | 前置依赖:第 01-03 课 | 预计学习时间:60 分钟
学习目标
完成本课后,你将能够:
- 画出从
cli.tsx到 REPL 就绪的完整启动时序图 - 说明三个入口点(CLI/MCP/SDK)的差异
- 列出
init.ts的 17 步初始化序列及其目的 - 理解迁移系统如何管理配置演进
4.1 三个入口点
Claude Code 不是只有一种启动方式。根据使用场景,有三个不同的入口:
entrypoints/
├── cli.tsx ← 终端交互模式(最常用)
├── mcp.ts ← 作为 MCP Server 运行
└── sdk/ ← 作为 Agent SDK 被其他程序调用
├── coreSchemas.ts (55KB,核心 Schema 定义)
├── controlSchemas.ts (控制 Schema)
└── coreTypes.ts (类型定义)
| 入口 | 使用场景 | 启动后的形态 |
|---|---|---|
cli.tsx | claude 命令运行 | 交互式 REPL,接受用户输入 |
mcp.ts | 作为 MCP 服务器 | stdio 模式,响应 MCP 客户端请求 |
sdk/ | 被其他应用调用 | 函数库,提供 API 接口 |
4.2 CLI 入口:cli.tsx 的快速路径
cli.tsx 是绝大多数用户的入口。它的第一个设计原则是:尽快响应简单请求。
快速路径(无需加载完整应用)
claude --version → 直接输出版本号,退出
claude --daemon-worker → 直接启动 daemon worker
claude daemon [cmd] → 直接管理 daemon
claude ps/logs/kill → 直接管理会话
这些命令不需要初始化 React、加载工具、连接 MCP — 它们在加载完整模块之前就返回了。
并行预取(启动优化)
对于需要完整启动的命令,cli.tsx 在加载模块的同时启动了三个并行预取:
// cli.tsx / main.tsx 最顶部(模块加载的副作用)
startMdmRawRead() // 1. 预读 MDM(移动设备管理)配置
startKeychainPrefetch() // 2. 预取 OAuth Token + Legacy API Key
// 3. 正常的模块导入同时进行
时间线 ──────────────────────────────────────────►
├── MDM 读取 ─────────────────┤
├── Keychain 预取 ────────────┤
├── 模块导入 ─────────────────────────┤
├── init() 开始
三个操作并行执行,总耗时取最慢的那个,而不是三者之和。
入口分流
cli.tsx
│
├── --version → 立即退出
├── --dump-system-prompt → (ant only) 输出 prompt,退出
├── --claude-in-chrome-mcp → 启动 Chrome MCP Server
├── --chrome-native-host → 启动 Chrome Native Host
├── --computer-use-mcp → 启动 Computer Use MCP
├── --daemon-worker → 启动 Daemon Worker
├── --remote-control/bridge → 启动 Bridge 环境
├── daemon [subcommand] → 管理 Daemon
├── ps/logs/attach/kill → 管理会话
│
└── (其他所有情况) → 加载完整应用 → main.tsx
4.3 main.tsx:800KB 的粘合层
main.tsx 是整个应用的中枢。它有 ~4,700 行代码,不是因为它有很多业务逻辑,而是因为它需要把所有模块粘合在一起。
模块加载阶段的副作用(第 1-20 行)
profileCheckpoint('main_tsx_entry') // 性能打点
startMdmRawRead() // MDM 预取(已并行启动)
startKeychainPrefetch() // Keychain 预取(已并行启动)
导入阶段(第 21-150 行)
50+ 个内部模块被导入,包括:
- Commander.js(CLI 框架)
- React(UI)
- Context、Hooks、Services、Tools、Permissions 等
条件导入阶段(第 68-81 行)
Feature-gated 的模块在此条件加载:
// 只在内部构建中存在
const coordinatorModule = feature('COORDINATOR_MODE')
? require('./coordinator/coordinatorMode.js')
: null
const kairosModule = feature('KAIROS')
? require('./assistant/index.js')
: null
const autoModeModule = feature('TRANSCRIPT_CLASSIFIER')
? require('./utils/autoMode.js')
: null
main() 函数(第 585 行)
main()
→ eagerLoadSettings() // 早期设置加载(在 init 之前)
→ run() // 启动 Commander.js 程序
run() 函数(第 884 行,最核心)
const program = new Command('claude')
.description('Claude Code - AI Agent')
.option('--debug', '调试模式')
.option('--print', '非交互模式')
.option('--bare', '最小模式')
.option('--model <model>', '指定模型')
// ... 数十个选项
preAction 钩子:真正的初始化入口
Commander.js 的 preAction 钩子在每个命令执行前触发。这是延迟初始化的关键 — 只有当用户确实要执行命令时才做重量级初始化:
preAction 钩子执行序列:
1. await MDM 预取完成
2. await Keychain 预取完成
3. init() ← 17 步初始化(见 4.4)
4. process.title = 'claude'
5. initSinks() ← 日志初始化
6. 加载 --plugin-dir 插件
7. runMigrations() ← 配置迁移(见 4.5)
8. 加载远程管理设置(非阻塞)
9. 上传用户设置(后台)
注册的子命令
claude mcp [subcommand] — MCP 服务器管理
claude plugin [subcommand] — 插件管理
claude auth [subcommand] — 认证管理
claude doctor — 健康检查
claude config — 配置管理
claude daemon [subcommand] — 守护进程管理
claude remote-control — 远程控制
claude ps/logs/attach/kill — 会话管理
4.4 init.ts:17 步初始化序列
init() 是记忆化的 — 只执行一次,后续调用直接返回。
init() 执行序列(按严格顺序):
① enableConfigs()
│ 加载 settings.json 配置系统
▼
② applySafeConfigEnvironmentVariables()
│ 应用安全的环境变量(在信任对话框之前)
▼
③ applyExtraCACertsFromConfig()
│ 加载额外的 CA 证书(在任何 TLS 握手之前)
▼
④ setupGracefulShutdown()
│ 注册优雅关闭处理器(SIGINT/SIGTERM)
▼
⑤ 1P 事件日志(fire-and-forget)
│ 第一方分析事件,异步发送
▼
⑥ GrowthBook 刷新处理器
│ Feature Flag 定期刷新
▼
⑦ populateOAuthAccountInfoIfNeeded()
│ OAuth 账户信息填充(异步)
▼
⑧ initJetBrainsDetection()
│ JetBrains IDE 检测(异步)
▼
⑨ detectCurrentRepository()
│ Git 仓库检测(异步)
▼
⑩ 初始化远程管理设置加载 Promise
│ 企业设置异步加载
▼
⑪ 初始化策略限制加载 Promise
│ 组织策略异步加载
▼
⑫ recordFirstStartTime()
│ 记录首次启动时间
▼
⑬ configureGlobalMTLS()
│ mTLS 证书配置
▼
⑭ configureGlobalAgents()
│ 代理和 mTLS 配置
▼
⑮ preconnectAnthropicApi()
│ TCP+TLS 预热连接(减少首次 API 调用延迟)
▼
⑯ 上游代理初始化(仅 CCR 容器)
│ CONNECT 中继,供子进程使用
▼
⑰ 清理注册
LSP 服务器关闭、团队清理等
顺序的重要性
初始化步骤的顺序是精心设计的,每步都有前置依赖:
CA 证书(③) 必须在 TLS 握手前完成
→ 所以 API 预连接(⑮) 在它之后
环境变量(②) 必须在信任对话框前应用
→ 所以配置加载(①) 在最前面
优雅关闭(④) 要尽早注册
→ 确保任何阶段的中断都能正确清理
4.5 迁移系统:管理配置演进
为什么需要迁移?
Claude Code 的配置和设置会随版本演化。例如:
- 模型名称变更:
fennec→opus,sonnet-1m→sonnet-4.5→sonnet-4.6 - 设置迁移:某个选项从 A 位置移到 B 位置
- 默认值重置:Pro 用户的默认模型重置为 Opus
迁移文件列表
migrations/
├── migrateAutoUpdatesToSettings.ts — 自动更新首选项移至 settings.json
├── migrateLegacyOpusToCurrent.ts — Opus 4.0/4.1 重映射为 'opus'
├── migrateOpusToOpus1m.ts — Opus → Opus 1M
├── migrateSonnet1mToSonnet45.ts — Sonnet 1M → Sonnet 4.5
├── migrateSonnet45ToSonnet46.ts — Sonnet 4.5 → Sonnet 4.6
├── migrateFennecToOpus.ts — Fennec(内部代号) → Opus (ant only)
├── migrateBypassPermissionsAcceptedToSettings.ts — 权限绕过设置迁移
├── migrateEnableAllProjectMcpServersToSettings.ts — MCP 服务器设置迁移
├── migrateReplBridgeEnabledToRemoteControlAtStartup.ts — Bridge 模式迁移
├── resetAutoModeOptInForDefaultOffer.ts — 自动模式选项重置
└── resetProToOpusDefault.ts — Pro 用户默认模型重置
迁移的执行结构
每个迁移是一个函数,遵循相同模式:
// 典型迁移模式(简化)
export async function migrateSonnet45ToSonnet46() {
// 1. 检查条件(如 API 提供者、当前设置)
const settings = getSettingsForSource('user')
if (settings.model !== 'sonnet-4.5') return // 不需要迁移
// 2. 执行迁移
updateSettingsForSource('user', {
...settings,
model: 'sonnet-4.6'
})
// 3. 记录迁移事件
logEvent('migration_applied', { name: 'sonnet45_to_sonnet46' })
// 4. 标记迁移版本
saveMigrationVersion(Date.now())
}
从迁移中读取产品历史
迁移文件名本身就是一部产品演化史:
时间线 ────────────────────────────────────────────►
Fennec(内部代号) → Opus → Opus 1M
Sonnet 1M → Sonnet 4.5 → Sonnet 4.6
Fennec 是 Opus 的内部动物代号,正如 Tengu 是 Claude Code 项目的代号。这类代号出现在内部构建中但不应泄露到外部 — 这正是 Undercover 模式要防护的内容。
4.6 MCP Server 入口
entrypoints/mcp.ts 让 Claude Code 作为 MCP 服务器运行,供其他应用(如 IDE 插件)调用:
// 简化的 MCP 入口
import { Server } from '@modelcontextprotocol/sdk'
const server = new Server()
// 注册工具列表请求处理器
server.setRequestHandler(ListToolsRequestSchema, async () => {
const tools = getTools({ permissionContext: empty })
return { tools: tools.map(t => t.toJsonSchema()) }
})
// 注册工具调用请求处理器
server.setRequestHandler(CallToolRequestSchema, async (req) => {
const tool = findToolByName(req.name)
return await tool.call(req.input)
})
// 以 stdio 模式运行
server.run({ transport: new StdioTransport() })
与 CLI 的关键差异:
- 无 React UI — 纯 API 模式
- 无交互式权限弹窗 — 使用空权限上下文
- 无 REPL — 响应式而非交互式
- 更轻的初始化 — 不需要终端渲染相关模块
4.7 完整启动时序图
时间 ─────────────────────────────────────────────────────────────►
cli.tsx 加载
├── 快速路径检查 (--version, daemon, ps...)
│ └── 匹配?→ 立即执行 → 退出
│
├── 并行启动 ← 模块加载副作用
│ ├── MDM 配置读取 ─────────────────┐
│ ├── Keychain 预取 ────────────────┤
│ └── 模块导入 ─────────────────────┤
│ │
main.tsx 加载 │
├── 性能打点 │
│ │
main() 执行 │
├── eagerLoadSettings() │
│ │
run() 执行 │
├── Commander.js 程序创建 │
├── 注册选项和子命令 │
│ │
preAction 钩子 ◄───────────────────────┘ ← 等待预取完成
├── ① await MDM 完成
├── ② await Keychain 完成
├── ③ init()
│ ├── 配置系统启用
│ ├── 环境变量应用
│ ├── CA 证书加载
│ ├── 优雅关闭注册
│ ├── 分析/GrowthBook 初始化
│ ├── OAuth/IDE/仓库检测(异步)
│ ├── mTLS/代理配置
│ └── API 预连接
├── ④ initSinks() — 日志初始化
├── ⑤ 加载插件
├── ⑥ runMigrations() — 配置迁移
├── ⑦ 远程设置加载(非阻塞)
│
action 处理器
├── 认证检查
├── MCP 配置加载
├── 工具初始化
├── React REPL 渲染
│
REPL 就绪 ✓
├── startDeferredPrefetches() ← 首次渲染后的后台预取
│ ├── 进程预热
│ ├── 缓存预热
│ └── 跳过 bare 模式
│
等待用户输入...
课后练习
练习 1:快速路径清单
阅读 cli.tsx 的前 200 行,列出所有快速路径(不需要完整初始化的命令路径)。思考:为什么这些命令被设计为快速路径?
练习 2:初始化依赖分析
在 init.ts 的 17 步中,找出哪些步骤之间有严格的顺序依赖(A 必须在 B 之前),哪些步骤可以并行执行。画出一个最优并行化方案。
练习 3:迁移考古
阅读 3 个迁移文件的实际代码,回答:
- 迁移如何知道自己已经被执行过?
- 如果迁移失败,会怎样?有回滚机制吗?
练习 4:入口点对比
对比 CLI、MCP、SDK 三个入口点的初始化流程差异,制作一张对比表。
本课小结
| 要点 | 内容 |
|---|---|
| 三个入口 | CLI(交互式)、MCP(服务器)、SDK(库) |
| 快速路径 | --version 等简单命令绕过完整初始化 |
| 并行预取 | MDM + Keychain + 模块导入同时进行 |
| 延迟初始化 | preAction 钩子确保只在需要时初始化 |
| 17 步 init | 配置 → 证书 → 网络 → 检测 → 预连接 |
| 迁移系统 | 11 个迁移脚本管理配置演进,记录产品历史 |
下一课预告
第 05 课:状态管理架构 — 深入 AppState 的完整形状定义、Store<T> 泛型模式的实现、不可变更新策略,以及状态变更如何触发 UI 重渲染。