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.
289 lines
9.6 KiB
289 lines
9.6 KiB
package echomeet
|
|
|
|
import (
|
|
"fmt"
|
|
"sort"
|
|
"strings"
|
|
|
|
"yunyan/comm"
|
|
"yunyan/lego/sys/log"
|
|
"yunyan/lego/sys/postgres"
|
|
)
|
|
|
|
// 编排解析层:从 console 共享库(postgres)读「会议记录编排 echomeet_orch + 服务池 svc_config」,
|
|
// 解析出本部署 (app_name, region) 作用域下三段(识别/翻译/总结)各自的服务列表与明文凭据。
|
|
//
|
|
// - 作用域分层:(app,region) → (app,0) → ('',region) → ('',0),对每个 kind 取「最具体的有条目的层」。
|
|
// - 服务池按 app_name 两层:应用覆盖全局(同 id 应用优先);区域字段覆盖(svc_region_override)按
|
|
// 服务行自身的作用域匹配。
|
|
// - 加密字段用 comm.Decrypt(${FIELD_ENCRYPT_KEY}) 解密;解密失败该服务整体不可用(fail-closed,
|
|
// 参照 voitrans 事故:密文当明文用会在深处炸成不可理解的错误)。
|
|
// - ASR 服务的 callback_path 是相对路径,完整回调 = app_registry.base_url + callback_path。
|
|
|
|
// resolvedSvc 解析完成的服务:明文字段 + 语言支持 + 拼好的回调地址。
|
|
type resolvedSvc struct {
|
|
SvcId string
|
|
Name string
|
|
Provider string
|
|
Fields map[string]string // 区域合并 + 解密后的明文字段
|
|
Languages []string // fields["languages"] 拆分(BCP-47),空=不限
|
|
CallbackURL string // ASR 用:base_url + callback_path;无 callback_path 则空
|
|
}
|
|
|
|
// orchSnapshot 一次完整解析的快照(providersComp 持有,读写锁保护整体替换)。
|
|
type orchSnapshot struct {
|
|
ASR []*comm.EchoOrchestration // priority 升序(仅 Enabled 且服务可用的条目)
|
|
MT []*comm.EchoOrchestration
|
|
LLM []*comm.EchoOrchestration
|
|
Setting comm.EchoOrchSetting
|
|
// Services 三段引用到的服务解析结果,key=svc_id。构建 provider 实例时消费。
|
|
Services map[string]*resolvedSvc
|
|
}
|
|
|
|
// scopeRank 作用域具体度:越大越具体。
|
|
func scopeRank(appName string, region int32, app string, reg int32) int {
|
|
switch {
|
|
case appName == app && region == reg && app != "":
|
|
return 4 // (app, region)
|
|
case appName == app && region == 0 && app != "":
|
|
return 3 // (app, 0)
|
|
case appName == "" && region == reg && reg != 0:
|
|
return 2 // ('', region)
|
|
case appName == "" && region == 0:
|
|
return 1 // ('', 0) 全局默认
|
|
default:
|
|
return 0 // 不属于本部署作用域链(如别的区域的行)
|
|
}
|
|
}
|
|
|
|
// loadOrchSnapshot 全量加载并解析编排。app/region 是本部署身份(region 为 pb.Region int32,
|
|
// regionCode 是 app_registry 用的字符串码),encKey 是字段解密密钥。
|
|
func loadOrchSnapshot(app string, region int32, regionCode, encKey string) (*orchSnapshot, error) {
|
|
pg := postgres.GetSys()
|
|
if pg == nil {
|
|
return nil, fmt.Errorf("postgres 未初始化")
|
|
}
|
|
|
|
// 1. 拉本部署作用域链上的全部编排行,按 kind 分层挑最具体。
|
|
rows := make([]*comm.EchoOrchestration, 0)
|
|
if err := postgres.Find(comm.TableEchomeetOrch, &rows,
|
|
"(app_name=? OR app_name='') AND (region=? OR region=0)", app, region); err != nil && err != postgres.ErrNoDocuments {
|
|
return nil, fmt.Errorf("读编排表失败: %w", err)
|
|
}
|
|
byKind := map[int32][]*comm.EchoOrchestration{}
|
|
kindRank := map[int32]int{}
|
|
for _, r := range rows {
|
|
rank := scopeRank(r.AppName, r.Region, app, region)
|
|
if rank == 0 {
|
|
continue
|
|
}
|
|
if rank > kindRank[r.Kind] {
|
|
kindRank[r.Kind] = rank
|
|
byKind[r.Kind] = byKind[r.Kind][:0]
|
|
}
|
|
if rank == kindRank[r.Kind] {
|
|
byKind[r.Kind] = append(byKind[r.Kind], r)
|
|
}
|
|
}
|
|
for _, list := range byKind {
|
|
sort.Slice(list, func(i, j int) bool { return list[i].Priority < list[j].Priority })
|
|
}
|
|
|
|
// 2. 作用域开关:同样取最具体的一行。
|
|
snap := &orchSnapshot{Services: map[string]*resolvedSvc{}}
|
|
settings := make([]*comm.EchoOrchSetting, 0)
|
|
if err := postgres.Find(comm.TableEchomeetOrchSetting, &settings,
|
|
"(app_name=? OR app_name='') AND (region=? OR region=0)", app, region); err != nil && err != postgres.ErrNoDocuments {
|
|
return nil, fmt.Errorf("读编排开关失败: %w", err)
|
|
}
|
|
best := 0
|
|
for _, s := range settings {
|
|
if rank := scopeRank(s.AppName, s.Region, app, region); rank > best {
|
|
best = rank
|
|
snap.Setting = *s
|
|
}
|
|
}
|
|
|
|
// 3. 收集引用到的 svc_id,拉服务池(应用覆盖全局)。
|
|
idset := map[string]bool{}
|
|
for _, list := range byKind {
|
|
for _, r := range list {
|
|
if r.Enabled {
|
|
idset[r.SvcId] = true
|
|
}
|
|
}
|
|
}
|
|
if len(idset) > 0 {
|
|
ids := make([]string, 0, len(idset))
|
|
for id := range idset {
|
|
ids = append(ids, id)
|
|
}
|
|
svcs := make([]*comm.ThirdSvcConfig, 0)
|
|
if err := postgres.Find(comm.TableSvcConfig, &svcs,
|
|
"(app_name=? OR app_name='') AND id IN ?", app, ids); err != nil && err != postgres.ErrNoDocuments {
|
|
return nil, fmt.Errorf("读服务池失败: %w", err)
|
|
}
|
|
picked := map[string]*comm.ThirdSvcConfig{}
|
|
for _, s := range svcs {
|
|
if !s.Enable {
|
|
continue
|
|
}
|
|
if exist, ok := picked[s.Id]; !ok || (exist.AppName == "" && s.AppName == app && app != "") {
|
|
picked[s.Id] = s
|
|
}
|
|
}
|
|
|
|
// 4. 区域字段覆盖:按服务行自身的作用域 + 本部署区域匹配。
|
|
ovrs := make([]*comm.SvcRegionOverride, 0)
|
|
if region != 0 {
|
|
if err := postgres.Find(comm.TableSvcRegionOverride, &ovrs,
|
|
"(app_name=? OR app_name='') AND svc_id IN ? AND region=?", app, ids, region); err != nil && err != postgres.ErrNoDocuments {
|
|
return nil, fmt.Errorf("读区域覆盖失败: %w", err)
|
|
}
|
|
}
|
|
ovrOf := func(svc *comm.ThirdSvcConfig) *comm.SvcRegionOverride {
|
|
for _, o := range ovrs {
|
|
if o.SvcId == svc.Id && o.AppName == svc.AppName {
|
|
return o
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// 5. 拼回调所需的部署对外地址(仅 ASR 服务带 callback_path 时用到)。
|
|
baseURL := loadAppBaseURL(app, regionCode)
|
|
|
|
for _, svc := range picked {
|
|
rs, err := resolveSvcFields(svc, ovrOf(svc), encKey)
|
|
if err != nil {
|
|
// fail-closed:字段解不开的服务不可用,选路会跳过它。
|
|
log.Warnf("echomeet 编排: 服务 %s 字段解析失败已跳过: %v", svc.Id, err)
|
|
continue
|
|
}
|
|
if p := rs.Fields["callback_path"]; p != "" {
|
|
if baseURL == "" {
|
|
log.Warnf("echomeet 编排: 服务 %s 配了 callback_path 但查不到部署对外地址(app_registry.base_url),回调留空、转写只能靠轮询", svc.Id)
|
|
} else {
|
|
rs.CallbackURL = strings.TrimRight(baseURL, "/") + p
|
|
}
|
|
}
|
|
snap.Services[svc.Id] = rs
|
|
}
|
|
}
|
|
|
|
// 6. 只保留「启用且服务已解析成功」的编排条目。
|
|
keep := func(list []*comm.EchoOrchestration) []*comm.EchoOrchestration {
|
|
out := make([]*comm.EchoOrchestration, 0, len(list))
|
|
for _, r := range list {
|
|
if r.Enabled && snap.Services[r.SvcId] != nil {
|
|
out = append(out, r)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
snap.ASR = keep(byKind[comm.EchoKindASR])
|
|
snap.MT = keep(byKind[comm.EchoKindMT])
|
|
snap.LLM = keep(byKind[comm.EchoKindLLM])
|
|
return snap, nil
|
|
}
|
|
|
|
// resolveSvcFields 合并区域覆盖并解密,产出明文字段表。
|
|
// 算法与 console 的 getSvcRegionOverride 同构:DefValue ← Overrides[key] 覆盖,
|
|
// 叠加 ExtraFields,剔除 DisabledKeys;Encrypted 字段 comm.Decrypt。
|
|
func resolveSvcFields(svc *comm.ThirdSvcConfig, ovr *comm.SvcRegionOverride, encKey string) (*resolvedSvc, 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 comm.SvcField, val string) error {
|
|
if val == "" {
|
|
fields[f.Key] = ""
|
|
return nil
|
|
}
|
|
if f.Encrypted {
|
|
plain, err := comm.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
|
|
}
|
|
}
|
|
}
|
|
rs := &resolvedSvc{SvcId: svc.Id, Name: svc.Name, Provider: svc.Provider, Fields: fields}
|
|
if langs := strings.TrimSpace(fields["languages"]); langs != "" {
|
|
for _, l := range strings.Split(langs, ",") {
|
|
if l = strings.TrimSpace(l); l != "" {
|
|
rs.Languages = append(rs.Languages, l)
|
|
}
|
|
}
|
|
}
|
|
return rs, nil
|
|
}
|
|
|
|
// loadAppBaseURL 从 app_registry 取本部署的对外地址(console 注册表,region 用字符串码)。
|
|
// 查不到返回空串(调用侧告警,回调留空走轮询兜底)。
|
|
func loadAppBaseURL(app, regionCode string) string {
|
|
if app == "" {
|
|
return ""
|
|
}
|
|
var baseURL string
|
|
pg := postgres.GetSys()
|
|
if pg == nil {
|
|
return ""
|
|
}
|
|
if err := pg.Table(comm.TableAppRegistry).Where("app_name=? AND region=?", app, regionCode).
|
|
Limit(1).Pluck("base_url", &baseURL).Error; err != nil {
|
|
log.Warnf("echomeet 编排: 读 app_registry.base_url 失败 app=%s region=%s err=%v", app, regionCode, err)
|
|
return ""
|
|
}
|
|
return baseURL
|
|
}
|
|
|
|
// svcSupportsLanguage 语言过滤:languages 为空=不限;否则要求精确命中或主语言段一致
|
|
// (zh 命中 zh-CN;zh-CN 命中 zh)。绝不做「未知语言→默认中文」式回退。
|
|
func svcSupportsLanguage(langs []string, bcp47 string) bool {
|
|
if len(langs) == 0 {
|
|
return true
|
|
}
|
|
want := strings.TrimSpace(bcp47)
|
|
if want == "" {
|
|
return true
|
|
}
|
|
wantBase, _, _ := strings.Cut(want, "-")
|
|
for _, l := range langs {
|
|
if strings.EqualFold(l, want) {
|
|
return true
|
|
}
|
|
base, _, _ := strings.Cut(l, "-")
|
|
if strings.EqualFold(base, wantBase) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|