From 55d55101df31de95258dc7b0b82e85fde062fa73 Mon Sep 17 00:00:00 2001 From: mlogclub Date: Tue, 28 Apr 2026 22:56:19 +0800 Subject: [PATCH] feat(widget-demo): add direct chat URL generation and copy functionality --- internal/services/customer_service.go | 17 +++- internal/services/customer_service_test.go | 92 ++++++++++++++++++++++ web/components/kefu/widget-demo.tsx | 65 +++++++++++++-- 3 files changed, 163 insertions(+), 11 deletions(-) create mode 100644 internal/services/customer_service_test.go diff --git a/internal/services/customer_service.go b/internal/services/customer_service.go index 580a2b2..e4dccca 100644 --- a/internal/services/customer_service.go +++ b/internal/services/customer_service.go @@ -121,10 +121,21 @@ func (s *customerService) EnsureExternalCustomer(db *gorm.DB, externalUser openi } now := time.Now() if identity := repositories.CustomerIdentityRepository.GetBy(db, externalSource, externalID); identity != nil { - _ = repositories.CustomerRepository.Updates(db, identity.CustomerID, map[string]any{ + updates := map[string]any{ "last_active_at": now, "updated_at": now, - }) + } + if strs.IsNotBlank(externalUser.ExternalName) { + updates["name"] = externalUser.ExternalName + } + if err := repositories.CustomerRepository.Updates(db, identity.CustomerID, updates); err != nil { + return 0, err + } + if strs.IsNotBlank(externalUser.ExternalName) { + if err := s.syncConversationCustomerName(db, identity.CustomerID, externalUser.ExternalName, nil, now); err != nil { + return 0, err + } + } return identity.CustomerID, nil } @@ -255,7 +266,7 @@ func (s *customerService) syncConversationCustomerName(db *gorm.DB, customerID i return nil } updates := map[string]any{ - "customer_name": strings.TrimSpace(name), + "customer_name": name, "updated_at": now, } if operator != nil { diff --git a/internal/services/customer_service_test.go b/internal/services/customer_service_test.go new file mode 100644 index 0000000..25a44ba --- /dev/null +++ b/internal/services/customer_service_test.go @@ -0,0 +1,92 @@ +package services_test + +import ( + "testing" + "time" + + "cs-agent/internal/models" + "cs-agent/internal/pkg/enums" + "cs-agent/internal/pkg/openidentity" + "cs-agent/internal/services" + + "github.com/glebarez/sqlite" + "github.com/mlogclub/simple/sqls" + "gorm.io/gorm" + "gorm.io/gorm/schema" +) + +func TestEnsureExternalCustomerUpdatesNameFromExternalIdentity(t *testing.T) { + db := setupCustomerServiceTestDB(t) + + firstID, err := services.CustomerService.EnsureExternalCustomer(db, openidentity.ExternalUser{ + ExternalSource: enums.ExternalSourceUser, + ExternalID: "user-1", + ExternalName: "张三", + }) + if err != nil { + t.Fatalf("EnsureExternalCustomer() first error = %v", err) + } + + conversation := &models.Conversation{ + CustomerID: firstID, + CustomerName: "张三", + Status: enums.IMConversationStatusActive, + AuditFields: models.AuditFields{CreatedAt: time.Now(), UpdatedAt: time.Now()}, + } + if err := db.Create(conversation).Error; err != nil { + t.Fatalf("create conversation error = %v", err) + } + + secondID, err := services.CustomerService.EnsureExternalCustomer(db, openidentity.ExternalUser{ + ExternalSource: enums.ExternalSourceUser, + ExternalID: "user-1", + ExternalName: "李四", + }) + if err != nil { + t.Fatalf("EnsureExternalCustomer() second error = %v", err) + } + if secondID != firstID { + t.Fatalf("expected same customer id, got %d and %d", firstID, secondID) + } + + customer := services.CustomerService.Get(firstID) + if customer == nil { + t.Fatalf("expected customer to exist") + } + if customer.Name != "李四" { + t.Fatalf("expected customer name updated, got %q", customer.Name) + } + + var updatedConversation models.Conversation + if err := db.First(&updatedConversation, conversation.ID).Error; err != nil { + t.Fatalf("get conversation error = %v", err) + } + if updatedConversation.CustomerName != "李四" { + t.Fatalf("expected conversation customer name updated, got %q", updatedConversation.CustomerName) + } +} + +func setupCustomerServiceTestDB(t *testing.T) *gorm.DB { + t.Helper() + + db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{ + NamingStrategy: schema.NamingStrategy{ + TablePrefix: "t_", + SingularTable: true, + }, + }) + if err != nil { + t.Fatalf("open sqlite error = %v", err) + } + t.Cleanup(func() { + sqlDB, err := db.DB() + if err == nil { + _ = sqlDB.Close() + } + }) + if err := db.AutoMigrate(&models.Customer{}, &models.CustomerIdentity{}, &models.Conversation{}); err != nil { + t.Fatalf("auto migrate error = %v", err) + } + sqls.SetDB(db) + return db +} diff --git a/web/components/kefu/widget-demo.tsx b/web/components/kefu/widget-demo.tsx index 8ad0ad5..9345cf9 100644 --- a/web/components/kefu/widget-demo.tsx +++ b/web/components/kefu/widget-demo.tsx @@ -28,7 +28,6 @@ declare global { CSAgentWidget?: { mount: (config: KefuWidgetHostConfig) => void destroy: () => void - open: () => void close: () => void } } @@ -134,6 +133,7 @@ export function KefuWidgetDemo() { const [status, setStatus] = useState("请填写 channelId") const [origin, setOrigin] = useState("") const [generatedToken, setGeneratedToken] = useState("") + const [copied, setCopied] = useState(false) async function mountWidget(configToMount: WidgetDemoConfig) { let userToken = "" @@ -200,6 +200,21 @@ ${configLines.join(",\n")} ` }, [config, generatedToken, origin]) + const directChatUrl = useMemo(() => { + const base = origin || "" + const channelId = (config.channelId || "").trim() + if (!base || !channelId) { + return "" + } + + const url = new URL("/kefu/chat/", base) + url.searchParams.set("channelId", channelId) + if (config.authMode === "jwt" && generatedToken) { + url.searchParams.set("userToken", generatedToken) + } + return url.toString() + }, [config.authMode, config.channelId, generatedToken, origin]) + function updateField( key: K, value: WidgetDemoConfig[K] @@ -217,6 +232,15 @@ ${configLines.join(",\n")} } } + async function handleCopyDirectUrl() { + if (!directChatUrl || typeof navigator === "undefined") { + return + } + await navigator.clipboard.writeText(directChatUrl) + setCopied(true) + window.setTimeout(() => setCopied(false), 1600) + } + return (
@@ -275,13 +299,6 @@ ${configLines.join(",\n")} > 挂载 - + +
+ + {generatedToken ? (
当前 userToken