深入拆解 Claude Code 源码(六):20 个后端服务如何支撑 AI 编程助手的"后勤大本营"

系列:深入拆解 Claude Code 源码 | 第 6 篇 / 共 8 篇
关键词:Claude Code, 服务层, MCP, OAuth, 推测执行, 团队记忆同步, 插件系统, 自动梦境


开篇:前台光鲜,后台更精彩

前几篇我们看到,Claude Code 有华丽的终端 UI(React + Ink),有精密的对话引擎(QueryEngine),有 77 个斜杠命令和 35+ 个工具。但这些都是"前台"。

你有没有想过:

  • AI 怎么知道你的项目用了什么 MCP 服务器?—— 需要一个 MCP 客户端管理器
  • 你输入密码登录后,token 过期了怎么办?—— 需要一个 OAuth 认证服务
  • 你改了一个文件,AI 已经提前猜到你会这么做,结果直接用?—— 需要一个 推测执行系统
  • 团队里其他人更新了共享记忆,你这边自动同步?—— 需要一个 记忆同步引擎
  • /install plugin-id 背后发生了什么?—— 需要一个 插件管理系统

这些"幕后英雄"全部住在 src/services/ 目录下,30+ 个子系统,每个都像一个微服务一样各司其职。今天我们就来揭开这个"后勤大本营"的面纱。


一、服务层全景图

┌─────────────────────────────────────────────────────┐
│                 src/services/ (30+ 子系统)            │
│                                                      │
│  ┌────────────────────────────────────────────────┐  │
│  │            工具执行框架 (3 层架构)               │  │
│  │  toolExecution.ts → toolHooks.ts → orchestration│  │
│  └────────────────────────────────────────────────┘  │
│                                                      │
│  ┌──────────────┐  ┌──────────────┐  ┌───────────┐  │
│  │   MCP 集成   │  │  OAuth 认证   │  │ API 客户端 │  │
│  │  client.ts   │  │  client.ts   │  │ claude.ts  │  │
│  │  types.ts    │  │              │  │ errors.ts  │  │
│  └──────────────┘  └──────────────┘  └───────────┘  │
│                                                      │
│  ┌──────────────┐  ┌──────────────┐  ┌───────────┐  │
│  │  推测执行    │  │ 团队记忆同步  │  │  插件系统  │  │
│  │ speculation  │  │  watcher.ts  │  │ pluginOps  │  │
│  │  (992 行)    │  │  secretScan  │  │ (1089 行)  │  │
│  └──────────────┘  └──────────────┘  └───────────┘  │
│                                                      │
│  ┌──────────────┐  ┌──────────────┐  ┌───────────┐  │
│  │  自动梦境    │  │  会话记忆    │  │  分析诊断  │  │
│  │  autoDream   │  │  sessionMem  │  │ analytics  │  │
│  └──────────────┘  └──────────────┘  └───────────┘  │
│                                                      │
│  ┌──────────────┐  ┌──────────────┐  ┌───────────┐  │
│  │  语音系统    │  │  对话压缩    │  │  Token估算 │  │
│  │  voice.ts    │  │  compact/    │  │ tokenEst   │  │
│  └──────────────┘  └──────────────┘  └───────────┘  │
└─────────────────────────────────────────────────────┘

完整目录清单(30+ 子系统)

src/services/ 下有 30 多个子目录和独立文件,每一个都是一个独立的"微服务":

子目录/文件 说明
analytics/ 分析事件追踪,所有遥测事件的统一出口
api/ Anthropic API 客户端,流式通信、用量统计、错误分类
autoDream/ 自动记忆整合(后台梦境),4 阶段整合流程
awaySummary/ 离开摘要,用户离开时生成对话摘要
claudeAiLimits/ Claude AI 订阅使用限额管理
compact/ 对话压缩,摘要生成,旧消息替换
diagnosticTracking/ 诊断追踪,问题排查信息收集
extractMemories/ 记忆提取,从对话中自动提取记忆
lsp/ 语言服务器协议,代码智能功能
MagicDocs/ Magic Docs 服务
mcp/ MCP 服务器管理,客户端连接、工具包装、资源读取
notifier.ts 通知服务,桌面通知和用户提醒
oauth/ OAuth 认证,token 管理、刷新、MCP OAuth 流程
policyLimits/ 组织级策略限制
plugins/ 插件系统,安装/卸载/更新/作用域管理(1089 行)
PromptSuggestion/ 提示建议与推测执行(992 行)
rateLimitMocking.ts 速率限制模拟 facade
remoteManagedSettings/ 远程管理设置加载
SessionMemory/ 会话记忆,自动维护的 markdown 笔记
settingsSync/ 设置同步
teamMemorySync/ 团队记忆同步,双向同步引擎 + 秘密扫描
tokenEstimation.ts Token 估算服务
tools/ 工具执行框架(3 层架构)
toolUseSummary/ 工具使用摘要生成
vcr.ts VCR 录制,API 请求/响应录制用于调试
voice.ts 语音系统,多平台录制 + WebSocket STT
preventSleep.ts 防止系统休眠
internalLogging.ts 内部日志记录
mockRateLimits.ts 模拟速率限制(ant-only)
rateLimitMessages.ts 速率限制用户提示消息

