Skip to content

fix(feishu): restore quoted card content by avoiding lossy interactive placeholder#32742

Closed
KimGLee wants to merge 1 commit intoopenclaw:mainfrom
KimGLee:fix/32712-feishu-quoted-card-regression
Closed

fix(feishu): restore quoted card content by avoiding lossy interactive placeholder#32742
KimGLee wants to merge 1 commit intoopenclaw:mainfrom
KimGLee:fix/32712-feishu-quoted-card-regression

Conversation

@KimGLee
Copy link
Copy Markdown
Contributor

@KimGLee KimGLee commented Mar 3, 2026

Summary

Fix Feishu quoted interactive-card regression by preserving raw quoted payload when card-text extraction cannot decode the card structure.

  • keep existing extraction for known interactive card element shapes
  • if extraction cannot find text (elements missing/unsupported), fallback to raw quoted payload instead of hardcoded "[Interactive Card]"
  • add regression test that simulates unknown interactive-card schema and verifies raw payload is preserved

Fixes #32712.


Root cause analysis (version diff)

Behavior in v2026.2.26 (worked)

In extensions/feishu/src/send.ts, quoted-message parsing for non-text messages effectively preserved useful raw content. For quoted interactive cards, OpenClaw still had structured payload available to downstream reasoning (at minimum raw JSON content), so quoting a card in Feishu groups remained usable.

Behavior introduced by v2026.3.1 (regression)

v2026.3.1 introduced stricter quoted parsing in extensions/feishu/src/send.ts (commit 6ea3a47da, fix(feishu): harden routing, parsing, and media delivery):

  • parseQuotedMessageContent() now routes msg_type === "interactive" into parseInteractiveCardContent()
  • parseInteractiveCardContent() only extracts text from a narrow shape (elements[] with markdown / div.text.content)
  • when that shape does not match, it returns hardcoded "[Interactive Card]"

That means quoted card payload gets flattened to a placeholder, which is exactly the user-visible regression:

[Replying to: "[Interactive Card]"]

So the regression is not failure to fetch quoted message ID; it is over-aggressive lossy normalization after fetch.


What this PR changes

Before

  • Unknown interactive-card schemas -> "[Interactive Card]" (payload lost)

After

  • Known schema -> extracted text (unchanged)
  • Unknown schema -> fallback to raw body.content JSON string (payload preserved)

This restores practical behavior from pre-regression versions while keeping improved parsing for recognized card structures.


Tests

Added regression coverage in extensions/feishu/src/send.test.ts:

  • falls back to raw interactive payload when card structure is unknown

Local run:

pnpm exec vitest run extensions/feishu/src/send.test.ts

Passed: 5/5 tests.

@openclaw-barnacle openclaw-barnacle Bot added channel: feishu Channel integration: feishu size: XS labels Mar 3, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented Mar 3, 2026

Greptile Summary

This PR fixes a regression introduced in v2026.3.1 where quoted Feishu interactive cards were reduced to the useless placeholder "[Interactive Card]". The fix is minimal and targeted: parseInteractiveCardContent now returns string | undefined instead of always returning a string, and the call site in parseQuotedMessageContent falls back to rawContent (the original JSON payload) when extraction yields nothing. This restores pre-regression behavior — downstream consumers receive the full card JSON rather than an opaque string — while still using structured text extraction for the known elements-based card shapes.

Key changes:

  • parseInteractiveCardContent return type widened to string | undefined; all three "[Interactive Card]" returns replaced with undefined
  • parseQuotedMessageContent uses extracted ?? rawContent so raw payload is preserved on extraction failure
  • New regression test verifies the fallback with a schema-2.0 card that lacks a top-level elements array

Minor observation: There is no test for the edge case where elements is present but contains only elements with unrecognised tags (e.g. image, action, column_set). In that case texts stays empty, extracted becomes "", and the function returns undefinedrawContent. This is a behaviour improvement over the old "[Interactive Card]" return but is not explicitly covered by the test suite. It is not a bug — just a gap in test coverage.

