package user import ( "sort" "yunyan/comm" "yunyan/lego/sys/log" "yunyan/lego/sys/postgres" "yunyan/pb" ) // 通话翻译配置的下发(user_getappconfig_v3 的 call_translate 字段)。 // // 对应后台「通话翻译配置」的两层模型:规则(call_translate_rule)只管「什么语言走哪个 agent」, // 具体调哪套 asr/mt/tts(或端到端 ast)在翻译 agent(agent_config, type=translate|ast-translate) 里。 // 两层一起下发,客户端按 comm.MatchCallTranslateRule 的口径选路(口径同步写在 proto 注释里)。 // // 之所以下发规则集而不是让客户端每次通话来问一次:通话建立时选路要即时,多一次往返就是首句延迟。 // callTranslateV3 组装下发客户端的通话翻译配置:本区域生效的规则 + 这些规则引用到的翻译 agent。 // // 三处筛选都在服务端做掉,客户端不必再判: // - 区域:只留本区域的规则与不限区域(RegionAny)的兜底规则; // - 启停:停用的规则、停用的 agent 都不下发; // - 悬空引用:agent 找不到或类型不对的规则整条丢掉——下发一条指向不存在 agent 的规则, // 客户端要到通话中途才发现没法翻译。 // // 取不到配置不让整个 appconfig 失败:通话翻译是可选能力,没配它的应用(乃至库里还没建这张表的 // 老部署)应当照常拿到其余配置,故这里只记日志、返回空壳(字段都在、列表为空)。 func (this *apiV3Comp) callTranslateV3(region pb.Region) *pb.CallTranslateItem { out := &pb.CallTranslateItem{ Rules: []*pb.CallTranslateRuleItem{}, Agents: []*pb.AgentItemV3{}, } app := comm.AppName() rules, err := loadCallTranslateRules(app, region) if err != nil { this.module.Error("加载通话翻译规则失败!", log.Field{Key: "app", Value: app}, log.Field{Key: "err", Value: err.Error()}) return out } if len(rules) == 0 { return out } list, err := pickAgentsV3(app, comm.AgentTypeIsTranslate) if err != nil { this.module.Error("加载翻译 agent 失败!", log.Field{Key: "app", Value: app}, log.Field{Key: "err", Value: err.Error()}) return out } byId := make(map[string]*comm.AgentConfig, len(list)) for _, a := range list { byId[a.Id] = a } providerOf := this.svcProviders(app) added := make(map[string]bool, len(byId)) for _, r := range rules { a, ok := byId[r.AgentId] if !ok { this.module.Warn("通话翻译规则引用不到可用的翻译 agent,已跳过", log.Field{Key: "rule", Value: r.Id}, log.Field{Key: "agent", Value: r.AgentId}) continue } out.Rules = append(out.Rules, &pb.CallTranslateRuleItem{ Id: r.Id, Name: r.Name, SrcLangs: r.SrcLangs, DstLangs: r.DstLangs, Bidir: r.Bidir, AgentId: r.AgentId, Priority: r.Priority, }) if !added[a.Id] { added[a.Id] = true out.Agents = append(out.Agents, agentItemV3Of(a, providerOf[voiceSvcIdOf(a)])) } } return out } // loadCallTranslateRules 取本应用作用域下、本区域生效且启用的规则,按生效顺序(优先级降序 → id 升序)排好。 // // 作用域是「整套覆盖」而非按条合并:应用自己配了规则就只用应用那套,一条都没有才回退全局默认。 // 规则之间靠优先级互相压制,两套混在一起优先级会打架——全局的兜底规则可能压过应用的精确规则。 // (规则引用的 agent 仍是按条解析、应用优先回退全局,见 pickAgentsV3,与 console 的 findAgentForRule 一致。) func loadCallTranslateRules(app string, region pb.Region) ([]*comm.CallTranslateRule, error) { rows := make([]*comm.CallTranslateRule, 0) if err := postgres.Find(comm.TableCallTranslateRule, &rows, "(app_name=? OR app_name='')", app); err != nil && err != postgres.ErrNoDocuments { return nil, err } return selectCallTranslateRules(rows, app, region), nil } // selectCallTranslateRules 从「应用行 + 全局行」的原始规则里挑出实际下发的那批(作用域覆盖 → 区域/启停过滤 → 排序)。 // 与查库分开,便于直接用数据验证选取口径。 func selectCallTranslateRules(rows []*comm.CallTranslateRule, app string, region pb.Region) []*comm.CallTranslateRule { scoped := make([]*comm.CallTranslateRule, 0, len(rows)) if app != "" { for _, r := range rows { if r.AppName == app { scoped = append(scoped, r) } } } if len(scoped) == 0 { for _, r := range rows { if r.AppName == "" { scoped = append(scoped, r) } } } out := make([]*comm.CallTranslateRule, 0, len(scoped)) for _, r := range scoped { if r.Enable && (r.Region == comm.RegionAny || r.Region == int32(region)) { out = append(out, r) } } sort.Slice(out, func(i, j int) bool { if out[i].Priority != out[j].Priority { return out[i].Priority > out[j].Priority } return out[i].Id < out[j].Id }) return out }