From c129a691fcf552a1cebe1e8a22ea8611ffc3b377 Mon Sep 17 00:00:00 2001 From: bmendonca3 Date: Tue, 3 Mar 2026 15:41:11 -0700 Subject: [PATCH] feishu: forward outbound reply target context --- extensions/feishu/src/outbound.test.ts | 39 +++++++++++++++++++++++++ extensions/feishu/src/outbound.ts | 40 ++++++++++++++++++++++---- 2 files changed, 74 insertions(+), 5 deletions(-) diff --git a/extensions/feishu/src/outbound.test.ts b/extensions/feishu/src/outbound.test.ts index 693772156031..5fbe6646490d 100644 --- a/extensions/feishu/src/outbound.test.ts +++ b/extensions/feishu/src/outbound.test.ts @@ -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", + }), + ); + }); }); diff --git a/extensions/feishu/src/outbound.ts b/extensions/feishu/src/outbound.ts index b9867c496f42..50c7a1fdbdad 100644 --- a/extensions/feishu/src/outbound.ts +++ b/extensions/feishu/src/outbound.ts @@ -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[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 }; },