Skip to content

Commit e5a42c0

Browse files
authored
fix(feishu): keep sender-scoped thread bootstrap across id types (#46651)
1 parent 92fc806 commit e5a42c0

File tree

2 files changed

+92
-8
lines changed

2 files changed

+92
-8
lines changed

extensions/feishu/src/bot.test.ts

Lines changed: 75 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,11 +77,13 @@ function createRuntimeEnv(): RuntimeEnv {
7777
}
7878

7979
async function dispatchMessage(params: { cfg: ClawdbotConfig; event: FeishuMessageEvent }) {
80+
const runtime = createRuntimeEnv();
8081
await handleFeishuMessage({
8182
cfg: params.cfg,
8283
event: params.event,
83-
runtime: createRuntimeEnv(),
84+
runtime,
8485
});
86+
return runtime;
8587
}
8688

8789
describe("buildFeishuAgentBody", () => {
@@ -147,6 +149,8 @@ describe("handleFeishuMessage command authorization", () => {
147149
beforeEach(() => {
148150
vi.clearAllMocks();
149151
mockShouldComputeCommandAuthorized.mockReset().mockReturnValue(true);
152+
mockGetMessageFeishu.mockReset().mockResolvedValue(null);
153+
mockListFeishuThreadMessages.mockReset().mockResolvedValue([]);
150154
mockReadSessionUpdatedAt.mockReturnValue(undefined);
151155
mockResolveStorePath.mockReturnValue("/tmp/feishu-sessions.json");
152156
mockResolveAgentRoute.mockReturnValue({
@@ -1841,6 +1845,76 @@ describe("handleFeishuMessage command authorization", () => {
18411845
);
18421846
});
18431847

1848+
it("keeps sender-scoped thread history when the inbound event and thread history use different sender ids", async () => {
1849+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
1850+
mockGetMessageFeishu.mockResolvedValue({
1851+
messageId: "om_topic_root",
1852+
chatId: "oc-group",
1853+
content: "root starter",
1854+
contentType: "text",
1855+
threadId: "omt_topic_1",
1856+
});
1857+
mockListFeishuThreadMessages.mockResolvedValue([
1858+
{
1859+
messageId: "om_bot_reply",
1860+
senderId: "app_1",
1861+
senderType: "app",
1862+
content: "assistant reply",
1863+
contentType: "text",
1864+
createTime: 1710000000000,
1865+
},
1866+
{
1867+
messageId: "om_follow_up",
1868+
senderId: "user_topic_1",
1869+
senderType: "user",
1870+
content: "follow-up question",
1871+
contentType: "text",
1872+
createTime: 1710000001000,
1873+
},
1874+
]);
1875+
1876+
const cfg: ClawdbotConfig = {
1877+
channels: {
1878+
feishu: {
1879+
groups: {
1880+
"oc-group": {
1881+
requireMention: false,
1882+
groupSessionScope: "group_topic_sender",
1883+
},
1884+
},
1885+
},
1886+
},
1887+
} as ClawdbotConfig;
1888+
1889+
const event: FeishuMessageEvent = {
1890+
sender: {
1891+
sender_id: {
1892+
open_id: "ou-topic-user",
1893+
user_id: "user_topic_1",
1894+
},
1895+
},
1896+
message: {
1897+
message_id: "om_topic_followup_mixed_ids",
1898+
root_id: "om_topic_root",
1899+
chat_id: "oc-group",
1900+
chat_type: "group",
1901+
message_type: "text",
1902+
content: JSON.stringify({ text: "current turn" }),
1903+
},
1904+
};
1905+
1906+
await dispatchMessage({ cfg, event });
1907+
1908+
expect(mockFinalizeInboundContext).toHaveBeenCalledWith(
1909+
expect.objectContaining({
1910+
ThreadStarterBody: "root starter",
1911+
ThreadHistoryBody: "assistant reply\n\nfollow-up question",
1912+
ThreadLabel: "Feishu thread in oc-group",
1913+
MessageThreadId: "om_topic_root",
1914+
}),
1915+
);
1916+
});
1917+
18441918
it("does not dispatch twice for the same image message_id (concurrent dedupe)", async () => {
18451919
mockShouldComputeCommandAuthorized.mockReturnValue(false);
18461920

extensions/feishu/src/bot.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1406,15 +1406,25 @@ export async function handleFeishuMessage(params: {
14061406
accountId: account.accountId,
14071407
});
14081408
const senderScoped = groupSession?.groupSessionScope === "group_topic_sender";
1409-
const relevantMessages = senderScoped
1410-
? threadMessages.filter(
1411-
(msg) => msg.senderType === "app" || msg.senderId === ctx.senderOpenId,
1412-
)
1413-
: threadMessages;
1409+
const senderIds = new Set(
1410+
[ctx.senderOpenId, senderUserId]
1411+
.map((id) => id?.trim())
1412+
.filter((id): id is string => id !== undefined && id.length > 0),
1413+
);
1414+
const relevantMessages =
1415+
(senderScoped
1416+
? threadMessages.filter(
1417+
(msg) =>
1418+
msg.senderType === "app" ||
1419+
(msg.senderId !== undefined && senderIds.has(msg.senderId.trim())),
1420+
)
1421+
: threadMessages) ?? [];
14141422

14151423
const threadStarterBody = rootMsg?.content ?? relevantMessages[0]?.content;
1416-
const historyMessages =
1417-
rootMsg?.content || ctx.rootId ? relevantMessages : relevantMessages.slice(1);
1424+
const includeStarterInHistory = Boolean(rootMsg?.content || ctx.rootId);
1425+
const historyMessages = includeStarterInHistory
1426+
? relevantMessages
1427+
: relevantMessages.slice(1);
14181428
const historyParts = historyMessages.map((msg) => {
14191429
const role = msg.senderType === "app" ? "assistant" : "user";
14201430
return core.channel.reply.formatAgentEnvelope({

0 commit comments

Comments
 (0)