feat(voice): WebSocket 端点 + 客户端↔网关协议(Phase 1 地基)

加 gorilla/websocket(无 genproto 冲突)。voice/protocol.go 定死单条 WS 的消息协议:
二进制帧=音频(上行麦克风/下行TTS),文本帧=JSON 控制/事件(ClientMsg:start/end/barge_in/
bye;ServerMsg:ready/transcript/task/speaking/tts_end/error);音频 PCM 16k 单声道。
handler/voice.go: GET /api/v1/voice/stream 升级 WS,鉴权走 AuthFromHeaderOrQuery(?token=,
WS 带不了 Bearer),会话外壳 + 协议读循环(音频帧/控制消息分派)已通,火山 ASR/TTS 客户端
在下一步挂 onAudio/onControl 的 TODO 点接入。build+vet+test 绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-21 17:17:58 +08:00
parent a28ae49b6a
commit 15f5a85612
5 changed files with 176 additions and 1 deletions
@@ -0,0 +1,54 @@
package voice
// 客户端 ↔ 网关的单条 WebSocket 消息协议(设计 VOICE_DESIGN.md D3):
// 一条连接同时承载上行音频、下行转写、下行 TTS 音频,用「帧类型 + JSON 消息类型」区分。
//
// - 二进制帧(BinaryMessage):纯音频 PCM
// · 上行 = 用户麦克风音频(喂 ASR)
// · 下行 = Agent 回答的 TTS 音频(客户端播放)
// - 文本帧(TextMessage, JSON):控制与事件(下方 ClientMsg / ServerMsg
// ClientMsg 是客户端发来的控制消息(文本帧)。音频走二进制帧,不在此。
type ClientMsg struct {
Type string `json:"type"`
// start:一轮语音开始(可带当前画布编排图,用它跑而非默认 DSL)
Graph string `json:"graph,omitempty"`
// 其它类型无额外字段:end(用户说完)、barge_in(打断,用户又开口)、bye(结束会话)
}
// 客户端消息类型。
const (
ClientStart = "start" // 一轮语音开始
ClientEnd = "end" // 用户说完(静音检测或手动结束)→ 触发任务
ClientBargeIn = "barge_in" // 打断:用户在 Agent 说话时又开口 → 停 TTS
ClientBye = "bye" // 结束整个语音会话
)
// ServerMsg 是网关下发的控制/事件消息(文本帧)。TTS 音频走二进制帧,不在此。
type ServerMsg struct {
Type string `json:"type"`
// transcriptASR 转写(Final=false 为实时部分结果,true 为最终)
Text string `json:"text,omitempty"`
Final bool `json:"final,omitempty"`
// task:转写完成、任务已提交,带 task_id 供客户端切运行视图
TaskID string `json:"task_id,omitempty"`
// error:出错文案
Msg string `json:"msg,omitempty"`
}
// 服务端消息类型。
const (
ServerReady = "ready" // 会话就绪,可以开始说话
ServerTranscript = "transcript" // ASR 转写结果(部分/最终)
ServerTask = "task" // 任务已提交(带 task_id
ServerSpeaking = "speaking" // Agent 开始出声(首段 TTS 音频将至)
ServerTTSEnd = "tts_end" // 本轮 TTS 播放完毕
ServerError = "error" // 出错
)
// 音频格式(与火山 ASR/TTS 约定,客户端按此采集/播放)。
const (
AudioSampleRate = 16000 // 上行 ASR16kHz
AudioBits = 16 // 16bit
AudioChannels = 1 // 单声道
)