每个子系统都像一个独立的微服务:有自己的类型定义、自己的状态管理、自己的生命周期。接下来我们深入其中最重要的几个。


二、工具执行框架:三层安全架构

Claude Code 的工具执行不是简单的"调一下就完事"。整个框架分为三层,每层都有明确的职责。这是整个服务层中最精妙的设计。

第一层:toolExecution.ts — 单工具生命周期管理

这是 1746 行的"巨无霸"文件,负责单个工具从生到死的完整生命周期。我们来看核心流程:

// toolExecution.ts — 核心入口函数
export async function* runToolUse(
  toolUse: ToolUseBlock,
  assistantMessage: AssistantMessage,
  canUseTool: CanUseToolFn,
  toolUseContext: ToolUseContext,
): AsyncGenerator<MessageUpdateLazy, void> {
  const toolName = toolUse.name

  // 第 1 步:查找工具(先在可用工具中找,再回退到别名)
  let tool = findToolByName(toolUseContext.options.tools, toolName)
  if (!tool) {
    const fallbackTool = findToolByName(getAllBaseTools(), toolName)
    if (fallbackTool && fallbackTool.aliases?.includes(toolName)) {
      tool = fallbackTool  // 支持已废弃工具的别名调用
    }
  }

  // 第 2 步:工具不存在 → 返回错误
  if (!tool) {
    yield { message: createUserMessage({
      content: [{ type: 'tool_result',
        content: `<tool_use_error>No such tool available: ${toolName}</tool_use_error>`,
        is_error: true, tool_use_id: toolUse.id }],
    })}
    return
  }

  // 第 3 步:检查中断信号
  if (toolUseContext.abortController.signal.aborted) {
    yield { message: createToolResultStopMessage(toolUse.id) }
    return
  }

  // 第 4 步:进入权限检查 + 执行流程
  for await (const update of streamedCheckPermissionsAndCallTool(
    tool, toolUse.id, toolInput, toolUseContext,
    canUseTool, assistantMessage, ...
  )) {
    yield update
  }
}

checkPermissionsAndCallTool 函数是真正干活的地方,它的流程是:

输入验证 (Zod schema) → 自定义验证 (validateInput) → 推测性分类器检查
→ PreToolUse Hooks → 权限决策 → 工具执行 → PostToolUse Hooks → 返回结果

每个步骤都有详细的遥测日志。Zod schema 验证失败时,还会对延迟加载的工具补充 schema 提示(buildSchemaNotSentHint),告诉模型先调用 ToolSearch 加载工具再重试。

一个特别巧妙的设计是 推测性分类器检查:在权限检查还没返回结果时,就提前启动 Bash 命令的分类器检查,让它和 Hook、权限对话框并行运行:

// 推测性启动 Bash 分类器 — 和权限检查并行运行
if (tool.name === BASH_TOOL_NAME && 'command' in parsedInput.data) {
  startSpeculativeClassifierCheck(
    (parsedInput.data as BashToolInput).command,
    appState.toolPermissionContext,
    toolUseContext.abortController.signal,
    toolUseContext.options.isNonInteractiveSession,
  )
}

第二层:toolHooks.ts — Hook 系统(651 行)

这是工具执行中最精妙的部分。每个工具调用都会触发 PreToolUse 和 PostToolUse 两组 Hook。

PreToolUse Hook — 工具执行前的守门人:

// toolHooks.ts — PreToolUse Hook 执行器
export async function* runPreToolUseHooks(
  toolUseContext: ToolUseContext,
  tool: Tool,
  processedInput: Record<string, unknown>,
  toolUseID: string,
  messageId: string,
  requestId: string | undefined,
  mcpServerType: McpServerType,
  mcpServerBaseUrl: string | undefined,
): AsyncGenerator<
  | { type: 'message'; message: MessageUpdateLazy }           // 附件/进度消息
  | { type: 'hookPermissionResult'; hookPermissionResult: PermissionResult }  // 权限决策
  | { type: 'hookUpdatedInput'; updatedInput: Record<string, unknown> }       // 修改输入
  | { type: 'preventContinuation'; shouldPreventContinuation: boolean }        // 阻止后续
  | { type: 'stopReason'; stopReason: string }                // 停止原因
  | { type: 'additionalContext'; message: MessageUpdateLazy } // 附加上下文
  | { type: 'stop' }                                          // 停止执行
