Type.Record(Type.String(), Type.Any()) serializes via TypeBox to an empty nested value schema (patternProperties: { "^.*$": {} }). AWS Bedrock's strict tool-schema validation rejects the empty sub-schema, breaking all agent turns for Bedrock-backed Anthropic users with Feishu bitable tools enabled.
Replace the three affected free-form field maps (create_record, update_record, create_field) with an explicit non-empty FlexibleFieldValue value schema that still accepts any JSON value. Type.Unknown() is avoided because it also serializes to an empty schema. Adds regression coverage asserting the emitted value schema is non-empty.
Co-authored-by: TwinsLee <1460694+TwinsLee@users.noreply.github.com>
* fix(feishu): catch unhandled promise rejection in streaming card flush timer
## What Problem This Solves
The scheduled flush update in FeishuStreamingSession called
`this.update(pending)` without catching potential rejections, which
could cause unhandled promise rejections when card content updates
fail due to transient network errors.
## Why This Change Was Made
Add a `.catch()` with diagnostic logging to the scheduled flush
timer's `update()` call so transient card update failures are visible
without causing unhandled promise rejections.
## User Impact
Scheduled streaming-card updates that fail transiently will log a
diagnostic instead of producing an unhandled promise rejection.
## Evidence
node scripts/run-vitest.mjs extensions/feishu/src/media.test.ts
Test Files 1 passed (1)
Tests 44 passed (44)
pnpm exec oxfmt --check ...
All matched files use the correct format.
node scripts/run-oxlint.mjs ...
oxlint_exit=0
git diff --check
exit=0
Co-Authored-By: Claude <noreply@anthropic.com>
* test(feishu): cover rejected scheduled card flush
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(interactive): preserve button command values in fallback text for degraded approval UX
* fix(interactive): keep callback values private in fallback text and narrow Feishu interactive detection
- P1: Skip rendering action.type === "callback" values in
renderMessagePresentationFallbackText to avoid leaking opaque
channel/plugin data into user-visible text. Command and legacy
values are still rendered.
- P2: Replace hasMessagePresentationBlocks/hasInteractiveReplyBlocks
with isMessagePresentationInteractiveBlock so Feishu comment
guidance only appears when the presentation actually contains
buttons or selects, not for text-only blocks.
- Update tests: callback button now shows label-only; all 137 tests pass.
* fix(interactive): only render typed command values in fallback text, keep legacy value private
* fix(feishu): gate document-comment command guidance on actual command action
* docs(message-presentation): document command/callback value fallback visibility
* fix(feishu): omit command guidance when URL overrides fallback command text
* docs: regenerate docs_map.md
* fix(interactive): exclude disabled buttons from fallback command rendering and guidance
* fix(interactive): extract hasRenderedCommandAction, exclude disabled buttons from command fallback
* fix(feishu): preserve command guidance marker through core presentation rendering
* fix(feishu): type-narrow channelData.feishu with isRecord before reading rendered-command marker
* fix(feishu): move hasRenderedCommandAction from public SDK into Feishu plugin as local helper
Keep the helper local to the only caller (Feishu outbound) instead of
adding a new public plugin SDK API contract. The shared fallback renderer
in renderMessagePresentationFallbackText already inlines the same
command-visibility logic; a local helper is sufficient for the Feishu
comment-thread guidance gate.
* refactor(feishu): tighten fallback command marker
---------
Co-authored-by: Peter Steinberger <steipete@gmail.com>
* fix(feishu): send blocks as independent messages when blockStreaming is enabled
* fix(feishu): preserve mention targets on first independently sent block
* fix(feishu): route independent block sends through chunked text delivery
Route the independent block-send path through sendChunkedTextReply
instead of calling sendMessageFeishu directly, so the configured
Feishu textChunkLimit is honored for completed blocks. Preserve
mention targets only on the first emitted block chunk and keep
sentBlockText based on the original block text for duplicate-final
suppression.
Add regression test with a small textChunkLimit that verifies a
long block is split into chunked messages and mentions only appear
on the first chunk.
* fix(feishu): preserve block delivery target
---------
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
Preserve the Tailscale timeout cleanup from #98134 and the Feishu TDZ fix from #98137 while satisfying the repository lint rules.
Credit: @zhangLei99586 authored both underlying fixes.
Co-authored-by: Peter Steinberger <58493+steipete@users.noreply.github.com>
Co-authored-by: zhangLei99586 <zhang.lei162@xydigit.com>
The finish() closure referenced timer which was declared with const after
finish was defined. If finish(false) ran via the abort signal check at
line 99 or via the abort event listener before the const timer assignment,
accessing timer in the temporal dead zone would throw a ReferenceError.
Hoist timer as a let binding above finish so it is safely undefined when
finish fires early, and the guard in finish handles it.
* [AI] fix(feishu): guard partial channelRuntime in monitor startup
When the gateway passes a partial channelRuntime (ChannelRuntimeSurface
with only runtimeContexts but no inbound/debounce), the ?? operator in
monitor.account.ts selects it over the full local runtime, causing
monitor startup to crash on channelRuntime.debounce access.
Mirror the same ?.inbound guard pattern from bot.ts:739 (PR #93466)
to monitor.account.ts:487. When the runtime lacks inbound, fall back
to getFeishuRuntime().channel which always has the full surface.
No test: the guard mirrors an already-merged pattern (#93466) and only
affects the partial-runtime edge case that requires a gateway context.
Related to #92595
* [AI] test(feishu): add channelRuntime guard tests for monitorSingleAccount
Three test cases for the channelRuntime guard at
monitor.account.ts:489:
1. Partial channelRuntime (no inbound) → falls back to local runtime
2. Full channelRuntime with inbound → uses provided runtime
3. No channelRuntime (undefined) → falls back to local runtime
Related to #92595
Add sendTarget param to createFeishuReplyDispatcher that overrides
the to: field for visible sends. Fix streaming card delivery target
by stripping routing prefixes (user:, chat:) before the Feishu
streaming API call. Route streaming card reply metadata through
suppressed sendReplyToMessageId.
Closes#83730
When a Feishu channel has top-level appId/appSecret using SecretRef
format and sub-accounts with their own inline appSecret, the secrets
resolver marks the top-level SecretRef as inactive because
isBaseFieldActiveForChannelSurface only checks whether any explicit
account inherits the field.
Feishu account listing always creates an implicit default account from
top-level credentials. The fix detects this implicit default account
and keeps the top-level appSecret active accordingly, without changing
the shared channel secret helper semantics for other channels.
Fixes#96929
Signed-off-by: 赵旺0668001248 <0668001248@duomai.com>
truncateSummary used clean.slice(0, max - 3), which can cut between the
two UTF-16 halves of a surrogate pair (emoji / astral char) straddling
the limit. The serialized card summary then carries a lone high
surrogate that Feishu renders as the replacement char.
Slice with the surrogate-safe sliceUtf16Safe helper instead, matching
the pattern already used in extensions/slack/src/truncate.ts, so a
straddling code point is dropped whole.
Co-authored-by: ly-wang19 <ly-wang19@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* chore(release): close out 2026.6.10 on main
* chore(release): align native app metadata for 2026.6.10
* chore(release): sync Android 2026.6.10 notes
* docs(changelog): preserve 2026.6.9 history
* docs(changelog): preserve 2026.6.9 history
Voice/audio messages sent to Feishu (opus) play fine but show no duration
on the bubble. Feishu derives the voice-bubble duration from the `duration`
parameter of the file upload API (`im/v1/files`); the audio message content
only carries `{file_key}` and has no duration field, so the duration was
never set.
`sendMediaFeishu` now probes the outgoing audio with `ffprobe` and passes the
result as the upload `duration` (ms). It probes the buffer that is actually
sent (after the existing voice transcode, which caps length via
`MEDIA_FFMPEG_MAX_AUDIO_DURATION_SECS`), so the reported length matches what
is played. Probing is best-effort: on failure it logs and omits the duration,
and the message still sends. The audio message content is unchanged.
Fixes#53798
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Summary:
- The branch replaces Feishu's module-load Axios `handlers` reset with public request-interceptor registration and adds tests that throw on private handler access.
- PR surface: Source +7, Tests +48. Total +55 across 2 files.
- Reproducibility: yes. for the source/dependency boundary: current main still writes `interceptors.request.ha ... l on that access before the production change. No live authenticated Feishu request failure was reproduced.
Automerge notes:
- No ClawSweeper repair was needed after automerge opt-in.
Validation:
- ClawSweeper review passed for head b87083193b.
- Required merge gates passed before the squash merge.
Prepared head SHA: b87083193b
Review: https://github.com/openclaw/openclaw/pull/89806#issuecomment-4611809953
Co-authored-by: Cornna <96944678+ymylive@users.noreply.github.com>
* fix(feishu): fetch quoted content before empty-message guard
Moves the quoted/replied message content fetching before the empty-message
early return so a reply with only @bot mention (no text, no media) is not
dropped when it quotes a message with meaningful content. The guard now also
checks that quoted text is empty before skipping.
Note: because the fetch is now unconditional on parentId after passing the
group admission/mention gate, an empty-text reply that quotes a parent in an
open group (requireMention: false) without mentioning the bot will now be
dispatched, where before it was dropped. This is the intended behavior for
open groups — any non-empty turn (including one where context comes from a
quote) should reach the agent. For requireMention:true groups, unmentioned
messages still exit at the mention gate before the fetch, so no over-fetch
occurs.
Adds group-based regression tests for the #90177 scenario:
- Positive: mention-only reply in requireMention:true group with quoted
parent — dispatches with [Replying to: "..."] in the body.
- Negative: empty reply with no bot mention in requireMention:true group —
getMessageFeishu is never called and nothing is dispatched.
* fix(feishu): fetch quoted content before empty-message guard (#90192) (thanks @bladin)
---------
Co-authored-by: 黑承亮0668000844 <bladin@users.noreply.github.com>
Co-authored-by: sliverp <870080352@qq.com>
* fix(feishu): paginate wiki node and space listing (fixes#37626)
client.wiki.spaceNode.list / wiki.space.list return at most one page (max
50 items); the tool ignored has_more/page_token and silently dropped every
node past the first page. Drain both endpoints via a bounded shared helper
that loops on has_more with a 100-page safety cap.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* fix(feishu): expose wiki pagination cursors
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
* fix(feishu): recover CJK filenames from JSON file_name field
Apply recoverUtf8FileNameFromLatin1Header to JSON-derived filenames in
extractFeishuDownloadMetadata, matching the behavior already present for
Content-Disposition headers in decodeDispositionFileName.
Fixes#81103
* fix(feishu): recover inbound CJK filenames
---------
Co-authored-by: Vincent Koc <25068+vincentkoc@users.noreply.github.com>