forked from bai/curriculum-project-hub
feat: add interactive approval/confirmation cards
- sendApprovalCard: interactive card with configurable buttons - resolveCard: patch card to show resolution state (green/red) - ApprovalManager: register/resolve lifecycle with 5min timeout - request_approval MCP tool: agent can ask user for confirmation - Wire onCardAction in trigger handler and server startup - Add unit tests (5) for card JSON, manager, and routing
This commit is contained in:
@@ -180,6 +180,73 @@ export async function patchTextMessage(rt: FeishuRuntime, messageId: string, tex
|
||||
}
|
||||
}
|
||||
|
||||
/** Send an approval card with buttons. Returns message_id on success. */
|
||||
export async function sendApprovalCard(
|
||||
rt: FeishuRuntime,
|
||||
chatId: string,
|
||||
title: string,
|
||||
body: string,
|
||||
buttons: ReadonlyArray<{ label: string; value: string; style?: "primary" | "default" | "danger" }>,
|
||||
): Promise<string | null> {
|
||||
const card = {
|
||||
config: { wide_screen_mode: true },
|
||||
header: { title: { content: title, tag: "plain_text" }, template: "orange" },
|
||||
elements: [
|
||||
{ tag: "markdown", content: body },
|
||||
{
|
||||
tag: "action",
|
||||
actions: buttons.map((button) => ({
|
||||
tag: "button",
|
||||
text: { tag: "plain_text", content: button.label },
|
||||
type: button.style ?? "default",
|
||||
value: { approval_action: button.value },
|
||||
})),
|
||||
},
|
||||
],
|
||||
};
|
||||
const client = rt.client as unknown as {
|
||||
im: { v1: { message: { create: (p: unknown) => Promise<MessageCreateResponse> } } };
|
||||
};
|
||||
try {
|
||||
const res = await client.im.v1.message.create({
|
||||
params: { receive_id_type: "chat_id" },
|
||||
data: { receive_id: chatId, msg_type: "interactive", content: JSON.stringify(card) },
|
||||
});
|
||||
return messageIdFromResponse(res);
|
||||
} catch (e) {
|
||||
rt.logger.warn({ chatId, err: e instanceof Error ? e.message : String(e) }, "sendApprovalCard failed");
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/** Update a card to show it has been resolved. */
|
||||
export async function resolveCard(
|
||||
rt: FeishuRuntime,
|
||||
messageId: string,
|
||||
icon: string,
|
||||
label: string,
|
||||
template: "green" | "red",
|
||||
resolvedBy: string,
|
||||
): Promise<void> {
|
||||
const card = {
|
||||
config: { wide_screen_mode: true },
|
||||
header: { title: { content: `${icon} ${label}`, tag: "plain_text" }, template },
|
||||
elements: [{ tag: "markdown", content: `${icon} **${label}** by ${resolvedBy}` }],
|
||||
};
|
||||
const client = rt.client as unknown as {
|
||||
im: { v1: { message: { patch: (p: unknown) => Promise<unknown> } } };
|
||||
};
|
||||
try {
|
||||
await client.im.v1.message.patch({
|
||||
path: { message_id: messageId },
|
||||
params: { msg_type: "interactive" },
|
||||
data: { content: JSON.stringify(card) },
|
||||
});
|
||||
} catch (e) {
|
||||
rt.logger.warn({ messageId, err: e instanceof Error ? e.message : String(e) }, "resolveCard failed");
|
||||
}
|
||||
}
|
||||
|
||||
/** React to a message with an emoji. */
|
||||
export async function reactToMessage(rt: FeishuRuntime, messageId: string, emoji: string): Promise<void> {
|
||||
try {
|
||||
|
||||
Reference in New Issue
Block a user