diff --git a/CHANGELOG.md b/CHANGELOG.md index 2cc1570b2cd..1c170c23f35 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -82,6 +82,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Slack/Bot attachment-only messages: when `allowBots: true`, bot messages with empty `text` now include non-forwarded attachment `text`/`fallback` content so webhook alerts are not silently dropped. (#27616) - Slack/Security ingress mismatch guard: drop slash-command and interaction payloads when app/team identifiers do not match the active Slack account context (including nested `team.id` interaction payloads), preventing cross-app or cross-workspace payload injection into system-event handling. (#29091) Thanks @Solvely-Colin. - Cron/Failure alerts: add configurable repeated-failure alerting with per-job overrides and Web UI cron editor support (`inherit|disabled|custom` with threshold/cooldown/channel/target fields). (#24789) Thanks xbrak. - Cron/Isolated model defaults: resolve isolated cron `subagents.model` (including object-form `primary`) through allowlist-aware model selection so isolated cron runs honor subagent model defaults unless explicitly overridden by job payload model. (#11474) Thanks @AnonO6. diff --git a/src/slack/monitor/message-handler/prepare.test.ts b/src/slack/monitor/message-handler/prepare.test.ts index 1be530ee039..286792b27e5 100644 --- a/src/slack/monitor/message-handler/prepare.test.ts +++ b/src/slack/monitor/message-handler/prepare.test.ts @@ -255,6 +255,36 @@ describe("slack prepareSlackMessage inbound contract", () => { expect(prepared!.ctxPayload.RawBody).toContain("[Slack file: file]"); }); + it("extracts attachment text for bot messages with empty text when allowBots is true (#27616)", async () => { + const slackCtx = createInboundSlackCtx({ + cfg: { + channels: { + slack: { enabled: true }, + }, + } as OpenClawConfig, + defaultRequireMention: false, + }); + // oxlint-disable-next-line typescript/no-explicit-any + slackCtx.resolveUserName = async () => ({ name: "Bot" }) as any; + + const account = createSlackAccount({ allowBots: true }); + const message = createSlackMessage({ + text: "", + bot_id: "B0AGV8EQYA3", + subtype: "bot_message", + attachments: [ + { + text: "Readiness probe failed: Get http://10.42.13.132:8000/status: context deadline exceeded", + }, + ], + }); + + const prepared = await prepareMessageWith(slackCtx, account, message); + + expect(prepared).toBeTruthy(); + expect(prepared!.ctxPayload.RawBody).toContain("Readiness probe failed"); + }); + it("keeps channel metadata out of GroupSystemPrompt", async () => { const slackCtx = createInboundSlackCtx({ cfg: { diff --git a/src/slack/monitor/message-handler/prepare.ts b/src/slack/monitor/message-handler/prepare.ts index 875b23bd92c..484e64ca4ef 100644 --- a/src/slack/monitor/message-handler/prepare.ts +++ b/src/slack/monitor/message-handler/prepare.ts @@ -357,8 +357,25 @@ export async function prepareSlackMessage(params: { : undefined; const fileOnlyPlaceholder = fileOnlyFallback ? `[Slack file: ${fileOnlyFallback}]` : undefined; + // Bot messages (e.g. Prometheus, Gatus webhooks) often carry content only in + // non-forwarded attachments (is_share !== true). Extract their text/fallback + // so the message isn't silently dropped when `allowBots: true` (#27616). + const botAttachmentText = + isBotMessage && !attachmentContent?.text + ? (message.attachments ?? []) + .map((a) => a.text?.trim() || a.fallback?.trim()) + .filter(Boolean) + .join("\n") + : undefined; + const rawBody = - [(message.text ?? "").trim(), attachmentContent?.text, mediaPlaceholder, fileOnlyPlaceholder] + [ + (message.text ?? "").trim(), + attachmentContent?.text, + botAttachmentText, + mediaPlaceholder, + fileOnlyPlaceholder, + ] .filter(Boolean) .join("\n") || ""; if (!rawBody) {