Skip to content

Feishu DM: MEDIA: attachment silently fails; streaming-card discard drops the image #94894

Description

@Shadow-Azure

Bug: Feishu DM — MEDIA: attachment silently fails; outbound text path also drops streaming card

Summary

In a Feishu DM (p2p) session, when the assistant replies with a MEDIA:<local-path> directive pointing at a PNG inside an allowed local-media root, the user never sees the image. The OpenClaw log shows feishu[default] Discarded streaming card: cardId=<id> followed by dispatch complete (queuedFinal=true, replies=1), but the final card delivered to the user contains no image and no explanatory error. The same happens with the read tool — the image is down-scaled to 1200px JPEG and injected into the model context, but no outbound attachment is produced.

Expected

  • The Feishu DM receives an image message with the uploaded image_key content.

Actual

  • The final card arrives in the user DM with only the bare local path echoed as text (e.g. 📎 /tmp/.../foo.png), not the image. Sometimes the entire reply is dropped (dispatch complete (replies=0)).
  • The console.error("[feishu] sendMediaFeishu failed: ...") in channel.runtime-CSBUy56o.js:539 does not appear in the gateway log, so the catch-block fallback to sendOutboundText isn't visibly firing — the failure is being swallowed even further upstream.

Reproduction

  1. Configure an OpenClaw agent with a Feishu DM channel (websocket mode).
  2. In the agent session, take any small PNG/JPG in /tmp/openclaw/... (a default preferredTmpDir local-media root).
  3. In the assistant reply, include a single line MEDIA:/tmp/openclaw/.../test.png (with or without surrounding prose).
  4. Send the message. Observe the Feishu DM on the user side: no image arrives.
  5. In the gateway log:
    • feishu[default] Started streaming: cardId=<id>, messageId=om_xxx
    • feishu[default] Discarded streaming card: cardId=<id>
    • feishu[default]: dispatch complete (queuedFinal=true, replies=1) (or replies=0 on some attempts)
    • No [feishu] sendMediaFeishu failed line.
  6. Calling the same im/v1/images + im/v1/messages endpoints directly from a standalone Node script with the same tenant_access_token (scopes: im:message) succeeds end-to-end. So the OpenClaw→Feishu path is the broken one, not the bot credentials or scopes.

Where the code path runs

  • payloads-DwYjapHo.js:341splitMediaFromOutput strips MEDIA: and pushes the path into a media[] array (the directive itself parses fine; isValidMedia returns true for absolute local paths inside preferredTmpDir).
  • web-media-BupJTPdR.jsassertLocalMediaAllowed uses fs.realpath on both the file and each allowed root, then isPathInside. On macOS the realpath-vs-resolve comparison still works because isPathInside ultimately compares path.relative of the realpathed file vs each realpathed root, and local-roots-Cc0PVjtu.js lists /tmp/openclaw/ (or its realpath) as a default root. So the allowlist check is not the blocker.
  • send-CVLKZYre.js:240uploadImageFeishu calls client.im.image.create({ data: { image_type: 'message', image: <buffer> } }). The same call works from a manual script with the same SDK client.
  • send-CVLKZYre.js:500sendMediaFeishu loads the file via loadWebMedia, uploads, then calls im.message.create. The dispatch entry is in channel.runtime-CSBUy56o.js:493 (the sendMedia adapter).
  • channel.runtime-CSBUy56o.js:539 — the catch block that would log [feishu] sendMediaFeishu failed: and fall back to sendOutboundText with 📎 <path> does not log in the failing cases I observed, so the failure is swallowed before this catch (possibly inside the streaming-card discard, or by the requestFeishuApi retry path).
  • monitor.account-CHMx6Lne.js:1274Discarded streaming card is the normal flow-control step (delete the streaming card and replace with a final card). When the image is part of that streaming card, the image resource vanishes along with the deleted message, leaving the replacement card empty.

Additional context

  • Image is < 100KB, well within Feishu limits (10MB, 12000x12000).
  • appId, appSecret, tenant_access_token, and the user open-id all work fine when called directly with urllib/https from a script (the image uploads to im/v1/images and is delivered as msg_type: image).
  • The same issue happens with the read tool's image output — confirmed it lands in agents/tool-images (down-scaled, outputMimeType image/jpeg, sourceWidth/sourceHeight logged) but never becomes an outbound attachment.
  • tools.profile (coding) strips the message tool, so we cannot call sendMediaFeishu directly from the assistant. The only OpenClaw-supported outbound path is the MEDIA: directive, which is what's broken.

Workaround

Bypass the OpenClaw feishu plugin entirely: call tenant_access_tokenim/v1/imagesim/v1/messages?receive_id_type=open_id directly. This works reliably. (A standalone Node script that does this is in our private tree; the script is the workaround, not a fix.)

Suggested fix directions (no claim, just observations)

  • Investigate why the streaming-card discard in monitor.account-CHMx6Lne.js doesn't preserve attached image resources, or why image upload errors during a streaming card aren't surfaced.
  • Make console.error in the sendMedia catch actually emit when the failure happens (it doesn't show up in the gateway log, which is the main reason this was so hard to debug).
  • The streaming card's discard() deletes the original message; if the final replacement message has no media fields set, the user sees an empty card with no error.

Metadata

Metadata

Assignees

No one assigned

    Labels

    P2Normal backlog priority with limited blast radius.clawsweeper:needs-live-reproClawSweeper needs live local, crabbox, or manual validation to confirm this issue.clawsweeper:needs-maintainer-reviewClawSweeper marked this issue as needing maintainer review before automation.clawsweeper:no-new-fix-prClawSweeper does not recommend queueing a new automated fix PR for this issue.impact:message-lossChannel message delivery can be lost, duplicated, or misrouted.issue-rating: 🐚 platinum hermitGood issue quality with a plausible reproduction path needing some confirmation.staleMarked as stale due to inactivity

    Type

    No type

    Fields

    Priority

    None yet

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions