[Bug]: Feishu post (rich text) messages silently drop top-level `files` attachments
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
- DM a Feishu bot: type a line of text, attach a
.mdfile in the same message (not as a separate send). - Gateway log shows the mismatch:
[Feishu] Received raw message type=post message_id=om_xxx [Feishu] Inbound dm message received: ... type=text ... media=0 - 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-levelfilesaway.parse_feishu_post_payload(~line 480) walks onlyresolved["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_resourceshas 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_payload → normalize_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
- Carry
filesthrough_to_post_payload(keep the "contentmust be a list" truthiness gate unchanged). - In
parse_feishu_post_payload, turn each entry into aFeishuPostMediaRef(file_key=…, file_name=…, resource_type="file"), append all of them tomedia_refs(deduped against inline refs), skipis_folder/ missingfile_key, and still emit an[Attachment: <name>]placeholder so nothing is invisible. - Set
media_text_inlinedhonestly on the builtMessageEvent, and inline a small text attachment by appending the extracted content after the caption — appending, not replacing, because the existingtext = extracted or textshape would swallow the user's actual instruction for a caption+file message. - 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