mirror of
https://fastgit.cc/github.com/openclaw/openclaw
synced 2026-04-30 14:02:56 +08:00
fix(slack): fix slash commands with button arg menu errors
Co-authored-by: Wang Siyuan <wsy0227@sjtu.edu.cn>
This commit is contained in:
@@ -23,6 +23,7 @@ Docs: https://docs.openclaw.ai
|
||||
- Models/probe: surface invalid-model probe failures as `format` instead of `unknown` in `models list --probe`, and lock the invalid-model fallback path in with regression coverage. (#50028) Thanks @xiwuqi.
|
||||
- Agents/failover: classify OpenAI-compatible `finish_reason: network_error` stream failures as timeout so model fallback retries continue instead of stopping with an unknown failover reason. (#61784) thanks @lawrence3699.
|
||||
- Onboarding/channels: normalize channel setup metadata before discovery and validation so malformed or mixed-shape channel plugin metadata no longer breaks setup and onboarding channel lists. (#66706) Thanks @darkamenosa.
|
||||
- Slack/native commands: fix option menus for slash commands such as `/verbose` when Slack renders native buttons by giving each button a unique action ID while still routing them through the shared `openclaw_cmdarg*` listener. Thanks @Wangmerlyn.
|
||||
|
||||
## 2026.4.14
|
||||
|
||||
|
||||
@@ -220,7 +220,7 @@ function createDeferred<T>() {
|
||||
|
||||
function createArgMenusHarness() {
|
||||
const commands = new Map<string, (args: unknown) => Promise<void>>();
|
||||
const actions = new Map<string, (args: unknown) => Promise<void>>();
|
||||
const actions = new Map<string | RegExp, (args: unknown) => Promise<void>>();
|
||||
const options = new Map<string, (args: unknown) => Promise<void>>();
|
||||
const optionsReceiverContexts: unknown[] = [];
|
||||
|
||||
@@ -230,7 +230,7 @@ function createArgMenusHarness() {
|
||||
command: (name: string, handler: (args: unknown) => Promise<void>) => {
|
||||
commands.set(name, handler);
|
||||
},
|
||||
action: (id: string, handler: (args: unknown) => Promise<void>) => {
|
||||
action: (id: string | RegExp, handler: (args: unknown) => Promise<void>) => {
|
||||
actions.set(id, handler);
|
||||
},
|
||||
options: function (this: unknown, id: string, handler: (args: unknown) => Promise<void>) {
|
||||
@@ -285,11 +285,16 @@ function createArgMenusHarness() {
|
||||
}
|
||||
|
||||
function requireHandler(
|
||||
handlers: Map<string, (args: unknown) => Promise<void>>,
|
||||
key: string,
|
||||
handlers: Map<string | RegExp, (args: unknown) => Promise<void>>,
|
||||
key: string | RegExp,
|
||||
label: string,
|
||||
): (args: unknown) => Promise<void> {
|
||||
const handler = handlers.get(key);
|
||||
const handler =
|
||||
key instanceof RegExp
|
||||
? Array.from(handlers.entries()).find(
|
||||
([candidate]) => candidate instanceof RegExp && String(candidate) === String(key),
|
||||
)?.[1]
|
||||
: handlers.get(key);
|
||||
if (!handler) {
|
||||
throw new Error(`Missing ${label} handler`);
|
||||
}
|
||||
@@ -414,7 +419,7 @@ describe("Slack native command argument menus", () => {
|
||||
reportLongHandler = requireHandler(harness.commands, "/reportlong", "/reportlong");
|
||||
unsafeConfirmHandler = requireHandler(harness.commands, "/unsafeconfirm", "/unsafeconfirm");
|
||||
agentStatusHandler = requireHandler(harness.commands, "/agentstatus", "/agentstatus");
|
||||
argMenuHandler = requireHandler(harness.actions, "openclaw_cmdarg", "arg-menu action");
|
||||
argMenuHandler = requireHandler(harness.actions, /^openclaw_cmdarg/, "arg-menu action");
|
||||
argMenuOptionsHandler = requireHandler(harness.options, "openclaw_cmdarg", "arg-menu options");
|
||||
});
|
||||
|
||||
@@ -426,21 +431,25 @@ describe("Slack native command argument menus", () => {
|
||||
const testHarness = createArgMenusHarness();
|
||||
await registerCommands(testHarness.ctx, testHarness.account);
|
||||
expect(testHarness.commands.size).toBeGreaterThan(0);
|
||||
expect(testHarness.actions.has("openclaw_cmdarg")).toBe(true);
|
||||
expect(
|
||||
Array.from(testHarness.actions.keys()).some(
|
||||
(key) => key instanceof RegExp && String(key) === String(/^openclaw_cmdarg/),
|
||||
),
|
||||
).toBe(true);
|
||||
expect(testHarness.options.has("openclaw_cmdarg")).toBe(true);
|
||||
expect(testHarness.optionsReceiverContexts[0]).toBe(testHarness.app);
|
||||
});
|
||||
|
||||
it("falls back to static menus when app.options() throws during registration", async () => {
|
||||
const commands = new Map<string, (args: unknown) => Promise<void>>();
|
||||
const actions = new Map<string, (args: unknown) => Promise<void>>();
|
||||
const actions = new Map<string | RegExp, (args: unknown) => Promise<void>>();
|
||||
const postEphemeral = vi.fn().mockResolvedValue({ ok: true });
|
||||
const app = {
|
||||
client: { chat: { postEphemeral } },
|
||||
command: (name: string, handler: (args: unknown) => Promise<void>) => {
|
||||
commands.set(name, handler);
|
||||
},
|
||||
action: (id: string, handler: (args: unknown) => Promise<void>) => {
|
||||
action: (id: string | RegExp, handler: (args: unknown) => Promise<void>) => {
|
||||
actions.set(id, handler);
|
||||
},
|
||||
// Simulate Bolt throwing during options registration (e.g. receiver not initialized)
|
||||
@@ -483,7 +492,11 @@ describe("Slack native command argument menus", () => {
|
||||
// Registration should not throw despite app.options() throwing
|
||||
await registerCommands(ctx, account);
|
||||
expect(commands.size).toBeGreaterThan(0);
|
||||
expect(actions.has("openclaw_cmdarg")).toBe(true);
|
||||
expect(
|
||||
Array.from(actions.keys()).some(
|
||||
(key) => key instanceof RegExp && String(key) === String(/^openclaw_cmdarg/),
|
||||
),
|
||||
).toBe(true);
|
||||
|
||||
// The /reportexternal command (140 choices) should fall back to static_select
|
||||
// instead of external_select since options registration failed
|
||||
@@ -508,6 +521,8 @@ describe("Slack native command argument menus", () => {
|
||||
const actions = expectArgMenuLayout(respond);
|
||||
const elementType = actions?.elements?.[0]?.type;
|
||||
expect(elementType).toBe("button");
|
||||
expect(actions?.elements?.[0]?.action_id).toBe("openclaw_cmdarg_0_0");
|
||||
expect(actions?.elements?.[1]?.action_id).toBe("openclaw_cmdarg_0_1");
|
||||
expect(actions?.elements?.[0]?.confirm).toBeTruthy();
|
||||
});
|
||||
|
||||
|
||||
@@ -34,6 +34,7 @@ import { resolveSlackRoomContextHints } from "./room-context.js";
|
||||
type SlackBlock = { type: string; [key: string]: unknown };
|
||||
|
||||
const SLACK_COMMAND_ARG_ACTION_ID = "openclaw_cmdarg";
|
||||
const SLACK_COMMAND_ARG_ACTION_LISTENER = /^openclaw_cmdarg/;
|
||||
const SLACK_COMMAND_ARG_VALUE_PREFIX = "cmdarg";
|
||||
const SLACK_COMMAND_ARG_BUTTON_ROW_SIZE = 5;
|
||||
const SLACK_COMMAND_ARG_OVERFLOW_MIN = 3;
|
||||
@@ -221,16 +222,18 @@ function buildSlackCommandArgMenuBlocks(params: {
|
||||
},
|
||||
]
|
||||
: encodedChoices.length <= SLACK_COMMAND_ARG_BUTTON_ROW_SIZE || !canUseStaticSelect
|
||||
? chunkItems(encodedChoices, SLACK_COMMAND_ARG_BUTTON_ROW_SIZE).map((choices) => ({
|
||||
type: "actions",
|
||||
elements: choices.map((choice) => ({
|
||||
type: "button",
|
||||
action_id: SLACK_COMMAND_ARG_ACTION_ID,
|
||||
text: { type: "plain_text", text: choice.label },
|
||||
value: choice.value,
|
||||
confirm: buildSlackArgMenuConfirm({ command: params.command, arg: params.arg }),
|
||||
})),
|
||||
}))
|
||||
? chunkItems(encodedChoices, SLACK_COMMAND_ARG_BUTTON_ROW_SIZE).map(
|
||||
(choices, rowIndex) => ({
|
||||
type: "actions",
|
||||
elements: choices.map((choice, colIndex) => ({
|
||||
type: "button",
|
||||
action_id: `${SLACK_COMMAND_ARG_ACTION_ID}_${rowIndex}_${colIndex}`,
|
||||
text: { type: "plain_text", text: choice.label },
|
||||
value: choice.value,
|
||||
confirm: buildSlackArgMenuConfirm({ command: params.command, arg: params.arg }),
|
||||
})),
|
||||
}),
|
||||
)
|
||||
: chunkItems(encodedChoices, SLACK_COMMAND_ARG_SELECT_OPTIONS_MAX).map(
|
||||
(choices, index) => ({
|
||||
type: "actions",
|
||||
@@ -804,7 +807,7 @@ export async function registerSlackMonitorSlashCommands(params: {
|
||||
);
|
||||
}
|
||||
|
||||
const registerArgAction = (actionId: string) => {
|
||||
const registerArgAction = (actionId: string | RegExp) => {
|
||||
(
|
||||
ctx.app as unknown as {
|
||||
action: NonNullable<(typeof ctx.app & { action?: unknown })["action"]>;
|
||||
@@ -882,5 +885,5 @@ export async function registerSlackMonitorSlashCommands(params: {
|
||||
});
|
||||
});
|
||||
};
|
||||
registerArgAction(SLACK_COMMAND_ARG_ACTION_ID);
|
||||
registerArgAction(SLACK_COMMAND_ARG_ACTION_LISTENER);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user