From 6210d2e238e16629c79840192da1a592ac44fa15 Mon Sep 17 00:00:00 2001 From: SidQin-cyber Date: Sat, 28 Feb 2026 13:03:37 +0800 Subject: [PATCH] fix(discord): prevent wildcard component registration collisions Assign distinct sentinel registration ids to Discord wildcard handlers while preserving wildcard parser keys, so select/menu/modal handlers no longer get dropped on runtimes that dedupe by raw customId. --- src/discord/components.ts | 8 ++- src/discord/monitor/agent-components.ts | 14 ++--- .../monitor/agent-components.wildcard.test.ts | 58 +++++++++++++++++++ 3 files changed, 71 insertions(+), 9 deletions(-) create mode 100644 src/discord/monitor/agent-components.wildcard.test.ts diff --git a/src/discord/components.ts b/src/discord/components.ts index acab337149c..fae3cba2bed 100644 --- a/src/discord/components.ts +++ b/src/discord/components.ts @@ -646,8 +646,12 @@ export function parseDiscordModalCustomId(id: string): string | null { return modalId; } +function isDiscordComponentWildcardRegistrationId(id: string): boolean { + return /^__openclaw_discord_component_[a-z_]+_wildcard__$/.test(id); +} + export function parseDiscordComponentCustomIdForCarbon(id: string): ComponentParserResult { - if (id === "*") { + if (id === "*" || isDiscordComponentWildcardRegistrationId(id)) { return { key: "*", data: {} }; } const parsed = parseCustomId(id); @@ -658,7 +662,7 @@ export function parseDiscordComponentCustomIdForCarbon(id: string): ComponentPar } export function parseDiscordModalCustomIdForCarbon(id: string): ComponentParserResult { - if (id === "*") { + if (id === "*" || id === "__openclaw_discord_component_modal_wildcard__") { return { key: "*", data: {} }; } const parsed = parseCustomId(id); diff --git a/src/discord/monitor/agent-components.ts b/src/discord/monitor/agent-components.ts index d33603697e9..38edd43deb3 100644 --- a/src/discord/monitor/agent-components.ts +++ b/src/discord/monitor/agent-components.ts @@ -1456,7 +1456,7 @@ export class AgentSelectMenu extends StringSelectMenu { class DiscordComponentButton extends Button { label = "component"; - customId = "*"; + customId = "__openclaw_discord_component_button_wildcard__"; style = ButtonStyle.Primary; customIdParser = parseDiscordComponentCustomIdForCarbon; private ctx: AgentComponentContext; @@ -1488,7 +1488,7 @@ class DiscordComponentButton extends Button { } class DiscordComponentStringSelect extends StringSelectMenu { - customId = "*"; + customId = "__openclaw_discord_component_string_select_wildcard__"; options: APIStringSelectComponent["options"] = []; customIdParser = parseDiscordComponentCustomIdForCarbon; private ctx: AgentComponentContext; @@ -1511,7 +1511,7 @@ class DiscordComponentStringSelect extends StringSelectMenu { } class DiscordComponentUserSelect extends UserSelectMenu { - customId = "*"; + customId = "__openclaw_discord_component_user_select_wildcard__"; customIdParser = parseDiscordComponentCustomIdForCarbon; private ctx: AgentComponentContext; @@ -1533,7 +1533,7 @@ class DiscordComponentUserSelect extends UserSelectMenu { } class DiscordComponentRoleSelect extends RoleSelectMenu { - customId = "*"; + customId = "__openclaw_discord_component_role_select_wildcard__"; customIdParser = parseDiscordComponentCustomIdForCarbon; private ctx: AgentComponentContext; @@ -1555,7 +1555,7 @@ class DiscordComponentRoleSelect extends RoleSelectMenu { } class DiscordComponentMentionableSelect extends MentionableSelectMenu { - customId = "*"; + customId = "__openclaw_discord_component_mentionable_select_wildcard__"; customIdParser = parseDiscordComponentCustomIdForCarbon; private ctx: AgentComponentContext; @@ -1577,7 +1577,7 @@ class DiscordComponentMentionableSelect extends MentionableSelectMenu { } class DiscordComponentChannelSelect extends ChannelSelectMenu { - customId = "*"; + customId = "__openclaw_discord_component_channel_select_wildcard__"; customIdParser = parseDiscordComponentCustomIdForCarbon; private ctx: AgentComponentContext; @@ -1600,7 +1600,7 @@ class DiscordComponentChannelSelect extends ChannelSelectMenu { class DiscordComponentModal extends Modal { title = "OpenClaw form"; - customId = "*"; + customId = "__openclaw_discord_component_modal_wildcard__"; components = []; customIdParser = parseDiscordModalCustomIdForCarbon; private ctx: AgentComponentContext; diff --git a/src/discord/monitor/agent-components.wildcard.test.ts b/src/discord/monitor/agent-components.wildcard.test.ts new file mode 100644 index 00000000000..232e3c365cb --- /dev/null +++ b/src/discord/monitor/agent-components.wildcard.test.ts @@ -0,0 +1,58 @@ +import { describe, expect, it } from "vitest"; +import { buildDiscordComponentCustomId, buildDiscordModalCustomId } from "../components.js"; +import { + createDiscordComponentButton, + createDiscordComponentChannelSelect, + createDiscordComponentMentionableSelect, + createDiscordComponentModal, + createDiscordComponentRoleSelect, + createDiscordComponentStringSelect, + createDiscordComponentUserSelect, +} from "./agent-components.js"; + +type WildcardComponent = { + customId: string; + customIdParser: (id: string) => { key: string; data: unknown }; +}; + +function asWildcardComponent(value: unknown): WildcardComponent { + return value as WildcardComponent; +} + +function createWildcardComponents() { + const context = {} as Parameters[0]; + return [ + asWildcardComponent(createDiscordComponentButton(context)), + asWildcardComponent(createDiscordComponentStringSelect(context)), + asWildcardComponent(createDiscordComponentUserSelect(context)), + asWildcardComponent(createDiscordComponentRoleSelect(context)), + asWildcardComponent(createDiscordComponentMentionableSelect(context)), + asWildcardComponent(createDiscordComponentChannelSelect(context)), + asWildcardComponent(createDiscordComponentModal(context)), + ]; +} + +describe("discord wildcard component registration ids", () => { + it("uses distinct sentinel customIds instead of a shared literal wildcard", () => { + const components = createWildcardComponents(); + const customIds = components.map((component) => component.customId); + + expect(customIds.every((id) => id !== "*")).toBe(true); + expect(new Set(customIds).size).toBe(customIds.length); + }); + + it("still resolves sentinel ids and runtime ids through wildcard parser key", () => { + const components = createWildcardComponents(); + const interactionCustomId = buildDiscordComponentCustomId({ componentId: "sel_test" }); + const interactionModalId = buildDiscordModalCustomId("mdl_test"); + + for (const component of components) { + expect(component.customIdParser(component.customId).key).toBe("*"); + if (component.customId.includes("_modal_")) { + expect(component.customIdParser(interactionModalId).key).toBe("*"); + } else { + expect(component.customIdParser(interactionCustomId).key).toBe("*"); + } + } + }); +});