diff --git a/CHANGELOG.md b/CHANGELOG.md index 14b80694c9d..7adc5521715 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ Docs: https://docs.openclaw.ai - Proxy/audio: convert standard `FormData` bodies before proxy-backed undici fetches, so audio transcription and multipart uploads no longer send `[object FormData]` when `HTTP_PROXY` or `HTTPS_PROXY` is configured. Fixes #48554. Thanks @dco5. - Discord: allow explicitly configured ack reactions in tool-only guild channels while keeping automatic lifecycle/status reactions suppressed. Fixes #74922. Thanks @samvilian and @BlueBirdBack. - Discord: preserve attachment and sticker filenames when saving inbound media, so agents can see human-readable file names instead of only UUID-based paths. Fixes #59744. Thanks @xela92 and @rockcent. +- Discord: preserve non-ASCII channel names in session display labels while keeping allowlist matching on the existing ASCII slug contract. Thanks @swjeong9. - Gateway/diagnostics: include a bounded redacted startup error message in stability bundles, so crash-loop reports identify the failing plugin or contract without exposing secrets. Refs #75797. Thanks @ymebosma. - Gateway/pricing: abort in-flight model pricing catalog fetches when Gateway shutdown stops the refresh loop, and avoid post-stop cache writes or refresh timers. Fixes #72208. Thanks @rzcq. - Codex/app-server: make startup retry cleanup ownership-aware so concurrent Codex lanes cannot close another lane's freshly restarted shared app-server client. Thanks @vincentkoc. diff --git a/extensions/discord/src/monitor/agent-components-context.ts b/extensions/discord/src/monitor/agent-components-context.ts index 21a02be223f..b12a0276036 100644 --- a/extensions/discord/src/monitor/agent-components-context.ts +++ b/extensions/discord/src/monitor/agent-components-context.ts @@ -8,7 +8,7 @@ import { type ComponentInteractionContext, type DiscordChannelContext, } from "./agent-components.types.js"; -import { normalizeDiscordSlug } from "./allow-list.js"; +import { normalizeDiscordDisplaySlug, normalizeDiscordSlug } from "./allow-list.js"; import { resolveDiscordChannelInfoSafe } from "./channel-access.js"; function formatUsername(user: { username: string; discriminator?: string | null }): string { @@ -72,6 +72,7 @@ export function resolveDiscordChannelContext( const channelInfo = resolveDiscordChannelInfoSafe(channel); const channelName = channelInfo.name; const channelSlug = channelName ? normalizeDiscordSlug(channelName) : ""; + const displayChannelSlug = channelName ? normalizeDiscordDisplaySlug(channelName) : ""; const channelType = channelInfo.type; const isThread = isThreadChannelType(channelType); @@ -86,7 +87,16 @@ export function resolveDiscordChannelContext( } } - return { channelName, channelSlug, channelType, isThread, parentId, parentName, parentSlug }; + return { + channelName, + channelSlug, + displayChannelSlug, + channelType, + isThread, + parentId, + parentName, + parentSlug, + }; } export async function resolveComponentInteractionContext(params: { diff --git a/extensions/discord/src/monitor/agent-components.dispatch.ts b/extensions/discord/src/monitor/agent-components.dispatch.ts index 7fb085dcd9a..c2fa90b690b 100644 --- a/extensions/discord/src/monitor/agent-components.dispatch.ts +++ b/extensions/discord/src/monitor/agent-components.dispatch.ts @@ -129,8 +129,8 @@ export async function dispatchDiscordComponentEvent(params: { const senderUsername = interactionCtx.user.username; const senderTag = formatDiscordUserTag(interactionCtx.user); const groupChannel = - !interactionCtx.isDirectMessage && channelCtx.channelSlug - ? `#${channelCtx.channelSlug}` + !interactionCtx.isDirectMessage && channelCtx.displayChannelSlug + ? `#${channelCtx.displayChannelSlug}` : undefined; const groupSubject = interactionCtx.isDirectMessage ? undefined : groupChannel; const channelConfig = resolveDiscordChannelConfigWithFallback({ diff --git a/extensions/discord/src/monitor/agent-components.types.ts b/extensions/discord/src/monitor/agent-components.types.ts index 14c3c17ea06..4752820fb98 100644 --- a/extensions/discord/src/monitor/agent-components.types.ts +++ b/extensions/discord/src/monitor/agent-components.types.ts @@ -26,6 +26,7 @@ export type AgentComponentInteraction = AgentComponentMessageInteraction | Modal export type DiscordChannelContext = { channelName: string | undefined; channelSlug: string; + displayChannelSlug: string; channelType: number | undefined; isThread: boolean; parentId: string | undefined; diff --git a/extensions/discord/src/monitor/allow-list.test.ts b/extensions/discord/src/monitor/allow-list.test.ts new file mode 100644 index 00000000000..eb197fb0320 --- /dev/null +++ b/extensions/discord/src/monitor/allow-list.test.ts @@ -0,0 +1,14 @@ +import { describe, expect, it } from "vitest"; +import { normalizeDiscordDisplaySlug, normalizeDiscordSlug } from "./allow-list.js"; + +describe("discord slug normalization", () => { + it("keeps config slugs ASCII-only", () => { + expect(normalizeDiscordSlug("\uC2E4\uD5D8")).toBe(""); + expect(normalizeDiscordSlug("baseline-\uAC80\uC99D")).toBe("baseline"); + }); + + it("preserves Unicode in display slugs", () => { + expect(normalizeDiscordDisplaySlug("\uC2E4\uD5D8")).toBe("\uC2E4\uD5D8"); + expect(normalizeDiscordDisplaySlug("baseline-\uAC80\uC99D")).toBe("baseline-\uAC80\uC99D"); + }); +}); diff --git a/extensions/discord/src/monitor/allow-list.ts b/extensions/discord/src/monitor/allow-list.ts index 8338ef83dd1..2541606fdcf 100644 --- a/extensions/discord/src/monitor/allow-list.ts +++ b/extensions/discord/src/monitor/allow-list.ts @@ -94,6 +94,16 @@ export function normalizeDiscordSlug(value: string) { .replace(/^-+|-+$/g, ""); } +export function normalizeDiscordDisplaySlug(value: string) { + return normalizeLowercaseStringOrEmpty(value) + .normalize("NFC") + .replace(/^#/, "") + .replace(/[\s_]+/g, "-") + .replace(/[^\p{L}\p{M}\p{N}-]+/gu, "-") + .replace(/-{2,}/g, "-") + .replace(/^-+|-+$/g, ""); +} + function resolveDiscordAllowListNameMatch( list: DiscordAllowList, candidate: { name?: string; tag?: string }, diff --git a/extensions/discord/src/monitor/message-handler.preflight-channel-context.test.ts b/extensions/discord/src/monitor/message-handler.preflight-channel-context.test.ts new file mode 100644 index 00000000000..957ecec0123 --- /dev/null +++ b/extensions/discord/src/monitor/message-handler.preflight-channel-context.test.ts @@ -0,0 +1,18 @@ +import { describe, expect, it } from "vitest"; +import { resolveDiscordPreflightChannelContext } from "./message-handler.preflight-channel-context.js"; + +describe("resolveDiscordPreflightChannelContext", () => { + it("uses Unicode channel names for display without changing config matching slugs", () => { + const context = resolveDiscordPreflightChannelContext({ + isGuildMessage: true, + messageChannelId: "channel-1", + channelName: "\uC2E4\uD5D8", + guildName: "Guild", + guildInfo: null, + threadChannel: undefined, + }); + + expect(context.configChannelSlug).toBe(""); + expect(context.displayChannelSlug).toBe("\uC2E4\uD5D8"); + }); +}); diff --git a/extensions/discord/src/monitor/message-handler.preflight-channel-context.ts b/extensions/discord/src/monitor/message-handler.preflight-channel-context.ts index 298bed0db98..aef446d8786 100644 --- a/extensions/discord/src/monitor/message-handler.preflight-channel-context.ts +++ b/extensions/discord/src/monitor/message-handler.preflight-channel-context.ts @@ -1,4 +1,5 @@ import { + normalizeDiscordDisplaySlug, normalizeDiscordSlug, resolveDiscordChannelConfigWithFallback, type DiscordGuildEntryResolved, @@ -19,7 +20,9 @@ export function resolveDiscordPreflightChannelContext(params: { const configChannelName = params.threadParentName ?? params.channelName; const configChannelSlug = configChannelName ? normalizeDiscordSlug(configChannelName) : ""; const displayChannelName = threadName ?? params.channelName; - const displayChannelSlug = displayChannelName ? normalizeDiscordSlug(displayChannelName) : ""; + const displayChannelSlug = displayChannelName + ? normalizeDiscordDisplaySlug(displayChannelName) + : ""; const guildSlug = params.guildInfo?.slug || (params.guildName ? normalizeDiscordSlug(params.guildName) : "");