Bug
Feishu approval cards (interactive message cards with Approve/Deny buttons) never visually update after the user clicks a button or the request times out. The buttons remain active but non-functional, causing user confusion.
Root Cause
_update_approval_card() in gateway/platforms/feishu.py uses im.v1.message.update (PUT) to replace the card content. However, Feishu's message.update API only supports updating text and rich-text messages — it silently ignores updates to interactive (card) messages without raising an error.
The correct API for updating card messages is im.v1.message.patch (PATCH), which is specifically designed for interactive card updates. Additionally, the original card must be sent with "update_multi": true in its config for the patch API to work.
Feishu docs: https://open.feishu.cn/document/server-docs/im-v1/message-card/patch
Fix
1. Add PatchMessage imports
from lark_oapi.api.im.v1 import (
...
PatchMessageRequest,
PatchMessageRequestBody,
)
2. Add update_multi: true to card config when sending
In send_exec_approval():
card = {
"config": {"wide_screen_mode": True, "update_multi": True},
...
}
3. Switch from message.update to message.patch in _update_approval_card()
# Before (broken — silently fails on interactive cards)
body = self._build_update_message_body(msg_type="interactive", content=payload)
request = self._build_update_message_request(message_id=message_id, request_body=body)
await asyncio.to_thread(self._client.im.v1.message.update, request)
# After (correct — uses patch API designed for cards)
body = self._build_patch_message_body(content=payload)
request = self._build_patch_message_request(message_id=message_id, request_body=body)
await asyncio.to_thread(self._client.im.v1.message.patch, request)
4. Add builder helpers
@staticmethod
def _build_patch_message_body(*, content: str) -> Any:
if "PatchMessageRequestBody" in globals():
return PatchMessageRequestBody.builder().content(content).build()
return SimpleNamespace(content=content)
@staticmethod
def _build_patch_message_request(message_id: str, request_body: Any) -> Any:
if "PatchMessageRequest" in globals():
return PatchMessageRequest.builder().message_id(message_id).request_body(request_body).build()
return SimpleNamespace(message_id=message_id, request_body=request_body)
Additional improvements needed for timeout/expired flows
- Timeout callback infrastructure (
tools/approval.py): Add register_gateway_timeout_notify() / _gateway_timeout_cbs so the adapter gets notified when a 5-minute approval timeout fires.
- Gateway bridge (
gateway/run.py): Add _approval_timeout_sync() using asyncio.run_coroutine_threadsafe to bridge sync agent thread to async adapter.
- Feishu
expire_approvals_for_session(): New method to iterate _approval_state, remove expired entries, update cards to grey "Timed Out".
- Feishu stale button click: Extract
open_message_id from callback context, update card to "Expired".
- Telegram parity: Same timeout/expired logic for Telegram adapter.
Tested
- Approve -> card turns green "Approved"
- Deny -> card turns red "Denied"
- Timeout (5 min) -> card turns grey "Timed Out" automatically
- Stale button click -> card turns grey "Expired"
Bug
Feishu approval cards (interactive message cards with Approve/Deny buttons) never visually update after the user clicks a button or the request times out. The buttons remain active but non-functional, causing user confusion.
Root Cause
_update_approval_card()ingateway/platforms/feishu.pyusesim.v1.message.update(PUT) to replace the card content. However, Feishu'smessage.updateAPI only supports updating text and rich-text messages — it silently ignores updates to interactive (card) messages without raising an error.The correct API for updating card messages is
im.v1.message.patch(PATCH), which is specifically designed for interactive card updates. Additionally, the original card must be sent with"update_multi": truein its config for the patch API to work.Feishu docs: https://open.feishu.cn/document/server-docs/im-v1/message-card/patch
Fix
1. Add
PatchMessageimports2. Add
update_multi: trueto card config when sendingIn
send_exec_approval():3. Switch from
message.updatetomessage.patchin_update_approval_card()4. Add builder helpers
Additional improvements needed for timeout/expired flows
tools/approval.py): Addregister_gateway_timeout_notify()/_gateway_timeout_cbsso the adapter gets notified when a 5-minute approval timeout fires.gateway/run.py): Add_approval_timeout_sync()usingasyncio.run_coroutine_threadsafeto bridge sync agent thread to async adapter.expire_approvals_for_session(): New method to iterate_approval_state, remove expired entries, update cards to grey "Timed Out".open_message_idfrom callback context, update card to "Expired".Tested