refactor: remove ExternalSource and ExternalID from Conversation model and related services, update references to use ChannelID instead

This commit is contained in:
mlogclub
2026-04-27 19:04:10 +08:00
parent 24105781d9
commit b3ef55b04e
21 changed files with 183 additions and 136 deletions
+3 -3
View File
@@ -19,8 +19,6 @@ func BuildConversation(item *models.Conversation) response.ConversationResponse
AIAgentID: item.AIAgentID,
ChannelID: item.ChannelID,
CustomerID: item.CustomerID,
ExternalSource: item.ExternalSource,
ExternalID: item.ExternalID,
Subject: item.Subject,
Status: item.Status,
ServiceMode: item.ServiceMode,
@@ -38,11 +36,13 @@ func BuildConversation(item *models.Conversation) response.ConversationResponse
AgentLastReadMessageID: readStateMessageID(agentReadState),
AgentLastReadSeqNo: readStateSeqNo(agentReadState),
AgentLastReadAt: readStateAt(agentReadState),
CustomerOnline: services.WsService.IsGuestOnline(item.ExternalID),
ClosedAt: utils.FormatTimePtr(item.ClosedAt),
ClosedBy: item.ClosedBy,
CloseReason: item.CloseReason,
}
if identity := services.ConversationService.GetConversationExternalIdentity(item); identity != nil {
ret.CustomerOnline = services.WsService.IsGuestOnline(identity.ExternalID)
}
if item.CurrentAssigneeID > 0 {
if user := services.UserService.Get(item.CurrentAssigneeID); user != nil {
ret.CurrentAssigneeName = user.Nickname
@@ -28,7 +28,6 @@ func (c *ConversationController) AnyList() *web.JsonResult {
cnd := params.NewPagedSqlCnd(c.Ctx,
params.QueryFilter{ParamName: "status"},
params.QueryFilter{ParamName: "externalSource"},
params.QueryFilter{ParamName: "serviceMode"},
params.QueryFilter{ParamName: "currentAssigneeId"},
).Desc("last_message_at").Desc("id")
@@ -37,7 +36,7 @@ func (c *ConversationController) AnyList() *web.JsonResult {
if keyword, _ := params.Get(c.Ctx, "keyword"); strs.IsNotBlank(keyword) {
keywordLike := "%" + strings.TrimSpace(keyword) + "%"
cnd.Where("subject LIKE ? OR external_id LIKE ? OR last_message_summary LIKE ?", keywordLike, keywordLike, keywordLike)
cnd.Where("subject LIKE ? OR last_message_summary LIKE ?", keywordLike, keywordLike)
}
// 标签搜索
+22 -24
View File
@@ -323,30 +323,28 @@ type Tag struct {
// Conversation 客服会话。
type Conversation struct {
ID int64 `gorm:"primaryKey;autoIncrement"` // ID 为会话主键。
AIAgentID int64 `gorm:"type:bigint;not null;default:0;index"` // AIAgentID 为当前会话绑定的 AI Agent ID。
ChannelID int64 `gorm:"type:bigint;not null;default:0;index"` // ChannelID 为该会话来源接入渠道ID。
CustomerID int64 `gorm:"type:bigint;not null;default:0;index"` // CustomerID 为已关联的 CRM 客户 ID;0 表示未关联(访客仅 ExternalUserID
ExternalSource enums.ExternalSource `gorm:"type:varchar(50);not null;default:'';index"` // ExternalSource 为外部身份来源
ExternalID string `gorm:"type:varchar(128);not null;default:'';index"` // ExternalID 为外部访客ID
Subject string `gorm:"type:varchar(255);not null;default:''"` // Subject 为会话标题或摘要
Status enums.IMConversationStatus `gorm:"type:int;not null;default:1;index"` // Status 为会话状态,如待接入、处理中、已关闭
ServiceMode enums.IMConversationServiceMode `gorm:"type:int;not null;default:3;index"` // ServiceMode 为服务模式,如仅AI、仅人工、AI优先人工接管
Priority int `gorm:"type:int;not null;default:0;index"` // Priority 为会话优先级
CurrentAssigneeID int64 `gorm:"type:bigint;not null;default:0;index"` // CurrentAssigneeID 为当前接待客服ID。
CurrentTeamID int64 `gorm:"type:bigint;not null;default:0;index"` // CurrentTeamID 为当前处理客服组ID
LastMessageID int64 `gorm:"type:bigint;not null;default:0;index"` // LastMessageID 为最后一条消息ID
LastMessageAt time.Time `gorm:"type:datetime;index"` // LastMessageAt 为最后消息时间
LastActiveAt time.Time `gorm:"type:datetime;index"` // LastActiveAt 为会话最近活跃时间
LastMessageSummary string `gorm:"type:varchar(255);not null;default:''"` // LastMessageSummary 为最后一条消息摘要
CustomerUnreadCount int `gorm:"type:int;not null;default:0"` // CustomerUnreadCount 为用户侧未读数
AgentUnreadCount int `gorm:"type:int;not null;default:0"` // AgentUnreadCount 为客服侧未读数
HandoffAt *time.Time `gorm:"type:datetime;index"` // HandoffAt 为最近一次转人工时间
HandoffReason string `gorm:"type:varchar(255);not null;default:''"` // HandoffReason 为最近一次转人工原因
AIReplyRounds int `gorm:"type:int;not null;default:0"` // AIReplyRounds 为当前会话内 AI 已成功回复次数
ClosedAt *time.Time `gorm:"type:datetime;index"` // ClosedAt 为会话关闭时间
ClosedBy int64 `gorm:"type:bigint;not null;default:0;index"` // ClosedBy 为关闭人用户ID,访客关闭时写0。
CloseReason string `gorm:"type:varchar(255);not null;default:''"` // CloseReason 为关闭原因。
ID int64 `gorm:"primaryKey;autoIncrement"` // ID 为会话主键。
AIAgentID int64 `gorm:"type:bigint;not null;default:0;index"` // AIAgentID 为当前会话绑定的 AI Agent ID。
ChannelID int64 `gorm:"type:bigint;not null;default:0;index"` // ChannelID 为该会话来源接入渠道ID。
CustomerID int64 `gorm:"type:bigint;not null;default:0;index"` // CustomerID 为会话所属客户 ID
Subject string `gorm:"type:varchar(255);not null;default:''"` // Subject 为会话标题或摘要
Status enums.IMConversationStatus `gorm:"type:int;not null;default:1;index"` // Status 为会话状态,如待接入、处理中、已关闭
ServiceMode enums.IMConversationServiceMode `gorm:"type:int;not null;default:3;index"` // ServiceMode 为服务模式,如仅AI、仅人工、AI优先人工接管
Priority int `gorm:"type:int;not null;default:0;index"` // Priority 为会话优先级
CurrentAssigneeID int64 `gorm:"type:bigint;not null;default:0;index"` // CurrentAssigneeID 为当前接待客服ID
CurrentTeamID int64 `gorm:"type:bigint;not null;default:0;index"` // CurrentTeamID 为当前处理客服组ID
LastMessageID int64 `gorm:"type:bigint;not null;default:0;index"` // LastMessageID 为最后一条消息ID。
LastMessageAt time.Time `gorm:"type:datetime;index"` // LastMessageAt 为最后消息时间
LastActiveAt time.Time `gorm:"type:datetime;index"` // LastActiveAt 为会话最近活跃时间
LastMessageSummary string `gorm:"type:varchar(255);not null;default:''"` // LastMessageSummary 为最后一条消息摘要
CustomerUnreadCount int `gorm:"type:int;not null;default:0"` // CustomerUnreadCount 为用户侧未读数
AgentUnreadCount int `gorm:"type:int;not null;default:0"` // AgentUnreadCount 为客服侧未读数
HandoffAt *time.Time `gorm:"type:datetime;index"` // HandoffAt 为最近一次转人工时间
HandoffReason string `gorm:"type:varchar(255);not null;default:''"` // HandoffReason 为最近一次转人工原因
AIReplyRounds int `gorm:"type:int;not null;default:0"` // AIReplyRounds 为当前会话内 AI 已成功回复次数
ClosedAt *time.Time `gorm:"type:datetime;index"` // ClosedAt 为会话关闭时间
ClosedBy int64 `gorm:"type:bigint;not null;default:0;index"` // ClosedBy 为关闭人用户ID,访客关闭时写0
CloseReason string `gorm:"type:varchar(255);not null;default:''"` // CloseReason 为关闭原因
AuditFields
}
@@ -12,7 +12,6 @@ const (
type ConversationListRequest struct {
Status int `json:"status"`
ExternalSource string `json:"externalSource"`
ServiceMode int `json:"serviceMode"`
CurrentAssigneeID int64 `json:"currentAssigneeId"`
Keyword string `json:"keyword"`
@@ -22,8 +22,6 @@ type ConversationResponse struct {
AIAgentID int64 `json:"aiAgentId"`
ChannelID int64 `json:"channelId"`
CustomerID int64 `json:"customerId"`
ExternalSource enums.ExternalSource `json:"externalSource"`
ExternalID string `json:"externalId"`
Subject string `json:"subject"`
Status enums.IMConversationStatus `json:"status"`
ServiceMode enums.IMConversationServiceMode `json:"serviceMode"`
@@ -45,6 +45,13 @@ func (r *customerIdentityRepository) GetBy(db *gorm.DB, externalSource enums.Ext
Eq("external_id", externalID))
}
func (r *customerIdentityRepository) FindByCustomerID(db *gorm.DB, customerID int64) []models.CustomerIdentity {
if customerID <= 0 {
return nil
}
return r.Find(db, sqls.NewCnd().Eq("customer_id", customerID).Eq("status", enums.StatusOk).Desc("id"))
}
func (r *customerIdentityRepository) Find(db *gorm.DB, cnd *sqls.Cnd) (list []models.CustomerIdentity) {
cnd.Find(db, &list)
return
@@ -78,7 +78,8 @@ func (s *channelMessageOutboxService) EnqueueWxWorkKFMessage(conversation *model
if conversation == nil || message == nil {
return nil
}
if conversation.ExternalSource != enums.ExternalSourceWxWorkKF {
channel := ChannelService.Get(conversation.ChannelID)
if channel == nil || channel.ChannelType != enums.ChannelTypeWxWorkKF {
return nil
}
if message.SenderType != enums.IMSenderTypeAgent && message.SenderType != enums.IMSenderTypeAI {
+114 -61
View File
@@ -25,6 +25,7 @@ import (
"github.com/mlogclub/simple/common/strs"
"github.com/mlogclub/simple/sqls"
"gorm.io/gorm"
)
var ConversationService = newConversationService()
@@ -60,7 +61,7 @@ func (s *conversationService) ListConversations(userID int64, filter request.Age
if strs.IsNotBlank(keyword) {
keyword = strings.TrimSpace(keyword)
cnd.Where("subject LIKE ? OR external_id LIKE ? OR last_message_summary LIKE ?", "%"+keyword+"%", "%"+keyword+"%", "%"+keyword+"%")
cnd.Where("subject LIKE ? OR last_message_summary LIKE ?", "%"+keyword+"%", "%"+keyword+"%")
}
switch filter {
@@ -86,51 +87,56 @@ func (s *conversationService) Updates(id int64, columns map[string]interface{})
return repositories.ConversationRepository.Updates(sqls.DB(), id, columns)
}
func (s *conversationService) getLatestNotFinished(externalInfo openidentity.ExternalInfo) *models.Conversation {
func (s *conversationService) getLatestNotFinishedByCustomerID(db *gorm.DB, customerID int64) *models.Conversation {
if customerID <= 0 {
return nil
}
cnd := sqls.NewCnd()
cnd.Eq("external_id", externalInfo.ExternalID)
cnd.Eq("external_source", externalInfo.ExternalSource)
cnd.Eq("customer_id", customerID)
cnd.In("status", []enums.IMConversationStatus{
enums.IMConversationStatusAIServing,
enums.IMConversationStatusPending,
enums.IMConversationStatusActive,
})
cnd.Desc("id")
return s.FindOne(cnd)
return repositories.ConversationRepository.FindOne(db, cnd)
}
func (s *conversationService) Create(externalInfo openidentity.ExternalInfo, channelID, aiAgentID int64) (*models.Conversation, error) {
subject := s.buildDefaultSubject(externalInfo)
// 会话存在,直接返回
if conversation := s.getLatestNotFinished(externalInfo); conversation != nil {
return conversation, nil
}
aiAgent := AIAgentService.Get(aiAgentID)
if aiAgent == nil || aiAgent.Status != enums.StatusOk {
return nil, errorsx.InvalidParam("AI Agent not found")
}
conversation := &models.Conversation{
AIAgentID: aiAgentID,
ChannelID: channelID,
ExternalSource: externalInfo.ExternalSource,
Subject: subject,
Status: s.resolveInitialStatus(aiAgent.ServiceMode),
ServiceMode: aiAgent.ServiceMode,
Priority: 0,
ExternalID: externalInfo.ExternalID,
CurrentAssigneeID: 0,
CurrentTeamID: 0,
LastMessageAt: time.Now(),
LastActiveAt: time.Now(),
AuditFields: utils.BuildAuditFields(nil),
}
if identity := repositories.CustomerIdentityRepository.GetBy(sqls.DB(), externalInfo.ExternalSource, externalInfo.ExternalID); identity != nil {
conversation.CustomerID = identity.CustomerID
}
var conversation *models.Conversation
created := false
if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
customerID, err := s.ensureExternalCustomer(ctx.Tx, externalInfo)
if err != nil {
return err
}
if existing := s.getLatestNotFinishedByCustomerID(ctx.Tx, customerID); existing != nil {
conversation = existing
return nil
}
created = true
now := time.Now()
conversation = &models.Conversation{
AIAgentID: aiAgentID,
ChannelID: channelID,
CustomerID: customerID,
Subject: subject,
Status: s.resolveInitialStatus(aiAgent.ServiceMode),
ServiceMode: aiAgent.ServiceMode,
Priority: 0,
CurrentAssigneeID: 0,
CurrentTeamID: 0,
LastMessageAt: now,
LastActiveAt: now,
AuditFields: utils.BuildAuditFields(nil),
}
if err := ctx.Tx.Create(conversation).Error; err != nil {
return err
}
@@ -141,6 +147,12 @@ func (s *conversationService) Create(externalInfo openidentity.ExternalInfo, cha
}); err != nil {
return nil, err
}
if conversation == nil {
return nil, errorsx.BusinessError(1, "创建会话失败")
}
if !created {
return conversation, nil
}
// 推送会话创建事件
WsService.PublishConversationChanged(conversation, enums.IMRealtimeEventConversationCreated)
@@ -158,6 +170,43 @@ func (s *conversationService) Create(externalInfo openidentity.ExternalInfo, cha
return s.Get(conversation.ID), nil
}
func (s *conversationService) ensureExternalCustomer(db *gorm.DB, externalInfo openidentity.ExternalInfo) (int64, error) {
externalSource := externalInfo.ExternalSource
externalID := strings.TrimSpace(externalInfo.ExternalID)
if strings.TrimSpace(string(externalSource)) == "" || externalID == "" {
return 0, errorsx.Unauthorized("外部用户标识不能为空")
}
now := time.Now()
if identity := repositories.CustomerIdentityRepository.GetBy(db, externalSource, externalID); identity != nil {
_ = repositories.CustomerRepository.Updates(db, identity.CustomerID, map[string]any{
"last_active_at": now,
"updated_at": now,
})
return identity.CustomerID, nil
}
customer := &models.Customer{
Name: s.buildDefaultSubject(externalInfo),
LastActiveAt: &now,
Status: enums.StatusOk,
AuditFields: utils.BuildAuditFields(nil),
}
if err := repositories.CustomerRepository.Create(db, customer); err != nil {
return 0, err
}
identity := &models.CustomerIdentity{
CustomerID: customer.ID,
ExternalSource: externalSource,
ExternalID: externalID,
Status: enums.StatusOk,
AuditFields: utils.BuildAuditFields(nil),
}
if err := repositories.CustomerIdentityRepository.Create(db, identity); err != nil {
return 0, err
}
return customer.ID, nil
}
func (s *conversationService) AssignConversation(req request.AssignConversationRequest, operator *dto.AuthPrincipal) error {
if operator == nil {
return errorsx.Unauthorized("未登录或登录已过期")
@@ -638,20 +687,14 @@ func (s *conversationService) IsCustomerConversationOwner(conversation *models.C
return false
}
extID := strings.TrimSpace(externalInfo.ExternalID)
if extID == "" || strings.TrimSpace(conversation.ExternalID) == "" {
if extID == "" || strings.TrimSpace(string(externalInfo.ExternalSource)) == "" || conversation.CustomerID <= 0 {
return false
}
if conversation.ExternalID != extID {
identity := repositories.CustomerIdentityRepository.GetBy(sqls.DB(), externalInfo.ExternalSource, extID)
if identity == nil {
return false
}
reqSrc := strings.TrimSpace(string(externalInfo.ExternalSource))
convSrc := strings.TrimSpace(string(conversation.ExternalSource))
if convSrc != "" {
if reqSrc == "" || reqSrc != convSrc {
return false
}
}
return true
return identity.CustomerID == conversation.CustomerID
}
func (s *conversationService) BuildConversationSummary(conversation *models.Conversation) string {
@@ -704,7 +747,7 @@ func (s *conversationService) buildEventPayload(payload map[string]any) string {
return string(data)
}
// LinkConversationCustomer 将会话绑定到指定客户;若会话带外部访客标识则维护 CustomerIdentity(与创建会话时逻辑一致)
// LinkConversationCustomer 将会话绑定到指定客户。
func (s *conversationService) LinkConversationCustomer(conversationID, customerID int64, operator *dto.AuthPrincipal) error {
if operator == nil {
return errorsx.Unauthorized("未登录或登录已过期")
@@ -727,33 +770,11 @@ func (s *conversationService) LinkConversationCustomer(conversationID, customerI
return errorsx.Forbidden("无权限关联该会话")
}
extID := strings.TrimSpace(conv.ExternalID)
extSrc := strings.TrimSpace(string(conv.ExternalSource))
err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
current := repositories.ConversationRepository.Get(ctx.Tx, conversationID)
if current == nil {
return errorsx.InvalidParam("会话不存在")
}
if extID != "" && extSrc != "" {
existing := repositories.CustomerIdentityRepository.GetBy(ctx.Tx, enums.ExternalSource(extSrc), extID)
if existing != nil {
if existing.CustomerID != customerID {
return errorsx.BusinessError(1, "该访客身份已绑定其他客户,无法关联到当前选择")
}
} else {
idRow := &models.CustomerIdentity{
CustomerID: customerID,
ExternalSource: enums.ExternalSource(extSrc),
ExternalID: extID,
Status: enums.StatusOk,
AuditFields: utils.BuildAuditFields(operator),
}
if err := repositories.CustomerIdentityRepository.Create(ctx.Tx, idRow); err != nil {
return err
}
}
}
now := time.Now()
return repositories.ConversationRepository.Updates(ctx.Tx, conversationID, map[string]any{
"customer_id": customerID,
@@ -771,6 +792,38 @@ func (s *conversationService) LinkConversationCustomer(conversationID, customerI
return nil
}
func (s *conversationService) GetConversationExternalIdentity(conversation *models.Conversation) *models.CustomerIdentity {
if conversation == nil || conversation.CustomerID <= 0 {
return nil
}
identities := repositories.CustomerIdentityRepository.FindByCustomerID(sqls.DB(), conversation.CustomerID)
if len(identities) == 0 {
return nil
}
if channel := ChannelService.Get(conversation.ChannelID); channel != nil {
expected := externalSourceForChannelType(channel.ChannelType)
if strings.TrimSpace(string(expected)) != "" {
for i := range identities {
if identities[i].ExternalSource == expected {
return &identities[i]
}
}
}
}
return &identities[0]
}
func externalSourceForChannelType(channelType string) enums.ExternalSource {
switch strings.TrimSpace(channelType) {
case enums.ChannelTypeWxWorkKF:
return enums.ExternalSourceWxWorkKF
case enums.ChannelTypeWeb:
return enums.ExternalSourceGuest
default:
return ""
}
}
func (s *conversationService) canLinkConversationCustomer(conv *models.Conversation, operator *dto.AuthPrincipal) bool {
if conv == nil || operator == nil {
return false
@@ -56,7 +56,7 @@ func buildConversationAssignedNotifyBody(conversation *models.Conversation, assi
lines := []string{
fmt.Sprintf("会话ID: #%d", conversation.ID),
fmt.Sprintf("会话主题: %s", strs.DefaultIfBlank(conversation.Subject, "-")),
fmt.Sprintf("接入渠道: %s", enums.GetExternalSourceLabel(conversation.ExternalSource)),
fmt.Sprintf("接入渠道: %s", resolveConversationChannelLabel(conversation)),
fmt.Sprintf("当前状态: %s", enums.GetIMConversationStatusLabel(conversation.Status)),
fmt.Sprintf("处理人: %s", resolveNotifyUserLabel(assigneeID)),
}
@@ -67,6 +67,16 @@ func buildConversationAssignedNotifyBody(conversation *models.Conversation, assi
return strings.Join(lines, "\n")
}
func resolveConversationChannelLabel(conversation *models.Conversation) string {
if conversation == nil || conversation.ChannelID <= 0 {
return "-"
}
if channel := services.ChannelService.Get(conversation.ChannelID); channel != nil {
return strs.DefaultIfBlank(channel.Name, channel.ChannelType)
}
return "-"
}
func resolveNotifyUserLabel(userID int64) string {
if userID <= 0 {
return "-"
-1
View File
@@ -410,7 +410,6 @@ func (s *messageService) sendMessage(conversationID int64, senderType enums.IMSe
slog.Error("enqueue wxwork kf outbox failed",
"conversation_id", conversation.ID,
"message_id", message.ID,
"external_source", conversation.ExternalSource,
"error", enqueueErr,
)
}
+11 -1
View File
@@ -862,7 +862,7 @@ func (s *ticketService) CreateFromConversation(req request.CreateTicketFromConve
Title: title,
Description: description,
Source: string(enums.TicketSourceConversation),
Channel: string(conversation.ExternalSource),
Channel: s.resolveConversationChannel(conversation),
CustomerID: conversation.CustomerID,
ConversationID: conversation.ID,
TagIDs: req.TagIDs,
@@ -1845,6 +1845,16 @@ func parseOptionalDateTime(value string) (*time.Time, error) {
return nil, fmt.Errorf("invalid datetime")
}
func (s *ticketService) resolveConversationChannel(conversation *models.Conversation) string {
if conversation == nil || conversation.ChannelID <= 0 {
return ""
}
if channel := ChannelService.Get(conversation.ChannelID); channel != nil {
return channel.ChannelType
}
return ""
}
func diffMinutes(start *time.Time, end time.Time) int {
if start == nil || start.IsZero() {
return 0
+2 -2
View File
@@ -514,8 +514,8 @@ func (s *wsService) routeConversationTopics(conversation *models.Conversation) [
}
topics := []string{s.conversationTopic(conversation.ID)}
if strings.TrimSpace(conversation.ExternalID) != "" {
topics = append(topics, s.guestTopic(conversation.ExternalID))
if identity := ConversationService.GetConversationExternalIdentity(conversation); identity != nil && strings.TrimSpace(identity.ExternalID) != "" {
topics = append(topics, s.guestTopic(identity.ExternalID))
}
if conversation.CurrentAssigneeID > 0 {
topics = append(topics, s.adminTopic(conversation.CurrentAssigneeID))
+3 -16
View File
@@ -13,7 +13,6 @@ import (
"cs-agent/internal/wxwork"
"github.com/mlogclub/simple/common/strs"
"github.com/mlogclub/simple/sqls"
"github.com/silenceper/wechat/v2/work/kf"
"github.com/silenceper/wechat/v2/work/kf/syncmsg"
)
@@ -378,21 +377,9 @@ func (s *wxWorkKFInboundService) ensureConversation(base syncmsg.BaseMessage, pr
}
external := s.buildExternalInfo(externalID)
conversation := ConversationService.FindOne(sqls.NewCnd().
Eq("external_source", external.ExternalSource).
Eq("external_id", external.ExternalID).
In("status", []enums.IMConversationStatus{
enums.IMConversationStatusAIServing,
enums.IMConversationStatusPending,
enums.IMConversationStatusActive,
}).
Desc("id"))
if conversation == nil {
conversation, err = ConversationService.Create(external, channel.ID, channel.AIAgentID)
if err != nil {
return nil, err
}
conversation, err := ConversationService.Create(external, channel.ID, channel.AIAgentID)
if err != nil {
return nil, err
}
if err := s.upsertConversationMapping(
@@ -339,8 +339,8 @@ export function ConversationDetailDialog({
value={currentConversation.currentAssigneeName || "-"}
/>
<InfoItem
label="渠道类型"
value={currentConversation.externalSource || "-"}
label="渠道ID"
value={`${currentConversation.channelId || "-"}`}
/>
<InfoItem
label="客服未读"
@@ -726,7 +726,7 @@ export default function DashboardConversationsPage() {
<div className="min-w-0">
<div className="font-medium">{item.subject || `会话 #${item.id}`}</div>
<div className="mt-1 text-sm text-muted-foreground">
{item.externalSource || "-"}
ID{item.channelId || "-"}
</div>
<div className="mt-1 line-clamp-2 text-sm text-muted-foreground">
{item.lastMessageSummary || "暂无最新消息摘要"}
@@ -168,10 +168,10 @@ function MissingCustomerEmpty({ conversation }: { conversation: AgentConversatio
</Button>
</div>
<div className="space-y-2">
<SectionHeading>访</SectionHeading>
<SectionHeading></SectionHeading>
<div className="space-y-2">
<DetailRow label="外部来源" value={conversation.externalSource} />
<DetailRow label="外部标识" value={conversation.externalId} />
<DetailRow label="渠道ID" value={conversation.channelId ? `${conversation.channelId}` : "-"} />
<DetailRow label="客户ID" value={conversation.customerId ? `${conversation.customerId}` : "-"} />
</div>
</div>
<CustomerLinkOrCreateDialog
@@ -101,12 +101,6 @@ export function ConversationList({ onAfterSelect }: ConversationListProps) {
>
{getEnumLabel(IMConversationStatusLabels, conversation.status)}
</span>
{conversation.externalSource ? (
<>
<span className="opacity-40">·</span>
<span className="truncate">{conversation.externalSource}</span>
</>
) : null}
</div>
</div>
</div>
+1 -3
View File
@@ -292,9 +292,7 @@ export default function ConversationsPage() {
</span>
</div>
<p className="mt-0.5 truncate text-xs text-muted-foreground sm:text-sm">
<span>{conversation.externalSource}</span>
<span className="text-muted-foreground/60"> / </span>
<span>{conversation.externalId}</span>
<span> #{conversation.channelId || "-"}</span>
{conversation.customerId ? (
<>
<span className="text-muted-foreground/60"> / </span>
-2
View File
@@ -109,8 +109,6 @@ export type ConversationParticipant = {
export type AdminConversation = {
id: number
channelId: number
externalSource: string
externalId: string
subject: string
status: number
serviceMode: number
-2
View File
@@ -37,8 +37,6 @@ export type AgentConversation = {
aiAgentId?: number
channelId?: number
customerId?: number
externalSource: string
externalId: string
subject: string
status: number
serviceMode: number
-2
View File
@@ -34,8 +34,6 @@ export type ImConversationParticipant = {
export type ImConversation = {
id: number
channelId: number
externalSource: string
externalId: string
subject: string
status: number
serviceMode: number