feishu: forward outbound reply target context

This commit is contained in:
bmendonca3
2026-03-03 15:41:11 -07:00
parent 2a733a8444
commit c129a691fc
2 changed files with 74 additions and 5 deletions

View File

@@ -136,6 +136,25 @@ describe("feishuOutbound.sendText local-image auto-convert", () => {
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
expect(result).toEqual(expect.objectContaining({ channel: "feishu", messageId: "card_msg" }));
});
it("forwards replyToId as replyToMessageId on sendText", async () => {
await sendText({
cfg: {} as any,
to: "chat_1",
text: "hello",
replyToId: "om_reply_1",
accountId: "main",
} as any);
expect(sendMessageFeishuMock).toHaveBeenCalledWith(
expect.objectContaining({
to: "chat_1",
text: "hello",
replyToMessageId: "om_reply_1",
accountId: "main",
}),
);
});
});
describe("feishuOutbound.sendMedia renderMode", () => {
@@ -178,4 +197,24 @@ describe("feishuOutbound.sendMedia renderMode", () => {
expect(sendMessageFeishuMock).not.toHaveBeenCalled();
expect(result).toEqual(expect.objectContaining({ channel: "feishu", messageId: "media_msg" }));
});
it("uses threadId fallback as replyToMessageId on sendMedia", async () => {
await feishuOutbound.sendMedia?.({
cfg: {} as any,
to: "chat_1",
text: "caption",
mediaUrl: "https://example.com/image.png",
threadId: "om_thread_1",
accountId: "main",
} as any);
expect(sendMediaFeishuMock).toHaveBeenCalledWith(
expect.objectContaining({
to: "chat_1",
mediaUrl: "https://example.com/image.png",
replyToMessageId: "om_thread_1",
accountId: "main",
}),
);
});
});

View File

@@ -43,21 +43,34 @@ function shouldUseCard(text: string): boolean {
return /```[\s\S]*?```/.test(text) || /\|.+\|[\r\n]+\|[-:| ]+\|/.test(text);
}
function resolveReplyToMessageId(params: {
replyToId?: string | null;
threadId?: string | number | null;
}): string | undefined {
const replyTarget = params.replyToId ?? params.threadId;
if (replyTarget == null) {
return undefined;
}
const trimmed = String(replyTarget).trim();
return trimmed || undefined;
}
async function sendOutboundText(params: {
cfg: Parameters<typeof sendMessageFeishu>[0]["cfg"];
to: string;
text: string;
accountId?: string;
replyToMessageId?: string;
}) {
const { cfg, to, text, accountId } = params;
const { cfg, to, text, accountId, replyToMessageId } = params;
const account = resolveFeishuAccount({ cfg, accountId });
const renderMode = account.config?.renderMode ?? "auto";
if (renderMode === "card" || (renderMode === "auto" && shouldUseCard(text))) {
return sendMarkdownCardFeishu({ cfg, to, text, accountId });
return sendMarkdownCardFeishu({ cfg, to, text, accountId, replyToMessageId });
}
return sendMessageFeishu({ cfg, to, text, accountId });
return sendMessageFeishu({ cfg, to, text, accountId, replyToMessageId });
}
export const feishuOutbound: ChannelOutboundAdapter = {
@@ -65,7 +78,8 @@ export const feishuOutbound: ChannelOutboundAdapter = {
chunker: (text, limit) => getFeishuRuntime().channel.text.chunkMarkdownText(text, limit),
chunkerMode: "markdown",
textChunkLimit: 4000,
sendText: async ({ cfg, to, text, accountId }) => {
sendText: async ({ cfg, to, text, accountId, replyToId, threadId }) => {
const replyToMessageId = resolveReplyToMessageId({ replyToId, threadId });
// Scheme A compatibility shim:
// when upstream accidentally returns a local image path as plain text,
// auto-upload and send as Feishu image message instead of leaking path text.
@@ -77,6 +91,7 @@ export const feishuOutbound: ChannelOutboundAdapter = {
to,
mediaUrl: localImagePath,
accountId: accountId ?? undefined,
replyToMessageId,
});
return { channel: "feishu", ...result };
} catch (err) {
@@ -90,10 +105,21 @@ export const feishuOutbound: ChannelOutboundAdapter = {
to,
text,
accountId: accountId ?? undefined,
replyToMessageId,
});
return { channel: "feishu", ...result };
},
sendMedia: async ({ cfg, to, text, mediaUrl, accountId, mediaLocalRoots }) => {
sendMedia: async ({
cfg,
to,
text,
mediaUrl,
accountId,
mediaLocalRoots,
replyToId,
threadId,
}) => {
const replyToMessageId = resolveReplyToMessageId({ replyToId, threadId });
// Send text first if provided
if (text?.trim()) {
await sendOutboundText({
@@ -101,6 +127,7 @@ export const feishuOutbound: ChannelOutboundAdapter = {
to,
text,
accountId: accountId ?? undefined,
replyToMessageId,
});
}
@@ -113,6 +140,7 @@ export const feishuOutbound: ChannelOutboundAdapter = {
mediaUrl,
accountId: accountId ?? undefined,
mediaLocalRoots,
replyToMessageId,
});
return { channel: "feishu", ...result };
} catch (err) {
@@ -125,6 +153,7 @@ export const feishuOutbound: ChannelOutboundAdapter = {
to,
text: fallbackText,
accountId: accountId ?? undefined,
replyToMessageId,
});
return { channel: "feishu", ...result };
}
@@ -136,6 +165,7 @@ export const feishuOutbound: ChannelOutboundAdapter = {
to,
text: text ?? "",
accountId: accountId ?? undefined,
replyToMessageId,
});
return { channel: "feishu", ...result };
},