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.
 
 
 
 
 
 

318 lines
11 KiB

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})
}