fix: preserve discord unicode channel labels

This commit is contained in:
Peter Steinberger
2026-05-02 10:09:40 +01:00
parent d50cd0f6cf
commit d419bcf5c9
8 changed files with 62 additions and 5 deletions

View File

@@ -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.

View File

@@ -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: {

View File

@@ -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({

View File

@@ -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;

View File

@@ -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");
});
});

View File

@@ -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 },

View File

@@ -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");
});
});

View File

@@ -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) : "");