Skip to content

Commit 2f9535c

Browse files
openclaw-clownfish[bot]BunsDev
authored andcommitted
fix(feishu): reply inside P2P direct-message threads
1 parent 83267e9 commit 2f9535c

3 files changed

Lines changed: 92 additions & 12 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai
2727

2828
### Fixes
2929

30+
- Feishu: reply inside P2P direct-message threads by targeting the thread root with Feishu thread replies while preserving plain P2P quote replies. Fixes #38806; carries forward #38808. Thanks @LiaoyuanNing.
3031
- Agents/sessions: emit a terminal lifecycle backstop when embedded timeout/error turns return without `agent_end`, so Gateway sessions no longer stay stuck in `running` after failover surfaces a timeout. Fixes #74607. Thanks @millerc79.
3132
- Agents/Codex: bound embedded-run cleanup, trajectory flushing, and command-lane task timeouts after runtime failures, so Discord and other chat sessions return to idle instead of staying stuck in processing. Thanks @vincentkoc.
3233
- Heartbeat/exec: consume successful metadata-only async exec completions silently so Telegram and other chat surfaces no longer ask users for missing command logs after `No session found`. Fixes #74595. Thanks @gkoch02.

extensions/feishu/src/bot.test.ts

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2573,6 +2573,79 @@ describe("handleFeishuMessage command authorization", () => {
25732573
);
25742574
});
25752575

