Fix WhatsApp media sends when mediaUrl is empty but mediaUrls is populated (#64394)

* Fix WhatsApp media fallback

Accept the first mediaUrls entry when mediaUrl is empty so outbound WhatsApp sends do not silently downgrade media messages to text.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>

* chore(changelog): credit WhatsApp mediaUrls fallback

* fix(changelog): restore 2026.4.10 release block

---------

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Vincent Koc <vincentkoc@ieee.org>
This commit is contained in:
eric-fr4
2026-04-12 12:58:40 -03:00
committed by GitHub
parent 2de988ae4b
commit ad826ea450
3 changed files with 35 additions and 7 deletions

View File

@@ -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.

View File

@@ -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({

View File

@@ -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<Buffer>;
@@ -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;