* fix(feishu): use message create_time instead of Date.now() for Timestamp field
When a message is sent offline and later retried by the Feishu client
upon reconnection, Date.now() captures the *delivery* time rather than
the *authoring* time. This causes downstream consumers to see a
timestamp that can be minutes or hours after the user actually composed
the message, leading to incorrect temporal semantics — for example, a
"delete this" command may target the wrong resource because the agent
believes the instruction was issued much later than it actually was.
Replace every Date.now() used for message timestamps with the original
create_time from the Feishu event payload (millisecond-epoch string),
falling back to Date.now() only when the field is absent. The
definition is also hoisted to the top of handleFeishuMessage so that
both the pending-history path and the main inbound-payload path share
the same authoritative value.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* test(feishu): verify Timestamp uses message create_time
Add two test cases:
1. When create_time is present, Timestamp must equal the parsed value
2. When create_time is absent, Timestamp falls back to Date.now()
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* chore: revert unrelated formatting change to lifecycle.test.ts
This file was inadvertently formatted in a prior commit. Reverting to
match main and keep the PR scoped to the Feishu timestamp fix only.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
* fix(feishu): use message create_time for inbound timestamps (#52809) (thanks @schumilin)
---------
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: George Zhang <georgezhangtj97@gmail.com>
Groups configured with groupPolicy: open are expected to respond to all
messages. Previously, requireMention defaulted to true regardless of
groupPolicy, causing image (and other non-text) messages to be silently
dropped because they cannot carry @-mentions.
Fix: when groupPolicy is 'open' and requireMention is not explicitly
configured, resolve it to false instead of true. Users who want
mention-required behaviour in open groups can still set requireMention: true
explicitly.
Adds three regression tests covering the new default, explicit override, and
the unchanged allowlist-policy behaviour.
Closes#52553
When OpenClaw restarts under load, the Feishu bot-info probe
(`/open-apis/bot/v3/info`) can exceed the 10-second timeout due to
event-loop contention during channel initialization. This leaves
`botOpenId` empty, causing `checkBotMentioned()` to return `false`
for every group message — silently dropping them all while DMs
continue to work fine.
Two fixes:
1. **Increase startup probe timeout from 10s to 30s** and make it
configurable via `OPENCLAW_FEISHU_STARTUP_PROBE_TIMEOUT_MS` env var.
The previous 10s budget was too tight when multiple channels
(Slack, Discord, Feishu) initialize concurrently.
2. **Graceful degradation in `checkBotMentioned()`**: when `botOpenId`
is unknown, return `true` (assume mentioned) instead of `false`.
This prevents group messages from being silently discarded when the
probe fails for any reason. The trade-off is that the bot may
respond to non-@-mentioned messages temporarily until the next
successful probe, which is far preferable to total silence.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feishu: harden media support and action surface
* feishu: format media action changes
* feishu: fix review follow-ups
* fix: scope Feishu target aliases to Feishu (#47968) (thanks @Takhoffman)
* fix(feishu): fetch thread context so AI can see bot replies in topic threads
When a user replies in a Feishu topic thread, the AI previously could only
see the quoted parent message but not the bot's own prior replies in the
thread. This made multi-turn conversations in threads feel broken.
- Add `threadId` (omt_xxx) to `FeishuMessageInfo` and `getMessageFeishu`
- Add `listFeishuThreadMessages()` using `container_id_type=thread` API
to fetch all messages in a thread including bot replies
- In `handleFeishuMessage`, fetch ThreadStarterBody and ThreadHistoryBody
for topic session modes and pass them to the AI context
- Reuse quoted message result when rootId === parentId to avoid redundant
API calls; exclude root message from thread history to prevent duplication
- Fall back to inbound ctx.threadId when rootId is absent or API fails
- Fetch newest messages first (ByCreateTimeDesc + reverse) so long threads
keep the most recent turns instead of the oldest
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): skip redundant thread context injection on subsequent turns
Only inject ThreadHistoryBody on the first turn of a thread session.
On subsequent turns the session already contains prior context, so
re-injecting thread history (and starter) would waste tokens.
The heuristic checks whether the current user has already sent a
non-root message in the thread — if so, the session has prior turns
and thread context injection is skipped entirely.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): handle thread_id-only events in prior-turn detection
When ctx.rootId is undefined (thread_id-only events), the starter
message exclusion check `msg.messageId !== ctx.rootId` was always
true, causing the first follow-up to be misclassified as a prior
turn. Fall back to the first message in the chronologically-sorted
thread history as the starter.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): bootstrap topic thread context via session state
* test(memory): pin remote embedding hostnames in offline suites
* fix(feishu): use plugin-safe session runtime for thread bootstrap
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
- Feishu/group slash command detection: normalize group mention wrappers before command-authorization probing so mention-prefixed commands are recognized in group routing.\n- Source PR: #36011\n- Contributor: @liuxiaopai-ai\n\nCo-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>\nCo-authored-by: liuxiaopai-ai <73659136+liuxiaopai-ai@users.noreply.github.com>
* fix(feishu): comprehensive reply mechanism fix — outbound replyToId forwarding + topic-aware reply targeting
- Forward replyToId from ChannelOutboundContext through sendText/sendMedia
to sendMessageFeishu/sendMarkdownCardFeishu/sendMediaFeishu, enabling
reply-to-message via the message tool.
- Fix group reply targeting: use ctx.messageId (triggering message) in
normal groups to prevent silent topic thread creation (#32980). Preserve
ctx.rootId targeting for topic-mode groups (group_topic/group_topic_sender)
and groups with explicit replyInThread config.
- Add regression tests for both fixes.
Fixes#32980Fixes#32958
Related #19784
* fix: normalize Feishu delivery.to before comparing with messaging tool targets
- Add normalizeDeliveryTarget helper to strip user:/chat: prefixes for Feishu
- Apply normalization in matchesMessagingToolDeliveryTarget before comparison
- This ensures cron duplicate suppression works when session uses prefixed targets
(user:ou_xxx) but messaging tool extract uses normalized bare IDs (ou_xxx)
Fixes review comment on PR #32755
(cherry picked from commit fc20106f16)
* fix(feishu): catch thrown SDK errors for withdrawn reply targets
The Feishu Lark SDK can throw exceptions (SDK errors with .code or
AxiosErrors with .response.data.code) for withdrawn/deleted reply
targets, in addition to returning error codes in the response object.
Wrap reply calls in sendMessageFeishu and sendCardFeishu with
try-catch to handle thrown withdrawn/not-found errors (230011,
231003) and fall back to client.im.message.create, matching the
existing response-level fallback behavior.
Also extract sendFallbackDirect helper to deduplicate the
direct-send fallback block across both functions.
Closes#33496
(cherry picked from commit ad0901aec1)
* feishu: forward outbound reply target context
(cherry picked from commit c129a691fcf552a1cebe1e8a22ea8611ffc3b377)
* feishu extension: tighten reply target fallback semantics
(cherry picked from commit f85ec610f267020b66713c09e648ec004b2e26f1)
* fix(feishu): align synthesized fallback typing and changelog attribution
* test(feishu): cover group_topic_sender reply targeting
---------
Co-authored-by: Xu Zimo <xuzimojimmy@163.com>
Co-authored-by: Munem Hashmi <munem.hashmi@gmail.com>
Co-authored-by: bmendonca3 <bmendonca3@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* feat(feishu): add broadcast support for multi-agent group observation
When multiple agents share a Feishu group chat, only the @mentioned
agent receives the message. This prevents observer agents from building
session memory of group activity they weren't directly addressed in.
Adds broadcast support (reusing the same cfg.broadcast schema as
WhatsApp) so all configured agents receive every group message in their
session transcripts. Only the @mentioned agent responds on Feishu;
observer agents process silently via no-op dispatchers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): guard sequential broadcast dispatch against single-agent failure
Wrap each dispatchForAgent() call in the sequential loop with try/catch
so one agent's dispatch failure doesn't abort delivery to remaining agents.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): avoid duplicate messages in broadcast observer mode and normalize agent IDs
- Skip recordPendingHistoryEntryIfEnabled for broadcast groups when not
mentioned, since the message is dispatched directly to all agents.
Previously the message appeared twice in the agent prompt.
- Normalize agent IDs with toLowerCase() before membership checks so
config casing mismatches don't silently skip valid agents.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): set WasMentioned per-agent and normalize broadcast IDs
- buildCtxPayloadForAgent now takes a wasMentioned parameter so active
agents get WasMentioned=true and observers get false (P1 fix)
- Normalize broadcastAgents to lowercase at resolution time and
lowercase activeAgentId so all comparisons and session key generation
use canonical IDs regardless of config casing (P2 fix)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): canonicalize broadcast agent IDs with normalizeAgentId
* fix(feishu): match ReplyDispatcher sync return types for noop dispatcher
The upstream ReplyDispatcher changed sendToolResult/sendBlockReply/
sendFinalReply to synchronous (returning boolean). Update the broadcast
observer noop dispatcher to match.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): deduplicate broadcast agent IDs after normalization
Config entries like "Main" and "main" collapse to the same canonical ID
after normalizeAgentId but were dispatched multiple times. Use Set to
deduplicate after normalization.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): honor requireMention=false when selecting broadcast responder
When requireMention is false, the routed agent should be active (reply
on Feishu) even without an explicit @mention. Previously activeAgentId
was null whenever ctx.mentionedBot was false, so all agents got the
noop dispatcher and no reply was sent — silently breaking groups that
disabled mention gating.
Hoist requireMention out of the if(isGroup) block so it's accessible
in the dispatch code.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): cross-account broadcast dedup to prevent duplicate dispatches
In multi-account Feishu setups, the same message event is delivered to
every bot account in a group. Without cross-account dedup, each account
independently dispatches broadcast agents, causing 2×N dispatches instead
of N (where N = number of broadcast agents).
Two changes:
1. requireMention=true + bot not mentioned: return early instead of
falling through to broadcast. The mentioned bot's handler will
dispatch for all agents. Non-mentioned handlers record to history.
2. Add cross-account broadcast dedup using a shared 'broadcast' namespace
(tryRecordMessagePersistent). The first handler to reach the broadcast
block claims the message; subsequent accounts skip. This handles the
requireMention=false multi-account case.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): strip CommandAuthorized from broadcast observer contexts
Broadcast observer agents inherited CommandAuthorized from the sender,
causing slash commands (e.g. /reset) to silently execute on every observer
session. Now only the active agent retains CommandAuthorized; observers
have it stripped before dispatch.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): use actual mention state for broadcast WasMentioned
The active broadcast agent's WasMentioned was set to true whenever
requireMention=false, even when the bot was not actually @mentioned.
Now uses ctx.mentionedBot && agentId === activeAgentId, consistent
with the single-agent path which passes ctx.mentionedBot directly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(feishu): skip history buffer for broadcast accounts and log parallel failures
1. In requireMention groups with broadcast, non-mentioned accounts no
longer buffer pending history — the mentioned handler's broadcast
dispatch already writes turns into all agent sessions. Buffering
caused duplicate replay via buildPendingHistoryContextFromMap.
2. Parallel broadcast dispatch now inspects Promise.allSettled results
and logs rejected entries, matching the sequential path's per-agent
error logging.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Changelog: note Feishu multi-agent broadcast dispatch
* Changelog: restore author credit for Feishu broadcast entry
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* feat(feishu): add parentId and rootId to inbound context
Add ParentMessageId and RootMessageId fields to Feishu inbound message context,
enabling agents to:
- Identify quoted/replied messages
- Fetch original message content via Feishu API
- Build proper message thread context
The parent_id and root_id fields already exist in FeishuMessageContext but were
not being passed to the agent's inbound context.
Fixes: Allows proper handling of quoted card messages and message thread reconstruction.
* feat(feishu): parse interactive card content in quoted messages
Add support for extracting readable text from interactive card messages
when fetching quoted/replied message content.
Previously, only text messages were parsed. Now interactive cards
(with div and markdown elements) are also converted to readable text.
* 更新 bot.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
* fix(types): add RootMessageId to MsgContext type definition
* style: fix formatting in bot.ts
* ci: trigger rebuild
* ci: retry flaky tests
* Feishu: add reply-context and interactive-quote regressions
---------
Co-authored-by: qiangu <qiangu@qq.com>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
Co-authored-by: 牛牛 <niuniu@openclaw.ai>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
* feat(feishu): extract embedded video/media from post (rich text) messages
Previously, parsePostContent() only extracted embedded images (img tags)
from rich text posts, ignoring embedded video/audio (media tags). Users
sending post messages with embedded videos would not have the media
downloaded or forwarded to the agent.
Changes:
- Extend parsePostContent() to also collect media tags with file_key
- Return new mediaKeys array alongside existing imageKeys
- Update resolveFeishuMediaList() to download embedded media files
from post messages using the messageResource API
- Add appropriate logging for embedded media discovery and download
* Feishu: keep embedded post media payloads type-safe
* Feishu: format post parser after media tag extraction
---------
Co-authored-by: laopuhuluwa <laopuhuluwa@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
- Respect groupConfig.enabled flag (was parsed but never enforced)
- Fix misleading log: group allowlist rejection now logs group ID and
policy instead of sender open_id