package comm import ( "yunyan/lego/sys/log" "yunyan/lego/sys/mysql" ) // 会议记录(echomeet)服务配置托管:把「识别 / 翻译 / 总结」三类服务配置全部搬进管理后台。 // // 与旧的「一会一服务商,三段同源」不同:识别/翻译/总结各自维护一份**按优先级排序的服务清单**, // - 识别:按优先级 + 各服务「支持语言」自动选择(跳过不支持源语言的服务); // - 翻译:按优先级自动选择; // - 总结:每条 = 一个具体文本大模型条目,客户端按 id 选择。 // // 存储沿用 [[app_module_config]] 的路子:存**应用自己的业务库**(mysql/pg,随部署)、**全局一份**(无 app_name)。 // console 后台经 getServiceDB 写、home 的 echomeet 启动时 LoadOrSeed 读(改配置需重启生效)。 // 密钥字段以 AES-256-GCM 密文落库(comm.Encrypt,${FIELD_ENCRYPT_KEY},console 与业务侧同一把 key), // 返回前端脱敏为 EncMask。字段清单由 EchoSvcCatalog 数据驱动(哪些字段、类型、是否密钥)。 // 服务类别。 const ( EchoSvcCatASR = 1 // 识别(转写) EchoSvcCatMT = 2 // 翻译 EchoSvcCatLLM = 3 // 总结(文本大模型) ) // EchoSvcConfig 一行 = 某类别下一条服务/模型条目。存应用业务库,全局一份。 type EchoSvcConfig struct { Id string `gorm:"primaryKey;column:id;size:64" json:"id"` // 稳定键:asr_ali / mt_bytedance / llm_qwen_plus Category int32 `gorm:"column:category;index:idx_echosvc_cat" json:"category"` // 1 识别 / 2 翻译 / 3 总结 Provider string `gorm:"column:provider;size:32" json:"provider"` // bytedance/ali/google/microsoft(识别/翻译);doubao/openai/gemini(总结) Name string `gorm:"column:name;size:128" json:"name"` // 展示名(总结类即模型友好名,客户端可见) Priority int32 `gorm:"column:priority" json:"priority"` // 同类内升序,小=优先 Enabled bool `gorm:"column:enabled" json:"enabled"` // 是否启用 SupportLangs string `gorm:"column:support_langs;type:text" json:"support_langs"` // 识别用:逗号分隔 BCP-47;空=按代码默认支持集 Values map[string]string `gorm:"column:values;type:text;serializer:json" json:"values"` // 参数/凭据值;密钥字段存密文 Createtime int64 `gorm:"column:createtime" json:"createtime"` Updatetime int64 `gorm:"column:updatetime" json:"updatetime"` } func (EchoSvcConfig) TableName() string { return TableEchomeetSvcConfig } // EchoSvcField 一个配置字段的元数据(驱动前端表单 + 后端密钥判定)。 type EchoSvcField struct { Key string `json:"key"` Label string `json:"label"` Type string `json:"type"` // text|password|textarea|url Secret bool `json:"secret"` Required bool `json:"required"` Placeholder string `json:"placeholder,omitempty"` Desc string `json:"desc,omitempty"` } // EchoSvcProviderDef 某类别下某服务商/模型实现的字段模板。 type EchoSvcProviderDef struct { Category int32 `json:"category"` // 所属类别 Provider string `json:"provider"` // 存库 Provider 值 Name string `json:"name"` // 展示名 HasLangs bool `json:"has_langs"` // 是否展示「支持语言」(识别类为 true) Multi bool `json:"multi"` // 是否允许同 provider 多条(总结类为 true:一个服务商多个模型) Fields []EchoSvcField `json:"fields"` } // 常用字段构造助手。 func esfText(key, label string, required bool) EchoSvcField { return EchoSvcField{Key: key, Label: label, Type: "text", Required: required} } func esfSecret(key, label string, required bool) EchoSvcField { return EchoSvcField{Key: key, Label: label, Type: "password", Secret: true, Required: required} } // EchoSvcCatalog 会议记录服务字段目录:识别/翻译/总结 × 各服务商,字段依据 home.yaml 的 echomeet 配置结构。 var EchoSvcCatalog = []EchoSvcProviderDef{ // ===== 识别(转写)===== {Category: EchoSvcCatASR, Provider: "bytedance", Name: "字节跳动", HasLangs: true, Fields: []EchoSvcField{ esfText("AppID", "AppID", true), esfSecret("Token", "Token", false), esfText("Callback", "回调地址", false), }}, {Category: EchoSvcCatASR, Provider: "ali", Name: "阿里云 DashScope", HasLangs: true, Fields: []EchoSvcField{ esfSecret("ApiKey", "ApiKey", true), esfText("Callback", "回调地址", false), }}, {Category: EchoSvcCatASR, Provider: "google", Name: "Google Cloud", HasLangs: true, Fields: []EchoSvcField{ {Key: "JsonPath", Label: "服务账号 JSON", Type: "textarea", Secret: true, Required: true, Desc: "服务账号 JSON 内容或服务器上的文件路径"}, }}, {Category: EchoSvcCatASR, Provider: "microsoft", Name: "Azure 语音", HasLangs: true, Fields: []EchoSvcField{ esfSecret("Key", "Key", true), esfText("Region", "Region", true), }}, // ===== 翻译 ===== {Category: EchoSvcCatMT, Provider: "bytedance", Name: "火山翻译", Fields: []EchoSvcField{ esfText("AccessKey", "AccessKey", true), esfSecret("SecretKey", "SecretKey", true), esfText("Region", "Region", false), }}, {Category: EchoSvcCatMT, Provider: "ali", Name: "阿里机器翻译", Fields: []EchoSvcField{ esfText("AccessKeyId", "AccessKeyId", true), esfSecret("AccessKeySecret", "AccessKeySecret", true), esfText("Region", "Region", false), }}, {Category: EchoSvcCatMT, Provider: "google", Name: "Google 翻译", Fields: []EchoSvcField{ {Key: "JsonPath", Label: "服务账号 JSON", Type: "textarea", Secret: true, Required: true, Desc: "服务账号 JSON 内容或服务器上的文件路径"}, }}, {Category: EchoSvcCatMT, Provider: "microsoft", Name: "Azure 翻译", Fields: []EchoSvcField{ esfSecret("Key", "Key", true), esfText("Region", "Region", false), esfText("Endpoint", "Endpoint", false), }}, // ===== 总结(文本大模型;一个服务商可配多条不同模型)===== {Category: EchoSvcCatLLM, Provider: "doubao", Name: "豆包(字节)", Multi: true, Fields: []EchoSvcField{ esfSecret("Apikey", "ApiKey", true), esfText("Model", "模型 ID", true), }}, {Category: EchoSvcCatLLM, Provider: "openai", Name: "OpenAI 兼容端点(通义/Azure OpenAI 等)", Multi: true, Fields: []EchoSvcField{ esfText("BaseURL", "BaseURL", true), esfSecret("Token", "Token", true), esfText("Model", "模型 ID", true), }}, {Category: EchoSvcCatLLM, Provider: "gemini", Name: "Gemini(Google)", Multi: true, Fields: []EchoSvcField{ esfSecret("Apikey", "ApiKey", true), esfText("Model", "模型 ID", true), esfText("Endpoint", "自定义 Endpoint(可选)", false), }}, } // FindEchoSvcDef 按 (类别, provider) 取字段模板;找不到返回 nil。 func FindEchoSvcDef(category int32, provider string) *EchoSvcProviderDef { for i := range EchoSvcCatalog { if EchoSvcCatalog[i].Category == category && EchoSvcCatalog[i].Provider == provider { return &EchoSvcCatalog[i] } } return nil } // EchoSvcFieldIsSecret 判断某 (类别,provider,字段) 是否密钥字段。目录优先,回退通用命名规则。 func EchoSvcFieldIsSecret(category int32, provider, key string) bool { if def := FindEchoSvcDef(category, provider); def != nil { for _, f := range def.Fields { if f.Key == key { return f.Secret } } } return IsSecretField(key) } // EncryptEchoValues 返回 values 的副本,把密钥字段加密(空值/已是 EncMask 的不动,交由调用方决定)。 func EncryptEchoValues(encKey string, category int32, provider string, values map[string]string) map[string]string { out := make(map[string]string, len(values)) for k, v := range values { if EchoSvcFieldIsSecret(category, provider, k) && v != "" && v != EncMask { if ct, err := Encrypt(encKey, v); err == nil { out[k] = ct continue } } out[k] = v } return out } // DecryptEchoValues 返回 values 的副本,把密钥字段解密(供运行时消费)。 func DecryptEchoValues(encKey string, category int32, provider string, values map[string]string) map[string]string { out := make(map[string]string, len(values)) for k, v := range values { if EchoSvcFieldIsSecret(category, provider, k) && v != "" { plain, err := Decrypt(encKey, v) if err == nil { out[k] = plain continue } // 仍沿用密文(保持既有行为),但必须留痕:否则密文会被当明文用作第三方 API key, // 表现为鉴权失败,而后台显示"已配置",无从排查。 log.Errorf("[EchoSvcConfig] 密钥字段解密失败 category=%d provider=%s key=%s err=%v —— 检查本服务 ${FIELD_ENCRYPT_KEY} 是否与 console 完全一致", category, provider, k, err) } out[k] = v } return out } // MaskEchoValues 返回 values 的副本,把密钥字段脱敏为 EncMask(返回前端用)。 func MaskEchoValues(category int32, provider string, values map[string]string) map[string]string { out := make(map[string]string, len(values)) for k, v := range values { if EchoSvcFieldIsSecret(category, provider, k) && v != "" { out[k] = EncMask continue } out[k] = v } return out } // EnsureEchoSvcTable 建表(幂等)。db 为应用业务库。 func EnsureEchoSvcTable(db mysql.ISys) error { if db == nil { return nil } return db.CreateTable(TableEchomeetSvcConfig, &EchoSvcConfig{}) } // LoadEchoSvcConfigs 读全部会议记录服务配置行(未解密)。 func LoadEchoSvcConfigs(db mysql.ISys) ([]*EchoSvcConfig, error) { if db == nil { return nil, nil } rows := make([]*EchoSvcConfig, 0) if err := db.Find(TableEchomeetSvcConfig, &rows, ""); err != nil { return nil, err } return rows, nil }