mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-17 19:55:44 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user