mirror of https://github.com/openclaw/openclaw.git
feat(feishu): extract action.option, action.options, and action.form_value from card callbacks (closes #42754)
Extend FeishuCardActionEvent type and handleFeishuCardAction() to extract data from select_static dropdowns (action.option), multi-select checkboxes (action.options), and form submissions (action.form_value). Previously only action.value was read, silently discarding interaction data from all non-button card components. The new fields are merged with the existing value object to provide complete context to downstream handlers. Existing button flows (value.text and value.command) are completely unaffected — the new extraction only activates when the component-specific fields are present. Add tests for select_static, checkbox, and form submission scenarios. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
parent
db20141993
commit
d9f1b69dbf
|
|
@ -60,4 +60,83 @@ describe("Feishu Card Action Handler", () => {
|
|||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it("handles select_static dropdown with action.option", async () => {
|
||||
const event: FeishuCardActionEvent = {
|
||||
operator: { open_id: "u123", user_id: "uid1", union_id: "un1" },
|
||||
token: "tok3",
|
||||
action: { value: { field: "model_selection" }, tag: "select_static", option: "gpt-4o" },
|
||||
context: { open_id: "u123", user_id: "uid1", chat_id: "chat1" },
|
||||
};
|
||||
|
||||
await handleFeishuCardAction({ cfg, event, runtime });
|
||||
|
||||
const call = vi
|
||||
.mocked(handleFeishuMessage)
|
||||
.mock.calls.find(
|
||||
(c: unknown[]) =>
|
||||
(c[0] as { event: { message: { message_id: string } } }).event.message.message_id ===
|
||||
"card-action-tok3",
|
||||
);
|
||||
expect(call).toBeDefined();
|
||||
const inner = JSON.parse(
|
||||
JSON.parse((call![0] as { event: { message: { content: string } } }).event.message.content)
|
||||
.text,
|
||||
);
|
||||
expect(inner).toMatchObject({ option: "gpt-4o" });
|
||||
});
|
||||
|
||||
it("handles multi-select checkbox with action.options", async () => {
|
||||
const event: FeishuCardActionEvent = {
|
||||
operator: { open_id: "u123", user_id: "uid1", union_id: "un1" },
|
||||
token: "tok4",
|
||||
action: { value: { field: "tags" }, tag: "checkbox", options: ["tag1", "tag2"] },
|
||||
context: { open_id: "u123", user_id: "uid1", chat_id: "chat1" },
|
||||
};
|
||||
|
||||
await handleFeishuCardAction({ cfg, event, runtime });
|
||||
|
||||
const call = vi
|
||||
.mocked(handleFeishuMessage)
|
||||
.mock.calls.find(
|
||||
(c: unknown[]) =>
|
||||
(c[0] as { event: { message: { message_id: string } } }).event.message.message_id ===
|
||||
"card-action-tok4",
|
||||
);
|
||||
expect(call).toBeDefined();
|
||||
const inner = JSON.parse(
|
||||
JSON.parse((call![0] as { event: { message: { content: string } } }).event.message.content)
|
||||
.text,
|
||||
);
|
||||
expect(inner).toMatchObject({ options: ["tag1", "tag2"] });
|
||||
});
|
||||
|
||||
it("handles form submission with action.form_value", async () => {
|
||||
const event: FeishuCardActionEvent = {
|
||||
operator: { open_id: "u123", user_id: "uid1", union_id: "un1" },
|
||||
token: "tok5",
|
||||
action: {
|
||||
value: { form_id: "approval" },
|
||||
tag: "form",
|
||||
form_value: { name: "Alice", approved: true },
|
||||
},
|
||||
context: { open_id: "u123", user_id: "uid1", chat_id: "chat1" },
|
||||
};
|
||||
|
||||
await handleFeishuCardAction({ cfg, event, runtime });
|
||||
|
||||
const call = vi
|
||||
.mocked(handleFeishuMessage)
|
||||
.mock.calls.find(
|
||||
(c: unknown[]) =>
|
||||
(c[0] as { event: { message: { message_id: string } } }).event.message.message_id ===
|
||||
"card-action-tok5",
|
||||
);
|
||||
expect(call).toBeDefined();
|
||||
const inner = JSON.parse(
|
||||
JSON.parse((call![0] as { event: { message: { content: string } } }).event.message.content)
|
||||
.text,
|
||||
);
|
||||
expect(inner).toMatchObject({ form_value: { name: "Alice", approved: true } });
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -12,6 +12,9 @@ export type FeishuCardActionEvent = {
|
|||
action: {
|
||||
value: Record<string, unknown>;
|
||||
tag: string;
|
||||
option?: string;
|
||||
options?: string[];
|
||||
form_value?: Record<string, unknown>;
|
||||
};
|
||||
context: {
|
||||
open_id: string;
|
||||
|
|
@ -19,21 +22,6 @@ export type FeishuCardActionEvent = {
|
|||
chat_id: string;
|
||||
};
|
||||
};
|
||||
|
||||
function buildCardActionTextFallback(event: FeishuCardActionEvent): string {
|
||||
const actionValue = event.action.value;
|
||||
if (typeof actionValue === "object" && actionValue !== null) {
|
||||
if ("text" in actionValue && typeof actionValue.text === "string") {
|
||||
return actionValue.text;
|
||||
}
|
||||
if ("command" in actionValue && typeof actionValue.command === "string") {
|
||||
return actionValue.command;
|
||||
}
|
||||
return JSON.stringify(actionValue);
|
||||
}
|
||||
return String(actionValue);
|
||||
}
|
||||
|
||||
export async function handleFeishuCardAction(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
event: FeishuCardActionEvent;
|
||||
|
|
@ -44,7 +32,71 @@ export async function handleFeishuCardAction(params: {
|
|||
const { cfg, event, runtime, accountId } = params;
|
||||
const account = resolveFeishuAccount({ cfg, accountId });
|
||||
const log = runtime?.log ?? console.log;
|
||||
const content = buildCardActionTextFallback(event);
|
||||
|
||||
// Extract action content — check component-specific fields before falling
|
||||
// through to the generic value.text / value.command path.
|
||||
const actionValue = event.action.value ?? {};
|
||||
let content = "";
|
||||
|
||||
// Interactive components: when the static value includes a command/text
|
||||
// routing key, preserve it as the content so downstream command detection
|
||||
// still works. Attach the dynamic user input as structured metadata that
|
||||
// the agent can inspect without breaking command probing.
|
||||
const hasCommand =
|
||||
typeof actionValue === "object" &&
|
||||
actionValue !== null &&
|
||||
"command" in actionValue &&
|
||||
typeof actionValue.command === "string";
|
||||
const hasText =
|
||||
typeof actionValue === "object" &&
|
||||
actionValue !== null &&
|
||||
"text" in actionValue &&
|
||||
typeof actionValue.text === "string";
|
||||
|
||||
if (event.action.form_value && typeof event.action.form_value === "object") {
|
||||
if (hasCommand) {
|
||||
content = `${actionValue.command} ${JSON.stringify({ form_value: event.action.form_value })}`;
|
||||
} else if (hasText) {
|
||||
content = `${actionValue.text} ${JSON.stringify({ form_value: event.action.form_value })}`;
|
||||
} else {
|
||||
const merged = { ...actionValue, form_value: event.action.form_value };
|
||||
content = JSON.stringify(merged);
|
||||
}
|
||||
}
|
||||
// multi-select (checkbox): merge options array into value
|
||||
else if (Array.isArray(event.action.options)) {
|
||||
if (hasCommand) {
|
||||
content = `${actionValue.command} ${JSON.stringify({ options: event.action.options })}`;
|
||||
} else if (hasText) {
|
||||
content = `${actionValue.text} ${JSON.stringify({ options: event.action.options })}`;
|
||||
} else {
|
||||
const merged = { ...actionValue, options: event.action.options };
|
||||
content = JSON.stringify(merged);
|
||||
}
|
||||
}
|
||||
// single-select (select_static dropdown): merge option into value
|
||||
else if (typeof event.action.option === "string") {
|
||||
if (hasCommand) {
|
||||
content = `${actionValue.command} ${JSON.stringify({ option: event.action.option })}`;
|
||||
} else if (hasText) {
|
||||
content = `${actionValue.text} ${JSON.stringify({ option: event.action.option })}`;
|
||||
} else {
|
||||
const merged = { ...actionValue, option: event.action.option };
|
||||
content = JSON.stringify(merged);
|
||||
}
|
||||
}
|
||||
// button: existing text / command / JSON fallback
|
||||
else if (typeof actionValue === "object" && actionValue !== null) {
|
||||
if ("text" in actionValue && typeof actionValue.text === "string") {
|
||||
content = actionValue.text;
|
||||
} else if ("command" in actionValue && typeof actionValue.command === "string") {
|
||||
content = actionValue.command;
|
||||
} else {
|
||||
content = JSON.stringify(actionValue);
|
||||
}
|
||||
} else {
|
||||
content = String(actionValue);
|
||||
}
|
||||
|
||||
// Construct a synthetic message event
|
||||
const messageEvent: FeishuMessageEvent = {
|
||||
|
|
|
|||
Loading…
Reference in New Issue