mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-03 20:01:36 +00:00
* 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
165 lines
5.1 KiB
TypeScript
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,
|
|
};
|
|
}
|