mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-25 03:45:46 -06:00
refactor(slack): reuse canonical interactive block renderer (#128760)
This commit is contained in:
committed by
GitHub
parent
a8722e74ec
commit
ec091478fc
@@ -1164,7 +1164,7 @@ extensions/slack/src/approval-native-gates.ts 1
|
||||
extensions/slack/src/approval-native.ts 2
|
||||
extensions/slack/src/blocks-fallback.ts 2
|
||||
extensions/slack/src/blocks-input.ts 2
|
||||
extensions/slack/src/blocks-render.ts 2
|
||||
extensions/slack/src/blocks-render.ts 1
|
||||
extensions/slack/src/channel-actions.ts 1
|
||||
extensions/slack/src/channel-type.ts 1
|
||||
extensions/slack/src/channel.ts 7
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { Block, KnownBlock } from "@slack/web-api";
|
||||
import { parseExecApprovalCommandText } from "openclaw/plugin-sdk/approval-reply-runtime";
|
||||
import {
|
||||
reduceLegacyInteractiveReply,
|
||||
legacyInteractiveReplyToPresentation,
|
||||
resolveMessagePresentationButtonAction,
|
||||
resolveMessagePresentationOptionAction,
|
||||
} from "openclaw/plugin-sdk/interactive-runtime";
|
||||
@@ -248,119 +248,10 @@ export function buildSlackInteractiveBlocks(
|
||||
interactive?: LegacyInteractiveReply,
|
||||
options: SlackBlockRenderOptions = {},
|
||||
): SlackBlock[] {
|
||||
const initialState = {
|
||||
blocks: [] as SlackBlock[],
|
||||
buttonIndex: options.buttonIndexOffset ?? 0,
|
||||
selectIndex: options.selectIndexOffset ?? 0,
|
||||
};
|
||||
return reduceLegacyInteractiveReply(interactive, initialState, (state, block) => {
|
||||
if (block.type === "text") {
|
||||
const trimmed = block.text.trim();
|
||||
if (!trimmed) {
|
||||
return state;
|
||||
}
|
||||
state.blocks.push({
|
||||
type: "section",
|
||||
text: {
|
||||
type: "mrkdwn",
|
||||
text: truncateSlackText(trimmed, SLACK_SECTION_TEXT_MAX),
|
||||
},
|
||||
});
|
||||
return state;
|
||||
}
|
||||
if (block.type === "buttons") {
|
||||
// Index is position in the question's options; core emits one buttons block in option order.
|
||||
const elements = block.buttons
|
||||
.flatMap((button, choiceIndex) => {
|
||||
const target = resolveSlackButtonTarget(button, choiceIndex);
|
||||
if (
|
||||
!target ||
|
||||
(target.kind === "link"
|
||||
? !isWithinSlackLimit(target.url, SLACK_BUTTON_URL_MAX)
|
||||
: !isWithinSlackLimit(target.value, SLACK_BUTTON_VALUE_MAX))
|
||||
) {
|
||||
return [];
|
||||
}
|
||||
const style = resolveSlackButtonStyle(button.style);
|
||||
return [
|
||||
{
|
||||
type: "button" as const,
|
||||
// Slack emits block_actions even for URL buttons; link-only actions must be ignored.
|
||||
action_id:
|
||||
target.kind === "link"
|
||||
? buildSlackReplyLinkActionId(state.buttonIndex + 1, choiceIndex)
|
||||
: target.kind === "approval"
|
||||
? buildSlackApprovalButtonActionId(state.buttonIndex + 1, choiceIndex)
|
||||
: target.kind === "callback"
|
||||
? buildSlackCallbackButtonActionId(state.buttonIndex + 1, choiceIndex)
|
||||
: target.kind === "question"
|
||||
? buildSlackQuestionButtonActionId(state.buttonIndex + 1, choiceIndex)
|
||||
: buildSlackReplyButtonActionId(state.buttonIndex + 1, choiceIndex),
|
||||
text: {
|
||||
type: "plain_text" as const,
|
||||
text: truncateSlackText(button.label, SLACK_ACTION_LABEL_MAX),
|
||||
emoji: true,
|
||||
},
|
||||
...(target.kind === "link" ? { url: target.url } : { value: target.value }),
|
||||
...(style ? { style } : {}),
|
||||
},
|
||||
];
|
||||
})
|
||||
.slice(0, SLACK_ACTION_BLOCK_ELEMENTS_MAX);
|
||||
if (elements.length === 0) {
|
||||
return state;
|
||||
}
|
||||
state.blocks.push({
|
||||
type: "actions",
|
||||
block_id: `openclaw_reply_buttons_${++state.buttonIndex}`,
|
||||
elements,
|
||||
});
|
||||
return state;
|
||||
}
|
||||
const optionsLocal = block.options
|
||||
.flatMap((option) => {
|
||||
const target = resolveSlackOptionTarget(option);
|
||||
return target ? [{ label: option.label, ...target }] : [];
|
||||
})
|
||||
.filter(isRenderableSlackOption)
|
||||
.slice(0, SLACK_STATIC_SELECT_OPTIONS_MAX);
|
||||
const optionKinds = new Set(optionsLocal.map((option) => option.kind));
|
||||
if (optionsLocal.length === 0 || optionKinds.size !== 1) {
|
||||
return state;
|
||||
}
|
||||
state.blocks.push({
|
||||
type: "actions",
|
||||
block_id: `openclaw_reply_select_${++state.selectIndex}`,
|
||||
elements: [
|
||||
{
|
||||
type: "static_select",
|
||||
action_id:
|
||||
optionsLocal[0]?.kind === "approval"
|
||||
? buildSlackApprovalSelectActionId(state.selectIndex)
|
||||
: optionsLocal[0]?.kind === "callback"
|
||||
? buildSlackCallbackSelectActionId(state.selectIndex)
|
||||
: buildSlackReplySelectActionId(state.selectIndex),
|
||||
placeholder: {
|
||||
type: "plain_text",
|
||||
text: truncateSlackText(
|
||||
normalizeOptionalString(block.placeholder) ?? "Choose an option",
|
||||
SLACK_ACTION_LABEL_MAX,
|
||||
),
|
||||
emoji: true,
|
||||
},
|
||||
options: optionsLocal.map((option, _choiceIndex) => ({
|
||||
text: {
|
||||
type: "plain_text",
|
||||
text: truncateSlackText(option.label, SLACK_ACTION_LABEL_MAX),
|
||||
emoji: true,
|
||||
},
|
||||
value: option.value,
|
||||
})),
|
||||
},
|
||||
],
|
||||
});
|
||||
return state;
|
||||
}).blocks;
|
||||
return buildSlackPresentationBlocks(
|
||||
interactive ? legacyInteractiveReplyToPresentation(interactive) : undefined,
|
||||
options,
|
||||
);
|
||||
}
|
||||
|
||||
/** Render portable presentation blocks as Slack Block Kit blocks. */
|
||||
|
||||
@@ -339,6 +339,52 @@ describe("buildSlackInteractiveBlocks", () => {
|
||||
expect(buttonBlock.elements?.[2]?.style).toBe("primary");
|
||||
expect(buttonBlock.elements?.[3]).not.toHaveProperty("style");
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "approval",
|
||||
action: {
|
||||
type: "approval" as const,
|
||||
approvalId: "request-1",
|
||||
approvalKind: "exec" as const,
|
||||
decision: "allow-once" as const,
|
||||
},
|
||||
actionId: "openclaw:approval_button:5:1",
|
||||
value:
|
||||
'openclaw:approval:v1:{"approvalId":"request-1","approvalKind":"exec","decision":"allow-once"}',
|
||||
},
|
||||
{
|
||||
name: "callback",
|
||||
action: { type: "callback" as const, value: "plugin:opaque|value" },
|
||||
actionId: "openclaw:callback_button:5:1",
|
||||
value: "plugin:opaque|value",
|
||||
},
|
||||
{
|
||||
name: "question",
|
||||
action: {
|
||||
type: "question" as const,
|
||||
questionId: "ask_0123456789abcdef0123456789abcdef",
|
||||
optionValue: "Production",
|
||||
},
|
||||
actionId: "openclaw:question_button:5:1",
|
||||
value: "slq1:ask_0123456789abcdef0123456789abcdef:0",
|
||||
},
|
||||
])(
|
||||
"preserves typed $name authority through the legacy renderer",
|
||||
({ action, actionId, value }) => {
|
||||
expect(
|
||||
buildSlackInteractiveBlocks(
|
||||
{ blocks: [{ type: "buttons", buttons: [{ label: "Continue", action }] }] },
|
||||
{ buttonIndexOffset: 4 },
|
||||
),
|
||||
).toMatchObject([
|
||||
{
|
||||
block_id: "openclaw_reply_buttons_5",
|
||||
elements: [{ action_id: actionId, value }],
|
||||
},
|
||||
]);
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
describe("buildSlackPresentationBlocks", () => {
|
||||
|
||||
Reference in New Issue
Block a user