mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-10 07:02:44 +00:00
Telegram: coalesce forwarded text+media bursts into one inbound turn (#19476)
Merged via /review-pr -> /prepare-pr -> /merge-pr.
Prepared head SHA: 09e0b4e9bd
Co-authored-by: napetrov <18015221+napetrov@users.noreply.github.com>
Co-authored-by: obviyus <22031114+obviyus@users.noreply.github.com>
Reviewed-by: @obviyus
This commit is contained in:
@@ -114,14 +114,33 @@ export const registerTelegramHandlers = ({
|
||||
let textFragmentProcessing: Promise<void> = Promise.resolve();
|
||||
|
||||
const debounceMs = resolveInboundDebounceMs({ cfg, channel: "telegram" });
|
||||
const FORWARD_BURST_DEBOUNCE_MS = 80;
|
||||
type TelegramDebounceLane = "default" | "forward";
|
||||
type TelegramDebounceEntry = {
|
||||
ctx: TelegramContext;
|
||||
msg: Message;
|
||||
allMedia: TelegramMediaRef[];
|
||||
storeAllowFrom: string[];
|
||||
debounceKey: string | null;
|
||||
debounceLane: TelegramDebounceLane;
|
||||
botUsername?: string;
|
||||
};
|
||||
const resolveTelegramDebounceLane = (msg: Message): TelegramDebounceLane => {
|
||||
const forwardMeta = msg as {
|
||||
forward_origin?: unknown;
|
||||
forward_from?: unknown;
|
||||
forward_from_chat?: unknown;
|
||||
forward_sender_name?: unknown;
|
||||
forward_date?: unknown;
|
||||
};
|
||||
return (forwardMeta.forward_origin ??
|
||||
forwardMeta.forward_from ??
|
||||
forwardMeta.forward_from_chat ??
|
||||
forwardMeta.forward_sender_name ??
|
||||
forwardMeta.forward_date)
|
||||
? "forward"
|
||||
: "default";
|
||||
};
|
||||
const buildSyntheticTextMessage = (params: {
|
||||
base: Message;
|
||||
text: string;
|
||||
@@ -148,16 +167,19 @@ export const registerTelegramHandlers = ({
|
||||
};
|
||||
const inboundDebouncer = createInboundDebouncer<TelegramDebounceEntry>({
|
||||
debounceMs,
|
||||
resolveDebounceMs: (entry) =>
|
||||
entry.debounceLane === "forward" ? FORWARD_BURST_DEBOUNCE_MS : debounceMs,
|
||||
buildKey: (entry) => entry.debounceKey,
|
||||
shouldDebounce: (entry) => {
|
||||
if (entry.allMedia.length > 0) {
|
||||
return false;
|
||||
}
|
||||
const text = entry.msg.text ?? entry.msg.caption ?? "";
|
||||
if (!text.trim()) {
|
||||
const hasText = text.trim().length > 0;
|
||||
if (hasText && hasControlCommand(text, cfg, { botUsername: entry.botUsername })) {
|
||||
return false;
|
||||
}
|
||||
return !hasControlCommand(text, cfg, { botUsername: entry.botUsername });
|
||||
if (entry.debounceLane === "forward") {
|
||||
return true;
|
||||
}
|
||||
return entry.allMedia.length === 0 && hasText;
|
||||
},
|
||||
onFlush: async (entries) => {
|
||||
const last = entries.at(-1);
|
||||
@@ -172,7 +194,8 @@ export const registerTelegramHandlers = ({
|
||||
.map((entry) => entry.msg.text ?? entry.msg.caption ?? "")
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
if (!combinedText.trim()) {
|
||||
const combinedMedia = entries.flatMap((entry) => entry.allMedia);
|
||||
if (!combinedText.trim() && combinedMedia.length === 0) {
|
||||
return;
|
||||
}
|
||||
const first = entries[0];
|
||||
@@ -185,7 +208,7 @@ export const registerTelegramHandlers = ({
|
||||
const messageIdOverride = last.msg.message_id ? String(last.msg.message_id) : undefined;
|
||||
await processMessage(
|
||||
buildSyntheticContext(baseCtx, syntheticMessage),
|
||||
[],
|
||||
combinedMedia,
|
||||
first.storeAllowFrom,
|
||||
messageIdOverride ? { messageIdOverride } : undefined,
|
||||
);
|
||||
@@ -722,8 +745,9 @@ export const registerTelegramHandlers = ({
|
||||
const senderId = msg.from?.id ? String(msg.from.id) : "";
|
||||
const conversationKey =
|
||||
resolvedThreadId != null ? `${chatId}:topic:${resolvedThreadId}` : String(chatId);
|
||||
const debounceLane = resolveTelegramDebounceLane(msg);
|
||||
const debounceKey = senderId
|
||||
? `telegram:${accountId ?? "default"}:${conversationKey}:${senderId}`
|
||||
? `telegram:${accountId ?? "default"}:${conversationKey}:${senderId}:${debounceLane}`
|
||||
: null;
|
||||
await inboundDebouncer.enqueue({
|
||||
ctx,
|
||||
@@ -731,6 +755,7 @@ export const registerTelegramHandlers = ({
|
||||
allMedia,
|
||||
storeAllowFrom,
|
||||
debounceKey,
|
||||
debounceLane,
|
||||
botUsername: ctx.me?.username,
|
||||
});
|
||||
};
|
||||
|
||||
@@ -322,6 +322,67 @@ describe("telegram media groups", () => {
|
||||
);
|
||||
});
|
||||
|
||||
describe("telegram forwarded bursts", () => {
|
||||
afterEach(() => {
|
||||
vi.clearAllTimers();
|
||||
vi.useRealTimers();
|
||||
});
|
||||
|
||||
const FORWARD_BURST_TEST_TIMEOUT_MS = process.platform === "win32" ? 45_000 : 20_000;
|
||||
|
||||
it(
|
||||
"coalesces forwarded text + forwarded attachment into a single processing turn with default debounce config",
|
||||
async () => {
|
||||
const runtimeError = vi.fn();
|
||||
const { handler, replySpy } = await createBotHandlerWithOptions({ runtimeError });
|
||||
const fetchSpy = mockTelegramPngDownload();
|
||||
|
||||
try {
|
||||
await handler({
|
||||
message: {
|
||||
chat: { id: 42, type: "private" },
|
||||
from: { id: 777, is_bot: false, first_name: "N" },
|
||||
message_id: 21,
|
||||
text: "Look at this",
|
||||
date: 1736380800,
|
||||
forward_origin: { type: "hidden_user", date: 1736380700, sender_user_name: "A" },
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({}),
|
||||
});
|
||||
|
||||
await handler({
|
||||
message: {
|
||||
chat: { id: 42, type: "private" },
|
||||
from: { id: 777, is_bot: false, first_name: "N" },
|
||||
message_id: 22,
|
||||
date: 1736380801,
|
||||
photo: [{ file_id: "fwd_photo_1" }],
|
||||
forward_origin: { type: "hidden_user", date: 1736380701, sender_user_name: "A" },
|
||||
},
|
||||
me: { username: "openclaw_bot" },
|
||||
getFile: async () => ({ file_path: "photos/fwd1.jpg" }),
|
||||
});
|
||||
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(replySpy).toHaveBeenCalledTimes(1);
|
||||
},
|
||||
{ timeout: FORWARD_BURST_TEST_TIMEOUT_MS, interval: 10 },
|
||||
);
|
||||
|
||||
expect(runtimeError).not.toHaveBeenCalled();
|
||||
const payload = replySpy.mock.calls[0][0];
|
||||
expect(payload.Body).toContain("Look at this");
|
||||
expect(payload.MediaPaths).toHaveLength(1);
|
||||
} finally {
|
||||
fetchSpy.mockRestore();
|
||||
}
|
||||
},
|
||||
FORWARD_BURST_TEST_TIMEOUT_MS,
|
||||
);
|
||||
});
|
||||
|
||||
describe("telegram stickers", () => {
|
||||
const STICKER_TEST_TIMEOUT_MS = process.platform === "win32" ? 30_000 : 20_000;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user