Files
sundynix-agentix/sundynix-gateway/internal/handler/voice_config.go
T
Blizzard 618184689e 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>
2026-07-21 17:00:35 +08:00

82 lines
2.6 KiB
Go

package handler
import (
"context"
"encoding/json"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"github.com/sundynix/sundynix-gateway/internal/voice"
)
// 语音(火山引擎豆包语音)配置的管理端存取。设计见 VOICE_DESIGN.md。
// AccessToken AES 加密入库;单管理员后台明文回显(同微信配置),方便核对/复制。
const SettingVoice = "voice_config" // 语音配置(setting 表)
func (h *Handler) loadVoiceConfig(ctx context.Context) voice.Config {
raw := h.db.GetSetting(ctx, SettingVoice)
if raw == "" {
return voice.Config{}
}
var c voice.Config
if json.Unmarshal([]byte(raw), &c) != nil {
return voice.Config{}
}
return c.DecryptFromStore()
}
// AdminGetVoiceConfig: GET /api/v1/admin/voice —— 回显语音配置(token 明文,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,
"asr_resource_id": cfg.ASRResourceID,
"tts_resource_id": cfg.TTSResourceID,
"tts_voice_type": cfg.TTSVoiceType,
"asr_enabled": cfg.ASREnabled(),
"tts_enabled": cfg.TTSEnabled(),
})
}
// AdminSaveVoiceConfig: PUT /api/v1/admin/voice —— 保存语音配置(token 空串=沿用已存)。
func (h *Handler) AdminSaveVoiceConfig(c *gin.Context) {
var b struct {
AppID string `json:"appid"`
AccessToken string `json:"access_token"`
ASRResourceID string `json:"asr_resource_id"`
TTSResourceID string `json:"tts_resource_id"`
TTSVoiceType string `json:"tts_voice_type"`
}
if err := c.ShouldBindJSON(&b); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "参数错误"})
return
}
ctx := c.Request.Context()
token := strings.TrimSpace(b.AccessToken)
if token == "" {
token = h.loadVoiceConfig(ctx).AccessToken // 留空=沿用已存
}
cfg := voice.Config{
AppID: strings.TrimSpace(b.AppID),
AccessToken: token,
ASRResourceID: strings.TrimSpace(b.ASRResourceID),
TTSResourceID: strings.TrimSpace(b.TTSResourceID),
TTSVoiceType: strings.TrimSpace(b.TTSVoiceType),
}
stored, err := cfg.EncryptedForStore()
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "密钥加密失败: " + err.Error()})
return
}
raw, _ := json.Marshal(stored)
if err := h.db.SetSetting(ctx, SettingVoice, string(raw)); err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"status": "ok", "asr_enabled": cfg.ASREnabled(), "tts_enabled": cfg.TTSEnabled()})
}