diff --git a/extensions/feishu/src/bot.ts b/extensions/feishu/src/bot.ts index ae86d96be30..690671e7487 100644 --- a/extensions/feishu/src/bot.ts +++ b/extensions/feishu/src/bot.ts @@ -1019,6 +1019,7 @@ export async function handleFeishuMessage(params: { chatId: ctx.chatId, replyToMessageId: ctx.messageId, replyInThread, + rootId: ctx.rootId, mentionTargets: ctx.mentionTargets, accountId: account.accountId, }); diff --git a/extensions/feishu/src/reply-dispatcher.test.ts b/extensions/feishu/src/reply-dispatcher.test.ts index 55834a8ab0d..b1da983054b 100644 --- a/extensions/feishu/src/reply-dispatcher.test.ts +++ b/extensions/feishu/src/reply-dispatcher.test.ts @@ -105,6 +105,7 @@ describe("createFeishuReplyDispatcher streaming behavior", () => { agentId: "agent", runtime: { log: vi.fn(), error: vi.fn() } as never, chatId: "oc_chat", + rootId: "om_root_topic", }); const options = createReplyDispatcherWithTypingMock.mock.calls[0]?.[0]; @@ -112,6 +113,11 @@ describe("createFeishuReplyDispatcher streaming behavior", () => { expect(streamingInstances).toHaveLength(1); expect(streamingInstances[0].start).toHaveBeenCalledTimes(1); + expect(streamingInstances[0].start).toHaveBeenCalledWith("oc_chat", "chat_id", { + replyToMessageId: undefined, + replyInThread: undefined, + rootId: "om_root_topic", + }); expect(streamingInstances[0].close).toHaveBeenCalledTimes(1); expect(sendMessageFeishuMock).not.toHaveBeenCalled(); expect(sendMarkdownCardFeishuMock).not.toHaveBeenCalled(); diff --git a/extensions/feishu/src/reply-dispatcher.ts b/extensions/feishu/src/reply-dispatcher.ts index 9cf836be267..cbdca4b570e 100644 --- a/extensions/feishu/src/reply-dispatcher.ts +++ b/extensions/feishu/src/reply-dispatcher.ts @@ -29,14 +29,23 @@ export type CreateFeishuReplyDispatcherParams = { chatId: string; replyToMessageId?: string; replyInThread?: boolean; + rootId?: string; mentionTargets?: MentionTarget[]; accountId?: string; }; export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherParams) { const core = getFeishuRuntime(); - const { cfg, agentId, chatId, replyToMessageId, replyInThread, mentionTargets, accountId } = - params; + const { + cfg, + agentId, + chatId, + replyToMessageId, + replyInThread, + rootId, + mentionTargets, + accountId, + } = params; const account = resolveFeishuAccount({ cfg, accountId }); const prefixContext = createReplyPrefixContext({ cfg, agentId }); @@ -105,6 +114,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP await streaming.start(chatId, resolveReceiveIdType(chatId), { replyToMessageId, replyInThread, + rootId, }); } catch (error) { params.runtime.error?.(`feishu: streaming start failed: ${String(error)}`); diff --git a/extensions/feishu/src/streaming-card.ts b/extensions/feishu/src/streaming-card.ts index 6eb72135c12..1929eac0d94 100644 --- a/extensions/feishu/src/streaming-card.ts +++ b/extensions/feishu/src/streaming-card.ts @@ -99,7 +99,7 @@ export class FeishuStreamingSession { async start( receiveId: string, receiveIdType: "open_id" | "user_id" | "union_id" | "email" | "chat_id" = "chat_id", - options?: { replyToMessageId?: string; replyInThread?: boolean }, + options?: { replyToMessageId?: string; replyInThread?: boolean; rootId?: string }, ): Promise { if (this.state) { return; @@ -144,9 +144,20 @@ export class FeishuStreamingSession { const cardId = createData.data.card_id; const cardContent = JSON.stringify({ type: "card", data: { card_id: cardId } }); - // Send card message — reply into thread when configured + // Topic-group replies require root_id routing. Prefer create+root_id when available. let sendRes; - if (options?.replyToMessageId) { + if (options?.rootId) { + const createData = { + receive_id: receiveId, + msg_type: "interactive", + content: cardContent, + root_id: options.rootId, + }; + sendRes = await this.client.im.message.create({ + params: { receive_id_type: receiveIdType }, + data: createData, + }); + } else if (options?.replyToMessageId) { sendRes = await this.client.im.message.reply({ path: { message_id: options.replyToMessageId }, data: { diff --git a/scripts/check-no-raw-channel-fetch.mjs b/scripts/check-no-raw-channel-fetch.mjs index 56008b3f1d8..814d3777918 100644 --- a/scripts/check-no-raw-channel-fetch.mjs +++ b/scripts/check-no-raw-channel-fetch.mjs @@ -24,9 +24,9 @@ const sourceRoots = [ const allowedRawFetchCallsites = new Set([ "extensions/bluebubbles/src/types.ts:131", "extensions/feishu/src/streaming-card.ts:31", - "extensions/feishu/src/streaming-card.ts:100", - "extensions/feishu/src/streaming-card.ts:141", - "extensions/feishu/src/streaming-card.ts:197", + "extensions/feishu/src/streaming-card.ts:101", + "extensions/feishu/src/streaming-card.ts:143", + "extensions/feishu/src/streaming-card.ts:199", "extensions/google-gemini-cli-auth/oauth.ts:372", "extensions/google-gemini-cli-auth/oauth.ts:408", "extensions/google-gemini-cli-auth/oauth.ts:447",