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.
285 lines
9.5 KiB
285 lines
9.5 KiB
package console
|
|
|
|
import (
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"yunyan/comm"
|
|
"yunyan/lego/sys/postgres"
|
|
"yunyan/pb"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
)
|
|
|
|
// ============================ Agent 配置域 (agent) ============================
|
|
//
|
|
// yunyan 自管 agents 的管理端 CRUD,存 console 主库(postgres.GetSys())。
|
|
// agent 编排引用第三方服务(ThirdSvcConfig)的 id;保存时按 Type 校验必需的服务引用「存在且类别匹配」。
|
|
// 配置「下发客户端」由后续阶段的下发接口 + modules/agents 改造承接。
|
|
|
|
// agentSvcRef 一个待校验的服务引用(哪个位置、引用的 svcId、允许的服务类别集合)。
|
|
// cats 是「任一命中即可」:LLM 槽位既收文本大模型也收多媒体大模型——
|
|
// 多媒体模型(GPT-4o / 千问 VL / Gemini / 豆包视觉)本身就能处理纯文本对话,
|
|
// 是文本模型能力的超集,没有理由把它挡在对话型 agent 之外。
|
|
type agentSvcRef struct {
|
|
label string
|
|
svcId string
|
|
cats []int32
|
|
}
|
|
|
|
// agentLLMCats 对话型 agent 的 LLM 槽位可选类别(与 admin agents.vue 的 CAT_LLM/CAT_LLM_VISION 对齐)。
|
|
var agentLLMCats = []int32{svcCatLLM, svcCatLLMVision}
|
|
|
|
// collectAgentSvcRefs 按 agent 类型收集其应校验的服务引用集合。
|
|
func collectAgentSvcRefs(a *AgentConfig) []agentSvcRef {
|
|
switch a.Type {
|
|
case AgentTypeChat:
|
|
return []agentSvcRef{
|
|
{"STT 语音识别", a.SttSvcId, []int32{svcCatSTT}},
|
|
{"LLM 大语言", a.LlmSvcId, agentLLMCats},
|
|
{"TTS 语音合成", a.TtsSvcId, []int32{svcCatTTS}},
|
|
}
|
|
case AgentTypeTranslate:
|
|
return []agentSvcRef{
|
|
{"STT 语音识别", a.SttSvcId, []int32{svcCatSTT}},
|
|
{"MT 机器翻译", a.MtSvcId, []int32{svcCatMT}},
|
|
{"TTS 语音合成", a.TtsSvcId, []int32{svcCatTTS}},
|
|
}
|
|
case AgentTypeStsChat:
|
|
return []agentSvcRef{{"STS 端到端对话", a.StsSvcId, []int32{svcCatSTS}}}
|
|
case AgentTypeAstTranslate:
|
|
return []agentSvcRef{{"AST 端到端翻译", a.AstSvcId, []int32{svcCatAST}}}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// svcHasAnyCategory 判断 ThirdSvcConfig.Categories("1,2,3") 是否命中 cats 中任意一个类别。
|
|
func svcHasAnyCategory(categories string, cats []int32) bool {
|
|
have := make(map[string]bool, 4)
|
|
for _, s := range strings.Split(categories, ",") {
|
|
if s = strings.TrimSpace(s); s != "" {
|
|
have[s] = true
|
|
}
|
|
}
|
|
for _, c := range cats {
|
|
if have[strconv.Itoa(int(c))] {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// validateAgentSvcRefs 校验 agent 引用的服务:必填、存在、类别匹配。返回错误说明(空串=通过)。
|
|
// 引用在 agent 自己的应用作用域内解析,未命中回退全局(app_name=”)——与运行时「应用优先、无则全局」一致。
|
|
func validateAgentSvcRefs(a *AgentConfig) string {
|
|
refs := collectAgentSvcRefs(a)
|
|
if len(refs) == 0 {
|
|
return "未知的智能体类型: " + a.Type
|
|
}
|
|
for _, r := range refs {
|
|
if strings.TrimSpace(r.svcId) == "" {
|
|
return r.label + " 必选"
|
|
}
|
|
cfg := &ThirdSvcConfig{}
|
|
err := postgres.FindOne(comm.TableSvcConfig, cfg, "app_name=? AND id=?", a.AppName, r.svcId)
|
|
if err != nil && a.AppName != "" { // 应用作用域未命中 → 回退全局
|
|
err = postgres.FindOne(comm.TableSvcConfig, cfg, "app_name=? AND id=?", "", r.svcId)
|
|
}
|
|
if err != nil {
|
|
return r.label + " 引用的服务不存在: " + r.svcId
|
|
}
|
|
if !svcHasAnyCategory(cfg.Categories, r.cats) {
|
|
return r.label + " 引用的服务类别不匹配: " + r.svcId
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// validateAgentType 校验类型可用。翻译类已下线,只在「历史行本来就是这个类型且没改」时放行,
|
|
// 保证老数据还能打开、改名字、停用,而不是一保存就被卡住。
|
|
func validateAgentType(a *AgentConfig, old *AgentConfig) string {
|
|
if comm.AgentTypeSelectable(a.Type) {
|
|
return ""
|
|
}
|
|
if old != nil && old.Type == a.Type {
|
|
return "" // 历史行保持原类型,放行
|
|
}
|
|
return "智能体类型不支持: " + a.Type + "(现只支持 对话(chat) / 端到端对话(sts-chat))"
|
|
}
|
|
|
|
// validateAgentMcpRefs 校验绑定的 MCP 服务:存在且类别含 MCP。空列表直接通过。
|
|
// 解析口径同服务引用:应用作用域优先、未命中回退全局。
|
|
func validateAgentMcpRefs(a *AgentConfig) string {
|
|
for _, id := range a.McpSvcIds {
|
|
id = strings.TrimSpace(id)
|
|
if id == "" {
|
|
continue
|
|
}
|
|
cfg := &ThirdSvcConfig{}
|
|
err := postgres.FindOne(comm.TableSvcConfig, cfg, "app_name=? AND id=?", a.AppName, id)
|
|
if err != nil && a.AppName != "" {
|
|
err = postgres.FindOne(comm.TableSvcConfig, cfg, "app_name=? AND id=?", "", id)
|
|
}
|
|
if err != nil {
|
|
return "绑定的 MCP 服务不存在: " + id
|
|
}
|
|
if !svcHasAnyCategory(cfg.Categories, []int32{svcCatMCP}) {
|
|
return "绑定的服务不是 MCP 类型: " + id
|
|
}
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// normalizeAgent 落库前清洗:去掉空的 MCP 引用与无 key 的变量项,避免脏数据下发到客户端。
|
|
func normalizeAgent(a *AgentConfig) {
|
|
ids := make([]string, 0, len(a.McpSvcIds))
|
|
seen := map[string]bool{}
|
|
for _, id := range a.McpSvcIds {
|
|
if id = strings.TrimSpace(id); id != "" && !seen[id] {
|
|
seen[id] = true
|
|
ids = append(ids, id)
|
|
}
|
|
}
|
|
a.McpSvcIds = ids
|
|
vars := make([]comm.AgentVariable, 0, len(a.Variables))
|
|
for _, v := range a.Variables {
|
|
if v.Key = strings.TrimSpace(v.Key); v.Key != "" {
|
|
vars = append(vars, v)
|
|
}
|
|
}
|
|
a.Variables = vars
|
|
}
|
|
|
|
// ============================== Agent CRUD ==============================
|
|
|
|
// getAgents 列出某作用域(app_name)下全部 agent 配置。app_name=” 为全局默认。
|
|
func (this *serverComp) getAgents(c *gin.Context) {
|
|
var req struct {
|
|
AppName string `json:"app_name"`
|
|
}
|
|
_ = c.ShouldBindJSON(&req)
|
|
items := make([]*AgentConfig, 0)
|
|
if err := postgres.Find(comm.TableAgentConfig, &items, "app_name=?", req.AppName); err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, err.Error())
|
|
return
|
|
}
|
|
writeOK(c, gin.H{"items": items})
|
|
}
|
|
|
|
// getAgent 取单个 agent 配置(按 app_name+id)。
|
|
func (this *serverComp) getAgent(c *gin.Context) {
|
|
var req struct {
|
|
AppName string `json:"app_name"`
|
|
Id string `json:"id"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.Id) == "" {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, "id 必填")
|
|
return
|
|
}
|
|
m := &AgentConfig{}
|
|
if err := postgres.FindOne(comm.TableAgentConfig, m, "app_name=? AND id=?", req.AppName, req.Id); err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, "智能体不存在: "+req.Id)
|
|
return
|
|
}
|
|
writeOK(c, gin.H{"agent": m})
|
|
}
|
|
|
|
// addAgent 新增 agent 配置(作用域内 app_name+id 唯一)。
|
|
func (this *serverComp) addAgent(c *gin.Context) {
|
|
var m AgentConfig
|
|
if err := c.ShouldBindJSON(&m); err != nil {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, err.Error())
|
|
return
|
|
}
|
|
if strings.TrimSpace(m.Id) == "" || strings.TrimSpace(m.Name) == "" {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, "智能体ID 和 名称 必填")
|
|
return
|
|
}
|
|
exist := &AgentConfig{}
|
|
if err := postgres.FindOne(comm.TableAgentConfig, exist, "app_name=? AND id=?", m.AppName, m.Id); err == nil {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, "智能体ID 已存在: "+m.Id)
|
|
return
|
|
}
|
|
if msg := validateAgentType(&m, nil); msg != "" {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, msg)
|
|
return
|
|
}
|
|
if msg := validateAgentSvcRefs(&m); msg != "" {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, msg)
|
|
return
|
|
}
|
|
if msg := validateAgentMcpRefs(&m); msg != "" {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, msg)
|
|
return
|
|
}
|
|
normalizeAgent(&m)
|
|
m.RowId = 0 // 代理主键由库自增回填
|
|
now := time.Now().UnixMilli()
|
|
m.Createtime = now
|
|
m.Updatetime = now
|
|
if err := postgres.Insert(comm.TableAgentConfig, &m); err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, err.Error())
|
|
return
|
|
}
|
|
broadcastConfigChanged(comm.ConfigKindAgent, "add", 0)
|
|
writeOK(c, &m)
|
|
}
|
|
|
|
// updateAgent 更新 agent 配置(按 app_name+id)。
|
|
func (this *serverComp) updateAgent(c *gin.Context) {
|
|
var m AgentConfig
|
|
if err := c.ShouldBindJSON(&m); err != nil {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, err.Error())
|
|
return
|
|
}
|
|
if strings.TrimSpace(m.Id) == "" {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, "智能体ID 必填")
|
|
return
|
|
}
|
|
old := &AgentConfig{}
|
|
if err := postgres.FindOne(comm.TableAgentConfig, old, "app_name=? AND id=?", m.AppName, m.Id); err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, "智能体不存在: "+m.Id)
|
|
return
|
|
}
|
|
if msg := validateAgentType(&m, old); msg != "" {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, msg)
|
|
return
|
|
}
|
|
if msg := validateAgentSvcRefs(&m); msg != "" {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, msg)
|
|
return
|
|
}
|
|
if msg := validateAgentMcpRefs(&m); msg != "" {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, msg)
|
|
return
|
|
}
|
|
normalizeAgent(&m)
|
|
m.RowId = old.RowId // 按代理主键更新本行
|
|
m.Createtime = old.Createtime
|
|
m.Updatetime = time.Now().UnixMilli()
|
|
if err := postgres.Save(comm.TableAgentConfig, &m); err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, err.Error())
|
|
return
|
|
}
|
|
broadcastConfigChanged(comm.ConfigKindAgent, "update", 0)
|
|
writeOK(c, &m)
|
|
}
|
|
|
|
// delAgent 删除 agent 配置(按 app_name+id)。
|
|
func (this *serverComp) delAgent(c *gin.Context) {
|
|
var req struct {
|
|
AppName string `json:"app_name"`
|
|
Id string `json:"id"`
|
|
}
|
|
if err := c.ShouldBindJSON(&req); err != nil || strings.TrimSpace(req.Id) == "" {
|
|
writeErr(c, pb.ErrorCode_ReqParameterError, "id 必填")
|
|
return
|
|
}
|
|
if err := postgres.Delete(comm.TableAgentConfig, "app_name=? AND id=?", req.AppName, req.Id); err != nil {
|
|
writeErr(c, pb.ErrorCode_DBError, err.Error())
|
|
return
|
|
}
|
|
broadcastConfigChanged(comm.ConfigKindAgent, "delete", 0)
|
|
writeOK(c, gin.H{"id": req.Id})
|
|
}
|
|
|