Skip to content

Commit 1997cc2

Browse files
committed
fix(feishu): skip reply anchor for DM streaming cards (fixes #94922)
- Fix streaming.start() to use sendReplyToMessageId instead of raw replyToMessageId - Add tests for DM skip, group topic preserve, and P2P thread preserve scenarios - All existing tests pass (85/85 in reply-dispatcher.test.ts, 87/87 in bot.test.ts) Fixes #94922
1 parent 940d33c commit 1997cc2

2 files changed

Lines changed: 126 additions & 1 deletion

File tree

extensions/feishu/src/reply-dispatcher.test.ts

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2108,4 +2108,129 @@ describe("createFeishuReplyDispatcher streaming behavior", () => {
21082108
nowSpy.mockRestore();
21092109
}
21102110
});
2111+
2112+
it("skips reply anchor for ordinary DM with streaming enabled (fixes #94922)", async () => {
2113+
// Arrange: Ordinary DM session (not group, not thread reply)
2114+
// skipReplyToInMessages=true simulates what bot.ts calculates for P2P DMs
2115+
resolveFeishuAccountMock.mockReturnValue({
2116+
accountId: "main",
2117+
appId: "app_id",
2118+
appSecret: "app_secret",
2119+
domain: "feishu",
2120+
config: {
2121+
renderMode: "auto",
2122+
streaming: true,
2123+
},
2124+
});
2125+
2126+
createFeishuReplyDispatcher({
2127+
cfg: {} as never,
2128+
agentId: "agent",
2129+
runtime: {} as never,
2130+
chatId: "oc_dm_chat",
2131+
replyToMessageId: "om_parent",
2132+
skipReplyToInMessages: true, // Key fix: DM should skip reply anchor
2133+
replyInThread: false,
2134+
threadReply: false,
2135+
});
2136+
2137+
const options = firstTypingDispatcherOptions();
2138+
2139+
// Act: Send a streaming reply
2140+
await options.deliver({ text: "DM reply without reply anchor" }, { kind: "final" });
2141+
await options.onIdle?.();
2142+
2143+
// Assert: streaming.start should be called with replyToMessageId: undefined
2144+
expect(streamingInstances).toHaveLength(1);
2145+
const startOptions = firstMockArg(streamingInstances[0].start, "streaming start", 2) as Record<
2146+
string,
2147+
unknown
2148+
>;
2149+
expect(startOptions.replyToMessageId).toBeUndefined();
2150+
expect(startOptions.replyInThread).toBe(false);
2151+
});
2152+
2153+
it("preserves reply anchor for group topic with streaming enabled", async () => {
2154+
// Arrange: Group topic session
2155+
resolveFeishuAccountMock.mockReturnValue({
2156+
accountId: "main",
2157+
appId: "app_id",
2158+
appSecret: "app_secret",
2159+
domain: "feishu",
2160+
config: {
2161+
renderMode: "auto",
2162+
streaming: true,
2163+
},
2164+
});
2165+
2166+
createFeishuReplyDispatcher({
2167+
cfg: {} as never,
2168+
agentId: "agent",
2169+
runtime: {} as never,
2170+
chatId: "oc_group_chat",
2171+
replyToMessageId: "om_topic_root",
2172+
rootId: "om_topic_root",
2173+
isGroup: true,
2174+
directThreadReply: false,
2175+
replyInThread: true,
2176+
});
2177+
2178+
const options = firstTypingDispatcherOptions();
2179+
2180+
// Act: Send a streaming reply to group topic
2181+
await options.deliver({ text: "Group topic reply" }, { kind: "final" });
2182+
await options.onIdle?.();
2183+
2184+
// Assert: streaming.start should preserve replyToMessageId for group topics
2185+
expect(streamingInstances).toHaveLength(1);
2186+
const startOptions = firstMockArg(streamingInstances[0].start, "streaming start", 2) as Record<
2187+
string,
2188+
unknown
2189+
>;
2190+
expect(startOptions.replyToMessageId).toBe("om_topic_root");
2191+
expect(startOptions.rootId).toBe("om_topic_root");
2192+
expect(startOptions.replyInThread).toBe(true);
2193+
});
2194+
2195+
it("preserves reply anchor for P2P thread reply with streaming enabled", async () => {
2196+
// Arrange: P2P thread reply session
2197+
resolveFeishuAccountMock.mockReturnValue({
2198+
accountId: "main",
2199+
appId: "app_id",
2200+
appSecret: "app_secret",
2201+
domain: "feishu",
2202+
config: {
2203+
renderMode: "auto",
2204+
streaming: true,
2205+
},
2206+
});
2207+
2208+
createFeishuReplyDispatcher({
2209+
cfg: {} as never,
2210+
agentId: "agent",
2211+
runtime: {} as never,
2212+
chatId: "oc_dm_thread",
2213+
replyToMessageId: "om_dm_thread_root",
2214+
rootId: "om_dm_thread_root",
2215+
isGroup: false,
2216+
directThreadReply: true,
2217+
threadReply: true,
2218+
});
2219+
2220+
const options = firstTypingDispatcherOptions();
2221+
2222+
// Act: Send a streaming reply in P2P thread
2223+
await options.deliver({ text: "P2P thread reply" }, { kind: "final" });
2224+
await options.onIdle?.();
2225+
2226+
// Assert: streaming.start should preserve replyToMessageId for P2P threads
2227+
expect(streamingInstances).toHaveLength(1);
2228+
const startOptions = firstMockArg(streamingInstances[0].start, "streaming start", 2) as Record<
2229+
string,
2230+
unknown
2231+
>;
2232+
expect(startOptions.replyToMessageId).toBe("om_dm_thread_root");
2233+
expect(startOptions.rootId).toBe("om_dm_thread_root");
2234+
expect(startOptions.replyInThread).toBe(true);
2235+
});
21112236
});

extensions/feishu/src/reply-dispatcher.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -392,7 +392,7 @@ export function createFeishuReplyDispatcher(params: CreateFeishuReplyDispatcherP
392392
const cardHeader = resolveCardHeader(agentId, identity);
393393
const cardNote = resolveCardNote(agentId, identity, prefixContext.prefixContext);
394394
await streaming.start(chatId, resolveReceiveIdType(chatId), {
395-
replyToMessageId,
395+
replyToMessageId: sendReplyToMessageId,
396396
replyInThread: effectiveReplyInThread,
397397
rootId,
398398
header: cardHeader,

0 commit comments

Comments
 (0)