Merge 33c89f302c9f253e975877467c53f78e78bdc84a into 43513cd1df63af0704dfb351ee7864607f955dcc
This commit is contained in:
commit
7377aa345e
@ -669,3 +669,263 @@ describe("draft stream initial message debounce", () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("HTML parse error fallback", () => {
|
||||
afterEach(() => {
|
||||
__testing.resetTelegramDraftStreamForTests();
|
||||
});
|
||||
|
||||
const htmlRenderer = (text: string) => ({ text: `<i>${text}</i>`, parseMode: "HTML" as const });
|
||||
|
||||
function createRenderedStream(
|
||||
api: ReturnType<typeof createMockDraftApi>,
|
||||
overrides: Omit<Partial<TelegramDraftStreamParams>, "api" | "chatId"> = {},
|
||||
) {
|
||||
return createDraftStream(api, { renderText: htmlRenderer, ...overrides });
|
||||
}
|
||||
|
||||
it("retries editMessageText as plain text when HTML parse error occurs", async () => {
|
||||
const api = createMockDraftApi();
|
||||
api.editMessageText
|
||||
.mockRejectedValueOnce(
|
||||
new Error("400: Bad Request: can't parse entities: unexpected end tag"),
|
||||
)
|
||||
.mockResolvedValueOnce(true);
|
||||
const warn = vi.fn();
|
||||
const stream = createRenderedStream(api, { warn });
|
||||
|
||||
stream.update("hello");
|
||||
await stream.flush();
|
||||
expect(api.sendMessage).toHaveBeenCalledWith(123, "<i>hello</i>", { parse_mode: "HTML" });
|
||||
|
||||
stream.update("hello again");
|
||||
await stream.flush();
|
||||
|
||||
expect(api.editMessageText).toHaveBeenCalledTimes(2);
|
||||
expect(api.editMessageText).toHaveBeenNthCalledWith(1, 123, 17, "<i>hello again</i>", {
|
||||
parse_mode: "HTML",
|
||||
});
|
||||
expect(api.editMessageText).toHaveBeenNthCalledWith(2, 123, 17, "hello again");
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
"telegram stream preview edit: HTML parse error, retrying as plain text",
|
||||
);
|
||||
});
|
||||
|
||||
it("propagates to outer handler when edit plain text retry also fails", async () => {
|
||||
const api = createMockDraftApi();
|
||||
api.editMessageText
|
||||
.mockRejectedValueOnce(
|
||||
new Error("400: Bad Request: can't parse entities: unexpected end tag"),
|
||||
)
|
||||
.mockRejectedValueOnce(new Error("500: Internal Server Error"));
|
||||
const warn = vi.fn();
|
||||
const stream = createRenderedStream(api, { warn });
|
||||
|
||||
stream.update("hello");
|
||||
await stream.flush();
|
||||
|
||||
stream.update("hello again");
|
||||
await stream.flush();
|
||||
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("telegram stream preview failed: 500: Internal Server Error"),
|
||||
);
|
||||
|
||||
// Stream is stopped — further updates are no-ops
|
||||
stream.update("third");
|
||||
await stream.flush();
|
||||
expect(api.editMessageText).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("retries first sendMessage as plain text when HTML parse error occurs", async () => {
|
||||
const api = createMockDraftApi();
|
||||
api.sendMessage
|
||||
.mockRejectedValueOnce(
|
||||
new Error("400: Bad Request: can't parse entities: unexpected end tag"),
|
||||
)
|
||||
.mockResolvedValueOnce({ message_id: 17 });
|
||||
const warn = vi.fn();
|
||||
const stream = createRenderedStream(api, { warn });
|
||||
|
||||
stream.update("hello");
|
||||
await stream.flush();
|
||||
|
||||
expect(api.sendMessage).toHaveBeenCalledTimes(2);
|
||||
expect(api.sendMessage).toHaveBeenNthCalledWith(1, 123, "<i>hello</i>", { parse_mode: "HTML" });
|
||||
expect(api.sendMessage).toHaveBeenNthCalledWith(2, 123, "hello", undefined);
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
"telegram stream preview send: HTML parse error, retrying as plain text",
|
||||
);
|
||||
|
||||
// Stream continues with captured message id
|
||||
stream.update("hello again");
|
||||
await stream.flush();
|
||||
expect(api.editMessageText).toHaveBeenCalledWith(123, 17, "hello again");
|
||||
});
|
||||
|
||||
it("retries sendMessageDraft as plain text when HTML parse error occurs", async () => {
|
||||
const api = createMockDraftApi();
|
||||
api.sendMessageDraft
|
||||
.mockRejectedValueOnce(
|
||||
new Error("400: Bad Request: can't parse entities: unexpected end tag"),
|
||||
)
|
||||
.mockResolvedValueOnce(true);
|
||||
const warn = vi.fn();
|
||||
const stream = createDraftStream(api, {
|
||||
thread: { id: 42, scope: "dm" },
|
||||
previewTransport: "draft",
|
||||
renderText: htmlRenderer,
|
||||
warn,
|
||||
});
|
||||
|
||||
stream.update("hello");
|
||||
await stream.flush();
|
||||
|
||||
expect(api.sendMessageDraft).toHaveBeenCalledTimes(2);
|
||||
expect(api.sendMessageDraft).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
123,
|
||||
expect.any(Number),
|
||||
"<i>hello</i>",
|
||||
{ message_thread_id: 42, parse_mode: "HTML" },
|
||||
);
|
||||
expect(api.sendMessageDraft).toHaveBeenNthCalledWith(2, 123, expect.any(Number), "hello", {
|
||||
message_thread_id: 42,
|
||||
});
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
"telegram stream draft preview: HTML parse error, retrying as plain text",
|
||||
);
|
||||
});
|
||||
|
||||
it("treats MESSAGE_NOT_MODIFIED on edit as success and continues streaming", async () => {
|
||||
const api = createMockDraftApi();
|
||||
api.editMessageText
|
||||
.mockRejectedValueOnce(new Error("400: Bad Request: message is not modified"))
|
||||
.mockResolvedValue(true);
|
||||
const stream = createDraftStream(api);
|
||||
|
||||
stream.update("hello");
|
||||
await stream.flush();
|
||||
expect(api.sendMessage).toHaveBeenCalledTimes(1);
|
||||
|
||||
stream.update("hello2");
|
||||
await stream.flush();
|
||||
expect(api.editMessageText).toHaveBeenCalledTimes(1);
|
||||
|
||||
// Stream continues working after MESSAGE_NOT_MODIFIED
|
||||
stream.update("hello3");
|
||||
await stream.flush();
|
||||
expect(api.editMessageText).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it("disables parse_mode for remaining updates in the same generation after a parse error", async () => {
|
||||
const api = createMockDraftApi();
|
||||
api.editMessageText
|
||||
.mockRejectedValueOnce(
|
||||
new Error("400: Bad Request: can't parse entities: unexpected end tag"),
|
||||
)
|
||||
.mockResolvedValue(true);
|
||||
const warn = vi.fn();
|
||||
const stream = createRenderedStream(api, { warn });
|
||||
|
||||
stream.update("hello");
|
||||
await stream.flush();
|
||||
expect(api.sendMessage).toHaveBeenCalledWith(123, "<i>hello</i>", { parse_mode: "HTML" });
|
||||
|
||||
// Edit triggers parse error → plain text retry
|
||||
stream.update("hello again");
|
||||
await stream.flush();
|
||||
expect(api.editMessageText).toHaveBeenNthCalledWith(2, 123, 17, "hello again");
|
||||
|
||||
// Subsequent update uses plain text directly (no parse_mode)
|
||||
stream.update("third update");
|
||||
await stream.flush();
|
||||
expect(api.editMessageText).toHaveBeenCalledTimes(3);
|
||||
expect(api.editMessageText.mock.calls[2]).toEqual([123, 17, "third update"]);
|
||||
});
|
||||
|
||||
it("re-enables HTML parse mode after forceNewMessage resets the generation", async () => {
|
||||
const api = createMockDraftApi();
|
||||
api.sendMessage
|
||||
.mockResolvedValueOnce({ message_id: 17 })
|
||||
.mockResolvedValueOnce({ message_id: 42 });
|
||||
api.editMessageText
|
||||
.mockRejectedValueOnce(
|
||||
new Error("400: Bad Request: can't parse entities: unexpected end tag"),
|
||||
)
|
||||
.mockResolvedValue(true);
|
||||
const warn = vi.fn();
|
||||
const stream = createRenderedStream(api, { warn });
|
||||
|
||||
stream.update("hello");
|
||||
await stream.flush();
|
||||
expect(api.sendMessage).toHaveBeenCalledWith(123, "<i>hello</i>", { parse_mode: "HTML" });
|
||||
|
||||
// Edit triggers parse error → disables parse mode for gen 0
|
||||
stream.update("hello again");
|
||||
await stream.flush();
|
||||
expect(api.editMessageText).toHaveBeenNthCalledWith(2, 123, 17, "hello again");
|
||||
|
||||
// Force new message → new generation, parse mode re-enabled
|
||||
stream.forceNewMessage();
|
||||
stream.update("new message");
|
||||
await stream.flush();
|
||||
|
||||
expect(api.sendMessage).toHaveBeenNthCalledWith(2, 123, "<i>new message</i>", {
|
||||
parse_mode: "HTML",
|
||||
});
|
||||
});
|
||||
|
||||
it("does not stop stream when HTML parse error reaches outer handler", async () => {
|
||||
const api = createMockDraftApi();
|
||||
api.editMessageText
|
||||
.mockRejectedValueOnce(
|
||||
new Error("400: Bad Request: can't parse entities: unexpected end tag"),
|
||||
)
|
||||
.mockRejectedValueOnce(
|
||||
new Error("400: Bad Request: can't parse entities: unexpected end tag"),
|
||||
)
|
||||
.mockResolvedValue(true);
|
||||
const warn = vi.fn();
|
||||
const stream = createRenderedStream(api, { warn });
|
||||
|
||||
stream.update("hello");
|
||||
await stream.flush();
|
||||
|
||||
// Both HTML edit and plain text retry fail with parse errors
|
||||
stream.update("hello again");
|
||||
await stream.flush();
|
||||
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
"telegram stream preview: HTML parse error escaped to outer handler (degrading to plain text)",
|
||||
);
|
||||
|
||||
// Stream still works (not stopped)
|
||||
stream.update("third update");
|
||||
await stream.flush();
|
||||
expect(api.editMessageText).toHaveBeenCalledTimes(3);
|
||||
expect(api.editMessageText.mock.calls[2]).toEqual([123, 17, "third update"]);
|
||||
});
|
||||
|
||||
it("stops stream on non-parse errors in outer handler", async () => {
|
||||
const api = createMockDraftApi();
|
||||
api.editMessageText.mockRejectedValueOnce(new Error("429: Too Many Requests"));
|
||||
const warn = vi.fn();
|
||||
const stream = createRenderedStream(api, { warn });
|
||||
|
||||
stream.update("hello");
|
||||
await stream.flush();
|
||||
|
||||
stream.update("hello again");
|
||||
await stream.flush();
|
||||
|
||||
expect(warn).toHaveBeenCalledWith(
|
||||
expect.stringContaining("telegram stream preview failed: 429: Too Many Requests"),
|
||||
);
|
||||
|
||||
// Stream is stopped — further updates are no-ops
|
||||
stream.update("third");
|
||||
await stream.flush();
|
||||
expect(api.editMessageText).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,9 +1,22 @@
|
||||
import type { Bot } from "grammy";
|
||||
import { createFinalizableDraftLifecycle } from "openclaw/plugin-sdk/channel-lifecycle";
|
||||
import { resolveGlobalSingleton } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { formatErrorMessage } from "../../../src/infra/errors.js";
|
||||
import { buildTelegramThreadParams, type TelegramThreadSpec } from "./bot/helpers.js";
|
||||
import { isSafeToRetrySendError, isTelegramClientRejection } from "./network-errors.js";
|
||||
|
||||
const PARSE_ERR_RE = /can't parse entities|parse entities|find end of the entity/i;
|
||||
const MESSAGE_NOT_MODIFIED_RE =
|
||||
/400:\s*Bad Request:\s*message is not modified|MESSAGE_NOT_MODIFIED/i;
|
||||
|
||||
function isTelegramHtmlParseError(err: unknown): boolean {
|
||||
return PARSE_ERR_RE.test(formatErrorMessage(err));
|
||||
}
|
||||
|
||||
function isMessageNotModifiedError(err: unknown): boolean {
|
||||
return MESSAGE_NOT_MODIFIED_RE.test(formatErrorMessage(err));
|
||||
}
|
||||
|
||||
const TELEGRAM_STREAM_MAX_CHARS = 4096;
|
||||
const DEFAULT_THROTTLE_MS = 1000;
|
||||
const TELEGRAM_DRAFT_ID_MAX = 2_147_483_647;
|
||||
@ -148,10 +161,14 @@ export function createTelegramDraftStream(params: {
|
||||
let lastSentParseMode: "HTML" | undefined;
|
||||
let previewRevision = 0;
|
||||
let generation = 0;
|
||||
/** Generation for which HTML parse_mode has been disabled due to parse errors. */
|
||||
let parseModeDisabledForGeneration: number | undefined;
|
||||
type PreviewSendParams = {
|
||||
renderedText: string;
|
||||
renderedParseMode: "HTML" | undefined;
|
||||
sendGeneration: number;
|
||||
/** Original unrendered text for plain-text fallback on HTML parse errors. */
|
||||
plainText: string;
|
||||
};
|
||||
const sendRenderedMessageWithThreadFallback = async (sendArgs: {
|
||||
renderedText: string;
|
||||
@ -195,14 +212,36 @@ export function createTelegramDraftStream(params: {
|
||||
renderedText,
|
||||
renderedParseMode,
|
||||
sendGeneration,
|
||||
plainText,
|
||||
}: PreviewSendParams): Promise<boolean> => {
|
||||
// Resolve effective parse mode: disabled for this generation after a prior parse error.
|
||||
const effectiveParseMode =
|
||||
parseModeDisabledForGeneration === sendGeneration ? undefined : renderedParseMode;
|
||||
const effectiveText = effectiveParseMode ? renderedText : plainText;
|
||||
|
||||
if (typeof streamMessageId === "number") {
|
||||
if (renderedParseMode) {
|
||||
await params.api.editMessageText(chatId, streamMessageId, renderedText, {
|
||||
parse_mode: renderedParseMode,
|
||||
});
|
||||
} else {
|
||||
await params.api.editMessageText(chatId, streamMessageId, renderedText);
|
||||
try {
|
||||
if (effectiveParseMode) {
|
||||
await params.api.editMessageText(chatId, streamMessageId, effectiveText, {
|
||||
parse_mode: effectiveParseMode,
|
||||
});
|
||||
} else {
|
||||
await params.api.editMessageText(chatId, streamMessageId, effectiveText);
|
||||
}
|
||||
} catch (err) {
|
||||
if (isMessageNotModifiedError(err)) {
|
||||
// Harmless noop — content identical to current message.
|
||||
return true;
|
||||
}
|
||||
if (effectiveParseMode && isTelegramHtmlParseError(err)) {
|
||||
// HTML rejected by Telegram — retry as plain text and disable
|
||||
// parse_mode for the rest of this generation.
|
||||
parseModeDisabledForGeneration = sendGeneration;
|
||||
params.warn?.("telegram stream preview edit: HTML parse error, retrying as plain text");
|
||||
await params.api.editMessageText(chatId, streamMessageId, plainText);
|
||||
return true;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@ -210,19 +249,40 @@ export function createTelegramDraftStream(params: {
|
||||
let sent: Awaited<ReturnType<typeof sendRenderedMessageWithThreadFallback>>["sent"];
|
||||
try {
|
||||
({ sent } = await sendRenderedMessageWithThreadFallback({
|
||||
renderedText,
|
||||
renderedParseMode,
|
||||
renderedText: effectiveText,
|
||||
renderedParseMode: effectiveParseMode,
|
||||
fallbackWarnMessage:
|
||||
"telegram stream preview send failed with message_thread_id, retrying without thread",
|
||||
}));
|
||||
} catch (err) {
|
||||
// Pre-connect failures (DNS, refused) and explicit Telegram rejections (4xx)
|
||||
// guarantee the message was never delivered — clear the flag so
|
||||
// sendMayHaveLanded() doesn't suppress fallback.
|
||||
if (isSafeToRetrySendError(err) || isTelegramClientRejection(err)) {
|
||||
messageSendAttempted = false;
|
||||
if (effectiveParseMode && isTelegramHtmlParseError(err)) {
|
||||
// HTML rejected on first send — retry as plain text.
|
||||
parseModeDisabledForGeneration = sendGeneration;
|
||||
params.warn?.("telegram stream preview send: HTML parse error, retrying as plain text");
|
||||
try {
|
||||
({ sent } = await sendRenderedMessageWithThreadFallback({
|
||||
renderedText: plainText,
|
||||
renderedParseMode: undefined,
|
||||
fallbackWarnMessage:
|
||||
"telegram stream preview send (plain) failed with message_thread_id, retrying without thread",
|
||||
}));
|
||||
} catch (plainErr) {
|
||||
// Plain text retry also failed — reset messageSendAttempted when the
|
||||
// error guarantees the message was never delivered.
|
||||
if (isSafeToRetrySendError(plainErr) || isTelegramClientRejection(plainErr)) {
|
||||
messageSendAttempted = false;
|
||||
}
|
||||
throw plainErr;
|
||||
}
|
||||
} else {
|
||||
// Pre-connect failures (DNS, refused) and explicit Telegram rejections (4xx)
|
||||
// guarantee the message was never delivered — clear the flag so
|
||||
// sendMayHaveLanded() doesn't suppress fallback.
|
||||
if (isSafeToRetrySendError(err) || isTelegramClientRejection(err)) {
|
||||
messageSendAttempted = false;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
const sentMessageId = sent?.message_id;
|
||||
if (typeof sentMessageId !== "number" || !Number.isFinite(sentMessageId)) {
|
||||
@ -245,21 +305,35 @@ export function createTelegramDraftStream(params: {
|
||||
const sendDraftTransportPreview = async ({
|
||||
renderedText,
|
||||
renderedParseMode,
|
||||
sendGeneration,
|
||||
plainText,
|
||||
}: PreviewSendParams): Promise<boolean> => {
|
||||
const effectiveParseMode =
|
||||
parseModeDisabledForGeneration === sendGeneration ? undefined : renderedParseMode;
|
||||
const effectiveText = effectiveParseMode ? renderedText : plainText;
|
||||
const draftId = streamDraftId ?? allocateTelegramDraftId();
|
||||
streamDraftId = draftId;
|
||||
const draftParams = {
|
||||
...(threadParams?.message_thread_id != null
|
||||
? { message_thread_id: threadParams.message_thread_id }
|
||||
: {}),
|
||||
...(renderedParseMode ? { parse_mode: renderedParseMode } : {}),
|
||||
const buildDraftParams = (parseMode: "HTML" | undefined) => {
|
||||
const p: { message_thread_id?: number; parse_mode?: "HTML" } = {};
|
||||
if (threadParams?.message_thread_id != null) {
|
||||
p.message_thread_id = threadParams.message_thread_id;
|
||||
}
|
||||
if (parseMode) {
|
||||
p.parse_mode = parseMode;
|
||||
}
|
||||
return Object.keys(p).length > 0 ? p : undefined;
|
||||
};
|
||||
await resolvedDraftApi!(
|
||||
chatId,
|
||||
draftId,
|
||||
renderedText,
|
||||
Object.keys(draftParams).length > 0 ? draftParams : undefined,
|
||||
);
|
||||
try {
|
||||
await resolvedDraftApi!(chatId, draftId, effectiveText, buildDraftParams(effectiveParseMode));
|
||||
} catch (err) {
|
||||
if (effectiveParseMode && isTelegramHtmlParseError(err)) {
|
||||
parseModeDisabledForGeneration = sendGeneration;
|
||||
params.warn?.("telegram stream draft preview: HTML parse error, retrying as plain text");
|
||||
await resolvedDraftApi!(chatId, draftId, plainText, buildDraftParams(undefined));
|
||||
} else {
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
@ -278,12 +352,17 @@ export function createTelegramDraftStream(params: {
|
||||
if (!renderedText) {
|
||||
return false;
|
||||
}
|
||||
if (renderedText.length > maxChars) {
|
||||
// Telegram text messages/edits cap at 4096 chars.
|
||||
// Telegram text messages/edits cap at 4096 chars.
|
||||
// Use the effective payload length: when HTML parse mode is disabled for
|
||||
// this generation, the actual payload is the shorter plain text, not the
|
||||
// expanded HTML renderedText.
|
||||
const effectiveLength =
|
||||
parseModeDisabledForGeneration === generation ? trimmed.length : renderedText.length;
|
||||
if (effectiveLength > maxChars) {
|
||||
// Stop streaming once we exceed the cap to avoid repeated API failures.
|
||||
streamState.stopped = true;
|
||||
params.warn?.(
|
||||
`telegram stream preview stopped (text length ${renderedText.length} > ${maxChars})`,
|
||||
`telegram stream preview stopped (text length ${effectiveLength} > ${maxChars})`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
@ -299,8 +378,6 @@ export function createTelegramDraftStream(params: {
|
||||
}
|
||||
}
|
||||
|
||||
lastSentText = renderedText;
|
||||
lastSentParseMode = renderedParseMode;
|
||||
try {
|
||||
let sent = false;
|
||||
if (previewTransport === "draft") {
|
||||
@ -309,6 +386,7 @@ export function createTelegramDraftStream(params: {
|
||||
renderedText,
|
||||
renderedParseMode,
|
||||
sendGeneration,
|
||||
plainText: trimmed,
|
||||
});
|
||||
} catch (err) {
|
||||
if (!shouldFallbackFromDraftTransport(err)) {
|
||||
@ -323,6 +401,7 @@ export function createTelegramDraftStream(params: {
|
||||
renderedText,
|
||||
renderedParseMode,
|
||||
sendGeneration,
|
||||
plainText: trimmed,
|
||||
});
|
||||
}
|
||||
} else {
|
||||
@ -330,14 +409,31 @@ export function createTelegramDraftStream(params: {
|
||||
renderedText,
|
||||
renderedParseMode,
|
||||
sendGeneration,
|
||||
plainText: trimmed,
|
||||
});
|
||||
}
|
||||
if (sent) {
|
||||
if (sent && sendGeneration === generation) {
|
||||
previewRevision += 1;
|
||||
lastDeliveredText = trimmed;
|
||||
// Reflect the actual text and parse mode that was delivered. When the
|
||||
// transport fell back to plain text, these may differ from the original
|
||||
// renderedText/renderedParseMode.
|
||||
lastSentText = parseModeDisabledForGeneration === sendGeneration ? trimmed : renderedText;
|
||||
lastSentParseMode =
|
||||
parseModeDisabledForGeneration === sendGeneration ? undefined : renderedParseMode;
|
||||
}
|
||||
return sent;
|
||||
} catch (err) {
|
||||
// HTML parse errors should not permanently kill the stream — the next
|
||||
// update will arrive with more text that may produce valid HTML, and
|
||||
// the transport functions already disable parse_mode for this generation.
|
||||
if (isTelegramHtmlParseError(err)) {
|
||||
parseModeDisabledForGeneration = sendGeneration;
|
||||
params.warn?.(
|
||||
`telegram stream preview: HTML parse error escaped to outer handler (degrading to plain text)`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
streamState.stopped = true;
|
||||
params.warn?.(
|
||||
`telegram stream preview failed: ${err instanceof Error ? err.message : String(err)}`,
|
||||
@ -345,7 +441,6 @@ export function createTelegramDraftStream(params: {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const { loop, update, stop, clear } = createFinalizableDraftLifecycle({
|
||||
throttleMs,
|
||||
state: streamState,
|
||||
@ -394,13 +489,21 @@ export function createTelegramDraftStream(params: {
|
||||
if (previewTransport === "message" && typeof streamMessageId === "number") {
|
||||
return streamMessageId;
|
||||
}
|
||||
// For draft transport, use the rendered snapshot first so parse_mode stays
|
||||
// aligned with the text being materialized.
|
||||
const renderedText = lastSentText || lastDeliveredText;
|
||||
// For draft transport, prefer the unrendered text when HTML parse mode has
|
||||
// been disabled for the current generation — avoids re-sending the same
|
||||
// malformed HTML that caused the parse error during streaming.
|
||||
const htmlDisabled = parseModeDisabledForGeneration === generation;
|
||||
const renderedText = htmlDisabled
|
||||
? lastDeliveredText || lastSentText
|
||||
: lastSentText || lastDeliveredText;
|
||||
if (!renderedText) {
|
||||
return undefined;
|
||||
}
|
||||
const renderedParseMode = lastSentText ? lastSentParseMode : undefined;
|
||||
const renderedParseMode = htmlDisabled
|
||||
? undefined
|
||||
: lastSentText
|
||||
? lastSentParseMode
|
||||
: undefined;
|
||||
try {
|
||||
const { sent, usedThreadParams } = await sendRenderedMessageWithThreadFallback({
|
||||
renderedText,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user