mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 05:40:44 +00:00
fix(whatsapp): remove exposeErrorText config (#74642)
* fix(whatsapp): remove exposeErrorText config * fix(whatsapp): mark internal system events trusted
This commit is contained in:
@@ -230,7 +230,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Channels/WhatsApp: detect explicit group `@mentions` again when the bot's own E.164 is in `allowFrom`, so shared-number setups no longer skip group pings that directly mention the bot. Fixes #49317. (#73453) Thanks @juan-flores077.
|
||||
- WhatsApp/reliability: publish real transport-liveness into WhatsApp channel status and force earlier reconnects on silent transport stalls, so quiet healthy sessions stay connected while wedged sockets recover before the later remote 408 path. (#72656) Thanks @Sathvik-1007.
|
||||
- Core/channels: tighten selected runtime, media, and plugin edge-case handling while preserving existing behavior. Thanks @jesse-merhi.
|
||||
- Channels/WhatsApp: strip leaked plural tool-call XML wrappers on every WhatsApp-visible outbound path and allow `channels.whatsapp.exposeErrorText` to suppress visible error text per channel or account. (#71830) Thanks @rubencu.
|
||||
- Channels/WhatsApp: strip leaked plural tool-call XML wrappers on every WhatsApp-visible outbound path and keep channel error payloads out of WhatsApp chats. (#71830) Thanks @rubencu.
|
||||
- Agents/embedded-runner: inject the resolved OAuth bearer (and forward the run abort signal) on the boundary-aware embedded stream fallback so models that route through `openai-codex-responses` and other boundary-aware transports stop failing with `401 Unauthorized: Missing bearer or basic authentication in header`. Fixes #73559. (#73588) Thanks @openperf.
|
||||
- Telegram/gateway: bound outbound Bot API calls and cache bundled plugin alias lookup so slow Telegram sends or WSL2 filesystem scans no longer wedge gateway replies. (#74210) Thanks @obviyus.
|
||||
- Configure/GitHub Copilot: reuse existing Copilot auth during configure and show the provider's manifest model catalog in the model picker. (#74276) Thanks @obviyus.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
bfd8b3ddcac047e486e9c43fdedc002cb9bf87b659f6563f9f11c850c5b2aaef config-baseline.json
|
||||
2af6bef21f530dc64e0379f7631bed410aee1d5c86604ef9fb149f546cfcb0e8 config-baseline.json
|
||||
8d75df355b7f6e44b9c2f195d9df86130beb697e26061469df7d60b7e8a2f204 config-baseline.core.json
|
||||
9f5fad66a49fa618d64a963470aa69fed9fe4b4639cc4321f9ec04bfb2f8aa50 config-baseline.channel.json
|
||||
fab66aa304db5697e87259165ad261006719eb6e6cdbd25f957fcba2b7b324e9 config-baseline.channel.json
|
||||
c4231c2194206547af8ad94342dc00aadb734f43cb49cc79d4c46bdbb80c3f95 config-baseline.plugin.json
|
||||
|
||||
@@ -398,22 +398,6 @@ When the linked self number is also present in `allowFrom`, WhatsApp self-chat s
|
||||
</Accordion>
|
||||
</AccordionGroup>
|
||||
|
||||
## Error visibility
|
||||
|
||||
`channels.whatsapp.exposeErrorText` controls whether agent/provider error text is delivered back into WhatsApp. The default is `true`. Set it to `false` to keep failures quiet on WhatsApp while preserving other channel behavior.
|
||||
|
||||
```json5
|
||||
{
|
||||
channels: {
|
||||
whatsapp: {
|
||||
exposeErrorText: false,
|
||||
},
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
Per-account overrides use `channels.whatsapp.accounts.<id>.exposeErrorText`.
|
||||
|
||||
## Reply quoting
|
||||
|
||||
WhatsApp supports native reply quoting, where outbound replies visibly quote the inbound message. Control it with `channels.whatsapp.replyToMode`.
|
||||
@@ -681,7 +665,7 @@ Primary reference:
|
||||
High-signal WhatsApp fields:
|
||||
|
||||
- access: `dmPolicy`, `allowFrom`, `groupPolicy`, `groupAllowFrom`, `groups`
|
||||
- delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `sendReadReceipts`, `ackReaction`, `reactionLevel`, `exposeErrorText`
|
||||
- delivery: `textChunkLimit`, `chunkMode`, `mediaMaxMb`, `sendReadReceipts`, `ackReaction`, `reactionLevel`
|
||||
- multi-account: `accounts.<id>.enabled`, `accounts.<id>.authDir`, account-level overrides
|
||||
- operations: `configWrites`, `debounceMs`, `web.enabled`, `web.heartbeatSeconds`, `web.reconnect.*`, `web.whatsapp.*`
|
||||
- session behavior: `session.dmScope`, `historyLimit`, `dmHistoryLimit`, `dms.<id>.historyLimit`
|
||||
|
||||
@@ -45,7 +45,6 @@ export type ResolvedWhatsAppAccount = {
|
||||
direct?: WhatsAppAccountConfig["direct"];
|
||||
debounceMs?: number;
|
||||
replyToMode?: ReplyToMode;
|
||||
exposeErrorText?: boolean;
|
||||
};
|
||||
|
||||
export const DEFAULT_WHATSAPP_MEDIA_MAX_MB = 50;
|
||||
@@ -157,7 +156,6 @@ export function resolveWhatsAppAccount(params: {
|
||||
direct: merged.direct,
|
||||
debounceMs: merged.debounceMs,
|
||||
replyToMode: merged.replyToMode,
|
||||
exposeErrorText: merged.exposeErrorText,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -176,7 +176,6 @@ export async function monitorWebChannel(
|
||||
mediaMaxMb: account.mediaMaxMb,
|
||||
blockStreaming: account.blockStreaming,
|
||||
groups: account.groups,
|
||||
exposeErrorText: account.exposeErrorText,
|
||||
},
|
||||
},
|
||||
} satisfies ReturnType<typeof getRuntimeConfig>;
|
||||
@@ -444,6 +443,7 @@ export async function monitorWebChannel(
|
||||
});
|
||||
enqueueSystemEvent(`WhatsApp gateway connected${selfE164 ? ` as ${selfE164}` : ""}.`, {
|
||||
sessionKey: connectRoute.sessionKey,
|
||||
trusted: true,
|
||||
});
|
||||
|
||||
const normalizedAccountId = normalizeReconnectAccountId(account.accountId);
|
||||
@@ -503,6 +503,7 @@ export async function monitorWebChannel(
|
||||
`WhatsApp gateway disconnected (status ${decision.normalized.statusLabel})`,
|
||||
{
|
||||
sessionKey: connectRoute.sessionKey,
|
||||
trusted: true,
|
||||
},
|
||||
);
|
||||
|
||||
|
||||
@@ -522,43 +522,11 @@ describe("whatsapp inbound dispatch", () => {
|
||||
expect(rememberSentText).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("suppresses error payload text when exposeErrorText is false", async () => {
|
||||
it("suppresses error payload text", async () => {
|
||||
const deliverReply = vi.fn(async () => undefined);
|
||||
const rememberSentText = vi.fn();
|
||||
|
||||
await dispatchBufferedReply({
|
||||
cfg: { channels: { whatsapp: { exposeErrorText: false } } } as never,
|
||||
deliverReply,
|
||||
rememberSentText,
|
||||
});
|
||||
|
||||
const deliver = getCapturedDeliver();
|
||||
expect(deliver).toBeTypeOf("function");
|
||||
|
||||
await deliver?.({ text: "provider exploded", isError: true }, { kind: "final" });
|
||||
|
||||
expect(deliverReply).not.toHaveBeenCalled();
|
||||
expect(rememberSentText).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("honors account-level exposeErrorText overrides for error payloads", async () => {
|
||||
const deliverReply = vi.fn(async () => undefined);
|
||||
const rememberSentText = vi.fn();
|
||||
|
||||
await dispatchBufferedReply({
|
||||
cfg: {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
accounts: {
|
||||
work: { exposeErrorText: false },
|
||||
},
|
||||
},
|
||||
},
|
||||
} as never,
|
||||
deliverReply,
|
||||
rememberSentText,
|
||||
route: makeRoute({ accountId: "work" }),
|
||||
});
|
||||
await dispatchBufferedReply({ deliverReply, rememberSentText });
|
||||
|
||||
const deliver = getCapturedDeliver();
|
||||
expect(deliver).toBeTypeOf("function");
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { resolveMergedWhatsAppAccountConfig } from "../../account-config.js";
|
||||
import {
|
||||
type DeliverableWhatsAppOutboundPayload,
|
||||
normalizeWhatsAppOutboundPayload,
|
||||
@@ -89,12 +88,11 @@ function resolveWhatsAppDisableBlockStreaming(cfg: ReturnType<LoadConfigFn>): bo
|
||||
function resolveWhatsAppDeliverablePayload(
|
||||
payload: ReplyPayload,
|
||||
info: { kind: ReplyLifecycleKind },
|
||||
options?: { exposeErrorText?: boolean },
|
||||
): ReplyPayload | null {
|
||||
if (payload.isReasoning === true || payload.isCompactionNotice === true) {
|
||||
return null;
|
||||
}
|
||||
if (payload.isError === true && options?.exposeErrorText === false) {
|
||||
if (payload.isError === true) {
|
||||
return null;
|
||||
}
|
||||
if (info.kind === "tool") {
|
||||
@@ -314,9 +312,6 @@ export async function dispatchWhatsAppBufferedReply(params: {
|
||||
});
|
||||
const mediaLocalRoots = getAgentScopedMediaLocalRoots(params.cfg, params.route.agentId);
|
||||
const disableBlockStreaming = resolveWhatsAppDisableBlockStreaming(params.cfg);
|
||||
const exposeErrorText =
|
||||
resolveMergedWhatsAppAccountConfig({ cfg: params.cfg, accountId: params.route.accountId })
|
||||
.exposeErrorText !== false;
|
||||
let didSendReply = false;
|
||||
let didLogHeartbeatStrip = false;
|
||||
|
||||
@@ -333,9 +328,7 @@ export async function dispatchWhatsAppBufferedReply(params: {
|
||||
}
|
||||
},
|
||||
deliver: async (payload: ReplyPayload, info: { kind: ReplyLifecycleKind }) => {
|
||||
const deliveryPayload = resolveWhatsAppDeliverablePayload(payload, info, {
|
||||
exposeErrorText,
|
||||
});
|
||||
const deliveryPayload = resolveWhatsAppDeliverablePayload(payload, info);
|
||||
if (!deliveryPayload) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -63,20 +63,6 @@ describe("whatsapp config schema", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts exposeErrorText at channel and account scope", () => {
|
||||
const res = expectWhatsAppConfigValid({
|
||||
exposeErrorText: false,
|
||||
accounts: {
|
||||
work: { exposeErrorText: true },
|
||||
},
|
||||
});
|
||||
|
||||
if (res.success) {
|
||||
expect(res.data.exposeErrorText).toBe(false);
|
||||
expect(res.data.accounts?.work?.exposeErrorText).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("accepts enabled", () => {
|
||||
expectWhatsAppConfigValid({
|
||||
enabled: true,
|
||||
|
||||
@@ -21,8 +21,4 @@ export const whatsAppChannelConfigUiHints = {
|
||||
label: "WhatsApp Config Writes",
|
||||
help: "Allow WhatsApp to write config in response to channel events/commands (default: true).",
|
||||
},
|
||||
exposeErrorText: {
|
||||
label: "WhatsApp Error Text",
|
||||
help: "Deliver user-visible agent/provider error text into WhatsApp (default: true). Disable to keep failures quiet on WhatsApp.",
|
||||
},
|
||||
} satisfies Record<string, ChannelConfigUiHint>;
|
||||
|
||||
@@ -167,11 +167,11 @@ describe("whatsappOutbound sendPayload", () => {
|
||||
expect(sendWhatsApp).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("suppresses routed error payloads when error text is hidden", async () => {
|
||||
it("suppresses routed error payloads", async () => {
|
||||
const sendWhatsApp = vi.fn();
|
||||
|
||||
const result = await whatsappOutbound.sendPayload!({
|
||||
cfg: { channels: { whatsapp: { exposeErrorText: false } } },
|
||||
cfg: {},
|
||||
to: "5511999999999@c.us",
|
||||
text: "",
|
||||
payload: { text: "provider exploded", isError: true },
|
||||
@@ -182,35 +182,6 @@ describe("whatsappOutbound sendPayload", () => {
|
||||
expect(sendWhatsApp).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("uses account-level error text visibility for routed payloads", async () => {
|
||||
const sendWhatsApp = vi.fn(async () => ({ messageId: "wa-1", toJid: "jid" }));
|
||||
|
||||
await whatsappOutbound.sendPayload!({
|
||||
cfg: {
|
||||
channels: {
|
||||
whatsapp: {
|
||||
exposeErrorText: false,
|
||||
accounts: {
|
||||
work: { exposeErrorText: true },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
accountId: "work",
|
||||
to: "5511999999999@c.us",
|
||||
text: "",
|
||||
payload: { text: "provider exploded", isError: true },
|
||||
deps: { sendWhatsApp },
|
||||
});
|
||||
|
||||
expect(sendWhatsApp).toHaveBeenCalledTimes(1);
|
||||
expect(sendWhatsApp).toHaveBeenCalledWith(
|
||||
"5511999999999@c.us",
|
||||
"provider exploded",
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
|
||||
it("sanitizes HTML-only text to whitespace-only payload", () => {
|
||||
expect(
|
||||
whatsappOutbound
|
||||
|
||||
@@ -11,7 +11,6 @@ import {
|
||||
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-types";
|
||||
import { resolveOutboundSendDep } from "openclaw/plugin-sdk/outbound-send-deps";
|
||||
import { sendTextMediaPayload } from "openclaw/plugin-sdk/reply-payload";
|
||||
import { resolveMergedWhatsAppAccountConfig } from "./account-config.js";
|
||||
import {
|
||||
normalizeWhatsAppOutboundPayload,
|
||||
normalizeWhatsAppPayloadText,
|
||||
@@ -220,11 +219,7 @@ export function createWhatsAppOutboundBase({
|
||||
return {
|
||||
...outbound,
|
||||
sendPayload: async (ctx) => {
|
||||
if (
|
||||
ctx.payload.isError === true &&
|
||||
resolveMergedWhatsAppAccountConfig({ cfg: ctx.cfg, accountId: ctx.accountId })
|
||||
.exposeErrorText === false
|
||||
) {
|
||||
if (ctx.payload.isError === true) {
|
||||
return { channel: "whatsapp", messageId: "" };
|
||||
}
|
||||
const payload = normalizeWhatsAppOutboundPayload(ctx.payload, { normalizeText });
|
||||
|
||||
@@ -15962,9 +15962,6 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
],
|
||||
},
|
||||
exposeErrorText: {
|
||||
type: "boolean",
|
||||
},
|
||||
heartbeat: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -16253,9 +16250,6 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
},
|
||||
],
|
||||
},
|
||||
exposeErrorText: {
|
||||
type: "boolean",
|
||||
},
|
||||
heartbeat: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -16344,10 +16338,6 @@ export const GENERATED_BUNDLED_CHANNEL_CONFIG_METADATA = [
|
||||
label: "WhatsApp Config Writes",
|
||||
help: "Allow WhatsApp to write config in response to channel events/commands (default: true).",
|
||||
},
|
||||
exposeErrorText: {
|
||||
label: "WhatsApp Error Text",
|
||||
help: "Deliver user-visible agent/provider error text into WhatsApp (default: true). Disable to keep failures quiet on WhatsApp.",
|
||||
},
|
||||
},
|
||||
unsupportedSecretRefSurfacePatterns: [
|
||||
"channels.whatsapp.accounts.*.creds.json",
|
||||
|
||||
@@ -105,8 +105,6 @@ type WhatsAppSharedConfig = {
|
||||
debounceMs?: number;
|
||||
/** Reply threading mode for auto-replies (off|first|all|batched). */
|
||||
replyToMode?: ReplyToMode;
|
||||
/** Whether WhatsApp should deliver user-visible error text. Default: true. */
|
||||
exposeErrorText?: boolean;
|
||||
/** Heartbeat visibility settings. */
|
||||
heartbeat?: ChannelHeartbeatVisibilityConfig;
|
||||
/** Channel health monitor overrides for this channel/account. */
|
||||
|
||||
@@ -72,21 +72,6 @@ describe("WhatsApp prompt config Zod validation", () => {
|
||||
}
|
||||
});
|
||||
|
||||
it("validates exposeErrorText at root and account scope", () => {
|
||||
const result = WhatsAppConfigSchema.safeParse({
|
||||
exposeErrorText: false,
|
||||
accounts: {
|
||||
work: { exposeErrorText: true },
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
if (result.success) {
|
||||
expect(result.data.exposeErrorText).toBe(false);
|
||||
expect(result.data.accounts?.work?.exposeErrorText).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it("validates WhatsAppAccountSchema directly", () => {
|
||||
const accountConfig = {
|
||||
name: "Personal Account",
|
||||
|
||||
@@ -83,7 +83,6 @@ function buildWhatsAppCommonShape(params: { useDefaults: boolean }) {
|
||||
? z.number().int().nonnegative().optional().default(0)
|
||||
: z.number().int().nonnegative().optional(),
|
||||
replyToMode: ReplyToModeSchema.optional(),
|
||||
exposeErrorText: z.boolean().optional(),
|
||||
heartbeat: ChannelHeartbeatVisibilitySchema,
|
||||
healthMonitor: ChannelHealthMonitorSchema,
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user