package console import ( "strings" "time" "yunyan/comm" "yunyan/lego/sys/postgres" "yunyan/pb" "github.com/gin-gonic/gin" ) // ============================ 通话翻译配置域 (calltranslate) ============================ // // 数据存 console 主库(postgres.GetSys()),模型见 comm/calltranslate.go。 // // 规则化模型(取代旧的 call_translate_pair 语言对表): // 一条规则 = 某应用作用域 + 某区域下的「多源语言 × 多目标语言 → 一个 agent」映射,带优先级。 // 运行时按 comm.MatchCallTranslateRule 选路:优先级大者先,同优先级精确规则压过 * 通配规则。 // 规则不再直接拼 asr/mt/tts —— 服务编排收敛在 agent 里,规则只决定「什么语言走哪个 agent」。 // 规则可引用的 agent 类型:三段式翻译 与 端到端翻译。chat 类不能用于通话翻译选路。 var callTranslateAgentTypes = []string{comm.AgentTypeTranslate, comm.AgentTypeAstTranslate} // findAgentForRule 在规则的应用作用域内找 agent,未命中回退全局(与 validateAgentSvcRefs 的解析口径一致)。 func findAgentForRule(appName, agentId string) (*AgentConfig, error) { a := &AgentConfig{} err := postgres.FindOne(comm.TableAgentConfig, a, "app_name=? AND id=?", appName, agentId) if err != nil && appName != "" { err = postgres.FindOne(comm.TableAgentConfig, a, "app_name=? AND id=?", "", agentId) } return a, err } // validateRule 校验规则自身与其引用的 agent,返回错误说明(空串=通过)。 func validateRule(m *comm.CallTranslateRule) string { if len(comm.SplitLangs(m.SrcLangs)) == 0 { return "源语言 必选(或选「任意语言」)" } if len(comm.SplitLangs(m.DstLangs)) == 0 { return "目标语言 必选(或选「任意语言」)" } if strings.TrimSpace(m.AgentId) == "" { return "翻译 Agent 必选" } a, err := findAgentForRule(m.AppName, m.AgentId) if err != nil { return "引用的 Agent 不存在: " + m.AgentId } ok := false for _, t := range callTranslateAgentTypes { if a.Type == t { ok = true break } } if !ok { return "Agent「" + a.Name + "」类型为 " + a.Type + ",通话翻译只能引用 translate(三段式)或 ast-translate(端到端)" } return "" } // getCallTranslateRules 列出某作用域下的规则;region>0 只取该区域 + 不限区域(0)的规则,region<=0 取全量。 // 返回已按生效顺序排好序(优先级降序 → id 升序),前端直接按序展示即可。 func (this *serverComp) getCallTranslateRules(c *gin.Context) { var req struct { AppName string `json:"app_name"` Region int32 `json:"region"` } _ = c.ShouldBindJSON(&req) items := make([]*comm.CallTranslateRule, 0) var err error if req.Region > 0 { err = postgres.Find(comm.TableCallTranslateRule, &items, "app_name=? AND (region=? OR region=?)", req.AppName, req.Region, comm.RegionAny) } else { err = postgres.Find(comm.TableCallTranslateRule, &items, "app_name=?", req.AppName) } if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } sortRulesByEffect(items) writeOK(c, gin.H{"items": items}) } // sortRulesByEffect 按运行时生效顺序排序:优先级降序 → id 升序(插入排序,规则量级很小)。 func sortRulesByEffect(items []*comm.CallTranslateRule) { for i := 1; i < len(items); i++ { cur := items[i] j := i - 1 for j >= 0 && (items[j].Priority < cur.Priority || (items[j].Priority == cur.Priority && items[j].Id > cur.Id)) { items[j+1] = items[j] j-- } items[j+1] = cur } } // saveCallTranslateRule 新增或更新一条规则(id=0 新增,否则按 id 更新)。 func (this *serverComp) saveCallTranslateRule(c *gin.Context) { var m comm.CallTranslateRule if err := c.ShouldBindJSON(&m); err != nil { writeErr(c, pb.ErrorCode_ReqParameterError, err.Error()) return } if msg := validateRule(&m); msg != "" { writeErr(c, pb.ErrorCode_ReqParameterError, msg) return } now := time.Now().UnixMilli() m.Updatetime = now var err error if m.Id == 0 { m.Createtime = now err = postgres.Insert(comm.TableCallTranslateRule, &m) } else { old := &comm.CallTranslateRule{} if e := postgres.FindOne(comm.TableCallTranslateRule, old, "id=?", m.Id); e != nil { writeErr(c, pb.ErrorCode_ReqParameterError, "规则不存在") return } m.Createtime = old.Createtime err = postgres.Save(comm.TableCallTranslateRule, &m) } if err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } broadcastConfigChanged(comm.ConfigKindCallTranslate, "update", m.Region) writeOK(c, &m) } // delCallTranslateRules 按 id 批量删除规则。 func (this *serverComp) delCallTranslateRules(c *gin.Context) { var req struct { Ids []uint64 `json:"ids"` } _ = c.ShouldBindJSON(&req) if len(req.Ids) == 0 { writeErr(c, pb.ErrorCode_ReqParameterError, "ids 必填") return } if err := postgres.Delete(comm.TableCallTranslateRule, "id IN ?", req.Ids); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } broadcastConfigChanged(comm.ConfigKindCallTranslate, "delete", 0) writeOK(c, gin.H{"deleted": len(req.Ids)}) } // syncCallTranslateRules 把指定规则复制到多个目标区域。 // 目标区域已有「同 agent + 同源语言集 + 同目标语言集」的规则则覆盖,否则新建;源区域自身跳过。 func (this *serverComp) syncCallTranslateRules(c *gin.Context) { var req struct { RuleIds []uint64 `json:"rule_ids"` TargetRegions []int32 `json:"target_regions"` } if err := c.ShouldBindJSON(&req); err != nil || len(req.RuleIds) == 0 { writeErr(c, pb.ErrorCode_ReqParameterError, "rule_ids 必填") return } if len(req.TargetRegions) == 0 { writeOK(c, gin.H{"synced": 0}) return } srcRules := make([]*comm.CallTranslateRule, 0, len(req.RuleIds)) if err := postgres.Find(comm.TableCallTranslateRule, &srcRules, "id IN ?", req.RuleIds); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } now := time.Now().UnixMilli() synced := 0 for _, r := range srcRules { for _, tgt := range req.TargetRegions { if r.Region == tgt { continue } old := &comm.CallTranslateRule{} _ = postgres.FindOne(comm.TableCallTranslateRule, old, "app_name=? AND region=? AND agent_id=? AND src_langs=? AND dst_langs=?", r.AppName, tgt, r.AgentId, r.SrcLangs, r.DstLangs) m := &comm.CallTranslateRule{ Id: old.Id, AppName: r.AppName, Region: tgt, Name: r.Name, SrcLangs: r.SrcLangs, DstLangs: r.DstLangs, Bidir: r.Bidir, AgentId: r.AgentId, Priority: r.Priority, Enable: r.Enable, Remark: r.Remark, Updatetime: now, } if old.Id == 0 { m.Createtime = now if err := postgres.Insert(comm.TableCallTranslateRule, m); err == nil { synced++ } } else { m.Createtime = old.Createtime if err := postgres.Save(comm.TableCallTranslateRule, m); err == nil { synced++ } } } } if synced > 0 { for _, tgt := range req.TargetRegions { broadcastConfigChanged(comm.ConfigKindCallTranslate, "update", tgt) } } writeOK(c, gin.H{"synced": synced}) } // testCallTranslateRule 选路试算:给定区域与源/目标语言,返回实际会命中的规则及其 agent。 // 规则一多就很难靠肉眼确认「zh-CN→ja-JP 到底走哪条」,故给后台一个所见即所得的验证入口, // 走的是与运行时完全相同的 comm.MatchCallTranslateRule。 func (this *serverComp) testCallTranslateRule(c *gin.Context) { var req struct { AppName string `json:"app_name"` Region int32 `json:"region"` SrcLang string `json:"src_lang"` DstLang string `json:"dst_lang"` } _ = c.ShouldBindJSON(&req) if strings.TrimSpace(req.SrcLang) == "" || strings.TrimSpace(req.DstLang) == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "src_lang / dst_lang 必填") return } items := make([]*comm.CallTranslateRule, 0) if err := postgres.Find(comm.TableCallTranslateRule, &items, "app_name=?", req.AppName); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } hit := comm.MatchCallTranslateRule(items, req.Region, req.SrcLang, req.DstLang) if hit == nil { writeOK(c, gin.H{"matched": false}) return } resp := gin.H{"matched": true, "rule": hit} if a, err := findAgentForRule(hit.AppName, hit.AgentId); err == nil { resp["agent"] = a } writeOK(c, resp) }