chenbhao commited on
Commit
f468102
·
1 Parent(s): eeeb2b6

chore: docs v1.1

Browse files
docs/README.md CHANGED
@@ -9,11 +9,17 @@
9
  |------|------|
10
  | [项目架构概览](architecture/overview.md) | 技术栈、入口流程、模块职责、架构亮点 |
11
  | [核心数据流](architecture/data-flow.md) | 5 大核心数据流:用户输入、语音捕获、远程桥接、Friend 表情、Provider 代理 |
 
 
 
 
 
12
 
13
  ### CLI 与工具
14
  | 文档 | 说明 |
15
  |------|------|
16
  | [CLI 命令系统](cli/overview.md) | 命令注册、Slash 命令大全 (~75+)、Skill/工作流系统 |
 
17
  | [AI 工具系统](tools/overview.md) | buildTool 框架、执行流程、权限系统、关键工具详解 |
18
  | [工具参考大全](tools/tool-reference.md) | 所有 ~60+ AI 工具的完整参考表 |
19
 
@@ -47,6 +53,7 @@
47
  | 文档 | 说明 |
48
  |------|------|
49
  | [服务总览](services/overview.md) | MCP、上下文压缩、Auto Dream、飞书/Telegram |
 
50
 
51
  ### 协调与自动化
52
  | 文档 | 说明 |
@@ -64,3 +71,8 @@
64
  |------|------|
65
  | [自动记忆系统](memory-context/memory.md) | Memdir、4 种记忆类型、生命周期 |
66
  | [上下文管理](memory-context/context.md) | 系统/用户上下文构建、压缩策略、React Contexts |
 
 
 
 
 
 
9
  |------|------|
10
  | [项目架构概览](architecture/overview.md) | 技术栈、入口流程、模块职责、架构亮点 |
11
  | [核心数据流](architecture/data-flow.md) | 5 大核心数据流:用户输入、语音捕获、远程桥接、Friend 表情、Provider 代理 |
12
+ | [Agent 循环深度解析](architecture/agent-loop.md) | 663 行 | queryLoop 完整流程、StreamingToolExecutor、5 阶段 turn pipeline、恢复机制 |
13
+ | [设计哲学与架构原则](architecture/design-philosophy.md) | 329 行 | 5 大价值、13 设计原则、与 Claude Code arXiv paper 的映射 |
14
+ | [安全与权限系统](architecture/safety-and-permissions.md) | 867 行 | 7 种权限模式、Deny-first 规则引擎、Auto-mode ML 分类器、授权流水线 |
15
+ | [Provider 多厂商认证](architecture/provider-auth.md) | 646 行 | OAuth PKCE、Fetch Override、Anthropic↔OpenAI 协议转换、NVIDIA NIM |
16
+ | [跨切面关注点](architecture/cross-cutting.md) | 254 行 | 错误处理层次、遥测系统、性能优化策略 |
17
 
18
  ### CLI 与工具
19
  | 文档 | 说明 |
20
  |------|------|
21
  | [CLI 命令系统](cli/overview.md) | 命令注册、Slash 命令大全 (~75+)、Skill/工作流系统 |
22
+ | [构建系统与功能标记](cli/build-system.md) | 676 行 | Bun 构建管道、48 个 feature flag、死代码消除、命令可用性门控 |
23
  | [AI 工具系统](tools/overview.md) | buildTool 框架、执行流程、权限系统、关键工具详解 |
24
  | [工具参考大全](tools/tool-reference.md) | 所有 ~60+ AI 工具的完整参考表 |
25
 
 
53
  | 文档 | 说明 |
54
  |------|------|
55
  | [服务总览](services/overview.md) | MCP、上下文压缩、Auto Dream、飞书/Telegram |
56
+ | [上下文压缩深度解析](services/compact-deep-dive.md) | 906 行 | 5 层压缩管线、Budget Reduction、Snip、Microcompact、Context Collapse、Auto-compact |
57
 
58
  ### 协调与自动化
59
  | 文档 | 说明 |
 
71
  |------|------|
72
  | [自动记忆系统](memory-context/memory.md) | Memdir、4 种记忆类型、生命周期 |
73
  | [上下文管理](memory-context/context.md) | 系统/用户上下文构建、压缩策略、React Contexts |
74
+
75
+ ### 面试准备
76
+ | 文档 | 说明 |
77
+ |------|------|
78
+ | [面试准备指南](interview-prep.md) | 714 行 | 高频面试题、源码级解析、架构对比、设计权衡 |
docs/architecture/agent-loop.md ADDED
@@ -0,0 +1,663 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Agent 循环与查询引擎深度分析
2
+
3
+ > 本文基于 `src/query.ts`、`src/QueryEngine.ts`、`src/query/*`、`src/services/tools/*` 等核心模块,
4
+ > 详细阐述 VersperClaw (Claude Code) 的 Agent 循环架构、模型调用管道、工具调度机制、恢复策略与继续决策逻辑。
5
+ > 文中列出具体文件路径、函数名称与行号,供开发者快速定位代码。
6
+
7
+ ---
8
+
9
+ ## 目录
10
+
11
+ 1. [主循环架构 (queryLoop)](#1-主循环架构-queryloop)
12
+ 2. [模型调用管道](#2-模型调用管道)
13
+ 3. [工具调度与流式执行](#3-工具调度与流式执行)
14
+ 4. [Stop Hooks (后处理管道)](#4-stop-hooks-后处理管道)
15
+ 5. [继续决策](#5-继续决策)
16
+ 6. [恢复机制](#6-恢复机制)
17
+ 7. [QueryEngine.ts 的角色](#7-queryenginets-的角色)
18
+ 8. [工具执行引擎](#8-工具执行引擎)
19
+ 9. [参考文件索引](#9-参考文件索引)
20
+
21
+ ---
22
+
23
+ ## 1. 主循环架构 (queryLoop)
24
+
25
+ ### 1.1 概述
26
+
27
+ `queryLoop()` 是整个 Agent 的核心,它是一个 `AsyncGenerator`,运行在 `query()` 函数内部的 `while (true)` 无限循环中。每次迭代代表一个 **turn**(回合),包括:输入处理、模型调用、工具执行、后处理钩子、继续决策。
28
+
29
+ ```
30
+ ┌─────────────────────────────────────────────────────────────────┐
31
+ │ queryLoop (AsyncGenerator) │
32
+ │ │
33
+ │ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
34
+ │ │ Pre-model │──>│ Model │──>│ Tool │──>│ Stop │──> │
35
+ │ │ Context │ │Invocation│ │Dispatch │ │ Hooks │ │
36
+ │ │ Shaping │ │(Stream) │ │& Exec │ │(Post-turn)│ │
37
+ │ └──────────┘ └──────────┘ └──────────┘ └──────────┘ │
38
+ │ │ │ │ │ │
39
+ │ │ │ │ │ │
40
+ │ └──────────────┴──────────────┴──────────────┘ │
41
+ │ │ │
42
+ │ ┌─────▼──────┐ │
43
+ │ │ Continuation│ │
44
+ │ │ Decision │──> continue (loop again) │
45
+ │ └─────┬──────┘ │
46
+ │ │ stop │
47
+ │ ▼ │
48
+ │ return Terminal │
49
+ └─────────────────────────────────────────────────────────────────┘
50
+ ```
51
+
52
+ **文件**: `src/query.ts`
53
+ - `query()` 函数: 第 219-239 行 — 公共入口,封装 queryLoop 并处理 consumedCommandUuids
54
+ - `queryLoop()` 函数: 第 241-1729 行 — 主循环体
55
+ - `QueryParams` 类型: 第 181-199 行 — 循环的输入参数
56
+ - `State` 类型: 第 204-217 行 — 跨迭代的可变状态
57
+
58
+ ### 1.2 五种核心阶段
59
+
60
+ 每一个 turn 循环包含以下阶段:
61
+
62
+ #### 阶段 A: Pre-model Context Shaping (第 365-548 行)
63
+
64
+ 在调用模型之前,对消息列表进行一系列上下文压缩和优化:
65
+
66
+ 1. **Tool Result Budget** (第 376-394 行): `applyToolResultBudget()` 限制每个消息中 tool_result 的总大小,防止工具输出膨胀。
67
+ 2. **Snip Compact** (第 400-410 行): `snipCompactIfNeeded()` — 被 `HISTORY_SNIP` 特性门控,裁剪历史消息中的冗余内容。
68
+ 3. **Microcompact** (第 413-426 行): `deps.microcompact()` — 对连续工具结果进行微压缩,减小上下文体积。
69
+ 4. **Context Collapse** (第 440-447 行): `applyCollapsesIfNeeded()` — 被 `CONTEXT_COLLAPSE` 门控,对历史消息进行投影式折叠。
70
+ 5. **Auto-compact** (第 454-543 行): `deps.autocompact()` — 全自动上下文压缩,当 Token 数超过阈值时触发。
71
+ 6. **Blocking Limit Check** (第 628-648 行): 计算是否已达到硬性阻塞限制,阻止 API 调用并返回 `blocking_limit`。
72
+
73
+ #### 阶段 B: Model Invocation (第 652-863 行)
74
+
75
+ 见第 2 节详细分析。
76
+
77
+ #### 阶段 C: Tool Dispatch & Execution (第 1360-1409 行)
78
+
79
+ - 如果启用 `StreamingToolExecutor`,使用 `getRemainingResults()` 处理流式工具结果
80
+ - 否则使用 `runTools()` 执行工具(通过 `toolOrchestration.ts`)
81
+ - 工具结果被收集到 `toolResults` 数组中
82
+
83
+ #### 阶段 D: Stop Hooks (第 1267-1306 行)
84
+
85
+ `handleStopHooks()` 处理后处理管道(见第 4 节)。
86
+
87
+ #### 阶段 E: Continuation Decision (第 1308-1357 行)
88
+
89
+ 根据 token 预算、stop hooks 结果等决定是否继续循环。
90
+
91
+ ### 1.3 状态管理
92
+
93
+ `queryLoop` 使用 `State` 类型(第 204-217 行)管理跨迭代的可变状态:
94
+
95
+ ```typescript
96
+ type State = {
97
+ messages: Message[]
98
+ toolUseContext: ToolUseContext
99
+ autoCompactTracking: AutoCompactTrackingState | undefined
100
+ maxOutputTokensRecoveryCount: number
101
+ hasAttemptedReactiveCompact: boolean
102
+ maxOutputTokensOverride: number | undefined
103
+ pendingToolUseSummary: Promise<ToolUseSummaryMessage | null> | undefined
104
+ stopHookActive: boolean | undefined
105
+ turnCount: number
106
+ transition: Continue | undefined
107
+ }
108
+ ```
109
+
110
+ 每次迭代开始时解构 state(第 311-321 行),在所有继续点(continue sites)通过 `state = { ... }`(第 1099-1116 行、第 1207-1221 行等)整体替换。
111
+
112
+ `transition` 字段记录上次迭代的继续原因,用于测试断言恢复路径是否正确触发。
113
+
114
+ ---
115
+
116
+ ## 2. 模型调用管道
117
+
118
+ ### 2.1 callModel 实现
119
+
120
+ 模型调用通过 `deps.callModel()`(第 659 行)进行,其类型为 `typeof queryModelWithStreaming`(`src/query/deps.ts` 第 23 行)。
121
+
122
+ `queryDeps.ts` (第 21-30 行) 定义了四种依赖:
123
+ - `callModel`: `typeof queryModelWithStreaming` — 流式 API 调用
124
+ - `microcompact`: `typeof microcompactMessages` — 微压缩
125
+ - `autocompact`: `typeof autoCompactIfNeeded` — 自动压缩
126
+ - `uuid`: `() => string` — UUID 生成
127
+
128
+ `productionDeps()` (第 33-39 行) 提供生产环境实现。
129
+
130
+ ### 2.2 流式事件处理
131
+
132
+ 模型输出的流式事件在 `for await (const message of deps.callModel({...}))` 循环中处理(第 659-863 行):
133
+
134
+ | 事件类型 | 处理位置 | 说明 |
135
+ |---------|---------|------|
136
+ | `text_delta` | 由 claude.ts 封装 | 文本增量 |
137
+ | `tool_use` | 第 829-845 行 | 提取 tool_use 块,推入 toolUseBlocks |
138
+ | `content_block` | 第 748-787 行 | 处理 content block,backfill tool_use input |
139
+ | `message_stop` | 第 866-892 行 | 处理缓存的微压缩边界消息 |
140
+
141
+ 关键逻辑:
142
+
143
+ - **Backfill tool_use input** (第 748-787 行): 当工具定义包含 `backfillObservableInput` 时,对 tool_use 块进行输入回填(如展开文件路径)。
144
+ - **Withhold 机制** (第 799-825 行): 可恢复的错误(prompt-too-long、max-output-tokens、media-size-error)在流中被扣留(withhold),不 yield 给调用方,直到恢复机制确认无法恢复后才暴露。
145
+ - **Streaming Fallback** (第 712-741 行): 当发生流式模型回退时,清空 previous assistant messages 和 tool results,创建新的 StreamingToolExecutor。
146
+
147
+ ### 2.3 max_output_tokens 恢复机制
148
+
149
+ 代码位置: 第 164 行、第 1188-1256 行
150
+
151
+ ```
152
+ 恢复步骤:
153
+ 1. 第1次: 设置 maxOutputTokensOverride = ESCALATED_MAX_TOKENS (64K) 重试
154
+ (第 1194-1221 行, 仅当 capEnabled 且第一次)
155
+ 2. 第2-4次: 注入恢复消息 "Output token limit hit. Resume directly..."
156
+ (第 1223-1252 行, 最多 3 次 = MAX_OUTPUT_TOKENS_RECOVERY_LIMIT)
157
+ 3. 超出限制: 暴露扣留的错误消息并返回 (第 1254-1256 行)
158
+ ```
159
+
160
+ 如果用户设置了 `CLAUDE_CODE_MAX_OUTPUT_TOKENS` 环境变量,8K→64K 的自动升级会被跳过(第 1202 行)。
161
+
162
+ ### 2.4 缓存控制
163
+
164
+ - `skipCacheWrite` 参数(第 192 行)— 传递给 API 调用选项(第 697 行),控制是否跳过缓存写入。
165
+ - `pendingCacheEdits`(第 423-425 行)— `CACHED_MICROCOMPACT` 特性门控,在 API 响应后使用实际 API 报告的 `cache_deleted_input_tokens` 生成边界消息(第 870-892 行)。
166
+
167
+ ---
168
+
169
+ ## 3. 工具调度与流式执行
170
+
171
+ ### 3.1 StreamingToolExecutor 架构
172
+
173
+ **文件**: `src/services/tools/StreamingToolExecutor.ts`
174
+
175
+ `StreamingToolExecutor` 是一个类(第 40-519 行),实现工具的流式执行调度。它在模型仍输出内容时就开始执行已到达的工具。
176
+
177
+ ```
178
+ 模型流式输出工具调用
179
+
180
+
181
+ StreamingToolExecutor.addTool()
182
+
183
+ ├── 并发安全工具(concurrency-safe)→ 并行执行
184
+ └── 非并发安全工具 → 独占执行
185
+
186
+
187
+ 收集结果 → getCompletedResults() / getRemainingResults()
188
+ ```
189
+
190
+ #### 核心数据结构
191
+
192
+ ```typescript
193
+ type TrackedTool = {
194
+ id: string
195
+ block: ToolUseBlock
196
+ assistantMessage: AssistantMessage
197
+ status: ToolStatus // 'queued' | 'executing' | 'completed' | 'yielded'
198
+ isConcurrencySafe: boolean
199
+ promise?: Promise<void>
200
+ results?: Message[]
201
+ pendingProgress: Message[]
202
+ contextModifiers?: Array<(context: ToolUseContext) => ToolUseContext>
203
+ }
204
+ ```
205
+
206
+ #### 并发控制
207
+
208
+ - **`addTool()`** (第 76-124 行): 将工具加入队列,立即触发 `processQueue()`。
209
+ - **`canExecuteTool()`** (第 129-135 行): 决定是否可以执行:
210
+ - 如果没有正在执行的工具 → 总是可以
211
+ - 如果工具是并发安全的且所有正在执行的工具也是并发安全的 → 可以
212
+ - 否则 → 阻塞
213
+ - **`processQueue()`** (第 140-150 行): 遍历队列,对每个 queued 工具检查执行条件。
214
+ - **`getCompletedResults()`** (第 412-440 行): 非阻塞收集已完成的结果,保持顺序(非并发工具会阻断后续工具的 yield)。
215
+ - **`getRemainingResults()`** (第 453-490 行): 等待所有工具完成,带进度唤醒。
216
+
217
+ #### Bash 错误级联
218
+
219
+ 当 Bash 工具失败时(第 359-363 行),`hasErrored` 被置为 `true`,兄弟工具通过 `siblingAbortController` 被取消。这防止了在 `mkdir` 失败后继续执行依赖的命令。
220
+
221
+ #### 进度通知
222
+
223
+ - **BashProgress**: Bash 工具执行的 stdout/stderr 增量更新
224
+ - **AgentProgress**: 子 agent 执行进度
225
+ - **MCPProgress**: MCP 工具执行的进度
226
+
227
+ 进度消息通过 `pendingProgress` 队列立即 yield(第 368-374 行),并通过 `progressAvailableResolve` 信号唤醒 `getRemainingResults()`。
228
+
229
+ ### 3.2 非流式工具执行 (runTools)
230
+
231
+ **文件**: `src/services/tools/toolOrchestration.ts`
232
+
233
+ 当 `StreamingToolExecutor` 未启用时,使用 `runTools()` 函数(第 19-82 行)。
234
+
235
+ #### 工具批处理分区
236
+
237
+ `partitionToolCalls()`(第 91-116 行)将工具调用分区为批次:
238
+ - **并发安全批次**: 多个工具并行执行(通过 `runToolsConcurrently()`)
239
+ - **非并发安全批次**: 单个工具串行执行(通过 `runToolsSerially()`)
240
+
241
+ 最大并发数由 `CLAUDE_CODE_MAX_TOOL_USE_CONCURRENCY` 环境变量控制,默认为 10(第 8-12 行)。
242
+
243
+ ---
244
+
245
+ ## 4. Stop Hooks (后处理管道)
246
+
247
+ **文件**: `src/query/stopHooks.ts`
248
+
249
+ ### 4.1 概要
250
+
251
+ `handleStopHooks()` 函数(第 65-473 行)在每一 turn 的模型响应结束后运行。它是一个 `AsyncGenerator`,`yield` 进度/附件消息,最终返回 `StopHookResult`。
252
+
253
+ ```typescript
254
+ type StopHookResult = {
255
+ blockingErrors: Message[] // 阻断性错误(钩子注入的消息)
256
+ preventContinuation: boolean // 是否阻止继续循环
257
+ }
258
+ ```
259
+
260
+ ### 4.2 执行流程
261
+
262
+ ```
263
+ 1. 保存 cacheSafeParams (第 96-98 行)
264
+ 2. 模板作业分类 (第 108-132 行)
265
+ 3. Prompt 建议 (第 139 行) [fire-and-forget]
266
+ 4. Memory 提取 (第 141-153 行) [fire-and-forget]
267
+ 5. Auto-dream (第 154-156 行) [fire-and-forget]
268
+ 6. MCP 清理 (第 164-173 行)
269
+ 7. executeStopHooks() (第 180 行) — 主钩子执行
270
+ 8. Teammate hooks (第 335-453 行)
271
+ ```
272
+
273
+ ### 4.3 Stop Hooks 执行
274
+
275
+ `executeStopHooks()`(第 180 行)返回一个 generator,产生进度消息和阻断错误。
276
+
277
+ 钩子结果处理(第 192-295 行):
278
+ - **进度消息** (第 201-215 行): 收集每个钩子的 `command` 和 `promptText`
279
+ - **阻断错误** (第 257-267 行): 创建 `createUserMessage({ isMeta: true })` 作为阻断消息
280
+ - **继续阻止** (第 269-280 行): 生成 `hook_stopped_continuation` 附件
281
+ - **中止检测** (第 283-294 行): 如果被中止,返回 `{ preventContinuation: true }`
282
+
283
+ ### 4.4 后台任务
284
+
285
+ | 任务 | 位置 | 说明 |
286
+ |------|------|------|
287
+ | `executePromptSuggestion()` | 第 139 行 | 生成提示建议(仅非 bare 模式) |
288
+ | `executeExtractMemories()` | 第 141-153 行 | 提取记忆(`EXTRACT_MEMORIES` 门控) |
289
+ | `executeAutoDream()` | 第 154-156 行 | 自动记忆整理 |
290
+ | `cleanupComputerUseAfterTurn()` | 第 164-173 行 | MCP 计算机使用清理(`CHICAGO_MCP` 门控) |
291
+
292
+ ### 4.5 模板作业分类
293
+
294
+ 当设置了 `CLAUDE_JOB_DIR` 环境变量时(第 110 行),`jobClassifierModule!.classifyAndWriteState()`(第 121 行)在每次 turn 后分类作业状态。加 60 秒超时(第 127-131 行)。
295
+
296
+ ### 4.6 Teammate Hooks
297
+
298
+ 在 teammate 模式下(第 335 行):
299
+
300
+ 1. **TaskCompleted hooks** (第 345-400 行): 对每个 `in_progress` 且属于当前 teammate 的任务执行 `executeTaskCompletedHooks()`。
301
+ 2. **TeammateIdle hooks** (第 402-441 行): 执行 `executeTeammateIdleHooks()`。
302
+
303
+ 这两个钩子都支持 `preventContinuation` 和 `blockingErrors`。
304
+
305
+ ### 4.7 阻断错误与继续抑制
306
+
307
+ - `blockingErrors`: 由钩子注入的系统消息,作为当前 turn 的继续输入(第 1282-1306 行)
308
+ - `preventContinuation`: 立即结束循环返回 `stop_hook_prevented`(第 1278 行)
309
+ - **错误免入死亡螺旋** (第 1260-1264 行): 当 lastMessage 是 API 错误时,跳过 stop hooks
310
+
311
+ ---
312
+
313
+ ## 5. 继续决策
314
+
315
+ **文件**: `src/query/tokenBudget.ts`
316
+
317
+ ### 5.1 checkTokenBudget
318
+
319
+ ```typescript
320
+ function checkTokenBudget(
321
+ tracker: BudgetTracker,
322
+ agentId: string | undefined,
323
+ budget: number | null, // getCurrentTurnTokenBudget()
324
+ globalTurnTokens: number, // getTurnOutputTokens()
325
+ ): TokenBudgetDecision
326
+ ```
327
+
328
+ 第 45-93 行。
329
+
330
+ ### 5.2 决策逻辑
331
+
332
+ ```
333
+ 1. 跳过条件: agentId 存在 OR budget 为 null/0 → stop (第 51-53 行)
334
+ 2. 计算使用率 pct = turnTokens / budget * 100 (第 56 行)
335
+ 3. 收益递减检测: 连续 3+ 次继续且每次增量 < 500 tokens (第 59-62 行)
336
+ 4. 如果 pct < 90% 且非收益递减 → continue (第 66-75 行)
337
+ 5. 否则 → stop (第 78-92 行)
338
+ ```
339
+
340
+ ### 5.3 90% 阈值
341
+
342
+ `COMPLETION_THRESHOLD = 0.9`(第 3 行): 当 token 消耗达到预算的 90% 时触发继续。
343
+
344
+ ### 5.4 收益递减检测
345
+
346
+ `DIMINISHING_THRESHOLD = 500`(第 4 行): 当连续 3+ 次继续且每次增量 < 500 tokens,认为模型收益递减,提前停止。
347
+
348
+ ### 5.5 集成到 queryLoop
349
+
350
+ 在 `queryLoop` 中(`src/query.ts` 第 1308-1355 行):
351
+
352
+ ```typescript
353
+ if (feature('TOKEN_BUDGET')) {
354
+ const decision = checkTokenBudget(budgetTracker!, ...)
355
+ if (decision.action === 'continue') {
356
+ // 注入 nudgemessage
357
+ // incrementBudgetContinuationCount()
358
+ // 设置 state.transition = { reason: 'token_budget_continuation' }
359
+ // continue (继续循环)
360
+ }
361
+ // 否则记录 completionEvent
362
+ }
363
+ // return { reason: 'completed' }
364
+ ```
365
+
366
+ ### 5.6 任务预算 (taskBudget)
367
+
368
+ `taskBudget`(`src/query.ts` 第 197-198 行)是 API 端的 `task_budget`(output_config.task_budget, beta task-budgets-2026-03-13)。与 `tokenBudget` +500k 自动继续的不同。
369
+
370
+ - 在每次压缩后计算 `taskBudgetRemaining`(第 508-515 行、第 1138-1146 行)
371
+ - 传递给 API 调用(第 699-706 行)
372
+
373
+ ---
374
+
375
+ ## 6. 恢复机制
376
+
377
+ ### 6.1 max_output_tokens 恢复
378
+
379
+ **文件**: `src/query.ts`
380
+
381
+ | 恢复阶段 | 触发条件 | 行为 | 行号 |
382
+ |---------|---------|------|------|
383
+ | 8K→64K 升级 | 首次命中上限,capEnabled 且无用户自定义 | 设置 maxOutputTokensOverride=64K,重试 | 第 1194-1221 行 |
384
+ | 恢复消息注入 | 已升级或 cap 关闭 | 注入 "Output token limit hit" 消息 | 第 1223-1252 行 |
385
+ | 限制耗尽 | 超过 3 次 | 暴露扣留的错误 | 第 1254-1256 行 |
386
+
387
+ `MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3`(第 164 行)。
388
+
389
+ ### 6.2 Context Collapse 压缩
390
+
391
+ **文件**: `src/query.ts`
392
+
393
+ 当 API 返回 prompt-too-long 错误时(第 1085-1118 行):
394
+
395
+ 1. 首先尝试 `contextCollapse.recoverFromOverflow()`(第 1094 行) — 从已分阶段(staged)的折叠队列中释放
396
+ 2. 如果已经尝试过 `collapse_drain_retry`(第 1092 行),则跳过直接走 reactive compact
397
+
398
+ ### 6.3 Reactive Compact
399
+
400
+ **文件**: `src/query.ts`
401
+
402
+ 当 prompt-too-long 错误和 reactive compact 都启用时(第 1119-1175 行):
403
+
404
+ 1. 调用 `reactiveCompact.tryReactiveCompact()`(第 1120 行)
405
+ 2. 成功→构建压缩消息,设置 `hasAttemptedReactiveCompact = true`
406
+ 3. 失败→暴露错误消息
407
+
408
+ ### 6.4 错误恢复策略总结
409
+
410
+ ```
411
+ API 413 (prompt too long)
412
+ ├── Context Collapse drain (第 1094 行)
413
+ │ └── 失败→ fall through
414
+ ├── Reactive Compact (第 1120 行)
415
+ │ └── 失败→ 返回 prompt_too_long
416
+ └── (如果两者都不可用) → 返回 blocking_limit
417
+
418
+ max_output_tokens
419
+ ├── 8K→64K escalate (第 1194 行)
420
+ ├── 恢复消息注入 x3 (第 1223 行)
421
+ └── 暴露错误 (第 1254 行)
422
+
423
+ model fallback (第 893-953 行)
424
+ └── 切换到备用模型,清理并重试
425
+
426
+ 一般错误 (第 955-997 行)
427
+ └── yield 错误消息,返回 model_error
428
+ ```
429
+
430
+ ---
431
+
432
+ ## 7. QueryEngine.ts 的角色
433
+
434
+ **文件**: `src/QueryEngine.ts`
435
+
436
+ ### 7.1 概述
437
+
438
+ `QueryEngine` 类(第 184-1177 行)封装了查询生命周期和会话状态,是 `ask()` 函数的核心引擎。它提取了在 headless/SDK 和 REPL 间共享的逻辑。
439
+
440
+ ### 7.2 与 query.ts 的关系
441
+
442
+ ```
443
+ ask() 函数 QueryEngine.submitMessage() 方法
444
+ │ │
445
+ │ 创建 QueryEngine 实例 │
446
+ │ (第 1249-1285 行) │
447
+ │ │
448
+ └─── query() ────────────────────────────┘
449
+
450
+ │ AsyncGenerator
451
+
452
+ yield 消息流 (assistant/user/attachment/stream_event/...)
453
+ ```
454
+
455
+ - `ask()`(第 1186-1295 行)是一个便利包装器,创建 `QueryEngine` 实例并调用 `submitMessage()`
456
+ - `QueryEngine.submitMessage()`(第 209-1156 行)处理完整的查询生命周期:
457
+ 1. 构建 `ProcessUserInputContext`(第 335-395 行)
458
+ 2. 处理用户输入(第 410-428 行)
459
+ 3. 记录 transcript(第 450-463 行)
460
+ 4. 调用 `query()`(第 675-686 行)
461
+ 5. 处理 query 产出的所有消息类型(第 757-969 行)
462
+ 6. 生成最终 result(第 1082-1155 行)
463
+
464
+ ### 7.3 关键职责
465
+
466
+ - **消息持久化**: `recordTranscript()`(第 717-732 行)
467
+ - **权限跟踪**: `wrappedCanUseTool()` 包装(第 244-271 行)记录权限拒绝
468
+ - **SDK 消息规范化**: `normalizeMessage()`(第 769、783、787 行)
469
+ - **预算检查**: USD 预算(第 972-1002 行)和结构化输出重试限制(第 1005-1048 行)
470
+ - **Snip 回放**: `snipReplay` 回调(第 905-914 行)在 SDK 模式下处理 snip 边界
471
+
472
+ ### 7.4 submitMessage 的消息处理
473
+
474
+ `submitMessage()` 的 `for await` 循环处理 10+ 种消息类型:
475
+
476
+ | 消息类型 | 处理 | 行号 |
477
+ |---------|------|------|
478
+ | `assistant` | push 到 mutableMessages,yield 规范化 | 第 761-769 行 |
479
+ | `user` | push,yield 规范化,turnCount++ | 第 753-787 行 |
480
+ | `progress` | push,记录 transcript | 第 771-783 行 |
481
+ | `stream_event` | 累积 usage,跟踪 stop_reason | 第 788-827 行 |
482
+ | `attachment` | 处理结构化输出、max_turns、queued_command | 第 829-893 行 |
483
+ | `system` | 处理 compact_boundary、api_error、snip | 第 897-958 行 |
484
+ | `tool_use_summary` | yield 工具使用摘要 | 第 959-969 行 |
485
+
486
+ ---
487
+
488
+ ## 8. 工具执行引擎
489
+
490
+ **文件**: `src/services/tools/toolExecution.ts`
491
+
492
+ ### 8.1 工具执行九步生命周期
493
+
494
+ `runToolUse()` 函数(第 337-490 行)实现工具的完整执行生命周期:
495
+
496
+ ```
497
+ 1. Tool Lookup & Validation
498
+ │ findToolByName() (第 345 行)
499
+ │ 别名回退 (第 350-355 行)
500
+
501
+ 2. Abort Check
502
+ │ abortController.signal.aborted (第 415 行)
503
+
504
+ 3. Input Validation (Zod)
505
+ │ tool.inputSchema.safeParse() (第 615 行)
506
+
507
+ 4. Custom Validation
508
+ │ tool.validateInput() (第 683 行)
509
+
510
+ 5. Pre-Tool Hooks
511
+ │ runPreToolUseHooks() (第 800 行)
512
+
513
+ 6. Permission Check & User Confirmation
514
+ │ resolveHookPermissionDecision() (第 921 行)
515
+ │ canUseTool() (第 927 行)
516
+
517
+ 7. Tool Execution
518
+ │ tool.call() (第 1207 行)
519
+
520
+ 8. Result Processing
521
+ │ tool.mapToolResultToToolResultBlockParam() (第 1292 行)
522
+ │ processToolResultBlock() (第 1415 行)
523
+
524
+ 9. Post-Tool Hooks
525
+ │ runPostToolUseHooks() (第 1483 行)
526
+ │ runPostToolUseFailureHooks() (第 1700 行)
527
+ ```
528
+
529
+ ### 8.2 详细步骤解析
530
+
531
+ #### 步骤 1-2: 工具查找与中止检查
532
+
533
+ `runToolUse()`(第 337-490 行):
534
+
535
+ ```typescript
536
+ // 1. 查找工具 (第 345-355 行)
537
+ let tool = findToolByName(toolUseContext.options.tools, toolName)
538
+ if (!tool) {
539
+ // 通过别名回退 (第 350-355 行)
540
+ }
541
+ // 2. 中止检查 (第 415-453 行)
542
+ if (toolUseContext.abortController.signal.aborted) {
543
+ // yield "cancelled" 消息
544
+ }
545
+ ```
546
+
547
+ #### 步骤 3-4: 输入验证
548
+
549
+ `checkPermissionsAndCallTool()`(第 599-1745 行):
550
+
551
+ **Zod Schema 验证**(第 615-679 行):
552
+ - `tool.inputSchema.safeParse(input)` 使用 Zod 验证模型输入
553
+ - 失败时生成格式化的 Zod 错误并附加 schema-not-sent 提示(第 578-597 行)
554
+ - `buildSchemaNotSentHint()`: 检测到延迟工具(deferred tool)的 schema 没有被发送到 API 时,提示模型使用 ToolSearch 重新加载
555
+
556
+ **自定义验证**(第 683-733 行):
557
+ - 每个工具可以定义自己的 `validateInput()` 方法
558
+ - 验证失败返回 `isValidCall.result === false`
559
+
560
+ #### 步骤 5: Pre-Tool Hooks
561
+
562
+ **文件**: `src/services/tools/toolHooks.ts`
563
+
564
+ `runPreToolUseHooks()`(第 435-650 行):
565
+
566
+ 返回多类型结果:
567
+ | 结果类型 | 说明 |
568
+ |---------|------|
569
+ | `message` | 进度消息或附件消息 |
570
+ | `hookPermissionResult` | 钩子做出的权限决定 (allow/deny/ask) |
571
+ | `hookUpdatedInput` | 钩子修改后的输入 (passthrough) |
572
+ | `preventContinuation` | 阻止继续 |
573
+ | `stopReason` | 停止原因 |
574
+ | `additionalContext` | 额外的上下文消息 |
575
+ | `stop` | 立即停止 |
576
+
577
+ #### 步骤 6: 权限检查
578
+
579
+ **文件**: `src/services/tools/toolHooks.ts`
580
+
581
+ `resolveHookPermissionDecision()`(第 332-433 行):
582
+
583
+ - **Hook allow**: 仍然检查 settings.json 的 deny/ask 规则(第 373-385 行)
584
+ - **Hook deny**: 直接拒绝(第 408-411 行)
585
+ - **无钩子决定**: 走正常权限流程,可能包含 forceDecision(第 413-432 行)
586
+ - **需用户交互**: 钩子批准后如果 `requiresUserInteraction()` 或 `requireCanUseTool`,仍调用 `canUseTool()`(第 356-370 行)
587
+
588
+ `canUseTool` 在 `src/hooks/useCanUseTool.tsx` 中实现(React hook),处理 interactive/coordinator/swarm 三种权限模式。
589
+
590
+ #### 步骤 7: 工具执行
591
+
592
+ `tool.call()`(第 1207 行):
593
+ - 使用处理后的输入调用工具
594
+ - 通过 `onToolProgress` 回调报告进度
595
+ - 使用 `toolAbortController`(第 301 行)实现 per-tool 取消
596
+
597
+ Bash 错误级联(第 359-363 行):
598
+ ```typescript
599
+ if (tool.block.name === BASH_TOOL_NAME) {
600
+ this.hasErrored = true
601
+ this.siblingAbortController.abort('sibling_error')
602
+ }
603
+ ```
604
+
605
+ #### 步骤 8: 结果处理
606
+
607
+ - `tool.mapToolResultToToolResultBlockParam()`(第 1292 行)映射工具结果
608
+ - `processPreMappedToolResultBlock()` / `processToolResultBlock()`(第 1409-1415 行)进行后处理
609
+ - `applyToolResultBudget()`(第 379 行)限制工具结果大小
610
+
611
+ #### 步骤 9: Post-Tool Hooks
612
+
613
+ **文件**: `src/services/tools/toolHooks.ts`
614
+
615
+ `runPostToolUseHooks()`(第 39-191 行):
616
+ - 对 MCP 工具,支持 `updatedMCPToolOutput`
617
+ - 支持 `blockingError`、`preventContinuation`、`additionalContext`
618
+
619
+ `runPostToolUseFailureHooks()`(第 193-319 行):
620
+ - 工具失败时执行
621
+ - 同样支持 `blockingError`、`preventContinuation`、`additionalContext`
622
+
623
+ ### 8.3 错误分类与处理
624
+
625
+ `classifyToolError()`(第 150-171 行)将错误分类为安全的 telemetry 字符串:
626
+ - `TelemetrySafeError`: 使用预审的 telemetryMessage
627
+ - Node.js `errno` 错误: 记录 `ENOENT` 等代码
628
+ - 已知错误类型: 使用构造函数名称
629
+ - 未知错误: 降级为 `"Error"`
630
+
631
+ ### 8.4 工具遥测
632
+
633
+ 工具执行的每个阶段都会发出遥测事件:
634
+
635
+ | 事件 | 触发时机 | 代码位置 |
636
+ |------|---------|---------|
637
+ | `tengu_tool_use_error` | 工具不存在 | 第 372 行 |
638
+ | `tengu_tool_use_cancelled` | 工具被取消 | 第 416 行 |
639
+ | `tengu_tool_use_progress` | 进度更新 | 第 523 行 |
640
+ | `tengu_tool_use_can_use_tool_rejected` | 权限拒绝 | 第 1001 行 |
641
+ | `tengu_tool_use_can_use_tool_allowed` | 权限批准 | 第 1105 行 |
642
+ | `tengu_tool_use_success` | 工具执行成功 | 第 1331 行 |
643
+ | `tool_decision` (OTel) | 权限决策 | 第 962 行 |
644
+ | `tool_result` (OTel) | 工具结果 | 第 1381 行 |
645
+
646
+ ---
647
+
648
+ ## 9. 参考文件索引
649
+
650
+ | 文件 | 路径 | 核心内容 |
651
+ |------|------|---------|
652
+ | 主查询循环 | `src/query.ts` | `query()`, `queryLoop()`, 完整 Agent 循环 |
653
+ | 查询配置 | `src/query/config.ts` | `buildQueryConfig()`, `QueryConfig` 类型 |
654
+ | 查询依赖 | `src/query/deps.ts` | `QueryDeps`, `productionDeps()` |
655
+ | 停止钩子 | `src/query/stopHooks.ts` | `handleStopHooks()`, StopHookResult |
656
+ | Token 预算 | `src/query/tokenBudget.ts` | `checkTokenBudget()`, `BudgetTracker` |
657
+ | 转换类型 | `src/query/transitions.ts` | `Terminal`, `Continue` 类型 |
658
+ | 查询引擎 | `src/QueryEngine.ts` | `QueryEngine` 类, `ask()` 函数 |
659
+ | 工具流式执行器 | `src/services/tools/StreamingToolExecutor.ts` | `StreamingToolExecutor` 类 |
660
+ | 工具执行 | `src/services/tools/toolExecution.ts` | `runToolUse()`, `checkPermissionsAndCallTool()` |
661
+ | 工具编排 | `src/services/tools/toolOrchestration.ts` | `runTools()`, `partitionToolCalls()` |
662
+ | 工具钩子 | `src/services/tools/toolHooks.ts` | `runPreToolUseHooks()`, `runPostToolUseHooks()`, `resolveHookPermissionDecision()` |
663
+ | 权限检查 | `src/hooks/useCanUseTool.tsx` | `useCanUseTool()` React hook, 权限模式 |
docs/architecture/cross-cutting.md ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 横切关注点 — 错误处理、日志、遥测与性能
2
+
3
+ ## 1. 错误处理架构
4
+
5
+ ### 1.1 错误类型层次
6
+
7
+ 代码库定义了一个多层级的错误类体系,所有自定义错误最终继承自 `Error`:
8
+
9
+ - **基类 `ClaudeError`** (`src/utils/errors.ts`) — 设置 `this.name = this.constructor.name`,是所有内部错误的根类。
10
+ - **中止错误** — `AbortError` 和 SDK 的 `APIUserAbortError` 通过 `isAbortError()` 统一检测;该函数也兼容 DOMException 的 `name === 'AbortError'` 模式。
11
+ - **Shell 错误** — `ShellError` 携带 `stdout`、`stderr`、`code`、`interrupted` 四个字段,用于统一处理子进程失败。
12
+ - **配置解析错误** — `ConfigParseError` 携带 `filePath` 和后备的 `defaultConfig`。
13
+ - **Axios 错误分类** — `classifyAxiosError()` 将 HTTP 客户端错误归类为 `auth`、`timeout`、`network`、`http`、`other` 五种类型。
14
+ - **遥测安全错误** — `TelemetrySafeError_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS` 在构造时分离用户可见消息和遥测安全消息。
15
+
16
+ 领域特定错误分布在各个模块中:
17
+
18
+ - `StopTaskError` (`src/tasks/stopTask.ts`) — 带有 `code` 字段区分 `not_found`、`not_running`、`unsupported_type`。
19
+ - `ToolExecutionError` — 工具执行过程中的运行时错误。
20
+ - `BridgeFatalError` / `BridgeHeadlessPermanentError` (`src/bridge/`) — Bridge 模式下的致命/永久错误。
21
+ - `CannotRetryError` / `FallbackTriggeredError` (`src/services/api/withRetry.ts`) — API 重试耗尽或模型降级时的错误。
22
+ - `McpAuthError` / `XaaTokenExchangeError` (`src/services/mcp/`) — MCP 协议认证相关错误。
23
+ - 辅助函数 `toError()`、`errorMessage()`、`shortErrorStack()`、`isFsInaccessible()` 提供了通用的错误处理基础设施。
24
+
25
+ ### 1.2 工具执行错误
26
+
27
+ 工具调用执行时产生的错误通过 Task 系统的 catch 边界捕获。每个 Task 类型(`LocalShellTask`、`LocalAgentTask`、`InProcessTeammateTask`)实现自己的错误处理逻辑:
28
+
29
+ - **`AbortError`** 在 agent 任务中被特殊处理 — 提取部分结果 (`extractPartialResult`) 后生成通知而非静默丢弃。Bash 任务中则静默抑制 "exit code 137" 通知以减少噪音。
30
+ - **`StopTaskError`** 区分任务不存在、未在运行和不支持的类型,让调用方可以根据 `error.code` 决定处理策略。
31
+ - **`MaxFileReadTokenExceededError`** (`src/tools/FileReadTool/`) — 文件读取超过 token 上限时抛出,触发调用方改用分块读取。
32
+ - **`SessionBranchingError`** (`src/utils/sessionBranching.ts`) — 会话分支操作失败时的特定错误。
33
+ - **`ConversationStartupError`** (`src/server/services/conversationService.ts`) — 服务器端会话恢复失败时的错误。
34
+ - 工具执行器在上层 catch 边界中将错误序列化为 `tool_result` 内容,确保 LLM 可以感知错误并调整行为。
35
+
36
+ ### 1.3 模型调用错误与恢复策略
37
+
38
+ `withRetry()` (`src/services/api/withRetry.ts`) 实现了完整的 API 调用重试引擎:
39
+
40
+ - **默认最大重试次数** 为 10(可通过 `CLAUDE_CODE_MAX_RETRIES` 环境变量覆盖)。
41
+ - **错误分类**: 529 过载、429 速率限制(含 fast-mode overage)、401 认证过期、403 token 吊销、ECONNRESET/EPIPE 连接中断、400 max_tokens context overflow。
42
+ - **退避策略**: 指数退避 + 随机 jitter(`BASE_DELAY_MS * 2^(attempt-1) + jitter`),优先使用 `Retry-After` 响应头。
43
+ - **Fast Mode 降级**: 短延迟直接重试(保留 prompt cache),长延迟触发 cooldown 切换为标准速度。
44
+ - **模型降级**: 连续 3 次 529 错误后触发 `FallbackTriggeredError`,切换到 fallback 模型。
45
+ - **持久模式** (`CLAUDE_CODE_UNATTENDED_RETRY`): 无限重试 429/529,最大退避 5 分钟,6 小时上限,每 30 秒输出心跳防止会话超时。
46
+ - **背景请求不重试**: 非前台 `QuerySource`(如摘要、标题、建议)在 529 时直接放弃,避免容量级联放大。`FOREGROUND_529_RETRY_SOURCES` 集合中仅包含用户等待结果的查询源。
47
+ - **认证错误处理链**: 401/403 错误触发 OAuth token 刷新 (`handleOAuth401Error`)、AWS credential cache 清理 (`clearAwsCredentialsCache`)、GCP credential cache 清理 (`clearGcpCredentialsCache`)。CCR 模式下 401/403 被视为瞬态错误(网络抖动)而非坏凭证。
48
+ - **Context Overflow 恢复**: `parseMaxTokensContextOverflowError()` 解析 "input length and `max_tokens` exceed context limit" 错误消息,提取 `inputTokens`、`contextLimit`,自动计算调整后的 `max_tokens` 并重试。至少保留 1000 token 安全缓冲区和 3000 输出 token 下限。
49
+ - **连接池管理**: ECONNRESET/EPIPE 错误触发 `disableKeepAlive()`,在重试时创建新连接而非复用可能损坏的 keep-alive socket。
50
+ - **Mock 错误集成**: Ant 员工通过 `/mock-limits` 命令触发的模拟速率限制错误 (`checkMockRateLimitError`) 在重试引擎中受到特殊处理 — 不被视为可重试错误,确保测试场景不会进入无限重试循环。
51
+
52
+ ### 1.4 致命错误 vs 可恢复错误
53
+
54
+ - **可恢复错误**: 网络抖动、速率限制、认证过期、token 吊销 — 通过重试、token 刷新、连接池禁用等机制自动恢复。
55
+ - **致命错误**: `BridgeFatalError`、`CannotRetryError`(重试耗尽后)、配置解析失败 — 需要用户干预或进程重启。
56
+
57
+ ---
58
+
59
+ ## 2. 日志系统
60
+
61
+ ### 2.1 控制台日志与文件日志
62
+
63
+ - **`logForDebugging()`** (`src/utils/debug.ts`) — 双重输出到 stderr 和文件。支持日志级别过滤(`CLAUDE_CODE_DEBUG_LOG_LEVEL` 从 verbose 到 error),`CLAUDE_CODE_DEBUG` 环境变量启用输出。`CLAUDE_CODE_DEBUG_FILTER` 按模块名称过滤减少噪音。
64
+ - **缓冲写入**: `BufferedWriter` 模式将高频日志积累到内存缓冲区后批量写入磁盘,减少细粒度 I/O 的系统调用开销。日志文件位于 `{CLAUDE_CONFIG_HOME}/debug.log`,使用符号链接跟踪最新会话,便于快速定位当前 session 的日志。
65
+ - **`logError()`** (`src/utils/log.ts`) — 专用错误日志函数,捕获完整错误栈和结构化元数据,输出到标准错误流。
66
+ - **日志文件轮转**: 通过 `getClaudeConfigHomeDir()` 确定日志目录路径,`registerCleanup` 在进程退出时确保缓冲区排空。CLAUDE_CODE_FORCE_FULL_LOGO 等构建时常量控制日志展示格式。
67
+ - **调试过滤器**: `parseDebugFilter()` 和 `shouldShowDebugMessage()` 允许按来源模块名称精确控制哪些调试消息可见,在开发高噪音模块时可以只关注特定子系统的输出。
68
+
69
+ ### 2.2 LogSelector UI 组件 (`src/components/LogSelector.tsx`)
70
+
71
+ 一个功能完整的日志浏览器,提供:
72
+
73
+ - **模糊搜索** (Fuse.js) — 按会话标题、摘要、内容搜索。
74
+ - **标签分类** — 按 agent 名称、自定义标签、项目过滤。
75
+ - **树形浏览** — 按日期层级组织日志。
76
+ - **对话预览** — 在侧面板显示选中会话的摘要消息。
77
+ - **智能搜索** — 可以通过 AI 驱动的语义搜索查找相关日志。
78
+ - **分页加载** (`onLoadMore`) — 支持无限滚动。
79
+
80
+ ### 2.3 诊断工具 (`/doctor` 命令 & DiagnosticsService)
81
+
82
+ - **`DiagnosticsService`** (`src/server/services/diagnosticsService.ts`) — 捕获 `console.error`、`console.warn`、`process.on('uncaughtException')`、`process.on('unhandledRejection')`,将诊断事件写入文件系统。7 天保留期,50MB 上限。
83
+ - **REST API** (`src/server/api/diagnostics.ts`) — 提供 `GET /api/diagnostics/status`、`GET /api/diagnostics/events`、`POST /api/diagnostics/export` 等端点。导出为压缩的 tar.gz,自动脱敏 API key、token 等敏感信息。
84
+ - **`DiagnosticsTrackingError`** (`src/services/diagnosticTracking.ts`) — 内部跟踪诊断事件类型的错误。
85
+
86
+ ---
87
+
88
+ ## 3. 遥测与分析
89
+
90
+ ### 3.1 GrowthBook 集成 (`src/services/analytics/growthbook.ts`)
91
+
92
+ GrowthBook 提供 feature flags 和 A/B 测试能力:
93
+
94
+ - **远程评估**: `remoteEval: true` 模式下,服务器端评估 feature flag 值,客户端缓存到内存 (`remoteEvalFeatureValues`) 和磁盘 (`cachedGrowthBookFeatures` 在 `~/.claude.json`)。
95
+ - **用户属性**: 发送 `deviceID`、`platform`、`organizationUUID`、`accountUUID`、`subscriptionType`、`rateLimitTier` 等用于定向。
96
+ - **缓存策略**:
97
+ - `getFeatureValue_CACHED_MAY_BE_STALE()` — 非阻塞,优先内存缓存,后备磁盘缓存,适用于启动关键路径。
98
+ - `getDynamicConfig_BLOCKS_ON_INIT()` — 阻塞直到初始化完成(最多 5 秒超时)。
99
+ - `checkSecurityRestrictionGate()` — 安全检查相关 gate,等待 re-init 完成以确保值新鲜。
100
+ - `checkGate_CACHED_OR_BLOCKING()` — 磁盘缓存为 true 时快速返回,false 时等待服务器确认(避免 false 误阻止用户功能)。
101
+ - **实验曝光**: 每个 feature 在一个 session 内只记录一次曝光事件 (`loggedExposures` 去重),通过 `logGrowthBookExperimentTo1P` 发送到第一方遥测。
102
+ - **周期刷新**: 非 Ant 构建每 6 小时刷新,Ant 构建每 20 分钟刷新。刷新时重建 `remoteEvalFeatureValues` 并同步到磁盘。
103
+ - **认证变更**: `refreshGrowthBookAfterAuthChange()` 销毁旧客户端并使用新认证头重建,防止 API key 变更后返回过期值。
104
+ - **环境变量覆盖**: Ant 员工可通过 `CLAUDE_INTERNAL_FC_OVERRIDES` 全局覆盖 feature flag(用于评估工具)。
105
+
106
+ ### 3.2 遥测 Sink 架构 (`src/services/analytics/sink.ts`)
107
+
108
+ 分析系统采用 sink 模式,支持热插拔遥测后端:
109
+
110
+ - **`AnalyticsSink` 接口** (`src/services/analytics/index.ts`) — 定义 `logEvent()` 同步和 `logEventAsync()` 异步方法,以及 `attachAnalyticsSink()` 注册函数。OSS 构建中所有实现体为空(no-op),降低二进制体积。
111
+ - **`initializeAnalyticsGates()` / `initializeAnalyticsSink()`** (`src/services/analytics/sink.ts`) — OSS 构建中为空函数。在完整构建中会初始化 GrowthBook 和 Datadog 后端。
112
+ - **`sinkKillswitch.ts`** — 通过 GrowthBook feature flag 在运行时动态禁用遥测发送,用于紧急情况下的数据收集开关。
113
+ - **元数据标记** — `AnalyticsMetadata_I_VERIFIED_THIS_IS_NOT_CODE_OR_FILEPATHS` 类型确保只有在调用方显式声明"此元数据不包含敏感信息"时才能传递,编译期防止数据泄露。
114
+
115
+ ### 3.3 第一方事件日志 (`src/services/analytics/firstPartyEventLogger.ts`)
116
+
117
+ Ant 内部构建使用第一方事件日志系统:
118
+
119
+ - **实验曝光日志**: `logGrowthBookExperimentTo1P()` 记录 A/B 实验的分组信息和用户属性。
120
+ - **批处理配置**: 通过 `tengu_1p_event_batch_config` 动态配置批量发送策略。使用 `onGrowthBookRefresh` 订阅配置变更,在运行时重建 LoggerProvider。
121
+ - **条件启用**: `is1PEventLoggingEnabled()` 检查隐私级别、用户认证状态等前置条件,只有满足所有条件时才启用。
122
+
123
+ ### 3.4 Datadog (`src/services/analytics/datadog.ts`)
124
+
125
+ OSS 构建中 Datadog 遥测被禁用(`initializeDatadog` 返回 `false`,`trackDatadogEvent` 函数体为空)。接口保留以便启动代码不需要对 OSS 变体特殊处理。Datadog 用于生产环境的性能监控、错误追踪和自定义指标仪表板。
126
+
127
+ ### 3.5 分析配置 (`src/services/analytics/config.ts`)
128
+
129
+ `isAnalyticsDisabled()` 在以下情况下禁用分析:NODE_ENV=test、Bedrock/Vertex/Foundry 第三方提供商、隐私级别为 no-telemetry 或 essential-traffic 模式。
130
+
131
+ ### 3.6 成本与令牌用量追踪 (`src/cost-tracker.ts`)
132
+
133
+ - **全局状态**: `getTotalCostUSD()`、`getTotalInputTokens()`、`getTotalOutputTokens()`、`getTotalAPIDuration()` 等函数从 bootstrap state 读取聚合数据。
134
+ - **按模型统计**: `getUsageForModel()` 按模型名跟踪 `inputTokens`、`outputTokens`、`cacheReadInputTokens`、`cacheCreationInputTokens`、`costUSD`。
135
+ - **会话持久化**: `saveCurrentSessionCosts()` 将当前 session 的成本写入 project config;`restoreCostStateForSession()` 在恢复 session 时重建成本状态。
136
+ - **成本展示**: `formatTotalCost()` 输出格式化成本摘要(总成本、API 时长、代码变更行数、按模型的令牌使用量)。
137
+ - **exit 回调**: `useCostSummary()` (`src/costHook.ts`) 在 `process.on('exit')` 时自动输出成本摘要并保存 session 数据。
138
+
139
+ ---
140
+
141
+ ## 4. 性能工程
142
+
143
+ ### 4.1 流式工具执行
144
+
145
+ 工具执行采用流式架构(`QueryEngine` 中的 `AsyncGenerator`),LLM 输出 token 流式到达的同时,工具结果可以并行返回:
146
+
147
+ - **`QueryEngine`** 将 LLM 的流式响应解析为 `TextBlock`、`ToolUseBlock`、`ContentBlockStop` 等事件,边解析边交付给 UI 渲染。
148
+ - **工具并行**: 当 LLM 发起多个工具调用时,`tools.ts` 中的调度器可以并行执行无依赖的工具(如同时读取多个文件),减少总等待时间。流式响应持续接收新的工具调用请求。
149
+ - **前台 vs 背景**: 前台 `QuerySource`(如 `repl_main_thread`)使用完整的重试、提示缓存和速率限制逻辑;背景任务(摘要、标题生成)使用轻量级路径以减少延迟。
150
+ - **`QuerySource` 分类**: 定义在 `src/constants/querySource.ts`,包括 `repl_main_thread`、`repl_main_thread:outputStyle:custom`、`sdk`、`agent:custom`、`compact`、`hook_agent`、`auto_mode` 等 20+ 种类型,影响重试策略和日志记录。
151
+
152
+ ### 4.2 提示缓存 (Prompt Caching)
153
+
154
+ - **缓存写入控制** (`skipCacheWrite`) — 某些场景(如快速诊断查询)跳过缓存写入以节省 cache creation token 成本。适用于可丢弃的中间查询,如标题生成、会话摘要等。
155
+ - **缓存读取追踪** — `cache_read_input_tokens` 和 `cache_creation_input_tokens` 通过 `cost-tracker.ts` 分模型追踪。API 响应中的 `usage.cache_read_input_tokens` 和 `usage.cache_creation_input_tokens` 字段在 `addToTotalSessionCost()` 中累加。
156
+ - **Fast Mode 缓存保留** — 在 fast mode 中,短延迟重试使用相同的模型名以保留 prompt cache(减少 cache creation 消耗);长延迟触发 cooldown 切换到标准模型时,失去缓存被视为可接受成本。
157
+ - **Cache Break Detection** (`PROMPT_CACHE_BREAK_DETECTION` feature flag) — 监控和检测 prompt cache 命中率异常下降的场景,帮助诊断缓存失效原因。
158
+ - **成本核算**: `calculateUSDCost()` 函数根据模型单价、输入/输出/cache read/cache creation token 用量计算精确成本。未知模型通过 `hasUnknownModelCost()` 标记。
159
+
160
+ ### 4.3 上下文预取
161
+
162
+ - **内存预取**: `memory` 系统在 agent 启动时主动从磁盘加载相关记忆片段。
163
+ - **技能预取**: `skills` 系统预加载已启用的技能描述和工具定义。
164
+ - **推测执行** (`SpeculationState`): 在用户输入到达之前,系统可以推测性地启动任务执行。
165
+
166
+ ### 4.4 令牌估算
167
+
168
+ `getContextWindowForModel()` 和 `getModelMaxOutputTokens()` 提供按模型的上下文窗口和输出上限。`parseMaxTokensContextOverflowError()` 解析 API 返回的 context overflow 错误并��动调整 `max_tokens` 参数。
169
+
170
+ ### 4.5 速率限制模拟 (`src/services/rateLimitMocking.ts`)
171
+
172
+ Ant 员工可以通过 `/mock-limits` 命令模拟各种速率限制场景(429、529、fast-mode cooldown),用于测试 UI 行为和恢复逻辑。与 `withRetry()` 集成,确保模拟错误不触发真实重试。
173
+
174
+ ### 4.6 背景任务卸载
175
+
176
+ 在 stop hooks 中使用 fire-and-forget 模式(`void` 前缀 + `.catch()`),确保关键路径不被背景任务阻塞。例如:`LogSelector.tsx` 中的日志写入、`onChangeAppState` 中的 side-effect 通知。
177
+
178
+ ---
179
+
180
+ ## 5. 跨组件通信模式
181
+
182
+ ### 5.0 Signal 模式 (`src/utils/signal.ts`)
183
+
184
+ `createSignal<T>()` 提供轻量级的事件信号原语,与 Store 不同,Signal 不持有状态快照,仅用于通知"某事发生了":
185
+
186
+ - **订阅/取消**: `subscribe(listener)` 返回取消函数,与 React useEffect 的 cleanup 模式天然兼容。
187
+ - **类型安全**: 通过泛型 `Args` 指定事件参数类型。
188
+ - **去重**: `clear()` 移除所有监听器,用于 dispose 和 reset 路径。
189
+ - **使用场景**: GrowthBook 刷新通知 (`refreshed` signal)、设置变更通知、`onChangeAppState` 中的事件广播。
190
+ - **与 Store 的区别**: Signal 无 `getState()`,不存储值,仅做事件通知。在代码库中替换了 ~15 处重复的 `new Set<Listener>()` + `subscribe/notify` 模式。
191
+
192
+ ## 6. 状态管理性能
193
+
194
+ ### 6.1 AppState 可观察存储 (`createStore` 模式)
195
+
196
+ `createStore<T>()` (`src/state/store.ts`) 是一个轻量级不可变状态存储,模式灵感来源于 Redux 但更简洁:
197
+
198
+ - `getState()` — O(1) 引用读取,无任何开销。
199
+ - `setState(updater)` — 接收 `(prev: T) => T` 更新函数。通过 `Object.is` 做引用相等性检查,避免无变更时通知监听器。`onChange` 回调在每次有效状态变更时同步触发,接收 `{ newState, oldState }` 上下文。
200
+ - `subscribe(listener)` — 基于 `Set<Listener>` 的发布-订阅,返回取消订阅函数。无额外分配。
201
+ - `AppStateStore` 类型 (`src/state/AppStateStore.ts`) 继承自 `Store<AppState>`,添加了 `AppState` 的默认值工厂函数 `getDefaultAppState()`。完整的 AppState 包含 `tasks`、`toolPermissionContext`、`settings`、`isUltraplanMode`、`viewingAgentTaskId` 等顶级字段。
202
+ - **不可变性保证**: `setState` 使用 updater 函数模式,每次变更产生新的状态对象。React 渲染层依赖引用相等性进行短路优化。
203
+
204
+ ### 6.2 onChangeAppState 副作用处理器 (`src/state/onChangeAppState.ts`)
205
+
206
+ 一个集中式 `onChange` 处理器,在 AppState 变更时协调跨系统的副作用:
207
+
208
+ - 权限模式同步(CCR/SDK)
209
+ - 会话元数据变更通知
210
+ - 设置变更应用 (`applySettingsChange`)
211
+ - API key 缓存清理
212
+
213
+ 这种模式避免了 React 组件中散落的 `useEffect` 链,让状态变更的副作用可预测且可测试。
214
+
215
+ ### 6.3 Selector 模式 (`src/state/selectors.ts`)
216
+
217
+ 纯函数选择器从 AppState 派生计算状态:
218
+
219
+ - `getViewedTeammateTask()` — 提取当前查看的 teammate 任务
220
+ - `getActiveAgentForInput()` — 确定用户输入路由目标(leader / viewed / named_agent)
221
+
222
+ 选择器保持轻量、无副作用,便于组合和测试。
223
+
224
+ ### 6.4 React Context 订阅
225
+
226
+ `AppStateProvider` (`src/state/AppState.tsx`) 使用 React Context + `useSyncExternalStore` 将存储桥接到 React 渲染周期。`VoiceProvider` 通过 `feature('VOICE_MODE')` 进行死代码消除 — 外部构建中它是一个无操作的包裹器。`useSettingsChange` hook 监听设置文件变更并增量更新状态。
227
+
228
+ ---
229
+
230
+ ## 7. 构建优化
231
+
232
+ ### 7.1 Feature Flag 死代码消除 (`bun:bundle feature()`)
233
+
234
+ Bun 的编译时 `feature()` 函数允许在构建阶段消除未使用的代码分支:
235
+
236
+ - `scripts/build.ts` 定义了完整的实验特性列表(如 `VOICE_MODE`、`AGENT_MEMORY_SNAPSHOT`、`BASH_CLASSIFIER` 等 50+ 个 flag)。
237
+ - 外部构建默认只启用 `VOICE_MODE`。
238
+ - 构建命令 `bun build --feature=<flag>` 将 feature flag 注入编译步骤,未启用的分支在 tree-shaking 中被完全移除。
239
+ - 这使得 ant-internal 代码可以直接嵌入仓库,而外部构建不携带任何内部逻辑。
240
+
241
+ ### 7.2 React Compiler 输出 (`src/components/LogoV2/`)
242
+
243
+ `LogoV2/` 目录中的组件(`LogoV2.tsx`、`AnimatedClawd.tsx`、`Feed.tsx`、`WelcomeV2.tsx` 等)使用了 React Compiler (`react/compiler-runtime`) 自动记忆化输出。编译器将 `useMemo`/`useCallback` 模式自动化,减少手动优化的工作量。
244
+
245
+ ### 7.3 WASM 二进制文件
246
+
247
+ VAD(Voice Activity Detection)服务 (`src/friend/voice/vad-service.ts`) 使用 `onnxruntime-web` 的 WASM 后端(而非 `onnxruntime-node` 原生插件),因为 Bun 不支持 Node-API 原生插件。WASM 二进制文件 (`onnxruntime-web`) 随 dist 打包。
248
+
249
+ ### 7.4 Build 脚本 (`scripts/build.ts`)
250
+
251
+ 构建管道使用 Bun 原生打包器,生成单一可执行文件:
252
+
253
+ 1. **预构建 Friend VRM 前端**: 检查 `src/components/friend/frontend/dist/index.html` 是否存在,不存在时执行 `npm run build`。
254
+ 2. **主构建**: 使用 `bun build --compile --target bun --minify --bytecode` 生成单个可执行文件。`--minify` 减小二进制体积,`--bytecode` 编译为 Bun 字节码提升启动速度。
255
+ 3. **编译时常量注入**:
256
+ - `MACRO.VERSION` — 语义版本号(开发版附加 git SHA 和时间戳)。
257
+ - `MACRO.BUILD_TIME` — ISO 8601 构建时间戳。
258
+ - `MACRO.FEEDBACK_CHANNEL` — 反馈渠道(OSS 构建为 `github`)。
259
+ - `MACRO.VERSION_CHANGELOG` — 最近的 git commit 日志或指向 GitHub 的 URL。
260
+ 4. **外部模块排除**: `@ant/*`、`audio-capture-napi`、`image-processor-napi`、`modifiers-napi`、`url-handler-napi` 通过 `--external` 排除捆绑,减小二进制体积并允许运行时加载原生模块。
261
+ 5. **环境变量定义**: `process.env.USER_TYPE='external'`、`process.env.CLAUDE_CODE_FORCE_FULL_LOGO='true'`、`process.env.CCR_FORCE_BUNDLE='true'` 等定义确保构建产物运行于正确模式。
262
+ 6. **完整特性集**: 使用 `--feature-set=dev-full` 启用全部实验特性(50+ feature flags),默认仅启用 `VOICE_MODE`。
263
+ 7. **输出产物**: 开发版本输出到 `./VersperClaw`,发布版本输出到 `./dist/cli`。
264
+
265
+ ---
266
+
267
+ ## 8. 资源管理
268
+
269
+ ### 8.1 子进程管理
270
+
271
+ - **音频采集** (`arecord` / `sox`) — 通过 `child_process.spawn` 管理,采样率 16kHz,16-bit PCM。
272
+ - **Whisper STT** (`src/services/voice/whisperSTT.ts`) — 本地语音识别子进程,管理其生命周期并处理输出解析。
273
+ - **Tauri 子进程** — 在 Friend 桌面模式下启动和管理 Tauri shell 进程。
274
+ - **子进程超时** — `RipgrepTimeoutError`、`StallTimeoutError` 确保子进程不会无限期挂起。`cleanupRegistry.ts` 注册进程清理回调,在优雅关闭时统一终止。
275
+
276
+ ### 8.2 定时器和超时
277
+
278
+ - **GrowthBook 周期刷新** — `setupPeriodicGrowthBookRefresh()` 使用 `setInterval`(非 Ant: 6h,Ant: 20min),`unref()` 保证不阻止进程退出。
279
+ - **API 重试退避** — `sleep()` + `AbortSignal` 组合,支持中途取消。
280
+ - **持久重试心跳** — 每 30 秒输出系统消息防止空闲超时。
281
+ - **优雅关闭** — `gracefulShutdown.ts` 中的 `CleanupTimeoutError` 确保关闭过程不会无限阻塞。
282
+
283
+ ### 8.3 优雅关闭与清理注册表
284
+
285
+ `cleanupRegistry.ts` 和 `gracefulShutdown.ts` 实现了进程级资源清理系统:
286
+
287
+ - **`registerCleanup()`** (`src/utils/cleanupRegistry.ts`) — 全局 `Set<() => Promise<void>>` 注册表。任何模块可以注册异步清理函数,返回取消注册函数。`runCleanupFunctions()` 使用 `Promise.all` 并行执行所有清理任务。
288
+ - **`gracefulShutdown()`** (`src/utils/gracefulShutdown.ts`) — 主关闭流程,按顺序执行:
289
+ 1. `onExit('signal-exit')` 捕获 SIGTERM/SIGINT,设置 `isShuttingDown` 标志。
290
+ 2. 同步恢复终端模式(退出 alt screen、恢复光标、禁用 mouse tracking、恢复 Kitty 键盘模式)。
291
+ 3. `runCleanupFunctions()` 并行运行所有注册的异步清理。
292
+ 4. 关闭 Datadog(`shutdownDatadog()`)和第一方事件日志。
293
+ 5. 最终日志发送(会话成本、诊断事件)。`CleanupTimeoutError` 防止清理阶段无限阻塞。
294
+
295
+ ### 8.4 文件句柄管理
296
+
297
+ - **SSE 客户端** — EventSource 连接在设置/清理生命周期中管理,使用 `AbortController` 确保断开。
298
+ - **音频文件** — 语音录制生成临时 WAV 文件,`FriendService` 负责生命周期管理。
299
+ - **调试日志** — `BufferedWriter` 管理文件写入,`registerCleanup` 注册关闭回调。
300
+ - **诊断事件** — `DiagnosticsService` 使用文件系统存储诊断事件,7 天保留期和 50MB 上限防止磁盘膨胀。
301
+
302
+ ### 8.5 VAD 内存状态管理
303
+
304
+ `SileroVad` (`src/friend/voice/vad-service.ts`) 在 ONNX 推理会话中维护 LSTM 状态张量 (`stateH`、`stateC`)。每个音频帧(512 采样 / 32ms)处理后更新状态。关键优化:
305
+
306
+ - **RMS 能量过滤**: 低于 `rmsThreshold`(默认 ~-48dBFS)的帧跳过 ONNX 推理,直接视为静音。在高噪声环境中可以减少约 60% 的推理次数。
307
+ - **状态重置**: `reset()` 方法清空 LSTM 状态和累积缓冲区,用于会话间清理。
308
+ - **预处理触发**: `preSpeechTriggerFrames`(默认 10 帧 / 320ms)过滤短时噪音爆发,避免误触发。
309
+
310
+ ---
311
+
312
+ ## 架构决策记录
313
+
314
+ | 决策 | 理由 |
315
+ |------|------|
316
+ | 错误使用 `class extends Error` 并显式设置 `this.name` | 确保压缩构建中 `instanceof` 失效时仍可通过 `name` 属性识别错误类型 |
317
+ | 重试引擎使用 generator (`yield`) | 允许在重试间隔中间向调用方输出系统消息(心跳、进度) |
318
+ | GrowthBook 使用 `remoteEval: true` | 服务器端评估避免客户端下载完整规则集,减少网络负载和延迟 |
319
+ | VAD 使用 WASM 而非原生插件 | 兼容 Bun 运行时(Node-API 原生插件在 Bun 中导致 segfault) |
320
+ | 选择器使用纯函数而非 memoized selector | AppState 不可变性保证引用相等性检查足够高效,不需要额外记忆化开销 |
321
+ | Feature flag 死代码消除在构建时而非运行时完成 | 显著减小外部构建的二进制体积,消除内部代码泄漏风险 |
322
+ | Datadog / 遥测在 OSS 构建中默认禁用 | 保持开放源代码版本的隐私友好特性,同时保持接口兼容性 |
docs/architecture/design-philosophy.md ADDED
@@ -0,0 +1,329 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 设计哲学与架构原则
2
+
3
+ > 本文档综合自 Dive into Claude Code (arXiv:2604.14228v1) 的架构分析以及 VersperClaw
4
+ > 源代码的实际实现模式,旨在为面试准备和系统设计讨论提供参考。
5
+ >
6
+ > Claude Code 的设计哲学源于五个核心人类价值,通过十三条设计原则转化为具体的架构决策。
7
+ > VersperClaw 在此基础上进行了多 Provider 和 Friend VRM 等扩展。
8
+
9
+ ---
10
+
11
+ ## 1. 五大核心价值 (Core Values)
12
+
13
+ Claude Code 的系统架构由五个根本性的人类价值驱动。这些价值不是事后总结,而是在架构设计
14
+ 之初就被确立为优先级排序的依据。
15
+
16
+ ### 1.1 人类决策权威 (Human Decision Authority)
17
+
18
+ 人类保留对所有系统行为的最终决定权。这一价值通过 **主体层级结构 (principal hierarchy)**
19
+ 来实现:Anthropic(作为模型开发者)→ operators(组织管理员)→ users(终端用户)。
20
+
21
+ 架构含义:
22
+ - 人类可以**实时观察**系统行为、**批准或拒绝**提议的操作、**中断**进行中的操作、并在
23
+ 事后**审计**所有操作记录。
24
+ - 当 Anthropic 发现用户批准 93% 的权限提示时,他们的反应不是增加更多警告,而是重新
25
+ 构建问题:通过在明确定义的边界(沙箱、auto-mode 分类器)内让代理自由工作,而非
26
+ 依赖用户逐操作审批(因为一旦习惯化,用户就会不加审查地批准)。
27
+ - 对应源文件:`src/utils/permissions/permissions.ts`、`src/utils/permissions/PermissionMode.ts`
28
+
29
+ ### 1.2 安全、安全与隐私 (Safety, Security, and Privacy)
30
+
31
+ 系统有义务保护人类、代码、数据和基础设施,**即使人类疏忽或犯错**。这与人类决策权威
32
+ 有本质区别:权威是关于人类的**选择权**,而安全是关于系统的**保护义务**。
33
+
34
+ 架构含义:
35
+ - 威胁模型涵盖四种风险:过度热心行为、诚实错误、提示注入和模型失准。
36
+ - 实现为**多层重叠安全机制**(拒绝优先、分类器、沙箱、钩子),任何一层都能独立阻止
37
+ 危险操作。
38
+ - 对应源文件:`src/utils/permissions/yoloClassifier.ts`、`src/tools/BashTool/shouldUseSandbox.ts`
39
+
40
+ ### 1.3 可靠执行 (Reliable Execution)
41
+
42
+ 代理正确执行用户的实际意图,在长时间内保持一致性,并支持在执行完成前验证工作。
43
+
44
+ 架构含义:
45
+ - 涵盖单轮正确性和长周期可靠性(跨上下文窗口边界、会话恢复、多代理委派)。
46
+ - 实现了 5 层压缩管道、优雅恢复机制(max_output_tokens 升级重试、auto-compact、
47
+ reactive compact)和自动断路器。
48
+ - 对应源文件:`src/query.ts`(queryLoop)、`src/services/compact/autoCompact.ts`
49
+
50
+ ### 1.4 能力放大 (Capability Amplification)
51
+
52
+ 系统显著提升开发者单位时间/成本的产出效率。Anthropic 内部调查显示约 27% 的任务属于
53
+ "如果没有工具就不会尝试的工作"——架构使**全新的工作流**成为可能,而不仅仅是加速
54
+ 现有流程。
55
+
56
+ 架构含义:
57
+ - 系统的创造者将其描述为 "Unix 工具而非传统产品"——由最小、有用、可理解和可扩展的
58
+ 构建块组成。
59
+ - 投资于**确定性基础设施**(上下文管理、工具路由、恢复机制)而非决策框架(显式
60
+ 规划器或状态图),前提是日益强大的模型更受益于丰富的操作环境而非约束性框架。
61
+ - 对应源文件:`src/tools/tools.ts`(assembleToolPool)、`src/query.ts`(~88 行主循环)
62
+
63
+ ### 1.5 上下文适应性 (Contextual Adaptability)
64
+
65
+ 系统适配用户的特定上下文(项目、工具、约定、技能水平),并且关系随时间改善。
66
+
67
+ 架构含义:
68
+ - 扩展架构(CLAUDE.md、skills、MCP、hooks、plugins)提供多层级可配置性,每层具有
69
+ 不同的上下文成本。
70
+ - 纵向数据显示人机关系是演化的:自动批准率从 <50 会话的 ~20% 增长到 750+ 会话的
71
+ >40%。信任是"由模型、用户和产品共同构建的"。
72
+ - 对应源文件:`src/context.ts`、`src/services/mcp/`、`src/utils/hooks/`
73
+
74
+ ---
75
+
76
+ ## 2. 十三条设计原则 (Design Principles)
77
+
78
+ 五大价值通过十三条设计原则可操作化。每条原则回答一个生产级编码代理必须解决的
79
+ 重复性问题。下表总结了每条原则、其服务的价值、设计问题以及关键实现文件。
80
+
81
+ | # | 原则 | 服务价值 | 设计问题 | 关键实现 |
82
+ |---|------|---------|---------|---------|
83
+ | 1 | **拒绝优先,人类升级** (Deny-first with human escalation) | Authority, Safety | 未识别的操作应被允许、阻止还是升级给人类? | `permissions.ts` (deny 规则优先于 allow 规则) |
84
+ | 2 | **渐进信任光谱** (Graduated trust spectrum) | Authority, Adaptability | 固定权限层级还是随时间演进的光谱? | `PermissionMode.ts` (7 个模式: plan → default → acceptEdits → auto → dontAsk → bypassPermissions → bubble) |
85
+ | 3 | **深度防御,分层机制** (Defense in depth with layered mechanisms) | Safety, Authority, Reliability | 单一安全边界还是多个重叠的? | 7 层独立机制 (pre-filter + deny-first + modes + classifier + sandbox + no-restore + hooks) |
86
+ | 4 | **外化可编程策略** (Externalized programmable policy) | Safety, Authority, Adaptability | 硬编码策略还是外化配置? | `CLAUDE.md` 层级, `hooks` 生命周期, `PermissionRule` |
87
+ | 5 | **上下文作为稀缺资源,渐进管理** (Context as scarce resource with progressive management) | Reliability, Capability | 绑定资源约束是什么?如何分级管理? | 5 层压缩管道 (budget → snip → microcompact → collapse → auto-compact), `query.ts:365-453` |
88
+ | 6 | **追加式持久状态** (Append-only durable state) | Reliability, Authority | 可变状态、快照还是追加日志? | JSONL 会话转录 (`sessionStorage.ts`), 侧链文件 |
89
+ | 7 | **最小脚手架,最大操作平台** (Minimal scaffolding, maximal operational harness) | Capability, Reliability | 投资于推理侧框架还是操作基础设施? | ~88 行 queryLoop; ~98.4% 的代码为确定性基础设施 |
90
+ | 8 | **价值观优于规则** (Values over rules) | Capability, Authority | 僵化的决策程序还是上下文的判断? | 系统提示设计基于原则而非穷举规则 |
91
+ | 9 | **可组合的多机制扩展** (Composable multi-mechanism extensibility) | Capability, Adaptability | 统一扩展 API 还是分层机制? | MCP + Plugins + Skills + Hooks 四种机制,上下文成本递增 |
92
+ | 10 | **可逆性加权风险评估** (Reversibility-weighted risk assessment) | Capability, Safety | 所有操作相同监督还是更轻的只读/可逆操作? | read-only 工具并行执行, 写操作串行化 |
93
+ | 11 | **透明的基于文件的配置与记忆** (Transparent file-based configuration and memory) | Adaptability, Authority | 不透明数据库、嵌入检索还是用户可见的文件? | `CLAUDE.md` 层级, auto-memory 文件, git 版本可控 |
94
+ | 12 | **隔离的子代理边界** (Isolated subagent boundaries) | Reliability, Safety, Capability | 子代理共享父上下文还是隔离运行? | `AgentTool.tsx`, `runAgent.ts`, 侧链转录, 独立上下文窗口 |
95
+ | 13 | **优雅恢复与韧性** (Graceful recovery and resilience) | Reliability, Capability | 错误时硬失败还是静默恢复? | max_output_tokens 升级 (3次)、reactive compact、fallback model、断路器 |
96
+
97
+ ### 2.1 原则的可选设计家族
98
+
99
+ 这些原则可以通过对比三种主流替代设计家族来理解:
100
+
101
+ - **基于规则的编排**:LangGraph 等框架将决策逻辑编码为显式状态图(typed edges),选择
102
+ 脚手架而非最小平台。
103
+ - **容器隔离执行**:SWE-Agent 和 OpenHands 依赖 Docker 隔离而非分层策略执行。
104
+ - **版本控制即安全**:Aider 使用 Git 回滚作为主要安全机制而非拒绝优先评估。
105
+
106
+ Claude Code 的原则组合的独特之处在于:最小决策脚手架 + 分层策略执行 + 基于价值观的
107
+ 判断 + 拒绝优先默认 + 渐进上下文管理 + 可组合扩展。
108
+
109
+ ### 2.2 值-原则-架构 映射
110
+
111
+ 每条价值通过其原则追踪到特定的架构决策:
112
+
113
+ | 价值 | 驱动的原则 | 架构体现 |
114
+ |------|-----------|---------|
115
+ | 人类决策权威 | 拒绝优先、渐进信任、追加状态、外部化策略、价值观优于规则 | 权限系统、审计日志、CLAUDE.md |
116
+ | 安全与隐私 | 深度防御、拒绝优先、可逆性加权、外部化策略、隔离子代理 | 7 层安全、沙箱、分类器 |
117
+ | 可靠执行 | 稀缺上下文、追加状态、优雅恢复、隔离子代理、深度防御 | 压缩管道、JSONL 转录、断路器 |
118
+ | 能力放大 | 最小脚手架、可组合扩展、可逆性加权、上下文管理、优雅恢复 | queryLoop、MCP、speculative 执行 |
119
+ | 上下文适应性 | 透明文件记忆、可组合扩展、渐进信任、外部化策略 | CLAUDE.md 层级、skills、hooks |
120
+
121
+ ---
122
+
123
+ ## 3. 架构权衡 (Architectural Trade-offs)
124
+
125
+ ### 3.1 安全 vs 自主权 (Safety vs. Autonomy)
126
+
127
+ 系统中最核心的张力:更高的自主权意味着更少的人类干预,但也意味着更大的风险。
128
+
129
+ - **Claude Code 的选择**:通过渐进信任光谱来管理这种张力。用户从 `plan` 或 `default`
130
+ 模式开始,随着时间向 `acceptEdits` → `auto` → `bypassPermissions` 演进。
131
+ - **权衡的体现**:当命令超过 50 个子命令时,权限系统退回到通用审批提示而非逐子命令
132
+ 检查,因为逐子命令解析会导致 UI 冻结。这是安全与性能之间结构性张力的实例。
133
+ - **相关代码**:`src/utils/permissions/getNextPermissionMode.ts`、
134
+ `src/utils/permissions/PermissionMode.ts`
135
+
136
+ ### 3.2 上下文效率 vs 透明度 (Context Efficiency vs. Transparency)
137
+
138
+ 压缩节省上下文但降低人类可读性。
139
+
140
+ - **Claude Code 的选择**:5 层压缩管道,从轻量级(budget reduction、snip)到重量级
141
+ (auto-compact),每层在成本和效果之间做出不同权衡。
142
+ - **权衡的体��**:auto-compact 使用模型生成摘要来替代原始对话,但摘要丢失了原始
143
+ 细节。上下文折叠 (context collapse) 作为只读投影避免了这个问题,但增加了实现
144
+ 复杂度。追加式 JSONL 日志虽然保留完整可审计历史,但在恢复时不还原权限状态,
145
+ 牺牲了便利性以换取安全性。
146
+ - **相关代码**:`src/services/compact/`(整个目录)、`src/utils/sessionStorage.ts`
147
+
148
+ ### 3.3 简单 vs 可扩展 (Simplicity vs. Extensibility)
149
+
150
+ 核心循环应该简单,但系统需要适应各种用例。
151
+
152
+ - **Claude Code 的选择**:`queryLoop()` 是 ~88 行的 while-true 循环。~98.4% 的代码
153
+ 存在于周围的子系统中:安全、扩展、上下文管理、委派和持久化。
154
+ - **权衡的体现**:为什么有四种扩展机制(MCP、plugins、skills、hooks)而不是一种?
155
+ 因为每种机制服务于不同的抽象级别和上下文成本。MCP 提供外部工具集成,plugins 打包
156
+ 组件,skills 注入领域指令,hooks 拦截生命周期。这种分层增加了概念复杂性,但允许
157
+ 在不同场景下使用适当的工具。
158
+ - **相关代码**:`src/query.ts`、`src/services/mcp/`、`src/plugins/`、`src/skills/`、
159
+ `src/utils/hooks/`
160
+
161
+ ### 3.4 对抗条件下的权限模型 (Permission Model Under Adversarial Conditions)
162
+
163
+ 当用户(或劫持用户的提示注入)主动尝试规避安全措施时。
164
+
165
+ - **Claude Code 的应对**:拒绝优先 + 深度防御的组合否认了单点失效。即使一个安全层
166
+ 被绕过(例如用户批准了恶意命令),其他层(沙箱、分类器、钩子)仍然可以拦截。
167
+ - **关键弱点**:共享实现约束导致安全层之间存在共性失效模式。例如,权限系统和 UI
168
+ 渲染共享主线程,当规则评估导致 UI 冻结时,两者同时失效。
169
+ - **相关代码**:`src/utils/permissions/permissions.ts`、
170
+ `src/utils/permissions/yoloClassifier.ts`
171
+
172
+ ---
173
+
174
+ ## 4. VersperClaw 与 Claude Code 的差异
175
+
176
+ VersperClaw 以 Claude Code 为上游基础,进行了以下主要变更和扩展:
177
+
178
+ ### 4.1 多 Provider 支持
179
+
180
+ - Claude Code 内置仅支持 Anthropic API,而 VersperClaw 通过 Provider 代理架构
181
+ 支持 OpenAI、Groq、DeepSeek 以及所有兼容 OpenAI 的 API。
182
+ - **实现模式**:`src/server/proxy/handler.ts` 实现双路由决策——
183
+ - 1P Anthropic 路径:直接调用 Anthropic SDK
184
+ - 3P Provider 路径:通过协议转换器(`anthropicToOpenaiChat.ts` → upstream API →
185
+ `openaiChatToAnthropic.ts`)
186
+ - **架构影响**:代理层引入额外的延迟和错误处理复杂度,但使得系统不受单一供应商限制。
187
+
188
+ ### 4.2 Friend VRM 系统
189
+
190
+ - 同进程 VRM 伴侣服务,使用 3D 虚拟角色(VRM 格式)作为交互界面。
191
+ - **实现模式**:`src/friend/FriendService.ts` 是单例服务,与 React 组件通过
192
+ `subscribe()` / `subscribeToInbound()` 模式同步。使用 SSE 广播将表情/TTS 推送到
193
+ VRM 前端。
194
+ - **关键组件**:
195
+ - Silero VAD:WASM 推理(`src/friend/voice/vad-service.ts`),通过 onnxruntime-web
196
+ 实现机器学习级语音活动检测
197
+ - 进程内音频捕获:cpal Rust 库(替代传统的 arecord/parecord 子进程)
198
+ - TTS 引擎:Edge TTS(默认)和 Qwen TTS(DashScope API)
199
+ - **架构意义**:展示了如何将 Claude Code 的扩展机制(钩子 + 工具)用于非开发场景,
200
+ 将编码代理转变为通用对话代理。
201
+
202
+ ### 4.3 移除 ant-internal 模块
203
+
204
+ - 移除了 Anthropic 内部使用的模块(feature flags、内部 API),使代码对社区完全可用。
205
+ - `src/query/transitions.ts` 等文件使用代理桩 (proxy stub) 替代缺少的内部模块,
206
+ 通过 `bun:bundle` 的 DCE 在构建时消除。
207
+
208
+ ### 4.4 社区贡献
209
+
210
+ - **onnxruntime-web WASM VAD**:首个在生产级 CLI 工具中集成基于 ML 的语音活动检测。
211
+ - **同进程 Friend 服务**:无需独立后台服务器子进程,简化了部署架构。
212
+
213
+ ---
214
+
215
+ ## 5. 关键架构模式 (Key Architecture Patterns)
216
+
217
+ ### 5.1 单一主循环模式 (Single Main Loop)
218
+
219
+ **实现**:`queryLoop()` 是 `src/query.ts` 中的 AsyncGenerator,约 88 行核心控制逻辑
220
+ (while-true),周围 ~98.4% 的代码是确定性基础设施。
221
+
222
+ **循环结构**(简化的伪代码):
223
+
224
+ ```
225
+ while (true) {
226
+ 1. 解构状态 (destructure state)
227
+ 2. 压缩管道 (5 shapers: budget → snip → microcompact → collapse → auto-compact)
228
+ 3. 调用模型 (for await over deps.callModel)
229
+ 4. 工具派发 (StreamingToolExecutor 或 runTools)
230
+ 5. 收集结果 → 更新状态 → 继续或终止
231
+ }
232
+ ```
233
+
234
+ **架构意义**:
235
+ - 生成器模式实现了流式输出,同时保持单一同步控制流。
236
+ - 七个"继续点"(continue sites)各自通过一次整体对象赋值(而不是逐个字段变更)
237
+ 来更新状态,保持了不变性的简单性。
238
+ - 所有入口(交互式 CLI、headless CLI、SDK、IDE 集成)汇聚到同一个 queryLoop,
239
+ 只有 UI/渲染层不同。
240
+
241
+ ### 5.2 追加日志模式 (Append-Only Log Pattern)
242
+
243
+ **实现**:`src/utils/sessionStorage.ts` 将会话转录存储为 JSONL 文件(每行一个 JSON
244
+ 事件)。
245
+
246
+ **核心选择**:
247
+ - 状态变更使用**追加写入**而非原地修改
248
+ - 子代理对话存储在单独的**侧链文件**中(`sessionStorage.ts:247`),避免膨胀父上下文
249
+ - 恢复/复刻操作从事务重建会话状态(`conversationRecovery.ts`)
250
+
251
+ **架构意义**:
252
+ - 写前日志 (Write-Ahead Log) 风格使得审计、调试和恢复成为一等公民。
253
+ - 但不在恢复时还原会话级权限——这是一个有意的设计选择,牺牲便利以换取安全
254
+ (防止权限状态被意外恢复)。
255
+
256
+ ### 5.3 分层安全模式 (Layered Security Pattern)
257
+
258
+ **实现**:7 层独立安全机制:
259
+
260
+ | 层 | 机制 | 源文件 | 作用时机 |
261
+ |----|------|--------|---------|
262
+ | 1 | 工具预过滤 | `tools.ts` (filterToolsByDenyRules) | 模型调用前 |
263
+ | 2 | 拒绝优先规则 | `permissions.ts` (toolMatchesRule) | 工具派发时 |
264
+ | 3 | 权限模式约束 | `PermissionMode.ts` | 模式切换时 |
265
+ | 4 | Auto-mode 分类器 | `yoloClassifier.ts` | auto 模式下 |
266
+ | 5 | Shell 沙箱 | `shouldUseSandbox.ts` | Bash 执行前 |
267
+ | 6 | 恢复时不恢复权限 | `conversationRecovery.ts` | 会话恢复时 |
268
+ | 7 | Hook 拦截 | `types/hooks.ts` | 工具生命周期各点 |
269
+
270
+ **架构意义**:
271
+ - 任何单层都不能完全信任——深度防御假设每层都可能失效,但多层同时失效的概率降低。
272
+ - 层之间共享实现约束(例如,超过 50 个子命令的命令退回到通用审批,因为逐子命令
273
+ 解析导致 UI 冻结)。
274
+
275
+ ### 5.4 渐进式上下文管理 (Progressive Context Management)
276
+
277
+ **实现**:5 层压缩管道,每层有不同成本效益比:
278
+
279
+ ```
280
+ Budget Reduction (工具结果大小限制)
281
+ → Snip (轻量级历史修剪)
282
+ → Microcompact (细粒度缓存感知压缩)
283
+ → Context Collapse (只读投影,不改变存储)
284
+ → Auto-compact (模型生成的语义摘要,最后手段)
285
+ ```
286
+
287
+ **决策顺序**:更早、更轻量的层先运行。只有当前置层不足以将上下文降到阈值以下时,
288
+ 才触发更重的层。
289
+
290
+ **架构意义**:
291
+ - 没有单一压缩策略能应对所有类型的上下文压力。
292
+ - Budget 针对单个工具输出溢出;Snip 处理时间深度;Microcompact 应对缓存开销;
293
+ Context Collapse 管理超长历史;Auto-compact 执行语义压缩。
294
+ - 同样的稀缺性思维体现在其他子系统:CLAUDE.md 懒加载、延迟工具模式、子代理仅返回
295
+ 摘要。
296
+
297
+ ### 5.5 流式工具执行 (Streaming Tool Execution)
298
+
299
+ **实现**:`src/services/tools/StreamingToolExecutor.ts`
300
+
301
+ - 工具在模型流式响应时就开始执行(不是等待完整响应)
302
+ - 只读操作可以并行执行;写操作(如 Bash 命令)串行化
303
+ - 兄弟终止控制器:当任何 Bash 工具出错时立即终止其他进行中的子进程
304
+ - 结果按工具发出顺序缓冲和发射,即使并行执行也保持顺序一致性
305
+
306
+ **架构意义**:
307
+ - 介于完全串行派发和激进推测执行(如 PASTE)之间的中间方案
308
+ - 在延迟降低和实现简单性之间取得平衡
309
+
310
+ ---
311
+
312
+ ## 6. 延伸阅读
313
+
314
+ - **Dive into Claude Code** (arXiv:2604.14228v1): 对本文档所基于的原始架构分析论文
315
+ - **Anthropic Safe Agents Framework**: 安全代理设计的原则文档
316
+ - **Claude Code 官方文档**: [https://code.claude.com/docs/](https://code.claude.com/docs/)
317
+ - **VersperClaw 源代码**: `/home/yuki/Code/Agent/VersperClaw/src/`
318
+ - 核心循环: `src/query.ts`
319
+ - 权限系统: `src/utils/permissions/`
320
+ - 压缩管道: `src/services/compact/`
321
+ - 扩展机制: `src/services/mcp/`, `src/utils/hooks/`
322
+ - 状态持久化: `src/utils/sessionStorage.ts`
323
+ - 多 Provider: `src/server/proxy/`
324
+ - Friend VRM: `src/friend/`
325
+
326
+ ---
327
+
328
+ > **文档版本**: v1.0 — 2026-06-22
329
+ > **作者**: 基于 Claude Code 设计哲学论文和 VersperClaw 源代码综合分析
docs/architecture/provider-auth.md ADDED
@@ -0,0 +1,646 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 多 Provider 认证与协议转换架构
2
+
3
+ > 本文档描述 VersperClaw (cc-haha) 的多 Provider 认证架构、OAuth 2.0 流程、API 协议转换机制以及 Provider 配置管理体系。
4
+ > 代码库:`/home/yuki/Code/Agent/VersperClaw`
5
+
6
+ ---
7
+
8
+ ## 1. 支持的认证提供者
9
+
10
+ VersperClaw 支持以下 Provider,按认证方式与协议类型分类:
11
+
12
+ | Provider | 认证方式 | 协议格式 | 实现位置 |
13
+ |---|---|---|---|
14
+ | **Anthropic (first-party)** | OAuth 2.0 + PKCE 或 API Key | Anthropic Messages | `src/services/oauth/`, `src/services/api/client.ts` |
15
+ | **Anthropic Bedrock** | AWS STS / IAM 凭证 | Anthropic Messages (SDK) | `src/services/api/client.ts` (条件导入) |
16
+ | **Anthropic Vertex AI** | GCP google-auth-library | Anthropic Messages (SDK) | `src/services/api/client.ts` (条件导入) |
17
+ | **Anthropic Foundry (Azure)** | API Key 或 Azure AD | Anthropic Messages (SDK) | `src/services/api/client.ts` (条件导入) |
18
+ | **OpenAI / Codex** | API Key 或 Codex OAuth | `openai_chat` 或 `openai_responses` | `src/server/services/openaiOfficialProvider.ts` |
19
+ | **OpenRouter** | API Key | `openai_responses` (Proxy) | `src/utils/model/providers.ts` |
20
+ | **OpenCode Zen** | API Key 或免费 (public) | `openai_chat` (Fetch Override) | `src/services/api/opencodeClient.ts` |
21
+ | **NVIDIA NIM** | API Key (build.nvidia.com) | `openai_chat` (Fetch Override) | `src/services/api/nvidiaClient.ts` |
22
+ | **Local (Ollama/LM Studio/vLLM)** | 无 (Dummy Key) | `anthropic` 或 `openai_chat` | `src/server/config/providerPresets.json` |
23
+ | **DeepSeek, Zhipu GLM, Kimi, MiniMax, 接口AI, 胜算云** | API Key (auth_token) | `anthropic` (Native) | `src/server/config/providerPresets.json` |
24
+
25
+ ---
26
+
27
+ ## 2. 认证架构模式
28
+
29
+ ### 2.1 两级架构总览
30
+
31
+ VersperClaw 拥有两套独立的 Provider 系统,设计目标不同:
32
+
33
+ ```
34
+ Tier 1: TUI 内置 Provider (原始 Claude Code)
35
+ 用途: 终端 /login 快速切换
36
+ 存储: ~/.claude.json (单字段 authProvider)
37
+ 实现: src/services/api/client.ts + src/utils/model/providers.ts
38
+
39
+ Tier 2: cc-haha Provider 预设系统 (VersperClaw 扩展)
40
+ 用途: 桌面端多 Provider 管理、预设配置、Proxy 转换
41
+ 存储: ~/.claude/cc-haha/providers.json (结构化索引)
42
+ 实现: src/server/services/providerService.ts
43
+ src/server/config/providerPresets.ts + .json
44
+ ```
45
+
46
+ 两个层级通过 `ProviderService.autoImportTuiProvider()` 自动同步:当桌面端检测到 TUI 已配置 Provider 但自身尚无活跃 Provider 时,自动导入 TUI 的 Provider 配置。
47
+
48
+ ### 2.2 API Provider 类型定义
49
+
50
+ Provider 类型定义在 `src/utils/model/providers.ts`:
51
+
52
+ ```typescript
53
+ export type APIProvider =
54
+ | 'firstParty'
55
+ | 'openrouter'
56
+ | 'openai'
57
+ | 'local'
58
+ | 'opencode'
59
+ | 'nvidia'
60
+ | 'bedrock'
61
+ | 'vertex'
62
+ | 'foundry'
63
+ ```
64
+
65
+ Provider 检测优先级:
66
+ 1. **显式环境变量覆盖** (`CLAUDE_CODE_API_PROVIDER` 或 `BETTER_CLAWD_API_PROVIDER`)
67
+ 2. **SDK 标志变量** (`CLAUDE_CODE_USE_BEDROCK`, `CLAUDE_CODE_USE_VERTEX`, `CLAUDE_CODE_USE_FOUNDRY`)
68
+ 3. **配置检测** (`isOpencodeConfigured()`, `isNvidiaConfigured()`, `isOpenAIConfigured()`, `isOpenRouterConfigured()`)
69
+ 4. **文件缓存** (`~/.claude.json` 中的 `authProvider` 字段)
70
+
71
+ ```mermaid
72
+ flowchart TD
73
+ A[getAPIProvider] --> B{显式 env 覆盖?}
74
+ B -->|是| C[返回覆盖值]
75
+ B -->|否| D{SDK 标志变量?}
76
+ D -->|Bedrock/Vertex/Foundry| E[返回 SDK 值]
77
+ D -->|否| F{配置检测}
78
+ F --> Opencode --> G["opencode"]
79
+ F --> NVIDIA --> H["nvidia"]
80
+ F --> OpenAI --> I["openai"]
81
+ F --> OpenRouter --> J["openrouter"]
82
+ F --> 缓存文件 --> K["firstParty / 缓存值"]
83
+ ```
84
+
85
+ ### 2.3 Anthropic OAuth 2.0 with PKCE
86
+
87
+ Anthropic first-party 认证使用标准的 OAuth 2.0 Authorization Code Flow + PKCE,完整流程:
88
+
89
+ ```
90
+ ┌─────────┐ ┌──────────────┐ ┌───────────────┐ ┌────────────┐
91
+ │ CLI │ │ localhost │ │ Anthropic │ │ Browser │
92
+ │ │ │ OAuth Server │ │ OAuth Provider│ │ (User) │
93
+ └────┬────┘ └──────┬───────┘ └───────┬───────┘ └─────┬──────┘
94
+ │ 1. start() │ │ │
95
+ │────────────────>│ │ │
96
+ │ 2. port │ │ │
97
+ │<────────────────│ │ │
98
+ │ │ │ │
99
+ │ 3. generateCodeVerifier(), challenge() │ │
100
+ │ 4. buildAuthUrl() │ │
101
+ │ │ │ │
102
+ │ 5. openBrowser(automaticUrl) │ │
103
+ │───────────────────────────────────────────────────────────>
104
+ │ │ │ │
105
+ │ │ 6. HTTP GET /callback?code=...&state= │
106
+ │ │<─────────────────────────────────────── │
107
+ │ │ │ │
108
+ │ 7. validate state, extract code │ │
109
+ │ 8. exchangeCodeForTokens(code) │ │
110
+ │───────────────────────────────────────>│ │
111
+ │ │ 9. access_token + refresh_token │
112
+ │<───────────────────────────────────────│ │
113
+ │ │ │ │
114
+ │10. store tokens, set API key to null │ │
115
+ │11. fetchProfileInfo() - 获取订阅类型 │ │
116
+ ```
117
+
118
+ **核心文件:**
119
+
120
+ | 职责 | 文件 |
121
+ |---|---|
122
+ | OAuth 服务入口 | `src/services/oauth/index.ts` - `OAuthService` 类 |
123
+ | PKCE 加密工具 | `src/services/oauth/crypto.ts` - SHA-256 code challenge |
124
+ | 授权码监听 | `src/services/oauth/auth-code-listener.ts` - 本地 HTTP 服务器 |
125
+ | Token 交换与刷新 | `src/services/oauth/client.ts` - axios POST 到 token endpoint |
126
+ | Profile 获取 | `src/services/oauth/getOauthProfile.ts` |
127
+ | OAuth 类型定义 | `src/services/oauth/types.ts` |
128
+ | OAuth 端点配置 | `src/constants/oauth.ts` - prod/staging/local 三级 |
129
+
130
+ **OAuth 端点配置** (`src/constants/oauth.ts`) 支持三层环境:
131
+
132
+ - **Production**: `api.anthropic.com`, `platform.claude.com`
133
+ - **Staging**: `api-staging.anthropic.com`, `platform.staging.ant.dev` (仅 ant 内部)
134
+ - **Local**: 可配置的 localhost 端口 (用于本地开发)
135
+ - **Custom (FedStart)**: 受限的白名单 Base URL 覆盖
136
+
137
+ ---
138
+
139
+ ## 3. Protocol Translation (Fetch Override 模式)
140
+
141
+ ### 3.1 核心问题
142
+
143
+ Claude Code 使用的 `@anthropic-ai/sdk` 只认识 Anthropic Messages API 格式。对于使用 OpenAI 协议的非 Anthropic Provider,必须进行协议转换。
144
+
145
+ ```
146
+ Anthropic Messages API ←→ OpenAI Chat Completions / Responses API
147
+ ────────────────────── ─────────────────────────────────────
148
+ POST /v1/messages POST /v1/chat/completions
149
+ POST /v1/messages?stream POST /v1/chat/completions?stream=true
150
+ GET /v1/models GET /v1/models
151
+ POST /v1/count_tokens (无对应端点,需要 Stub)
152
+ ```
153
+
154
+ ### 3.2 Fetch Override 机制
155
+
156
+ 每个非 Anthropic Provider 实现一个 `createXxxFetchOverride()` 函数,通过 Anthropic SDK 的 `ClientOptions['fetch']` 钩子注入:
157
+
158
+ ```typescript
159
+ export function createNvidiaFetchOverride():
160
+ (input: RequestInfo | URL, init?: RequestInit) => Promise<Response>
161
+ ```
162
+
163
+ 该重写在 `getAnthropicClient()` 中按 Provider 选择性注入:
164
+
165
+ ```typescript
166
+ // src/services/api/client.ts 第 150-163 行
167
+ const provider = getAPIProvider()
168
+ if (provider === 'opencode') {
169
+ opencodeFetchOverride = createOpenCodeFetchOverride(resolvedModel)
170
+ }
171
+ if (provider === 'nvidia') {
172
+ nvidiaFetchOverride = createNvidiaFetchOverride()
173
+ }
174
+ const resolvedFetch = buildFetch(fetchOverride || opencodeFetchOverride || nvidiaFetchOverride, source)
175
+ ```
176
+
177
+ ### 3.3 Fetch Override 通用模式
178
+
179
+ 所有 Fetch Override 实现共享相同的拦截模式:
180
+
181
+ ```mermaid
182
+ flowchart LR
183
+ A[SDK 发出请求] --> B{URL 匹配?}
184
+ B -->|/messages 或 /v1/| C{端点类型}
185
+ B -->|其他| D[透传 fetch]
186
+ C -->|/count_tokens| E[Stub: 返回 0]
187
+ C -->|/models| F[Stub: 返回空列表]
188
+ C -->|/messages| G[解析 Anthropic Body]
189
+ G --> H[转换消息格式]
190
+ H --> I[附加 Provider Auth Header]
191
+ I --> J[调用上游 API]
192
+ J --> K{流式?}
193
+ K -->|是| L[转换 SSE 流]
194
+ K -->|否| M[转换响应体]
195
+ L --> N[返回 Anthropic 格式 Response]
196
+ M --> N
197
+ ```
198
+
199
+ ### 3.4 消息格式转换
200
+
201
+ 核心转换函数族位于 `src/services/api/copilotClient.ts`:
202
+
203
+ ```
204
+ Anthropic → OpenAI:
205
+ convertAnthropicMessagesToOpenAI(messages, systemPrompt)
206
+ - system → role: 'system' 消息
207
+ - image block → image_url (data: URI)
208
+ - tool_result → role: 'tool' 消息
209
+ - tool_use → tool_calls 数组
210
+ - thinking → reasoning_content (提供商扩展)
211
+
212
+ convertAnthropicToolsToOpenAI(tools)
213
+ - { name, description, input_schema } → { type: 'function', function: { ... } }
214
+
215
+ OpenAI → Anthropic (流式):
216
+ convertOpenAIStreamToAnthropic(openaiStream, model)
217
+ - SSE data: {"choices":[{ "delta":{ "content":"..." } }]}
218
+ → event: content_block_delta\ndata: {"delta":{"type":"text_delta","text":"..."}}
219
+
220
+ OpenAI → Anthropic (非流式):
221
+ - choices[0].message.content → content: [{ type: 'text', text: ... }]
222
+ - tool_calls → tool_use content blocks
223
+ - finish_reason 'stop' → 'end_turn'
224
+ - finish_reason 'tool_calls' → 'tool_use'
225
+ ```
226
+
227
+ **Streaming SSE 事件对照表:**
228
+
229
+ | Anthropic (输出) | OpenAI (输入) |
230
+ |---|---|
231
+ | `message_start` | 合成,包含 message ID |
232
+ | `content_block_start` | 首次 `delta.content` 或 `delta.tool_calls.id` |
233
+ | `content_block_delta` text_delta | `delta.content` |
234
+ | `content_block_delta` thinking_delta | `delta.reasoning_content` |
235
+ | `content_block_start` tool_use | `delta.tool_calls[].id` |
236
+ | `content_block_delta` tool_use_delta | `delta.tool_calls[].function.arguments` |
237
+ | `content_block_stop` | delta 结束 / tool_call 完成 |
238
+ | `message_delta` | `choices[0].finish_reason` |
239
+ | `message_stop` | `[DONE]` |
240
+
241
+ ### 3.5 服务端 Proxy 模式
242
+
243
+ 除了客户端的 Fetch Override 模式,VersperClaw 还实现了一套**服务端 Proxy**,位于 `src/server/proxy/handler.ts`。
244
+
245
+ 设计目标:将协议转换逻辑从客户端分离到独立 HTTP 服务,支持更灵活的 Provider 管理。
246
+
247
+ ```
248
+ CLI/SDK Proxy Server Upstream
249
+ │ │ │
250
+ │ POST /proxy/v1/messages │ │
251
+ │ (Anthropic 格式请求) │ │
252
+ │ ───────────────────────────────────────> │ │
253
+ │ │ 获取 Provider 配置 │
254
+ │ │ 读取 baseUrl, apiKey, apiFormat │
255
+ │ │ │
256
+ │ │ anthropicToOpenaiChat(body) │
257
+ │ │ ─ 或 ─ │
258
+ │ │ anthropicToOpenaiResponses(body) │
259
+ │ │ │
260
+ │ │ POST /v1/chat/completions │
261
+ │ │ 或 POST /v1/responses │
262
+ │ │ ──────────────────────────────────> │
263
+ │ │ │
264
+ │ │ openaiChatToAnthropic(res) │
265
+ │ │ 或 openaiResponsesToAnthropic(res) │
266
+ │ │ (流式: XxxStreamToAnthropic) │
267
+ │ │ │
268
+ │ ← Anthropic 格式响应 │ │
269
+ │ <─────────────────────────────────────── │ │
270
+ ```
271
+
272
+ **服务端 Proxy 转换文件结构:**
273
+
274
+ ```
275
+ src/server/proxy/
276
+ ├── handler.ts # 主入口: POST 分发 + 错误处理
277
+ ├── transform/
278
+ │ ├── types.ts # Anthropic & OpenAI 类型定义
279
+ │ ├── anthropicToOpenaiChat.ts # 请求转换: Messages → Chat Completions
280
+ │ ├── anthropicToOpenaiResponses.ts # 请求转换: Messages → Responses API
281
+ │ ├── openaiChatToAnthropic.ts # 响应转换: Chat Completions → Messages
282
+ │ ├── openaiResponsesToAnthropic.ts # 响应转换: Responses API → Messages
283
+ │ └── toolArguments.ts # Tool 参数格式修正工具
284
+ └── streaming/
285
+ ├── openaiChatStreamToAnthropic.ts # 流式转换: Chat Completions SSE
286
+ ├── openaiResponsesStreamToAnthropic.ts # 流式转换: Responses API SSE
287
+ └── openaiResponsesStreamToAnthropicResponse.ts
288
+ ```
289
+
290
+ ### 3.6 DeepSeek 推理兼容性
291
+
292
+ Proxy 还处理 DeepSeek 的特殊格式:
293
+
294
+ ```typescript
295
+ function shouldUseDeepSeekReasoningCompat(baseUrl: string): boolean {
296
+ return /(^|[./-])deepseek([./-]|$)/i.test(baseUrl) ||
297
+ /(^|[./-])opencode\.ai([:/]|$)/i.test(baseUrl)
298
+ }
299
+ ```
300
+
301
+ 启用后,**thinking blocks** 会通过 `reasoning_content` 字段回传,���且 Anthropic 的 `thinking.type` 会被转换为 DeepSeek 兼容格式。
302
+
303
+ ---
304
+
305
+ ## 4. 各 Provider 集成详解
306
+
307
+ ### 4.1 NVIDIA NIM
308
+
309
+ **文件**: `src/services/api/nvidiaClient.ts`
310
+
311
+ - **认证**: `getNvidiaApiKey()` → 写入 `Authorization: Bearer <key>` 请求头
312
+ - **特殊头**: `HTTP-Referer: https://claude.ai/`, `X-BILLING-INVOKE-ORIGIN: Better-Clawd`
313
+ - **端点**: `{baseUrl}/v1/chat/completions` (默认 `https://integrate.api.nvidia.com/v1`)
314
+ - **Model 列表**: 从 `/v1/models` 动态拉取,缓存于 `cachedNvidiaModels` 模块变量
315
+ - **默认 Model**: `nvidia/llama-3.1-nemotron-70b-instruct` (可通过 `NVIDIA_MODEL` 环境变量覆盖)
316
+
317
+ ### 4.2 OpenCode Zen
318
+
319
+ **文件**: `src/services/api/opencodeClient.ts`
320
+
321
+ - **认证**: 支持 API Key 和匿名免费使用
322
+ - **免费模式**: 当 `apiKey` 为 `undefined` 或 `'public'` 时,注入 billing 特征码 (x-anthropic-billing-header)
323
+ - **端点**: `https://opencode.ai/zen/v1/chat/completions`
324
+ - **Model 发现**:
325
+ - 从 `https://models.dev/api.json` 动态获取模型元数据 (云端成本策略)
326
+ - 从 `https://api.github.com/repos/anomalyco/opencode/releases/latest` 获取版本信息
327
+ - 缓存于 `cachedModels` 模块变量
328
+ - **动态 UA**: 根据版本和运行时自动构建 `User-Agent`
329
+ - **推理内容**: 支持 `reasoning_content` 到 `thinking` block 的转换
330
+
331
+ ### 4.3 OpenAI / Codex Official
332
+
333
+ **文件**: `src/server/services/openaiOfficialProvider.ts`
334
+
335
+ - **认证**: Codex OAuth (通过 `hahaOpenAIOAuthService`)
336
+ - **协议**: `openai_responses` (Responses API)
337
+ - **运行时种类**: `openai_oauth` (需要 Token 刷新)
338
+ - **Base URL**: `/backend-api/codex` (从 `OPENAI_CODEX_API_ENDPOINT` 派生)
339
+ - **Model 列表**: 来自 `src/services/openaiAuth/models.ts` 的 `OPENAI_CODEX_MODEL_CATALOG`
340
+
341
+ ### 4.4 自定义 OpenAI 兼容 Provider
342
+
343
+ **文件**: `src/services/api/customOpenAIClient.ts`
344
+
345
+ - **认证**: `ConnectedProviderInfo.apiKey` → `Bearer <key>`
346
+ - **协议**: `openai_chat` (Chat Completions)
347
+ - **Model**: 使用 `custom-openai:<modelId>` 前缀选择
348
+ - **Model 拉取**: `fetchOpenAICompatibleModelIds()` 从 `/v1/models` 获取
349
+ - **适用场景**: vLLM, Together AI, Groq 等任何 OpenAI 兼容端点
350
+
351
+ ### 4.5 GitHub Copilot
352
+
353
+ **文件**: `src/services/api/copilotClient.ts` (原 copilotClient,现已演化为通用转换库)
354
+
355
+ - **认证**: OAuth Token (通过 `connectedProviders['github-copilot']`)
356
+ - **协议**: `openai_chat`
357
+ - **端点**: `https://api.githubcopilot.com/chat/completions`
358
+ - **特殊头**: `Openai-Intent: conversation-edits`, `x-initiator: user`
359
+ - **Model 发现**: 双源策略 — 优先从 Copilot API 获取,fallback 到 `models.dev/api.json`
360
+ - **兼容性缓存**: `copilotCompatibilityCache` 持久化模型兼容性信息到 `~/.claude.json`
361
+ - **Token 参数自动修复**: 当 API 返回 `"Use 'max_completion_tokens' instead"` 时自动切换参数
362
+
363
+ ---
364
+
365
+ ## 5. Provider 预设系统 (cc-haha)
366
+
367
+ ### 5.1 预设配置
368
+
369
+ Provider 预设定义在 `src/server/config/providerPresets.json`,每个预设包含:
370
+
371
+ ```json
372
+ {
373
+ "id": "deepseek",
374
+ "name": "DeepSeek",
375
+ "baseUrl": "https://api.deepseek.com/anthropic",
376
+ "apiFormat": "anthropic",
377
+ "defaultModels": {
378
+ "main": "deepseek-v4-pro",
379
+ "haiku": "deepseek-v4-flash",
380
+ "sonnet": "deepseek-v4-pro",
381
+ "opus": "deepseek-v4-pro"
382
+ },
383
+ "needsApiKey": true,
384
+ "authStrategy": "auth_token",
385
+ "defaultEnv": {
386
+ "ANTHROPIC_DEFAULT_HAIKU_MODEL_SUPPORTED_CAPABILITIES": "thinking,effort,..."
387
+ },
388
+ "modelContextWindows": {
389
+ "deepseek-v4-pro": 1000000
390
+ }
391
+ }
392
+ ```
393
+
394
+ **预设支持的 Provider (截至当前):**
395
+
396
+ | Preset | API Format | Auth Strategy |
397
+ |---|---|---|
398
+ | official | anthropic | — |
399
+ | deepseek | anthropic | auth_token |
400
+ | zhipuglm | anthropic | auth_token |
401
+ | kimi | anthropic | auth_token |
402
+ | minimax | anthropic | auth_token |
403
+ | jiekouai | anthropic | auth_token |
404
+ | shengsuanyun | anthropic | auth_token |
405
+ | lmstudio | anthropic | auth_token_empty_api_key |
406
+ | ollama | anthropic | auth_token_empty_api_key |
407
+ | nvidia | openai_chat | api_key |
408
+ | custom | anthropic | auth_token |
409
+
410
+ ### 5.2 认证策略 (`ProviderAuthStrategy`)
411
+
412
+ ```typescript
413
+ type ProviderAuthStrategy =
414
+ | 'api_key' // x-api-key: <key> (NVIDIA)
415
+ | 'auth_token' // Authorization: Bearer <key> (DeepSeek, Zhipu, Kimi...)
416
+ | 'auth_token_empty_api_key' // Bearer + dummy x-api-key (LM Studio, Ollama)
417
+ | 'dual_same_token' // x-api-key + Bearer 同一值
418
+ | 'dual_dummy' // x-api-key: dummy + Bearer: dummy (OpenAI OAuth)
419
+ ```
420
+
421
+ ### 5.3 API Format
422
+
423
+ ```typescript
424
+ type ApiFormat =
425
+ | 'anthropic' // 原生 Anthropic Messages API (直连,无需 Proxy)
426
+ | 'openai_chat' // OpenAI Chat Completions /v1/chat/completions
427
+ | 'openai_responses' // OpenAI Responses API /v1/responses
428
+ ```
429
+
430
+ ### 5.4 存储结构
431
+
432
+ cc-haha 的 Provider 数据存储在:
433
+
434
+ ```
435
+ ~/.claude/cc-haha/
436
+ ├── providers.json # Provider 索引 (活跃 ID + Provider 列表)
437
+ └── settings.json # 同步到 SDK 的环境变量
438
+ ```
439
+
440
+ 迁移历史通过 `persistentStorageMigrations.ts` 管理,使用 `CURRENT_PROVIDER_INDEX_SCHEMA_VERSION` 追踪 schema 版本。
441
+
442
+ ---
443
+
444
+ ## 6. Model 管理与切换
445
+
446
+ ### 6.1 Model 解析链
447
+
448
+ ```
449
+ src/utils/model/
450
+ ├── providers.ts → getAPIProvider() - 确定当前 Provider
451
+ ├── modelStrings.ts → getModelStrings() - 解析 Provider 对应的 Model ID
452
+ ├── model.ts → getMainLoopModel() - 最终选择的 Model
453
+ └── configs.ts → ALL_MODEL_CONFIGS - 每个 Model 在各 Provider 的映射
454
+ ```
455
+
456
+ **Model 字符串解析流程:**
457
+
458
+ ```mermaid
459
+ flowchart TD
460
+ A[getModelStrings] --> B{STATE.modelStrings 已缓存?}
461
+ B -->|是| C[返回缓存值]
462
+ B -->|否| D{Provider 类型}
463
+ D -->|firstParty| E[ALL_MODEL_CONFIGS 默认值]
464
+ D -->|openai| F[firstParty 默认 + openai 覆盖]
465
+ D -->|opencode| G[firstParty 默认 + opencode 覆盖]
466
+ D -->|openrouter| H[firstParty 默认 + openrouter 覆盖]
467
+ D -->|nvidia| I[所有模型 = NVIDIA_MODEL env / 默认]
468
+ D -->|bedrock| J[拉取 Bedrock 推理配置 → 匹配]
469
+ E --> K[applyModelOverrides]
470
+ F --> K
471
+ G --> K
472
+ H --> K
473
+ I --> K
474
+ J --> K
475
+ K --> L[返回 ModelStrings]
476
+ ```
477
+
478
+ ### 6.2 Provider 切换时的缓存清除
479
+
480
+ **重要**:切换 Provider 后必须调用 `clearModelStrings()`,否则旧的 Model ID 会持续生效:
481
+
482
+ ```typescript
483
+ // src/utils/model/modelStrings.ts
484
+ export function clearModelStrings(): void {
485
+ setModelStringsState(null as unknown as ModelStrings)
486
+ }
487
+ ```
488
+
489
+ ### 6.3 Model Context Windows
490
+
491
+ 两种方式配置模型上下文窗口:
492
+
493
+ 1. **Preset 预设** (`providerPresets.json` 中的 `modelContextWindows` 字段)
494
+ 2. **用户覆盖** (`~/.claude/cc-haha/providers.json` 中的 `modelContextWindows` 字段)
495
+
496
+ ---
497
+
498
+ ## 7. Provider 运行时环境
499
+
500
+ ### 7.1 运行环境构建
501
+
502
+ `ProviderService.syncToSettings()` 将活跃 Provider 的配置写入 `~/.claude/cc-haha/settings.json`:
503
+
504
+ ```
505
+ ANTHROPIC_BASE_URL=http://localhost:port/proxy/providers/<id>
506
+ ANTHROPIC_AUTH_TOKEN=dummy # 用于 Anthropic SDK 认证
507
+ ANTHROPIC_API_KEY=dummy # 同上
508
+ API_TIMEOUT_MS=300000
509
+ ANTHROPIC_MODEL=<model-id>
510
+ ANTHROPIC_DEFAULT_HAIKU_MODEL=...
511
+ ANTHROPIC_DEFAULT_SONNET_MODEL=...
512
+ ANTHROPIC_DEFAULT_OPUS_MODEL=...
513
+ MODEL_CONTEXT_WINDOWS={"model-id": 1000000}
514
+ ```
515
+
516
+ ### 7.2 OpenAI OAuth 运行时
517
+
518
+ 对于 OpenAI Official Provider,特殊的环境变量:
519
+
520
+ ```
521
+ CC_HAHA_OPENAI_OAUTH_PROVIDER=1
522
+ OPENAI_CODEX_OAUTH_FILE=<path-to-oauth-file>
523
+ ```
524
+
525
+ ---
526
+
527
+ ## 8. 架构图汇总
528
+
529
+ ```mermaid
530
+ graph TB
531
+ subgraph "Tier 1: TUI 内置 Provider"
532
+ A1[~/.claude.json]
533
+ A2[src/utils/model/providers.ts]
534
+ A3[src/services/api/client.ts]
535
+ A4[src/services/api/nvidiaClient.ts]
536
+ A5[src/services/api/opencodeClient.ts]
537
+ A2 -->|getAPIProvider| A3
538
+ A3 -->|createNvidiaFetchOverride| A4
539
+ A3 -->|createOpenCodeFetchOverride| A5
540
+ end
541
+
542
+ subgraph "Tier 2: cc-haha Provider 系统"
543
+ B1[~/.claude/cc-haha/providers.json]
544
+ B2[~/.claude/cc-haha/settings.json]
545
+ B3[src/server/services/providerService.ts]
546
+ B4[src/server/config/providerPresets.json]
547
+ B5[src/server/proxy/handler.ts]
548
+ B3 -->|读写| B1
549
+ B3 -->|syncToSettings| B2
550
+ B3 -->|activateProvider| B4
551
+ B3 -->|getProviderForProxy| B5
552
+ end
553
+
554
+ subgraph "Anthropic SDK"
555
+ C1[@anthropic-ai/sdk]
556
+ C2[AnthropicBedrock]
557
+ C3[AnthropicVertex]
558
+ C4[AnthropicFoundry]
559
+ end
560
+
561
+ subgraph "协议转换层"
562
+ D1[anthropicToOpenaiChat]
563
+ D2[anthropicToOpenaiResponses]
564
+ D3[openaiChatToAnthropic]
565
+ D4[openaiResponsesToAnthropic]
566
+ D5[openaiChatStreamToAnthropic]
567
+ D6[openaiResponsesStreamToAnthropic]
568
+ D7[copilotClient.ts - 通用转换]
569
+ end
570
+
571
+ subgraph "OAuth 2.0 流程"
572
+ E1[src/services/oauth/index.ts]
573
+ E2[src/services/oauth/client.ts]
574
+ E3[src/services/oauth/crypto.ts]
575
+ E4[src/services/oauth/auth-code-listener.ts]
576
+ E5[src/constants/oauth.ts]
577
+ end
578
+
579
+ A3 -->|第一方| C1
580
+ A3 -->|Bedrock| C2
581
+ A3 -->|Vertex| C3
582
+ A3 -->|Foundry| C4
583
+ A3 -->|Fetch Override| D7
584
+
585
+ B5 --> D1
586
+ B5 --> D2
587
+ B5 --> D3
588
+ B5 --> D4
589
+ B5 --> D5
590
+ B5 --> D6
591
+
592
+ C1 --> E1
593
+ ```
594
+
595
+ ---
596
+
597
+ ## 9. 添加新 Provider 的标准流程
598
+
599
+ 当需要支持一个新的 OpenAI 兼容 Provider 时,按以下步骤操作:
600
+
601
+ 1. **Provider 类型**: 在 `src/utils/model/providers.ts` 的 `APIProvider` 联合类型中添加
602
+ 2. **检测函数**: 实现 `isXxxConfigured()` 并在 `getAPIProvider()` 调用链中加入
603
+ 3. **Model 字符串**: 在 `src/utils/model/modelStrings.ts` 的 `getBuiltinModelStrings()` 中添加映射
604
+ 4. **Fetch Override**: 如果协议需要转换,实现 `createXxxFetchOverride()` (参考 `nvidiaClient.ts`)
605
+ 5. **客户端集成**: 在 `src/services/api/client.ts` 的 `getAnthropicClient()` 中注入 Override
606
+ 6. **预设配置**: 在 `src/server/config/providerPresets.json` 中添加预设项
607
+ 7. **环境变量**: 在 `providerRuntimeEnv.ts` 的 `getManagedEnvKeys()` 中添加变量清理
608
+ 8. **认证策略**: 如果使用非标准认证,在 `buildAnthropicAuthHeaders()` 中添加策略
609
+
610
+ ---
611
+
612
+ ## 10. 关键陷阱与注意事项
613
+
614
+ ### 10.1 Model Strings 缓存
615
+
616
+ **问题**: `getModelStrings()` 缓存 Provider 特定的 Model ID 到 `STATE.modelStrings` 中。切换 Provider 后,如果不调用 `clearModelStrings()`,旧的 Model ID 会持续生效,导致模型解析错误。
617
+
618
+ **解决**: 所有 Provider 切换路径 (`/login`, Provider 激活) 都必须调用 `clearModelStrings()`。
619
+
620
+ ### 10.2 DeepSeek 的 max_tokens 限制
621
+
622
+ Claude Code 默认发送非常大的 `max_tokens` (如 128K),但 DeepSeek 限制为 8192。服务端 Proxy 的 `anthropicToOpenaiChat()` 已省略 `max_tokens` 透传,让上游使用自己的默认值。
623
+
624
+ ### 10.3 OAuth Token 刷新竞态
625
+
626
+ `refreshOAuthToken()` 中有一个关键优化:当全局配置和 Secure Storage 中都有 profile 数据时,跳过 `/api/oauth/profile` 的额外网络请求。但在 `installOAuthTokens` → `performLogout` 的 re-login 路径中,需要穿透缓存以确保订阅类型正确。
627
+
628
+ ### 10.4 Streaming SSE 转义
629
+
630
+ `convertOpenAIStreamToAnthropic()` 中使用 `JSON.stringify(reasoning_content).slice(1, -1)` 来转义内容中的特殊字符。如果 reasoning_content 包含换行符或 Unicode 字符,直接拼接字符串会导致 SSE 格式损坏。
631
+
632
+ ### 10.5 Copilot 的 max_tokens → max_completion_tokens 自动修复
633
+
634
+ Copilot API 对部分模型使用 `max_completion_tokens` 而非 `max_tokens`。`sendCopilotChatCompletion()` 在收到特定错误信息时会自动切换参数并重试,并将兼容性信息缓存 24 小时。
635
+
636
+ ---
637
+
638
+ ## 11. 参考资料
639
+
640
+ - **cc-switch** (原始 Proxy 参考实现): https://github.com/farion1231/cc-switch
641
+ - **Anthropic Messages API**: https://docs.anthropic.com/en/api/messages
642
+ - **OpenAI Chat Completions API**: https://platform.openai.com/docs/api-reference/chat
643
+ - **OpenAI Responses API**: https://platform.openai.com/docs/api-reference/responses
644
+ - **OAuth 2.0 with PKCE**: https://oauth.net/2/pkce/
645
+ - **NVIDIA NIM**: https://build.nvidia.com/docs
646
+ - **OpenCode Zen**: https://opencode.ai
docs/architecture/safety-and-permissions.md ADDED
@@ -0,0 +1,867 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 安全与权限系统深度分析
2
+
3
+ > 本文基于 VersperClaw (Claude Code) 源代码,深入分析其安全架构与权限子系统。
4
+ > 版本参考:commit `835ff5a` / `cdb3bdd`
5
+
6
+ ---
7
+
8
+ ## 1. 设计哲学
9
+
10
+ ### 1.1 Deny-First 原则
11
+
12
+ 系统中所有权限检查的**默认行为是拒绝**。未在规则中明确允许的操作,最终都会向用户发起询问或直接被拒绝。这一原则贯穿整个权限管道(Authorization Pipeline),体现在:
13
+
14
+ - `PermissionResult` 的默认行为是 `ask`(询问用户),而非 `allow`
15
+ - 工具实现的 `checkPermissions()` 方法若返回 `passthrough`,上层会将其转换为 `ask`(`src/utils/permissions/permissions.ts:1300-1310`)
16
+ - `bypassPermissions` 模式是唯一能跳过所有检查的模式,但该模式可以通过 Statsig 门控(`tengu_disable_bypass_permissions_mode`)被完全禁用
17
+
18
+ ### 1.2 人类决策权威
19
+
20
+ 用户始终拥有最终决定权。在任何权限模式下,用户都可以通过终端对话框批准或拒绝操作。即使在 `auto` 模式下,当分类器(Classifier)判定需阻止操作时,用户仍可通过交互式对话框覆盖分类器决定。
21
+
22
+ 系统通过以下机制保障人类决策权威:
23
+
24
+ - **建议系统**(`generateSuggestions`, `src/utils/permissions/filesystem.ts:1414-1473`):当询问用户时,同时提供可操作的建议(如"允许本次会话的所有编辑"、"添加目录到工作区")
25
+ - **权限解释器**(`PermissionExplainer`, `src/utils/permissions/permissionExplainer.ts`):使用 Haiku 模型解释命令的风险等级(LOW/MEDIUM/HIGH)、目的和潜在风险,辅助用户决策
26
+ - **渐进式暴露**:只对高风险操作发起询问,低风险操作在适当模式下自动批准
27
+
28
+ ### 1.3 防御纵深
29
+
30
+ 系统采用**多层重叠的安全机制**,而非依赖单一安全边界。这意味着即使某一层被绕过,后续层仍能提供保护。
31
+
32
+ ```
33
+ ┌─────────────────────────────────────────────────────┐
34
+ │ 防御纵深架构 │
35
+ ├─────────────────────────────────────────────────────┤
36
+ │ 1. 规则系统 (deny/allow/ask rules) │
37
+ │ 2. PreToolUse Hook (用户自定义拦截) │
38
+ │ 3. 路径安全检查 (Dangerous Files, Windows 模式) │
39
+ │ 4. 模式驱动的权限处理 (Mode-based Decision) │
40
+ │ 5. 自动模式分类器 (Auto Mode Classifier) │
41
+ │ 6. 用户确认对话框 (User Confirmation Dialog) │
42
+ │ 7. 沙箱 (Sandbox) 容器化执行 │
43
+ │ 8. 拒绝跟踪 (Denial Tracking) 防滥用 │
44
+ └─────────────────────────────────────────────────────┘
45
+ ```
46
+
47
+ ### 1.4 渐进信任
48
+
49
+ 系统支持用户通过时间建立信任轨迹。随着用户批准更多操作,系统会逐步提高自动化程度:
50
+
51
+ - **默认模式**:auto-approve rate 起始约 20%,每一步都需确认
52
+ - **auto 模式改进**:系统通过拒绝跟踪(`denialTracking.ts`)记录分类器连续拒绝次数,超过阈值(连续 3 次或总计 20 次)后回退到用户询问模式
53
+ - **规则积累**:用户可逐步添加 `alwaysAllow` 规则(如 `Bash(ls:*)`),减少未来对低风险操作的询问
54
+ - **acceptEdits 模式**:一旦用户选择此模式,工作目录内的所有文件编辑操作自动批准
55
+
56
+ ---
57
+
58
+ ## 2. 七种权限模式
59
+
60
+ 权限模式定义在 `src/types/permissions.ts:16-36`,运行时配置在 `src/utils/permissions/PermissionMode.ts:42-91`。
61
+
62
+ ### 模式总览
63
+
64
+ | 模式 | 内部名称 | 符号 | 自动批准 | 需要确认 | 风险等级 |
65
+ |------|----------|------|----------|----------|----------|
66
+ | 默认 | `default` | — | 无 | 所有操作 | 低(最安全) |
67
+ | 接受编辑 | `acceptEdits` | ⏵⏵ | 工作目录内文件编辑 | Bash 命令、目录外写入、MCP | 中低 |
68
+ | 计划 | `plan` | ⏸ | 读操作(文件读取、搜索) | 写操作、Bash 命令 | 低 |
69
+ | 自动 | `auto` | ⏵⏵ | 分类器批准的 + 安全放行列表 | 分类器阻止的 | 中 |
70
+ | 不询问 | `dontAsk` | ⏵⏵ | 无(所有 ask → deny) | 无法操作 | 高 |
71
+ | 绕过权限 | `bypassPermissions` | ⏵⏵ | 所有操作(除安全检查外) | 安全检查(.git/ 等) | 最高 |
72
+ | Bubble | `bubble` | — | Ant 内部使用 | Ant 内部使用 | — |
73
+
74
+ ### 2.1 Default(默认模式)
75
+
76
+ **文件**: `src/utils/permissions/PermissionMode.ts:45-50`
77
+
78
+ ```typescript
79
+ default: {
80
+ title: 'Default',
81
+ shortTitle: 'Default',
82
+ symbol: '',
83
+ color: 'text',
84
+ external: 'default',
85
+ }
86
+ ```
87
+
88
+ - **行为**: 每一步操作都需要用户确认
89
+ - **自动批准**: 无
90
+ - **询问**: 所有工具调用
91
+ - **适用场景**: 新项目、不信任 AI 操作时
92
+ - **风险**: 最低,但效率也最低
93
+
94
+ ### 2.2 AcceptEdits(接受编辑模式)
95
+
96
+ **文件**: `src/utils/permissions/PermissionMode.ts:59-65`
97
+
98
+ ```typescript
99
+ acceptEdits: {
100
+ title: 'Accept edits',
101
+ shortTitle: 'Accept',
102
+ symbol: '⏵⏵',
103
+ color: 'autoAccept',
104
+ external: 'acceptEdits',
105
+ }
106
+ ```
107
+
108
+ - **行为**: 自动批准工作目录内的文件编辑操作(`FileEditTool`, `FileWriteTool`)
109
+ - **自动批准**: 工作目录(`getOriginalCwd()` 已在 `additionalWorkingDirectories` 中的路径)内的文件写入
110
+ - **询问**: Bash 命令、工作目录外的文件写入、MCP 工具、网络操作
111
+ - **实现参考**: `src/utils/permissions/filesystem.ts:1360-1375` — 当 `mode === 'acceptEdits'` 且路径在工作目录内时,直接返回 `allow`
112
+ - **适用场景**: 用户希望 AI 可以直接修改代码,但不想让其执行任意命令
113
+
114
+ ### 2.3 Plan(计划模式)
115
+
116
+ **文件**: `src/utils/permissions/PermissionMode.ts:52-58`
117
+
118
+ ```typescript
119
+ plan: {
120
+ title: 'Plan Mode',
121
+ shortTitle: 'Plan',
122
+ symbol: PAUSE_ICON,
123
+ color: 'planMode',
124
+ external: 'plan',
125
+ }
126
+ ```
127
+
128
+ - **行为**: 只读模式 + 计划讨论
129
+ - **自动批准**: 所有读操作(文件读取、搜索、列表等)
130
+ - **询问**: 文件编辑、Bash 命令、网络操作
131
+ - **内部机制**: 当从 auto 模式进入 plan 时,auto 分类器仍在后台运行(`prePlanMode` 记录),退出 plan 时恢复 auto 状态(`src/utils/permissions/permissionSetup.ts:1462-1493`)
132
+ - **适用场景**: 探索代码库、制定重构计划、代码审查
133
+
134
+ **与 Auto 模式的联动**(`permissionSetup.ts:1446-1455`):
135
+
136
+ 当用户已选择加入 auto 模式且 `useAutoModeDuringPlan` 启用时,plan 模式下分类器仍处于激活状态。这通过 `shouldPlanUseAutoMode()` 函数判断。
137
+
138
+ ### 2.4 Auto(自动模式)
139
+
140
+ **文件**: `src/utils/permissions/PermissionMode.ts:80-90`
141
+
142
+ ```typescript
143
+ ...(feature('TRANSCRIPT_CLASSIFIER')
144
+ ? {
145
+ auto: {
146
+ title: 'Auto mode',
147
+ shortTitle: 'Auto',
148
+ symbol: '⏵⏵',
149
+ color: 'warning',
150
+ external: 'default',
151
+ },
152
+ }
153
+ : {}),
154
+ ```
155
+
156
+ - **行为**: 使用 AI 分类器自动审批操作
157
+ - **内部依赖**: `feature('TRANSCRIPT_CLASSIFIER')` — 编译期 feature flag,外部构建中通过 DCE(死代码消除)完全移除
158
+ - **自动批准**:
159
+ 1. `acceptEdits` 快速路径可批准的(工作目录内编辑)
160
+ 2. 安全放行列表中的工具(`isAutoModeAllowlistedTool`, `classifierDecision.ts:96-100`)
161
+ 3. 分类器判定为安全的操作
162
+ - **询问**: 分类器判定为需阻止的操作(可被用户覆盖)
163
+ - **拒绝限制**: 连续 3 次阻止 → 回退到询问;总计 20 次阻止 → 重置并回退(`denialTracking.ts:12-14`)
164
+
165
+ ### 2.5 DontAsk(不询问模式)
166
+
167
+ **文件**: `src/utils/permissions/PermissionMode.ts:73-79`
168
+
169
+ ```typescript
170
+ dontAsk: {
171
+ title: "Don't Ask",
172
+ shortTitle: 'DontAsk',
173
+ symbol: '⏵⏵',
174
+ color: 'error',
175
+ external: 'dontAsk',
176
+ }
177
+ ```
178
+
179
+ - **行为**: 将所有 `ask` 决策转换为 `deny`
180
+ - **转换**: `src/utils/permissions/permissions.ts:505-518`
181
+ ```typescript
182
+ if (appState.toolPermissionContext.mode === 'dontAsk') {
183
+ return {
184
+ behavior: 'deny',
185
+ decisionReason: { type: 'mode', mode: 'dontAsk' },
186
+ message: DONT_ASK_REJECT_MESSAGE(tool.name),
187
+ }
188
+ }
189
+ ```
190
+ - **适用场景**: 测试、CI/CD 环境或不想让 AI 执行任何操作的场景
191
+
192
+ ### 2.6 BypassPermissions(绕过权限模式)
193
+
194
+ **文件**: `src/utils/permissions/PermissionMode.ts:66-72`
195
+
196
+ - **行为**: 绕过所有权限检查(除安全检查和内容特定 ask 规则外)
197
+ - **禁用机制**: 可通过 Statsig 门控 `tengu_disable_bypass_permissions_mode` 或 settings 中 `permissions.disableBypassPermissionsMode` 完全禁用(`permissionSetup.ts:695-711`)
198
+ - **例外**: `bypassPermissions` 不影响 `requiresUserInteraction` 检查(`permissions.ts:1231-1236`)、安全检查(`permissions.ts:1255-1260`)和内容特定 ask 规则(`permissions.ts:1244-1250`)
199
+
200
+ ### 2.7 Bubble(气泡模式)
201
+
202
+ - **用途**: Ant 内部使用模式,外部构建不可见(`PermissionMode.ts:104` 中 `process.env.USER_TYPE !== 'ant'` 时被排除)
203
+ - **文档**: 无外部可用信息
204
+
205
+ ---
206
+
207
+ ## 3. 授权管道 (Authorization Pipeline)
208
+
209
+ 核心实现位于 `src/utils/permissions/permissions.ts` 的 `hasPermissionsToUseToolInner()` 函数(第 1158-1319 行)和 `hasPermissionsToUseTool()`(第 473-956 行)。
210
+
211
+ ### 管道全景图
212
+
213
+ ```
214
+ 用户输入工具调用
215
+
216
+
217
+ ┌─────────────────────────────────────┐
218
+ │ 1. 预过滤 │
219
+ │ 1a. 全局 deny 规则检查 │
220
+ │ 1b. 全局 ask 规则检查 │
221
+ │ 1c. 工具特定 checkPermissions() │
222
+ │ 1d. 工具实现 deny │
223
+ │ 1e. requiresUserInteraction 检查 │
224
+ │ 1f. 内容特定 ask 规则检查 │
225
+ │ 1g. 安全检查 (safety check) │
226
+ └─────────────────┬───────────────────┘
227
+ │ (2a-2b)
228
+
229
+ ┌─────────────────────────────────────┐
230
+ │ 2. 权限处理 │
231
+ │ 2a. bypassPermissions 模式放行 │
232
+ │ 2b. 全局 allow 规则放行 │
233
+ │ 2c. passthrough → ask 转换 │
234
+ └─────────────────┬───────────────────┘
235
+ │ (返回 ask/allow/deny)
236
+
237
+ ┌─────────────────────────────────────┐
238
+ │ 3. 模式转换 (在 hasPermissionsToUse) │
239
+ │ 3a. dontAsk 模式: ask → deny │
240
+ │ 3b. Auto 模式: │
241
+ │ ├ 非分类器可批准的安全检查: 返回 │
242
+ │ ├ PowerShell 默认拒绝 │
243
+ │ ├ acceptEdits 快速路径 │
244
+ │ ├ 安全放行列表 │
245
+ │ └ YOLO 分类器评估 │
246
+ │ 3c. 无提示模式: hooks → auto-deny│
247
+ └─────────────────┬───────────────────┘
248
+
249
+
250
+ ┌─────────────────┐
251
+ │ 用户确认对话框 │
252
+ │ (useCanUseTool) │
253
+ └─────────────────┘
254
+ ```
255
+
256
+ ### 3.1 第 1 步:规则预过滤
257
+
258
+ 在 `hasPermissionsToUseToolInner()`(第 1158-1260 行)中执行:
259
+
260
+ **1a. 全局 Deny 规则**(第 1171-1181 行):
261
+ ```typescript
262
+ const denyRule = getDenyRuleForTool(appState.toolPermissionContext, tool)
263
+ if (denyRule) { return { behavior: 'deny', message: `Permission to use ${tool.name} has been denied.` } }
264
+ ```
265
+
266
+ **1b. 全局 Ask 规则**(第 1184-1206 行):检查工具是否在 alwaysAsk 列表中。例外:如果 sandbox 启用且 `autoAllowBashIfSandboxed` 为 true,sandbox 内运行的 Bash 命令可跳过此检查。
267
+
268
+ **1c. 工具特定权限检查**(第 1214-1223 行):
269
+ ```typescript
270
+ const parsedInput = tool.inputSchema.parse(input)
271
+ toolPermissionResult = await tool.checkPermissions(parsedInput, context)
272
+ ```
273
+ 每个工具实现自己的 `checkPermissions()` 方法。例如 `BashTool` 实现命令级规则(前缀匹配、通配符匹配)。
274
+
275
+ **1d. 工具实现 Deny**(第 1226-1228 行):如果工具的 `checkPermissions` 返回 `deny`,直接返回。
276
+
277
+ **1e. 用户交互要求**(第 1231-1236 行):如果工具标记为 `requiresUserInteraction()`,即使是 `bypassPermissions` 模式也必须询问。
278
+
279
+ **1f. 内容特定 Ask 规则**(第 1244-1250 行):如 `Bash(npm publish:*)` 这样的规则,即使是 `bypassPermissions` 也必须尊重。
280
+
281
+ **1g. 安全检查**(第 1255-1260 行):`checkPathSafetyForAutoEdit` 返回的安全检查(如 `.git/`、`.claude/`、shell 配置文件)是 bypass-immune 的。
282
+
283
+ ### 3.2 第 2 步:权限处理
284
+
285
+ **2a. bypassPermissions 模式**(第 1268-1281 行):直接放行所有操作(1a-1g 已过滤的危险操作除外)。
286
+
287
+ **2b. 全局 Allow 规则**(第 1284-1297 行):如果工具在 `alwaysAllow` 列表中,直接放行。
288
+
289
+ **2c. passthrough → ask 转换**(第 1300-1310 行):如果 `checkPermissions` 返回 `passthrough`,转换为 `ask`。
290
+
291
+ ### 3.3 第 3 步:模式转换
292
+
293
+ 在 `hasPermissionsToUseTool()`(第 473-956 行)中执行:
294
+
295
+ **3a. dontAsk**: 将所有 `ask` 转换为 `deny`(第 505-518 行)。
296
+
297
+ **3b. Auto 模式**: 复杂的分类器驱动审批流程(第 520-926 行):
298
+ 1. 非分类器可批准的安全检查 → 保持询问
299
+ 2. `requiresUserInteraction()` 的工具 → 保持询问
300
+ 3. 拒绝跟踪检查
301
+ 4. PowerShell 默认拒绝(除非 `POWERSHELL_AUTO_MODE` 特性启用)
302
+ 5. **acceptEdits 快速路径**: 如果当前操作在 acceptEdits 模式下会被批准,直接放行(第 600-655 行)
303
+ 6. **安全放行列表**: `isAutoModeAllowlistedTool()` 中的工具(第 660-685 行)
304
+ 7. **分类器评估**: `classifyYoloAction()` 执行完整的安全评估(第 692-926 行)
305
+
306
+ **3c. 无提示模式**: `shouldAvoidPermissionPrompts` 为 true 时(后台/headless agent),执行 PermissionRequest hooks,若 hook 未决定则 auto-deny(第 932-952 行)。
307
+
308
+ ### 3.4 CLI Hook 整合
309
+
310
+ 在 `src/hooks/useCanUseTool.tsx` 中,结果经过:
311
+ 1. `hasPermissionsToUseTool()` 管道
312
+ 2. 如果 `allow` → 立即批准,记录分类器批准信息
313
+ 3. 如果 `deny` → 拒绝,记录 auto mode denial,发送通知
314
+ 4. 如果 `ask` → 进入交互式权限处理流程:
315
+ - `awaitAutomatedChecksBeforeDialog` → 协调器处理
316
+ - Swarm Worker 处理(`handleSwarmWorkerPermission`)
317
+ - 竞争性分类器检查(Bash 命令的投机性分类器评估,`peekSpeculativeClassifierCheck`)
318
+ - 最终 `handleInteractivePermission` 显示对话框
319
+
320
+ ---
321
+
322
+ ## 4. Auto-Mode 分类器
323
+
324
+ ### 4.1 架构概览
325
+
326
+ Auto mode 分类器是系统的核心智能审批组件,使用 Claude 模型(默认使用主循环模型,可被 `CLAUDE_CODE_AUTO_MODE_MODEL` 环境变量或 GrowthBook 配置覆盖)对每个待审批的工具调用进行安全评估。
327
+
328
+ **核心文件**:
329
+ - `src/utils/permissions/yoloClassifier.ts` — 分类器主逻辑、2 阶段 XML 分类器
330
+ - `src/utils/permissions/classifierDecision.ts` — 安全放行列表
331
+ - `src/utils/permissions/classifierShared.ts` — 共享解析工具
332
+ - `src/utils/permissions/yolo-classifier-prompts/` — 系统提示模板
333
+
334
+ ### 4.2 两阶段分类器
335
+
336
+ 系统支持三种分类器模式,通过 GrowthBook `tengu_auto_mode_config.twoStageClassifier` 配置:
337
+
338
+ | 模式 | 说明 | 适用场景 |
339
+ |------|------|----------|
340
+ | `both` (默认) | Stage 1 (fast) + Stage 2 (thinking) | 平衡速度与准确率 |
341
+ | `fast` | 仅 Stage 1,max_tokens=256,最终裁定 | 追求速度 |
342
+ | `thinking` | 仅 Stage 2,跳过 Stage 1 | 需要深度推理 |
343
+
344
+ **Stage 1 - 快速判断**(`yoloClassifier.ts:769-858`):
345
+ - `max_tokens=64`(fast-only 模式为 256)
346
+ - `stop_sequences=['</block>']` — 一旦输出 `</block>` 立即停止
347
+ - 后缀:`"\nErr on the side of blocking. <block> immediately."`
348
+ - 如果 stage 1 判定允许(`<block>no</block>`),直接返回
349
+ - 如果 stage 1 判定阻止(`<block>yes</block>`),进入 stage 2
350
+
351
+ **Stage 2 - 深度推理**(`yoloClassifier.ts:861-940`):
352
+ - `max_tokens=4096`
353
+ - 包含 `xml_s2_t2` 后缀,要求 chain-of-thought 推理
354
+ - 可包含 `<thinking>` 标签
355
+ - 最终输出 `<block>yes/no</block>` 和可选的 `<reason>...</reason>`
356
+
357
+ ### 4.3 双阶段上下文预算
358
+
359
+ ```
360
+ ┌──────────────────────────────────────────────────────┐
361
+ │ 上下文窗口管理 │
362
+ ├──────────────────────────────────────────────────────┤
363
+ │ System Block (cache_control) ← 稳定缓存前缀 │
364
+ │ └ 基础提示 + 权限模板 + 用户 allow/deny 规则 │
365
+ │ │
366
+ │ CLAUDE.md (cache_control) ← 静态用户指令 │
367
+ │ │
368
+ │ Transcript Entries │
369
+ │ ├ User: "..." │
370
+ │ ├ Bash ls │
371
+ │ ├ User: "..." │
372
+ │ └ ... │
373
+ │ │
374
+ │ Action (cache_control) ← 正在评估的操作 │
375
+ │ └ 只有 system + CLAUDE.md + action 三个 cache_control│
376
+ │ block(不超过 API 限制的 4 个) │
377
+ └──────────────────────────────────────────────────────┘
378
+ ```
379
+
380
+ ### 4.4 评估指标与风险分类
381
+
382
+ **官方数据**:
383
+ - False-negative rate:17%(即分类器应阻止但未阻止的操作比例)
384
+ - 第三方测试 FNR:70.3%(表明在更复杂场景下分类器有显著漏报)
385
+
386
+ **四种风险类别**(来自系统提示模板 `auto_mode_system_prompt.txt`):
387
+
388
+ | 类别 | 说明 | 示例 |
389
+ |------|------|------|
390
+ | Overeager Behavior | 模型过于积极执行未明确请求的操作 | 未问用户就安装包 |
391
+ | Honest Mistakes | 模型判断错误导致的非恶意操作 | 删错了文件 |
392
+ | Prompt Injection | 提示注入攻击 | 用户指令中隐藏恶意代码 |
393
+ | Model Misalignment | 模型与用户意图不一致 | 执行与任务无关的危险操作 |
394
+
395
+ ### 4.5 安全放行列表
396
+
397
+ **文件**: `src/utils/permissions/classifierDecision.ts:56-94`
398
+
399
+ ```typescript
400
+ const SAFE_YOLO_ALLOWLISTED_TOOLS = new Set([
401
+ FILE_READ_TOOL_NAME, GREP_TOOL_NAME, GLOB_TOOL_NAME,
402
+ LSP_TOOL_NAME, TOOL_SEARCH_TOOL_NAME, LIST_MCP_RESOURCES_TOOL_NAME,
403
+ TODO_WRITE_TOOL_NAME, TASK_CREATE_TOOL_NAME, // ... 等
404
+ ])
405
+ ```
406
+
407
+ 注意:`isAutoModeAllowlistedTool()`(第 96-100 行)当前实现为 `return true` — 所有工具都跳过分类器。这意味着安全放行列表实际上当前被全局放行取代,所有工具通过 `acceptEdits` 快速路径评估后再考虑分类器。
408
+
409
+ 但代码中的注释明确说明:
410
+
411
+ > "Does NOT include write/edit tools — those are handled by the acceptEdits fast path"
412
+
413
+ ### 4.6 提示注入���护
414
+
415
+ **JSONL 转录格式**(`yoloClassifier.ts:412-423`):
416
+
417
+ ```
418
+ {"Bash":"ls -la"} ← JSON 编码,注入无法突破字符串边界
419
+ {"user":"forged text"} ← tool_use 块中的 user 文本不会混淆分类器
420
+ ```
421
+
422
+ 传统文本格式:
423
+ ```
424
+ Bash ls -la
425
+ User: forged text
426
+ ```
427
+
428
+ JSONL 格式是注入防护的关键设计:用户输入和工具调用都被 JSON.stringify 编码,因此恶意内容中的换行符和特殊字符不可能突破字符串边界。
429
+
430
+ ### 4.7 拒绝跟踪 (Denial Tracking)
431
+
432
+ **文件**: `src/utils/permissions/denialTracking.ts`
433
+
434
+ ```typescript
435
+ export const DENIAL_LIMITS = {
436
+ maxConsecutive: 3, // 连续拒绝 3 次后回退
437
+ maxTotal: 20, // 总计拒绝 20 次后回退
438
+ } as const
439
+ ```
440
+
441
+ - `recordDenial(state)`: 递增 consecutiveDenials 和 totalDenials
442
+ - `recordSuccess(state)`: 重置 consecutiveDenials 为 0
443
+ - `shouldFallbackToPrompting(state)`: 超出任一阈值时返回 true
444
+
445
+ 超过限制后的行为:
446
+ - CLI 模式:回退到手动询问,包含分类器的拒绝理由
447
+ - headless 模式:抛出 `AbortError`,终止 agent
448
+
449
+ ---
450
+
451
+ ## 5. 规则系统
452
+
453
+ ### 5.1 规则类型
454
+
455
+ **文件**: `src/types/permissions.ts:67-79`, `src/utils/permissions/permissions.ts:238-302`
456
+
457
+ 三种规则行为(`PermissionBehavior`):
458
+
459
+ | 行为 | 效果 | 适用场景 |
460
+ |------|------|----------|
461
+ | `allow` | 工具被允许,跳过后续检查 | `--allowed-tools Bash(ls:*)` |
462
+ | `deny` | 工具被拒绝,不执行 | `--disallowed-tools Bash(rm:*)` |
463
+ | `ask` | 强制询问用户 | `Bash(npm publish:*)` |
464
+
465
+ ### 5.2 规则来源
466
+
467
+ **文件**: `src/types/permissions.ts:54-62`
468
+
469
+ | 来源 | 持久化 | 说明 |
470
+ |------|--------|------|
471
+ | `userSettings` | `~/.claude/settings.json` | 全局用户设置 |
472
+ | `projectSettings` | `.claude/settings.json` | 项目级别,可提交到 git |
473
+ | `localSettings` | `.claude/settings.local.json` | 项目级别,gitignored |
474
+ | `policySettings` | 企业策略 | 只读,不可删除 |
475
+ | `flagSettings` | 编译期标志 | 只读 |
476
+ | `cliArg` | CLI 参数 | `--allowed-tools`, `--disallowed-tools` |
477
+ | `command` | 斜杠命令 | 命令前导块中的规则 |
478
+ | `session` | 内存 | 临时会话规则 |
479
+
480
+ 规则加载流程(`permissionsLoader.ts:120-133`):
481
+ 1. 如果 `allowManagedPermissionRulesOnly` 为 true → 只加载 `policySettings`
482
+ 2. 否则加载所有已启用的设置源
483
+
484
+ ### 5.3 路径模式匹配
485
+
486
+ **文件**: `src/utils/permissions/filesystem.ts:960-1025`
487
+
488
+ 使用 gitignore 风格的 `ignore` 库进行路径匹配:
489
+
490
+ ```typescript
491
+ export function matchingRuleForInput(
492
+ path: string,
493
+ toolPermissionContext: ToolPermissionContext,
494
+ toolType: 'edit' | 'read',
495
+ behavior: 'allow' | 'deny' | 'ask',
496
+ ): PermissionRule | null
497
+ ```
498
+
499
+ **关键要点**:
500
+ - 路径被规范化为 POSIX 格式(`relativePath` 函数,第 170-179 行)
501
+ - 双斜杠前缀 `//` 表示相对于根 `/` 的路径
502
+ - 波浪号前缀 `~/` 表示相对于用户主目录的路径
503
+ - 模式 `/**` 后缀被简化为匹配目录本身及其所有子项
504
+ - 大小写标准化(`normalizeCaseForComparison`)防止大小写绕过
505
+
506
+ ### 5.4 路径安全检查
507
+
508
+ **文件**: `src/utils/permissions/filesystem.ts:620-665`
509
+
510
+ `checkPathSafetyForAutoEdit()` 检查:
511
+
512
+ 1. **Windows 可疑路径模式**(`hasSuspiciousWindowsPathPattern`, `filesystem.ts:537-602`):
513
+ - NTFS Alternate Data Streams(`:` 号,仅 Windows/WSL)
514
+ - 8.3 短文件名(`~` 后跟数字)
515
+ - 长路径前缀(`\\?\`, `//?/` 等)
516
+ - 尾部点和空格(`.git.`, `.claude.`)
517
+ - DOS 设备名(`CON`, `PRN`, `AUX` 等)
518
+ - 三个连续点(`...`)
519
+ - UNC 路径(`\\server\share`)
520
+
521
+ 2. **Claude 配置文件**(`isClaudeConfigFilePath`, `filesystem.ts:225-242`):
522
+ - `settings.json`, `settings.local.json`
523
+ - `.claude/commands/`, `.claude/agents/`, `.claude/skills/`
524
+
525
+ 3. **危险文件**(`DANGEROUS_FILES`, `filesystem.ts:57-68`):
526
+ - `.gitconfig`, `.gitmodules`
527
+ - `.bashrc`, `.bash_profile`, `.zshrc`, `.zprofile`, `.profile`
528
+ - `.ripgreprc`, `.mcp.json`, `.claude.json`
529
+
530
+ 4. **危险目录**(`DANGEROUS_DIRECTORIES`, `filesystem.ts:74-79`):
531
+ - `.git`, `.vscode`, `.idea`, `.claude`
532
+
533
+ 安全检查也适用于解析后的符号链接路径,防止通过符号链接绕过。
534
+
535
+ ### 5.5 危险权限检测
536
+
537
+ **文件**:
538
+ - `src/utils/permissions/permissionSetup.ts:94-285`
539
+ - `src/utils/permissions/dangerousPatterns.ts`
540
+
541
+ 进入 auto 模式时,系统自动检测并剥离(strip)危险权限:
542
+
543
+ **危险 Bash 规则**(`isDangerousBashPermission`):
544
+ ```typescript
545
+ // 完全通配: Bash, Bash(*), Bash() → 允许所有命令 → 危险
546
+ // 解释器通配: Bash(python:*) → 允许任意 Python 代码 → 危险
547
+ // 包管理器: Bash(npm run:*) → 可执行任意脚本 → 危险
548
+ ```
549
+
550
+ 匹配模式列表(`dangerousPatterns.ts`):
551
+ ```typescript
552
+ CROSS_PLATFORM_CODE_EXEC = [
553
+ 'python', 'python3', 'node', 'deno', 'ruby', 'perl', 'php', 'lua',
554
+ 'npx', 'bunx', 'npm run', 'yarn run', 'pnpm run', 'bun run',
555
+ 'bash', 'sh', 'ssh',
556
+ ]
557
+ // ant-only: gh, curl, wget, git, kubectl, aws, gcloud, gsutil
558
+ ```
559
+
560
+ **危险 PowerShell 规则**(`isDangerousPowerShellPermission`, `permissionSetup.ts:157-233`):
561
+ 额外包含 `iex`, `invoke-expression`, `start-process`, `add-type`, `new-object` 等。
562
+
563
+ **危险 Agent 规则**(`isDangerousTaskPermission`):
564
+ 任何 `Agent` 工具的 allow 规则都会绕过分类器对子 agent 的评估。
565
+
566
+ ### 5.6 影子规则检测
567
+
568
+ **文件**: `src/utils/permissions/shadowedRuleDetection.ts`
569
+
570
+ 检测 allow 规则是否被 ask/deny 规则"屏蔽"(即永远无法生效):
571
+
572
+ ```typescript
573
+ // 示例: allow 规则 Bash(ls:*) 被全局 ask 规则 Bash 屏蔽
574
+ // 因为 ask 规则在评估顺序中先于 allow 规则
575
+ ```
576
+
577
+ - **Deny 屏蔽**: 工具级别的 deny 规则使该工具的所有 allow 规则不可达
578
+ - **Ask 屏蔽**: 工具级别的 ask 规则使带内容的 allow 规则不可达(总是会先询问)
579
+ - **沙箱例外**: 如果 sandbox 启用且 `autoAllowBashIfSandboxed` 为 true,个人设置的 ask 规则不屏蔽 allow 规则
580
+
581
+ ---
582
+
583
+ ## 6. Shell 沙箱
584
+
585
+ ### 6.1 容器化执行环境
586
+
587
+ **文件**: `src/utils/sandbox/sandbox-adapter.ts`
588
+
589
+ SandboxManager 提供容器化的命令执行环境,支持:
590
+
591
+ - **文件系统隔离**: `getFsReadConfig()` / `getFsWriteConfig()` 控制可读写的路径
592
+ - **网络隔离**: `getNetworkRestrictionConfig()` 控制网络访问
593
+ - **命令排除**: `excludedCommands` 列表中的命令不被沙箱化
594
+
595
+ ### 6.2 AutoAllowBashIfSandboxed
596
+
597
+ **文件**: `src/utils/sandbox/sandbox-adapter.ts:469-472`
598
+
599
+ ```typescript
600
+ function isAutoAllowBashIfSandboxed(): boolean {
601
+ const settings = getSettings_DEPRECATED()
602
+ return settings?.sandbox?.autoAllowBashIfSandboxed ?? true // 默认开启
603
+ }
604
+ ```
605
+
606
+ 当此选项启用时:
607
+ 1. 所有 Bash 命令在沙箱内运行
608
+ 2. Bash 命令的权限检查被自动放行(`permissions.ts:1189-1193`)
609
+ 3. 命令不在沙箱内运行时(`dangerouslyDisableSandbox = true` 或 `excludedCommands`),仍遵循正常权限检查
610
+
611
+ ### 6.3 平台支持
612
+
613
+ 支持通过 `enabledPlatforms` 配置控制哪些平台启用沙箱:
614
+ ```typescript
615
+ // src/entrypoints/sandboxTypes.ts:108-112
616
+ // 为了 NVIDIA 企业部署,最初仅 macOS 启用沙箱
617
+ // Linux/WSL 沙箱支持较新,在扩展前需要更多验证
618
+ ```
619
+
620
+ ### 6.4 Sandbox 写允许列表
621
+
622
+ 在路径验证中(`pathValidation.ts:101-123`),当沙箱启用时,沙箱配置的写允许列表作为额外的工作目录:
623
+
624
+ ```typescript
625
+ export function isPathInSandboxWriteAllowlist(resolvedPath: string): boolean {
626
+ const { allowOnly, denyWithinAllow } = SandboxManager.getFsWriteConfig()
627
+ // 检查路径是否在 allowOnly 中且不在 denyWithinAllow 中
628
+ }
629
+ ```
630
+
631
+ ---
632
+
633
+ ## 7. 安全相关实现细节
634
+
635
+ ### 7.1 Protected Paths
636
+
637
+ **文件**: `src/utils/permissions/filesystem.ts`
638
+
639
+ 系统定义了多层保护路径,防止 AI 修改关键配置:
640
+
641
+ **内部可编辑路径**(`checkEditableInternalPath`, `filesystem.ts:1479-1605`)——自动允许编辑:
642
+ - 当前会话的计划文件(`isSessionPlanFile`)
643
+ - 临时目录(`isScratchpadPath`)
644
+ - 模板任务目录(`CLAUDE_JOB_DIR`,`feature('TEMPLATES')` 时)
645
+ - Agent 记忆目录(`isAgentMemoryPath`)
646
+ - 自动记忆目录(`isAutoMemPath`,无覆盖路径时)
647
+ - `.claude/launch.json`(桌面预览配置)
648
+
649
+ **内部可读路径**(`checkReadableInternalPath`, `filesystem.ts:1611-1777`)——自动允许读取:
650
+ - 会话记忆目录(`isSessionMemoryPath`)
651
+ - 项目目录(`isProjectDirPath`)
652
+ - 计划文件
653
+ - 工具结果目录(`getToolResultsDir`)
654
+ - 临时目录
655
+ - Agent 记忆目录
656
+ - 任务目录(`.claude/tasks/`)
657
+ - 团队目录(`.claude/teams/`)
658
+ - 内置技能参考文件(`getBundledSkillsRoot`)
659
+
660
+ ### 7.2 Hook 系统
661
+
662
+ **文件**: `src/utils/hooks.ts:4157-4192`
663
+
664
+ #### PreToolUse Hooks
665
+
666
+ `executePermissionRequestHooks()` 在权限检查流程中调用,提供用户自定义的拦截逻辑:
667
+
668
+ ```typescript
669
+ export async function* executePermissionRequestHooks<ToolInput>(
670
+ toolName: string,
671
+ toolUseID: string,
672
+ toolInput: ToolInput,
673
+ toolUseContext: ToolUseContext,
674
+ permissionMode?: string,
675
+ permissionSuggestions?: PermissionUpdate[],
676
+ signal?: AbortSignal,
677
+ timeoutMs: number = TOOL_HOOK_EXECUTION_TIMEOUT_MS,
678
+ // ...
679
+ )
680
+ ```
681
+
682
+ Hook 可以返回:
683
+ - `{ behavior: 'allow', updatedInput, updatedPermissions }` → 放行(含输入修改和权限更新)
684
+ - `{ behavior: 'deny', message, interrupt }` → 拒绝(含可选的中断信号)
685
+
686
+ 使用场景:
687
+ - headless/async agent 不能显示权限提示框时通过 hook 授权
688
+ - CI/CD 环境中的自动化策略执行
689
+
690
+ #### PostToolUse Hooks
691
+
692
+ 代码中存在 `PostToolUse` 钩子类型(在 hook 类型定义中),用于工具执行后的审计、日志记录和副作用处理。
693
+
694
+ ### 7.3 远程会话权限桥接
695
+
696
+ **文件**: `src/utils/permissions/permissionSetup.ts:748-758`
697
+
698
+ 当 `CLAUDE_CODE_REMOTE` 环境变量设置时,运行在远程桥接模式,权限模式受限:
699
+
700
+ ```typescript
701
+ if (
702
+ isEnvTruthy(process.env.CLAUDE_CODE_REMOTE) &&
703
+ !['acceptEdits', 'plan', 'default'].includes(settingsMode)
704
+ ) {
705
+ // 只有 acceptEdits、plan、default 模式支持 CCR
706
+ logEvent('tengu_ccr_unsupported_default_mode_ignored', { mode: settingsMode })
707
+ }
708
+ ```
709
+
710
+ 这意味着在远程会话中,`bypassPermissions` 和 `auto` 模式被禁用,增强了对远程连接的安全控制。
711
+
712
+ ### 7.4 权限解释器
713
+
714
+ **文件**: `src/utils/permissions/permissionExplainer.ts`
715
+
716
+ 在询问用户时,系统可调用 Haiku 模型生成操作的风险评估:
717
+
718
+ ```typescript
719
+ export async function generatePermissionExplanation({
720
+ toolName, toolInput, toolDescription, messages, signal,
721
+ }): Promise<PermissionExplanation | null>
722
+ ```
723
+
724
+ 输出包含:
725
+ - `riskLevel`: `LOW` | `MEDIUM` | `HIGH`
726
+ - `explanation`: 操作说明(1-2 句话)
727
+ - `reasoning`: 执行此操作的原因
728
+ - `risk`: 可能的风险(15 字以内)
729
+
730
+ 此功能可通过 `permissionExplainerEnabled` 配置禁用。
731
+
732
+ ### 7.5 权限更新的持久化
733
+
734
+ **文件**: `src/utils/permissions/PermissionUpdate.ts`, `PermissionUpdateSchema.ts`
735
+
736
+ 权限变更(添加规则、删除规则、更改模式、添加目录)通过 `permissionRuleParser.ts` 进行序列化和反序列化,并写入到对应的设置文件。
737
+
738
+ ```typescript
739
+ export type PermissionUpdate =
740
+ | { type: 'addRules'; destination: PermissionUpdateDestination; rules: PermissionRuleValue[]; behavior: PermissionBehavior }
741
+ | { type: 'replaceRules'; destination: ...; rules: ...; behavior: ... }
742
+ | { type: 'removeRules'; destination: ...; rules: ...; behavior: ... }
743
+ | { type: 'setMode'; destination: ...; mode: ExternalPermissionMode }
744
+ | { type: 'addDirectories'; destination: ...; directories: string[] }
745
+ | { type: 'removeDirectories'; destination: ...; directories: string[] }
746
+ ```
747
+
748
+ ### 7.6 特征标识与死代码消除
749
+
750
+ 整个权限系统大量使用 Bun 编译期的 `feature()` 函数进行条件编译:
751
+
752
+ | Feature Flag | 控制的特性 | 文件 |
753
+ |-------------|-----------|------|
754
+ | `TRANSCRIPT_CLASSIFIER` | 整个 auto 模式系统(分类器、状态管理、危险权限剥离) | 多处 |
755
+ | `BASH_CLASSIFIER` | Bash 命令提示词分类器 | `bashClassifier.ts` |
756
+ | `POWERSHELL_AUTO_MODE` | PowerShell 自动模式 | `yoloClassifier.ts` |
757
+ | `TEMPLATES` | 模板任务目录自动编辑 | `filesystem.ts` |
758
+
759
+ 这样,外部构建中所有 auto 模式相关代码被完全消除,减小了二进制体积并简化了安全模型。
760
+
761
+ ### 7.7 拒绝统计与滥用防护
762
+
763
+ **文件**: `src/utils/permissions/denialTracking.ts`
764
+
765
+ ```typescript
766
+ export const DENIAL_LIMITS = {
767
+ maxConsecutive: 3, // 连续 3 次拒绝 → 回退到手动
768
+ maxTotal: 20, // 总计 20 次拒绝 → 回退到手动
769
+ } as const
770
+ ```
771
+
772
+ 拒绝跟踪统计记录在 `AppState.denialTracking` 中,在 auto 模式下每次分类器判定后更新:
773
+
774
+ - 成功(allow)→ `recordSuccess()` → 重置连续拒绝计数
775
+ - 失败(block)→ `recordDenial()` → 递增两个计数器
776
+ - 超出阈值 → `shouldFallbackToPrompting()` → 回退到交互式询问
777
+
778
+ headless agent 模式下,超出限制直接终止 agent(`permissions.ts:1023-1027`)。
779
+
780
+ ### 7.8 CLI 与 Settings 初始化流程
781
+
782
+ **文件**: `src/utils/permissions/permissionSetup.ts:872-1033`
783
+
784
+ `initializeToolPermissionContext()` 的完整流程:
785
+
786
+ 1. 解析 `--allowed-tools`, `--disallowed-tools`, `--base-tools` CLI 参数
787
+ 2. 检测 `bypassPermissions` 模式是否可用(Statsig 门控 + settings 检查)
788
+ 3. 从磁盘加载所有权限规则(`loadAllPermissionRulesFromDisk`)
789
+ 4. 检测危险和过宽的 Shell 权限
790
+ 5. 应用规则到 `ToolPermissionContext`
791
+ 6. 处理工作目录的符号链接(`process.env.PWD` 与 `getOriginalCwd()` 的差异)
792
+ 7. 验证并添加附加目录(`--add-dir`)
793
+
794
+ ---
795
+
796
+ ## 附录 A:关键文件索引
797
+
798
+ | 文件 | 职责 | 关键行 |
799
+ |------|------|--------|
800
+ | `src/types/permissions.ts` | 权限类型定义 | 16-36 (模式), 54-62 (规则来源), 75-79 (规则), 271-324 (DecisionReason) |
801
+ | `src/utils/permissions/PermissionMode.ts` | 权限模式配置与 UI 展示 | 42-91 (模式配置), 97-105 (外部模式过滤) |
802
+ | `src/utils/permissions/permissions.ts` | 核心权限检查管道 | 473-956 (hasPermissionsToUseTool), 1158-1319 (hasPermissionsToUseToolInner), 1060-1156 (checkRuleBasedPermissions) |
803
+ | `src/utils/permissions/filesystem.ts` | 文件系统权限检查 | 57-68 (危险文件), 74-79 (危险目录), 620-665 (安全检查), 960-1025 (路径匹配), 1479-1605 (内部可编辑路径), 1611-1777 (内部可读路径) |
804
+ | `src/utils/permissions/pathValidation.ts` | 路径验证 | 101-123 (沙箱写允许), 141-263 (isPathAllowed), 331-367 (危险删除路径), 373-485 (validatePath) |
805
+ | `src/utils/permissions/yoloClassifier.ts` | Auto 模式分类器 | 711-996 (XML 2 阶段分类器), 1012-1306 (classifyYoloAction), 1484-1495 (formatActionForClassifier) |
806
+ | `src/utils/permissions/classifierDecision.ts` | 分类器决策与放行列表 | 56-94 (SAFE_YOLO_ALLOWLISTED_TOOLS), 96-100 (isAutoModeAllowlistedTool) |
807
+ | `src/utils/permissions/permissionSetup.ts` | 模式切换、危险权限剥离 | 94-147 (isDangerousBashPermission), 157-233 (isDangerousPowerShellPermission), 510-553 (stripDangerousPermissionsForAutoMode), 597-646 (transitionPermissionMode), 872-1033 (initializeToolPermissionContext) |
808
+ | `src/utils/permissions/permissionsLoader.ts` | 规则加载与持久化 | 120-133 (loadAllPermissionRulesFromDisk), 229-296 (addPermissionRulesToSettings) |
809
+ | `src/utils/permissions/shellRuleMatching.ts` | Shell 规则解析与匹配 | 43-48 (prefix 提取), 90-153 (通配符匹配) |
810
+ | `src/utils/permissions/denialTracking.ts` | 拒绝跟踪 | 12-15 (DENIAL_LIMITS), 40-44 (shouldFallbackToPrompting) |
811
+ | `src/utils/permissions/shadowedRuleDetection.ts` | 影子规则检测 | 193-234 (detectUnreachableRules) |
812
+ | `src/utils/permissions/dangerousPatterns.ts` | 危险命令模式 | 18-42 (CROSS_PLATFORM_CODE_EXEC), 44-80 (DANGEROUS_BASH_PATTERNS) |
813
+ | `src/utils/permissions/permissionExplainer.ts` | 权限解释器 | 147-250 (generatePermissionExplanation) |
814
+ | `src/hooks/useCanUseTool.tsx` | React 权限 hook | 28-203 (useCanUseTool) |
815
+ | `src/utils/sandbox/sandbox-adapter.ts` | 沙箱适配器 | 469-472 (isAutoAllowBashIfSandboxed) |
816
+ | `src/utils/hooks.ts` | Hook 执行系统 | 4157-4192 (executePermissionRequestHooks) |
817
+ | `src/utils/permissions/bypassPermissionsKillswitch.ts` | 绕过权限禁用开关 | 19-47 (checkAndDisableBypassPermissionsIfNeeded), 74-117 (checkAndDisableAutoModeIfNeeded) |
818
+
819
+ ## 附录 B:数据流图(权限检查)
820
+
821
+ ```
822
+ 工具调用请求
823
+
824
+
825
+ ┌──────────────────────────────────────┐
826
+ │ hasPermissionsToUseTool() │
827
+ │ src/utils/permissions/permissions.ts│
828
+ │ │
829
+ │ 1. 规则预过滤段 │
830
+ │ ├── 全局 deny 规则 │
831
+ │ ├── 全局 ask 规则 (sandbox 快速放行)│
832
+ │ ├── tool.checkPermissions() │
833
+ │ ├── tool.requiresUserInteraction │
834
+ │ ├── 内容 ask 规则 │
835
+ │ └── 安全检查 │
836
+ │ │
837
+ │ 2. 权限处理段 │
838
+ │ ├── bypassPermissions / plan │
839
+ │ └── 全局 allow 规则 │
840
+ └──────────┬───────────────────────────┘
841
+
842
+
843
+ ┌──────────────────────────────────────┐
844
+ │ hasPermissionsToUseTool() — 模式后处理│
845
+ │ │
846
+ │ 3. dontAsk → ask→deny │
847
+ │ 4. Auto 模式: │
848
+ │ ├─ acceptEdits 快速路径 │
849
+ │ ├─ 放行列表 │
850
+ │ └─ YOLO 分类器评估 │
851
+ │ 5. Headless → hooks → auto-deny │
852
+ └──────────┬───────────────────────────┘
853
+
854
+
855
+ ┌──────────────────────────────────────┐
856
+ │ useCanUseTool (React Hook) │
857
+ │ src/hooks/useCanUseTool.tsx │
858
+ │ │
859
+ │ allow → 立即执行 │
860
+ │ deny → 拒绝 + 通知 │
861
+ │ ask → 交互式处理 │
862
+ │ ├─ 协调器检查 │
863
+ │ ├─ Swarm Worker 转发 │
864
+ │ ├─ 投机性分类器 (Bash) │
865
+ │ └─ 用户确认对话框 │
866
+ └──────────────────────────────────────┘
867
+ ```
docs/interview-prep.md ADDED
@@ -0,0 +1,714 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 面试准备指南 — VersperClaw / Claude Code 架构知识体系
2
+
3
+ > 适用场景: 系统设计面试、技术深挖面试、架构师/高级工程师面试
4
+ > 目标: 覆盖 AI CLI 代理的核心设计决策、权衡、以及可以引申到通用分布式系统的知识点
5
+
6
+ ---
7
+
8
+ ## 1. 项目概述 (30-second pitch)
9
+
10
+ **VersperClaw 是什么?**
11
+
12
+ VersperClaw 是从 Anthropic Claude Code fork 出来的 AI CLI 代理 (Agentic Coding Assistant),运行在终端中,核心能力是理解自然语言开发指令并自动执行多步骤编码任务。
13
+
14
+ **三个核心差异化:**
15
+
16
+ 1. **多 Provider 支持** — 不锁定 Anthropic API,可接入 OpenAI、NVIDIA NIM、opencode、vLLM 等第三方 LLM Provider
17
+ 2. **VRM 桌面伴侣 (Friend)** — 带 3D 虚拟角色 (VRM 模型)、情感表情、语音对话能力的同进程桌宠系统
18
+ 3. **语音对话** — 实时语音输入 (WASM VAD) + STT/TTS,支持端到端语音编程交互
19
+
20
+ **一句话概括:**
21
+
22
+ > "一个带 3D 桌宠的 AI 编程助手 CLI"
23
+
24
+ ---
25
+
26
+ ## 2. 核心架构问答 (Q&A format)
27
+
28
+ ---
29
+
30
+ ### Q: 解释 Agent Loop (代理主循环) 的工作原理
31
+
32
+ Agent Loop 是整个系统的核心, 实现在 `src/agent/agent-loop.ts`, 约 88 行核心逻辑, 是一个 async generator。
33
+
34
+ **5 阶段循环:**
35
+
36
+ ```
37
+ [Pre-model Shaping] → [Model Invocation] → [Tool Execution] → [Stop Hooks] → [Continuation Decision]
38
+
39
+ ┌─────┘
40
+
41
+ 回到 [Pre-model Shaping]
42
+ ```
43
+
44
+ 1. **Pre-model Shaping** — 处理 Hook, 注入系统提示词, 计算预算, 决定是否触发压缩 (compact)
45
+ 2. **Model Invocation** — 调用 LLM, 处理 API 错误和重试逻辑
46
+ 3. **Tool Execution** — 执行 LLM 返回的工具调用, 收集结果
47
+ 4. **Stop Hooks** — 处理 Stop 状态 (token 耗尽、tool_use、end_turn、max_tokens)
48
+ 5. **Continuation Decision** — 决定是否继续循环
49
+
50
+ **关键数字:**
51
+
52
+ - `queryLoop()` 核心逻辑 ~88 行
53
+ - 周边基础设施 (工具注册、权限检查、压缩管道、预算跟踪等) 构成 98.4% 的代码量
54
+ - 仅 1.6% 是 AI 决策逻辑 (LLM 调用)
55
+
56
+ **关键特征:**
57
+
58
+ - **追加式状态 (Append-only JSONL)** — 所有消息追加到消息列表, 永不修改历史, 保证可恢复性
59
+ - **恢复机制 (Resilience)** — `max_output_tokens` 耗尽时自动重试 3 次, token 限额从 8K 自动升级到 64K
60
+ - **预算跟踪 (Budget Tracking)** — 每次迭代跟踪输入/输出 token, 超出预算时触发 Reactive Compact
61
+
62
+ ---
63
+
64
+ ### Q: 描述权限系统的 7 种模式和防御纵深
65
+
66
+ **7 种权限模式 (安全光谱从严格到宽松):**
67
+
68
+ | 模式 | 行为 | 适用场景 |
69
+ |------|------|----------|
70
+ | `default` | 每次工具调用都询问用户 | 默认/安全模式 |
71
+ | `acceptEdits` | 自动批准编辑类工具, 其余询问 | 开发日常 |
72
+ | `plan` | 仅允许读操作, 拒绝写操作 | 探索/设计阶段 |
73
+ | `auto` | ML 分类器自动决策 | 有经验的开发者 |
74
+ | `bypassPermissions` | 完全绕过权限检查 | 调试/开发 |
75
+ | `dontAsk` | 静默拒绝所有非白名单工具 | 受限环境 |
76
+ | `bubble` | 权限检查冒泡到父进程 | CI/CD 集成 |
77
+
78
+ **4 层防御纵深:**
79
+
80
+ ```
81
+ Layer 1: Pre-filtering (Deny Rules)
82
+ → 在权限系统外先做 deny 规则匹配, 如 `--dangerously-skip-permissions`
83
+ 会直接跳过某些工具的白名单检查
84
+
85
+ Layer 2: Hooks
86
+ → 用户自定义 hook, 在权限检查前后注入逻辑
87
+ → 可实现自定义审批流程 (如通知 Slack)
88
+
89
+ Layer 3: Rule Evaluation
90
+ → 工具级别的 allow/deny 规则
91
+ → 基于工具名称、参数、文件路径的正则匹配
92
+
93
+ Layer 4: Permission Handler
94
+ → 最终决策层, 根据 mode 决定是否向用户显示提示
95
+ ```
96
+
97
+ **Auto Mode 的 ML 分类器:**
98
+
99
+ - 两阶段分类: 先判断工具类别 (Read vs Action), 再决定自动批准/询问
100
+ - 官方报告的 FNR (假阴性率) 约 17% — 即 17% 本该自动批准的操作被错误地询问用户
101
+ - 渐进信任机制: 系统跟踪用户的 auto-approve rate, 从初期 ~20% 增长到熟练用户的 40%+
102
+
103
+ **核心设计原则:**
104
+
105
+ - **Deny-first**: 任何未明确允许的操作都被拒绝
106
+ - **渐进信任**: 用户必须主动证明可靠性才能获得更多自主权
107
+
108
+ ---
109
+
110
+ ### Q: 上下文压缩的 5 层管道是什么?
111
+
112
+ 上下文窗口有限, 每次消息增长都需要压缩。压缩管道在 `src/agent/compact.ts` 实现。
113
+
114
+ **5 层压缩管道 (按触发顺序):**
115
+
116
+ ```
117
+ Layer 1: Budget Reduction (预算削减)
118
+ → 截断超出预算的历史消息
119
+ → 优先丢弃旧消息, 保留最近的工具调用结果
120
+ → 触发条件: 总 token 超过 budget
121
+
122
+ Layer 2: Snip (低价值裁剪)
123
+ → 移除低价值消息
124
+ → 判断标准: 工具输出是否为错误/空/重复
125
+ → 保留工具调用本身但删除冗余输出
126
+
127
+ Layer 3: Micro-compact (微压缩)
128
+ → 对单条消息做摘要
129
+ → 使用 "Condense" 工具让 LLM 对长输出做一句话总结
130
+ → 保留原始消息的语义但大幅缩减 token
131
+
132
+ Layer 4: Context Collapse (上下文折叠)
133
+ → 折叠多轮交互
134
+ → 将多轮 tool_use + tool_result 对合并为一段摘要
135
+ → 保留最终状态但丢失中间过程
136
+
137
+ Layer 5: Auto-compact (自动摘要)
138
+ → 完整语义摘要
139
+ → 使用 LLM 对整个对话历史执行摘要
140
+ → 最激进, 丢失信息最多, 但 token 节省最大
141
+ ```
142
+
143
+ **核心权衡:**
144
+
145
+ ```
146
+ Context Efficiency (节省 token, 降低成本, 减少超预算风险)
147
+ vs
148
+ Transparency (丢失细节, LLM 可能遗忘关键上下文)
149
+ ```
150
+
151
+ **补充机制:**
152
+
153
+ - **Reactive Compact**: 当 LLM 回复 `max_tokens` 截断时自动触发压缩重试
154
+ - **JSONL 持久化**: 压缩只影响发送给 LLM 的上下文, 原始 JSONL 日志完整保留
155
+
156
+ ---
157
+
158
+ ### Q: 多 Provider 架构如何实现?
159
+
160
+ VersperClaw 支持多种 LLM Provider, 架构分为两层:
161
+
162
+ **Tier 1: Anthropic 原生通道**
163
+
164
+ - 直接使用 Anthropic SDK
165
+ - 支持 Bedrock、Vertex AI、Foundry 三种部署方式
166
+ - 不需要协议转换, 性能最优
167
+
168
+ **Tier 2: 第三方 Provider 通道**
169
+
170
+ - 使用 **Fetch Override 模式**: 拦截 `globalThis.fetch` 方法
171
+ - 将 Anthropic Messages API 请求重写到目标 Provider 的 API 格式
172
+ - 协议转换: Anthropic Messages ↔ OpenAI Chat/Responses API
173
+
174
+ **Fetch Override 的工作原理:**
175
+
176
+ ```
177
+ 原始调用: client.messages.create({model, messages, tools})
178
+ → SDK 内部调用 fetch("https://api.anthropic.com/v1/messages", body)
179
+ → 被 override 拦截
180
+ → 转换 body 格式 (Anthropic → OpenAI)
181
+ → 发送到目标 Provider (如 https://api.openai.com/v1/chat/completions)
182
+ → 转换 response 格式 (OpenAI → Anthropic)
183
+ → 返回给 SDK
184
+ ```
185
+
186
+ **模型列表管理:**
187
+
188
+ - `modelStrings()` 缓存所有可用模型
189
+ - Provider 切换后必须调用 `clearModelStrings()` 清除缓存
190
+ - 模型信息包括: Provider 名、模型 ID、上下文窗口、价格、速率限制
191
+
192
+ **为什么用 Fetch Override 而不是独立 SDK?**
193
+
194
+ 1. 保持统一的 Anthropic Messages 接口, 不需要为每个 Provider 写独立适配
195
+ 2. Fetch Override 是无侵入的: 所有依赖 Anthropic SDK 的代码无需修改
196
+ 3. 对用户透明: 用户配置 Provider 后, 体验完全一致
197
+
198
+ ---
199
+
200
+ ### Q: Friend VRM 系统为什么设计为同进程 + SSE?
201
+
202
+ Friend 是 VersperClaw 的 3D 桌面伴侣系统, 使用 VRM 模型 (3D 虚拟角色), 具备表情、动作、语音对话能力。
203
+
204
+ **架构选择:**
205
+
206
+ ```
207
+ 同进程 (In-process)
208
+ └── Agent 直接调用 messageQueueManager.enqueue()
209
+ └── VRM 渲染在独立窗口 (WebKitGTK)
210
+ └── SSE 从 Agent 进程推到 VRM 窗口
211
+
212
+ vs 微服务 (Microservices)
213
+ └── 需要 IPC/进程间通信
214
+ └── 额外的 HTTP Server 部署
215
+ └── 开发、调试复杂度高
216
+ ```
217
+
218
+ **选择同进程的原因:**
219
+
220
+ 1. **不需要 IPC/进程间通信** — 直接调用 `messageQueueManager.enqueue()` 即可推送消息
221
+ 2. **低延迟** — 同进程调用延迟 <1ms, IPC 至少 1-10ms
222
+ 3. **简化部署** — 用户只需要启动一个二进制文件
223
+ 4. **状态共享** — Agent 的上下文、配置、日志直接可访问
224
+
225
+ **选择 SSE (Server-Sent Events) 而不是 WebSocket 的原因:**
226
+
227
+ | 维度 | SSE | WebSocket |
228
+ |------|-----|-----------|
229
+ | 通信方向 | Server → Client 单向 | 双向 |
230
+ | 协议 | HTTP (简单) | WS (复杂, 需要握手机制) |
231
+ | 适用场景 | LLM → VRM 广播 (推送表情/动作指令) | 双向实时通信 |
232
+ | 浏览器支持 | EventSource API | WebSocket API |
233
+ | 自动重连 | 原生支持 | 需手动实现 |
234
+
235
+ - **核心原因**: VRM 系统的主要通信模式是 Agent → VRM 的单向推送 (LLM 决定表情 → 推送到 VRM 渲染), 不存在 VRM → Agent 的实时控制需求, 因此 SSE 完全足够, 且比 WebSocket 更轻量
236
+
237
+ **WASM VAD 的选择:**
238
+
239
+ - 使用 Silero VAD 的 WASM 编译版本
240
+ - 原因: 避免 `onnxruntime-node` 在 Bun 运行时下的 segfault (Bun 的 napi 兼容性问题)
241
+ - WASM 运行在 WebView 沙箱中, 更稳定
242
+
243
+ **服务端采音 (Server-side Audio Capture):**
244
+
245
+ - 不依赖浏览器 `getUserMedia` API
246
+ - 避免 WebKitGTK 的权限弹窗问题
247
+ - 使用 PulseAudio/ALSA 直接在服务端录制麦克风
248
+
249
+ ---
250
+
251
+ ### Q: Feature Flag 系统如何实现死代码消除?
252
+
253
+ VersperClaw 使用编译时 Feature Flag 系统, 实现 `#ifdef` 风格的死代码消除。
254
+
255
+ **实现位置:** `src/feature/feature.ts` / `feature()` 函数
256
+
257
+ **工作原理:**
258
+
259
+ ```
260
+ 编译时: Bun build --define 注入常量值
261
+
262
+ if (feature("VOICE_MODE")) {
263
+ // 注册语音工具
264
+ registerVoiceTools();
265
+ } else {
266
+ // 这段代码在编译时被消除
267
+ // 不产生任何字节码
268
+ }
269
+ ```
270
+
271
+ **Bun 编译器的支持:**
272
+
273
+ - Bun 的 `feature()` 函数在编���时做 if/ternary 位置的静态分析
274
+ - 当 feature 为 false 时, 整个分支被 DCE (Dead Code Elimination)
275
+ - 最终二进制中完全不包含被禁用的功能代码
276
+
277
+ **编译标志:**
278
+
279
+ - 命令行: `--feature=VOICE_MODE` (编译时启用)
280
+ - 环境变量: `FEATURE_VOICE_MODE=1` (开发时启用)
281
+ - 两者效果等价
282
+
283
+ **Feature 统计:**
284
+
285
+ - 总共 48 个实验性 feature
286
+ - 默认仅 `VOICE_MODE` 启用
287
+ - 其他 feature 如: `ANTHROPIC_BRAZIL`, `BYPASS_PERMISSIONS`, `ORGANIZATION_CODE`, `TOOLTIP` 等
288
+
289
+ **用户类型门控:**
290
+
291
+ - `USER_TYPE='ant'` → Anthropic 内部员工, 解锁内部工具和功能
292
+ - `USER_TYPE='external'` → 外部用户, 仅暴露稳定的公共功能
293
+ - 在编译时通过 `feature()` 检查, 用户类型相关的内部代码完全不会出现在外部构建中
294
+
295
+ ---
296
+
297
+ ### Q: 错误恢复策略有哪些?
298
+
299
+ 一个多层级、渐进式的错误恢复系统:
300
+
301
+ ```
302
+ 1. max_output_tokens 恢复
303
+ └── LLM 输出被截断 (max_tokens 耗尽)
304
+ └── 自动升级 token 限额: 8K → 16K → 32K → 64K
305
+ └── 最多重试 3 次, 之后不再尝试
306
+
307
+ 2. 流式回退 (Streaming Fallback)
308
+ └── SSE 流式连接中断
309
+ └── 自动回退到非流式 (non-streaming) 模式
310
+ └── 用户感知: 延迟增加但可用
311
+
312
+ 3. Reactive Compact (响应式压缩)
313
+ └── LLM 回复被 max_tokens 截断
314
+ └── 触发自动压缩管道, 压缩上下文
315
+ └── 然后重试请求
316
+
317
+ 4. 工具执行重试
318
+ └── 工具调用失败 (网络错误、文件权限等)
319
+ └── 最多重试 3 次
320
+ └── 指数退避 (100ms, 200ms, 400ms)
321
+
322
+ 5. API Fallback Provider
323
+ └── 当前 Provider 不可用 (429/5xx)
324
+ └── 自动切换到下一个配置的 Provider
325
+ └── 配置在 environment.json 中
326
+ ```
327
+
328
+ **核心原则:** 3 次重试后不再尝试 — 避免无限重试的资源浪费和用户等待。
329
+
330
+ ---
331
+
332
+ ## 3. 系统设计面试题
333
+
334
+ ---
335
+
336
+ ### 设计一个 AI 编程助手的权限系统
337
+
338
+ **需求分析:**
339
+
340
+ - AI Agent 可以执行文件读写、命令执行、网络请求等敏感操作
341
+ - 用户需要控制 Agent 的能力范围
342
+ - 不同用户有不同的风险和信任水平
343
+ - 需要有审计和回溯能力
344
+
345
+ **方案设计 (参考 VersperClaw):**
346
+
347
+ ```
348
+ 权限光谱: default → acceptEdits → plan → auto → bypassPermissions → dontAsk → bubble
349
+
350
+ 防御纵深:
351
+ Layer 1: Deny Rules (静态规则)
352
+ └── 基于文件名/路径/工具名的正则匹配
353
+ └── 如: 拒绝所有 /etc/shadow 的读写
354
+
355
+ Layer 2: Permission Hooks (自定义逻辑)
356
+ └── 用户可注入 $HOME/.claude/settings.json 中的钩子
357
+ └── 如: 检查 git status 后才允许 git commit
358
+
359
+ Layer 3: Mode-based Decision (模式决策)
360
+ └── 根据当前 mode 决定审批流程
361
+ └── auto mode 走 ML 分类器, default mode 询问用户
362
+
363
+ Layer 4: ML Classifier (自动分类)
364
+ └── 两阶段: Read vs Action
365
+ └── 特征: 工具名、参数路径、文件类型、操作频率
366
+ └── 输出: allow / ask / deny
367
+
368
+ Layer 5: Execution Sandbox (执行沙箱)
369
+ └── 工具执行在受限环境
370
+ └── 不允许绕过操作系统权限
371
+ ```
372
+
373
+ **面试讨论要点:**
374
+
375
+ 1. **17% FNR 意味着什么?** 每 6 个操作就有 1 个被错误询问用户, 累积使用会造成显著的摩擦。改进方向: 用户反馈闭环、个性化模型微调、规则叠加 ML 的混合系统。
376
+
377
+ 2. **安全 vs 体验的平衡:** 太严格的权限系统用户会绕过 (直接终端操作), 太宽松的系统有安全风险。渐进信任是核心思路。
378
+
379
+ 3. **审计与追溯:** 所有权限决策写 JSONL 日志, 支持后续分析。
380
+
381
+ ---
382
+
383
+ ### 设计一个实时语音聊天系统 (类似 Friend F2)
384
+
385
+ **需求分析:**
386
+
387
+ - 用户通过语音与 AI Agent 对话
388
+ - AI 回复也通过语音播放
389
+ - 需要低延迟 (实时感)
390
+ - 3D 虚拟角色根据对话内容做表情和动作
391
+
392
+ **架构选择: 同进程 vs 微服务**
393
+
394
+ ```
395
+ 方案 A: 同进程 (VersperClaw 的选择)
396
+ ┌─────────────────────────────┐
397
+ │ Agent Process │
398
+ │ ┌──────┐ ┌──────────────┐ │
399
+ │ │ LLM │ │ Audio Engine │ │
400
+ │ │ Call │→│ STT → VAD │ │
401
+ │ └──────┘ │ TTS → Mixer │ │
402
+ │ └──────────────┘ │
403
+ │ ┌────────────────────────┐ │
404
+ │ │ VRM (3D Avatar) │ │
405
+ │ │ SSE ← Emotion Queue │ │
406
+ │ └────────────────────────┘ │
407
+ └─────────────────────────────┘
408
+
409
+ 方案 B: 微服务
410
+ ┌─────────┐ ┌─────────┐ ┌─────────┐
411
+ │ Agent ���──▶│ Audio │──▶│ VRM │
412
+ │ Service │ │ Service │ │ Service │
413
+ └─────────┘ └─────────┘ └─────────┘
414
+ ```
415
+
416
+ **VAD (Voice Activity Detection) 策略:**
417
+
418
+ - **Silero ML VAD** (WASM): 深度学习模型, 准确率高, 能区分人声和环境噪音
419
+ - **Energy-based VAD** (备选): 基于音量的简单检测, 适合信噪比高的环境
420
+ - **回声消除**: 播放 AI 回复时关闭 VAD/采音, 避免识别到 AI 自己的声音
421
+
422
+ **静音策略:**
423
+
424
+ - AI 处理期间: 全程阻断采音 (Press-to-mute)
425
+ - 用户说完后: VAD 检测到静音 → 触发 LLM 调用
426
+ - LLM 回复时: 音频输出独占, 不接收新输入
427
+
428
+ **Provider 切换:**
429
+
430
+ - STT: Whisper (本地) / Azure Speech / Google STT
431
+ - TTS: Piper TTS (本地) / ElevenLabs / Azure TTS
432
+ - 可热切换, 无需重启 Agent
433
+
434
+ **面试追问:**
435
+
436
+ 1. **为什么不用 WebSocket 而是 SSE?** — 因为 VRM 的主要通信是单向推送 (Agent → Avatar), SSE 更轻量, 原生支持自动重连。WebSocket 的额外开销 (握手机制、帧协议) 在单向场景下是过度设计。
437
+
438
+ 2. **为什么不用 onnxruntime-node?** — Bun 运行时对 napi 的兼容性问题, 导致 segfault。WASM 在 WebView 沙箱中运行更稳定。
439
+
440
+ ---
441
+
442
+ ### 设计多 Provider LLM 代理
443
+
444
+ **需求分析:**
445
+
446
+ - 支持多种 LLM Provider (Anthropic, OpenAI, NVIDIA, opencode, vLLM)
447
+ - 统一的接口, 对用户透明
448
+ - Provider 切换不影响 Agent 状态
449
+ - 优雅降级 (Provider 不可用时自动切换)
450
+
451
+ **方案对比:**
452
+
453
+ ```
454
+ 方案 A: Fetch Override (VersperClaw 选型)
455
+ 优点:
456
+ - 无侵入: 不修改 SDK, 不修改 Agent 核心逻辑
457
+ - 统一接口: 所有代码只认识 Anthropic Messages 格式
458
+ - 易于扩展: 新增 Provider 只需要写协议转换层
459
+ 缺点:
460
+ - 依赖 fetch API 的完整性
461
+ - 调试复杂 (请求经过转换层)
462
+ - 无法利用 SDK 原生功能 (如 streaming 的细节)
463
+
464
+ 方案 B: Proxy 模式
465
+ 优点:
466
+ - 请求在中间层转换, 客户端 SDK 无需修改
467
+ - 可以添加缓存、限流、日志
468
+ 缺点:
469
+ - 需要额外部署 Proxy 服务
470
+ - 增加网络延迟
471
+ ```
472
+
473
+ **协议转换 (Anthropic ↔ OpenAI):**
474
+
475
+ ```
476
+ Anthropic Messages → OpenAI Chat/Responses API
477
+
478
+ 关键映射:
479
+ system: system_message
480
+ messages: messages (角色映射: assistant/assistant, user/user)
481
+ tools: tools (function calling 格式)
482
+ tool_use: tool_calls
483
+ tool_result: tool (function response)
484
+ max_tokens: max_tokens
485
+ stop_sequences: stop
486
+
487
+ OpenAI → Anthropic 逆映射同理
488
+ ```
489
+
490
+ **模型列表管理:**
491
+
492
+ - `modelStrings()` 函数缓存所有可用模型的元数据
493
+ - 缓存包括: Provider、模型 ID、上下文窗口、价格信息
494
+ - Provider 切换时必须调用 `clearModelStrings()` 清除缓存
495
+ - 缓存预热: 启动时异步加载所有 Provider 的模型列表
496
+
497
+ **面试追问:**
498
+
499
+ 1. **为什么用 Fetch Override 而不是独立 SDK?** — 对现有代码的侵入最小化。所有依赖 Anthropic SDK 的代码 (包括第三方库) 无需任何修改即可支持新 Provider。
500
+
501
+ 2. **如何处理 Provider 特有的能力?** — 有些 Provider 不支持 tool use 或 streaming, Fetch Override 层需要做降级处理。
502
+
503
+ ---
504
+
505
+ ## 4. 架构权衡 (Trade-offs)
506
+
507
+ ---
508
+
509
+ ### Safety vs. Autonomy (安全 vs 自主性)
510
+
511
+ ```
512
+ 安全优先 自主性优先
513
+ │ │
514
+ │ │
515
+ └── default ─ auto ─ bypassPermissions ──┐
516
+
517
+ 更多权限提示 更少摩擦, 更多风险
518
+ 更安全 更高效
519
+ ```
520
+
521
+ - **37signals 二分法**: 权限提示是摩擦, 但也是安全护栏
522
+ - **VersperClaw 的答案**: 渐进信任 + 4 层防御 + ML 辅助
523
+ - **面试价值**: 展示对安全架构和 UX 权衡的深度理解
524
+
525
+ ---
526
+
527
+ ### Context Efficiency vs. Transparency (上下文效率 vs 透明度)
528
+
529
+ ```
530
+ 节省 token, 降低成本 保留完整上下文
531
+ │ │
532
+ │ │
533
+ └── budget reduction ─ auto-compact ──┐
534
+
535
+ 更便宜的调用 LLM "记住" 更多细节
536
+ 更快响应 更高质量的推理
537
+ 但可能丢失关键信息 但 token 成本更高
538
+ ```
539
+
540
+ - **VersperClaw 的答案**: 5 层压缩管道 + JSONL 持久化 + Append-only 日志
541
+ - **关键洞察**: 压缩丢弃的是"发送给 LLM 的内容", 不是"系统记录的内容"
542
+ - **面试价值**: 展示对 LLM 上下文窗口限制的实际工程理解
543
+
544
+ ---
545
+
546
+ ### Simplicity vs. Extensibility (简单性 vs 可扩展性)
547
+
548
+ ```
549
+ Agent Loop 简单 (~88 行) 扩展机制丰富
550
+ │ │
551
+ │ │
552
+ └── MCP ─ Plugin ─ Skill ─ Hook ──┐
553
+
554
+ 核心逻辑易于理解 4 种不同的扩展点
555
+ 但扩展需要理解多套机制 灵活但复杂度分散
556
+ ```
557
+
558
+ - **4 种扩展机制:**
559
+ 1. **MCP** (Model Context Protocol): 外部工具和资源, 标准化协议
560
+ 2. **Plugin**: 内部插件系统, 可注册新工具和事件监听
561
+ 3. **Skill**: 可组合的预定义工作流 (如 `/review-pr`, `/commit`)
562
+ 4. **Hook**: settings.json 配置, 在事件前后注入用户定义逻辑
563
+
564
+ - **面试价值**: 展示对"保持核心简单, 外围可扩展"架构哲学的理解
565
+
566
+ ---
567
+
568
+ ### 同进程 vs 微服务 (Friend 系统的选择)
569
+
570
+ ```
571
+ 同进程 微服务
572
+ │ │
573
+ │ │
574
+ └── 低延迟 ─ 简单部署 ─ 状态共享 ──┐
575
+
576
+ 适合单用户桌面应用 适合多租户/云端
577
+ 不需要分布式能力 但部署复杂
578
+ 开发效率高 但调试困难
579
+ ```
580
+
581
+ - **VersperClaw 的答案**: 同进程, 因为这是一个单用户终端工具, 不是分布式系统
582
+ - **何时应该选微服务?** 多用户 Web 服务、需要独立扩缩容、团队分工明确
583
+ - **面试价值**: 展示架构选型不是技术炫耀, 而是根据实际场景做合理决策
584
+
585
+ ---
586
+
587
+ ## 5. 关键数据
588
+
589
+ | 指标 | 数值 | 说明 |
590
+ |------|------|------|
591
+ | 代码量 | ~512K 行 TypeScript | 比 Claude Code 原始 fork 增加约 30% |
592
+ | 文件数 | ~1,900 | 模块化程度高 |
593
+ | 测试 | 55 文件, ~22K 行 | 覆盖率低, 无 CI/CD |
594
+ | 构建产物 | 192-202MB 编译二进制 | 包含 Bun runtime + JS bundle |
595
+ | JS Bundle | ~20MB | 除去 Bun runtime 后的纯 JS |
596
+ | Feature Flags | 48 个实验性 feature | 默认仅 VOICE_MODE 启用 |
597
+ | 权限模式 | 7 种 | default → bubble |
598
+ | 压缩管道 | 5 层 | Budget Reduction → Auto-compact |
599
+ | 扩展机制 | 4 种 | MCP / Plugin / Skill / Hook |
600
+ | AI 工具 | ~60+ | 文件操作、Shell 执行、搜索等 |
601
+ | Slash 命令 | ~75+ | /commit, /review-pr, /clear 等 |
602
+ | OpenTelemetry | ~5K 行基础设施 | OSS 构建中全部 stub |
603
+ | VAD 模型 | WASM Silero VAD | 在 WebView 沙箱中运行 |
604
+ | 3D 渲染 | WebKitGTK + Three.js | VRM 模型渲染 |
605
+
606
+ ---
607
+
608
+ ## 6. 常见面试追问
609
+
610
+ ### "为什么不直接用 Vector DB 做记忆?"
611
+
612
+ **答案:** Memdir 文件系统优先, LLM 选择检索。
613
+
614
+ - Vector DB 引入额外的运维复杂度 (需要部署、索引、备份)
615
+ - 文件系统 (Memdir) 更简单、可审计、可编辑
616
+ - LLM 自己决定检索什么: 不是系统自动做 RAG, 而是通过 `Read` 工具让 LLM 按需读取 memdir 文件
617
+ - 适用场景: 单用户桌面工具, 不需要多租户的向量检索
618
+
619
+ ### "为什么 Agent Loop 这么短?"
620
+
621
+ **答案:** 确定性基础设施在周围, 不是在里面。
622
+
623
+ - 88 行核心循环只做"编排" (orchestration), 不做"实现"
624
+ - 工具注册、权限检查、压缩、预算跟踪等逻辑被拆到各自的模块
625
+ - 这是**策略模式 (Strategy Pattern)** 的体现: 主循环是稳定的骨架, 各个阶段的行为通过依赖注入可配置
626
+
627
+ ### "SSE 和 WebSocket 怎么选?"
628
+
629
+ **答案:** 看通信方向。
630
+
631
+ | 场景 | 推荐 | 原因 |
632
+ |------|------|------|
633
+ | Server → Client 单向推送 | SSE | 更轻量, 原生重连, HTTP 友好 |
634
+ | 双向实时通信 | WebSocket | 全双工, 低延迟 |
635
+ | 浏览器 → Server 流式上传 | WebSocket | SSE 只支持下行 |
636
+
637
+ Friend VRM 的场景是 Agent → Avatar 的单向广播, SSE 是最优解。如果未来需要 Avatar → Agent 的控制 (如用户点击 VRM 触发动作), 那才需要 WebSocket。
638
+
639
+ ### "为什么不用 onnxruntime-node?"
640
+
641
+ **答案:** Bun 的 napi 兼容性问题。
642
+
643
+ - `onnxruntime-node` 依赖 Node.js 的 napi (Native API), Bun 的实现在某些版本存在 segfault
644
+ - WASM 版本在 WebView 沙箱中运行, 稳定性更好
645
+ - 这不是架构决策, 是运行时兼容性的务实现实
646
+
647
+ ### "Compaction 丢失信息怎么办?"
648
+
649
+ **答案:** JSONL 持久化, append-only 日志。
650
+
651
+ - 压缩只影响"发送给 LLM 的上下文", 不影响"系统记录的数据"
652
+ - 所有原始消息追加到 JSONL 文件, 永不删除
653
+ - 如果 LLM 需要回看被压缩的内容, 可以通过 `Read` 工具读取 JSONL 日志
654
+ - 这也是为什么压缩管道有 5 层: 渐进式压缩, 先丢最不重要的, 最后才做语义摘要
655
+
656
+ ### "如何测试一个 AI Agent 系统?"
657
+
658
+ **答案:** 测试 AI Agent 的挑战和策略。
659
+
660
+ - **黄金数据集**: 录制真实的 Agent 交互 (JSONL), 用作回归测试
661
+ - **Tool 模拟**: Mock 工具执行结果, 测试 LLM 的决策逻辑
662
+ - **快照测试**: 对比压缩/权限决策的输出快照
663
+ - **E2E 测试**: 实际调用 LLM (成本高, 运行慢), 只在关键路径使用
664
+ - **当前状态**: 55 个测试文件, ~22K 行, 但无 CI/CD — 这是需要改进的地方
665
+
666
+ ### "这个系统的最大弱点是什么?"
667
+
668
+ **诚实回答 (面试加分项):**
669
+
670
+ 1. **测试覆盖不足** — 无 CI/CD, 55 个测试文件对 ~512K 行代码几乎不可靠
671
+ 2. **Monorepo 膨胀** — ~1,900 文件, 构建产物 200MB, 模块边界模糊
672
+ 3. **ML 分类器的 17% FNR** — 虽然可以接受, 但累积使用会造成显著摩擦
673
+ 4. **同进程限制扩展** — Friend 系统无法独立部署, 难以支持多实例
674
+ 5. **依赖 Bun 生态** — Bun 的稳定性影响整个系统 (napi 问题, undici fetch 兼容性等)
675
+
676
+ ---
677
+
678
+ ## 附录: 面试应答策略
679
+
680
+ ### 当被问到不熟悉的问题时
681
+
682
+ - "这个问题我没有直接经验, 但基于我对系统的理解, 我会这样分析..."
683
+ - **STAR 法则**: Situation → Task → Action → Result
684
+ - 始终展示**架构思维**: 不管多小的功能, 都能讨论 trade-off
685
+
686
+ ### 描述项目的三种粒度
687
+
688
+ ```
689
+ 30 秒: "一个带 3D 桌宠的 AI 编程助手 CLI"
690
+ 2 分钟: "VersperClaw 是从 Claude Code fork 的 AI CLI 代理,
691
+ 核心增强是多 Provider 支持和 VRM 桌面伴侣,
692
+ 采用同进程 + SSE 架构实现低延迟语音对话"
693
+ 10 分钟: 深入 Agent Loop、权限系统、压缩管道、Friend 架构
694
+ ```
695
+
696
+ ### 把 VersperClaw 经验映射到通用系统设计
697
+
698
+ | VersperClaw 概念 | 通用系统设计概念 |
699
+ |------------------|-------------------|
700
+ | Agent Loop | Event-driven orchestration |
701
+ | 4 层防御纵深 | Defense in depth |
702
+ | 5 层压缩管道 | Multi-stage data processing pipeline |
703
+ | Append-only JSONL | Event sourcing / Write-ahead log |
704
+ | Feature Flag 死代码消除 | Compile-time configuration |
705
+ | Fetch Override | API Gateway / Proxy pattern |
706
+ | Provider 切换 | Circuit breaker / Fallback |
707
+ | 渐进信任 | Zero-trust architecture (gradual) |
708
+ | 同进程 Friend | Embedded system / Co-located deployment |
709
+ | SSE 推送 | Publisher-Subscriber pattern (one-way) |
710
+
711
+ ---
712
+
713
+ > 最后更新: 2026-06-22
714
+ > 基于 VersperClaw main branch (commit 835ff5a)
docs/services/compact-deep-dive.md ADDED
@@ -0,0 +1,906 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 上下文压缩管道 (Compaction Pipeline) 深度分析
2
+
3
+ > 本文档基于 VersperClaw 源代码分析,涵盖 `/src/services/compact/` 目录下的全部压缩机制,
4
+ > 以及 `/src/query.ts` 中压缩管道编排逻辑。
5
+
6
+ ---
7
+
8
+ ## 1. 压缩管道的动机
9
+
10
+ ### 1.1 上下文窗口是绑定资源约束
11
+
12
+ 大语言模型(LLM)的推理上下文存在**硬性 token 上限**。对 Claude 系列模型而言,
13
+ 这个上限通常是 200K token。一旦消息历史超过该上限,API 将返回 `prompt_too_long` 错误,
14
+ 对话无法继续。
15
+
16
+ 在实际使用中,上下文窗口面临三个核心矛盾:
17
+
18
+ ```
19
+ Token 使用量
20
+ ^
21
+ | / 硬上限 (200K)
22
+ | /
23
+ | / 自动压缩阈值 (~window - 13K)
24
+ | /
25
+ | / 警告阈值 (~window - 20K)
26
+ | /
27
+ | / 实际用量 (随时间增长)
28
+ | /
29
+ +------------------------------------------> 时间/轮次
30
+ ```
31
+
32
+ ### 1.2 三个关键约束
33
+
34
+ | 约束 | 说明 | 代码证据 |
35
+ |------|------|----------|
36
+ | **Token 硬限制** | 超过模型上下文窗口后 API 拒绝请求 | `compact.ts:106-107` 的 `PROMPT_TOO_LONG_ERROR_MESSAGE` |
37
+ | **90% 阈值后性能下降** | 接近窗口上限时,模型的检索精度和推理质量显著下降 | `autoCompact.ts:62` 的 `AUTOCOMPACT_BUFFER_TOKENS = 13_000` 保留缓冲 |
38
+ | **Cache 效率衰减** | 大上下文降低 prompt caching 命中率,增加 API 成本和延迟 | `compact.ts:435-438` 的 `tengu_compact_cache_prefix` 实验开关 |
39
+
40
+ ### 1.3 压缩管道的设计目标
41
+
42
+ 压缩管道的核心目标是:**在保证对话质量的前提下,以最小代价持续将上下文维持在可用窗口内**。
43
+
44
+ 设计原则:
45
+ - **分层递进**:从零成本到高成本逐层尝试,避免过早触发昂贵的 LLM 调用
46
+ - **优先级有向**:保留高价值消息(用户意图、关键决策、代码变更),裁剪低价值内容(工具结果、确认消息)
47
+ - **无损恢复**:压缩后的关键状态(文件附件、计划、技能)自动恢复
48
+ - **语义连续**:模型感知不到压缩的发生,对话体验自然衔接
49
+
50
+ ---
51
+
52
+ ## 2. 五层压缩管道
53
+
54
+ 压缩管道在 `src/query.ts` 的主循环中按固定顺序编排执行。
55
+ 每一层如果成功缓解了上下文压力,后续更昂贵的层就不会触发。
56
+
57
+ ```
58
+ 查询开始
59
+
60
+
61
+ ┌─────────────────────────────────────┐
62
+ │ Layer 1: 预算削减 (Budget Re- │ 零成本
63
+ │ duction) │
64
+ │ 工具结果 → 磁盘换预览 │
65
+ └─────────────────────────────────────┘
66
+
67
+
68
+ ┌─────────────────────────────────────┐
69
+ │ Layer 2: 剪切 (Snip) │ 零成本
70
+ │ 移除低价值消息 │
71
+ └─────────────────────────────────────┘
72
+
73
+
74
+ ┌─────────────────────────────────────┐
75
+ │ Layer 3: 微压缩 (Micro-compact) │ 近零成本
76
+ │ 删除旧工具结果 / 缓存编辑 │
77
+ └─────────────────────────────────────┘
78
+
79
+
80
+ ┌─────────────────────────────────────┐
81
+ │ Layer 4: 上下文折叠 (Context │ O(1)
82
+ │ Collapse) │
83
+ │ 多轮交互 → 紧凑提交 │
84
+ └─────────────────────────────────────┘
85
+
86
+
87
+ ┌─────────────────────────────────────┐
88
+ │ Layer 5: 自动压缩 (Auto-compact) │ 高成本
89
+ │ LLM 语义摘要 │ (一次 API 调用)
90
+ └─────────────────────────────────────┘
91
+
92
+
93
+ 发送给 API
94
+ ```
95
+
96
+ ---
97
+
98
+ ### Layer 1: 预算削减 (Budget Reduction)
99
+
100
+ **文件**: `src/utils/toolResultStorage.ts` (第 740-839 行)
101
+ **函数**: `enforceToolResultBudget()`
102
+ **成本**: 零 LLM 调用。纯内存操作,涉及可选的磁盘 I/O。
103
+
104
+ #### 触发条件
105
+
106
+ 每次模型调用前无条件执行。它有一个内部"预算"概念:每条用户消息中的工具结果
107
+ (`tool_result`)聚合大小超过**单条消息预算限制**(通过 GrowthBook 的
108
+ `tengu_plum_marten` 特性标识配置,见 `getPerMessageBudgetLimit()`)。
109
+
110
+ #### 算法/策略
111
+
112
+ ```
113
+ 对于每条用户消息的工具结果组:
114
+ 1. ��分已知结果(seenIds 中已存在)和新鲜结果
115
+ 2. 已知结果 → 从缓存中重新应用相同的预览替换
116
+ 3. 新鲜结果 → 检查聚合大小是否超限
117
+ 4. 超限 → 选择最大的新鲜结果持久化到磁盘,替换为预览摘要
118
+ 5. 记录替换决策到 seenIds 和 replacements Map
119
+ ```
120
+
121
+ 核心逻辑(`enforceToolResultBudget` 第 769 行):
122
+ - `seenIds`:追踪已处理过的 `tool_use_id`,避免同一结果被反复替换
123
+ - `replacements`:缓存替换后的预览内容,后续调用直接复用(零 I/O)
124
+ - 每条消息独立计算,不会跨消息"借用"预算
125
+
126
+ #### 保留什么
127
+
128
+ - 所有不超过预算的消息保持原样
129
+ - 用户自然语言消息始终完整保留
130
+ - 不超过预算的工具结果完整保留
131
+
132
+ #### 丢弃什么
133
+
134
+ - 超预算消息中**最大的新鲜工具结果**被替换为磁盘文件引用 + 摘要预览
135
+ - 替换是幂等的:同一 `tool_use_id` 每次得到相同的预览
136
+
137
+ #### 为什么这是第一层
138
+
139
+ 此层在物理上压缩消息内容但不改变消息结构。它运行在微压缩**之前**,
140
+ 因为缓存微压缩(Cached MC)只通过 `tool_use_id` 操作——它对内容替换不可见,
141
+ 两者可以干净地组合(`query.ts:369-394` 注释)。
142
+
143
+ ---
144
+
145
+ ### Layer 2: 剪切 (Snip)
146
+
147
+ **文件**: `src/services/compact/snipCompact.ts`
148
+ **成本**: 零 LLM 调用。纯消息过滤。
149
+
150
+ #### 触发条件
151
+
152
+ 由 `feature('HISTORY_SNIP')` 特性标识控制,仅在内部版本中启用
153
+ (`query.ts:115-116`)。外部构建中是空操作桩(stub)。
154
+
155
+ 外部构建中的状态(`snipCompact.ts:9`):
156
+
157
+ ```typescript
158
+ export function isSnipRuntimeEnabled(): boolean {
159
+ return false
160
+ }
161
+ ```
162
+
163
+ #### 算法/策略
164
+
165
+ 移除低价值消息。典型的可剪切消息包括:
166
+ - 用户简短确认("ok"、"好的"、"继续")
167
+ - 简单的工具结果(bash 命令退出码 0、文件写入成功)
168
+ - 重复的系统消息
169
+
170
+ 剪切操作会插入一个 `snip_marker` 边界消息(`subtype === 'snip_marker'`)
171
+ 以标记消息已被移除,同时保持消息链的连续性。
172
+
173
+ #### Token 节省传递
174
+
175
+ `query.ts:400-410` 中,snip 操作产生的 `tokensFreed` 被传递给后续的
176
+ `shouldAutoCompact()` 检查。这是因为 `tokenCountWithEstimation` 从存活
177
+ assistant 消息的 usage 字段读取 token 计数,而 snip 移除的是用户消息,
178
+ 导致计费 token 与实际压缩后的上下文不一致。
179
+
180
+ ---
181
+
182
+ ### Layer 3: 微压缩 (Micro-compact)
183
+
184
+ **文件**: `src/services/compact/microCompact.ts`
185
+ **函数**: `microcompactMessages()` (第 253 行)
186
+ **成本**: 近零。纯 JS 逻辑,无 LLM 调用。
187
+
188
+ 微压缩有三个子路径,按优先级执行:
189
+
190
+ ```
191
+ microcompactMessages()
192
+
193
+ ├── 时间触发微压缩 (Time-based MC)
194
+ │ 如果自上次 assistant 消息超过阈值 → 清除旧工具结果内容
195
+
196
+ ├── 缓存微压缩 (Cached MC)
197
+ │ 使用 API cache_edits 在不破坏缓存前缀的前提下删除工具结果
198
+
199
+ └── 传统路径 (已移除)
200
+ 之前使用本地消息修改,已被 Cached MC 完全取代
201
+ ```
202
+
203
+ #### 3a: 时间触发微压缩 (Time-based MC)
204
+
205
+ **文件**: `src/services/compact/timeBasedMCConfig.ts`
206
+ **配置键**: `tengu_slate_heron`
207
+ **触发**: `evaluateTimeBasedTrigger()` (第 422 行)
208
+
209
+ 当满足以下条件时触发:
210
+ 1. 特性启用(`enabled: true`)
211
+ 2. 主线程查询源(`querySource` 以 `repl_main_thread` 开头或为空)
212
+ 3. 最后一条 assistant 消息的时间戳与当前时间之差超过配置阈值(默认 60 分钟)
213
+ 4. 存在可压缩的工具结果
214
+
215
+ 此时,服务器端 prompt cache 几乎肯定已过期(60 分钟 TTL),
216
+ 全部前缀将被重写——因此在请求前清除旧工具结果内容以缩小重写范围。
217
+
218
+ 操作:将除最近 N 个(默认 5 个,`keepRecent: 5`)之外的所有可压缩工具
219
+ 结果内容替换为 `'[Old tool result content cleared]'` 标记
220
+ (`microCompact.ts:36` 的 `TIME_BASED_MC_CLEARED_MESSAGE`)。
221
+
222
+ **重要副作用**:内容更改使服务器缓存失效,因此重置缓存微压缩状态
223
+ (`resetMicrocompactState()`),防止后续缓存编辑引用已不存在的工具。
224
+
225
+ #### 3b: 缓存微压缩 (Cached MC)
226
+
227
+ **文件**: `src/services/compact/cachedMicrocompact.ts`
228
+ **函数**: `cachedMicrocompactPath()` (第 305 行)
229
+ **配置**: `cachedMCConfig.ts`
230
+
231
+ 这是真正巧妙的微压缩——利用 Anthropic API 的 `cache_edits` 功能,
232
+ 在不改变本地消息数组的情况下,通过 API 层面删除旧工具结果。
233
+ prompt cache 前缀保持完整,无需重新计算。
234
+
235
+ ```
236
+ 传统方式 (已废弃) 缓存编辑方式 (当前)
237
+ ┌────────────────┐ ┌────────────────┐
238
+ │ 修改消息内容 │ │ 消息内容不变 │
239
+ │ 删除工具结果块 │ │ 添加 cache_edits│
240
+ │ Prompt cache 失效│ �� Prompt cache 命中│
241
+ │ 重新发送全部前缀 │ │ 只发送差异编辑 │
242
+ └────────────────┘ └────────────────┘
243
+ ```
244
+
245
+ 状态管理(`CachedMCState`):
246
+ - `registeredTools`: 已注册的工具结果 ID 集合
247
+ - `toolOrder`: 工具结果注册的顺序列表
248
+ - `deletedRefs`: 已被删除的引用集合
249
+ - `pinnedEdits`: 需要固定在特定用户消息位置的编辑块
250
+
251
+ 触发条件:
252
+ - 已注册的工具结果数 >= `triggerThreshold`(默认 12 个)
253
+ - 模型支持缓存编辑(`supportedModels: ['claude-opus-4-6', 'claude-sonnet-4-6']`)
254
+ - 主线程查询源
255
+
256
+ 执行流程:
257
+ 1. 扫描消息,收集所有可压缩工具 ID(`collectCompactableToolIds()`,第 226 行)
258
+ 2. 将新的工具结果注册到状态中(`registerToolResult()`/`registerToolMessage()`)
259
+ 3. 检查是否需要触发删除(`getToolResultsToDelete()`,第 62 行)
260
+ 4. 创建 `cache_edits` 块(`createCacheEditsBlock()`,第 73 行)
261
+ 5. 将待处理的编辑排入队列,在 API 调用时发送
262
+
263
+ **可压缩工具列表**(`microCompact.ts:41-50`):
264
+
265
+ ```typescript
266
+ const COMPACTABLE_TOOLS = new Set([
267
+ FILE_READ_TOOL_NAME, // Read
268
+ ...SHELL_TOOL_NAMES, // Bash
269
+ GREP_TOOL_NAME, // Grep
270
+ GLOB_TOOL_NAME, // Glob
271
+ WEB_SEARCH_TOOL_NAME, // WebSearch
272
+ WEB_FETCH_TOOL_NAME, // WebFetch
273
+ FILE_EDIT_TOOL_NAME, // Edit
274
+ FILE_WRITE_TOOL_NAME, // Write
275
+ ])
276
+ ```
277
+
278
+ 这些工具的结果通常体积大但信息密度低,且模型可以直接重新调用这些工具
279
+ 获取最新信息。
280
+
281
+ #### 3c: API 微压缩 (API Micro-compact)
282
+
283
+ **文件**: `src/services/compact/apiMicrocompact.ts`
284
+ **函数**: `getAPIContextManagement()` (第 64 行)
285
+
286
+ 提供服务器端上下文管理策略配置:
287
+ - `clear_tool_uses_20250919`: 按输入 token 阈值清除旧工具结果/工具调用
288
+ - `clear_thinking_20251015`: 按轮次保留/清除思考块
289
+
290
+ 仅在 `process.env.USER_TYPE === 'ant'` 且设置了环境变量时生效。
291
+
292
+ ---
293
+
294
+ ### Layer 4: 上下文折叠 (Context Collapse)
295
+
296
+ **文件**: `src/services/contextCollapse/index.ts`
297
+ **成本**: O(1) 投影操作。无 LLM 调用。
298
+
299
+ #### 触发条件
300
+
301
+ 由 `feature('CONTEXT_COLLAPSE')` 特性标识控制。当前版本中为**空操作桩**,
302
+ 仅在内部构建中实际启用(`query.ts:18-19`)。
303
+
304
+ ```typescript
305
+ // 外部构建:
306
+ export function isContextCollapseEnabled(): boolean { return false }
307
+ export async function applyCollapsesIfNeeded(messages, _, __) { return { messages } }
308
+ ```
309
+
310
+ #### 概念
311
+
312
+ 上下文折叠是一种**增量提交**机制。它将多轮交互压缩为紧凑的摘要提交(commit),
313
+ 同时保持主 REPL 数组的完整性。核心思想是将压缩操作从一次性大规模 API 调用
314
+ 分解为持续的增量操作。
315
+
316
+ 折叠状态通过提交日志持久化,每次投影视图时重新应用。这使得折叠在对话轮次之间保持稳定。
317
+
318
+ #### 与自动压缩的关系
319
+
320
+ 当上下文折叠启用时,自动压缩(Auto-compact)**被抑制**(`autoCompact.ts:215-223`):
321
+
322
+ ```typescript
323
+ if (feature('CONTEXT_COLLAPSE')) {
324
+ const { isContextCollapseEnabled } = require('../contextCollapse/index.js')
325
+ if (isContextCollapseEnabled()) {
326
+ return false // 阻止自动压缩
327
+ }
328
+ }
329
+ ```
330
+
331
+ 原因是折叠系统在 90%(提交起点)和 95%(阻塞阈值)之间自主管理上下文空间。
332
+ 自动压缩在 ~93% 的阈值触发,会与折叠竞争并通常获胜——破坏折叠即将保存的
333
+ 细粒度上下文。
334
+
335
+ ---
336
+
337
+ ### Layer 5: 自动压缩 (Auto-compact)
338
+
339
+ **文件**: `src/services/compact/autoCompact.ts` (编排), `compact.ts` (执行), `prompt.ts` (提示词)
340
+ **函数**: `autoCompactIfNeeded()` (第 241 行) -> `compactConversation()` (第 387 行)
341
+ **成本**: **高。** 一次完整的 LLM API 调用,内容包括整个待压缩消息集 + 系统提示词。
342
+
343
+ #### 触发条件
344
+
345
+ `shouldAutoCompact()` (第 160 行) 计算:
346
+
347
+ ```
348
+ 阈值 = getEffectiveContextWindowSize(model) - AUTOCOMPACT_BUFFER_TOKENS(13,000)
349
+ = (contextWindow - min(maxOutputTokens, 20,000)) - 13,000
350
+ ```
351
+
352
+ ```
353
+ getEffectiveContextWindowSize(model) = contextWindow - min(maxOutput, 20,000)
354
+
355
+ getAutoCompactThreshold(model) = effectiveWindow - 13,000 (AUTOCOMPACT_BUFFER_TOKENS)
356
+ ```
357
+
358
+ 环境变量覆盖:
359
+ - `CLAUDE_CODE_AUTO_COMPACT_WINDOW`: 直接设置上下文窗口上限
360
+ - `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE`: 按百分比设置阈值(用于测试)
361
+
362
+ **递归保护**(第 171-183 行):
363
+ - `querySource === 'session_memory'` 或 `'compact'` → 跳过(分叉代理会死锁)
364
+ - `querySource === 'marble_origami'` → 跳过(上下文折叠分叉会破坏主线程状态)
365
+
366
+ **熔断机制**(`MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES = 3`):
367
+ 当上下文不可恢复地超限时(如 `prompt_too_long`),连续失败 3 次后
368
+ 自动压缩永久跳过当前会���——否则全局每天浪费约 25 万次无效 API 调用。
369
+
370
+ #### 算法/策略
371
+
372
+ 自动压缩首先尝试**会话记忆压缩**(见第 5 节,无 LLM 调用),
373
+ 只有在该路径失败时才退回到完整的 LLM 摘要压缩。
374
+
375
+ ```
376
+ autoCompactIfNeeded()
377
+
378
+ ├── 尝试会话记忆压缩 (trySessionMemoryCompaction)
379
+ │ ├── 成功 → 返回 CompactionResult (无 API 调用)
380
+ │ └── 失败 → 继续
381
+
382
+ └── 完整压缩 (compactConversation)
383
+ ├── 执行 PreCompact 钩子
384
+ ├── 生成 LLM 摘要
385
+ ├── 恢复文件附件
386
+ ├── 创建边界标记
387
+ └── 执行 PostCompact 钩子
388
+ ```
389
+
390
+ `compactConversation()` 的执行流程:
391
+
392
+ ```
393
+ 1. 验证消息非空
394
+ 2. 执行 PreCompact 钩子(自定义指令合并)
395
+ 3. 构建摘要提示词 (getCompactPrompt)
396
+ 4. 流式调用 LLM 获取摘要
397
+ ├── 优先使用缓存共享路径 (forked agent)
398
+ │ 复用主线程的 prompt cache,无需重新计算
399
+ │ 节省 ~90% 的输入 token 成本
400
+ └── 失败时退回到常规流式路径
401
+ 5. 保存预压缩文件状态,清空读文件缓存
402
+ 6. 并行生成:
403
+ ├── 文件附件恢复 (createPostCompactFileAttachments)
404
+ ├── 异步代理状态附件 (createAsyncAgentAttachmentsIfNeeded)
405
+ ├── 计划文件附件 (createPlanAttachmentIfNeeded)
406
+ ├── 计划模式指令附件 (createPlanModeAttachmentIfNeeded)
407
+ ├── 技能内容附件 (createSkillAttachmentIfNeeded)
408
+ ├── 延迟工具增量声明 (getDeferredToolsDeltaAttachment)
409
+ ├── 代理列表增量声明 (getAgentListingDeltaAttachment)
410
+ └── MCP 指令增量声明 (getMcpInstructionsDeltaAttachment)
411
+ 7. 执行 SessionStart 钩子
412
+ 8. 创建压缩边界标记 (createCompactBoundaryMessage)
413
+ 9. 构建摘要用户消息
414
+ 10. 执行 PostCompact 钩子
415
+ 11. 返回 CompactionResult
416
+ ```
417
+
418
+ #### 缓存共享路径
419
+
420
+ `streamCompactSummary()` (第 1136 行) 使用 `runForkedAgent()` 创建分叉代理。
421
+
422
+ 关键的优化:**不设置 `maxOutputTokens`**。分叉代理复用主线程的 prompt cache,
423
+ 而 `maxOutputTokens` 会影响思考配置的 `budget_tokens`,设置它会破坏缓存匹配,
424
+ 导致缓存未命中。
425
+
426
+ 当缓存共享失败时(约 2.79% 的 Sonnet 4.6 调用),退回到在消息末尾附加
427
+ 摘要请求的常规流式路径。
428
+
429
+ #### 摘要提示词结构
430
+
431
+ **文件**: `src/services/compact/prompt.ts`
432
+
433
+ 提示词包含一个强力的"无工具"前缀(`NO_TOOLS_PREAMBLE`,第 19 行):
434
+
435
+ ```text
436
+ CRITICAL: Respond with TEXT ONLY. Do NOT call any tools.
437
+ - Do NOT use Read, Bash, Grep, Glob, Edit, Write, or ANY other tool.
438
+ - Tool calls will be REJECTED and will waste your only turn.
439
+ ```
440
+
441
+ 提示词要求模型生成 `<analysis>` 草稿块 + `<summary>` 结构化摘要。
442
+ 摘要包含 9 个部分:
443
+ 1. 主要请求和意图
444
+ 2. 关键技术概念
445
+ 3. 文件和代码部分
446
+ 4. 错误和修复
447
+ 5. 问题解决
448
+ 6. 所有用户消息
449
+ 7. 待处理任务
450
+ 8. 当前工作
451
+ 9. 可选下一步
452
+
453
+ `formatCompactSummary()` (第 311 行) 在摘要到达上下文之前剥离 `<analysis>` 草稿块。
454
+
455
+ #### 图像剥离
456
+
457
+ 在发送给摘要 LLM 之前,`stripImagesFromMessages()` (第 145 行) 将用户消息中的
458
+ 图像/文档块替换为 `[image]` / `[document]` 文本标记。原因:
459
+ - 图像对生成会话摘要不是必需的
460
+ - 图像可能导致压缩 API 调用本身命中 `prompt_too_long` 限制
461
+ - 在 CCD 会话中用户经常附加图像,这个问题尤为明显
462
+
463
+ #### Prompt-Too-Long 重试
464
+
465
+ 当摘要请求本身命中 `prompt_too_long`(CC-1180)时,
466
+ `truncateHeadForPTLRetry()` (第 243 行) 按 API 轮次组从最早的轮次开始丢弃消息,
467
+ 直到释放足够的 token 空间。最多重试 `MAX_PTL_RETRIES = 3` 次。
468
+
469
+ ---
470
+
471
+ ## 3. 压缩触发时机
472
+
473
+ 压缩可以在四个不同的时间点触发:
474
+
475
+ ### 3.1 预模型上下文塑造 (每次模型调用前)
476
+
477
+ **位置**: `src/query.ts` 第 369-467 行
478
+ **始终发生**: 预算削减和微压缩**在每次 API 调用前执行**。
479
+
480
+ 这些是低成本的防御性操作,确保发送给 LLM 的上下文尽可能精简。
481
+
482
+ 执行顺序:
483
+ ```
484
+ applyToolResultBudget() → Layer 1 (无条件)
485
+ snipCompactIfNeeded() → Layer 2 (仅内部构建)
486
+ microcompactMessages() → Layer 3 (无条件)
487
+ contextCollapse → Layer 4 (仅内部构建)
488
+ autoCompactIfNeeded() → Layer 5 (条件触发)
489
+ ```
490
+
491
+ ### 3.2 反应式压缩 (API 返回 prompt_too_long 时)
492
+
493
+ **文件**: `src/services/compact/reactiveCompact.ts` (外部构建桩)
494
+ **触发**: 当 API 返回 `prompt_too_long` 错误时
495
+
496
+ 即使所有五层都正确执行,理论上仍可能命中 `prompt_too_long`(token 预估不准确)。
497
+ 反应式压缩是"最后一道防线"——当 API 拒绝请求时触发。
498
+
499
+ 由 `feature('REACTIVE_COMPACT')` 控制。
500
+
501
+ ### 3.3 手动压缩 (/compact 命令)
502
+
503
+ **文件**: `src/commands/compact/compact.ts`
504
+ **触发**: 用户输入 `/compact` 或在 UI 中点击压缩按钮
505
+
506
+ 手动压缩的流程:
507
+ 1. 首先尝试会话记忆压缩(如果没有自定义指令)
508
+ 2. 如果启用了反应式模式,通过反应式路径路由
509
+ 3. 退回到传统完整压缩
510
+ 4. 可选:传递自定义压缩指令(`/compact 请重点关注测试文件`)
511
+
512
+ 手动压缩始终 `isAutoCompact = false`,这意味着失败时会显示错误通知。
513
+
514
+ ### 3.4 自动压缩 (定期触发)
515
+
516
+ **触发**: `shouldAutoCompact()` 在每次模型调用前检查
517
+
518
+ 如上所述,通过 token 阈值自动判断是否触发。自动压缩会抑制后续提问
519
+ (`suppressFollowUpQuestions: true`),模型摘要后直接继续工作。
520
+
521
+ ---
522
+
523
+ ## 4. 部分压缩 vs 完整压缩
524
+
525
+ ### 4.1 完整压缩 (compactConversation)
526
+
527
+ **文件**: `src/services/compact/compact.ts:387`
528
+ **函数**: `compactConversation(messages, context, cacheSafeParams, suppressFollowUp, customInstructions, isAutoCompact)`
529
+
530
+ 作用于**所有消息**。LLM 对整个对话生成结构化摘要,用摘要替换所有早期消息,
531
+ 保留最近的附件和状态。
532
+
533
+ ```
534
+ 压缩前 压缩后
535
+ ┌──────────────────┐ ┌──────────────────┐
536
+ │ System Prompt │ │ System Prompt │
537
+ │ User: "写一个API" │ │ User: "写一个API" │
538
+ │ Assistant: [工具调用]│ ──► │ Boundary Marker │
539
+ │ User: [工具结果] │ │ Summary: "用户要求 │
540
+ │ Assistant: "完成了"│ │ 写API,使用了Read │
541
+ │ User: "再加日志" │ │ 和Edit..." │
542
+ │ Assistant: [工具调用]│ │ File Attachments │
543
+ │ ... │ │ Skill Attachments │
544
+ └──────────────────┘ │ Hook Results │
545
+ └──────────────────┘
546
+ ```
547
+
548
+ ### 4.2 部分压缩 (partialCompactConversation)
549
+
550
+ **文件**: `src/services/compact/compact.ts:772`
551
+ **函数**: `partialCompactConversation(allMessages, pivotIndex, context, cacheSafeParams, userFeedback, direction)`
552
+
553
+ 作用于**枢轴点附近的消息**。有两种方向:
554
+
555
+ #### 方向 'from' (前缀保留)
556
+
557
+ 压缩枢轴点**之后**的消息,保留之前的消息。prompt cache 对保留的(早期)消息有效。
558
+
559
+ ```
560
+ ├── 保留 ──┤ ←── 压缩 ──→
561
+ [早期消息] [枢轴] [后续消息]
562
+ 保留 摘要
563
+ (cache 命中)
564
+ ```
565
+
566
+ #### 方向 'up_to' (后缀保留)
567
+
568
+ 压缩枢轴点**之前**的消息,保留之后的消息。prompt cache 失效(摘要位于保留消息之前)。
569
+
570
+ ```
571
+ ←── 压缩 ──→ ├── 保留 ──┤
572
+ [早期消息] [枢轴] [后续消息]
573
+ 摘要 保留
574
+ (cache 失效)
575
+ ```
576
+
577
+ #### 哪些消息类型可以被压缩
578
+
579
+ 部分压缩接受完整的消息类型,但 `up_to` 方向会**剥离**旧压缩边界和摘要消息
580
+ (第 790-799 行):
581
+
582
+ ```typescript
583
+ const messagesToKeep = direction === 'up_to'
584
+ ? allMessages.slice(pivotIndex).filter(
585
+ m => m.type !== 'progress' &&
586
+ !isCompactBoundaryMessage(m) &&
587
+ !(m.type === 'user' && m.isCompactSummary))
588
+ : allMessages.slice(0, pivotIndex).filter(m => m.type !== 'progress')
589
+ ```
590
+
591
+ `progress` 类型消息在两种方向下都被排除(不可记录)。
592
+
593
+ #### 哪些需要保留
594
+
595
+ 以下内容**不在压缩范围内**,需要在压缩后恢复:
596
+
597
+ | 内容 | 恢复机制 | 文件 |
598
+ |------|----------|------|
599
+ | **System Prompt** | 始终作为前缀发送,不受压缩影响 | - |
600
+ | **工具定义** | 每次 API 调用由 `normalizeMessagesForAPI()` 注入 | `messages.ts` |
601
+ | **CLAUDE.md** | `processSessionStartHooks()` 在 SessionStart 钩子中恢复 | `sessionStart.ts` |
602
+ | **已读文件** | `createPostCompactFileAttachments()` 恢复最近 5 个文件 | `compact.ts:1415` |
603
+ | **计划文件** | `createPlanAttachmentIfNeeded()` | `compact.ts:1470` |
604
+ | **已调用技能** | `createSkillAttachmentIfNeeded()` | `compact.ts:1494` |
605
+ | **异步代理状态** | `createAsyncAgentAttachmentsIfNeeded()` | `compact.ts:1568` |
606
+ | **延迟工具声明** | `getDeferredToolsDeltaAttachment()` | `attachments.ts` |
607
+ | **MCP 指令** | `getMcpInstructionsDeltaAttachment()` | `attachments.ts` |
608
+ | **计划模式指令** | `createPlanModeAttachmentIfNeeded()` | `compact.ts:1542` |
609
+
610
+ #### 文件附件恢复策略
611
+
612
+ `createPostCompactFileAttachments()` (第 1415 行) 的策略:
613
+
614
+ 1. 从预压缩读文件状态获取最近访问的文件
615
+ 2. 排除计划文件、所有类型的 `CLAUDE.md` 文件
616
+ 3. 排除已在保留消息中出现过的读文件结果(`collectReadToolFilePaths()`,第 1610 行)
617
+ - 识别 `FILE_UNCHANGED_STUB` 标记以正确处理去重存根
618
+ 4. 按时间戳降序排列,取前 `POST_COMPACT_MAX_FILES_TO_RESTORE = 5` 个
619
+ 5. 使用 `FileReadTool` 重新读取文件获取最新内容(限制: `POST_COMPACT_MAX_TOKENS_PER_FILE = 5,000`)
620
+ 6. 按 `POST_COMPACT_TOKEN_BUDGET = 50,000` 的预算过滤
621
+
622
+ 技能附件策略(`createSkillAttachmentIfNeeded()`, 第 1494 行):
623
+ - 按调用时间降序排列技能(最近调用优先)
624
+ - 每个技能截断到 `POST_COMPACT_MAX_TOKENS_PER_SKILL = 5,000`
625
+ - 总预算 `POST_COMPACT_SKILLS_TOKEN_BUDGET = 25,000`(约 5 个技能)
626
+ - 截断标记告知模型可通过 Read 获取完整内容
627
+
628
+ ---
629
+
630
+ ## 5. 会话记忆压缩 (Session Memory Compact)
631
+
632
+ **文件**: `src/services/compact/sessionMemoryCompact.ts`
633
+ **函数**: `trySessionMemoryCompaction()` (第 514 行)
634
+
635
+ ### 5.1 动机
636
+
637
+ 会话记忆压缩是自动压缩的第一候选路径,目的**完全避免 LLM 摘要调用**。
638
+ 它利用已有的会话记忆文件(通过 SessionMemory 服务异步生成的结构化记忆)
639
+ 直接构建压缩后的上下文。
640
+
641
+ ### 5.2 工作流程
642
+
643
+ ```
644
+ trySessionMemoryCompaction()
645
+
646
+ ├── 检查特性标识 (tengu_session_memory + tengu_sm_compact)
647
+
648
+ ├── 初始化远程配置 (GrowthBook's tengu_sm_compact_config)
649
+ │ 默认: minTokens=10,000, minTextBlockMessages=5, maxTokens=40,000
650
+
651
+ ├── 等待进行中的会话记忆提取完成
652
+
653
+ ├── 获取 lastSummarizedMessageId (上次摘要到的消息)
654
+
655
+ ├── 读取会话记忆文件内容
656
+
657
+ ├── 计算保留消息的起始索引 (calculateMessagesToKeepIndex)
658
+ │ ├── 从 lastSummarizedMessageId 之后开始
659
+ │ ├── 向后扩展直到满足最小 token 和文本块消息数
660
+ │ └── 调整以确保 tool_use/tool_result 配对不分裂
661
+
662
+ ├── 创建 CompactionResult (无 API 调用)
663
+ │ ├── 边界标记
664
+ │ ├── 基于会话记忆的摘要消息
665
+ │ ├── 保留的消息 (messagesToKeep)
666
+ │ └── 计划文件附件
667
+
668
+ └── 检查后压缩 token 计数是否低于自动压缩阈值
669
+ ```
670
+
671
+ ### 5.3 与自动压缩的集成
672
+
673
+ 在 `autoCompact.ts:287-310` 中,会话记忆压缩优先于完整压缩:
674
+
675
+ ```typescript
676
+ const sessionMemoryResult = await trySessionMemoryCompaction(
677
+ messages, toolUseContext.agentId, recompactionInfo.autoCompactThreshold,
678
+ )
679
+ if (sessionMemoryResult) {
680
+ setLastSummarizedMessageId(undefined)
681
+ runPostCompactCleanup(querySource)
682
+ // ...
683
+ return { wasCompacted: true, compactionResult: sessionMemoryResult }
684
+ }
685
+ ```
686
+
687
+ ### 5.4 关键算法: calculateMessagesToKeepIndex
688
+
689
+ `adjustIndexToPreserveAPIInvariants()` (第 232 行) 确保压缩不会分裂
690
+ `tool_use`/`tool_result` 配对:
691
+
692
+ ```
693
+ 场景: 流式输出产生的不同 message.id 但相同 content.id 的消息
694
+
695
+ 索引 N: assistant, message.id=X, content: [thinking]
696
+ 索引 N+1: assistant, message.id=X, content: [tool_use: ORPHAN_ID]
697
+ 索引 N+2: assistant, message.id=X, content: [tool_use: VALID_ID]
698
+ 索引 N+3: user, content: [tool_result: ORPHAN_ID, tool_result: VALID_ID]
699
+
700
+ 如果 startIndex = N+2:
701
+ - N+2 保留但 N 被丢弃 → thinking 块丢失
702
+ - normalizeMessagesForAPI 合并后 → 孤立 tool_result ORPHAN_ID
703
+ - API 错误!
704
+
705
+ 修正: 检测 N+2 与 N 共享 message.id,将起始索引前移到 N
706
+ ```
707
+
708
+ 同样处理思考块合并问题:如果某条 assistant 消息的 `message.id` 与保留范围内
709
+ 的消息相同(流式拆分的 thinking 块),则向前扩展以包含所有相关块。
710
+
711
+ ### 5.5 会话记忆压缩 vs memdir 系统
712
+
713
+ 会话记忆压缩与 memdir(记忆目录)系统是**互补关系**:
714
+
715
+ | 系统 | 作用域 | 生成方式 | 保留内容 |
716
+ |------|--------|----------|----------|
717
+ | **SessionMemory** | 单个会话 | 异步提取 (每轮自动) | 用户意图、关键决策、代码模式 |
718
+ | **memdir** | 跨会话持久知识 | autoDream 定期整理 | 长期记忆、项目知识、用户偏好 |
719
+
720
+ 会话记忆压缩读取的是 `SessionMemory` 服务的输出文件,
721
+ 而非 memdir 中的长期记忆。
722
+
723
+ ---
724
+
725
+ ## 6. 压缩状态的恢复
726
+
727
+ ### 6.1 压缩边界标记
728
+
729
+ 每次压缩后插入一个 `SystemCompactBoundaryMessage` (创建于 `compact.ts:598-611`)。
730
+ 这是下游恢复的锚点:
731
+
732
+ ```typescript
733
+ const boundaryMarker = createCompactBoundaryMessage(
734
+ isAutoCompact ? 'auto' : 'manual',
735
+ preCompactTokenCount ?? 0,
736
+ messages.at(-1)?.uuid,
737
+ )
738
+ ```
739
+
740
+ 边界标记携带元数据:
741
+ - `preCompactDiscoveredTools`: 压缩前发现的延迟工具名称列表
742
+ - `preservedSegment` (部分压缩): 保留的 `messagesToKeep` 的头/锚/尾 UUID
743
+ - 用于 `annotateBoundaryWithPreservedSegment()` (第 349 行) 在后压缩加载时
744
+ 将保留段重新链接回摘要链
745
+
746
+ ### 6.2 历史导航 (/history) 与压缩的协同
747
+
748
+ `/history` 命令利用压缩边界标记来导航。
749
+ `getMessagesAfterCompactBoundary()` 从最近的边界标记之后加载消息,
750
+ 跳过已被摘要覆盖的历史部分。这使得用户可以通过边界标记快速跳转到
751
+ 压缩前的活动工作区。
752
+
753
+ ### 6.3 计划/skill/代理状态的保持
754
+
755
+ | 状态类型 | 保持方式 | 关键代码 |
756
+ |----------|----------|----------|
757
+ | **Plan (计划)** | 创建 `plan_file_reference` 附件 | `compact.ts:1470-1486` |
758
+ | **Plan Mode** | 创建 `plan_mode` 附件(含 reminderType: 'full') | `compact.ts:1542-1560` |
759
+ | **已调用技能** | 创建 `invoked_skills` 附件(含截断内容) | `compact.ts:1494-1534` |
760
+ | **Async Agents** | 创建 `task_status` 附件(含进度/结果) | `compact.ts:1568-1599` |
761
+ | **Deferred Tools** | 增量声明当前工具集(diff 模式) | `attachments.ts` |
762
+ | **已发现的工具** | 在边界标记中保存名称列表 | `compact.ts:607-611` |
763
+
764
+ ### 6.4 预压缩状态缓存清理
765
+
766
+ 压缩后需要清理多个缓存以反映上下文变化:
767
+
768
+ ```typescript
769
+ // postCompactCleanup.ts
770
+ context.readFileState.clear() // 清理读文件状态
771
+ context.loadedNestedMemoryPaths?.clear() // 清理内存文件路径缓存
772
+ getUserContext.cache.clear?.() // 清理用户上下文缓存
773
+ resetGetMemoryFilesCache('compact') // 清理记忆文件缓存
774
+ clearSystemPromptSections() // 清理系统提示词节
775
+ clearClassifierApprovals() // 清理分类器审批状态
776
+ clearSpeculativeChecks() // 清理推测性检查
777
+ clearBetaTracingState() // 清理追踪状态
778
+ clearSessionMessagesCache() // 清理消息缓存
779
+ ```
780
+
781
+ **注意**: 技能内容(`sentSkillNames`)故意不清除——重新注入完整的 `skill_listing`
782
+ (约 4K token)纯属浪费。模型仍然有 `SkillTool` 在 schema 中,
783
+ `invoked_skills` 附件保留了已使用的技能内容。
784
+
785
+ ---
786
+
787
+ ## 7. VersperClaw 中的具体实现
788
+
789
+ ### 7.1 与官方 Claude Code 的差异
790
+
791
+ VersperClaw 的压缩实现相比于 Anthropic 官方 Claude Code 有以下主要差异和保留:
792
+
793
+ #### 保留的核心能力
794
+
795
+ 1. **五层管道完整保留**: 预算削减 → Snip → Micro-compact → Context Collapse → Auto-compact
796
+ 的全部结构保持与官方版一致。
797
+
798
+ 2. **缓存共享路径**: `tengu_compact_cache_prefix` 特性通过 forkedAgent 复用主线程
799
+ prompt cache,在外部构建中同样生效。
800
+
801
+ 3. **会话记忆压缩**: 完整的 `trySessionMemoryCompaction` 实现,
802
+ 包括 `calculateMessagesToKeepIndex` 和 API 不变性保护。
803
+
804
+ 4. **Cached Microcompact**: 使用 API `cache_edits` 删除旧工具结果的机制完整保留,
805
+ 包括状态管理和 pinnedEdits 机制。
806
+
807
+ #### 差异点
808
+
809
+ 1. **Feature Gate 差异**:
810
+ - `feature('HISTORY_SNIP')` → 外部构建中 Snip 是空操作(`snipCompact.ts` 返回空结果)
811
+ - `feature('CONTEXT_COLLAPSE')` → 外部构建中 Context Collapse 是空操作桩
812
+ (`contextCollapse/index.ts` 全部返回空/默认值)
813
+ - `feature('REACTIVE_COMPACT')` → 外部构建中是占位符桩
814
+ (`reactiveCompact.ts` 导出一个 noop 代理)
815
+ - `feature('CACHED_MICROCOMPACT')` → 外部构建中启用,Cached MC 正常运行
816
+ - `feature('KAIROS')` → 会话记录分段写入,外部构建中禁用
817
+ - `feature('PROACTIVE')` → 自主模式压缩提示词适配,外部构建中禁用
818
+
819
+ 2. **外部构建中的 Cached MC**:
820
+ 外部构建使用 `getCachedMCConfig()` 的默认配置(`enabled: false`),
821
+ 因此完整 Cached MC 路径在外部构建中默认不激活。
822
+ 微压缩退回到时间触发 MC 路径或直接返回未修改的消息。
823
+
824
+ 3. **反应式压缩桩**:
825
+ `reactiveCompact.ts`(第 1-35 行)是完全的生成桩,
826
+ 所有命名导出通过 Proxy 代理返回空操作。这是为满足 `bun build` 的
827
+ 引用解析要求而存在的占位符。
828
+
829
+ 4. **API 微压缩策略**:
830
+ `apiMicrocompact.ts` 中的 `getAPIContextManagement()` 仅在
831
+ `process.env.USER_TYPE === 'ant'` 时包含工具清除策略。
832
+ 外部构建仅包含思考块保留策略。
833
+
834
+ ### 7.2 核心文件引用速查
835
+
836
+ | 功能 | 文件 | 关键函数/导出 |
837
+ |------|------|---------------|
838
+ | **预算削减** | `src/utils/toolResultStorage.ts` | `enforceToolResultBudget()` (L769) |
839
+ | **剪切** | `src/services/compact/snipCompact.ts` | `snipCompactIfNeeded()` (L25, 桩) |
840
+ | **微压缩** | `src/services/compact/microCompact.ts` | `microcompactMessages()` (L253) |
841
+ | **缓存微压缩** | `src/services/compact/cachedMicrocompact.ts` | `createCachedMCState()`, `registerToolResult()` |
842
+ | **时间触发 MC** | `src/services/compact/timeBasedMCConfig.ts` | `getTimeBasedMCConfig()` (L36) |
843
+ | **Cached MC 配置** | `src/services/compact/cachedMCConfig.ts` | `getCachedMCConfig()` (L17) |
844
+ | **API MC 策略** | `src/services/compact/apiMicrocompact.ts` | `getAPIContextManagement()` (L64) |
845
+ | **上下文折叠** | `src/services/contextCollapse/index.ts` | `applyCollapsesIfNeeded()` (L43, 桩) |
846
+ | **自动压缩编排** | `src/services/compact/autoCompact.ts` | `autoCompactIfNeeded()` (L241), `shouldAutoCompact()` (L160) |
847
+ | **完整压缩执行** | `src/services/compact/compact.ts` | `compactConversation()` (L387), `partialCompactConversation()` (L772) |
848
+ | **摘要提示词** | `src/services/compact/prompt.ts` | `getCompactPrompt()` (L293), `getPartialCompactPrompt()` (L274), `formatCompactSummary()` (L311) |
849
+ | **消息分组** | `src/services/compact/grouping.ts` | `groupMessagesByApiRound()` (L22) |
850
+ | **后压缩清理** | `src/services/compact/postCompactCleanup.ts` | `runPostCompactCleanup()` (L31) |
851
+ | **手动压缩命令** | `src/commands/compact/compact.ts` | `call` (L40) |
852
+ | **管道编排** | `src/query.ts` | L369-543: 五层管道的完整编排 |
853
+ | **会话记忆压缩** | `src/services/compact/sessionMemoryCompact.ts` | `trySessionMemoryCompaction()` (L514), `calculateMessagesToKeepIndex()` (L324) |
854
+
855
+ ### 7.3 关键常量一览
856
+
857
+ | 常量 | 值 | 定义位置 |
858
+ |------|-----|----------|
859
+ | `POST_COMPACT_MAX_FILES_TO_RESTORE` | 5 | `compact.ts:122` |
860
+ | `POST_COMPACT_TOKEN_BUDGET` | 50,000 | `compact.ts:123` |
861
+ | `POST_COMPACT_MAX_TOKENS_PER_FILE` | 5,000 | `compact.ts:124` |
862
+ | `POST_COMPACT_MAX_TOKENS_PER_SKILL` | 5,000 | `compact.ts:129` |
863
+ | `POST_COMPACT_SKILLS_TOKEN_BUDGET` | 25,000 | `compact.ts:130` |
864
+ | `MAX_COMPACT_STREAMING_RETRIES` | 2 | `compact.ts:131` |
865
+ | `MAX_PTL_RETRIES` | 3 | `compact.ts:228` |
866
+ | `MAX_OUTPUT_TOKENS_FOR_SUMMARY` | 20,000 | `autoCompact.ts:30` |
867
+ | `AUTOCOMPACT_BUFFER_TOKENS` | 13,000 | `autoCompact.ts:62` |
868
+ | `WARNING_THRESHOLD_BUFFER_TOKENS` | 20,000 | `autoCompact.ts:63` |
869
+ | `ERROR_THRESHOLD_BUFFER_TOKENS` | 20,000 | `autoCompact.ts:64` |
870
+ | `MANUAL_COMPACT_BUFFER_TOKENS` | 3,000 | `autoCompact.ts:65` |
871
+ | `MAX_CONSECUTIVE_AUTOCOMPACT_FAILURES` | 3 | `autoCompact.ts:70` |
872
+ | SM 压缩 `minTokens` | 10,000 | `sessionMemoryCompact.ts:58` |
873
+ | SM 压缩 `minTextBlockMessages` | 5 | `sessionMemoryCompact.ts:59` |
874
+ | SM 压缩 `maxTokens` | 40,000 | `sessionMemoryCompact.ts:60` |
875
+ | Cached MC `triggerThreshold` | 12 | `cachedMCConfig.ts:11` |
876
+ | Cached MC `keepRecent` | 3 | `cachedMCConfig.ts:12` |
877
+ | 时间触发 MC `gapThresholdMinutes` | 60 | `timeBasedMCConfig.ts:32` |
878
+
879
+ ---
880
+
881
+ ## A. 附录:事件遥测
882
+
883
+ 压缩管道在各关键节点发出遥测事件,用于性能监控和调试:
884
+
885
+ | 事件名称 | 触发时机 | 定义位置 |
886
+ |----------|----------|----------|
887
+ | `tengu_compact` | 完整压缩完成 | `compact.ts:650` |
888
+ | `tengu_compact_failed` | 完整压缩失败 | `compact.ts:470/498/508/1379` |
889
+ | `tengu_compact_ptl_retry` | 压缩请求命中 PTL,重试丢弃消息 | `compact.ts:479` |
890
+ | `tengu_compact_cache_sharing_success` | 缓存共享路径成功 | `compact.ts:1214` |
891
+ | `tengu_compact_cache_sharing_fallback` | 缓存共享失败,退回流式 | `compact.ts:1235/1242` |
892
+ | `tengu_compact_streaming_retry` | 流式路径重试 | `compact.ts:1364` |
893
+ | `tengu_partial_compact` | 部分压缩完成 | `compact.ts:990` |
894
+ | `tengu_partial_compact_failed` | 部分压缩失败 | `compact.ts:880/901` |
895
+ | `tengu_auto_compact_succeeded` | 自动压缩成功 | `query.ts:478` |
896
+ | `tengu_cached_microcompact` | 缓存微压缩删除工具 | `microCompact.ts:346` |
897
+ | `tengu_time_based_microcompact` | 时间触发 MC 清除工具结果 | `microCompact.ts:498` |
898
+ | `tengu_sm_compact` | 会话记忆压缩尝试结果 | `sessionMemoryCompact.ts` 内多个事件 |
899
+ | `tengu_sm_compact_no_session_memory` | 无会话记忆文件 | `sessionMemoryCompact.ts:534` |
900
+ | `tengu_sm_compact_empty_template` | 会话记忆为空(模板文件) | `sessionMemoryCompact.ts:541` |
901
+ | `tengu_sm_compact_threshold_exceeded` | 压缩后仍超阈值,回退 | `sessionMemoryCompact.ts:609` |
902
+
903
+ ---
904
+
905
+ > 文档版本: 基于 VersperClaw `cdb3bdd` 提交分析
906
+ > 最后更新: 2026-06-22