Confidence Score: 4/5

  • Safe to merge — the change is a small, focused fallback improvement with a direct regression test and no breaking side-effects.
  • The diff is minimal (three return-value changes + one call-site tweak). The logic is easy to trace: undefined propagates the nullish-coalescing fallback to rawContent, which is strictly more informative than "[Interactive Card]". The new test correctly exercises the targeted path. The only deduction is the uncovered edge case where elements exists but all tags are unrecognised, leaving that behaviour improvement implicit rather than asserted.
  • No files require special attention.

Last reviewed commit: 7df0e43

@openclaw-barnacle openclaw-barnacle Bot added agents Agent runtime and tooling size: S and removed size: XS labels Mar 3, 2026
@KimGLee KimGLee force-pushed the fix/32712-feishu-quoted-card-regression branch 2 times, most recently from 2ba1cf1 to efc0d79 Compare March 3, 2026 08:36
@openclaw-barnacle openclaw-barnacle Bot added size: XS and removed agents Agent runtime and tooling size: S labels Mar 3, 2026
@aisle-research-bot
Copy link
Copy Markdown

aisle-research-bot Bot commented Mar 12, 2026

🔒 Aisle Security Analysis

We found 3 potential security issue(s) in this PR:

# Severity Title
1 🟠 High Feishu interactive-card raw JSON fallback can leak sensitive data to logs/LLM context
2 🟡 Medium Unbounded interactive card JSON returned as message content (resource exhaustion risk)
3 🟡 Medium Untrusted Feishu interactive card JSON returned as message content (prompt/log injection risk)

1. 🟠 Feishu interactive-card raw JSON fallback can leak sensitive data to logs/LLM context

Property Value
Severity High
CWE CWE-201
Location extensions/feishu/src/send.ts:235-238

Description

parseFeishuMessageContent() now returns the entire raw interactive-card JSON payload (rawContent) when text extraction fails.

This is risky because Feishu interactive cards commonly include:

  • internal identifiers (chat/user/open_id/tenant ids)
  • deep links (sometimes containing tokens or signed parameters)
  • metadata and embedded fields not intended for display

Once returned as content, this raw JSON is treated as message text elsewhere (e.g., quoted message previews, thread-history bootstrap, and tool/action outputs), which can:

  • expose sensitive data to application logs (preview logging)
  • exfiltrate sensitive payloads to third-party LLM providers (thread history / quoted content included in agent prompts)
  • surface raw payloads to other systems/consumers that expect only human-readable text

Vulnerable code (new behavior):

if (msgType === "interactive") {
  const extracted = parseInteractiveCardContent(parsed);
  return extracted ?? rawContent;
}

This behavior is also codified in the new test falls back to raw interactive payload when card structure is unknown.

Recommendation

Do not fall back to returning the full raw interactive-card JSON as message text.

Safer options:

  1. Keep a placeholder when extraction fails (previous behavior), optionally with minimal non-sensitive context:
if (msgType === "interactive") {
  const extracted = parseInteractiveCardContent(parsed);
  return extracted ?? "[Interactive Card]";
}
  1. If raw payload is needed for debugging, gate it behind an explicit debug/diagnostic flag and redact common sensitive fields before logging or returning it.

Additionally, consider storing raw card JSON in a separate field (not content) that is never logged or forwarded to LLM prompts by default.


2. 🟡 Unbounded interactive card JSON returned as message content (resource exhaustion risk)

Property Value
Severity Medium
CWE CWE-400
Location extensions/feishu/src/send.ts:235-238

Description

parseFeishuMessageContent() now falls back to returning the full raw interactive-card payload when extraction fails:

  • Input rawContent comes directly from the Feishu API response (item.body.content).
  • For msg_type === "interactive", if parseInteractiveCardContent() cannot recognize the expected card schema, the code returns rawContent unchanged.
  • This can propagate very large interactive-card JSON into downstream flows that expect human-readable text (e.g., quoted-message fetch and thread-history fetch).
  • The resulting large content is later embedded into agent prompts/history construction (e.g., buildFeishuAgentBody() uses quotedContent verbatim), increasing memory usage, token usage/cost, and risk of exceeding model/context limits or storage quotas.

Vulnerable code:

if (msgType === "interactive") {
  const extracted = parseInteractiveCardContent(parsed);
  return extracted ?? rawContent;
}

