fix(telegram): add initial message debounce for better push notifications (#18147)

Merged via /review-pr -> /prepare-pr -> /merge-pr.

Prepared head SHA: 5e2285b6a0
Co-authored-by: Marvae <11957602+Marvae@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
This commit is contained in:
Hongwei Ma
2026-02-17 13:51:49 +08:00
committed by GitHub
parent 2e375a5498
commit 7ffc8f9f7c
6 changed files with 261 additions and 32 deletions

View File

@@ -1,4 +1,10 @@
import type { Bot } from "grammy";
import type { OpenClawConfig, ReplyToMode, TelegramAccountConfig } from "../config/types.js";
import type { RuntimeEnv } from "../runtime.js";
import type { TelegramMessageContext } from "./bot-message-context.js";
import type { TelegramBotOptions } from "./bot.js";
import type { TelegramStreamMode } from "./bot/types.js";
import type { TelegramInlineButtons } from "./button-types.js";
import { resolveAgentDir } from "../agents/agent-scope.js";
import {
findModelInCatalog,
@@ -15,15 +21,9 @@ import { logAckFailure, logTypingFailure } from "../channels/logging.js";
import { createReplyPrefixOptions } from "../channels/reply-prefix.js";
import { createTypingCallbacks } from "../channels/typing.js";
import { resolveMarkdownTableMode } from "../config/markdown-tables.js";
import type { OpenClawConfig, ReplyToMode, TelegramAccountConfig } from "../config/types.js";
import { danger, logVerbose } from "../globals.js";
import { getAgentScopedMediaLocalRoots } from "../media/local-roots.js";
import type { RuntimeEnv } from "../runtime.js";
import type { TelegramMessageContext } from "./bot-message-context.js";
import type { TelegramBotOptions } from "./bot.js";
import { deliverReplies } from "./bot/delivery.js";
import type { TelegramStreamMode } from "./bot/types.js";
import type { TelegramInlineButtons } from "./button-types.js";
import { resolveTelegramDraftStreamingChunking } from "./draft-chunking.js";
import { createTelegramDraftStream } from "./draft-stream.js";
import { editMessageTelegram } from "./send.js";
@@ -31,6 +31,9 @@ import { cacheSticker, describeStickerImage } from "./sticker-cache.js";
const EMPTY_RESPONSE_FALLBACK = "No response generated. Please try again.";
/** Minimum chars before sending first streaming message (improves push notification UX) */
const DRAFT_MIN_INITIAL_CHARS = 30;
async function resolveStickerVisionSupport(cfg: OpenClawConfig, agentId: string) {
try {
const catalog = await loadModelCatalog({ config: cfg });
@@ -101,6 +104,7 @@ export const dispatchTelegramMessage = async ({
maxChars: draftMaxChars,
thread: threadSpec,
replyToMessageId: draftReplyToMessageId,
minInitialChars: DRAFT_MIN_INITIAL_CHARS,
log: logVerbose,
warn: logVerbose,
})
@@ -314,7 +318,7 @@ export const dispatchTelegramMessage = async ({
finalText.length <= draftMaxChars &&
!payload.isError;
if (canFinalizeViaPreviewEdit) {
draftStream?.stop();
await draftStream?.stop();
draftStoppedForPreviewEdit = true;
if (
currentPreviewText &&
@@ -353,7 +357,36 @@ export const dispatchTelegramMessage = async ({
);
}
if (!draftStoppedForPreviewEdit) {
draftStream?.stop();
await draftStream?.stop();
}
// Check if stop() sent a message (debounce released on isFinal)
// If so, edit that message instead of sending a new one
const messageIdAfterStop = draftStream?.messageId();
if (
!finalizedViaPreviewMessage &&
typeof messageIdAfterStop === "number" &&
typeof finalText === "string" &&
finalText.length > 0 &&
finalText.length <= draftMaxChars &&
!hasMedia &&
!payload.isError
) {
try {
await editMessageTelegram(chatId, messageIdAfterStop, finalText, {
api: bot.api,
cfg,
accountId: route.accountId,
linkPreview: telegramCfg.linkPreview,
buttons: previewButtons,
});
finalizedViaPreviewMessage = true;
deliveryState.delivered = true;
return;
} catch (err) {
logVerbose(
`telegram: post-stop preview edit failed; falling back to standard send (${String(err)})`,
);
}
}
}
const result = await deliverReplies({
@@ -421,10 +454,11 @@ export const dispatchTelegramMessage = async ({
},
}));
} finally {
// Must stop() first to flush debounced content before clear() wipes state
await draftStream?.stop();
if (!finalizedViaPreviewMessage) {
await draftStream?.clear();
}
draftStream?.stop();
}
let sentFallback = false;
if (!deliveryState.delivered && deliveryState.skippedNonSilent > 0) {