package services import ( "encoding/json" "slices" "strings" "time" "cs-agent/internal/models" "cs-agent/internal/pkg/config" "cs-agent/internal/pkg/dto" "cs-agent/internal/pkg/dto/request" "cs-agent/internal/pkg/enums" "cs-agent/internal/pkg/errorsx" "cs-agent/internal/pkg/utils" "cs-agent/internal/repositories" "github.com/mlogclub/simple/sqls" "github.com/mlogclub/simple/web/params" ) var AIAgentService = newAIAgentService() func newAIAgentService() *aIAgentService { return &aIAgentService{} } type aIAgentService struct { } func (s *aIAgentService) Get(id int64) *models.AIAgent { if id <= 0 { return nil } return repositories.AIAgentRepository.Get(sqls.DB(), id) } func (s *aIAgentService) Take(where ...interface{}) *models.AIAgent { return repositories.AIAgentRepository.Take(sqls.DB(), where...) } func (s *aIAgentService) Find(cnd *sqls.Cnd) []models.AIAgent { return repositories.AIAgentRepository.Find(sqls.DB(), cnd) } func (s *aIAgentService) FindOne(cnd *sqls.Cnd) *models.AIAgent { return repositories.AIAgentRepository.FindOne(sqls.DB(), cnd) } func (s *aIAgentService) FindPageByParams(params *params.QueryParams) (list []models.AIAgent, paging *sqls.Paging) { return repositories.AIAgentRepository.FindPageByParams(sqls.DB(), params) } func (s *aIAgentService) FindPageByCnd(cnd *sqls.Cnd) (list []models.AIAgent, paging *sqls.Paging) { return repositories.AIAgentRepository.FindPageByCnd(sqls.DB(), cnd) } func (s *aIAgentService) Count(cnd *sqls.Cnd) int64 { return repositories.AIAgentRepository.Count(sqls.DB(), cnd) } func (s *aIAgentService) FindByIds(ids []int64) []models.AIAgent { return repositories.AIAgentRepository.FindByIds(sqls.DB(), ids) } func (s *aIAgentService) CreateAIAgent(req request.CreateAIAgentRequest, operator *dto.AuthPrincipal) (*models.AIAgent, error) { if operator == nil { return nil, errorsx.Unauthorized("未登录或登录已过期") } item, err := s.buildAIAgentModel(0, req) if err != nil { return nil, err } item.Status = enums.StatusOk item.SortNo = 0 item.AuditFields = utils.BuildAuditFields(operator) if err := repositories.AIAgentRepository.Create(sqls.DB(), item); err != nil { return nil, err } return item, nil } func (s *aIAgentService) UpdateAIAgent(req request.UpdateAIAgentRequest, operator *dto.AuthPrincipal) error { if operator == nil { return errorsx.Unauthorized("未登录或登录已过期") } if s.Get(req.ID) == nil { return errorsx.InvalidParam("AI Agent 不存在") } item, err := s.buildAIAgentModel(req.ID, req.CreateAIAgentRequest) if err != nil { return err } return repositories.AIAgentRepository.Updates(sqls.DB(), req.ID, map[string]any{ "name": item.Name, "description": item.Description, "ai_config_id": item.AIConfigID, "service_mode": item.ServiceMode, "system_prompt": item.SystemPrompt, "welcome_message": item.WelcomeMessage, "reply_timeout_seconds": item.ReplyTimeoutSeconds, "team_ids": item.TeamIDs, "handoff_mode": item.HandoffMode, "max_ai_reply_rounds": item.MaxAIReplyRounds, "fallback_mode": item.FallbackMode, "fallback_message": item.FallbackMessage, "knowledge_ids": item.KnowledgeIDs, "skill_ids": item.SkillIDs, "allowed_mcp_tools": item.AllowedMCPTools, "remark": item.Remark, "update_user_id": operator.UserID, "update_user_name": operator.Username, "updated_at": time.Now(), }) } func (s *aIAgentService) DeleteAIAgent(id int64, operator *dto.AuthPrincipal) error { current := s.Get(id) if current == nil { return errorsx.InvalidParam("AI Agent 不存在") } if ChannelService.Take("ai_agent_id = ?", id) != nil { return errorsx.Forbidden("已有接入渠道绑定该 AI Agent,无法删除") } return repositories.AIAgentRepository.Updates(sqls.DB(), id, map[string]any{ "status": enums.StatusDeleted, "update_user_id": operator.UserID, "update_user_name": operator.Username, "updated_at": time.Now(), }) } func (s *aIAgentService) buildAIAgentModel(id int64, req request.CreateAIAgentRequest) (*models.AIAgent, error) { name := strings.TrimSpace(req.Name) if name == "" { return nil, errorsx.InvalidParam("AI Agent 名称不能为空") } if exists := s.Take("name = ? AND id <> ?", name, id); exists != nil { return nil, errorsx.InvalidParam("AI Agent 名称已存在") } if req.AIConfigID <= 0 { return nil, errorsx.InvalidParam("AI 配置不能为空") } aiConfig := AIConfigService.Get(req.AIConfigID) if aiConfig == nil { return nil, errorsx.InvalidParam("AI 配置不存在") } if aiConfig.Status != enums.StatusOk { return nil, errorsx.InvalidParam("AI 配置未启用") } if !slices.Contains(enums.IMConversationServiceModeValues, req.ServiceMode) { return nil, errorsx.InvalidParam("服务模式不合法") } teamIDs, err := s.normalizeTeamIDs(req.TeamIDs) if err != nil { return nil, err } if !slices.Contains(enums.AIAgentHandoffModeValues, enums.AIAgentHandoffMode(req.HandoffMode)) { return nil, errorsx.InvalidParam("转人工模式不合法") } if enums.AIAgentHandoffMode(req.HandoffMode) == enums.AIAgentHandoffModeDefaultTeamPool && len(teamIDs) == 0 { return nil, errorsx.InvalidParam("默认客服组待接入池模式必须至少选择一个客服组") } if !slices.Contains(enums.AIAgentFallbackModeValues, enums.AIAgentFallbackMode(req.FallbackMode)) { return nil, errorsx.InvalidParam("兜底模式不合法") } if req.ReplyTimeoutSeconds < 0 { return nil, errorsx.InvalidParam("回复超时秒数不能小于 0") } knowledgeIDs, err := s.normalizeKnowledgeIDs(req.KnowledgeIDs) if err != nil { return nil, err } if len(knowledgeIDs) == 0 { return nil, errorsx.InvalidParam("请至少选择一个知识库") } skillIDs, err := s.normalizeSkillIDs(req.SkillIDs) if err != nil { return nil, err } directTools, err := s.normalizeDirectTools(req.DirectTools) if err != nil { return nil, err } directToolsJSON := "" if len(directTools) > 0 { buf, marshalErr := json.Marshal(directTools) if marshalErr != nil { return nil, errorsx.InvalidParam("Direct Tools 配置格式不合法") } directToolsJSON = string(buf) } return &models.AIAgent{ Name: name, Description: strings.TrimSpace(req.Description), AIConfigID: req.AIConfigID, ServiceMode: req.ServiceMode, SystemPrompt: strings.TrimSpace(req.SystemPrompt), WelcomeMessage: strings.TrimSpace(req.WelcomeMessage), ReplyTimeoutSeconds: req.ReplyTimeoutSeconds, TeamIDs: utils.JoinInt64s(teamIDs), HandoffMode: req.HandoffMode, MaxAIReplyRounds: req.MaxAIReplyRounds, FallbackMode: req.FallbackMode, FallbackMessage: strings.TrimSpace(req.FallbackMessage), KnowledgeIDs: utils.JoinInt64s(knowledgeIDs), SkillIDs: utils.JoinInt64s(skillIDs), AllowedMCPTools: directToolsJSON, Remark: strings.TrimSpace(req.Remark), }, nil } func (s *aIAgentService) normalizeTeamIDs(input []int64) ([]int64, error) { ret := make([]int64, 0, len(input)) seen := make(map[int64]struct{}) for _, id := range input { if id <= 0 { continue } if _, exists := seen[id]; exists { continue } team := AgentTeamService.Get(id) if team == nil { return nil, errorsx.InvalidParam("客服组不存在") } if team.Status != enums.StatusOk { return nil, errorsx.InvalidParam("客服组未启用") } seen[id] = struct{}{} ret = append(ret, id) } slices.Sort(ret) return ret, nil } func (s *aIAgentService) normalizeKnowledgeIDs(input []int64) ([]int64, error) { ret := make([]int64, 0, len(input)) seen := make(map[int64]struct{}) for _, id := range input { if id <= 0 { continue } if _, exists := seen[id]; exists { continue } kb := KnowledgeBaseService.Get(id) if kb == nil { return nil, errorsx.InvalidParam("知识库不存在") } if kb.Status != enums.StatusOk { return nil, errorsx.InvalidParam("知识库未启用") } seen[id] = struct{}{} ret = append(ret, id) } return ret, nil } func (s *aIAgentService) normalizeSkillIDs(input []int64) ([]int64, error) { ret := make([]int64, 0, len(input)) seen := make(map[int64]struct{}) for _, id := range input { if id <= 0 { continue } if _, exists := seen[id]; exists { continue } skill := SkillDefinitionService.Get(id) if skill == nil { return nil, errorsx.InvalidParam("Skill 不存在") } if skill.Status != enums.StatusOk { return nil, errorsx.InvalidParam("Skill 未启用") } seen[id] = struct{}{} ret = append(ret, id) } return ret, nil } func (s *aIAgentService) normalizeDirectTools(input []request.AIAgentMCPToolRequest) ([]request.AIAgentMCPToolRequest, error) { if len(input) == 0 { return nil, nil } cfg := config.Current() if !cfg.MCP.Enabled { return nil, errorsx.InvalidParam("系统未启用 MCP,不能配置 Direct Tool") } ret := make([]request.AIAgentMCPToolRequest, 0, len(input)) seen := make(map[string]struct{}) for _, item := range input { serverCode := strings.TrimSpace(item.ServerCode) toolName := strings.TrimSpace(item.ToolName) if serverCode == "" || toolName == "" { return nil, errorsx.InvalidParam("Direct Tool 的 serverCode 和 toolName 不能为空") } server, ok := cfg.MCP.Servers[serverCode] if !ok || !server.Enabled { return nil, errorsx.InvalidParam("Direct Tool 绑定的 MCP 服务不存在或未启用") } key := serverCode + "/" + toolName if _, exists := seen[key]; exists { continue } seen[key] = struct{}{} normalized := request.AIAgentMCPToolRequest{ ServerCode: serverCode, ToolName: toolName, Title: strings.TrimSpace(item.Title), Description: strings.TrimSpace(item.Description), } if len(item.Arguments) > 0 { normalized.Arguments = make(map[string]string, len(item.Arguments)) for key, value := range item.Arguments { key = strings.TrimSpace(key) value = strings.TrimSpace(value) if key == "" || value == "" { continue } normalized.Arguments[key] = value } } ret = append(ret, normalized) } return ret, nil } func (s *aIAgentService) UpdateSort(ids []int64) error { return sqls.WithTransaction(func(ctx *sqls.TxContext) error { for i, id := range ids { if err := repositories.AIAgentRepository.UpdateColumn(ctx.Tx, id, "sort_no", i+1); err != nil { return err } } return nil }) } func (s *aIAgentService) UpdateStatus(id int64, status int, operator *dto.AuthPrincipal) error { if operator == nil { return errorsx.Unauthorized("未登录或登录已过期") } current := s.Get(id) if current == nil { return errorsx.InvalidParam("AI Agent 不存在") } if status != int(enums.StatusOk) && status != int(enums.StatusDisabled) { return errorsx.InvalidParam("状态值不合法") } return repositories.AIAgentRepository.Updates(sqls.DB(), id, map[string]any{ "status": status, "update_user_id": operator.UserID, "update_user_name": operator.Username, "updated_at": time.Now(), }) }