mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 13:26:04 -06:00
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:
committed by
GitHub
parent
71b5de4061
commit
963fafbb16
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user