






















@@ -52,6 +52,10 @@ type CreateLaneTextDelivererParams = {
5252text: string;
5353buttons?: TelegramInlineButtons;
5454}) => Promise<void>;
55+resolveFinalTextCandidate?: (params: {
56+finalText: string;
57+laneName: LaneName;
58+}) => Promise<string | undefined> | string | undefined;
5559log: (message: string) => void;
5660markDelivered: () => void;
5761};
@@ -101,6 +105,53 @@ function compactChunks(chunks: readonly string[]): string[] {
101105return out;
102106}
103107108+function stripTrailingEllipsis(text: string): string {
109+return text.replace(/(?:\s*(?:\.{3}|\u2026))+$/u, "").trimEnd();
110+}
111+112+const MIN_TRUNCATED_FINAL_PREFIX_CHARS = 48;
113+const MIN_TRUNCATED_FINAL_CONTINUATION_CHARS = 24;
114+115+function isPotentialTruncatedFinal(finalText: string): boolean {
116+const trimmedFinal = finalText.trimEnd();
117+const untruncatedFinal = stripTrailingEllipsis(trimmedFinal);
118+return (
119+untruncatedFinal.length >= MIN_TRUNCATED_FINAL_PREFIX_CHARS && untruncatedFinal !== trimmedFinal
120+);
121+}
122+123+function selectLongerPreviewForFinal(params: {
124+finalText: string;
125+candidateTexts: readonly (string | undefined)[];
126+}): string | undefined {
127+const finalText = params.finalText.trimEnd();
128+const untruncatedFinal = stripTrailingEllipsis(finalText);
129+if (
130+untruncatedFinal.length < MIN_TRUNCATED_FINAL_PREFIX_CHARS ||
131+untruncatedFinal === finalText
132+) {
133+return undefined;
134+}
135+for (const candidate of params.candidateTexts) {
136+const candidateText = candidate?.trimEnd();
137+if (
138+!candidateText ||
139+candidateText.length <= finalText.length ||
140+!candidateText.startsWith(untruncatedFinal)
141+) {
142+continue;
143+}
144+const continuation = candidateText.slice(untruncatedFinal.length).trimStart();
145+if (
146+continuation.length >= MIN_TRUNCATED_FINAL_CONTINUATION_CHARS &&
147+/^[\p{L}\p{N}]/u.test(continuation)
148+) {
149+return candidateText;
150+}
151+}
152+return undefined;
153+}
154+104155export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
105156const followUpPayload = (payload: ReplyPayload, text: string) =>
106157params.applyTextToFollowUpPayload
@@ -138,6 +189,53 @@ export function createLaneTextDeliverer(params: CreateLaneTextDelivererParams) {
138189return undefined;
139190}
140191192+const retainedPreview =
193+isFinal && remainingChunks.length === 0 && isPotentialTruncatedFinal(text)
194+ ? selectLongerPreviewForFinal({
195+finalText: text,
196+candidateTexts: [
197+await params.resolveFinalTextCandidate?.({ finalText: text, laneName }),
198+stream.lastDeliveredText?.(),
199+lane.lastPartialText,
200+],
201+})
202+ : undefined;
203+if (retainedPreview && (!buttons || retainedPreview.length <= params.draftMaxChars)) {
204+const previewText = retainedPreview;
205+lane.lastPartialText = previewText;
206+lane.hasStreamedMessage = true;
207+await params.stopDraftLane(lane);
208+const messageId = stream.messageId();
209+if (typeof messageId !== "number") {
210+if (stream.sendMayHaveLanded?.()) {
211+lane.finalized = true;
212+params.markDelivered();
213+return result("preview-retained");
214+}
215+return undefined;
216+}
217+const deliveredStreamText = stream.lastDeliveredText?.();
218+if (deliveredStreamText !== undefined && deliveredStreamText !== previewText) {
219+return undefined;
220+}
221+if (buttons) {
222+try {
223+await params.editStreamMessage({ laneName, messageId, text: previewText, buttons });
224+} catch (err) {
225+params.log(`telegram: ${laneName} stream button edit failed: ${String(err)}`);
226+}
227+}
228+for (const chunk of remainingChunks) {
229+if (chunk.trim().length === 0) {
230+continue;
231+}
232+await params.sendPayload(followUpPayload(payload, chunk));
233+}
234+lane.finalized = true;
235+params.markDelivered();
236+return result("preview-finalized", { content: previewText, messageId });
237+}
238+141239lane.lastPartialText = firstChunk;
142240lane.hasStreamedMessage = true;
143241lane.finalized = false;
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。