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() } // resolveJarvis 解析某用户**有效**的 JARVIS 设定:火山配置 + 助手名 + 人设。 // - 火山配置:用户自带豆包齐全(Enabled)→ 用用户的;否则回落系统配置(voice_config)。 // - 名字/人设:来自用户的 UserJarvis 记录(可空,空则代码里再兜默认);与火山配置相互独立 // (常见情形:多数用户不配自己的豆包、只改名字和人设)。 func (h *Handler) resolveJarvis(ctx context.Context, uid string) (cfg voice.Config, name, persona string) { cfg = h.loadVoiceConfig(ctx) // 系统兜底 j := h.db.GetUserJarvis(ctx, uid) if j == nil { return cfg, "", "" } name, persona = j.Name, j.Persona user := voice.Config{APIKey: j.APIKey, ASRResourceID: j.ASRResourceID, TTSResourceID: j.TTSResourceID, TTSVoiceType: j.TTSVoiceType}.DecryptFromStore() if user.Enabled() { // 用户自带豆包齐全 → 整套用用户的(key 与 resource-id 必须同账号,不混用) cfg = user } return cfg, name, persona } // 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{ "api_key": cfg.APIKey, "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 —— 保存语音配置(api_key 空串=沿用已存)。 func (h *Handler) AdminSaveVoiceConfig(c *gin.Context) { var b struct { APIKey string `json:"api_key"` 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() key := strings.TrimSpace(b.APIKey) if key == "" { key = h.loadVoiceConfig(ctx).APIKey // 留空=沿用已存 } cfg := voice.Config{ APIKey: key, 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()}) }