#114176·hermes-agent

[Bug]: Feishu post (rich text) messages silently drop top-level `files` attachments

Author: douwenquanCreated Sep 17, 2026Updated Sep 17, 2026
Labelstype/bugcomp/pluginsplatform/feishuP3sweeper:risk-message-delivery

Bug Description

Inbound post (rich text) messages that carry an attachment silently drop it.

When a user sends a "text + file" message in one bubble, the Feishu client delivers it as msg_type=post with the attachment in a top-level files array — not as an inline {"tag":"file"} element inside content. The adapter parses only the content rows, so media_refs ends up empty, the message normalises to type=text / media=0, no resource download is attempted, and nothing is logged. The agent answers as if no file was ever sent, while the user believes it handed the file over.

Steps to Reproduce

  1. DM a Feishu bot: type a line of text, attach a .md file in the same message (not as a separate send).
  2. Gateway log shows the mismatch:
    [Feishu] Received raw message type=post message_id=om_xxx
    [Feishu] Inbound dm message received: ... type=text ... media=0
    
  3. The agent's turn contains no attachment and no document note.

Raw body from GET /open-apis/im/v1/messages/{message_id} (identifiers redacted):

{
  "title": "",
  "content": [[{"tag": "text", "text": "please look at this", "style": []}]],
  "content_v2": [[{"tag": "md", "text": "please look at this"}]],
  "files": [{
    "file_key": "file_v3_xxx",
    "file_name": "analysis.md",
    "is_folder": false
  }]
}

Expected Behavior

The attachment is downloaded and handed to the agent exactly like a standalone msg_type=file message — a caption and a file in one bubble may arrive together. Multiple files in files must all be collected, not just the first.

Actual Behavior

The file is silently discarded. Root cause, in plugins/platforms/feishu/adapter.py:

  • _to_post_payload (~line 528) rebuilds the payload as {title, content} and throws the top-level files away.
  • parse_feishu_post_payload (~line 480) walks only resolved["content"] rows.
  • _render_post_element (~line 582) understands an inline {"tag":"file"} element, but nothing reads the top-level array.
  • FeishuNormalizedMessage.media_refs == []_download_feishu_message_resources has nothing to do → type=text / media=0, no warning, not even an [Attachment: ...] placeholder.

Verified against current main (c712f06dcd) with the captured payload:

raw_type              : post
preferred_type        : text
media_refs            : []
_to_post_payload keys : ['title', 'content']

Secondary bug in the same class: the adapter never sets MessageEvent.media_text_inlined. So once media_urls becomes non-empty, the gateway's document note (gateway/run.py::_build_document_context_note, content_inlined defaults to True) tells the model "Its content has been included below" for a file that was never inlined — a wrong-cause message where the real remedy is "read the cached path yourself".

Notes on the payload field

The public docs (接收消息内容结构, updated 2026-07-07) list post inbound fields as title / content / content_v2 plus the tags text, a, at, img, media, emotion, code_block, hr, md — there is no top-level files documented, and content_v2 has no handling in the codebase either (separate feature, out of scope here). So the fix must be defensive: skip is_folder: true and entries without a file_key, and never assume the list is present.

Workaround until fixed

Send the file as its own message (no caption in the same bubble) — that takes the msg_type=file path.

Related but distinct

  • #79256 — mention-gated replies cannot recover referenced file attachments
  • #87088 — report failed attachment downloads to the agent (different failure: the download errors, here the payload is never parsed)
  • #113830 / #75939 — outbound file/media sends into threads fail with [99992402]

Debug Report

Deliberately omitted: hermes debug share uploads system info, config and logs to a public paste service. A redacted snippet can be provided on request.

Environment: macOS, Hermes gateway via the Feishu WS long connection, Python 3.11.14.

Root Cause Analysis

_to_post_payload is the single point where the post body is narrowed to {title, content} — everything else in the post pipeline (parse_feishu_post_payloadnormalize_feishu_message_download_feishu_message_resources) only ever sees the narrowed dict. The same narrowing also means merge_forward entries (which reuse parse_feishu_post_payload) can never report a forwarded post's file.

Proposed Fix

  1. Carry files through _to_post_payload (keep the "content must be a list" truthiness gate unchanged).
  2. In parse_feishu_post_payload, turn each entry into a FeishuPostMediaRef(file_key=…, file_name=…, resource_type="file"), append all of them to media_refs (deduped against inline refs), skip is_folder / missing file_key, and still emit an [Attachment: <name>] placeholder so nothing is invisible.
  3. Set media_text_inlined honestly on the built MessageEvent, and inline a small text attachment by appending the extracted content after the caption — appending, not replacing, because the existing text = extracted or text shape would swallow the user's actual instruction for a caption+file message.
  4. Two invariant tests (payload → media_refs; event → media_text_inlined + caption preserved).

Happy to open a PR implementing the above if the approach is welcome.

Source: NousResearch/hermes-agent