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: {
|
action: {
|
||||||
value: Record<string, unknown>;
|
value: Record<string, unknown>;
|
||||||
tag: string;
|
tag: string;
|
||||||
|
option?: string;
|
||||||
|
options?: string[];
|
||||||
|
form_value?: Record<string, unknown>;
|
||||||
};
|
};
|
||||||
context: {
|
context: {
|
||||||
open_id: string;
|
open_id: string;
|
||||||
|
|
@ -19,21 +22,6 @@ export type FeishuCardActionEvent = {
|
||||||
chat_id: string;
|
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: {
|
export async function handleFeishuCardAction(params: {
|
||||||
cfg: ClawdbotConfig;
|
cfg: ClawdbotConfig;
|
||||||
event: FeishuCardActionEvent;
|
event: FeishuCardActionEvent;
|
||||||
|
|
@ -44,7 +32,71 @@ export async function handleFeishuCardAction(params: {
|
||||||
const { cfg, event, runtime, accountId } = params;
|
const { cfg, event, runtime, accountId } = params;
|
||||||
const account = resolveFeishuAccount({ cfg, accountId });
|
const account = resolveFeishuAccount({ cfg, accountId });
|
||||||
const log = runtime?.log ?? console.log;
|
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
|
// Construct a synthetic message event
|
||||||
const messageEvent: FeishuMessageEvent = {
|
const messageEvent: FeishuMessageEvent = {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue