You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

259 lines
13 KiB

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
// SvcCatAST 端到端实时翻译(通话翻译走的阿里 livetranslate 等,与 admin 前端 SCAT 的 4 一致)。
// 客户端拿着它的凭据**直连服务商**,翻译过程不经过我们的服务器——VIP 过期后唯一的服务端手段
// 就是不下发这一类凭据(见 modules/user 的 filterSvcsByVip)。
const SvcCatAST int32 = 4
// 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},
}
}