fix(voice): 改用火山「新版控制台」API Key 鉴权(ASR 握手真机验通)

一直误用旧版 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 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-22 10:42:36 +08:00
parent c5397438d4
commit d6c1a787f4
6 changed files with 16 additions and 26 deletions
-1
View File
@@ -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"),
-1
View File
@@ -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"),
+2 -2
View File
@@ -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)
+1 -2
View File
@@ -8,8 +8,7 @@ import "github.com/sundynix/sundynix-shared/secrets"
// AuthorizationBearer <APIKey>+ X-Api-Resource-Id(区分服务,ASR 与双向 TTS 各一个)。
// 端点/音频格式(PCM 16k 单声道等)由代码固定,不入用户配置。
type Config struct {
APIKey string `json:"api_key"` // 新版 API KeyX-Api-Access-Key,密文入库)
AppID string `json:"app_id"` // 火山 App IDX-Api-App-ID,解析账号资源授权用;V3 流式端点需要)
APIKey string `json:"api_key"` // 新版控制台 API KeyX-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
+11 -18
View File
@@ -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-KeyX-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 状态 + 火山排障用的
+2 -2
View File
@@ -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)