Skip to content

feat(feishu): enhance interactive card parsing to extract markdown content#42218

Closed
just2gooo wants to merge 1 commit intoopenclaw:mainfrom
just2gooo:feat/feishu-card-parser
Closed

feat(feishu): enhance interactive card parsing to extract markdown content#42218
just2gooo wants to merge 1 commit intoopenclaw:mainfrom
just2gooo:feat/feishu-card-parser

Conversation

@just2gooo
Copy link
Copy Markdown

@just2gooo just2gooo commented Mar 10, 2026

Summary

  • Problem: Feishu interactive card messages were not being properly parsed when users quote/reply to them. The original code only handled simple div and markdown tags, returning [unknown: ...] for complex cards.
  • Why it matters: Users expect to see the actual content of quoted card messages in conversations.
  • What changed:
    • Added comprehensive card-parser.ts supporting 15+ element types (heading, list, table, code_block, code_span, link, button, action, action_link, blockquote, card_header, div, hr, br, etc.)
    • Added card_msg_content_type=raw_card_content API parameter to fetch raw card JSON
    • Fixed heading level parsing to read from property.level
    • Fixed button text fallback when no actions array
    • Added code block language identifiers
    • Added blockquote support with > prefix
    • Handles card_header with both object and string title formats
  • What did NOT change: Other message types (text, post, image) remain unaffected.

The example intercative card:
image

Markdown content parsed please see in test/fixtures/feishu-card-parser/card-expected.md:

Change Type

  • Feature

Scope

  • Integrations (Feishu)

Linked Issue/PR

User-visible / Behavior Changes

  • When a user quotes/replies to a Feishu interactive card message, the bot now extracts and displays the card content as markdown instead of showing [Interactive Card] or [unknown: ...]

Security Impact

  • New permissions/capabilities? No
  • Secrets/tokens handling changed? No
  • New/changed network calls? No (same API call, just added one parameter)
  • Command/tool execution surface changed? No
  • Data access scope changed? No

Test Coverage

Expanded from 1 fixture test to 20 unit tests covering: invalid input, empty objects, null, JSON strings, heading levels, ordered/unordered lists, code blocks with language, code spans, blockquotes, tables, card headers (object + string title), links, buttons (with/without actions), hr/br, deep nesting truncation, and top-level header+body structure.

Repro + Verification

Environment

  • OS: macOS/Linux
  • Runtime: Node 22
  • Integration: Feishu

Steps

  1. Receive a Feishu message that quotes/replies to an interactive card
  2. Bot processes the quoted message
  3. Verify card content is extracted as markdown

Expected

Card content displayed in conversation

Actual

Works correctly in testing

Evidence

  • Tested locally with quoted card messages
  • All 349 feishu extension tests pass

Human Verification

  • Verified scenarios: Manual testing with quoted card messages in Feishu
  • Edge cases checked: Various card types (text, image placeholder, buttons, tables)
  • What you did NOT verify: Live Feishu API with complex nested cards

Compatibility / Migration

  • Backward compatible? Yes (falls back to [Interactive Card] for unknown tags)
  • Config/env changes? None
  • Migration needed? No

Risks and Mitigations

  • Risk: Parser may encounter unexpected card structures
    • Mitigation: Default fallback returns [unknown: ...] with serialized content

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

greptile-apps Bot commented Mar 10, 2026

Greptile Summary

This PR adds a comprehensive Feishu interactive card parser (card-parser.ts) that extracts markdown content from quoted/replied card messages, replacing the previous [unknown: ...] / [Interactive Card] fallback behavior. It also adds the card_msg_content_type=raw_card_content API parameter to getMessageFeishu to fetch raw card JSON for parsing.

Key changes:

  • New card-parser.ts handles 15+ element types: headings, lists (ordered/unordered), tables, code blocks with language identifiers, code spans, blockquotes, card headers, links, buttons (with and without action URLs), div, hr, br, and graceful fallback for unknown tags
  • All three previously reviewed issues (heading level from property.level, button text without actions, code block language) are correctly implemented in this version
  • 20 unit tests added covering edge cases including invalid input, deep nesting truncation, and fixture-based integration test

