diff --git a/CHANGELOG.md b/CHANGELOG.md index 71ce69e328b..f71838d0645 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- WhatsApp/outbound: fall back to the first `mediaUrls` entry when `mediaUrl` is empty so gateway media sends stop silently dropping attachments that already have a resolved media list. (#64394) Thanks @eric-fr4 and @vincentkoc. - Gateway/auth: blank the shipped example gateway credential in `.env.example` and fail startup when a copied placeholder token or password is still configured, so operators cannot accidentally launch with a publicly known secret. (#64586) Thanks @navarrotech and @vincentkoc. - Memory/active-memory+dreaming: keep active-memory recall runs on the strongest resolved channel, consume managed dreaming heartbeat events exactly once, stop dreaming from re-ingesting its own narrative transcripts, and add explicit repair/dedupe recovery flows in CLI, doctor, and the Dreams UI. - Gateway/keepalive: stop marking WebSocket tick broadcasts as droppable so slow or backpressured clients do not self-disconnect with `tick timeout` while long-running work is still alive. (#65256) Thanks @100yenadmin and @vincentkoc. diff --git a/extensions/whatsapp/src/send.test.ts b/extensions/whatsapp/src/send.test.ts index b84fc7f0957..c135543d483 100644 --- a/extensions/whatsapp/src/send.test.ts +++ b/extensions/whatsapp/src/send.test.ts @@ -221,6 +221,26 @@ describe("web outbound", () => { expect(sendMessage).toHaveBeenLastCalledWith("+1555", "pic", buf, "image/jpeg"); }); + it("falls back to the first mediaUrls entry when mediaUrl is omitted", async () => { + const buf = Buffer.from("img"); + loadWebMediaMock.mockResolvedValueOnce({ + buffer: buf, + contentType: "image/jpeg", + kind: "image", + }); + await sendMessageWhatsApp("+1555", "pic", { + verbose: false, + mediaUrls: [" ", " /tmp/pic.jpg "], + }); + expect(loadWebMediaMock).toHaveBeenCalledWith( + "/tmp/pic.jpg", + expect.objectContaining({ + hostReadCapability: false, + }), + ); + expect(sendMessage).toHaveBeenLastCalledWith("+1555", "pic", buf, "image/jpeg"); + }); + it("maps other kinds to document with filename", async () => { const buf = Buffer.from("pdf"); loadWebMediaMock.mockResolvedValueOnce({ diff --git a/extensions/whatsapp/src/send.ts b/extensions/whatsapp/src/send.ts index 5ae65fb59fb..50c71b9370b 100644 --- a/extensions/whatsapp/src/send.ts +++ b/extensions/whatsapp/src/send.ts @@ -35,6 +35,7 @@ export async function sendMessageWhatsApp( verbose: boolean; cfg?: OpenClawConfig; mediaUrl?: string; + mediaUrls?: readonly string[]; mediaAccess?: { localRoots?: readonly string[]; readFile?: (filePath: string) => Promise; @@ -47,7 +48,13 @@ export async function sendMessageWhatsApp( ): Promise<{ messageId: string; toJid: string }> { let text = body.trimStart(); const jid = toWhatsappJid(to); - if (!text && !options.mediaUrl) { + const mediaUrls = Array.isArray(options.mediaUrls) + ? options.mediaUrls + .map((entry) => (typeof entry === "string" ? entry.trim() : "")) + .filter(Boolean) + : []; + const primaryMediaUrl = options.mediaUrl?.trim() || mediaUrls[0]; + if (!text && !primaryMediaUrl) { return { messageId: "", toJid: jid }; } const correlationId = generateSecureUuid(); @@ -81,8 +88,8 @@ export async function sendMessageWhatsApp( let mediaBuffer: Buffer | undefined; let mediaType: string | undefined; let documentFileName: string | undefined; - if (options.mediaUrl) { - const media = await loadOutboundMediaFromUrl(options.mediaUrl, { + if (primaryMediaUrl) { + const media = await loadOutboundMediaFromUrl(primaryMediaUrl, { maxBytes: resolveWhatsAppMediaMaxBytes(account), mediaAccess: options.mediaAccess, mediaLocalRoots: options.mediaLocalRoots, @@ -106,8 +113,8 @@ export async function sendMessageWhatsApp( documentFileName = media.fileName; } } - outboundLog.info(`Sending message -> ${redactedJid}${options.mediaUrl ? " (media)" : ""}`); - logger.info({ jid: redactedJid, hasMedia: Boolean(options.mediaUrl) }, "sending message"); + outboundLog.info(`Sending message -> ${redactedJid}${primaryMediaUrl ? " (media)" : ""}`); + logger.info({ jid: redactedJid, hasMedia: Boolean(primaryMediaUrl) }, "sending message"); await active.sendComposingTo(to); const hasExplicitAccountId = Boolean(options.accountId?.trim()); const accountId = hasExplicitAccountId ? resolvedAccountId : undefined; @@ -125,13 +132,13 @@ export async function sendMessageWhatsApp( const messageId = (result as { messageId?: string })?.messageId ?? "unknown"; const durationMs = Date.now() - startedAt; outboundLog.info( - `Sent message ${messageId} -> ${redactedJid}${options.mediaUrl ? " (media)" : ""} (${durationMs}ms)`, + `Sent message ${messageId} -> ${redactedJid}${primaryMediaUrl ? " (media)" : ""} (${durationMs}ms)`, ); logger.info({ jid: redactedJid, messageId }, "sent message"); return { messageId, toJid: jid }; } catch (err) { logger.error( - { err: String(err), to: redactedTo, hasMedia: Boolean(options.mediaUrl) }, + { err: String(err), to: redactedTo, hasMedia: Boolean(primaryMediaUrl) }, "failed to send via web session", ); throw err;