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)
|
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.
|
// extractCardImageKeys recursively extracts all image keys from a Feishu interactive card.
|
||||||
// Image keys are used to download images from Feishu API.
|
// Image keys are used to download images from Feishu API.
|
||||||
func extractCardImageKeys(rawContent string) []string {
|
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) {
|
func TestExtractCardImageKeys(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|
|
||||||
|
|
@ -508,8 +508,8 @@ func extractContent(messageType, rawContent string) string {
|
||||||
return rawContent
|
return rawContent
|
||||||
|
|
||||||
case larkim.MsgTypeInteractive:
|
case larkim.MsgTypeInteractive:
|
||||||
// Extract text content from interactive card messages
|
// Pass raw JSON to LLM — structured card is more informative than flattened text
|
||||||
return extractCardText(rawContent)
|
return rawContent
|
||||||
|
|
||||||
case larkim.MsgTypeImage:
|
case larkim.MsgTypeImage:
|
||||||
// Image messages don't have text content
|
// Image messages don't have text content
|
||||||
|
|
|
||||||
|
|
@ -76,22 +76,22 @@ func TestExtractContent(t *testing.T) {
|
||||||
want: "",
|
want: "",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "interactive card with markdown content",
|
name: "interactive card returns raw JSON",
|
||||||
messageType: "interactive",
|
messageType: "interactive",
|
||||||
rawContent: `{"schema":"2.0","body":{"elements":[{"tag":"markdown","content":"Hello from card"}]}}`,
|
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",
|
messageType: "interactive",
|
||||||
rawContent: `{"header":{"title":{"tag":"plain_text","content":"Title"}},"elements":[{"tag":"div","text":{"tag":"lark_md","content":"Card content"}}]}`,
|
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",
|
messageType: "interactive",
|
||||||
rawContent: `not valid json`,
|
rawContent: `not valid json`,
|
||||||
want: "",
|
want: `not valid json`,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue