From 0aae1ea81663d755207dbb44fe554a3a735ff2a5 Mon Sep 17 00:00:00 2001 From: lzyyzznl Date: Thu, 9 Jul 2026 19:37:49 +0800 Subject: [PATCH] fix(slack): use truncateUtf16Safe for message body preview truncation (#102612) * fix(slack): use truncateUtf16Safe for message body preview truncation One .slice(0, 160) truncation site in the Slack message handler may cut UTF-16 surrogate pairs in half when the message body preview contains multi-byte characters such as emoji. Replace it with truncateUtf16Safe to keep the truncated output valid Unicode. * test(slack): cover UTF-16 preview boundary --------- Co-authored-by: Peter Steinberger --- .../slack/src/monitor/message-handler/prepare.test.ts | 8 ++++++++ extensions/slack/src/monitor/message-handler/prepare.ts | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/extensions/slack/src/monitor/message-handler/prepare.test.ts b/extensions/slack/src/monitor/message-handler/prepare.test.ts index 0ea12b699ba..48c7296a089 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.test.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.test.ts @@ -188,6 +188,14 @@ describe("slack prepareSlackMessage inbound contract", () => { expect(prepared.ctxPayload.BodyForAgent).toContain(body); }); + it("keeps a whole code point when the inbound preview boundary crosses an emoji", async () => { + const prefix = "a".repeat(159); + const prepared = await prepareWithDefaultCtx(createSlackMessage({ text: `${prefix}😀tail` })); + + assertPrepared(prepared); + expect(prepared.preview).toBe(prefix); + }); + it("logs inbound metadata without logging message content", async () => { const body = "confidential acquisition target: northstar; do not include this text in logs"; shouldLogVerboseMock.mockReturnValue(true); diff --git a/extensions/slack/src/monitor/message-handler/prepare.ts b/extensions/slack/src/monitor/message-handler/prepare.ts index 08cf005b39f..b3c17c6bf5c 100644 --- a/extensions/slack/src/monitor/message-handler/prepare.ts +++ b/extensions/slack/src/monitor/message-handler/prepare.ts @@ -35,6 +35,7 @@ import { normalizeOptionalString, } from "openclaw/plugin-sdk/string-coerce-runtime"; import { enqueueSystemEvent } from "openclaw/plugin-sdk/system-event-runtime"; +import { truncateUtf16Safe } from "openclaw/plugin-sdk/text-utility-runtime"; import { resolveSlackReplyToMode } from "../../account-reply-mode.js"; import type { ResolvedSlackAccount } from "../../accounts.js"; import { reactSlackMessage } from "../../actions.js"; @@ -1210,7 +1211,7 @@ export async function prepareSlackMessage(params: { const roomLabel = channelName ? `#${channelName}` : `#${message.channel}`; const senderName = await resolveSenderName(); - const preview = rawBody.replace(/\s+/g, " ").slice(0, 160); + const preview = truncateUtf16Safe(rawBody.replace(/\s+/g, " "), 160); const inboundLabel = isDirectMessage ? `Slack DM from ${senderName}` : `Slack message in ${roomLabel} from ${senderName}`;