2576+
it("replies to P2P direct-message thread roots when thread_id is present", async () => {
2577+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
2578+
2579+
const cfg: ClawdbotConfig = {
2580+
channels: {
2581+
feishu: {
2582+
dmPolicy: "open",
2583+
},
2584+
},
2585+
} as ClawdbotConfig;
2586+
2587+
const event: FeishuMessageEvent = {
2588+
sender: { sender_id: { open_id: "ou-p2p-thread-user" } },
2589+
message: {
2590+
message_id: "om_p2p_thread_reply",
2591+
root_id: "om_p2p_thread_root",
2592+
thread_id: "omt_p2p_thread",
2593+
chat_id: "oc-dm",
2594+
chat_type: "p2p",
2595+
message_type: "text",
2596+
content: JSON.stringify({ text: "hello in dm thread" }),
2597+
},
2598+
};
2599+
2600+
await dispatchMessage({ cfg, event });
2601+
2602+
expect(mockCreateFeishuReplyDispatcher).toHaveBeenCalledWith(
2603+
expect.objectContaining({
2604+
replyToMessageId: "om_p2p_thread_root",
2605+
skipReplyToInMessages: false,
2606+
replyInThread: true,
2607+
rootId: "om_p2p_thread_root",
2608+
threadReply: true,
2609+
}),
2610+
);
2611+
});
2612+
2613+
it("preserves non-thread P2P root_id quote replies", async () => {
2614+
mockShouldComputeCommandAuthorized.mockReturnValue(false);
2615+
2616+
const cfg: ClawdbotConfig = {
2617+
channels: {
2618+
feishu: {
2619+
dmPolicy: "open",
2620+
},
2621+
},
2622+
} as ClawdbotConfig;
2623+
2624+
const event: FeishuMessageEvent = {
2625+
sender: { sender_id: { open_id: "ou-p2p-quote-user" } },
2626+
message: {
2627+
message_id: "om_p2p_quote_reply",
2628+
root_id: "om_p2p_quote_root",
2629+
chat_id: "oc-dm",
2630+
chat_type: "p2p",
2631+
message_type: "text",
2632+
content: JSON.stringify({ text: "plain quote reply" }),
2633+
},
2634+
};
2635+
2636+
await dispatchMessage({ cfg, event });
2637+
2638+
expect(mockCreateFeishuReplyDispatcher).toHaveBeenCalledWith(
2639+
expect.objectContaining({
2640+
replyToMessageId: "om_p2p_quote_reply",
2641+
skipReplyToInMessages: true,
2642+
replyInThread: false,
2643+
rootId: "om_p2p_quote_root",
2644+
threadReply: false,
2645+
}),
2646+
);
2647+
});
2648+
25762649
it("replies to topic root in topic-mode group with root_id", async () => {
25772650
mockShouldComputeCommandAuthorized.mockReturnValue(false);
25782651

extensions/feishu/src/bot.ts

Lines changed: 18 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -714,7 +714,10 @@ export async function handleFeishuMessage(params: {
714714
const feishuTo = isGroup ? `chat:${ctx.chatId}` : `user:${ctx.senderOpenId}`;
715715
const peerId = isGroup ? (groupSession?.peerId ?? ctx.chatId) : ctx.senderOpenId;
716716
const parentPeer = isGroup ? (groupSession?.parentPeer ?? null) : null;
717-
const replyInThread = isGroup ? (groupSession?.replyInThread ?? false) : false;
717+
const directThreadRootId = normalizeOptionalString(ctx.rootId);
718+
const directThreadReply =
719+
ctx.chatType === "p2p" && normalizeOptionalString(ctx.threadId) !== undefined;
720+
const replyInThread = isGroup ? (groupSession?.replyInThread ?? false) : directThreadReply;
718721
const feishuAcpConversationSupported =
719722
!isGroup ||
720723
groupSession?.groupSessionScope === "group_topic" ||
@@ -818,13 +821,15 @@ export async function handleFeishuMessage(params: {
818821
(groupSession?.groupSessionScope === "group_topic" ||
819822
groupSession?.groupSessionScope === "group_topic_sender")
820823
? (ctx.rootId ?? ctx.messageId)
821-
: ctx.messageId;
824+
: directThreadReply
825+
? (directThreadRootId ?? ctx.messageId)
826+
: ctx.messageId;
822827
await sendMessageFeishu({
823828
cfg: effectiveCfg,
824829
to: `chat:${ctx.chatId}`,
825830
text: `⚠️ Failed to initialize the configured ACP session for this Feishu conversation: ${ensured.error}`,
826831
replyToMessageId: replyTargetMessageId,
827-
replyInThread: isGroup ? (groupSession?.replyInThread ?? false) : false,
832+
replyInThread,
828833
accountId: account.accountId,
829834
}).catch((err) => {
830835
log(`feishu[${account.accountId}]: failed to send ACP init error reply: ${String(err)}`);
@@ -1224,13 +1229,14 @@ export async function handleFeishuMessage(params: {
12241229
const configReplyInThread =
12251230
isGroup &&
12261231
(groupConfig?.replyInThread ?? feishuCfg?.replyInThread ?? "disabled") === "enabled";
1227-
const replyTargetMessageId =
1228-
isTopicSession || configReplyInThread
1229-
? (ctx.rootId ??
1230-
ctx.replyTargetMessageId ??
1231-
(ctx.suppressReplyTarget ? undefined : ctx.messageId))
1232-
: (ctx.replyTargetMessageId ?? (ctx.suppressReplyTarget ? undefined : ctx.messageId));
1233-
const threadReply = isGroup ? (groupSession?.threadReply ?? false) : false;
1232+
const defaultReplyTargetMessageId =
1233+
ctx.replyTargetMessageId ?? (ctx.suppressReplyTarget ? undefined : ctx.messageId);
1234+
const replyTargetMessageId = directThreadReply
1235+
? (directThreadRootId ?? defaultReplyTargetMessageId)
1236+
: isTopicSession || configReplyInThread
1237+
? (ctx.rootId ?? defaultReplyTargetMessageId)
1238+
: defaultReplyTargetMessageId;
1239+
const threadReply = isGroup ? (groupSession?.threadReply ?? false) : directThreadReply;
12341240

12351241
if (broadcastAgents) {
12361242
// Cross-account dedup: in multi-account setups, Feishu delivers the same
@@ -1289,7 +1295,7 @@ export async function handleFeishuMessage(params: {
12891295
chatId: ctx.chatId,
12901296
allowReasoningPreview,
12911297
replyToMessageId: replyTargetMessageId,
1292-
skipReplyToInMessages: !isGroup,
1298+
skipReplyToInMessages: !isGroup && !directThreadReply,
12931299
replyInThread,
12941300
rootId: ctx.rootId,
12951301
threadReply,
@@ -1398,7 +1404,7 @@ export async function handleFeishuMessage(params: {
13981404
chatId: ctx.chatId,
13991405
allowReasoningPreview,
14001406
replyToMessageId: replyTargetMessageId,
1401-
skipReplyToInMessages: !isGroup,
1407+
skipReplyToInMessages: !isGroup && !directThreadReply,
14021408
replyInThread,
14031409
rootId: ctx.rootId,
14041410
threadReply,

0 commit comments

Comments
 (0)