Files
openclaw/extensions/whatsapp/src/channel-outbound.ts
Peter Steinberger d7de67ae02 feat: ask_user follow-ups — harness convergence, channel finalization + reactions, native cards, docked web panel (#110372)
* feat(ask-user): follow-up harness slice

* feat(ask-user): follow-up channels slice

* feat(ask-user): follow-up native slice

* feat(ui): dock question panel above composer with stepper and compact stream summaries

* docs: refresh follow-up integration maps

* test(ui): align terminal summary proof

* fix(infra): echo declared option answers in terminal status when free-text is allowed

* fix(infra): keep reaction answering when display labels are formatter-adjusted

* fix(agents): settle plain-text claims only after question registration commits

* fix(agents,apps): commit-ordered claim persistence, claim-aware prompt delivery, non-blocking question refresh

* fix(harness,infra): reaction-appropriate question copy, caller presentations honored

* fix(native,infra,agents): local-expiry eviction, value-addressed reactions, reserve-before-request

* fix(infra,android): dual-mode question resolver for compact callbacks; reset terminal retention on replayed pending

* fix(harness,discord): claim-aware prompt delivery in run helper; escape finalization labels

* fix(macos): merge transient-content visibility with question cards after main sync

* fix: repair ask user follow-up CI

* test: update limited bootstrap scope expectation

* fix: retain shared question card API

* chore: refresh native i18n inventory
2026-07-18 03:32:39 -04:00

132 lines
4.5 KiB
TypeScript

// Whatsapp plugin module implements channel outbound behavior.
import {
createMessageReceiptFromOutboundResults,
defineChannelMessageAdapter,
type ChannelMessageSendResult,
} from "openclaw/plugin-sdk/channel-outbound";
import type { ChannelOutboundAdapter } from "openclaw/plugin-sdk/channel-send-result";
import { createLazyRuntimeModule } from "openclaw/plugin-sdk/lazy-runtime";
import { questionGatewayRuntime } from "openclaw/plugin-sdk/question-gateway-runtime";
import { chunkText } from "openclaw/plugin-sdk/reply-chunking";
import { createWhatsAppOutboundBase } from "./outbound-base.js";
import { normalizeWhatsAppPayloadTextPreservingIndentation } from "./outbound-media-contract.js";
import { resolveWhatsAppOutboundTarget } from "./resolve-outbound-target.js";
import { getWhatsAppRuntime } from "./runtime.js";
import { sendMessageWhatsApp, sendPollWhatsApp } from "./send.js";
const loadWhatsAppApprovalReactionsModule = createLazyRuntimeModule(
() => import("./approval-reactions.js"),
);
const loadWhatsAppQuestionReactionsModule = createLazyRuntimeModule(
() => import("./question-reactions.js"),
);
function normalizeWhatsAppChannelPayloadText(text: string | undefined): string {
return normalizeWhatsAppPayloadTextPreservingIndentation(text);
}
function normalizeWhatsAppChannelSendText(text: string | undefined): string {
const normalized = normalizeWhatsAppChannelPayloadText(text);
return normalized.trim() ? normalized : "";
}
async function prepareWhatsAppApprovalPayloadForDelivery(
params: Parameters<NonNullable<ChannelOutboundAdapter["renderPresentation"]>>[0],
) {
const questionPayload = questionGatewayRuntime.prepareReactionPayloadForDelivery({
payload: params.payload,
presentation: params.presentation,
});
if (questionPayload) {
return questionPayload;
}
return (await loadWhatsAppApprovalReactionsModule()).prepareWhatsAppApprovalPayloadForDelivery({
payload: params.payload,
presentation: params.presentation,
});
}
async function registerDeliveredWhatsAppApprovalPayload(
params: Parameters<NonNullable<ChannelOutboundAdapter["afterDeliverPayload"]>>[0],
): Promise<void> {
(
await loadWhatsAppQuestionReactionsModule()
).registerWhatsAppQuestionReactionTargetForDeliveredPayload(params);
(
await loadWhatsAppApprovalReactionsModule()
).registerWhatsAppApprovalReactionTargetForDeliveredPayload(params);
}
export const whatsappChannelOutbound = {
...createWhatsAppOutboundBase({
chunker: chunkText,
sendMessageWhatsApp: async (to, text, options) =>
await sendMessageWhatsApp(to, text, {
...options,
preserveLeadingWhitespace: true,
}),
sendPollWhatsApp,
shouldLogVerbose: () => getWhatsAppRuntime().logging.shouldLogVerbose(),
resolveTarget: ({ to, allowFrom, mode }) =>
resolveWhatsAppOutboundTarget({ to, allowFrom, mode }),
normalizeText: normalizeWhatsAppChannelSendText,
}),
sendTextOnlyErrorPayloads: true,
renderPresentation: prepareWhatsAppApprovalPayloadForDelivery,
afterDeliverPayload: registerDeliveredWhatsAppApprovalPayload,
normalizePayload: ({ payload }: { payload: { text?: string } }) => ({
...payload,
text: normalizeWhatsAppChannelPayloadText(payload.text),
}),
};
function toWhatsAppMessageSendResult(
result: Awaited<ReturnType<NonNullable<typeof whatsappChannelOutbound.sendText>>>,
replyToId?: string | null,
): ChannelMessageSendResult {
const source = result as typeof result & { toJid?: string };
const receipt =
result.receipt ??
createMessageReceiptFromOutboundResults({
results: result.messageId
? [
{
channel: "whatsapp",
messageId: result.messageId,
toJid: source.toJid,
},
]
: [],
kind: "text",
...(replyToId ? { replyToId } : {}),
});
return {
messageId: result.messageId || receipt.primaryPlatformMessageId,
receipt,
};
}
export const whatsappMessageAdapter = defineChannelMessageAdapter({
id: "whatsapp",
durableFinal: {
capabilities: {
text: true,
replyTo: true,
messageSendingHooks: true,
},
},
send: {
text: async ({ onDeliveryResult, ...ctx }) => {
const result = await whatsappChannelOutbound.sendText!({
...ctx,
onDeliveryResult: onDeliveryResult
? async (progress) => {
await onDeliveryResult(toWhatsAppMessageSendResult(progress, ctx.replyToId));
}
: undefined,
});
return toWhatsAppMessageSendResult(result, ctx.replyToId);
},
},
});