openclaw
Version:
Multi-channel AI gateway with extensible messaging integrations
594 lines (593 loc) • 24.4 kB
JavaScript
import { S as parseStrictInteger } from "./number-coercion-CuVhdCBs.mjs";
import { t as formatErrorMessage } from "./errors-O33D01XW.mjs";
import { E as takeMessageIdAfterStop, T as createFinalizableDraftStreamControlsForState } from "./channel-outbound-BaV16y8h.mjs";
import "./error-runtime-BmospGvZ.mjs";
import { n as isSingleUseReplyToMode } from "./reply-reference-DZm_0i8_.mjs";
import { b as isTelegramRateLimitError, f as isSafeToRetrySendError, h as isTelegramClientRejection, l as TelegramRequestNotStartedError, u as isRecoverableTelegramNetworkError, v as isTelegramMessageNotModifiedError } from "./fetch-DRGAKny1.mjs";
import "./reply-reference-_BhpYpUH.mjs";
import { N as telegramHtmlToPlainTextFallback, P as escapeTelegramHtml, h as withTelegramPlainFallback, m as warnTelegramRichBlocksDegradations, o as removeTelegramRichNativeQuoteParam, r as TELEGRAM_RICH_TEXT_LIMIT, t as TELEGRAM_TEXT_CHUNK_LIMIT } from "./text-chunk-limit-DDOUtk0-.mjs";
import { c as buildTelegramThreadParams } from "./helpers-8klSsO8Z.mjs";
import "./outbound-adapter-DVqZX8gE.mjs";
import { T as planTelegramTextDeliveryPages, _t as runReplaceableTelegramRequest, dt as withTelegramNativeQuoteFallback, gt as runAuthorizedTelegramRequest, ut as buildTelegramThreadReplyParams } from "./send-wlidaQLJ.mjs";
//#region extensions/telegram/src/draft-stream-message.ts
function toDraftSnapshot(page) {
return {
text: page.plainText,
sourceText: page.sourceText,
sourceTextMode: page.sourceTextMode
};
}
function fallbackSnapshot(plainText) {
return {
text: plainText,
sourceText: escapeTelegramHtml(plainText),
sourceTextMode: "html"
};
}
function createTelegramDraftMessageEditor(api, chatId, linkPreviewParams) {
return async (messageId, text, other) => {
const merged = other ? {
...other,
...linkPreviewParams
} : linkPreviewParams;
return Object.keys(merged).length > 0 ? await api.editMessageText(chatId, messageId, text, merged) : await api.editMessageText(chatId, messageId, text);
};
}
async function sendTelegramDraftMessage(params) {
const { chatId, linkPreviewParams, page, assertCurrentSend } = params;
const sendPlannedMessage = async (sendMessageParams) => {
const request = (send) => {
assertCurrentSend();
return send();
};
const richMessage = page.richMessage;
if (richMessage) warnTelegramRichBlocksDegradations({
context: "stream preview",
reasons: page.degradationReasons ?? [],
warn: (message) => params.warn?.(message)
});
if (!richMessage && page.sourceTextMode !== "html") return {
message: await request(() => params.api.sendMessage(chatId, page.plainText, {
...sendMessageParams,
...linkPreviewParams
})),
snapshot: toDraftSnapshot(page)
};
return await withTelegramPlainFallback({
kind: richMessage ? "rich" : "html",
context: "stream preview",
plainText: page.plainText,
warn: (message) => params.warn?.(message),
sendFormatted: async () => ({
message: richMessage ? await request(() => params.api.raw.sendRichMessage({
chat_id: chatId,
rich_message: richMessage,
...sendMessageParams
})) : await request(() => params.api.sendMessage(chatId, page.htmlText ?? page.sourceText, {
parse_mode: "HTML",
...sendMessageParams,
...linkPreviewParams
})),
snapshot: toDraftSnapshot(page)
}),
sendPlain: async (plan) => ({
message: await request(() => params.api.sendMessage(chatId, plan.plainText, {
...sendMessageParams,
...linkPreviewParams
})),
snapshot: fallbackSnapshot(plan.plainText)
})
});
};
return (await withTelegramNativeQuoteFallback({
label: "stream-preview",
requestParams: params.sendMessageParams,
...params.page.richMessage ? { removeNativeQuoteParam: removeTelegramRichNativeQuoteParam } : {},
request: sendPlannedMessage
})).result;
}
//#endregion
//#region extensions/telegram/src/draft-stream.ts
const DEFAULT_THROTTLE_MS = 1e3;
const MAX_CONSECUTIVE_PREVIEW_FAILURES = 3;
const MIN_PREVIEW_DWELL_MS = 4e3;
function createTelegramDraftStream(params) {
const richMessages = params.richMessages === true;
const transportLimit = richMessages ? TELEGRAM_RICH_TEXT_LIMIT : TELEGRAM_TEXT_CHUNK_LIMIT;
const maxChars = Math.min(params.maxChars ?? transportLimit, transportLimit);
const throttleMs = Math.max(250, params.throttleMs ?? DEFAULT_THROTTLE_MS);
const minInitialChars = params.minInitialChars;
const chatId = params.chatId;
const threadParams = buildTelegramThreadParams(params.thread);
const replyToMessageId = parseStrictInteger(params.replyToMessageId);
const quoteParams = params.replyQuote ? buildTelegramThreadReplyParams({
replyToMessageId,
replyQuoteMessageId: replyToMessageId,
replyQuoteText: params.replyQuote.text,
replyQuotePosition: params.replyQuote.position,
replyQuoteEntities: params.replyQuote.entities
}).reply_parameters : void 0;
const initialSendMessageParams = replyToMessageId != null ? {
...threadParams,
reply_parameters: {
message_id: replyToMessageId,
allow_sending_without_reply: true,
...quoteParams
}
} : threadParams ?? {};
const consumesReplyTarget = replyToMessageId != null && params.replyToMode !== void 0 && isSingleUseReplyToMode(params.replyToMode);
let replyTargetState = { kind: "available" };
const reserveReplyTargetForSend = (sendGeneration) => {
if (!consumesReplyTarget) return initialSendMessageParams;
if (replyTargetState.kind !== "available") return threadParams ?? {};
replyTargetState = {
kind: "pending",
generation: sendGeneration
};
return initialSendMessageParams;
};
const releasePendingReplyTarget = (sendGeneration) => {
if (replyTargetState.kind === "pending" && replyTargetState.generation === sendGeneration) replyTargetState = { kind: "available" };
};
const retainReplyTarget = (sendGeneration, messageId) => {
if (replyTargetState.kind === "pending" && replyTargetState.generation === sendGeneration) replyTargetState = {
kind: "retained",
generation: sendGeneration,
messageId
};
};
const streamState = {
stopped: false,
final: false
};
let messageSendAttempted = false;
let consecutivePreviewFailures = 0;
let streamMessageId;
let streamMessageSnapshot;
let streamProviderMessage;
let terminalDeliveryError;
const pendingProviderObservations = /* @__PURE__ */ new Set();
let streamVisibleSinceMs;
let lastSentPreviewKey = "";
let lastDeliveredText = "";
let lastRequestedText = "";
let lastRequestedPreview;
let pendingPlatformSendDispatch;
let pendingPlatformSendAuthorization;
let generation = 0;
let finalPagePlan;
const repositionedSendGenerations = /* @__PURE__ */ new Set();
let supersededPreviews = [];
const previewRequest = (assertCurrent, run) => runAuthorizedTelegramRequest(assertCurrent, () => streamState.final ? run() : runReplaceableTelegramRequest(run));
const scheduleProviderMessageObservation = (message) => {
if (!message) return;
const observation = (async () => {
try {
await params.onProviderMessage?.(message);
} catch (err) {
try {
params.warn?.(`telegram stream preview observation failed: ${formatErrorMessage(err)}`);
} catch {}
}
})();
pendingProviderObservations.add(observation);
observation.then(() => {
pendingProviderObservations.delete(observation);
});
};
const observeCurrentProviderMessage = () => {
const message = streamProviderMessage;
streamProviderMessage = void 0;
scheduleProviderMessageObservation(message);
};
const drainProviderMessageObservations = async () => {
await Promise.all(pendingProviderObservations);
};
const sendMessageTransportPreview = async (page, sendGeneration, disableLinkPreview) => {
const linkPreviewParams = disableLinkPreview ? { link_preview_options: { is_disabled: true } } : {};
const editDraftMessage = createTelegramDraftMessageEditor(params.api, chatId, linkPreviewParams);
if (pendingPlatformSendDispatch) {
await pendingPlatformSendDispatch();
pendingPlatformSendDispatch = void 0;
}
const assertPlatformSendAuthorized = pendingPlatformSendAuthorization;
assertPlatformSendAuthorized?.();
pendingPlatformSendAuthorization = void 0;
const targetMessageId = streamMessageId;
if (typeof targetMessageId === "number") {
streamVisibleSinceMs ??= Date.now();
let acceptedSnapshot = toDraftSnapshot(page);
const richMessage = page.richMessage;
if (richMessage) warnTelegramRichBlocksDegradations({
context: "stream preview edit",
reasons: page.degradationReasons ?? [],
warn: (message) => params.warn?.(message)
});
if (richMessage || page.sourceTextMode === "html") acceptedSnapshot = await previewRequest(assertPlatformSendAuthorized, () => withTelegramPlainFallback({
kind: richMessage ? "rich" : "html",
context: "stream preview edit",
plainText: page.plainText,
warn: (message) => params.warn?.(message),
sendFormatted: async () => {
if (richMessage) await params.api.raw.editMessageText({
chat_id: chatId,
message_id: targetMessageId,
rich_message: richMessage
});
else await editDraftMessage(targetMessageId, page.htmlText ?? page.sourceText, { parse_mode: "HTML" });
return toDraftSnapshot(page);
},
sendPlain: async (plan) => {
await editDraftMessage(targetMessageId, plan.plainText);
return fallbackSnapshot(plan.plainText);
}
}));
else await previewRequest(assertPlatformSendAuthorized, () => editDraftMessage(targetMessageId, page.sourceText));
if (sendGeneration === generation && streamMessageId === targetMessageId) streamMessageSnapshot = acceptedSnapshot;
return true;
}
messageSendAttempted = true;
const sendMessageParams = reserveReplyTargetForSend(sendGeneration);
let sent;
try {
sent = await previewRequest(assertPlatformSendAuthorized, () => sendTelegramDraftMessage({
api: params.api,
chatId,
page,
sendMessageParams,
linkPreviewParams,
warn: params.warn,
assertCurrentSend: () => {
if (sendGeneration !== generation || streamState.stopped) throw new TelegramRequestNotStartedError("Telegram preview generation retired");
assertPlatformSendAuthorized?.();
}
}));
} catch (err) {
const definitelyRejected = isSafeToRetrySendError(err) || isTelegramClientRejection(err);
if (sendGeneration === generation && definitelyRejected) messageSendAttempted = false;
if (definitelyRejected) releasePendingReplyTarget(sendGeneration);
throw err;
}
const sentMessageId = sent.message?.message_id;
const normalizedMessageId = typeof sentMessageId === "number" && Number.isFinite(sentMessageId) ? Math.trunc(sentMessageId) : void 0;
if (normalizedMessageId === void 0) {
if (sendGeneration === generation) {
streamState.stopped = true;
params.warn?.("telegram stream preview stopped (missing message id from sendMessage)");
return false;
}
return true;
}
retainReplyTarget(sendGeneration, normalizedMessageId);
try {
if (params.validateProviderMessage) await params.validateProviderMessage(sent.message);
} catch (error) {
terminalDeliveryError ??= error instanceof Error ? error : new Error(formatErrorMessage(error));
streamState.stopped = true;
if (sendGeneration === generation) {
streamMessageId = normalizedMessageId;
streamMessageSnapshot = sent.snapshot;
streamProviderMessage = sent.message;
streamVisibleSinceMs = Date.now();
} else if (repositionedSendGenerations.delete(sendGeneration)) retireWhenReplaced(normalizedMessageId, Date.now());
return false;
}
if (sendGeneration !== generation) {
const visibleSinceMs = Date.now();
if (repositionedSendGenerations.delete(sendGeneration)) {
retireWhenReplaced(normalizedMessageId, visibleSinceMs);
return true;
}
params.onRetainedPage?.({
messageId: normalizedMessageId,
textSnapshot: sent.snapshot.text,
visibleSinceMs
});
scheduleProviderMessageObservation(sent.message);
return true;
}
const visibleSinceMs = Date.now();
streamMessageId = normalizedMessageId;
streamMessageSnapshot = sent.snapshot;
streamProviderMessage = sent.message;
streamVisibleSinceMs = visibleSinceMs;
retireSupersededPreviews();
return true;
};
const sendOrEditPlannedPage = async (page, complete = false, disableLinkPreview = params.linkPreview === false) => {
const renderedPreviewKey = JSON.stringify([
page.sourceTextMode,
page.sourceText,
page.richMessage?.skip_entity_detection === true,
disableLinkPreview
]);
if (renderedPreviewKey === lastSentPreviewKey) return true;
const sendGeneration = generation;
if (typeof streamMessageId !== "number" && minInitialChars != null && !streamState.final && !complete) {
if (page.plainText.length < minInitialChars) return false;
}
const previousSentPreviewKey = lastSentPreviewKey;
lastSentPreviewKey = renderedPreviewKey;
try {
const sent = await sendMessageTransportPreview(page, sendGeneration, disableLinkPreview);
if (sendGeneration !== generation) return true;
if (sent) consecutivePreviewFailures = 0;
return sent;
} catch (err) {
if (sendGeneration !== generation) return true;
const isEdit = typeof streamMessageId === "number";
if (isEdit && isTelegramMessageNotModifiedError(err)) {
consecutivePreviewFailures = 0;
streamMessageSnapshot = toDraftSnapshot(page);
return true;
}
lastSentPreviewKey = previousSentPreviewKey;
if (!streamState.final && isTelegramRateLimitError(err)) return false;
const retryable = isTelegramRateLimitError(err) || (isEdit ? isRecoverableTelegramNetworkError(err) : isSafeToRetrySendError(err));
consecutivePreviewFailures += 1;
if (retryable && consecutivePreviewFailures <= MAX_CONSECUTIVE_PREVIEW_FAILURES) {
params.warn?.(`telegram stream preview ${isEdit ? "edit" : "send"} failed (retrying): ${formatErrorMessage(err)}`);
return false;
}
streamState.stopped = true;
params.warn?.(`telegram stream preview failed: ${formatErrorMessage(err)}`);
return false;
}
};
const retainCurrentPage = () => {
if (typeof streamMessageId !== "number" || !streamMessageSnapshot?.text) return;
params.onRetainedPage?.({
messageId: streamMessageId,
textSnapshot: streamMessageSnapshot.text,
visibleSinceMs: streamVisibleSinceMs
});
observeCurrentProviderMessage();
};
const resolveExactRemainingPage = (plan) => {
if (plan.nextPageIndex <= 0 || plan.nextPageIndex >= plan.pages.length) return;
const fullSourceText = plan.pages[0]?.fullSourceText;
if (fullSourceText === void 0) return;
const acceptedSourceText = plan.pages.slice(0, plan.nextPageIndex).map((page) => page.sourceText).join("");
if (!fullSourceText?.startsWith(acceptedSourceText)) return;
const sourceText = fullSourceText.slice(acceptedSourceText.length);
const plainText = telegramHtmlToPlainTextFallback(sourceText);
return plainText.length <= maxChars ? {
plainText,
sourceText,
sourceTextMode: "html",
fullSourceText,
htmlText: sourceText
} : void 0;
};
const sendOrEditStreamMessage = async (update) => {
const isLazy = typeof update !== "string";
const text = isLazy ? update.resolveText() : update;
if (text === void 0) return true;
if (isLazy) {
lastRequestedPreview = void 0;
lastRequestedText = text;
}
if (streamState.stopped && !streamState.final) return false;
const trimmed = text.trimEnd();
if (!trimmed) return false;
const fullPreview = lastRequestedPreview?.text === trimmed ? lastRequestedPreview : params.renderText?.(trimmed) ?? { text: trimmed };
const pages = streamState.final && finalPagePlan ? finalPagePlan.pages : planTelegramTextDeliveryPages({
text: fullPreview.markdownSource?.text ?? fullPreview.text,
maxChars,
richMessages,
richMessage: fullPreview.richMessage,
tableMode: fullPreview.markdownSource?.tableMode,
...richMessages || fullPreview.markdownSource ? {} : { textMode: fullPreview.parseMode === "HTML" ? "html" : "plain" }
});
const firstPage = pages[0];
if (!firstPage) return false;
const disableLinkPreview = fullPreview.linkPreview === false || params.linkPreview === false;
if (!streamState.final) {
finalPagePlan = void 0;
const updateGeneration = generation;
const sent = await sendOrEditPlannedPage(firstPage, fullPreview.complete, disableLinkPreview);
if (updateGeneration !== generation) return true;
if (sent) lastDeliveredText = pages.length === 1 ? trimmed : firstPage.plainText.trimEnd();
return sent;
}
const activePlan = finalPagePlan ??= {
pages,
nextPageIndex: 0
};
for (let index = activePlan.nextPageIndex; index < pages.length; index += 1) {
const exactRemainingPage = resolveExactRemainingPage(activePlan);
const page = exactRemainingPage ?? pages[index];
if (index > 0 && typeof streamMessageId === "number") {
retainCurrentPage();
resetStreamToNewMessage(true);
}
if (!await sendOrEditPlannedPage(page, false, disableLinkPreview)) return false;
if (finalPagePlan !== activePlan) return true;
activePlan.nextPageIndex = exactRemainingPage ? pages.length : index + 1;
if (exactRemainingPage) break;
}
finalPagePlan = void 0;
lastDeliveredText = trimmed;
return true;
};
const { loop, update: updateDraft, stopForClear } = createFinalizableDraftStreamControlsForState({
throttleMs,
state: streamState,
sendOrEditStreamMessage
});
const throwTerminalDeliveryError = () => {
if (terminalDeliveryError !== void 0) throw terminalDeliveryError;
};
const waitForInFlight = async () => {
await loop.waitForInFlight();
throwTerminalDeliveryError();
};
const flush = async () => {
await waitForInFlight();
if (!streamState.stopped) await loop.flush();
throwTerminalDeliveryError();
};
const requestDraftUpdate = (text, preview, onPlatformSendDispatch, assertPlatformSendAuthorized) => {
if (streamState.stopped || streamState.final) return;
lastRequestedPreview = preview;
lastRequestedText = text;
pendingPlatformSendDispatch = onPlatformSendDispatch;
pendingPlatformSendAuthorization = assertPlatformSendAuthorized;
updateDraft(text);
};
const updatePreview = (preview) => {
const text = preview.text.trimEnd();
if (!text) return;
requestDraftUpdate(text, {
...preview,
text
});
};
const stop = async () => {
const stopGeneration = generation;
streamState.final = true;
loop.resetThrottleWindow();
await loop.waitForInFlight();
throwTerminalDeliveryError();
if (generation !== stopGeneration || streamState.stopped) return;
await flush();
if (generation !== stopGeneration || streamState.stopped) return;
const finalText = lastRequestedText.trimEnd();
if (finalText && finalText !== lastDeliveredText.trimEnd()) for (let attempt = 0; attempt < 2; attempt += 1) {
if (generation !== stopGeneration || streamState.stopped) return;
const sent = await sendOrEditStreamMessage(finalText);
throwTerminalDeliveryError();
if (generation !== stopGeneration) return;
if (sent) {
loop.resetPending();
break;
}
if (!finalPagePlan || streamState.stopped) break;
}
streamState.final = true;
observeCurrentProviderMessage();
await drainProviderMessageObservations();
pendingPlatformSendDispatch = void 0;
pendingPlatformSendAuthorization = void 0;
};
const remainingFinalContent = () => {
const plan = finalPagePlan;
if (!plan || plan.nextPageIndex <= 0 || plan.nextPageIndex >= plan.pages.length) return;
const pages = plan.pages.slice(plan.nextPageIndex);
const exactRemainingPage = resolveExactRemainingPage(plan);
const sourceText = exactRemainingPage?.sourceText || pages.map((page) => page.sourceTextMode === "html" ? page.sourceText : escapeTelegramHtml(page.plainText)).join("");
return {
text: exactRemainingPage?.plainText ?? pages.map((page) => page.plainText).join(""),
sourceText,
sourceTextMode: "html"
};
};
const resetStreamToNewMessage = (continueFinalPagination = false, retainCurrentProviderMessage = false) => {
if (retainCurrentProviderMessage) observeCurrentProviderMessage();
streamState.stopped = false;
streamState.final = continueFinalPagination;
if (!continueFinalPagination) generation += 1;
messageSendAttempted = false;
streamMessageId = void 0;
streamMessageSnapshot = void 0;
streamProviderMessage = void 0;
streamVisibleSinceMs = void 0;
lastSentPreviewKey = "";
if (!continueFinalPagination) {
finalPagePlan = void 0;
lastRequestedText = "";
lastDeliveredText = "";
loop.resetPending();
lastRequestedPreview = void 0;
}
loop.resetThrottleWindow();
};
const scheduleDetachedDelete = (messageId, visibleSince, minDelayMs = 0) => {
const runDelete = async () => {
try {
if (!await params.api.deleteMessage(chatId, messageId)) {
params.warn?.(`telegram stream preview cleanup was not confirmed (chat=${chatId}, message=${messageId})`);
return;
}
if (replyTargetState.kind === "retained" && replyTargetState.messageId === messageId) replyTargetState = { kind: "available" };
params.log?.(`telegram stream preview deleted (chat=${chatId}, message=${messageId})`);
} catch (err) {
params.warn?.(`telegram stream preview cleanup failed: ${formatErrorMessage(err)}`);
}
};
const elapsedMs = typeof visibleSince === "number" ? Date.now() - visibleSince : MIN_PREVIEW_DWELL_MS;
const remainingDwellMs = Math.max(0, MIN_PREVIEW_DWELL_MS - elapsedMs);
const delayMs = Math.max(remainingDwellMs, minDelayMs);
if (delayMs <= 0) runDelete();
else setTimeout(() => {
runDelete();
}, delayMs);
};
const retireSupersededPreviews = () => {
const previews = supersededPreviews;
supersededPreviews = [];
for (const { messageId, visibleSinceMs } of previews) scheduleDetachedDelete(messageId, visibleSinceMs, REPOSITION_DELETE_DELAY_MS);
};
const retireWhenReplaced = (messageId, visibleSinceMs) => {
supersededPreviews.push({
messageId,
visibleSinceMs
});
if (typeof streamMessageId === "number") retireSupersededPreviews();
};
const clear = async () => {
retireSupersededPreviews();
const visibleSince = streamVisibleSinceMs;
const messageId = await takeMessageIdAfterStop({
stopForClear,
readMessageId: () => streamMessageId,
clearMessageId: () => {
streamMessageId = void 0;
streamMessageSnapshot = void 0;
streamProviderMessage = void 0;
}
});
retireSupersededPreviews();
if (typeof messageId === "number" && Number.isFinite(messageId)) scheduleDetachedDelete(messageId, visibleSince);
await drainProviderMessageObservations();
};
const REPOSITION_DELETE_DELAY_MS = 1500;
const rotateToNewMessageDeferringDelete = () => {
const supersededMessageId = streamMessageId;
const supersededVisibleSince = streamVisibleSinceMs;
if (messageSendAttempted && streamMessageId === void 0) repositionedSendGenerations.add(generation);
resetStreamToNewMessage();
if (typeof supersededMessageId === "number" && Number.isFinite(supersededMessageId)) retireWhenReplaced(supersededMessageId, supersededVisibleSince);
};
params.log?.(`telegram stream preview ready (maxChars=${maxChars}, throttleMs=${throttleMs})`);
return {
update: (text, options) => requestDraftUpdate(text, void 0, options?.onPlatformSendDispatch, options?.assertPlatformSendAuthorized),
updateLazy: (resolveText) => updateDraft({ resolveText }),
updatePreview,
flush,
waitForInFlight,
messageId: () => streamMessageId,
lastDeliveredText: () => lastDeliveredText,
currentMessageSnapshot: () => {
const ownsReplyTarget = !consumesReplyTarget || replyTargetState.kind === "retained" && replyTargetState.messageId === streamMessageId;
return streamMessageSnapshot && ownsReplyTarget && replyToMessageId !== void 0 ? {
...streamMessageSnapshot,
replyToMessageId
} : streamMessageSnapshot;
},
clear,
stop,
discard: async () => {
await stopForClear();
observeCurrentProviderMessage();
await drainProviderMessageObservations();
},
remainingFinalContent,
hasConsumedReplyTarget: () => replyTargetState.kind !== "available",
forceNewMessage: () => resetStreamToNewMessage(false, true),
rotateToNewMessageDeferringDelete,
sendMayHaveLanded: () => messageSendAttempted && typeof streamMessageId !== "number"
};
}
//#endregion
export { createTelegramDraftStream as t };