> {
  for await (const result of executePreToolHooks(
    tool.name, toolUseID, processedInput, toolUseContext,
    appState.toolPermissionContext.mode,
    toolUseContext.abortController.signal,
  )) {
    // blockingError → 转换为 deny 权限决策
    if (result.blockingError) {
      yield {
        type: 'hookPermissionResult',
        hookPermissionResult: {
          behavior: 'deny',
          message: getPreToolHookBlockingMessage(
            `PreToolUse:${tool.name}`, result.blockingError
          ),
          decisionReason: { type: 'hook', hookName: `PreToolUse:${tool.name}` },
        },
      }
    }

    // permissionBehavior → 直接的权限决策(allow/deny/ask)
    if (result.permissionBehavior !== undefined) {
      if (result.permissionBehavior === 'allow') {
        yield { type: 'hookPermissionResult',
          hookPermissionResult: { behavior: 'allow', updatedInput: result.updatedInput }
        }
      } else if (result.permissionBehavior === 'ask') {
        yield { type: 'hookPermissionResult',
          hookPermissionResult: { behavior: 'ask', updatedInput: result.updatedInput,
            message: result.hookPermissionDecisionReason || '...' }
        }
      } else {
        yield { type: 'hookPermissionResult',
          hookPermissionResult: { behavior: result.permissionBehavior,
            message: result.hookPermissionDecisionReason || '...' }
        }
      }
    }

    // updatedInput 但没有权限决策 → passthrough,让正常权限流程继续
    if (result.updatedInput && result.permissionBehavior === undefined) {
      yield { type: 'hookUpdatedInput', updatedInput: result.updatedInput }
    }
  }
}

PostToolUse Hook — 工具执行后的观察者,可以修改输出、阻断后续、附加上下文:

export async function* runPostToolUseHooks<Input, Output>(
  toolUseContext, tool, toolUseID, toolInput, toolResponse, ...
): AsyncGenerator<PostToolUseHooksResult<Output>> {
  let toolOutput = toolResponse
  for await (const result of executePostToolHooks(...)) {
    if (result.blockingError)       yield { message: createAttachmentMessage({ type: 'hook_blocking_error', ... }) }
    if (result.preventContinuation) { yield { message: ... }; return }
    if (result.additionalContexts)  yield { message: createAttachmentMessage({ type: 'hook_additional_context', ... }) }
    if (result.updatedMCPToolOutput && isMcpTool(tool)) {
      toolOutput = result.updatedMCPToolOutput
      yield { updatedMCPToolOutput: toolOutput }
    }
  }
}

PostToolUseFailure Hook — 工具执行失败后也能触发 Hook,用于日志记录、重试建议等。处理逻辑与 PostToolUse 相同(blockingError、additionalContexts 等)。

Hook 结果类型汇总

类型 作用 出现位置
blockingError 阻断性错误,直接阻止工具执行 Pre/Post/PostFailure
preventContinuation 阻止 AI 继续后续操作 Pre/Post
additionalContext 给 AI 附加上下文信息 Pre/Post/PostFailure
updatedMCPToolOutput 修改 MCP 工具的输出 Post
permissionBehavior 权限行为决策(allow/deny/ask) Pre

关键不变量:Hook 的 allow 决策 不会 绕过 settings.json 中的 deny/ask 规则。这个不变量由 resolveHookPermissionDecision 函数保证:

export async function resolveHookPermissionDecision(
  hookPermissionResult, tool, input, toolUseContext, canUseTool, ...
): Promise<{ decision: PermissionDecision; input: Record<string, unknown> }> {
  if (hookPermissionResult?.behavior === 'allow') {
    // 即使 Hook 说 allow,仍然要检查 settings 规则
    const ruleCheck = await checkRuleBasedPermissions(tool, hookInput, toolUseContext)
    if (ruleCheck === null) return { decision: hookPermissionResult, input: hookInput }
    if (ruleCheck.behavior === 'deny') return { decision: ruleCheck, input: hookInput }
    // ask 规则 → 弹出权限对话框
    return { decision: await canUseTool(...), input: hookInput }
  }
  if (hookPermissionResult?.behavior === 'deny') return { decision: hookPermissionResult, input }
  // 没有 Hook 决策 → 正常权限流程
  return { decision: await canUseTool(...), input }
}

第三层:toolOrchestration.ts — 并发编排(189 行)

这一层负责"调度":哪些工具可以并发执行,哪些必须串行。

// toolOrchestration.ts — 并发编排逻辑
function getMaxToolUseConcurrency(): number {
  return parseInt(process.env.CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY || '', 10) || 10
}

