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
This commit is contained in:
parent
622e11b929
commit
2e9cdf83b5
4 changed files with 8 additions and 212 deletions
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
},
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue