Vendor OpenClaw source as Adolf fork baseline
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11), free to diverge. Tree copied sans upstream .git; upstream remote added for future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19. Preserves docs/ARCHITECTURE.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
This commit is contained in:
605
extensions/telegram/src/lane-delivery-text-deliverer.ts
Normal file
605
extensions/telegram/src/lane-delivery-text-deliverer.ts
Normal file
@@ -0,0 +1,605 @@
|
||||
// Telegram plugin module implements lane delivery text deliverer behavior.
|
||||
import {
|
||||
createPreviewMessageReceipt,
|
||||
type MessageReceipt,
|
||||
} from "openclaw/plugin-sdk/channel-outbound";
|
||||
import {
|
||||
isPotentialTruncatedFinal,
|
||||
selectLongerFinalText,
|
||||
} from "openclaw/plugin-sdk/channel-outbound";
|
||||
import {
|
||||
buildTtsSupplementMediaPayload,
|
||||
getReplyPayloadTtsSupplement,
|
||||
resolveSendableOutboundReplyParts,
|
||||
} from "openclaw/plugin-sdk/reply-payload";
|
||||
import type { ReplyPayload } from "openclaw/plugin-sdk/reply-runtime";
|
||||
import type { TelegramInlineButtons } from "./button-types.js";
|
||||
import type { TelegramDraftStream } from "./draft-stream.js";
|
||||
|
||||
export type LaneName = "answer" | "reasoning";
|
||||
|
||||
export type DraftLaneState = {
|
||||
stream: TelegramDraftStream | undefined;
|
||||
lastPartialText: string;
|
||||
hasStreamedMessage: boolean;
|
||||
finalized: boolean;
|
||||
activeChunkIndex: number;
|
||||
};
|
||||
|
||||
type LanePreviewFinalizedDelivery = {
|
||||
content: string;
|
||||
promptContextContent?: string;
|
||||
messageId: number;
|
||||
buttonsAttached?: boolean;
|
||||
receipt: MessageReceipt;
|
||||
};
|
||||
|
||||
type LanePreviewFinalizedDeliveryInput = Omit<LanePreviewFinalizedDelivery, "receipt"> & {
|
||||
receipt?: MessageReceipt;
|
||||
};
|
||||
|
||||
export type LaneDeliveryResult =
|
||||
| {
|
||||
kind: "preview-finalized";
|
||||
delivery: LanePreviewFinalizedDelivery;
|
||||
}
|
||||
| { kind: "preview-retained" | "preview-updated" | "sent" | "skipped" };
|
||||
|
||||
type CreateLaneTextDelivererParams = {
|
||||
lanes: Record<LaneName, DraftLaneState>;
|
||||
draftMaxChars: number;
|
||||
applyTextToPayload: (payload: ReplyPayload, text: string) => ReplyPayload;
|
||||
applyTextToFollowUpPayload?: (payload: ReplyPayload, text: string) => ReplyPayload;
|
||||
splitFinalTextForStream?: (text: string) => readonly string[];
|
||||
sendPayload: (
|
||||
payload: ReplyPayload,
|
||||
options?: { durable?: boolean; silent?: boolean },
|
||||
) => Promise<boolean>;
|
||||
flushDraftLane: (lane: DraftLaneState) => Promise<void>;
|
||||
stopDraftLane: (lane: DraftLaneState) => Promise<void>;
|
||||
clearDraftLane: (lane: DraftLaneState) => Promise<void>;
|
||||
editStreamMessage: (params: {
|
||||
laneName: LaneName;
|
||||
messageId: number;
|
||||
text: string;
|
||||
buttons?: TelegramInlineButtons;
|
||||
}) => Promise<void>;
|
||||
resolveFinalTextCandidate?: (params: {
|
||||
finalText: string;
|
||||
laneName: LaneName;
|
||||
}) => Promise<string | undefined> | string | undefined;
|
||||
log: (message: string) => void;
|
||||
markDelivered: () => void;
|
||||
};
|
||||
|
||||
type DeliverLaneTextParams = {
|
||||
laneName: LaneName;
|
||||
text: string;
|
||||
payload: ReplyPayload;
|
||||
infoKind: string;
|
||||
buttons?: TelegramInlineButtons;
|
||||
finalizePreview?: boolean;
|
||||
durable?: boolean;
|
||||
};
|
||||
|
||||
function result(
|
||||
kind: LaneDeliveryResult["kind"],
|
||||
delivery?: LanePreviewFinalizedDeliveryInput,
|
||||
): LaneDeliveryResult {
|
||||
if (kind === "preview-finalized") {
|
||||
const finalized = delivery!;
|
||||
return {
|
||||
kind,
|
||||
delivery: {
|
||||
...finalized,
|
||||
receipt: finalized.receipt ?? createPreviewMessageReceipt({ id: finalized.messageId }),
|
||||
},
|
||||
};
|
||||
}
|
||||
return { kind };
|
||||
}
|
||||
|
||||
function compactChunks(chunks: readonly string[]): string[] {
|
||||
const out: string[] = [];
|
||||
let whitespace = "";
|
||||
for (const chunk of chunks) {
|
||||
if (!chunk) {
|
||||
continue;
|
||||
}
|
||||
if (chunk.trim().length === 0) {
|
||||
whitespace += chunk;
|
||||
continue;
|
||||
}
|
||||
out.push(`${whitespace}${chunk}`);
|
||||
whitespace = "";
|
||||
}
|
||||
if (whitespace && out.length > 0) {
|
||||
out[out.length - 1] = `${out[out.length - 1]}${whitespace}`;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
function isDeliveredPrefix(params: { deliveredText: string | undefined; finalText: string }) {
|
||||
if (!params.deliveredText || params.deliveredText.length === 0) {
|
||||
return false;
|
||||
}
|
||||
return (
|
||||
params.finalText === params.deliveredText || params.finalText.startsWith(params.deliveredText)
|
||||
);
|
||||
}
|
||||
|
||||
export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
|
||||
const followUpPayload = (payload: ReplyPayload, text: string) =>
|
||||
params.applyTextToFollowUpPayload
|
||||
? params.applyTextToFollowUpPayload(payload, text)
|
||||
: params.applyTextToPayload(payload, text);
|
||||
const textOnlyPayload = (payload: ReplyPayload): ReplyPayload => {
|
||||
const {
|
||||
mediaUrl: _mediaUrl,
|
||||
mediaUrls: _mediaUrls,
|
||||
audioAsVoice: _audioAsVoice,
|
||||
spokenText: _spokenText,
|
||||
...rest
|
||||
} = payload;
|
||||
return rest;
|
||||
};
|
||||
const mediaChannelData = (
|
||||
channelData: ReplyPayload["channelData"],
|
||||
options?: { stripButtons?: boolean },
|
||||
): ReplyPayload["channelData"] => {
|
||||
if (!options?.stripButtons) {
|
||||
return channelData;
|
||||
}
|
||||
const telegramData = channelData?.telegram;
|
||||
if (!telegramData || typeof telegramData !== "object" || Array.isArray(telegramData)) {
|
||||
return channelData;
|
||||
}
|
||||
const { buttons: _buttons, ...telegramRest } = telegramData as Record<string, unknown>;
|
||||
if (_buttons === undefined) {
|
||||
return channelData;
|
||||
}
|
||||
const next: Record<string, unknown> = { ...channelData };
|
||||
if (Object.keys(telegramRest).length > 0) {
|
||||
next.telegram = telegramRest;
|
||||
} else {
|
||||
delete next.telegram;
|
||||
}
|
||||
return Object.keys(next).length > 0 ? next : undefined;
|
||||
};
|
||||
const withMediaChannelData = (
|
||||
payload: ReplyPayload,
|
||||
options?: { stripButtons?: boolean },
|
||||
): ReplyPayload => {
|
||||
const channelData = mediaChannelData(payload.channelData, options);
|
||||
if (channelData === payload.channelData) {
|
||||
return payload;
|
||||
}
|
||||
if (channelData) {
|
||||
return { ...payload, channelData };
|
||||
}
|
||||
const { channelData: _channelData, ...rest } = payload;
|
||||
return rest;
|
||||
};
|
||||
const withFallbackTelegramButtons = (
|
||||
payload: ReplyPayload,
|
||||
buttons?: TelegramInlineButtons,
|
||||
): ReplyPayload => {
|
||||
if (!buttons) {
|
||||
return payload;
|
||||
}
|
||||
const channelData = payload.channelData ?? {};
|
||||
const telegramData = channelData.telegram;
|
||||
if (
|
||||
telegramData &&
|
||||
typeof telegramData === "object" &&
|
||||
!Array.isArray(telegramData) &&
|
||||
"buttons" in telegramData
|
||||
) {
|
||||
return payload;
|
||||
}
|
||||
const telegramRest =
|
||||
telegramData && typeof telegramData === "object" && !Array.isArray(telegramData)
|
||||
? (telegramData as Record<string, unknown>)
|
||||
: {};
|
||||
return {
|
||||
...payload,
|
||||
channelData: {
|
||||
...channelData,
|
||||
telegram: {
|
||||
...telegramRest,
|
||||
buttons,
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
const mediaOnlyPayload = (
|
||||
payload: ReplyPayload,
|
||||
text: string,
|
||||
options?: { stripButtons?: boolean; fallbackButtons?: TelegramInlineButtons },
|
||||
): ReplyPayload => {
|
||||
if (getReplyPayloadTtsSupplement(payload)) {
|
||||
return withFallbackTelegramButtons(
|
||||
withMediaChannelData(
|
||||
buildTtsSupplementMediaPayload(params.applyTextToPayload(payload, text)),
|
||||
options,
|
||||
),
|
||||
options?.fallbackButtons,
|
||||
);
|
||||
}
|
||||
if (payload.audioAsVoice === true) {
|
||||
const {
|
||||
text: _text,
|
||||
presentation: _presentation,
|
||||
interactive: _interactive,
|
||||
btw: _btw,
|
||||
spokenText: _spokenText,
|
||||
...voicePayload
|
||||
} = params.applyTextToPayload(payload, text);
|
||||
return withFallbackTelegramButtons(
|
||||
withMediaChannelData({ ...voicePayload, spokenText: text }, options),
|
||||
options?.fallbackButtons,
|
||||
);
|
||||
}
|
||||
const {
|
||||
text: _text,
|
||||
presentation: _presentation,
|
||||
interactive: _interactive,
|
||||
btw: _btw,
|
||||
...rest
|
||||
} = payload;
|
||||
return withFallbackTelegramButtons(
|
||||
withMediaChannelData(rest, options),
|
||||
options?.fallbackButtons,
|
||||
);
|
||||
};
|
||||
|
||||
const clearUnfinalizedStream = async (lane: DraftLaneState) => {
|
||||
if (!lane.stream || lane.finalized) {
|
||||
return;
|
||||
}
|
||||
await params.clearDraftLane(lane);
|
||||
lane.lastPartialText = "";
|
||||
lane.hasStreamedMessage = false;
|
||||
};
|
||||
|
||||
const discardUnmaterializedStream = async (lane: DraftLaneState) => {
|
||||
const stream = lane.stream;
|
||||
if (stream) {
|
||||
await stream.discard?.();
|
||||
stream.forceNewMessage();
|
||||
}
|
||||
lane.lastPartialText = "";
|
||||
lane.hasStreamedMessage = false;
|
||||
lane.finalized = false;
|
||||
};
|
||||
|
||||
const rotateFinalizedStream = (lane: DraftLaneState) => {
|
||||
if (!lane.stream || !lane.finalized) {
|
||||
return;
|
||||
}
|
||||
lane.stream.forceNewMessage();
|
||||
lane.lastPartialText = "";
|
||||
lane.hasStreamedMessage = false;
|
||||
lane.finalized = false;
|
||||
};
|
||||
|
||||
const streamText = async (
|
||||
laneName: LaneName,
|
||||
lane: DraftLaneState,
|
||||
text: string,
|
||||
payload: ReplyPayload,
|
||||
useFinalTextRecovery: boolean,
|
||||
finalizePreview: boolean,
|
||||
buttons?: TelegramInlineButtons,
|
||||
): Promise<LaneDeliveryResult | undefined> => {
|
||||
const stream = lane.stream;
|
||||
if (!stream || text.length === 0 || payload.isError) {
|
||||
return undefined;
|
||||
}
|
||||
rotateFinalizedStream(lane);
|
||||
|
||||
const chunks =
|
||||
text.length > params.draftMaxChars
|
||||
? compactChunks(params.splitFinalTextForStream?.(text) ?? [])
|
||||
: [text];
|
||||
|
||||
const clampActiveChunkIndex = () =>
|
||||
Math.min(lane.activeChunkIndex, Math.max(0, chunks.length - 1));
|
||||
const activeChunkIndex = clampActiveChunkIndex();
|
||||
const activeChunk = chunks[activeChunkIndex];
|
||||
const remainingChunks = chunks.slice(activeChunkIndex + 1);
|
||||
|
||||
if (!activeChunk || activeChunk.length > params.draftMaxChars) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const activeFullText = chunks.slice(activeChunkIndex).join("");
|
||||
const finalText = activeFullText.trimEnd();
|
||||
const deliveredStreamTextBeforeUpdate = stream.lastDeliveredText?.();
|
||||
const deliveredPrefixBeforeUpdate =
|
||||
useFinalTextRecovery &&
|
||||
deliveredStreamTextBeforeUpdate !== undefined &&
|
||||
isDeliveredPrefix({
|
||||
deliveredText: deliveredStreamTextBeforeUpdate,
|
||||
finalText,
|
||||
}) &&
|
||||
deliveredStreamTextBeforeUpdate.length > activeChunk.trimEnd().length;
|
||||
|
||||
const finalizeDeliveredPrefix = async (
|
||||
deliveredStreamText: string,
|
||||
messageId: number,
|
||||
): Promise<LaneDeliveryResult> => {
|
||||
lane.finalized = true;
|
||||
params.markDelivered();
|
||||
let buttonsAttached = false;
|
||||
if (buttons) {
|
||||
const deliveredChunks = compactChunks(
|
||||
params.splitFinalTextForStream?.(deliveredStreamText) ?? [],
|
||||
);
|
||||
const currentChunk = deliveredChunks.at(-1);
|
||||
if (currentChunk && currentChunk.length <= params.draftMaxChars) {
|
||||
try {
|
||||
await params.editStreamMessage({ laneName, messageId, text: currentChunk, buttons });
|
||||
buttonsAttached = true;
|
||||
} catch (err) {
|
||||
params.log(`telegram: ${laneName} stream button edit failed: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
const suffix = activeFullText.slice(deliveredStreamText.length);
|
||||
if (suffix.trim().length > 0) {
|
||||
for (const chunk of compactChunks(params.splitFinalTextForStream?.(suffix) ?? [])) {
|
||||
if (chunk.trim().length === 0) {
|
||||
continue;
|
||||
}
|
||||
await params.sendPayload(followUpPayload(payload, chunk));
|
||||
}
|
||||
}
|
||||
return result("preview-finalized", {
|
||||
content: text,
|
||||
promptContextContent: deliveredStreamText,
|
||||
messageId,
|
||||
buttonsAttached,
|
||||
});
|
||||
};
|
||||
|
||||
const candidateTexts = [stream.lastDeliveredText?.(), lane.lastPartialText];
|
||||
if (
|
||||
useFinalTextRecovery &&
|
||||
remainingChunks.length === 0 &&
|
||||
isPotentialTruncatedFinal(activeFullText)
|
||||
) {
|
||||
const resolvedFullCandidate = await params.resolveFinalTextCandidate?.({
|
||||
finalText: text,
|
||||
laneName,
|
||||
});
|
||||
if (resolvedFullCandidate) {
|
||||
const resolvedChunks =
|
||||
resolvedFullCandidate.length > params.draftMaxChars
|
||||
? compactChunks(params.splitFinalTextForStream?.(resolvedFullCandidate) ?? [])
|
||||
: [resolvedFullCandidate];
|
||||
candidateTexts.push(resolvedChunks.slice(activeChunkIndex).join(""));
|
||||
}
|
||||
}
|
||||
|
||||
const retainedPreview =
|
||||
useFinalTextRecovery &&
|
||||
remainingChunks.length === 0 &&
|
||||
isPotentialTruncatedFinal(activeFullText)
|
||||
? selectLongerFinalText({
|
||||
finalText: activeFullText,
|
||||
candidateTexts,
|
||||
})
|
||||
: undefined;
|
||||
|
||||
if (retainedPreview && (!buttons || retainedPreview.length <= params.draftMaxChars)) {
|
||||
const previewText = retainedPreview;
|
||||
lane.lastPartialText = previewText;
|
||||
lane.hasStreamedMessage = true;
|
||||
await params.stopDraftLane(lane);
|
||||
const messageId = stream.messageId();
|
||||
if (typeof messageId !== "number") {
|
||||
if (stream.sendMayHaveLanded?.()) {
|
||||
lane.finalized = true;
|
||||
params.markDelivered();
|
||||
return result("preview-retained");
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
const deliveredStreamTextAfterStop = stream.lastDeliveredText?.();
|
||||
if (
|
||||
deliveredStreamTextAfterStop !== undefined &&
|
||||
deliveredStreamTextAfterStop !== previewText
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
let buttonsAttached = false;
|
||||
if (buttons) {
|
||||
try {
|
||||
await params.editStreamMessage({ laneName, messageId, text: previewText, buttons });
|
||||
buttonsAttached = true;
|
||||
} catch (err) {
|
||||
params.log(`telegram: ${laneName} stream button edit failed: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
for (const chunk of remainingChunks) {
|
||||
if (chunk.trim().length === 0) {
|
||||
continue;
|
||||
}
|
||||
await params.sendPayload(followUpPayload(payload, chunk));
|
||||
}
|
||||
lane.finalized = true;
|
||||
params.markDelivered();
|
||||
return result("preview-finalized", {
|
||||
content: previewText,
|
||||
promptContextContent: previewText,
|
||||
messageId,
|
||||
buttonsAttached,
|
||||
});
|
||||
}
|
||||
|
||||
if (!deliveredPrefixBeforeUpdate) {
|
||||
lane.lastPartialText = activeChunk;
|
||||
lane.hasStreamedMessage = true;
|
||||
lane.finalized = false;
|
||||
stream.update(activeChunk);
|
||||
}
|
||||
if (finalizePreview) {
|
||||
await params.stopDraftLane(lane);
|
||||
} else {
|
||||
await params.flushDraftLane(lane);
|
||||
}
|
||||
const activeChunkIndexAfterStop = useFinalTextRecovery
|
||||
? clampActiveChunkIndex()
|
||||
: activeChunkIndex;
|
||||
const deliveredStreamTextAfterStop = stream.lastDeliveredText?.();
|
||||
const retainedOriginalActiveChunkAfterStop =
|
||||
activeChunkIndexAfterStop > activeChunkIndex &&
|
||||
deliveredStreamTextAfterStop === activeChunk.trimEnd();
|
||||
// `activeChunkIndex` is advanced by retained preview callbacks. If callbacks
|
||||
// outrun the stream's delivered text, trust the delivered text and replay the gap.
|
||||
const effectiveActiveChunkIndexAfterStop = retainedOriginalActiveChunkAfterStop
|
||||
? activeChunkIndex
|
||||
: activeChunkIndexAfterStop;
|
||||
const activeChunkAfterStop = chunks[effectiveActiveChunkIndexAfterStop] ?? activeChunk;
|
||||
const remainingChunksAfterStop = chunks.slice(effectiveActiveChunkIndexAfterStop + 1);
|
||||
|
||||
const messageId = stream.messageId();
|
||||
if (typeof messageId !== "number") {
|
||||
if (finalizePreview && stream.sendMayHaveLanded?.()) {
|
||||
lane.finalized = true;
|
||||
params.markDelivered();
|
||||
return result("preview-retained");
|
||||
}
|
||||
if (!finalizePreview) {
|
||||
await discardUnmaterializedStream(lane);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const activeChunkTextAfterStop = activeChunkAfterStop.trimEnd();
|
||||
if (
|
||||
finalizePreview &&
|
||||
deliveredStreamTextAfterStop !== undefined &&
|
||||
deliveredStreamTextAfterStop !== activeChunkTextAfterStop &&
|
||||
!retainedOriginalActiveChunkAfterStop
|
||||
) {
|
||||
if (
|
||||
useFinalTextRecovery &&
|
||||
isDeliveredPrefix({ deliveredText: deliveredStreamTextAfterStop, finalText }) &&
|
||||
deliveredStreamTextAfterStop.length > activeChunkTextAfterStop.length
|
||||
) {
|
||||
return await finalizeDeliveredPrefix(deliveredStreamTextAfterStop, messageId);
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (deliveredPrefixBeforeUpdate && deliveredStreamTextAfterStop === undefined) {
|
||||
return await finalizeDeliveredPrefix(deliveredStreamTextBeforeUpdate, messageId);
|
||||
}
|
||||
|
||||
params.markDelivered();
|
||||
let buttonsAttached = false;
|
||||
if (buttons) {
|
||||
try {
|
||||
await params.editStreamMessage({
|
||||
laneName,
|
||||
messageId,
|
||||
text: activeChunkAfterStop,
|
||||
buttons,
|
||||
});
|
||||
buttonsAttached = true;
|
||||
} catch (err) {
|
||||
params.log(`telegram: ${laneName} stream button edit failed: ${String(err)}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (finalizePreview) {
|
||||
lane.finalized = true;
|
||||
for (const chunk of remainingChunksAfterStop) {
|
||||
if (chunk.trim().length === 0) {
|
||||
continue;
|
||||
}
|
||||
await params.sendPayload(followUpPayload(payload, chunk));
|
||||
}
|
||||
return result("preview-finalized", {
|
||||
content: text,
|
||||
promptContextContent: activeChunkAfterStop,
|
||||
messageId,
|
||||
buttonsAttached,
|
||||
});
|
||||
}
|
||||
|
||||
return result("preview-updated");
|
||||
};
|
||||
|
||||
return async ({
|
||||
laneName,
|
||||
text,
|
||||
payload,
|
||||
infoKind,
|
||||
buttons,
|
||||
finalizePreview: requestedFinalizePreview,
|
||||
durable: requestedDurable,
|
||||
}: DeliverLaneTextParams): Promise<LaneDeliveryResult> => {
|
||||
const lane = params.lanes[laneName];
|
||||
const reply = resolveSendableOutboundReplyParts(payload, { text });
|
||||
const isDurableFinal = infoKind === "final";
|
||||
const finalizePreview = requestedFinalizePreview ?? isDurableFinal;
|
||||
const durable = requestedDurable ?? isDurableFinal;
|
||||
const streamed = !reply.hasMedia
|
||||
? await streamText(laneName, lane, text, payload, isDurableFinal, finalizePreview, buttons)
|
||||
: undefined;
|
||||
if (streamed) {
|
||||
return streamed;
|
||||
}
|
||||
|
||||
if (
|
||||
finalizePreview &&
|
||||
reply.hasMedia &&
|
||||
lane.stream &&
|
||||
lane.hasStreamedMessage &&
|
||||
!lane.finalized &&
|
||||
text.trim().length > 0
|
||||
) {
|
||||
const finalizedPreview = await streamText(
|
||||
laneName,
|
||||
lane,
|
||||
text,
|
||||
textOnlyPayload(payload),
|
||||
isDurableFinal,
|
||||
true,
|
||||
buttons,
|
||||
);
|
||||
if (finalizedPreview) {
|
||||
const stripButtons =
|
||||
finalizedPreview.kind === "preview-finalized" &&
|
||||
finalizedPreview.delivery.buttonsAttached === true;
|
||||
const mediaText =
|
||||
finalizedPreview.kind === "preview-finalized" ? finalizedPreview.delivery.content : text;
|
||||
await params.sendPayload(
|
||||
mediaOnlyPayload(payload, mediaText, {
|
||||
stripButtons,
|
||||
fallbackButtons: stripButtons ? undefined : buttons,
|
||||
}),
|
||||
{
|
||||
durable,
|
||||
},
|
||||
);
|
||||
return finalizedPreview;
|
||||
}
|
||||
}
|
||||
|
||||
if (finalizePreview) {
|
||||
await clearUnfinalizedStream(lane);
|
||||
}
|
||||
|
||||
const delivered = await params.sendPayload(params.applyTextToPayload(payload, text), {
|
||||
durable,
|
||||
});
|
||||
if (delivered && finalizePreview) {
|
||||
lane.finalized = true;
|
||||
}
|
||||
return delivered ? result("sent") : result("skipped");
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user