feat(voice): 语音配置控制面——火山豆包 appid/token/resource-id/音色(参数落地处)

JARVIS 不依赖火山账号能先做的第二块:配置层,做成'你参数一填就落库'的形态。
- voice.Config: AppID/AccessToken/ASRResourceID/TTSResourceID/TTSVoiceType,AccessToken
  走 secrets AES 加密入库(镜像微信配置);ASREnabled/TTSEnabled 可各自独立判断。
- admin GET/PUT /admin/voice(RequireAdmin),token 空串=沿用已存、明文回显。
- admin「语音设置」页(运维组):五个字段 + ASR/TTS 就绪徽标 + '去哪拿参数'清单。

端点/音频格式(PCM 16k 单声道)由代码固定不入配置。WS 会话 + 火山 ASR/TTS 客户端等
用户回传 resource-id 后按官方 demo 协议做。build+tsc 绿。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Blizzard
2026-07-21 17:00:35 +08:00
parent 644b635d73
commit 618184689e
6 changed files with 295 additions and 0 deletions
+50
View File
@@ -0,0 +1,50 @@
package voice
import "github.com/sundynix/sundynix-shared/secrets"
// Config 是语音交互(火山引擎豆包语音)所需配置。AccessToken 加密入库、后台明文回显(同微信配置)。
//
// 火山鉴权走 WS 握手 headerX-Api-App-Key(AppID) / X-Api-Access-Key(AccessToken) /
// X-Api-Resource-Id(区分服务,ASR 与双向 TTS 各一个)。AppID/Token 通常同一应用共用。
// 端点/音频格式(PCM 16k 单声道等)由代码固定,不入用户配置。
type Config struct {
AppID string `json:"appid"` // 火山应用 AppIDX-Api-App-Key
AccessToken string `json:"access_token"` // 火山 Access TokenX-Api-Access-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
}
// ASREnabled / TTSEnabled 分别报告耳朵、嘴是否配齐(可各自独立启用)。
func (c Config) ASREnabled() bool {
return c.AppID != "" && c.AccessToken != "" && c.ASRResourceID != ""
}
func (c Config) TTSEnabled() bool {
return c.AppID != "" && c.AccessToken != "" && c.TTSResourceID != "" && c.TTSVoiceType != ""
}
// Enabled 报告语音整体是否可用(耳朵 + 嘴都配齐)。
func (c Config) Enabled() bool { return c.ASREnabled() && c.TTSEnabled() }
// EncryptedForStore 返回 AccessToken 已加密的副本,用于落库。
func (c Config) EncryptedForStore() (Config, error) {
if c.AccessToken == "" {
return c, nil
}
enc, err := secrets.Encrypt(c.AccessToken)
if err != nil {
return c, err
}
c.AccessToken = enc
return c, nil
}
// DecryptFromStore 把库内密文 AccessToken 还原为明文。
func (c Config) DecryptFromStore() Config {
if c.AccessToken != "" {
if plain, err := secrets.Decrypt(c.AccessToken); err == nil {
c.AccessToken = plain
}
}
return c
}