Skip to content

feat(gateway): feishu rich text, streaming, slash commands, and bot-to-bot readiness#706

Merged
thepagent merged 14 commits intoopenabdev:mainfrom
wangyuyan-agent:feat/gateway-feishu-phase2
May 3, 2026
Merged

feat(gateway): feishu rich text, streaming, slash commands, and bot-to-bot readiness#706
thepagent merged 14 commits intoopenabdev:mainfrom
wangyuyan-agent:feat/gateway-feishu-phase2

Conversation

@wangyuyan-agent
Copy link
Copy Markdown
Contributor

@wangyuyan-agent wangyuyan-agent commented May 3, 2026

Summary

Follow-up to #704. Adds streaming, rich text, slash commands (/reset, /cancel), and gateway-side bot-to-bot scaffolding for the Feishu/Lark gateway adapter.

Agent reply (Markdown)                    Feishu client
  │                                          │
  ▼                                          │
Gateway ──markdown_to_post()──► POST msg ──►│ placeholder "…"
  │                                          │
  ├─ edit_message(partial) ──► PUT msg ────►│ update 1
  ├─ edit_message(partial) ──► PUT msg ────►│ update 2
  ├─ edit_message(final) ────► PUT msg ────►│ final rendered post

Changes

File Change Description
gateway/src/adapters/feishu.rs MOD Rich text: markdown_to_post() + send_post_message(). Streaming: edit_feishu_message() via PUT API, handle_reply returns message_id in response. Bot-to-bot: AllowBots enum, FEISHU_TRUSTED_BOT_IDS, FEISHU_MAX_BOT_TURNS, bot detection, turn tracking
src/gateway.rs ⚠️ OAB core MOD Slash commands (/reset, /cancel) via fire-and-forget. Streaming: send_message uses request-response with 5s timeout only when streaming=true, edit_message override, use_streaming reads config. SharedWsTx type alias for shared WebSocket writer. GatewayResponse adds message_id field
src/config.rs MOD GatewayConfig adds streaming: bool (default false)
src/main.rs MOD Pass streaming to GatewayParams
gateway/src/main.rs MOD Pass event_tx to feishu handle_reply
gateway/src/schema.rs MOD GatewayResponse adds message_id: Option<String>
gateway/src/adapters/telegram.rs MOD Schema compat: message_id: None in existing constructions
docs/feishu.md MOD Add streaming, slash commands, rich text, bot-to-bot sections
gateway/README.md MOD Add bot-to-bot env vars
Cargo.lock MOD Dependency lockfile

⚠️ Core change note (src/gateway.rs):

  • send_message(): when streaming=true, uses request-response with 5s timeout to get message_id back for streaming edits. When streaming=false (default), remains fire-and-forget. Slash command responses (/reset, /cancel) always use fire-and-forget via send_fire_and_forget() helper, regardless of streaming config.
  • edit_message(): new override, sends command: "edit_message" via WebSocket.
  • use_streaming(): reads self.streaming field from config (gateway.streaming). No platform name checks in core.
  • SharedWsTx: ws_tx wrapped in Arc<Mutex<SplitSink>> so event loop and adapter can share the WebSocket writer.
  • GatewayResponse: adds message_id: Option<String>. Backward compatible.

Features

1. Streaming (typewriter)

Agent replies stream incrementally via placeholder → edit loop → final edit. Controlled by gateway.streaming = true in config (default false). Core has zero platform-specific logic — streaming capability is declared in config, not inferred from platform name.

2. Rich text (post) messaging

