2026-04-09 10:01:23 +08:00
|
|
|
package builders
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"strings"
|
|
|
|
|
|
2026-08-21 00:41:07 +08:00
|
|
|
"code.tczkiot.com/wlw/ai-agent/internal/models"
|
|
|
|
|
"code.tczkiot.com/wlw/ai-agent/internal/pkg/dto/response"
|
|
|
|
|
"code.tczkiot.com/wlw/ai-agent/internal/pkg/enums"
|
|
|
|
|
"code.tczkiot.com/wlw/ai-agent/internal/pkg/i18nx"
|
|
|
|
|
"code.tczkiot.com/wlw/ai-agent/internal/pkg/utils"
|
|
|
|
|
"code.tczkiot.com/wlw/ai-agent/internal/services"
|
2026-04-09 10:01:23 +08:00
|
|
|
|
|
|
|
|
"github.com/mlogclub/simple/sqls"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func BuildConversation(item *models.Conversation) response.ConversationResponse {
|
2026-05-31 20:06:44 +08:00
|
|
|
return BuildConversationWithLocale(item, i18nx.DefaultLocale)
|
2026-05-25 12:06:15 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func BuildConversationWithLocale(item *models.Conversation, locale string) response.ConversationResponse {
|
2026-04-09 10:01:23 +08:00
|
|
|
agentReadState, customerReadState := services.ConversationReadStateService.GetConversationReadStates(item.ID)
|
|
|
|
|
ret := response.ConversationResponse{
|
|
|
|
|
ID: item.ID,
|
|
|
|
|
AIAgentID: item.AIAgentID,
|
2026-04-27 17:59:23 +08:00
|
|
|
ChannelID: item.ChannelID,
|
2026-08-28 22:23:13 +08:00
|
|
|
CustomerType: item.CustomerType,
|
2026-04-09 10:01:23 +08:00
|
|
|
CustomerID: item.CustomerID,
|
2026-08-28 22:23:13 +08:00
|
|
|
CustomerExternalID: item.CustomerExternalID,
|
2026-04-27 19:27:24 +08:00
|
|
|
CustomerName: item.CustomerName,
|
2026-04-09 10:01:23 +08:00
|
|
|
Status: item.Status,
|
|
|
|
|
ServiceMode: item.ServiceMode,
|
|
|
|
|
Priority: item.Priority,
|
|
|
|
|
CurrentAssigneeID: item.CurrentAssigneeID,
|
2026-05-02 11:34:32 +08:00
|
|
|
CurrentTeamID: item.CurrentTeamID,
|
2026-04-09 10:01:23 +08:00
|
|
|
LastMessageID: item.LastMessageID,
|
|
|
|
|
LastMessageAt: utils.FormatTime(item.LastMessageAt),
|
|
|
|
|
LastActiveAt: utils.FormatTime(item.LastActiveAt),
|
2026-05-25 12:06:15 +08:00
|
|
|
LastMessageSummary: localizeConversationSummary(locale, item.LastMessageSummary),
|
2026-04-09 10:01:23 +08:00
|
|
|
CustomerUnreadCount: item.CustomerUnreadCount,
|
|
|
|
|
AgentUnreadCount: item.AgentUnreadCount,
|
|
|
|
|
CustomerLastReadMessageID: readStateMessageID(customerReadState),
|
|
|
|
|
CustomerLastReadAt: readStateAt(customerReadState),
|
|
|
|
|
AgentLastReadMessageID: readStateMessageID(agentReadState),
|
|
|
|
|
AgentLastReadAt: readStateAt(agentReadState),
|
|
|
|
|
ClosedAt: utils.FormatTimePtr(item.ClosedAt),
|
|
|
|
|
ClosedBy: item.ClosedBy,
|
|
|
|
|
CloseReason: item.CloseReason,
|
|
|
|
|
}
|
2026-04-27 19:04:10 +08:00
|
|
|
if identity := services.ConversationService.GetConversationExternalIdentity(item); identity != nil {
|
|
|
|
|
ret.CustomerOnline = services.WsService.IsGuestOnline(identity.ExternalID)
|
|
|
|
|
}
|
2026-08-28 22:23:13 +08:00
|
|
|
queueSnapshot := services.ConversationQueueService.GetSnapshot(item)
|
|
|
|
|
if queueSnapshot.Queued {
|
|
|
|
|
ret.QueueEnteredAt = utils.FormatTimePtr(queueSnapshot.EnteredAt)
|
|
|
|
|
ret.QueuePosition = queueSnapshot.Position
|
|
|
|
|
ret.QueueAheadCount = queueSnapshot.AheadCount
|
|
|
|
|
ret.QueueWaitingCount = queueSnapshot.WaitingCount
|
|
|
|
|
ret.QueueWaitSeconds = queueSnapshot.WaitSeconds
|
|
|
|
|
ret.QueueEstimatedWaitSeconds = queueSnapshot.EstimatedWaitSeconds
|
|
|
|
|
ret.QueueEscalationLevel = queueSnapshot.EscalationLevel
|
|
|
|
|
ret.EffectivePriority = queueSnapshot.EffectivePriority
|
|
|
|
|
ret.QueueServiceOnline = queueSnapshot.ServiceOnline
|
|
|
|
|
}
|
2026-04-09 10:01:23 +08:00
|
|
|
if item.CurrentAssigneeID > 0 {
|
|
|
|
|
if user := services.UserService.Get(item.CurrentAssigneeID); user != nil {
|
|
|
|
|
ret.CurrentAssigneeName = user.Nickname
|
|
|
|
|
if ret.CurrentAssigneeName == "" {
|
|
|
|
|
ret.CurrentAssigneeName = user.Username
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-05-02 11:34:32 +08:00
|
|
|
if item.CurrentTeamID > 0 {
|
|
|
|
|
if team := services.AgentTeamService.Get(item.CurrentTeamID); team != nil {
|
|
|
|
|
ret.CurrentTeamName = team.Name
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-09 10:01:23 +08:00
|
|
|
if item.ClosedBy > 0 {
|
|
|
|
|
if user := services.UserService.Get(item.ClosedBy); user != nil {
|
|
|
|
|
ret.ClosedByName = user.Nickname
|
|
|
|
|
if ret.ClosedByName == "" {
|
|
|
|
|
ret.ClosedByName = user.Username
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ret
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 12:06:15 +08:00
|
|
|
func localizeConversationSummary(locale string, summary string) string {
|
|
|
|
|
if i18nx.NormalizeLocale(locale) != i18nx.LocaleEnUS {
|
|
|
|
|
return summary
|
|
|
|
|
}
|
|
|
|
|
switch {
|
|
|
|
|
case summary == "[图片]":
|
|
|
|
|
return "[Image]"
|
|
|
|
|
case strings.HasPrefix(summary, "[图片] "):
|
|
|
|
|
return "[Image] " + strings.TrimPrefix(summary, "[图片] ")
|
|
|
|
|
case summary == "[附件]":
|
|
|
|
|
return "[Attachment]"
|
|
|
|
|
case strings.HasPrefix(summary, "[附件] "):
|
|
|
|
|
return "[Attachment] " + strings.TrimPrefix(summary, "[附件] ")
|
|
|
|
|
case summary == "该消息已撤回":
|
|
|
|
|
return "This message was recalled."
|
|
|
|
|
default:
|
|
|
|
|
return summary
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 10:01:23 +08:00
|
|
|
func BuildParticipantResponses(conversationID int64) []response.ConversationParticipantResponse {
|
|
|
|
|
list := services.ConversationParticipantService.Find(sqls.NewCnd().Eq("conversation_id", conversationID).Asc("id"))
|
|
|
|
|
if len(list) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
ret := make([]response.ConversationParticipantResponse, 0, len(list))
|
|
|
|
|
for _, item := range list {
|
|
|
|
|
ret = append(ret, response.ConversationParticipantResponse{
|
|
|
|
|
ID: item.ID,
|
|
|
|
|
ParticipantType: item.ParticipantType,
|
|
|
|
|
ParticipantID: item.ParticipantID,
|
|
|
|
|
ExternalParticipantID: item.ExternalParticipantID,
|
|
|
|
|
JoinedAt: utils.FormatTimePtr(item.JoinedAt),
|
|
|
|
|
LeftAt: utils.FormatTimePtr(item.LeftAt),
|
|
|
|
|
Status: item.Status,
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
return ret
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func BuildMessages(list []models.Message) []response.MessageResponse {
|
2026-05-31 20:06:44 +08:00
|
|
|
return BuildMessagesWithLocale(list, i18nx.DefaultLocale)
|
2026-05-25 12:06:15 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func BuildMessagesWithLocale(list []models.Message, locale string) []response.MessageResponse {
|
2026-04-09 10:01:23 +08:00
|
|
|
if len(list) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
agentReadState, customerReadState := services.ConversationReadStateService.GetConversationReadStates(list[0].ConversationID)
|
2026-08-28 22:23:13 +08:00
|
|
|
aiSenders, userSenderNames := collectMessageSenderMaps(list)
|
2026-04-09 10:01:23 +08:00
|
|
|
agentProfiles := collectAgentProfilesByMessages(list)
|
|
|
|
|
ret := make([]response.MessageResponse, 0, len(list))
|
|
|
|
|
for i := range list {
|
2026-08-28 22:23:13 +08:00
|
|
|
ret = append(ret, BuildMessageWithReadStatesAndLocale(&list[i], agentReadState, customerReadState, aiSenders, userSenderNames, agentProfiles, locale))
|
2026-04-09 10:01:23 +08:00
|
|
|
}
|
|
|
|
|
return ret
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func BuildMessage(item *models.Message) response.MessageResponse {
|
2026-05-31 20:06:44 +08:00
|
|
|
return BuildMessageWithLocale(item, i18nx.DefaultLocale)
|
2026-05-25 12:06:15 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func BuildMessageWithLocale(item *models.Message, locale string) response.MessageResponse {
|
2026-04-09 10:01:23 +08:00
|
|
|
agentReadState, customerReadState := services.ConversationReadStateService.GetConversationReadStates(item.ConversationID)
|
2026-05-25 12:06:15 +08:00
|
|
|
return BuildMessageWithReadStatesAndLocale(item, agentReadState, customerReadState, nil, nil, nil, locale)
|
2026-04-09 10:01:23 +08:00
|
|
|
}
|
|
|
|
|
|
2026-08-28 22:23:13 +08:00
|
|
|
func BuildMessageWithReadStates(item *models.Message, agentReadState, customerReadState *models.ConversationReadState, aiSenders map[int64]*models.AIAgent, userSenderNames map[int64]string, agentProfiles map[int64]*models.AgentProfile) response.MessageResponse {
|
|
|
|
|
return BuildMessageWithReadStatesAndLocale(item, agentReadState, customerReadState, aiSenders, userSenderNames, agentProfiles, i18nx.DefaultLocale)
|
2026-05-25 12:06:15 +08:00
|
|
|
}
|
|
|
|
|
|
2026-08-28 22:23:13 +08:00
|
|
|
func BuildMessageWithReadStatesAndLocale(item *models.Message, agentReadState, customerReadState *models.ConversationReadState, aiSenders map[int64]*models.AIAgent, userSenderNames map[int64]string, agentProfiles map[int64]*models.AgentProfile, locale string) response.MessageResponse {
|
2026-04-17 12:40:47 +08:00
|
|
|
content, payload := utils.BuildRenderableMessage(item)
|
2026-04-09 10:01:23 +08:00
|
|
|
ret := response.MessageResponse{
|
|
|
|
|
ID: item.ID,
|
|
|
|
|
ConversationID: item.ConversationID,
|
2026-05-27 22:18:43 +08:00
|
|
|
RequestID: item.RequestID,
|
2026-04-09 10:01:23 +08:00
|
|
|
ClientMsgID: item.ClientMsgID,
|
|
|
|
|
SenderType: item.SenderType,
|
|
|
|
|
SenderID: item.SenderID,
|
|
|
|
|
MessageType: item.MessageType,
|
2026-05-25 12:06:15 +08:00
|
|
|
Content: localizeRenderableMessageContent(locale, content),
|
2026-04-09 10:01:23 +08:00
|
|
|
Payload: payload,
|
|
|
|
|
SendStatus: item.SendStatus,
|
|
|
|
|
SentAt: utils.FormatTimePtr(item.SentAt),
|
|
|
|
|
DeliveredAt: utils.FormatTimePtr(item.DeliveredAt),
|
|
|
|
|
ReadAt: utils.FormatTimePtr(item.ReadAt),
|
|
|
|
|
CustomerRead: isMessageRead(item, customerReadState),
|
|
|
|
|
CustomerReadAt: readMessageAt(item, customerReadState),
|
|
|
|
|
AgentRead: isMessageRead(item, agentReadState),
|
|
|
|
|
AgentReadAt: readMessageAt(item, agentReadState),
|
|
|
|
|
RecalledAt: utils.FormatTimePtr(item.RecalledAt),
|
|
|
|
|
QuotedMessageID: item.QuotedMessageID,
|
|
|
|
|
}
|
|
|
|
|
if item.SenderID > 0 {
|
|
|
|
|
if item.SenderType == enums.IMSenderTypeAI {
|
2026-08-28 22:23:13 +08:00
|
|
|
aiAgent := aiSenders[item.SenderID]
|
|
|
|
|
if aiAgent == nil {
|
|
|
|
|
aiAgent = services.AIAgentService.Get(item.SenderID)
|
|
|
|
|
}
|
|
|
|
|
if aiAgent != nil {
|
2026-04-09 10:01:23 +08:00
|
|
|
ret.SenderName = aiAgent.Name
|
2026-08-28 22:23:13 +08:00
|
|
|
ret.SenderAvatar = strings.TrimSpace(aiAgent.Avatar)
|
2026-04-09 10:01:23 +08:00
|
|
|
}
|
|
|
|
|
} else if item.SenderType == enums.IMSenderTypeAgent {
|
|
|
|
|
profile := agentProfiles[item.SenderID]
|
|
|
|
|
if profile != nil {
|
|
|
|
|
if dn := strings.TrimSpace(profile.DisplayName); dn != "" {
|
|
|
|
|
ret.SenderName = dn
|
|
|
|
|
}
|
|
|
|
|
if av := strings.TrimSpace(profile.Avatar); av != "" {
|
|
|
|
|
ret.SenderAvatar = av
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if ret.SenderName == "" {
|
|
|
|
|
if userSenderNames != nil {
|
|
|
|
|
ret.SenderName = userSenderNames[item.SenderID]
|
|
|
|
|
} else if user := services.UserService.Get(item.SenderID); user != nil {
|
|
|
|
|
ret.SenderName = user.Nickname
|
|
|
|
|
if ret.SenderName == "" {
|
|
|
|
|
ret.SenderName = user.Username
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if userSenderNames != nil {
|
|
|
|
|
ret.SenderName = userSenderNames[item.SenderID]
|
|
|
|
|
} else if user := services.UserService.Get(item.SenderID); user != nil {
|
|
|
|
|
ret.SenderName = user.Nickname
|
|
|
|
|
if ret.SenderName == "" {
|
|
|
|
|
ret.SenderName = user.Username
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ret
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-25 12:06:15 +08:00
|
|
|
func localizeRenderableMessageContent(locale string, content string) string {
|
|
|
|
|
if i18nx.NormalizeLocale(locale) != i18nx.LocaleEnUS {
|
|
|
|
|
return content
|
|
|
|
|
}
|
|
|
|
|
if content == "该消息已撤回" {
|
|
|
|
|
return "This message was recalled."
|
|
|
|
|
}
|
|
|
|
|
return content
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-09 10:01:23 +08:00
|
|
|
func collectAgentProfilesByMessages(list []models.Message) map[int64]*models.AgentProfile {
|
|
|
|
|
var agentUserIDs []int64
|
|
|
|
|
seen := make(map[int64]struct{})
|
|
|
|
|
for i := range list {
|
|
|
|
|
m := &list[i]
|
|
|
|
|
if m.SenderType != enums.IMSenderTypeAgent || m.SenderID <= 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if _, ok := seen[m.SenderID]; ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seen[m.SenderID] = struct{}{}
|
|
|
|
|
agentUserIDs = append(agentUserIDs, m.SenderID)
|
|
|
|
|
}
|
|
|
|
|
if len(agentUserIDs) == 0 {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
profiles := services.AgentProfileService.Find(sqls.NewCnd().In("user_id", agentUserIDs))
|
|
|
|
|
out := make(map[int64]*models.AgentProfile, len(profiles))
|
|
|
|
|
for i := range profiles {
|
|
|
|
|
out[profiles[i].UserID] = &profiles[i]
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-28 22:23:13 +08:00
|
|
|
func collectMessageSenderMaps(list []models.Message) (aiSenders map[int64]*models.AIAgent, userNames map[int64]string) {
|
|
|
|
|
aiSenders = make(map[int64]*models.AIAgent)
|
2026-04-09 10:01:23 +08:00
|
|
|
userNames = make(map[int64]string)
|
|
|
|
|
var aiIDs, userIDs []int64
|
|
|
|
|
seenAI := make(map[int64]struct{})
|
|
|
|
|
seenUser := make(map[int64]struct{})
|
|
|
|
|
for i := range list {
|
|
|
|
|
m := &list[i]
|
|
|
|
|
if m.SenderID <= 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if m.SenderType == enums.IMSenderTypeAI {
|
|
|
|
|
if _, ok := seenAI[m.SenderID]; ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seenAI[m.SenderID] = struct{}{}
|
|
|
|
|
aiIDs = append(aiIDs, m.SenderID)
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if _, ok := seenUser[m.SenderID]; ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seenUser[m.SenderID] = struct{}{}
|
|
|
|
|
userIDs = append(userIDs, m.SenderID)
|
|
|
|
|
}
|
|
|
|
|
for _, a := range services.AIAgentService.FindByIds(aiIDs) {
|
2026-08-28 22:23:13 +08:00
|
|
|
agent := a
|
|
|
|
|
aiSenders[a.ID] = &agent
|
2026-04-09 10:01:23 +08:00
|
|
|
}
|
|
|
|
|
for _, u := range services.UserService.FindByIds(userIDs) {
|
|
|
|
|
name := u.Nickname
|
|
|
|
|
if name == "" {
|
|
|
|
|
name = u.Username
|
|
|
|
|
}
|
|
|
|
|
userNames[u.ID] = name
|
|
|
|
|
}
|
2026-08-28 22:23:13 +08:00
|
|
|
return aiSenders, userNames
|
2026-04-09 10:01:23 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func isMessageRead(item *models.Message, state *models.ConversationReadState) bool {
|
2026-06-24 22:53:17 +08:00
|
|
|
return item != nil && state != nil && state.LastReadMessageID >= item.ID
|
2026-04-09 10:01:23 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func readMessageAt(item *models.Message, state *models.ConversationReadState) string {
|
|
|
|
|
if !isMessageRead(item, state) {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return utils.FormatTimePtr(state.LastReadAt)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func readStateMessageID(state *models.ConversationReadState) int64 {
|
|
|
|
|
if state == nil {
|
|
|
|
|
return 0
|
|
|
|
|
}
|
|
|
|
|
return state.LastReadMessageID
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func readStateAt(state *models.ConversationReadState) string {
|
|
|
|
|
if state == nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
return utils.FormatTimePtr(state.LastReadAt)
|
|
|
|
|
}
|