Recommendation

Apply a size cap / truncation (and ideally avoid embedding raw card JSON into prompts at all).

Options:

  1. Keep a bounded placeholder fallback:
if (msgType === "interactive") {
  const extracted = parseInteractiveCardContent(parsed);
  return extracted ?? "[Interactive Card]";
}
  1. If raw fallback is required for debugging, truncate to a safe maximum and annotate:
const MAX_LEN = 2000;
if (msgType === "interactive") {
  const extracted = parseInteractiveCardContent(parsed);
  if (extracted) return extracted;
  const truncated = rawContent.length > MAX_LEN
    ? rawContent.slice(0, MAX_LEN) + `… [truncated ${rawContent.length - MAX_LEN} chars]`
    : rawContent;
  return truncated;
}

Also consider keeping raw card JSON in a separate field that is not fed into LLM prompts or persisted in chat history.


3. 🟡 Untrusted Feishu interactive card JSON returned as message content (prompt/log injection risk)

Property Value
Severity Medium
CWE CWE-116
Location extensions/feishu/src/send.ts:235-238

Description

The Feishu message parser now falls back to returning the raw interactive card JSON payload when it cannot extract human-readable text.

This is risky because rawContent is untrusted, attacker-controlled message content received from Feishu. Returning it verbatim:

  • Propagates arbitrary strings/markup-like sequences (e.g., spoofed [System: ...]-style instructions, control characters) into downstream consumers.
  • The resulting content is later used in:
    • agent context construction (quoted message/thread history bodies), and
    • logging (quoted-message debug logs),
      without any escaping/sanitization.

If an attacker can cause an interactive message with unexpected schema to be present (e.g., via integrations/bots), they can inject misleading content into the LLM prompt context and/or forge log lines.

Vulnerable code:

if (msgType === "interactive") {
  const extracted = parseInteractiveCardContent(parsed);
  return extracted ?? rawContent;
}

Recommendation

Do not return raw interactive JSON as a fallback. Prefer a safe placeholder and/or a tightly-scoped, sanitized summary.

Option A (restore safe placeholder fallback):

if (msgType === "interactive") {
  const extracted = parseInteractiveCardContent(parsed);
  return extracted ?? "[Interactive Card]";
}

Option B (sanitized JSON preview with strict limits):

function safePreview(s: string, max = 500): string {// strip control chars + collapse whitespace to prevent log/prompt forging
  return s.replace(/[\u0000-\u001F\u007F]/g, " ").replace(/\s+/g, " ").slice(0, max);
}

if (msgType === "interactive") {
  const extracted = parseInteractiveCardContent(parsed);
  return extracted ?? `[Interactive Card: ${safePreview(rawContent)}]`;
}

Additionally, consider:

  • enforcing a maximum length for content inserted into agent prompt/thread history
  • escaping/normalizing newlines before logging quoted message content (CWE-117 hardening)

Analyzed PR: #32742 at commit 00c6634

Last updated on: 2026-03-22T05:38:05Z

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: cd6bc7215c

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread extensions/feishu/src/send.ts Outdated
Comment on lines +231 to +234
content,
contentType: msgType,
createTime: item.create_time ? parseInt(String(item.create_time), 10) : 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.

P1 Badge Preserve threadId when returning fetched Feishu messages

getMessageFeishu now returns a message object without threadId, even though downstream thread bootstrap relies on rootMsg?.threadId when the inbound event lacks ctx.threadId (extensions/feishu/src/bot.ts:1387). In topic sessions where Feishu does not include thread_id on the event but does provide it on the root message fetch, this change makes feishuThreadId stay empty, so thread history loading is skipped and agents lose prior thread context.

Useful? React with 👍 / 👎.