Minor issue found:

  • The blockquote handler splits inner content on \n without first trimming trailing newlines, which can produce > lines (blockquote markers with a trailing space) in the output when inner elements like headings or code blocks contribute trailing newlines. This is a cosmetic output-quality issue.

Confidence Score: 4/5

  • This PR is safe to merge; the new parser is well-isolated, backward-compatible, and covered by 20 unit tests.
  • The implementation is thorough, all three previously flagged bugs are fixed, and 349 extension tests pass. The only remaining concern is a minor cosmetic output issue in the blockquote handler (trailing > empty lines) that does not affect correctness or cause errors.
  • No files require special attention beyond the minor blockquote output issue in extensions/feishu/src/card-parser.ts.

Last reviewed commit: 8c028f0

Comment thread extensions/feishu/src/card-parser.ts Outdated
}

case "heading": {
const level = (obj.level as number) || 1;
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.

Heading level always defaults to 1

The code reads level from obj.level, but the Feishu card JSON stores it in obj.property.level (as shown in the fixture). Since obj.level is always undefined, every heading falls back to level 1 and renders as #, regardless of the actual heading depth in the input.

The test fixture encodes this buggy behavior — all expected headings are # even when the source JSON has "level": 2 or "level": 3.

Fix by checking the property object as well, consistent with how getContent already handles property fallbacks:

Suggested change
const level = (obj.level as number) || 1;
const level = (obj.level as number) || ((obj.property as Record<string, unknown>)?.level as number) || 1;
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/feishu/src/card-parser.ts
Line: 91

Comment:
**Heading level always defaults to 1**

The code reads `level` from `obj.level`, but the Feishu card JSON stores it in `obj.property.level` (as shown in the fixture). Since `obj.level` is always undefined, every heading falls back to level `1` and renders as `#`, regardless of the actual heading depth in the input.

The test fixture encodes this buggy behavior — all expected headings are `#` even when the source JSON has `"level": 2` or `"level": 3`.

Fix by checking the `property` object as well, consistent with how `getContent` already handles property fallbacks:

```suggestion
        const level = (obj.level as number) || ((obj.property as Record<string, unknown>)?.level as number) || 1;
```

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

Comment on lines +142 to +162
case "button": {
const property = obj.property as Record<string, unknown> | undefined;
const text = property?.text;
const actions = property?.actions as CardElement[] | undefined;
if (actions && Array.isArray(actions)) {
return actions
.map((ia) => {
const iaObj = ia as Record<string, unknown>;
const url = (iaObj.action as Record<string, unknown>)?.url || iaObj.url;
const textObj = text as Record<string, unknown> | undefined;
const textProp = textObj?.property as CardElement | undefined;
const btnText =
typeof text === "string"
? text
: (textProp?.content as string) || (textObj?.content as string) || "button";
return url ? `[${btnText}](${url})` : btnText;
})
.join(" ");
}
return "";
}
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.

Button text silently dropped when no actions array

The button handler only produces output when an actions array exists. If a button has a text property but no actions, the label is completely lost and an empty string is returned. The btnText variable is computed inside the actions loop, so it's only used when actions are present.

Consider falling back to render button text alone when no actions are available:

Suggested change
case "button": {
const property = obj.property as Record<string, unknown> | undefined;
const text = property?.text;
const actions = property?.actions as CardElement[] | undefined;
if (actions && Array.isArray(actions)) {
return actions
.map((ia) => {
const iaObj = ia as Record<string, unknown>;
const url = (iaObj.action as Record<string, unknown>)?.url || iaObj.url;
const textObj = text as Record<string, unknown> | undefined;
const textProp = textObj?.property as CardElement | undefined;
const btnText =
typeof text === "string"
? text
: (textProp?.content as string) || (textObj?.content as string) || "button";
return url ? `[${btnText}](${url})` : btnText;
})
.join(" ");
}
return "";
}
}
// Fallback: render button text without a URL
const textObj = text as Record<string, unknown> | undefined;
const textProp = textObj?.property as CardElement | undefined;
const btnText =
typeof text === "string"
? text
: (textProp?.content as string) || (textObj?.content as string) || "";
return btnText;
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/feishu/src/card-parser.ts
Line: 142-162

