Files
openclaw/extensions/slack/src/native-data-fallback.ts
Peter Steinberger 4349687b9d refactor(extensions): burn down Discord and Slack dead exports (#106017)
* refactor(discord): trim dead internal exports

* refactor(slack): trim dead internal exports

* test(discord): cover inherited streaming migration

* chore(tooling): ratchet reduced extension LOC

* refactor(discord): remove stale approval helper import

* chore(deadcode): remove reintroduced transcript baseline
2026-07-12 22:58:22 -07:00

165 lines
5.1 KiB
TypeScript

import type { Block, KnownBlock } from "@slack/web-api";
import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
import { renderSlackBlockFallbackText } from "./blocks-fallback.js";
import { SLACK_MAX_BLOCKS } from "./blocks-input.js";
import { SLACK_MESSAGE_TEXT_HARD_LIMIT } from "./limits.js";
import {
buildSlackNativeDataAccessibilityText,
appendSlackNativeDataPlainTextFallback,
createSlackNativeDataBaseTextConsumer,
hasSlackNativeDataBlock,
SLACK_MALFORMED_NATIVE_DATA_FALLBACK,
stripSlackNativeDataBlocks,
} from "./native-data-blocks.js";
const SLACK_SECTION_PLAIN_TEXT_MAX = 3_000;
const SLACK_EMPTY_BLOCK_FALLBACK = "Shared a Block Kit message";
export type SlackFormattingDisabledMessage = {
text: string;
blocks?: (Block | KnownBlock)[];
mrkdwn: false;
};
type SlackNativeDataDeliveryPlan = {
accessibilityText: string;
fallbackMessages: SlackFormattingDisabledMessage[];
skipOriginalBlocks: boolean;
};
type OrderedFallbackBlock = {
block: Block | KnownBlock;
text?: string;
continuesText?: boolean;
};
export function chunkSlackTextAtHardLimit(
text: string,
limit = SLACK_MESSAGE_TEXT_HARD_LIMIT,
): string[] {
const effectiveLimit = Math.max(2, Math.floor(limit));
const chunks: string[] = [];
let offset = 0;
while (offset < text.length) {
const chunk = sliceUtf16Safe(text, offset, Math.min(text.length, offset + effectiveLimit));
if (!chunk) {
throw new Error("Slack plain-text fallback chunking made no progress.");
}
chunks.push(chunk);
offset += chunk.length;
}
return chunks;
}
function buildPlainTextBlocks(text: string): OrderedFallbackBlock[] {
return chunkSlackTextAtHardLimit(text, SLACK_SECTION_PLAIN_TEXT_MAX).map((chunk, index) => {
const fallbackBlock: OrderedFallbackBlock = {
block: {
type: "section",
text: { type: "plain_text", text: chunk },
},
text: chunk,
};
if (index > 0) {
fallbackBlock.continuesText = true;
}
return fallbackBlock;
});
}
function renderNativeDataPlainText(block: unknown): string {
return (
appendSlackNativeDataPlainTextFallback("", [block]).trim() ||
SLACK_MALFORMED_NATIVE_DATA_FALLBACK
);
}
function buildOrderedFallbackBlocks(params: {
baseText: string;
blocks: readonly (Block | KnownBlock)[];
}): OrderedFallbackBlock[] {
const entries: OrderedFallbackBlock[] = [];
const consumeFromBase = createSlackNativeDataBaseTextConsumer(params.baseText);
if (params.baseText) {
entries.push(...buildPlainTextBlocks(params.baseText));
}
for (const block of params.blocks) {
if (hasSlackNativeDataBlock([block])) {
const nativeText = renderNativeDataPlainText(block);
if (!consumeFromBase(nativeText)) {
entries.push(...buildPlainTextBlocks(nativeText));
}
continue;
}
const text = renderSlackBlockFallbackText(block, { nativeDataFormat: "plain" });
entries.push({ block, ...(text ? { text } : {}) });
}
return entries;
}
function buildOrderedBlockMessages(entries: readonly OrderedFallbackBlock[]) {
const messages: SlackFormattingDisabledMessage[] = [];
let blocks: (Block | KnownBlock)[] = [];
let text = "";
const flush = () => {
if (blocks.length === 0) {
return;
}
messages.push({
text: text || SLACK_EMPTY_BLOCK_FALLBACK,
blocks,
mrkdwn: false,
});
blocks = [];
text = "";
};
for (const entry of entries) {
const separator = text && entry.text && !entry.continuesText ? "\n\n" : "";
const nextText = entry.text ? `${text}${separator}${entry.text}` : text;
if (blocks.length >= SLACK_MAX_BLOCKS || nextText.length > SLACK_MESSAGE_TEXT_HARD_LIMIT) {
flush();
}
const freshSeparator = text && entry.text && !entry.continuesText ? "\n\n" : "";
const freshText = entry.text ? `${text}${freshSeparator}${entry.text}` : text;
if (freshText.length > SLACK_MESSAGE_TEXT_HARD_LIMIT) {
throw new Error("One Slack fallback block exceeds the message text hard limit.");
}
blocks.push(entry.block);
text = freshText;
}
flush();
return messages;
}
/** Build one complete, ordered retry plan after Slack rejects native data blocks. */
export function buildSlackNativeDataDeliveryPlan(params: {
baseText?: string;
blocks: readonly (Block | KnownBlock)[];
}): SlackNativeDataDeliveryPlan {
const baseText = params.baseText?.trim() ?? "";
const hasNativeData = hasSlackNativeDataBlock(params.blocks);
const accessibilityText =
buildSlackNativeDataAccessibilityText(baseText, params.blocks) ||
(hasNativeData ? SLACK_MALFORMED_NATIVE_DATA_FALLBACK : SLACK_EMPTY_BLOCK_FALLBACK);
const survivorBlocks = stripSlackNativeDataBlocks(params.blocks);
const fallbackMessages =
survivorBlocks.length === 0
? chunkSlackTextAtHardLimit(accessibilityText).map((text) => ({
text,
mrkdwn: false as const,
}))
: buildOrderedBlockMessages(
buildOrderedFallbackBlocks({
baseText,
blocks: params.blocks,
}),
);
return {
accessibilityText,
fallbackMessages,
skipOriginalBlocks: accessibilityText.length > SLACK_MESSAGE_TEXT_HARD_LIMIT,
};
}