diff --git a/CHANGELOG.md b/CHANGELOG.md index 34d05e06c4e..67afe89281e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -17,6 +17,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Slack: recover full inbound DM text from top-level rich-text blocks when Slack sends a shortened message preview, so long direct messages still reach the agent intact. Fixes #55358. Thanks @tonyjwinter. - Replies: strip legacy `[TOOL_CALL]{tool => ..., args => ...}[/TOOL_CALL]` pseudo-call text from user-facing replies and flag it in tool-call diagnostics instead of showing raw tool syntax in channels. Fixes #63610. Thanks @canh0chua. - WhatsApp: close long-lived web sockets through Baileys `end(error)` before falling back to raw websocket close, so listener teardown runs Baileys cleanup instead of leaving zombie sockets. Fixes #52442. Thanks @essendigitalgroup-cyber. - Twitch/plugins: emit a flat JSON Schema for Twitch channel config so single-account and multi-account configs validate before runtime load, and add source-checkout diagnostics for missing pnpm workspace dependencies. Thanks @vincentkoc. diff --git a/extensions/slack/src/monitor/message-handler/prepare-content.ts b/extensions/slack/src/monitor/message-handler/prepare-content.ts index 94905c3a538..da130e73fac 100644 --- a/extensions/slack/src/monitor/message-handler/prepare-content.ts +++ b/extensions/slack/src/monitor/message-handler/prepare-content.ts @@ -14,6 +14,31 @@ type SlackResolvedMessageContent = { const SLACK_MENTION_RESOLUTION_CONCURRENCY = 4; const SLACK_MENTION_RESOLUTION_MAX_LOOKUPS_PER_MESSAGE = 20; +type SlackTextObject = { + text?: unknown; +}; + +type SlackRichTextElement = { + type?: unknown; + text?: unknown; + url?: unknown; + user_id?: unknown; + channel_id?: unknown; + usergroup_id?: unknown; + name?: unknown; + range?: unknown; + elements?: unknown; +}; + +type SlackBlockLike = { + type?: unknown; + text?: unknown; + elements?: unknown; + fields?: unknown; + alt_text?: unknown; + title?: unknown; +}; + type SlackMediaModule = typeof import("../media.js"); let slackMediaModulePromise: Promise | undefined; @@ -54,6 +79,152 @@ function renderSlackUserMentions( }); } +function readString(value: unknown): string | undefined { + return typeof value === "string" ? value : undefined; +} + +function readTextObject(value: unknown): string | undefined { + if (!value || typeof value !== "object") { + return undefined; + } + return normalizeOptionalString(readString((value as SlackTextObject).text)); +} + +function renderSlackRichTextLeaf(element: SlackRichTextElement): string { + switch (element.type) { + case "text": + return readString(element.text) ?? ""; + case "link": + return readString(element.text) ?? readString(element.url) ?? ""; + case "user": { + const userId = readString(element.user_id); + return userId ? `<@${userId}>` : ""; + } + case "channel": { + const channelId = readString(element.channel_id); + return channelId ? `<#${channelId}>` : ""; + } + case "usergroup": { + const usergroupId = readString(element.usergroup_id); + return usergroupId ? `` : ""; + } + case "broadcast": { + const range = readString(element.range); + return range ? `` : ""; + } + case "emoji": { + const name = readString(element.name); + return name ? `:${name}:` : ""; + } + default: + return ""; + } +} + +function renderSlackRichTextElements(elements: unknown): string { + if (!Array.isArray(elements)) { + return ""; + } + const parts: string[] = []; + for (const rawElement of elements) { + if (!rawElement || typeof rawElement !== "object") { + continue; + } + const element = rawElement as SlackRichTextElement; + switch (element.type) { + case "rich_text_section": + case "rich_text_preformatted": + case "rich_text_quote": { + parts.push(renderSlackRichTextElements(element.elements)); + break; + } + case "rich_text_list": { + const listText = Array.isArray(element.elements) + ? element.elements + .map((child) => + child && typeof child === "object" + ? renderSlackRichTextElements((child as SlackRichTextElement).elements) + : "", + ) + .filter(Boolean) + .join("\n") + : ""; + parts.push(listText); + break; + } + default: + parts.push(renderSlackRichTextLeaf(element)); + break; + } + } + return parts.join(""); +} + +function readSlackBlockText(block: unknown): string | undefined { + if (!block || typeof block !== "object") { + return undefined; + } + const blockLike = block as SlackBlockLike; + switch (blockLike.type) { + case "rich_text": + return normalizeOptionalString(renderSlackRichTextElements(blockLike.elements)); + case "section": { + const text = readTextObject(blockLike.text); + if (text) { + return text; + } + if (Array.isArray(blockLike.fields)) { + const fields = blockLike.fields.map(readTextObject).filter(Boolean); + return fields.length > 0 ? fields.join("\n") : undefined; + } + return undefined; + } + case "header": + return readTextObject(blockLike.text); + case "context": { + if (!Array.isArray(blockLike.elements)) { + return undefined; + } + const parts = blockLike.elements.map(readTextObject).filter(Boolean); + return parts.length > 0 ? parts.join(" ") : undefined; + } + case "image": + return ( + normalizeOptionalString(readString(blockLike.alt_text)) ?? readTextObject(blockLike.title) + ); + case "video": + return ( + readTextObject(blockLike.title) ?? normalizeOptionalString(readString(blockLike.alt_text)) + ); + default: + return undefined; + } +} + +function resolveSlackBlocksText(blocks: unknown[] | undefined): string | undefined { + if (!blocks?.length) { + return undefined; + } + const parts = blocks.map(readSlackBlockText).filter(Boolean); + return parts.length > 0 ? parts.join("\n") : undefined; +} + +function chooseSlackPrimaryText(params: { + messageText: string | undefined; + blocksText: string | undefined; +}): string | undefined { + const { messageText, blocksText } = params; + if (!blocksText) { + return messageText; + } + if (!messageText) { + return blocksText; + } + return blocksText.length > messageText.length && blocksText.startsWith(messageText) + ? blocksText + : messageText; +} + function filterInheritedParentFiles(params: { files: SlackFile[] | undefined; isThreadReply: boolean; @@ -143,11 +314,12 @@ export async function resolveSlackMessageContent(params: { .join("\n") : undefined; - const textParts = [ - normalizeOptionalString(params.message.text), - attachmentContent?.text, - botAttachmentText, - ]; + const blocksText = resolveSlackBlocksText(params.message.blocks); + const primaryText = chooseSlackPrimaryText({ + messageText: normalizeOptionalString(params.message.text), + blocksText, + }); + const textParts = [primaryText, attachmentContent?.text, botAttachmentText]; const renderedMentions = new Map(); const resolveUserName = params.resolveUserName; if (resolveUserName) { diff --git a/extensions/slack/src/monitor/message-handler/prepare.test.ts b/extensions/slack/src/monitor/message-handler/prepare.test.ts index 0e6d8332747..f5988d4fbdc 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.test.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.test.ts @@ -409,6 +409,33 @@ describe("slack prepareSlackMessage inbound contract", () => { expect(prepared!.ctxPayload.RawBody).toContain("[Forwarded message from Bob]\nForwarded hello"); }); + it("recovers full Slack DM text from top-level rich text blocks when text is only a preview", async () => { + const preview = "Yo Molty what is uppppp ".repeat(7).slice(0, 160); + const fullText = `${preview}and this tail should still reach the agent`; + + const prepared = await prepareWithDefaultCtx( + createSlackMessage({ + text: preview, + blocks: [ + { + type: "rich_text", + block_id: "b1", + elements: [ + { + type: "rich_text_section", + elements: [{ type: "text", text: fullText }], + }, + ], + }, + ], + }), + ); + + expect(prepared).toBeTruthy(); + expect(prepared!.ctxPayload.RawBody).toBe(fullText); + expect(prepared!.ctxPayload.BodyForAgent).toContain(fullText); + }); + it("ignores non-forward attachments when no direct text/files are present", async () => { const prepared = await prepareWithDefaultCtx( createSlackMessage({ diff --git a/extensions/slack/src/types.ts b/extensions/slack/src/types.ts index 6de9fcb5a2d..889bf189c0f 100644 --- a/extensions/slack/src/types.ts +++ b/extensions/slack/src/types.ts @@ -41,6 +41,7 @@ export type SlackMessageEvent = { parent_user_id?: string; channel: string; channel_type?: "im" | "mpim" | "channel" | "group"; + blocks?: unknown[]; files?: SlackFile[]; attachments?: SlackAttachment[]; };