mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-12 21:53:00 -06:00
fix(telegram): preserve unsent media while deduplicating streamed replies (#121141)
* fix(telegram): preserve unsent media while deduplicating streamed replies ## What Problem This Solves A Telegram final reply could resend an attachment already delivered in a streamed block when the legacy mediaUrl still referenced that sent attachment while another mediaUrls item remained. Conversely, unsent legacy-only attachments must not be discarded. ## Why This Change Was Made The Telegram media-deduplication owner now clears mediaUrl only when its normalized attachment was actually sent, preserving independently unsent legacy attachments and preventing downstream outbound planning from restoring delivered media. ## User Impact Telegram replies retain every unsent attachment exactly once and no longer duplicate already streamed images in mixed final-message payloads. ## Context Seven zero-dependency scenarios passed against the actual media owner and outbound planner, including mixed legacy/vector attachments, whitespace normalization, visible-send handling, and remaining-media preservation. The exact frozen campaign baseline passed 844 tests. Existing oxfmt formatting and staged whitespace checks passed; focused Vitest CI remains pending. * refactor(telegram): inline legacy media dedup check
This commit is contained in:
committed by
GitHub
parent
7d2031986c
commit
b8f861364c
@@ -1,3 +1,7 @@
|
||||
import {
|
||||
createOutboundPayloadPlan,
|
||||
projectOutboundPayloadPlanForDelivery,
|
||||
} from "openclaw/plugin-sdk/channel-outbound";
|
||||
import { describe, expect, it, vi } from "vitest";
|
||||
import {
|
||||
describeTelegramDispatch,
|
||||
@@ -296,6 +300,45 @@ describeTelegramDispatch("dispatchTelegramMessage fallback-topic-media", () => {
|
||||
expect(finalDeliveryPayload().mediaUrls).toEqual([]);
|
||||
});
|
||||
|
||||
it("does not restore block-sent legacy media when the final includes another attachment", async () => {
|
||||
const sentMediaUrl = "/tmp/cat.jpg";
|
||||
const remainingMediaUrl = "/tmp/dog.jpg";
|
||||
deliverReplies.mockResolvedValue({ delivered: true });
|
||||
deliverInboundReplyWithMessageSendContext.mockResolvedValue({
|
||||
status: "handled_visible",
|
||||
delivery: { messageIds: ["101"], visibleReplySent: true },
|
||||
});
|
||||
dispatchReplyWithBufferedBlockDispatcher.mockImplementation(async ({ dispatcherOptions }) => {
|
||||
await dispatcherOptions.deliver({ mediaUrl: sentMediaUrl }, { kind: "block" });
|
||||
await dispatcherOptions.deliver(
|
||||
{
|
||||
text: "Here are the images",
|
||||
mediaUrls: [remainingMediaUrl],
|
||||
mediaUrl: sentMediaUrl,
|
||||
},
|
||||
{ kind: "final" },
|
||||
);
|
||||
return { queuedFinal: true };
|
||||
});
|
||||
|
||||
await dispatchWithContext({
|
||||
context: createContext(),
|
||||
streamMode: "off",
|
||||
telegramDeps: telegramDepsForTest,
|
||||
});
|
||||
|
||||
const finalPayload = finalDeliveryPayload();
|
||||
expect(finalPayload).toMatchObject({
|
||||
text: "Here are the images",
|
||||
mediaUrl: undefined,
|
||||
mediaUrls: [remainingMediaUrl],
|
||||
});
|
||||
expect(
|
||||
projectOutboundPayloadPlanForDelivery(createOutboundPayloadPlan([finalPayload]))[0]
|
||||
?.mediaUrls,
|
||||
).toEqual([remainingMediaUrl]);
|
||||
});
|
||||
|
||||
it("preserves final media when block delivery reports no visible send", async () => {
|
||||
deliverReplies.mockResolvedValueOnce({ delivered: false });
|
||||
deliverReplies.mockResolvedValue({ delivered: true });
|
||||
|
||||
@@ -55,7 +55,7 @@ describe("deduplicateBlockSentMedia", () => {
|
||||
expect(result).toEqual({ text: "captioned", mediaUrl: undefined, mediaUrls: [] });
|
||||
});
|
||||
|
||||
it("preserves legacy mediaUrl when some mediaUrls remain", () => {
|
||||
it("clears already-sent legacy mediaUrl when other mediaUrls remain", () => {
|
||||
const payload = {
|
||||
text: "hey",
|
||||
mediaUrl: "/tmp/a.jpg",
|
||||
@@ -63,6 +63,42 @@ describe("deduplicateBlockSentMedia", () => {
|
||||
};
|
||||
const sent = new Set(["/tmp/a.jpg"]);
|
||||
const result = deduplicateBlockSentMedia(payload, sent);
|
||||
expect(result).toEqual({ text: "hey", mediaUrl: "/tmp/a.jpg", mediaUrls: ["/tmp/b.jpg"] });
|
||||
expect(result).toEqual({ text: "hey", mediaUrl: undefined, mediaUrls: ["/tmp/b.jpg"] });
|
||||
});
|
||||
|
||||
it("preserves legacy mediaUrl when its attachment remains unsent", () => {
|
||||
const payload = {
|
||||
text: "hey",
|
||||
mediaUrl: "/tmp/b.jpg",
|
||||
mediaUrls: ["/tmp/a.jpg", "/tmp/b.jpg"],
|
||||
};
|
||||
const sent = new Set(["/tmp/a.jpg"]);
|
||||
const result = deduplicateBlockSentMedia(payload, sent);
|
||||
expect(result).toEqual({ text: "hey", mediaUrl: "/tmp/b.jpg", mediaUrls: ["/tmp/b.jpg"] });
|
||||
});
|
||||
|
||||
it.each(["/tmp/dog.jpg", " /tmp/dog.jpg "])(
|
||||
"preserves unsent legacy mediaUrl outside the remaining mediaUrls (%s)",
|
||||
(mediaUrl) => {
|
||||
const payload = {
|
||||
text: "hey",
|
||||
mediaUrl,
|
||||
mediaUrls: ["/tmp/cat.jpg", "/tmp/bird.jpg"],
|
||||
};
|
||||
const sent = new Set(["/tmp/cat.jpg"]);
|
||||
const result = deduplicateBlockSentMedia(payload, sent);
|
||||
expect(result).toEqual({ text: "hey", mediaUrl, mediaUrls: ["/tmp/bird.jpg"] });
|
||||
},
|
||||
);
|
||||
|
||||
it("clears whitespace-padded legacy mediaUrl after its normalized attachment was sent", () => {
|
||||
const payload = {
|
||||
text: "hey",
|
||||
mediaUrl: " /tmp/a.jpg ",
|
||||
mediaUrls: ["/tmp/a.jpg", "/tmp/b.jpg"],
|
||||
};
|
||||
const sent = new Set(["/tmp/a.jpg"]);
|
||||
const result = deduplicateBlockSentMedia(payload, sent);
|
||||
expect(result).toEqual({ text: "hey", mediaUrl: undefined, mediaUrls: ["/tmp/b.jpg"] });
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// Telegram plugin module implements bot message dispatch.media dedup behavior.
|
||||
// Keep sent-block media out of both delivery fields so outbound planning cannot restore it.
|
||||
export function deduplicateBlockSentMedia<
|
||||
T extends { mediaUrl?: string; mediaUrls?: string[]; text?: string },
|
||||
>(payload: T, sentBlockMediaUrls: ReadonlySet<string>): T | undefined {
|
||||
@@ -15,6 +15,6 @@ export function deduplicateBlockSentMedia<
|
||||
return {
|
||||
...payload,
|
||||
mediaUrls: remainingMedia,
|
||||
mediaUrl: remainingMedia.length === 0 ? undefined : payload.mediaUrl,
|
||||
mediaUrl: sentBlockMediaUrls.has(payload.mediaUrl?.trim() ?? "") ? undefined : payload.mediaUrl,
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user