From 2e9cdf83b5b91102a4dfbd7b02f47162c13df075 Mon Sep 17 00:00:00 2001 From: ywj <138745068+yangwenjie1231@users.noreply.github.com> Date: Sat, 14 Mar 2026 16:51:59 +0800 Subject: [PATCH] fix(feishu): simplify card parsing - pass raw JSON, only extract images Address review feedback: text extraction cannot exhaustively handle all card formats (i18n_elements, div.fields, etc.). Pass raw JSON to LLM instead - same approach as MsgTypePost. Only image extraction remains as images must be downloaded for LLM to process. - Remove extractCardText() and helper functions - extractContent() now returns raw JSON for MsgTypeInteractive - Keep extractCardImageKeys() for downloading embedded images - Update tests to expect raw JSON for interactive cards --- pkg/channels/feishu/common.go | 94 ---------------------- pkg/channels/feishu/common_test.go | 110 -------------------------- pkg/channels/feishu/feishu_64.go | 4 +- pkg/channels/feishu/feishu_64_test.go | 12 +-- 4 files changed, 8 insertions(+), 212 deletions(-) diff --git a/pkg/channels/feishu/common.go b/pkg/channels/feishu/common.go index b1dc17a44..1bce0e74e 100644 --- a/pkg/channels/feishu/common.go +++ b/pkg/channels/feishu/common.go @@ -85,100 +85,6 @@ func stripMentionPlaceholders(content string, mentions []*larkim.MentionEvent) s return strings.TrimSpace(content) } -// extractCardText recursively extracts all text content from a Feishu interactive card. -// It handles both JSON 1.0 (legacy) and JSON 2.0 schema formats. -func extractCardText(rawContent string) string { - if rawContent == "" { - return "" - } - - var card map[string]any - if err := json.Unmarshal([]byte(rawContent), &card); err != nil { - return "" - } - - var texts []string - - // Extract header title - if header, ok := card["header"].(map[string]any); ok { - if title := extractTextFromElement(header["title"]); title != "" { - texts = append(texts, title) - } - } - - // JSON 2.0 schema: body.elements - if body, ok := card["body"].(map[string]any); ok { - if elements, ok := body["elements"].([]any); ok { - for _, elem := range elements { - extractTextFromElementsRecursive(elem, &texts) - } - } - } - - // JSON 1.0 schema: elements (legacy format) - if elements, ok := card["elements"].([]any); ok { - for _, elem := range elements { - extractTextFromElementsRecursive(elem, &texts) - } - } - - if len(texts) == 0 { - return "" - } - return strings.Join(texts, "\n") -} - -// extractTextFromElementsRecursive recursively traverses card elements to extract text. -func extractTextFromElementsRecursive(v any, texts *[]string) { - switch val := v.(type) { - case map[string]any: - // Check for text content in common fields - if text := extractTextFromElement(val); text != "" { - *texts = append(*texts, text) - } - // Recurse into nested structures - for key, child := range val { - switch key { - case "elements", "actions", "columns", "extra": - extractTextFromElementsRecursive(child, texts) - } - } - case []any: - for _, item := range val { - extractTextFromElementsRecursive(item, texts) - } - } -} - -// extractTextFromElement extracts text from a single card element. -func extractTextFromElement(elem any) string { - m, ok := elem.(map[string]any) - if !ok { - return "" - } - - // Direct content field (markdown element in JSON 2.0) - if content, ok := m["content"].(string); ok && content != "" { - return content - } - - // Text object with tag and content (lark_md, plain_text) - if text, ok := m["text"].(map[string]any); ok { - if content, ok := text["content"].(string); ok && content != "" { - return content - } - } - - // Title object with tag and content - if title, ok := m["title"].(map[string]any); ok { - if content, ok := title["content"].(string); ok && content != "" { - return content - } - } - - return "" -} - // extractCardImageKeys recursively extracts all image keys from a Feishu interactive card. // Image keys are used to download images from Feishu API. func extractCardImageKeys(rawContent string) []string { diff --git a/pkg/channels/feishu/common_test.go b/pkg/channels/feishu/common_test.go index 39e718d82..c8b77f655 100644 --- a/pkg/channels/feishu/common_test.go +++ b/pkg/channels/feishu/common_test.go @@ -291,116 +291,6 @@ func TestStripMentionPlaceholders(t *testing.T) { } } -func TestExtractCardText(t *testing.T) { - tests := []struct { - name string - content string - want string - }{ - { - name: "empty content", - content: "", - want: "", - }, - { - name: "invalid JSON", - content: "not json", - want: "", - }, - { - name: "JSON 2.0 schema with markdown element", - content: `{ - "schema": "2.0", - "body": { - "elements": [ - {"tag": "markdown", "content": "Hello **world**"} - ] - } - }`, - want: "Hello **world**", - }, - { - name: "JSON 2.0 schema with multiple elements", - content: `{ - "schema": "2.0", - "header": { - "title": {"tag": "plain_text", "content": "Card Title"} - }, - "body": { - "elements": [ - {"tag": "markdown", "content": "First paragraph"}, - {"tag": "markdown", "content": "Second paragraph"} - ] - } - }`, - want: "Card Title\nFirst paragraph\nSecond paragraph", - }, - { - name: "JSON 1.0 legacy format with div and lark_md", - content: `{ - "elements": [ - { - "tag": "div", - "text": {"tag": "lark_md", "content": "Content with **bold**"} - } - ] - }`, - want: "Content with **bold**", - }, - { - name: "nested elements in columns", - content: `{ - "elements": [ - { - "tag": "div", - "columns": [ - {"text": {"tag": "plain_text", "content": "Column 1"}}, - {"text": {"tag": "plain_text", "content": "Column 2"}} - ] - } - ] - }`, - want: "Column 1\nColumn 2", - }, - { - name: "action with buttons", - content: `{ - "elements": [ - { - "tag": "action", - "actions": [ - {"tag": "button", "text": {"tag": "plain_text", "content": "OK"}}, - {"tag": "button", "text": {"tag": "plain_text", "content": "Cancel"}} - ] - } - ] - }`, - want: "OK\nCancel", - }, - { - name: "card with no text content", - content: `{ - "schema": "2.0", - "body": { - "elements": [ - {"tag": "hr"} - ] - } - }`, - want: "", - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - got := extractCardText(tt.content) - if got != tt.want { - t.Errorf("extractCardText() = %q, want %q", got, tt.want) - } - }) - } -} - func TestExtractCardImageKeys(t *testing.T) { tests := []struct { name string diff --git a/pkg/channels/feishu/feishu_64.go b/pkg/channels/feishu/feishu_64.go index 54bfa82f9..632a662e0 100644 --- a/pkg/channels/feishu/feishu_64.go +++ b/pkg/channels/feishu/feishu_64.go @@ -508,8 +508,8 @@ func extractContent(messageType, rawContent string) string { return rawContent case larkim.MsgTypeInteractive: - // Extract text content from interactive card messages - return extractCardText(rawContent) + // Pass raw JSON to LLM — structured card is more informative than flattened text + return rawContent case larkim.MsgTypeImage: // Image messages don't have text content diff --git a/pkg/channels/feishu/feishu_64_test.go b/pkg/channels/feishu/feishu_64_test.go index d769fa231..1fadc774f 100644 --- a/pkg/channels/feishu/feishu_64_test.go +++ b/pkg/channels/feishu/feishu_64_test.go @@ -76,22 +76,22 @@ func TestExtractContent(t *testing.T) { want: "", }, { - name: "interactive card with markdown content", + name: "interactive card returns raw JSON", messageType: "interactive", rawContent: `{"schema":"2.0","body":{"elements":[{"tag":"markdown","content":"Hello from card"}]}}`, - want: "Hello from card", + want: `{"schema":"2.0","body":{"elements":[{"tag":"markdown","content":"Hello from card"}]}}`, }, { - name: "interactive card with header and body", + name: "interactive card with complex structure returns raw JSON", messageType: "interactive", rawContent: `{"header":{"title":{"tag":"plain_text","content":"Title"}},"elements":[{"tag":"div","text":{"tag":"lark_md","content":"Card content"}}]}`, - want: "Title\nCard content", + want: `{"header":{"title":{"tag":"plain_text","content":"Title"}},"elements":[{"tag":"div","text":{"tag":"lark_md","content":"Card content"}}]}`, }, { - name: "interactive card invalid JSON returns empty", + name: "interactive card invalid JSON returns as-is", messageType: "interactive", rawContent: `not valid json`, - want: "", + want: `not valid json`, }, }