From 4230f0fff1c02ab4ea58115cbf061bdb4c113a97 Mon Sep 17 00:00:00 2001 From: claude-code-best Date: Thu, 7 May 2026 14:34:39 +0800 Subject: [PATCH] chore: remove learn directory study notes Co-Authored-By: glm-5-turbo --- learn/LEARN.md | 152 ------ learn/phase-1-qa.md | 273 ---------- learn/phase-1-startup-flow.md | 597 ---------------------- learn/phase-2-conversation-loop.md | 774 ----------------------------- learn/phase-2-qa.md | 372 -------------- 5 files changed, 2168 deletions(-) delete mode 100644 learn/LEARN.md delete mode 100644 learn/phase-1-qa.md delete mode 100644 learn/phase-1-startup-flow.md delete mode 100644 learn/phase-2-conversation-loop.md delete mode 100644 learn/phase-2-qa.md diff --git a/learn/LEARN.md b/learn/LEARN.md deleted file mode 100644 index 189bfc9fd..000000000 --- a/learn/LEARN.md +++ /dev/null @@ -1,152 +0,0 @@ -# Claude Code 源码学习路线 - -> 基于反编译版 Claude Code CLI (v2.1.888) 的源码学习跟踪 -> -> 各阶段详细笔记见同目录下的 `phase-*.md` 文件 - -## 第一阶段:启动流程(入口链路) ✅ - -详细笔记:[phase-1-startup-flow.md](phase-1-startup-flow.md) - -理解程序从命令行启动到用户看到交互界面的完整路径。 - -- [x] `src/entrypoints/cli.tsx` — 真正入口,polyfill 注入 + 快速路径分发 - - [x] 全局 polyfill:`feature()` 永远返回 false、`MACRO` 全局对象、`BUILD_*` 常量 - - [x] 快速路径设计:按开销从低到高检查,能早返回就早返回 - - [x] 动态 import 模式:`await import()` 延迟加载,减少启动时间 - - [x] 最终出口:`import("../main.jsx")` → `cliMain()` -- [x] `src/main.tsx` — Commander.js CLI 定义,重型初始化(4683 行) - - [x] 三段式结构:辅助函数(1-584) → main()(585-856) → run()(884-4683) - - [x] side-effect import:profileCheckpoint、startMdmRawRead、startKeychainPrefetch 并行预加载 - - [x] preAction 钩子:MDM 等待、init()、迁移、远程设置 - - [x] Commander 参数定义:40+ CLI 选项 - - [x] action handler(2800 行):参数解析 → 服务初始化 → showSetupScreens → launchRepl() - - [x] --print 分支走 print.ts;交互分支走 launchRepl()(7 个场景分支) - - [x] 子命令注册:mcp/auth/plugin/doctor/update/install 等 -- [x] `src/replLauncher.tsx` — 桥梁(22 行),组合 `` + `` 渲染到终端 -- [x] `src/screens/REPL.tsx` — 交互式 REPL 界面(5009 行) - - [x] Props:commands、tools、messages、systemPrompt、thinkingConfig 等 - - [x] 50+ 状态:messages、inputValue、screen、streamingText、queryGuard 等 - - [x] 核心数据流:onSubmit → handlePromptSubmit → onQuery → onQueryImpl → query() → onQueryEvent - - [x] QueryGuard 并发控制:idle → running → idle,防止重复查询 - - [x] 渲染:Transcript 模式(只读历史)/ Prompt 模式(Messages + PermissionRequest + PromptInput) - -**数据流**:`bun run dev` → `package.json scripts.dev` → `bun run src/entrypoints/cli.tsx` → 快速路径检查 → `main.tsx:main()` → `launchRepl()` → `` - ---- - -## 第二阶段:核心对话循环 ✅ - -详细笔记:[phase-2-conversation-loop.md](phase-2-conversation-loop.md) - -理解用户发一句话后,如何变成 API 请求、如何处理流式响应和工具调用。 - -- [x] `src/query.ts` — 核心查询循环(1732 行) - - [x] `query()` AsyncGenerator 入口,委托给 `queryLoop()` - - [x] `queryLoop()` — while(true) 主循环,State 对象管理迭代状态 - - [x] 消息预处理(autocompact、compact boundary) - - [x] `deps.callModel()` → 流式 API 调用 - - [x] StreamingToolExecutor — API 流式返回时并行执行工具 - - [x] 工具调用循环(tool use → 执行 → result → continue) - - [x] 错误恢复(prompt-too-long、max_output_tokens 升级+多轮恢复) - - [x] 模型降级(FallbackTriggeredError → 切换 fallbackModel) - - [x] Withheld 消息模式(暂扣可恢复错误) -- [x] `src/QueryEngine.ts` — 高层编排器(1320 行) - - [x] QueryEngine 类 — 一个 conversation 一个实例 - - [x] `submitMessage()` — 处理用户输入 → 调用 `query()` → 消费事件流 - - [x] SDK/print 模式专用(REPL 直接调用 query()) - - [x] 会话持久化(recordTranscript) - - [x] Usage 跟踪、权限拒绝记录 - - [x] `ask()` 便捷包装函数 -- [x] `src/services/api/claude.ts` — API 客户端(3420 行) - - [x] `queryModelWithStreaming` / `queryModelWithoutStreaming` — 两个公开入口 - - [x] `queryModel()` — 核心私有函数(2400 行) - - [x] 请求参数组装(system prompt、betas、tools、cache control) - - [x] Anthropic SDK 流式调用(`anthropic.beta.messages.stream()`) - - [x] `BetaRawMessageStreamEvent` 事件处理(message_start/content_block_*/message_delta/stop) - - [x] withRetry 重试策略(429/500/529 + 模型降级) - - [x] Prompt Caching 策略(ephemeral/1h TTL/global scope) - - [x] 多 provider 支持(Anthropic / Bedrock / Vertex / Azure) - -**数据流**:REPL.onSubmit → handlePromptSubmit → onQuery → onQueryImpl → `query()` AsyncGenerator → `queryLoop()` while(true) → `deps.callModel()` → `claude.ts queryModel()` → `anthropic.beta.messages.stream()` → 流式事件 → 收集 tool_use → 执行工具 → 结果追加到 messages → continue → 无工具调用时 return - ---- - -## 第三阶段:工具系统 - -理解 Claude 如何定义、注册、调用工具。先读框架,再挑具体工具。 - -- [ ] `src/Tool.ts` — Tool 接口定义 - - [ ] `Tool` 类型结构(name、description、inputSchema、call) - - [ ] `findToolByName`、`toolMatchesName` 工具函数 -- [ ] `src/tools.ts` — 工具注册表 - - [ ] 工具列表组装逻辑 - - [ ] 条件加载(feature flag、USER_TYPE) -- [ ] 具体工具实现(挑选 2-3 个深入阅读): - - [ ] `src/tools/BashTool/` — 执行 shell 命令,最常用的工具 - - [ ] `src/tools/FileReadTool/` — 读取文件,简单直观,适合理解工具模式 - - [ ] `src/tools/FileEditTool/` — 编辑文件,理解 diff/patch 机制 - - [ ] `src/tools/AgentTool/` — 子 Agent 机制,较复杂但核心 - ---- - -## 第四阶段:上下文与系统提示 - -理解 Claude 如何"知道"项目信息、用户偏好等上下文。 - -- [ ] `src/context.ts` — 系统/用户上下文构建 - - [ ] git 状态注入 - - [ ] CLAUDE.md 内容加载 - - [ ] 内存文件(memory)注入 - - [ ] 日期、平台等环境信息 -- [ ] `src/utils/claudemd.ts` — CLAUDE.md 发现与加载 - - [ ] 项目层级搜索逻辑 - - [ ] 多级 CLAUDE.md 合并 - ---- - -## 第五阶段:UI 层(按兴趣选读) - -理解终端 UI 的渲染机制(React/Ink)。 - -- [ ] `src/components/App.tsx` — 根组件,Provider 注入 -- [ ] `src/state/AppState.tsx` — 全局状态类型与 Context -- [ ] `src/components/permissions/` — 工具权限审批 UI -- [ ] `src/components/messages/` — 消息渲染组件 - ---- - -## 第六阶段:外围系统(按需探索) - -- [ ] `src/services/mcp/` — MCP 协议(Model Context Protocol) -- [ ] `src/skills/` — 技能系统(/commit 等斜杠命令) -- [ ] `src/commands/` — CLI 子命令 -- [ ] `src/tasks/` — 后台任务系统 -- [ ] `src/utils/model/providers.ts` — 多 provider 选择逻辑 - ---- - -## 学习笔记 - -### 关键设计模式 - -| 模式 | 位置 | 说明 | -|------|------|------| -| 快速路径 | cli.tsx | 按开销从低到高逐级检查,减少不必要的模块加载 | -| 动态 import | cli.tsx / main.tsx | `await import()` 延迟加载,优化启动时间 | -| feature flag | 全局 | `feature()` 永远返回 false,所有内部功能禁用 | -| React/Ink | UI 层 | 用 React 组件模型渲染终端 UI | -| 工具循环 | query.ts | AI 返回工具调用 → 执行 → 结果回传 → 继续,直到无工具调用 | -| AsyncGenerator 链 | query.ts → claude.ts | `yield*` 透传事件流,形成管道 | -| State 对象 | query.ts queryLoop | 循环间通过不可变 State + transition 字段传递状态 | -| StreamingToolExecutor | query.ts | API 流式返回时并行执行工具 | -| Withheld 消息 | query.ts | 暂扣可恢复错误,恢复成功则吞掉 | -| withRetry | claude.ts | 429/500/529 自动重试 + 模型降级 | -| Prompt Caching | claude.ts | 缓存系统提示和历史消息,减少 token 消耗 | - -### 需要忽略的内容 - -- `_c()` 调用 — React Compiler 反编译产物 -- `feature('...')` 后面的代码块 — 全部是死代码 -- tsc 类型错误 — 反编译导致,不影响 Bun 运行 -- `packages/@ant/` — stub 包,无实际实现 \ No newline at end of file diff --git a/learn/phase-1-qa.md b/learn/phase-1-qa.md deleted file mode 100644 index 71d692e73..000000000 --- a/learn/phase-1-qa.md +++ /dev/null @@ -1,273 +0,0 @@ -# 第一阶段 Q&A - -## Q1:cli.tsx 的快速路径分发具体在做什么? - -**核心思想**:根据用户输入的命令参数,尽早决定走哪条路,避免加载不需要的代码。cli.tsx 充当一个轻量级路由器,把简单请求就地处理,只有真正需要完整 CLI 时才加载 main.tsx。 - -### 场景对比 - -#### 场景 1:`claude --version`(命中快速路径) - -``` -cli.tsx main() 开始执行 - ├── args = ["--version"] - ├── 命中第 64 行: args[0] === "--version" ✅ - ├── console.log("2.1.888 (Claude Code)") - └── return ← 立即退出,零 import,~10ms -``` - -#### 场景 2:`claude --claude-in-chrome-mcp`(命中中间路径) - -``` -cli.tsx main() 开始执行 - ├── 第 64 行: --version? ❌ - ├── 第 75 行: 加载 profileCheckpoint(仅此一个 import) - ├── 第 81 行: feature("DUMP_SYSTEM_PROMPT") → false ❌ - ├── 第 95 行: --claude-in-chrome-mcp? ✅ 命中 - ├── await import("../utils/claudeInChrome/mcpServer.js") ← 只加载这一个模块 - └── return ← 没有加载 main.tsx 的 200+ import -``` - -#### 场景 3:`claude`(无参数,最常见,全部未命中) - -``` -cli.tsx main() 开始执行 - ├── --version? ❌ - ├── profileCheckpoint 加载 - ├── feature(DUMP)? ❌ (feature=false) - ├── --chrome-mcp? ❌ - ├── --chrome-native? ❌ - ├── feature(CHICAGO)? ❌ (feature=false) - ├── feature(DAEMON)? ❌ (feature=false) - ├── feature(BRIDGE)? ❌ (feature=false) - ├── ... 所有快速路径逐一检查,全部未命中 - │ - ├── 走到第 310 行 ← 最终出口 - ├── await import("../main.jsx") ← 加载完整 CLI(200+ import,~135ms) - └── await cliMain() ← 进入 main.tsx 重型初始化 -``` - -### 性能对比 - -| 方式 | `claude --version` 耗时 | -|------|------------------------| -| 无快速路径(全部走 main.tsx) | ~200ms(加载 200+ import → 初始化 Commander → 解析参数 → 打印) | -| 有快速路径(cli.tsx 拦截) | ~10ms(读 args → 打印 → 退出) | - -### feature() 的加速作用 - -大量快速路径被 `feature()` 守护: - -```ts -if (feature("DAEMON") && args[0] === "daemon") { ... } -``` - -`feature()` 返回 false → `&&` 短路求值 → 连 `args[0]` 都不检查,直接跳过。在反编译版本中这些路径等于不存在,进一步加速了"全部没命中 → 走默认路径"的过程。 - ---- - -## Q2:main.tsx 中不同命令的具体执行流程是怎样的? - -所有命令都会经过 main() → run(),但在 run() 内部根据 Commander 路由到不同分支。 - -### 场景 1:`claude`(无参数 — 启动交互 REPL) - -最常见的场景,走完整条主命令路径: - -``` -main() (第 585 行) - ├── 信号处理注册(SIGINT、exit) - ├── feature flag 路径全部跳过 - ├── isNonInteractive = false(有 TTY,没有 -p) - ├── clientType = 'cli' - └── await run() - │ - ▼ - run() (第 884 行) - ├── Commander 初始化 + preAction 钩子 + 主命令选项注册 - ├── isPrintMode = false → 注册所有子命令 - └── program.parseAsync(process.argv) - │ Commander 匹配到主命令,先执行 preAction - ▼ - preAction (第 907 行) - ├── await ensureMdmSettingsLoaded() ← 等 side-effect import 的子进程完成 - ├── await ensureKeychainPrefetchCompleted() ← 等 keychain 预读完成 - ├── await init() ← 遥测、配置、信任 - ├── initSinks() ← 分析日志 - ├── runMigrations() ← 数据迁移 - └── loadRemoteManagedSettings() / loadPolicyLimits() ← 非阻塞 - │ 然后执行 action handler - ▼ - action(undefined, options) (第 1007 行) ← prompt = undefined - ├── [参数解析] permissionMode, model, thinkingConfig... - ├── [工具加载] tools = getTools(toolPermissionContext) - ├── [并行初始化] - │ ├── setup() ← worktree、CWD - │ ├── getCommands() ← 加载斜杠命令 - │ └── getAgentDefinitionsWithOverrides() ← 加载 agent 定义 - ├── [MCP 连接] 连接配置的 MCP 服务器 - ├── [构建初始状态] initialState = { tools, mcp, permissions, ... } - │ - ├── [UI 初始化](交互模式专属) - │ ├── createRoot() ← 创建 Ink 渲染根节点 - │ └── showSetupScreens() ← 信任对话框 / OAuth / 引导 - │ - ├── [后续初始化] LSP、插件版本、session 注册 - │ - └── 默认分支 (第 3760 行) ← 没有 --continue/--resume/--print - └── await launchRepl(root, { - initialState - }, { - ...sessionConfig, - initialMessages: undefined ← 全新对话,无历史消息 - }, renderAndRun) - │ - ▼ - REPL.tsx 渲染,用户看到空白对话界面 -``` - -### 场景 2:`echo "explain this" | claude -p`(管道/非交互模式) - -``` -main() → - ├── isNonInteractive = true(-p 标志 + stdin 不是 TTY) - ├── clientType = 'sdk-cli' - └── run() - │ - ▼ - run() - ├── Commander 初始化 + preAction + 主命令选项 - ├── isPrintMode = true - │ → ★ 跳过所有子命令注册(节省 ~65ms) - └── program.parseAsync() ← 直接解析,Commander 路由到主命令 action - │ - ▼ - preAction → init、迁移等(同场景 1) - │ - ▼ - action("", { print: true, ... }) - ├── inputPrompt = await getInputPrompt("") - │ ├── stdin.isTTY = false → 从 stdin 读数据 - │ ├── 等待最多 3s 读入: "explain this" - │ └── 返回 "explain this" - ├── tools = getTools() - ├── setup() + getCommands()(并行) - │ - ├── isNonInteractiveSession = true → 走 --print 分支(第 2584 行) - │ ├── applyConfigEnvironmentVariables() ← -p 模式信任隐含 - │ ├── 构建 headlessInitialState(无 UI) - │ ├── headlessStore = createStore(headlessInitialState) - │ │ - │ ├── await import('src/cli/print.js') - │ └── runHeadless(inputPrompt, ...) ★ 不走 REPL - │ ├── 发送 API 请求 - │ ├── 流式输出到 stdout - │ └── 完成后 process.exit() - │ - └── ← 不走 createRoot()、showSetupScreens()、launchRepl() -``` - -**关键差异**: -- 检测到 `-p` 后跳过子命令注册(节省 ~65ms) -- 不创建 Ink UI,不调用 `showSetupScreens()` -- 从 stdin 读取输入(`getInputPrompt` 第 857 行) -- 走 `print.js` 路径直接执行查询输出到 stdout - -### 场景 3:`claude -c`(继续最近对话) - -``` -... main() → run() → preAction → action(前半部分同场景 1) - │ - ▼ - action(undefined, { continue: true, ... }) - ├── [参数解析 + 工具加载 + 并行初始化 + UI 初始化](同场景 1) - │ - ├── options.continue = true → 命中第 3101 行 - │ ├── clearSessionCaches() ← 清除过期缓存 - │ ├── result = await loadConversationForResume() - │ │ └── 从 ~/.claude/projects// 读最近的会话 JSONL - │ │ - │ ├── result 为 null? → exitWithError("No conversation found") - │ │ - │ ├── loaded = await processResumedConversation(result) - │ │ ├── 解析 JSONL → messages[] - │ │ ├── 恢复文件历史快照 - │ │ └── 重建 initialState - │ │ - │ └── await launchRepl(root, { - │ initialState: loaded.initialState - │ }, { - │ ...sessionConfig, - │ initialMessages: loaded.messages, ★ 带上历史消息 - │ initialFileHistorySnapshots: loaded.fileHistorySnapshots, - │ initialAgentName: loaded.agentName - │ }, renderAndRun) - │ │ - │ ▼ - │ REPL.tsx 渲染,显示历史对话,用户继续聊天 - │ - └── ← 其他分支不执行 -``` - -**关键差异**:`initialMessages` 有值(历史消息),REPL 启动时会渲染之前的对话内容。 - -### 场景 4:`claude mcp list`(子命令) - -``` -main() → run() - │ - ▼ - run() - ├── Commander 初始化 + preAction 钩子 - ├── 注册主命令 .action(...) - ├── isPrintMode = false → 注册所有子命令 - │ ├── program.command('mcp') (第 3894 行) - │ │ ├── mcp.command('serve').action(...) - │ │ ├── mcp.command('add').action(...) - │ │ ├── mcp.command('list').action(async () => { ★ - │ │ │ const { mcpListHandler } = await import('./cli/handlers/mcp.js'); - │ │ │ await mcpListHandler(); - │ │ │ }) - │ │ └── ... - │ ├── program.command('auth') - │ ├── program.command('doctor') - │ └── ... - │ - └── program.parseAsync(["node", "claude", "mcp", "list"]) - │ Commander 匹配到 mcp → list - ▼ - preAction (第 907 行) ← 子命令也触发 preAction - ├── await init() - ├── initSinks() - ├── runMigrations() - └── ... - │ - ▼ 执行子命令自己的 action(不走主命令 action) - mcp list action - ├── await import('./cli/handlers/mcp.js') - └── await mcpListHandler() - ├── 读取 MCP 配置(user/project/local 三级) - ├── 连接每个服务器做健康检查 - ├── 格式化输出到终端 - └── 退出 - - ← 主命令的 action handler 完全不执行 - ← 没有 REPL、没有 Ink UI、没有 showSetupScreens -``` - -**关键差异**: -- Commander 路由到子命令,**主命令 action 完全跳过** -- `preAction` 仍然执行(基础初始化所有命令都需要) -- 子命令有自己独立的轻量 action - -### 四种场景对比 - -| | `claude` | `claude -p` | `claude -c` | `claude mcp list` | -|---|---------|------------|------------|-------------------| -| preAction | 执行 | 执行 | 执行 | 执行 | -| 主命令 action | 执行 | 执行 | 执行 | **跳过** | -| 子命令注册 | 注册 | **跳过** | 注册 | 注册 | -| showSetupScreens | 执行 | **跳过** | 执行 | **跳过** | -| createRoot (Ink) | 执行 | **跳过** | 执行 | **跳过** | -| 加载历史消息 | 否 | 否 | **是** | 否 | -| 最终出口 | launchRepl | print.js | launchRepl | 子命令 action | \ No newline at end of file diff --git a/learn/phase-1-startup-flow.md b/learn/phase-1-startup-flow.md deleted file mode 100644 index 17e3c5595..000000000 --- a/learn/phase-1-startup-flow.md +++ /dev/null @@ -1,597 +0,0 @@ -# 第一阶段:启动流程详解 - -> 从 `bun run dev` 到用户看到交互界面的完整路径 - -## 启动链路总览 - -``` -bun run dev - → package.json scripts.dev: "bun run src/entrypoints/cli.tsx" - → cli.tsx: polyfill 注入 + 快速路径检查 - → import("../main.jsx") → cliMain() - → main.tsx: main() → run() - → Commander 参数解析 → preAction 钩子 - → action handler: 服务初始化 → showSetupScreens - → launchRepl() - → replLauncher.tsx: - → REPL.tsx: 渲染交互界面,等待用户输入 -``` - ---- - -## 1. cli.tsx(321 行)— 入口与快速路径分发 - -**文件路径**: `src/entrypoints/cli.tsx` - -### 1.1 全局 Polyfill(第 1-53 行) - -模块加载时立即执行的 side-effect,在 `main()` 之前运行。 - -#### feature() 桩函数(第 3 行) - -```ts -const feature = (_name: string) => false; -``` - -原版 Claude Code 构建时,Bun bundler 通过 `bun:bundle` 提供 `feature()` 函数,用于**编译时 feature flag**(类似 C 的 `#ifdef`)。反编译版没有构建流程,所以直接定义为永远返回 `false`。 - -**效果**:所有 Anthropic 内部功能分支全部禁用,包括: -- `COORDINATOR_MODE` — 协调器模式 -- `KAIROS` — 助手模式 -- `DAEMON` — 后台守护进程 -- `BRIDGE_MODE` — 远程控制 -- `SSH_REMOTE` — SSH 远程 -- `BG_SESSIONS` — 后台会话 -- ... 等 20+ 个 flag - -#### MACRO 全局对象(第 4-14 行) - -```ts -globalThis.MACRO = { - VERSION: "2.1.888", - BUILD_TIME: new Date().toISOString(), - FEEDBACK_CHANNEL: "", - ISSUES_EXPLAINER: "", - NATIVE_PACKAGE_URL: "", - PACKAGE_URL: "", - VERSION_CHANGELOG: "", -}; -``` - -原版构建时 Bun 会把这些值内联到代码里。这里模拟注入,让后续代码读 `MACRO.VERSION` 时能拿到值。 - -#### 构建常量(第 16-18 行) - -```ts -BUILD_TARGET = "external"; // 标记为"外部"构建(非 Anthropic 内部) -BUILD_ENV = "production"; // 生产环境 -INTERFACE_TYPE = "stdio"; // 标准输入输出模式 -``` - -这三个全局变量在代码各处被读取,用来区分运行环境。`"external"` 意味着很多 `("external" as string) === 'ant'` 的检查会返回 false。 - -#### 环境修补(第 22-33 行) - -- 禁用 corepack 自动 pin(防止污染 package.json) -- 远程模式下设置 Node.js 堆内存上限 8GB - -#### ABLATION_BASELINE(第 40-53 行) - -```ts -if (feature("ABLATION_BASELINE") && ...) { ... } -``` - -`feature()` 返回 false,**永远不执行**。Anthropic 内部 A/B 测试代码。 - -### 1.2 main() 函数(第 60-317 行) - -设计模式:**分层快速路径(fast path cascading)**——按开销从低到高逐级检查,命中即返回。 - -#### 快速路径列表 - -| 优先级 | 行号 | 检查条件 | 功能 | 开销 | 可执行 | -|--------|------|---------|------|------|--------| -| 1 | 64-72 | `--version` / `-v` | 打印版本号退出 | **零 import** | 是 | -| 2 | 81-94 | `feature("DUMP_SYSTEM_PROMPT")` | 导出系统提示 | - | 否(flag) | -| 3 | 95-99 | `--claude-in-chrome-mcp` | Chrome MCP 服务 | 动态 import | 是 | -| 4 | 101-105 | `--chrome-native-host` | Chrome Native Host | 动态 import | 是 | -| 5 | 108-116 | `feature("CHICAGO_MCP")` | Computer Use MCP | - | 否(flag) | -| 6 | 123-127 | `feature("DAEMON")` | Daemon Worker | - | 否(flag) | -| 7 | 133-178 | `feature("BRIDGE_MODE")` | 远程控制 | - | 否(flag) | -| 8 | 181-190 | `feature("DAEMON")` | Daemon 主进程 | - | 否(flag) | -| 9 | 195-225 | `feature("BG_SESSIONS")` | ps/logs/attach/kill | - | 否(flag) | -| 10 | 228-240 | `feature("TEMPLATES")` | 模板任务 | - | 否(flag) | -| 11 | 244-253 | `feature("BYOC_ENVIRONMENT_RUNNER")` | BYOC 运行器 | - | 否(flag) | -| 12 | 258-264 | `feature("SELF_HOSTED_RUNNER")` | 自托管运行器 | - | 否(flag) | -| 13 | 267-293 | `--tmux` + `--worktree` | tmux worktree | 动态 import | 是 | - -#### 参数修正(第 296-307 行) - -```ts -// --update/--upgrade → 重写为 update 子命令 -if (args[0] === "--update") process.argv = [..., "update"]; -// --bare → 设置简单模式环境变量 -if (args.includes("--bare")) process.env.CLAUDE_CODE_SIMPLE = "1"; -``` - -#### 最终出口(第 310-316 行) - -```ts -const { startCapturingEarlyInput } = await import("../utils/earlyInput.js"); -startCapturingEarlyInput(); // 捕获用户提前输入的内容 -const { main: cliMain } = await import("../main.jsx"); -await cliMain(); // 进入 main.tsx 重型初始化 -``` - -所有快速路径都没命中时(99% 的情况),才走到这里。 - -### 1.3 启动(第 320 行) - -```ts -void main(); -``` - -`void` 表示不关心 Promise 返回值。 - -### 1.4 关键设计思想 - -- **快速路径**:`--version` 零开销返回,不加载任何模块 -- **动态 import**:`await import()` 替代静态 import,每条路径只加载自己需要的模块 -- **feature flag 过滤**:`feature()` 返回 false 使大量内部功能成为死代码 - ---- - -## 2. main.tsx(4683 行)— 重型初始化与 Commander CLI - -**文件路径**: `src/main.tsx` - -整个项目最大的单文件,但结构清晰:**辅助函数 → main() → run()**。 - -### 2.1 Import 区(第 1-215 行) - -200+ 行 import,加载几乎所有子系统。关键的是前三个 **side-effect import**(import 即执行): - -```ts -// 第 9 行:记录时间戳 -profileCheckpoint('main_tsx_entry'); - -// 第 16 行:启动 MDM 子进程读取(macOS plutil) -startMdmRawRead(); - -// 第 20 行:启动 keychain 预读取(OAuth token、API key) -startKeychainPrefetch(); -``` - -这三个在 import 阶段就**并行启动子进程**,和后续 ~135ms 的模块加载同时进行——**用并行隐藏延迟**。 - -### 2.2 辅助函数(第 216-584 行) - -| 函数 | 行号 | 作用 | -|------|------|------| -| `logManagedSettings()` | 216 | 记录企业托管设置到分析日志 | -| `isBeingDebugged()` | 232 | 检测调试模式,**外部构建下直接 exit(1)**(第 266 行) | -| `logSessionTelemetry()` | 279 | Session 遥测(技能、插件) | -| `getCertEnvVarTelemetry()` | 291 | SSL 证书环境变量收集 | -| `runMigrations()` | 326 | 数据迁移(模型重命名、设置格式升级等) | -| `prefetchSystemContextIfSafe()` | 360 | 信任关系建立后安全预取系统上下文 | -| `startDeferredPrefetches()` | 388 | REPL 首次渲染后的延迟预取 | -| `eagerLoadSettings()` | 502 | 在 init() 之前提前加载 `--settings` 参数 | -| `initializeEntrypoint()` | 517 | 根据运行模式设置 `CLAUDE_CODE_ENTRYPOINT` | - -还有 `_pendingConnect`、`_pendingSSH`、`_pendingAssistantChat` 三个状态变量(第 542-583 行),用于暂存子命令参数。 - -### 2.3 main() 函数(第 585-856 行) - -`main()` 本身不长,做完环境检测后调用 `run()`: - -``` -main() -├── 安全设置(NoDefaultCurrentDirectoryInExePath) -├── 信号处理(SIGINT → exit, exit → 恢复光标) -├── feature flag 保护的特殊路径(全部跳过) -├── 检测 -p/--print / --init-only → 判断是否交互模式 -├── clientType 判断(cli / sdk-typescript / remote / github-action 等) -├── eagerLoadSettings() -└── await run() ← 进入真正的逻辑 -``` - -### 2.4 run() 函数(第 884-4683 行) - -占 3800 行,是整个文件的核心。 - -#### Commander 初始化 + preAction 钩子(第 884-967 行) - -```ts -const program = new CommanderCommand() - .configureHelp(createSortedHelpConfig()) - .enablePositionalOptions(); -``` - -**preAction 钩子**(所有命令执行前都会运行): - -``` -preAction -├── await ensureMdmSettingsLoaded() ← 等 MDM 子进程完成 -├── await ensureKeychainPrefetchCompleted() ← 等 keychain 预读完成 -├── await init() ← 一次性初始化 -├── initSinks() ← 分析日志接收器 -├── runMigrations() ← 数据迁移 -├── loadRemoteManagedSettings() ← 企业远程设置(非阻塞) -└── loadPolicyLimits() ← 策略限制(非阻塞) -``` - -#### 主命令 Option 定义(第 968-1006 行) - -定义了 40+ CLI 参数,关键的包括: - -| 参数 | 作用 | -|------|------| -| `-p, --print` | 非交互模式,输出后退出 | -| `--model ` | 指定模型(如 sonnet、opus) | -| `--permission-mode ` | 权限模式 | -| `-c, --continue` | 继续最近对话 | -| `-r, --resume` | 恢复指定对话 | -| `--mcp-config` | MCP 服务器配置文件 | -| `--allowedTools` | 允许的工具列表 | -| `--system-prompt` | 自定义系统提示 | -| `--dangerously-skip-permissions` | 跳过所有权限检查 | -| `--output-format` | 输出格式(text/json/stream-json) | -| `--effort ` | 推理努力级别(low/medium/high/max) | -| `--bare` | 最小模式 | - -#### action 处理器(第 1006-3808 行) - -主命令的执行逻辑,内部按阶段和场景分支: - -``` -action(async (prompt, options) => { - │ - ├── [1007-1600] 参数解析与预处理 - │ ├── --bare 模式 - │ ├── 解析 model / permission-mode / thinking / effort - │ ├── 解析 MCP 配置、工具列表、系统提示 - │ └── 初始化工具权限上下文 - │ - ├── [1600-2220] 服务初始化 - │ ├── MCP 客户端连接 - │ ├── 插件加载 + 技能初始化 - │ ├── 工具列表组装 - │ └── 初始 AppState 构建 - │ - ├── [2220-2315] UI 初始化(交互模式) - │ ├── createRoot() — 创建 Ink 渲染根节点 - │ ├── showSetupScreens() — 信任对话框、OAuth 登录、引导 - │ └── 登录后刷新各种服务 - │ - ├── [2315-2582] 后续初始化 - │ ├── LSP 管理器、插件版本管理 - │ ├── session 注册、遥测日志 - │ └── 遥测上报 - │ - ├── [2584-3050] --print 非交互模式分支 - │ ├── 构建 headless AppState + store - │ └── 交给 print.ts 执行 - │ - └── [3050-3808] 交互模式:启动 REPL(7 个分支) - ├── --continue → 加载最近对话 → launchRepl() - ├── DIRECT_CONNECT → ❌ flag 关闭 - ├── SSH_REMOTE → ❌ flag 关闭 - ├── KAIROS assistant → ❌ flag 关闭 - ├── --resume → 恢复指定对话 → launchRepl() - ├── --resume 无 ID → 显示对话选择器 - └── 默认(无参数) → launchRepl() ★最常走的路径 -}) -``` - -#### 子命令注册(第 3808-4683 行) - -| 子命令 | 行号 | 作用 | -|--------|------|------| -| `claude mcp` | 3892 | MCP 服务器管理(serve/add/remove/list/get) | -| `claude server` | 3960 | Session 服务器(❌ flag 关闭) | -| `claude auth` | 4098 | 认证管理(login/logout/status/token) | -| `claude plugin` | 4148 | 插件管理(install/uninstall/list/update) | -| `claude setup-token` | 4267 | 设置长期认证 token | -| `claude agents` | 4278 | 列出已配置的 agents | -| `claude doctor` | 4346 | 健康检查 | -| `claude update` | 4362 | 检查更新 | -| `claude install` | 4394 | 安装原生构建 | -| `claude log` | 4411 | 查看对话日志(内部) | -| `claude completion` | 4491 | Shell 自动补全 | - -最后执行解析: - -```ts -await program.parseAsync(process.argv); -``` - -### 2.5 main.tsx 学习建议 - -- **不要通读**。记住三段结构:辅助函数 → main() → run() -- `feature()` 返回 false 的分支全部跳过,可忽略 50%+ 代码 -- `("external" as string) === 'ant'` 的分支也跳过(内部构建专用) -- 需要深入某功能时,通过搜索定位对应代码段 - ---- - -## 3. replLauncher.tsx(22 行)— 胶水层 - -**文件路径**: `src/replLauncher.tsx` - -极其简单,就做一件事: - -```tsx -export async function launchRepl(root, appProps, replProps, renderAndRun) { - const { App } = await import('./components/App.js'); - const { REPL } = await import('./screens/REPL.js'); - await renderAndRun(root, ); -} -``` - -- `App` — 全局 Provider(AppState、Stats、FpsMetrics) -- `REPL` — 交互界面组件 -- `renderAndRun` — 把 React 元素渲染到 Ink 终端 - -动态 import 保持了按需加载的策略。 - ---- - -## 4. REPL.tsx(5009 行)— 交互界面 - -**文件路径**: `src/screens/REPL.tsx` - -项目第二大文件,是用户直接交互的界面。一个巨型 React 函数组件。 - -### 4.1 文件结构 - -``` -REPL.tsx (5009 行) -├── [1-310] Import 区(150+ import) -├── [312-525] 辅助组件 -│ ├── median() — 数学工具函数 -│ ├── TranscriptModeFooter — 转录模式底栏 -│ ├── TranscriptSearchBar — 转录搜索栏 -│ └── AnimatedTerminalTitle — 终端标题动画 -├── [527-571] Props 类型定义 -└── [573-5009] REPL() 组件主体 - ├── [600-900] 状态声明(50+ 个 useState/useRef/useAppState) - ├── [900-2750] 副作用与回调(useEffect/useCallback) - ├── [2750-2860] onQueryImpl — 核心:执行 API 查询 - ├── [2860-3030] onQuery — 查询守卫与并发控制 - ├── [3030-3145] 查询相关辅助回调 - ├── [3146-3550] onSubmit — 用户提交处理 - ├── [3550-4395] 更多副作用与状态管理 - └── [4396-5009] JSX 渲染 -``` - -### 4.2 Props - -从 main.tsx 通过 launchRepl() 传入: - -| Prop | 类型 | 含义 | -|------|------|------| -| `commands` | `Command[]` | 可用的斜杠命令 | -| `debug` | `boolean` | 调试模式 | -| `initialTools` | `Tool[]` | 初始工具集 | -| `initialMessages` | `MessageType[]` | 初始消息(恢复对话时有值) | -| `pendingHookMessages` | `Promise<...>` | 延迟加载的 hook 消息 | -| `mcpClients` | `MCPServerConnection[]` | MCP 服务器连接 | -| `systemPrompt` | `string` | 自定义系统提示 | -| `appendSystemPrompt` | `string` | 追加系统提示 | -| `onBeforeQuery` | `fn` | 查询前回调,返回 false 可阻止查询 | -| `onTurnComplete` | `fn` | 轮次完成回调 | -| `mainThreadAgentDefinition` | `AgentDefinition` | 主线程 Agent 定义 | -| `thinkingConfig` | `ThinkingConfig` | 思考模式配置 | -| `disabled` | `boolean` | 禁用输入 | - -### 4.3 状态管理 - -分三层: - -**全局 AppState(通过 useAppState 选择器读取):** - -```ts -const toolPermissionContext = useAppState(s => s.toolPermissionContext); -const verbose = useAppState(s => s.verbose); -const mcp = useAppState(s => s.mcp); -const plugins = useAppState(s => s.plugins); -const agentDefinitions = useAppState(s => s.agentDefinitions); -``` - -**本地状态(useState):** - -```ts -const [messages, setMessages] = useState(initialMessages ?? []); -const [inputValue, setInputValue] = useState(''); -const [screen, setScreen] = useState('prompt'); -const [streamingText, setStreamingText] = useState(null); -const [streamingToolUses, setStreamingToolUses] = useState([]); -// ... 50+ 个状态 -``` - -**关键 Ref:** - -```ts -const queryGuard = useRef(new QueryGuard()).current; // 查询并发控制 -const messagesRef = useRef(messages); // 消息的同步引用(避免闭包问题) -const abortController = ...; // 取消请求控制器 -const responseLengthRef = useRef(0); // 响应长度追踪 -``` - -### 4.4 核心数据流:用户输入 → API 调用 - -``` -用户按回车 - │ - ▼ -onSubmit (第 3146 行) - ├── 斜杠命令?→ immediate command 直接执行 或 handlePromptSubmit 路由 - ├── 空输入?→ 忽略 - ├── 空闲检测 → 可能弹出"是否开始新对话"对话框 - ├── 加入历史记录 - │ - ▼ -handlePromptSubmit (外部函数,src/utils/handlePromptSubmit.ts) - ├── 斜杠命令 → 路由到对应 Command handler - ├── 普通文本 → 构建 UserMessage,调用 onQuery() - │ - ▼ -onQuery (第 2860 行) — 并发守卫层 - ├── queryGuard.tryStart() → 已有查询?排队等待 - ├── setMessages([...old, ...newMessages]) — 追加用户消息 - ├── onQueryImpl() - │ - ▼ -onQueryImpl (第 2750 行) — 真正执行 API 调用 - │ - ├── 1. 并行加载上下文: - │ await Promise.all([ - │ getSystemPrompt(), // 构建系统提示 - │ getUserContext(), // 用户上下文 - │ getSystemContext(), // 系统上下文(git、平台等) - │ ]) - │ - ├── 2. buildEffectiveSystemPrompt() — 合成最终系统提示 - │ - ├── 3. for await (const event of query({...})) ★核心★ - │ │ 调用 src/query.ts 的 query() AsyncGenerator - │ │ 流式产出事件 - │ │ - │ └── onQueryEvent(event) — 处理每个流式事件 - │ ├── 更新 streamingText(打字机效果) - │ ├── 更新 messages(工具调用结果) - │ └── 更新 inProgressToolUseIDs - │ - └── 4. 收尾:resetLoadingState()、onTurnComplete() -``` - -**核心代码(第 2797-2807 行)**: - -```ts -for await (const event of query({ - messages: messagesIncludingNewMessages, - systemPrompt, - userContext, - systemContext, - canUseTool, - toolUseContext, - querySource: getQuerySourceForREPL() -})) { - onQueryEvent(event); -} -``` - -`query()` 来自 `src/query.ts`,是第二阶段要学的核心函数。 - -### 4.5 QueryGuard 并发控制 - -防止同时发起多个 API 请求的状态机: - -``` -idle ──tryStart()──▶ running ──end()──▶ idle - │ - └── tryStart() 返回 null(已在运行) - → 新消息排入队列 -``` - -- `tryStart()` — 原子操作,检查并转换 idle→running,返回 generation 号 -- `end(generation)` — 检查 generation 匹配后转换 running→idle -- 防止 cancel+resubmit 竞态条件 - -### 4.6 JSX 渲染 - -两个互斥的渲染分支: - -#### Transcript 模式(第 4396-4493 行) - -按 `v` 键切换,只读浏览对话历史,支持搜索: - -```tsx - - - - - - } - bottom={} - /> - -``` - -#### Prompt 模式(第 4552-5009 行) - -主交互界面,从上到下: - -```tsx - - // 终端 tab 标题 - // 全局快捷键 - // 命令快捷键 - // 滚动快捷键 - // Ctrl+C 取消 - // MCP 连接管理 - } // 权限审批覆盖层 - scrollable={ // 可滚动区域 - <> - // ★ 对话消息渲染 - // 用户输入占位 - {toolJSX} // 工具 UI - // 加载动画 - - } - bottom={ // 固定底部 - <> - {/* 各种对话框 */} - - - - - - - {/* ★ 用户输入框 */} - - - } - /> - - -``` - -### 4.7 REPL.tsx 学习建议 - -- 核心只有一条线:`onSubmit → onQuery → query() → onQueryEvent → 更新消息` -- 其余 4000+ 行是 UI 细节:快捷键、对话框、动画、边界情况处理 -- `feature('...')` 保护的 JSX 全部跳过 -- `("external" as string) === 'ant'` 的分支也跳过 - ---- - -## 关键设计模式总结 - -| 模式 | 位置 | 说明 | -|------|------|------| -| 快速路径 | cli.tsx | 按开销从低到高逐级检查,零开销处理简单请求 | -| 动态 import | cli.tsx / main.tsx | `await import()` 延迟加载,每条路径只加载需要的模块 | -| Side-effect import | main.tsx 顶部 | import 阶段就并行启动子进程,用并行隐藏延迟 | -| feature flag | 全局 | `feature()` 永远返回 false,编译时消除死代码 | -| preAction 钩子 | main.tsx run() | Commander.js 命令执行前统一初始化 | -| QueryGuard | REPL.tsx | 状态机防止并发 API 请求,带 generation 计数防竞态 | -| React/Ink | UI 层 | 用 React 组件模型渲染终端 UI,支持全屏和虚拟滚动 | - -## 需要忽略的代码模式 - -| 模式 | 来源 | 说明 | -|------|------|------| -| `_c(N)` 调用 | React Compiler | 反编译产生的 memoization 样板代码 | -| `feature('FLAG')` 后面的代码 | Bun bundler | 全部是死代码,在当前版本不会执行 | -| `("external" as string) === 'ant'` | 构建目标检查 | 永远为 false(external !== ant) | -| tsc 类型错误 | 反编译 | `unknown`/`never`/`{}` 类型,不影响 Bun 运行 | -| `packages/@ant/` | stub 包 | 空实现,仅满足 import 依赖 | \ No newline at end of file diff --git a/learn/phase-2-conversation-loop.md b/learn/phase-2-conversation-loop.md deleted file mode 100644 index fc99227c2..000000000 --- a/learn/phase-2-conversation-loop.md +++ /dev/null @@ -1,774 +0,0 @@ -# 第二阶段:核心对话循环详解 - -> 用户发一句话后,如何变成 API 请求、如何处理流式响应和工具调用 - -## 对话循环总览 - -``` -用户输入 "帮我读取 README.md" - │ - ▼ -REPL.tsx: onSubmit → onQuery → onQueryImpl - │ - ├── 1. 并行加载上下文: - │ getSystemPrompt() + getUserContext() + getSystemContext() - │ - ├── 2. buildEffectiveSystemPrompt() — 合成最终系统提示 - │ - ├── 3. for await (const event of query({...})) ★ 核心循环 - │ │ - │ │ query.ts: queryLoop() - │ │ ├── while (true) { - │ │ │ ├── autocompact / microcompact 处理 - │ │ │ ├── deps.callModel() → claude.ts 流式 API 调用 - │ │ │ │ └── for await (message of stream) { yield message } - │ │ │ │ - │ │ │ ├── 收集 assistant 消息中的 tool_use 块 - │ │ │ │ - │ │ │ ├── needsFollowUp? - │ │ │ │ ├── true → 执行工具 → 收集结果 → state = next → continue - │ │ │ │ └── false → 检查错误恢复 → return { reason: 'completed' } - │ │ │ } - │ │ - │ └── onQueryEvent(event) — 更新 UI 状态 - │ - └── 4. 收尾: resetLoadingState(), onTurnComplete() -``` - -### 两条数据路径 - -| 路径 | 调用方 | 说明 | -|------|--------|------| -| **交互式(REPL)** | REPL.tsx → `query()` | 直接调用 `query()` AsyncGenerator | -| **非交互式(SDK/print)** | print.ts → `QueryEngine.submitMessage()` → `query()` | 通过 QueryEngine 包装,增加了会话持久化、usage 跟踪等 | - ---- - -## 1. query.ts(1732 行)— 核心查询循环 - -**文件路径**: `src/query.ts` - -### 1.1 文件结构 - -``` -query.ts (1732 行) -├── [0-120] Import 区 + feature flag 条件模块加载 -├── [122-148] yieldMissingToolResultBlocks() — 为未配对的 tool_use 生成错误 tool_result -├── [150-178] 常量与辅助函数 (MAX_OUTPUT_TOKENS_RECOVERY_LIMIT, isWithheldMaxOutputTokens) -├── [180-198] QueryParams 类型定义 -├── [200-216] State 类型 — 循环迭代间的可变状态 -├── [218-238] query() — 导出的 AsyncGenerator,委托给 queryLoop() -├── [240-1732] queryLoop() — 核心 while(true) 循环 -│ ├── [241-306] 初始化 State + 内存预取 -│ ├── [307-448] 循环开头:解构 state、消息预处理(snip/microcompact/context collapse) -│ ├── [449-578] 系统提示构建(第449行) + autocompact(第453行) + StreamingToolExecutor 初始化(第562行) -│ ├── [650-866] ★ deps.callModel()(第659行) + 流式响应处理 + tool_use 收集 -│ ├── [896-956] 错误处理(FallbackTriggeredError、通用错误) -│ ├── [1002-1054] 中断处理(abortController.signal.aborted) -│ ├── [1065-1360] 无 followUp 时的终止/恢复逻辑 -│ │ ├── prompt-too-long 恢复 -│ │ ├── max_output_tokens 恢复(升级 + 多轮) -│ │ ├── stop hooks 执行 -│ │ └── return { reason: 'completed' } -│ └── [1360-1732] 有 followUp 时的工具执行 + 下一轮准备 -│ ├── 工具执行(streaming 或 sequential) -│ ├── attachment 注入(排队命令、内存预取、技能发现) -│ ├── maxTurns 检查 -│ └── state = next → continue -``` - -### 1.2 入口:query() 函数(第 219 行) - -```ts -export async function* query(params: QueryParams): - AsyncGenerator { - const consumedCommandUuids: string[] = [] - const terminal = yield* queryLoop(params, consumedCommandUuids) - // 通知所有消费的排队命令已完成 - for (const uuid of consumedCommandUuids) { - notifyCommandLifecycle(uuid, 'completed') - } - return terminal -} -``` - -`query()` 本身很薄,只做两件事: -1. 委托给 `queryLoop()` 执行实际逻辑 -2. 在正常返回后通知排队命令的生命周期 - -### 1.3 QueryParams(第 181 行) - -```ts -type QueryParams = { - messages: Message[] // 当前对话消息 - systemPrompt: SystemPrompt // 系统提示 - userContext: { [k: string]: string } // 用户上下文(CLAUDE.md 等) - systemContext: { [k: string]: string } // 系统上下文(git 状态等) - canUseTool: CanUseToolFn // 工具权限检查函数 - toolUseContext: ToolUseContext // 工具执行上下文 - fallbackModel?: string // 备用模型 - querySource: QuerySource // 查询来源标识 - maxTurns?: number // 最大轮次限制 - taskBudget?: { total: number } // 令牌预算 -} -``` - -### 1.4 State — 循环迭代间的可变状态(第 204 行) - -```ts -type State = { - messages: Message[] // 累积的消息列表 - toolUseContext: ToolUseContext // 工具执行上下文 - autoCompactTracking: ... // 自动压缩跟踪 - maxOutputTokensRecoveryCount: number // 输出令牌恢复尝试次数 - hasAttemptedReactiveCompact: boolean // 是否已尝试响应式压缩 - maxOutputTokensOverride: number | undefined // 输出令牌覆盖 - pendingToolUseSummary: Promise<...> // 待处理的工具使用摘要 - stopHookActive: boolean | undefined // stop hook 是否活跃 - turnCount: number // 当前轮次 - transition: Continue | undefined // 上一次迭代为何 continue -} -``` - -**设计关键**:每次 `continue` 时通过 `state = { ... }` 一次性更新所有状态,而不是分散的 9 个赋值。`transition` 字段记录了为什么要继续循环(便于调试和测试)。 - -### 1.5 queryLoop() 核心流程(第 241 行) - -`while (true)` 循环(第 307 行)的每次迭代代表一次 API 调用。循环直到: -- 模型不需要工具调用 → `return { reason: 'completed' }` -- 被用户中断 → `return { reason: 'aborted_*' }` -- 达到最大轮次 → `return { reason: 'max_turns' }` -- 遇到不可恢复的错误 → `return { reason: 'model_error' }` - -#### 步骤 1:消息预处理 - -``` -每次迭代开头: - ├── 解构 state → messages, toolUseContext, tracking, ... - ├── getMessagesAfterCompactBoundary() — 只保留压缩边界后的消息 - ├── snip 处理(feature flag,跳过) - ├── microcompact 处理(feature flag,跳过) - └── autocompact 检查 — 消息过长时自动压缩 -``` - -#### 步骤 2:系统提示构建(第 449 行) - -```ts -const fullSystemPrompt = asSystemPrompt( - appendSystemContext(systemPrompt, systemContext), -) -``` - -将系统上下文(git 状态、日期等)追加到系统提示。注意:用户上下文(CLAUDE.md 等)不在这里注入,而是在 `deps.callModel()` 调用时通过 `prependUserContext(messagesForQuery, userContext)` 注入到消息数组的最前面(第 660 行)。 - -#### 步骤 3:Autocompact(第 454-543 行) - -当消息历史过长时自动压缩: - -``` -autocompact 流程: - ├── 检查 token 数量是否超过阈值 - ├── 超过 → 调用 compact API(用 Haiku 总结历史) - │ ├── yield compactBoundaryMessage ← 标记压缩边界 - │ └── 更新 messages 为压缩后的版本 - └── 未超过 → 继续 -``` - -#### 步骤 4:调用 API(第 559-708 行)— 核心 - -StreamingToolExecutor 在第 562 行初始化,API 调用在第 659 行开始: - -```ts -// 第 562 行:初始化流式工具执行器 -let streamingToolExecutor = useStreamingToolExecution - ? new StreamingToolExecutor( - toolUseContext.options.tools, canUseTool, toolUseContext, - ) - : null - -// 第 659 行:调用 API -for await (const message of deps.callModel({ - messages: prependUserContext(messagesForQuery, userContext), // ← 用户上下文注入到消息最前面 - systemPrompt: fullSystemPrompt, - thinkingConfig: toolUseContext.options.thinkingConfig, - tools: toolUseContext.options.tools, - signal: toolUseContext.abortController.signal, - options: { model: currentModel, querySource, fallbackModel, ... } -})) { - // 处理每条流式消息(第 708-866 行) -} -``` - -`deps.callModel()` 最终调用 `claude.ts` 的 `queryModelWithStreaming()`。 - -#### 步骤 5:流式响应处理(第 708-866 行) - -处理逻辑在 `for await` 循环体内(第 708 行的 `})` 之后到第 866 行): - -``` -for await (const message of stream): - ├── message.type === 'assistant'? - │ ├── 记录到 assistantMessages[] - │ ├── 提取 tool_use 块 → toolUseBlocks[] - │ ├── needsFollowUp = true(如果有 tool_use) - │ └── streamingToolExecutor.addTool() ← 流式工具并行执行 - │ - ├── withheld? (prompt-too-long / max_output_tokens) - │ └── 暂扣不 yield,等后面恢复逻辑处理 - │ - └── yield message ← 正常 yield 给上层(REPL/QueryEngine) -``` - -**StreamingToolExecutor**:在 API 流式返回的同时就开始执行工具(如读文件),不等流结束。通过 `addTool()` 添加待执行工具,`getCompletedResults()` 获取已完成的结果。 - -#### 步骤 6A:无 followUp — 终止/恢复(第 1065-1360 行) - -当模型没有请求工具调用时(`needsFollowUp === false`): - -``` -无 followUp: - ├── prompt-too-long 恢复? - │ ├── context collapse drain(feature flag,跳过) - │ ├── reactive compact → 压缩消息重试 - │ └── 都失败 → yield 错误 + return - │ - ├── max_output_tokens 恢复? - │ ├── 第一次 → 升级到 64k token 限制,continue - │ ├── 后续 → 注入恢复消息("继续,别道歉"),continue - │ └── 超过 3 次 → yield 错误 + return - │ - ├── stop hooks 执行 - │ ├── preventContinuation? → return - │ └── blockingErrors? → 将错误加入消息,continue - │ - └── return { reason: 'completed' } ★ 正常结束 -``` - -**恢复消息内容(第 1229 行)**: -``` -"Output token limit hit. Resume directly — no apology, no recap of what -you were doing. Pick up mid-thought if that is where the cut happened. -Break remaining work into smaller pieces." -``` - -#### 步骤 6B:有 followUp — 工具执行 + 下一轮(第 1363-1731 行) - -当模型请求了工具调用时(`needsFollowUp === true`): - -``` -有 followUp: - ├── 工具执行(两种模式) - │ ├── streamingToolExecutor? → getRemainingResults()(流式已启动) - │ └── 否 → runTools()(传统顺序执行) - │ - ├── for await (const update of toolUpdates): - │ ├── yield update.message ← 工具结果消息 - │ └── toolResults.push(...) ← 收集工具结果 - │ - ├── 中断检查(abortController.signal.aborted) - │ └── return { reason: 'aborted_tools' } - │ - ├── attachment 注入 - │ ├── 排队命令(其他线程提交的消息) - │ ├── 内存预取(相关记忆文件) - │ └── 技能发现预取 - │ - ├── maxTurns 检查 - │ └── 超过 → yield max_turns_reached + return - │ - └── state = { messages: [...old, ...assistant, ...toolResults], turnCount: +1 } - → continue ★ 回到循环顶部,发起下一次 API 调用 -``` - -### 1.6 错误处理与模型降级(第 897-956 行) - -``` -API 调用出错: - ├── FallbackTriggeredError(529 过载)? - │ ├── 切换到 fallbackModel - │ ├── 清空本轮 assistant/tool 消息 - │ ├── yield 系统消息 "Switched to X due to high demand for Y" - │ └── continue(重试整个请求) - │ - └── 其他错误 - ├── ImageSizeError/ImageResizeError → yield 友好错误 + return - ├── yieldMissingToolResultBlocks() — 补全未配对的 tool_result - └── yield API 错误消息 + return -``` - -### 1.7 关键设计思想 - -| 设计 | 说明 | -|------|------| -| **AsyncGenerator 模式** | `query()` 是 `async function*`,通过 `yield` 逐条产出事件,调用者用 `for await` 消费 | -| **while(true) + state 对象** | 每次 `continue` 构建新 State 对象,避免分散的状态修改 | -| **transition 字段** | 记录为什么要 continue(`next_turn`、`max_output_tokens_recovery`、`reactive_compact_retry`...),便于调试 | -| **StreamingToolExecutor** | API 流式返回时就并行执行工具,不等流结束 | -| **Withheld 消息** | 可恢复错误先暂扣,恢复成功则不 yield 错误,失败才 yield | - ---- - -## 2. QueryEngine.ts(1320 行)— 高层编排器 - -**文件路径**: `src/QueryEngine.ts` - -### 2.1 定位 - -QueryEngine 是 `query()` 的**上层包装**,主要用于: -- **print 模式**(`claude -p`):通过 `ask()` → `QueryEngine.submitMessage()` -- **SDK 模式**:外部程序通过 SDK 调用 -- **REPL 不用它**:REPL 直接调用 `query()` - -### 2.2 文件结构 - -``` -QueryEngine.ts (1320 行) -├── [0-130] Import 区 + feature flag 条件模块 -├── [131-174] QueryEngineConfig 类型定义 -├── [185-1202] QueryEngine 类 -│ ├── [185-208] 成员变量 + constructor -│ ├── [210-1181] submitMessage() — 核心方法(~970 行) -│ │ ├── [210-400] 参数解析 + processUserInputContext 构建 -│ │ ├── [400-465] 用户输入处理 + 会话持久化 -│ │ ├── [465-660] 斜杠命令处理 + 无需查询的快速返回 -│ │ ├── [660-690] 文件历史快照 -│ │ ├── [679-1074] ★ for await (const message of query({...})) — 消费 query() -│ │ └── [1074-1181] 结果提取 + yield result -│ ├── [1183-1202] interrupt() / getMessages() / setModel() 辅助方法 -├── [1210-1320] ask() — 便捷包装函数 -``` - -### 2.3 QueryEngineConfig - -```ts -type QueryEngineConfig = { - cwd: string // 工作目录 - tools: Tools // 工具列表 - commands: Command[] // 斜杠命令 - mcpClients: MCPServerConnection[] // MCP 服务器连接 - agents: AgentDefinition[] // Agent 定义 - canUseTool: CanUseToolFn // 权限检查 - getAppState / setAppState // 全局状态存取 - initialMessages?: Message[] // 初始消息(恢复对话) - readFileCache: FileStateCache // 文件读取缓存 - customSystemPrompt?: string // 自定义系统提示 - thinkingConfig?: ThinkingConfig // 思考模式配置 - maxTurns?: number // 最大轮次 - maxBudgetUsd?: number // USD 预算上限 - jsonSchema?: Record<...> // 结构化输出 schema - // ... 更多配置 -} -``` - -### 2.4 submitMessage() 核心流程 - -``` -submitMessage(prompt) - │ - ├── 1. 参数准备 - │ ├── 解构 config 获取 tools, commands, model, ... - │ ├── 构建 wrappedCanUseTool(包装权限检查,跟踪拒绝) - │ ├── fetchSystemPromptParts() — 获取系统提示各部分 - │ └── 构建 processUserInputContext - │ - ├── 2. 用户输入处理 - │ ├── processUserInput(prompt) — 解析斜杠命令 / 普通文本 - │ ├── mutableMessages.push(...messagesFromUserInput) - │ └── recordTranscript(messages) — 持久化到 JSONL - │ - ├── 3. yield buildSystemInitMessage() — SDK 初始化消息 - │ - ├── 4. shouldQuery === false?(斜杠命令的本地执行结果) - │ ├── yield 命令输出 - │ ├── yield { type: 'result', subtype: 'success' } - │ └── return - │ - ├── 5. ★ for await (const message of query({...})) - │ │ 消费 query() 产出的每条消息 - │ │ - │ ├── message.type === 'assistant' - │ │ ├── mutableMessages.push(msg) - │ │ ├── recordTranscript() ← fire-and-forget - │ │ ├── yield* normalizeMessage(msg) — 转换为 SDK 格式 - │ │ └── 捕获 stop_reason - │ │ - │ ├── message.type === 'user'(工具结果) - │ │ ├── mutableMessages.push(msg) - │ │ ├── turnCount++ - │ │ └── yield* normalizeMessage(msg) - │ │ - │ ├── message.type === 'stream_event' - │ │ ├── 跟踪 usage(message_start/delta/stop) - │ │ └── includePartialMessages? → yield 流事件 - │ │ - │ ├── message.type === 'system' - │ │ ├── compact_boundary → GC 旧消息 + yield 给 SDK - │ │ └── api_error → yield 重试信息 - │ │ - │ └── maxBudgetUsd 检查 → 超预算则 yield error + return - │ - └── 6. yield { type: 'result', subtype: 'success', result: textResult } -``` - -### 2.5 ask() 便捷函数(第 1211 行) - -```ts -export async function* ask({ prompt, tools, ... }) { - const engine = new QueryEngine({ ... }) - try { - yield* engine.submitMessage(prompt) - } finally { - setReadFileCache(engine.getReadFileState()) - } -} -``` - -`ask()` 是 `QueryEngine` 的一次性包装,创建 engine → 提交消息 → 清理。用于 `print.ts` 的 `--print` 模式。 - -### 2.6 QueryEngine vs REPL 直接调用 query() - -| 特性 | QueryEngine (SDK/print) | REPL 直接调用 query() | -|------|------------------------|---------------------| -| 会话持久化 | 自动 recordTranscript | 由 useLogMessages 处理 | -| Usage 跟踪 | 内部 totalUsage 累积 | 由外层 cost-tracker 处理 | -| 权限拒绝跟踪 | 记录 permissionDenials[] | 直接 UI 交互 | -| 结果格式 | yield SDKMessage 格式 | 原始 Message 格式 | -| 消息 GC | compact_boundary 后释放旧消息 | UI 需要保留完整历史 | - ---- - -## 3. claude.ts(3420 行)— API 客户端 - -**文件路径**: `src/services/api/claude.ts` - -### 3.1 文件结构 - -``` -claude.ts (3420 行) -├── [0-260] Import 区(大量 SDK 类型、工具函数) -├── [272-331] getExtraBodyParams() — 构建额外请求体参数 -├── [333-502] 缓存相关(getPromptCachingEnabled, getCacheControl, should1hCacheTTL, configureEffortParams, configureTaskBudgetParams) -├── [504-587] verifyApiKey() — API 密钥验证 -├── [589-675] 消息转换(userMessageToMessageParam, assistantMessageToMessageParam) -├── [677-708] Options 类型定义 -├── [710-781] queryModelWithoutStreaming / queryModelWithStreaming — 公开的两个入口 -├── [783-813] 辅助函数(shouldDeferLspTool, getNonstreamingFallbackTimeoutMs) -├── [819-918] executeNonStreamingRequest() — 非流式请求辅助 -├── [920-999] 更多辅助函数(getPreviousRequestIdFromMessages, stripExcessMediaItems) -├── [1018-3420] ★ queryModel() — 核心私有函数(2400 行) -│ ├── [1018-1370] 前置检查 + 工具 schema 构建 + 消息归一化 + 系统提示组装 -│ ├── [1539-1730] paramsFromContext() — 构建 API 请求参数 -│ ├── [1777-2100] withRetry + 流式 API 调用(anthropic.beta.messages.create + stream) -│ ├── [1941-2300] 流式事件处理(for await of stream) -│ └── [2300-3420] 非流式降级 + 日志、分析、清理 -``` - -### 3.2 两个公开入口 - -```ts -// 入口 1:流式(主要路径) -export async function* queryModelWithStreaming({ - messages, systemPrompt, thinkingConfig, tools, signal, options -}) { - yield* withStreamingVCR(messages, async function* () { - yield* queryModel(messages, systemPrompt, thinkingConfig, tools, signal, options) - }) -} - -// 入口 2:非流式(compact 等内部用途) -export async function queryModelWithoutStreaming({ - messages, systemPrompt, thinkingConfig, tools, signal, options -}) { - let assistantMessage - for await (const message of ...) { - if (message.type === 'assistant') assistantMessage = message - } - return assistantMessage -} -``` - -两者都委托给内部的 `queryModel()`。`withStreamingVCR` 是一个 VCR(录像/回放)包装器,用于调试。 - -### 3.3 Options 类型(第 677 行) - -```ts -type Options = { - getToolPermissionContext: () => Promise - model: string // 模型名称 - toolChoice?: BetaToolChoiceTool // 强制使用特定工具 - isNonInteractiveSession: boolean // 是否非交互模式 - fallbackModel?: string // 备用模型 - querySource: QuerySource // 查询来源 - agents: AgentDefinition[] // Agent 定义 - enablePromptCaching?: boolean // 启用提示缓存 - effortValue?: EffortValue // 推理努力级别 - mcpTools: Tools // MCP 工具 - fastMode?: boolean // 快速模式 - taskBudget?: { total: number; remaining?: number } // 令牌预算 -} -``` - -### 3.4 queryModel() 核心流程(第 1018 行) - -这是整个 API 调用的核心,2400 行。关键步骤: - -#### 阶段 1:前置准备(1018-1400 行) - -``` -queryModel() - ├── off-switch 检查(Opus 过载时的全局关闭开关) - ├── beta headers 组装(getMergedBetas) - │ ├── 基础 betas - │ ├── advisor beta(如果启用) - │ ├── tool search beta(如果启用) - │ ├── cache scope beta - │ └── effort / task budget betas - │ - ├── 工具过滤 - │ ├── tool search 启用 → 只包含已发现的 deferred tools - │ └── tool search 未启用 → 过滤掉 ToolSearchTool - │ - ├── toolToAPISchema() — 每个工具转为 API 格式 - │ - ├── normalizeMessagesForAPI() — 消息转换为 API 格式 - │ ├── UserMessage → { role: 'user', content: ... } - │ ├── AssistantMessage → { role: 'assistant', content: ... } - │ └── 跳过 system/attachment/progress 等内部消息类型 - │ - └── 系统提示最终组装 - ├── getAttributionHeader(fingerprint) - ├── getCLISyspromptPrefix() - ├── ...systemPrompt - └── advisor 指令(如果启用) -``` - -#### 阶段 2:构建请求参数 — paramsFromContext()(第 1539-1730 行) - -```ts -const paramsFromContext = (retryContext: RetryContext) => { - // ... 动态 beta headers、effort、task budget 配置 ... - - // 思考模式配置(adaptive 或 enabled + budget) - let thinking = undefined - if (hasThinking && modelSupportsThinking(options.model)) { - if (modelSupportsAdaptiveThinking(options.model)) { - thinking = { type: 'adaptive' } - } else { - thinking = { type: 'enabled', budget_tokens: thinkingBudget } - } - } - - return { - model: normalizeModelStringForAPI(options.model), - messages: addCacheBreakpoints(messagesForAPI, ...), // 带缓存标记的消息 - system, // 系统提示块(已构建好) - tools: allTools, // 工具 schema - tool_choice: options.toolChoice, - max_tokens: maxOutputTokens, - thinking, - ...(temperature !== undefined && { temperature }), - ...(useBetas && { betas: betasParams }), - metadata: getAPIMetadata(), - ...extraBodyParams, - ...(speed !== undefined && { speed }), // 快速模式 - } -} -``` - -#### 阶段 3:流式 API 调用(第 1779-1858 行) - -```ts -// 使用 withRetry 包装,自动处理重试 -const generator = withRetry( - () => getAnthropicClient({ maxRetries: 0, model, source: querySource }), - async (anthropic, attempt, context) => { - const params = paramsFromContext(context) - - // ★ 核心 API 调用(第 1823 行) - // 使用 .create() + stream: true(而非 .stream()) - // 避免 BetaMessageStream 的 O(n²) partial JSON 解析开销 - const result = await anthropic.beta.messages - .create( - { ...params, stream: true }, - { signal, ...(clientRequestId && { headers: { ... } }) }, - ) - .withResponse() - - return result.data // Stream - }, - { model, fallbackModel, thinkingConfig, signal, querySource } -) - -// 消费 withRetry 的系统错误消息(重试通知等) -let e -do { - e = await generator.next() - if (!('controller' in e.value)) yield e.value // yield API 错误消息 -} while (!e.done) -stream = e.value // 获取最终的 Stream 对象 - -// 处理流式事件(第 1941 行) -for await (const part of stream) { - switch (part.type) { - case 'message_start': // 记录 request_id、usage - case 'content_block_start': // 新的内容块开始(text/thinking/tool_use) - case 'content_block_delta': // 增量内容 → yield stream_event 给 UI - case 'content_block_stop': // 内容块完成 → yield AssistantMessage - case 'message_delta': // stop_reason、usage 更新 - case 'message_stop': // 整条消息完成 - } -} -``` - -#### 阶段 4:withRetry 重试策略 - -``` -withRetry 逻辑: - ├── 429 (Rate Limit) → 等待 Retry-After 后重试 - ├── 529 (Overloaded) → 切换到 fallbackModel,throw FallbackTriggeredError - ├── 500 (Server Error) → 指数退避重试 - ├── 408 (Timeout) → 重试 - ├── 其他错误 → 不重试,直接抛出 - └── 最大重试次数: 根据模型和错误类型动态计算 -``` - -#### 阶段 5:非流式降级 - -当流式请求中途失败时,可能降级为非流式请求: - -``` -流式失败(部分响应已收到): - ├── 已接收的内容 → yield 给上层 - ├── 剩余部分 → 降级为非流式请求(anthropic.beta.messages.create) - └── 非流式结果 → 转换格式 yield -``` - -### 3.5 消息转换函数 - -```ts -// UserMessage → API 格式 -userMessageToMessageParam(message, addCache, enablePromptCaching, querySource) - → { role: 'user', content: [...] } - // addCache=true 时最后一个 content block 添加 cache_control - -// AssistantMessage → API 格式 -assistantMessageToMessageParam(message, addCache, enablePromptCaching, querySource) - → { role: 'assistant', content: [...] } - // thinking/redacted_thinking 块不加 cache_control -``` - -### 3.6 Prompt Caching 策略 - -``` -缓存策略: - ├── cache_control: { type: 'ephemeral' } — 默认,5 分钟 TTL - ├── cache_control: { type: 'ephemeral', ttl: '1h' } — 订阅用户/Ant,1 小时 - ├── cache_control: { ..., scope: 'global' } — 跨会话共享(无 MCP 工具时) - └── 禁用条件: - ├── DISABLE_PROMPT_CACHING 环境变量 - ├── DISABLE_PROMPT_CACHING_HAIKU(仅 Haiku) - └── DISABLE_PROMPT_CACHING_SONNET(仅 Sonnet) -``` - -### 3.7 多 Provider 支持 - -`getAnthropicClient()` 根据配置返回不同的 SDK 客户端: - -| Provider | 入口 | 说明 | -|----------|------|------| -| Anthropic | 直接 API | 默认,`api.anthropic.com` | -| AWS Bedrock | 通过 Bedrock | 使用 `@anthropic-ai/bedrock-sdk` | -| Google Vertex | 通过 Vertex | 使用 `@anthropic-ai/vertex-sdk` | -| Azure | 通过 Azure | 类似 Bedrock 的包装 | - -Provider 选择逻辑在 `src/utils/model/providers.ts` 的 `getAPIProvider()` 中。 - ---- - -## 完整数据流:一次工具调用的生命周期 - -以用户输入 "读取 README.md" 为例: - -``` -1. REPL.tsx: 用户按回车 - onSubmit("读取 README.md") - └── handlePromptSubmit() - └── onQuery([userMessage]) - -2. REPL.tsx: onQueryImpl() - ├── getSystemPrompt() + getUserContext() + getSystemContext() - └── for await (event of query({messages, systemPrompt, ...})) - -3. query.ts: queryLoop() — 第 1 次迭代 - ├── messagesForQuery = [...messages] // 包含用户消息 - ├── deps.callModel({...}) - │ └── claude.ts: queryModel() - │ ├── 构建 API 参数 - │ └── anthropic.beta.messages.create({ ...params, stream: true }) - │ - ├── API 流式返回: - │ content_block_start: { type: 'tool_use', name: 'Read', id: 'toolu_123' } - │ content_block_delta: { input: '{"file_path": "/path/to/README.md"}' } - │ content_block_stop - │ message_delta: { stop_reason: 'tool_use' } - │ - ├── 收集: toolUseBlocks = [{ name: 'Read', id: 'toolu_123', input: {...} }] - ├── needsFollowUp = true - │ - ├── 工具执行: - │ streamingToolExecutor.getRemainingResults() - │ └── Read 工具执行 → 返回文件内容 - │ yield toolResultMessage ← 包含文件内容 - │ - └── state = { messages: [...old, assistantMsg, toolResultMsg], turnCount: 2 } - → continue - -4. query.ts: queryLoop() — 第 2 次迭代 - ├── messagesForQuery 现在包含: - │ [userMsg, assistantMsg(tool_use), userMsg(tool_result)] - │ - ├── deps.callModel({...}) ← 再次调用 API - │ - ├── API 返回: - │ content_block_start: { type: 'text' } - │ content_block_delta: { text: "README.md 的内容是..." } - │ content_block_stop - │ message_delta: { stop_reason: 'end_turn' } - │ - ├── toolUseBlocks = [] ← 没有工具调用 - ├── needsFollowUp = false - │ - └── return { reason: 'completed' } ★ 循环结束 - -5. REPL.tsx: onQueryEvent(event) - ├── 更新 streamingText(打字机效果) - ├── 更新 messages 数组 - └── 重新渲染 UI -``` - ---- - -## 关键设计模式总结 - -| 模式 | 位置 | 说明 | -|------|------|------| -| AsyncGenerator 链式传递 | query.ts → claude.ts | `yield*` 将底层事件透传给上层,形成事件流管道 | -| while(true) + State 对象 | query.ts queryLoop | 循环迭代间通过不可变 State 传递,transition 字段记录原因 | -| StreamingToolExecutor | query.ts | API 流式返回时并行执行工具,不等流结束 | -| Withheld 消息 | query.ts | 可恢复错误先暂扣不 yield,恢复成功则吞掉错误 | -| withRetry 重试 | claude.ts | 429/500/529 自动重试,529 触发模型降级 | -| Prompt Caching | claude.ts | 缓存系统提示和历史消息,减少 API token 消耗 | -| 非流式降级 | claude.ts | 流式请求中途失败时降级为非流式完成剩余部分 | -| QueryEngine 包装 | QueryEngine.ts | 为 SDK/print 提供会话管理、持久化、usage 跟踪 | - -## 需要忽略的代码 - -| 模式 | 说明 | -|------|------| -| `feature('REACTIVE_COMPACT')` / `feature('CONTEXT_COLLAPSE')` 等 | 所有 feature flag 保护的代码 — 全部是死代码 | -| `feature('CACHED_MICROCOMPACT')` | 缓存微压缩 — 死代码 | -| `feature('HISTORY_SNIP')` / `snipModule` | 历史截断 — 死代码 | -| `feature('TOKEN_BUDGET')` / `budgetTracker` | 令牌预算 — 死代码 | -| `feature('BG_SESSIONS')` / `taskSummaryModule` | 后台会话 — 死代码 | -| `process.env.USER_TYPE === 'ant'` | Anthropic 内部专用代码 | -| VCR (withStreamingVCR/withVCR) | 调试录像/回放包装器,不影响正常流程 | \ No newline at end of file diff --git a/learn/phase-2-qa.md b/learn/phase-2-qa.md deleted file mode 100644 index 8ebe71d8a..000000000 --- a/learn/phase-2-qa.md +++ /dev/null @@ -1,372 +0,0 @@ -# 第二阶段 Q&A - -## Q1:query.ts 的流式消息处理具体是怎样的? - -**核心问题**:`deps.callModel()` yield 出的每一条消息,在 `queryLoop()` 的 `for await` 循环体(L659-866)中具体经历了什么处理? - -### 场景 - -用户说:**"帮我看看 package.json 的内容"** - -模型回复:一段文字 "我来读取文件。" + 一个 Read 工具调用。 - -### callModel yield 的完整消息序列 - -claude.ts 的 `queryModel()` 会 yield 两种类型的消息: - -| 类型标记 | 含义 | 产出时机 | -|---------|------|---------| -| `stream_event` | 原始 SSE 事件包装 | 每个 SSE 事件都产出一条 | -| `assistant` | 完整的 AssistantMessage | 仅在 `content_block_stop` 时产出 | - -本例中 callModel 依次 yield **共 13 条消息**: - -``` -#1 { type: 'stream_event', event: { type: 'message_start', ... }, ttftMs: 342 } -#2 { type: 'stream_event', event: { type: 'content_block_start', index: 0, content_block: { type: 'text' } } } -#3 { type: 'stream_event', event: { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: '我来' } } } -#4 { type: 'stream_event', event: { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: '读取文件。' } } } -#5 { type: 'stream_event', event: { type: 'content_block_stop', index: 0 } } -#6 { type: 'assistant', uuid: 'uuid-1', message: { content: [{ type: 'text', text: '我来读取文件。' }], stop_reason: null } } -#7 { type: 'stream_event', event: { type: 'content_block_start', index: 1, content_block: { type: 'tool_use', id: 'toolu_001', name: 'Read' } } } -#8 { type: 'stream_event', event: { type: 'content_block_delta', index: 1, delta: { type: 'input_json_delta', partial_json: '{"file_path":' } } } -#9 { type: 'stream_event', event: { type: 'content_block_delta', index: 1, delta: { type: 'input_json_delta', partial_json: '"/path/package.json"}' } } } -#10 { type: 'stream_event', event: { type: 'content_block_stop', index: 1 } } -#11 { type: 'assistant', uuid: 'uuid-2', message: { content: [{ type: 'tool_use', id: 'toolu_001', name: 'Read', input: { file_path: '/path/package.json' } }], stop_reason: null } } -#12 { type: 'stream_event', event: { type: 'message_delta', delta: { stop_reason: 'tool_use' }, usage: { output_tokens: 87 } } } -#13 { type: 'stream_event', event: { type: 'message_stop' } } -``` - -注意 `#6` 和 `#11` 是 **assistant 类型**(content_block_stop 时由 claude.ts 组装),其余全是 **stream_event 类型**。 - -### 循环体结构 - -循环体在 L708-866,结构如下: - -``` -for await (const message of deps.callModel({...})) { // L659 - // A. 降级检查 (L712) - // B. backfill (L747-789) - // C. withheld 检查 (L801-824) - // D. yield (L825-827) - // E. assistant 收集 + addTool (L828-848) - // F. getCompletedResults (L850-865) -} -``` - -### 逐条走循环体 - -#### #1 stream_event (message_start) - -``` -A. L712: streamingFallbackOccured = false → 跳过 - -B. L748: message.type === 'assistant'? - → 'stream_event' !== 'assistant' → 跳过整个 backfill 块 - -C. L801-824: withheld 检查 - → 不是 assistant 类型,各项检查均为 false → withheld = false - -D. L825: yield message ✅ → 透传给 REPL(REPL 记录 ttftMs) - -E. L828: message.type === 'assistant'? → 否 → 跳过 - -F. L850-854: streamingToolExecutor.getCompletedResults() - → tools 数组为空 → 无结果 -``` - -**净效果**:`yield` 透传。 - ---- - -#### #2 stream_event (content_block_start, type: text) - -``` -A-C. 同 #1 -D. yield message ✅ → REPL 设置 spinner 为 "Responding..." -E-F. 同 #1 -``` - -**净效果**:`yield` 透传。 - ---- - -#### #3 stream_event (text_delta: "我来") - -``` -A-C. 同 #1 -D. yield message ✅ → REPL 追加 streamingText += "我来"(打字机效果) -E-F. 同 #1 -``` - -**净效果**:`yield` 透传。 - ---- - -#### #4 stream_event (text_delta: "读取文件。") - -``` -同 #3 -D. yield message ✅ → REPL streamingText += "读取文件。" -``` - -**净效果**:`yield` 透传。 - ---- - -#### #5 stream_event (content_block_stop, index:0) - -``` -同 #2 -D. yield message ✅ → REPL 无特殊操作(真正的 AssistantMessage 在下一条 #6) -``` - -**净效果**:`yield` 透传。 - ---- - -#### #6 assistant (text block 完整消息) ★ - -第一条 `type: 'assistant'` 的消息,走**完全不同的路径**: - -``` -A. L712: streamingFallbackOccured = false → 跳过 - -B. L748: message.type === 'assistant'? → ✅ 进入 backfill - L750: contentArr = [{ type: 'text', text: '我来读取文件。' }] - L752: for i=0: block.type === 'text' - L754: block.type === 'tool_use'? → 否 → 跳过 - L783: clonedContent 为 undefined → yieldMessage = message(原样不变) - -C. L801: let withheld = false - L802: feature('CONTEXT_COLLAPSE') → false → 跳过 - L813: reactiveCompact?.isWithheldPromptTooLong(message) → 否 → false - L822: isWithheldMaxOutputTokens(message) - → message.message.stop_reason === null → false - → withheld = false - -D. L825: yield message ✅ → REPL 清除 streamingText,添加完整 text 消息到列表 - -E. L828: message.type === 'assistant'? → ✅ - L830: assistantMessages.push(message) - → assistantMessages = [uuid-1(text)] - - L832-834: msgToolUseBlocks = content.filter(type === 'tool_use') - → [](这是 text block,没有 tool_use) - - L835: length > 0? → 否 → 不设 needsFollowUp - L844: msgToolUseBlocks 为空 → 不调用 addTool - -F. L854: getCompletedResults() → 空 -``` - -**净效果**:`yield` 消息 + `assistantMessages` 增加一条。`needsFollowUp` 仍为 `false`。 - ---- - -#### #7 stream_event (content_block_start, tool_use: Read) - -``` -A-C. 同 stream_event 通用路径 -D. yield message ✅ → REPL 设置 spinner 为 "tool-input",添加 streamingToolUse -E. 不是 assistant → 跳过 -F. getCompletedResults() → 空 -``` - ---- - -#### #8 stream_event (input_json_delta: `'{"file_path":'`) - -``` -D. yield message ✅ → REPL 追加工具输入 JSON 碎片 -F. getCompletedResults() → 空 -``` - ---- - -#### #9 stream_event (input_json_delta: '"/path/package.json"}') - -``` -D. yield message ✅ -F. getCompletedResults() → 空 -``` - ---- - -#### #10 stream_event (content_block_stop, index:1) - -``` -D. yield message ✅ -F. getCompletedResults() → 空 -``` - ---- - -#### #11 assistant (tool_use block 完整消息) ★★ - -这条是**最关键的**——触发工具执行: - -``` -A. L712: streamingFallbackOccured = false → 跳过 - -B. L748: message.type === 'assistant'? → ✅ 进入 backfill - L750: contentArr = [{ type: 'tool_use', id: 'toolu_001', name: 'Read', - input: { file_path: '/path/package.json' } }] - L752: for i=0: - L754: block.type === 'tool_use'? → ✅ - L756: typeof block.input === 'object' && !== null? → ✅ - L759: tool = findToolByName(tools, 'Read') → Read 工具定义 - L763: tool.backfillObservableInput 存在? → 假设存在 - L764-766: inputCopy = { file_path: '/path/package.json' } - tool.backfillObservableInput(inputCopy) - → 可能添加 absolutePath 字段 - L773-776: addedFields? → 假设有新增字段 - clonedContent = [...contentArr] - clonedContent[0] = { ...block, input: inputCopy } - L783-788: yieldMessage = { - ...message, // uuid, type, timestamp 不变 - message: { - ...message.message, // stop_reason, usage 不变 - content: clonedContent // ★ 替换为带 absolutePath 的副本 - } - } - // ★ 原始 message 保持不变(回传 API 保证缓存一致) - -C. L801-824: withheld 检查 → 全部 false → withheld = false - -D. L825: yield yieldMessage ✅ - → yield 的是克隆版(带 backfill 字段),给 REPL 和 SDK 用 - → 原始 message 下面存进 assistantMessages,回传 API 保证缓存一致 - -E. L828: message.type === 'assistant'? → ✅ - L830: assistantMessages.push(message) // ★ push 原始 message,不是 yieldMessage - → assistantMessages = [uuid-1(text), uuid-2(tool_use)] - - L832-834: msgToolUseBlocks = content.filter(type === 'tool_use') - → [{ type: 'tool_use', id: 'toolu_001', name: 'Read', input: {...} }] - - L835: length > 0? → ✅ - L836: toolUseBlocks.push(...msgToolUseBlocks) - → toolUseBlocks = [Read_block] - L837: needsFollowUp = true // ★★★ 决定 while(true) 不会终止 - - L840-842: streamingToolExecutor 存在 ✓ && !aborted ✓ - L844-846: for (const toolBlock of msgToolUseBlocks): - streamingToolExecutor.addTool(Read_block, uuid-2消息) - // ★★★ 工具开始执行! - // → StreamingToolExecutor 内部: - // isConcurrencySafe = true(Read 是安全的) - // queued → processQueue() → canExecuteTool() → true - // → executeTool() → runToolUse() → 后台异步读文件 - -F. L850-854: getCompletedResults() - → Read 刚开始执行,status = 'executing' → 无完成结果 -``` - -**净效果**: -- `yield` 克隆消息(带 backfill 字段) -- `assistantMessages` push 原始消息 -- `needsFollowUp = true` -- **Read 工具在后台异步开始执行** - ---- - -#### #12 stream_event (message_delta, stop_reason: 'tool_use') - -``` -A-C. 同 stream_event 通用路径 -D. yield message ✅ - -E. 不是 assistant → 跳过 - -F. L854: getCompletedResults() - → ★ 此时 Read 可能已经完成了!(读文件通常 <1ms) - → 如果完成: status = 'completed', results 有值 - L428(StreamingToolExecutor): tool.status = 'yielded' - L431-432: yield { message: UserMsg(tool_result) } - → 回到 query.ts: - L855: result.message 存在 - L856: yield result.message ✅ → REPL 显示工具结果 - L857-862: toolResults.push(normalizeMessagesForAPI([result.message])...) - → toolResults = [Read 的 tool_result] -``` - -**净效果**:`yield` stream_event + **可能 yield 工具结果**(如果工具已完成)。 - ---- - -#### #13 stream_event (message_stop) - -``` -D. yield message ✅ -F. getCompletedResults() - → 如果 Read 在 #12 已被收割 → 空 - → 如果 Read 此时才完成 → yield 工具结果(同 #12 的 F 逻辑) -``` - ---- - -### for await 循环退出后 - -``` -L1018: aborted? → false → 跳过 - -L1065: if (!needsFollowUp) - → needsFollowUp = true → 不进入 → 跳过终止逻辑 - -L1383: toolUpdates = streamingToolExecutor.getRemainingResults() - → 如果 Read 已在 #12/#13 被收割 → 立即返回空 - → 如果 Read 还没完成 → 阻塞等待 → 完成后 yield 结果 - -L1387-1404: for await (const update of toolUpdates) { - yield update.message → REPL 显示 - toolResults.push(...) → 收集 - } - -L1718-1730: 构建 next State: - state = { - messages: [ - ...messagesForQuery, // [UserMessage("帮我看看...")] - ...assistantMessages, // [AssistantMsg(text), AssistantMsg(tool_use)] - ...toolResults, // [UserMsg(tool_result)] - ], - turnCount: 1, - transition: { reason: 'next_turn' }, - } - → continue → while(true) 第 2 次迭代 → 带着工具结果再次调 API -``` - -### 循环体判定树总结 - -``` -for await (const message of deps.callModel(...)) { - │ - ├─ message.type === 'stream_event'? - │ │ - │ └─ YES → 几乎零操作 - │ ├─ yield message(透传给 REPL 做实时 UI) - │ └─ getCompletedResults()(顺便检查有没有完成的工具) - │ - └─ message.type === 'assistant'? - │ - ├─ B. backfill: 有 tool_use + backfillObservableInput? - │ ├─ YES → 克隆消息,yield 克隆版(原始消息保留给 API) - │ └─ NO → yield 原始消息 - │ - ├─ C. withheld: prompt_too_long / max_output_tokens? - │ ├─ YES → 不 yield(暂扣,等后面恢复逻辑处理) - │ └─ NO → yield - │ - ├─ E. assistantMessages.push(原始 message) - │ - ├─ E. 有 tool_use block? - │ ├─ YES → toolUseBlocks.push() - │ │ + needsFollowUp = true - │ │ + streamingToolExecutor.addTool() → ★ 立即开始执行工具 - │ └─ NO → 什么都不做 - │ - └─ F. getCompletedResults() → 收割已完成的工具结果 -} -``` - -**一句话总结**:stream_event 透传不处理;assistant 消息才是"真正的货"——收集起来、判断要不要暂扣、有工具就立即开始执行、顺便收割已完成的工具结果。 \ No newline at end of file