From d6c1a787f48dedeee10aef79454ae821b1027198 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Wed, 22 Jul 2026 10:42:36 +0800 Subject: [PATCH] =?UTF-8?q?fix(voice):=20=E6=94=B9=E7=94=A8=E7=81=AB?= =?UTF-8?q?=E5=B1=B1=E3=80=8C=E6=96=B0=E7=89=88=E6=8E=A7=E5=88=B6=E5=8F=B0?= =?UTF-8?q?=E3=80=8DAPI=20Key=20=E9=89=B4=E6=9D=83(ASR=20=E6=8F=A1?= =?UTF-8?q?=E6=89=8B=E7=9C=9F=E6=9C=BA=E9=AA=8C=E9=80=9A=E2=9C=85)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 一直误用旧版 5 头方案(X-Api-App-Key+Access-Key+App-ID),火山始终 401 grant not found。 用户提供官方文档「新版本控制台」证实:新版只需**单个 X-Api-Key** 头。改对后真机联调: - frame.go setVolcAuthHeaders: X-Api-Key + X-Api-Resource-Id + X-Api-Request-Id + X-Api-Sequence:-1 (删旧 App-Key/Access-Key/App-ID/Connect-Id;去 appID 参数) - asr/tts 调用点同步;config.go 删 Config.AppID;voicecheck/voiceconfig 删 VOLC_APP_ID - 删临时诊断工具 voiceprobe 真机结果:ASR 握手✅(volc.bigasr.sauc.duration 有效);TTS 用 seed-tts-2.0 会话正常建立 (不再 resource mismatch),但暂无音频返回→双向 TTS 事件/payload 待调(下一步)。 Co-Authored-By: Claude Opus 4.8 --- sundynix-gateway/cmd/voicecheck/main.go | 1 - sundynix-gateway/cmd/voiceconfig/main.go | 1 - sundynix-gateway/internal/voice/asr.go | 4 ++-- sundynix-gateway/internal/voice/config.go | 3 +-- sundynix-gateway/internal/voice/frame.go | 29 +++++++++-------------- sundynix-gateway/internal/voice/tts.go | 4 ++-- 6 files changed, 16 insertions(+), 26 deletions(-) diff --git a/sundynix-gateway/cmd/voicecheck/main.go b/sundynix-gateway/cmd/voicecheck/main.go index acc91dd..e19cc27 100644 --- a/sundynix-gateway/cmd/voicecheck/main.go +++ b/sundynix-gateway/cmd/voicecheck/main.go @@ -28,7 +28,6 @@ import ( func main() { cfg := voice.Config{ APIKey: os.Getenv("VOLC_API_KEY"), - AppID: os.Getenv("VOLC_APP_ID"), ASRResourceID: os.Getenv("VOLC_ASR_RESOURCE_ID"), TTSResourceID: os.Getenv("VOLC_TTS_RESOURCE_ID"), TTSVoiceType: os.Getenv("VOLC_TTS_VOICE"), diff --git a/sundynix-gateway/cmd/voiceconfig/main.go b/sundynix-gateway/cmd/voiceconfig/main.go index 214bf4d..18c01b2 100644 --- a/sundynix-gateway/cmd/voiceconfig/main.go +++ b/sundynix-gateway/cmd/voiceconfig/main.go @@ -32,7 +32,6 @@ func main() { } cfg := voice.Config{ APIKey: os.Getenv("VOLC_API_KEY"), - AppID: os.Getenv("VOLC_APP_ID"), ASRResourceID: os.Getenv("VOLC_ASR_RESOURCE_ID"), TTSResourceID: os.Getenv("VOLC_TTS_RESOURCE_ID"), TTSVoiceType: os.Getenv("VOLC_TTS_VOICE"), diff --git a/sundynix-gateway/internal/voice/asr.go b/sundynix-gateway/internal/voice/asr.go index dcae15f..0aa0293 100644 --- a/sundynix-gateway/internal/voice/asr.go +++ b/sundynix-gateway/internal/voice/asr.go @@ -14,7 +14,7 @@ import ( ) // 火山引擎 V3 大模型流式语音识别(SeedASR / SAUC)客户端。协议见 voice/frame.go 与 voice-jarvis 记忆。 -// 端点固定;鉴权走**新版 API Key**(X-Api-Access-Key + 固定 App Key + App ID,见 frame.go)。 +// 端点固定;鉴权走**新版控制台 API Key**(单个 X-Api-Key,见 frame.go)。 const asrEndpoint = "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel" @@ -91,7 +91,7 @@ func StartASR(ctx context.Context, cfg Config, uid string) (*ASRSession, error) return nil, fmt.Errorf("ASR 未配置") } hdr := http.Header{} - setVolcAuthHeaders(hdr, cfg.APIKey, cfg.AppID, cfg.ASRResourceID) + setVolcAuthHeaders(hdr, cfg.APIKey, cfg.ASRResourceID) dialer := websocket.Dialer{HandshakeTimeout: 10 * time.Second} conn, resp, err := dialer.DialContext(ctx, asrEndpoint, hdr) diff --git a/sundynix-gateway/internal/voice/config.go b/sundynix-gateway/internal/voice/config.go index ac03a1e..b570b7e 100644 --- a/sundynix-gateway/internal/voice/config.go +++ b/sundynix-gateway/internal/voice/config.go @@ -8,8 +8,7 @@ import "github.com/sundynix/sundynix-shared/secrets" // Authorization(Bearer )+ X-Api-Resource-Id(区分服务,ASR 与双向 TTS 各一个)。 // 端点/音频格式(PCM 16k 单声道等)由代码固定,不入用户配置。 type Config struct { - APIKey string `json:"api_key"` // 新版 API Key(X-Api-Access-Key,密文入库) - AppID string `json:"app_id"` // 火山 App ID(X-Api-App-ID,解析账号资源授权用;V3 流式端点需要) + APIKey string `json:"api_key"` // 新版控制台 API Key(X-Api-Key,密文入库) ASRResourceID string `json:"asr_resource_id"` // 流式语音识别 X-Api-Resource-Id TTSResourceID string `json:"tts_resource_id"` // 双向流式 TTS X-Api-Resource-Id TTSVoiceType string `json:"tts_voice_type"` // 音色(如 zh_male_… / BV700_streaming) diff --git a/sundynix-gateway/internal/voice/frame.go b/sundynix-gateway/internal/voice/frame.go index d9abffc..304a4d2 100644 --- a/sundynix-gateway/internal/voice/frame.go +++ b/sundynix-gateway/internal/voice/frame.go @@ -8,25 +8,18 @@ import ( "strings" ) -// volcAppKey 是火山语音网关 V3 端点(ASR/TTS/实时对话)**固定**的 App Key 常量——所有客户端 -// 都用同一个值(官方 demo 硬编码),与账号无关;真正的账号鉴权走 X-Api-Access-Key(新版 API Key)。 -// 缺这个头握手会被拒:HTTP 400 "app key not found in header or query"。 -const volcAppKey = "PlgvMymc7f3tQnJ6" - -// setVolcAuthHeaders 按火山 V3 二进制流端点的鉴权规范挂头:固定 App Key + 账号 App ID + -// 新版 API Key(Access Key) + 资源 ID + 连接 ID。**不是** Authorization: Bearer(那是 OpenAI -// 兼容端点用的,流式二进制端点不认)。App ID 用于解析账号资源授权(缺它会 401 grant not found)。 -func setVolcAuthHeaders(hdr http.Header, apiKey, appID, resourceID string) { - // 直接赋 map(不走 Set)以**保留精确大小写**:Go 的 Header.Set 会把 "X-Api-App-ID" - // 规范化成 "X-Api-App-Id",而火山网关按精确大小写匹配(官方 demo 用 X-Api-App-ID)。 - // 新版 API Key 走 X-Api-Access-Key;X-Api-App-Key 是网关固定常量,缺它 400 "app key not found"。 - hdr["X-Api-App-Key"] = []string{volcAppKey} - hdr["X-Api-Access-Key"] = []string{apiKey} +// setVolcAuthHeaders 按火山「新版控制台」API Key 鉴权挂头(文档 6561/1354869「新版本控制台」表)。 +// 新版只需**单个 X-Api-Key**(控制台的 APP Key)——**不用**旧版的 X-Api-App-Key+X-Api-Access-Key+App-ID, +// 也不是 Authorization: Bearer。此前误用旧 5 头方案,火山始终 401 "grant not found in SaaS storage"。 +// - X-Api-Key = 控制台 API Key(用户那把 key) +// - X-Api-Resource-Id= 资源 ID(如 volc.bigasr.sauc.duration / volc.seedasr.sauc.duration / volc.service_type.10029) +// - X-Api-Request-Id = 随机 UUID +// - X-Api-Sequence = 固定 "-1" +func setVolcAuthHeaders(hdr http.Header, apiKey, resourceID string) { + hdr["X-Api-Key"] = []string{apiKey} hdr["X-Api-Resource-Id"] = []string{resourceID} - hdr["X-Api-Connect-Id"] = []string{newConnectID()} - if appID != "" { - hdr["X-Api-App-ID"] = []string{appID} - } + hdr["X-Api-Request-Id"] = []string{newConnectID()} + hdr["X-Api-Sequence"] = []string{"-1"} } // handshakeDetail 从失败的 WS 握手响应里榨出可诊断信息:HTTP 状态 + 火山排障用的 diff --git a/sundynix-gateway/internal/voice/tts.go b/sundynix-gateway/internal/voice/tts.go index 0ac4efb..c33735e 100644 --- a/sundynix-gateway/internal/voice/tts.go +++ b/sundynix-gateway/internal/voice/tts.go @@ -12,7 +12,7 @@ import ( ) // 火山「双向流式 TTS V3」客户端(seed-tts-2.0)。边推文字边收音频,配 LLM token 流做连续朗读。 -// 帧协议见 tts_frame.go;鉴权走**新版 API Key**(X-Api-Access-Key + 固定 App Key + App ID,见 frame.go)。 +// 帧协议见 tts_frame.go;鉴权走**新版控制台 API Key**(单个 X-Api-Key,见 frame.go)。 // // 一轮朗读的生命周期:StartTTS(连接+StartSession) → Speak(逐句推文字/事件200) → Finish(FinishSession) // → 客户端 range Audio() 收音频直到 channel 关闭 → Close()。一个 TTSSession = 一次 Agent 回答。 @@ -58,7 +58,7 @@ func StartTTS(ctx context.Context, cfg Config) (*TTSSession, error) { return nil, fmt.Errorf("TTS 未配置") } hdr := http.Header{} - setVolcAuthHeaders(hdr, cfg.APIKey, cfg.AppID, cfg.TTSResourceID) + setVolcAuthHeaders(hdr, cfg.APIKey, cfg.TTSResourceID) dialer := websocket.Dialer{HandshakeTimeout: 10 * time.Second} conn, resp, err := dialer.DialContext(ctx, ttsEndpoint, hdr)