package console import ( "sort" "strings" "time" "yunyan/comm" "yunyan/lego/sys/log" "yunyan/lego/sys/postgres" "yunyan/pb" "github.com/gin-gonic/gin" ) // ============================ 第三方服务商模板域 (svctemplate) ============================ // // 数据驱动模板:管理员在「第三方服务配置」新增服务时选一个模板,自动带出字段(可再增删)。 // 模板存 console 主库(postgres.GetSys()),启动时幂等 seed 内置常见服务商(见 seedSvcTemplates)。 // 模板字段只作 schema 预设,不存真实密钥、不加密。 // ============================== 模板 CRUD ============================== // getSvcTemplates 列出全部服务商模板;category>0 时按类别过滤,按 (category, sort, id) 排序返回。 func (this *serverComp) getSvcTemplates(c *gin.Context) { var req struct { Category int32 `json:"category"` } _ = c.ShouldBindJSON(&req) items := make([]*ThirdSvcTemplate, 0) var err error if req.Category > 0 { err = postgres.Find(comm.TableThirdSvcTemplate, &items, "category=?", req.Category) } else { err = postgres.Find(comm.TableThirdSvcTemplate, &items, "") } if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } sort.SliceStable(items, func(i, j int) bool { if items[i].Category != items[j].Category { return items[i].Category < items[j].Category } if items[i].Sort != items[j].Sort { return items[i].Sort < items[j].Sort } return items[i].Id < items[j].Id }) writeOK(c, gin.H{"items": items}) } // addSvcTemplate 新增自定义服务商模板(Builtin=false)。 func (this *serverComp) addSvcTemplate(c *gin.Context) { var m ThirdSvcTemplate if err := c.ShouldBindJSON(&m); err != nil { writeErr(c, pb.ErrorCode_ReqParameterError, err.Error()) return } if strings.TrimSpace(m.Id) == "" || strings.TrimSpace(m.Name) == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "模板ID 和 名称 必填") return } exist := &ThirdSvcTemplate{} if err := postgres.FindOne(comm.TableThirdSvcTemplate, exist, "id=?", m.Id); err == nil { writeErr(c, pb.ErrorCode_ReqParameterError, "模板ID 已存在: "+m.Id) return } m.Builtin = false now := time.Now().UnixMilli() m.Createtime = now m.Updatetime = now if err := postgres.Insert(comm.TableThirdSvcTemplate, &m); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, &m) } // updateSvcTemplate 更新服务商模板(按 id)。保留旧记录的 Builtin/Createtime。 func (this *serverComp) updateSvcTemplate(c *gin.Context) { var m ThirdSvcTemplate if err := c.ShouldBindJSON(&m); err != nil { writeErr(c, pb.ErrorCode_ReqParameterError, err.Error()) return } if strings.TrimSpace(m.Id) == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "模板ID 必填") return } old := &ThirdSvcTemplate{} if err := postgres.FindOne(comm.TableThirdSvcTemplate, old, "id=?", m.Id); err != nil { writeErr(c, pb.ErrorCode_DBError, "模板不存在: "+m.Id) return } m.Builtin = old.Builtin m.Createtime = old.Createtime m.Updatetime = time.Now().UnixMilli() if err := postgres.Save(comm.TableThirdSvcTemplate, &m); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, &m) } // delSvcTemplate 删除服务商模板(按 id)。删除 builtin 允许,但下次启动会被 seed 重新补回。 func (this *serverComp) delSvcTemplate(c *gin.Context) { var req struct { Id string `json:"id"` } if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.Id) == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "id 必填") return } if err := postgres.Delete(comm.TableThirdSvcTemplate, "id=?", req.Id); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } writeOK(c, gin.H{"id": req.Id}) } // ============================== 内置模板 seed ============================== // 服务类别常量(与 admin 前端 SCAT / serviceconfig.vue 一致)。每个服务实例只归属其中一种。 const ( svcCatSTT int32 = 1 // 语音识别 svcCatTTS int32 = 2 // 语音合成 svcCatMT int32 = 3 // 机器翻译 svcCatAST int32 = 4 // 端到端语音翻译 svcCatLLM int32 = 5 // 大语言模型 svcCatStorage int32 = 6 // 对象存储 svcCatSTS int32 = 7 // 端到端语音对话 ) // sf 构造一个模板字段(sort 由调用顺序在 tpl 内自动填)。 func sf(key, desc string, encrypted bool) SvcField { return SvcField{Key: key, Description: desc, Encrypted: encrypted} } // tpl 构造一个内置模板(Builtin=true,字段 sort 按传入顺序自动编号)。 func tpl(id, provider, name string, category, order int32, fields ...SvcField) ThirdSvcTemplate { for i := range fields { fields[i].Sort = int32(i) } return ThirdSvcTemplate{ Id: id, Provider: provider, Name: name, Category: category, Sort: order, Fields: fields, Builtin: true, } } // audioOutputCats 需要输出音频、须配置「支持语言 + 默认语言」的服务类别(TTS/AST/STS)。 var audioOutputCats = map[int32]bool{svcCatTTS: true, svcCatAST: true, svcCatSTS: true} // builtinSvcTemplates 内置服务商模板清单(参考 polychat-agent 的服务商与字段归纳,可后续增改)。 func builtinSvcTemplates() []ThirdSvcTemplate { list := []ThirdSvcTemplate{ // STT 语音识别 tpl("stt_azure", "azure", "微软 Azure STT", svcCatSTT, 0, sf("subscription_key", "订阅密钥", true), sf("region", "区域/终结点,如 eastus", false), sf("language", "语言代码,如 zh-CN", false)), tpl("stt_openai", "openai", "OpenAI Whisper", svcCatSTT, 1, sf("api_key", "API Key", true), sf("model", "模型,如 whisper-1", false), sf("language", "语言代码", false)), tpl("stt_volcengine", "volcengine", "火山引擎 STT", svcCatSTT, 2, sf("appid", "应用 AppID", false), sf("token", "访问 Token", true), sf("cluster", "集群标识", false)), tpl("stt_alibaba", "alibaba", "阿里云 STT", svcCatSTT, 3, sf("access_key_id", "AccessKey ID", true), sf("access_key_secret", "AccessKey Secret", true), sf("app_key", "项目 AppKey", false)), // TTS 语音合成 tpl("tts_azure", "azure", "微软 Azure TTS", svcCatTTS, 0, sf("subscription_key", "订阅密钥", true), sf("region", "区域/终结点,如 eastus", false), sf("voice_name", "音色,如 zh-CN-XiaoxiaoNeural", false), sf("rate", "语速,-50%~+50%", false), sf("pitch", "音调,-50Hz~+50Hz", false)), tpl("tts_openai", "openai", "OpenAI TTS", svcCatTTS, 1, sf("api_key", "API Key", true), sf("model", "模型,如 tts-1", false), sf("voice", "音色,如 alloy", false)), tpl("tts_volcengine", "volcengine", "火山引擎 TTS", svcCatTTS, 2, sf("appid", "应用 AppID", false), sf("token", "访问 Token", true), sf("cluster", "集群标识", false)), tpl("tts_alibaba", "alibaba", "阿里云 TTS", svcCatTTS, 3, sf("access_key_id", "AccessKey ID", true), sf("access_key_secret", "AccessKey Secret", true)), tpl("tts_elevenlabs", "elevenlabs", "ElevenLabs TTS", svcCatTTS, 4, sf("api_key", "API Key", true), sf("voice_id", "音色 ID", false)), // MT 机器翻译 tpl("mt_azure", "azure", "微软 Azure 翻译", svcCatMT, 0, sf("subscription_key", "订阅密钥", true), sf("region", "区域,如 eastasia", false)), tpl("mt_google", "google", "Google 翻译", svcCatMT, 1, sf("api_key", "API Key", true)), tpl("mt_doubao", "doubao", "豆包(火山)翻译", svcCatMT, 2, sf("api_key", "API Key", true)), tpl("mt_alibaba", "alibaba", "阿里云 翻译", svcCatMT, 3, sf("access_key_id", "AccessKey ID", true), sf("access_key_secret", "AccessKey Secret", true)), // AST 端到端语音翻译 tpl("ast_azure", "azure", "微软 Azure AST", svcCatAST, 0, sf("subscription_key", "订阅密钥", true), sf("region", "区域,如 eastus", false)), tpl("ast_volcengine", "volcengine", "火山引擎 AST", svcCatAST, 1, sf("appid", "应用 AppID", false), sf("token", "访问 Token", true), sf("cluster", "集群标识", false)), tpl("ast_alibaba", "alibaba", "阿里云 AST", svcCatAST, 2, sf("access_key_id", "AccessKey ID", true), sf("access_key_secret", "AccessKey Secret", true)), // LLM 大语言模型 tpl("llm_openai", "openai", "OpenAI", svcCatLLM, 0, sf("api_key", "API Key", true), sf("model", "模型,如 gpt-4o", false), sf("base_url", "接口地址(可选)", false), sf("temperature", "生成温度 0~2", false), sf("max_tokens", "最大输出 Token", false)), tpl("llm_doubao", "doubao", "豆包(火山引擎)", svcCatLLM, 1, sf("api_key", "API Key", true), sf("endpoint_id", "推理接入点 ID", false), sf("base_url", "接口地址", false)), tpl("llm_anthropic", "anthropic", "Anthropic Claude", svcCatLLM, 2, sf("api_key", "API Key", true), sf("model", "模型,如 claude-sonnet-4", false)), tpl("llm_deepseek", "deepseek", "DeepSeek", svcCatLLM, 3, sf("api_key", "API Key", true), sf("model", "模型,如 deepseek-chat", false), sf("base_url", "接口地址", false)), tpl("llm_qwen", "qwen", "通义千问(阿里)", svcCatLLM, 4, sf("api_key", "API Key", true), sf("model", "模型,如 qwen-max", false)), // STS 端到端语音对话 tpl("sts_volcengine", "volcengine", "火山引擎 STS", svcCatSTS, 0, sf("appid", "应用 AppID", false), sf("token", "访问 Token", true), sf("cluster", "集群标识", false)), tpl("sts_doubao", "doubao", "豆包(火山)STS", svcCatSTS, 1, sf("appid", "应用 AppID", false), sf("token", "访问 Token", true), sf("cluster", "集群标识", false)), tpl("sts_azure", "azure", "微软 Azure STS", svcCatSTS, 2, sf("subscription_key", "订阅密钥", true), sf("region", "区域,如 eastus", false)), tpl("sts_google", "google", "Google STS", svcCatSTS, 3, sf("api_key", "API Key", true)), // 存储 对象存储 tpl("storage_aliyun_oss", "aliyun_oss", "阿里云 OSS", svcCatStorage, 0, sf("access_key_id", "AccessKey ID", true), sf("access_key_secret", "AccessKey Secret", true), sf("bucket", "存储桶", false), sf("endpoint", "地域节点 Endpoint", false)), tpl("storage_tencent_cos", "tencent_cos", "腾讯云 COS", svcCatStorage, 1, sf("secret_id", "SecretId", true), sf("secret_key", "SecretKey", true), sf("bucket", "存储桶", false), sf("region", "地域,如 ap-guangzhou", false)), } // 语音输出类服务(TTS/AST/STS)统一前置「支持语言 + 默认语言」两字段: // languages(langs) 存逗号分隔 BCP-47(如 zh-CN,en-US),default_lang(lang) 存单个码。 // 不绑定单一语言,音色/语速等仍由各服务商自有字段决定,运行时按请求语言选择。 for i := range list { if !audioOutputCats[list[i].Category] { continue } langFields := []SvcField{ {Key: "languages", Description: "支持的语言(可多选)", Type: "langs"}, {Key: "default_lang", Description: "默认语言", Type: "lang"}, } list[i].Fields = append(langFields, list[i].Fields...) for j := range list[i].Fields { list[i].Fields[j].Sort = int32(j) } } return list } // seedSvcTemplates 幂等 seed 内置模板:仅当同 id 不存在时插入,不覆盖用户已改动的记录。 // 在 ensureConfigTables 之后调用(启动一次)。 func seedSvcTemplates() { now := time.Now().UnixMilli() inserted := 0 for _, t := range builtinSvcTemplates() { exist := &ThirdSvcTemplate{} if err := postgres.FindOne(comm.TableThirdSvcTemplate, exist, "id=?", t.Id); err == nil { continue // 已存在,跳过(保留用户改动) } t.Createtime = now t.Updatetime = now if err := postgres.Insert(comm.TableThirdSvcTemplate, &t); err != nil { log.Warn("console: 内置服务商模板 seed 失败", log.Field{Key: "id", Value: t.Id}, log.Field{Key: "err", Value: err.Error()}) continue } inserted++ } if inserted > 0 { log.Info("console: 内置服务商模板 seed 完成", log.Field{Key: "inserted", Value: inserted}) } }