Resolve the current main conflict, treat reply timeouts as inactivity windows, and confirm liveness through Harness instead of sticky interaction ownership.
Address review feedback on the interaction cards:
1. Bind card decisions to the initiating actor. The card-action operator
(operatorOpenId) is now passed into the approval and question handlers:
- submitByApprovalId receives { actor } so another allowed group member
cannot approve/reject someone else's approval;
- the answer branch requires pending.actor === operator.
2. Include the question index in the answer button action and validate it
against the current pending question, so a stale card from an earlier
question in a multi-question interaction cannot be applied to the next one.
3. When both the card send and the plain-text fallback fail, the error is no
longer swallowed: it propagates so the interaction is not marked as
presented and the existing retry/reconnect behaviour can run.
Adds regression tests for all three cases.
Render Harness approval and single-choice question interactions as
interactive Feishu cards with approve/reject and answer buttons,
matching the already-available interactionCards behaviour. Cards are now
the default; set DSH_IM_INTERACTION_CARDS=0 (or interactionCards=false)
to keep the plain-text reply flow.
- bridge.mjs: approve:/reject:/answer: card actions, interactionCards
option, gated approval render hook, interactive question presentation,
and operationArguments/printableText helpers; refactor the inline
question answer into #submitQuestionAnswer so both text replies and
card buttons share one path.
- feishu-cards.mjs: approvalCard (approve/reject) and questionCard
(option buttons) templates.
- feishu-runtime.mjs: default interactionCards from env (on).
- harness-approval.mjs: optional channel render hook + submitByApprovalId.
- i18n-en: add the new card t() keys.
- Tests: pin the plain-text reply flow in state-machine suites that
drive it, and add dedicated card tests for the default approve/reject,
answer buttons, and the text fallback. Full suite shows no new
failures versus upstream.
A long-running Harness task was falsely reported as MODEL_REPLY_TIMEOUT
after the fixed 10-minute deadline even though the backend was still
working. Replace the hard wall-clock deadline with an activity-based
stall window: the wait renews whenever the turn produces new events, the
control ownership is active, or (mid-turn with no new events) Harness
still reports the session as running. Only a genuine stall with no
progress for the whole timeoutMs window fails fast.
This is the shared harness-client layer, exercised end-to-end by the
Feishu channel (which already exposes HARNESS_REPLY_TIMEOUT_MS); other
channels inherit the same fix.
Adds two regression tests: a long-running turn with periodic activity
completes instead of timing out, and a genuinely stalled turn still
fails with harness-reply-timeout.
webSocketProxyUrl() picks up https_proxy/HTTPS_PROXY/http_proxy/
HTTP_PROXY and forces the Feishu WSS long connection through the proxy,
without consulting NO_PROXY/no_proxy. With a local proxy exported in
the shell (Clash/V2Ray setups), every long-connection attempt is
rejected by Feishu's server and the runtime force-closes and retries
in a loop, so the bot never reaches a stable connected state.
Resolve NO_PROXY/no_proxy for the long-connection endpoints
(open.feishu.cn / open.larksuite.com) before falling back to the proxy
env: exact host, parent-domain (.cn, feishu.cn) and * entries follow
the common NO_PROXY semantics.
- production.mjs: NO_PROXY gate in front of the proxy lookup
- feishu-proxy.test.mjs: NO_PROXY exclusion and fallthrough cases
- 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
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
Resolve qq-bridge.mjs by keeping the inbound-file support (hasFiles,
files passthrough) and dropping the upstream stream session, which
this branch replaces with the discrete turn-feed notices.
Show what the agent is actually doing during a turn, the way Claude
Code does: interim explanation text, every Tool call, and the error
detail when a tool fails, each as its own pushed message before the
final markdown answer.
- HarnessReplyTracker.consume now returns every frame of a polling
batch in order (same-batch text frames collapse to the latest)
instead of dropping all but the last event, which silently ate
tool-call frames that shared a batch with their result
- tool frames carry callId; status frames carry the correlated
toolName and a flattened error text parsed from the tool/result
error payload (message, or name: code)
- the QQ bridge holds interim step text until the next tool call
(or the end of the turn) pushes it, emits 'Tool call <name>' per
call, emits 'Tool call <name>\nError: <reason>' for failures, and
skips re-sending interim text that already equals the final answer
Other channels keep their existing behaviour: the status frame still
reads '正在整理结果…' and they ignore the new optional fields.
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.