Skip to content

Feature Request: Add Feishu Interactive Card Parsing support #48281 #56795

Open
zeel2104 wants to merge 3 commits intoopenclaw:mainfrom
zeel2104:feat/feishu-interactive-card-parse
Open

Feature Request: Add Feishu Interactive Card Parsing support #48281 #56795
zeel2104 wants to merge 3 commits intoopenclaw:mainfrom
zeel2104:feat/feishu-interactive-card-parse

Conversation

@zeel2104
Copy link
Copy Markdown

Summary

  • Problem: Feishu inbound interactive messages were parsed as [Interactive Card] only, so card header/elements/actions were not available to the agent.
  • Why it matters: Blocks card-based workflows (weekly reports, approvals, form-like actions) because the bot cannot read card content.
  • What changed: Added interactive card parsing for inbound events, fetches richer card payload via raw_card_content, extracts readable text, and forwards raw card JSON to context for advanced parsing.
  • What did NOT change (scope boundary): No outbound card rendering/protocol changes, no new Feishu permissions, no changes to non-Feishu channels.

Change Type (select all)

  • Bug fix
  • Feature
  • Refactor required for the fix
  • Docs
  • Security hardening
  • Chore/infra

Scope (select all touched areas)

  • Gateway / orchestration
  • Skills / tool execution
  • Auth / tokens
  • Memory / storage
  • Integrations
  • API / contracts
  • UI / DX
  • CI/CD / infra

Linked Issue/PR

Root Cause / Regression History (if applicable)

  • Root cause: Inbound Feishu interactive payloads were not parsed beyond placeholder text in the inbound event path.
  • Missing detection / guardrail: No focused inbound interactive-card test asserting extracted content + raw card payload propagation.
  • Prior context (git blame, prior PR, issue, or refactor if known): Existing parsing focused on text/post + outbound card sending.
  • Why this regressed now: N/A (feature gap / missing implementation rather than a recent regression).
  • If unknown, what was ruled out: Confirmed issue is in inbound parse path, not outbound send path.

Regression Test Plan (if applicable)

  • Coverage level that should have caught this:
    • Unit test
    • Seam / integration test
    • End-to-end test
    • Existing coverage already sufficient
  • Target test or file:
    • extensions/feishu/src/bot.checkBotMentioned.test.ts
    • extensions/feishu/src/bot.test.ts
    • extensions/feishu/src/send.test.ts
  • Scenario the test should lock in: Inbound interactive card -> extracted readable text + raw card JSON available in inbound context (RawBody).
  • Why this is the smallest reliable guardrail: Covers both parser behavior and message-handling wiring without requiring live Feishu infra.
  • Existing test that already covers this (if any): send.test.ts had partial interactive extraction coverage; extended/leveraged.
  • If no new test is added, why not: N/A (new tests added).

User-visible / Behavior Changes

  • Incoming Feishu interactive cards are now readable by the bot (header + element text extraction).
  • Inbound context now includes raw interactive card JSON for advanced card/action parsing.
  • Placeholder-only behavior for interactive cards is no longer the default when card payload is available.
Before:
[user sends Feishu interactive card] -> [bot sees "[Interactive Card]"] -> [cannot analyze real card content]

After:
[user sends Feishu interactive card] -> [fetch + parse full card payload] -> [text extracted + raw card JSON attached] -> [bot can analyze/report/action]

## Security Impact (required)

- New permissions/capabilities? (`No`)
- Secrets/tokens handling changed? (`No`)
- New/changed network calls? (`Yes`)
- Command/tool execution surface changed? (`No`)
- Data access scope changed? (`No`)
- If any `Yes`, explain risk + mitigation:
   Added targeted Feishu message fetch for inbound interactive messages to obtain richer card payload.
  Mitigation: Uses existing authenticated Feishu client and existing message scope; no new credentials/privileges introduced.

## Repro + Verification

### Environment

OS: Windows (PowerShell)
Runtime/container: Node 22 + pnpm (corepack)
Model/provider: N/A
Integration/channel (if any): Feishu extension
Relevant config (redacted): channels.feishu.dmPolicy=open (test/mocked configs)

