package comm import "strings" // 通话翻译规则(表 TableCallTranslateRule,存 console 共享库 postgres)。 // // 与 svcpool.go / agentcfg.go 同一模式:console 后台读写(modules/console 用 type alias 引用), // 业务服务运行时直读并用 MatchCallTranslateRule 选路。 // // 取代旧的 call_translate_pair(一行只能表达一个 src→dst、且直接拼 asr/mt/tts 服务): // 一条规则可覆盖「多源语言 × 多目标语言」,并按优先级择一命中;具体用什么服务不再由规则拼装, // 而是引用一个 AgentConfig(类型必须是 translate 三段式 或 ast-translate 端到端), // 服务编排收敛到 agent 里,规则只负责「什么语言走哪个 agent」。 // LangAny 语言通配符:出现在 SrcLangs/DstLangs 里表示「任意语言」。 const LangAny = "*" // RegionAny 区域通配:Region=0 表示该规则不限区域(REGIONS 枚举从 1 开始,0 未占用)。 const RegionAny int32 = 0 // CallTranslateRule 通话翻译选路规则。一行 = 某应用作用域 + 某区域下的一条「语言集合 → agent」映射。 // // SrcLangs / DstLangs:逗号分隔 BCP-47(与 ThirdSvcConfig.Languages、AgentConfig.SupportLangs 同一约定), // 单个 "*" 表示任意语言。Bidir=true 时反向(dst→src)也命中同一条规则。 // Priority 越大越优先;同优先级时精确规则先于通配规则(见 MatchCallTranslateRule)。 type CallTranslateRule struct { Id uint64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"` AppName string `gorm:"column:app_name;size:64;index" json:"app_name"` // ''=全局默认 Region int32 `gorm:"column:region;index" json:"region"` // 0=任意区域 Name string `gorm:"column:name;size:64" json:"name"` // 规则名,仅后台展示用 SrcLangs string `gorm:"column:src_langs;size:512" json:"src_langs"` DstLangs string `gorm:"column:dst_langs;size:512" json:"dst_langs"` Bidir bool `gorm:"column:bidir" json:"bidir"` AgentId string `gorm:"column:agent_id;size:64" json:"agent_id"` // 引用 AgentConfig.Id(translate / ast-translate) Priority int32 `gorm:"column:priority;index" json:"priority"` // 越大越优先 Enable bool `gorm:"column:enable" json:"enable"` Remark string `gorm:"column:remark" json:"remark"` Createtime int64 `gorm:"column:createtime" json:"createtime"` Updatetime int64 `gorm:"column:updatetime" json:"updatetime"` } // SplitLangs 把逗号分隔的语言串切成切片(去空白、丢空项)。 func SplitLangs(s string) []string { out := make([]string, 0, 4) for _, x := range strings.Split(s, ",") { if v := strings.TrimSpace(x); v != "" { out = append(out, v) } } return out } // baseLang 取 BCP-47 的主语言子标签:zh-CN → zh。 func baseLang(s string) string { s = strings.TrimSpace(strings.ToLower(s)) if i := strings.IndexByte(s, '-'); i > 0 { return s[:i] } return s } // 单侧语言的匹配质量。不能用「先严格扫一轮、没命中再放宽」的两轮做法: // 通配规则在严格轮就能接住任意输入,宽松轮永远轮不到,zh-CN 的规则会被 * 兜底规则吃掉。 // 故把质量并入排序——全等 > 主语言相同 > 通配。 const ( langMiss = -1 // 不命中 langWild = 0 // 被 "*" 接住 langBase = 1 // 主语言子标签相同(zh-CN 规则接住 zh / zh-TW 的请求) langExact = 2 // 全等 ) // langSetScore 给「语言集合 × 目标语言」打分,取集合内的最好一档;不命中返回 langMiss。 func langSetScore(set, lang string) int { lang = strings.TrimSpace(lang) if lang == "" { return langMiss } best := langMiss for _, x := range SplitLangs(set) { s := langMiss switch { case x == LangAny: s = langWild case strings.EqualFold(x, lang): s = langExact case baseLang(x) == baseLang(lang): s = langBase } if s > best { best = s } } return best } // ruleMatchScore 单条规则对 src→dst 的匹配得分(两侧分数之和,0~4);不命中返回 langMiss。 // Bidir 规则同时试反向,取更好的一次。 func ruleMatchScore(r *CallTranslateRule, src, dst string) int { score := langMiss if a, b := langSetScore(r.SrcLangs, src), langSetScore(r.DstLangs, dst); a >= 0 && b >= 0 { score = a + b } if r.Bidir { if a, b := langSetScore(r.SrcLangs, dst), langSetScore(r.DstLangs, src); a >= 0 && b >= 0 && a+b > score { score = a + b } } return score } // ruleSpecificity 规则精确度:src/dst 每一侧非通配各记 1 分。用于同优先级下让精确规则压过兜底规则。 func ruleSpecificity(r *CallTranslateRule) int { n := 0 if strings.TrimSpace(r.SrcLangs) != LangAny { n++ } if strings.TrimSpace(r.DstLangs) != LangAny { n++ } return n } // betterRule 判断 a 是否比 b 更该被选中:优先级大 > 匹配得分高 > 精确度高 > id 小(先建的先生效)。 func betterRule(a *CallTranslateRule, aScore int, b *CallTranslateRule, bScore int) bool { if a.Priority != b.Priority { return a.Priority > b.Priority } if aScore != bScore { return aScore > bScore } if sa, sb := ruleSpecificity(a), ruleSpecificity(b); sa != sb { return sa > sb } return a.Id < b.Id } // MatchCallTranslateRule 从规则集里为 region + src→dst 选出唯一生效的规则,无命中返回 nil。 // // 选路顺序: // 1. 只看 Enable 的规则;规则的 Region 须等于入参 region,或为 RegionAny(0) 表示不限区域。 // 2. 语言两侧都命中才算候选("*" 通配、全等、主语言子标签相同都算命中,见 langSetScore)。 // 3. 候选间按 betterRule 取最优:优先级 > 匹配得分 > 精确度 > 建立顺序。 // // 调用方拿到规则后用 AgentId 去 agent_config 取具体服务编排(三段式或端到端由 agent 类型决定)。 func MatchCallTranslateRule(rules []*CallTranslateRule, region int32, srcLang, dstLang string) *CallTranslateRule { var best *CallTranslateRule bestScore := langMiss for _, r := range rules { if r == nil || !r.Enable { continue } if r.Region != RegionAny && r.Region != region { continue } score := ruleMatchScore(r, srcLang, dstLang) if score < 0 { continue } if best == nil || betterRule(r, score, best, bestScore) { best, bestScore = r, score } } return best }