Comment:
**Button text silently dropped when no `actions` array**

The `button` handler only produces output when an `actions` array exists. If a button has a `text` property but no `actions`, the label is completely lost and an empty string is returned. The `btnText` variable is computed inside the actions loop, so it's only used when actions are present.

Consider falling back to render button text alone when no actions are available:

```suggestion
        }
        // Fallback: render button text without a URL
        const textObj = text as Record<string, unknown> | undefined;
        const textProp = textObj?.property as CardElement | undefined;
        const btnText =
          typeof text === "string"
            ? text
            : (textProp?.content as string) || (textObj?.content as string) || "";
        return btnText;
```

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

Comment on lines +219 to +235
case "code_block": {
const property = obj.property as Record<string, unknown> | undefined;
const contents = property?.contents as Array<Record<string, unknown>> | undefined;
if (contents && Array.isArray(contents)) {
const text = contents
.map((c) => {
const inner = (c.contents as unknown) || c;
if (Array.isArray(inner)) {
return inner.map((x) => (x as Record<string, unknown>).content || "").join("");
}
return ((inner as Record<string, unknown>).content as string) || "";
})
.join("");
return `\n\`\`\`\n${text}\n\`\`\`\n`;
}
return "";
}
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.

Code block language identifier not included

The code_block handler ignores the language field from the card JSON (e.g., "language": "javascript"). The test fixture contains code blocks with language: "bash" and language: "javascript", but the output always renders as bare ``` without any language hint, losing syntax highlighting in markdown renderers.

Include the language identifier in the fenced code block:

Suggested change
case "code_block": {
const property = obj.property as Record<string, unknown> | undefined;
const contents = property?.contents as Array<Record<string, unknown>> | undefined;
if (contents && Array.isArray(contents)) {
const text = contents
.map((c) => {
const inner = (c.contents as unknown) || c;
if (Array.isArray(inner)) {
return inner.map((x) => (x as Record<string, unknown>).content || "").join("");
}
return ((inner as Record<string, unknown>).content as string) || "";
})
.join("");
return `\n\`\`\`\n${text}\n\`\`\`\n`;
}
return "";
}
return `\n\`\`\`${(property?.language as string) || ""}\n${text}\n\`\`\`\n`;
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/feishu/src/card-parser.ts
Line: 219-235

Comment:
**Code block language identifier not included**

The `code_block` handler ignores the `language` field from the card JSON (e.g., `"language": "javascript"`). The test fixture contains code blocks with `language: "bash"` and `language: "javascript"`, but the output always renders as bare ` ``` ` without any language hint, losing syntax highlighting in markdown renderers.

Include the language identifier in the fenced code block:

```suggestion
          return `\n\`\`\`${(property?.language as string) || ""}\n${text}\n\`\`\`\n`;
```

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: 95ffdce76e

ℹ️ 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 thread extensions/feishu/src/card-parser.ts Outdated
Comment on lines +91 to +93
const level = (obj.level as number) || 1;
const content = getContent(obj);
if (content) return `${"#".repeat(level)} ${content}\n`;
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 Read heading level from the element property

Feishu heading nodes in raw card payloads carry their level under property.level (the new fixture uses that shape), but this parser reads obj.level, so every heading without a top-level level is rendered as H1. That flattens section hierarchy in quoted card content and degrades the structure the agent receives from multi-level docs/cards.

Useful? React with 👍 / 👎.

try {
if (!obj.tag && (obj.body || obj.header)) {
const parts: string[] = [];
if (obj.header) parts.push(parseFeishuCardToMarkdown(obj.header as CardElement, depth + 1));
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 Parse top-level card headers before generic fallback

Top-level Feishu card headers are commonly objects like { header: { title: { tag: "plain_text", content: "..." } } }, but this path sends obj.header back into the generic parser, which has no title handling for untagged header objects and emits an [unknown: ...] block instead of the title text. As a result, quoted interactive cards with headers lose their primary heading content.

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: 0f4378d120

ℹ️ 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 thread extensions/feishu/src/card-parser.ts Outdated
Comment on lines +131 to +133
const title =
(obj.title as string) || ((obj.property as Record<string, unknown>)?.title as string);
return title ? `# ${parseFeishuCardToMarkdown({ content: title }, depth + 1)}\n` : "";
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 Parse object titles in card_header nodes

