fix(delivery): strip HTML tags for plain-text messaging surfaces

Models occasionally produce HTML tags in their output. While these render
fine on web surfaces, they appear as literal text on WhatsApp, Signal,
SMS, IRC, and Telegram.

Add sanitizeForPlainText() utility that converts common inline HTML to
lightweight-markup equivalents and strips remaining tags. Applied in the
outbound delivery pipeline for non-HTML surfaces only.

Closes #31884
See also: #18558
This commit is contained in:
AytuncYildizli
2026-03-02 19:43:12 +03:00
committed by Peter Steinberger
parent a19a7f5e6e
commit 62d0cfeee7
3 changed files with 190 additions and 7 deletions

View File

@@ -33,6 +33,7 @@ import { ackDelivery, enqueueDelivery, failDelivery } from "./delivery-queue.js"
import type { OutboundIdentity } from "./identity.js";
import type { NormalizedOutboundPayload } from "./payloads.js";
import { normalizeReplyPayloadsForDelivery } from "./payloads.js";
import { isPlainTextSurface, sanitizeForPlainText } from "./sanitize-text.js";
import type { OutboundSessionContext } from "./session-context.js";
import type { OutboundChannel } from "./targets.js";
@@ -445,13 +446,23 @@ async function deliverOutboundPayloadsCore(
text: normalizedText,
};
};
const normalizedPayloads = normalizeReplyPayloadsForDelivery(payloads).flatMap((payload) => {
if (channel !== "whatsapp") {
return [payload];
}
const normalized = normalizeWhatsAppPayload(payload);
return normalized ? [normalized] : [];
});
const normalizedPayloads = normalizeReplyPayloadsForDelivery(payloads)
.flatMap((payload) => {
if (channel !== "whatsapp") {
return [payload];
}
const normalized = normalizeWhatsAppPayload(payload);
return normalized ? [normalized] : [];
})
.map((payload) => {
// Strip HTML tags for plain-text surfaces (WhatsApp, Signal, etc.)
// Models occasionally produce <br>, <b>, etc. that render as literal text.
// See https://github.com/openclaw/openclaw/issues/31884
if (!isPlainTextSurface(channel) || !payload.text) {
return payload;
}
return { ...payload, text: sanitizeForPlainText(payload.text) };
});
const hookRunner = getGlobalHookRunner();
const sessionKeyForInternalHooks = params.mirror?.sessionKey ?? params.session?.key;
if (