From 16db1cf811dfb946a5015829e22c70a992fd889f Mon Sep 17 00:00:00 2001 From: togotago <88701594+togotago@users.noreply.github.com> Date: Wed, 18 Feb 2026 23:00:55 +0100 Subject: [PATCH] feat: strip webchat envelope metadata from user messages (#10) When OpenClaw's webchat relays user messages, it wraps them in metadata (conversation info JSON block + UTC timestamp prefix). This strips the envelope at render time so users see only their actual text. - Add hasWebchatEnvelope() and stripWebchatEnvelope() to systemEvent.ts - Chain webchat + webhook stripping via stripAll() helper in ChatMessage - Fix 'webhook' label only appearing for actual webhook messages, not for envelope-stripped webchat messages Co-authored-by: togotago --- src/components/ChatMessage.tsx | 32 ++++++++++++++++++-------- src/lib/systemEvent.ts | 42 ++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 10 deletions(-) diff --git a/src/components/ChatMessage.tsx b/src/components/ChatMessage.tsx index 1b840f4..63041f5 100644 --- a/src/components/ChatMessage.tsx +++ b/src/components/ChatMessage.tsx @@ -14,7 +14,7 @@ import { copyToClipboard } from '../lib/clipboard'; import { Bot, User, Wrench, Copy, Check, CheckCheck, RefreshCw, Zap, Info, Webhook, Braces, Clock, AlertCircle, Bookmark, ChevronDown, Reply } from 'lucide-react'; import { t, getLocale } from '../lib/i18n'; import { useLocale } from '../hooks/useLocale'; -import { stripWebhookScaffolding, hasWebhookScaffolding } from '../lib/systemEvent'; +import { stripWebhookScaffolding, hasWebhookScaffolding, hasWebchatEnvelope, stripWebchatEnvelope } from '../lib/systemEvent'; // ChevronDown, ChevronRight, Wrench still used by InternalOnlyMessage /** Avatar image with fallback to Bot icon on load error */ @@ -454,26 +454,38 @@ export const ChatMessageComponent = memo(function ChatMessageComponent({ message const isLight = resolvedTheme === 'light'; const [showRawJson, setShowRawJson] = useState(false); - // Strip webhook/hook scaffolding from user messages before rendering + // Strip webhook/hook scaffolding and webchat envelope from user messages before rendering const message = useMemo(() => { if (rawMessage.role !== 'user') return rawMessage; const content = rawMessage.content || ''; const textBlocks = getTextBlocks(rawMessage.blocks); - const contentHasScaffolding = hasWebhookScaffolding(content); - const anyBlockHasScaffolding = textBlocks.some(b => - hasWebhookScaffolding((b as Extract).text) - ); - if (!contentHasScaffolding && !anyBlockHasScaffolding) return rawMessage; + + // Helper: apply all applicable strip functions to a string + const stripAll = (text: string): string => { + let result = text; + if (hasWebhookScaffolding(result)) result = stripWebhookScaffolding(result); + if (hasWebchatEnvelope(result)) result = stripWebchatEnvelope(result); + return result; + }; + + const contentNeedsStrip = hasWebhookScaffolding(content) || hasWebchatEnvelope(content); + const anyBlockNeedsStrip = textBlocks.some(b => { + const t = (b as Extract).text; + return hasWebhookScaffolding(t) || hasWebchatEnvelope(t); + }); + + if (!contentNeedsStrip && !anyBlockNeedsStrip) return rawMessage; + // Clean the content and blocks const cleaned: ChatMessageType = { ...rawMessage }; if (cleaned.content) { - cleaned.content = stripWebhookScaffolding(cleaned.content); + cleaned.content = stripAll(cleaned.content); } if (cleaned.blocks.length > 0) { cleaned.blocks = cleaned.blocks.map(b => { if (b.type === 'text') { const tb = b as Extract; - return { ...tb, text: stripWebhookScaffolding(tb.text) }; + return { ...tb, text: stripAll(tb.text) }; } return b; }); @@ -481,7 +493,7 @@ export const ChatMessageComponent = memo(function ChatMessageComponent({ message return cleaned; }, [rawMessage]); - const wasWebhookMessage = rawMessage !== message; + const wasWebhookMessage = rawMessage !== message && hasWebhookScaffolding(rawMessage.content || ''); const isUser = message.role === 'user'; diff --git a/src/lib/systemEvent.ts b/src/lib/systemEvent.ts index af1d3de..0d094ea 100644 --- a/src/lib/systemEvent.ts +++ b/src/lib/systemEvent.ts @@ -93,3 +93,45 @@ export function hasWebhookScaffolding(text: string): boolean { return /<<>>/.test(text) || /---\s*SECURITY NOTICE\s*---/i.test(text); } + +/** + * Detect OpenClaw webchat envelope metadata in a user message. + * + * Pattern: + * Conversation info (untrusted metadata): + * ```json + * { ... } + * ``` + * + * [Wed 2026-02-18 14:06 UTC] actual message + */ +export function hasWebchatEnvelope(text: string): boolean { + return /Conversation info \(untrusted metadata\):/.test(text) || + /^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) \d{4}-\d{2}-\d{2} \d{2}:\d{2} UTC\] /.test(text.trim()); +} + +/** + * Strip webchat envelope metadata from a user message, returning only the + * actual user text. + * + * Removes: + * - "Conversation info (untrusted metadata):" header + trailing JSON code fence + * - Timestamp prefix "[Wed 2026-02-18 14:06 UTC] " + */ +export function stripWebchatEnvelope(text: string): string { + let cleaned = text; + + // Remove the "Conversation info (untrusted metadata):" block + JSON code fence + cleaned = cleaned.replace( + /Conversation info \(untrusted metadata\):\s*```json\s*[\s\S]*?```\s*/, + '' + ); + + // Strip timestamp prefix: [Wed 2026-02-18 14:06 UTC] + cleaned = cleaned.replace( + /^\[(?:Mon|Tue|Wed|Thu|Fri|Sat|Sun) \d{4}-\d{2}-\d{2} \d{2}:\d{2} UTC\] /, + '' + ); + + return cleaned.trim() || text.trim(); +}