export async function* runTools(toolUseMessages, assistantMessages, canUseTool, toolUseContext) {
  let currentContext = toolUseContext
  for (const { isConcurrencySafe, blocks } of partitionToolCalls(toolUseMessages, currentContext)) {
    if (isConcurrencySafe) {
      // 并发安全批次:Read, Glob, Grep 等只读工具
      const queuedContextModifiers: Record<string, ((ctx) => ctx)[]> = {}
      for await (const update of runToolsConcurrently(blocks, ...)) {
        if (update.contextModifier) {
          // 上下文修改器排队 — 等所有并行工具完成后再统一应用
          queuedContextModifiers[update.contextModifier.toolUseID] ??= []
          queuedContextModifiers[update.contextModifier.toolUseID].push(update.contextModifier.modifyContext)
        }
        yield { message: update.message, newContext: currentContext }
      }
      // 所有并行工具完成后,统一应用上下文修改器
      for (const block of blocks) {
        for (const modifier of queuedContextModifiers[block.id] ?? []) {
          currentContext = modifier(currentContext)
        }
      }
    } else {
      // 非并发安全批次:Bash, FileEdit, FileWrite 等敏感操作
      for await (const update of runToolsSerially(blocks, ...)) {
        if (update.newContext) currentContext = update.newContext
        yield { message: update.message, newContext: currentContext }
      }
    }
  }
}

分区逻辑partitionToolCalls 如何决定并发/串行:

// 分区:连续的只读工具合并为一个并发批次,敏感操作单独串行
function partitionToolCalls(toolUseMessages, toolUseContext): Batch[] {
  return toolUseMessages.reduce((acc, toolUse) => {
    const tool = findToolByName(toolUseContext.options.tools, toolUse.name)
    const parsedInput = tool?.inputSchema.safeParse(toolUse.input)
    const isConcurrencySafe = parsedInput?.success
      ? Boolean(tool?.isConcurrencySafe(parsedInput.data))
      : false

    // 连续的并发安全工具合并到同一个批次
    if (isConcurrencySafe && acc[acc.length - 1]?.isConcurrencySafe) {
      acc[acc.length - 1].blocks.push(toolUse)
    } else {
      acc.push({ isConcurrencySafe, blocks: [toolUse] })
    }
    return acc
  }, [])
}

并发执行器 — 使用 all() generator combinator:

// 并发执行 — 使用 all() 组合器限制最大并发数
async function* runToolsConcurrently(
  toolUseMessages, assistantMessages, canUseTool, toolUseContext,
): AsyncGenerator<MessageUpdateLazy, void> {
  yield* all(
    toolUseMessages.map(async function* (toolUse) {
      toolUseContext.setInProgressToolUseIDs(prev => new Set(prev).add(toolUse.id))
      yield* runToolUse(toolUse, assistantMessages.find(...), canUseTool, toolUseContext)
      markToolUseAsComplete(toolUseContext, toolUse.id)
    }),
    getMaxToolUseConcurrency(),  // 默认 10
  )
}

// 串行执行 — 一个接一个,上下文实时更新
async function* runToolsSerially(
  toolUseMessages, assistantMessages, canUseTool, toolUseContext,
): AsyncGenerator<MessageUpdate, void> {
  let currentContext = toolUseContext
  for (const toolUse of toolUseMessages) {
    for await (const update of runToolUse(toolUse, ..., canUseTool, currentContext)) {
      if (update.contextModifier) {
        currentContext = update.contextModifier.modifyContext(currentContext)
      }
      yield { message: update.message, newContext: currentContext }
    }
  }
}

设计亮点:并发批次中的上下文修改器不会立即生效,而是排队等待所有并行工具完成后再统一应用。这避免了并发修改上下文导致的竞态条件。


三、MCP 集成:AI 的"万能插头"

MCP (Model Context Protocol) 是 Claude Code 最强大的扩展机制。通过 MCP,你可以让 AI 连接数据库、调用 API、操作外部服务。

客户端管理

src/services/mcp/client.ts 负责:

  • 服务器发现与连接管理 — 自动发现并连接配置的 MCP 服务器
  • 工具列表获取与缓存 — 获取服务器提供的工具列表并缓存
  • 工具调用转发 — 将 AI 的工具调用请求转发给对应的 MCP 服务器
  • 认证流程处理 — 处理 MCP 服务器的 OAuth 认证
  • 资源读取支持 — 读取 MCP 服务器暴露的资源

类型系统

// MCP 核心类型
type MCPServerConfig = {
  name: string           // 服务器名称
  transport: string      // 传输方式(stdio/sse/streamable-http)
  command?: string       // 启动命令(stdio 模式)
  args?: string[]        // 命令参数
  env?: Record<string, string>  // 环境变量
  url?: string           // HTTP URL(sse/http 模式)
}

type MCPTool = {
  name: string           // 工具名称
  description: string    // 工具描述
  inputSchema: object    // 输入参数 JSON Schema
  isMcp: true            // 标记为 MCP 工具
}

