forked from EduCraft/curriculum-project-hub
fix: reply to Feishu trigger messages
This commit is contained in:
+106
-56
@@ -28,6 +28,10 @@ export interface OutboundPayload {
|
||||
readonly content: string;
|
||||
}
|
||||
|
||||
export interface SendMessageOptions {
|
||||
readonly replyToMessageId?: string | undefined;
|
||||
}
|
||||
|
||||
export async function withRetry<T>(
|
||||
fn: () => Promise<T>,
|
||||
options: {
|
||||
@@ -193,29 +197,24 @@ export function parsePostMessage(content: string): { text: string; imageKeys: st
|
||||
}
|
||||
|
||||
/** Send a text message using the best Feishu message type for the content. */
|
||||
export async function sendTextMessage(rt: FeishuRuntime, chatId: string, text: string): Promise<string | null> {
|
||||
export async function sendTextMessage(
|
||||
rt: FeishuRuntime,
|
||||
chatId: string,
|
||||
text: string,
|
||||
options?: SendMessageOptions,
|
||||
): Promise<string | null> {
|
||||
const payload = buildOutboundPayload(text);
|
||||
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: payload.msgType, content: payload.content },
|
||||
});
|
||||
return messageIdFromResponse(res);
|
||||
return await sendMessagePayload(rt, chatId, payload, options);
|
||||
} catch (e) {
|
||||
if (payload.msgType === "post" && isPostContentFormatError(e)) {
|
||||
try {
|
||||
const res = await client.im.v1.message.create({
|
||||
params: { receive_id_type: "chat_id" },
|
||||
data: {
|
||||
receive_id: chatId,
|
||||
msg_type: "text",
|
||||
content: JSON.stringify({ text: _stripMarkdownToPlainText(text) }),
|
||||
},
|
||||
});
|
||||
return messageIdFromResponse(res);
|
||||
return await sendMessagePayload(
|
||||
rt,
|
||||
chatId,
|
||||
{ msgType: "text", content: JSON.stringify({ text: _stripMarkdownToPlainText(text) }) },
|
||||
options,
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -225,10 +224,15 @@ export async function sendTextMessage(rt: FeishuRuntime, chatId: string, text: s
|
||||
}
|
||||
|
||||
/** Send long text as multiple Feishu messages. Returns the last successful message_id. */
|
||||
export async function sendLongText(rt: FeishuRuntime, chatId: string, text: string): Promise<string | null> {
|
||||
export async function sendLongText(
|
||||
rt: FeishuRuntime,
|
||||
chatId: string,
|
||||
text: string,
|
||||
options?: SendMessageOptions,
|
||||
): Promise<string | null> {
|
||||
let lastMessageId: string | null = null;
|
||||
for (const chunk of splitAtBoundary(text, DEFAULT_MAX_MESSAGE_LENGTH)) {
|
||||
const messageId = await sendTextMessage(rt, chatId, chunk);
|
||||
const messageId = await sendTextMessage(rt, chatId, chunk, options);
|
||||
if (messageId !== null) {
|
||||
lastMessageId = messageId;
|
||||
}
|
||||
@@ -237,23 +241,31 @@ export async function sendLongText(rt: FeishuRuntime, chatId: string, text: stri
|
||||
}
|
||||
|
||||
/** Send a plain text message (fire-and-forget). */
|
||||
export async function sendText(rt: FeishuRuntime, chatId: string, text: string): Promise<void> {
|
||||
await sendTextMessage(rt, chatId, text);
|
||||
export async function sendText(
|
||||
rt: FeishuRuntime,
|
||||
chatId: string,
|
||||
text: string,
|
||||
options?: SendMessageOptions,
|
||||
): Promise<void> {
|
||||
await sendTextMessage(rt, chatId, text, options);
|
||||
}
|
||||
|
||||
/** Send an interactive card message (always patchable). Used for streaming. */
|
||||
export async function sendInteractiveCardMessage(rt: FeishuRuntime, chatId: string, text: string): Promise<string | null> {
|
||||
export async function sendInteractiveCardMessage(
|
||||
rt: FeishuRuntime,
|
||||
chatId: string,
|
||||
text: string,
|
||||
options?: SendMessageOptions,
|
||||
): Promise<string | null> {
|
||||
const payload = buildOutboundPayload(text);
|
||||
const card = buildInteractiveCardFromOutboundPayload(payload);
|
||||
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);
|
||||
return await sendMessagePayload(
|
||||
rt,
|
||||
chatId,
|
||||
{ msgType: "interactive", content: JSON.stringify(card) },
|
||||
options,
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -278,16 +290,19 @@ export async function patchTextMessage(rt: FeishuRuntime, messageId: string, tex
|
||||
}
|
||||
|
||||
/** Send a raw interactive card (arbitrary JSON). Returns message_id on success. */
|
||||
export async function sendCard(rt: FeishuRuntime, chatId: string, card: Record<string, unknown>): Promise<string | null> {
|
||||
const client = rt.client as unknown as {
|
||||
im: { v1: { message: { create: (p: unknown) => Promise<MessageCreateResponse> } } };
|
||||
};
|
||||
export async function sendCard(
|
||||
rt: FeishuRuntime,
|
||||
chatId: string,
|
||||
card: Record<string, unknown>,
|
||||
options?: SendMessageOptions,
|
||||
): Promise<string | null> {
|
||||
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);
|
||||
return await sendMessagePayload(
|
||||
rt,
|
||||
chatId,
|
||||
{ msgType: "interactive", content: JSON.stringify(card) },
|
||||
options,
|
||||
);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
@@ -318,6 +333,7 @@ export async function sendApprovalCard(
|
||||
title: string,
|
||||
body: string,
|
||||
buttons: ReadonlyArray<{ label: string; value: string; style?: "primary" | "default" | "danger" }>,
|
||||
options?: SendMessageOptions,
|
||||
): Promise<string | null> {
|
||||
const card = {
|
||||
config: { wide_screen_mode: true },
|
||||
@@ -335,15 +351,13 @@ export async function sendApprovalCard(
|
||||
},
|
||||
],
|
||||
};
|
||||
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);
|
||||
return await sendMessagePayload(
|
||||
rt,
|
||||
chatId,
|
||||
{ msgType: "interactive", content: JSON.stringify(card) },
|
||||
options,
|
||||
);
|
||||
} catch (e) {
|
||||
rt.logger.warn({ chatId, err: e instanceof Error ? e.message : String(e) }, "sendApprovalCard failed");
|
||||
return null;
|
||||
@@ -491,11 +505,16 @@ export async function downloadMessageFile(
|
||||
}
|
||||
|
||||
/** Upload and send a file to a chat. */
|
||||
export async function sendFile(rt: FeishuRuntime, chatId: string, filePath: string, fileName: string): Promise<string | null> {
|
||||
export async function sendFile(
|
||||
rt: FeishuRuntime,
|
||||
chatId: string,
|
||||
filePath: string,
|
||||
fileName: string,
|
||||
options?: SendMessageOptions,
|
||||
): Promise<string | null> {
|
||||
const client = rt.client as unknown as {
|
||||
im: { v1: {
|
||||
file: { create: (p: unknown) => Promise<FileCreateResponse> }
|
||||
message: { create: (p: unknown) => Promise<MessageCreateResponse> }
|
||||
} };
|
||||
};
|
||||
try {
|
||||
@@ -510,13 +529,9 @@ export async function sendFile(rt: FeishuRuntime, chatId: string, filePath: stri
|
||||
rt.logger.warn({ chatId, filePath }, "sendFile failed: upload response missing file_key");
|
||||
return null;
|
||||
}
|
||||
const msgRes = await withRetry(async () =>
|
||||
client.im.v1.message.create({
|
||||
params: { receive_id_type: "chat_id" },
|
||||
data: { receive_id: chatId, msg_type: "file", content: JSON.stringify({ file_key: fileKey }) },
|
||||
}),
|
||||
const messageId = await withRetry(async () =>
|
||||
sendMessagePayload(rt, chatId, { msgType: "file", content: JSON.stringify({ file_key: fileKey }) }, options),
|
||||
);
|
||||
const messageId = messageIdFromResponse(msgRes);
|
||||
if (messageId === null) {
|
||||
rt.logger.warn({ chatId, filePath }, "sendFile failed: message response missing message_id");
|
||||
}
|
||||
@@ -527,6 +542,41 @@ export async function sendFile(rt: FeishuRuntime, chatId: string, filePath: stri
|
||||
}
|
||||
}
|
||||
|
||||
async function sendMessagePayload(
|
||||
rt: FeishuRuntime,
|
||||
chatId: string,
|
||||
payload: OutboundPayload,
|
||||
options?: SendMessageOptions,
|
||||
): Promise<string | null> {
|
||||
const client = rt.client as unknown as {
|
||||
im: { v1: { message: {
|
||||
create: (p: unknown) => Promise<MessageCreateResponse>;
|
||||
reply: (p: unknown) => Promise<MessageCreateResponse>;
|
||||
} } };
|
||||
};
|
||||
const replyToMessageId = normalizeReplyMessageId(options?.replyToMessageId);
|
||||
if (replyToMessageId !== null) {
|
||||
const res = await client.im.v1.message.reply({
|
||||
path: { message_id: replyToMessageId },
|
||||
data: {
|
||||
msg_type: payload.msgType,
|
||||
content: payload.content,
|
||||
},
|
||||
});
|
||||
return messageIdFromResponse(res);
|
||||
}
|
||||
|
||||
const res = await client.im.v1.message.create({
|
||||
params: { receive_id_type: "chat_id" },
|
||||
data: { receive_id: chatId, msg_type: payload.msgType, content: payload.content },
|
||||
});
|
||||
return messageIdFromResponse(res);
|
||||
}
|
||||
|
||||
function normalizeReplyMessageId(messageId: string | undefined): string | null {
|
||||
return messageId === undefined || messageId === "" ? null : messageId;
|
||||
}
|
||||
|
||||
type MessageCreateResponse = {
|
||||
readonly message_id?: string | undefined;
|
||||
readonly data?: { readonly message_id?: string | undefined } | undefined;
|
||||
|
||||
Reference in New Issue
Block a user