chore: docs v1.0
Browse files- docs/README.md +66 -0
- docs/architecture/data-flow.md +560 -0
- docs/architecture/overview.md +213 -0
- docs/coordinator/goals-auto-mode.md +222 -0
- docs/coordinator/multi-agent.md +205 -0
- docs/friend/architecture.md +419 -0
- docs/friend/data-flow.md +369 -0
- docs/friend/emotion-map.md +195 -0
- docs/friend/frontend-3d.md +573 -0
- docs/friend/overview.md +138 -0
- docs/friend/voice-vad.md +525 -0
- docs/ink-ui/overview.md +585 -0
- docs/memory-context/context.md +194 -0
- docs/memory-context/memory.md +210 -0
- docs/remote-bridge/overview.md +199 -0
- docs/server/overview.md +185 -0
- docs/server/proxy-provider.md +135 -0
- docs/services/overview.md +231 -0
- docs/tools/overview.md +254 -0
- docs/tools/tool-reference.md +186 -0
- docs/voice/overview.md +279 -0
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)中决定,根据可用性和用户配置选择合适的提供商。
|