18c9354095
- 注入数据库、运行时配置、统一响应、文件存储和平台 AI 能力,补充业务读写工具与客户快捷操作契约。 - 移除模块内重复的组织、客户、工单、标签、技能、旧工作流、MCP 和迁移实现,将身份权限与业务主体交由宿主管理。 - 使用 libSQL 重构向量存储,并完善图片消息、访客身份、排队调度、企业微信和支持聊天页面。 - 统一 HTTP、DTO 与 WebSocket 的 snake_case 协议,补齐模块初始化、业务动作和公共载荷等回归测试。
299 lines
12 KiB
Go
299 lines
12 KiB
Go
package services
|
|
|
|
import (
|
|
"context"
|
|
"testing"
|
|
|
|
"code.tczkiot.com/wlw/ai-agent/contract"
|
|
"code.tczkiot.com/wlw/ai-agent/identity"
|
|
"code.tczkiot.com/wlw/ai-agent/internal/models"
|
|
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
|
|
)
|
|
|
|
func TestCustomerQuickActionsResolveKnownCardWithoutChangingGuestOwnership(t *testing.T) {
|
|
db := setupMessageWelcomeTestDB(t)
|
|
aiAgent := createWelcomeTestAIAgent(t, db, "")
|
|
external := welcomeTestExternalUser("card:50506783")
|
|
conversation, err := ConversationService.Create(external, 11, aiAgent.ID)
|
|
if err != nil {
|
|
t.Fatalf("create conversation: %v", err)
|
|
}
|
|
originalCustomerType := conversation.CustomerType
|
|
SetQuerySubjects(func(_ context.Context, query identity.Query) ([]identity.Subject, error) {
|
|
if len(query.Types) == 1 && query.Types[0] == identity.SubjectCard && query.Keyword == "50506783" {
|
|
return []identity.Subject{{
|
|
Type: identity.SubjectCard, Category: identity.CategoryUser, ID: 17443,
|
|
Username: "50506783", Identifier: "898608691025D4186783", Name: "卡号 50506783", Enabled: true,
|
|
}}, nil
|
|
}
|
|
return nil, nil
|
|
})
|
|
t.Cleanup(func() { SetQuerySubjects(nil) })
|
|
|
|
var executedContext contract.BusinessReadContext
|
|
if err := SetCustomerQuickActions([]contract.CustomerQuickAction{
|
|
{
|
|
Code: "card/traffic", Title: "查流量", Message: "请查询流量", CustomerTypes: []string{"card"},
|
|
Execute: func(_ context.Context, businessContext contract.BusinessReadContext) (string, error) {
|
|
executedContext = businessContext
|
|
return "剩余流量 30G", nil
|
|
},
|
|
},
|
|
{
|
|
Code: "device/status", Title: "查设备", Message: "请查询设备", CustomerTypes: []string{"device"},
|
|
Execute: func(context.Context, contract.BusinessReadContext) (string, error) { return "设备正常", nil },
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("SetCustomerQuickActions() error = %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = SetCustomerQuickActions(nil) })
|
|
|
|
actions, err := CustomerQuickActionService.ListForConversation(context.Background(), conversation)
|
|
if err != nil || len(actions) != 1 || actions[0].Code != "card/traffic" {
|
|
t.Fatalf("known card actions = %#v, err = %v", actions, err)
|
|
}
|
|
if conversation.CustomerType != originalCustomerType || !ConversationService.IsCustomerConversationOwner(conversation, external) {
|
|
t.Fatalf("quick-action resolution changed guest ownership: %#v", conversation)
|
|
}
|
|
if _, _, err := CustomerQuickActionService.ExecuteAndRecord(
|
|
context.Background(), conversation.ID, "card/traffic", "known-card-1", external, "known-card-request-1",
|
|
); err != nil {
|
|
t.Fatalf("ExecuteAndRecord() error = %v", err)
|
|
}
|
|
if executedContext.CustomerType != "card" || executedContext.CustomerID != 17443 || executedContext.CustomerExternalID != "50506783" {
|
|
t.Fatalf("unexpected business context: %#v", executedContext)
|
|
}
|
|
}
|
|
|
|
func TestCustomerQuickActionsResolveDeviceFromConversationHistory(t *testing.T) {
|
|
db := setupMessageWelcomeTestDB(t)
|
|
aiAgent := createWelcomeTestAIAgent(t, db, "")
|
|
external := welcomeTestExternalUser("quick-device-history")
|
|
conversation, err := ConversationService.Create(external, 11, aiAgent.ID)
|
|
if err != nil {
|
|
t.Fatalf("create conversation: %v", err)
|
|
}
|
|
SetQuerySubjects(func(_ context.Context, query identity.Query) ([]identity.Subject, error) {
|
|
if len(query.Types) == 1 && query.Types[0] == identity.SubjectDevice && query.Keyword == "37012627000987" {
|
|
return []identity.Subject{{
|
|
Type: identity.SubjectDevice, Category: identity.CategoryUser, ID: 27,
|
|
Username: "37012627000987", Identifier: "37012627000987", Name: "设备号 37012627000987", Enabled: true,
|
|
}}, nil
|
|
}
|
|
return nil, nil
|
|
})
|
|
t.Cleanup(func() { SetQuerySubjects(nil) })
|
|
if _, err := MessageService.SendCustomerMessageWithoutAIReplyWithRequestID(
|
|
conversation.ID, "known-device-message", enums.IMMessageTypeHTML,
|
|
"<p>设备号 37012627000987</p>", "", external, "known-device-request",
|
|
); err != nil {
|
|
t.Fatalf("send identity message: %v", err)
|
|
}
|
|
if err := SetCustomerQuickActions([]contract.CustomerQuickAction{{
|
|
Code: "device/wifi", Title: "WiFi 信息", Message: "查询 WiFi", CustomerTypes: []string{"device"},
|
|
Execute: func(context.Context, contract.BusinessReadContext) (string, error) { return "WiFi 正常", nil },
|
|
}}); err != nil {
|
|
t.Fatalf("SetCustomerQuickActions() error = %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = SetCustomerQuickActions(nil) })
|
|
|
|
actions, err := CustomerQuickActionService.ListForConversation(context.Background(), conversation)
|
|
if err != nil || len(actions) != 1 || actions[0].Code != "device/wifi" {
|
|
t.Fatalf("known device actions = %#v, err = %v", actions, err)
|
|
}
|
|
}
|
|
|
|
func TestCustomerQuickActionRecordsReplyWithoutTriggeringAI(t *testing.T) {
|
|
db := setupMessageWelcomeTestDB(t)
|
|
aiAgent := createWelcomeTestAIAgent(t, db, "")
|
|
external := welcomeTestExternalUser("quick-action-user")
|
|
conversation, err := ConversationService.Create(external, 11, aiAgent.ID)
|
|
if err != nil {
|
|
t.Fatalf("create conversation: %v", err)
|
|
}
|
|
|
|
if err := SetCustomerQuickActions([]contract.CustomerQuickAction{{
|
|
Code: "test/status",
|
|
Title: "查状态",
|
|
Message: "请查询状态",
|
|
Execute: func(context.Context, contract.BusinessReadContext) (string, error) {
|
|
return "当前状态正常", nil
|
|
},
|
|
}}); err != nil {
|
|
t.Fatalf("SetCustomerQuickActions() error = %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = SetCustomerQuickActions(nil) })
|
|
|
|
previousHook := TriggerAIReplyAsyncHook
|
|
called := false
|
|
TriggerAIReplyAsyncHook = func(context.Context, models.Conversation, models.Message) { called = true }
|
|
t.Cleanup(func() { TriggerAIReplyAsyncHook = previousHook })
|
|
|
|
customerMessage, replyMessage, err := CustomerQuickActionService.ExecuteAndRecord(
|
|
context.Background(), conversation.ID, "test/status", "quick-client-1", external, "quick-request-1",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("ExecuteAndRecord() error = %v", err)
|
|
}
|
|
if customerMessage.Content != "请查询状态" || customerMessage.SenderType != enums.IMSenderTypeCustomer {
|
|
t.Fatalf("unexpected customer message: %#v", customerMessage)
|
|
}
|
|
if replyMessage.Content != "当前状态正常" || replyMessage.SenderType != enums.IMSenderTypeAI {
|
|
t.Fatalf("unexpected automatic reply: %#v", replyMessage)
|
|
}
|
|
if called {
|
|
t.Fatalf("quick action customer message must not trigger an AI reply")
|
|
}
|
|
|
|
var count int64
|
|
if err := db.Model(&models.Message{}).Where("conversation_id = ?", conversation.ID).Count(&count).Error; err != nil {
|
|
t.Fatalf("count messages: %v", err)
|
|
}
|
|
if count != 2 {
|
|
t.Fatalf("message count = %d, want 2", count)
|
|
}
|
|
}
|
|
|
|
func TestCustomerQuickActionAvailabilityIsCheckedForListAndExecution(t *testing.T) {
|
|
db := setupMessageWelcomeTestDB(t)
|
|
aiAgent := createWelcomeTestAIAgent(t, db, "")
|
|
external := welcomeTestExternalUser("quick-action-availability-user")
|
|
conversation, err := ConversationService.Create(external, 11, aiAgent.ID)
|
|
if err != nil {
|
|
t.Fatalf("create conversation: %v", err)
|
|
}
|
|
available := false
|
|
if err := SetCustomerQuickActions([]contract.CustomerQuickAction{{
|
|
Code: "test/dynamic", Title: "动态操作", Message: "执行动态操作",
|
|
Available: func(context.Context, contract.BusinessReadContext) (bool, error) {
|
|
return available, nil
|
|
},
|
|
Execute: func(context.Context, contract.BusinessReadContext) (string, error) {
|
|
return "执行成功", nil
|
|
},
|
|
}}); err != nil {
|
|
t.Fatalf("SetCustomerQuickActions() error = %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = SetCustomerQuickActions(nil) })
|
|
|
|
actions, err := CustomerQuickActionService.ListForConversation(context.Background(), conversation)
|
|
if err != nil || len(actions) != 0 {
|
|
t.Fatalf("unavailable action leaked into list: actions=%#v err=%v", actions, err)
|
|
}
|
|
if _, _, err := CustomerQuickActionService.ExecuteAndRecord(context.Background(), conversation.ID, "test/dynamic", "quick-dynamic-1", external, "quick-dynamic-request-1"); err == nil {
|
|
t.Fatal("unavailable action was executed")
|
|
}
|
|
|
|
available = true
|
|
actions, err = CustomerQuickActionService.ListForConversation(context.Background(), conversation)
|
|
if err != nil || len(actions) != 1 || actions[0].Code != "test/dynamic" {
|
|
t.Fatalf("available action missing from list: actions=%#v err=%v", actions, err)
|
|
}
|
|
}
|
|
|
|
func TestCustomerQuickActionExecutesMatchedRecordedMessage(t *testing.T) {
|
|
db := setupMessageWelcomeTestDB(t)
|
|
aiAgent := createWelcomeTestAIAgent(t, db, "")
|
|
external := welcomeTestExternalUser("matched-action-user")
|
|
conversation, err := ConversationService.Create(external, 11, aiAgent.ID)
|
|
if err != nil {
|
|
t.Fatalf("create conversation: %v", err)
|
|
}
|
|
if err := SetCustomerQuickActions([]contract.CustomerQuickAction{{
|
|
Code: "test/traffic", Title: "查流量", Message: "请查询流量",
|
|
MatchIntent: func(message string) bool { return message == "卡号 50506783,请查询流量" },
|
|
Execute: func(context.Context, contract.BusinessReadContext) (string, error) {
|
|
return "剩余流量:58.38G", nil
|
|
},
|
|
}}); err != nil {
|
|
t.Fatalf("SetCustomerQuickActions() error = %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = SetCustomerQuickActions(nil) })
|
|
|
|
message, err := MessageService.SendCustomerMessageWithoutAIReplyWithRequestID(
|
|
conversation.ID, "matched-customer-1", enums.IMMessageTypeText,
|
|
"卡号 50506783,请查询流量", "", external, "matched-request-1",
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("send customer message: %v", err)
|
|
}
|
|
matched, err := CustomerQuickActionService.ExecuteMatchedReply(
|
|
context.Background(), conversation, message.Content, message.RequestID, message.ID,
|
|
)
|
|
if err != nil || !matched {
|
|
t.Fatalf("ExecuteMatchedReply() matched=%v err=%v", matched, err)
|
|
}
|
|
list, _, _ := MessageService.FindByConversationIDCursor(conversation.ID, 0, 20, "", "")
|
|
if len(list) != 2 || list[1].Content != "剩余流量:58.38G" || list[1].SenderType != enums.IMSenderTypeAI {
|
|
t.Fatalf("unexpected messages: %#v", list)
|
|
}
|
|
}
|
|
|
|
func TestCustomerQuickActionExecutesSelectedMenuItem(t *testing.T) {
|
|
db := setupMessageWelcomeTestDB(t)
|
|
aiAgent := createWelcomeTestAIAgent(t, db, "")
|
|
external := welcomeTestExternalUser("selected-action-user")
|
|
conversation, err := ConversationService.Create(external, 11, aiAgent.ID)
|
|
if err != nil {
|
|
t.Fatalf("create conversation: %v", err)
|
|
}
|
|
if err := SetCustomerQuickActions([]contract.CustomerQuickAction{{
|
|
Code: "test/traffic", Title: "查流量", Message: "请查询流量", Sort: 10,
|
|
Execute: func(context.Context, contract.BusinessReadContext) (string, error) {
|
|
return "剩余流量:58.38G", nil
|
|
},
|
|
}}); err != nil {
|
|
t.Fatalf("SetCustomerQuickActions() error = %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = SetCustomerQuickActions(nil) })
|
|
|
|
matched, aiMessage, err := CustomerQuickActionService.ExecuteSelectedReply(
|
|
context.Background(), conversation, 1, "selected-request-1", 100,
|
|
)
|
|
if err != nil || !matched || aiMessage != "" {
|
|
t.Fatalf("ExecuteSelectedReply() matched=%v aiMessage=%q err=%v", matched, aiMessage, err)
|
|
}
|
|
list, _, _ := MessageService.FindByConversationIDCursor(conversation.ID, 0, 20, "", "")
|
|
if len(list) != 1 || list[0].Content != "剩余流量:58.38G" {
|
|
t.Fatalf("unexpected selected action messages: %#v", list)
|
|
}
|
|
}
|
|
|
|
func TestCustomerQuickActionExecutesActionByCode(t *testing.T) {
|
|
db := setupMessageWelcomeTestDB(t)
|
|
aiAgent := createWelcomeTestAIAgent(t, db, "")
|
|
external := welcomeTestExternalUser("coded-action-user")
|
|
conversation, err := ConversationService.Create(external, 11, aiAgent.ID)
|
|
if err != nil {
|
|
t.Fatalf("create conversation: %v", err)
|
|
}
|
|
if err := SetCustomerQuickActions([]contract.CustomerQuickAction{
|
|
{
|
|
Code: "test/status", Title: "查状态", Message: "请查询状态", Sort: 10,
|
|
Execute: func(context.Context, contract.BusinessReadContext) (string, error) {
|
|
return "当前状态正常", nil
|
|
},
|
|
},
|
|
{
|
|
Code: "test/diagnosis", Title: "智能检测", Message: "请智能检测", Sort: 20,
|
|
Execute: func(context.Context, contract.BusinessReadContext) (string, error) {
|
|
return "智能检测结果:网络异常", nil
|
|
},
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("SetCustomerQuickActions() error = %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = SetCustomerQuickActions(nil) })
|
|
|
|
matched, aiMessage, err := CustomerQuickActionService.ExecuteActionReply(
|
|
context.Background(), conversation, "test/diagnosis", "coded-request-1", 101,
|
|
)
|
|
if err != nil || !matched || aiMessage != "" {
|
|
t.Fatalf("ExecuteActionReply() matched=%v aiMessage=%q err=%v", matched, aiMessage, err)
|
|
}
|
|
list, _, _ := MessageService.FindByConversationIDCursor(conversation.ID, 0, 20, "", "")
|
|
if len(list) != 1 || list[0].Content != "智能检测结果:网络异常" {
|
|
t.Fatalf("unexpected coded action messages: %#v", list)
|
|
}
|
|
}
|