Skip to content

fix(feishu): extract content from interactive card variants when quoting#38776

Open
lishuaigit wants to merge 2 commits intoopenclaw:mainfrom
lishuaigit:fix/feishu-interactive-card-quote
Open

fix(feishu): extract content from interactive card variants when quoting#38776
lishuaigit wants to merge 2 commits intoopenclaw:mainfrom
lishuaigit:fix/feishu-interactive-card-quote

Conversation

@lishuaigit
Copy link
Copy Markdown
Contributor

Summary

Fix quoted interactive card messages showing only [Interactive Card] instead of actual content.

Closes #32712

Problem

When a user quotes a Feishu interactive card and @mentions the bot, the bot only receives:
[Replying to: "[Interactive Card]"]

This is because parseInteractiveCardContent only handled flat v1 cards with top-level elements. Feishu cards come in many shapes (with headers, i18n, templates, column layouts, body wrappers) and those all fell through to the fallback.

Changes

extensions/feishu/src/send.ts:

  • Extract header title when present
  • Support body.elements (card kit v2 wrapper)
  • Support i18n_elements with locale fallback (zh_cn preferred)
  • Support template cards (template_variable extraction)
  • Recurse into column_set columns and nested elements (form, collapsible)
  • Extract helper extractTextsFromElements for recursive traversal

extensions/feishu/src/send.test.ts:

  • Add test for card with header
  • Add test for body.elements (card kit v2) path
  • Add test for i18n_elements
  • Add test for template cards
  • Add test for column_set nested elements

Testing

All 346 feishu extension tests pass (345 existing + 1 new for body.elements path, plus 4 new card variant tests).

@openclaw-barnacle openclaw-barnacle bot added channel: feishu Channel integration: feishu size: M labels Mar 7, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 7, 2026

Greptile Summary

This PR fixes quoted Feishu interactive card messages by expanding parseInteractiveCardContent to handle the five major card shapes the Feishu API can return, and extracts a reusable extractTextsFromElements helper with proper recursion into column_set and generic nested containers.

Changes:

  • extractTextsFromElements is extracted and now recurses into column_set columns and generic nested elements (forms, collapsibles)
  • parseInteractiveCardContent resolves elements from card.elements, body.elements (card kit v2), and i18n_elements in priority order, and falls back to template_variable string values for template cards
  • Header title is prepended to the extracted text when present
  • Five new test cases cover all new card variants

Potential concerns:

  • Using ?? to select a locale from i18n_elements means an empty zh_cn: [] array silences other locales that may have content (flagged inline)
  • Template variable extraction only handles string values; numeric/boolean variables (e.g. error codes) are silently dropped (flagged inline)
  • Template cards that also have a header will only surface the header title — template variables are conditional on texts.length === 0, so they are never tried if a header is present. This may be intentional but is undocumented in the code.

Confidence Score: 4/5

  • Safe to merge — the change only affects quoted-card text extraction and falls back gracefully to "[Interactive Card]" on any unrecognized shape.
  • The implementation is well-structured, all five new card variants are tested, and the logic correctly falls back to "[Interactive Card]" for unrecognised inputs. Two edge cases lower the score slightly: an empty zh_cn locale array silencing other locales, and non-string template variables being silently dropped. Neither causes a crash, but both can produce a less helpful fallback string than the fix intends.
  • extensions/feishu/src/send.ts — specifically the i18n_elements locale selection logic (lines 174–178) and the template variable loop (lines 187–193).

Last reviewed commit: 664acbc