### Steps

1 Send an inbound Feishu interactive message/card to the bot.
2 Observe parsed inbound content/context in Feishu handling path.
3 Run targeted tests listed below.

### Expected

Card text content is extracted (header + elements).
Raw card JSON is available in context (RawBody) for advanced parsing.
-

### Actual

-Verified as expected; targeted tests pass.

## Evidence

Attach at least one:

- [x] Failing test/log before + passing after
- [x ] Trace/log snippets
- [ ] Screenshot/recording
- [ ] Perf numbers (if relevant)

## Human Verification (required)

What you personally verified (not just CI), and how:

- Verified scenarios:
  Targeted tests pass for interactive parsing and context propagation:
  corepack pnpm test -- extensions/feishu/src/send.test.ts -t interactive
  corepack pnpm test -- extensions/feishu/src/bot.checkBotMentioned.test.ts -t interactive
  corepack pnpm test -- extensions/feishu/src/bot.test.ts -t "fetches full interactive card content and forwards raw card JSON in context"
  Type-check passed: corepack pnpm tsgo

- Edge cases checked:
  Placeholder fallback path
  Card header + mixed element extraction
  Raw card payload forwarding

- What you did **not** verify:
   Live Feishu tenant end-to-end in production workspace (manual real-tenant validation still recommended).

## Review Conversations

- [x] I replied to or resolved every bot review conversation I addressed in this PR.
- [x] I left unresolved only the conversations that still need reviewer or maintainer judgment.

If a bot review conversation is addressed by this PR, resolve that conversation yourself. Do not leave bot review conversation cleanup for maintainers.

## Compatibility / Migration

- Backward compatible? (`Yes`)
- Config/env changes? (`No`)
- Migration needed? (`No`)
- If yes, exact upgrade steps: (N/A)

## Risks and Mitigations

Risk: Unexpected card schemas may still produce partial text extraction.
Mitigation: Conservative fallback to placeholder/raw content path and centralized parser for future schema extension.
Risk: Extra Feishu API call for inbound interactive messages can increase latency slightly.
Mitigation: Call is scoped to interactive type only and uses existing client/retry/error handling paths.


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

greptile-apps Bot commented Mar 29, 2026

Greptile Summary

This PR adds inbound Feishu interactive card parsing so the bot can extract readable text (header + elements) and forward the raw card JSON as context to the agent, closing a long-standing feature gap where cards were seen only as [Interactive Card]. The implementation introduces a dedicated interactive-card.ts module shared across bot.ts, send.ts, and bot-content.ts, and enriches FeishuMessageContext/FeishuMessageInfo with an optional rawContent field.

Key findings:

  • P1 — Wrong type guard for i18n_elements.zh_cn (interactive-card.ts:150-153): isRecord returns true for both arrays and plain objects, so a malformed zh_cn that is a plain object passes the guard but then throws TypeError: i18nElements is not iterable when spread on line 155. The catch block in handleFeishuMessage silently swallows this error and the feature falls back to the placeholder text. Should use Array.isArray instead.
  • P2 — No recursion depth limit in collectElementText (interactive-card.ts:66): The function recurses into note, fields, options, actions, and elements without a depth cap, leaving the parse open to stack overflow on pathologically deep card payloads.
  • P2 — as unknown double-cast for raw_card_content (send.ts:267): The cast bypasses SDK type-checking; if the parameter is silently ignored by the SDK version in use, the richer payload is never returned and the feature degrades without any warning or log signal.

Confidence Score: 4/5

Safe to merge after fixing the isRecordArray.isArray guard for i18n_elements.zh_cn to prevent a silent runtime TypeError on i18n cards.

One real P1 defect: using isRecord instead of Array.isArray to guard zh_cn means a non-array zh_cn value causes a TypeError at spread time, silently collapsing the feature to placeholder output. The P2 items (recursion depth, SDK cast) are non-blocking quality improvements. All other changes are well-structured and covered by tests.

extensions/feishu/src/interactive-card.ts — fix the i18n_elements.zh_cn type guard and consider a depth limit in collectElementText.