type MCPResource = {
  uri: string            // 资源 URI
  name: string           // 资源名称
  description?: string   // 资源描述
  mimeType?: string      // MIME 类型
}

MCP 工具会被包装成与内置工具统一的接口,用户和 AI 都感受不到差异。在 toolExecution.ts 中,MCP 工具和内置工具走的是同一条执行路径,只是在 PostToolUse 阶段有细微差别:

// MCP 工具 vs 内置工具的处理差异
if (!isMcpTool(tool)) {
  // 内置工具:先添加结果,再运行 PostToolUse hooks
  await addToolResult(toolOutput, mappedToolResultBlock)
}

// PostToolUse hooks 运行...

if (isMcpTool(tool)) {
  // MCP 工具:hooks 可能修改输出,所以最后才添加结果
  await addToolResult(toolOutput)
}

MCP 服务器类型与认证

支持 8 种传输类型:stdio(本地进程)、sse(Server-Sent Events)、http(HTTP streaming)、ws(WebSocket)、sdksse-ide/ws-ide(IDE 集成)、claudeai-proxy

当 MCP 服务器的 OAuth token 过期时,系统会捕获 McpAuthError 并自动将服务器状态更新为 needs-auth,UI 会显示"需要重新授权"。


四、推测执行:AI 的"预判"能力

这是 Claude Code 中最"科幻"的功能之一。src/services/PromptSuggestion/speculation.ts992 行代码,实现了浏览器级别的"预渲染"思想。

工作原理

当你在思考下一步要做什么时,推测执行系统已经:

  1. 预判你的意图 — 基于对话历史预测你可能的下一步操作
  2. 提前执行 — 在一个隔离的 overlay 目录中执行预测的操作
  3. 等待确认 — 如果猜对了,直接使用结果;猜错了,静默丢弃

核心常量与分类

const MAX_SPECULATION_TURNS = 20      // 最大推测轮数
const MAX_SPECULATION_MESSAGES = 100  // 最大推测消息数

// 工具分类决定推测边界
const WRITE_TOOLS = new Set(['Edit', 'Write', 'NotebookEdit'])
const SAFE_READ_ONLY_TOOLS = new Set([
  'Read', 'Glob', 'Grep', 'ToolSearch',
  'LSP', 'TaskGet', 'TaskList'
])

Overlay 隔离机制

推测执行在一个"影子文件系统"中运行:

// overlay 目录路径:按进程 PID 和推测 ID 隔离
function getOverlayPath(id: string): string {
  return join(getClaudeTempDir(), 'speculation', String(process.pid), id)
}

// 安全清理 overlay — 即使失败也不影响主流程
function safeRemoveOverlay(overlayPath: string): void {
  rm(overlayPath, { recursive: true, force: true, maxRetries: 3, retryDelay: 100 }, () => {})
}
机制 说明
文件写入 写入临时 overlay 目录,不影响真实文件
读取重定向 读取时先检查 overlay,再检查原始文件
Copy-on-Write 首次写入时才复制原始文件到 overlay
边界类型 bash(非只读命令)、edit(需权限)、denied_tool(未知工具)、complete(完成)

接受与丢弃

// 如果推测正确,将 overlay 复制到主目录
async function copyOverlayToMain(
  overlayPath: string,
  writtenPaths: Set<string>,
  cwd: string,
): Promise<boolean> {
  let allCopied = true
  for (const rel of writtenPaths) {
    const src = join(overlayPath, rel)
    const dest = join(cwd, rel)
    try {
      await mkdir(dirname(dest), { recursive: true })
      await copyFile(src, dest)
    } catch {
      allCopied = false
      logForDebugging(`[Speculation] Failed to copy ${rel} to main`)
    }
  }
  return allCopied
}

// 如果推测错误,静默丢弃 overlay
// (overlay 目录在下次推测时被 safeRemoveOverlay 清理)

每次推测都会记录 tengu_speculation 遥测事件,包含 outcome(accepted/aborted/error)、duration_ms、tools_executed、boundary_type(bash/edit/denied_tool/complete)等。

门控条件USER_TYPE === 'ant'(Anthropic 内部用户)+ 用户启用 speculationEnabled 设置。目前这个功能还在内测阶段。


五、团队记忆同步:多人协作的"共享大脑"

团队记忆同步是 Claude Code 企业版的核心功能之一。它让团队成员可以共享项目知识、编码规范、架构决策等记忆。

同步引擎

src/services/teamMemorySync/index.ts 实现了双向同步:

  • OAuth 认证 — 使用 OAuth token 进行身份验证
  • SHA256 内容哈希 — 用于检测变更和冲突
  • 412 冲突解决 — 通过 ?view=hashes 探测服务端状态
  • 批量分割 — 200KB 限制,超出自动分批
  • 结构化 413 处理 — 处理"条目过多"的错误

类型系统(157 行)

