diff --git a/CHANGELOG.md b/CHANGELOG.md index f0c181abd6d..17fdf9ddecf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -48,6 +48,7 @@ Docs: https://docs.openclaw.ai - Agents/output: drop copied inbound metadata-only assistant replay turns before provider replay instead of synthesizing a placeholder, so Telegram and other channels cannot receive `[assistant copied inbound metadata omitted]` as model output. Fixes #74745. Thanks @adamwdear and @Marvae. - Doctor/memory: suppress skipped embedding-readiness warnings for key-optional providers such as Ollama and LM Studio while preserving timeout and not-ready diagnostics. Fixes #74608 and #73882. Thanks @hclsys. - Channels/groups: preserve observe-only turn suppression for prepared dispatch paths and restore deprecated channel turn runtime aliases, so passive observer/group flows stay silent while older plugins keep compiling. Thanks @vincentkoc. +- Feishu: skip empty-text messages (e.g. `{"text":""}`) that carry no media, so no blank user turn is written to the session and downstream LLM providers cannot reject the request with "messages must not be empty". (#74634) Thanks @xdengli and @hclsys. - Feishu/Bitable: clean up newly created placeholder rows whose fields contain only default empty values while preserving meaningful link, attachment, user, number, boolean, and location values during create-app cleanup. (#73920) Carries forward #40602. Thanks @boat2moon. - macOS app: keep attach-only mode and the Debug Settings launchd toggle marker-only, so launching with `--attach-only`/`--no-launchd` no longer uninstalls the Gateway LaunchAgent or drops active sessions. (#72174) Thanks @DolencLuka. - Plugin SDK: restore the deprecated `plugin-sdk/zalouser` command-auth facade so published Lark/Zalo plugins that import it load on current hosts. Fixes #74702. Thanks @Goron01. diff --git a/extensions/feishu/src/bot.test.ts b/extensions/feishu/src/bot.test.ts index baf69539883..c60377942d1 100644 --- a/extensions/feishu/src/bot.test.ts +++ b/extensions/feishu/src/bot.test.ts @@ -2999,4 +2999,42 @@ describe("handleFeishuMessage command authorization", () => { await Promise.all([dispatchMessage({ cfg, event }), dispatchMessage({ cfg, event })]); expect(mockDispatchReplyFromConfig).toHaveBeenCalledTimes(1); }); + + it("skips empty-text messages with no media to prevent blank user turns in session (#74634)", async () => { + // Feishu can deliver { "text": "" } events (empty-text or media-stripped + // messages). Writing blank user content to the session causes downstream + // LLM providers such as MiniMax to reject requests with "messages must not + // be empty". The handler should drop such events before queuing a reply. + mockShouldComputeCommandAuthorized.mockReturnValue(false); + + const cfg: ClawdbotConfig = { + channels: { + feishu: { + dmPolicy: "open", + allowFrom: ["*"], + }, + }, + } as ClawdbotConfig; + + const event: FeishuMessageEvent = { + sender: { + sender_id: { + open_id: "ou-empty-text-sender", + }, + }, + message: { + message_id: "msg-empty-text-74634", + chat_id: "oc-dm", + chat_type: "p2p", + message_type: "text", + // Feishu encodes empty text as {"text":""} + content: JSON.stringify({ text: "" }), + }, + }; + + await dispatchMessage({ cfg, event }); + + // No reply should be dispatched: empty message is silently skipped + expect(mockDispatchReplyFromConfig).not.toHaveBeenCalled(); + }); }); diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index f22d6434705..4f89b128837 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -859,6 +859,19 @@ export async function handleFeishuMessage(params: { log, accountId: account.accountId, }); + // Skip messages with no text content and no media attachments. Feishu can + // deliver empty-text events (e.g. `{"text":""}`) when a user sends a blank + // message or when media parsing produces an empty string. Writing a blank + // user turn to the session causes downstream LLM providers (e.g. MiniMax) + // to reject the request with "messages must not be empty" errors. Logging + // the skip avoids silent loss without polluting the agent session. + if (!ctx.content.trim() && mediaList.length === 0) { + log( + `feishu[${account.accountId}]: skipping empty message (no text, no media) from ${ctx.senderOpenId}`, + ); + return; + } + const mediaPayload = buildAgentMediaPayload(mediaList); const audioTranscript = await resolveFeishuAudioPreflightTranscript({ cfg: effectiveCfg,