Skip to content

feat(feishu): interactive picker cards (/model, /l, second-step) + card-callback anchor fix - #103587

Open
Bin-Peng wants to merge 3 commits into
NousResearch:mainfrom
Bin-Peng:pr-feishu-cards
Open

Bin-Peng wants to merge 3 commits into
NousResearch:mainfrom
Bin-Peng:pr-feishu-cards

Conversation

@Bin-Peng

@Bin-Peng Bin-Peng commented Sep 5, 2026 •

Copy link
Copy Markdown

What does this PR do?

Brings the interactive-card surfaces the gateway already exposes for Telegram/Discord/Slack/Matrix to the Feishu adapter, so Feishu stops silently degrading to text listings:

  • /model sends a select_static model-picker card (full provider catalog) instead of the text fallback
  • /l (alias /list) sends a quick-command menu card with server-side allowlisted button dispatch; every non-Feishu surface falls back to /commands
  • Second-step choice-picker cards (reasoning effort / fast mode) through the gateway's generic choice picker
  • select_static callback parsing walks the action payload recursively across SDK/callback shapes (action.option.value as str or dict, action.value.key, nested option objects) instead of assuming one fixed shape, and logs a WARNING for still-unmatched callbacks so the real payload shape can be root-caused from gateway.log
  • Bug fix: synthetic card-callback events are no longer used as a Feishu reply anchor. Their message_id is a callback token/UUID, not an Feishu open_message_id, so replying to it makes the API reject the follow-up with 99992354 (invalid open_message_id). Card responses are sent as fresh messages instead.

Related Issue

Feature requests: #20520, #23588 (interactive model picker for Feishu).

Related but not fixed here: #122649 ("Feishu approval card button click not responding") is the same class of payload-shape fragility at a different call site — _on_card_action_trigger's hermes_action lookup still requires the action value to be a dict, while Feishu can deliver a namespace object. This PR fixes the picker path only; the approval path is left untouched to keep the change reviewable.

Type of Change

  • 🐛 Bug fix (non-breaking change that fixes an issue)
  • ✨ New feature (non-breaking change that adds functionality)

Changes Made

File Change
plugins/platforms/feishu/adapter.py +589: send_model_picker, send_choice_picker, send_command_list; recursive select_static value extraction; card-callback dispatch; one-shot picker state
gateway/platforms/base.py +10: suppress the reply anchor for card-callback events (the 99992354 fix)
gateway/slash_commands.py +20: _handle_l_command — card on Feishu, /commands fallback elsewhere
hermes_cli/commands.py +2: register l (alias list) in COMMAND_REGISTRY
hermes_cli/commands_platforms.py +3/-1: demote l on Slack via /hermes l so /insights stays under the 50-slash cap
gateway/run_busy.py +1/-1: add l to the busy-path plain-command allowlist
tests/gateway/test_feishu_approval_buttons.py +347: model-picker + approval-button callback cases
tests/gateway/test_feishu_list_command.py +206: /l menu + registry cases
tests/gateway/test_feishu_choice_picker.py +189: choice-picker cases

Revision note