types.ts 使用 Zod v4 定义了完整的同步协议:TeamMemoryContentSchema(entries + entryChecksums)、TeamMemoryDataSchema(orgId/repo/version/checksum/content)、TeamMemoryTooManyEntriesSchema(413 结构化错误)。结果类型包括 TeamMemorySyncFetchResultTeamMemoryHashesResultTeamMemorySyncPushResult(filesUploaded/conflict/skippedSecrets)、SkippedSecretFile(path/ruleId/label)等。

秘密防护

这是最值得学习的设计之一。在同步记忆到云端之前,系统会进行客户端秘密扫描:

// 35+ gitleaks 规则,扫描 API key、密码、token 等
// teamMemSecretGuard.ts (45 行)
export function checkTeamMemSecrets(filePath: string, content: string): string | null {
  // 检查路径是否为团队记忆
  // 扫描内容中的秘密
  // 返回错误消息或 null
}

这个检查在 FileWriteToolFileEditToolvalidateInput() 中调用,从源头阻止秘密泄露。实现使用 dynamic require() 加载 teamMemPaths 和 secretScanner,避免循环依赖。

文件监控器(388 行)

watcher.ts 实现了基于 fs.watch 的目录监控,不使用 chokidar

// watcher.ts — 核心状态
const DEBOUNCE_MS = 2000  // 防抖 2 秒
let watcher: FSWatcher | null = null
let pushInProgress = false
let pushSuppressedReason: string | null = null  // 永久抑制原因

// 永久失败判断 — no_oauth/no_repo 或 4xx(除 409/429)
export function isPermanentFailure(r: TeamMemorySyncPushResult): boolean {
  if (r.errorType === 'no_oauth' || r.errorType === 'no_repo') return true
  if (r.httpStatus >= 400 && r.httpStatus < 500 &&
      r.httpStatus !== 409 && r.httpStatus !== 429) return true
  return false
}
设计决策 原因
不用 chokidar chokidar 4+ 移除了 fsevents,Bun 的 fs.watch fallback 用 kqueue,每个文件一个 fd — 500+ 文件 = 500+ 永久 fd
recursive: true macOS 用 FSEvents(O(1) fd),Linux 用 inotify(O(subdirs))
防抖 2 秒 等待写入稳定后再推送
始终启动监控 即使服务器无内容也启动,避免 bootstrap 死区
抑制清除 文件删除(unlink)是 too-many-entries 的恢复操作 — stat 返回 ENOENT → 清除抑制
优雅关闭 在 2s graceful shutdown budget 内刷新,HTTP PUT 超时则被 process.exit() 杀死

推送抑制机制

条件 处理
无 OAuth / 无仓库 永久抑制
4xx(除 409/429) 永久抑制(404 缺失仓库、413 条目过多、403 权限)
409 冲突 不抑制(下次 pull 后重试可能成功)
429 限流 不抑制(监控器驱动的退避即可)

门控条件(4 层):

  1. feature('TEAMMEM') 编译时标志
  2. isTeamMemoryEnabled() 运行时检查
  3. isTeamMemorySyncAvailable() OAuth 可用性
  4. getGithubRepo() github.com 远程检查

六、记忆三兄弟:自动梦境、会话记忆、团队记忆

Claude Code 有三套独立的记忆系统,各有分工:

自动梦境(autoDream)

名字很科幻,本质是后台记忆整合。在空闲时自动整合会话记忆,就像人类在睡眠中整合白天的记忆一样。

4 阶段梦境流程

// consolidationPrompt.ts — 构建 4 阶段梦境提示词
// Phase 1: Orient     → ls memory dir, 读取入口文件, 浏览已有文件
// Phase 2: Gather     → 日志, 漂移的记忆, 转录搜索
// Phase 3: Consolidate → 合并, 相对日期转绝对日期, 删除矛盾
// Phase 4: Prune      → 更新入口文件, 限制行数和大小

基于文件的锁机制consolidationLock.ts(141 行),核心设计是 mtime 就是 lastConsolidatedAt

const LOCK_FILE = '.consolidate-lock'
const HOLDER_STALE_MS = 60 * 60 * 1000  // 1 小时 PID 复用保护

// 读取上次整合时间 — 每个 turn 只需一次 stat
async function readLastConsolidatedAt(): Promise<number> {
  try { return (await stat(lockPath())).mtimeMs } catch { return 0 }
}

// 获取锁 — 写入 PID,验证没有竞态
async function tryAcquireConsolidationLock(): Promise<number | null> {
  // 1. 读取现有锁:stat + readFile 并行
  // 2. 检查是否被活跃 PID 持有(HOLDER_STALE_MS 内)
  // 3. 写入当前 PID
  // 4. 验证写入成功(竞态:两个回收者同时写 → 最后写入者赢得 PID)
  // 返回获取前的 mtime(用于失败时回滚)
}

