perf(whatsapp): reuse inbound message projection (#127958)

Amp-Thread-ID: https://ampcode.com/threads/T-01a021f5-984a-7628-a30c-491c166ff247

Co-authored-by: Amp <amp@ampcode.com>
This commit is contained in:
Peter Steinberger
2026-08-22 07:51:03 -07:00
committed by GitHub
parent 71b5de4061
commit 963fafbb16
5 changed files with 85 additions and 53 deletions
+2 -2
View File
@@ -1416,8 +1416,8 @@ extensions/whatsapp/src/inbound/extract.ts 10
extensions/whatsapp/src/inbound/group-metadata-cache.ts 7
extensions/whatsapp/src/inbound/ingress-lifecycle.ts 2
extensions/whatsapp/src/inbound/media.ts 4
extensions/whatsapp/src/inbound/message-delivery.ts 2
extensions/whatsapp/src/inbound/message-enrichment.ts 4
extensions/whatsapp/src/inbound/message-delivery.ts 1
extensions/whatsapp/src/inbound/message-enrichment.ts 3
extensions/whatsapp/src/inbound/outbound-mentions.ts 1
extensions/whatsapp/src/inbound/send-api.ts 5
extensions/whatsapp/src/inbound/socket-session.ts 5
+60 -34
View File
@@ -35,7 +35,12 @@ function getFutureProofInnerMessage(message: proto.IMessage): proto.IMessage | u
return undefined;
}
function buildMessageChain(message: proto.IMessage | undefined): proto.IMessage[] {
type WhatsAppInboundMessageProjection = readonly proto.IMessage[];
type WhatsAppInboundMessageSource = proto.IMessage | WhatsAppInboundMessageProjection | undefined;
export function projectWhatsAppInboundMessage(
message: proto.IMessage | undefined,
): WhatsAppInboundMessageProjection {
const chain: proto.IMessage[] = [];
let current = normalizeMessageContent(message);
while (current && chain.length < 4) {
@@ -45,11 +50,25 @@ function buildMessageChain(message: proto.IMessage | undefined): proto.IMessage[
return chain;
}
function isWhatsAppInboundMessageProjection(
message: WhatsAppInboundMessageSource,
): message is WhatsAppInboundMessageProjection {
return Array.isArray(message);
}
function resolveWhatsAppInboundMessageProjection(
message: WhatsAppInboundMessageSource,
): WhatsAppInboundMessageProjection {
return isWhatsAppInboundMessageProjection(message)
? message
: projectWhatsAppInboundMessage(message);
}
export function findMessageSection<K extends keyof proto.IMessage>(
rawMessage: proto.IMessage | undefined,
sectionNames: readonly K[],
): { name: K; value: Record<string, unknown> } | undefined {
const chain = buildMessageChain(rawMessage);
const chain = projectWhatsAppInboundMessage(rawMessage);
for (const name of sectionNames) {
for (const message of chain) {
const value = message[name];
@@ -61,9 +80,8 @@ export function findMessageSection<K extends keyof proto.IMessage>(
return undefined;
}
function unwrapMessage(message: proto.IMessage | undefined): proto.IMessage | undefined {
const chain = buildMessageChain(message);
return chain.at(-1);
function unwrapMessage(message: WhatsAppInboundMessageSource): proto.IMessage | undefined {
return resolveWhatsAppInboundMessageProjection(message).at(-1);
}
function extractContextInfoFromMessage(message: proto.IMessage): proto.IContextInfo | undefined {
@@ -117,9 +135,9 @@ function extractContextInfoFromMessage(message: proto.IMessage): proto.IContextI
}
export function extractContextInfo(
message: proto.IMessage | undefined,
message: WhatsAppInboundMessageSource,
): proto.IContextInfo | undefined {
for (const candidate of buildMessageChain(message)) {
for (const candidate of resolveWhatsAppInboundMessageProjection(message)) {
const contextInfo = extractContextInfoFromMessage(candidate);
if (contextInfo) {
return contextInfo;
@@ -128,9 +146,9 @@ export function extractContextInfo(
return undefined;
}
export function extractMentionedJids(rawMessage: proto.IMessage | undefined): string[] | undefined {
export function extractMentionedJids(message: WhatsAppInboundMessageSource): string[] | undefined {
// Context ownership already follows Baileys envelopes without entering quoted messages.
const mentionedJids = extractContextInfo(rawMessage)?.mentionedJid?.filter(Boolean);
const mentionedJids = extractContextInfo(message)?.mentionedJid?.filter(Boolean);
if (!mentionedJids?.length) {
return undefined;
}
@@ -157,8 +175,9 @@ function extractNativeFlowResponseText(
}
}
export function extractText(rawMessage: proto.IMessage | undefined): string | undefined {
const message = unwrapMessage(rawMessage);
export function extractText(source: WhatsAppInboundMessageSource): string | undefined {
const projection = resolveWhatsAppInboundMessageProjection(source);
const message = unwrapMessage(projection);
if (!message) {
return undefined;
}
@@ -215,7 +234,7 @@ export function extractText(rawMessage: proto.IMessage | undefined): string | un
}
}
const contactPlaceholder =
extractContactPlaceholder(message) ??
extractContactPlaceholder(projection) ??
(extracted && extracted !== message
? extractContactPlaceholder(extracted as proto.IMessage | undefined)
: undefined);
@@ -225,14 +244,14 @@ export function extractText(rawMessage: proto.IMessage | undefined): string | un
return undefined;
}
export function extractExternalAdReplyContext(rawMessage: proto.IMessage | undefined):
export function extractExternalAdReplyContext(source: WhatsAppInboundMessageSource):
| {
title?: string;
sourceUrl?: string;
body?: string;
}
| undefined {
const message = unwrapMessage(rawMessage);
const message = unwrapMessage(source);
const adReply =
message?.imageMessage?.contextInfo?.externalAdReply ??
message?.videoMessage?.contextInfo?.externalAdReply;
@@ -246,9 +265,9 @@ export function extractExternalAdReplyContext(rawMessage: proto.IMessage | undef
}
export function extractMediaKind(
rawMessage: proto.IMessage | undefined,
source: WhatsAppInboundMessageSource,
): NonNullable<ChannelInboundMediaInput["kind"]> | undefined {
const message = unwrapMessage(rawMessage);
const message = unwrapMessage(source);
if (!message) {
return undefined;
}
@@ -271,8 +290,8 @@ export function extractMediaKind(
return undefined;
}
function extractContactPlaceholder(rawMessage: proto.IMessage | undefined): string | undefined {
const contactContext = extractContactContext(rawMessage);
function extractContactPlaceholder(source: WhatsAppInboundMessageSource): string | undefined {
const contactContext = extractContactContext(source);
if (!contactContext) {
return undefined;
}
@@ -284,9 +303,9 @@ function extractContactPlaceholder(rawMessage: proto.IMessage | undefined): stri
}
export function extractContactContext(
rawMessage: proto.IMessage | undefined,
source: WhatsAppInboundMessageSource,
): WhatsAppStructuredContactContext | undefined {
const message = unwrapMessage(rawMessage);
const message = unwrapMessage(source);
if (!message) {
return undefined;
}
@@ -326,9 +345,9 @@ function describeContact(input: { displayName?: string | null; vcard?: string |
}
export function extractLocationData(
rawMessage: proto.IMessage | undefined,
source: WhatsAppInboundMessageSource,
): NormalizedLocation | null {
const message = unwrapMessage(rawMessage);
const message = unwrapMessage(source);
if (!message) {
return null;
}
@@ -380,13 +399,18 @@ export function extractLocationData(
}
export function describeReplyContext(
rawMessage: proto.IMessage | undefined,
source: WhatsAppInboundMessageSource,
): WhatsAppReplyContext | null {
const message = unwrapMessage(rawMessage);
const projection = resolveWhatsAppInboundMessageProjection(source);
const message = unwrapMessage(projection);
if (!message) {
return null;
}
const contextInfo = extractContextInfo(message);
const contextProjection =
projection.length === 1 && projection[0] === message
? projection
: projectWhatsAppInboundMessage(message);
const contextInfo = extractContextInfo(contextProjection);
const quoted = normalizeMessageContent(contextInfo?.quotedMessage as proto.IMessage | undefined);
if (!quoted && !contextInfo?.stanzaId) {
return null;
@@ -404,11 +428,12 @@ export function describeReplyContext(
sender,
};
}
const location = extractLocationData(quoted);
const quotedProjection = projectWhatsAppInboundMessage(quoted);
const location = extractLocationData(quotedProjection);
const locationText = location ? formatLocationText(location) : undefined;
const text = extractText(quoted);
const text = extractText(quotedProjection);
const body = [text, locationText].filter(Boolean).join("\n").trim();
const mediaKind = extractMediaKind(quoted);
const mediaKind = extractMediaKind(quotedProjection);
const media = mediaKind
? { kind: mediaKind, contentType: resolveInboundMediaMimetype(quoted) }
: undefined;
@@ -450,22 +475,23 @@ function hasInteractiveResponseContent(message: proto.IMessage | undefined): boo
* `messages.upsert` stream as real messages but should not trigger pairing
* access-control side effects.
*/
export function hasInboundUserContent(rawMessage: proto.IMessage | undefined): boolean {
if (!rawMessage) {
export function hasInboundUserContent(source: WhatsAppInboundMessageSource): boolean {
const projection = resolveWhatsAppInboundMessageProjection(source);
if (projection.length === 0) {
return false;
}
if (extractText(rawMessage)) {
if (extractText(projection)) {
return true;
}
if (extractMediaKind(rawMessage)) {
if (extractMediaKind(projection)) {
return true;
}
if (extractLocationData(rawMessage)) {
if (extractLocationData(projection)) {
return true;
}
// Walk wrappers (ephemeral, viewOnce, etc.) — interactive responses
// can arrive nested.
for (const candidate of buildMessageChain(rawMessage)) {
for (const candidate of projection) {
if (hasInteractiveResponseContent(candidate)) {
return true;
}
+2 -1
View File
@@ -16,8 +16,9 @@ export async function downloadInboundMedia(
msg: proto.IWebMessageInfo,
sock: Awaited<ReturnType<typeof createWaSocket>>,
maxBytes = 50 * 1024 * 1024,
normalizedMessage?: proto.IMessage,
): Promise<{ saved: SavedMedia; mimetype?: string; fileName?: string } | undefined> {
const message = unwrapMessage(msg.message as proto.IMessage | undefined);
const message = normalizedMessage ?? unwrapMessage(msg.message as proto.IMessage | undefined);
if (!message) {
return undefined;
}
@@ -1,12 +1,6 @@
// Whatsapp plugin module owns inbound message admission and delivery.
import { createHash } from "node:crypto";
import type {
AnyMessageContent,
MiscMessageGenerationOptions,
proto,
WAMessage,
WASocket,
} from "baileys";
import type { AnyMessageContent, MiscMessageGenerationOptions, WAMessage, WASocket } from "baileys";
import { recordChannelActivity } from "openclaw/plugin-sdk/channel-activity-runtime";
import { getChildLogger } from "openclaw/plugin-sdk/logging-core";
import { parseStrictFiniteNumber } from "openclaw/plugin-sdk/number-runtime";
@@ -27,7 +21,6 @@ import {
type WhatsAppIngressLifecycle,
type WhatsAppReadReceiptTarget,
} from "./durable-receive.js";
import { extractMentionedJids } from "./extract.js";
import type { WhatsAppGroupMetadataCacheOwner } from "./group-metadata-cache.js";
import {
createWhatsAppInboundMessageDebouncer,
@@ -270,7 +263,7 @@ export function createWhatsAppMessageDeliveryCoordinator(options: WhatsAppMessag
return normalizeWhatsAppSendResult(result, "media");
};
const timestamp = inbound.messageTimestampMs;
const mentionedJids = extractMentionedJids(msg.message as proto.IMessage | undefined);
const mentionedJids = enriched.mentionedJids;
const senderName = msg.pushName ?? undefined;
inboundLogger.info(
@@ -14,7 +14,9 @@ import {
extractExternalAdReplyContext,
extractLocationData,
extractMediaKind,
extractMentionedJids,
extractText,
projectWhatsAppInboundMessage,
} from "./extract.js";
import { resolveInboundMediaMimetype } from "./media-mimetype.js";
import { downloadInboundMedia, downloadQuotedInboundMedia } from "./media.js";
@@ -78,6 +80,7 @@ export type WhatsAppEnrichedInboundMessage = {
mediaFileName?: string;
mediaKind?: NonNullable<ReturnType<typeof extractMediaKind>>;
nativeMedia?: MediaPlaceholderTextFact;
mentionedJids?: string[];
};
export async function enrichWhatsAppInboundMessage(params: {
@@ -87,12 +90,13 @@ export async function enrichWhatsAppInboundMessage(params: {
logVerbose: (message: string) => void;
}): Promise<WhatsAppEnrichedInboundMessage | null> {
const { msg, sock } = params;
const location = extractLocationData(msg.message ?? undefined);
const messageProjection = projectWhatsAppInboundMessage(msg.message ?? undefined);
const location = extractLocationData(messageProjection);
const locationText = location ? formatLocationText(location) : undefined;
const contactContext = extractContactContext(msg.message ?? undefined);
const externalAdReplyContext = extractExternalAdReplyContext(msg.message ?? undefined);
let mediaKind = extractMediaKind(msg.message ?? undefined);
let body = extractText(msg.message ?? undefined);
const contactContext = extractContactContext(messageProjection);
const externalAdReplyContext = extractExternalAdReplyContext(messageProjection);
let mediaKind = extractMediaKind(messageProjection);
let body = extractText(messageProjection);
if (locationText) {
body = [body, locationText].filter(Boolean).join("\n").trim();
}
@@ -101,7 +105,7 @@ export async function enrichWhatsAppInboundMessage(params: {
}
body = body ?? "";
const commandBody = body;
const replyContext = describeReplyContext(msg.message as proto.IMessage | undefined);
const replyContext = describeReplyContext(messageProjection);
let mediaPath: string | undefined;
let mediaType = mediaKind
@@ -123,8 +127,15 @@ export async function enrichWhatsAppInboundMessage(params: {
mediaFileName = inboundMedia.fileName;
};
try {
// Entry zero is exactly the Baileys normalization that downloadInboundMedia performed here
// previously; later projection entries are extraction-only future-proof payloads.
await saveInboundMedia(
await downloadInboundMedia(msg as proto.IWebMessageInfo, sock, maxBytes),
await downloadInboundMedia(
msg as proto.IWebMessageInfo,
sock,
maxBytes,
messageProjection[0],
),
);
} catch (error) {
logMediaMaterializationFailure({
@@ -175,5 +186,6 @@ export async function enrichWhatsAppInboundMessage(params: {
mediaFileName,
mediaKind,
nativeMedia,
mentionedJids: extractMentionedJids(messageProjection),
};
}