跳转至

Deep Dive | cli/print.ts 5594 行 CLI 输出与 SDK 模式拆解

重要性:⭐⭐⭐⭐(SDK 模式的"主入口" + 输出格式化的真相真实位置src/cli/print.ts5594 行项目最大文件!) 核心角色: - SDK 模式主入口(runHeadless / runHeadlessStreaming) - 4 种输出格式(text / json / stream-json / markdown) - 20+ handle 函数(处理 SDK control protocol 消息) - MCP 服务器动态配置 - 权限决策路由

关联topics/big-files-untold-stories.mdphase-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)

type PromptValue = string | ContentBlockParam[]

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 setup
  • QueryEngine —— 复用 topics/deep-dive-query-engine.md 的 class
  • onMessage callback —— 允许 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 关键设计

  1. async generator —— 流式输出,消费者 for-await
  2. 每个 event 独立处理 —— 200+ 行 switch
  3. 转换 + yield —— 内部事件转 SDK 消息
  4. 错误恢复 —— 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 行)

export function getCanUseToolFn(
  permissionContext: ToolPermissionContext,
): CanUseToolFn

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 行)

function handleSetPermissionMode(
  request: SetPermissionModeRequest,
  context: SDKContext,
): void

94 行切权限模式 —— 推测: - default / acceptEdits / bypassPermissions / plan - 同步更新 ToolPermissionContext - 通知 listeners

8.4 handleChannelEnable(行 4662-4785,~124 行)

function handleChannelEnable(
  request: ChannelEnableRequest,
  context: SDKContext,
): void

124 行的"通道启用" —— MCP 通道控制。

8.5 reregisterChannelHandlerAfterReconnect(行 4786-4840,~55 行)

function reregisterChannelHandlerAfterReconnect(
  context: SDKContext,
): void

重连后重注册 handler —— 推测:保持 SDK 协议层的状态。

8.6 emitLoadError(行 4841-4874,~34 行)

function emitLoadError(error: Error, context: SDKContext): void

34 行 —— 发送 load 错误给 SDK 消费者。

8.7 removeInterruptedMessage(行 4875-4886)

export function removeInterruptedMessage(
  context: SDKContext,
  messageId: string,
): void

中断后清理 —— 把"中断中"的消息从 UI 删除。

8.8 loadInitialMessages(行 4893-5198,~306 行)

async function loadInitialMessages(
  options: LoadOptions,
): Promise<LoadInitialMessagesResult>

306 行的"加载初始消息" —— /resume 时调: 1. 读 sessionStorage 2. 反序列化 3. 转换 SDK 格式 4. 处理 attachments 5. 处理 tool_results


9. I 段:结构化 I/O(行 5199-5240)

function getStructuredIO(): {
  getOutput: () => string
  setOutput: (s: string) => void
}

推测 —— 结构化 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 懒加载"模式

const foo = feature('X') ? require('./foo.js') : null

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 标准模式

const foo = feature('X') ? require('./foo.js') : null

每个 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. 阅读清单

  1. ✅ 完整通读 src/cli/print.ts(5594 行)
  2. ✅ 读 topics/deep-dive-query-engine.md 配合
  3. ✅ 读 phase-04-components.md § 4.5.1
  4. 📌 读 src/services/api/claude.ts(行 4149 引用)
  5. 📌 读 src/services/mcp/MCPConnectionManager.tsx(MCP 部分)
  6. 📌 读 docs/BRIDGE_PROTOCOL.md 区分 Bridge vs SDK Protocol

18. 练习任务

  1. 数 6 个 feature flag 懒加载 —— 都用于什么产品线?
  2. 画出 SDK Protocol vs Bridge Protocol 对比表 —— 消息类型、用途、协议风格
  3. 设计一个 4 种输出格式的简化 SDK —— text / json / stream-json / markdown
  4. 思考:为什么 Claude Code 同时有"巨型函数"和"小函数"两种风格?是不是有规律?