refactor: 将客服后端重构为宿主可嵌入模块
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。 - 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。 - 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。 - 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"strings"
|
||||
"sync"
|
||||
@@ -64,8 +65,6 @@ func setupMessageWelcomeTestDB(t *testing.T) *gorm.DB {
|
||||
&models.AIAgent{},
|
||||
&models.Channel{},
|
||||
&models.ChannelMessageOutbox{},
|
||||
&models.Customer{},
|
||||
&models.CustomerIdentity{},
|
||||
&models.Conversation{},
|
||||
&models.ConversationParticipant{},
|
||||
&models.ConversationReadState{},
|
||||
@@ -378,38 +377,6 @@ func TestUnreadCountUsesLastReadMessageID(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendAIMessageStoresWorkflowRunID(t *testing.T) {
|
||||
db := setupMessageWelcomeTestDB(t)
|
||||
aiAgent := createWelcomeTestAIAgent(t, db, "")
|
||||
conversation := createMessageTestConversation(t, db, aiAgent.ID)
|
||||
|
||||
message, err := MessageService.SendAIMessageWithRequestIDAndWorkflowRunID(
|
||||
conversation.ID,
|
||||
aiAgent.ID,
|
||||
"ai-reply-workflow-1",
|
||||
enums.IMMessageTypeText,
|
||||
"AI reply",
|
||||
"",
|
||||
workflowTestAIPrincipal(),
|
||||
"trace-workflow-1",
|
||||
9988,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("SendAIMessageWithRequestIDAndWorkflowRunID() error = %v", err)
|
||||
}
|
||||
if message.WorkflowRunID != 9988 {
|
||||
t.Fatalf("message.WorkflowRunID=%d want 9988", message.WorkflowRunID)
|
||||
}
|
||||
|
||||
var stored models.Message
|
||||
if err := db.First(&stored, message.ID).Error; err != nil {
|
||||
t.Fatalf("find message: %v", err)
|
||||
}
|
||||
if stored.WorkflowRunID != 9988 {
|
||||
t.Fatalf("stored.WorkflowRunID=%d want 9988", stored.WorkflowRunID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConversationCreateDoesNotDuplicateWelcomeMessageForExistingConversation(t *testing.T) {
|
||||
db := setupMessageWelcomeTestDB(t)
|
||||
aiAgent := createWelcomeTestAIAgent(t, db, "欢迎咨询")
|
||||
@@ -436,6 +403,88 @@ func TestConversationCreateDoesNotDuplicateWelcomeMessageForExistingConversation
|
||||
}
|
||||
}
|
||||
|
||||
func TestConversationCreateDoesNotReuseConversationFromAnotherChannel(t *testing.T) {
|
||||
db := setupMessageWelcomeTestDB(t)
|
||||
aiAgent := createWelcomeTestAIAgent(t, db, "")
|
||||
external := welcomeTestExternalUser("channel-isolation-1")
|
||||
|
||||
first, err := ConversationService.Create(external, 11, aiAgent.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("create first channel conversation: %v", err)
|
||||
}
|
||||
second, err := ConversationService.Create(external, 12, aiAgent.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("create second channel conversation: %v", err)
|
||||
}
|
||||
if first.ID == second.ID {
|
||||
t.Fatalf("conversation %d was incorrectly reused across channels", first.ID)
|
||||
}
|
||||
if first.ChannelID != 11 || second.ChannelID != 12 {
|
||||
t.Fatalf("unexpected channel ids: first=%d second=%d", first.ChannelID, second.ChannelID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConversationCreateSynchronizesLatestAIAgentForUnassignedExistingConversation(t *testing.T) {
|
||||
db := setupMessageWelcomeTestDB(t)
|
||||
external := welcomeTestExternalUser("sync-agent-1")
|
||||
|
||||
first, err := ConversationService.Create(external, 11, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("create human conversation: %v", err)
|
||||
}
|
||||
aiAgent := createWelcomeTestAIAgent(t, db, "")
|
||||
aiAgent.ServiceMode = enums.IMConversationServiceModeAIFirst
|
||||
if err := db.Model(aiAgent).Update("service_mode", aiAgent.ServiceMode).Error; err != nil {
|
||||
t.Fatalf("update ai agent service mode: %v", err)
|
||||
}
|
||||
|
||||
second, err := ConversationService.Create(external, 11, aiAgent.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("reuse conversation with ai agent: %v", err)
|
||||
}
|
||||
if second.ID != first.ID {
|
||||
t.Fatalf("conversation id = %d, want existing %d", second.ID, first.ID)
|
||||
}
|
||||
if second.ChannelID != 11 || second.AIAgentID != aiAgent.ID {
|
||||
t.Fatalf("channel/agent = %d/%d, want 11/%d", second.ChannelID, second.AIAgentID, aiAgent.ID)
|
||||
}
|
||||
if second.ServiceMode != enums.IMConversationServiceModeAIFirst || second.Status != enums.IMConversationStatusAIServing {
|
||||
t.Fatalf("mode/status = %d/%d, want ai-first/ai-serving", second.ServiceMode, second.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConversationCreateStartsNewAIConversationWhenAssignedConversationUsesStaleMode(t *testing.T) {
|
||||
db := setupMessageWelcomeTestDB(t)
|
||||
external := welcomeTestExternalUser("keep-human-1")
|
||||
|
||||
conversation, err := ConversationService.Create(external, 11, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("create human conversation: %v", err)
|
||||
}
|
||||
if err := db.Model(conversation).Updates(map[string]any{
|
||||
"current_assignee_id": 9,
|
||||
"status": enums.IMConversationStatusActive,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("assign conversation: %v", err)
|
||||
}
|
||||
aiAgent := createWelcomeTestAIAgent(t, db, "")
|
||||
|
||||
reused, err := ConversationService.Create(external, 11, aiAgent.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("create ai conversation: %v", err)
|
||||
}
|
||||
if reused.ID == conversation.ID {
|
||||
t.Fatalf("stale assigned conversation %d was reused", conversation.ID)
|
||||
}
|
||||
if reused.AIAgentID != aiAgent.ID || reused.ServiceMode != aiAgent.ServiceMode || reused.Status != enums.IMConversationStatusAIServing {
|
||||
t.Fatalf("new conversation did not use latest ai config: %#v", reused)
|
||||
}
|
||||
preserved := ConversationService.Get(conversation.ID)
|
||||
if preserved == nil || preserved.CurrentAssigneeID != 9 || preserved.Status != enums.IMConversationStatusActive {
|
||||
t.Fatalf("old assigned conversation state changed: %#v", preserved)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConversationCreateSkipsBlankWelcomeMessage(t *testing.T) {
|
||||
db := setupMessageWelcomeTestDB(t)
|
||||
aiAgent := createWelcomeTestAIAgent(t, db, " ")
|
||||
@@ -474,7 +523,7 @@ func TestConversationCreateWelcomeMessageDoesNotTriggerAIReplyHook(t *testing.T)
|
||||
|
||||
previousHook := TriggerAIReplyAsyncHook
|
||||
called := false
|
||||
TriggerAIReplyAsyncHook = func(conversation models.Conversation, message models.Message) {
|
||||
TriggerAIReplyAsyncHook = func(_ context.Context, conversation models.Conversation, message models.Message) {
|
||||
called = true
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
|
||||
Reference in New Issue
Block a user