card_header title fields are often rich-text objects (for example plain_text nodes), but this code coerces obj.title / obj.property.title to a string and then re-parses that value. When the title is an object, the output becomes # [object Object] instead of the real header text, so quoted interactive cards lose their primary heading content.

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: 8a6a3892f8

ℹ️ 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 +47 to +49
if (!obj.tag && (obj.body || obj.header)) {
const parts: string[] = [];
if (obj.header) parts.push(parseFeishuCardToMarkdown(obj.header as CardElement, depth + 1));
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 Parse top-level header title instead of recursing generically

When a raw card includes a top-level header object like { "header": { "title": { "tag": "plain_text", "content": "..." } } }, this branch recurses into obj.header as an untagged object, which falls through to the default formatter and emits [unknown: ...] instead of the actual title text. In quoted interactive cards that use this common header shape, the primary heading is lost, so the parsed markdown omits key context.

Useful? React with 👍 / 👎.

@just2gooo just2gooo force-pushed the feat/feishu-card-parser branch 3 times, most recently from 3d4a143 to d0aec1c Compare March 11, 2026 12:54
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: d0aec1c735

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

try {
if (!obj.tag && (obj.body || obj.header)) {
const parts: string[] = [];
if (obj.header) parts.push(parseFeishuCardToMarkdown(obj.header as CardElement, depth + 1));
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 Handle top-level header.title objects explicitly

When the card root has a header object (for example the shape produced by extensions/feishu/src/streaming-card.ts with header.title.tag = "plain_text"), this line recurses into the untagged header object and falls through to the unknown formatter, yielding [unknown: ...] instead of the actual title text. As a result, quoted interactive cards lose their primary heading context even though the title is present in payload.

Useful? React with 👍 / 👎.

if (content) return content;
const elems = getElements(obj);
if (elems) {
return elems.map((e) => parseFeishuCardToMarkdown(e, depth + 1)).join("");
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 Avoid concatenating block children without separators

For markdown nodes that carry rich children in property.elements, this joins every child with "", so block-level nodes (for example blockquote, which exists in test/fixtures/feishu-card-parser/card-input.json) get glued to adjacent paragraphs (API.Note in the fixture output) instead of being separated as distinct blocks. This degrades readability and loses card structure in quoted content.

Useful? React with 👍 / 👎.

@just2gooo just2gooo force-pushed the feat/feishu-card-parser branch 6 times, most recently from 80b8030 to 212348f Compare March 11, 2026 13:37
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: 212348fbff

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

"| " +
columns
.map((_, idx) => {
const cell = row[idx.toString()] as Record<string, 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 Read table cells using declared column names

This parser indexes each row with idx.toString() instead of the column’s declared key, so any table whose columns[].name values are not "0", "1", etc. will render blank cells even though row data exists. In cards that use semantic column keys, quoted table content loses most of its data.

Useful? React with 👍 / 👎.

Comment on lines +200 to +204
((obj.property as Record<string, unknown>)?.url as string);
const text =
(obj.text as string) ||
((obj.property as Record<string, unknown>)?.text as string) ||
"link";
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 Normalize object fields in action_link nodes

The action_link branch assumes url and text are strings, so payloads that provide structured values (for example property.url as { url: "..." } or rich-text text) are interpolated as [object Object] and produce broken markdown links. This causes quoted cards with structured action links to lose readable/clickable link output.

Useful? React with 👍 / 👎.

@just2gooo just2gooo force-pushed the feat/feishu-card-parser branch from 212348f to 0beb8ec Compare March 11, 2026 13:47
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: 0beb8ec6ac

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

return actions
.map((ia) => {
const iaObj = ia as Record<string, unknown>;
const url = (iaObj.action as Record<string, unknown>)?.url || iaObj.url;
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 Normalize object URLs before rendering button links

When a button action carries a structured URL (for example { "url": "https://..." }, which is a shape already handled in the link branch), this code stores the whole object in url and interpolates it directly, producing markdown like [Go]([object Object]) instead of a clickable link. This breaks quoted card output for cards whose button action URL is object-typed rather than a plain string.

Useful? React with 👍 / 👎.

…ntent

- Add card-parser.ts for parsing Feishu card JSON to markdown
- Support 15+ element types: heading, list, table, code_block, link, button, etc.
- Add card_msg_content_type=raw_card_content param when fetching quoted messages
- Add blockquote support with > prefix
- Add unit tests with test fixtures
- Exclude test/fixtures/ from oxfmt formatting
@just2gooo just2gooo force-pushed the feat/feishu-card-parser branch from 0beb8ec to 8c028f0 Compare March 11, 2026 13:55
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: 8c028f0119

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


function cleanMarkdown(text: string): string {
if (typeof text !== "string") return "";
return text.replace(/\n{3,}/g, "\n\n").trim();
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 blank lines when normalizing parsed markdown

The global text.replace(/\n{3,}/g, "\n\n") normalization also runs inside fenced code blocks, so cards containing code with intentional multiple blank lines are silently rewritten (e.g. line1\n\n\nline2 becomes line1\n\nline2). This loses fidelity of quoted interactive card content and can change the meaning/readability of code snippets users are replying to.

Useful? React with 👍 / 👎.

@just2gooo
Copy link
Copy Markdown
Author

Addressing Bot Review Feedback

All review suggestions from @greptile-apps and @chatgpt-codex-connector have been reviewed.

Fixed in latest commit

Issue Status
Heading level always defaults to 1 (greptile, codex P2) Fixed. Reads from both obj.level and obj.property.level. Unit test added.
Button text silently dropped (greptile) Fixed. Fallback renders label directly when no actions. Unit test added.
Code block language not included (greptile) Fixed. Language identifier now included. Unit test added.
card_header object title (codex P1/P2) Fixed. Handles both object and string titles via recursive parsing. Unit test added.
Top-level header.title objects (codex P1) Fixed. Recurses into card_header handler. Unit test added.
Block children without separators (codex P2) Fixed. Added explicit blockquote handler with > prefix. Unit test added.

Acknowledged (acceptable for current scope)

Issue Rationale
Table cells by column name (codex P2) Feishu API consistently uses "0", "1" as column names. Can be enhanced if non-numeric names are encountered.
Object URLs in action_link/button (codex P2) link case already handles nested { url: "..." }. Structured URLs in action_link/button not observed in real payloads.
Blank lines in code blocks (codex P2) Triple blank lines in Feishu card code blocks are extremely rare. Acceptable tradeoff.

@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

@just2gooo
Copy link
Copy Markdown
Author

Addressing Bot Review Feedback

All review suggestions from @greptile-apps and @chatgpt-codex-connector have been reviewed.

Fixed in latest commit

Issue Status
Heading level always defaults to 1 (greptile, codex P2) Fixed. Reads from both obj.level and obj.property.level. Unit test added.
Button text silently dropped (greptile) Fixed. Fallback renders label directly when no actions. Unit test added.
Code block language not included (greptile) Fixed. Language identifier now included. Unit test added.
card_header object title (codex P1/P2) Fixed. Handles both object and string titles via recursive parsing. Unit test added.
Top-level header.title objects (codex P1) Fixed. Recurses into card_header handler. Unit test added.
Block children without separators (codex P2) Fixed. Added explicit blockquote handler with > prefix. Unit test added.

Acknowledged (acceptable for current scope)

Issue Rationale
Table cells by column name (codex P2) Feishu API consistently uses "0", "1" as column names. Can be enhanced if non-numeric names are encountered.
Object URLs in action_link/button (codex P2) link case already handles nested { url: "..." }. Structured URLs in action_link/button not observed in real payloads.
Blank lines in code blocks (codex P2) Triple blank lines in Feishu card code blocks are extremely rare. Acceptable tradeoff.

@chatgpt-codex-connector
Copy link
Copy Markdown

To use Codex here, create a Codex account and connect to github.

Comment on lines +143 to +156
const elems = getElements(obj);
if (elems) {
const inner = elems.map((e) => parseFeishuCardToMarkdown(e, depth + 1)).join("");
return (
"\n" +
inner
.split("\n")
.map((line) => `> ${line}`)
.join("\n") +
"\n"
);
}
return "";
}
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.

Blockquote prefixes blank lines with > (trailing space)

When an inner element produces content that ends in a newline (e.g., headings return "# text\n\n", code blocks, or hr elements), the .split("\n") produces trailing empty strings that each get > prepended. This results in lines like > (blockquote marker with a trailing space) in the rendered output, which is technically valid CommonMark but clutters the output and may be visually odd in raw markdown views.

Consider filtering empty lines before prefixing, or trimming trailing newlines from inner before splitting:

Suggested change
const elems = getElements(obj);
if (elems) {
const inner = elems.map((e) => parseFeishuCardToMarkdown(e, depth + 1)).join("");
return (
"\n" +
inner
.split("\n")
.map((line) => `> ${line}`)
.join("\n") +
"\n"
);
}
return "";
}
case "blockquote": {
const elems = getElements(obj);
if (elems) {
const inner = elems.map((e) => parseFeishuCardToMarkdown(e, depth + 1)).join("");
return (
"\n" +
inner
.trimEnd()
.split("\n")
.map((line) => `> ${line}`)
.join("\n") +
"\n"
);
}
return "";
}
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/feishu/src/card-parser.ts
Line: 143-156

Comment:
**Blockquote prefixes blank lines with `> ` (trailing space)**

When an inner element produces content that ends in a newline (e.g., headings return `"# text\n\n"`, code blocks, or `hr` elements), the `.split("\n")` produces trailing empty strings that each get `> ` prepended. This results in lines like `> ` (blockquote marker with a trailing space) in the rendered output, which is technically valid CommonMark but clutters the output and may be visually odd in raw markdown views.

Consider filtering empty lines before prefixing, or trimming trailing newlines from `inner` before splitting:

```suggestion
      case "blockquote": {
        const elems = getElements(obj);
        if (elems) {
          const inner = elems.map((e) => parseFeishuCardToMarkdown(e, depth + 1)).join("");
          return (
            "\n" +
            inner
              .trimEnd()
              .split("\n")
              .map((line) => `> ${line}`)
              .join("\n") +
            "\n"
          );
        }
        return "";
      }
```

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

@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 27, 2026
@clawsweeper
Copy link
Copy Markdown
Contributor

clawsweeper Bot commented Apr 27, 2026

Closing this as duplicate or superseded after Codex automated review.

Close this PR as superseded. The focused quoted/replied Feishu interactive-card extraction was merged in #72397, while the broader raw-card/full inbound parser scope remains tracked by #41609 and #56795.

Best possible solution:

Close this PR as superseded. Keep the merged #72397 implementation for the focused quoted/replied-card extraction, continue broader inbound/raw-card parsing review in #41609 and #56795, and reintroduce richer markdown element support only as a narrower Feishu parser patch with focused fixtures and RawBody compatibility review.

What I checked:

So I’m closing this here and keeping the remaining discussion on the canonical linked item.

Codex Review notes: model gpt-5.5, reasoning high; reviewed against 91e835ebe0ab; fix evidence: commit c6cf37068cae.

@clawsweeper clawsweeper Bot closed this Apr 27, 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: XL stale Marked as stale due to inactivity

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant