diff --git a/internal/im/dingtalk/adapter.go b/internal/im/dingtalk/adapter.go index 28e6e34e..e975aae5 100644 --- a/internal/im/dingtalk/adapter.go +++ b/internal/im/dingtalk/adapter.go @@ -176,23 +176,14 @@ func parseCallbackMessage(msg *callbackMessage) *im.IncomingMessage { userID = msg.SenderID } - content := "" + text := "" if msg.Text != nil { - content = strings.TrimSpace(msg.Text.Content) + text = msg.Text.Content } - messageType := im.MessageTypeText - fileKey := "" + messageType, content, fileKey := parseDingTalkStreamPayload(msg.Msgtype, msg.Content, text) fileName := "" - if strings.EqualFold(msg.Msgtype, "picture") { - messageType = im.MessageTypeImage + if messageType == im.MessageTypeImage { fileName = msg.MsgID + ".png" - content = "" - var picture struct { - DownloadCode string `json:"downloadCode"` - } - if json.Unmarshal(msg.Content, &picture) == nil { - fileKey = strings.TrimSpace(picture.DownloadCode) - } } return &im.IncomingMessage{ diff --git a/internal/im/dingtalk/adapter_test.go b/internal/im/dingtalk/adapter_test.go index 0ecda467..8fde313c 100644 --- a/internal/im/dingtalk/adapter_test.go +++ b/internal/im/dingtalk/adapter_test.go @@ -41,6 +41,28 @@ func TestParseCallbackMessagePicture(t *testing.T) { } } +func TestParseCallbackMessageRichTextWithPictureAndText(t *testing.T) { + msg := &callbackMessage{ + MsgID: "msg-richtext-1", + Msgtype: "richText", + Content: []byte(`{"richText":[{"text":" 请描述这张图片 "},{"type":"picture","downloadCode":"rich-webhook-code"}]}`), + } + + got := parseCallbackMessage(msg) + if got.MessageType != im.MessageTypeImage { + t.Fatalf("MessageType = %q, want image", got.MessageType) + } + if got.Content != "请描述这张图片" { + t.Fatalf("Content = %q", got.Content) + } + if got.FileKey != "rich-webhook-code" { + t.Fatalf("FileKey = %q", got.FileKey) + } + if got.FileName != "msg-richtext-1.png" { + t.Fatalf("FileName = %q", got.FileName) + } +} + func TestAdapterDownloadFileUsesMessageFilesAPI(t *testing.T) { var sawToken bool server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { diff --git a/internal/im/dingtalk/longconn.go b/internal/im/dingtalk/longconn.go index c6e5203b..aac0abef 100644 --- a/internal/im/dingtalk/longconn.go +++ b/internal/im/dingtalk/longconn.go @@ -102,9 +102,11 @@ func (c *LongConnClient) onChatBotMessage(ctx context.Context, data *chatbot.Bot } func parseDingTalkStreamPayload(msgType string, rawContent interface{}, text string) (im.MessageType, string, string) { - if !strings.EqualFold(strings.TrimSpace(msgType), "picture") { + normalizedType := strings.ToLower(strings.TrimSpace(msgType)) + if normalizedType != "picture" && normalizedType != "richtext" { return im.MessageTypeText, strings.TrimSpace(text), "" } + var payload []byte switch value := rawContent.(type) { case string: @@ -120,6 +122,41 @@ func parseDingTalkStreamPayload(msgType string, rawContent interface{}, text str return im.MessageTypeImage, "", "" } } + + if normalizedType == "richtext" { + var rich struct { + Items []struct { + Text string `json:"text"` + Type string `json:"type"` + DownloadCode string `json:"downloadCode"` + } `json:"richText"` + } + if json.Unmarshal(payload, &rich) != nil { + return im.MessageTypeText, strings.TrimSpace(text), "" + } + + var textParts []string + fileKey := "" + hasPicture := false + for _, item := range rich.Items { + if itemText := strings.TrimSpace(item.Text); itemText != "" { + textParts = append(textParts, itemText) + } + if strings.EqualFold(strings.TrimSpace(item.Type), "picture") { + hasPicture = true + if fileKey == "" { + fileKey = strings.TrimSpace(item.DownloadCode) + } + } + } + + content := strings.Join(textParts, "\n") + if hasPicture { + return im.MessageTypeImage, content, fileKey + } + return im.MessageTypeText, content, "" + } + var picture struct { DownloadCode string `json:"downloadCode"` } diff --git a/internal/im/dingtalk/longconn_test.go b/internal/im/dingtalk/longconn_test.go index 5579c0e6..dec5054f 100644 --- a/internal/im/dingtalk/longconn_test.go +++ b/internal/im/dingtalk/longconn_test.go @@ -35,3 +35,58 @@ func TestParseDingTalkStreamPayloadPictureJSONString(t *testing.T) { t.Fatalf("got type=%q fileKey=%q", messageType, fileKey) } } + +func TestParseDingTalkStreamPayloadRichTextWithPictureAndText(t *testing.T) { + messageType, content, fileKey := parseDingTalkStreamPayload("richText", map[string]interface{}{ + "richText": []interface{}{ + map[string]interface{}{"text": " 请回答图片中的问题 "}, + map[string]interface{}{"type": "picture", "downloadCode": "rich-stream-code"}, + }, + }, "") + + if messageType != im.MessageTypeImage { + t.Fatalf("MessageType = %q, want image", messageType) + } + if content != "请回答图片中的问题" { + t.Fatalf("content = %q", content) + } + if fileKey != "rich-stream-code" { + t.Fatalf("fileKey = %q", fileKey) + } +} + +func TestParseDingTalkStreamPayloadRichTextJoinsTextParts(t *testing.T) { + messageType, content, fileKey := parseDingTalkStreamPayload("richText", map[string]interface{}{ + "richText": []interface{}{ + map[string]interface{}{"text": "第一段"}, + map[string]interface{}{"type": "picture", "downloadCode": "rich-code"}, + map[string]interface{}{"text": "第二段"}, + }, + }, "") + if messageType != im.MessageTypeImage || content != "第一段\n第二段" || fileKey != "rich-code" { + t.Fatalf("got type=%q content=%q fileKey=%q", messageType, content, fileKey) + } +} + +func TestParseDingTalkStreamPayloadRichTextWithoutPictureStaysText(t *testing.T) { + messageType, content, fileKey := parseDingTalkStreamPayload("richText", map[string]interface{}{ + "richText": []interface{}{ + map[string]interface{}{"text": " 只有文字 "}, + }, + }, "") + if messageType != im.MessageTypeText || content != "只有文字" || fileKey != "" { + t.Fatalf("got type=%q content=%q fileKey=%q", messageType, content, fileKey) + } +} + +func TestParseDingTalkStreamPayloadRichTextPictureWithoutDownloadCodeStaysImage(t *testing.T) { + messageType, content, fileKey := parseDingTalkStreamPayload("richText", map[string]interface{}{ + "richText": []interface{}{ + map[string]interface{}{"text": "识别图片"}, + map[string]interface{}{"type": "picture"}, + }, + }, "") + if messageType != im.MessageTypeImage || content != "识别图片" || fileKey != "" { + t.Fatalf("got type=%q content=%q fileKey=%q", messageType, content, fileKey) + } +}