Failure notices (turn failures, sub-flow errors from list/card/preset/
model/compact/stop/bind/switch paths, batch-input results, and the
card-queue rate-limit notice) were sent as fresh messages keyed only by
chat_id, so in topic groups they landed in the group's default area
instead of the topic the interaction belongs to.
- #sendFailure accepts options.replyTo; every call site forwards the
anchor available in its context (command message, card message, or
watch/list anchor)
- #handleMessageFailure and #finishBatchResult thread to the triggering
message
- /repair stays private-chat-only by design; proactive delivery keeps
its existing behavior (no thread context exists there)
- lib/index.js: regenerated host bundle
Watch-completion pushes rebuilt a fresh completion card keyed only by
chat_id, so in topic groups the "task finished" notice landed in the
group's default area instead of the topic where the watch was created.
- #runWatch persists the anchor message id (command message or card)
on the durable watch entry
- #deliverCompletion sends the completion card as a reply to that
anchor; entries without an anchor keep the previous behavior
- batch watch_add passes the card message as the anchor
- lib/index.js: regenerated host bundle
Card-button sub-flows (bind session, watch/unwatch, workspace switch,
compact, stop, steer, preset/model selection and their confirmations)
sent fresh messages keyed only by chat_id, so in topic groups the
confirmations landed in the group's default area instead of the topic
containing the card.
- #handleCardAction anchors every confirmation and sub-flow to the
card's message id; #handleMenuPick anchors to the replying message
- #bindSession / #switchWorkspace / #handleCompact / #handleStop /
#handlePreset* / #handleModelSelect / #sendSteer accept and forward
the anchor; the expired-menu and steer-form notices follow suit
- lib/index.js: regenerated host bundle
Plain command replies (menu cards, session/workspace/watch lists, /new,
/status, /compact, /archived, model and preset commands) and Harness
approval prompts were sent as fresh messages keyed only by chat_id, so
in topic groups they landed in the group's default area instead of the
topic the conversation belongs to.
Deliver them as replies to the triggering message, mirroring how stream
answers already thread:
- #sendCard accepts { replyTo } and replies interactively, falling back
to a plain card when the referenced message is gone
- command replies, status text, menu/session/workspace/watch cards,
approval prompts and resolved-question notices all carry the
triggering message id
- batch-input failure notices follow the same rule
- bridge.test.mjs: /new in a topic group must thread the confirmation
text and the menu card to the command message
- lib/index.js: regenerated host bundle
Regular answers reuse the stream card, which is created through the
reply API targeting the triggering message, so they land in the right
Feishu thread. Pending Harness questions (ask_user_question) were sent
through a fresh message.create with only the chat_id, so in topic
groups they appeared in the group's default area instead of the topic
the conversation belongs to.
Thread question delivery the same way as answers:
- #send accepts { replyTo } and uses im.v1.message.reply, falling back
to a plain message when the referenced one is gone
- the triggering message id now travels into the interaction context
and pending state, and #presentInteraction replies to it
- resolved-question notices reply to the user's answer message
- bridge.test.mjs: assert the question is delivered via the reply API
targeting the triggering message inside a topic group
- lib/index.js: regenerated host bundle
conversationKey derived the group session key from chat_id alone, so in
Feishu topic groups every topic shared one Harness session: context,
/new, pending approvals, and batch-input state all leaked across topics.
Append the event's thread_id to the session key when present. Topic
groups stamp every message with a thread_id, so each topic now maps to
its own group:<chat_id>🧵<thread_id> session. Regular group chats
carry no thread_id and keep the single shared group:<chat_id> key; p2p
keys are untouched.
- message-utils.mjs: thread-aware conversationKey
- message-utils.test.mjs: topic isolation + regular-group fallback cases
- lib/index.js: regenerated host bundle
The Feishu bot now calls the app_slash_commands OpenAPI at startup to register its common commands (menu, new, help, status, compact, sessionlist, workspacelist, watch, unwatch, watchlist, archived) as native Slash Commands, so typing / in a Feishu direct-message input box pops the command panel and tapping a command triggers it.
The command list is owned and pushed by dsh-im; it does not depend on the dsh/Harness backend. Registration is best-effort and runs asynchronously, so it never blocks the long-connection startup or message delivery when the app lacks the application:app_slash_command:read/write scopes.
Adds src/channels/feishu/slash-command-registry.mjs (manifest, list/create/delete, idempotent sync), wires it into feishu-runtime.mjs (slashCommands config flag + status fields), and covers it with tests.
Keep a bounded streaming preview, then deliver the complete final answer across multiple cards. Preserve all provider message IDs and cover long snapshots and Unicode boundaries with regression tests.
Fixes#78
Add shared /history parsing, read-only session access, message filtering, and bounded replies. Default to three messages, cap at five, and exclude saved history commands.
Include channel integration tests, translations, command help, and the Issue #62 implementation plan.
Treat attachment-free Feishu rich-text posts containing bot commands like ordinary text commands instead of forwarding them to the Harness.
Co-authored-by: OpenClaw Agent <agent@openclaw.ai>
- Move preset/model/archive controls from settings card to main menu
- Arrange 4 dropdowns in 2x2 grid with external icons
- Align steer and archive side by side
- Replace #showSettingsCard with #sendMenuCard for preset/model/archive updates
- Add #sendMenuCard after 'new' button action for consistency
- Update help card to reflect current menu layout (12 items)
- Clean up duplicate sessionlist entries in menuHelpText
- Add missing English dictionary entries
Wrap all user-facing chat text in src/channels with a host-side t()
translator (Chinese is the identity default, so untranslated text is
always sent verbatim in Chinese). English is selected via the host
plugin config `language: en` or the DSH_IM_LANGUAGE environment
variable, and the settings UI keeps following the DSH web locale.
- add src/channels/shared/i18n.mjs with setImHostLanguage/t() and a
Chinese-key -> English dictionary split by area (shared + channels)
- complete the English dictionary for every t() key in src/channels,
including the feishu channel (welcome/help, repair flow, interactive
cards, watch flow) and the shared command/approval/stream text
- wrap previously untranslated chat-facing strings in feishu bridge,
feishu runtime, editable-message-stream, harness-client,
connection-test and image-prompt
- dedupe dictionary entries that existed in multiple channel files and
canonicalize conflicting translations into the shared dictionaries
- extend the i18n unit test to decode string-literal escapes so keys
containing \n are checked correctly
- document the language switch in README.md, README.en.md and
CHANGELOG.md, and rebuild lib/index.js
Added English support for bot chat messages across all nine IM
channels and AI Office without changing any Chinese output.
The menu card renders the repair fallback as **5**<emoji>修复, so the two assertions that matched **5**修复 are broadened to tolerate the marker emoji. The number-reply on a later session page now reads the last text confirmation instead of assuming the last outgoing message is text, since #bindSession refreshes the menu card after binding. Rebuild bundle.
- read form data from action.form_value (Card 2.0) instead of formValue
- wrap custom steer input and submit in a form container with submit button
- reuse runModelCommand for dropdown model picks so IDs with multiple / keep working
- pass key when sending the help card so back-to-menu stays valid
- compute initial_index from {value} option arrays
- add in-flight tests for menu stop, steer dropdown, and steer form
Add stop/compact/settings/watchlist buttons to the hub menu card, route repair through the numeric fallback (reply 6) without an extra button, and sync the menu help and card-structure test assertions. Rebuild the lib bundle so the published artifact carries the changes.
Replace the flat button-only command menu with an information-architected
assistant center: status summary header, session dropdown (current session
pre-selected), workspace/model/preset dropdowns with current value shown,
archive toggle, task control (stop/compact/steer), watch multi-select, and a
collapsed settings panel. All commands expose card or dropdown entry points.
Each session row is a column_set with a fixed 90px watch-toggle column
(⭐关注/⭐取关 for already-watched sessions) and a weighted session button
carrying the number label; number replies bind the session. Replaces the
stacked button pair; also honors the per-bot archived-session policy.
The session list (and the numeric /watch index) honors a per-bot persisted
policy: /archived off hides archived sessions from cards and index
resolution, /archived on restores them. Defaults to on.
- /watch resolves the target READ-ONLY: sessions are validated against
registered workspace listings (current or any other) without binding the
conversation or switching workspaces. /unwatch and /watchlist round out
the command set; the watchlist card supports button and number-reply
unwatching.
- Watches persist in the state store (sessionId + title + chatId + lastSeq)
and resume at runtime start: the bridge starts the global event-mux
watcher in its constructor when the harness supports it.
- Completion pushes fire on turn/end, deduped by sessionId + turn id, with
the seq watermark persisted per watch. After a mux reconnect the bridge
replays each watched session's recent history (session.history) through
the normal handler, so missed turn/end events are compensated without
duplicates.
- Bound-session push targets are persisted (chatTargets) instead of living
only in memory, and refresh on every accepted message.
- Watch failures map to safe user-facing messages.