refactor: 将客服后端重构为宿主可嵌入模块

- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。

- 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。

- 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。

- 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
This commit is contained in:
t
2026-08-28 22:23:13 +08:00
parent 6845c728f8
commit 18c9354095
377 changed files with 13199 additions and 22881 deletions
@@ -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 {