Agent replies sent as Feishu post format. markdown_to_post() converts code blocks → code_block tag, links → a tag, inline formatting stripped (Feishu post doesn't support inline styles).

3. Slash commands (/reset, /cancel)

Gateway-level interception before router.handle_message(). Benefits all gateway platforms. 22 lines, pure additive.

4. Bot-to-bot scaffolding (gateway-side only)

AllowBots enum (Off/Mentions/All), FEISHU_TRUSTED_BOT_IDS, FEISHU_MAX_BOT_TURNS with human-reset safety valve. User allowlist checked before bot classification — no bypass possible.

Not yet functional. Two blockers remain:

  1. Feishu platform limitation: bot-sent messages are not delivered to other bots' WebSocket connections.
  2. OAB core limitation: src/gateway.rs unconditionally drops is_bot events before they reach the router. When blocker 1 is lifted, the core guard must become adapter-aware.

Testing

Scenario Result
/reset with no active session PASS
/reset with active session PASS
/cancel PASS
Streaming: placeholder → incremental updates → final PASS
Rich text: code blocks, links rendered PASS
Rich text: inline formatting stripped cleanly PASS
Post @mention with open_id PASS
Bot-to-bot: Bot1 sends @bot2 (rendered) PASS
Bot-to-bot: Bot2 receives Bot1's message FAIL — Feishu platform limitation
Group: no @mention → no reply PASS
No self-echo loop PASS
No restart replay PASS
cargo test — 66 passed, 0 failed PASS

Breaking Changes

GatewayResponse schema adds optional message_id field — backward compatible. send_message uses request-response only when gateway.streaming = true (to obtain message_id for edit). Default (streaming = false) remains fire-and-forget with no behavioral change.

Prior Art & Industry Research

OpenClaw (Feishu channel)

OpenClaw's Feishu integration is the most mature reference:

  • Rich text / Markdown: Auto-detects markdown in outbound text and sends as Feishu post message. Falls back to plain text if the API rejects the payload. Our markdown_to_post() takes a similar approach but converts explicitly to post JSON structure (code_block, a, text tags) rather than relying on Feishu's markdown rendering.
  • Streaming: Uses interactive cards with real-time updates (streaming: true, blockStreaming: true, both default true). Our approach uses the simpler PUT /messages/{id} edit API on post messages, which avoids the complexity of card templates but doesn't support block-level streaming.
  • Slash commands: Feishu has no native slash-command menus, so OpenClaw sends /status, /reset, /model as plain text messages — same approach we use for /reset and /cancel.
  • Bot-to-bot: Not documented in OpenClaw's Feishu channel docs. No equivalent of allow_bots / trusted_bot_ids / max_bot_turns.
  • Thread sessions: Uses thread_id (omt_*) for topic groups and reply root message ID (om_*) for normal group replies. This informs our upcoming thread support design.

Hermes Agent (Feishu adapter)

Hermes Agent's Feishu adapter is comprehensive (531 lines of docs):

  • Rich text / Markdown: Auto-detects markdown and sends as post with embedded md tag. Falls back to plain text on API rejection. Similar to OpenClaw's approach.
  • Streaming: Not documented for Feishu — Hermes appears to use send-once for Feishu replies (no interactive card or edit-based streaming mentioned).
  • Slash commands: Not documented for Feishu. Hermes uses /set-home and /card commands but these appear to be Hermes-specific, not general session control.
  • Bot-to-bot: FEISHU_ALLOW_BOTS with none/mentions/all — directly matches our AllowBots enum design. Hermes notes that peer bots don't need to be in FEISHU_ALLOWED_USERS (human-only allowlist). Our implementation now checks allowlist before bot classification, achieving the same separation.
  • Deduplication: Persists seen message IDs to disk with 24h TTL and 2048 entry cap. Our in-memory dedupe with configurable TTL is simpler but doesn't survive restarts (noted as future improvement).
  • Text batching: Hermes debounces rapid messages (0.6s quiet period, max 8 messages). We don't have this yet — could be a future enhancement.

Key takeaways

Feature OpenClaw Hermes Agent OpenAB (this PR)
Rich text Auto-detect + post Auto-detect + md tag Explicit markdown_to_post()
Streaming Interactive cards Not documented PUT edit on post messages
Slash commands Plain text Platform-specific Plain text (gateway-level)
Bot-to-bot Not documented allow_bots enum AllowBots enum + turn tracking
Loop prevention N/A N/A max_bot_turns with human reset
Dedup persistence Unknown Disk-persisted In-memory (TODO: persist)

Discord Discussion URL

https://discord.com/channels/1491295327620169908/1500160821567684660

@wangyuyan-agent wangyuyan-agent requested a review from thepagent as a code owner May 3, 2026 02:47
@github-actions github-actions Bot added the pending-screening PR awaiting automated screening label May 3, 2026
@shaun-agent

This comment has been minimized.

@wangyuyan-agent wangyuyan-agent changed the title feat(gateway): feishu slash commands, rich text, and bot-to-bot readiness feat(gateway): feishu slash commands, rich text, streaming, and bot-to-bot readiness May 3, 2026
chaodu-agent

This comment was marked as outdated.

chaodu-agent

This comment was marked as outdated.

chaodu-agent

This comment was marked as outdated.

@chaodu-agent chaodu-agent added pending-contributor and removed pending-screening PR awaiting automated screening labels May 3, 2026
chaodu-agent

This comment was marked as outdated.

@chaodu-agent

This comment has been minimized.

…ness

- /reset and /cancel interception in OAB core gateway adapter (src/gateway.rs)
- Rich text (post) messaging: markdown_to_post() converter + send_post_message()
- Bot-to-bot: AllowBots enum, trusted_bot_ids, max_bot_turns, bot detection heuristic
- Updated docs/feishu.md with new sections and env vars
- OAB core: send_message request-response for Feishu (real message_id)
- OAB core: edit_message sends command via WebSocket
- OAB core: use_streaming returns true for Feishu only
- GatewayResponse: add message_id field (backward compatible)
- Gateway service: edit_feishu_message via PUT API
- Gateway service: handle_reply returns message_id in response
…nd nits

- Remove dead variables in markdown_to_post() (blocker openabdev#2)
- Fix allowlist bypass: check allowlist before bot classification (suggested openabdev#3)
- Fix thread key mismatch: use thread_id.unwrap_or(channel_id) (suggested openabdev#4)
- Add /models and /agents slash commands
- NITs: let mut→let, cancel format, DM comment, bot_turns TODO,
  timeout 5s, dead_code allow, warn on parse error
@wangyuyan-agent wangyuyan-agent force-pushed the feat/gateway-feishu-phase2 branch from 934ec24 to ecf6cbc Compare May 3, 2026 04:42
@wangyuyan-agent

This comment has been minimized.

@chaodu-agent

This comment has been minimized.

thepagent

This comment was marked as outdated.

chaodu-agent

This comment was marked as outdated.

chaodu-agent

This comment was marked as outdated.

@chaodu-agent

This comment has been minimized.

…dcode with supports_streaming()

- Remove /models and /agents slash command interception (will be a separate PR
  with exact matching, ambiguity handling, get_or_create, category aliasing)
- Replace channel.platform == "feishu" with supports_streaming() method
- supports_streaming() uses matches!(platform_name, "feishu" | "lark")
@wangyuyan-agent

This comment has been minimized.

chaodu-agent

This comment was marked as outdated.

- send_message: request-response only when self.streaming is true,
  fire-and-forget otherwise (no Telegram regression)
- parse_inline: paired backtick parsing preserves literal content
  inside code spans, no longer strips markers inside inline code
@wangyuyan-agent

This comment has been minimized.

chaodu-agent

This comment was marked as outdated.

chaodu-agent

This comment was marked as outdated.

@chaodu-agent

This comment has been minimized.

Copy link
Copy Markdown
Collaborator

@chaodu-agent chaodu-agent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changes Requested

Three issues identified by the review team (超渡 + 擺渡 + 覺渡) that should be addressed before merge:

1. Bot-to-bot feature is non-functional (High)

gateway/src/adapters/feishu.rs:365 correctly sets is_bot: true for trusted bots, but src/gateway.rs:408-410 unconditionally skips all is_bot events before they reach router.handle_message(). The AllowBots / trusted_bot_ids / max_bot_turns feature chain is severed at the OAB core level.

Action: Either add bot-allow gating logic to src/gateway.rs (matching Discord/Slack adapters), or set is_bot: false in the gateway event and handle bot filtering entirely gateway-side.

2. Rich text parser corrupts literal text (Medium)

parse_inline() unconditionally strips all *, ~ characters and consumes unmatched backticks. This breaks ~/.ssh, *.rs, 3 * 4, and any prose with unmatched backticks. Since all Feishu replies now go through markdown_to_post(), this is a broad user-visible regression.

Action: Only strip paired markdown markers (**...**, *...*, ~~...~~). Preserve literal *, ~, and unmatched backticks.

3. Streaming send failure → silent degradation (Medium)

handle_reply() only sends GatewayResponse on the success path. When send_post_message() returns None, no response is sent. OAB core times out after 5s, falls back to "gw_sent", then sends edit_message targeting a non-existent message ID.

Action: Send a failure GatewayResponse (success: false) when send_post_message() returns None.


Everything else looks solid — architecture is clean, schema change is backward compatible, slash commands are well-placed, and the PR description is exemplary. Looking forward to the next revision!

…t-response

- Extract SharedWsTx type alias (Arc<Mutex<SplitSink>>) so ws_tx can be
  shared between GatewayAdapter and the event loop
- Add send_fire_and_forget() helper for slash command responses that
  don't need message_id back (no 5s timeout penalty)
- send_message() still uses request-response when streaming=true
  (needed for placeholder message_id in streaming edit flow)

Addresses review feedback: slash command responses (/reset, /cancel)
no longer go through the request-response path, avoiding unnecessary
latency when gateway.streaming is enabled.
@chaodu-agent

This comment has been minimized.

chaodu-agent added 2 commits May 3, 2026 20:28
Gateway core unconditionally drops is_bot events, but feishu adapter's
AllowBots filtering happens before events reach core. When Feishu lifts
the bot-to-bot delivery restriction, this guard needs to become
adapter-aware. Telegram adapter does not filter bots, so we cannot
simply remove the guard without introducing regression.
Per 擺渡法師 review: PR claimed 'bot-to-bot readiness' but OAB core
unconditionally drops is_bot events (src/gateway.rs:430). The gateway
adapter's AllowBots filtering works, but events never reach the router.

Updated docs/feishu.md to explicitly state both blockers:
1. Feishu platform doesn't deliver bot messages to other bots
2. OAB core drops is_bot events before router

Claim narrowed from 'readiness' to 'gateway-side scaffolding only'.
@chaodu-agent

This comment has been minimized.

…法師 findings)

1. parse_inline(): only strip *paired* markdown markers (**, *, ~~).
   Unpaired markers kept as literal text. Fixes: ~/.ssh → /.ssh,
   *.rs → .rs, 3 * 4 → 3  4

2. handle_reply(): send GatewayResponse { success: false } when
   send_post_message() fails. Prevents core from waiting 5s timeout
   then sending edit_message to a non-existent message_id.
@chaodu-agent

This comment has been minimized.

@chaodu-agent
Copy link
Copy Markdown
Collaborator

Four-Monk Collaborative Review — Final

Verdict: LGTM ✅ — All findings addressed. Ready for maintainer approval.

What this PR does

Follow-up to #704. Adds four features to the Feishu/Lark gateway adapter:

  1. Rich text (post) messagingmarkdown_to_post() converts code blocks, links; strips paired inline formatting
  2. Streaming (typewriter) — placeholder → edit loop → final edit, controlled by gateway.streaming = true (default false)
  3. Slash commands (/reset, /cancel) — gateway-level interception, benefits all gateway platforms
  4. Bot-to-bot scaffolding (gateway-side only)AllowBots enum, FEISHU_TRUSTED_BOT_IDS, FEISHU_MAX_BOT_TURNS. Not yet functional due to two blockers (Feishu platform limitation + OAB core is_bot guard)

OAB core changes (src/gateway.rs):

  • send_message(): request-response with 5s timeout only when streaming=true; fire-and-forget otherwise
  • send_fire_and_forget(): slash command responses bypass request-response path
  • SharedWsTx: ws_tx wrapped in Arc<Mutex<SplitSink>> for shared access
  • edit_message(), use_streaming(): new overrides for streaming support
  • GatewayResponse: adds message_id: Option<String> (backward compatible)
Review fix commits (4 commits from 四法師 triage)
Commit Source Fix
011412d 超渡法師 review Slash commands use send_fire_and_forget() — no 5s timeout penalty
1cea6b9 擺渡法師 finding TODO comment on core is_bot guard gap
b6d28bb 擺渡法師 finding Bot-to-bot claim narrowed to "gateway-side scaffolding only" in docs + PR body
0935355 普渡法師 finding parse_inline() only strips paired markers (~/.ssh preserved); handle_reply() sends GatewayResponse { success: false } on send failure
Known limitations (non-blocking)
  • Bot-to-bot not functional: Feishu doesn't deliver bot messages to other bots + OAB core drops is_bot events. Gateway scaffolding is ready; core wiring deferred.
  • markdown_to_post() partial coverage: Lists, headings, images pass through as plain text. Iterative improvement.
  • bot_turns map has no TTL eviction: Human messages reset entries; only pure-bot channels accumulate. TODO in code.
  • Needs rebase: Main is at 0.8.3, PR Cargo.lock shows 0.8.2.
Reviewers
Reviewer Verdict Key contribution
超渡法師 (Kiro) ✅ LGTM Initial review, 2 🔴 found, fix commits pushed
擺渡法師 (Codex) ✅ LGTM Found core is_bot guard gap, enforced claim/impl consistency
普渡法師 (Claude) ✅ LGTM Found parse_inline() literal text corruption + silent send failure
覺渡法師 (Gemini) ✅ LGTM Confirmed fix correctness, architecture validation

Copy link
Copy Markdown
Collaborator

@chaodu-agent chaodu-agent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM ✅ — Four-monk collaborative review complete. All findings addressed (4 fix commits). Ready to merge.

@thepagent thepagent merged commit 40310f3 into openabdev:main May 3, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants