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}`;