Important Files Changed

Filename Overview
extensions/feishu/src/interactive-card.ts New module for parsing inbound Feishu interactive card payloads. Contains a P1 type-guard bug (isRecord instead of Array.isArray for i18n_elements.zh_cn) that can cause a TypeError at spread time, and a P2 unbounded recursion issue in collectElementText.
extensions/feishu/src/bot.ts Adds interactive card fetch + context enrichment block and updates RawBody/rawContent propagation. Logic is sound; the content-selection heuristic and raw-card JSON forwarding look correct.
extensions/feishu/src/send.ts Removes old parseInteractiveCardContent in favour of the shared parseInteractiveCardPayload, adds rawContent to parsed message items, and introduces an unsafe SDK cast to pass raw_card_content: true.
extensions/feishu/src/bot-content.ts Minimal change — adds an interactive branch that delegates to the new shared parser, consistent with the post branch pattern.
extensions/feishu/src/types.ts Adds optional rawContent field to FeishuMessageContext and FeishuMessageInfo. Backward-compatible change.
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/feishu/src/interactive-card.ts
Line: 150-153

Comment:
**Wrong type guard for `zh_cn` array**

`isRecord` returns `true` for both plain objects and arrays (since `typeof [] === "object"` and `[] !== null`). When `zh_cn` is a plain object (a malformed but structurally valid JSON response), the cast to `unknown[]` succeeds at compile time, but spreading a non-iterable plain object at runtime (`[...i18nElements]` on line 155) throws `TypeError: i18nElements is not iterable`. The catch block in `handleFeishuMessage` would swallow the error and silently fall back to the placeholder, making the feature appear broken for any card that includes `i18n_elements.zh_cn` as a non-array value.

Use `Array.isArray` here instead:

```suggestion
  const i18nElements =
    isRecord(card.i18n_elements) && Array.isArray((card.i18n_elements as Record<string, unknown>).zh_cn)
      ? ((card.i18n_elements as Record<string, unknown>).zh_cn as unknown[])
      : [];
```

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

---

This is a comment left during a code review.
Path: extensions/feishu/src/interactive-card.ts
Line: 66-131

Comment:
**No recursion depth guard in `collectElementText`**

`collectElementText` calls itself recursively for `note`, `fields`, `options`, `actions`, and `elements` on every visited node, with no depth limit or cycle detection. A deeply nested or adversarially crafted card payload could trigger a stack overflow. In Node.js the call stack is typically a few thousand frames, and a legitimate card schema from an untrusted Feishu user could be structured to exceed this (e.g., elements nested inside elements nested inside actions, etc.).

Consider adding a simple depth parameter:

```typescript
function collectElementText(node: unknown, out: string[], depth = 0): void {
  if (depth > 20 || !node) return;
  // ... recursive calls pass depth + 1
}
```

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

---

This is a comment left during a code review.
Path: extensions/feishu/src/send.ts
Line: 267-274

Comment:
**Type-unsafe SDK cast for `raw_card_content` parameter**

The double `as unknown as (...)` cast bypasses TypeScript's type checking entirely for the `raw_card_content: true` query parameter. If the underlying Feishu SDK version in use does not actually accept or forward this parameter, the call will silently succeed but return a response without the richer card payload — `parseInteractiveCardPayload` would then fall back to re-parsing whatever the SDK returns, potentially producing the placeholder text without any runtime error or log warning.

If `raw_card_content` is a documented Feishu API param but simply absent from the SDK's type definitions, consider adding a comment noting the SDK type gap and the Feishu API version that introduced the field:

```typescript
// raw_card_content is a Feishu IM API v2 param not yet typed in the SDK.
// See: https://open.feishu.cn/document/server-docs/im-v1/message/get
const getMessageWithRawCard = client.im.message.get as unknown as (
  opts: FeishuMessageGetRequestWithRawCard,
) => Promise<FeishuGetMessageResponse>;
```

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

Reviews (1): Last reviewed commit: "feishu: parse inbound interactive cards ..." | Re-trigger Greptile

Comment on lines +150 to +153
const i18nElements =
isRecord(card.i18n_elements) && isRecord((card.i18n_elements as Record<string, unknown>).zh_cn)
? ((card.i18n_elements as Record<string, unknown>).zh_cn as unknown[])
: [];
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.

P1 Wrong type guard for zh_cn array

isRecord returns true for both plain objects and arrays (since typeof [] === "object" and [] !== null). When zh_cn is a plain object (a malformed but structurally valid JSON response), the cast to unknown[] succeeds at compile time, but spreading a non-iterable plain object at runtime ([...i18nElements] on line 155) throws TypeError: i18nElements is not iterable. The catch block in handleFeishuMessage would swallow the error and silently fall back to the placeholder, making the feature appear broken for any card that includes i18n_elements.zh_cn as a non-array value.

Use Array.isArray here instead:

Suggested change
const i18nElements =
isRecord(card.i18n_elements) && isRecord((card.i18n_elements as Record<string, unknown>).zh_cn)
? ((card.i18n_elements as Record<string, unknown>).zh_cn as unknown[])
: [];
const i18nElements =
isRecord(card.i18n_elements) && Array.isArray((card.i18n_elements as Record<string, unknown>).zh_cn)
? ((card.i18n_elements as Record<string, unknown>).zh_cn as unknown[])
: [];
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/feishu/src/interactive-card.ts
Line: 150-153

Comment:
**Wrong type guard for `zh_cn` array**

`isRecord` returns `true` for both plain objects and arrays (since `typeof [] === "object"` and `[] !== null`). When `zh_cn` is a plain object (a malformed but structurally valid JSON response), the cast to `unknown[]` succeeds at compile time, but spreading a non-iterable plain object at runtime (`[...i18nElements]` on line 155) throws `TypeError: i18nElements is not iterable`. The catch block in `handleFeishuMessage` would swallow the error and silently fall back to the placeholder, making the feature appear broken for any card that includes `i18n_elements.zh_cn` as a non-array value.

Use `Array.isArray` here instead:

```suggestion
  const i18nElements =
    isRecord(card.i18n_elements) && Array.isArray((card.i18n_elements as Record<string, unknown>).zh_cn)
      ? ((card.i18n_elements as Record<string, unknown>).zh_cn as unknown[])
      : [];
```

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

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Fixed in follow-up commit: switched zh_cn to Array.isArray

Comment on lines +66 to +131
function collectElementText(node: unknown, out: string[]): void {
if (!node) {
return;
}
if (typeof node === "string") {
pushText(out, node);
return;
}
if (!isRecord(node)) {
return;
}

const obj = node as Record<string, unknown>;
const textField = obj.text;
if (isRecord(textField)) {
pushText(out, textField.content);
} else {
pushText(out, textField);
}
pushText(out, obj.content);

const title = obj.title;
if (isRecord(title)) {
pushText(out, title.content);
pushText(out, title.text);
} else {
pushText(out, title);
}

const note = obj.note;
if (Array.isArray(note)) {
for (const item of note) {
collectElementText(item, out);
}
} else {
collectElementText(note, out);
}

const fields = obj.fields;
if (Array.isArray(fields)) {
for (const field of fields) {
collectElementText(field, out);
}
}

const options = obj.options;
if (Array.isArray(options)) {
for (const option of options) {
collectElementText(option, out);
}
}

const actions = obj.actions;
if (Array.isArray(actions)) {
for (const action of actions) {
collectElementText(action, out);
}
}

const children = obj.elements;
if (Array.isArray(children)) {
for (const child of children) {
collectElementText(child, out);
}
}
}
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.

P2 No recursion depth guard in collectElementText

collectElementText calls itself recursively for note, fields, options, actions, and elements on every visited node, with no depth limit or cycle detection. A deeply nested or adversarially crafted card payload could trigger a stack overflow. In Node.js the call stack is typically a few thousand frames, and a legitimate card schema from an untrusted Feishu user could be structured to exceed this (e.g., elements nested inside elements nested inside actions, etc.).

Consider adding a simple depth parameter:

function collectElementText(node: unknown, out: string[], depth = 0): void {
  if (depth > 20 || !node) return;
  // ... recursive calls pass depth + 1
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/feishu/src/interactive-card.ts
Line: 66-131

Comment:
**No recursion depth guard in `collectElementText`**

`collectElementText` calls itself recursively for `note`, `fields`, `options`, `actions`, and `elements` on every visited node, with no depth limit or cycle detection. A deeply nested or adversarially crafted card payload could trigger a stack overflow. In Node.js the call stack is typically a few thousand frames, and a legitimate card schema from an untrusted Feishu user could be structured to exceed this (e.g., elements nested inside elements nested inside actions, etc.).

Consider adding a simple depth parameter:

```typescript
function collectElementText(node: unknown, out: string[], depth = 0): void {
  if (depth > 20 || !node) return;
  // ... recursive calls pass depth + 1
}
```

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

Comment on lines +267 to +274
const getMessageWithRawCard = client.im.message.get as unknown as (
opts: FeishuMessageGetRequestWithRawCard,
) => Promise<FeishuGetMessageResponse>;

try {
const response = (await client.im.message.get({
const response = await getMessageWithRawCard({
path: { message_id: messageId },
})) as FeishuGetMessageResponse;
params: { raw_card_content: true },
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.

P2 Type-unsafe SDK cast for raw_card_content parameter

The double as unknown as (...) cast bypasses TypeScript's type checking entirely for the raw_card_content: true query parameter. If the underlying Feishu SDK version in use does not actually accept or forward this parameter, the call will silently succeed but return a response without the richer card payload — parseInteractiveCardPayload would then fall back to re-parsing whatever the SDK returns, potentially producing the placeholder text without any runtime error or log warning.

If raw_card_content is a documented Feishu API param but simply absent from the SDK's type definitions, consider adding a comment noting the SDK type gap and the Feishu API version that introduced the field:

// raw_card_content is a Feishu IM API v2 param not yet typed in the SDK.
// See: https://open.feishu.cn/document/server-docs/im-v1/message/get
const getMessageWithRawCard = client.im.message.get as unknown as (
  opts: FeishuMessageGetRequestWithRawCard,
) => Promise<FeishuGetMessageResponse>;
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/feishu/src/send.ts
Line: 267-274

Comment:
**Type-unsafe SDK cast for `raw_card_content` parameter**

The double `as unknown as (...)` cast bypasses TypeScript's type checking entirely for the `raw_card_content: true` query parameter. If the underlying Feishu SDK version in use does not actually accept or forward this parameter, the call will silently succeed but return a response without the richer card payload — `parseInteractiveCardPayload` would then fall back to re-parsing whatever the SDK returns, potentially producing the placeholder text without any runtime error or log warning.

If `raw_card_content` is a documented Feishu API param but simply absent from the SDK's type definitions, consider adding a comment noting the SDK type gap and the Feishu API version that introduced the field:

```typescript
// raw_card_content is a Feishu IM API v2 param not yet typed in the SDK.
// See: https://open.feishu.cn/document/server-docs/im-v1/message/get
const getMessageWithRawCard = client.im.message.get as unknown as (
  opts: FeishuMessageGetRequestWithRawCard,
) => Promise<FeishuGetMessageResponse>;
```

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

ℹ️ 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 +36 to +38
if (typeof asRecord.card === "string") {
return parseJsonObject(asRecord.card);
}
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 Fall back to raw_card_content when card string is not JSON

Do not return immediately from the card string branch when parsing fails. Right now resolveCardObject returns parseJsonObject(asRecord.card) directly, so if card is a non-JSON placeholder string but raw_card_content contains the real card payload, the function exits early and skips the raw_card_content fallback, leaving interactive messages stuck as [Interactive Card] and dropping actionable card content.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

added fallback from non-JSON card to raw_card_content

Comment on lines +151 to +152
isRecord(card.i18n_elements) && isRecord((card.i18n_elements as Record<string, unknown>).zh_cn)
? ((card.i18n_elements as Record<string, unknown>).zh_cn as unknown[])
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 Require array shape before spreading i18n card elements

collectCardText accepts any object for i18n_elements.zh_cn (isRecord) and later spreads it as an array. If that field is object-shaped instead of array-shaped, the spread throws a TypeError and message handling aborts instead of gracefully falling back. This should check Array.isArray(...) (or normalize object forms) before treating it as iterable.

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: 362e2bb72c

ℹ️ 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 +155 to +157
isRecord(card.i18n_elements) &&
Array.isArray((card.i18n_elements as Record<string, unknown>).zh_cn)
? ((card.i18n_elements as Record<string, unknown>).zh_cn as unknown[])
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 Support non-zh_cn i18n card elements

collectCardText only reads card.i18n_elements.zh_cn, so interactive cards that localize content under another locale key (for example en_us) will drop their body text and often collapse back to [Interactive Card]. This is a functional parsing gap introduced by the new interactive-card path: multilingual cards become unreadable unless they also duplicate elements into body.elements or zh_cn, which means agents miss actionable card content for non-Chinese tenants.

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: 06568d8f0b

ℹ️ 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".

ReplyToId: ctx.parentId,
RootMessageId: ctx.rootId,
RawBody: ctx.content,
RawBody: ctx.rawContent ?? ctx.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.

P2 Badge Preserve RawBody plain text outside interactive cards

RawBody now unconditionally prefers ctx.rawContent, but parseFeishuMessageEvent now populates rawContent with the raw Feishu payload (event.message.content) for all message types, so regular text DMs/groups become JSON envelopes like {"text":"..."} instead of the legacy plain text body. This is a backward-compatibility regression for Feishu users who still consume RawBody directly in hooks/templates (where RawBody is documented as the legacy alias of CommandBody) and can break string/pattern matching flows that previously worked.

Useful? React with 👍 / 👎.

@openclaw-barnacle
Copy link
Copy Markdown

This pull request has been automatically marked as stale due to inactivity.
Please add updates or it will be closed.

@openclaw-barnacle openclaw-barnacle Bot added the stale Marked as stale due to inactivity label Apr 30, 2026
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 30, 2026

Codex review: needs changes before merge.

Summary
This PR adds Feishu inbound interactive-card parsing, fetches richer message content, carries optional raw card JSON through Feishu message context, and adds focused Feishu tests.

Reproducibility: yes. A high-confidence source-level reproduction is an inbound Feishu event with message_type: "interactive" whose event content is only placeholder/fallback text; current main does not fetch richer card content for that direct inbound path, and the PR adds a mocked test for it.

Next step before merge
A narrow automated repair can address the two concrete blockers and reconcile the PR with current main without a product, core API, release, or security decision.

Security
Cleared: The diff adds no dependencies, install scripts, workflow changes, secret handling, or code-execution path; the added Feishu read uses the existing authenticated client and message scope.

Review findings

  • [P1] Request Feishu's card content format — extensions/feishu/src/send.ts:274
  • [P2] Keep RawBody on normalized message text — extensions/feishu/src/bot.ts:942
Review details

Best possible solution:

Keep the fix inside the Feishu plugin by fetching card JSON through Feishu's message-content query, preserving the normalized body contract, and reusing current-main interactive-card parser coverage.

Do we have a high-confidence way to reproduce the issue?

Yes. A high-confidence source-level reproduction is an inbound Feishu event with message_type: "interactive" whose event content is only placeholder/fallback text; current main does not fetch richer card content for that direct inbound path, and the PR adds a mocked test for it.

Is this the best way to solve the issue?

No. The PR targets the right Feishu-owned boundary, but it is not the best merge shape until the Feishu query parameter is corrected, RawBody remains compatible for ordinary messages, and the branch is reconciled with current-main parser fixes.

Full review comments:

  • [P1] Request Feishu's card content format — extensions/feishu/src/send.ts:274
    The new fetch sends params: { raw_card_content: true }, but the inspected Feishu message API uses card_msg_content_type to control card return format and the SDK type surface does not include this boolean key. With this request shape, the follow-up fetch can still return placeholder card content while the mock passes. Use the Feishu query shape and assert it in the test.
    Confidence: 0.91
  • [P2] Keep RawBody on normalized message text — extensions/feishu/src/bot.ts:942
    parseFeishuMessageEvent now populates ctx.rawContent for every message type, and this line makes RawBody prefer that raw Feishu envelope. Regular text messages would expose values like {"text":"hello"} instead of the documented legacy CommandBody alias, breaking hooks/templates that match plain user text. Scope raw card JSON to interactive-card metadata instead of replacing RawBody globally.
    Confidence: 0.88

Overall correctness: patch is incorrect
Overall confidence: 0.89

Acceptance criteria:

  • pnpm test extensions/feishu/src/send.test.ts extensions/feishu/src/bot.checkBotMentioned.test.ts extensions/feishu/src/bot.test.ts extensions/feishu/src/interactive-card.test.ts
  • pnpm exec oxfmt --check --threads=1 extensions/feishu/src/send.ts extensions/feishu/src/bot.ts extensions/feishu/src/bot-content.ts extensions/feishu/src/types.ts extensions/feishu/src/*.test.ts
  • pnpm check:changed

What I checked:

  • Current main inbound event parser still lacks an interactive-card fetch path: parseMessageContent handles post/text/media/share_chat/merge_forward and otherwise returns the original event content, so direct inbound interactive messages still rely on the event payload instead of fetching full card JSON. (extensions/feishu/src/bot-content.ts:133, 2fd1e7b32dd8)
  • Current main only parses fetched interactive message bodies: parseFeishuMessageContent parses msgType === "interactive" for fetched messages, but getMessageFeishu calls client.im.message.get without a card-content query parameter. (extensions/feishu/src/send.ts:332, 2fd1e7b32dd8)
  • PR sends the wrong raw-card query shape: The PR calls getMessageWithRawCard with params: { raw_card_content: true }, which is not the Feishu message API query shape inspected during review. (extensions/feishu/src/send.ts:274, 06568d8f0b3b)
  • Feishu docs use card_msg_content_type for card return format: The official Feishu message get/list markdown docs expose card_msg_content_type and describe user_card_content as returning the original sent card JSON; they do not document a boolean raw_card_content query key.
  • SDK type surface does not type the PR query key: The @larksuiteoapi/node-sdk@1.62.1 message-get params type only exposes user_id_type, so any raw-card parameter needs an intentional typed adapter matching the upstream HTTP contract.
  • RawBody compatibility contract: Docs define RawBody as the legacy alias for CommandBody, and current main passes normalized agentFacingContent for both fields; the PR makes RawBody prefer raw Feishu JSON for every message type. Public docs: docs/concepts/messages.md. (docs/concepts/messages.md:101, 2fd1e7b32dd8)

Likely related people:

  • vincentkoc: Merged the current-main Feishu interactive-card extraction repair and recently split Feishu message/mention types that this PR extends. (role: recent feature maintainer; confidence: high; commits: c6cf37068cae, 2b96f53f9782; files: extensions/feishu/src/send.ts, extensions/feishu/src/types.ts)
  • steipete: Recent Feishu API diagnostics and inbound channel-kernel work touched the same send/bot paths and current context routing. (role: recent maintainer; confidence: high; commits: 1ecb2fc2c72a, ffe67e9cdc9e; files: extensions/feishu/src/send.ts, extensions/feishu/src/bot.ts)
  • Coobiw: Introduced getMessageFeishu/thread-history fetch behavior and FeishuMessageInfo fields that this PR changes for richer interactive-card content. (role: introduced adjacent fetch/history behavior; confidence: medium; commits: 8a607d755333; files: extensions/feishu/src/send.ts, extensions/feishu/src/types.ts, extensions/feishu/src/bot.ts)
  • nszhsl: Authored the structured Feishu card and streaming-card work that created much of the current interactive-card sending surface affected by this parsing gap. (role: adjacent card-surface contributor; confidence: medium; commits: df3a247db2a9; files: extensions/feishu/src/send.ts)

Remaining risk / open question:

Codex review notes: model gpt-5.5, reasoning high; reviewed against 2fd1e7b32dd8.

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.

Feature Request: Add Feishu Interactive Card Parsing Support

1 participant