package console import ( "sort" "strconv" "strings" "time" "yunyan/comm" "yunyan/lego/sys/postgres" "yunyan/pb" "github.com/gin-gonic/gin" ) // ============================ 会议记录服务编排 (echomeet orch) ============================ // // admin「会议记录服务」页的后端。编排**不存凭据**,只引用第三方服务配置(svc_config)里配好的 // 服务 id,按作用域 (app_name, region) 分层:app_name='' 且 region=0 = 全局默认兜底。 // 数据存 console 共享库(postgres),与 svc_config 同库。 // // 前端流程与「通话翻译配置」一致:先 getsvcconfigs 拿服务池、再 getechoorch 拿编排,前端组装。 // web api(handleWeb 分发),前端统一用 webApi。 // echoOrchScope 解析请求里的作用域参数。region<0 归一为 0(全区域)。 type echoOrchScope struct { AppName string `json:"app_name"` Region int32 `json:"region"` } func normScope(s *echoOrchScope) { s.AppName = strings.TrimSpace(s.AppName) if s.Region < 0 { s.Region = 0 } } // getEchoOrch 列出某作用域下的编排 + 作用域开关。app_name='' 且 region=0 为全局默认。 func (this *serverComp) getEchoOrch(c *gin.Context) { var req echoOrchScope _ = c.ShouldBindJSON(&req) normScope(&req) items := make([]*EchoOrchestration, 0) if err := postgres.Find(comm.TableEchomeetOrch, &items, "app_name=? AND region=?", req.AppName, req.Region); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } sort.SliceStable(items, func(i, j int) bool { if items[i].Kind != items[j].Kind { return items[i].Kind < items[j].Kind } return items[i].Priority < items[j].Priority }) setting := &EchoOrchSetting{} if err := postgres.FindOne(comm.TableEchomeetOrchSetting, setting, "app_name=? AND region=?", req.AppName, req.Region); err != nil { setting = &EchoOrchSetting{AppName: req.AppName, Region: req.Region} // 未设置 → 零值 } writeOK(c, gin.H{ "items": items, "setting": setting, "available": this.echoOrchAvailableSvcs(req.AppName), // 各编排类别对应的服务池类别,前端据此从「可选服务」里筛出每段能选的服务。 "kind_cats": gin.H{ "1": echoKindSvcCats[echoKindASR], "2": echoKindSvcCats[echoKindMT], "3": echoKindSvcCats[echoKindLLM], }, }) } // echoOrchSvcView 供前端选服务/显示名/识别语言的精简服务视图(不含凭据)。 type echoOrchSvcView struct { Id string `json:"id"` Name string `json:"name"` Provider string `json:"provider"` Categories string `json:"categories"` // 逗号分隔类别,前端按 kind_cats 筛 Languages string `json:"languages"` // 识别服务的支持语言(逗号分隔 BCP-47),用于选路预览 } // echoOrchAvailableSvcs 返回该作用域可见的启用服务(本作用域 + 全局兜底,同 id 应用覆盖全局)。 func (this *serverComp) echoOrchAvailableSvcs(appName string) []echoOrchSvcView { rows := make([]*ThirdSvcConfig, 0) if err := postgres.Find(comm.TableSvcConfig, &rows, "(app_name=? OR app_name='') AND enable=true", appName); err != nil { return nil } // 去重:同业务 id,应用作用域优先于全局兜底。 pick := make(map[string]*ThirdSvcConfig, len(rows)) for _, r := range rows { if cur, ok := pick[r.Id]; !ok || (r.AppName != "" && cur.AppName == "") { pick[r.Id] = r } } out := make([]echoOrchSvcView, 0, len(pick)) for _, r := range pick { langs := "" for _, f := range r.Fields { if f.Key == "languages" { langs = f.DefValue break } } out = append(out, echoOrchSvcView{ Id: r.Id, Name: r.Name, Provider: r.Provider, Categories: r.Categories, Languages: langs, }) } sort.SliceStable(out, func(i, j int) bool { return out[i].Id < out[j].Id }) return out } // echoOrchSvcAllowed 校验 svc_id 在该作用域可见(本作用域或全局兜底)且类别与 kind 匹配。 // 返回 (ok, 提示)。服务池按 app_name 分作用域,应用作用域看不到别的应用、但能继承全局。 func (this *serverComp) echoOrchSvcAllowed(appName, svcId string, kind int32) (bool, string) { wantCats := echoKindSvcCats[kind] if len(wantCats) == 0 { return false, "未知的编排类别" } // 该作用域可见的服务 = app_name 自己的 + 全局默认的。 rows := make([]*ThirdSvcConfig, 0) if err := postgres.Find(comm.TableSvcConfig, &rows, "(app_name=? OR app_name='') AND id=?", appName, svcId); err != nil { return false, err.Error() } for _, r := range rows { if !r.Enable { continue } for _, cat := range strings.Split(r.Categories, ",") { for _, want := range wantCats { if strings.TrimSpace(cat) == strconv.Itoa(int(want)) { return true, "" } } } } return false, "所选服务不存在、已停用,或类别与该段不匹配" } // saveEchoOrch 新增/更新一条编排(按 row_id;0 为新增)。 // 总结类默认模型同作用域唯一;非总结类强制清零 is_default/client_visible。 func (this *serverComp) saveEchoOrch(c *gin.Context) { var req struct { echoOrchScope Orch EchoOrchestration `json:"orch"` } if err := c.ShouldBindJSON(&req); err != nil { writeErr(c, pb.ErrorCode_ReqParameterError, err.Error()) return } normScope(&req.echoOrchScope) o := req.Orch o.AppName = req.AppName o.Region = req.Region o.SvcId = strings.TrimSpace(o.SvcId) if o.SvcId == "" { writeErr(c, pb.ErrorCode_ReqParameterError, "svc_id 必填") return } if _, ok := echoKindSvcCats[o.Kind]; !ok { writeErr(c, pb.ErrorCode_ReqParameterError, "未知的编排类别 kind") return } if ok, msg := this.echoOrchSvcAllowed(req.AppName, o.SvcId, o.Kind); !ok { writeErr(c, pb.ErrorCode_ReqParameterError, msg) return } // 只有总结类有默认/客户端可见;其余强制清零。停用的不能当默认。 if o.Kind != echoKindLLM { o.IsDefault = false o.ClientVisible = false } if !o.Enabled { o.IsDefault = false } now := time.Now().Unix() o.Updatetime = now if o.RowId != 0 { old := &EchoOrchestration{} if err := postgres.FindOne(comm.TableEchomeetOrch, old, "row_id=?", o.RowId); err != nil { writeErr(c, pb.ErrorCode_DBError, "编排不存在: "+err.Error()) return } o.AppName, o.Region, o.Createtime = old.AppName, old.Region, old.Createtime if err := postgres.Save(comm.TableEchomeetOrch, &o); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } } else { // 同作用域同类同服务不可重复。 dup := &EchoOrchestration{} if postgres.FindOne(comm.TableEchomeetOrch, dup, "app_name=? AND region=? AND kind=? AND svc_id=?", o.AppName, o.Region, o.Kind, o.SvcId) == nil && dup.RowId != 0 { writeErr(c, pb.ErrorCode_ReqParameterError, "该服务已在此段添加") return } o.Createtime = now if err := postgres.Insert(comm.TableEchomeetOrch, &o); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } } // 默认模型同作用域唯一:本行置默认后,抹掉同作用域同类其余行。先写后清,避免中途失败留空默认。 if o.IsDefault { if res := postgres.Exec( "UPDATE "+comm.TableEchomeetOrch+" SET is_default=false WHERE app_name=? AND region=? AND kind=? AND row_id<>?", o.AppName, o.Region, o.Kind, o.RowId); res.Error != nil { writeErr(c, pb.ErrorCode_DBError, "清理旧默认模型失败: "+res.Error.Error()) return } } writeOK(c, gin.H{"row_id": o.RowId}) } // delEchoOrch 删除一条编排。删掉的若是默认模型,顺延同作用域同类一个可用的。 func (this *serverComp) delEchoOrch(c *gin.Context) { var req struct { RowId uint64 `json:"row_id"` } if err := c.ShouldBindJSON(&req); err != nil || req.RowId == 0 { writeErr(c, pb.ErrorCode_ReqParameterError, "row_id 必填") return } old := &EchoOrchestration{} had := postgres.FindOne(comm.TableEchomeetOrch, old, "row_id=?", req.RowId) == nil && old.RowId != 0 if err := postgres.Delete(comm.TableEchomeetOrch, "row_id=?", req.RowId); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } if had && old.IsDefault && old.Kind == echoKindLLM { this.promoteEchoOrchDefault(old.AppName, old.Region) } writeOK(c, gin.H{"deleted": req.RowId}) } // promoteEchoOrchDefault 该作用域总结类无默认时,把优先级最高的启用条目提为默认。best-effort。 func (this *serverComp) promoteEchoOrchDefault(appName string, region int32) { rows := make([]*EchoOrchestration, 0) if err := postgres.Find(comm.TableEchomeetOrch, &rows, "app_name=? AND region=? AND kind=?", appName, region, echoKindLLM); err != nil { return } var best *EchoOrchestration for _, r := range rows { if r.IsDefault { return // 还有默认 } if !r.Enabled { continue } if best == nil || r.Priority < best.Priority { best = r } } if best == nil { return } postgres.Exec("UPDATE "+comm.TableEchomeetOrch+" SET is_default=true WHERE row_id=?", best.RowId) } // reorderEchoOrch 批量调整同作用域内的优先级(拖拽/上下移后提交)。 func (this *serverComp) reorderEchoOrch(c *gin.Context) { var req struct { Items []struct { RowId uint64 `json:"row_id"` Priority int32 `json:"priority"` } `json:"items"` } if err := c.ShouldBindJSON(&req); err != nil { writeErr(c, pb.ErrorCode_ReqParameterError, err.Error()) return } now := time.Now().Unix() updated := 0 for _, it := range req.Items { if it.RowId == 0 { continue } if res := postgres.Exec("UPDATE "+comm.TableEchomeetOrch+" SET priority=?, updatetime=? WHERE row_id=?", it.Priority, now, it.RowId); res.Error != nil { writeErr(c, pb.ErrorCode_DBError, res.Error.Error()) return } updated++ } writeOK(c, gin.H{"updated": updated}) } // saveEchoOrchSetting 保存作用域级开关(客户端可否指定总结模型),upsert 单行。 func (this *serverComp) saveEchoOrchSetting(c *gin.Context) { var req struct { echoOrchScope LLMClientChoice bool `json:"llm_client_choice"` } if err := c.ShouldBindJSON(&req); err != nil { writeErr(c, pb.ErrorCode_ReqParameterError, err.Error()) return } normScope(&req.echoOrchScope) now := time.Now().Unix() old := &EchoOrchSetting{} if postgres.FindOne(comm.TableEchomeetOrchSetting, old, "app_name=? AND region=?", req.AppName, req.Region) == nil && old.RowId != 0 { old.LLMClientChoice = req.LLMClientChoice old.Updatetime = now if err := postgres.Save(comm.TableEchomeetOrchSetting, old); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } } else { row := &EchoOrchSetting{AppName: req.AppName, Region: req.Region, LLMClientChoice: req.LLMClientChoice, Updatetime: now} if err := postgres.Insert(comm.TableEchomeetOrchSetting, row); err != nil { writeErr(c, pb.ErrorCode_DBError, err.Error()) return } } writeOK(c, gin.H{"llm_client_choice": req.LLMClientChoice}) }