Comment on lines +174 to +178
if (card.i18n_elements && typeof card.i18n_elements === "object") {
elements =
card.i18n_elements.zh_cn ??
(Object.values(card.i18n_elements).find((v) => Array.isArray(v)) as unknown[] | undefined);
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Empty zh_cn array silently skips other locales

?? only short-circuits on null/undefined, so if card.i18n_elements.zh_cn is an empty array [] (a valid, non-nullish value), elements is set to []. extractTextsFromElements([]) returns nothing, and any content present in other locales (e.g. en_us) is silently dropped, causing the function to fall through to "[Interactive Card]".

Consider guarding against an empty array as well:

Suggested change
if (card.i18n_elements && typeof card.i18n_elements === "object") {
elements =
card.i18n_elements.zh_cn ??
(Object.values(card.i18n_elements).find((v) => Array.isArray(v)) as unknown[] | undefined);
}
elements =
(Array.isArray(card.i18n_elements.zh_cn) && card.i18n_elements.zh_cn.length > 0
? card.i18n_elements.zh_cn
: undefined) ??
(Object.values(card.i18n_elements).find((v) => Array.isArray(v) && v.length > 0) as unknown[] | undefined);
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/feishu/src/send.ts
Line: 174-178

Comment:
**Empty `zh_cn` array silently skips other locales**

`??` only short-circuits on `null`/`undefined`, so if `card.i18n_elements.zh_cn` is an empty array `[]` (a valid, non-nullish value), `elements` is set to `[]`. `extractTextsFromElements([])` returns nothing, and any content present in other locales (e.g. `en_us`) is silently dropped, causing the function to fall through to `"[Interactive Card]"`.

Consider guarding against an empty array as well:

```suggestion
      elements =
        (Array.isArray(card.i18n_elements.zh_cn) && card.i18n_elements.zh_cn.length > 0
          ? card.i18n_elements.zh_cn
          : undefined) ??
        (Object.values(card.i18n_elements).find((v) => Array.isArray(v) && v.length > 0) as unknown[] | undefined);
```

How can I resolve this? If you propose a fix, please make it concise.

Comment on lines +186 to 193
if (texts.length === 0 && card.type === "template" && card.data?.template_variable) {
const vars = card.data.template_variable;
for (const val of Object.values(vars)) {
if (typeof val === "string" && val.trim()) {
texts.push(val);
}
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Non-string template variables are silently skipped

Template variables like { "error_code": 500, "retries": 3 } are silently dropped because only typeof val === "string" values are considered. Numeric (or boolean) values can carry meaningful context in alert/notification cards and would currently fall through to "[Interactive Card]" if no string variable exists.

Consider coercing primitive values to strings:

Suggested change
if (texts.length === 0 && card.type === "template" && card.data?.template_variable) {
const vars = card.data.template_variable;
for (const val of Object.values(vars)) {
if (typeof val === "string" && val.trim()) {
texts.push(val);
}
}
}
for (const val of Object.values(vars)) {
if ((typeof val === "string" || typeof val === "number" || typeof val === "boolean") && String(val).trim()) {
texts.push(String(val));
}
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/feishu/src/send.ts
Line: 186-193

Comment:
**Non-string template variables are silently skipped**

Template variables like `{ "error_code": 500, "retries": 3 }` are silently dropped because only `typeof val === "string"` values are considered. Numeric (or boolean) values can carry meaningful context in alert/notification cards and would currently fall through to `"[Interactive Card]"` if no string variable exists.

Consider coercing primitive values to strings:

```suggestion
    for (const val of Object.values(vars)) {
      if ((typeof val === "string" || typeof val === "number" || typeof val === "boolean") && String(val).trim()) {
        texts.push(String(val));
      }
    }
```

How can I resolve this? If you propose a fix, please make it concise.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 2a623d6dca

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +176 to +177
card.i18n_elements.zh_cn ??
(Object.values(card.i18n_elements).find((v) => Array.isArray(v)) as unknown[] | undefined);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Fall back from empty zh_cn i18n elements to another locale

The new i18n locale selection uses card.i18n_elements.zh_cn ?? ..., which treats an empty zh_cn array as a valid hit and prevents fallback to other locales. In cards where zh_cn is present but empty (while en_us or another locale has actual elements), elements becomes [] and the parser returns [Interactive Card] instead of real quoted content. This makes quoting fail for multilingual cards unless zh_cn is populated.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 91716491db

ℹ️ About Codex in GitHub

Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".

Comment on lines +167 to +170
let elements: unknown[] | undefined = card.elements;
if (!Array.isArray(elements)) {
// Try body.elements (card kit v2)
elements = card.body?.elements;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Continue fallback when top-level elements is empty

The resolver treats card.elements as final as soon as it is an array, so an empty elements: [] prevents checking body.elements and i18n_elements. In payloads that include an empty legacy elements field plus real content under body.elements (or locale-specific elements), this still returns [Interactive Card] instead of quoted text, which defeats the variant fallback this change is trying to add.

Useful? React with 👍 / 👎.

@lishuaigit
Copy link
Copy Markdown
Contributor Author

@steipete Fixes content extraction from Feishu interactive cards when quoting — handles header, body.elements, and i18n_elements variants. Includes comprehensive test coverage. CI green 🙏

@lishuaigit
Copy link
Copy Markdown
Contributor Author

Friendly follow-up @steipete 👋 This PR has been open for a while now. CI failures are all in the shared bun/windows-shard flaky tests — not related to this change. Happy to rebase or address any feedback. Would appreciate a review when you have time 🙏

Previously, parseInteractiveCardContent only handled flat v1 cards with
top-level `elements`. When a user quoted a card message that used a
header, i18n_elements, template, column_set, or body wrapper, the bot
could only extract "[Interactive Card]" instead of the actual content.

Now the parser handles:
- Card header title extraction
- body.elements (card kit v2)
- i18n_elements with locale fallback (zh_cn preferred)
- Template cards (template_variable values)
- Nested structures: column_set columns, form/collapsible elements

Closes openclaw#32712
Address Greptile review feedback: the body.elements extraction path
used by the bot's own buildMarkdownCard output now has a dedicated test.
@lishuaigit lishuaigit force-pushed the fix/feishu-interactive-card-quote branch from 9171649 to 40557ca Compare March 30, 2026 03:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: feishu Channel integration: feishu size: M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Bug]: Bot cannot retrieve card message content when a Feishu card is quoted and @mentioned in a group

1 participant