// 失败时回滚 — utimes 恢复 mtime,unlink 恢复无文件状态
async function rollbackConsolidationLock(priorMtime: number): Promise<void>

关键函数readLastConsolidatedAt()(mtime 即时间戳)、tryAcquireConsolidationLock()(PID + mtime 双重验证)、rollbackConsolidationLock()(失败回滚)、listSessionsTouchedSince()(mtime 非 birthtime,因为 ext4 上 birthtime 为 0)、recordConsolidation()(手动 /dream 命令)。

竞态条件处理:两个回收者同时写入 → 最后写入者赢得 PID,失败者在重新读取时退出。

会话记忆(SessionMemory)

自动维护的 markdown 笔记,通过 forked subagent 生成。

默认模板prompts.ts,325 行)包含 9 个章节:

const sections = [
  'Session Title',        // 会话标题
  'Current State',        // 当前状态
  'Task specification',   // 任务规格
  'Files and Functions',  // 文件与函数
  'Workflow',             // 工作流
  'Errors & Corrections', // 错误与修正
  'Codebase and System Documentation',  // 代码库文档
  'Learnings',            // 学习记录
  'Key results, Worklog', // 关键结果、工作日志
]

触发条件sessionMemoryUtils.ts,208 行):

const DEFAULT_SESSION_MEMORY_CONFIG = {
  minimumMessageTokensToInit: 10000,   // 初始化:累计 10000 token
  minimumTokensBetweenUpdate: 5000,    // 更新间隔:5000 token 增长
  toolCallsBetweenUpdates: 3,          // 更新间隔:3 次工具调用
}

两个条件都满足才触发更新:token 增长 >= 5000 工具调用 >= 3 次。核心常量:MAX_SECTION_LENGTH = 2000 token(单章节上限),MAX_TOTAL_SESSION_MEMORY_TOKENS = 12000(总量上限),EXTRACTION_WAIT_TIMEOUT_MS = 15s,EXTRACTION_STALE_THRESHOLD_MS = 60s。

自定义支持:模板 ~/.claude/session-memory/config/template.md,提示词 ~/.claude/session-memory/config/prompt.md。关键函数:waitForSessionMemoryExtraction()(每秒轮询)、hasMetInitializationThreshold()hasMetUpdateThreshold()truncateSessionMemoryForCompact()(行边界截断)、isSessionMemoryEmpty()(检测是否未提取)。

团队记忆(TeamMemorySync)

前面已经详细介绍,负责团队间的记忆同步。


七、插件系统:可扩展的"应用商店"

src/services/plugins/pluginOperations.ts1089 行代码,是服务层中最大的单文件。

安装流程

// installPluginOp() — 设置优先方法
function installPluginOp() {
  // 1. 搜索物化市场(找到插件源)
  // 2. 写入设置(THE ACTION — 这是真正生效的一步)
  // 3. 缓存插件(下载到本地)
}

作用域管理

操作 有效范围
安装 ['user', 'project', 'local']
更新 ['user', 'project', 'local', 'managed']
优先级 local > project > user

跨范围搜索findPluginInSettings 按优先级搜索:

// 优先级:local (matching project) > project (matching project) > user > first available
function getPluginInstallationFromV2(pluginId, projectDir) {
  // 按优先级搜索所有范围
}

卸载清理

卸载一个插件需要 5 步:

  1. 在 allPlugins 中查找(回退到 V2 数据处理 delisted 插件)
  2. 移除设置 + installed_plugins_v2.json
  3. 标记版本为孤立
  4. 清理插件数据目录
  5. 警告反向依赖

CLI 命令层(345 行)

pluginCliCommands.ts 是插件操作的 CLI 包装器,添加控制台输出和 process.exit()

// 命令类型
type PluginCommand = 'install' | 'uninstall' | 'enable' | 'disable' | 'disable-all' | 'update'

// 错误处理 — 统一的错误记录和遥测
function handlePluginCommandError(error) {
  logError(error)
  logEvent('tengu_plugin_command_failed', { ... })
  process.exit(1)
}

八、更多服务详解

