fix(canvas): stop self-closing embed from starting a greedy block match

This commit is contained in:
Vincent Koc
2026-06-24 23:28:45 +08:00
committed by GitHub
parent 5779f88dae
commit 7253bb298e
3 changed files with 72 additions and 11 deletions

View File

@@ -69,6 +69,7 @@ import {
resolveDefaultModelForAgent,
} from "./bot-message-dispatch.agent.runtime.js";
import { deduplicateBlockSentMedia } from "./bot-message-dispatch.media-dedup.js";
import { clipTelegramProgressText } from "./truncate.js";
import {
generateTopicLabel,
getAgentScopedMediaLocalRoots,
@@ -364,22 +365,14 @@ async function mirrorTelegramAssistantReplyToTranscript(params: {
}
}
const MAX_PROGRESS_MARKDOWN_TEXT_CHARS = 300;
const TELEGRAM_GENERAL_TOPIC_ID = 1;
function clipProgressMarkdownText(text: string): string {
if (text.length <= MAX_PROGRESS_MARKDOWN_TEXT_CHARS) {
return text;
}
return `${text.slice(0, MAX_PROGRESS_MARKDOWN_TEXT_CHARS - 1).trimEnd()}`;
}
function sanitizeProgressMarkdownText(text: string): string {
return text.replaceAll("`", "'");
}
function formatProgressAsMarkdownCode(text: string): string {
const clipped = clipProgressMarkdownText(text);
const clipped = clipTelegramProgressText(text);
return `\`${sanitizeProgressMarkdownText(clipped)}\``;
}
@@ -399,7 +392,7 @@ function escapeTelegramProgressHtml(text: string): string {
}
function renderTelegramProgressStringLine(text: string): string {
const clipped = clipProgressMarkdownText(text.trim());
const clipped = clipTelegramProgressText(text.trim());
const italic = clipped.match(/^_(.*)_$/u);
if (italic) {
return `<i>${escapeTelegramProgressHtml(italic[1] ?? "")}</i>`;
@@ -418,7 +411,7 @@ function renderTelegramProgressLine(line: ChannelProgressDraftCompositorLine): s
const parts = [`<b>${escapeTelegramProgressHtml(label)}</b>`];
const detail = line.detail && line.detail !== line.label ? line.detail : undefined;
if (detail) {
parts.push(`<code>${escapeTelegramProgressHtml(clipProgressMarkdownText(detail))}</code>`);
parts.push(`<code>${escapeTelegramProgressHtml(clipTelegramProgressText(detail))}</code>`);
} else {
const text = line.text.trim();
if (text && text !== label) {

View File

@@ -0,0 +1,48 @@
// Telegram tests cover progress text clipping behavior.
import { describe, expect, it } from "vitest";
import { clipTelegramProgressText, TELEGRAM_PROGRESS_MAX_CHARS } from "./truncate.js";
describe("clipTelegramProgressText", () => {
it("drops a surrogate-pair emoji whole when it straddles the limit", () => {
// 😀 is U+1F600, encoded as two UTF-16 code units (high \uD83D + low \uDE00).
// Placing the emoji at positions [MAX-2, MAX-1] (0-indexed) puts its high
// surrogate right on the .slice(0, MAX-1) cut edge. A raw .slice keeps only
// \uD83D — an unpaired high surrogate — which is invalid in a Telegram payload.
const base = "a".repeat(TELEGRAM_PROGRESS_MAX_CHARS - 2); // 298 'a's
const out = clipTelegramProgressText(`${base}😀tail`);
expect(out).toBe(`${base}`);
// No dangling high surrogate (high not followed by a low surrogate).
expect(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/.test(out)).toBe(false);
});
it("keeps an emoji that fits entirely before the cut", () => {
// 296 'a's + '😀' (2 units) + 'xyz' (3 units) = 301 total > 300.
// The emoji sits at [296, 297] — entirely before the cut at 299 — so it stays.
const base = "a".repeat(TELEGRAM_PROGRESS_MAX_CHARS - 4); // 296 'a's
const out = clipTelegramProgressText(`${base}😀xyz`);
expect(out).toBe(`${base}😀x…`);
expect(/[\uD800-\uDBFF](?![\uDC00-\uDFFF])/.test(out)).toBe(false);
});
it("returns text unchanged when it is within the limit", () => {
const short = "hello 😀 world";
expect(clipTelegramProgressText(short)).toBe(short);
});
it("trims trailing whitespace before the ellipsis", () => {
// The sliced portion may end in spaces when trailing spaces straddle the cut.
const text = `${"a".repeat(TELEGRAM_PROGRESS_MAX_CHARS - 2)} rest`;
const out = clipTelegramProgressText(text);
expect(out).not.toContain(" …");
expect(out.endsWith("…")).toBe(true);
});
it("handles plain ASCII that fills exactly to the limit", () => {
const exact = "x".repeat(TELEGRAM_PROGRESS_MAX_CHARS);
expect(clipTelegramProgressText(exact)).toBe(exact);
const oneOver = `${"x".repeat(TELEGRAM_PROGRESS_MAX_CHARS)}y`;
const out = clipTelegramProgressText(oneOver);
expect(out.length).toBeLessThanOrEqual(TELEGRAM_PROGRESS_MAX_CHARS);
expect(out.endsWith("…")).toBe(true);
});
});

View File

@@ -0,0 +1,20 @@
// Telegram tests cover progress text clipping behavior.
import { sliceUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime";
export const TELEGRAM_PROGRESS_MAX_CHARS = 300;
/**
* Clips Telegram progress text to at most {@link TELEGRAM_PROGRESS_MAX_CHARS} UTF-16 code units,
* slicing on a code-point boundary so a surrogate pair straddling the limit is
* dropped whole rather than leaving a lone high surrogate in the payload.
*/
export function clipTelegramProgressText(text: string): string {
if (text.length <= TELEGRAM_PROGRESS_MAX_CHARS) {
return text;
}
// Slice on a code-point boundary so an emoji (or any astral character) that
// straddles the limit is dropped whole instead of leaving a lone \uD83D-style
// high surrogate before the ellipsis, which serializes to an invalid character
// in the Telegram Bot API payload.
return `${sliceUtf16Safe(text, 0, TELEGRAM_PROGRESS_MAX_CHARS - 1).trimEnd()}`;
}