模块二:启动与状态 | 前置依赖:第 01-03 课 | 预计学习时间:60 分钟
学习目标
完成本课后,你将能够:
1. 画出从 cli.tsx到 REPL 就绪的完整启动时序图2. 说明三个入口点(CLI/MCP/SDK)的差异 3. 列出 init.ts的 17 步初始化序列及其目的4. 理解迁移系统如何管理配置演进
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 | |
mcp.ts | ||
sdk/ |
4.2 CLI 入口:cli.tsx 的快速路径
cli.tsx 是绝大多数用户的入口。它的第一个设计原则是:尽快响应简单请求。
快速路径(无需加载完整应用)
claude --version → 直接输出版本号,退出claude --daemon-worker → 直接启动 daemon workerclaude daemon [cmd] → 直接管理 daemonclaude 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.tsx4.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') : nullconst kairosModule = feature('KAIROS') ? require('./assistant/index.js') : nullconst autoModeModule = feature('TRANSCRIPT_CLASSIFIER') ? require('./utils/autoMode.js') : nullmain() 函数(第 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.6Fennec 是 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 三个入口点的初始化流程差异,制作一张对比表。
本课小结
下一课预告
第 05 课:状态管理架构 — 深入 AppState 的完整形状定义、Store<T> 泛型模式的实现、不可变更新策略,以及状态变更如何触发 UI 重渲染。
夜雨聆风