Comment thread extensions/feishu/src/send.ts Outdated
Comment on lines +82 to +84
const candidate = parsed as { elements?: unknown };
if (!Array.isArray(candidate.elements)) {
return 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 Keep schema 2.0 card parsing in interactive quote extraction

The interactive-card parser now only checks top-level elements and no longer reads body.elements, which is the structure produced by schema 2.0 cards (for example from buildMarkdownCard). As a result, standard quoted interactive cards stop yielding extracted text and always fall back to raw JSON payloads, reducing message quality and context compared to prior behavior.

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: af7653e828

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread extensions/feishu/src/send.ts Outdated
if (currentMessageId && item.message_id === currentMessageId) continue;
if (rootMessageId && item.message_id === rootMessageId) continue;

const parsed = parseFeishuMessageItem(item);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Reintroduce parseFeishuMessageItem before thread listing

listFeishuThreadMessages now calls parseFeishuMessageItem, but that helper was removed in this commit, so any non-empty thread will throw a ReferenceError at runtime instead of returning history. This path is used during Feishu thread-context bootstrap, so threaded conversations lose prior context when message listing runs.

Useful? React with 👍 / 👎.

Comment thread extensions/ollama/package.json Outdated
@@ -1,6 +1,6 @@
{
"name": "@openclaw/ollama-provider",
"version": "2026.3.14",
"version": "2026.3.11",
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 Revert accidental provider version downgrade

This change rolls the built-in provider manifest version backward (2026.3.142026.3.11), and the same downgrade is present in the sibling sglang and vllm provider manifests in this commit. Plugin discovery exposes manifest versions directly, so these providers will report older metadata than the current CLI release, which can confuse version-based update/audit flows and release consistency.

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: 64177c889a

ℹ️ About Codex in GitHub

Your team has set up Codex to 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 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread extensions/feishu/src/send.ts Outdated
*/
export async function sendStructuredCardFeishu(params: {
export async function sendMarkdownCardFeishu(params: {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Restore structured-card exports from send.ts

send.ts no longer exports sendStructuredCardFeishu, resolveFeishuCardTemplate, or CardHeaderConfig, but other Feishu runtime modules still import them (for example outbound and streaming card paths). In ESM this is a hard module-link error (does not provide an export named ...), so loading the Feishu runtime fails before handling messages, which breaks Feishu channel actions at runtime.

Useful? React with 👍 / 👎.

Comment thread extensions/feishu/src/send.ts Outdated
Comment on lines +313 to +315
if (shouldFallbackFromReplyTarget(response)) {
return sendFallbackDirect(client, directParams, "Feishu send failed");
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Keep thread replies from falling back to top-level sends

When replyInThread is true and Feishu reports the parent message as withdrawn/not found, this branch now falls back to message.create instead of failing, which posts a top-level message outside the topic thread. That leaks thread-only replies into the main chat and breaks thread continuity; the same regression pattern appears in both text and card send paths.

Useful? React with 👍 / 👎.

Comment thread extensions/feishu/src/send.ts Outdated
Comment on lines +459 to +467
const messageText = getFeishuRuntime().channel.text.convertMarkdownTables(text ?? "", tableMode);

const { content, msgType } = buildFeishuPostMessagePayload({ messageText });

const response = await client.im.message.update({
path: { message_id: messageId },
data: {
msg_type: msgType,
content,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve interactive card editing in editMessageFeishu

This rewrite removed the card-edit branch and now always derives a post payload from text ?? "", so callers that send card-only edits end up overwriting the message with an empty/incorrect post instead of patching interactive content. Since Feishu edit flows still pass optional card payloads, card updates will silently stop working and can corrupt edited message content.

Useful? React with 👍 / 👎.

@KimGLee KimGLee force-pushed the fix/32712-feishu-quoted-card-regression branch from 64177c8 to f6d5ea7 Compare March 22, 2026 05:10
@KimGLee KimGLee force-pushed the fix/32712-feishu-quoted-card-regression branch from f6d5ea7 to 00c6634 Compare March 22, 2026 05:14
@openclaw-barnacle openclaw-barnacle Bot removed the cli CLI command changes label Mar 22, 2026
@KimGLee
Copy link
Copy Markdown
Contributor Author

KimGLee commented Mar 31, 2026

Closing this PR because the current mainline Feishu direction has moved on: the official plugin now covers streaming cards and v2 card support, so this older fix path is no longer necessary to keep advancing. If related gaps remain in the future, they should be revisited against the current Feishu implementation rather than continued from this branch.

@KimGLee KimGLee closed this Mar 31, 2026
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: S

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