语音系统(voice.ts + voiceStreamSTT.ts

多平台录制,使用原生音频捕获。录制后端按平台选择:macOS 用 cpal(Core Audio,通过 audio-capture-napi 原生模块),Linux 用 arecord(ALSA),Windows 用 SoX

const RECORDING_SAMPLE_RATE = 16000  // 16kHz 采样率
const RECORDING_CHANNELS = 1         // 单声道
const SILENCE_DURATION_SECS = '2.0'  // 静音 2 秒后停止
const SILENCE_THRESHOLD = '3%'       // 静音阈值

原生模块延迟加载 — 避免启动时冻结(dlopen 同步阻塞 ~1s 热启动,~8s 冷启动):

function loadAudioNapi(): Promise<AudioNapi> {
  audioNapiPromise ??= (async () => {
    const t0 = Date.now()
    const mod = await import('audio-capture-napi')
    mod.isNativeAudioAvailable()  // 触发真正的 .node 加载
    audioNapi = mod
    logForDebugging(`[voice] audio-capture-napi loaded in ${Date.now() - t0}ms`)
    return mod
  })()
  return audioNapiPromise
}

WebSocket STT 使用 Anthropic 的 voice_stream endpoint,支持 Deepgram Nova 3 模型,流式处理实时音频数据。

防止休眠(preventSleep.ts

macOS 专用的引用计数系统,使用 caffeinate 命令防止系统休眠:

const CAFFEINATE_TIMEOUT_SECONDS = 300       // 5 分钟超时
const RESTART_INTERVAL_MS = 4 * 60 * 1000   // 4 分钟重启周期
let refCount = 0

export function startPreventSleep(): void {
  refCount++
  if (refCount === 1) { spawnCaffeinate(); startRestartInterval() }
}
export function stopPreventSleep(): void {
  if (refCount > 0) refCount--
  if (refCount === 0) { stopRestartInterval(); killCaffeinate() }
}
// caffeinate -i -t 300 + unref() = SIGKILL 安全的自愈机制

5 分钟超时 + 4 分钟重启周期 = 即使 Node 被 SIGKILL 杀死,caffeinate 也会在 5 分钟后自动退出。

其他服务速览

服务 文件 说明
Token 估算 tokenEstimation.ts countTokensWithAPI() 精确计算,roughTokenCountEstimationForFileType() 基于文件类型的粗略估算
VCR 录制 vcr.ts 录制 API 请求/响应对,用于调试和测试回放
LSP 管理器 lsp/manager.ts 语言服务器协议,支持补全、跳转定义、引用查找等代码智能功能
策略限制 policyLimits/index.ts 组织级策略限制(禁止工具、限制 API 频率等)
远程设置 remoteManagedSettings/ 企业管理员远程配置中心下发策略
速率限制模拟 mockRateLimits.ts 20+ 模拟场景(ant-only),测试速率限制处理
对话压缩 compact/compact.ts 上下文窗口快满时,自动将旧消息替换为摘要
分析诊断 analytics/ 统一事件追踪,类型名 AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS 提醒不泄露代码/路径

九、条件加载汇总

服务 启用条件
团队记忆同步 feature('TEAMMEM') 编译时标志
语音系统 平台特定(macOS/Linux/Windows)
推测执行 USER_TYPE === 'ant' + 用户设置
自动梦境 KAIROS feature flag
提示建议 KAIROS feature flag
模拟速率限制 USER_TYPE === 'ant'
MCP OAuth 服务器配置要求

十、设计亮点总结

设计决策 价值
三层工具执行框架 执行→Hook→编排,职责分离,安全可控
Hook allow 不绕过 settings deny 安全不变量:即使 Hook 改变了权限,系统设置仍然有效
并发编排的上下文修改器排队 避免并发修改上下文导致的竞态条件
推测执行 + Overlay 隔离 Copy-on-Write 保证安全,预判提升响应速度
推测性分类器检查 和权限检查并行运行,减少等待时间
团队记忆秘密扫描 35+ gitleaks 规则,从源头阻止秘密泄露
fs.watch 替代 chokidar 避免 fd 耗尽,O(1) fd 复杂度
推送抑制机制 区分永久失败和瞬态失败,避免无效重试(167K 事件/2.5 天的真实案例)
记忆三兄弟分工 自动梦境整合、会话记忆笔记、团队记忆同步,各司其职
自动梦境的文件锁 mtime 即 lastConsolidatedAt,PID 复用保护,竞态安全
插件作用域优先级 local > project > user,灵活的配置覆盖
防止休眠的引用计数 5 分钟超时 + 4 分钟重启 = 自愈机制,SIGKILL 安全
语音系统的延迟加载 避免启动冻结,首次按键时才加载原生模块
分析事件的 PII 保护 类型名强制提醒不泄露代码/路径

下篇预告

第七篇:终端里的 React 渲染引擎

服务层是"后勤保障",但用户看到的一切都是终端 UI。Claude Code 的 components/ 目录有 389 个 React 组件,从消息渲染到 Diff 显示,从状态栏到 MCP 管理面板。

更惊人的是,Anthropic 没有直接用 npm 上的 Ink,而是 fork 了一份放在 src/ink/(76+ 文件),做了双缓冲渲染、字符池复用、字素感知等深度优化。

下一篇,我们将深入这个"终端里的 React",看看它是如何在命令行中做出如此精美界面的。


标签: Claude Code 服务层 MCP OAuth 推测执行 团队记忆同步 插件系统 自动梦境 源码分析

更多推荐