package comm import ( "fmt" "strconv" "strings" "yunyan/pb" ) // 第三方服务池 + 会议记录服务编排 的共享数据结构。 // // 这些表都存 console 共享库(postgres):console 后台读写(modules/console 里用 type alias 引用), // 业务服务(如 home 的 echomeet 模块)运行时直读消费。加密字段与 console 用同一把 // ${FIELD_ENCRYPT_KEY}(comm.Encrypt/Decrypt,AES-256-GCM),两侧必须配同一个 key。 // // 嵌套字段([]SvcField / map[string]string)借助 gorm `serializer:json` 存 jsonb 列。 // SvcField 第三方服务的自定义字段项(内嵌于 ThirdSvcConfig.fields jsonb 列,也复用于模板/区域专属字段)。 // 加密字段:DefValue 存 AES-GCM 密文;console API 对加密字段只返回 "[encrypted]" 占位。 type SvcField struct { Key string `json:"key"` // 字段名,同服务内唯一,如 secret_key Description string `json:"description"` // 字段用途描述,前端展示 Type string `json:"type"` // 字段类型:空/text 普通文本;langs 支持语言多选(值=逗号分隔 BCP-47);lang 单选默认语言 Encrypted bool `json:"encrypted"` // 是否加密存储 DefValue string `json:"def_value"` // 默认值(全区域共用;加密字段存 AES-GCM 密文) Sort int32 `json:"sort"` // 展示排序,越小越靠前 } // ThirdSvcConfig 第三方服务配置(表 TableSvcConfig,按应用分离)。每行 = 某应用作用域下的一个第三方服务。 // AppName=''为全局默认、'X'为应用X的覆盖;解析时应用有则用应用的、无则回退全局。 // (app_name,id) 唯一;categories 存逗号分隔的类别枚举值。凭据全部装在 Fields。 type ThirdSvcConfig struct { RowId uint64 `gorm:"primaryKey;autoIncrement;column:row_id" json:"row_id"` AppName string `gorm:"column:app_name;size:64;uniqueIndex:uidx_svc_config,priority:1" json:"app_name"` // ''=全局默认 Id string `gorm:"column:id;size:64;uniqueIndex:uidx_svc_config,priority:2" json:"id"` Name string `gorm:"column:name;size:64" json:"name"` Provider string `gorm:"column:provider;size:64" json:"provider"` Categories string `gorm:"column:categories;size:64" json:"categories"` Description string `gorm:"column:description" json:"description"` Enable bool `gorm:"column:enable" json:"enable"` Fields []SvcField `gorm:"column:fields;type:jsonb;serializer:json" json:"fields"` Createtime int64 `gorm:"column:createtime" json:"createtime"` Updatetime int64 `gorm:"column:updatetime" json:"updatetime"` } // SvcRegionOverride 第三方服务区域字段覆盖(表 TableSvcRegionOverride)。 // (app_name,svc_id,region) 唯一;svc_id 引用同作用域下的 ThirdSvcConfig.Id。 type SvcRegionOverride struct { Id uint64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` AppName string `gorm:"column:app_name;size:64;uniqueIndex:uidx_svc_region,priority:1" json:"app_name"` // ''=全局默认 SvcId string `gorm:"column:svc_id;size:64;uniqueIndex:uidx_svc_region,priority:2" json:"svc_id"` Region int32 `gorm:"column:region;uniqueIndex:uidx_svc_region,priority:3" json:"region"` Overrides map[string]string `gorm:"column:overrides;type:jsonb;serializer:json" json:"overrides"` // 默认字段的值覆盖 ExtraFields []SvcField `gorm:"column:extra_fields;type:jsonb;serializer:json" json:"extra_fields"` // 区域专属新增字段 DisabledKeys []string `gorm:"column:disabled_keys;type:jsonb;serializer:json" json:"disabled_keys"` // 区域不适用的默认字段 key Updatetime int64 `gorm:"column:updatetime" json:"updatetime"` } // 会议记录编排类别(三段)。 const ( EchoKindASR int32 = 1 // 识别(录音文件转写) EchoKindMT int32 = 2 // 翻译 EchoKindLLM int32 = 3 // 总结(大模型) ) // EchoOrchestration 会议记录服务编排(表 TableEchomeetOrch)。一行 = 某作用域(app_name,region) // 下某类别选中的一个服务引用 + 排序/标记。不存凭据,svc_id 引用 ThirdSvcConfig.Id。 // 作用域解析顺序:(app,region) → (app,0) → ('',region) → ('',0) 全局默认兜底,越具体越优先。 type EchoOrchestration struct { RowId uint64 `gorm:"primaryKey;autoIncrement;column:row_id" json:"row_id"` AppName string `gorm:"column:app_name;size:64;uniqueIndex:uidx_echo_orch,priority:1" json:"app_name"` // ''=全局默认兜底 Region int32 `gorm:"column:region;uniqueIndex:uidx_echo_orch,priority:2" json:"region"` // 0=全区域兜底,>0=特定区域 Kind int32 `gorm:"column:kind;uniqueIndex:uidx_echo_orch,priority:3" json:"kind"` // EchoKindASR/MT/LLM SvcId string `gorm:"column:svc_id;size:64;uniqueIndex:uidx_echo_orch,priority:4" json:"svc_id"` // 引用 svc_config 的服务 id Priority int32 `gorm:"column:priority" json:"priority"` // 同类内升序,小=优先 IsDefault bool `gorm:"column:is_default" json:"is_default"` // 总结用:默认模型,同作用域内至多一条 ClientVisible bool `gorm:"column:client_visible" json:"client_visible"` // 总结用:是否允许客户端选择 CostHint float64 `gorm:"column:cost_hint" json:"cost_hint"` // 参考单价,仅排序/展示 Enabled bool `gorm:"column:enabled" json:"enabled"` Createtime int64 `gorm:"column:createtime" json:"createtime"` Updatetime int64 `gorm:"column:updatetime" json:"updatetime"` } // EchoOrchSetting 会议记录编排的作用域级开关(表 TableEchomeetOrchSetting)。每 (app_name, region) 一行。 type EchoOrchSetting struct { RowId uint64 `gorm:"primaryKey;autoIncrement;column:row_id" json:"row_id"` AppName string `gorm:"column:app_name;size:64;uniqueIndex:uidx_echo_orch_set,priority:1" json:"app_name"` Region int32 `gorm:"column:region;uniqueIndex:uidx_echo_orch_set,priority:2" json:"region"` // LLMClientChoice 是否允许客户端指定总结模型。false=强制默认模型;true=客户端可在 ClientVisible 且 Enabled 里选。 LLMClientChoice bool `gorm:"column:llm_client_choice" json:"llm_client_choice"` Updatetime int64 `gorm:"column:updatetime" json:"updatetime"` } // ResolveSvcPlainFields 合并区域覆盖并解密,产出某第三方服务的明文字段表。 // 算法与 console 的 getSvcRegionOverride 同构:DefValue ← Overrides[key] 覆盖, // 叠加 ExtraFields,剔除 DisabledKeys;Encrypted 字段用 Decrypt(encKey) 解密。 // 任一字段解密失败即返回 error(fail-closed,调用方应整体跳过该服务——密文当明文用 // 会在业务深处炸成不可理解的错误)。echomeet 编排与客户端下发共用本函数。 func ResolveSvcPlainFields(svc *ThirdSvcConfig, ovr *SvcRegionOverride, encKey string) (map[string]string, error) { disabled := map[string]bool{} if ovr != nil { for _, k := range ovr.DisabledKeys { disabled[k] = true } } fields := make(map[string]string, len(svc.Fields)+4) put := func(f SvcField, val string) error { if val == "" { fields[f.Key] = "" return nil } if f.Encrypted { plain, err := Decrypt(encKey, val) if err != nil { return fmt.Errorf("字段 %s 解密失败(检查 FIELD_ENCRYPT_KEY 是否与 console 一致): %w", f.Key, err) } fields[f.Key] = plain return nil } fields[f.Key] = val return nil } for _, f := range svc.Fields { if disabled[f.Key] { continue } val := f.DefValue if ovr != nil { if v, ok := ovr.Overrides[f.Key]; ok { val = v } } if err := put(f, val); err != nil { return nil, err } } if ovr != nil { for _, f := range ovr.ExtraFields { if err := put(f, f.DefValue); err != nil { return nil, err } } } return fields, nil } // ============================== 服务端专用类别(凭据绝不下发客户端) ============================== // // svc_config 里的服务默认是「下发给客户端用的第三方能力」——resolveThirdSvcs 会把 encrypted 字段 // **解密成明文**塞进 user_getthirdsvcs_v2 / user_getappconfig_v3 的响应里。 // // 但有些第三方能力天然只能由服务端调用,凭据是**主账号级**的(如实名认证用的阿里云 AccessKey、 // 腾讯云 SecretKey,拿到就能操作整个云账号)。这类服务放进 svc_config 是为了统一管理入口 // (后台「第三方服务配置 → 添加服务」),但**必须在下发口拦掉**。 // // 于是有了这份「服务端专用类别」名单:categories 命中任一即整条服务不下发。 // 新增服务端专用类别时,只需在 serverOnlySvcCats 里加一行。 // SvcCatMT 机器翻译(与 console svcCatMT、admin 前端 SCAT 的 3 一致)。 // 实时翻译接口 user_translate 按这个类别挑服务;它**不是**服务端专用类别—— // 只是客户端现在不直接消费第三方服务下发,改由服务端代调,凭据因此不出网关。 const SvcCatMT int32 = 3 // SvcCatIdVerify 身份证实名核验(阿里云 Id2MetaVerify / 腾讯云 IdCardVerification)。 // 服务端专用——凭据是云账号主 AK/SK,绝不下发客户端。 const SvcCatIdVerify int32 = 11 // serverOnlySvcCats 服务端专用类别集合:命中的服务不随任何客户端接口下发。 var serverOnlySvcCats = map[int32]bool{ SvcCatIdVerify: true, } // IsServerOnlySvc 判断某服务(按其逗号分隔的 categories)是否服务端专用、不得下发客户端。 // 只要类别里含任一服务端专用类别就返回 true——宁可少发也不能把主账号凭据发出去。 func IsServerOnlySvc(categories string) bool { for _, p := range strings.Split(categories, ",") { v, err := strconv.Atoi(strings.TrimSpace(p)) if err != nil { continue } if serverOnlySvcCats[int32(v)] { return true } } return false } // CategoriesHas 判断逗号分隔的 categories 是否含指定类别值。 func CategoriesHas(categories string, cat int32) bool { want := strconv.Itoa(int(cat)) for _, p := range strings.Split(categories, ",") { if strings.TrimSpace(p) == want { return true } } return false } // ============================== MCP 服务(并入 svc_config 的 MCP 服务类型) ============================== // // MCP(模型上下文协议)服务已并入第三方服务:categories 含 SvcCatMCP,字段固定为 url/type/tools(均明文)。 // 服务定义一次(全局 app_name=''),各区域的 url/tools 通过 SvcRegionOverride 分叉;运行时按客户端区域解析。 // SvcCatMCP MCP 服务在 svc_config.categories 中的类别值(与 console svcCatMCP、admin 前端 SCAT 一致)。 const SvcCatMCP int32 = 10 // CategoriesHasMCP 判断逗号分隔的 categories 是否包含 MCP 类别。 func CategoriesHasMCP(categories string) bool { want := strconv.Itoa(int(SvcCatMCP)) for _, p := range strings.Split(categories, ",") { if strings.TrimSpace(p) == want { return true } } return false } // ResolveMcpServer 把一个 MCP 类第三方服务在指定区域解析成 *pb.DBMcpServer。 // 复用 ResolveSvcPlainFields 叠加区域覆盖(MCP 字段均明文,encKey 传 "")。 // 解析后的 url 为空(该区域未配置 / 被停用)返回 nil——调用方应跳过。 func ResolveMcpServer(svc *ThirdSvcConfig, ovr *SvcRegionOverride, region int32) *pb.DBMcpServer { fields, err := ResolveSvcPlainFields(svc, ovr, "") if err != nil { return nil } url := strings.TrimSpace(fields["url"]) if url == "" { return nil } stype := int32(0) if v, e := strconv.Atoi(strings.TrimSpace(fields["type"])); e == nil { stype = int32(v) } return &pb.DBMcpServer{ Id: svc.Id, Name: svc.Name, Region: pb.Region(region), Description: svc.Description, Url: url, Stype: pb.MCPServerType(stype), Tools: fields["tools"], Enable: true, } } // McpBaseFields 返回 MCP 服务基础行的固定字段定义(DefValue 留空,实际值走区域覆盖)。 func McpBaseFields() []SvcField { return []SvcField{ {Key: "url", Description: "服务地址,如 https://...", Sort: 0}, {Key: "type", Description: "传输类型:0=HTTP,1=SSE", Sort: 1}, {Key: "tools", Description: "启用工具(逗号分隔,空=全部)", Sort: 2}, } }