From bb07b2b93a9ef2f1f1b5f3d29cb78b0579dc6f75 Mon Sep 17 00:00:00 2001 From: liuxiaopai-ai <73659136+liuxiaopai-ai@users.noreply.github.com> Date: Tue, 3 Mar 2026 16:51:48 +0800 Subject: [PATCH] Outbound: avoid empty multi-media fallback sends --- src/infra/outbound/deliver.test.ts | 50 ++++++++++++++++++++++++++++++ src/infra/outbound/deliver.ts | 26 ++++++++++++++++ 2 files changed, 76 insertions(+) diff --git a/src/infra/outbound/deliver.test.ts b/src/infra/outbound/deliver.test.ts index cbab6d00c..236d66c78 100644 --- a/src/infra/outbound/deliver.test.ts +++ b/src/infra/outbound/deliver.test.ts @@ -918,9 +918,59 @@ describe("deliverOutboundPayloads", () => { text: "caption", }), ); + expect(logMocks.warn).toHaveBeenCalledWith( + "Plugin outbound adapter does not implement sendMedia; media URLs will be dropped and text fallback will be used", + expect.objectContaining({ + channel: "matrix", + mediaCount: 1, + }), + ); expect(results).toEqual([{ channel: "matrix", messageId: "mx-1" }]); }); + it("falls back to one sendText call for multi-media payloads when sendMedia is omitted", async () => { + const sendText = vi.fn().mockResolvedValue({ channel: "matrix", messageId: "mx-2" }); + setActivePluginRegistry( + createTestRegistry([ + { + pluginId: "matrix", + source: "test", + plugin: createOutboundTestPlugin({ + id: "matrix", + outbound: { deliveryMode: "direct", sendText }, + }), + }, + ]), + ); + + const results = await deliverOutboundPayloads({ + cfg: {}, + channel: "matrix", + to: "!room:1", + payloads: [ + { + text: "caption", + mediaUrls: ["https://example.com/a.png", "https://example.com/b.png"], + }, + ], + }); + + expect(sendText).toHaveBeenCalledTimes(1); + expect(sendText).toHaveBeenCalledWith( + expect.objectContaining({ + text: "caption", + }), + ); + expect(logMocks.warn).toHaveBeenCalledWith( + "Plugin outbound adapter does not implement sendMedia; media URLs will be dropped and text fallback will be used", + expect.objectContaining({ + channel: "matrix", + mediaCount: 2, + }), + ); + expect(results).toEqual([{ channel: "matrix", messageId: "mx-2" }]); + }); + it("emits message_sent failure when delivery errors", async () => { hookMocks.runner.hasHooks.mockReturnValue(true); const sendWhatsApp = vi.fn().mockRejectedValue(new Error("downstream failed")); diff --git a/src/infra/outbound/deliver.ts b/src/infra/outbound/deliver.ts index 6dcffddc1..f110a1750 100644 --- a/src/infra/outbound/deliver.ts +++ b/src/infra/outbound/deliver.ts @@ -97,6 +97,7 @@ type ChannelHandler = { chunker: Chunker | null; chunkerMode?: "text" | "markdown"; textChunkLimit?: number; + supportsMedia: boolean; sendPayload?: ( payload: ReplyPayload, overrides?: { @@ -169,6 +170,7 @@ function createPluginHandler( chunker, chunkerMode, textChunkLimit: outbound.textChunkLimit, + supportsMedia: Boolean(sendMedia), sendPayload: outbound.sendPayload ? async (payload, overrides) => outbound.sendPayload!({ @@ -737,6 +739,30 @@ async function deliverOutboundPayloadsCore( continue; } + if (!handler.supportsMedia) { + log.warn( + "Plugin outbound adapter does not implement sendMedia; media URLs will be dropped and text fallback will be used", + { + channel, + to, + mediaCount: payloadSummary.mediaUrls.length, + }, + ); + const fallbackText = payloadSummary.text.trim(); + if (!fallbackText) { + continue; + } + const beforeCount = results.length; + await sendTextChunks(fallbackText, sendOverrides); + const messageId = results.at(-1)?.messageId; + emitMessageSent({ + success: results.length > beforeCount, + content: payloadSummary.text, + messageId, + }); + continue; + } + let first = true; let lastMessageId: string | undefined; for (const url of payloadSummary.mediaUrls) {