This revision narrows the PR to the Feishu feature only (the cron start-time/crash-artifact commit it previously carried has been split out; the drift measurement from that work is shared in #111454) and makes the suite green in an environment without platform extras, which is what CI installs.

How to Test

CI-equivalent runner, no platform extras installed, Python 3.14.7:

scripts/run_tests.sh tests/gateway/test_feishu_list_command.py tests/gateway/test_feishu_choice_picker.py tests/gateway/test_feishu_approval_buttons.py -q
# → 3 files, 50 passed, 0 failed

Covered: picker card rendering and one-shot consumption, tampered index, unauthorized user, chat mismatch, /l allowlist dispatch, reply-anchor suppression for card callbacks, and the SDK-absent path: the adapter binds lark's card classes once at import time and returns None when they are unavailable, so the suites bind stand-in card classes only when lark_oapi is missing — a real SDK keeps full fidelity.

Manual: on a configured Feishu channel — /model → tap a model; /l → tap a menu entry; /model → second-step selection (reasoning/fast) → tap.

Platform tested on: macOS 26.6.2 (Python 3.14.7, and 3.11.15 with lark-oapi installed).

Checklist

Code

  • I've read the Contributing Guide
  • My commit messages follow Conventional Commits (feat(feishu): …)
  • I searched for existing PRs — see the overlap note below
  • My PR contains only changes related to this feature
  • scripts/run_tests.sh passes on the affected suites (50 passed; the full suite was not run)
  • I've added tests for my changes
  • I've tested on my platform: macOS 26.6.2

Documentation & Housekeeping

  • Documentation — N/A
  • cli-config.yaml.example — N/A
  • CONTRIBUTING.md / AGENTS.md — N/A
  • Cross-platform impact — N/A (adapter + slash-command routing only; no file I/O, process or terminal handling)
  • Tool descriptions/schemas — N/A

Note on overlapping work

Independent implementation. Other open work targets the same surface (#84476, #129974, #82170 and this one), plus feature requests #20520 / #23588. If the maintainers would rather consolidate on a different branch, say so and I'll close this or port the 99992354 fix + select_static robustness there.

@alt-glitch alt-glitch added type/feature New feature or request P3 Low — cosmetic, nice to have comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins comp/cli CLI entry point, hermes_cli/, setup wizard platform/feishu Feishu / Lark adapter sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages labels Sep 5, 2026
@Enough1122

Copy link
Copy Markdown

AI code review — automated review for reference; please use your judgment.

PR 103587 — Feishu: interactive card surfaces for /model, /l, and second-step pickers

Summary: Adds Feishu send_model_picker / send_command_list (/l) / send_choice_picker cards plus callback validation, suppresses the Feishu reply anchor for card callbacks (99992354 fix), and registers the l command. Server-side index resolution, allowlist-gated /l dispatch, and one-time pop semantics are solid. Findings are non-blocking.

Non-blocking findings:

  1. plugins/platforms/feishu/adapter.py:104-109,204-209,403-408 — _model_picker_state / _choice_picker_state entries are only removed when the picker is tapped. Cards that are never tapped (user ignores them, card expires) leak state forever; itertools.count ids also grow monotonically. Consider a TTL/eviction sweep (or a bounded size) matching whatever the approval-state path does, if anything.
  2. gateway/platforms/base.py:9-18 — anchor suppression now applies to any Feishu event whose raw payload has an action attribute, including pre-existing approval / update-prompt card callbacks, not just the new pickers. If any of those flows relied on the old anchor behavior, this silently changes them. Please confirm that is intended (tests cover the new synthetic events; existing approval-callback anchor expectations, if any, should be re-checked).
  3. plugins/platforms/feishu/adapter.py:589-622 — _scan_for_picker_value first-match-wins over the whole action payload. Since any nested string with the hermes_model_picker: prefix matches, a crafted/nested duplicate value in a tampered callback could resolve to an unexpected entry — though the subsequent index-bounds + chat + allowlist checks still gate it, and the resolved model comes from server-side state, so impact is minimal.
  4. plugins/platforms/feishu/adapter.py:310,329-337 — the synthetic /l event drops thread_id (thread_id=None) and uses a naive datetime.now() timestamp. Minor context loss for threaded chats; flagging in case thread propagation was intended.
  5. plugins/platforms/feishu/adapter.py:483-505,688-712 — picker result posts use metadata=None / reply_to=None (fresh message by design per the 99992354 fix). Consistent, just noting the result message will not thread even when the originating command was threaded.

Tests: New suites (test_feishu_choice_picker.py, test_feishu_list_command.py) plus model-picker cases cover tampered index, unauthorized user, chat mismatch, one-time consumption, and anchor suppression. Good coverage; no blocking issues found.

@Bin-Peng Bin-Peng changed the title Feishu: interactive card surfaces for /model, /l, and second-step pickers feat(feishu): interactive picker cards (/model, /l, second-step) + card-callback anchor fix Oct 4, 2026

This branch has not been deployed

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

Labels

comp/cli CLI entry point, hermes_cli/, setup wizard comp/gateway Gateway runner, session dispatch, delivery comp/plugins Plugin system and bundled plugins P3 Low — cosmetic, nice to have platform/feishu Feishu / Lark adapter sweeper:risk-message-delivery Sweeper risk: may drop, duplicate, misroute, or suppress messages type/feature New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants