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

chore: docs v1.0

Browse files
docs/README.md ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # VersperClaw 文档
2
+
3
+ > VersperClaw — 基于 Anthropic Claude Code 的增强型 AI CLI 代理,集成 VRM 桌面伴侣、多 Provider、语音对话和自动化模式。
4
+
5
+ ## 目录
6
+
7
+ ### 架构
8
+ | 文档 | 说明 |
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
+
20
+ ### Friend VRM 伴侣
21
+ | 文档 | 说明 |
22
+ |------|------|
23
+ | [系统总览](friend/overview.md) | 定位、组件、交互模式、架构图 |
24
+ | [系统架构](friend/architecture.md) | FriendService、SSE、HTTP 服务器、TTS/STT/VAD 详细分析 |
25
+ | [数据流](friend/data-flow.md) | 文字对话/F2 语音/情绪表情/SSE 事件 四种流 |
26
+ | [情绪→3D 表情映射](friend/emotion-map.md) | 13 种情绪的 blend shape 组合、动作映射 |
27
+ | [前端 3D 渲染](friend/frontend-3d.md) | Three.js、EmoteController、MotionController、LipSync、TextBubble |
28
+ | [语音捕获与 VAD](friend/voice-vad.md) | Silero VAD 状态机、STT 提供者链、静音系统 |
29
+
30
+ ### 语音
31
+ | 文档 | 说明 |
32
+ |------|------|
33
+ | [语音系统总览](voice/overview.md) | STT/TTS 提供者、Voice Stream 协议 |
34
+
35
+ ### Ink 终端 UI
36
+ | 文档 | 说明 |
37
+ |------|------|
38
+ | [Ink UI 框架](ink-ui/overview.md) | 自定义 Fork 的渲染管线、组件库、事件系统 |
39
+
40
+ ### 桌面服务器
41
+ | 文档 | 说明 |
42
+ |------|------|
43
+ | [HTTP/WS 服务器](server/overview.md) | Bun.serve()、REST API、WebSocket、服务层 |
44
+ | [Provider 代理](server/proxy-provider.md) | Anthropic ↔ OpenAI 协议转换、多提供商支持 |
45
+
46
+ ### 后端服务
47
+ | 文档 | 说明 |
48
+ |------|------|
49
+ | [服务总览](services/overview.md) | MCP、上下文压缩、Auto Dream、飞书/Telegram |
50
+
51
+ ### 协调与自动化
52
+ | 文档 | 说明 |
53
+ |------|------|
54
+ | [目标与自动模式](coordinator/goals-auto-mode.md) | Goal 系统、Auto Mode、查询循环、Task 系统 |
55
+ | [多代理协调](coordinator/multi-agent.md) | Coordinator 模式、Worker 派发、并行策略 |
56
+
57
+ ### 远程桥接
58
+ | 文档 | 说明 |
59
+ |------|------|
60
+ | [远程桥接总览](remote-bridge/overview.md) | Bridge 模式、传输协议、认证机制 |
61
+
62
+ ### 记忆与上下文
63
+ | 文档 | 说明 |
64
+ |------|------|
65
+ | [自动记忆系统](memory-context/memory.md) | Memdir、4 种记忆类型、生命周期 |
66
+ | [上下文管理](memory-context/context.md) | 系统/用户上下文构建、压缩策略、React Contexts |
docs/architecture/data-flow.md ADDED
@@ -0,0 +1,560 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 核心数据流
2
+
3
+ 本文档详细描述 VersperClaw 的六大核心数据流,包含 ASCII 序列图和关键代码路径。
4
+
5
+ ---
6
+
7
+ ## 1. 用户输入流
8
+
9
+ 用户通过终端输入文本到 AI 响应的完整链路。
10
+
11
+ ```
12
+ User (键盘)
13
+
14
+ │ 键入文本 + Enter
15
+
16
+ PromptInput 组件
17
+ (screens/PromptInput.tsx)
18
+
19
+ │ handlePromptSubmit()
20
+
21
+ messageQueueManager.enqueue()
22
+ (utils/messageQueueManager.ts)
23
+
24
+ │ 命令入队 (priority: 'now' | 'next' | 'later')
25
+ │ FIFO 按优先级排序
26
+
27
+ useCommandQueue hook
28
+ (hooks/useCommandQueue.ts)
29
+
30
+ │ 出队 → 交给 REPL 主循环
31
+
32
+ query() 函数
33
+ (query.ts → QueryEngine.ts)
34
+
35
+ ├── 组装消息历史
36
+ ├── 获取系统提示词 (src/context.ts)
37
+ ├── 合并工具列表 (assembleToolPool)
38
+ └── 选择 Provider
39
+
40
+ ├── [1P Anthropic] ── Anthropic SDK ── POST /v1/messages
41
+
42
+ └── [3P Provider] ── server/proxy/handler.ts
43
+
44
+ ├── anthropicToOpenaiChat.ts (请求转换)
45
+ ├── POST provider API (OpenAI/Groq/DeepSeek...)
46
+ └── openaiChatToAnthropic.ts (响应转换)
47
+
48
+
49
+ 流式响应回到 query()
50
+
51
+
52
+ 工具调用分发 (Tool Execution)
53
+
54
+
55
+ 渲染响应到终端 (Ink UI)
56
+ ```
57
+
58
+ ### 关键代码路径
59
+
60
+ | 步骤 | 文件 | 核心函数 |
61
+ |------|------|----------|
62
+ | 输入 | `src/components/PromptInput/PromptInput.tsx` | `handlePromptSubmit()` |
63
+ | 队列 | `src/utils/messageQueueManager.ts` | `enqueue()`, `dequeue()` |
64
+ | 查询 | `src/query.ts` | `query()` |
65
+ | 引擎 | `src/QueryEngine.ts` | `QueryEngine` 类 |
66
+ | 工具 | `src/tools.ts` | `assembleToolPool()`, `getTools()` |
67
+ | 代理 | `src/server/proxy/handler.ts` | `handleProxyRequest()` |
68
+ | 响应渲染 | `src/screens/REPL.tsx` | 消息列表 + 流式渲染 |
69
+
70
+ ### 队列优先级机制
71
+
72
+ ```
73
+ 优先级: 'now' > 'next' > 'later'
74
+ 同一优先级内 FIFO
75
+
76
+ ┌─────────┐ ┌──────────┐ ┌─────────┐
77
+ │ 'now' │ │ 'next' │ │ 'later' │
78
+ │ (紧急) │ │ (正常) │ │ (通知) │
79
+ ├─────────┤ ├──────────┤ ├─────────┤
80
+ │ ═══▶ │ │ ═══▶ │ │ ═══▶ │
81
+ │ 出队优先 │ │ │ │ │
82
+ └─────────┘ └──────────┘ └─────────┘
83
+ ```
84
+
85
+ ---
86
+
87
+ ## 2. 语音捕获流
88
+
89
+ 从麦克风到 VRM 前端语音播放的完整管道。
90
+
91
+ ### 语音捕获架构
92
+
93
+ VersperClaw 使用进程内音频捕获(cpal Rust 库,通过 `native-modules/audio-capture-napi`),而不是传统的 arecord/parecord 子进程。
94
+
95
+ ```
96
+ 麦克风 (硬件)
97
+
98
+ │ 16kHz S16LE PCM
99
+
100
+ cpal (Rust 原生模块)
101
+ (native-modules/audio-capture-napi)
102
+
103
+ │ onData(chunk: Buffer)
104
+
105
+ FriendService.startVoiceCapture()
106
+ (friend/FriendService.ts)
107
+
108
+ ├──▶ Silero VAD (语音活动检测)
109
+ │ (friend/voice/vad-service.ts)
110
+ │ │
111
+ │ │ onnxruntime-web WASM 推理
112
+ │ │ 512 samples/frame @ 16kHz (32ms)
113
+ │ │
114
+ │ ├── 说话开始 → 开始积累音频
115
+ │ ├── 检测持续语音 (≥10 帧 ≈ 320ms 激活)
116
+ │ └── 检测静音 (≥20 帧 ≈ 640ms) → onSpeechEnd
117
+ │ │
118
+ │ ▼
119
+ │ 音频段 (Float32Array)
120
+ │ │
121
+ │ ▼
122
+ │ STT (语音转文字)
123
+ │ ├── Doubao ASM (火山引擎)
124
+ │ │ (doubaoime-asr 包)
125
+ │ ├── Anthropic STT
126
+ │ └── Whisper (OpenAI)
127
+ │ │
128
+ │ ▼
129
+ │ 转录文本 (transcript)
130
+
131
+ └──▶ messageQueueManager.enqueue()
132
+
133
+ │ { value: transcript, mode: 'prompt', origin: { kind: 'channel', server: 'friend' } }
134
+
135
+ AI 处理 (同用户输入流)
136
+
137
+ │ AI 响应文本
138
+
139
+ TTS (文本转语音)
140
+ ├── Edge TTS (node-edge-tts, 默认 zh-CN-XiaoxiaoNeural)
141
+ └── Qwen TTS (DashScope API, qwen3-tts-flash)
142
+
143
+ │ 生成 MP3 文件
144
+
145
+ broadcastToVrm()
146
+ (friend/sse.ts)
147
+
148
+ │ SSE → { text, audioUrl, audioIndex }
149
+
150
+ VRM 前端
151
+ ├── 播放音频
152
+ ├── 显示文本气泡
153
+ └── ��发口型同步
154
+ ```
155
+
156
+ ### VAD 状态机
157
+
158
+ ```
159
+ Silero VAD 内部状态:
160
+
161
+ [静音] ────────────── 帧概率 < 正阈值 ──────────────▶ [静音]
162
+ │ ▲
163
+ │ 帧概率 ≥ positiveSpeechThreshold │
164
+ │ (连续 ≥ preSpeechTriggerFrames) │ 帧概率 < 负阈值
165
+ ▼ │ (连续 ≥ redemptionFrames)
166
+ [可能说话] ────── 帧概率 < 正阈值 ──────▶ [确认说话] ──┘
167
+ │ │
168
+ │ onSpeechStart() │ onSpeechEnd(audio)
169
+ ▼ ▼
170
+ [积累音频帧] [发送音频段到 STT]
171
+ ```
172
+
173
+ | 参数 | 默认值 | 含义 |
174
+ |------|--------|------|
175
+ | `positiveSpeechThreshold` | 0.75 | 判定为语音的置信度阈值 |
176
+ | `negativeSpeechThreshold` | 0.50 | 判定为静音的置信度阈值 |
177
+ | `preSpeechTriggerFrames` | 10 | 激活前需要连续语音帧数 |
178
+ | `redemptionFrames` | 20 | 结束前需要连续静音帧数 |
179
+ | `minSpeechFrames` | 6 | 最小有效语音帧数 |
180
+ | `rmsThreshold` | 0.004 | RMS 能量阈值 (-48dBFS),低于此值跳过推理 |
181
+
182
+ ### 回声消除机制
183
+
184
+ ```
185
+ FriendService 在 TTS 播放时自动静音麦克风:
186
+
187
+ AI 响应文本
188
+
189
+
190
+ TTS 开始生成
191
+
192
+
193
+ friend.muted = true
194
+ │ muteTimer = setTimeout(unmute, estimatedTtsDuration)
195
+
196
+
197
+ TTS 播放期间: 麦克风数据 → 丢弃 (不送 VAD/STT)
198
+
199
+
200
+ TTS 播放结束 (或估算时间到)
201
+
202
+
203
+ friend.muted = false
204
+
205
+
206
+ 恢复正常捕获
207
+ ```
208
+
209
+ ---
210
+
211
+ ## 3. 远程桥接流
212
+
213
+ Local CLI 到 `claude.ai` 远程会话的 WebSocket 桥接。
214
+
215
+ ```
216
+ Local CLI (你的机器) claude.ai 服务器
217
+ ───────────────────── ─────────────────────
218
+
219
+ claude --remote start
220
+
221
+
222
+ bridgeMain()
223
+ (bridge/bridgeMain.ts)
224
+
225
+ │ 创建本地 HTTP 服务
226
+ │ 发起 OAuth 登录
227
+
228
+ WebSocket 连接
229
+ (bridge/replBridgeTransport.ts)
230
+ │ ◄══════════► claude.ai 远程会话
231
+ │ wss://api.claude.ai/...
232
+
233
+
234
+ replBridge.ts
235
+
236
+ ├── 从远程接收输入 → 入队到本地 messageQueueManager
237
+ ├── 将本地输出/工具结果 → 发送回远程
238
+ └── 同步本地命令到远程白名单
239
+ (BRIDGE_SAFE_COMMANDS)
240
+ ```
241
+
242
+ ### 命令过滤
243
+
244
+ 远程模式下只允许安全命令执行:
245
+
246
+ ```
247
+ REMOTE_SAFE_COMMANDS = {
248
+ session, // 显示 QR/URL
249
+ exit, // 退出
250
+ clear, // 清屏
251
+ help, // 帮助
252
+ theme, // 主题
253
+ color, // 颜色
254
+ vim, // Vim 模式
255
+ cost, // 计费
256
+ usage, // 用量
257
+ copy, // 复制
258
+ btw, // 便签
259
+ feedback, // 反馈
260
+ plan, // 计划模式
261
+ keybindings, // 快捷键
262
+ statusline, // 状态栏
263
+ stickers, // 贴纸
264
+ mobile, // 移动端
265
+ }
266
+ ```
267
+
268
+ 桥接安全命令(从移动端也可执行):
269
+
270
+ ```
271
+ BRIDGE_SAFE_COMMANDS = {
272
+ compact, // 压缩上下文
273
+ clear, // 清屏
274
+ cost, // 计费
275
+ summary, // 会话总结
276
+ releaseNotes, // 更新日志
277
+ files, // 文件列表
278
+ }
279
+ ```
280
+
281
+ ---
282
+
283
+ ## 4. Friend 表情流
284
+
285
+ LLM 调用 `friend_emotion` 工具到 VRM 前端 3D 角色表情渲染的完整链路。
286
+
287
+ ```
288
+ LLM 推理
289
+
290
+ │ 思考上下文后调用 FriendEmotionTool
291
+
292
+ FriendEmotionTool.call({ emotion, intensity, mood_delta })
293
+ (tools/FriendEmotionTool.ts)
294
+
295
+ ├── 更新心情指数 moodIndex (0-100, 基准值 60)
296
+ │ ├── mood_delta > 0: moodIndex += delta (上限 100)
297
+ │ └── mood_delta < 0: moodIndex -= delta (下限 0)
298
+
299
+ └── broadcastToVrm({
300
+ │ emotion: 'happy'|'sad'|'angry'|...,
301
+ │ emotionIntensity: 0..1,
302
+ │ moodDelta: n,
303
+ │ moodIndex: n
304
+ │ })
305
+
306
+
307
+ broadcastToVrm()
308
+ (friend/sse.ts)
309
+
310
+ │ SSE data: JSON.stringify(payload) + "\n\n"
311
+ │ 发送给所有已连接的 SSE 客户端
312
+
313
+ VRM 前端 (Three.js / React Three Fiber)
314
+
315
+ ├── SSE 事件解析
316
+
317
+ ├── VRMScene 组件
318
+ │ ├── EmoteController — 设置 BlendShape 混合形状
319
+ │ │ (happy → 嘴角上扬, sad → 眉眼下垂, 等)
320
+ │ ├── MotionController — 触发生理动画 (呼吸、眨眼)
321
+ │ └── TextBubble — 显示聊天文本气泡
322
+
323
+ └── AudioPlayback (如���有 audioUrl)
324
+ └── 播放 TTS 音频 + 口型同步
325
+ ```
326
+
327
+ ### 可用表情列表
328
+
329
+ | 表情 | 说明 |
330
+ |------|------|
331
+ | `happy` | 开心 |
332
+ | `sad` | 悲伤 |
333
+ | `angry` | 生气 |
334
+ | `surprised` | 惊讶 |
335
+ | `think` | 思考 |
336
+ | `awkward` | 尴尬 |
337
+ | `question` | 疑问 |
338
+ | `curious` | 好奇 |
339
+ | `neutral` | 中性/默认 |
340
+ | `love` | 喜爱 |
341
+ | `flirty` | 调情 |
342
+ | `greeting` | 打招呼 |
343
+ | `relaxed` | 放松 |
344
+
345
+ ### 心情系统
346
+
347
+ ```
348
+ 心情指数 (moodIndex) 范围 0-100,基准值 60
349
+
350
+ 0 ────────────── 60 ────────────── 100
351
+ [低落] [中性] [高涨]
352
+
353
+ 每次 mood_delta: -3 到 +3 (最小绝对值 1)
354
+ 通过 prefs 持久化 (friend/prefs.ts)
355
+
356
+ moodIndex 影响:
357
+ - 文本气泡颜色/样式
358
+ - 坐姿姿态 (放松 vs 紧张)
359
+ - 手臂动作幅度
360
+ ```
361
+
362
+ ### SSE 广播事件类型
363
+
364
+ Friend SSE 通道通过 `broadcastToVrm()` 发送 JSON 事件,所有字段均为可选:
365
+
366
+ ```typescript
367
+ type VrmBroadcastPayload = {
368
+ text?: string; // 显示文本
369
+ emotion?: string; // 表情名称
370
+ emotionIntensity?: number; // 表情强度 0-1
371
+ audioUrl?: string; // TTS 音频 URL
372
+ audioIndex?: number; // 音频序列号
373
+ clearText?: boolean; // 清除当前文本
374
+ imageUrl?: string; // 显示的图片 URL
375
+ moodDelta?: number; // 心情变化量
376
+ moodIndex?: number; // 当前心情指数
377
+ sendFirstTts?: boolean; // 发送第一个 TTS 段
378
+ appendText?: boolean; // 追加到已有文本
379
+ replyDone?: boolean; // 响应结束标记
380
+ };
381
+ ```
382
+
383
+ ---
384
+
385
+ ## 5. Provider 代理流
386
+
387
+ CLI 通过统一接口向不同 LLM Provider 发送请求的完整路径。
388
+
389
+ ### 双路由架构
390
+
391
+ ```
392
+ CLI (Anthropic Messages API format)
393
+
394
+ │ 选择 Provider
395
+
396
+ ┌────────────────────────────────────────┐
397
+ │ Provider 路由决策 │
398
+ │ │
399
+ │ isFirstPartyAnthropicBaseUrl() │
400
+ │ + isClaudeAISubscriber() │
401
+ │ + !isUsing3PServices() │
402
+ │ │ │ │
403
+ │ 是/ │ 否 │
404
+ │ │ │ │
405
+ │ ▼ ▼ │
406
+ │ ┌──────────┐ ┌──────────────────┐ │
407
+ │ │ Anthropic│ │ Proxy Handler │ │
408
+ │ │ SDK 直连 │ │ (server/proxy/ │ │
409
+ │ │ │ │ handler.ts) │ │
410
+ │ │POST /v1/ │ │ │ │
411
+ │ │messages │ │ apiFormat? │ │
412
+ │ └──────────┘ │ │ │
413
+ │ │ ┌── 'openai' ──▶ │ │
414
+ │ │ │ Anthropic → │ │
415
+ │ │ │ OpenAI Chat │ │
416
+ │ │ │ Completions │ │
417
+ │ │ │ │ │
418
+ │ │ │── 'openai- │ │
419
+ │ │ │ responses' │ │
420
+ │ │ │ Anthropic → │ │
421
+ │ │ │ OpenAI │ │
422
+ │ │ │ Responses API │ │
423
+ │ │ └────────────────│ │
424
+ │ └──────────────────┘ │
425
+ └────────────────────────────────────────┘
426
+ ```
427
+
428
+ ### 请求/响应转换流程
429
+
430
+ ```
431
+ Anthropic Messages API 请求
432
+ {
433
+ model: "claude-3-opus",
434
+ messages: [{role: "user", content: "Hello"}],
435
+ system: "You are helpful",
436
+ max_tokens: 1024,
437
+ stream: true
438
+ }
439
+
440
+
441
+ anthropicToOpenaiChat.ts / anthropicToOpenaiResponses.ts
442
+
443
+ │ 转换逻辑:
444
+ │ - system → system message (prefix)
445
+ │ - messages[] → messages[] (role 映射)
446
+ │ - max_tokens → max_tokens
447
+ │ - stream → stream
448
+ │ - tools → tools (格式适配)
449
+
450
+
451
+ OpenAI Chat/Responses API 请求
452
+ {
453
+ model: "gpt-4",
454
+ messages: [
455
+ {role: "system", content: "You are helpful"},
456
+ {role: "user", content: "Hello"}
457
+ ],
458
+ max_tokens: 1024,
459
+ stream: true
460
+ }
461
+
462
+ │ POST 到上游 Provider URL
463
+
464
+ OpenAI 流式响应
465
+
466
+
467
+ openaiChatToAnthropic.ts / openaiResponsesToAnthropic.ts (流式)
468
+
469
+ │ 转换逻辑:
470
+ │ - delta.content → delta.text
471
+ │ - tool_calls → content_block / tool_use
472
+ │ - finish_reason → stop_reason
473
+
474
+
475
+ Anthropic 格式流式事件回 CLI
476
+ ```
477
+
478
+ ### Provider 配置
479
+
480
+ 通过 ProviderService (`src/server/services/providerService.ts`) 管理:
481
+
482
+ ```typescript
483
+ type ProviderConfig = {
484
+ id: string;
485
+ name: string;
486
+ apiFormat: 'openai' | 'openai-responses' | 'anthropic';
487
+ baseUrl: string;
488
+ apiKey: string;
489
+ models: string[];
490
+ };
491
+ ```
492
+
493
+ 支持的内置 Provider(部分):
494
+ - OpenAI (GPT-4, GPT-4o)
495
+ - Groq (Llama, Mixtral)
496
+ - DeepSeek
497
+ - 自定义 OpenAI 兼容 API
498
+
499
+ ---
500
+
501
+ ## 数据流汇总图
502
+
503
+ ```
504
+ ┌─────────────────────────┐
505
+ │ 用户的输入来源 │
506
+ ├─────┬─────┬──────┬──────┤
507
+ │键盘 │语音 │远程 │ Bot │
508
+ │ │ │WebSocket │
509
+ └──┬──┴──┬──┴──┬───┴──┬──┘
510
+ │ │ │ │
511
+ ▼ │ │ │
512
+ ┌──────────┐ │ │ │
513
+ │ message │◄┘ │ │
514
+ │ Queue │◄──────┘ │
515
+ │ Manager │◄─────────────┘
516
+ └─────┬────┘
517
+
518
+
519
+ ┌──────────┐
520
+ │ query() │
521
+ │ AI 引擎 │
522
+ └──┬────┬──┘
523
+ │ │
524
+ ┌──────────┘ └──────────┐
525
+ ▼ ▼
526
+ ┌──────────────┐ ┌────────────────┐
527
+ │ Tool 执行 │ │ LLM 文本响应 │
528
+ │ (Bash/Read/ │ └───────┬────────┘
529
+ │ Edit/... │ │
530
+ └──────────────┘ ┌───────┴────────┐
531
+ │ 分发到消费者 │
532
+ ├────┬────┬──────┤
533
+ │TUI │TTS │SSE │
534
+ │显示 │播放 │推送 │
535
+ └────┴────┴──────┘
536
+ ```
537
+
538
+ ---
539
+
540
+ ## 参考文件路径
541
+
542
+ | 文件 | 说明 |
543
+ |------|------|
544
+ | `src/utils/messageQueueManager.ts` | 统一命令队列 |
545
+ | `src/query.ts` | AI 查询入口 |
546
+ | `src/QueryEngine.ts` | 查询引擎实现 |
547
+ | `src/friend/FriendService.ts` | Friend 语音/文本服务 |
548
+ | `src/friend/voice/vad-service.ts` | Silero VAD 实现 |
549
+ | `src/friend/tts.ts` | Edge TTS / Qwen TTS |
550
+ | `src/friend/sse.ts` | SSE 广播与客户端管理 |
551
+ | `src/friend/server.ts` | Friend HTTP 服务 (Bun.serve) |
552
+ | `src/tools/FriendEmotionTool.ts` | VRM 表情工具 |
553
+ | `src/friend/constants.ts` | 情绪列表常量 |
554
+ | `src/server/proxy/handler.ts` | Provider 代理句柄 |
555
+ | `src/server/proxy/transform/` | 协议转换器 |
556
+ | `src/server/proxy/streaming/` | 流式响应转换 |
557
+ | `src/server/services/providerService.ts` | Provider 配置管理 |
558
+ | `src/bridge/bridgeMain.ts` | 远程桥接主入口 |
559
+ | `src/bridge/replBridgeTransport.ts` | WebSocket 传输层 |
560
+ | `src/bridge/replBridge.ts` | REPL 桥接逻辑 |
docs/architecture/overview.md ADDED
@@ -0,0 +1,213 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 项目架构概览
2
+
3
+ ## 项目定位
4
+
5
+ VersperClaw 是一个 AI CLI 智能代理,基于 Anthropic Claude Code 源代码构建,增强了对以下场景的支持:
6
+
7
+ - **VRM 桌面伴侣**(Friend):在同一进程中运行 HTTP/SSE 服务,驱动 3D VRM 角色的表情、语音和文本气泡
8
+ - **多模型提供者**:通过反向代理层将 Anthropic Messages API 协议翻译为 OpenAI Chat/Responses API 协议,支持数十种第三方模型
9
+ - **语音对话**:基于 cpal(Rust 原生音频库)的进程内音频捕获,结合 Silero VAD(ONNX 实时语音活动检测)和多种 STT/TTS 引擎
10
+ - **自动化模式**:目标系统(Goals)、后台任务(Background Tasks)、MCP 工具集成、Feishu/Telegram Bot 桥接
11
+
12
+ ## 技术栈
13
+
14
+ | 类别 | 技术 |
15
+ |------|------|
16
+ | 运行时 | **Bun 1.3+**(JavaScript/TypeScript 运行时与打包器) |
17
+ | 语言 | **TypeScript**(全仓库使用 strict 模式) |
18
+ | 终端 UI | **Ink v6**(React 终端渲染框架)+ **React 19** |
19
+ | 3D 渲染 | **Three.js**(VRM 前端使用,通过 {React} Three Fiber) |
20
+ | 语音活动检测 | **onnxruntime-web (WASM)** — Silero VAD ONNX 模型(非 onnxruntime-node,避免 Bun 段错误) |
21
+ | 桌面窗口 | **Tauri**(可选桌面窗口模式) |
22
+ | 包管理 | **Bun** workspace(monorepo),`packageManager: bun@1.3.11` |
23
+ | 构建 | Bun 内建打包 `scripts/build.ts`,支持 `feature()` 死代码消除 |
24
+
25
+ ## 入口流程
26
+
27
+ ```
28
+ cli.tsx (bootstrap)
29
+
30
+
31
+ main.tsx (Commander CLI 定义)
32
+ │ ├── 注册 ~180+ 命令(commands.ts → command modules)
33
+ │ └── 注册 ~60+ AI 工具(tools.ts → tool modules)
34
+
35
+
36
+ replLauncher.tsx
37
+
38
+
39
+ Ink REPL (screens/REPL.tsx)
40
+ ├── PromptInput(文本输入组件)
41
+ ├── messageQueueManager(统一命令队列)
42
+ ├── query.ts(AI 请求分发)
43
+ └── useMergedTools(工具池装配)
44
+ ```
45
+
46
+ ### 启动阶段详解
47
+
48
+ 1. **`src/entrypoints/cli.tsx`** — 最外层入口。先扫描 `--version`/`-v`、`--dump-system-prompt`、`--bridge`、`--daemon`、`--tmux` 等快速路径标志。若无匹配则动态导入 `main.tsx` 并调用 `cliMain()`。
49
+ 2. **`src/main.tsx`** — 构建 Commander CLI 定义(约 180+ 命令)。执行配置加载、策略检查、遥测初始化、会话恢复等。最后调用 `launchRepl()` 渲染终端 UI。
50
+ 3. **`src/replLauncher.tsx`** — 负责动态加载 `<App>` 和 `<REPL>` 组件并调用 `renderAndRun()`。
51
+ 4. **`src/screens/REPL.tsx`** — 主 REPL 屏幕,包含消息历史、PromptInput、工具调用渲染等。
52
+
53
+ ## 架构亮点
54
+
55
+ ### 1. 同进程架构
56
+
57
+ Friend VRM 服务与 CLI 运行在**同一 Bun 进程**中:
58
+
59
+ - `friend/server.ts` 通过 `Bun.serve()` 监听 `127.0.0.1:3456`
60
+ - SSE(Server-Sent Events)用于服务端到 VRM 前端的实时推送(表情、音频、文本气泡)
61
+ - HTTP POST 用于 VRM 前端向 FriendService 发送消息
62
+ - 消除了独立的子进程管理和 CLI SDK 会话的开销
63
+
64
+ ### 2. 模块化工具系统
65
+
66
+ 60+ AI 可调用工具通过 `buildTool()` 框架注册。核心工具包括:
67
+
68
+ - **文件操作**: `BashTool`, `FileReadTool`, `FileEditTool`, `FileWriteTool`, `GlobTool`, `GrepTool`
69
+ - **Web/搜索**: `WebFetchTool`, `WebSearchTool`
70
+ - **任务管理**: `TaskCreateTool`, `TaskGetTool`, `TaskUpdateTool`, `TaskListTool`, `TaskStopTool`
71
+ - **MCP**: `ListMcpResourcesTool`, `ReadMcpResourceTool`
72
+ - **Friend 特有**: `FriendEmotionTool`(VRM 表情 + 心情管理), `FriendScreenObserveTool`
73
+ - **目标系统**: `GoalCreateTool`, `GoalGetTool`, `GoalUpdateTool`
74
+ - **工作流**: `WorkflowTool`(通过 WORKFLOW_SCRIPTS feature flag)
75
+ - **其他**: `AgentTool`, `SkillTool`, `TodoWriteTool`, `ToolSearchTool`, `ConfigTool` 等
76
+
77
+ 工具通过 `assembleToolPool()` 与 MCP 工具合并去重,统一提供给 AI 模型。
78
+
79
+ ### 3. 多 Provider 代理
80
+
81
+ `src/server/proxy/handler.ts` 实现协议翻译反向代理:
82
+
83
+ - **输入**: CLI 以 Anthropic Messages API 格式发送请求到 `POST /proxy/v1/messages` 或 `POST /proxy/providers/:id/v1/messages`
84
+ - **转换**: `anthropicToOpenaiChat.ts` / `anthropicToOpenaiResponses.ts` 将请求体翻译为 OpenAI 格式
85
+ - **转发**: 发送到上游第三方提供商的 API
86
+ - **回传**: `openaiChatToAnthropic.ts` / `openaiResponsesToAnthropic.ts` 将响应翻译回 Anthropic 格式
87
+ - **流式**: 支持 SSE 流式响应的双向转换(`openaiChatStreamToAnthropic.ts` / `openaiResponsesStreamToAnthropic.ts`)
88
+
89
+ ### 4. Feature Flag 系统
90
+
91
+ 通过 `bun:bundle` 的 `feature()` 函数实现编译期死代码消除:
92
+
93
+ ```typescript
94
+ import { feature } from 'bun:bundle'
95
+
96
+ // 以下代码在非 KAIROS 构建中被完全消除
97
+ const assistantModule = feature('KAIROS')
98
+ ? require('./assistant/index.js')
99
+ : null
100
+ ```
101
+
102
+ 已定义的 feature flags(部分):`VOICE_MODE`, `KAIROS`, `DAEMON`, `BRIDGE_MODE`, `COORDINATOR_MODE`, `PROACTIVE`, `AGENT_TRIGGERS`, `MCP_SKILLS`, `WORKFLOW_SCRIPTS`, `BUDDY`, `FORK_SUBAGENT`, `MONITOR_TOOL`, `WEB_BROWSER_TOOL`, `TERMINAL_PANEL` 等。
103
+
104
+ ## 核心模块一览
105
+
106
+ | 目录 | 职责 |
107
+ |------|------|
108
+ | `src/entrypoints/` | 程序入口点(cli.tsx bootstrap, init.ts 初始化) |
109
+ | `src/main.tsx` | Commander CLI 定义、启动流程编排 |
110
+ | `src/commands/` | 180+ 斜杠命令(`/clear`, `/commit`, `/config`, `/friend` 等) |
111
+ | `src/commands.ts` | 命令注册中心,从各模块加载并导出命令列表 |
112
+ | `src/tools/` | 60+ AI 工具实现(Bash, Read, Edit, WebSearch, FriendEmotion 等) |
113
+ | `src/tools.ts` | 工具注册中心,`getAllBaseTools()` 与 `assembleToolPool()` |
114
+ | `src/ink/` | Ink 终端渲染引擎(自定义 fork,包含 reconciler、layout、renderer 等) |
115
+ | `src/screens/` | 主要 UI 屏幕(REPL 主屏幕、设置向导等) |
116
+ | `src/components/` | React 组件(App, PromptInput, 权限请求等) |
117
+ | `src/hooks/` | React hooks(useMergedTools, useCommandQueue, useReplBridge 等) |
118
+ | `src/friend/` | VRM 桌面伴侣服务(FriendService, SSE, TTS, VAD, STT) |
119
+ | `src/server/` | HTTP/WebSocket 服务器(API 路由、Provider 代理、MCP、H5 访问) |
120
+ | `src/server/proxy/` | 多 Provider 反向代理(协议翻译:Anthropic ↔ OpenAI) |
121
+ | `src/bridge/` | 远程桥接(WebSocket ↔ claude.ai 远程会话) |
122
+ | `src/query/` | AI 请求查询引擎(`query.ts`、`QueryEngine.ts`) |
123
+ | `src/context/` | 系统上下文构建(`context.ts`,生成系统提示词) |
124
+ | `src/services/` | 后端服务(API client, Analytics, MCP, PolicyLimits, Compact 等) |
125
+ | `src/utils/` | 工具函数(配置、认证、消息队列、权限、MCP 插件、设置等) |
126
+ | `src/state/` | 应用状态管理(AppStateStore) |
127
+ | `src/types/` | TypeScript 类型定义 |
128
+ | `src/constants/` | 常量(OAuth、产品名、提示词、工具定义等) |
129
+ | `src/voice/` | 语音模式入口(`voiceModeEnabled.ts`) |
130
+ | `src/assistant/` | 助手模式(KAIROS feature flag 守护) |
131
+ | `src/coordinator/` | 协调器模式(多 agent 协作) |
132
+ | `src/buddy/` | Buddy 子 agent 系统(BUDDY feature flag) |
133
+ | `src/daemon/` | 守护进程模式(长期运行的后台服务) |
134
+ | `src/plugins/` | 插件系统(插件发现、加载、生命周期) |
135
+ | `src/skills/` | 技能系统(用户自定义 prompt 式命令) |
136
+ | `src/tasks/` | 任务系统(LocalAgentTask, LocalShellTask, RemoteAgentTask 等) |
137
+ | `src/cli/` | CLI 工具(bg.ts 后台会话管理、templateJobs 模板任务) |
138
+ | `src/bootstrap/` | 启动状态(state.ts 导入前状态设置) |
139
+ | `src/config/` | 配置处理 |
140
+ | `src/vim/` | Vim 模式支持 |
141
+ | `src/migrations/` | 数据迁移 |
142
+ | `src/memdir/` | 记忆目录支持 |
143
+ | `src/upstreamproxy/` | 上游代理支持 |
144
+ | `src/remote/` | 远程控制支持 |
145
+ | `native-modules/` | Rust/C++ 原生模块(cpal 音频捕获等) |
146
+
147
+ ## 数据流总览
148
+
149
+ ```
150
+ ┌─────────────────────────────────────────────────────────────────────┐
151
+ │ CLI 进程 (Bun) │
152
+ │ │
153
+ │ ┌──────────┐ ┌───────────┐ ┌───────────┐ ┌─────────────┐ │
154
+ │ │ cli.tsx │───▶│ main.tsx │───▶│ REPL.tsx │───▶│ PromptInput │ │
155
+ │ │ (bootstrap) │ (Commander) │ (Ink UI) │ │ (输入框) │ │
156
+ │ └──────────┘ └───────────┘ └─────┬─────┘ └──────┬──────┘ │
157
+ │ │ │ │
158
+ │ │ ┌────────────▼────┐ │
159
+ │ │ │ messageQueue │ │
160
+ │ │ │ Manager.enqueue │ │
161
+ │ │ └────────┬────────┘ │
162
+ │ │ │ │
163
+ │ │ ┌────────▼────────┐ │
164
+ │ │ │ query() │ │
165
+ │ │ │ (AI 请求) │ │
166
+ │ │ └──┬────┬────────┘ │
167
+ │ │ │ │ │
168
+ │ ┌────────────────────┼────────┘ │ │
169
+ │ │ │ │ │
170
+ │ ▼ │ ┌────────▼────────┐ │
171
+ │ ┌─────────────────────────┐ │ │ Tool Execution │ │
172
+ │ │ Anthropic SDK (1P) │ │ │ (Bash/Read/Edit │ │
173
+ │ │ POST /v1/messages │ │ │ ...等 ~60 工具) │ │
174
+ │ └─────────────────────────┘ │ └─────────────────┘ │
175
+ │ │ │ │
176
+ │ ┌──────────▼──────────┐ │ │
177
+ │ │ server/proxy/ │ │ │
178
+ │ │ handler.ts │ │ │
179
+ │ │ (3P Provider 代理) │ │ │
180
+ │ └──────────┬──────────┘ │ │
181
+ │ │ │ │
182
+ │ ▼ │ │
183
+ │ ┌──────────────────────────────┐ │ │
184
+ │ │ OpenAI Chat/Responses API │ │ │
185
+ │ │ (OpenAI/Groq/DeepSeek 等) │ │ │
186
+ │ └──────────────────────────────┘ │ │
187
+ │ │ │
188
+ │ ┌──────────────────────────────────────┼──────────────┐ │
189
+ │ │ FriendService (同进程) │ │ │
190
+ │ │ ┌─────────┐ ┌──────────┐ ┌───────▼──────┐ │ │
191
+ │ │ │ Silero │ │ STT │ │ SSE Server │ │ │
192
+ │ │ │ VAD │──│ (Doubao/ │ │ :3456 │ │ │
193
+ │ │ │ (ONNX) │ │ Whisper) │ └──────┬───────┘ │ │
194
+ │ │ └─────────┘ └──────────┘ │ │ │
195
+ │ │ ┌─────────┐ ┌──────────┐ │ │ │
196
+ │ │ │ TTS │ │ EdgeTTS/ │ │ │ │
197
+ │ │ │ Audio │──│ QwenTTS │ │ │ │
198
+ │ │ └─────────┘ └──────────┘ │ │ │
199
+ │ └────────────────────────────────────┼──────────────┘ │
200
+ └───────────────────────────────────────┼──────────────────────────┘
201
+
202
+
203
+ ┌─────────────────────────────────────┐
204
+ │ VRM 前端 (Web/Desktop) │
205
+ │ ┌─────────────────────────────────┐ │
206
+ │ │ SSE → VRMScene → TextBubble │ │
207
+ │ │ → EmoteController │ │
208
+ │ │ → MotionController │ │
209
+ │ │ → AudioPlayback │ │
210
+ │ └─────────────────────────────────┘ │
211
+ │ HTTP POST → FriendService.sendText │ │
212
+ └─────────────────────────────────────┘
213
+ ```
docs/coordinator/goals-auto-mode.md ADDED
@@ -0,0 +1,222 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 目标系统与自动模式
2
+
3
+ ## Goal 系统
4
+
5
+ Goal(目标)系统允许用户通过 `/goal <objective>` 命令设定一个长期目标,让 Agent 在多个对话轮次中持续追寻该目标,直至完成、受阻或暂停。
6
+
7
+ ### 状态机
8
+
9
+ Goal 的生命周期由以下状态构成:
10
+
11
+ ```
12
+ pursuing → paused → achieved / blocked
13
+ → usage-limited / budget-limited
14
+ ```
15
+
16
+ - **pursuing(追寻中)**:初始状态,Agent 正在积极地朝目标推进。
17
+ - **paused(已暂停)**:用户使用 `/goal pause` 暂停自动续跑,可用 `/goal resume` 恢复。
18
+ - **achieved(已完成)**:Agent 认定目标已完全达成。需经过严格的完成审计(Completion Audit)。
19
+ - **blocked(已阻塞)**:Agent 遇到无法继续的障碍,需要用户介入。
20
+ - **usage-limited / budget-limited**:因 token 预算或费用限制而停止。
21
+
22
+ ### 核心文件
23
+
24
+ | 文件 | 路径 | 用途 |
25
+ |------|------|------|
26
+ | goal.tsx | `src/commands/goal/goal.tsx` | `/goal` 命令的 JSX 实现(React Ink 渲染) |
27
+ | index.ts | `src/commands/goal/index.ts` | 命令注册入口 |
28
+ | goal.ts | `src/utils/goal.ts` | 工具函数:构建续跑提示、状态格式化 |
29
+ | useGoalAutoContinue.ts | `src/hooks/useGoalAutoContinue.ts` | React Hook:在每轮对话结束后自动注入续跑提示 |
30
+
31
+ ### 持续提示
32
+
33
+ `buildContinuationPrompt()`(定义于 `src/utils/goal.ts`)在每轮自动续跑时注入完整的上下文提示,包含:
34
+
35
+ - **目标描述**(`<objective>` XML 标签包裹)
36
+ - **续跑行为指导**:保持目标完整性,不允许缩小范围
37
+ - **基于证据的工作原则**:依赖当前工作区状态而非对话记忆
38
+ - **完成审计要求**:必须逐项验证所有需求
39
+ - **阻塞审计规则**:连续 3 轮相同阻塞条件才允许标记 blocked
40
+
41
+ `useGoalAutoContinue` hook(`src/hooks/useGoalAutoContinue.ts`)监听 `QueryGuard` 状态:
42
+ - 当查询从活跃(running)转为空闲(idle)时触发
43
+ - 仅在 goal 状态为 `pursuing` 且非 plan 模式时注入
44
+ - 自动清理旧目标的排队续跑,防止冲突
45
+ - 每次续跑递增 `continuationCount`
46
+
47
+ ### 完成审计(Completion Audit)
48
+
49
+ Agent 在决定标记目标为 `achieved` 前,必须执行严格的完成审计:
50
+
51
+ 1. 从目标描述中推导具体的、可验证的需求
52
+ 2. 不重新定义成功标准——保留原始范围
53
+ 3. 对每个需求,检查当前状态中的权威证据
54
+ 4. 确认证据足以证明完成,而非仅未发现未完成的工作
55
+ 5. 只有在当前证据能够经受逐项审查时,才标记为已达成
56
+
57
+ ### Blocked 审计
58
+
59
+ 防止 Agent 过早放弃的机制(`src/utils/goal.ts` 中 `BLOCKED_AUDIT_TURNS = 3`):
60
+
61
+ - 阻塞条件必须连续出现 **至少 3 轮** 才能标记 blocked
62
+ - 用户恢复已阻塞的目标后,阻塞审计重置
63
+ - 仅在确实无法推进、需要用户输入或外部状态变更时才使用
64
+ - 不因任务困难、缓慢或需要澄清而标记 blocked
65
+
66
+ ### 预算跟踪
67
+
68
+ `checkTokenBudget()`(定义于 `src/query/tokenBudget.ts`)根据 token 消耗决定是否继续自动续跑:
69
+
70
+ - 使用 `createBudgetTracker()` 跟踪每次续跑的 token 消耗
71
+ - `COMPLETION_THRESHOLD = 0.9`:token 消耗达到预算的 90% 时停止
72
+ - `DIMINISHING_THRESHOLD = 500`:连续 3 次续跑且每次增量 < 500 token 时视为收益递减
73
+ - 返回 `continue` 或 `stop` 决策
74
+
75
+ ---
76
+
77
+ ## Auto Mode(自动模式)
78
+
79
+ ### 激活方式
80
+
81
+ - 用户通过 `/goal <objective>` 设置目标后自动进入自动模式
82
+ - Plan 模式下自动模式被禁用(`inPlanMode` 检查)
83
+
84
+ ### 自动续跑机制
85
+
86
+ 每次 Agent 响应完成后,`useGoalAutoContinue` hook 自动向命令队列注入 `[goal] Continue` 提示:
87
+
88
+ ```typescript
89
+ const prompt = buildContinuationPrompt(goal, now)
90
+ enqueue({
91
+ mode: 'prompt',
92
+ value: prompt,
93
+ priority: 'later',
94
+ isMeta: true,
95
+ })
96
+ ```
97
+
98
+ - 只在 `pursuing` 状态下触发
99
+ - 跳过 plan 模式
100
+ - 用户输入优先于自动续跑(有阻塞性用户输入时不注入)
101
+ - 避免为同一目标重复排队
102
+
103
+ ### update_goal 工具
104
+
105
+ Agent 可通过 `update_goal` 工具报告进度:
106
+
107
+ - `status='complete'`:目标达成,停止续跑
108
+ - `status='blocked'`:受阻,需用户介入
109
+ - 调用时必须包含 `goal_id`(与目标 ID 匹配)
110
+
111
+ ### 目标管理命令
112
+
113
+ | 命令 | 功能 |
114
+ |------|------|
115
+ | `/goal <objective>` | 设置新目标(若已有活跃目标则提示覆盖确认) |
116
+ | `/goal set <objective>` | 同上 |
117
+ | `/goal pause` | 暂停当前目标 |
118
+ | `/goal resume` | 恢复被暂停的目标(重置续跑计数和预算窗口) |
119
+ | `/goal edit <new objective>` | 编辑目标描述 |
120
+ | `/goal clear` | 清除当前目标 |
121
+ | `/goal`(无参数) | 查看当前目标状态 |
122
+
123
+ ---
124
+
125
+ ## 查询循环(query.ts)
126
+
127
+ ### queryLoop() 主循环
128
+
129
+ `src/query.ts` 中的 `queryLoop()` 是 Agent 的核心执行循环,在 `QueryEngine.ts` 中被调用。每次迭代处理一个完整的模型调用���期:
130
+
131
+ ```
132
+ 模型调用 → 流式响应处理 → 工具执行 → stop hooks → 继续决策
133
+ ```
134
+
135
+ 1. **模型调用**:`queryModelWithStreaming()` 发送消息到 API
136
+ 2. **流处理**:处理 `content_block_start/delta` 事件
137
+ 3. **工具执行**:检测 `tool_use` 并执行
138
+ 4. **stop hooks**:执行 `handleStopHooks()`(记忆提取、自动 dream、prompt 建议等)
139
+ 5. **继续决策**:检查是否需要继续循环(工具调用、max_output_tokens 恢复、goal 续跑)
140
+
141
+ ### max_output_tokens 恢复
142
+
143
+ 当模型响应被 `max_output_tokens` 截断时,queryLoop 提供最多 **3 次** 恢复尝试(`MAX_OUTPUT_TOKENS_RECOVERY_LIMIT`):
144
+
145
+ 1. 注入恢复提示,指导模型从中断处继续
146
+ 2. 若 8K 默认输出限制触发,自动升级到 64K(一次,`tengu_otk_slot_v1` 功能门控)
147
+ 3. 恢复尝试耗尽后,将错误信息输出给用户
148
+
149
+ 相关变量(`src/query.ts`):
150
+ - `maxOutputTokensRecoveryCount`:当前恢复尝试次数
151
+ - `MAX_OUTPUT_TOKENS_RECOVERY_LIMIT = 3`:最大恢复次数
152
+ - `ESCALATED_MAX_TOKENS`:升级后的输出 token 上限
153
+
154
+ ### Reactive Compaction
155
+
156
+ 当上下文超出 API 限制时自动触发压缩:
157
+
158
+ - `hasAttemptedReactiveCompact`:防止在同一轮迭代中重复压缩
159
+ - 上下文超时(prompt-too-long)触发的压缩流程
160
+ - 压缩后保持 stop hooks 的阻塞错误处理
161
+ - 保留反应式压缩保护(不因 stop hook 错误而重置,防止无限循环)
162
+
163
+ ---
164
+
165
+ ## Task 系统
166
+
167
+ Task(任务)系统管理 Agent 派发的异步子任务。
168
+
169
+ ### Task 类型
170
+
171
+ 定义于 `src/Task.ts`:
172
+
173
+ | 类型 | 标识 | 用途 |
174
+ |------|------|------|
175
+ | `local_bash` | `b` | 本地 Shell 命令执行 |
176
+ | `local_agent` | `a` | 本地子 Agent |
177
+ | `remote_agent` | `r` | 远程 Agent |
178
+ | `in_process_teammate` | `t` | 进程内队友 |
179
+ | `local_workflow` | `w` | 本地工作流脚本 |
180
+ | `dream` | `d` | 记忆整理任务 |
181
+ | `monitor_mcp` | `m` | MCP 监控 |
182
+
183
+ ### 状态
184
+
185
+ ```
186
+ pending → running → completed / failed / killed
187
+ ```
188
+
189
+ - `pending`:等待分配
190
+ - `running`:正在执行
191
+ - `completed`:正常完成
192
+ - `failed`:执行失败
193
+ - `killed`:被终止
194
+
195
+ ### 后台/前台任务
196
+
197
+ 任务系统区分前台任务(阻塞用户交互)和后台任务(在后台运行),但 Task 类型本身不区分配置字段;区分体现在调用上下文中。
198
+
199
+ ### 任务 ID 生成
200
+
201
+ `generateTaskId()`(`src/Task.ts`)生成格式为 `{类型前缀}{8位随机字母数字}` 的 ID:
202
+ - 使用 `randomBytes(8)` 生成安全随机数
203
+ - 36 进制字母表(数字 + 小写字母)
204
+ - 36^8 ≈ 2.8 万亿种组合,抗暴力枚举
205
+
206
+ ### stopTask(优雅终止)
207
+
208
+ Task 的 `kill()` 方法实现优雅终止,每个 Task 类型提供独立的 `kill` 实现(`src/tasks.ts`):
209
+
210
+ ```typescript
211
+ export type Task = {
212
+ name: string
213
+ type: TaskType
214
+ kill(taskId: string, setAppState: SetAppState): Promise<void>
215
+ }
216
+ ```
217
+
218
+ ### 任务注册
219
+
220
+ `src/tasks.ts` 中的 `getAllTasks()` 收集所有可用 Task:
221
+ - 始终注册:`LocalShellTask`, `LocalAgentTask`, `RemoteAgentTask`, `DreamTask`
222
+ - 条件注册(通过 feature gate):`LocalWorkflowTask`, `MonitorMcpTask`
docs/coordinator/multi-agent.md ADDED
@@ -0,0 +1,205 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Coordinator 多代理模式
2
+
3
+ ## 概述
4
+
5
+ Coordinator 模式是 VersperClaw 的一种高级执行模式,允许一个协调者(Coordinator)LLM 通过 `AgentTool` 派生子代理(Worker)并行执行任务。通过 `CLAUDE_CODE_COORDINATOR_MODE` 环境变量激活。
6
+
7
+ ### 激活方式
8
+
9
+ ```bash
10
+ export CLAUDE_CODE_COORDINATOR_MODE=1
11
+ ```
12
+
13
+ 检测逻辑位于 `src/coordinator/coordinatorMode.ts`:
14
+
15
+ ```typescript
16
+ export function isCoordinatorMode(): boolean {
17
+ if (feature('COORDINATOR_MODE')) {
18
+ return isEnvTruthy(process.env.CLAUDE_CODE_COORDINATOR_MODE)
19
+ }
20
+ return false
21
+ }
22
+ ```
23
+
24
+ ### 会话模式匹配
25
+
26
+ `matchSessionMode()` 确保恢复的会话与当前 coordinator 模式一致。如果环境变量与会话存储的模式不匹配(例如在 coordinator 模式下创建会话,然后在 normal 模式下恢复),会自动翻转环境变量并记录事件。
27
+
28
+ ---
29
+
30
+ ## 工作模式
31
+
32
+ ### 核心架构
33
+
34
+ ```
35
+ User → Coordinator LLM → AgentTool → Worker 1
36
+ → Worker 2
37
+ → Worker 3 (并行)
38
+ ```
39
+
40
+ - **Coordinator LLM**:接收用户请求,制定计划,派发任务,综合结果
41
+ - **Worker**:通过 `AgentTool` 派生的子代理,执行具体任务
42
+ - **通信**:Worker 结果通过 `<task-notification>` XML 格式返回
43
+
44
+ ### Worker 通信格式
45
+
46
+ Worker 完成时,结果以 `user-role` 消息形式送达 Coordinator:
47
+
48
+ ```xml
49
+ <task-notification>
50
+ <task-id>{agentId}</task-id>
51
+ <status>completed|failed|killed</status>
52
+ <summary>{human-readable status summary}</summary>
53
+ <result>{agent's final text response}</result>
54
+ <usage>
55
+ <total_tokens>N</total_tokens>
56
+ <tool_uses>N</tool_uses>
57
+ <duration_ms>N</duration_ms>
58
+ </usage>
59
+ </task-notification>
60
+ ```
61
+
62
+ Coordinator 必须区分工作结果消息和真实用户消息:工作结果包含 `<task-notification>` 标签。
63
+
64
+ ---
65
+
66
+ ## 工作流:四阶段模型
67
+
68
+ ### Phase 1: Research(研究)
69
+ - Coordinator 并行启动多个 Worker 进行独立研究
70
+ - 每个 Worker 负责调查代码库的不同方面
71
+ - 只读任务,可自由并行
72
+
73
+ ### Phase 2: Synthesis(综合)
74
+ - Coordinator **亲自**阅读所有研究发现
75
+ - 理解问题本质,编写具体的实现规格
76
+ - **关键原则**:Coordinator 必须理解研究发现再分配任务,不能将理解工作委托给 Worker
77
+
78
+ ### Phase 3: Implementation(实现)
79
+ - 根据综合后的规格派发实现任务
80
+ - 按文件集串行处理写操作,避免冲突
81
+
82
+ ### Phase 4: Verification(验证)
83
+ - 独立的 Worker 验证实现结果
84
+ - 真正的验证意味着证明代码工作,而非确认代码存在
85
+ - 测试、类型检查和边缘用例
86
+
87
+ ---
88
+
89
+ ## 并行策略
90
+
91
+ ### 只读任务全并行
92
+ 研究阶段的所有 Worker 可同时启动,互不影响。
93
+
94
+ ### 写任务按文件集串行
95
+ 实现阶段的写操作需谨慎:同一组文件的操作需要串行执行。
96
+
97
+ ### 验证与实现可部分并行
98
+ 实现的不同文件区域可同时进行验证。
99
+
100
+ ---
101
+
102
+ ## 工具集
103
+
104
+ Coordinator 拥有以下专用工具(定义于 `src/tools/AgentTool/AgentTool.tsx`):
105
+
106
+ | 工具 | 用途 |
107
+ |------|------|
108
+ | `AgentTool`(`Agent`) | 创建新的 Worker |
109
+ | `SendMessageTool` | 向已存在的 Worker 发送后续指令 |
110
+ | `TaskStopTool` | 停止正在运行的 Worker |
111
+ | `subscribe_pr_activity` | 订阅 GitHub PR 事件 |
112
+
113
+ ### AgentTool 的使用
114
+
115
+ ```typescript
116
+ AgentTool({
117
+ description: "Investigate auth bug",
118
+ subagent_type: "worker",
119
+ prompt: "..."
120
+ })
121
+ ```
122
+
123
+ 重要规则:
124
+ - 不要用一个 Worker 去检查另一个 Worker 的状态——Worker 完成时会自动通知
125
+ - 不要为简单的文件读取或命令执行创建 Worker
126
+ - 不要设置 model 参数——Worker 使用默认模型
127
+ - 已完成工作的 Worker 应通过 `SendMessageTool` 继续使用其加载的上下文
128
+
129
+ ---
130
+
131
+ ## Prompt 编写规则
132
+
133
+ ### Worker 看不到 Coordinator 的对话
134
+
135
+ 每个 Worker prompt 必须**自包含**,包含执行任务所需的全部信息。Coordinator 不能假设 Worker 知道对话中发生过什么。
136
+
137
+ ### 好的 Prompt 示例
138
+
139
+ ```typescript
140
+ // 好的 prompt:包含具体路径、行号和精确指令
141
+ AgentTool({
142
+ prompt: "Fix the null pointer in src/auth/validate.ts:42. " +
143
+ "The user field can be undefined when the session expires. " +
144
+ "Add a null check and return early with an appropriate error. " +
145
+ "Commit and report the hash."
146
+ })
147
+ ```
148
+
149
+ ### 坏的 Prompt 示例(反模式)
150
+
151
+ ```typescript
152
+ // 坏的 prompt:模糊、依赖上下文
153
+ AgentTool({ prompt: "Fix the bug we discussed" }) // 错误:Worker 看不到讨论
154
+ AgentTool({ prompt: "Based on your findings, implement the fix" }) // 错误:懒惰的委托
155
+ ```
156
+
157
+ ### 目的陈述
158
+
159
+ 为 Prompt 添加目的说明,帮助 Worker 校准深度和重点:
160
+
161
+ - "This research will inform a PR description — focus on user-facing changes."
162
+ - "I need this to plan an implementation — report file paths, line numbers, and type signatures."
163
+
164
+ ### continue vs spawn 的选择
165
+
166
+ | 场景 | 策略 | 原因 |
167
+ |------|------|------|
168
+ | 研究恰好覆盖了需编辑的文件 | **Continue**(SendMessageTool) | Worker 已有文件在上下文中 |
169
+ | 研究范围广,实现范围窄 | **Spawn fresh**(AgentTool) | 避免携带探索噪音 |
170
+ | 纠正错误或扩展近期工作 | **Continue** | Worker 有错误上下文 |
171
+ | 验证其他 Worker 的代码 | **Spawn fresh** | 验证者需以新视角查看代码 |
172
+ | 完全不同的任务 | **Spawn fresh** | 无有用上下文可复用 |
173
+
174
+ ---
175
+
176
+ ## 实现细节
177
+
178
+ ### 核心文件
179
+
180
+ | 文件 | 路径 | 用途 |
181
+ |------|------|------|
182
+ | coordinatorMode.ts | `src/coordinator/coordinatorMode.ts` | Coordinator 模式检测、会话匹配、用户上下文构建 |
183
+ | AgentTool.tsx | `src/tools/AgentTool/AgentTool.tsx` | Agent 工具的实现 |
184
+ | builtInAgents.ts | `src/tools/AgentTool/builtInAgents.ts` | 内置 Agent 定义 |
185
+
186
+ ### Worker 工具白名单
187
+
188
+ 内部 Worker 工具(由 `INTERNAL_WORKER_TOOLS` 定义的集合)对 Worker 不可见:
189
+
190
+ - `TeamCreateTool`
191
+ - `TeamDeleteTool`
192
+ - `SendMessageTool`
193
+ - `SyntheticOutputTool`
194
+
195
+ 在简单模式(`CLAUDE_CODE_SIMPLE`)下,Worker 仅有权访问:Bash、Read、Edit 工具,外加 MCP 工具。
196
+
197
+ ### Worker 错误处理
198
+
199
+ 当 Worker 报告失败时:
200
+ - 使用 `SendMessageTool` 继续同一 Worker——它保留完整的错误上下文
201
+ - 如果纠正尝试也失败,尝试不同方法或报告用户
202
+
203
+ ### 停止 Worker
204
+
205
+ 使用 `TaskStopTool` 停止方向错误的 Worker。已停止的 Worker 可通过 `SendMessageTool` 继续。
docs/friend/architecture.md ADDED
@@ -0,0 +1,419 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Friend 系统架构
2
+
3
+ 本文档详细描述 Friend VRM 桌面伴侣系统的各个组件架构。
4
+
5
+ ---
6
+
7
+ ## 1. FriendService — 核心编排器
8
+
9
+ **文件**: `src/friend/FriendService.ts` (~31KB, 897 行)
10
+
11
+ ### 1.1 生命周期管理
12
+
13
+ ```
14
+ start() ──► 状态: 'starting'
15
+
16
+ ├── 初始化 Silero VAD (非致命失败,失败后语音捕获降级为 F2-only)
17
+ │ └── 配置: threshold 0.75, preSpeechTriggerFrames 10, redemptionFrames 20
18
+
19
+ └── 状态: 'running'
20
+
21
+ stop() ──► 停止语音捕获 → 清理 STT 连接 → 状态: 'stopped'
22
+ ```
23
+
24
+ ### 1.2 React external store 接口
25
+
26
+ FriendService 实现了类似 React `useSyncExternalStore` 的接口:
27
+
28
+ ```typescript
29
+ subscribe(listener: Listener): () => void // 添加状态监听
30
+ subscribeToInbound(listener): () => void // 监听用户输入事件
31
+ getStateSnapshot(): FriendServiceState // 获取当前状态快照
32
+ ```
33
+
34
+ `FriendServiceState` 包含:
35
+ - `status`: `'stopped' | 'starting' | 'running' | 'error'`
36
+ - `lastError`: 可选错误信息
37
+ - `displayClientCount`: SSE 显示器客户端数量
38
+ - `captureStatus`: 语音捕获状态(`capturing` + `interimText`)
39
+
40
+ ### 1.3 文本中继
41
+
42
+ ```typescript
43
+ sendText(text: string): void
44
+ ```
45
+
46
+ 流程:
47
+ 1. 通知所有 `inboundListeners`(bridge hook 用于 turn tracking)
48
+ 2. 动态导入 `messageQueueManager.enqueue()`(避免循环依赖)
49
+ 3. 使用 `bridgeOrigin: true` 和 `origin: { kind: 'channel', server: 'friend' }` 标记来源
50
+
51
+ ### 1.4 语音捕获
52
+
53
+ 参见 [语音捕获与 VAD 策略](voice-vad.md) 详细文档。
54
+
55
+ ```
56
+ startVoiceCapture()
57
+
58
+ ├── 检测 STT provider(自动降级)
59
+ ├── 创建 STT 连接(8s 超时)
60
+ ├── 启动 arecord/parecord 子进程(500ms 验证窗口)
61
+ └── 启动 VAD 检测
62
+ ```
63
+
64
+ ### 1.5 静音系统
65
+
66
+ AI 处理全过程阻止麦克风采音进入 STT/VAD,防止 TTS 播放时的回声。
67
+
68
+ ```
69
+ startAiTurnMute()
70
+ │ muted = true, VAD pause
71
+ │ 设置 30s 安全性计时器
72
+
73
+ ├── AI 处理 → TTS 生成
74
+
75
+ └── extendMuteForTts(audioId)
76
+ │ 解析 MP3 精确时长
77
+ │ 重置计时器为精确播放时长
78
+
79
+ └── unmute()
80
+ │ muted = false, VAD resume
81
+ ```
82
+
83
+ ### 1.6 STT 自动检测
84
+
85
+ ```typescript
86
+ detectAvailableSttProvider()
87
+
88
+ ├── 1. Groq Whisper API (最快, REST 调用)
89
+ ├── 2. Local Whisper (pip install openai-whisper)
90
+ ├── 3. Anthropic Voice Stream
91
+ ├── 4. Doubao ASR (检查 ~/.claude/tts/doubao/credentials.json)
92
+
93
+ └── 全不可用则抛出错误
94
+ ```
95
+
96
+ ### 1.7 TTS 生成
97
+
98
+ ```typescript
99
+ generateTts(text: string)
100
+
101
+ ├── prefs.provider === 'qwen' + prefs.qwenKey 存在
102
+ │ └── Qwen DashScope TTS (qwen3-tts-flash)
103
+
104
+ └── else
105
+ └── Edge TTS (node-edge-tts, 默认 zh-CN-XiaoxiaoNeural)
106
+ ```
107
+
108
+ ### 1.8 SSE 广播
109
+
110
+ ```typescript
111
+ broadcastResponse(text: string)
112
+
113
+ ├── broadcastToVrm({ text }) // 发送文字到前端 TextBubble
114
+ ├── if TTS enabled:
115
+ │ ├── generateTts(text)
116
+ │ ├── broadcastToVrm({ audioUrl, sendFirstTts: true })
117
+ │ └── extendMuteForTts(audioId)
118
+ └── broadcastToVrm({ replyDone: true }) // 信号回复完成
119
+ ```
120
+
121
+ ### 1.9 MP3 时长解析
122
+
123
+ `getMp3DurationMs()` 方法使用帧同步头扫描法精确计算 MP3 时长:
124
+
125
+ 1. 跳过 ID3v2 标签头
126
+ 2. 查找前两个帧同步字(0xFF + 0xE0)
127
+ 3. 计算实际帧间隔(CBR 模式)
128
+ 4. 解析帧头获取采样率和每帧采样数
129
+ 5. 按步长计数帧数
130
+ 6. 计算 `(帧数 * 每帧采样数 / 采样率) * 1000`
131
+
132
+ ---
133
+
134
+ ## 2. SSE 模块
135
+
136
+ **文件**: `src/friend/sse.ts`
137
+
138
+ ### 2.1 客户端注册表
139
+
140
+ ```typescript
141
+ Set<SseClient> // 全局 SSE 客户端集合
142
+ ```
143
+
144
+ - `addSseClient(client)`: 注册新客户端
145
+ - `removeSseClient(client)`: 移除客户端
146
+ - `getSseClientCount()`: 获取活跃客户端数
147
+ - `createSseClientId()`: 生成唯一 ID (`sse-{counter}-{timestamp}`)
148
+
149
+ ### 2.2 VrmBroadcastPayload 类型
150
+
151
+ ```typescript
152
+ type VrmBroadcastPayload = {
153
+ text?: string; // 回复文字
154
+ emotion?: string; // 表情名
155
+ emotionIntensity?: number; // 表情强度 0-1
156
+ audioUrl?: string; // TTS 音频 URL
157
+ audioIndex?: number; // 音频索引(多句排序)
158
+ clearText?: boolean; // 清空气泡文字
159
+ imageUrl?: string; // 显示图片
160
+ moodDelta?: number; // 心情变化
161
+ moodIndex?: number; // 当前心情指数 0-100
162
+ sendFirstTts?: boolean; // 开始 TTS 播放信号
163
+ appendText?: boolean; // 追加文字(后续句子)
164
+ replyDone?: boolean; // 回复结束信号
165
+ };
166
+ ```
167
+
168
+ ### 2.3 广播机制
169
+
170
+ ```typescript
171
+ broadcastToVrm(payload: VrmBroadcastPayload)
172
+ ├── 序列化为 SSE data 格式: `data: {json}\n\n`
173
+ ├── 遍历所有客户端,逐个写入
174
+ └── 写入失败的客户端自动移除
175
+ ```
176
+
177
+ ### 2.4 连接建立
178
+
179
+ `createSseResponse()` 创建 Bun ReadableStream,返回 `text/event-stream` 响应。
180
+ 初始发送空行以确认连接建立。客户端断开时自动取消注册。
181
+
182
+ ---
183
+
184
+ ## 3. HTTP 服务器
185
+
186
+ **文件**: `src/friend/server.ts`
187
+
188
+ ### 3.1 Bun.serve() 配置
189
+
190
+ - 端口: 3456
191
+ - 主机: 127.0.0.1
192
+ - `idleTimeout`: 60s(容纳慢速 STT 初始化)
193
+
194
+ ### 3.2 端口管理
195
+
196
+ `freePort()` 方法在启动时尝试释放被占用的端口:
197
+ 1. 使用 `ss -tlnp` 查找端口占用进程
198
+ 2. 验证进程是否为 `bun`/`VersperClaw`/`claude-*`/`node`
199
+ 3. 发送 SIGTERM,等待 3s,失败则 SIGKILL
200
+
201
+ ### 3.3 路由
202
+
203
+ | 路径 | 方法 | 功能 |
204
+ |------|------|------|
205
+ | `/plugins/friend/events` | GET | SSE 事件流 |
206
+ | `/plugins/friend/*` | ANY | Friend API 路由(见下文) |
207
+ | `/friend/*` | GET | 静态文件 |
208
+ | WebSocket 升级 | ANY | 返回 426(不支持) |
209
+
210
+ ---
211
+
212
+ ## 4. Friend API 路由
213
+
214
+ **文件**: `src/server/api/friend.ts` (~793 行)
215
+
216
+ ### 4.1 完整路由表
217
+
218
+ | 端点 | 方法 | 功能 |
219
+ |------|------|------|
220
+ | `/plugins/friend/events` | GET | SSE 事件流 |
221
+ | `/plugins/friend/audio/:id` | GET | 提供 TTS 音频文件 |
222
+ | `/plugins/friend/media/:id` | GET | 提供媒体文件 |
223
+ | `/plugins/friend/chat` | POST | 文字聊天消息 |
224
+ | `/plugins/friend/voice/stt-segment` | POST | 浏览器 VAD 语音片段 |
225
+ | `/plugins/friend/voice/start` | POST | 开始服务器端语音捕获 |
226
+ | `/plugins/friend/voice/stop` | POST | 停止语音捕获 |
227
+ | `/plugins/friend/voice/status` | POST | 获取捕获状态 |
228
+ | `/plugins/friend/touch` | POST | 触摸交互事件 |
229
+ | `/plugins/friend/voice` | GET/POST | 语音设置 |
230
+ | `/plugins/friend/stt/config` | GET | STT 配置 |
231
+ | `/plugins/friend/stt/file` | POST | 文件转录 |
232
+ | `/plugins/friend/preview` | POST | TTS 预览 |
233
+ | `/plugins/friend/settings` | GET/POST | 通用设置 |
234
+ | `/plugins/friend/persona` | GET/POST | 角色设定 |
235
+ | `/plugins/friend/model/list` | GET | 模型列表 |
236
+ | `/plugins/friend/model/serve/:file` | GET | 提供 VRM 模型文件 |
237
+ | `/plugins/friend/model/import` | POST | 导入 VRM 模型 |
238
+ | `/plugins/friend/history` | GET | 对话历史 |
239
+ | `/plugins/friend/context/clear` | POST | 清空上下文 |
240
+ | `/plugins/friend/mood/adjust` | POST | 调整心情 |
241
+ | `/plugins/friend/session/memo` | POST | 记录会话备注 |
242
+ | `/plugins/friend/dance/list` | GET | 舞蹈列表 |
243
+ | `/plugins/friend/dance/import` | POST | 导入舞蹈 VMD/MP3 |
244
+ | `/plugins/friend/dance/delete` | POST | 删除舞蹈 |
245
+ | `/plugins/friend/dance/serve/:file` | GET | 提供舞蹈文件 |
246
+ | `/plugins/friend/persona/screenshot` | POST | 保存 VRM 截图 |
247
+ | `/plugins/friend/persona/generate` | POST | AI 生成角色设定 |
248
+ | `/plugins/friend/screen/observe` | POST | 屏观察触发 |
249
+ | `/friend/api/window-close` | POST | Tauri 窗口关闭事件 |
250
+
251
+ ### 4.2 MIME 类型支持
252
+
253
+ - 音频: `.mp3`, `.opus`, `.ogg`, `.wav`, `.webm`
254
+ - 图片: `.png`, `.jpg`, `.jpeg`, `.gif`, `.webp`, `.svg`, `.bmp`
255
+
256
+ ---
257
+
258
+ ## 5. TTS 服务
259
+
260
+ **文件**: `src/friend/tts.ts`
261
+
262
+ ### 5.1 Edge TTS
263
+
264
+ ```typescript
265
+ edgeTts({ text, voice? })
266
+ ├── 使用 node-edge-tts 库
267
+ ├── 默认语音: zh-CN-XiaoxiaoNeural
268
+ ├── 输出: 临时目录中的 MP3 文件
269
+ └── 返回: { success, audioPath, error }
270
+ ```
271
+
272
+ ### 5.2 Qwen DashScope TTS
273
+
274
+ ```typescript
275
+ qwenTts({ text, apiKey, voice?, model?, language? })
276
+ ├── 端点: dashscope.aliyuncs.com / dashscope-intl.aliyuncs.com
277
+ ├── 默认模型: qwen3-tts-flash
278
+ ├── 默认语音: Cherry
279
+ ├── 超时: 30s
280
+ └── 返回: { success, audioPath, error }
281
+ ```
282
+
283
+ ### 5.3 音频文件注册表
284
+
285
+ ```typescript
286
+ audioFiles = Map<string, string> // id → 文件路径
287
+
288
+ registerAudioFile(filePath) // 注册文件,5 分钟后自动过期
289
+ getAudioFile(id) // 获取文件路径
290
+ ```
291
+
292
+ ---
293
+
294
+ ## 6. STT 服务
295
+
296
+ **文件**: `src/friend/stt-service.ts`
297
+
298
+ 提供基于文件的语音转录,用于 REST 端点。流式/进程内捕获由 FriendService 处理。
299
+
300
+ 支持 Provider:
301
+ - `anthropic`: Anthropic Voice Stream
302
+ - `local`: Local Whisper (openai-whisper)
303
+ - `doubao`: Doubao ASR
304
+ - `browser`: 浏览器 VAD (仅前端的占位符)
305
+
306
+ ---
307
+
308
+ ## 7. VAD 服务
309
+
310
+ **文件**: `src/friend/voice/vad-service.ts`
311
+
312
+ 参见 [语音捕获与 VAD 策略](voice-vad.md) 详细文档。
313
+
314
+ ### 7.1 核心架构
315
+
316
+ - **模型**: Silero VAD legacy ONNX (来自 `@ericedouard/vad-node-realtime`)
317
+ - **运行时**: onnxruntime-web WASM 后端(Bun 不兼容 onnxruntime-node)
318
+ - **帧大小**: 512 采样 @ 16kHz = 32ms/帧
319
+ - **预处理**: RMS 能量预过滤 (<0.004 跳过推理)
320
+
321
+ ### 7.2 状态机
322
+
323
+ ```
324
+ pre-speech phase (preSpeechCount < preSpeechTriggerFrames)
325
+ │ ├── 非语音帧 → 重置计数
326
+ │ └── 连续语音帧达到阈值 → 进入 speaking
327
+
328
+ speaking (confirmed speech segment)
329
+ │ ├── 语音帧 → 重置 redemptionCounter
330
+ │ └── 静音帧 → redemptionCounter++
331
+ │ └── redemptionCounter >= redemptionFrames → endSpeech()
332
+
333
+ silence redemption (grace period)
334
+ └── endSpeech() → onSpeechEnd 回调
335
+ ```
336
+
337
+ ---
338
+
339
+ ## 8. 偏好设置
340
+
341
+ **文件**: `src/friend/prefs.ts`
342
+
343
+ ```typescript
344
+ interface FriendPrefs {
345
+ enabled?: boolean;
346
+ voice?: string; // TTS 语音
347
+ provider?: string; // TTS provider (edge | qwen)
348
+ qwenKey?: string; // Qwen API Key
349
+ qwenModel?: string; // Qwen 模型名
350
+ modelPath?: string; // VRM 模型路径
351
+ ttsEnabled?: boolean; // TTS 开启
352
+ showText?: boolean; // 显示文字气泡
353
+ hideUI?: boolean; // 隐藏 UI
354
+ tracking?: 'mouse' | 'camera'; // 眼球追踪模式
355
+ volume?: number; // 音量 0-1
356
+ uiAlign?: 'left' | 'right'; // UI 对齐
357
+ screenObserve?: boolean; // 屏幕观察
358
+ screenObserveInterval?: number; // 观察间隔(秒)
359
+ language?: 'zh' | 'en'; // 语言
360
+ currentDance?: string; // 当前舞蹈
361
+ hideMood?: boolean; // 隐藏心情
362
+ sttProvider?: string; // STT provider
363
+ sttLanguage?: string; // STT 语言
364
+ groqApiKey?: string; // Groq API Key
365
+ }
366
+ ```
367
+
368
+ 持久化路径: `~/.config/VersperClaw/friend.json`
369
+
370
+ ---
371
+
372
+ ## 9. Tauri Launcher
373
+
374
+ **文件**: `src/friend/tauri-launcher.ts`
375
+
376
+ - 查找 Tauri 二进制文件(release → debug)
377
+ - 启动为 detached 子进程
378
+ - 管道 stdout/stderr 到主进程日志
379
+ - 退出时自动清理
380
+
381
+ 启动路径: `src/components/friend/frontend/src-tauri/target/{release|debug}/versperclaw-friend`
382
+
383
+ ---
384
+
385
+ ## 10. 常量
386
+
387
+ **文件**: `src/friend/constants.ts`
388
+
389
+ ```typescript
390
+ GATEWAY_URL = 'http://127.0.0.1:3456'
391
+ FRIEND_SESSION_KEY = 'agent:main:main'
392
+ CHANNEL_ID = 'friend'
393
+ VALID_EMOTIONS = ['happy', 'sad', 'angry', 'surprised', 'think', 'awkward',
394
+ 'question', 'curious', 'neutral', 'love', 'flirty',
395
+ 'greeting', 'relaxed']
396
+ ```
397
+
398
+ ---
399
+
400
+ ## 依赖关系图
401
+
402
+ ```
403
+ server.ts ──┬── sse.ts ──────────► FriendService.ts ──┬── tts.ts
404
+ │ │ ├── vad-service.ts
405
+ │ │ ├── prefs.ts
406
+ │ │ └── text-utils.ts
407
+ │ │
408
+ └── api/friend.ts ───┤
409
+ ├── sse.ts
410
+ ├── prefs.ts
411
+ ├── tts.ts
412
+ ├── stt-service.ts
413
+ └── FriendService.ts
414
+
415
+ FriendEmotionTool.ts ──► sse.ts, prefs.ts
416
+ FriendScreenObserveTool.ts ──► sse.ts
417
+
418
+ tauri-launcher.ts (独立启动)
419
+ ```
docs/friend/data-flow.md ADDED
@@ -0,0 +1,369 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Friend 数据流
2
+
3
+ 本文档详细描述 Friend 系统中的四种核心数据流,以及 SSE 事件格式规范。
4
+
5
+ ---
6
+
7
+ ## 1. 文字对话流
8
+
9
+ 用户在前端输入框输入文字,按下 Enter 发送。
10
+
11
+ ```
12
+ 用户输入文字
13
+
14
+
15
+ ChatInput.tsx ── POST /plugins/friend/chat ──────────────────┐
16
+ │ { message: "你好" } │
17
+ │ │
18
+ ▼ │
19
+ handleFriendApi() ── friendService.start() │
20
+ │ friendService.sendText(message) │
21
+ ▼ │
22
+ FriendService.sendText() │
23
+ │ │
24
+ ├── 通知 inboundListeners (bridge hook turn tracking) │
25
+ │ │
26
+ └── messageQueueManager.enqueue() │
27
+ │ { mode: 'prompt', skipSlashCommands: true, │
28
+ │ bridgeOrigin: true, origin: { server: 'friend' }} │
29
+ │ │
30
+ ▼ │
31
+ AI Provider (Anthropic/NVIDIA/OpenAI) │
32
+ │ │
33
+ ├── 处理消息 │
34
+ ├── 可调用 friend_emotion 工具设置表情 │
35
+ ├── 可调用 friend_screen_observe 观察屏幕 │
36
+ │ │
37
+ ▼ │
38
+ AI 回复流回 (通过 useFriendBridge / REPL) │
39
+ │ │
40
+ ▼ │
41
+ FriendService.broadcastResponse(text) │
42
+ │ │
43
+ ├── broadcastToVrm({ text }) │
44
+ │ │ │
45
+ │ ▼ SSE data │
46
+ │ TextBubble 收到 │
47
+ │ │ │
48
+ │ ├── 重置气泡,显示文字 │
49
+ │ ├── 启动打字机效果 (逐字符显示) │
50
+ │ │ - CJK: 200ms/char (TTS开启) / 80ms (关闭) │
51
+ │ │ - English: 60ms/char (TTS开启) / 30ms (关闭)│
52
+ │ ├── 打字机完成后渲染 Markdown │
53
+ │ └── 1秒后触发 onMessage → VRMScene 表情动作 │
54
+ │ │
55
+ ├── if TTS enabled: │
56
+ │ ├── generateTts(text) │
57
+ │ │ ├── stripForTts() 清洗文本 │
58
+ │ │ └── EdgeTTS 或 QwenTTS 生成 MP3 │
59
+ │ │ │
60
+ │ ├── broadcastToVrm({ audioUrl, sendFirstTts }) │
61
+ │ │ │ │
62
+ │ │ ▼ SSE data │
63
+ │ │ TextBubble 开始音频队列播放 │
64
+ │ │ │ │
65
+ │ │ └── LipSync.playAudio(url) │
66
+ │ │ ├── fetch 音频文件 │
67
+ │ │ ├── decodeAudioData │
68
+ │ │ ├── 连接到 lipSyncNode (分析) + gainNode (扬声器) │
69
+ │ │ └── 播放时实时更新 VRM 嘴形 │
70
+ │ │ │
71
+ │ └── extendMuteForTts(audioId) │
72
+ │ └── 精确计算 MP3 时长,更新静音定时器 │
73
+ │ │
74
+ └── broadcastToVrm({ replyDone: true }) │
75
+ │ │
76
+ ▼ SSE data │
77
+ TextBubble 调度气泡隐藏 (2s 延迟) │
78
+ 或等待后续 appendText 消息 │
79
+ ```
80
+
81
+ ---
82
+
83
+ ## 2. 语音捕获流 (F2 通话模式)
84
+
85
+ 用户按下 F2 进入连续语音通话模式,再次按下 F2 结束通话。
86
+
87
+ ### 2.1 启动通话
88
+
89
+ ```
90
+ 用户按 F2
91
+
92
+
93
+ ChatInput.startVoiceCall()
94
+
95
+ ├── setVoiceCallActive(true)
96
+
97
+ └── useServerStt.startStreaming()
98
+
99
+ └── POST /plugins/friend/voice/start
100
+
101
+
102
+ handleFriendApi()
103
+
104
+ └── friendService.startVoiceCapture()
105
+
106
+ ├── 检测 STT provider (Groq→Whisper→Anthropic→Doubao)
107
+
108
+ ├── startSttConnection() (8s 超时)
109
+ │ ├── Groq: connectGroqStream()
110
+ │ ├── Local: connectLocalWhisperStream()
111
+ │ ├── Anthropic: connectVoiceStream()
112
+ │ └── Doubao: connectDoubaoStream()
113
+
114
+ ├── loadAudioCapture()
115
+ │ ├── 尝试 arecord (ALSA)
116
+ │ │ args: -D default -r 16000 -f S16_LE -c 1 -t raw -q
117
+ │ ├── 失败则尝试 parecord (PulseAudio)
118
+ │ │ args: --raw --rate=16000 --format=s16le --channels=1
119
+ │ └── 500ms 验证窗口: 确认子进程输出音频数据
120
+
121
+ ├── arecord 数据回调:
122
+ │ ├── if not muted → 转发到 STT connection.send(chunk)
123
+ │ └── if not muted → 转发到 VAD processAudio(float32)
124
+
125
+ └── vadInstance.start()
126
+ ```
127
+
128
+ ### 2.2 语音检测与转录
129
+
130
+ ```
131
+ 麦克风音频流 (16kHz S16LE)
132
+
133
+ ├──► STT Connection.send(chunk) (实时流式转录)
134
+
135
+ └──► SileroVad.processAudio(float32)
136
+
137
+ ├── RMS 预过滤 (阈值 0.004)
138
+ │ ├── < 阈值 → 概率 = 0 (跳过推理)
139
+ │ └── >= 阈值 → ONNX 推理
140
+
141
+ ├── 状态机
142
+ │ ├── pre-speech: 需要连续 10 帧 (~320ms) 确认说话
143
+ │ ├── speaking: 语音持续中
144
+ │ └── silence redemption: 连续 20 帧 (~640ms) 静音触发 endSpeech
145
+
146
+ └── onSpeechEnd callback
147
+
148
+
149
+ FriendService._flushVadSegment()
150
+
151
+ ├── 创建新的 STT 连接 (旧的连接继续处理)
152
+
153
+ ├── 等待旧连接 finalize()
154
+ │ └── 获取转录文本推入 captureTranscripts
155
+
156
+ ├── if 有转录文本:
157
+ │ ├── this.sendText(transcript)
158
+ │ │ │
159
+ │ │ ▼
160
+ │ │ messageQueueManager.enqueue() → AI 开始处理
161
+ │ │
162
+ │ └── this.startAiTurnMute()
163
+ │ ├── muted = true
164
+ │ ├── VAD pause
165
+ │ └── 30s 超时安全性定时器
166
+
167
+ └── 循环继续监听下一段语音
168
+ ```
169
+
170
+ ### 2.3 AI 回复与静音解除
171
+
172
+ ```
173
+ AI 处理完成
174
+
175
+
176
+ FriendService.broadcastResponse(text)
177
+
178
+ ├── broadcastToVrm({ text }) // 显示文字
179
+
180
+ ├── if TTS enabled:
181
+ │ ├── generateTts(text)
182
+ │ │ │
183
+ │ │ ▼
184
+ │ ├── broadcastToVrm({ audioUrl, sendFirstTts: true })
185
+ │ │
186
+ │ └── extendMuteForTts(audioId)
187
+ │ ├── getMp3DurationMs() 精确计算
188
+ │ ├── 取消 30s 安全性定时器
189
+ │ └── 设定精确的播放时长定时器
190
+
191
+ └── broadcastToVrm({ replyDone: true })
192
+
193
+
194
+ 播放完成后 → unmute()
195
+ ├── muted = false
196
+ └── VAD resume (可继续接收语音)
197
+ ```
198
+
199
+ ### 2.4 结束通话
200
+
201
+ ```
202
+ 用户按 F2 (再次)
203
+
204
+
205
+ ChatInput.endVoiceCall()
206
+
207
+ └── useServerStt.stopStreaming()
208
+
209
+ └── POST /plugins/friend/voice/stop
210
+
211
+
212
+ friendService.stopVoiceCapture()
213
+
214
+ ├── arecord.kill('SIGTERM') → 2s 后 SIGKILL
215
+ ├── capturing = false
216
+ ├── clearMute()
217
+ ├── VAD reset()
218
+ ├── STT connection.finalize() + close()
219
+ ├── 发送剩余转录文本
220
+ └── 返回完整转录
221
+ ```
222
+
223
+ ---
224
+
225
+ ## 3. 情绪表情流
226
+
227
+ LLM 调用 `friend_emotion` 工具触发情绪更新。
228
+
229
+ ```
230
+ LLM 处理完成,调用 friend_emotion 工具
231
+
232
+
233
+ FriendEmotionTool.call({ emotion: 'happy', intensity: 0.8, mood_delta: 2 })
234
+
235
+ ├── broadcastToVrm({ emotion: 'happy', emotionIntensity: 0.8 })
236
+ │ │
237
+ │ ▼ SSE data
238
+ │ App.tsx handleVrmMessage
239
+ │ │
240
+ │ ├── emotionActionMap['happy'] = 'happy'
241
+ │ │
242
+ │ ├── sceneRef.current.setEmotionWithReset('happy', 5000, 0.8)
243
+ │ │ │
244
+ │ │ ▼
245
+ │ │ VRMScene.setEmotionWithReset (via forwardRef)
246
+ │ │ │
247
+ │ │ └── EmoteController.setEmotionWithReset('happy', 5000, 0.8)
248
+ │ │ ├── setEmotion('happy', 0.8)
249
+ │ │ │ ├── 获取 happy 的 blend shapes: [{name:'happy', val:0.2}, {name:'aa', val:0.8}]
250
+ │ │ │ ├── 应用 intensity: aa = 0.8*0.8 = 0.64, happy = 0.2*0.8 = 0.16
251
+ │ │ │ ├── isTransitioning = true
252
+ │ │ │ └── 记录目标 blendshape 值
253
+ │ │ │
254
+ │ │ └── setTimeout(5000ms → setEmotion('neutral'))
255
+ │ │
256
+ │ └── sceneRef.current.playAction('happy')
257
+ │ │
258
+ │ ▼
259
+ │ MotionController.playAction('happy')
260
+ │ ├── loadClip('happy.fbx')
261
+ │ ├── crossFadeTo(clip, 0.3s)
262
+ │ ├── LoopOnce + clampWhenFinished
263
+ │ └── 完成后 crossFade 回 idle 动画
264
+
265
+ ├── 处理 mood_delta
266
+ │ ├── 读取当前 moodIndex = 60
267
+ │ ├── newMood = clamp(60 + 2, 0, 100) = 62
268
+ │ ├── 持久化到 prefs
269
+ │ └── broadcastToVrm({ moodDelta: 2, moodIndex: 62 })
270
+ │ │
271
+ │ ▼ SSE data
272
+ │ MoodIndicator 收到
273
+ │ ├── 显示心情数值变化气泡 (+2)
274
+ │ ├── Canvas 动画: displayPercent 从 60 → 62 渐变
275
+ │ └── 5s 后自动隐藏
276
+
277
+ └── 返回 tool result
278
+ ```
279
+
280
+ ### 每帧更新循环 (VRMScene animate)
281
+
282
+ ```
283
+ requestAnimationFrame 循环 (约 60fps)
284
+
285
+ ├── 1. MotionController.update(delta)
286
+ │ └── AnimationMixer.update(delta)
287
+
288
+ ├── 2. 应用 Relaxed Hand Pose (非舞蹈状态)
289
+ │ └── 手指自然弯曲 + 微妙颤动
290
+
291
+ ├── 3. Humanoid.update()
292
+
293
+ ├── 4. 眼球追踪 (camera 模式)
294
+ │ └── lookAtTarget = camera.position
295
+
296
+ ├── 5. LookAt.update(delta)
297
+
298
+ ├── 6. Eye Saccades Controller.update()
299
+ │ └── 每隔 400-1200ms 添加随机眼球微动偏移
300
+
301
+ ├── 7. Blink State Machine.update()
302
+ │ └── 随机眨眼 (间隔 1-6s, 时长 150ms, sin 曲线)
303
+
304
+ ├── 8. EmoteController.update(delta)
305
+ │ └── cubic ease 过渡到目标 blendshape 值
306
+
307
+ ├── 9. LipSync.update(vrm, delta)
308
+ │ ├── 读取 wlipsync 音频分析节点的音素权重
309
+ │ ├── 选择胜者/亚军音素
310
+ │ ├── Attack/Release 平滑 (50/30)
311
+ │ └── 设置 VRM 嘴形 blendshapes (aa, ee, ih, oh, ou)
312
+
313
+ ├── 10. ExpressionManager.update()
314
+
315
+ └── 11. SpringBoneManager.update(delta)
316
+ └── 物理头发/衣服/饰品模拟
317
+ ```
318
+
319
+ ---
320
+
321
+ ## 4. SSE 事件格式
322
+
323
+ 所有前端 SSE 事件通过 `GET /plugins/friend/events` 接收,格式为标准 SSE (`data: {json}\n\n`)。
324
+
325
+ ### 4.1 VrmBroadcastPayload 字段说明
326
+
327
+ | 字段 | 类型 | 必填 | 说明 |
328
+ |------|------|------|------|
329
+ | `text` | string | 否 | AI 回复文字,TextBubble 显示并启动打字机效果 |
330
+ | `emotion` | string | 否 | VRM 表情名,触发 EmoteController 切换 blend shapes |
331
+ | `emotionIntensity` | number | 否 | 表情强度 0-1,默认 1 |
332
+ | `audioUrl` | string | 否 | TTS 音频 URL,TextBubble 触发 LipSync 播放 |
333
+ | `audioIndex` | number | 否 | 音频播放顺序索引,用于多句排序 |
334
+ | `clearText` | boolean | 否 | 清空气泡文字和音频队列 |
335
+ | `imageUrl` | string | 否 | 图片 URL,在气泡中显示 |
336
+ | `moodDelta` | number | 否 | 心情变化量,MoodIndicator 显示浮动气泡 |
337
+ | `moodIndex` | number | 否 | 当前心情指数 0-100,MoodIndicator Canvas 更新 |
338
+ | `sendFirstTts` | boolean | 否 | 开始 TTS 播放的信号,重置音频队列 |
339
+ | `appendText` | boolean | 否 | 追加文字模式,后续句子的文字和音频配对 |
340
+ | `replyDone` | boolean | 否 | 回复完成信号,TextBubble 调度气泡隐藏 |
341
+
342
+ ### 4.2 典型回复序列
343
+
344
+ ```
345
+ 1. { text: "你好!今天心情不错啊!", replyDone: true }
346
+ → 显示文字,打字机效果,1s 后触发表情
347
+
348
+ 2. { text: "一起玩吧!" }
349
+ { audioUrl: "http://127.0.0.1:3456/plugins/friend/audio/123-1", sendFirstTts: true }
350
+ { replyDone: true }
351
+ → 显示文字 + 播放 TTS + 语音结束后隐藏气泡
352
+
353
+ 3. { text: "今天天气真好。" }
354
+ { audioUrl: "...", sendFirstTts: true, emotion: "happy", emotionIntensity: 0.8 }
355
+ { appendText: true, text: "要不要出去走走?", audioUrl: "...", audioIndex: 1 }
356
+ { appendText: true, text: "我知道一个好地方。", audioUrl: "...", audioIndex: 2 }
357
+ { replyDone: true }
358
+ → 多句子回复,每句独立音频,按索引顺序播放
359
+ → 第一句发送时触发 happy 表情
360
+
361
+ 4. { emotion: "think", emotionIntensity: 0.7 }
362
+ → 思考阶段的表情更新(LLM 处理中)
363
+
364
+ 5. { clearText: true }
365
+ → 清空气泡(新会话)
366
+
367
+ 6. { moodDelta: 3, moodIndex: 63 }
368
+ → 心情更新,显示 +3 浮动气泡,Canvas 液态填充变化
369
+ ```
docs/friend/emotion-map.md ADDED
@@ -0,0 +1,195 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 情绪 → 3D 表情映射
2
+
3
+ Friend 系统支持 13 种情绪,每种映射到 VRM blend shapes 组合、过渡时间和肢体动作。
4
+
5
+ ---
6
+
7
+ ## 完整映射表
8
+
9
+ 数据来源: `src/components/friend/frontend/emote.ts`
10
+
11
+ | 情绪 | Blend Shapes 组合 | 过渡时间 | 肢体动作 |
12
+ |------|------------------|---------|---------|
13
+ | `happy` | happy(0.2) + aa(0.8) | 0.4s | `happy` (开心.fbx) |
14
+ | `sad` | sad(0.7) + oh(0.15) | 0.4s | `shy` (害羞.fbx) |
15
+ | `angry` | angry(0.7) + ee(0.3) | 0.3s | `angry` (生气.fbx) |
16
+ | `surprised` | surprised(0.8) + oh(0.4) | 0.15s | `excited` (兴奋.fbx) |
17
+ | `think` | think(0.7) | 0.5s | `scratchHead` (挠头.vrma) |
18
+ | `awkward` | sad(0.3) + ee(0.2) | 0.5s | `playFingers` (搓手.vrma) |
19
+ | `question` | surprised(0.4) + think(0.3) | 0.4s | `point` (指点.fbx) |
20
+ | `curious` | think(0.5) + surprised(0.2) | 0.4s | `scratchHead` (挠头.vrma) |
21
+ | `neutral` | neutral(1.0) | 0.6s | `salute` (敬礼.fbx) |
22
+ | `love` | happy(0.2) + relaxed(0.4) | 0.4s | `shy` (害羞.fbx) |
23
+ | `flirty` | happy(0.2) + relaxed(0.3) + aa(0.15) | 0.4s | `shy` (害羞.fbx) |
24
+ | `greeting` | happy(0.2) + aa(0.3) | 0.3s | `greeting` (招呼.fbx) |
25
+ | `relaxed` | relaxed(0.8) | 0.5s | `salute` (敬礼.fbx) |
26
+
27
+ ### 使用的 VRM Blend Shapes
28
+
29
+ VRM 标准 blendshape 名称及其对应的面部区域:
30
+
31
+ | Blend Shape | 面部区域 |
32
+ |-------------|---------|
33
+ | `happy` | 嘴角上扬 (smile) |
34
+ | `sad` | 嘴角下垂 |
35
+ | `angry` | 皱眉 |
36
+ | `surprised` | 眉毛上抬 |
37
+ | `think` | 思考表情 |
38
+ | `neutral` | 自然表情 |
39
+ | `relaxed` | 放松表情 |
40
+ | `aa` | 张嘴 (A 音) |
41
+ | `ee` | 露齿 (E 音) |
42
+ | `oh` | 嘟嘴 (O 音) |
43
+ | `ih` | 微张嘴 (I 音, 主要用于唇同步) |
44
+ | `ou` | 收唇 (U 音, 主要用于唇同步) |
45
+ | `blink` | 闭眼 (由眨眼系统独立控制) |
46
+
47
+ ---
48
+
49
+ ## EmoteController 过渡系统
50
+
51
+ **文件**: `src/components/friend/frontend/emote.ts`
52
+
53
+ ### 过渡算法
54
+
55
+ ```typescript
56
+ // cubic ease 缓动函数
57
+ setEmotion('happy', intensity = 0.8)
58
+
59
+ ├── 1. 获取情绪定义
60
+ │ happy: [{name:'happy', value:0.2}, {name:'aa', value:0.8}]
61
+
62
+ ├── 2. 乘以 intensity
63
+ │ happy = 0.2 * 0.8 = 0.16
64
+ │ aa = 0.8 * 0.8 = 0.64
65
+
66
+ ├── 3. 记录当前 blendshape 值 (起始值)
67
+ │ currentValues = { happy: 0.05, aa: 0, ... }
68
+
69
+ ├── 4. 设置目标值
70
+ │ targetValues = { happy: 0.16, aa: 0.64 }
71
+
72
+ ├── 5. 开始过渡 (isTransitioning = true)
73
+
74
+ └── 每帧 update(deltaTime):
75
+
76
+ ├── transitionProgress += deltaTime / blendDuration(0.4s)
77
+
78
+ ├── if progress >= 1: transition end
79
+
80
+ ├── cubic ease:
81
+ │ t < 0.5 → 4 * t³
82
+ │ t >= 0.5 → 1 - (-2t + 2)³ / 2
83
+
84
+ └── value = start + (target - start) * ease(t)
85
+ ```
86
+
87
+ ### 自动回中系统
88
+
89
+ ```typescript
90
+ setEmotionWithReset('happy', durationMs = 5000, intensity = 0.8)
91
+ ├── setEmotion('happy', 0.8) // 立即开始过渡
92
+ └── setTimeout(5000ms)
93
+ └── setEmotion('neutral') // 自动回中到自然表情
94
+ ```
95
+
96
+ ### 完整重置
97
+
98
+ ```typescript
99
+ resetAll()
100
+ ├── 清除 resetTimer
101
+ ├── isTransitioning = false
102
+ ├── 将所有 blendshape 值设为 0
103
+ └── 清空 currentValues / targetValues
104
+ ```
105
+
106
+ ---
107
+
108
+ ## 情绪 → 动作映射
109
+
110
+ **文件**: `src/components/friend/frontend/App.tsx`
111
+
112
+ App.tsx 中的 `emotionActionMap` 定义了情绪与动作的关联:
113
+
114
+ ```typescript
115
+ const emotionActionMap: Record<string, string> = {
116
+ think: 'scratchHead', // 挠头
117
+ question: 'point', // 指
118
+ curious: 'scratchHead', // 挠头
119
+ happy: 'happy', // 开心
120
+ surprised: 'excited', // 兴奋
121
+ angry: 'angry', // 生气
122
+ awkward: 'playFingers', // 搓手指
123
+ sad: 'shy', // 害羞
124
+ love: 'shy', // 害羞
125
+ flirty: 'shy', // 害羞
126
+ greeting: 'greeting', // 招呼
127
+ relaxed: 'salute', // 敬礼
128
+ neutral: 'salute', // 敬礼
129
+ }
130
+ ```
131
+
132
+ 动作文件类型:
133
+ - `.vrma`: VRM Animation 格式 (挠头、搓手、伸展、叉腰)
134
+ - `.fbx`: Mixamo FBX 格式 (开心、生气、招呼、兴奋、害羞、指点、敬礼、暴怒)
135
+
136
+ ---
137
+
138
+ ## 心情指数系统
139
+
140
+ 除了即时表情,Friend 还有持续的心情指数系统:
141
+
142
+ ```
143
+ moodIndex: 0-100
144
+ ├── 0-29: 低 (灰色, rgb(160,168,180))
145
+ ├── 30-49: 偏低 (蓝色, rgb(78,168,222))
146
+ ├── 50-69: 中等 (绿色, rgb(72,199,142))
147
+ ├── 70-89: 良好 (橙色, rgb(255,165,70))
148
+ └── 90-100: 优秀 (粉色, rgb(255,107,157))
149
+ ```
150
+
151
+ - LLM 通过 `friend_emotion` 工具的 `mood_delta` 参数调整 (-3 到 +3)
152
+ - 前端 MoodIndicator 组件以液态填充柱状图 + 爱心图标可视化
153
+ - Canvas 动画使用贝塞尔波浪动画和颜色渐变
154
+
155
+ ---
156
+
157
+ ## 触摸交互反应系统
158
+
159
+ **文件**: `src/components/friend/frontend/App.tsx`
160
+
161
+ 6 个触摸区域各自有多个可能的反应:
162
+
163
+ | 区域 | 可能的反应 (情绪 + 动作) |
164
+ |------|------------------------|
165
+ | head (头) | relaxed+happy, relaxed+shy, angry+angryPump, relaxed+excited |
166
+ | arm (手臂) | surprised+excited, happy+happy, relaxed+greeting, relaxed+akimbo |
167
+ | chest (胸) | angry+angryPump, angry+angry, angry+point |
168
+ | belly (肚子) | angry+angryPump, angry+angry, awkward+playFingers |
169
+ | buttocks (屁股) | angry+angryPump, angry+point, sad+shy |
170
+ | leg (腿) | sad+shy, angry+angry, awkward+playFingers |
171
+
172
+ 触摸交互逻辑:
173
+ 1. 双击模型触发区域检测(射线检测 + 最近骨骼匹配)
174
+ 2. 随机选择该区域的一个反应组合
175
+ 3. 立即播放表情 + 动作
176
+ 4. 3s 冷却期写入 session memo([用户摸了摸你的xx])
177
+ 5. 60s 冷却期 + 50% 概率发送文字回复到 AI(POST /plugins/friend/touch)
178
+
179
+ ---
180
+
181
+ ## 空闲小动作系统
182
+
183
+ 当用户 30 秒无活动时,VRM 角色会自动做一些小动作:
184
+
185
+ ```
186
+ idleMs >= 30,000ms → 每 15s 检查一次
187
+
188
+ ├── 50% 概率触发
189
+ ├── 随机选择 13 种情绪之一
190
+ ├── 随机选择 12 种动作之一
191
+ ├── 强度: 0.4-0.8 (随机)
192
+ └── 持续: 3-5s (随机)
193
+ ```
194
+
195
+ 此系统防止角色长时间静止不动,增加生动感。
docs/friend/frontend-3d.md ADDED
@@ -0,0 +1,573 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 前端 3D 渲染
2
+
3
+ 本文档描述 Friend VRM 桌面伴侣的前端 3D 渲染系统。
4
+
5
+ ---
6
+
7
+ ## 技术栈
8
+
9
+ | 技术 | 用途 |
10
+ |------|------|
11
+ | **Three.js** | 3D 渲染引擎 |
12
+ | **@pixiv/three-vrm** | VRM 模型加载与控制 |
13
+ | **@pixiv/three-vrm-animation** | VRM 动画加载与 LookAt |
14
+ | **@pixiv/three-vrm-animation** | VRMA 动画支持 |
15
+ | **Tauri (WebKitGTK)** | 桌面窗口外壳 |
16
+ | **React** | UI 框架 |
17
+ | **Vite** | 前端构建工具 |
18
+ | **Bun** | 构建运行环境 |
19
+ | **CSS-in-JS** (内联 style) | 组件样式 |
20
+ | **Lucide React** | 图标库 |
21
+ | **marked** | Markdown 渲染 |
22
+ | **wlipsync** | WebAudio 唇形同步 |
23
+ | **Intl.Segmenter** | Unicode 字符分割 |
24
+
25
+ ---
26
+
27
+ ## VRMScene.tsx — 核心 3D 场景
28
+
29
+ **文件**: `src/components/friend/frontend/components/VRMScene.tsx` (~871 行)
30
+
31
+ ### 组件接口
32
+
33
+ 通过 `forwardRef` 暴露的操作句柄:
34
+
35
+ ```typescript
36
+ interface VRMSceneHandle {
37
+ setEmotion(emotion: string, intensity?: number): void
38
+ setEmotionWithReset(emotion: string, durationMs: number, intensity?: number): void
39
+ resetCamera(): void
40
+ setTrackingMode(mode: 'mouse' | 'camera'): void
41
+ playAction(name: string, hold?: boolean): void
42
+ captureScreenshot(): string | null
43
+ panCamera(dx: number, dy: number): void
44
+ rotateCamera(dx: number, dy: number): void
45
+ playDance(nameOrPreset: string | DancePreset): void
46
+ stopDance(): void
47
+ isDancing(): boolean
48
+ setBgmVolume(v: number): void
49
+ reset(): void
50
+ }
51
+ ```
52
+
53
+ ### 场景初始化
54
+
55
+ ```typescript
56
+ // VRMScene 组件创建时
57
+ const renderer = new THREE.WebGLRenderer({
58
+ canvas,
59
+ alpha: true, // 透明背景
60
+ antialias: true, // 抗锯齿
61
+ preserveDrawingBuffer: true, // 截图支持
62
+ })
63
+ renderer.setClearColor(0x000000, 0) // 完全透明
64
+
65
+ // 透视相机
66
+ const FOV = 40
67
+ const camera = new THREE.PerspectiveCamera(FOV, aspect, 0.1, 100)
68
+ // 轨道控制: 围绕 pivot 点的球面坐标
69
+
70
+ // 光照
71
+ const ambientLight = new THREE.AmbientLight(0xffffff, 0.6)
72
+ const directionalLight = new THREE.DirectionalLight(0xffffff, 1.2) // 主光
73
+ const fillLight = new THREE.DirectionalLight(0xffffff, 0.4) // 补光
74
+ ```
75
+
76
+ ### 模型加载
77
+
78
+ ```typescript
79
+ loader.load(modelPath, async (gltf) => {
80
+ // 1. 获取 VRM 数据
81
+ const loadedVrm = gltf.userData.vrm
82
+
83
+ // 2. 优化: 移除冗余顶点 + 合并骨架
84
+ VRMUtils.removeUnnecessaryVertices(loadedVrm.scene)
85
+ VRMUtils.combineSkeletons(loadedVrm.scene)
86
+
87
+ // 3. 添加 LookAt 四元数代理
88
+ const lookAtQuatProxy = new VRMLookAtQuaternionProxy(loadedVrm.lookAt)
89
+ loadedVrm.scene.add(lookAtQuatProxy)
90
+
91
+ // 4. 标准化 VRM 0.x → 1.0 姿态
92
+ VRMUtils.rotateVRM0(loadedVrm)
93
+
94
+ // 5. 计算自动相机位置 (根据模型包围盒)
95
+ const box = new THREE.Box3().setFromObject(loadedVrm.scene)
96
+ // pivot 在颈部高度
97
+ // orbitRadius = modelSize.y / 4.2 / tan(FOV/2)
98
+
99
+ // 6. 初始化控制器
100
+ emote = new EmoteController(loadedVrm)
101
+ motion = new MotionController(loadedVrm)
102
+ handPoseCache = buildHandPoseCache(loadedVrm)
103
+
104
+ // 7. 加载空闲动画
105
+ motion.loadIdle('/friend/idle_loop.vrma')
106
+ })
107
+ ```
108
+
109
+ ### 每帧更新循环 (11 步)
110
+
111
+ ```typescript
112
+ function animate() {
113
+ // 1. Animation Mixer (MotionController)
114
+ motion.update(delta)
115
+
116
+ // 2. 放松手部姿态 (非舞蹈时)
117
+ if (handPose && !motion.isDancing)
118
+ applyRelaxedHandPose(handPose, elapsedTime)
119
+
120
+ // 3. Humanoid 骨骼更新
121
+ vrm.humanoid.update()
122
+
123
+ // 4. Camera tracking mode → lookAt
124
+ if (trackingMode === 'camera')
125
+ saccades.instantUpdate(vrm, camera.position)
126
+
127
+ // 5. LookAt 更新
128
+ vrm.lookAt.update(delta)
129
+
130
+ // 6. 眼球微动 (saccades)
131
+ saccadesController.update(vrm, lookAtTarget, delta)
132
+
133
+ // 7. 眨眼 (blink)
134
+ updateBlink(vrm, delta, blinkState)
135
+
136
+ // 8. 表情过渡 (EmoteController)
137
+ emote.update(delta)
138
+
139
+ // 9. 唇形同步 (LipSync)
140
+ lipSync.update(vrm, delta)
141
+
142
+ // 10. Expression Manager
143
+ vrm.expressionManager.update()
144
+
145
+ // 11. Spring Bone 物理
146
+ vrm.springBoneManager.update(delta)
147
+
148
+ renderer.render(scene, camera)
149
+ }
150
+ ```
151
+
152
+ ### 相机控制系统
153
+
154
+ 球面坐标相机控制:
155
+
156
+ ```typescript
157
+ // 参数: pivot(旋转中心), orbitRadius(距离), orbitTheta(水平角), orbitPhi(垂直角)
158
+ camera.position.set(
159
+ pivot.x + radius * sin(phi) * sin(theta),
160
+ pivot.y + radius * cos(phi),
161
+ pivot.z + radius * sin(phi) * cos(theta),
162
+ )
163
+ camera.lookAt(pivot)
164
+ ```
165
+
166
+ - 鼠标滚轮: 缩放 (0.8-5.0)
167
+ - 左键拖拽 VRM 模型: 双击触发射线碰撞检测身体区域
168
+ - 中键拖拽: 推拉 (dolly)
169
+ - 右键拖拽: 旋转视角
170
+ - 菜单栏拖拽按钮: 平移 / 旋转 视角
171
+
172
+ ### 眼球追踪系统
173
+
174
+ 两种模式:
175
+ - `mouse`: 鼠标在屏幕上的位置决定 VRM 视线的交点(通过射线平面求交)
176
+ - `camera`: VRM 始终看向相机位置
177
+
178
+ 眼球微动控制器 (`EyeSaccadeController`):
179
+ - 每 400-1200ms 添加随机偏移 (-0.25 到 0.25 units)
180
+ - 瞬间更新 + 每帧 lerp 平滑
181
+
182
+ ### 眨眼系统
183
+
184
+ - 随机间��: 1-6s
185
+ - 眨眼时长: 150ms
186
+ - 使用 sin(π * progress) 曲线实现自然闭合
187
+ - 每帧更新 `blink` blendshape
188
+
189
+ ### 放松手部姿态
190
+
191
+ 当 VRM 模型没有手指动画轨道时,手指会保持 T-pose 僵硬状态。
192
+ 解决方案:每帧手动设置手指旋转。
193
+
194
+ ```typescript
195
+ // 手指自然弯曲 (从拇指到小指递增)
196
+ const curlMap = {
197
+ Thumb: [0.25, 0.15, 0.10],
198
+ Index: [0.20, 0.30, 0.20],
199
+ Middle: [0.25, 0.35, 0.25],
200
+ Ring: [0.30, 0.40, 0.30],
201
+ Little: [0.35, 0.45, 0.30],
202
+ }
203
+ // 手指自然张开
204
+ const spreadMap = { Thumb: 0.15, Index: 0.04, Middle: 0, Ring: -0.04, Little: -0.08 }
205
+ // 微妙颤动: sin(time * freq + seed) * 0.02
206
+ ```
207
+
208
+ ### 触摸区域检测
209
+
210
+ 射线检测 + 最近骨骼匹配:
211
+
212
+ ```typescript
213
+ const boneRegionMap: [string, TouchRegion][] = [
214
+ ['head', 'head'], ['neck', 'head'],
215
+ ['leftShoulder', 'arm'], /* ...所有手臂骨骼... */
216
+ ['chest', 'chest'], ['spine', 'belly'],
217
+ ['hips', 'buttocks'],
218
+ ['leftUpperLeg', 'leg'], /* ...所有腿部骨骼... */
219
+ ]
220
+ // 计算点击点与所有骨骼的世界坐标距离
221
+ // 选择最近的骨骼对应的区域
222
+ ```
223
+
224
+ 双击确认(500ms 窗口)+ 5s 冷却。
225
+
226
+ ### 窗口穿透点击检测
227
+
228
+ ```typescript
229
+ // 渲染到 1x1 offscreen render target
230
+ // 读取光标位置的 alpha 通道
231
+ // alpha > 10 → 点击在模型上 → 不穿透
232
+ // alpha <= 10 → 点击在透明背景 → 穿透窗口
233
+ ```
234
+
235
+ ---
236
+
237
+ ## EmoteController — Blend Shape 动画系统
238
+
239
+ **文件**: `src/components/friend/frontend/emote.ts` (~207 行)
240
+
241
+ 详细情绪映射见 [情绪 → 3D 表情映射](emotion-map.md)。
242
+
243
+ ### 核心机制
244
+
245
+ ```
246
+ EmoteController
247
+ ├── emotionStates: Map<emotionName, { expression[], blendDuration }>
248
+ │ 13 种情绪的 blend shapes 组合定义
249
+
250
+ ├── setEmotion(name, intensity)
251
+ │ 开始过渡:记录起始值、设置目标值、启动过渡
252
+
253
+ ├── setEmotionWithReset(name, durationMs, intensity)
254
+ │ 设置表情 + 定时自动回中到 neutral
255
+
256
+ ├── update(deltaTime)
257
+ │ 每帧计算 cubic ease 过渡
258
+
259
+ └── resetAll()
260
+ 立即清零所有 blendshape
261
+ ```
262
+
263
+ ---
264
+
265
+ ## MotionController — 动画系统
266
+
267
+ **文件**: `src/components/friend/frontend/motion-controller.ts` (~421 行)
268
+
269
+ ### 支持的文件格式
270
+
271
+ | 格式 | 描述 | 来源 |
272
+ |------|------|------|
273
+ | VRMA | VRM Animation 格式 | 标准动画文件 |
274
+ | VMD | MikuMikuDance 格式 | 舞蹈动画 (极乐净土/恋爱循环) |
275
+ | FBX | Autodesk FBX 格式 | Mixamo 动画 (开心/生气等) |
276
+
277
+ ### 动作预设
278
+
279
+ **短动作** (单次触发,完成后回归 idle):
280
+
281
+ ```typescript
282
+ akimbo: { label: '叉腰', type: 'vrma' }
283
+ playFingers: { label: '搓手', type: 'vrma' }
284
+ scratchHead: { label: '挠头', type: 'vrma' }
285
+ stretch: { label: '伸展', type: 'vrma' }
286
+ happy: { label: '开心', type: 'fbx' }
287
+ angry: { label: '生气', type: 'fbx' }
288
+ greeting: { label: '招呼', type: 'fbx' }
289
+ excited: { label: '兴奋', type: 'fbx' }
290
+ shy: { label: '害羞', type: 'fbx' }
291
+ point: { label: '指点', type: 'fbx' }
292
+ salute: { label: '敬礼', type: 'fbx' }
293
+ angryPump: { label: '暴怒', type: 'fbx' }
294
+ ```
295
+
296
+ **舞蹈** (循环播放,支持 BGM):
297
+
298
+ ```typescript
299
+ jile: { label: '极乐净土', type: 'vmd', bgm: '/friend/jile.mp3' }
300
+ love: { label: '恋爱循环', type: 'vmd', bgm: '/friend/love.mp3' }
301
+ ```
302
+
303
+ ### 动画过渡系统
304
+
305
+ 使用单个持久的 `AnimationMixer` 配合 `crossFade` 过渡,避免 T-pose 闪烁:
306
+
307
+ ```typescript
308
+ private crossFadeTo(newAction, duration = 0.3s) {
309
+ newAction.reset().setEffectiveWeight(1).play()
310
+ const prev = this.currentAction ?? this.idleAction
311
+ if (prev && prev !== newAction) {
312
+ prev.crossFadeTo(newAction, duration, false)
313
+ }
314
+ this.currentAction = newAction
315
+ }
316
+ ```
317
+
318
+ ### 动作生命周期
319
+
320
+ ```
321
+ playAction('happy')
322
+
323
+ ├── 检查并发锁 (_actionPlaying / _isDancing)
324
+
325
+ ├── 异步加载动画文件 (带 generation 标记)
326
+ │ ├── VRMA: GLTFLoader + VRMAnimationLoaderPlugin
327
+ │ ├── FBX: loadMixamoAnimation
328
+ │ └── VMD: parseVMDAnimation + bindVMDToVRM + IK
329
+
330
+ ├── crossFadeTo(action, 0.3s)
331
+
332
+ ├── LoopOnce + clampWhenFinished
333
+
334
+ ├── finished 事件 → 回归 idle
335
+ │ └── hold 模式: 保持 10s 后回归 idle
336
+
337
+ └── 安全性超时: (duration + 1s) 后强制释放
338
+ ```
339
+
340
+ ### VMD 舞蹈系统
341
+
342
+ 特殊处理 VMD 格式:
343
+
344
+ ```typescript
345
+ loadVMDWithIK(url)
346
+ ├── 1. 解析 VMD 文件 (parseVMDAnimation)
347
+ │ - 解析 VMD 二进制格式
348
+ │ - 从 VRM 骨骼映射到关键帧
349
+ │ - 缓存解析结果 (解析开销大)
350
+
351
+ ├── 2. 绑定到 VRM (bindVMDToVRM)
352
+ │ - 生成 Three.js AnimationClip
353
+ │ - 创建 IK 目标对象
354
+ │ - 启用 IK 处理器
355
+
356
+ └── 3. 建立复用的 IK 处理器 (VRMIKHandler)
357
+ - 使用 FABRIK 算法
358
+ - 支持手臂 IK
359
+ - 每帧在 mixer.update 后运行
360
+ ```
361
+
362
+ 舞蹈启动时自动切换相机视角到合适位置(臀部高度为中心)。
363
+
364
+ ### BGM 系统
365
+
366
+ ```typescript
367
+ // 舞蹈开始时
368
+ this.bgmAudio = new Audio(preset.bgm)
369
+ this.bgmAudio.loop = true
370
+ this.bgmAudio.volume = this._volume
371
+ this.bgmAudio.play()
372
+
373
+ // 舞蹈停止时: 淡出 (每隔 50ms 降低 0.1)
374
+ const fadeInterval = setInterval(() => {
375
+ audio.volume = Math.max(0, audio.volume - 0.1)
376
+ if (audio.volume <= 0) { clearInterval(fadeInterval); audio.pause() }
377
+ }, 50)
378
+ ```
379
+
380
+ ---
381
+
382
+ ## LipSync — 唇形同步
383
+
384
+ **文件**: `src/components/friend/frontend/lip-sync.ts` (~187 行)
385
+
386
+ ### 技术实现
387
+
388
+ 使用 `wlipsync` 库的 WebAudio 音频分析节点:
389
+
390
+ ```typescript
391
+ // 初始化
392
+ this.lipSyncNode = await createWLipSyncNode(audioContext, profile)
393
+ // lipSyncNode 只分析音频,不连接扬声器
394
+ // gainNode 连接扬声器
395
+
396
+ // 播放音频时,同时连接到 lipSyncNode 和 gainNode
397
+ source.connect(this.lipSyncNode) // 分析
398
+ source.connect(this.gainNode) // 扬声器
399
+ ```
400
+
401
+ ### 音素 → VRM Blend Shape 映射
402
+
403
+ | wlipsync 分析键 | VRM Blend Shape |
404
+ |----------------|----------------|
405
+ | `A` | `aa` (张嘴) |
406
+ | `E` | `ee` (露齿) |
407
+ | `I` | `ih` (微张嘴) |
408
+ | `O` | `oh` (嘟嘴) |
409
+ | `U` | `ou` (收唇) |
410
+ | `S` | 映射到 `I`/`ih` |
411
+
412
+ 双胜者策略:取概率最高的两个音素,胜者 cap 0.7,亚军 cap 0.35。
413
+
414
+ ### 平滑参数
415
+
416
+ - `ATTACK`: 50 (上升速率)
417
+ - `RELEASE`: 30 (衰减速率)
418
+ - `CAP`: 0.7 (最大权重)
419
+ - `SILENCE_VOL`: 0.04 (静音音量阈值)
420
+ - `SILENCE_GAIN`: 0.05 (静音增益阈值)
421
+ - `IDLE_MS`: 160 (静音判定窗口)
422
+
423
+ 公式: `smoothed = from + (to - from) * (1 - exp(-rate * delta))`
424
+
425
+ 静音时完全跳过 blendshape 设置(让 EmoteController 控制嘴部)。
426
+
427
+ ---
428
+
429
+ ## TextBubble.tsx — 文字气泡
430
+
431
+ **文件**: `src/components/friend/frontend/components/TextBubble.tsx` (~608 行)
432
+
433
+ ### 核心功能
434
+
435
+ 1. **SSE 驱动**:通过 `EventSource` 实时接收 `VrmBroadcastPayload`
436
+ 2. **打字机效果**:逐字符显示,CJK/English 自适应速率
437
+ 3. **音频队列**:多句子音频按索引顺序播放,支持 `appendText` 配对
438
+ 4. **Markdown 渲染**:打字机完成后使用 `marked` 渲染
439
+ 5. **发送首 TTS 队列**:支持排队 `sendFirstTts` 信号
440
+ 6. **看门狗**:30s 超时自动隐藏(防止卡死)
441
+
442
+ ### 打字机速率计算
443
+
444
+ ```typescript
445
+ function getCharRate(text: string, ttsEnabled: boolean): number {
446
+ const ratio = cjkRatio(text)
447
+ if (ttsEnabled) {
448
+ return Math.round(200 * ratio + 60 * (1 - ratio)) // CJK: 200ms, EN: 60ms
449
+ }
450
+ return Math.round(80 * ratio + 30 * (1 - ratio)) // CJK: 80ms, EN: 30ms
451
+ }
452
+ ```
453
+
454
+ ### 消息处理逻辑
455
+
456
+ ```
457
+ handleMessage(msg)
458
+
459
+ ├── Audio-only: 直接入音频队列
460
+
461
+ ├── appendText: 配对文字和音频索引,等待播放时揭示文字
462
+
463
+ ├── Image-only: 显示图片,15s 自动隐藏
464
+
465
+ ├── Emotion-only: 转发 onMessage 回调 (不改变气泡)
466
+
467
+ └── Text message:
468
+ ├── sendFirstTts: 重置音频队列,播放首句 TTS
469
+ ├── 设置文字 → 启动打字机
470
+ ├── 打字机完成后隐藏调度 (2s)
471
+ └── 如果没有 TTS → 打字机完成后直接调度隐藏
472
+ ```
473
+
474
+ ### 隐藏调度逻辑
475
+
476
+ ```
477
+ tryScheduleHide()
478
+ ├── 打字机完成? (typewriterRef === null)
479
+ ├── 音频播放完毕? (!audioPlaying && queue empty)
480
+ ├── sendFirstTts 队列为空?
481
+ ├── replyDone 已收到?
482
+
483
+ └── 全部满足 → setTimeout(hideBubble, 2000ms)
484
+ ```
485
+
486
+ ---
487
+
488
+ ## ChatInput.tsx — 输入栏
489
+
490
+ **文件**: `src/components/friend/frontend/components/ChatInput.tsx` (~475 行)
491
+
492
+ ### 三种模式
493
+
494
+ 1. **文字输入模式**: 输入框 + 发送按钮 + 回车发送
495
+ 2. **PTT 模式**: 按住麦克风按钮录音,松开停止并转录
496
+ 3. **语音通话模式** (F2): 连续语音,VAD 自动分段
497
+
498
+ ### 全局快捷键
499
+
500
+ - `Enter`: 打开输入栏 / 发送消息
501
+ - `Escape`: 关闭输入栏
502
+ - `F2`: 切换语音通话
503
+ - `F4`: 设置面板
504
+ - `F5`: 刷新前端
505
+ - `Tab`: 折叠/展开菜单
506
+ - `Ctrl+D`: 清空输入
507
+
508
+ ### 语音通话 TTS 中断
509
+
510
+ ```typescript
511
+ const scheduleInterrupt = useCallback(() => {
512
+ // 1s 延迟中断 TTS 播放
513
+ // 当用户开始说话时,延迟 1s 后中断当前 TTS
514
+ // 避免用户的"嗯"等短暂声音打断对话
515
+ setTimeout(() => {
516
+ ;(window as any).__clawInterruptAudio?.()
517
+ }, 1000)
518
+ }, [])
519
+ ```
520
+
521
+ ---
522
+
523
+ ## MoodIndicator.tsx — 心情指示器
524
+
525
+ **文件**: `src/components/friend/frontend/components/MoodIndicator.tsx` (~373 行)
526
+
527
+ ### Canvas 动画
528
+
529
+ - **液态填充柱状图**: 使用二次贝塞尔波浪动画 + Canvas clip
530
+ - **爱心图标**: 同样的波浪填充,经典心形路径
531
+ - **双波浪层**: 不同速度和透明度叠加,产���液态流动效果
532
+ - **颜色分级**: 90+ 粉色 → 70+ 橙色 → 50+ 绿色 → 30+ 蓝色 → 0+ 灰色
533
+ - **浮动气泡**: 心情变化时显示 `❤️+3` 或 `🩶-2` 浮动动画
534
+
535
+ ### 交互
536
+
537
+ - 正常状态: 半透明 (opacity 0.5)
538
+ - 鼠标悬停: 全透明 (opacity 1.0)
539
+ - 拖拽: 可自由移动到任意位置
540
+ - 自动隐藏: 5s 无交互后恢复半透明
541
+
542
+ ### 波浪参数
543
+
544
+ ```typescript
545
+ WAVE_LENGTH = 8 // 波长
546
+ WAVE_HEIGHT = 2.5 // 波高 (px)
547
+
548
+ // 双波浪叠加
549
+ drawWave(scrollDir: -1, alpha: 0.55) // 底层波浪,慢速反向
550
+ drawWave(scrollDir: 1, alpha: 1.0) // 顶层波浪,快速正向
551
+ ```
552
+
553
+ ---
554
+
555
+ ## 其他组件
556
+
557
+ ### ResizeHandles
558
+ 窗口大小调整手柄,支持拖拽调整窗口尺寸。
559
+
560
+ ### SettingsPanel
561
+ 设置面板 (F4 打开),包含:
562
+ - VRM 模型选择/导入
563
+ - TTS 开关 + 语音选择
564
+ - 显示设置 (文字气泡、UI、心情)
565
+ - 追踪模式 (鼠标/相机)
566
+ - 音量控制
567
+ - 屏幕观察设置
568
+ - 语言设置
569
+ - STT Provider 选择
570
+ - 舞蹈选择
571
+
572
+ ### HistoryPanel
573
+ 对话历史面板,显示最近 100 条消息,支持拖拽移动位置。
docs/friend/overview.md ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # VRM 桌面伴侣系统
2
+
3
+ ## 定位
4
+
5
+ Friend 是 VersperClaw 的 VRM 3D 桌面伙伴系统,与 CLI 共享同一进程运行。它通过 SSE + HTTP 与 Tauri 前端通信,无需独立的子进程或外部服务。用户可以与 VRM 角色进行文字聊天、语音对话,角色会通过 3D 表情、肢体动作和语音进行反馈。
6
+
7
+ 核心设计理念:**同进程集成** — FriendService 作为单例运行在 CLI 主进程中,消息通过 `messageQueueManager.enqueue()` 直接注入到对话流程中,AI 回复通过 SSE 实时广播到前端显示。
8
+
9
+ ---
10
+
11
+ ## 核心组件
12
+
13
+ | 组件 | 文件 | 职责 |
14
+ |------|------|------|
15
+ | **FriendService** | `src/friend/FriendService.ts` (~31KB) | 核心编排器,管理生命周期、语音捕获、STT/TTS、SSE 广播、静音系统 |
16
+ | **SSE 模块** | `src/friend/sse.ts` | 客户端注册表,类型化广播 `VrmBroadcastPayload` |
17
+ | **HTTP 服务器** | `src/friend/server.ts` | Bun.serve() 在 3456 端口,处理静态文件、API 路由、SSE 连接 |
18
+ | **API 路由** | `src/server/api/friend.ts` | `/plugins/friend/*` 的所有 REST 端点 |
19
+ | **TTS 服务** | `src/friend/tts.ts` | Edge TTS + Qwen DashScope TTS,音频文件注册表 |
20
+ | **STT 服务** | `src/friend/stt-service.ts` | 基于文件的语音转录(REST 端点) |
21
+ | **VAD 服务** | `src/friend/voice/vad-service.ts` | Silero VAD ONNX 模型,onnxruntime-web WASM 后端 |
22
+ | **偏好设置** | `src/friend/prefs.ts` | 持久化到 `~/.config/VersperClaw/friend.json` |
23
+ | **Tauri 启动器** | `src/friend/tauri-launcher.ts` | 启动 Tauri 桌面窗口 |
24
+ | **前端应用** | `src/components/friend/frontend/` | React + Three.js + @pixiv/three-vrm |
25
+
26
+ ---
27
+
28
+ ## LLM 集成
29
+
30
+ Friend 通过三种方式与 LLM 深度集成:
31
+
32
+ ### 1. `friend_emotion` 工具
33
+ - 定义在 `src/tools/FriendEmotionTool.ts`
34
+ - LLM 可在每次回复后调用,设置角色表情和心情
35
+ - 参数:`emotion` (13种情绪之一)、`intensity` (0-1)、`mood_delta` (-3 到 +3)
36
+ - 通过 `broadcastToVrm()` 向 SSE 客户端广播表情切换
37
+ - `mood_delta` 会持久化到 prefs 中的 `_moodIndex`,并广播给前端心情指示器
38
+
39
+ ### 2. `friend_screen_observe` 工具
40
+ - 定义在 `src/tools/FriendScreenObserveTool.ts`
41
+ - 捕获桌面截图,返回图片路径
42
+ - LLM 使用 Read 工具查看截图后,以同伴身份回应
43
+ - 自动广播 `think` 表情到前端
44
+
45
+ ### 3. friendPrompt 技能注入
46
+ - 定义在 `src/skills/bundled/friendPrompt.ts`
47
+ - 自动注入系统提示,告知 LLM 拥有 VRM 虚拟形象
48
+ - 包含情绪列表、心情指数、对话风格指引
49
+
50
+ ---
51
+
52
+ ## 情绪系统
53
+
54
+ Friend 支持 13 种情绪,映射到 VRM blend shapes 和骨骼动画:
55
+
56
+ - `happy`, `sad`, `angry`, `surprised`, `think`, `awkward`, `question`, `curious`, `neutral`, `love`, `flirty`, `greeting`, `relaxed`
57
+
58
+ 每种情绪在 `src/components/friend/frontend/emote.ts` 中定义了:
59
+ - VRM blend shapes 组合及权重
60
+ - 过渡时间(cubic ease 缓动)
61
+ - 自动回中到 neutral 的定时器
62
+
63
+ 情绪与肢体动作在 `App.tsx` 的 `emotionActionMap` 中关联。
64
+
65
+ ---
66
+
67
+ ## 交互模式
68
+
69
+ ### 文字聊天
70
+ 用户在输入框输入文字,通过 `POST /plugins/friend/chat` 发送到后端,FriendService 调用 `sendText()` 将消息通过 `messageQueueManager.enqueue()` 注入 CLI 对话流程。
71
+
72
+ ### PTT 按键通话
73
+ 按住麦克风按钮进行语音录制,松开后语音数据被发送到 STT 服务转录为文字,然后提交到对话流程。
74
+
75
+ ### F2 语音通话
76
+ 按下 F2 进入连续语音通话模式。服务器端通过 `arecord`/`parecord` 持续捕获麦克风音频,Silero VAD 自动检测语音段落边界,转录后自动发送给 AI 处理。AI 回复通过 TTS 播放,期间静音系统阻止回声。
77
+
78
+ ---
79
+
80
+ ## 简要架构图
81
+
82
+ ```
83
+ ┌─────────────────────────────────────────────────────────────────┐
84
+ │ VersperClaw 主进程 (Bun) │
85
+ │ │
86
+ │ ┌──────────────┐ ┌──────────────────┐ ┌───────────────┐ │
87
+ │ │ CLI TUI │ │ LLM Provider │ │ Bridge API │ │
88
+ │ │ (React Ink) │◄──►│ (Anthropic/NIM) │◄──►│ (SSE/HTTP) │ │
89
+ │ └──────┬───────┘ └────────┬─────────┘ └───────────────┘ │
90
+ │ │ │ │
91
+ │ ┌──────▼─────────────────────▼──────────────────────────────┐ │
92
+ │ │ FriendService (单例) │ │
93
+ │ │ ┌────────────┐ ┌───────────┐ ┌───────────┐ ┌───────┐ │ │
94
+ │ │ │ STT 连接器 │ │ TTS 生成器│ │ VAD 检测 │ │静音系统│ │ │
95
+ │ │ │(Groq/等) │ │(Edge/Qwen)│ │(Silero) │ │ │ │ │
96
+ │ │ └────────────┘ └───────────┘ └───────────┘ └───────┘ │ │
97
+ │ └───────────────────────┬────────────────────────────────────┘ │
98
+ │ │ │
99
+ │ ┌───────────────────────▼────────────────────────────────────┐ │
100
+ │ │ HTTP 服务器 (Bun.serve :3456) │ │
101
+ │ │ ┌─────────────┐ ┌──────────────┐ ┌──────────────────┐ │ │
102
+ │ │ │ SSE /events │ │ REST API │ │ 静态文件服务 │ │ │
103
+ │ │ │ │ │ /chat /voice │ │ /friend/* │ │ │
104
+ │ │ └─────────────┘ └──────────────┘ └──────────────────┘ │ │
105
+ │ └───────────────────────┬────────────────────────────────────┘ │
106
+ └──────────────────────────┼────────────────────────────────────────┘
107
+ │ SSE + HTTP
108
+
109
+ ┌──────────────────────────────────────────────────────────────────┐
110
+ │ Tauri 桌面窗口 (WebKitGTK) │
111
+ │ │
112
+ │ ┌──────────────────────────────────────────────────────────┐ │
113
+ │ │ App.tsx (React 应用) │ │
114
+ │ │ ┌──────────┐ ┌──────────┐ ┌────────┐ ┌──────────────┐ │ │
115
+ │ │ │ VRMScene │ │TextBubble│ │ChatInput│ │ MoodIndicator│ │ │
116
+ │ │ │ Three.js │ │SSE驱动 │ │PTT/通话│ │ Canvas 动画 │ │ │
117
+ │ │ └──────────┘ └──────────┘ └────────┘ └──────────────┘ │ │
118
+ │ └──────────────────────────────────────────────────────────┘ │
119
+ │ │
120
+ │ 核心 3D 子系统: │
121
+ │ ┌─────────────┐ ┌────────────────┐ ┌──────────┐ ┌──────────┐ │
122
+ │ │EmoteController│ MotionController │ LipSync │ TextBubble │ │
123
+ │ │blend shapes │ VRMA/VMD/FBX │ WebAudio │ 打字机效果 │ │
124
+ │ │13种情绪映射 │ 舞蹈系统 │ 唇形同步 │ Markdown │ │
125
+ │ └─────────────┘ └────────────────┘ └──────────┘ └──────────┘ │
126
+ └──────────────────────────────────────────────────────────────────┘
127
+ ```
128
+
129
+ ---
130
+
131
+ ## 文件位置
132
+
133
+ - 后端: `src/friend/` — FriendService, SSE, TTS, STT, VAD, prefs, server, launcher
134
+ - 前端: `src/components/friend/frontend/` — React app, Three.js 3D 场景
135
+ - API 路由: `src/server/api/friend.ts` — 所有 HTTP 端点
136
+ - LLM 工具: `src/tools/FriendEmotionTool.ts`, `src/tools/FriendScreenObserveTool.ts`
137
+ - 技能注入: `src/skills/bundled/friendPrompt.ts`
138
+ - 配置: `~/.config/VersperClaw/friend.json`
docs/friend/voice-vad.md ADDED
@@ -0,0 +1,525 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 语音捕获与 VAD 策略
2
+
3
+ 本文档详细描述 Friend 系统的音频捕获、语音活动检测 (VAD)、语音转文字 (STT) 和静音管理策略。
4
+
5
+ ---
6
+
7
+ ## 1. 音频捕获
8
+
9
+ ### 1.1 子进程架构
10
+
11
+ Friend 使用子进程方式进行音频捕获,而非原生 NAPI 绑定。这是因为 cpal 的同步 NAPI 调用在 ALSA 初始化卡顿时会阻塞事件循环,且无法从 JS 侧超时。
12
+
13
+ **文件**: `src/friend/FriendService.ts` — `loadAudioCapture()` 方法
14
+
15
+ ```typescript
16
+ private async loadAudioCapture(): Promise<AudioCaptureProvider> {
17
+ // 使用 arecord / parecord 子进程
18
+ // 尝试顺序: arecord (ALSA) → parecord (PulseAudio)
19
+ }
20
+ ```
21
+
22
+ ### 1.2 工具选择与参数
23
+
24
+ **arecord** (ALSA):
25
+ ```
26
+ -D default # 默认设备
27
+ -r 16000 # 采样率 16kHz
28
+ -f S16_LE # 16位有符号小端 PCM
29
+ -c 1 # 单声道
30
+ -t raw # 原始 PCM 格式
31
+ -q # 静默模式
32
+ ```
33
+
34
+ **parecord** (PulseAudio):
35
+ ```
36
+ --raw # 原始 PCM
37
+ --rate=16000 # 16kHz
38
+ --format=s16le # 16位有符号小端
39
+ --channels=1 # 单声道
40
+ --latency-msec=20 # 低延迟
41
+ ```
42
+
43
+ ### 1.3 验证机制 (500ms 窗口)
44
+
45
+ 子进程启动后有 500ms 验证窗口:
46
+
47
+ ```typescript
48
+ // 启动子进程后等待 500ms
49
+ // 若子进程在此时间内产生了音频数据 → 验证通过
50
+ // 若子进程退出且未产出数据 → 验证失败,尝试下一个工具
51
+ // 若 500ms 无数据 → 验证失败
52
+ ```
53
+
54
+ 这防止了 `parecord` 在 PulseAudio 不可用时静默失败的问题(进程启动但立即退出)。
55
+
56
+ ### 1.4 数据回调
57
+
58
+ ```typescript
59
+ const feedAudio = (chunk: Buffer) => {
60
+ // 1. 静音过滤: muted 时不转发(防止 TTS 回声)
61
+ if (this.muted) return;
62
+
63
+ // 2. 转发到 STT 连接
64
+ onData(chunk);
65
+
66
+ // 3. 转发到 VAD 检测
67
+ if (this.vadInstance) {
68
+ const float32 = new Float32Array(chunk.length / 2);
69
+ for (let i = 0; i < float32.length; i++) {
70
+ float32[i] = chunk.readInt16LE(i * 2) / 32768;
71
+ }
72
+ this.vadInstance.processAudio(float32).catch(() => {});
73
+ }
74
+ };
75
+ ```
76
+
77
+ ### 1.5 停止逻辑
78
+
79
+ ```typescript
80
+ stopRecording: async () => {
81
+ if (captureProc) {
82
+ captureProc.kill('SIGTERM');
83
+ // 2s 后强制 SIGKILL(防止僵进程)
84
+ setTimeout(() => {
85
+ try { captureProc?.kill('SIGKILL'); } catch {}
86
+ }, 2000);
87
+ captureProc = null;
88
+ }
89
+ },
90
+ ```
91
+
92
+ ---
93
+
94
+ ## 2. STT Provider 检测与降级
95
+
96
+ ### 2.1 自动检测链
97
+
98
+ **文件**: `src/friend/FriendService.ts` — `detectAvailableSttProvider()` 方法
99
+
100
+ ```typescript
101
+ detectAvailableSttProvider()
102
+
103
+ ├── 1. Groq Whisper (isGroqAvailable)
104
+ │ 最快 - REST API 调用,无需 Python
105
+ │ 检测: 检查 API key 是否存在
106
+
107
+ ├── 2. Local Whisper (checkLocalWhisperAvailable)
108
+ │ 本地运行,无需网络
109
+ │ 检测: 导入 connectLocalWhisperStream 检查
110
+
111
+ ├── 3. Anthropic Voice Stream (isVoiceStreamAvailable)
112
+ │ 通过 Anthropic API 的流式语音识别
113
+ │ 检测: 检查登录状态和 API key
114
+
115
+ ├── 4. Doubao ASR
116
+ │ 通过豆包 API
117
+ │ 检测: 检查 ~/.claude/tts/doubao/credentials.json
118
+
119
+ └── 全不可用 → 抛出错误
120
+ "No STT provider available. Install local Whisper: pip install openai-whisper"
121
+ ```
122
+
123
+ ### 2.2 STT Provider 特性对比
124
+
125
+ | Provider | 延迟 | 依赖 | 是否需要网络 | 支持语言 |
126
+ |----------|------|------|-------------|---------|
127
+ | **Groq Whisper** | 低 | API key | 是 | 多语言 |
128
+ | **Local Whisper** | 中 | Python + pip | 否 | 多语言 |
129
+ | **Anthropic Voice Stream** | 低 | Anthropic 登录 | 是 | 多语言 (keyterms支持) |
130
+ | **Doubao ASR** | 中 | 凭据文件 | 是 | 中文最佳 |
131
+
132
+ ### 2.3 STT 连接超时
133
+
134
+ 所有 provider 连接都有 8 秒超时:
135
+
136
+ ```typescript
137
+ startSttConnectionWithTimeout(provider, language)
138
+ ├── 超时 8s
139
+ └── 超时错误提示: "STT provider '{provider}' timed out after 8s."
140
+ └── provider === 'local' 时附带 pip 安装提示
141
+ ```
142
+
143
+ ### 2.4 连接工厂
144
+
145
+ ```typescript
146
+ startSttConnection(provider, language)
147
+
148
+ ├── anthropic: connectVoiceStream(callbacks, { language, keyterms })
149
+ │ keyterms: ['code', 'versperclaw'] 提高相关词汇识别率
150
+
151
+ ├── local: preloadWhisperModel + connectLocalWhisperStream
152
+ │ 需预加载模型(首次加载较慢)
153
+
154
+ ├── doubao: connectDoubaoStream(callbacks, { language })
155
+
156
+ └── groq: connectGroqStream(callbacks, { language })
157
+ 最快,纯 REST 流式调用
158
+ ```
159
+
160
+ ### 2.5 回调接口
161
+
162
+ ```typescript
163
+ const callbacks = {
164
+ onTranscript: (text: string, isFinal: boolean) => {
165
+ if (isFinal) {
166
+ this.captureTranscripts.push(text); // 最终文本入队列
167
+ this.captureInterimText = '';
168
+ } else {
169
+ this.captureInterimText = text; // 临时文本(前端轮询显示)
170
+ }
171
+ // 更新状态供前端轮询
172
+ this.setState({
173
+ captureStatus: { capturing: true, interimText: this.captureInterimText },
174
+ });
175
+ },
176
+ onError: (_error: string) => {},
177
+ onClose: () => {},
178
+ onReady: (_conn: any) => {},
179
+ };
180
+ ```
181
+
182
+ ---
183
+
184
+ ## 3. Silero VAD 架构
185
+
186
+ **文件**: `src/friend/voice/vad-service.ts` (~322 行)
187
+
188
+ ### 3.1 为什么选择 onnxruntime-web WASM
189
+
190
+ Bun 不支持 onnxruntime-node 原生插件(会触发 segfault),因此使用 onnxruntime-web 的 WASM 后端。WASM 二进制文件来自 `onnxruntime-web/dist`。
191
+
192
+ ### 3.2 模型规格
193
+
194
+ - **模型**: Silero VAD legacy ONNX (来自 `@ericedouard/vad-node-realtime`)
195
+ - **模型文件**: `silero_vad_legacy.onnx`
196
+ - **输入**: 512 采样帧 @ 16kHz (32ms)
197
+ - **输出**: 语音概率 (0-1)
198
+ - **LSTM 状态**: h=[2,1,64], c=[2,1,64]
199
+
200
+ ### 3.3 配置参数
201
+
202
+ ```typescript
203
+ this.opts = {
204
+ // 说话判定阈值
205
+ positiveSpeechThreshold: 0.75, // 超过此值判定为语音帧
206
+ negativeSpeechThreshold: 0.50, // 低于此值判定为静音帧
207
+
208
+ // 触发条件
209
+ preSpeechTriggerFrames: 10, // 需要连续 10 帧 (320ms) 确认说话
210
+ minSpeechFrames: 6, // 最少 6 帧 (192ms) 有效语音
211
+
212
+ // 静音消音
213
+ redemptionFrames: 20, // 连续 20 帧 (640ms) 静音结束段落
214
+
215
+ // 前置填充
216
+ preSpeechPadFrames: 10, // 段落开头包含 10 帧前置音频
217
+
218
+ // 能量过滤
219
+ rmsThreshold: 0.004, // RMS 能量阈值 (-48dBFS 噪声底限)
220
+
221
+ // 采样率
222
+ sampleRate: 16000, // 16kHz
223
+ };
224
+ ```
225
+
226
+ ### 3.4 RMS 能量预过滤
227
+
228
+ 在运行 ONNX 推理之前,先计算帧的 RMS 能量:
229
+
230
+ ```typescript
231
+ let sumSq = 0;
232
+ for (let i = 0; i < frame.length; i++) {
233
+ sumSq += frame[i] * frame[i];
234
+ }
235
+ const rms = Math.sqrt(sumSq / frame.length);
236
+
237
+ if (rms < this.opts.rmsThreshold) {
238
+ prob = 0; // 低于噪声底限 → 跳过 ONNX 推理
239
+ } else {
240
+ // 执行 ONNX 推理
241
+ prob = await this.session.run({ input, sr, h, c });
242
+ }
243
+ ```
244
+
245
+ 作用:
246
+ - 节省 CPU 资源(大量帧无语音信号)
247
+ - 过滤机械噪声/麦克风碰撞/环境静音
248
+ - 降低误触发率
249
+
250
+ ### 3.5 状态机详解
251
+
252
+ ```
253
+ ┌──────────────────────────────────────────────────┐
254
+ │ pre-speech phase │
255
+ │ preSpeechCount < preSpeechTriggerFrames (10) │
256
+ │ │
257
+ │ 语音帧 → preSpeechCount++ │
258
+ │ 非语音帧 → preSpeechCount = 0 │
259
+ │ │
260
+ │ 当 preSpeechCount >= 10: │
261
+ │ → speaking = true │
262
+ │ → speechFrameCount = preSpeechCount │
263
+ │ → onSpeechStart() │
264
+ └──────────────────────┬───────────────────────────┘
265
+
266
+
267
+ ┌──────────────────────────────────────────────────┐
268
+ │ speaking phase │
269
+ │ │
270
+ │ 语音帧 → redemptionCounter = 0 │
271
+ │ 静音帧 → redemptionCounter++ │
272
+ │ │
273
+ │ 当 redemptionCounter >= 20 (640ms): │
274
+ │ → endSpeech() │
275
+ │ → onSpeechEnd(audioSegment) │
276
+ └──────────────────────────────────────────────────┘
277
+ ```
278
+
279
+ ### 3.6 段落构建
280
+
281
+ 当 onSpeechEnd 触发时,构建包含前置填充的音频段:
282
+
283
+ ```typescript
284
+ private endSpeech(): void {
285
+ // 1. 检查最小语音帧数 (防止误触发)
286
+ if (this.speechFrameCount < this.opts.minSpeechFrames) {
287
+ this.callbacks.onVADMisfire(); // 误触发回调
288
+ return;
289
+ }
290
+
291
+ // 2. 构建音频段 (含前置填充)
292
+ const total = this.frameHistory.length;
293
+ const prePad = Math.min(this.opts.preSpeechPadFrames, total);
294
+ const segFrames = this.frameHistory.slice(
295
+ total - prePad - this.speechFrameCount,
296
+ total
297
+ );
298
+ // 合并所有帧为一个 Float32Array
299
+ const segment = new Float32Array(totalSamples);
300
+ for (const f of segFrames) { segment.set(f.frame, offset); offset += ... }
301
+
302
+ // 3. 回调
303
+ this.callbacks.onSpeechEnd(segment);
304
+ }
305
+ ```
306
+
307
+ ### 3.7 VAD 生命周期
308
+
309
+ ```typescript
310
+ class SileroVad {
311
+ async init() // 加载 ONNX 模型 (初始化)
312
+ start() // 激活 VAD 处理
313
+ pause() // 暂停 + 结束当前语音段
314
+ processAudio() // 处理 PCM 音频帧
315
+ flush() // 刷新剩余缓冲区 + 结束段
316
+ reset() // 重置全部状态 (保留 session)
317
+ destroy() // 清理资源
318
+ }
319
+ ```
320
+
321
+ ### 3.8 VAD 初始化失败的处理
322
+
323
+ VAD 初始化失败是非致命的:
324
+
325
+ ```typescript
326
+ vad.init()
327
+ .then(() => { this.vadInstance = vad; })
328
+ .catch((e) => {
329
+ console.warn('[FriendService] VAD init failed (non-fatal, voice capture falls back to F2-only):', e);
330
+ });
331
+ ```
332
+
333
+ 当 VAD 不可用时,F2 语音通话模式降级为手动分段(仍可通过 PTT 模式使用语音)。
334
+
335
+ ---
336
+
337
+ ## 4. 静音系统
338
+
339
+ ### 4.1 为什么需要静音
340
+
341
+ 当 AI 回复通过 TTS 播放时,扬声器声音会被麦克风捕获,如果不做静音处理会产生两种问题:
342
+ 1. **TTS 回声**: 自己的语音进入 STT 造成重复识别
343
+ 2. **打断 AI 回复**: 用户未说话但环境噪声导致 VAD 误触发
344
+
345
+ ### 4.2 静音策略
346
+
347
+ ```
348
+ startAiTurnMute() ──── 在语音片段提交时立即静音
349
+
350
+ ├── muted = true
351
+ ├── vadInstance.pause()
352
+ └── 30s 超时定时器 (安全性保障)
353
+
354
+
355
+ AI 处理 (工具调用、深度思考)
356
+
357
+
358
+ broadcastResponse() → generateTts()
359
+
360
+ ├── TTS 成功:
361
+ │ └── extendMuteForTts(audioId)
362
+ │ ├── 取消 30s 定时器
363
+ │ └── 设置精确的 TTS 播放时长定时器
364
+
365
+ ├── TTS 失败:
366
+ │ └── unmute() (立即解除静音)
367
+
368
+
369
+ TTS 播放完毕 → unmute()
370
+ ├── muted = false
371
+ ├── muteTimer = null
372
+ └── vadInstance.start() (恢复 VAD 监听)
373
+ ```
374
+
375
+ ### 4.3 定时器精确控制
376
+
377
+ - **初始静音**: 30s(覆盖几乎所有 AI 响应周期)
378
+ - **精确调整**: TTS 生成后通过 MP3 时长解析精确控制
379
+ - **安全性保障**: 任何情况下都不会永久静音
380
+
381
+ ### 4.4 清除静音 (紧急情况)
382
+
383
+ ```typescript
384
+ stopVoiceCapture() → _stopCapture()
385
+ ├── 停止音频捕获
386
+ ├── clearMute() (立即解除静音)
387
+ └── VAD reset()
388
+ ```
389
+
390
+ ---
391
+
392
+ ## 5. MP3 时长解析
393
+
394
+ **文件**: `src/friend/FriendService.ts` — `getMp3DurationMs()` 方法
395
+
396
+ ### 5.1 帧同步头扫描法
397
+
398
+ 不使用 bitrate 查找表(容易出错),而是通过实际帧间隔计算:
399
+
400
+ ```typescript
401
+ private getMp3DurationMs(audioId: string): number {
402
+ // 1. 获取音频文件路径
403
+ const filePath = getAudioFile(audioId);
404
+ const buf = readFileSync(filePath);
405
+
406
+ // 2. 跳过 ID3v2 标签 (如果存在)
407
+ if (buf[0..2] === 'ID3') {
408
+ offset = 10 + syncsafe_int(buf[6..9]);
409
+ }
410
+
411
+ // 3. 找到前两个帧同步字
412
+ // 帧同步: 0xFF 字节 + 0xE0 掩码
413
+ for (let i = offset; i < buf.length - 3; i++) {
414
+ if (isSync(i)) {
415
+ if (firstSync === -1) firstSync = i;
416
+ else { secondSync = i; break; }
417
+ }
418
+ }
419
+
420
+ // 4. 计算帧间隔 (CBR 模式)
421
+ const frameSize = secondSync - firstSync;
422
+
423
+ // 5. 解析帧头获取采样率
424
+ const h = read32BE(firstSync);
425
+ const version = (h >> 19) & 0x3;
426
+ const sampleRateIdx = (h >> 10) & 0x3;
427
+ // MPEG1 → 1152 samples/frame, MPEG2/2.5 → 576
428
+
429
+ // 6. 按步长计数帧数 (帧损坏时扫描到下一个同步字)
430
+ for (let pos = firstSync; pos + 3 < buf.length; pos += frameSize) {
431
+ if (!isSync(pos)) {
432
+ // 损坏帧: 扫描到下一个同步字
433
+ while (pos < buf.length - 3 && !isSync(pos)) pos++;
434
+ }
435
+ frames++;
436
+ }
437
+
438
+ // 7. 计算时长
439
+ return Math.round((frames * spf) / sampleRate * 1000);
440
+ }
441
+ ```
442
+
443
+ ### 5.2 为什么自实现 MP3 解析
444
+
445
+ - Edge TTS 输出 CBR MP3
446
+ - 不依赖外部库(减少依赖项)
447
+ - 帧同步头扫描法对 CBR 精确且鲁棒
448
+ - 处理文件损坏的帧 (向前扫描下一个同步字)
449
+
450
+ ---
451
+
452
+ ## 6. 前端语音捕获 (useServerStt hook)
453
+
454
+ **文件**: `src/components/friend/frontend/hooks/useServerStt.ts`
455
+
456
+ ### 6.1 设计原因
457
+
458
+ Tauri 使用 WebKitGTK,不兼容 `onnxruntime-web` WASM 后端,因此浏览器 VAD (`@ricky0123/vad-web`) 不可用。前端将语音捕获完全委托给后端。
459
+
460
+ ### 6.2 两种模式
461
+
462
+ **Push-to-Talk (PTT)**:
463
+ ```typescript
464
+ startPushToTalk() → POST /voice/start // 后端开始捕获
465
+ stopPushToTalk() → POST /voice/stop // 后端停止并返回转录
466
+ ```
467
+
468
+ **Voice Call (F2 模式)**:
469
+ ```typescript
470
+ startStreaming(onTranscript, onError) → POST /voice/start
471
+
472
+ ├── 后端持续捕获麦克风音频
473
+ ├── 后端自动分段 (VAD / 定时 5s) 并转录
474
+ ├── 前端每 1s 轮询 POST /voice/status 获取 interimText
475
+ └── 显示在输入栏中
476
+
477
+ stopStreaming() → POST /voice/stop
478
+ └── 结束后端捕获,返回完整转录
479
+ ```
480
+
481
+ ### 6.3 前端语音通���交互
482
+
483
+ ```typescript
484
+ // TTS 中断: 用户说话时延迟 1s 中断当前 TTS
485
+ const scheduleInterrupt = useCallback(() => {
486
+ setTimeout(() => {
487
+ ;(window as any).__clawInterruptAudio?.()
488
+ }, 1000)
489
+ }, [])
490
+
491
+ // 显示控制: 转录文字显示 3s 后自动清除
492
+ setTimeout(() => {
493
+ if (voiceCallActiveRef.current) {
494
+ setText('')
495
+ }
496
+ }, 3000)
497
+ ```
498
+
499
+ ---
500
+
501
+ ## 7. 浏览器 VAD 片段转录
502
+
503
+ 前端浏览器 VAD(非 WebKitGTK 环境)检测到语音段落后,通过 HTTP 发送到后端:
504
+
505
+ ```typescript
506
+ // POST /plugins/friend/voice/stt-segment
507
+ // Body: raw PCM/WAV buffer
508
+ handleFriendApi → friendService.transcribeAudioSegment(audioBuffer)
509
+
510
+ ├── 检测 STT provider (自动降级)
511
+ ├── 创建临时 STT 连接
512
+ ├── 发送音频 → 等待 finalize
513
+ └── 发送转录文本到 AI 对话
514
+ ```
515
+
516
+ ---
517
+
518
+ ## 总结: 语音路径选择
519
+
520
+ | 使用场景 | VAD | STT Provider | 音频来源 | 静音需求 |
521
+ |---------|-----|------------|---------|---------|
522
+ | Push-to-Talk (PTT) | 否 (人工分段) | 自动检测 | arecord/parecord | 否 |
523
+ | F2 语音通话 | Silero VAD | 自动检测 → 分段 flush | arecord/parecord | 是 |
524
+ | 浏览器 VAD (非 Tauri) | 浏览器 VAD | STT segment API | getUserMedia | 否 |
525
+ | 文字输入 | 不适用 | 不适用 | 键盘 | 不适用 |
docs/ink-ui/overview.md ADDED
@@ -0,0 +1,585 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Ink 终端 UI 框架
2
+
3
+ ## 概述
4
+
5
+ VersperClaw 的 Ink 是基于 [vadimdemedes/ink](https://github.com/vadimdemedes/ink) v6 的深度定制分叉(fork)。Ink 使用 React 组件模型渲染终端用户界面。它将 React 的虚拟 DOM 映射到终端字符网格,支持 Yoga Flexbox 布局、样式继承、事件处理、文本选择、搜索高亮等丰富的交互能力。
6
+
7
+ > 与上游相比,VersperClaw 的 Ink 新增/修改了以下核心能力:自定义 DOM 节点树、压缩屏幕缓冲区(Int32Array)、基于 Yoga WASM 的布局引擎、Kitty 键盘协议支持、SGR 鼠标跟踪、文本选择模式、搜索高亮、双向文本处理、scrollable 容器、声明式光标位置等。
8
+
9
+ ---
10
+
11
+ ## 核心架构
12
+
13
+ Ink 的渲染管线分为以下几个层次:
14
+
15
+ ```
16
+ React 组件树
17
+
18
+
19
+ React Reconciler (自定义 Fiber 协调器)
20
+
21
+
22
+ DOM 节点树 (ink-root, ink-box, ink-text, ...)
23
+
24
+
25
+ Yoga 布局引擎 (WASM Flexbox)
26
+
27
+
28
+ Renderer → Output (screen buffer)
29
+
30
+
31
+ LogUpdate (差异比较)
32
+
33
+
34
+ Patch[] → ANSI 序列 → stdout
35
+ ```
36
+
37
+ ### Ink 主类
38
+
39
+ 文件:`src/ink/ink.tsx`
40
+
41
+ `Ink` 类(约 1723 行)管理整个渲染生命周期:
42
+
43
+ - **构造函数**:设置 `LogUpdate`、节流渲染调度器(`scheduleRender`,使用 `FRAME_INTERVAL_MS=16ms`)、`FocusManager`、Yoga `onComputeLayout` 回调、React reconciler 容器
44
+ - **`onRender()`**:核心帧循环,每个渲染周期执行以下步骤:
45
+ 1. 调用 `createRenderer` 执行 DOM → Yoga 布局 → 屏幕缓冲区的渲染
46
+ 2. 处理选择滚动追踪
47
+ 3. 应用选择/搜索覆盖层
48
+ 4. 全损坏回退(full-damage backstop)
49
+ 5. 计算差异(diff)
50
+ 6. 缓冲池交换
51
+ 7. 补丁优化与序列化
52
+ 8. 光标定位(原生光标或声明式光标)
53
+ 9. 写入 stdout
54
+ - **选择管理**:`startSelection`, `extendSelection`, `wordSelect`, `lineSelect`, `dragScrollCapture`, `shiftSelection`
55
+ - **stdin 挂起/恢复**:用于外部编辑器集成
56
+ - **控制台/stderr 补丁**:拦截 console.log 等输出重定向
57
+ - **drainStdin**:清理时清空 stdin 缓冲区
58
+
59
+ ### React Reconciler(协调器)
60
+
61
+ 文件:`src/ink/reconciler.ts`
62
+
63
+ 使用 `react-reconciler` 包创建自定义 Fiber 协调器,将 React 组件映射到 Ink 的 DOM 节点。
64
+
65
+ **关键宿主配置**:
66
+
67
+ | 钩子 | 说明 |
68
+ |------|------|
69
+ | `createInstance` | 创建 `DOMElement`(`ink-box`, `ink-text` 等),在 `<Text>` 内部嵌套时将 `ink-text` 转换为 `ink-virtual-text` |
70
+ | `createTextInstance` | 创建 `TextNode`,要求必须在 `<Text>` 内使用 |
71
+ | `commitUpdate` | React 19 使用新旧 props 直接比较,调用 `setStyle`, `setAttribute`, `setTextStyles`, `setEventHandler` |
72
+ | `commitTextUpdate` | 更新文本节点 |
73
+ | `appendChild` / `insertBefore` / `removeChild` | DOM 树操作,同步更新 Yoga 节点树 |
74
+ | `resetAfterCommit` | 提交后触发 `onComputeLayout`(Yoga 布局)和 `onRender`(渲染帧) |
75
+ | `hideInstance` / `unhideInstance` | 设置/取消 Yoga `display: none` |
76
+
77
+ **调试工具**:
78
+ - `getOwnerChain()`:从 React Fiber `_debugOwner` 提取组件名链,用于识别重绘来源
79
+ - `CLAUDE_CODE_DEBUG_REPAINTS` 环境变量:启用后记录组件树归属
80
+ - `CLAUDE_CODE_COMMIT_LOG`:记录提交/渲染性能日志
81
+
82
+ ### DOM 节点树
83
+
84
+ 文件:`src/ink/dom.ts`
85
+
86
+ 自定义 DOM 树,与浏览器 DOM 不同,专为终端渲染优化。
87
+
88
+ **节点类型**:
89
+
90
+ | 节点名 | 说明 |
91
+ |--------|------|
92
+ | `ink-root` | 根节点,持有 `FocusManager` |
93
+ | `ink-box` | 容器(类似 `<div>`),有 Yoga 节点 |
94
+ | `ink-text` | 文本容器,有测量函数 |
95
+ | `ink-virtual-text` | `<Text>` 内嵌套的文本,无 Yoga 节点 |
96
+ | `ink-link` | 超链接,无 Yoga 节点 |
97
+ | `ink-progress` | 进度条,无 Yoga 节点 |
98
+ | `ink-raw-ansi` | 预渲染 ANSI 字符串,固定尺寸测量 |
99
+ | `#text` | 文本叶子节点 |
100
+
101
+ **DOM 操作**:
102
+ - `createNode()` / `createTextNode()` —— 创建节点
103
+ - `appendChildNode()` / `insertBeforeNode()` / `removeChildNode()` —— DOM 树操作,同步更新 Yoga 节点
104
+ - `setStyle()` / `setAttribute()` / `setTextStyles()` —— 属性更新,含脏检测(shallow equal 跳过未变更的渲染)
105
+ - `markDirty()` —— 标记节点及其所有祖先为脏
106
+ - `scheduleRenderFrom()` —— 从指定节点触发渲染(用于非 React 触发的 DOM 变更)
107
+
108
+ **脏标记(Dirty Flag)**:每个 `DOMElement` 有 `dirty` 属性(boolean),`markDirty()` 从当前节点向上遍历直到根节点。渲染完成后清除。这允许增量重绘 —— 只有脏子树被重新布局和渲染。
109
+
110
+ ### Screen(屏幕缓冲区)
111
+
112
+ 文件:`src/ink/screen.ts`
113
+
114
+ Screen 是终端渲染的核心数据结构,使用**压缩的 Int32Array** 存储每个单元格,避免为每个单元格分配对象(200x120 屏幕可避免分配 24000 个对象)。
115
+
116
+ **单元格存储布局**:
117
+ 每个单元���占用 2 个 Int32(共 8 字节):
118
+ - `word0`:字符 ID(索引到 `CharPool`,32 位)
119
+ - `word1`:`styleId[31:17] | hyperlinkId[16:2] | width[1:0]`
120
+
121
+ **池化系统**:
122
+
123
+ | 池 | 说明 |
124
+ |----|------|
125
+ | `CharPool` | 字符字符串池,共享跨所有屏幕。ASCII 字符使用 `Int32Array` 快速查找;非 ASCII 使用 `Map`。索引 0 固定为空格,索引 1 为 spacer 空字符串。 |
126
+ | `StylePool` | ANSI 样式池,位 0 标识样式是否在空格上可见(背景色、反色、下划线等)。提供 `transition(fromId, toId)` 方法生成缓存的 ANSI 过渡字符串。`withInverse()`、`withCurrentMatch()`、`withSelectionBg()` 用于选择/搜索覆盖层。 |
127
+ | `HyperlinkPool` | OSC 8 超链接池,索引 0 表示无链接。 |
128
+
129
+ **单元格宽度枚举(`CellWidth`)**:
130
+ | 值 | 名称 | 说明 |
131
+ |----|------|------|
132
+ | 0 | Narrow | 单宽字符 |
133
+ | 1 | Wide | 宽字符(CJK、emoji),占用两列 |
134
+ | 2 | SpacerTail | 宽字符的第二列占位符 |
135
+ | 3 | SpacerHead | 软换行时在行尾标记宽字符延续 |
136
+
137
+ **关键函数**:
138
+ - `createScreen()` —— 创建屏幕,8 字节/单元格的 ArrayBuffer
139
+ - `resetScreen()` —— 重用屏幕,使用 `BigInt64Array.fill()` 快速清空
140
+ - `setCellAt()` —— 写入单元格,自动创建宽字符的 SpacerTail
141
+ - `setCellStyleId()` —— 仅更新样式 ID,用于选择/搜索覆盖层
142
+ - `cellAt()` / `cellAtIndex()` —— 读取单元格
143
+ - `diff()` / `diffEach()` —— 比较两个屏幕,使用 `findNextDiff()` 快速跳过相同区域
144
+ - `blitRegion()` —— 批量复制矩形区域(`TypedArray.set()`)
145
+ - `clearRegion()` —— 快速清空矩形区域
146
+ - `shiftRows()` —— 行位移(`copyWithin`),用于 DECSTBM 滚动优化
147
+ - `markNoSelectRegion()` —— 标记选择排除区域
148
+ - `migrateScreenPools()` —— 跨代池重置时重新 intern
149
+
150
+ ### Output(输出缓冲)
151
+
152
+ 文件:`src/ink/output.ts`
153
+
154
+ 收集渲染操作(Operations),在 `get()` 中统一应用到 Screen 缓冲区。
155
+
156
+ **操作类型**:
157
+ | 类型 | 说明 |
158
+ |------|------|
159
+ | `write` | 写入 ANSI 文本到指定位置 |
160
+ | `blit` | 复制源屏幕的矩形区域 |
161
+ | `clear` | 清空矩形区域 |
162
+ | `clip` / `unclip` | 裁剪区域(嵌套 `overflow:hidden`) |
163
+ | `shift` | 行位移 |
164
+ | `noSelect` | 标记选择排除区域(最后应用) |
165
+
166
+ **字符缓存**(`charCache`):
167
+ 每行文本经过 tokenize(ANSI 解析)、grapheme 聚类、双向文本重排、样式 ID + 超链接预计算后缓存。大多数行在帧间不变,缓存命中后只需读取属性并调用 `setCellAt()`。
168
+
169
+ **`writeLineToScreen()`**:
170
+ 核心写入函数,处理:
171
+ - C0 控制字符(tab → 空格展开,ESC → CSI/single-char 序列跳过)
172
+ - 零宽字符(组合标记等)静默跳过
173
+ - 宽字符在行尾时放置 SpacerHead
174
+ - 写时损坏区域追踪(`screen.damage`)
175
+
176
+ ### LogUpdate(差异比较与补丁生成)
177
+
178
+ 文件:`src/ink/log-update.ts`
179
+
180
+ 比较前一帧和当前帧的 Screen 缓冲区,生成最小补丁序列(`Diff = Patch[]`)。
181
+
182
+ **补丁类型**:
183
+ | 类型 | 说明 |
184
+ |------|------|
185
+ | `stdout` | 原始 ANSI 字符串输出 |
186
+ | `clear` | 清空 N 行 |
187
+ | `clearTerminal` | 完全清屏(全损坏,引发闪烁) |
188
+ | `cursorHide` / `cursorShow` | 光标显隐 |
189
+ | `cursorMove` | 光标相对移动 |
190
+ | `cursorTo` | 光标绝对定位到列 |
191
+ | `carriageReturn` | CR |
192
+ | `hyperlink` | OSC 8 超链接 |
193
+ | `styleStr` | 预序列化的 ANSI 样式过渡字符串 |
194
+
195
+ **DECSTBM 滚动优化**:
196
+ 当 ScrollBox 的 `scrollTop` 变化时,使用硬件滚动(CSI `top;bottom r` + CSI `n S/T`)替代重写整个滚动区域,大幅减少输出字节。
197
+
198
+ **全损坏重置**(`fullResetSequence_CAUSES_FLICKER`):
199
+ 在以下情况触发终端完全清屏:
200
+ - 终端尺寸变化(resize)
201
+ - 内容高度超出视口后又缩小到视口内
202
+ - 需要更新的行在滚动缓冲区中(不可达)
203
+ - 这些重置会引发可见的闪烁
204
+
205
+ **VirtualScreen**:
206
+ 内部辅助类,追踪虚拟光标位置和差异补丁列表,支持事务性操作(`txn()`)批量提交补丁。
207
+
208
+ ### Frame(帧结构)
209
+
210
+ 文件:`src/ink/frame.ts`
211
+
212
+ ```typescript
213
+ type Frame = {
214
+ screen: Screen // 屏幕缓冲区
215
+ viewport: Size // 视口尺寸
216
+ cursor: Cursor // 光标状态
217
+ scrollHint?: ScrollHint // DECSTBM 滚动提示
218
+ scrollDrainPending?: boolean // 是否需要继续帧
219
+ }
220
+ ```
221
+
222
+ 辅助函数:
223
+ - `shouldClearScreen()` —— 判断是否需要清屏(resize 或 offscreen)
224
+ - `emptyFrame()` —— 创建空帧
225
+
226
+ ---
227
+
228
+ ## 布局引擎(Yoga)
229
+
230
+ 文件:`src/ink/layout/` 目录
231
+
232
+ 使用 [Yoga](https://yogalayout.com/) 的 WASM 实现进行 Flexbox 布局计算。
233
+
234
+ **在渲染管线中的位置**:
235
+ 1. React commit 后,`resetAfterCommit` 调用 `rootNode.onComputeLayout()`
236
+ 2. `ink.tsx` 中的 `onComputeLayout` 调用 `root.yogaNode.calculateLayout()`
237
+ 3. Yoga 执行完整的 Flexbox 布局计��(含 `measureFunc` 回调用于文本测量)
238
+ 4. Renderer 读取 `getComputedWidth/Height/Top/Left` 获取布局结果
239
+
240
+ **布局节点**:文件 `src/ink/layout/node.ts`,定义 `LayoutNode` 接口和所有布局常量(`LayoutEdge`, `LayoutDisplay`, `LayoutFlexDirection`, `LayoutJustify`, `LayoutAlign`, `LayoutOverflow`, `LayoutPositionType`, `LayoutWrap`, `LayoutGutter`)。
241
+
242
+ **布局引擎**:文件 `src/ink/layout/engine.ts`,创建和管理 Yoga WASM 实例。提供 `createLayoutNode()` 和 `getYogaCounters()` 等函数。
243
+
244
+ **测量函数**:
245
+ - `ink-text`:`measureTextNode()` —— 处理文本换行、tab 展开、bi-di 重排
246
+ - `ink-raw-ansi`:`measureRawAnsiNode()` —— 使用预设的 rawWidth/rawHeight
247
+
248
+ ---
249
+
250
+ ## 样式系统
251
+
252
+ 文件:`src/ink/styles.ts`
253
+
254
+ 样式类型定义(`Styles`)涵盖所有 CSS Flexbox 属性:
255
+
256
+ **布局**:`display`, `position`, `overflow`, `flexDirection`, `flexWrap`, `flexGrow`, `flexShrink`, `flexBasis`, `alignItems`, `alignSelf`, `justifyContent`
257
+
258
+ **尺寸**:`width`, `height`, `minWidth`, `maxWidth`, `minHeight`, `maxHeight`(支持数值和百分比字符串)
259
+
260
+ **间距**:`margin`, `padding`, `gap`(含 X/Y 方向缩写)
261
+
262
+ **边框**:`borderStyle`(圆角/单线/双线等), `borderColor`, `borderTop/Right/Bottom/Left`(控制各边显隐)
263
+
264
+ **文本**:`textWrap`(8 种模式:`wrap`, `wrap-trim`, `end`, `middle`, `truncate-end`, `truncate`, `truncate-middle`, `truncate-start`)
265
+
266
+ **其他**:`backgroundColor`, `opaque`, `noSelect`(选择排除), `borderText`(边框内文本)
267
+
268
+ `applyStyles()` 函数将 `Styles` 对象映射到 Yoga 节点属性。每个样式类别有独立的函数(`applyPositionStyles`, `applyOverflowStyles`, `applyFlexStyles` 等)。
269
+
270
+ ### TextStyles
271
+
272
+ 文件:`src/ink/styles.ts`
273
+
274
+ ```typescript
275
+ type TextStyles = {
276
+ color?, backgroundColor? // 颜色
277
+ dim?, bold?, italic? // 字体样式
278
+ underline?, strikethrough? // 装饰
279
+ inverse? // 反色
280
+ }
281
+ ```
282
+
283
+ `TextStyles` 在 `<Text>` 组件中声明,通过 `ink-text` 节点的 `textStyles` 属性传递。渲染时由 `render-node-to-output.ts` 的 `buildTextStyles()` 转换为 ANSI SGR 序列。
284
+
285
+ ---
286
+
287
+ ## 事件系统
288
+
289
+ ### 键盘事件
290
+
291
+ **Kitty 键盘协议**:在 raw mode 中启用 CSI u 扩展键编码(`ENABLE_KITTY_KEYBOARD` + `ENABLE_MODIFY_OTHER_KEYS`),使 Ctrl+Shift+字母 与 Ctrl+字母 可区分。终端识别通过在 XTVERSION 查询后设置。
292
+
293
+ **输入解析**:文件 `src/ink/parse-keypress.ts`,状态机解析 stdin 的原始字节流。支持:
294
+ - 普通字符(UTF-8)
295
+ - CSI 序列(方向键、功能键等)
296
+ - Kitty CSI u 序列(带修饰键编码)
297
+ - SGR 鼠标编码(`<row;col;button M/m`)
298
+ - DEC 私有模式响应(DCS 序列)
299
+ - OSC 序列(操作系统命令)
300
+ - 粘贴模式(`IN_PASTE` 状态)
301
+
302
+ **`App` 组件中的处理**(文件 `src/ink/components/App.tsx`):
303
+ - `handleReadable()` —— stdin `readable` 事件回调
304
+ - `processInput()` —— 调用 `parseMultipleKeypresses()` 解析多按键
305
+ - `processKeysInBatch()` —— 在 `reconciler.discreteUpdates` 中批量处理按键,避免"最大更新深度超出"错误
306
+ - 处理 `Ctrl+C`(退出)、`Ctrl+Z`(挂起)、终端焦点事件(DECSET 1004)
307
+
308
+ ### 鼠标事件
309
+
310
+ 通过 DECSET 1003(SGR 鼠标跟踪)启用/禁用。
311
+
312
+ **`processKeysInBatch` 中的鼠标处理**(`handleMouseEvent()`):
313
+ - **press**:开始选择、多击检测(双击选中单词、三击选中行)
314
+ - **drag**:扩展选择(字符/单词/行模式)
315
+ - **release**:结束选择
316
+ - **no-button motion**(模式 1003):悬停事件分发(`onMouseEnter`/`onMouseLeave`)
317
+ - **超链接**:单点击时延迟打开,双击取消
318
+
319
+ **`handleMouseEvent()` 中的选择逻辑**:
320
+ - `startSelection()` / `finishSelection()` / `hasSelection()`
321
+ - 多击跟踪:`clickCount` 在 500ms 内递增,支持双击单词选择和三击行选择
322
+ - `lastPressHadAlt`:记录 Alt 修饰键,用于终端判定
323
+
324
+ ### 焦点管理
325
+
326
+ 文件:`src/ink/focus.ts`
327
+
328
+ `FocusManager` 存储在每个根节点,类似于浏览器 `document.activeElement`。
329
+
330
+ **功能**:
331
+ - `focus(node)` / `blur()` —— 获取/失去焦点
332
+ - `handleNodeRemoved()` —— 节点移除时自动恢复焦点到栈中上一个
333
+ - `handleAutoFocus()` —— 自动聚焦(`autoFocus` prop)
334
+ - `handleClickFocus()` —— 点击聚焦(`tabIndex` prop)
335
+ - `focusNext()` / `focusPrevious()` —— Tab / Shift+Tab 导航
336
+
337
+ **焦点栈**:最多 32 层,用于节点移除后的焦点恢复。
338
+
339
+ **事件分发**:事件系统使用 `Dispatcher` 类(`src/ink/events/dispatcher.ts`)处理捕获(Capture)和冒泡(Bubble)阶段。`EVENT_HANDLER_PROPS` 集合定义哪些 props 是事件处理器(`onClick`, `onKeyDown` 等)。
340
+
341
+ ---
342
+
343
+ ## 组件库
344
+
345
+ 文件:`src/ink/components/`
346
+
347
+ | 组件 | 文件 | 说明 |
348
+ |------|------|------|
349
+ | `App` | `App.tsx` | 根组件,管理 stdin/stdout、raw mode、光标、挂起/恢复、事件分发、多击选择 |
350
+ | `Box` | `Box.tsx` | 布局容器(类似 `<div>`),支持完整 Flexbox 样式和事件处理器(onClick, onKeyDown, onMouseEnter, onMouseLeave, onFocus, onBlur) |
351
+ | `Text` | `Text.tsx` | 文本组件,支持颜色(color, backgroundColor)、加粗(bold)、斜体(italic)、下划线(underline)、删除线(strikethrough)、反色(inverse)、换行模式(wrap) |
352
+ | `Button` | `Button.tsx` | 按钮组件(继承 Box 的点击能力) |
353
+ | `Link` | `Link.tsx` | 超链接(使用 OSC 8 协议),终端不支持时回退到纯文本 |
354
+ | `Spacer` | `Spacer.tsx` | 弹性空间填充(`flexGrow: 1`) |
355
+ | `Newline` | `Newline.tsx` | 插入空白行 |
356
+ | `ScrollBox` | `ScrollBox.tsx` | 滚动容器(`overflow: scroll`),支持 `scrollTo()`, `scrollToElement()` 和粘性滚动 |
357
+ | `AlternateScreen` | `AlternateScreen.tsx` | 替代屏幕(alt-screen)管理 |
358
+ | `RawAnsi` | `RawAnsi.tsx` | 原始 ANSI 字符串渲染(预计算的宽度和高度) |
359
+ | `NoSelect` | `NoSelect.tsx` | 选择排除区域(如行号、diff 标记) |
360
+ | `Ansi` | (在 ink.ts 中 re-export) | ANSI 文本渲染 |
361
+ | `ErrorOverview` | `ErrorOverview.tsx` | 错误概览组件 |
362
+
363
+ **Context 提供者**:
364
+
365
+ | Context | 文件 | 提供内容 |
366
+ |---------|------|----------|
367
+ | `AppContext` | `AppContext.ts` | `exit()` 退出函数 |
368
+ | `StdinContext` | `StdinContext.ts` | `stdin`, `setRawMode`, `internal_eventEmitter`, `internal_querier` |
369
+ | `TerminalSizeContext` | `TerminalSizeContext.tsx` | `columns`, `rows` 终端尺寸 |
370
+ | `TerminalFocusContext` | `TerminalFocusContext.tsx` | `isTerminalFocused` 终端焦点状态 |
371
+ | `ClockContext` | `ClockContext.tsx` | 时钟/定时器(根据焦点状态调整间隔) |
372
+ | `CursorDeclarationContext` | `CursorDeclarationContext.ts` | 声明式光标位置 |
373
+
374
+ ---
375
+
376
+ ## Hooks
377
+
378
+ 文件:`src/ink/hooks/`
379
+
380
+ | Hook | 文件 | 说明 |
381
+ |------|------|------|
382
+ | `useInput` | `use-input.ts` | 处理用户键盘输入(`useLayoutEffect` 同步启用 raw mode),注册 `input` 事件监听器 |
383
+ | `useStdin` | `use-stdin.ts` | 访问 stdin/setRawMode/EventEmitter |
384
+ | `useApp` | `use-app.ts` | 访问 `exit()` 函数 |
385
+ | `useAnimationFrame` | `use-animation-frame.ts` | 动画帧钩子(使用 `FRAME_INTERVAL_MS=16ms` 节流渲染) |
386
+ | `useInterval` | `use-interval.ts` | 定时器(终端失焦时可选减速) |
387
+ | `useSelection` | `use-selection.ts` | 文本选择状态管理(anchor, focus, word/line 模式) |
388
+ | `useSearchHighlight` | `use-search-highlight.ts` | 搜索高亮状态(匹配位置、当前匹配导航) |
389
+ | `useDeclaredCursor` | `use-declared-cursor.ts` | 声明式光标位置(用于 IME 输入和屏幕阅读器) |
390
+ | `useTabStatus` | `use-tab-status.ts` | Tab 状态管理 |
391
+ | `useTerminalFocus` | `use-terminal-focus.ts` | 终端焦点变化监听 |
392
+ | `useTerminalTitle` | `use-terminal-title.ts` | 终端标题设置 |
393
+ | `useTerminalViewport` | `use-terminal-viewport.ts` | 终端视口信息 |
394
+
395
+ ---
396
+
397
+ ## 文本选择
398
+
399
+ 文件:`src/ink/selection.ts`
400
+
401
+ `SelectionState` 管理全屏模式下的文本选择:
402
+
403
+ - **anchor**:选择起点(鼠标按下处)
404
+ - **focus**:选择终点(鼠标拖动处)
405
+ - **anchorSpan**:多击(双击/三击)时初始单词/行的范围
406
+ - **scrolledOffAbove/Below**:选择过程中滚动出视口的文本
407
+ - **softWrap 标记**:跟踪软换行位置
408
+
409
+ **选择操作**:
410
+ - `startSelection()` / `updateSelection()` / `finishSelection()`
411
+ - `extendSelection()` —— 字符/单词/行模式扩展
412
+ - `getSelectedText()` —— 获取选中文本(拼接跨行的选择,含滚动出视口的文本)
413
+ - `applySelectionOverlay()` —— 在屏幕覆盖层中高亮选中区域
414
+
415
+ ---
416
+
417
+ ## 搜索高亮
418
+
419
+ 文件:`src/ink/render-to-screen.ts` 和 `src/ink/components/App.tsx`
420
+
421
+ 搜索高亮系统分为两个层次:
422
+
423
+ 1. **`renderToScreen()`** —— 在独立屏幕缓冲区中渲染单个消息,扫描查询字符串的位置
424
+ 2. **`applySearchHighlight()`** / **`applyPositionedHighlight()`** —— 将搜索匹配覆盖层应用到实际屏幕
425
+
426
+ **匹配视觉风格**:
427
+ - 普通匹配:反色(SGR 7,白色底,从主题继承)
428
+ - 当前匹配(用户导航到的位置):黄色背景(通过黄色前景 + 反色实现)+ 加粗 + 下划线
429
+
430
+ ---
431
+
432
+ ## 渲染管线完整流程
433
+
434
+ 从 React 组件到终端输出的完整流程:
435
+
436
+ ```
437
+ 1. React 更新 (setState / 新 props)
438
+
439
+ 2. Reconciler commit (resetAfterCommit)
440
+ ├── onComputeLayout: Yoga calculateLayout()
441
+ └── onRender: scheduleRender (节流, 16ms)
442
+
443
+ 3. createRenderer → renderNodeToOutput
444
+ ├── 递归遍历 DOM 树
445
+ ├── 应用 scrollTop 偏移
446
+ ├── 写操作 -> Output buffer
447
+ └── blit 操作 -> Output buffer
448
+
449
+ 4. Output.get()
450
+ ├── 应用所有操作到 Screen
451
+ ├── 处理裁剪区域
452
+ ├── 处理 absolute 清除
453
+ └── 更新 softWrap 标记
454
+
455
+ 5. LogUpdate.render()
456
+ ├── 检查是否要全损坏重置
457
+ ├── DECSTBM 滚动优化
458
+ ├── diffEach 比较前后帧
459
+ ├── 处理增长/收缩
460
+ └── 生成 Patch[]
461
+
462
+ 6. optimize() — 补丁合并与去重
463
+
464
+ 7. writeDiffToTerminal() — 补丁序列化为 ANSI → stdout
465
+
466
+ 8. 屏幕选择/搜索覆盖层更新
467
+
468
+ 9. 光标定位
469
+ └── 下一帧
470
+ ```
471
+
472
+ ---
473
+
474
+ ## 特殊功能
475
+
476
+ ### 双缓冲(Double Buffering)
477
+
478
+ 使用前后帧(`frontFrame` / `backFrame`)实现双缓冲:
479
+ - `backFrame.screen` 是当前渲染目标
480
+ - `frontFrame.screen` 是前一帧的屏幕,用于差异比较和 blit
481
+ - 渲染帧切换后进行缓冲池交换(`swapFrames()`)
482
+
483
+ ### 屏幕伤害追踪(Damage Tracking)
484
+
485
+ 每个 `Screen` 有一个 `damage` 矩形区域,记录自上次重置以来发生写操作的区域。`diffEach()` 只扫描损伤区域,避免遍历整个屏幕。
486
+
487
+ ### 双向文本(BiDi)
488
+
489
+ 文件:`src/ink/bidi.ts`
490
+
491
+ 使用 Unicode Bidirectional Algorithm 对文本行进行重排。在 `writeLineToScreen()` 中,每行文本在 tokenize 和 grapheme 聚类后执行 `reorderBidi()`。
492
+
493
+ ### ANSI 解析与 Grapheme 聚类
494
+
495
+ 使用 `@alcalzone/ansi-tokenize` 解析 ANSI 序列,然后通过 `Intl.Segmenter` 进行 grapheme 聚类(支持家族 emoji 等多码点字符)。结果缓存到 `charCache`,帧间重用。
496
+
497
+ ### 终端查询(Terminal Querier)
498
+
499
+ 文件:`src/ink/terminal-querier.ts`
500
+
501
+ `TerminalQuerier` 通过 DEC 请求序列(如 DA1、XTVERSION、DECRPM)与终端通信。它维护一个待处理请求的 Promise 队列,在收到终端响应时解析相应的 Promise。
502
+
503
+ ### DEC 私有模式
504
+
505
+ 文件:`src/ink/termio/dec.ts`
506
+
507
+ 管理 DEC 私有模式:
508
+ - 鼠标跟踪:`ENABLE_MOUSE_TRACKING(SGR, 1003)` / `DISABLE_MOUSE_TRACKING`
509
+ - 光标显隐:`HIDE_CURSOR` / `SHOW_CURSOR`
510
+ - 焦点报告:`EFE` / `DFE`(DECSET 1004)
511
+ - 滚轮支持:通过 `DECRPM` 查询终端能力
512
+
513
+ ### Kitty 键盘协议
514
+
515
+ 文件:`src/ink/termio/ansi.ts` 和 `src/ink/termio/csi.ts`
516
+
517
+ 通过 CSI `>1u`(Kitty 协议)和 CSI `>4;2m`(xterm modifyOtherKeys)启用扩展键盘报告,使修饰键组合(Ctrl+Shift+A 等)可区分。
518
+
519
+ ### 替代屏幕(Alt Screen)
520
+
521
+ `<AlternateScreen>` 组件管理终端的替代屏幕缓冲区。启用时:
522
+ - 内容始终恰好占满终端行数
523
+ - 光标隐藏
524
+ - 启用 DECSET 1000/1002/1003 鼠标跟踪
525
+ - 支持 DECSTBM 滚动优化
526
+ - 退出时恢复主屏幕内容
527
+
528
+ ### 声明式光标(Declared Cursor)
529
+
530
+ `useDeclaredCursor` 钩子和 `CursorDeclarationContext` 允许组件声明当前光标位置(如输入框),使 Ink 在每一帧后将终端光标放置在该位置,支持 IME 输入和屏幕阅读器。
531
+
532
+ ### 滚动容器(ScrollBox)
533
+
534
+ `<ScrollBox>` 组件(`overflow: scroll`)实现:
535
+ - `scrollTop`:可编程滚动位置
536
+ - `pendingScrollDelta`:累积的滚动增量(限制 SCROLL_MAX_PER_FRAME 行/帧,防止大幅跳动)
537
+ - `stickyScroll`:粘性滚动(内容增长时自动保持在底部)
538
+ - `scrollTo(y)` / `scrollToElement(el, offset)`:可编程滚动
539
+ - `scrollHeight` / `scrollViewportHeight`:滚动信息查询
540
+ - `scrollClampMin` / `scrollClampMax`:虚拟滚动边界保护
541
+ - 渲染时通过 `output.shift()` 实现 DECSTBM 硬件滚动
542
+
543
+ ### 选择排除(NoSelect)
544
+
545
+ `<NoSelect>` 组件标记区域为选择排除:
546
+ - `noSelect` 模式:仅排除该盒子的精确区域
547
+ - `from-left-edge` 模式:从第 0 列到盒子右边缘的整行区域
548
+ - `screen.noSelect` 位图每帧重置,`blitRegion` 复制时连带复制
549
+
550
+ ### 调试与性能分析
551
+
552
+ - `CLAUDE_CODE_DEBUG_REPAINTS`:追踪重绘来源组件
553
+ - `CLAUDE_CODE_COMMIT_LOG`:记录提交/渲染时间日志
554
+ - YOGA 计数器:`getYogaCounters()` 报告每个框架的节点访问、测量、缓存命中
555
+ - `FRAME_INTERVAL_MS = 16`:60fps 帧率
556
+ - Blit/Write 比率日志:高频写入(而非 blit)时发出警告
557
+
558
+ ---
559
+
560
+ ## 文件索引
561
+
562
+ 按功能类别整理的核心文件:
563
+
564
+ | 类别 | 文件 |
565
+ |------|------|
566
+ | 主入口 | `src/ink.ts`, `src/ink/ink.tsx` |
567
+ | DOM | `src/ink/dom.ts` |
568
+ | Reconciler | `src/ink/reconciler.ts` |
569
+ | Renderer | `src/ink/renderer.ts`, `src/ink/render-node-to-output.ts` |
570
+ | Screen | `src/ink/screen.ts` |
571
+ | Output | `src/ink/output.ts` |
572
+ | LogUpdate | `src/ink/log-update.ts` |
573
+ | Frame | `src/ink/frame.ts` |
574
+ | Styles | `src/ink/styles.ts` |
575
+ | Focus | `src/ink/focus.ts` |
576
+ | Selection | `src/ink/selection.ts` |
577
+ | Layout | `src/ink/layout/engine.ts`, `node.ts` |
578
+ | Event Dispatcher | `src/ink/events/dispatcher.ts` |
579
+ | Event Handlers | `src/ink/events/event-handlers.ts` |
580
+ | Components | `src/ink/components/` (App, Box, Text, Button, Link, Spacer, Newline, ScrollBox, AlternateScreen, RawAnsi, NoSelect, ErrorOverview) |
581
+ | Hooks | `src/ink/hooks/` (use-input, use-stdin, use-app, use-animation-frame, use-interval, use-selection, use-search-highlight, use-declared-cursor, use-tab-status, use-terminal-focus, use-terminal-title, use-terminal-viewport) |
582
+ | Termio | `src/ink/termio/ansi.ts`, `csi.ts`, `dec.ts`, `osc.ts` |
583
+ | Bidi | `src/ink/bidi.ts` |
584
+ | Constants | `src/ink/constants.ts` |
585
+ | Contexts | `src/ink/components/AppContext.ts`, `StdinContext.ts`, `TerminalSizeContext.tsx`, `TerminalFocusContext.tsx`, `ClockContext.tsx`, `CursorDeclarationContext.ts` |
docs/memory-context/context.md ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 上下文管理
2
+
3
+ ## 概述
4
+
5
+ VersperClaw 的上下文管理系统负责构建模型会话的输入上下文,包含系统级信息和用户级信息,并在上下文超过限制时自动压缩。
6
+
7
+ ---
8
+
9
+ ## 系统上下文(getSystemContext)
10
+
11
+ 定义于 `src/context.ts`,通过 `getSystemContext()` 构建,缓存在会话期间不变。
12
+
13
+ ### 包含内容
14
+
15
+ 1. **Git 状态**(`getGitStatus()`)
16
+ - 当前分支、主分支、Git 用户
17
+ - 工作区状态(staged/unstaged 修改)
18
+ - 最近 5 条 commit 记录
19
+ - 限制在 2000 字符以内,超出时截断并提示用户使用 `git status`
20
+ - 在 CCR 远程模式或 Git 指令禁用时跳过
21
+
22
+ 2. **缓存破坏标记**(Cache Breaker)
23
+ - 仅在 `BREAK_CACHE_COMMAND` feature gate 启用时注入
24
+ - 用于紧急调试状态更新
25
+
26
+ ### 代码结构
27
+
28
+ ```typescript
29
+ export const getSystemContext = memoize(
30
+ async (): Promise<{ [k: string]: string }> => {
31
+ const gitStatus = isEnvTruthy(process.env.CLAUDE_CODE_REMOTE)
32
+ ? null
33
+ : await getGitStatus()
34
+ return {
35
+ ...(gitStatus && { gitStatus }),
36
+ ...(injection && { cacheBreaker: `[CACHE_BREAKER: ${injection}]` }),
37
+ }
38
+ },
39
+ )
40
+ ```
41
+
42
+ ---
43
+
44
+ ## 用户上下文(getUserContext)
45
+
46
+ 定义于 `src/context.ts`,通过 `getUserContext()` 构建,同样缓存在会话期间不变。
47
+
48
+ ### 包含内容
49
+
50
+ 1. **CLAUDE.md 文件**
51
+ - 自动发现项目中的 CLAUDE.md 文件(通过 `getClaudeMds()`)
52
+ - 支持 `--add-dir` 在 bare 模式下添加额外的 CLAUDE.md 目录
53
+ - 可通过 `CLAUDE_CODE_DISABLE_CLAUDE_MDS` 环境变量禁用
54
+ - 记忆文件(memory files)被过滤后注入
55
+
56
+ 2. **当前日期**
57
+ - 格式:`Today's date is YYYY-MM-DD.`
58
+ - 使用 `getLocalISODate()` 获取本地日期
59
+
60
+ ### 代码结构
61
+
62
+ ```typescript
63
+ export const getUserContext = memoize(
64
+ async (): Promise<{ [k: string]: string }> => {
65
+ const claudeMd = shouldDisableClaudeMd
66
+ ? null
67
+ : getClaudeMds(filterInjectedMemoryFiles(await getMemoryFiles()))
68
+ return {
69
+ ...(claudeMd && { claudeMd }),
70
+ currentDate: `Today's date is ${getLocalISODate()}.`,
71
+ }
72
+ },
73
+ )
74
+ ```
75
+
76
+ ---
77
+
78
+ ## 上下文压缩(Compact)
79
+
80
+ 当对话上下文超过 API 限制或用户手动触发时,自动或手动压缩对话历史。
81
+
82
+ ### 压缩触发
83
+
84
+ | 触发方式 | 说明 |
85
+ |----------|------|
86
+ | **自动压缩** | 上下文超过阈值时自动触发(`autoCompactThreshold`) |
87
+ | **手动压缩** | 用户执行 `/compact` 命令 |
88
+ | **部分压缩** | 用户选择特定消息前后的历史进行压缩 |
89
+ | **反应式压缩** | API 返回 prompt-too-long 时自动触发 |
90
+
91
+ ### 压缩流程(`src/services/compact/compact.ts`)
92
+
93
+ `compactConversation()` 主流程:
94
+
95
+ 1. **PreCompact Hook**:执行预压缩钩子
96
+ 2. **摘要生成**:将旧消息发送给模型生成摘要
97
+ - 支持 prompt cache 共享(通过 forked agent 复用主会话的缓存前缀)
98
+ - 自动重试(最多 3 次 PTL 重试 + 2 次流式重试)
99
+ 3. **文件状态恢复**:重建最近读取的文件附件(最多 5 个文件,50K token 预算)
100
+ 4. **状态恢复**:plan 文件、技能附件、异步 Agent 状态
101
+ 5. **工具和 Agent 清单重新声明**:延迟工具、MCP 指令、Agent 列表
102
+ 6. **SessionStart Hook**:执行会话启动钩子
103
+ 7. **PostCompact Hook**:执行压缩后钩子
104
+
105
+ ### 压缩边界消息
106
+
107
+ 压缩后在消息流中插入 `SystemCompactBoundaryMessage`,包含:
108
+ - 压缩原因(auto/manual)
109
+ - 压缩前的 token 计数
110
+ - 上一个消息的 UUID(用于链式追踪)
111
+ - 用户反馈(手动压缩时)
112
+ - 压缩的消息数量
113
+
114
+ ### 部分压缩(Partial Compact)
115
+
116
+ `partialCompactConversation()` 支持两种方向:
117
+
118
+ | 方向 | 说明 | 缓存影响 |
119
+ |------|------|----------|
120
+ | `from` | 压缩 pivot 索引之后的消息,保留之前的内容 | 保留前缀缓存 |
121
+ | `up_to` | 压缩 pivot 索引之前的消息,保留之后的内容 | 缓存失效 |
122
+
123
+ ### 文件附件恢复
124
+
125
+ `createPostCompactFileAttachments()` 在压缩后自动恢复最近读取的文件:
126
+
127
+ - 基于读取时间戳排序,恢复最近的文件
128
+ - 跳过已在保留消息中的文件(避免重复)
129
+ - 跳过 plan 文件和记忆文件
130
+ - 受文件数量(5)和 token 预算(50K)双重限制
131
+ - 跳过 `FILE_UNCHANGED_STUB` 对应的文件(压缩时已处理的重复读取)
132
+
133
+ ### 压缩注意事项
134
+
135
+ - **Plan 模式保持**:如果在 plan 模式下压缩,自动注入 plan mode 附件
136
+ - **技能保留**:已调用的技能内容会被保留(每个技能 5K token 预算,总 25K)
137
+ - **缓存破坏检测**:压缩后通知缓存破坏检测系统
138
+ - **会话元数据**:重新追加会话元数据(自定义标题、标签)
139
+ - **助手模式转录**:在 KAIROS 模式下,将压缩的对话段写入转录文件
140
+
141
+ ---
142
+
143
+ ## React Contexts(src/context/)
144
+
145
+ CLI 用户界面使用 React Context 管理多种状态:
146
+
147
+ ### voice.tsx(语音上下文)
148
+ - `VoiceProvider` 提供语音状态管理
149
+ - 状态:`idle` / `recording` / `processing`
150
+ - 包含:录音状态、错误信息、实时转录文本、音频电平、预热状态
151
+ - 基于 React Context + Zustand-style store 模式
152
+
153
+ ### mailbox.tsx(消息邮箱)
154
+ - 管理应用内消息队列
155
+ - 处理消息的发送、接收和状态更新
156
+
157
+ ### stats.tsx(状态统计)
158
+ - 提供性能统计数据的上下文
159
+ - 包括 token 使用量、API 调用次数等
160
+
161
+ ### notifications.tsx(通知系统)
162
+ - 管理应用内通知
163
+ - 支持不同优先级(immediate、later)
164
+ - 支持不同颜色(error、info 等)
165
+
166
+ ### fpsMetrics.tsx(FPS 指标)
167
+ - 跟踪 UI 渲染性能
168
+ - 用于调试和优化
169
+
170
+ ### modalContext.tsx(模态框上下文)
171
+ - 管理模态对话框的显示和隐藏
172
+ - 支持堆叠模式
173
+
174
+ ### promptOverlayContext.tsx(提示覆盖层)
175
+ - 管理提示输入区域的覆盖层
176
+
177
+ ### overlayContext.tsx(覆盖层上下文)
178
+ - 管理通用覆盖层组件
179
+
180
+ ### QueuedMessageContext.tsx(队列消息上下文)
181
+ - 管理消息队列状态
182
+ - 处理消息的排队和调度
183
+
184
+ ---
185
+
186
+ ## 核心文件
187
+
188
+ | 文件 | 路径 | 用途 |
189
+ |------|------|------|
190
+ | context.ts | `src/context.ts` | 系统和用户上下文构建(getSystemContext / getUserContext) |
191
+ | compact.ts | `src/services/compact/compact.ts` | 对话压缩主逻辑 |
192
+ | compactWarningHook.ts | `src/services/compact/compactWarningHook.ts` | 压缩警告钩子 |
193
+ | compactWarningState.ts | `src/services/compact/compactWarningState.ts` | 压缩警告状态管理 |
194
+ | postCompactCleanup.ts | `src/services/compact/postCompactCleanup.ts` | 压缩后清理 |
docs/memory-context/memory.md ADDED
@@ -0,0 +1,210 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 自动记忆系统
2
+
3
+ ## 概述
4
+
5
+ VersperClaw 拥有一个基于文件的持久化记忆存储系统(Memdir),允许 Agent 在不同会话间记住用户信息、偏好、项目上下文等。记忆存储在项目对应的 `~/.claude/projects/<slug>/memory/` 目录中。
6
+
7
+ ---
8
+
9
+ ## Memdir 系统
10
+
11
+ ### 目录结构
12
+
13
+ ```
14
+ ~/.claude/projects/<project-slug>/memory/
15
+ ├── MEMORY.md # 入口索引文件
16
+ ├── user_role.md # 用户类型记忆文件
17
+ ├── feedback_testing.md # 反馈类型记忆文件
18
+ ├── project_deadlines.md # 项目类型记忆文件
19
+ └── reference_dashboards.md # 参考类型记忆文件
20
+ ```
21
+
22
+ ### 记忆类型
23
+
24
+ 系统定义了四种记忆类型(`src/memdir/memoryTypes.ts`),限制为无法从项目当前状态推导出的信息:
25
+
26
+ | 类型 | 用途 | 示例 |
27
+ |------|------|------|
28
+ | **user** | 用户角色、职责、知识背景 | "用户是资深 Go 开发者,首次接触 React" |
29
+ | **feedback** | 用户对工作方式的指导 | "不要 mock 数据库——测试必须用真实数据库" |
30
+ | **project** | 项目上下文、目标、事件 | "2026-03-05 之后冻结所有非关键合并" |
31
+ | **reference** | 外部系统的指针 | "Pipeline bugs 在 Linear 项目 INGEST 中跟踪" |
32
+
33
+ ### 不应该保存的内容
34
+
35
+ - 代码模式、架构、文件路径——这些可从当前项目状态推导
36
+ - Git 历史、最近的变更——`git log` / `git blame` 是权威来源
37
+ - 调试解决方案——修复在代码中,commit message 包含上下文
38
+ - 已在 CLAUDE.md 中记录的内容
39
+ - 临时任务细节——进行中的工作、临时状态
40
+
41
+ ---
42
+
43
+ ## MEMORY.md 入口索引
44
+
45
+ ### 文件规范
46
+
47
+ MEMORY.md 是记忆系统的入口索引文件(非记忆本身):
48
+
49
+ - 路径:`<memoryDir>/MEMORY.md`
50
+ - 最大行数:**200 行**(`MAX_ENTRYPOINT_LINES`)
51
+ - 最大字节数:**25,000 字节**(`MAX_ENTRYPOINT_BYTES`)
52
+ - 格式:每行一个条目:`- [Title](file.md) — 简短描述`
53
+ - 超过限制时自动截断并追加警告
54
+
55
+ ### 截断策略
56
+
57
+ 定义于 `src/memdir/memdir.ts` 的 `truncateEntrypointContent()`:
58
+
59
+ 1. 首先按行数截断(保留前 200 行)
60
+ 2. 然后按字节数截断(在最后一个换行符处切割,避免中断行)
61
+ 3. 追加截断警告说明原因
62
+
63
+ ---
64
+
65
+ ## 记忆生命周期
66
+
67
+ ### 保存(Save)
68
+
69
+ 两种保存方式:
70
+
71
+ 1. **显式请求**:用户要求 Agent 记住某事时立即保存
72
+ 2. **系统自动提取**:通过 `extractMemories`(`src/services/extractMemories/extractMemories.ts`)在后台自动提取有价值的记忆
73
+
74
+ 保存是两步过程:
75
+ 1. 写入记忆文件(如 `user_role.md`),使用 frontmatter 格式
76
+ 2. 在 `MEMORY.md` 中添加指向该文件的索引条目
77
+
78
+ ### Frontmatter 格式
79
+
80
+ ```markdown
81
+ ---
82
+ name: {{记忆名称}}
83
+ description: {{一行描述——用于判断相关性,越具体越好}}
84
+ type: {{user / feedback / project / reference}}
85
+ ---
86
+
87
+ {{记忆内容 — 对于 feedback/project 类型,结构为:规则/事实,然后 **Why:** 和 **How to apply:** 行}}
88
+ ```
89
+
90
+ ### 检索(Recall)
91
+
92
+ `findRelevantMemories()`(`src/memdir/findRelevantMemories.ts`)使用 Sonnet 模型选择与当前查询相关的记忆:
93
+
94
+ 1. 扫描记忆目录中的所有文件,提取文件名和描述
95
+ 2. 将查询和可用记忆清单发送给 Sonnet 模型
96
+ 3. 模型返回最相关的记忆文件名列表(最多 5 个)
97
+ 4. 返回绝对文件路径和 mtime
98
+
99
+ ```typescript
100
+ export async function findRelevantMemories(
101
+ query: string, // 用户查询
102
+ memoryDir: string, // 记忆目录路径
103
+ signal: AbortSignal, // 取消信号
104
+ recentTools?: string[], // 最近使用的工具(过滤不相关的 API 文档)
105
+ alreadySurfaced?: Set<string>, // 已展示的文件(避免重复选择)
106
+ ): Promise<RelevantMemory[]>
107
+ ```
108
+
109
+ **选择性过滤**:
110
+ - 排除已在对话中展示的文件(`alreadySurfaced`)
111
+ - 排除最近正在使用的工具的参考文档
112
+ - 仍选择包含警告、陷阱、已知问题的记忆
113
+
114
+ ### 更新(Update)
115
+
116
+ - 记忆文件的内容可以直接覆盖
117
+ - MEMORY.md 中的索引条目保持最新
118
+ - 语义而非时间顺序组织记忆
119
+
120
+ ### 清除(Delete)
121
+
122
+ 用户要求忘记时,找到并删除相关条目(记忆文件 + MEMORY.md 索引)。
123
+
124
+ ---
125
+
126
+ ## 记忆 Age(新鲜度追踪)
127
+
128
+ ### mtime 追踪
129
+
130
+ 每个记忆文件都有 `mtimeMs`(修改时间戳),在检索时返回:
131
+
132
+ ```typescript
133
+ export type RelevantMemory = {
134
+ path: string
135
+ mtimeMs: number
136
+ }
137
+ ```
138
+
139
+ ### 新鲜度提示
140
+
141
+ - `MEMORY_DRIFT_CAVEAT`:记忆会随时间过时。在基于记忆回答前,需验证记忆是否仍准确
142
+ - 如果回忆的记忆与当前信息冲突,信任当前观察,更新或移除过时记忆
143
+
144
+ ### 记忆推荐前的验证
145
+
146
+ 即使回忆到的记忆命名了特定函数、文件或标志,也只是"在写入时存在"的声明。在推荐前:
147
+
148
+ - 如果记忆提到文件路径:检查文件是否存在
149
+ - 如果记忆提到函数或标志:通过 grep 确认
150
+ - 如果用户即将基于推荐采取行动:先验证
151
+
152
+ ---
153
+
154
+ ## 高级特性
155
+
156
+ ### 团队记忆(Team Memory)
157
+
158
+ 通过 `TEAMMEM` feature gate 启用。使用 `getTeamMemPath()`(`src/memdir/teamMemPaths.ts`)获取团队记忆目录路径。
159
+
160
+ 团队记忆和私有记忆使用不同的作用域标签(`<scope>`),共享相同的四种记忆类型。
161
+
162
+ ### 助手模式每日日志(Assistant Daily Log)
163
+
164
+ 通过 `KAIROS` feature gate 启用。长期运行的助手模式的记忆策略:
165
+
166
+ - 按日期追加到 `logs/YYYY/MM/YYYY-MM-DD.md`
167
+ - 夜间 `/dream` 技能将日志蒸馏为主题文件 + MEMORY.md
168
+ - 避免在长时间会话中频繁重写 MEMORY.md
169
+
170
+ ### 自动 Dream(Auto Dream)
171
+
172
+ `src/services/autoDream/autoDream.ts` 在后台自动执行记忆整理,包括:
173
+ - 合并重复记忆
174
+ - 更新过时信息
175
+ - 清除无关条目
176
+
177
+ ### 团队记忆同步(Team Memory Sync)
178
+
179
+ `src/services/teamMemorySync/` 提供团队记忆的实时同步功能,包括文件监控(watcher)和秘密保护(secret guard)。
180
+
181
+ ---
182
+
183
+ ## 配置文件
184
+
185
+ ### 路径解析
186
+
187
+ `getAutoMemPath()`(`src/memdir/paths.ts`)的路径解析顺序:
188
+
189
+ 1. `CLAUDE_COWORK_MEMORY_PATH_OVERRIDE` 环境变量(完全路径覆盖)
190
+ 2. `autoMemoryDirectory` 设置(来自 settings.json 的可信源)
191
+ 3. `<memoryBase>/projects/<sanitized-git-root>/memory/`
192
+
193
+ ### 启用/禁用
194
+
195
+ 禁用链(优先级从高到低):
196
+ 1. `CLAUDE_CODE_DISABLE_AUTO_MEMORY=1/true` 环境变量
197
+ 2. `CLAUDE_CODE_SIMPLE`(--bare 模式)
198
+ 3. 远程模式但未设置 `CLAUDE_CODE_REMOTE_MEMORY_DIR`
199
+ 4. `autoMemoryEnabled: false` 在 settings.json 中
200
+ 5. 默认:启用
201
+
202
+ ### 核心文件
203
+
204
+ | 文件 | 路径 | 用途 |
205
+ |------|------|------|
206
+ | memdir.ts | `src/memdir/memdir.ts` | 记忆提示构建、入口索引管理 |
207
+ | paths.ts | `src/memdir/paths.ts` | 路径解析、启停检查 |
208
+ | findRelevantMemories.ts | `src/memdir/findRelevantMemories.ts` | 基于 Sonnet 的相关记忆检索 |
209
+ | memoryTypes.ts | `src/memdir/memoryTypes.ts` | 记忆类型定义和提示文本 |
210
+ | memoryScan.ts | `src/memdir/memoryScan.ts` | 记忆文件扫描 |
docs/remote-bridge/overview.md ADDED
@@ -0,0 +1,199 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 远程桥接 / Remote Control
2
+
3
+ ## 概述
4
+
5
+ Bridge(桥接)模式将本地 CLI 连接到 Anthropic 的远程会话基础设施(CCR, Claude Code Remote),使用户可以通过 claude.ai/code 从浏览器控制本地终端。
6
+
7
+ ---
8
+
9
+ ## 两种传输模式
10
+
11
+ ### 1. 基于环境 (env-based)
12
+
13
+ 通过 Environments API 进行 poll/dispatch:
14
+
15
+ 1. CLI 注册为一个 "环境"(environment)到 Anthropic 服务器
16
+ 2. 服务器通过 `pollForWork` API 分发工作
17
+ 3. 客户端领取(acknowledge)工作、执行、发送心跳、返回结果
18
+
19
+ **核心 API(`src/bridge/bridgeApi.ts`)**:
20
+
21
+ | 端点 | 方法 | 用途 |
22
+ |------|------|------|
23
+ | `POST /v1/environments/bridge` | registerBridgeEnvironment | 注册当前终端为可用的执行环境 |
24
+ | `GET .../work/poll` | pollForWork | 轮询待处理的工作 |
25
+ | `POST .../work/{id}/ack` | acknowledgeWork | 确认领取工作 |
26
+ | `POST .../work/{id}/heartbeat` | heartbeatWork | 发送心跳(延长租约) |
27
+ | `POST .../work/{id}/stop` | stopWork | 停止工作 |
28
+ | `DELETE .../environments/bridge/{id}` | deregisterEnvironment | 注销环境 |
29
+
30
+ **认证**:使用 OAuth token 或 Bridge Access Token。
31
+
32
+ ### 2. 无环境 (env-less)
33
+
34
+ 直接通过 OAuth → Worker JWT 交换来连接远程会话:
35
+
36
+ 1. 通过 OAuth 获取访问令牌
37
+ 2. 直接连接到远程会话 WebSocket(`/v1/sessions/ws/{sessionId}/subscribe`)
38
+ 3. 使用 SDK 消息格式进行双向通信
39
+
40
+ **核心组件(`src/remote/`)**:
41
+
42
+ | 文件 | 路径 | 用途 |
43
+ |------|------|------|
44
+ | SessionsWebSocket.ts | `src/remote/SessionsWebSocket.ts` | WebSocket 客户端 |
45
+ | sdkMessageAdapter.ts | `src/remote/sdkMessageAdapter.ts` | SDK 消息转换适配器 |
46
+
47
+ ---
48
+
49
+ ## 核心组件
50
+
51
+ ### bridgeApi.ts(`src/bridge/bridgeApi.ts`)
52
+
53
+ HTTP 客户端,封装了所有 Bridge API 调用:
54
+
55
+ - **OAuth 认证**:自动 401 重试 + token 刷新(通过 `onAuth401` 回调)
56
+ - **BridgeFatalError**:不可重试的错误(认证失败、权限不足、会话过期)
57
+ - **ID 验证**:`validateBridgeId()` 防止路径遍历攻击
58
+ - **错误处理**:对 401/403/404/410/429 状态码分别处理
59
+
60
+ ```typescript
61
+ export class BridgeFatalError extends Error {
62
+ readonly status: number
63
+ readonly errorType: string | undefined
64
+ }
65
+ ```
66
+
67
+ ### bridgeMain.ts(`src/bridge/bridgeMain.ts`)
68
+
69
+ 主桥接逻辑,约 3000 行。负责:
70
+
71
+ - 环境注册与生命周期管理
72
+ - 工作(work)的 poll/dispatch 循环
73
+ - 会话(session)的创建、运行、恢复
74
+ - 多会话支持(`--spawn`, `--capacity`, `--create-session-in-dir`)
75
+ - 优雅关闭(SIGTERM → SIGKILL 宽限期)
76
+ - 退避策略(连接退避、通用退避、stopWork 退避)
77
+
78
+ ```typescript
79
+ export type BackoffConfig = {
80
+ connInitialMs: number // 连接初始延迟
81
+ connCapMs: number // 连接最大延迟 (2min)
82
+ connGiveUpMs: number // 连接放弃时间 (10min)
83
+ generalInitialMs: number // 通用初始延迟
84
+ generalCapMs: number // 通用最大延迟 (30s)
85
+ generalGiveUpMs: number // 通用放弃时间 (10min)
86
+ shutdownGraceMs?: number // SIGTERM→SIGKILL 宽限期
87
+ }
88
+ ```
89
+
90
+ ### replBridge.ts(`src/bridge/replBridge.ts`)
91
+
92
+ REPL 集成桥接,约 2400 行。在 REPL(交互式终端)模式下将 CLI 连接到远程会话:
93
+
94
+ - 通过 `HybridTransport` 实现消息转发
95
+ - 支持 CCR v1/v2 协议(`createV1ReplTransport` / `createV2ReplTransport`)
96
+ - 消息入口(ingress)处理
97
+ - 控制请求/响应(`SDKControlRequest` / `SDKControlResponse`)
98
+ - 容量唤醒(capacity wake)信号
99
+
100
+ ```typescript
101
+ export type ReplBridgeHandle = {
102
+ bridgeSessionId: string
103
+ environmentId: string
104
+ sessionIngressUrl: string
105
+ writeMessages(messages: Message[]): void
106
+ writeSdkMessages(messages: SDKMessage[]): void
107
+ sendControlRequest(request: SDKControlRequest): void
108
+ sendControlResponse(response: SDKControlResponse): void
109
+ sendControlCancelRequest(requestId: string): void
110
+ sendResult(): void
111
+ teardown(): Promise<void>
112
+ }
113
+ ```
114
+
115
+ ### SessionsWebSocket.ts(`src/remote/SessionsWebSocket.ts`)
116
+
117
+ WebSocket 客户端,用于直接连接远程会话:
118
+
119
+ **协议**:
120
+ 1. 连接到 `wss://api.anthropic.com/v1/sessions/ws/{sessionId}/subscribe?organization_uuid=...`
121
+ 2. 发送认证消息:`{ type: 'auth', credential: { type: 'oauth', token: '...' } }`
122
+ 3. 接收 SDK 消息流
123
+
124
+ **重连机制**:
125
+ - `RECONNECT_DELAY_MS = 2000`:重连延迟 2 秒
126
+ - `MAX_RECONNECT_ATTEMPTS = 5`:最大重连次数
127
+ - `PING_INTERVAL_MS = 30000`:30 秒心跳间隔
128
+ - `PERMANENT_CLOSE_CODES = new Set([4003])`:4003(未授权)为永久关闭,不重连
129
+ - `MAX_SESSION_NOT_FOUND_RETRIES = 3`:4001(会话未找到)有限重试(压缩期间可能短暂出现)
130
+
131
+ ```typescript
132
+ type SessionsWebSocketCallbacks = {
133
+ onMessage: (message: SessionsMessage) => void
134
+ onClose?: () => void
135
+ onError?: (error: Error) => void
136
+ onConnected?: () => void
137
+ onReconnecting?: () => void
138
+ }
139
+ ```
140
+
141
+ ### sdkMessageAdapter.ts(`src/remote/sdkMessageAdapter.ts`)
142
+
143
+ SDK 消息格式转换器。将 CCR 发送的 SDK 格式消息(`SDKMessage`)转换为 CLI 内部的消息类型(`Message`):
144
+
145
+ - `convertAssistantMessage()`: `SDKAssistantMessage` → `AssistantMessage`
146
+ - `convertStreamEvent()`: `SDKPartialAssistantMessage` → `StreamEvent`
147
+ - 处理多种消息类型:assistant、system、compact_boundary、status、tool_progress、result 等
148
+
149
+ ### remotePermissionBridge.ts
150
+
151
+ 在远程会话中处理权限请求桥接(位于 `src/hooks/useSSHSession.ts`、`useRemoteSession.ts`、`useDirectConnect.ts`),将远程权限提示通过 WebSocket 转发给用户。
152
+
153
+ ---
154
+
155
+ ## 认证机制
156
+
157
+ | 机制 | 说明 |
158
+ |------|------|
159
+ | **OAuth Token** | 通过 OAuth 2.0 流程获取的访问令牌 |
160
+ | **Bridge Access Token** | 桥接模式专用的访问令牌,通过 `workSecret.ts` 中的 `decodeWorkSecret()` 解码 |
161
+ | **Trusted Device Token** | `X-Trusted-Device-Token` 头部,用于权限提升 |
162
+ | **Token 刷新** | `handleOAuth401Error` 在 401 时自动刷新 |
163
+
164
+ ---
165
+
166
+ ## WebSocket 协议(CCR v1/v2)
167
+
168
+ ### 认证流程
169
+
170
+ ```
171
+ Client → Server: { type: "auth", credential: { type: "oauth", token: "..." } }
172
+ Server → Client: { type: "auth_ok" } 或 { type: "auth_error" }
173
+ ```
174
+
175
+ ### 消息格式
176
+
177
+ - **CCR v1**:通过 `replBridgeTransport.ts` 的 `createV1ReplTransport` 处理
178
+ - **CCR v2**:通过 `createV2ReplTransport` 处理,使用 `buildCCRv2SdkUrl()` 构建 URL
179
+
180
+ ### 心跳保活
181
+
182
+ - 标准 WebSocket ping:30 秒间隔
183
+ - 会话活动信号(`sendSessionActivitySignal()`)在压缩等长时间操作期间发送,防止 WebSocket 因 idle 超时被断开
184
+
185
+ ---
186
+
187
+ ## 其他组件
188
+
189
+ | 文件 | 路径 | 用途 |
190
+ |------|------|------|
191
+ | bridgeConfig.ts | `src/bridge/bridgeConfig.ts` | 桥接配置管理 |
192
+ | bridgeMessaging.ts | `src/bridge/bridgeMessaging.ts` | 桥接消息处理逻辑 |
193
+ | capacityWake.ts | `src/bridge/capacityWake.ts` | 容量唤醒信号 |
194
+ | codeSessionApi.ts | `src/bridge/codeSessionApi.ts` | 代码会话 API |
195
+ | trustedDevice.ts | `src/bridge/trustedDevice.ts` | 受信任设备管理 |
196
+ | workSecret.ts | `src/bridge/workSecret.ts` | Work Secret 编解码 |
197
+ | sessionIdCompat.ts | `src/bridge/sessionIdCompat.ts` | 会话 ID 兼容性转换 |
198
+ | pollConfig.ts | `src/bridge/pollConfig.ts` | 轮询间隔配置 |
199
+ | types.ts | `src/bridge/types.ts` | 桥接模块类型定义 |
docs/server/overview.md ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 桌面 HTTP/WebSocket 服务器
2
+
3
+ ## 概述
4
+
5
+ VersperClaw 桌面服务器是一个同进程 HTTP + WebSocket 服务器,基于 **Bun.serve()** 运行,为 Tauri 桌面应用提供全套 API 接口。服务器与 CLI 子进程通过 SDK WebSocket Bridge 通信,桌面 UI 通过客户端 WebSocket 与服务器交互。
6
+
7
+ - **入口文件**: `src/server/server.ts`
8
+ - **运行时**: Bun (JavaScript/TypeScript 运行时)
9
+ - **默认端口**: 3456
10
+ - **架构**: 单进程,HTTP + WebSocket 共存
11
+
12
+ ## 路由系统
13
+
14
+ 路由定义在 `src/server/router.ts`。所有 API 请求统一由 `handleApiRequest()` 函数处理,根据 URL 路径第二段(resource)分发到对应的 API handler。
15
+
16
+ 当前支持约 **28 个 API 资源路径**,每个 handler 部署在 `src/server/api/` 目录下独立的文件中。
17
+
18
+ ## API 端点
19
+
20
+ ### 会话管理
21
+
22
+ | 路径 | 描述 |
23
+ |------|------|
24
+ | `/api/sessions` | 会话 CRUD — 创建、读取、更新、删除会话 |
25
+ | `/api/sessions/:id/chat/*` | 会话内的对话操作(由 conversations handler 处理) |
26
+
27
+ 对应文件: `api/sessions.ts`
28
+
29
+ ### 对话管理
30
+
31
+ | 路径 | 描述 |
32
+ |------|------|
33
+ | `/api/conversations` | 对话 CRUD — 会话消息的读写管理 |
34
+
35
+ 对应文件: `api/conversations.ts`
36
+
37
+ ### 模型配置
38
+
39
+ | 路径 | 描述 |
40
+ |------|------|
41
+ | `/api/models` | 获取可用模型列表 |
42
+ | `/api/models/current` | 获取/切换当前选中的模型 |
43
+ | `/api/effort` | 获取/设置 Effort 等级(low / medium / high / max) |
44
+
45
+ 对应文件: `api/models.ts`
46
+
47
+ ### 设置与权限
48
+
49
+ | 路径 | 描述 |
50
+ |------|------|
51
+ | `/api/settings` | 应用设置读写 |
52
+ | `/api/permissions` | 权限配置(由 settings handler 处理) |
53
+
54
+ 对应文件: `api/settings.ts`
55
+
56
+ ### MCP
57
+
58
+ | 路径 | 描述 |
59
+ |------|------|
60
+ | `/api/mcp` | MCP 服务器管理 — 连接配置与状态 |
61
+
62
+ 对应文件: `api/mcp.ts`
63
+
64
+ ### 插件与技能
65
+
66
+ | 路径 | 描述 |
67
+ |------|------|
68
+ | `/api/plugins` | 插件管理 — 安装、卸载、更新、启用/禁用 |
69
+ | `/api/skills` | 技能管理 |
70
+
71
+ 对应文件: `api/plugins.ts`, `api/skills.ts`
72
+
73
+ ### 诊断
74
+
75
+ | 路径 | 描述 |
76
+ |------|------|
77
+ | `/api/doctor` | 环境诊断 — 检查系统配置是否正常 |
78
+ | `/api/diagnostics` | 诊断事件记录与查询 |
79
+
80
+ 对应文件: `api/doctor.ts`, `api/diagnostics.ts`
81
+
82
+ ### 好友系统
83
+
84
+ | 路径 | 描述 |
85
+ |------|------|
86
+ | `/api/friend` | VRM 虚拟形象前端路由(由 Tauri 桌面应用调用),包含 TTS/STT、SSE、偏好设置等 |
87
+
88
+ 路由定义在单独模块中(非 router.ts 直接管理),对应文件: `api/friend.ts`
89
+
90
+ ### 文件与工作区
91
+
92
+ | 路径 | 描述 |
93
+ |------|------|
94
+ | `/api/filesystem` | 文件系统访问操作 |
95
+ | `/api/workspaces` | Git 工作区管理(会话工作区创建、差异查看等) |
96
+
97
+ 对应文件: `api/filesystem.ts`(filesystem 由专用路由函数处理)
98
+
99
+ ### 其他端点
100
+
101
+ | 路径 | 描述 |
102
+ |------|------|
103
+ | `/api/scheduled-tasks` | 定时任务管理 |
104
+ | `/api/search` | 搜索 |
105
+ | `/api/agents` / `/api/tasks` | Agent 任务管理 |
106
+ | `/api/status` | 服务器状态查询 |
107
+ | `/api/teams` | 团队配置 |
108
+ | `/api/providers` | Provider 提供商配置 |
109
+ | `/api/adapters` | 适配器管理 |
110
+ | `/api/computer-use` | Computer Use 功能控制 |
111
+ | `/api/haha-oauth` | haha 自定义 OAuth 认证 |
112
+ | `/api/haha-openai-oauth` | haha OpenAI OAuth 认证 |
113
+ | `/api/h5-access` | H5 访问策略 |
114
+ | `/api/activity-stats` | 活动统计 |
115
+ | `/api/open-targets` | OpenTarget 管理 |
116
+ | `/api/memory` | 记忆管理 |
117
+ | `/api/desktop-ui` | 桌面 UI 偏好 |
118
+ | `/api/cli-auth` | CLI 认证 |
119
+ | `/api/cli-proxy` | CLI 代理 |
120
+
121
+ ## WebSocket
122
+
123
+ **文件**: `ws/handler.ts` (约 68KB)
124
+
125
+ WebSocket 连接处理器管理完整的连接生命周期:
126
+
127
+ - **会话管理**: 创建、启动、停止、清理会话
128
+ - **消息路由**: 将用户消息通过 CLI 子进程(stream-json 模式)处理
129
+ - **命令处理**: 解析和处理斜杠命令(slash command)
130
+ - **权限控制**: Computer Use 审批、运行时覆盖
131
+ - **自动标题**: 追踪用户消息计数,自动生成对话标题
132
+ - **会话预热**: 预启动空闲会话以加快响应
133
+ - **断线重连**: 客户端断线后保持会话 5 分钟,支持重连
134
+ - **消息转换**: CLI stdout 消息转换为 ServerMessage 并转发到 WebSocket
135
+
136
+ 消息协议定义在 `ws/events.ts`,包含 `ClientMessage` 和 `ServerMessage` 类型。
137
+
138
+ ## 服务层
139
+
140
+ 所有后端服务部署在 `src/server/services/` 目录下:
141
+
142
+ ### conversationService.ts
143
+ CLI 子进程管理器。每个桌面会话拥有一个 CLI 子进程,子进程通过 SDK WebSocket Bridge 与桌面服务器通信。
144
+
145
+ ### sessionService.ts
146
+ 会话 CRUD 操作封装。读写 CLI 持久化在 `~/.claude/projects/{path}/{sessionId}.jsonl` 的会话数据,确保桌面应用与 CLI 数据完全互通。使用 SQLite 间接管理会话索引。
147
+
148
+ ### providerService.ts
149
+ 多 Provider 配置管理。基于预设(Preset)系��,支持 OpenCode 兼容配置。存储位置: `~/.claude/cc-haha/providers.json`,活跃 Provider 的环境变量写入 `~/.claude/cc-haha/settings.json`(与原始 Claude Code 的 `~/.claude/settings.json` 隔离)。
150
+
151
+ ### workspaceService.ts
152
+ Git 工作区管理。支持差异查看、文件历史快照、会话工作区初始化等功能。
153
+
154
+ ### pluginService.ts
155
+ 插件管理系统。支持安装、卸载、更新、启用/禁用插件,管理 MCP 服务器集成与 LSP 服务器集成。
156
+
157
+ ### cronScheduler.ts
158
+ Cron 任务调度引擎。定期检查所有定时任务,在匹配 cron 表达式时通过 CLI 子进程执行任务,执行历史持久化到 `~/.claude/scheduled_tasks_log.json`。
159
+
160
+ ### desktopCliLauncherService.ts
161
+ 桌面 CLI 启动器。负责安装和管理 `claude-haha` CLI 命令,维护 PATH 环境变量。
162
+
163
+ ### 其他服务
164
+
165
+ - `diagnosticsService.ts` / `doctorService.ts`: 系统诊断
166
+ - `searchService.ts`: 搜索服务
167
+ - `titleService.ts`: 对话标题自动生成
168
+ - `taskService.ts`: 任务管理
169
+ - `teamService.ts` / `teamWatcher.ts`: 团队协作
170
+ - `notificationService.ts`: 桌面通知
171
+ - `networkSettings.ts`: 网络代理设置
172
+ - `managedSettingsService.ts`: 托管设置
173
+ - `h5AccessService.ts`: H5 访问策略
174
+ - `computerUseApprovalService.ts`: Computer Use 审批
175
+ - `mcpHostPreflight.ts`: MCP 预检
176
+ - `attributionHeaderPolicy.ts`: 归因头策略
177
+ - `agentService.ts`: Agent 服务
178
+ - `adapterService.ts`: 适配器服务
179
+ - `openaiOfficialProvider.ts`: OpenAI 官方提供商
180
+ - `hahaOAuthService.ts` / `hahaOpenAIOAuthService.ts`: OAuth 认证
181
+ - `repositoryLaunchService.ts`: 仓库启动(Git worktree 集成)
182
+ - `filesystemAccessRoots.ts`: 文件系统访问根目录
183
+ - `recoverableJsonFile.ts`: 可恢复 JSON 文件读写
184
+ - `persistentStorageMigrations.ts`: 持久化存储迁移
185
+ - `providerRuntimeEnv.ts`: Provider 运行时环境
docs/server/proxy-provider.md ADDED
@@ -0,0 +1,135 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Provider 代理与多模型支持
2
+
3
+ ## Proxy 协议转换
4
+
5
+ **文件**: `src/server/proxy/handler.ts`
6
+
7
+ Proxy Handler 是一个**协议转换反向代理**,接收 Anthropic Messages API 格式的请求,将其转换为 OpenAI Chat Completions 或 Responses API 格式,转发到上游第三方(3P)Provider,再将响应转换回 Anthropic 格式。
8
+
9
+ ### 转换路线
10
+
11
+ ```
12
+ Anthropic Messages API ←→ OpenAI Chat Completions API
13
+ Anthropic Messages API ←→ OpenAI Responses API
14
+ ```
15
+
16
+ ### 转换模块
17
+
18
+ 所有转换逻辑位于 `src/server/proxy/transform/` 和 `src/server/proxy/streaming/`:
19
+
20
+ **请求转换(Anthropic → OpenAI)**:
21
+ - `anthropicToOpenaiChat.ts` — 转换非流式 Chat 请求
22
+ - `anthropicToOpenaiResponses.ts` — 转换非流式 Responses 请求
23
+
24
+ **响应转换(OpenAI → Anthropic)**:
25
+ - `openaiChatToAnthropic.ts` — Chat 响应转 Anthropic 格式
26
+ - `openaiResponsesToAnthropic.ts` — Responses 响应转 Anthropic 格式
27
+
28
+ **流式转换(OpenAI SSE stream → Anthropic SSE stream)**:
29
+ - `openaiChatStreamToAnthropic.ts` — Chat 流式响应转换
30
+ - `openaiResponsesStreamToAnthropic.ts` — Responses 流式响应转换
31
+ - `openaiResponsesStreamToAnthropicResponse.ts` — Responses 流聚合
32
+
33
+ **工具参数**:
34
+ - `toolArguments.ts` — 工具参数的兼容处理
35
+
36
+ **类型定义**:
37
+ - `types.ts` — `AnthropicRequest` 等共享类型
38
+
39
+ ### 支持的 Provider
40
+
41
+ Proxy 支持以下第三方提供商(3P Provider):
42
+
43
+ - **Groq** — 通过 OpenAI Chat API
44
+ - **OpenRouter** — 通过 OpenAI Chat API
45
+ - **OpenCode** — 通过 OpenAI Chat API,含 DeepSeek 推理兼容适配
46
+ - **本地模型** — 任何兼容 OpenAI API 的本地推理端点
47
+ - **其他 OpenAI 兼容 API** — 只要符合 OpenAI Chat/Responses 格式即可
48
+
49
+ ### DeepSeek 兼容模式
50
+
51
+ 当检测到上游 baseUrl 包含 `deepseek` 或 `opencode.ai` 时,自动启用 DeepSeek 推理兼容模式:
52
+ - 启用手动往返推理内容(`roundTripReasoningContent`)
53
+ - 传递 thinking toggle(`passThinkingToggle`)
54
+
55
+ ## Provider 配置系统
56
+
57
+ **文件**: `src/server/services/providerService.ts`
58
+
59
+ ProviderService 实现了一个基于预设(Preset)的 Provider 配置系统。
60
+
61
+ ### 数据存储
62
+
63
+ - **索引文件**: `~/.claude/cc-haha/providers.json`(轻量级索引)
64
+ - **环境变量**: 活跃 Provider 配置写入 `~/.claude/cc-haha/settings.json`
65
+ - **隔离策略**: 与原始 Claude Code 的 `~/.claude/settings.json` 完全隔离
66
+
67
+ ### Preset 预设系统
68
+
69
+ **文件**: `src/server/config/providerPresets.ts` + `providerPresets.json`
70
+
71
+ 每个预设包含:
72
+ - `id` / `name`: 唯一标识和显示名
73
+ - `baseUrl`: API 端点地址
74
+ - `apiFormat`: 格式类型(`openai_chat` 或 `openai_responses`)
75
+ - `defaultModels`: 模型映射(main / haiku / sonnet / opus)
76
+ - `needsApiKey`: 是否需要 API Key
77
+ - `authStrategy`: 认证策略
78
+ - `defaultEnv`: 默认环境变量
79
+ - `modelContextWindows`: 模型上下文窗口大小
80
+
81
+ ### OpenCode 兼容
82
+
83
+ 系统支持导入 OpenCode 的 Provider 配置,包括模型映射和 API 端点设置。
84
+
85
+ ### haha 认证
86
+
87
+ 自定义认证机制,支持两种 OAuth 流程:
88
+ - `hahaOAuthService.ts` — 通用 haha OAuth
89
+ - `hahaOpenAIOAuthService.ts` — OpenAI 专用 OAuth
90
+
91
+ ## Two-Tier 访问架构
92
+
93
+ 系统采用双层访问模式:
94
+
95
+ ### Tier 1: 原生 SDK 直连
96
+
97
+ 适用于 **Anthropic 用户**,直接使用官方 SDK:
98
+
99
+ - **Anthropic API** — 直接使用 `@anthropic-ai/sdk`
100
+ - **AWS Bedrock** — 通过 Bedrock SDK
101
+ - **GCP Vertex AI** — 通过 Vertex AI SDK
102
+
103
+ 无需经过 Proxy 转换,性能最优。
104
+
105
+ ### Tier 2: Proxy 协议转换
106
+
107
+ 适用于 **第三方(3P)用户**,通过 Proxy 转换为 OpenAI 格式:
108
+
109
+ - 请求路径: `/proxy/v1/messages`(使用活跃 Provider)
110
+ - 请求路径: `/proxy/providers/:providerId/v1/messages`(指定特定 Provider)
111
+ - 支持 OpenAI Chat Completions API
112
+ - 支持 OpenAI Responses API
113
+ - 支持流式(SSE)和非流式响应
114
+
115
+ ### OpenAI 官方提供商
116
+
117
+ **文件**: `src/server/services/openaiOfficialProvider.ts`
118
+
119
+ OpenAI 官方提供商作为特殊的内置 Provider,使用 OpenAI Codex API,通过 OAuth 认证获取令牌,支持完整的模型目录(`OPENAI_CODEX_MODEL_CATALOG`)。
120
+
121
+ ### Provider 运行时环境
122
+
123
+ **文件**: `src/server/services/providerRuntimeEnv.ts`
124
+
125
+ 管理 Provider 的运行时环境变量:
126
+ - 构建 Provider 认证环境
127
+ - 规范化模型映射
128
+ - 获取托管环境变量键
129
+ - 解析预设认证策略
130
+
131
+ ### 归因头策略
132
+
133
+ **文件**: `src/server/services/attributionHeaderPolicy.ts`
134
+
135
+ 自动为请求添加 Claude Code 归因头,确保第三方 API 调用中的来源标识。
docs/services/overview.md ADDED
@@ -0,0 +1,231 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 后端服务
2
+
3
+ VersperClaw 提供一系列后台服务,涵盖 MCP 集成、上下文压缩、自主记忆整合、即时通讯集成、OAuth 认证、分析监控及工具执行引擎。
4
+
5
+ ---
6
+
7
+ ## MCP (Model Context Protocol)
8
+
9
+ **目录**: `src/services/mcp/`
10
+
11
+ MCP 客户端实现,基于 `@modelcontextprotocol/sdk`,支持多种传输层协议和 OAuth 认证。
12
+
13
+ ### 核心文件
14
+
15
+ | 文件 | 描述 |
16
+ |------|------|
17
+ | `client.ts` | MCP 客户端 — 支持 Stdio、SSE、Streamable HTTP 三种传输协议;管理工具/资源/命令的发现与调用 |
18
+ | `auth.ts` | MCP OAuth 认证 — 基于 Authorization Server Metadata 发现流程,支持 PKCE、令牌刷新、本地 HTTP 服务器捕获授权码 |
19
+ | `config.ts` | MCP Server 配置解析 |
20
+ | `types.ts` | MCP 相关类型定义 |
21
+ | `InProcessTransport.ts` | 同进程传输层实现 |
22
+ | `SdkControlTransport.ts` | SDK Control 传输层 |
23
+ | `normalization.ts` | MCP 结果规范化 |
24
+ | `envExpansion.ts` | 环境变量展开 |
25
+ | `headersHelper.ts` | HTTP 头部辅助 |
26
+ | `elicitationHandler.ts` | MCP Elicit 请求处理 |
27
+ | `mcpStringUtils.ts` | MCP 字符串工具 |
28
+ | `claudeai.ts` | Claude AI 集成 |
29
+ | `officialRegistry.ts` | 官方 MCP 注册表 |
30
+ | `useManageMCPConnections.ts` | MCP 连接管理 Hook |
31
+ | `vscodeSdkMcp.ts` | VSCode SDK 兼容层 |
32
+ | `channelAllowlist.ts` / `channelPermissions.ts` / `channelNotification.ts` | 频道权限与通知 |
33
+ | `MCPConnectionManager.tsx` | MCP 连接管理器 UI 组件 |
34
+ | `xaa.ts` / `xaaIdpLogin.ts` | XAA 身份提供方登录 |
35
+ | `oauthPort.ts` | OAuth 回调端口管理 |
36
+
37
+ ### 支持的传输协议
38
+
39
+ 1. **StdioClientTransport** — 子进程标准输入/输出
40
+ 2. **SSEClientTransport** — Server-Sent Events
41
+ 3. **StreamableHTTPClientTransport** — 可流式 HTTP
42
+
43
+ ### 发现能力
44
+
45
+ 工具(Tools)、资源(Resources)、提示(Prompts)的自动发现与调用。
46
+
47
+ ---
48
+
49
+ ## 上下文压缩
50
+
51
+ **目录**: `src/services/compact/`
52
+
53
+ 上下文压缩引擎,在对话历史超出上下文窗口时自动触发压缩,将冗长的历史会话压缩为精炼的摘要。
54
+
55
+ ### 核心文件
56
+
57
+ | 文件 | 大小 | 描述 |
58
+ |------|------|------|
59
+ | `compact.ts` | 61 KB | **核心压缩引擎** — 将对话历史压缩为摘要,包含完整的压缩策略与边界消息管理 |
60
+ | `autoCompact.ts` | — | **自动压缩** — 当上下文窗口使用量超过阈值时自动触发压缩,管理 maxTokens、重试逻辑、token 估算 |
61
+ | `microCompact.ts` | — | **微压缩** — 细粒度工具结果压缩,仅压缩特定工具(FileRead、FileEdit、FileWrite、Glob、Grep、WebFetch、WebSearch 等)的输出 |
62
+ | `sessionMemoryCompact.ts` | — | **会话记忆压缩** — 实验性功能,将会话记忆压缩整合到会话恢复流程中 |
63
+ | `cachedMicrocompact.ts` | — | 缓存式微压缩,提升重复压缩效率 |
64
+ | `cachedMCConfig.ts` | — | 缓存微压缩配置 |
65
+ | `snipCompact.ts` / `snipProjection.ts` | — | 截断式压缩与投影 |
66
+ | `grouping.ts` | — | 消息分组策略 |
67
+ | `reactiveCompact.ts` | — | 响应式压缩 |
68
+ | `timeBasedMCConfig.ts` | — | 基于时间的微压缩配置 |
69
+ | `postCompactCleanup.ts` | — | 压缩后清理 |
70
+ | `prompt.ts` | — | 压缩提示词模板 |
71
+ | `apiMicrocompact.ts` | — | API 微压缩接口 |
72
+ | `compactWarningHook.ts` / `compactWarningState.ts` | — | 压缩警告 UI 状态管理 |
73
+
74
+ ### 压缩流程
75
+
76
+ 1. `autoCompact` 监控上下文窗口使用率
77
+ 2. 当超过阈值时,调用 `compactConversation()`(在 `compact.ts` 中)
78
+ 3. 压缩引擎提取历史消息,调用 AI 生成摘要
79
+ 4. 摘要替换原始历史消息,压缩边界消息标记范围
80
+ 5. `sessionMemoryCompact` 可选地将会话记忆额外压缩
81
+ 6. `microCompact` 对工具结果进行细粒度压缩以进一步节省 token
82
+ 7. `postCompactCleanup` 执行压缩后清理
83
+
84
+ ---
85
+
86
+ ## Auto Dream (自主记忆整合)
87
+
88
+ **目录**: `src/services/autoDream/`
89
+
90
+ Auto Dream 是一个后台记忆整合系统,在对话间期自动运行,将学习到的信息整理为持久化的记忆文件。
91
+
92
+ ### 核心文件
93
+
94
+ | 文件 | 描述 |
95
+ |------|------|
96
+ | `autoDream.ts` | 主模块 — 后台记忆中整合,使用 `runForkedAgent` 执行 `/dream` 提示词。使用三道门控(Gate)按成本递增顺序判断是否执行:时间(距上次整合 >= minHours)、会话数(新会话 >= minSessions)、锁(无其他进程正在整合) |
97
+ | `config.ts` | 配置 — 是否启用 Auto Dream |
98
+ | `consolidationLock.ts` | 整合锁 — 基于 PID 和 mtime 的文件锁,防止多个进程同时整合。锁文件位于记忆目录,mtime 作为 `lastConsolidatedAt` 时间戳 |
99
+ | `consolidationPrompt.ts` | 整合提示词 — 构建 `/dream` 的系统提示词,指导 AI 反思记忆文件并整理为持久化知识 |
100
+
101
+ ### 门控顺序
102
+
103
+ 1. **时间门控**: 距上次整合 >= 最小小时数(一个 stat 调用,最便宜)
104
+ 2. **会话门控**: 新产生的会话记录数 >= 最小会话数
105
+ 3. **锁门控**: 无其他进程正在进行整合(最昂贵)
106
+
107
+ ### 整合内容
108
+
109
+ - 回顾记忆目录中的 `.md` 文件
110
+ - 分析近期会话记录(transcript)
111
+ - 合成新的记忆,整理现有知识
112
+ - 确保记忆文件对后续会话具有良好可读性
113
+
114
+ ---
115
+
116
+ ## 飞书集成
117
+
118
+ **目录**: `src/services/feishu/`
119
+
120
+ 飞书机器人集成,基于 `@larksuite/channel` SDK。
121
+
122
+ ### 核心文件
123
+
124
+ | 文件 | 描述 |
125
+ |------|------|
126
+ | `FeishuService.ts` | 飞书机器人服务 — 创建 LarkChannel、注册应用、处理消息事件、管理聊天模式、引用回复、保活机制 |
127
+ | `feishuConfig.ts` | 配置管理 — 读写 `~/.claude/adapters.json` 中的飞书配置段;管理已配对用户、授权用户白名单、Group 模式设置 |
128
+ | `vendor/` | 自包含的第三方实现模块(核心日志、机器人 pending 队列、保活、引用回复、访问策略) |
129
+
130
+ ### 功能
131
+
132
+ - 应用注册与事件处理
133
+ - DM 私聊白名单
134
+ - Group 群聊模式
135
+ - 引用上下文回复
136
+ - 管理员权限控制
137
+ - 自动保活(Keepalive)
138
+
139
+ ---
140
+
141
+ ## Telegram 集成
142
+
143
+ **文件**: `src/services/telegram/TelegramService.ts`
144
+
145
+ Telegram 机器人服务,基于 Telegram Bot API 实现消息收发、指令处理、行内键盘等功能。
146
+
147
+ ### 功能
148
+
149
+ - `getMe` — 获取机器人信息
150
+ - `sendMessage` — 发送消息(支持 Markdown / HTML 格式)
151
+ - `editMessageText` — 编辑已发送消息
152
+ - `answerCallbackQuery` — 响应回调查询
153
+ - `sendChatAction` — 发送聊天动作指示器
154
+ - `setMyCommands` — 设置机器人命令列表
155
+ - `getUpdates` — 轮询获取更新
156
+
157
+ 配置管理: `telegramConfig.ts` / `telegramTypes.ts`
158
+
159
+ ---
160
+
161
+ ## OAuth
162
+
163
+ **目录**: `src/services/oauth/`
164
+
165
+ 通用 OAuth 2.0 认证服务,支持授权码流程(Authorization Code Flow with PKCE)。
166
+
167
+ ### 核心文件
168
+
169
+ | 文件 | 描述 |
170
+ |------|------|
171
+ | `index.ts` | `OAuthService` — 主类,管理完整 OAuth 流程:生成 code_verifier、启动本地 HTTP 服务器监听回调、令牌交换 |
172
+ | `auth-code-listener.ts` | 授权码监听器 — 启动本地 HTTP 服务器捕获回调中的授权码 |
173
+ | `client.ts` | OAuth 客户端 HTTP 请求封装 |
174
+ | `crypto.ts` | PKCE 加密工具 — code_verifier / code_challenge 生成 |
175
+ | `getOauthProfile.ts` | 获取 OAuth 用户档案 |
176
+ | `types.ts` | OAuth 类型定义(令牌、速率限制、订阅类型等) |
177
+
178
+ ### 流程
179
+
180
+ 1. 生成 PKCE code_verifier 和 code_challenge
181
+ 2. 构建授权 URL 并打开浏览器
182
+ 3. 本地 HTTP 服务器监听回调 / 用户手动粘贴授权码
183
+ 4. 用授权码换取令牌(access_token + refresh_token)
184
+ 5. 支持令牌刷新和持久化
185
+
186
+ ---
187
+
188
+ ## 分析 (Analytics)
189
+
190
+ **目录**: `src/services/analytics/`
191
+
192
+ ### 核心组件
193
+
194
+ | 文件 | 描述 |
195
+ |------|------|
196
+ | `index.ts` | 分析日志入口 — 标准化事件日志接口 |
197
+ | `growthbook.ts` | GrowthBook 特性标记集成 — 远程配置和 A/B 测试功能开关 |
198
+ | `config.ts` | Analytics 配置 |
199
+ | `metadata.ts` | 分析元数据提取 — 工具名称脱敏、MCP 工具详情、文件扩展名 |
200
+ | `datadog.ts` | Datadog 集成 |
201
+ | `firstPartyEventLogger.ts` / `firstPartyEventLoggingExporter.ts` | 第一方事件日志与导出 |
202
+ | `sink.ts` / `sinkKillswitch.ts` | 事件接收器与终止开关 |
203
+
204
+ ### 功能
205
+
206
+ - 工具使用统计与耗时追踪
207
+ - 模型调用分析
208
+ - GrowthBook 远程特性配置
209
+ - Datadog APM 集成
210
+
211
+ ---
212
+
213
+ ## 工具执行引擎
214
+
215
+ **目录**: `src/services/tools/`
216
+
217
+ ### 核心文件
218
+
219
+ | 文件 | 大小 | 描述 |
220
+ |------|------|------|
221
+ | `toolExecution.ts` | 60 KB | **工具执行引擎** — 核心工具运行逻辑,管理工具调用生命周期、权限检查、进度报告、结果处理 |
222
+ | `toolHooks.ts` | — | **工具钩子系统** — 执行 Pre-Tool 和 Post-Tool 挂钩,支持基于规则和基于审批的权限决策 |
223
+ | `StreamingToolExecutor.ts` | — | **流式工具执行器** — 管理并发工具执行队列,支持工具状态追踪(queued / executing / completed / yielded),支持进度消息即时推流 |
224
+ | `toolOrchestration.ts` | — | **工具编排** — 高级工具调度与编排逻辑 |
225
+
226
+ ### 工具执行流程
227
+
228
+ 1. **Pre-Tool Hooks**: 权限检查、规则匹配、审批流程
229
+ 2. **工具调用**: 查找工具定义 -> 执行 -> 收集结果
230
+ 3. **Post-Tool Hooks**: 结果处理、日志记录、统计更新
231
+ 4. **Streaming**: 支持并发工具执行,进度实时推送
docs/tools/overview.md ADDED
@@ -0,0 +1,254 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # AI 工具系统
2
+
3
+ ## Tool 抽象
4
+
5
+ 所有 AI 可调用的工具(Tool)都通过 **`buildTool()`** 框架构建。每个工具定义以下核心属性:
6
+
7
+ ```
8
+ Tool<Input, Output, Progress>
9
+ ```
10
+
11
+ ### 核心字段
12
+
13
+ | 字段 | 类型 | 说明 |
14
+ |------|------|------|
15
+ | `name` | `string` | 工具名称,用于 API 调用标识 |
16
+ | `aliases` | `string[]` | 可选的别名,用于向后兼容 |
17
+ | `searchHint` | `string` | 3-10 词的关键词提示,辅助 ToolSearch 匹配 |
18
+ | `inputSchema` | `Zod Schema` | 输入参数的 Zod 校验 schema |
19
+ | `inputJSONSchema` | `object` | 可选的 JSON Schema 直接输出(MCP 工具使用) |
20
+ | `outputSchema` | `Zod Schema` | 输出参数的 Zod schema |
21
+ | `call()` | 函数 | 工具的核心执行逻辑 |
22
+ | `description()` | 函数 | 工具的描述字符串 |
23
+ | `prompt()` | 函数 | 返回工具在系统提示词中的内容 |
24
+ | `isEnabled()` | 函数 | 工具是否启用(默认 true) |
25
+ | `isConcurrencySafe()` | 函数 | 是否支持并发执行(默认 false) |
26
+ | `isReadOnly()` | 函数 | 是否为只读操作(默认 false) |
27
+ | `isDestructive()` | 函数 | 是否为破坏性操作(删除、覆盖、发送;默认 false) |
28
+ | `shouldDefer` | `boolean` | 是否延迟加载(配合 ToolSearch 使用) |
29
+ | `alwaysLoad` | `boolean` | 是否始终加载(即使在 ToolSearch 模式下) |
30
+ | `maxResultSizeChars` | `number` | 结果超过此大小时持久化到磁盘 |
31
+ | `strict` | `boolean` | 是否启用工具调用严格模式 |
32
+ | `checkPermissions()` | 函数 | 检查用户权限 |
33
+ | `validateInput()` | 函数 | 输入值校验逻辑 |
34
+ | `renderToolUseMessage()` | 函数 | 渲染工具调用 UI |
35
+ | `renderToolResultMessage()` | 函数 | 渲染工具结果 UI |
36
+ | `renderToolUseProgressMessage()` | 函数 | 渲染执行进度 UI |
37
+ | `userFacingName()` | 函数 | 面向用户显示的名称 |
38
+ | `getActivityDescription()` | 函数 | 进度提示文本 |
39
+ | `mapToolResultToToolResultBlockParam()` | 函数 | 将结果映射为 API 格式 |
40
+
41
+ ### buildTool() 默认值
42
+
43
+ `buildTool()` 提供以下安全默认值:
44
+
45
+ | 方法 | 默认值 |
46
+ |------|--------|
47
+ | `isEnabled()` | `true` |
48
+ | `isConcurrencySafe()` | `false`(默认不支持并发) |
49
+ | `isReadOnly()` | `false`(默认可能写入) |
50
+ | `isDestructive()` | `false` |
51
+ | `checkPermissions()` | `{ behavior: 'allow', updatedInput: input }`(放行) |
52
+ | `toAutoClassifierInput()` | `''`(跳过分类器) |
53
+ | `userFacingName()` | `name` |
54
+
55
+ 相关文件:`/home/yuki/Code/Agent/VersperClaw/src/Tool.ts`
56
+
57
+ ---
58
+
59
+ ## 注册机制
60
+
61
+ ### getTools() 流程
62
+
63
+ `src/tools.ts` 的 **`getTools()`** 函数是工具注册的核心入口:
64
+
65
+ ```
66
+ getTools(permissionContext) → Tool[]
67
+ ```
68
+
69
+ 执行流程:
70
+
71
+ 1. **`CLAUDE_CODE_SIMPLE` 模式**:仅返回 BashTool、FileReadTool、FileEditTool(极简模式)
72
+ 2. **`getAllBaseTools()`**:收集所有内置工具,按 feature flag 和条件编译
73
+ 3. **`filterToolsByDenyRules()`**:检查 deny rules,过滤被禁止的工具
74
+ 4. **`REPL 模式过滤`**:当 REPL 启用时,隐藏原始工具(`REPL_ONLY_TOOLS`)
75
+ 5. **`isEnabled()` 过滤**:逐个检查工具是否启用
76
+
77
+ ### assembleToolPool()
78
+
79
+ `assembleToolPool()` 是内置工具与 MCP 工具的汇聚函数:
80
+
81
+ 1. 调用 `getTools()` 获取内置工具
82
+ 2. 通过 `filterToolsByDenyRules()` 过滤 MCP 工具
83
+ 3. 按名称去重(内置工具优先)
84
+ 4. 按名称排序(保持 prompt cache 稳定性)
85
+
86
+ ### getMergedTools()
87
+
88
+ 单纯合并内置工具与 MCP 工具(不去重、不排序),用于工具搜索阈值计算等场景。
89
+
90
+ 相关文件:`/home/yuki/Code/Agent/VersperClaw/src/tools.ts`
91
+
92
+ ---
93
+
94
+ ## 执行引擎
95
+
96
+ ### runToolUse()
97
+
98
+ `src/services/tools/toolExecution.ts` 的 **`runToolUse()`** 是工具调用的入口点,接收 `ToolUseBlock` 并返回异步生成器。
99
+
100
+ 执行生命周期:
101
+
102
+ ```
103
+ runToolUse(toolUse, assistantMessage, canUseTool, toolUseContext)
104
+ → AsyncGenerator<MessageUpdateLazy>
105
+ ```
106
+
107
+ ### 完整生命周期
108
+
109
+ ```
110
+ LLM 请求工具调用
111
+
112
+ ├─ 1. 查找工具 ── findToolByName()
113
+ │ ├─ 找到 → 继续
114
+ │ └─ 未找到 → 尝试别名匹配 → 返回"工具不存在"错误
115
+
116
+ ├─ 2. 输入校验 ── inputSchema.safeParse(input)
117
+ │ ├─ 成功 → 继续
118
+ │ └─ 失败 → 返回 Zod 校验错误(含 ToolSearch schema 未发送提示)
119
+
120
+ ├─ 3. 自定义校验 ── validateInput()
121
+ │ ├─ 通过 → 继续
122
+ │ └─ 拒绝 → 返回自定义错误消息
123
+
124
+ ├─ 4. PreToolUse Hooks ── runPreToolUseHooks()
125
+ │ ├─ 产生消息(进度、附件)
126
+ │ ├─ 产生 hookPermissionResult(提前决定权限)
127
+ │ ├─ 产生 hookUpdatedInput(修改输入)
128
+ │ └─ 产生 stop(钩子要求停止执行)
129
+
130
+ ├─ 5. 权限检查 ── resolveHookPermissionDecision()
131
+ │ ├─ permission mode 决定交互方式
132
+ │ ├─ 权限缓存 / alwaysAllow / alwaysDeny / alwaysAsk
133
+ │ ├─ 用户交互弹窗(default 模式)
134
+ │ ├─ 自动分类器(auto 模式)
135
+ │ └─ 结果: allow / reject / ask
136
+
137
+ ├─ 6. 工具执行 ── tool.call()
138
+ │ ├─ 执行核心逻辑
139
+ │ ├─ 发送进度通知 (onProgress)
140
+ │ └─ 返回 ToolResult
141
+
142
+ ├─ 7. 结果处理 ── mapToolResultToToolResultBlockParam()
143
+ │ ├─ 大结果持久化到磁盘
144
+ │ └─ 生成预览
145
+
146
+ ├─ 8. PostToolUse Hooks ── runPostToolUseHooks()
147
+ │ ├─ MCP 工具: 可修改 toolOutput
148
+ │ └─ 非 MCP 工具: 添加附件消息
149
+
150
+ └─ 9. 返回结果 ── 生成 MessageUpdateLazy
151
+ ├─ 成功: tool_result 消息
152
+ ├─ 错误: tool_use_error 消息
153
+ └─ 中断: tool_result_stop 消息
154
+ ```
155
+
156
+ ---
157
+
158
+ ## 权限系统
159
+
160
+ 权限系统基于 `permission mode` 决定工具调用是否需要用户确认:
161
+
162
+ ### Permission Mode
163
+
164
+ | 模式 | 说明 |
165
+ |------|------|
166
+ | `default` | 默认模式,敏感操作需用户确认 |
167
+ | `acceptEdits` | 自动接受文件编辑类操作 |
168
+ | `bypassPermissions` | 绕过所有权限检查 |
169
+ | `plan` | 规划模式,限制工具使用 |
170
+ | `auto` | 自动模式,分类器决定权限 |
171
+
172
+ ### 权限规则覆盖
173
+
174
+ 根据 `ToolPermissionContext`,系统支持:
175
+
176
+ - **alwaysAllowRules**: 始终允许的规则(按工具名称、文件路径模式、shell 命令前缀)
177
+ - **alwaysDenyRules**: 始终拒绝的规则
178
+ - **alwaysAskRules**: 始终询问的规则
179
+ - **denyRules**: 在工具注册阶段过滤掉的工具
180
+
181
+ ### 权限决定来源(OTel `source` 词汇)
182
+
183
+ | 来源 | 说明 |
184
+ |------|------|
185
+ | `user_temporary` | 用户会话级临时允许 |
186
+ | `user_permanent` | 用户永久允许(存盘) |
187
+ | `user_reject` | 用户拒绝 |
188
+ | `hook` | 钩子系统决定 |
189
+ | `config` | 配置/预设规则决定 |
190
+
191
+ ---
192
+
193
+ ## 进度通知系统
194
+
195
+ 工具执行过程中通过 `onProgress` 回调发送进度通知,不同类型的工具有不同的进度类型:
196
+
197
+ | 进度类型 | 说明 | 对应工具 |
198
+ |----------|------|----------|
199
+ | `BashProgress` | Shell 命令执行进度 | BashTool |
200
+ | `AgentProgress` | 子代理执行进度 | AgentTool |
201
+ | `MCPProgress` | MCP 工具调用进度 | MCP 工具 |
202
+ | `SkillToolProgress` | 技能执行进度 | SkillTool |
203
+ | `TaskOutputProgress` | 后台任务输出进度 | TaskOutputTool |
204
+ | `WebSearchProgress` | 网络搜索进度 | WebSearchTool |
205
+ | `REPLToolProgress` | REPL 工具进度 | REPLTool |
206
+ | `HookProgress` | 钩子执行进度 | Hooks |
207
+
208
+ 相关文件:
209
+ - `/home/yuki/Code/Agent/VersperClaw/src/services/tools/toolExecution.ts` — 执行引擎
210
+ - `/home/yuki/Code/Agent/VersperClaw/src/types/tools.ts` — 进度类型定义
211
+ - `/home/yuki/Code/Agent/VersperClaw/src/hooks/toolPermission/` — 权限钩子
212
+ - `/home/yuki/Code/Agent/VersperClaw/src/utils/permissions/` — 权限工具函数
213
+
214
+ ---
215
+
216
+ ## 关键工具详述
217
+
218
+ ### AgentTool
219
+
220
+ 执行子代理任务的核心工具。
221
+
222
+ - **输入参数**: `description`, `prompt`, `subagent_type`, `model`, `run_in_background`, `name`, `team_name`, `mode`, `isolation`, `cwd`
223
+ - **行为**: 启动一个独立的子代理会话,可指定代理类型、模型、隔离方式
224
+ - **权限**: 可能需要用户确认
225
+ - **进度**: `AgentProgress` 类型
226
+
227
+ ### BashTool
228
+
229
+ 在本地 Shell 中执行命令。
230
+
231
+ - **输入参数**: `command`, `timeout`, `description`, `run_in_background`, `dangerouslyDisableSandbox`
232
+ - **行为**: 执行任意 shell 命令,支持超时控制、后台运行、沙箱模式
233
+ - **权限**: 按命令前缀匹配权限规则
234
+ - **进度**: 超过 2 秒显示 `BashProgress`
235
+ - **自动分类**: search/read/list 命令会折叠显示
236
+
237
+ ### Read / Edit / Write
238
+
239
+ 三大文件操作工具:
240
+
241
+ - **FileReadTool**: 读取文件内容,支持 PDF、图片、Jupyter Notebook
242
+ - **FileEditTool**: 精确替换文件中的字符串,支持 diff 显示和 git diff 跟踪
243
+ - **FileWriteTool**: 创建或覆盖写入文件内容,包含文件变更追踪
244
+
245
+ ### WebSearch / WebFetch
246
+
247
+ - **WebSearchTool**: 使用 Tavily API 或本地 SearXNG 进行网络搜索
248
+ - **WebFetchTool**: 抓取 URL 内容并应用 prompt 处理(提取、总结)
249
+
250
+ 相关文件:
251
+ - `/home/yuki/Code/Agent/VersperClaw/src/Tool.ts` — Tool 类型与 buildTool 框架
252
+ - `/home/yuki/Code/Agent/VersperClaw/src/tools.ts` — 工具注册与过滤
253
+ - `/home/yuki/Code/Agent/VersperClaw/src/services/tools/toolExecution.ts` — 执行引擎
254
+ - `/home/yuki/Code/Agent/VersperClaw/src/services/tools/toolHooks.ts` — 工具钩子
docs/tools/tool-reference.md ADDED
@@ -0,0 +1,186 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 工具参考大全
2
+
3
+ > 完整收录 VersperClaw 中所有 AI 可调用的工具(约 60+),按功能分类。
4
+
5
+ ---
6
+
7
+ ## 核心文件操作
8
+
9
+ | 工具名称 | 分类 | 用途描述 | 关键参数 |
10
+ |----------|------|----------|----------|
11
+ | **Read** (FileReadTool) | 核心 | 读取文件内容,支持 PDF、图片、Jupyter Notebook。自动推断文件编码和语言 | `file_path` (必填), `offset`, `limit`, `pages` (PDF), `stop_sequence` |
12
+ | **Edit** (FileEditTool) | 核心 | 对现有文件执行精确字符串替换。支持 diff 显示、git diff 跟踪、行尾风格检测 | `file_path` (必填), `old_string` (必填), `new_string` (必填), `replace_all`, `is_undo` |
13
+ | **Write** (FileWriteTool) | 核心 | 创建新文件或覆盖写文件。不支持替换编辑(用途与 Edit 互补) | `file_path` (必填), `content` (必填) |
14
+ | **Glob** (GlobTool) | 核心 | 使用 glob 模式快速搜索文件名。支持 `.gitignore` 排除规则 | `pattern` (必填), `path` (可选) |
15
+ | **Grep** (GrepTool) | 核心 | 使用正则表达式搜索文件内容。基于 ripgrep,支持多种输出模式 | `pattern` (必填), `path` (可选), `glob`, `output_mode`, `-B`, `-A`, `-C`, `-i`, `type`, `head_limit`, `multiline` |
16
+ | **NotebookEdit** (NotebookEditTool) | 核心 | 编辑 Jupyter Notebook (.ipynb) 文件的单元格。支持 replace / insert / delete 模式 | `notebook_path` (必填), `cell_id`, `new_source`, `cell_type`, `edit_mode` |
17
+
18
+ ---
19
+
20
+ ## 命令执行
21
+
22
+ | 工具名称 | 分类 | 用途描述 | 关键参数 |
23
+ |----------|------|----------|----------|
24
+ | **Bash** (BashTool) | 核心 | 执行 Shell 命令。支持超时控制、后台执行、沙箱模式。自动检测 search/read 类命令以折叠显示 | `command` (必填), `timeout` (可选), `description` (可选), `run_in_background` (可选), `dangerouslyDisableSandbox` (可选) |
25
+ | **Agent** (AgentTool) | 核心 | 启动独立的子代理执行子任务。支持指定代理类型、模型、隔离方式、团队协作 | `description` (必填), `prompt` (必填), `subagent_type`, `model`, `run_in_background`, `name`, `team_name`, `mode`, `isolation` (worktree/remote), `cwd` |
26
+ | **PowerShell** (PowerShellTool) | 命令 | Windows PowerShell 命令执行。仅在 Windows 平台可用 | `command` (必填), `timeout`, `description`, `run_in_background` |
27
+ | **TaskStop** (TaskStopTool) | 命令 | 停止正在运行的后台任务。继承自废弃的 KillShell 工具 | `task_id` (可选), `shell_id` (废弃) |
28
+ | **TaskOutput** (TaskOutputTool) | 命令 | 获取后台任务的执行输出。支持阻塞等待和超时控制 | `task_id` (必填), `block` (默认 true), `timeout` (默认 30000ms) |
29
+
30
+ ---
31
+
32
+ ## 搜索
33
+
34
+ | 工具名称 | 分类 | 用途描述 | 关键参数 |
35
+ |----------|------|----------|----------|
36
+ | **WebSearch** (WebSearchTool) | 搜索 | 执行网络搜索。支持 Tavily API 和本地 SearXNG 两种后端(优先本地 SearXNG) | `query` (必填, 最少 2 字符) |
37
+ | **WebFetch** (WebFetchTool) | 搜索 | 抓取指定 URL 的内容,并对内容执行 prompt 处理(提取、总结等) | `url` (必填), `prompt` (必填) |
38
+ | **ToolSearch** (ToolSearchTool) | 搜索 | 搜索延迟加载的工具。当 ToolSearch 启用时,部分工具的 schema 不会随初始提示发送,需通过此工具查询后再调用 | `query` (必填, 支持 "select:name" 精确选择), `max_results` (默认 5) |
39
+
40
+ ---
41
+
42
+ ## MCP (Model Context Protocol)
43
+
44
+ | 工具名称 | 分类 | 用途描述 | 关键参数 |
45
+ |----------|------|----------|----------|
46
+ | **ListMcpResources** (ListMcpResourcesTool) | MCP | 列出已连接 MCP 服务器提供的可用资源 | `server` (可选, 过滤) |
47
+ | **ReadMcpResource** (ReadMcpResourceTool) | MCP | 读取 MCP 资源内容。支持文本和二进制内容(二进制会保存到磁盘) | `server` (必填), `uri` (必填) |
48
+ | **MCPTool** (动态) | MCP | 由 MCP 服务器动态注册的工具。名称格式 `mcp__server__tool`。数量和功能取决于已连接的 MCP 服务器 | 由 MCP 服务器定义 |
49
+ | **McpAuth** (MCP) | MCP | MCP 服务器授权管理。当工具调用返回 McpAuthError 时触发重授权流程 | 由 MCP 服务器定义 |
50
+
51
+ ---
52
+
53
+ ## 任务管理 (Todo V2)
54
+
55
+ | 工具名称 | 分类 | 用途描述 | 关键参数 |
56
+ |----------|------|----------|----------|
57
+ | **TaskCreate** (TaskCreateTool) | 任务 | 创建新的任务项。支持设置负责人(owner)、依赖关系、元数据 | `subject` (必填), `description` (必填), `activeForm`, `metadata` |
58
+ | **TaskGet** (TaskGetTool) | 任务 | 通过 ID 获取单个任务的详细信息 | `taskId` (必填) |
59
+ | **TaskUpdate** (TaskUpdateTool) | 任务 | 更新任务的状态、描述、负责人、依赖关系、元数据。支持标记为 "deleted" 删除 | `taskId` (必填), `subject`, `description`, `status`, `addBlocks`, `addBlockedBy`, `owner`, `metadata` |
60
+ | **TaskList** (TaskListTool) | 任务 | 列出所有任务的概览(ID、标题、状态、负责人、阻塞关系) | 无参数 |
61
+ | **TodoWrite** (TodoWriteTool) | 任务 | (遗留) 会话级任务清单管理。V1 版本,当 Todo V2 未启用时生效 | `todos` (必填) |
62
+
63
+ ---
64
+
65
+ ## 团队与多代理
66
+
67
+ | 工具名称 | 分类 | 用途描述 | 关键参数 |
68
+ |----------|------|----------|----------|
69
+ | **TeamCreate** (TeamCreateTool) | 团队 | 创建新团队并设置团队领导。支持指定代理类型,自动生成团队文件 | `team_name` (必填), `description`, `agent_type` |
70
+ | **TeamDelete** (TeamDeleteTool) | 团队 | 删除已存在的团队 | `team_id` (必填) |
71
+ | **SendMessage** (SendMessageTool) | 团队 | 向指定队友发送消息。支持普通文本和结构化消息(关闭请求/响应、计划审批等) | `to` (必填), `message` (必填), `type`, `request_id` |
72
+ | **ListPeers** (ListPeersTool) | 团队 | 列出本地 UDS 对等节点(统一数据空间连接的对等体) | 无参数 |
73
+
74
+ ---
75
+
76
+ ## 规划与工作树
77
+
78
+ | 工具名称 | 分类 | 用途描述 | 关键参数 |
79
+ |----------|------|----------|----------|
80
+ | **EnterPlanMode** (EnterPlanModeTool) | 规划 | 进入规划模式。在执行复杂任务前,可用于探索和设计方案 | 无参数 |
81
+ | **ExitPlanMode** (ExitPlanModeV2Tool) | 规划 | 退出规划模式。提交最终计划并恢复为之前的权限模式 | `plan` (必填), `plan_approval`, `output`, `mode` |
82
+ | **EnterWorktree** (EnterWorktreeTool) | 规划 | 创建隔离的 git worktree 并将会话切换到其中。用于安全地进行实验性修改 | `name` (可选) |
83
+ | **ExitWorktree** (ExitWorktreeTool) | 规划 | 退出 git worktree 并返回原工作目录。支持 keep(保留)或 remove(删除) | `action` (必填, keep/remove), `discard_changes` |
84
+
85
+ ---
86
+
87
+ ## 技能
88
+
89
+ | 工具名称 | 分类 | 用途描述 | 关键参数 |
90
+ |----------|------|----------|----------|
91
+ | **Skill** (SkillTool) | 技能 | 执行 Markdown 定义的技能。技能可以是打包的、用户自定义的、插件提供的或 MCP 提供的 | 参数取决于具体技能 |
92
+ | **DiscoverSkills** (DiscoverSkillsTool) | 技能 | 发现新的可用技能。扫描技能目录并返回当前可用的技能列表 | 无参数 |
93
+
94
+ ---
95
+
96
+ ## 目标系统 (Goal)
97
+
98
+ | 工具名称 | 分类 | 用途描述 | 关键参数 |
99
+ |----------|------|----------|----------|
100
+ | **GoalCreate** (GoalCreateTool) | 目标 | 创建新的会话目标。当当前无活跃目标时生效 | `objective` (必填) |
101
+ | **GoalGet** (GoalGetTool) | 目标 | 查看当前活跃目标的状态、经过时间、续用次数 | 无参数 |
102
+ | **GoalUpdate** (GoalUpdateTool) | 目标 | 声明目标完成或受阻。需提供原因说明 | `goal_id` (必填), `status` (必填, complete/blocked), `reason` (必填) |
103
+
104
+ ---
105
+
106
+ ## Friend 虚拟宠物
107
+
108
+ | 工具名称 | 分类 | 用途描述 | 关键参数 |
109
+ |----------|------|----------|----------|
110
+ | **FriendEmotion** (FriendEmotionTool) | Friend | 控制 VRM 虚拟形象的表情和情绪状态。设置情绪类型、强度,调整自身心情指数 | `emotion` (必填, 如 happy/sad/angry), `intensity` (0-1, 默认 1), `mood_delta` (-3 到 +3) |
111
+ | **FriendScreenObserve** (FriendScreenObserveTool) | Friend | 捕获当前桌面屏幕截图,供 LLM "观察" 用户桌面环境。返回图片路径 | 无参数 |
112
+
113
+ ---
114
+
115
+ ## 特殊工具
116
+
117
+ | 工具名称 | 分类 | 用途描述 | 关键参数 |
118
+ |----------|------|----------|----------|
119
+ | **AskUserQuestion** (AskUserQuestionTool) | 特殊 | 向用户提出需要即时回答的问题 | 取决于实现 |
120
+ | **Config** (ConfigTool) | 特殊 | 读取或修改 Claude Code 配置设置(ant 内部版本) | `setting` (必填), `value` (可选) |
121
+ | **Sleep** (SleepTool) | 特殊 | 让代理休眠一段时间。用于延迟执行或等待条件成熟 | 取决于实现 |
122
+ | **CronCreate** (CronCreateTool) | 特殊 | 创建定时触发任务(cron 调度) | 取决于实现 |
123
+ | **CronDelete** (CronDeleteTool) | 特殊 | 删除已存在的 cron 任务 | 取决于实现 |
124
+ | **CronList** (CronListTool) | 特殊 | 列出所有已注册的 cron 任务 | 取决于实现 |
125
+ | **RemoteTrigger** (RemoteTriggerTool) | 特殊 | 远程触发任务执行 | 取决于实现 |
126
+ | **Monitor** (MonitorTool) | 特殊 | 监控资源或任务状态 | 取决于实现 |
127
+ | **Brief** (BriefTool) | 特殊 | 生成简报输出。在 KAIROS/BRIEF 模式下可用 | 取决于实现 |
128
+ | **Snip** (SnipTool) | 特殊 | 强制截断历史上下文。HISTORY_SNIP feature 控制 | 取决于实现 |
129
+ | **SubscribePR** (SubscribePRTool) | 特殊 | 订阅 GitHub PR 的通知 | 取决于实现 |
130
+ | **PushNotification** (PushNotificationTool) | 特殊 | 发送推送通知 | 取决于实现 |
131
+ | **SendUserFile** (SendUserFileTool) | 特殊 | 向用户发送文件 | 取决于实现 |
132
+
133
+ ---
134
+
135
+ ## Workflow
136
+
137
+ | 工具名称 | 分类 | 用途描述 | 关键参数 |
138
+ |----------|------|----------|----------|
139
+ | **WorkflowTool** (WorkflowTool) | Workflow | 执行多步骤工作流脚本。工作流由 YAML/JSON 定义,支持顺序执行、条件分支、变量传递 | ��决于工作流定义 |
140
+
141
+ ---
142
+
143
+ ## Ant 内部工具
144
+
145
+ 以下工具仅在 `USER_TYPE=ant` 的内部构建中可用,公开构建中被过滤:
146
+
147
+ | 工具名称 | 分类 | 用途描述 |
148
+ |----------|------|----------|
149
+ | **REPLTool** | Ant 内部 | REPL 模式的 VM 执行器。在 REPL 模式下包装 Bash/Read/Edit 等工具 |
150
+ | **TungstenTool** | Ant 内部 | Tungsten 内部工具 |
151
+ | **SuggestBackgroundPRTool** | Ant 内部 | 后台 PR 建议工具 |
152
+ | **CtxInspectTool** | Ant 内部 | 上下文折叠检查(CONTEXT_COLLAPSE) |
153
+ | **TerminalCaptureTool** | Ant 内部 | 终端面板捕获(TERMINAL_PANEL) |
154
+ | **OverflowTestTool** | Ant 内部 | 溢出测试工具 |
155
+ | **VerifyPlanExecutionTool** | Ant 内部 | 计划执行验证 |
156
+ | **TestingPermissionTool** | Ant 内部 | 测试权限工具(仅 NODE_ENV=test) |
157
+ | **WebBrowserTool** | Ant 内部 | 网页浏览器工具 |
158
+
159
+ ---
160
+
161
+ ## feature-flag 条件编译对照
162
+
163
+ 以下工具根据 feature flag 条件编译,在公开构建中通过 bun build DCE 消除:
164
+
165
+ | Feature Flag | 包含的工具 |
166
+ |--------------|-----------|
167
+ | `PROACTIVE` / `KAIROS` | SleepTool |
168
+ | `AGENT_TRIGGERS` | CronCreateTool, CronDeleteTool, CronListTool |
169
+ | `AGENT_TRIGGERS_REMOTE` | RemoteTriggerTool |
170
+ | `MONITOR_TOOL` | MonitorTool |
171
+ | `KAIROS` | SendUserFileTool, PushNotificationTool |
172
+ | `KAIROS_PUSH_NOTIFICATION` | PushNotificationTool |
173
+ | `KAIROS_GITHUB_WEBHOOKS` | SubscribePRTool |
174
+ | `WORKFLOW_SCRIPTS` | WorkflowTool |
175
+ | `COORDINATOR_MODE` | 协调模式相关工具 |
176
+ | `HISTORY_SNIP` | SnipTool |
177
+ | `UDS_INBOX` | ListPeersTool |
178
+ | `ENABLE_LSP_TOOL` | LSPTool |
179
+ | `WEB_BROWSER_TOOL` | WebBrowserTool |
180
+ | `OVERFLOW_TEST_TOOL` | OverflowTestTool |
181
+ | `CONTEXT_COLLAPSE` | CtxInspectTool |
182
+ | `TERMINAL_PANEL` | TerminalCaptureTool |
183
+
184
+ ---
185
+
186
+ > 注: 本参考基于 `/home/yuki/Code/Agent/VersperClaw/src/tools.ts` 的 `getAllBaseTools()` 函数。MCP 工具的完整列表取决于用户配置的 MCP 服务器,未在此表中逐一列出。
docs/voice/overview.md ADDED
@@ -0,0 +1,279 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # 语音系统
2
+
3
+ ## 概述
4
+
5
+ VersperClaw 的语音系统支持**语音转文字(STT, Speech-to-Text)** 和 **文字转语音(TTS, Text-to-Speech)** 功能。语音录制使用底层音频捕获库(cpal),含有 SoX 和 arecord 回退方案。语音识别支持多个提供商,包括云端 API 和本地模型;语音合成同样支持多个引擎,涵盖 Edge TTS 和 DashScope TTS。
6
+
7
+ > 注意:部分功能受构建特性门控(feature gate)限制,仅在 ant-internal 构建中可用。
8
+
9
+ ---
10
+
11
+ ## 语音录制(Voice Recording)
12
+
13
+ 核心文件:`src/services/voice.ts`
14
+
15
+ 录音模块使用分层策略:
16
+
17
+ | 层级 | 方案 | 适用平台 |
18
+ |------|------|----------|
19
+ | 原生 | `audio-capture-napi`(cpal) | macOS, Linux, Windows |
20
+ | 回退 1 | SoX `rec` | Linux (有 SoX) |
21
+ | 回退 2 | `arecord` (ALSA) | Linux (有 ALSA) |
22
+
23
+ 关键特征:
24
+
25
+ - **懒加载原生模块**:`loadAudioNapi()` 在首次按键时异步加载 `audio-capture-napi`,避免启动时阻塞(dlopen 可能耗时 1-8 秒)。
26
+ - **静音检测**:使用 SoX 或 arecord 时启用静音检测(阈值 3%,持续时间 2 秒),自动结束录音。
27
+ - **录音常量**:采样率 16000 Hz,单声道,16-bit PCM。
28
+ - **依赖检查**:`checkVoiceDependencies()` 探测可用性,显示每个方案的可用状态。
29
+
30
+ ---
31
+
32
+ ## STT 提供商(Speech-to-Text)
33
+
34
+ ### 抽象接口
35
+
36
+ 文件:`src/services/voice/providers.ts`
37
+
38
+ ```typescript
39
+ interface TranscriptionProvider {
40
+ name: string
41
+ transcribe(wavPath: string, language?: string): Promise<TranscriptionResult>
42
+ }
43
+ ```
44
+
45
+ 所有 STT 提供商实现此接口,`LocalWhisperSTT` 和 `DoubaoSTTProvider` 是内置实现。
46
+
47
+ ---
48
+
49
+ ### 1. Groq Whisper(云端)
50
+
51
+ 文件:`src/services/voice/groqSTT.ts`
52
+
53
+ 通过官方 `groq-sdk` npm 包调用 Groq LPU API,使用 Whisper 模型。
54
+
55
+ **模型回退**:
56
+ 1. 优先使用 `whisper-large-v3`
57
+ 2. 遇到 429(限速)或 5xx(服务端错误)时自动回退到 `whisper-large-v3-turbo`
58
+ 3. 其他错误(4xx)直接抛出,不重试
59
+
60
+ **API 密钥解析**(优先级从高到低):
61
+ 1. 显式传入的 `apiKey` 参数
62
+ 2. `friend.json` 中的 `groqApiKey` 配置
63
+ 3. 环境变量 `GROQ_API_KEY`
64
+ 4. `~/.claude/settings.json` 中的 `env.groqApiKey`
65
+
66
+ **核心流程**:
67
+ - `connectGroqStream()` 返回 `VoiceStreamConnection` 接口
68
+ - 接收 PCM 音频块并缓冲
69
+ - `finalize()` 时将 PCM 转换为 WAV(`pcmToWav()`,16-bit 单声道,16000 Hz)
70
+ - 通过 `File` API 上传 WAV 到 Groq
71
+
72
+ ---
73
+
74
+ ### 2. Local Whisper(本地模型)
75
+
76
+ 文件:`src/services/voice/whisperSTT.ts`
77
+
78
+ 基于 Python `openai-whisper` 的本地部署方案。
79
+
80
+ **架构**:
81
+ - 启动一个长期运行的 Python 子进程(`whisper_server.py`)
82
+ - 通过 stdin/stdout 的 JSON 行协议通信
83
+ - 支持预加载模型(`preloadWhisperModel()`)
84
+
85
+ **通信协议**:
86
+ - `{"type":"load","model":"small"}` —— 加载模型
87
+ - `{"type":"transcribe","wav":"/path/to/audio.wav","language":"en"}` —— 转写
88
+ - 服务端以 `{"type":"result","text":"...","language":"..."}` 或 `{"type":"error","message":"..."}` 响应
89
+
90
+ **进程管理**:
91
+ - 进程崩溃后自动重启
92
+ - 30 秒超时保护
93
+ - 临时文件自动清理
94
+
95
+ **可用性检查**:使用 Python `importlib.util.find_spec("whisper")` 探测 whisper 模块是否可导入,避免加载 PyTorch 的耗时。
96
+
97
+ ---
98
+
99
+ ### 3. Anthropic Voice Stream(WebSocket)
100
+
101
+ 文件:`src/services/voiceStreamSTT.ts`
102
+
103
+ 通过 Anthropic 的 voice_stream WebSocket 端点传输语音,仅在 ant-internal 构建中可用(由 `feature('VOICE_MODE')` 门控)。
104
+
105
+ **WebSocket 协议**:
106
+ - 端点:`wss://api.anthropic.com/api/ws/speech_to_text/voice_stream`
107
+ - 认证:OAuth Bearer Token(与 Claude Code 共享凭证)
108
+ - 消息类型:
109
+ - `KeepAlive` —— 每 8 秒发送保持连接
110
+ - `CloseStream` —— 结束流
111
+ - 服务端推送 `TranscriptText`、`TranscriptEndpoint`、`TranscriptError`
112
+
113
+ **连接生命周期**:
114
+ 1. `connectVoiceStream()` 建立 WebSocket 连接
115
+ 2. `send(audioChunk)` 发送二进制音频帧
116
+ 3. `finalize()` 发送 `CloseStream`,等待服务端返回 `TranscriptEndpoint`
117
+ 4. `FinalizeSource` 枚举标识解析路径:`post_closestream_endpoint`、`no_data_timeout`、`safety_timeout`、`ws_close`、`ws_already_closed`
118
+
119
+ **Deepgram Nova 3 门控**:通过 GrowthBook 特性标记 `deepgram_nova_3_gate` 控制是否使用 Deepgram Nova 3 模型。
120
+
121
+ **Voice Keyterms**:通过查询参数传递关键词列表,提高领域术语的识别准确率。
122
+
123
+ ---
124
+
125
+ ### 4. Doubao(豆包 STT)
126
+
127
+ 文件:`src/services/voice/doubaoSTT.ts`
128
+
129
+ 此文件是一个自动生成的存根(stub),对应 ant-internal 的 `feature()` 门控模块。外部构建中所有代码路径在 DCE(死代码消除)后不会实际执行。
130
+
131
+ 存根使用 JavaScript `Proxy` 将任何属性访问、函数调用、构造操作映射到无操作(noop)处理器。导出 `connectDoubaoStream`、`normalizeLanguageForSTT` 等函数作为占位符。
132
+
133
+ `DoubaoSTTProvider`(在 `providers.ts` 中)封装了此存根的调用逻辑,通过动态导入(`import('./doubaoSTT.js')`)在运行时解析。
134
+
135
+ ---
136
+
137
+ ## TTS 提供商(Text-to-Speech)
138
+
139
+ ### 抽象接口
140
+
141
+ 文件:`src/services/voice/providers.ts`
142
+
143
+ ```typescript
144
+ interface TTSProvider {
145
+ name: string
146
+ synthesize(text: string): Promise<SynthesisResult>
147
+ }
148
+ ```
149
+
150
+ `EdgeTTSProvider` 和 `CommandTTSProvider` 是内置实现。
151
+
152
+ ---
153
+
154
+ ### 1. Edge TTS(微软神经网络语音)
155
+
156
+ 文件:`src/services/voice/providers.ts`(类 `EdgeTTSProvider`)
157
+
158
+ **实现路径一(Provider 接口)**:
159
+ - 直接调用 `edge-tts` 命令行工具
160
+ - 使用 Node.js `child_process.spawn` 执行子进程
161
+ - 通过 `--voice`、`--text`、`--write-media` 参数控制输出
162
+ - 默认语音:`en-US-AriaNeural`
163
+
164
+ **实现路径二(独立函数)**:
165
+ 文件:`src/services/voice/edgeTTS.ts`(`speakWithEdgeTTS()`)
166
+ - 调用 `scripts/speak.py` Python 脚本
167
+ - 依赖 `.venv/bin/python` 或系统 Python
168
+ - 返回标准化的 `TTSResult` 接口
169
+
170
+ **实现路径三(Friend 模块)**:
171
+ 文件:`src/friend/tts.ts`(`edgeTts()`)
172
+ - 使用 `node-edge-tts` npm 包(Node.js 原生实现,无需 Python)
173
+ - 默认语音:`zh-CN-XiaoxiaoNeural`(中文语音)
174
+ - 输出为 MP3 文件
175
+
176
+ **播放功能**(`src/services/voice/edgeTTS.ts` `playAudioFile()`):
177
+ | 平台 | 播放器 | 说明 |
178
+ |------|--------|------|
179
+ | macOS | `afplay` | 原生 |
180
+ | Linux | `ffplay` | 先 `pkill` 已有进程,再启动新进程 |
181
+ | Windows | `start` | 系统默认播放器 |
182
+
183
+ ---
184
+
185
+ ### 2. Qwen DashScope TTS(通义千问语音合成)
186
+
187
+ 文件:`src/friend/tts.ts`(`qwenTts()`)
188
+
189
+ 调用阿里云 DashScope API 进行语音合成。
190
+
191
+ **API 信息**:
192
+ - 国内端点:`https://dashscope.aliyuncs.com/api/v1/services/aigc/multimodal-generation/generation`
193
+ - 国际端点:`https://dashscope-intl.aliyuncs.com/...`
194
+ - 默认模型:`qwen3-tts-flash`
195
+ - 默认语音:`Cherry`
196
+ - 认证:Bearer Token(`apiKey`)
197
+
198
+ **参数**:
199
+ - `voice`:语音角色(如 Cherry, Polly 等)
200
+ - `model`:模型版本
201
+ - `language`:语言(`zh` 或 `en`),决定端点选择和 `language_type`
202
+
203
+ **流程**:
204
+ 1. POST 请求获取合成音频的 URL
205
+ 2. 从 URL 下载音频数据
206
+ 3. 保存为 WAV 临时文件
207
+ 4. 支持 30 秒超时保护
208
+
209
+ **音频文件注册**:`registerAudioFile()` 将文件路径注册到内存映射,5 分钟后自动过期,用于跨模块引用。
210
+
211
+ ---
212
+
213
+ ### 3. Command TTS(命令模板回退)
214
+
215
+ 文件:`src/services/voice/providers.ts`(类 `CommandTTSProvider`)
216
+
217
+ 通用 shell 命令 TTS,支持模板占位符:
218
+ - `{input}` / `{input_path}` —— 输入文本文件路径
219
+ - `{output_path}` —— 输出音频文件路径
220
+
221
+ 适用于调用任意外部 TTS 命令行工具。
222
+
223
+ ---
224
+
225
+ ## Voice Stream 协议
226
+
227
+ 文件:`src/services/voiceStreamSTT.ts`
228
+
229
+ Voice Stream 是 Anthropic 的 WebSocket 协议,用于实时语音识别。
230
+
231
+ ### 消息类型
232
+
233
+ | 方向 | 类型 | 说明 |
234
+ |------|------|------|
235
+ | 客户端 → 服务端 | `KeepAlive` | 心跳,每 8 秒 |
236
+ | 客户端 → 服务端 | `CloseStream` | 结束音频流 |
237
+ | 客户端 → 服务端 | 二进制帧 | PCM 音频数据 |
238
+ | 服务端 → 客户端 | `TranscriptText` | 转写文本片段 |
239
+ | 服务端 → 客户端 | `TranscriptEndpoint` | 转写结束标记 |
240
+ | 服务端 → 客户端 | `TranscriptError` | 错误信息 |
241
+
242
+ ### FinalizeSource 枚举
243
+
244
+ `finalize()` 方法的解析路径:
245
+
246
+ | 值 | 说明 |
247
+ |----|------|
248
+ | `post_closestream_endpoint` | 正常流程:发送 CloseStream 后收到 TranscriptEndpoint |
249
+ | `no_data_timeout` | 发送 CloseStream 后 1.5 秒无响应 |
250
+ | `safety_timeout` | WebSocket 挂起超过 5 秒 |
251
+ | `ws_close` | WebSocket 连接关闭 |
252
+ | `ws_already_closed` | 已关闭的连接被重复调用 |
253
+
254
+ ### Keyterms(关键词)
255
+
256
+ 通过 WebSocket 查询参数 `keyterms` 传递关键词列表,格式为逗号分隔的 URL 编码值。关键词可提高模型对特定术语的识别准确率。
257
+
258
+ ---
259
+
260
+ ## Voice Mode(语音模式)
261
+
262
+ 语音模式是 Push-to-Talk(按住说话)的实现:
263
+
264
+ 1. **开始录音**:用户按下语音快捷键
265
+ 2. **音频采集**:底层 cpal 或回退方案开始采集 16kHz 单声道 PCM 音频
266
+ 3. **音频传输**:音频块通过 Voice Stream WebSocket 实时发送
267
+ 4. **释放停止**:用户松开快捷键,发送 CloseStream
268
+ 5. **等待转写**:接收服务端返回的 TranscriptText 和 TranscriptEndpoint
269
+ 6. **提交文本**:转写文本进入对话输入流
270
+
271
+ 当使用本地 Whisper 时,流程类似但使用子进程通信而非 WebSocket。
272
+
273
+ ---
274
+
275
+ ## 提供商注册与选择
276
+
277
+ 文件:`src/services/voice/providers.ts`
278
+
279
+ 系统通过 `TranscriptionProvider` 和 `TTSProvider` 接口实现多提供商支持。每个提供商有自己的名称(`name` 属性)和实现逻辑。选择策略在调用方(如 `useVoice` hook)中决定,根据可用性和用户配置选择合适的提供商。