refactor: 将客服后端重构为宿主可嵌入模块
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。 - 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。 - 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。 - 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
This commit is contained in:
@@ -2,6 +2,7 @@ package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -123,6 +124,19 @@ func (s *conversationHumanDispatchService) ApplyHumanChannelCreate(conversationI
|
||||
if err := s.sendAIText(conversationID, 0, HandoffWaitingMessage); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dispatched, err := ConversationDispatchService.DispatchConversation(conversationID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if dispatched != nil {
|
||||
WsService.PublishConversationChanged(dispatched, enums.IMRealtimeEventConversationAssigned)
|
||||
return &HandoffDecisionResult{
|
||||
Decision: HandoffDecisionAssigned,
|
||||
TeamID: dispatched.CurrentTeamID,
|
||||
AssigneeID: dispatched.CurrentAssigneeID,
|
||||
Message: HandoffWaitingMessage,
|
||||
}, nil
|
||||
}
|
||||
return &HandoffDecisionResult{Decision: HandoffDecisionGlobalPool, Message: HandoffWaitingMessage}, nil
|
||||
}
|
||||
|
||||
@@ -142,9 +156,15 @@ func (s *conversationHumanDispatchService) DispatchPendingConversation(conversat
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(candidates) > 0 {
|
||||
dispatched, err := ConversationDispatchService.tryAssignConversation(conversationID, candidates[0].profile, "自动分配")
|
||||
for _, candidate := range candidates {
|
||||
dispatched, err := ConversationDispatchService.tryAssignConversation(conversationID, candidate.profile, "自动分配")
|
||||
if err != nil {
|
||||
if errors.Is(err, errDispatchCandidateUnavailable) {
|
||||
continue
|
||||
}
|
||||
if errors.Is(err, errConversationDispatchConflict) {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0137")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if dispatched != nil {
|
||||
@@ -180,9 +200,15 @@ func (s *conversationHumanDispatchService) dispatchAfterHandoffWithRequestID(con
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if len(candidates) > 0 {
|
||||
dispatched, err := ConversationDispatchService.tryAssignConversation(conversationID, candidates[0].profile, "自动分配")
|
||||
for _, candidate := range candidates {
|
||||
dispatched, err := ConversationDispatchService.tryAssignConversation(conversationID, candidate.profile, "自动分配")
|
||||
if err != nil {
|
||||
if errors.Is(err, errDispatchCandidateUnavailable) {
|
||||
continue
|
||||
}
|
||||
if errors.Is(err, errConversationDispatchConflict) {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0137")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if dispatched != nil {
|
||||
@@ -220,9 +246,14 @@ func (s *conversationHumanDispatchService) markHandoff(conversationID int64, aiA
|
||||
now := time.Now()
|
||||
trimmedReason := strings.TrimSpace(reason)
|
||||
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
||||
conversation := repositories.ConversationRepository.Get(ctx.Tx, conversationID)
|
||||
if conversation == nil {
|
||||
return errorsx.InvalidParamI18n("error.e0116")
|
||||
}
|
||||
if err := repositories.ConversationRepository.Updates(ctx.Tx, conversationID, map[string]any{
|
||||
"handoff_at": now,
|
||||
"handoff_reason": trimmedReason,
|
||||
"queue_entered_at": queueEnteredAtForTransition(conversation, now),
|
||||
"status": enums.IMConversationStatusPending,
|
||||
"current_team_id": 0,
|
||||
"current_assignee_id": 0,
|
||||
@@ -255,6 +286,7 @@ func (s *conversationHumanDispatchService) moveToTeamPoolWithRequestID(conversat
|
||||
"status": enums.IMConversationStatusPending,
|
||||
"current_team_id": teamID,
|
||||
"current_assignee_id": 0,
|
||||
"queue_entered_at": queueEnteredAtForTransition(current, now),
|
||||
"update_user_id": 0,
|
||||
"update_user_name": "system",
|
||||
"updated_at": now,
|
||||
@@ -262,19 +294,21 @@ func (s *conversationHumanDispatchService) moveToTeamPoolWithRequestID(conversat
|
||||
return err
|
||||
}
|
||||
if err := ConversationEventLogService.CreateEventWithRequestID(ctx, conversationID, requestID, enums.IMEventTypeTransfer, enums.IMSenderTypeSystem, 0, "会话进入客服组待接入", ConversationService.buildEventPayload(map[string]any{
|
||||
"fromStatus": current.Status,
|
||||
"toStatus": enums.IMConversationStatusPending,
|
||||
"fromAssigneeId": current.CurrentAssigneeID,
|
||||
"toAssigneeId": int64(0),
|
||||
"toTeamId": teamID,
|
||||
"reason": strings.TrimSpace(reason),
|
||||
"decision": string(HandoffDecisionTeamPool),
|
||||
"from_status": current.Status,
|
||||
"to_status": enums.IMConversationStatusPending,
|
||||
"from_assignee_id": current.CurrentAssigneeID,
|
||||
"to_assignee_id": int64(0),
|
||||
"to_team_id": teamID,
|
||||
"reason": strings.TrimSpace(reason),
|
||||
"decision": string(HandoffDecisionTeamPool),
|
||||
})); err != nil {
|
||||
return err
|
||||
}
|
||||
current.Status = enums.IMConversationStatusPending
|
||||
current.CurrentTeamID = teamID
|
||||
current.CurrentAssigneeID = 0
|
||||
queueEnteredAt := queueEnteredAtForTransition(current, now)
|
||||
current.QueueEnteredAt = &queueEnteredAt
|
||||
current.UpdateUserID = 0
|
||||
current.UpdateUserName = "system"
|
||||
current.UpdatedAt = now
|
||||
@@ -284,12 +318,13 @@ func (s *conversationHumanDispatchService) moveToTeamPoolWithRequestID(conversat
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ConversationQueueService.PublishPoolUpdates(teamID)
|
||||
return conversation, nil
|
||||
}
|
||||
|
||||
func (s *conversationHumanDispatchService) moveToGlobalPool(conversationID int64, operatorName string) error {
|
||||
now := time.Now()
|
||||
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
||||
err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
||||
conversation := repositories.ConversationRepository.Get(ctx.Tx, conversationID)
|
||||
if conversation == nil {
|
||||
return errorsx.InvalidParamI18n("error.e0116")
|
||||
@@ -298,6 +333,7 @@ func (s *conversationHumanDispatchService) moveToGlobalPool(conversationID int64
|
||||
"status": enums.IMConversationStatusPending,
|
||||
"current_team_id": 0,
|
||||
"current_assignee_id": 0,
|
||||
"queue_entered_at": queueEnteredAtForTransition(conversation, now),
|
||||
"update_user_id": 0,
|
||||
"update_user_name": operatorName,
|
||||
"updated_at": now,
|
||||
@@ -305,11 +341,16 @@ func (s *conversationHumanDispatchService) moveToGlobalPool(conversationID int64
|
||||
return err
|
||||
}
|
||||
return ConversationEventLogService.CreateEvent(ctx, conversationID, enums.IMEventTypeTransfer, enums.IMSenderTypeSystem, 0, "会话进入全局待接入", ConversationService.buildEventPayload(map[string]any{
|
||||
"fromStatus": conversation.Status,
|
||||
"toStatus": enums.IMConversationStatusPending,
|
||||
"decision": string(HandoffDecisionGlobalPool),
|
||||
"from_status": conversation.Status,
|
||||
"to_status": enums.IMConversationStatusPending,
|
||||
"decision": string(HandoffDecisionGlobalPool),
|
||||
}))
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
ConversationQueueService.PublishPoolUpdates(0)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *conversationHumanDispatchService) createEvent(conversationID int64, eventType enums.IMEventType, senderType enums.IMSenderType, senderID int64, content, payload string) error {
|
||||
|
||||
Reference in New Issue
Block a user