diff --git a/internal/builders/conversation_builder.go b/internal/builders/conversation_builder.go index 2fb377d..7d633cb 100644 --- a/internal/builders/conversation_builder.go +++ b/internal/builders/conversation_builder.go @@ -101,7 +101,7 @@ func BuildMessage(item *models.Message) response.MessageResponse { } func BuildMessageWithReadStates(item *models.Message, agentReadState, customerReadState *models.ConversationReadState, aiSenderNames, userSenderNames map[int64]string, agentProfiles map[int64]*models.AgentProfile) response.MessageResponse { - content, payload := buildMessageResponseContent(item) + content, payload := services.BuildRenderableMessage(item) ret := response.MessageResponse{ ID: item.ID, ConversationID: item.ConversationID, @@ -162,16 +162,6 @@ func BuildMessageWithReadStates(item *models.Message, agentReadState, customerRe return ret } -func buildMessageResponseContent(item *models.Message) (content, payload string) { - if item == nil { - return "", "" - } - if item.RecalledAt != nil || item.SendStatus == int(enums.IMMessageStatusRecalled) { - return "该消息已撤回", "" - } - return item.Content, item.Payload -} - func collectAgentProfilesByMessages(list []models.Message) map[int64]*models.AgentProfile { var agentUserIDs []int64 seen := make(map[int64]struct{}) diff --git a/internal/models/models.go b/internal/models/models.go index e67865b..58840ca 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -377,22 +377,22 @@ type ConversationReadState struct { // Message 会话消息。 type Message struct { - ID int64 `gorm:"primaryKey;autoIncrement"` - ConversationID int64 `gorm:"type:bigint;not null;index;uniqueIndex:uk_conversation_seq;uniqueIndex:uk_conversation_client_msg"` - ClientMsgID string `gorm:"type:varchar(128);not null;default:'';uniqueIndex:uk_conversation_client_msg"` - SenderType enums.IMSenderType `gorm:"type:varchar(30);not null;default:'';index"` - SenderID int64 `gorm:"type:bigint;not null;default:0;index"` - ReceiverType string `gorm:"type:varchar(30);not null;default:'';index"` - MessageType enums.IMMessageType `gorm:"type:varchar(30);not null;default:'';index"` - Content string `gorm:"type:text"` - Payload string `gorm:"type:text"` - SeqNo int64 `gorm:"type:bigint;not null;default:0;uniqueIndex:uk_conversation_seq"` - SendStatus int `gorm:"type:int;not null;default:2;index"` - SentAt *time.Time `gorm:"type:datetime;index"` - DeliveredAt *time.Time `gorm:"type:datetime"` - ReadAt *time.Time `gorm:"type:datetime"` - RecalledAt *time.Time `gorm:"type:datetime"` - QuotedMessageID int64 `gorm:"type:bigint;not null;default:0;index"` + ID int64 `gorm:"primaryKey;autoIncrement"` + ConversationID int64 `gorm:"type:bigint;not null;index;uniqueIndex:uk_conversation_seq;uniqueIndex:uk_conversation_client_msg"` + ClientMsgID string `gorm:"type:varchar(128);not null;default:'';uniqueIndex:uk_conversation_client_msg"` + SenderType enums.IMSenderType `gorm:"type:varchar(30);not null;default:'';index"` + SenderID int64 `gorm:"type:bigint;not null;default:0;index"` + ReceiverType string `gorm:"type:varchar(30);not null;default:'';index"` + MessageType enums.IMMessageType `gorm:"type:varchar(30);not null;default:'';index"` + Content string `gorm:"type:text"` + Payload string `gorm:"type:text"` + SeqNo int64 `gorm:"type:bigint;not null;default:0;uniqueIndex:uk_conversation_seq"` + SendStatus enums.IMMessageStatus `gorm:"type:int;not null;default:2;index"` + SentAt *time.Time `gorm:"type:datetime;index"` + DeliveredAt *time.Time `gorm:"type:datetime"` + ReadAt *time.Time `gorm:"type:datetime"` + RecalledAt *time.Time `gorm:"type:datetime"` + QuotedMessageID int64 `gorm:"type:bigint;not null;default:0;index"` AuditFields } diff --git a/internal/pkg/dto/response/message_response.go b/internal/pkg/dto/response/message_response.go index 7610c78..e1b0c4c 100644 --- a/internal/pkg/dto/response/message_response.go +++ b/internal/pkg/dto/response/message_response.go @@ -3,25 +3,25 @@ package response import "cs-agent/internal/pkg/enums" type MessageResponse struct { - ID int64 `json:"id"` - ConversationID int64 `json:"conversationId"` - ClientMsgID string `json:"clientMsgId,omitempty"` - SenderType enums.IMSenderType `json:"senderType"` - SenderID int64 `json:"senderId"` - SenderName string `json:"senderName,omitempty"` - SenderAvatar string `json:"senderAvatar,omitempty"` - MessageType enums.IMMessageType `json:"messageType"` - Content string `json:"content"` - Payload string `json:"payload,omitempty"` - SeqNo int64 `json:"seqNo"` - SendStatus int `json:"sendStatus"` - SentAt string `json:"sentAt,omitempty"` - DeliveredAt string `json:"deliveredAt,omitempty"` - ReadAt string `json:"readAt,omitempty"` - CustomerRead bool `json:"customerRead"` - CustomerReadAt string `json:"customerReadAt,omitempty"` - AgentRead bool `json:"agentRead"` - AgentReadAt string `json:"agentReadAt,omitempty"` - RecalledAt string `json:"recalledAt,omitempty"` - QuotedMessageID int64 `json:"quotedMessageId,omitempty"` + ID int64 `json:"id"` + ConversationID int64 `json:"conversationId"` + ClientMsgID string `json:"clientMsgId,omitempty"` + SenderType enums.IMSenderType `json:"senderType"` + SenderID int64 `json:"senderId"` + SenderName string `json:"senderName,omitempty"` + SenderAvatar string `json:"senderAvatar,omitempty"` + MessageType enums.IMMessageType `json:"messageType"` + Content string `json:"content"` + Payload string `json:"payload,omitempty"` + SeqNo int64 `json:"seqNo"` + SendStatus enums.IMMessageStatus `json:"sendStatus"` + SentAt string `json:"sentAt,omitempty"` + DeliveredAt string `json:"deliveredAt,omitempty"` + ReadAt string `json:"readAt,omitempty"` + CustomerRead bool `json:"customerRead"` + CustomerReadAt string `json:"customerReadAt,omitempty"` + AgentRead bool `json:"agentRead"` + AgentReadAt string `json:"agentReadAt,omitempty"` + RecalledAt string `json:"recalledAt,omitempty"` + QuotedMessageID int64 `json:"quotedMessageId,omitempty"` } diff --git a/internal/services/im_message_asset.go b/internal/services/im_message_asset.go index ff5a1ec..f237513 100644 --- a/internal/services/im_message_asset.go +++ b/internal/services/im_message_asset.go @@ -10,11 +10,13 @@ import ( ) type imMessageAssetPayload struct { - AssetID string `json:"assetId"` - Filename string `json:"filename,omitempty"` - FileSize int64 `json:"fileSize,omitempty"` - MimeType string `json:"mimeType,omitempty"` - URL string `json:"url,omitempty"` + AssetID string `json:"assetId"` + Provider enums.AssetProvider `json:"provider,omitempty"` + StorageKey string `json:"storageKey,omitempty"` + Filename string `json:"filename,omitempty"` + FileSize int64 `json:"fileSize,omitempty"` + MimeType string `json:"mimeType,omitempty"` + URL string `json:"url,omitempty"` } func parseIMMessageAssetPayload(payload string) (*imMessageAssetPayload, error) { @@ -27,6 +29,8 @@ func parseIMMessageAssetPayload(payload string) (*imMessageAssetPayload, error) return nil, errorsx.InvalidParam("附件消息 payload 格式错误") } ret.AssetID = strings.TrimSpace(ret.AssetID) + ret.Provider = enums.AssetProvider(strings.TrimSpace(string(ret.Provider))) + ret.StorageKey = strings.TrimSpace(ret.StorageKey) if ret.AssetID == "" { return nil, errorsx.InvalidParam("附件消息缺少 assetId") } @@ -37,16 +41,13 @@ func buildIMMessageAssetPayload(asset *models.Asset) (string, error) { if asset == nil { return "", errorsx.InvalidParam("附件不存在") } - provider, err := storage.NewProvider(asset.Provider) - if err != nil { - return "", err - } payload, err := json.Marshal(imMessageAssetPayload{ - AssetID: asset.AssetID, - Filename: asset.Filename, - FileSize: asset.FileSize, - MimeType: asset.MimeType, - URL: provider.GetURL(asset.StorageKey), + AssetID: asset.AssetID, + Provider: asset.Provider, + StorageKey: asset.StorageKey, + Filename: asset.Filename, + FileSize: asset.FileSize, + MimeType: asset.MimeType, }) if err != nil { return "", err @@ -54,6 +55,56 @@ func buildIMMessageAssetPayload(asset *models.Asset) (string, error) { return string(payload), nil } +func buildIMMessageAssetPayloadForResponse(payload string) string { + assetPayload, err := parseIMMessageAssetPayload(payload) + if err != nil { + return strings.TrimSpace(payload) + } + assetPayload = hydrateIMMessageAssetPayload(assetPayload) + if assetPayload.Provider != "" && assetPayload.StorageKey != "" { + if provider, err := storage.NewProvider(assetPayload.Provider); err == nil { + assetPayload.URL = provider.GetSignedURL(assetPayload.StorageKey) + } + } + data, err := json.Marshal(assetPayload) + if err != nil { + return strings.TrimSpace(payload) + } + return string(data) +} + +func hydrateIMMessageAssetPayload(payload *imMessageAssetPayload) *imMessageAssetPayload { + if payload == nil { + return nil + } + if payload.Provider != "" && payload.StorageKey != "" { + return payload + } + if payload.AssetID == "" { + return payload + } + asset := AssetService.GetByAssetID(payload.AssetID) + if asset == nil { + return payload + } + if payload.Provider == "" { + payload.Provider = asset.Provider + } + if payload.StorageKey == "" { + payload.StorageKey = strings.TrimSpace(asset.StorageKey) + } + if payload.Filename == "" { + payload.Filename = strings.TrimSpace(asset.Filename) + } + if payload.FileSize <= 0 { + payload.FileSize = asset.FileSize + } + if payload.MimeType == "" { + payload.MimeType = strings.TrimSpace(asset.MimeType) + } + return payload +} + func validateConversationAsset(asset *models.Asset, conversationID int64, messageType enums.IMMessageType) error { if asset == nil { return errorsx.InvalidParam("附件不存在") diff --git a/internal/services/message_render_test.go b/internal/services/message_render_test.go new file mode 100644 index 0000000..b1ebfe5 --- /dev/null +++ b/internal/services/message_render_test.go @@ -0,0 +1,96 @@ +package services + +import ( + "cs-agent/internal/models" + "cs-agent/internal/pkg/config" + "cs-agent/internal/pkg/enums" + "strings" + "testing" +) + +func TestBuildIMMessageAssetPayloadForResponseAddsSignedURL(t *testing.T) { + config.SetCurrent(&config.Config{ + Storage: config.StorageConfig{ + Default: enums.AssetProviderLocal, + Local: config.LocalStorageConfig{ + BaseURL: "https://files.example.com", + }, + }, + }) + + payload := `{"assetId":"asset_1","provider":"local","storageKey":"attachments/demo.png","filename":"demo.png"}` + got := buildIMMessageAssetPayloadForResponse(payload) + + if !strings.Contains(got, `"provider":"local"`) { + t.Fatalf("expected provider in payload, got: %s", got) + } + if !strings.Contains(got, `"storageKey":"attachments/demo.png"`) { + t.Fatalf("expected storageKey in payload, got: %s", got) + } + if !strings.Contains(got, `"url":"https://files.example.com/attachments/demo.png"`) { + t.Fatalf("expected signed url in payload, got: %s", got) + } +} + +func TestSanitizeMessageHTMLStripsStoredSrcForManagedImages(t *testing.T) { + html := `
