# 深入拆解 Claude Code 源码(六):20 个后端服务如何支撑 AI 编程助手的“后勤大本营“
深入拆解 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)、sdk、sse-ide/ws-ide(IDE 集成)、claudeai-proxy。
当 MCP 服务器的 OAuth token 过期时,系统会捕获 McpAuthError 并自动将服务器状态更新为 needs-auth,UI 会显示"需要重新授权"。
四、推测执行:AI 的"预判"能力
这是 Claude Code 中最"科幻"的功能之一。src/services/PromptSuggestion/speculation.ts 有 992 行代码,实现了浏览器级别的"预渲染"思想。
工作原理
当你在思考下一步要做什么时,推测执行系统已经:
- 预判你的意图 — 基于对话历史预测你可能的下一步操作
- 提前执行 — 在一个隔离的 overlay 目录中执行预测的操作
- 等待确认 — 如果猜对了,直接使用结果;猜错了,静默丢弃
核心常量与分类
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 结构化错误)。结果类型包括 TeamMemorySyncFetchResult、TeamMemoryHashesResult、TeamMemorySyncPushResult(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
}
这个检查在 FileWriteTool 和 FileEditTool 的 validateInput() 中调用,从源头阻止秘密泄露。实现使用 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 层):
feature('TEAMMEM')编译时标志isTeamMemoryEnabled()运行时检查isTeamMemorySyncAvailable()OAuth 可用性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.ts 有 1089 行代码,是服务层中最大的单文件。
安装流程
// 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 步:
- 在 allPlugins 中查找(回退到 V2 数据处理 delisted 插件)
- 移除设置 +
installed_plugins_v2.json - 标记版本为孤立
- 清理插件数据目录
- 警告反向依赖
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服务层MCPOAuth推测执行团队记忆同步插件系统自动梦境源码分析
更多推荐




所有评论(0)