mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 08:50:43 +00:00
fix(slack): recover long dm text from blocks
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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<SlackMediaModule> | 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 ? `<!subteam^${usergroupId}>` : "";
|
||||
}
|
||||
case "broadcast": {
|
||||
const range = readString(element.range);
|
||||
return range ? `<!${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<string, string | null>();
|
||||
const resolveUserName = params.resolveUserName;
|
||||
if (resolveUserName) {
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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[];
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user