Feature Flag:
FEATURE_VOICE_MODE=1实现状态:完整可用(双后端:Anthropic OAuth / 豆包 ASR) 引用数:46
VOICE_MODE 实现"按键说话"(Push-to-Talk)语音输入。用户按住空格键录音,音频流式传输到 STT 后端,实时转录显示在终端中。支持两个后端:
- Anthropic STT(默认):通过 WebSocket 流式传输到 Nova 3 端点,需要 Anthropic OAuth
- 豆包 ASR(Doubao):通过
doubaoime-asr包的 AsyncGenerator 协议流式识别,使用独立凭证文件,无需 Anthropic OAuth
- Push-to-Talk:长按空格键录音,释放后自动发送
- 流式转录:录音过程中实时显示中间转录结果
- 无缝集成:转录文本直接作为用户消息提交到对话
- 双后端切换:通过
/voice命令参数选择 STT 后端,持久化到 settings.json
| 操作 | 行为 |
|---|---|
| 长按空格 | 开始录音,显示录音状态 |
| 释放空格 | 停止录音,转录结果自动提交 |
/voice |
切换语音模式开关(默认使用 Anthropic 后端) |
/voice doubao |
启用语音模式并使用豆包 ASR 后端 |
/voice anthropic |
切换回 Anthropic STT 后端 |
- 录音指示器:录音时显示红色/脉冲动画
- 中间转录:录音过程中显示 STT 实时识别文本
- 最终转录:完成后替换中间结果
文件:src/voice/voiceModeEnabled.ts
两层检查函数:
// Anthropic 后端(需要 OAuth)
isVoiceModeEnabled() = hasVoiceAuth() && isVoiceGrowthBookEnabled()
// 豆包后端 / 通用可用性检查(不需要 OAuth)
isVoiceAvailable() = isVoiceGrowthBookEnabled()- Feature Flag:
feature('VOICE_MODE')— 编译时/运行时开关 - GrowthBook Kill-Switch:
!getFeatureValue_CACHED_MAY_BE_STALE('tengu_amber_quartz_disabled', false)— 紧急关闭开关(默认 false = 未禁用) - Auth 检查(仅 Anthropic):
hasVoiceAuth()— 需要 Anthropic OAuth token(非 API key) - Provider 检查:
voiceProvider设置决定使用哪个后端,豆包后端跳过 OAuth 检查
| 模块 | 职责 |
|---|---|
src/voice/voiceModeEnabled.ts |
Feature flag + GrowthBook + Auth 三层门控 |
src/hooks/useVoice.ts |
React hook 管理录音状态和后端连接 |
src/services/voiceStreamSTT.ts |
Anthropic WebSocket 流式 STT |
src/services/doubaoSTT.ts |
豆包 ASR 适配器(AsyncGenerator → VoiceStreamConnection) |
src/commands/voice/voice.ts |
/voice 命令实现,处理后端选择和持久化 |
src/hooks/useVoiceEnabled.ts |
语音启用状态 hook,根据 provider 决定是否跳过 OAuth |
src/utils/settings/types.ts |
`voiceProvider: 'anthropic' |
用户按下空格键
│
▼
useVoice hook 激活
│
▼
macOS 原生音频 / SoX 开始录音
│
▼
WebSocket 连接到 Anthropic STT 端点
│
├──→ 中间转录结果 → 实时显示
│
▼
用户释放空格键
│
▼
停止录音,等待最终转录
│
▼
转录文本 → 插入输入框 → 自动提交
用户按下空格键
│
▼
useVoice hook 激活(检测到 voiceProvider === 'doubao')
│
▼
macOS 原生音频 / SoX 开始录音
│
▼
connectDoubaoStream() 创建 AudioChunkQueue + VoiceStreamConnection
│
├──→ onReady 立即触发(无需等待握手)
│
▼
音频数据通过 AudioChunkQueue 传入 transcribeRealtime()
│
├──→ INTERIM_RESULT → 实时显示中间转录
├──→ FINAL_RESULT → 显示最终转录
│
▼
用户释放空格键
│
▼
finalize() 立即返回(豆包在录音过程中已返回结果,无需等待)
│
▼
转录文本 → 插入输入框 → 自动提交
支持两种音频后端(两个 STT 后端共享):
- macOS 原生音频:优先使用,低延迟
- SoX(Sound eXchange):回退方案,跨平台
文件:src/services/doubaoSTT.ts
豆包后端使用适配器模式,将 doubaoime-asr 的 AsyncGenerator 协议桥接到 VoiceStreamConnection 接口:
AudioChunkQueue — push 式异步队列:
- 实现
AsyncIterable<Uint8Array>接口 push(chunk)将音频数据入队,push(null)发送结束信号- 内部维护等待者(waiting)和缓冲队列(chunks)两个状态
connectDoubaoStream() — 连接入口:
- 动态导入
doubaoime-asr(optionalDependencies) - 从
~/.claude/tts/doubao/credentials.json加载凭证 - 创建 AudioChunkQueue 和 VoiceStreamConnection
- 立即触发
onReady(避免与 useVoice 的音频缓冲死锁) finalize()立即返回(豆包在录音过程中已返回结果)- 后台 async IIFE 消费
transcribeRealtimegenerator,映射响应类型到回调
响应类型映射:
| doubaoime-asr ResponseType | 回调映射 |
|---|---|
| SESSION_STARTED | 日志记录 |
| VAD_START | 日志记录 |
| INTERIM_RESULT | onTranscript(text, false) |
| FINAL_RESULT | onTranscript(text, true) |
| ERROR | onError(errorMsg) |
| SESSION_FINISHED | 日志记录 |
文件:src/hooks/useVoice.ts
// 判断当前 provider
isDoubaoProvider() → 读取 settings.voiceProvider
// handleKeyEvent 中的可用性检查
const sttAvailable = isDoubaoProvider()
? isDoubaoAvailableSync() // 乐观检查(首次返回 true)
: isVoiceStreamAvailable() // Anthropic WebSocket 检查
// attemptConnect 中的连接函数选择
const connectFn = isDoubaoProvider()
? connectDoubaoStream
: connectVoiceStream豆包后端的特殊处理:
- 跳过
getVoiceKeyterms()调用(豆包无需关键词提示) - 跳过 Focus Mode(
if (!enabled || !focusMode || isDoubaoProvider()))
- 双后端共存:豆包后端作为独立适配器与 Anthropic 后端并存,不替换原有流程,通过
voiceProvider设置切换 - 设置持久化:
voiceProvider存储在settings.json,通过/voice命令修改,跨会话生效 - OAuth 独占(Anthropic):Anthropic 后端使用
voice_stream端点(claude.ai),仅 OAuth 用户可用 - 豆包无需 OAuth:豆包后端使用独立凭证文件,不依赖 Anthropic 认证,通过
isVoiceAvailable()放宽门控 - GrowthBook 负向门控:
tengu_amber_quartz_disabled默认false,新安装自动可用 - onReady 立即触发:豆包后端在连接建立后立即触发
onReady,避免与 useVoice 音频缓冲的时序死锁(Anthropic 需要等待 WebSocket 握手) - finalize() 立即返回:豆包在录音过程中已返回所有结果,用户抬手时无需等待处理
- 乐观可用性检查:
isDoubaoAvailableSync()在首次调用时返回true,实际导入错误在connectDoubaoStream中处理 - optionalDependencies:
doubaoime-asr作为可选依赖,安装失败不影响 Anthropic 后端
# 启用 feature
FEATURE_VOICE_MODE=1 bun run dev
# 在 REPL 中使用 Anthropic 后端
# 1. 确保已通过 OAuth 登录(claude.ai 订阅)
# 2. 输入 /voice 启用
# 3. 按住空格键说话
# 4. 释放空格键等待转录
# 在 REPL 中使用豆包 ASR 后端
# 1. 确保 doubaoime-asr 已安装(bun add doubaoime-asr)
# 2. 配置凭证文件:~/.claude/tts/doubao/credentials.json
# 3. 输入 /voice doubao 启用
# 4. 按住空格键说话
# 5. 释放空格键,转录结果即刻显示
# 切换后端
/voice doubao # 切换到豆包 ASR
/voice anthropic # 切换回 Anthropic STT
/voice # 关闭语音模式凭证文件路径:~/.claude/tts/doubao/credentials.json
{
"deviceId": "...",
"installId": "...",
"cdid": "...",
"openudid": "...",
"clientudid": "...",
"token": "..."
}| 依赖 | 说明 | 适用后端 |
|---|---|---|
| Anthropic OAuth | claude.ai 订阅登录,非 API key | Anthropic |
| GrowthBook | tengu_amber_quartz_disabled 紧急关闭 |
通用 |
| macOS 原生音频 或 SoX | 音频录制 | 通用 |
| Nova 3 STT | Anthropic 语音转文本模型 | Anthropic |
| doubaoime-asr | 豆包 ASR SDK(optionalDependencies) | 豆包 |
| 凭证文件 | ~/.claude/tts/doubao/credentials.json |
豆包 |
| 文件 | 职责 |
|---|---|
src/voice/voiceModeEnabled.ts |
三层门控逻辑 + isVoiceAvailable() |
src/hooks/useVoice.ts |
React hook(录音状态 + 后端选择 + 连接管理) |
src/hooks/useVoiceEnabled.ts |
语音启用状态 hook(按 provider 决定 OAuth 检查) |
src/services/voiceStreamSTT.ts |
Anthropic STT WebSocket 流式传输 |
src/services/doubaoSTT.ts |
豆包 ASR 适配器(AudioChunkQueue + connectDoubaoStream) |
src/commands/voice/voice.ts |
/voice 命令(开关 + 后端选择) |
src/commands/voice/index.ts |
命令注册(去除 availability 限制) |
src/utils/settings/types.ts |
voiceProvider 类型定义 |