# 第 1 章:进程与标准 I/O
> 任何程序,无论多复杂,最终都运行在一个进程里,通过文件描述符与世界交换数据。
---
## 1.1 核心问题
在构建任何 UI、调用任何 API、执行任何工具之前,我们先回答一个最基础的问题:
**一个 Coding Agent 的进程从哪里读输入,往哪里写输出?**
这个问题的答案决定了整个系统的"外壳"——其他所有层(终端 UI、LLM 客户端、工具引擎)都运行在这个外壳之内。
---
## 1.2 原理讲解
### Node.js 进程模型
每个 Node.js 程序启动时,操作系统分配给它三个标准文件描述符:
| 描述符 | 名称 | Node.js 对象 | 默认连接 |
|--------|------|-------------|---------|
| 0 | stdin | `process.stdin` | 键盘输入 |
| 1 | stdout | `process.stdout` | 终端显示 |
| 2 | stderr | `process.stderr` | 终端显示(错误) |
这三个流是**双向管道**:可以重定向(`echo "hello" | node main.js`),也可以直接读写。
Node.js 的核心是**事件循环**:单线程轮询 I/O 事件,收到数据则触发回调。程序不会因等待 I/O 而阻塞,但同时只能处理一件事。
### TypeScript 的运行方式
Claude Code 使用 TypeScript 编写,通过 `tsx`(基于 esbuild 的即时编译器)直接运行,无需预先编译:
```bash
npx tsx src/main.ts # 直接运行 .ts 文件
```
生产环境中 Claude Code 使用 Bun 打包,但开发和学习阶段用 `tsx` 足够。
### 进程退出与信号处理
进程退出时返回一个**退出码**(exit code):`0` 表示成功,非零表示错误。
操作系统还可以向进程发送**信号**:
| 信号 | 触发方式 | 默认行为 |
|------|---------|---------|
| `SIGINT` | Ctrl-C | 终止进程 |
| `SIGTERM` | `kill <pid>` | 终止进程 |
Claude Code 拦截这两个信号,在退出前写入会话档案、清理子进程。
### Claude Code 的全局单例状态
`src/bootstrap/state.ts` 定义了一个进程级的单例对象,保存整个会话期间不变的元数据:
```typescript
// src/bootstrap/state.ts(简化)
type State = {
sessionId: SessionId // 每次启动唯一的 UUID
startTime: number // 进程启动时间戳(ms)
cwd: string // 当前工作目录
originalCwd: string // 启动时的工作目录(不随 cd 变化)
projectRoot: string // 项目根目录(用于 CLAUDE.md 查找)
isInteractive: boolean // 是否是交互模式(有 TTY)
totalCostUSD: number // 本次会话累计 API 费用
// ... 以及数十个其他字段
}
```
**为什么需要全局单例?** 因为 Claude Code 的各个模块(UI、工具、LLM 客户端)都需要访问会话 ID、工作目录等共享信息。全局单例比在每个函数调用链中逐层传递参数更实用。
> **注意**:`bootstrap/state.ts` 是有意保持为**导入依赖树的叶节点**(leaf node)——它不能导入其他 Claude Code 模块,防止循环依赖。这是贯穿整个代码库的架构约束。
---
## 1.3 Claude Code 源码中的关键细节
### 启动顺序(`src/main.tsx` 前几行)
```typescript
// 1. 标记启动时间点(用于性能分析)
profileCheckpoint('main_tsx_entry')
// 2. 提前启动需要时间的后台任务(并行化启动耗时)
startMdmRawRead() // 读取 MDM 企业策略(macOS: plutil,Windows: reg query)
startKeychainPrefetch() // 预取 Keychain 中的 OAuth token 和 API key
// 3. 之后才是 Commander 路由、Ink 启动等主体逻辑
```
这个模式——**在 import 阶段就启动 I/O 密集型后台任务**——使 Claude Code 的冷启动时间从 ~200ms 降到 ~65ms。
### Session ID 的生成
```typescript
// src/bootstrap/state.ts
import { randomUUID } from 'src/utils/crypto.js'
sessionId: randomUUID() as SessionId
```
`sessionId` 用于:
- 本地会话存档文件命名(`~/.claude/projects/<hash>/<sessionId>.jsonl`)
- API 请求的 `X-Session-Id` 请求头(用于计费追踪)
- 子 Agent 与父 Agent 的关联(`parentSessionId`)
### `isInteractive` 的判断
```typescript
isInteractive: process.stdin.isTTY === true
```
当 stdin 连接到真实终端时为 `true`;通过管道(`echo "..." | claude`)或 CI 环境时为 `false`。
这个标志决定是否渲染 Ink UI、是否显示权限确认弹窗。
---
## 1.4 最小化产出物
> 代码骨架位于 `../chapters/01/src/`,参考实现位于 `../chapters/01/solution/`。
### 本章要实现什么
在 `../chapters/01/src/state.ts` 中完成以下内容。
**接口规范**(已提供,不要修改):
```typescript
// 全局状态对象
export const state: {
sessionId: string // UUID,每次启动唯一
startTime: number // 启动时间戳(ms)
cwd: string // 当前工作目录
isInteractive: boolean // 是否连接到真实终端
}
// 信号处理注册函数
export function registerSignalHandlers(onExit?: () => void): void
```
**你需要实现**:
1. `state` 对象的四个字段(`randomUUID()`、`Date.now()`、`process.cwd()`、`process.stdin.isTTY`)
2. `registerSignalHandlers()`:
- `SIGINT`(Ctrl-C):打印提示 + 会话时长,调用 `onExit?.()` 后退出
- `SIGTERM`:调用 `onExit?.()` 后退出
**关键约束**:
- `state.ts` 不能 `import` 任何其他章节的模块(它是整个系列的叶节点)
- 信号处理器注册一次即可,后续章节 import 此文件时自动生效
### 验收
```bash
cd docs/chapters/01
npm install
npm test
```
初始状态会失败(TODO 未填写)。填写 `src/state.ts` 后再次运行,预期输出:
```
=== 第 1 章:进程与标准 I/O 验收 ===
── 文件检查 ──
✓ src/state.ts 存在
✓ src/main.ts 存在
── 实现检查 ──
✓ src/state.ts 已实现
── 功能验收 ──
✓ 管道模式:回显输入内容
✓ 管道模式:处理多行输入(第1行)
✓ 管道模式:处理多行输入(第3行)
✓ --debug 输出 sessionId
✓ --debug 输出 cwd
✓ --debug 输出 isInteractive: false
✓ --debug 模式下 stdout 仍正常输出
✓ --debug 信息不出现在 stdout
✓ state.sessionId 是 UUID(36字符)
✓ state.startTime 是数字
✓ state.cwd 非空
✓ state.isInteractive 为 false(管道环境)
✓ registerSignalHandlers 是函数
结果:16 通过,0 失败
✓ 全部通过!可以进入第 2 章了。
```
卡住时查看 `../chapters/01/solution/state.ts`。
---
## 1.5 本章小结
| 概念 | 要记住的要点 |
|------|-------------|
| 三个标准流 | stdin(0) / stdout(1) / stderr(2),可以重定向 |
| 事件循环 | 单线程,非阻塞 I/O,程序通过回调响应事件 |
| tsx | TypeScript 的即时运行器,无需预编译 |
| SIGINT / SIGTERM | Ctrl-C 发送 SIGINT,必须拦截后才能优雅退出 |
| 全局单例 state | 进程级元数据(sessionId / cwd / startTime),是导入树的叶节点 |
| `isInteractive` | 判断是否有 TTY,决定是否启动交互 UI |
| `state.ts` 独立导出 | state 和 registerSignalHandlers 被拆分到独立文件,供第 2、3 章 import 复用 |
这一章的产出物是整个系列的**地基**:一个可以读取输入并回显输出的进程,以及一个可被后续章节直接复用的 `state.ts` 模块。接下来,我们在这个地基上架起终端 UI。
---
## 下一章
→ [第 2 章:终端 UI 外壳(Ink + React)](02-terminal-ui)