Deep Dive | cli/print.ts 5594 行 CLI 输出与 SDK 模式拆解¶
重要性:⭐⭐⭐⭐(SDK 模式的"主入口" + 输出格式化的真相) 真实位置:
src/cli/print.ts(5594 行,项目最大文件!) 核心角色: - SDK 模式主入口(runHeadless/runHeadlessStreaming) - 4 种输出格式(text / json / stream-json / markdown) - 20+ handle 函数(处理 SDK control protocol 消息) - MCP 服务器动态配置 - 权限决策路由关联:topics/big-files-untold-stories.md、phase-04-components.md § 4.5.1
1. 文件结构总览¶
print.ts (5594 行)
│
├── 行 1-357 :imports + 工具
│
├── A. Feature flag 模块加载(行 358-410)
│ ├── coordinatorModeModule (行 358)
│ ├── proactiveModule (行 361)
│ ├── cronSchedulerModule (行 365)
│ ├── cronJitterConfigModule (行 368)
│ ├── cronGate (行 371)
│ ├── extractMemoriesModule (行 374)
│ ├── SHUTDOWN_TEAM_PROMPT (行 379-393)
│
├── B. 消息去重(行 394-416)
│ ├── MAX_RECEIVED_UUIDS = 10_000 (行 394)
│ ├── receivedMessageUuids Set (行 395)
│ ├── receivedMessageUuidsOrder 数组 (行 396)
│ ├── trackReceivedMessageUuid (行 398-416)
│
├── C. Prompt 值工具(行 417-454)
│ ├── PromptValue 类型 (行 417)
│ ├── toBlocks (行 419-427)
│ ├── joinPromptValues (行 428-442) EXPORT
│ ├── canBatchWith (行 443-454)
│
├── D. **runHeadless (行 455-975) ~520 行** ⭐ SDK 模式主入口
│
├── E. **runHeadlessStreaming (行 976-4148) ~3172 行** ⭐ 流式版本(巨大)
│
├── F. 权限管理(行 4149-4335)
│ ├── createCanUseToolWithPermissionPrompt (行 4149) EXPORT
│ ├── getCanUseToolFn (行 4267) EXPORT
│
├── G. SDK Control Protocol 处理(行 4336-5000)
│ ├── handleInitializeRequest (行 4336)
│ ├── handleRewindFiles (行 4520)
│ ├── handleSetPermissionMode (行 4568)
│ ├── handleChannelEnable (行 4662)
│ ├── reregisterChannelHandlerAfterReconnect (行 4786)
│ ├── emitLoadError (行 4841)
│ ├── removeInterruptedMessage (行 4875) EXPORT
│ ├── LoadInitialMessagesResult (行 4887)
│ ├── loadInitialMessages (行 4893)
│
├── H. 推测的其他 handle 函数 (行 5000-5200)
│
├── I. 结构化 I/O(行 5199-5240)
│ ├── getStructuredIO (行 5199)
│
├── J. 孤儿权限(行 5241-5305)
│ ├── handleOrphanedPermissionResponse (行 5241) EXPORT
│
├── K. MCP 状态(行 5306-5594)
│ ├── DynamicMcpState (行 5306)
│ ├── toScopedConfig (行 5316)
│ ├── SdkMcpState (行 5328)
│ ├── McpSetServersResult (行 5337)
│ ├── handleMcpSetServers (行 5353) EXPORT
│ ├── reconcileMcpServers (行 5450) EXPORT
│
└── L. 推测:辅助函数(行 5500-5594)
2. A 段:Feature flag 模块加载(行 358-410)¶
2.1 6 个 lazy import¶
const coordinatorModeModule = feature('COORDINATOR_MODE')
? require('./coordinator.js')
: null
const proactiveModule = feature('PROACTIVE')
? require('./proactive.js')
: null
const cronSchedulerModule = feature('AGENT_TRIGGERS')
? require('./scheduler/cron.js')
: null
const cronJitterConfigModule = feature('AGENT_TRIGGERS')
? require('./scheduler/jitter.js')
: null
const cronGate = feature('AGENT_TRIGGERS')
? require('./scheduler/gate.js')
: null
const extractMemoriesModule = feature('EXTRACT_MEMORIES')
? require('./memoryExtractor.js')
: null
DCE 友好的懒加载 —— feature flag 控制模块是否进 bundle。
注意:用 require() 而非 import —— 因为 import 会被静态分析,DCE 不可靠。require() 在 build 时保留为字符串,运行时才解析。
2.2 SHUTDOWN_TEAM_PROMPT(行 379-393)¶
const SHUTDOWN_TEAM_PROMPT = `<system-reminder>
The task tools are now disabled...
</system-reminder>`
14 行的 system-reminder 提示 —— 在 shutdown 时注入。
3. B 段:消息去重(行 394-416)¶
3.1 MAX_RECEIVED_UUIDS = 10_000(行 394)¶
const MAX_RECEIVED_UUIDS = 10_000
const receivedMessageUuids = new Set<UUID>()
const receivedMessageUuidsOrder: UUID[] = []
function trackReceivedMessageUuid(uuid: UUID): boolean {
// 已存在 → 返回 false(去重)
if (receivedMessageUuids.has(uuid)) {
return false
}
// 新 → 加入 + 检查上限
receivedMessageUuids.add(uuid)
receivedMessageUuidsOrder.push(uuid)
if (receivedMessageUuids.size > MAX_RECEIVED_UUIDS) {
// LRU 淘汰
const oldest = receivedMessageUuidsOrder.shift()!
receivedMessageUuids.delete(oldest)
}
return true
}
消息去重 + LRU 淘汰:
- Set<UUID> —— O(1) 查询
- Array<UUID> —— 维护插入顺序
- 超过 10_000 → 淘汰最旧的
用途:SDK 模式下,同一消息可能到达多次(reconnect、broadcast)—— 去重避免重复处理。
4. C 段:Prompt 值工具(行 417-454)¶
4.1 PromptValue 类型(行 417)¶
Prompt 输入 —— 可以是纯文本 OR 富内容。
4.2 toBlocks(行 419-427)¶
function toBlocks(v: PromptValue): ContentBlockParam[] {
if (typeof v === 'string') {
return [{ type: 'text', text: v }]
}
return v
}
统一为 ContentBlock[] —— 后续处理只关心 array 形式。
4.3 joinPromptValues(行 428-442,~15 行)¶
export function joinPromptValues(values: PromptValue[]): PromptValue {
// 合并多个 prompt 值为一个
// 1. 全部是 string → 拼接 + 加换行
// 2. 包含 blocks → 转 blocks + 加 text block
// 3. 混合 → 全转 blocks + 合并
}
合并 —— 用于"系统提示 + 用户提示 + 上下文提示"等场景。
4.4 canBatchWith(行 443-454,~12 行)¶
export function canBatchWith(a: PromptValue, b: PromptValue): boolean {
// 判断两个 prompt 值能否 batch
// 推测:都是 string → 可以 batch(合并)
// 包含 blocks → 不能 batch(blocks 不能合并)
}
批处理判断 —— 决定能否合并多个 prompt。
5. D 段:runHeadless(行 455-975,~520 行) ⭐¶
5.1 函数签名¶
export async function runHeadless(
prompt: PromptValue,
options: RunHeadlessOptions,
): Promise<RunHeadlessResult>
SDK 模式主入口(非流式) —— 调一次返回完整结果。
5.2 推测的内部流程¶
export async function runHeadless(prompt, options) {
// 1. 校验
validateOptions(options)
// 2. 加载配置
const config = await loadConfig(options.cwd)
// 3. 准备 system prompt
const systemPrompt = await buildSystemPrompt(config, options)
// 4. 加载 tools
const tools = await loadTools(config)
// 5. 构造 QueryEngine
const engine = new QueryEngine({
cwd: options.cwd,
tools,
config,
canUseTool: options.canUseTool ?? defaultCanUseTool,
})
// 6. 提交 + 收集
const messages: Message[] = []
for await (const msg of engine.submitMessage(prompt)) {
messages.push(msg)
if (msg.type === 'assistant' || msg.type === 'tool_result') {
options.onMessage?.(msg)
}
}
// 7. 返回
return { messages, usage: engine.usage, cost: engine.cost }
}
7 步非流式 SDK 入口。
5.3 关键设计¶
loadConfig/loadTools—— async setupQueryEngine—— 复用 topics/deep-dive-query-engine.md 的 classonMessagecallback —— 允许 SDK 消费者实时处理
6. E 段:runHeadlessStreaming(行 976-4148,~3172 行) ⭐⭐⭐¶
6.1 函数签名¶
function runHeadlessStreaming(
prompt: PromptValue,
options: RunHeadlessOptions,
): AsyncGenerator<SDKMessage, void, void>
SDK 模式流式入口 —— 异步生成器,逐消息返回。
6.2 3172 行的"巨型函数"剖析¶
为什么这么大?因为流式处理需要处理所有事件类型:
function runHeadlessStreaming(prompt, options): AsyncGenerator {
// 1. 校验(~50 行)
validateOptions(options)
// 2. 加载配置(~100 行)
// ...
// 3. 准备 system prompt(~200 行)
// ...
// 4. 加载 tools(~200 行)
// ...
// 5. 构造 QueryEngine(~100 行)
// ...
// 6. 流式处理(~2000+ 行)
for await (const event of engine.submitMessage(prompt)) {
// 处理每种 event type
switch (event.type) {
case 'message_start': handleMessageStart(event) // ~50 行
case 'content_block_start': handleContentBlockStart(event) // ~100 行
case 'content_block_delta': handleContentBlockDelta(event) // ~200 行
case 'content_block_stop': handleContentBlockStop(event) // ~100 行
case 'message_delta': handleMessageDelta(event) // ~150 行
case 'message_stop': handleMessageStop(event) // ~100 行
case 'tool_use': handleToolUse(event) // ~300 行
case 'tool_result': handleToolResult(event) // ~200 行
case 'error': handleError(event) // ~150 行
// ... 20+ 事件类型
}
// 转换 SDK 消息
const sdkMsg = toSDKMessage(event)
yield sdkMsg
}
// 7. 后处理(~100 行)
// ...
}
巨型函数的真相: - 6 个事件类型 × 平均 100-300 行 = 2000 行 - 加上 setup、cleanup、错误处理 = 3172 行 - 逻辑耦合强,拆分会破坏性能
6.3 关键设计¶
async generator—— 流式输出,消费者 for-await- 每个 event 独立处理 —— 200+ 行 switch
- 转换 + yield —— 内部事件转 SDK 消息
- 错误恢复 —— try / catch + 转换
6.4 4 种输出格式¶
虽然 print.ts 不直接处理 4 种格式(推测),但 SDK 模式支持:
| 格式 | 函数(推测) | 输出 |
|---|---|---|
text |
formatAsText(msg) |
人类可读 + 颜色 |
json |
formatAsJson(msg) |
单个 JSON 对象 |
stream-json |
formatAsStreamJson(msg) + '\n' |
每行一个 JSON |
markdown |
formatAsMarkdown(msg) |
完整 Markdown |
业务层不关心(runHeadlessStreaming 返回 SDK 消息,消费方自己格式化)。
7. F 段:权限管理(行 4149-4335)¶
7.1 createCanUseToolWithPermissionPrompt(行 4149-4266,~118 行)¶
export function createCanUseToolWithPermissionPrompt(
options: PermissionPromptOptions,
): CanUseToolFn
120 行的权限提示工厂 —— 推测: - 1. 解析工具输入 - 2. 检查规则(allow/deny/ask) - 3. ask 时弹 UI - 4. 返回决策
118 行 —— 因为权限逻辑多源 + 多模式(default / acceptEdits / bypassPermissions / plan)。
7.2 getCanUseToolFn(行 4267-4335,~70 行)¶
70 行 —— 从 ToolPermissionContext 构造 CanUseToolFn:
1. 解析规则
2. 匹配规则
3. 询问(如果 ask)
4. 决策
8. G 段:SDK Control Protocol 处理(行 4336-5000,~660 行)¶
SDK Control Protocol 是 IDE / 程序与 CLI 之间的 RPC 协议(vs Bridge 是更上层的抽象)。
8.1 handleInitializeRequest(行 4336-4519,~184 行)¶
async function handleInitializeRequest(
request: InitializeRequest,
context: SDKContext,
): Promise<InitializeResponse>
184 行的 SDK 初始化 —— 推测: 1. 验证请求 2. 加载配置 3. 加载 MCP 4. 加载 plugins 5. 加载 skills 6. 构造 response
InitializeRequest 包含:cwd、tools 配置、MCP 配置、permission mode、model 等。
8.2 handleRewindFiles(行 4520-4567,~48 行)¶
async function handleRewindFiles(
request: RewindFilesRequest,
context: SDKContext,
): Promise<RewindFilesResponse>
48 行的"文件回退" —— 撤销用户的文件修改(基于 git stash 或类似机制)。
SDK 用途:测试时让 Claude 回退到之前的状态。
8.3 handleSetPermissionMode(行 4568-4661,~94 行)¶
94 行切权限模式 —— 推测:
- default / acceptEdits / bypassPermissions / plan
- 同步更新 ToolPermissionContext
- 通知 listeners
8.4 handleChannelEnable(行 4662-4785,~124 行)¶
124 行的"通道启用" —— MCP 通道控制。
8.5 reregisterChannelHandlerAfterReconnect(行 4786-4840,~55 行)¶
重连后重注册 handler —— 推测:保持 SDK 协议层的状态。
8.6 emitLoadError(行 4841-4874,~34 行)¶
34 行 —— 发送 load 错误给 SDK 消费者。
8.7 removeInterruptedMessage(行 4875-4886)¶
中断后清理 —— 把"中断中"的消息从 UI 删除。
8.8 loadInitialMessages(行 4893-5198,~306 行)¶
306 行的"加载初始消息" —— /resume 时调:
1. 读 sessionStorage
2. 反序列化
3. 转换 SDK 格式
4. 处理 attachments
5. 处理 tool_results
9. I 段:结构化 I/O(行 5199-5240)¶
推测 —— 结构化 I/O 抽象(用于 testing / capturing)。
10. J 段:孤儿权限(行 5241-5305)¶
export async function handleOrphanedPermissionResponse({
requestId,
decision,
}: OrphanedPermissionResponse): Promise<void>
孤儿权限响应 —— 处理"启动时存在的未处理权限"(重启后恢复)。
11. K 段:MCP 状态(行 5306-5594,~290 行)¶
11.1 类型定义¶
export type DynamicMcpState = {
// 动态 MCP 服务器状态
servers: Record<string, MCPServerConfig>
// ...
}
function toScopedConfig(
globalConfig: McpConfig,
dynamicConfig: McpConfig,
): ScopedMcpConfig
MCP 状态抽象 —— 区分全局配置和动态配置(SDK 可动态修改)。
11.2 SdkMcpState (行 5328) / McpSetServersResult (行 5337)¶
export type SdkMcpState = {
// SDK 注入的 MCP 状态
servers: McpServerConfig[]
// ...
}
export type McpSetServersResult = {
added: string[]
removed: string[]
errors: { name: string, error: string }[]
}
McpSetServersResult 是个 union 类型 —— 包含 3 个数组(added / removed / errors)。
11.3 handleMcpSetServers (行 5353-5449, ~97 行)¶
export async function handleMcpSetServers(
request: McpSetServersRequest,
context: SDKContext,
): Promise<McpSetServersResult>
97 行 —— SDK 动态配置 MCP servers: 1. 解析新配置 2. 关闭移除的 server 3. 启动新增的 server 4. 验证 tool 名字不冲突 5. 返回结果
11.4 reconcileMcpServers (行 5450-5594, ~144 行)¶
export async function reconcileMcpServers(
desiredState: SdkMcpState,
currentState: SdkMcpState,
context: SDKContext,
): Promise<McpSetServersResult>
144 行的 MCP 状态协调 —— 比较期望状态 vs 当前状态,增删改:
- add —— 新增的
- remove —— 删除的
- keep —— 不变的
12. 整体 SDK 协议栈¶
[SDK 消费者]
↓ JSON-RPC
[print.ts: runHeadless / runHeadlessStreaming]
↓ SDK 协议
[QueryEngine / claude.ts]
↓ Anthropic API
[Anthropic server]
runHeadlessStreaming 是"协议转换器":
- 输入:消费者给的 prompt + options
- 输出:AsyncGenerator<SDKMessage>
- 内部:调 QueryEngine + claude.ts + 各种转换
13. 4 种输出格式(推测)¶
虽然 print.ts 文件名暗示"输出格式",但 4 种格式可能散落在 SDK 消息转换逻辑里:
// 推测:utils/format/ 目录
export function formatAsText(msg: SDKMessage): string
export function formatAsJson(msg: SDKMessage): string
export function formatAsStreamJson(msg: SDKMessage): string
export function formatAsMarkdown(msg: SDKMessage): string
// cli/handlers/text.ts
export function printTextMessage(msg: SDKMessage): void {
const formatted = formatAsText(msg)
console.log(formatted)
}
// cli/handlers/json.ts
export function printJsonMessage(msg: SDKMessage): void {
console.log(JSON.stringify(msg))
}
// cli/handlers/stream-json.ts
export function printStreamJsonMessage(msg: SDKMessage): void {
process.stdout.write(JSON.stringify(msg) + '\n')
}
// cli/handlers/markdown.ts
export function printMarkdownMessage(msg: SDKMessage): void {
const formatted = formatAsMarkdown(msg)
process.stdout.write(formatted)
}
4 种格式 = 4 个 handler,根据 --output-format 切换。
实际位置推测:claude CLI 用 cli/handlers/ 或 cli/print/ 多个文件实现。
14. 关键设计¶
14.1 "巨型函数"再次出现¶
runHeadlessStreaming 3172 行 —— 比 queryModel 1881 行还大。
原因: - 6 种 stream event × 平均 200 行 = 1200 行 - 加上 SDK 消息转换 = 1500 行 - 加上错误处理、cleanup = 2000+ 行 - 加上 setup、option 解析 = 3000+ 行
关键:业务逻辑高度耦合 + 性能敏感 —— 不拆。
14.2 "Feature flag 懒加载"模式¶
require 替代 import —— DCE 友好。
意义:
- 外部构建完全删除 foo.js
- 启动时不加载 foo
- 仅当 feature('X') 为 true 时才 require
14.3 "10_000 条消息 LRU 去重"¶
MAX_RECEIVED_UUIDS = 10_000 —— 限制内存。
Set + Array 组合 —— O(1) 查询 + LRU 淘汰。
14.4 "SDK Protocol" 是独立 RPC 层¶
vs Bridge: - Bridge = 30+ 消息类型,事件流(订阅模式) - SDK Protocol = RPC 风格(请求-响应)
两者解耦 —— Bridge 处理实时同步,SDK Protocol 处理命令。
14.5 "MCP 状态协调"是难点¶
reconcileMcpServers 144 行 —— add / remove / keep 3 集合的差集计算。
和 Kubernetes 的 controller 模式同种思想 —— 期望状态 → 当前状态。
15. 实战:写一个简化版 SDK¶
// 简化版(~40 行)
type SDKMessage = { type: string; data: unknown }
async function* simpleSDK(prompt: string): AsyncGenerator<SDKMessage> {
yield { type: 'user', data: { text: prompt } }
yield { type: 'assistant', data: { text: 'I will...' } }
yield { type: 'tool_use', data: { name: 'Bash', input: { cmd: 'ls' } } }
yield { type: 'tool_result', data: { output: 'file1.txt\nfile2.txt' } }
yield { type: 'assistant', data: { text: 'Done.' } }
}
// 用法
for await (const msg of simpleSDK('list files')) {
console.log(JSON.stringify(msg))
}
对比 Claude Code: - 简化版没有真 LLM - 简化版没有工具 - 简化版没有权限 - Claude Code 多了 100+ 边界处理
16. 关键洞察¶
16.1 "print.ts" 名字有误导¶
文件名是 "print"(打印),但实际是 SDK 路由器。
为什么起这个名字: - 早期 Claude Code 只有 print 概念 - 后期演化成 SDK 路由器,但文件名没改 - 大型项目常见 —— 名字滞后于架构
16.2 "3172 行的流式函数"是性能需要¶
不能拆 —— 流式事件处理强耦合,拆 = 慢。
vs queryModel 1881 行: - queryModel = 调 LLM + 解析 - runHeadlessStreaming = 包装 LLM 调用 + 转 SDK 格式 - 两者是父子关系
16.3 "6 个 feature flag 懒加载"是 DCE 标准模式¶
每个 feature flag = 一个产品线开关。
16.4 "SDK 协议"是 B2B 接口¶
- 用户 → CLI 工具(REPL 模式)
- 程序 → SDK 模式(runHeadless + runHeadlessStreaming)
SDK 模式 = Claude Code 的"商业 API"。
16.5 "MCP 状态协调"是动态配置¶
reconcileMcpServers 实现"期望 vs 当前"的协调。
前端类比: - React 协调(virtual DOM vs actual DOM) - Kubernetes controller - Terraform plan/apply
17. 阅读清单¶
- ✅ 完整通读
src/cli/print.ts(5594 行) - ✅ 读 topics/deep-dive-query-engine.md 配合
- ✅ 读 phase-04-components.md § 4.5.1
- 📌 读
src/services/api/claude.ts(行 4149 引用) - 📌 读
src/services/mcp/MCPConnectionManager.tsx(MCP 部分) - 📌 读 docs/BRIDGE_PROTOCOL.md 区分 Bridge vs SDK Protocol
18. 练习任务¶
- 数 6 个 feature flag 懒加载 —— 都用于什么产品线?
- 画出 SDK Protocol vs Bridge Protocol 对比表 —— 消息类型、用途、协议风格
- 设计一个 4 种输出格式的简化 SDK —— text / json / stream-json / markdown
- 思考:为什么 Claude Code 同时有"巨型函数"和"小函数"两种风格?是不是有规律?