diff --git a/internal/services/conversation_human_dispatch_service.go b/internal/services/conversation_human_dispatch_service.go index dcb2323..2910483 100644 --- a/internal/services/conversation_human_dispatch_service.go +++ b/internal/services/conversation_human_dispatch_service.go @@ -83,6 +83,42 @@ func (s *conversationHumanDispatchService) ApplyHumanOnlyCreate(conversationID i return s.dispatchAfterHandoff(conversationID, aiAgent.ID, activeTeamIDs, "仅人工模式新会话", false) } +func (s *conversationHumanDispatchService) DispatchPendingConversation(conversationID int64, aiAgent models.AIAgent) (*HandoffDecisionResult, error) { + conversation := ConversationService.Get(conversationID) + if conversation == nil { + return nil, errorsx.InvalidParam("会话不存在") + } + if conversation.Status != enums.IMConversationStatusPending || conversation.CurrentAssigneeID > 0 { + return nil, errorsx.InvalidParam("只有待接入未分配会话允许自动分配") + } + activeTeamIDs := ConversationDispatchService.findActiveScheduleTeamIDs(orderedPositiveIDs(aiAgent.TeamIDs), time.Now()) + if len(activeTeamIDs) == 0 { + return &HandoffDecisionResult{Decision: HandoffDecisionOffHours}, nil + } + candidates, _, err := ConversationDispatchService.pickDispatchCandidates(activeTeamIDs, time.Now()) + if err != nil { + return nil, err + } + if len(candidates) > 0 { + dispatched, err := ConversationDispatchService.tryAssignConversation(conversationID, candidates[0].profile, "自动分配") + if err != nil { + return nil, err + } + if dispatched != nil { + return &HandoffDecisionResult{ + Decision: HandoffDecisionAssigned, + TeamID: dispatched.CurrentTeamID, + AssigneeID: dispatched.CurrentAssigneeID, + }, nil + } + } + teamID := activeTeamIDs[0] + if err := s.moveToTeamPool(conversationID, teamID, "手动触发自动分配"); err != nil { + return nil, err + } + return &HandoffDecisionResult{Decision: HandoffDecisionTeamPool, TeamID: teamID}, nil +} + func (s *conversationHumanDispatchService) dispatchAfterHandoff(conversationID, aiAgentID int64, activeTeamIDs []int64, reason string, publishAssignEvent bool) (*HandoffDecisionResult, error) { if err := s.sendAIText(conversationID, aiAgentID, HandoffWaitingMessage); err != nil { return nil, err diff --git a/internal/services/conversation_human_dispatch_service_test.go b/internal/services/conversation_human_dispatch_service_test.go index 962675f..f44a387 100644 --- a/internal/services/conversation_human_dispatch_service_test.go +++ b/internal/services/conversation_human_dispatch_service_test.go @@ -6,6 +6,7 @@ import ( "time" "cs-agent/internal/models" + "cs-agent/internal/pkg/dto" "cs-agent/internal/pkg/enums" "cs-agent/internal/pkg/openidentity" "cs-agent/internal/services" @@ -149,6 +150,37 @@ func TestConversationHumanDispatchHumanOnlyCreateAssignsAvailableAgent(t *testin } } +func TestConversationAutoAssignManualDispatchOffHoursReturnsBusinessMessage(t *testing.T) { + db := setupConversationHumanDispatchTestDB(t) + aiAgent := createHumanDispatchAIAgent(t, db, enums.IMConversationServiceModeAIFirst, "1") + conversation := createHumanDispatchConversation(t, db, aiAgent.ID, enums.IMConversationStatusPending) + + err := services.ConversationService.AutoAssignConversation(conversation.ID, testHumanDispatchOperator()) + if err == nil { + t.Fatalf("expected off-hours manual dispatch to fail") + } + if !strings.Contains(err.Error(), "当前暂不在人工客服服务时间内") { + t.Fatalf("expected off-hours error, got %v", err) + } +} + +func TestConversationAutoAssignManualDispatchFallsBackToTeamPool(t *testing.T) { + db := setupConversationHumanDispatchTestDB(t) + aiAgent := createHumanDispatchAIAgent(t, db, enums.IMConversationServiceModeAIFirst, "1") + createHumanDispatchTeam(t, db, 1, "售后支持组") + createHumanDispatchActiveSchedule(t, db, 1) + conversation := createHumanDispatchConversation(t, db, aiAgent.ID, enums.IMConversationStatusPending) + + err := services.ConversationService.AutoAssignConversation(conversation.ID, testHumanDispatchOperator()) + if err != nil { + t.Fatalf("AutoAssignConversation() error = %v", err) + } + current := services.ConversationService.Get(conversation.ID) + if current.Status != enums.IMConversationStatusPending || current.CurrentTeamID != 1 || current.CurrentAssigneeID != 0 { + t.Fatalf("expected team-pool pending conversation, got %+v", current) + } +} + func setupConversationHumanDispatchTestDB(t *testing.T) *gorm.DB { t.Helper() dbName := strings.NewReplacer("/", "_", " ", "_").Replace(t.Name()) @@ -265,3 +297,7 @@ func createHumanDispatchConversation(t *testing.T, db *gorm.DB, aiAgentID int64, } return item } + +func testHumanDispatchOperator() *dto.AuthPrincipal { + return &dto.AuthPrincipal{UserID: 9, Username: "dispatcher", Nickname: "调度员"} +} diff --git a/internal/services/conversation_service.go b/internal/services/conversation_service.go index 36fc852..fb18455 100644 --- a/internal/services/conversation_service.go +++ b/internal/services/conversation_service.go @@ -248,12 +248,16 @@ func (s *conversationService) AutoAssignConversation(conversationID int64, opera return errorsx.InvalidParam("当前会话已分配客服") } - dispatched, err := ConversationDispatchService.DispatchConversation(conversationID) + aiAgent := AIAgentService.Get(conversation.AIAgentID) + if aiAgent == nil || aiAgent.Status != enums.StatusOk { + return errorsx.InvalidParam("AI Agent 不存在或已停用") + } + result, err := ConversationHumanDispatchService.DispatchPendingConversation(conversationID, *aiAgent) if err != nil { return err } - if dispatched == nil { - return errorsx.InvalidParam("当前暂无可自动分配的值班客服") + if result == nil || result.Decision == HandoffDecisionOffHours { + return errorsx.InvalidParam("当前暂不在人工客服服务时间内") } return nil }