From a28ae49b6aaabf125d73a3235ad958614d30cf32 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Tue, 21 Jul 2026 17:06:22 +0800 Subject: [PATCH] =?UTF-8?q?refactor(voice):=20=E8=AF=AD=E9=9F=B3=E9=85=8D?= =?UTF-8?q?=E7=BD=AE=E6=94=B9=E7=94=A8=E6=96=B0=E7=89=88=20API=20Key=20?= =?UTF-8?q?=E9=89=B4=E6=9D=83(=E5=BC=83=E6=97=A7=E7=89=88=20appid+token)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 用户提醒:火山新版走 API Key 鉴权,不用旧版 appid+access_token。新版 WS 握手只带两个 header——Authorization(Bearer ) + X-Api-Resource-Id。 配置从 {appid, access_token, 2×resource-id, voice} 收敛为 {api_key, 2×resource-id, voice}:APIKey 走 secrets AES 加密入库;ASREnabled/TTSEnabled 改为只看 api_key+对应 resource-id。admin 配置页两个字段并一个 API Key 字段,清单同步改为新版口径。build+tsc 绿。 Co-Authored-By: Claude Opus 4.8 --- sundynix-admin/src/api.ts | 11 +++---- sundynix-admin/src/pages/VoiceConfigPage.tsx | 23 +++++-------- .../internal/handler/voice_config.go | 19 +++++------ sundynix-gateway/internal/voice/config.go | 33 +++++++++---------- 4 files changed, 36 insertions(+), 50 deletions(-) diff --git a/sundynix-admin/src/api.ts b/sundynix-admin/src/api.ts index 08b6c13..c82f157 100644 --- a/sundynix-admin/src/api.ts +++ b/sundynix-admin/src/api.ts @@ -267,10 +267,9 @@ export async function saveWechatMP(body: { appid: string; app_secret: string; to return { enabled: !!d.enabled }; } -// —— 语音(火山豆包)配置 —— +// —— 语音(火山豆包)配置 · 新版 API Key 鉴权 —— export interface VoiceConfig { - appid: string; - access_token: string; // 明文回显(单管理员后台) + api_key: string; // 明文回显(单管理员后台) asr_resource_id: string; tts_resource_id: string; tts_voice_type: string; @@ -283,15 +282,15 @@ export async function getVoiceConfig(): Promise { const d = (await res.json().catch(() => ({}))) as Partial & { error?: string }; if (!res.ok) throw new Error(d.error ?? `voice config failed: ${res.status}`); return { - appid: d.appid ?? "", access_token: d.access_token ?? "", asr_resource_id: d.asr_resource_id ?? "", + api_key: d.api_key ?? "", asr_resource_id: d.asr_resource_id ?? "", tts_resource_id: d.tts_resource_id ?? "", tts_voice_type: d.tts_voice_type ?? "", asr_enabled: !!d.asr_enabled, tts_enabled: !!d.tts_enabled, }; } -// access_token 传空串 = 沿用已保存的。 +// api_key 传空串 = 沿用已保存的。 export async function saveVoiceConfig(body: { - appid: string; access_token: string; asr_resource_id: string; tts_resource_id: string; tts_voice_type: string; + api_key: string; asr_resource_id: string; tts_resource_id: string; tts_voice_type: string; }): Promise<{ asr_enabled: boolean; tts_enabled: boolean }> { const res = guard(await fetch(`${ADMIN}/voice`, { method: "PUT", headers: authHeaders(true), body: JSON.stringify(body) })); const d = (await res.json().catch(() => ({}))) as { asr_enabled?: boolean; tts_enabled?: boolean; error?: string }; diff --git a/sundynix-admin/src/pages/VoiceConfigPage.tsx b/sundynix-admin/src/pages/VoiceConfigPage.tsx index c1e3bc1..c69bed1 100644 --- a/sundynix-admin/src/pages/VoiceConfigPage.tsx +++ b/sundynix-admin/src/pages/VoiceConfigPage.tsx @@ -4,8 +4,7 @@ import { getVoiceConfig, saveVoiceConfig } from "../api"; // 运维 · 语音设置:火山引擎豆包语音(流式 ASR 耳朵 + 双向流式 TTS 嘴)。 // AccessToken 明文回显(单管理员后台,方便核对;密文仍加密入库)。设计见 VOICE_DESIGN.md。 export function VoiceConfigPage() { - const [appid, setAppid] = useState(""); - const [token, setToken] = useState(""); + const [apiKey, setApiKey] = useState(""); const [asrRes, setAsrRes] = useState(""); const [ttsRes, setTtsRes] = useState(""); const [voice, setVoice] = useState(""); @@ -18,8 +17,7 @@ export function VoiceConfigPage() { useEffect(() => { getVoiceConfig() .then((c) => { - setAppid(c.appid); - setToken(c.access_token); + setApiKey(c.api_key); setAsrRes(c.asr_resource_id); setTtsRes(c.tts_resource_id); setVoice(c.tts_voice_type); @@ -36,8 +34,7 @@ export function VoiceConfigPage() { setOk(false); try { const r = await saveVoiceConfig({ - appid: appid.trim(), - access_token: token, + api_key: apiKey, asr_resource_id: asrRes.trim(), tts_resource_id: ttsRes.trim(), tts_voice_type: voice.trim(), @@ -75,13 +72,9 @@ export function VoiceConfigPage() {
- -
-

去哪拿这些参数

+

去哪拿这些参数(新版 API Key 鉴权)

    -
  1. 火山「豆包语音控制台」→ 你的应用详情页,拿 AppIDAccess Token(ASR/TTS 通常同一应用共用)
  2. +
  3. 火山控制台生成 API Key(新版鉴权,一个 Key 走 Authorization 头,不再用旧版 appid+token)
  4. ASR Resource-Id:在「流式语音识别 2.0」文档的鉴权小节(X-Api-Resource-Id 那一栏)
  5. TTS Resource-Id:在「双向流式语音合成」文档的鉴权小节
  6. 音色:控制台「音色管理」里挑一个(JARVIS 建议沉稳男声),填其 voice_type
  7. diff --git a/sundynix-gateway/internal/handler/voice_config.go b/sundynix-gateway/internal/handler/voice_config.go index 83cd711..486b2eb 100644 --- a/sundynix-gateway/internal/handler/voice_config.go +++ b/sundynix-gateway/internal/handler/voice_config.go @@ -28,12 +28,11 @@ func (h *Handler) loadVoiceConfig(ctx context.Context) voice.Config { return c.DecryptFromStore() } -// AdminGetVoiceConfig: GET /api/v1/admin/voice —— 回显语音配置(token 明文,RequireAdmin 已拦)。 +// AdminGetVoiceConfig: GET /api/v1/admin/voice —— 回显语音配置(api_key 明文,RequireAdmin 已拦)。 func (h *Handler) AdminGetVoiceConfig(c *gin.Context) { cfg := h.loadVoiceConfig(c.Request.Context()) c.JSON(http.StatusOK, gin.H{ - "appid": cfg.AppID, - "access_token": cfg.AccessToken, + "api_key": cfg.APIKey, "asr_resource_id": cfg.ASRResourceID, "tts_resource_id": cfg.TTSResourceID, "tts_voice_type": cfg.TTSVoiceType, @@ -42,11 +41,10 @@ func (h *Handler) AdminGetVoiceConfig(c *gin.Context) { }) } -// AdminSaveVoiceConfig: PUT /api/v1/admin/voice —— 保存语音配置(token 空串=沿用已存)。 +// AdminSaveVoiceConfig: PUT /api/v1/admin/voice —— 保存语音配置(api_key 空串=沿用已存)。 func (h *Handler) AdminSaveVoiceConfig(c *gin.Context) { var b struct { - AppID string `json:"appid"` - AccessToken string `json:"access_token"` + APIKey string `json:"api_key"` ASRResourceID string `json:"asr_resource_id"` TTSResourceID string `json:"tts_resource_id"` TTSVoiceType string `json:"tts_voice_type"` @@ -56,13 +54,12 @@ func (h *Handler) AdminSaveVoiceConfig(c *gin.Context) { return } ctx := c.Request.Context() - token := strings.TrimSpace(b.AccessToken) - if token == "" { - token = h.loadVoiceConfig(ctx).AccessToken // 留空=沿用已存 + key := strings.TrimSpace(b.APIKey) + if key == "" { + key = h.loadVoiceConfig(ctx).APIKey // 留空=沿用已存 } cfg := voice.Config{ - AppID: strings.TrimSpace(b.AppID), - AccessToken: token, + APIKey: key, ASRResourceID: strings.TrimSpace(b.ASRResourceID), TTSResourceID: strings.TrimSpace(b.TTSResourceID), TTSVoiceType: strings.TrimSpace(b.TTSVoiceType), diff --git a/sundynix-gateway/internal/voice/config.go b/sundynix-gateway/internal/voice/config.go index d49bb4a..671f914 100644 --- a/sundynix-gateway/internal/voice/config.go +++ b/sundynix-gateway/internal/voice/config.go @@ -2,48 +2,45 @@ package voice import "github.com/sundynix/sundynix-shared/secrets" -// Config 是语音交互(火山引擎豆包语音)所需配置。AccessToken 加密入库、后台明文回显(同微信配置)。 +// Config 是语音交互(火山引擎豆包语音)所需配置。APIKey 加密入库、后台明文回显(同微信配置)。 // -// 火山鉴权走 WS 握手 header:X-Api-App-Key(AppID) / X-Api-Access-Key(AccessToken) / -// X-Api-Resource-Id(区分服务,ASR 与双向 TTS 各一个)。AppID/Token 通常同一应用共用。 +// 用**新版 API Key 鉴权**(非旧版 appid+access_token):WS 握手只带两个 header—— +// Authorization(Bearer )+ X-Api-Resource-Id(区分服务,ASR 与双向 TTS 各一个)。 // 端点/音频格式(PCM 16k 单声道等)由代码固定,不入用户配置。 type Config struct { - AppID string `json:"appid"` // 火山应用 AppID(X-Api-App-Key) - AccessToken string `json:"access_token"` // 火山 Access Token(X-Api-Access-Key,密文入库) + APIKey string `json:"api_key"` // 新版 API Key(Authorization: Bearer ,密文入库) 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) } -// ASREnabled / TTSEnabled 分别报告耳朵、嘴是否配齐(可各自独立启用)。 -func (c Config) ASREnabled() bool { - return c.AppID != "" && c.AccessToken != "" && c.ASRResourceID != "" -} +// ASREnabled / TTSEnabled 分别报告耳朵、嘴是否配齐(共用同一 API Key,各自还需对应 resource-id)。 +func (c Config) ASREnabled() bool { return c.APIKey != "" && c.ASRResourceID != "" } func (c Config) TTSEnabled() bool { - return c.AppID != "" && c.AccessToken != "" && c.TTSResourceID != "" && c.TTSVoiceType != "" + return c.APIKey != "" && c.TTSResourceID != "" && c.TTSVoiceType != "" } // Enabled 报告语音整体是否可用(耳朵 + 嘴都配齐)。 func (c Config) Enabled() bool { return c.ASREnabled() && c.TTSEnabled() } -// EncryptedForStore 返回 AccessToken 已加密的副本,用于落库。 +// EncryptedForStore 返回 APIKey 已加密的副本,用于落库。 func (c Config) EncryptedForStore() (Config, error) { - if c.AccessToken == "" { + if c.APIKey == "" { return c, nil } - enc, err := secrets.Encrypt(c.AccessToken) + enc, err := secrets.Encrypt(c.APIKey) if err != nil { return c, err } - c.AccessToken = enc + c.APIKey = enc return c, nil } -// DecryptFromStore 把库内密文 AccessToken 还原为明文。 +// DecryptFromStore 把库内密文 APIKey 还原为明文。 func (c Config) DecryptFromStore() Config { - if c.AccessToken != "" { - if plain, err := secrets.Decrypt(c.AccessToken); err == nil { - c.AccessToken = plain + if c.APIKey != "" { + if plain, err := secrets.Decrypt(c.APIKey); err == nil { + c.APIKey = plain } } return c