Skip to content

fix(feishu): refine streaming card delivery#53534

Closed
allan0509 wants to merge 5 commits intoopenclaw:mainfrom
allan0509:codex/pr-feishu-streaming-card
Closed

fix(feishu): refine streaming card delivery#53534
allan0509 wants to merge 5 commits intoopenclaw:mainfrom
allan0509:codex/pr-feishu-streaming-card

Conversation

@allan0509
Copy link
Copy Markdown

Summary

  • throttle tiny intermediate streaming-card edits so Feishu cards update on meaningful deltas or natural text boundaries
  • prefer a single markdown card for long structured final replies when live streaming is unavailable
  • fall back to chunked markdown cards only after single-card delivery fails, with updated dispatcher coverage

Testing

  • pnpm test -- extensions/feishu/src/reply-dispatcher.test.ts
  • pnpm test -- extensions/feishu/src/streaming-card.test.ts

Notes

  • this PR is scoped to streaming-card delivery behavior and its tests

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

greptile-apps Bot commented Mar 24, 2026

Greptile Summary

This PR refines Feishu streaming-card delivery in two ways: it improves the throttle logic in streaming-card.ts (160 ms window, force-flush on natural language boundaries or meaningful character deltas) and removes the note/header complexity from the streaming card session. The streaming-card.ts changes themselves are solid. However, the PR is scoped to only two files — streaming-card.ts and reply-dispatcher.test.ts — while reply-dispatcher.ts (unchanged) still uses the old API:

  • It calls streaming.close(text, { note: finalNote }) — the second argument was removed from close()'s signature; TypeScript will error and the note is silently dropped at runtime.
  • It calls streaming.start(…, { …, header, note })note was removed from StreamingStartOptions; TypeScript will flag excess property.
  • It still calls sendStructuredCardFeishu for non-streaming card paths, but the test mock no longer exports that function.

As a result, the updated tests in reply-dispatcher.test.ts assert behavior that the current reply-dispatcher.ts implementation does not yet match, meaning these tests will fail on the test runs called out in the PR description (pnpm test -- extensions/feishu/src/reply-dispatcher.test.ts). The PR needs a corresponding update to reply-dispatcher.ts to replace sendStructuredCardFeishu with sendMarkdownCardFeishu, drop note from both start() and close(), and remove the header argument from start() if that is also intended.

Confidence Score: 2/5

  • Not safe to merge — updated tests are written against an unimplemented reply-dispatcher.ts API and will fail as-is.
  • The streaming-card.ts changes are well-reasoned, but the companion reply-dispatcher.ts was not updated to match the new API (close() signature, removed note from start(), sendMarkdownCardFeishu vs sendStructuredCardFeishu). The test suite explicitly targets these paths and the mismatches would cause immediate TypeScript errors and runtime test failures.
  • extensions/feishu/src/reply-dispatcher.ts (not in diff) — needs to be updated to use sendMarkdownCardFeishu, remove note from streaming.start(), and remove the second argument from streaming.close().
Prompt To Fix All With AI
This is a comment left during a code review.
Path: extensions/feishu/src/reply-dispatcher.test.ts
Line: 17-20

Comment:
**`sendStructuredCardFeishu` removed from mock but still used in implementation**

`sendStructuredCardFeishu` has been removed from the `vi.mock("./send.js", ...)` factory, yet `reply-dispatcher.ts` (line 424) still calls it:

```ts
await sendStructuredCardFeishu({ cfg, to: chatId, text: chunk, ... });
```

Because the module mock now only exports `sendMessageFeishu` and `sendMarkdownCardFeishu`, `sendStructuredCardFeishu` will resolve to `undefined` in tests. Any test that hits a non-streaming card path — such as "passes replyInThread to sendMarkdownCardFeishu for card text" (`renderMode: "card", streaming: false`) and "disables streaming for thread replies and keeps reply metadata" (`threadReply: true`) — will throw `TypeError: sendStructuredCardFeishu is not a function`.

The test assertions expect `sendMarkdownCardFeishuMock` to have been called, but the production code hasn't been updated to call `sendMarkdownCardFeishu`. `reply-dispatcher.ts` needs to be updated alongside these test changes for them to pass.

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/reply-dispatcher.test.ts
Line: 357-361

Comment:
**Test assertions diverge from actual `streaming.start()` call arguments**

The test now asserts `streaming.start` is called with exactly `{ replyToMessageId, replyInThread, rootId }`, but `reply-dispatcher.ts` (lines 275–282) still passes `header` and `note` as well:

```ts
await streaming.start(chatId, resolveReceiveIdType(chatId), {
  replyToMessageId,
  replyInThread: effectiveReplyInThread,
  rootId,
  header: cardHeader,   // still present
  note: cardNote,       // still present (also removed from StreamingStartOptions)
});
```

`toHaveBeenCalledWith` uses deep equality, so these tests will fail because the actual third argument has two extra keys. The same mismatch applies to the "passes replyToMessageId and replyInThread to streaming.start()" test at line 691.

`reply-dispatcher.ts` must drop `header` and `note` from the `start()` call (matching the removed `StreamingCardOptions`) for these assertions to hold.

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/streaming-card.ts
Line: 415-416

Comment:
**`this.state` not cleared after `close()` completes**

The previous code explicitly nulled `this.state` and `this.pendingText` at the end of `close()`:

```ts
// old
const finalState = this.state;
this.state = null;
this.pendingText = null;
this.log?.(`Closed streaming: cardId=${finalState.cardId}`);
```

The new code removed those assignments, so after `close()` returns the instance continues to hold `cardId`, `messageId`, `currentText`, and any remaining `pendingText` in memory indefinitely. `this.closed = true` correctly prevents any further API calls, but the heap references linger until the session object itself is GC'd. Consider adding `this.pendingText = null` (and optionally `this.state = null`) after the final log line so closed sessions release their data promptly.

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

Reviews (1): Last reviewed commit: "Feishu: refine streaming card delivery" | Re-trigger Greptile

Comment thread extensions/feishu/src/reply-dispatcher.test.ts
Comment thread extensions/feishu/src/reply-dispatcher.test.ts Outdated
Comment thread extensions/feishu/src/streaming-card.ts Outdated
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: eddbc04de5

ℹ️ 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/streaming-card.ts Outdated
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: 132792daf8

ℹ️ 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/streaming-card.ts
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: afb251c035

ℹ️ 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/streaming-card.ts Outdated
import type { Client } from "@larksuiteoapi/node-sdk";
import { fetchWithSsrFGuard } from "../runtime-api.js";
import { resolveFeishuCardTemplate, type CardHeaderConfig } from "./send.js";
import { fetchWithSsrFGuard } from "openclaw/plugin-sdk/feishu";
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 local runtime import to avoid self-referential SDK load

Importing fetchWithSsrFGuard from openclaw/plugin-sdk/feishu inside extensions/feishu creates a self-import through the extension’s public SDK facade, which eagerly pulls in unrelated Feishu re-exports (including ../../extensions/feishu/setup-api.js and ../../extensions/feishu/api.js from src/plugin-sdk/feishu.ts) during normal reply streaming. That increases circular-initialization risk and couples runtime message delivery to setup surfaces; it also violates the explicit root guideline in AGENTS.md (“inside an extension package, do not import that same extension via openclaw/plugin-sdk/<extension> from production files”). This should keep using the local runtime barrel instead of the SDK self-import.

Useful? React with 👍 / 👎.

@allan0509
Copy link
Copy Markdown
Author

The latest PR head 218efb5fa8 is green on CI and the PR is currently CLEAN / MERGEABLE on my side.

I attempted to merge from this account, but GitHub rejected MergePullRequest due to missing upstream permissions.

This one looks ready for a maintainer to merge.

@vincentkoc vincentkoc added dedupe:child Duplicate issue/PR child in dedupe cluster close:superseded PR close reason labels Apr 25, 2026
@vincentkoc vincentkoc self-assigned this Apr 25, 2026
@vincentkoc
Copy link
Copy Markdown
Member

Thanks for pushing this. I landed the current canonical version in #71523, with the changelog credit preserving your representative streaming-card delivery path.

I’m closing this PR as superseded because the new PR rebases the throttle/pending-flush behavior onto current main and adds current dispatcher coverage for one-card final delivery. If there’s a gap from your original branch that I missed, tell me and I can reopen/re-check it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: feishu Channel integration: feishu close:superseded PR close reason dedupe:child Duplicate issue/PR child in dedupe cluster size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants