mirror of
https://github.com/openclaw/openclaw.git
synced 2026-07-28 22:21:15 +00:00
206 lines
7.1 KiB
TypeScript
206 lines
7.1 KiB
TypeScript
// Telegram progress-draft formatting and HTML preview rendering.
|
|
import type { ChannelProgressDraftCompositorLine } from "openclaw/plugin-sdk/channel-outbound";
|
|
import type { TelegramDraftPreview } from "./draft-stream.js";
|
|
import { renderTelegramHtmlText } from "./format.js";
|
|
import {
|
|
boldRichText,
|
|
codeRichText,
|
|
italicRichText,
|
|
paragraphBlock,
|
|
type InputRichBlock,
|
|
type RichText,
|
|
} from "./rich-block-model.js";
|
|
import { markdownToTelegramRichBlocks } from "./rich-blocks.js";
|
|
import { buildTelegramRichBlocksPlan } from "./rich-message.js";
|
|
import { clipTelegramProgressText } from "./truncate.js";
|
|
|
|
function sanitizeProgressMarkdownText(text: string): string {
|
|
return text.replaceAll("`", "'");
|
|
}
|
|
|
|
function formatProgressAsMarkdownCode(text: string): string {
|
|
const clipped = clipTelegramProgressText(text);
|
|
return `\`${sanitizeProgressMarkdownText(clipped)}\``;
|
|
}
|
|
|
|
export function formatTelegramProgressLine(text: string): string {
|
|
const trimmed = text.trim();
|
|
return trimmed.startsWith("_") && trimmed.endsWith("_")
|
|
? trimmed
|
|
: formatProgressAsMarkdownCode(text);
|
|
}
|
|
|
|
function escapeTelegramProgressHtml(text: string): string {
|
|
return text
|
|
.replaceAll("&", "&")
|
|
.replaceAll("<", "<")
|
|
.replaceAll(">", ">")
|
|
.replaceAll('"', """);
|
|
}
|
|
|
|
function renderTelegramProgressStringLine(text: string): string {
|
|
// Reasoning/commentary lanes carry model-authored markdown. Render through
|
|
// renderTelegramHtmlText (parse_mode HTML-safe), not the full rich block
|
|
// converter — block output from headings/lists can reject the edit.
|
|
const trimmed = text.trim();
|
|
const italic = trimmed.match(/^(\S+ )?_(.*)_$/u);
|
|
const clipped = italic
|
|
? `${italic[1] ?? ""}_${clipTelegramProgressText(italic[2] ?? "")}_`
|
|
: clipTelegramProgressText(trimmed);
|
|
return renderTelegramHtmlText(clipped);
|
|
}
|
|
|
|
function renderTelegramProgressLine(line: ChannelProgressDraftCompositorLine): string {
|
|
if (typeof line === "string") {
|
|
return line.split(/\r?\n/u).map(renderTelegramProgressStringLine).filter(Boolean).join("<br>");
|
|
}
|
|
if (!line.icon && line.label === "Commentary") {
|
|
return line.text
|
|
.split(/\r?\n/u)
|
|
.map(renderTelegramProgressStringLine)
|
|
.filter(Boolean)
|
|
.join("<br>");
|
|
}
|
|
const label = [line.icon, line.label].filter(Boolean).join(" ");
|
|
const parts = [`<b>${escapeTelegramProgressHtml(label)}</b>`];
|
|
const detail = line.detail && line.detail !== line.label ? line.detail : undefined;
|
|
if (detail) {
|
|
parts.push(`<code>${escapeTelegramProgressHtml(clipTelegramProgressText(detail))}</code>`);
|
|
} else {
|
|
const text = line.text.trim();
|
|
if (text && text !== label) {
|
|
parts.push(`<code>${escapeTelegramProgressHtml(clipTelegramProgressText(text))}</code>`);
|
|
}
|
|
}
|
|
if (line.status && line.status !== "completed" && line.status !== line.detail) {
|
|
parts.push(`<i>${escapeTelegramProgressHtml(line.status)}</i>`);
|
|
}
|
|
return parts.join(" ");
|
|
}
|
|
|
|
function joinRichText(parts: RichText[], separator: string): RichText {
|
|
if (parts.length === 0) {
|
|
return "";
|
|
}
|
|
if (parts.length === 1) {
|
|
return parts[0] ?? "";
|
|
}
|
|
const result: RichText[] = [];
|
|
for (const [index, part] of parts.entries()) {
|
|
if (index > 0) {
|
|
result.push(separator);
|
|
}
|
|
result.push(part);
|
|
}
|
|
return result;
|
|
}
|
|
|
|
function markdownLineToRichText(text: string): RichText {
|
|
const trimmed = text.trim();
|
|
const italic = trimmed.match(/^(\S+ )?_(.*)_$/u);
|
|
const clipped = italic
|
|
? `${italic[1] ?? ""}_${clipTelegramProgressText(italic[2] ?? "")}_`
|
|
: clipTelegramProgressText(trimmed);
|
|
const { blocks } = markdownToTelegramRichBlocks(clipped, { skipEntityDetection: true });
|
|
const first = blocks[0];
|
|
if (first?.type === "paragraph") {
|
|
return first.text;
|
|
}
|
|
return clipped;
|
|
}
|
|
|
|
function progressLineToRichText(line: ChannelProgressDraftCompositorLine): RichText | undefined {
|
|
if (typeof line === "string") {
|
|
const parts = line
|
|
.split(/\r?\n/u)
|
|
.map(markdownLineToRichText)
|
|
.filter((part) => part !== "");
|
|
return parts.length ? joinRichText(parts, "\n") : undefined;
|
|
}
|
|
if (!line.icon && line.label === "Commentary") {
|
|
const parts = line.text
|
|
.split(/\r?\n/u)
|
|
.map(markdownLineToRichText)
|
|
.filter((part) => part !== "");
|
|
return parts.length ? joinRichText(parts, "\n") : undefined;
|
|
}
|
|
const label = [line.icon, line.label].filter(Boolean).join(" ");
|
|
const parts: RichText[] = [boldRichText(label)];
|
|
const detail = line.detail && line.detail !== line.label ? line.detail : undefined;
|
|
if (detail) {
|
|
parts.push(codeRichText(clipTelegramProgressText(detail)));
|
|
} else {
|
|
const text = line.text.trim();
|
|
if (text && text !== label) {
|
|
parts.push(codeRichText(clipTelegramProgressText(text)));
|
|
}
|
|
}
|
|
if (line.status && line.status !== "completed" && line.status !== line.detail) {
|
|
parts.push(italicRichText(line.status));
|
|
}
|
|
return joinRichText(parts, " ");
|
|
}
|
|
|
|
function buildProgressRichBlocks(parts: RichText[]): InputRichBlock[] {
|
|
return [paragraphBlock(joinRichText(parts, "\n"))];
|
|
}
|
|
|
|
export function renderTelegramProgressDraftPreview(
|
|
text: string,
|
|
lines: readonly ChannelProgressDraftCompositorLine[],
|
|
richMessages: boolean,
|
|
statusHeadlineActive = false,
|
|
): TelegramDraftPreview {
|
|
const trimmed = text.trimEnd();
|
|
if (statusHeadlineActive) {
|
|
const statusLines = trimmed
|
|
.split(/\r?\n/u)
|
|
.map((line) => line.trim())
|
|
.filter(Boolean);
|
|
if (!richMessages) {
|
|
const html =
|
|
statusLines.length > 1
|
|
? [
|
|
`<b>${escapeTelegramProgressHtml(statusLines[0] ?? "")}</b>`,
|
|
...statusLines.slice(1).map(renderTelegramProgressStringLine),
|
|
].join("<br>")
|
|
: statusLines.map(renderTelegramProgressStringLine).join("<br>");
|
|
return { text: html, parseMode: "HTML" };
|
|
}
|
|
const richParts: RichText[] =
|
|
statusLines.length > 1
|
|
? [boldRichText(statusLines[0] ?? ""), ...statusLines.slice(1).map(markdownLineToRichText)]
|
|
: statusLines.map(markdownLineToRichText);
|
|
return {
|
|
text: trimmed,
|
|
richMessage: buildTelegramRichBlocksPlan(buildProgressRichBlocks(richParts), {
|
|
skipEntityDetection: true,
|
|
plainText: trimmed,
|
|
}).richMessage,
|
|
};
|
|
}
|
|
const renderedLines = lines.map(renderTelegramProgressLine).filter(Boolean);
|
|
const textLines = trimmed
|
|
.split(/\r?\n/u)
|
|
.map((line) => line.trim())
|
|
.filter(Boolean);
|
|
const heading = textLines.length > renderedLines.length ? textLines[0] : undefined;
|
|
if (!richMessages) {
|
|
const htmlParts = heading
|
|
? [`<b>${escapeTelegramProgressHtml(heading)}</b>`, ...renderedLines]
|
|
: renderedLines;
|
|
return { text: htmlParts.join("<br>"), parseMode: "HTML" };
|
|
}
|
|
const richLineParts = lines
|
|
.map(progressLineToRichText)
|
|
.filter((part): part is RichText => part !== undefined);
|
|
const richParts = heading ? [boldRichText(heading), ...richLineParts] : richLineParts;
|
|
return {
|
|
text: trimmed,
|
|
richMessage: buildTelegramRichBlocksPlan(buildProgressRichBlocks(richParts), {
|
|
skipEntityDetection: true,
|
|
plainText: trimmed,
|
|
}).richMessage,
|
|
};
|
|
}
|