Skip to content

Commit 4ad49de

Browse files
qianguqiangugreptile-apps[bot]牛牛Takhoffman
authored
feat(feishu): add parent/root inbound context for quote support (#18529)
* feat(feishu): add parentId and rootId to inbound context Add ParentMessageId and RootMessageId fields to Feishu inbound message context, enabling agents to: - Identify quoted/replied messages - Fetch original message content via Feishu API - Build proper message thread context The parent_id and root_id fields already exist in FeishuMessageContext but were not being passed to the agent's inbound context. Fixes: Allows proper handling of quoted card messages and message thread reconstruction. * feat(feishu): parse interactive card content in quoted messages Add support for extracting readable text from interactive card messages when fetching quoted/replied message content. Previously, only text messages were parsed. Now interactive cards (with div and markdown elements) are also converted to readable text. * 更新 bot.ts Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> * fix(types): add RootMessageId to MsgContext type definition * style: fix formatting in bot.ts * ci: trigger rebuild * ci: retry flaky tests * Feishu: add reply-context and interactive-quote regressions --------- Co-authored-by: qiangu <[email protected]> Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: 牛牛 <[email protected]> Co-authored-by: Tak Hoffman <[email protected]>
1 parent 9b39490 commit 4ad49de

File tree

6 files changed

+137
-0
lines changed

6 files changed

+137
-0
lines changed

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ Docs: https://docs.openclaw.ai
3131
- Feishu/Inbound sender fallback: fall back to `sender_id.user_id` when `sender_id.open_id` is missing on inbound events, and use ID-type-aware sender lookup so mobile-delivered messages keep stable sender identity/routing. (#26703) Thanks @NewdlDewdl.
3232
- Feishu/Inbound rich-text parsing: preserve `share_chat` payload summaries when available and add explicit parsing for rich-text `code`/`code_block`/`pre` tags so forwarded and code-heavy messages keep useful context in agent input. (#28591) Thanks @kevinWangSheng.
3333
- Feishu/Post markdown parsing: parse rich-text `post` payloads through a shared markdown-aware parser with locale-wrapper support, preserved mention/image metadata extraction, and inline/fenced code fidelity for agent input rendering. (#12755)
34+
- Feishu/Reply context metadata: include inbound `parent_id` and `root_id` as `ReplyToId`/`RootMessageId` in inbound context, and parse interactive-card quote bodies into readable text when fetching replied messages. (#18529)
3435
- Feishu/Post embedded media: extract `media` tags from inbound rich-text (`post`) messages and download embedded video/audio files alongside existing embedded-image handling, with regression coverage. (#21786) Thanks @laopuhuluwa.
3536
- Feishu/Local media sends: propagate `mediaLocalRoots` through Feishu outbound media sending into `loadWebMedia` so local path attachments work with post-CVE local-root enforcement. (#27884) Thanks @joelnishanth.
3637
- Feishu/Group sender allowlist fallback: add global `channels.feishu.groupSenderAllowFrom` sender authorization for group chats, with per-group `groups.<id>.allowFrom` precedence and regression coverage for allow/block/precedence behavior. (#29174) Thanks @1MoreBuild.

extensions/feishu/src/bot.test.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -287,6 +287,51 @@ describe("handleFeishuMessage command authorization", () => {
287287
expect(mockCreateFeishuClient).not.toHaveBeenCalled();
288288
});
289289

290+
it("propagates parent/root message ids into inbound context for reply reconstruction", async () => {
291+
mockGetMessageFeishu.mockResolvedValueOnce({
292+
messageId: "om_parent_001",
293+
chatId: "oc-group",
294+
content: "quoted content",
295+
contentType: "text",
296+
});
297+
298+
const cfg: ClawdbotConfig = {
299+
channels: {
300+
feishu: {
301+
enabled: true,
302+
dmPolicy: "open",
303+
},
304+
},
305+
} as ClawdbotConfig;
306+
307+
const event: FeishuMessageEvent = {
308+
sender: {
309+
sender_id: {
310+
open_id: "ou-replier",
311+
},
312+
},
313+
message: {
314+
message_id: "om_reply_001",
315+
root_id: "om_root_001",
316+
parent_id: "om_parent_001",
317+
chat_id: "oc-dm",
318+
chat_type: "p2p",
319+
message_type: "text",
320+
content: JSON.stringify({ text: "reply text" }),
321+
},
322+
};
323+
324+
await dispatchMessage({ cfg, event });
325+
326+
expect(mockFinalizeInboundContext).toHaveBeenCalledWith(
327+
expect.objectContaining({
328+
ReplyToId: "om_parent_001",
329+
RootMessageId: "om_root_001",
330+
ReplyToBody: "quoted content",
331+
}),
332+
);
333+
});
334+
290335
it("creates pairing request and drops unauthorized DMs in pairing mode", async () => {
291336
mockShouldComputeCommandAuthorized.mockReturnValue(false);
292337
mockReadAllowFromStore.mockResolvedValue([]);

extensions/feishu/src/bot.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1135,6 +1135,10 @@ export async function handleFeishuMessage(params: {
11351135
Body: combinedBody,
11361136
BodyForAgent: messageBody,
11371137
InboundHistory: inboundHistory,
1138+
// Quote/reply message support: use standard ReplyToId for parent,
1139+
// and pass root_id for thread reconstruction.
1140+
ReplyToId: ctx.parentId,
1141+
RootMessageId: ctx.rootId,
11381142
RawBody: ctx.content,
11391143
CommandBody: ctx.content,
11401144
From: feishuFrom,

extensions/feishu/src/send.test.ts

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
2+
import { beforeEach, describe, expect, it, vi } from "vitest";
3+
import { getMessageFeishu } from "./send.js";
4+
5+
const { mockClientGet, mockCreateFeishuClient, mockResolveFeishuAccount } = vi.hoisted(() => ({
6+
mockClientGet: vi.fn(),
7+
mockCreateFeishuClient: vi.fn(),
8+
mockResolveFeishuAccount: vi.fn(),
9+
}));
10+
11+
vi.mock("./client.js", () => ({
12+
createFeishuClient: mockCreateFeishuClient,
13+
}));
14+
15+
vi.mock("./accounts.js", () => ({
16+
resolveFeishuAccount: mockResolveFeishuAccount,
17+
}));
18+
19+
describe("getMessageFeishu", () => {
20+
beforeEach(() => {
21+
vi.clearAllMocks();
22+
mockResolveFeishuAccount.mockReturnValue({
23+
accountId: "default",
24+
configured: true,
25+
});
26+
mockCreateFeishuClient.mockReturnValue({
27+
im: {
28+
message: {
29+
get: mockClientGet,
30+
},
31+
},
32+
});
33+
});
34+
35+
it("extracts text content from interactive card elements", async () => {
36+
mockClientGet.mockResolvedValueOnce({
37+
code: 0,
38+
data: {
39+
items: [
40+
{
41+
message_id: "om_1",
42+
chat_id: "oc_1",
43+
msg_type: "interactive",
44+
body: {
45+
content: JSON.stringify({
46+
elements: [
47+
{ tag: "markdown", content: "hello markdown" },
48+
{ tag: "div", text: { content: "hello div" } },
49+
],
50+
}),
51+
},
52+
},
53+
],
54+
},
55+
});
56+
57+
const result = await getMessageFeishu({
58+
cfg: {} as ClawdbotConfig,
59+
messageId: "om_1",
60+
});
61+
62+
expect(result).toEqual(
63+
expect.objectContaining({
64+
messageId: "om_1",
65+
chatId: "oc_1",
66+
contentType: "interactive",
67+
content: "hello markdown\nhello div",
68+
}),
69+
);
70+
});
71+
});

extensions/feishu/src/send.ts

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,17 @@ export async function getMessageFeishu(params: {
7373
const parsed = JSON.parse(content);
7474
if (item.msg_type === "text" && parsed.text) {
7575
content = parsed.text;
76+
} else if (item.msg_type === "interactive" && parsed.elements) {
77+
// Extract text from interactive card
78+
const texts: string[] = [];
79+
for (const element of parsed.elements) {
80+
if (element.tag === "div" && element.text?.content) {
81+
texts.push(element.text.content);
82+
} else if (element.tag === "markdown" && element.content) {
83+
texts.push(element.content);
84+
}
85+
}
86+
content = texts.join("\n") || "[Interactive Card]";
7687
}
7788
} catch {
7889
// Keep raw content if parsing fails

src/auto-reply/templating.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ export type MsgContext = {
5454
MessageSidFirst?: string;
5555
MessageSidLast?: string;
5656
ReplyToId?: string;
57+
/**
58+
* Root message id for thread reconstruction (used by Feishu for root_id).
59+
* When a message is part of a thread, this is the id of the first message.
60+
*/
61+
RootMessageId?: string;
5762
/** Provider-specific full reply-to id when ReplyToId is a shortened alias. */
5863
ReplyToIdFull?: string;
5964
ReplyToBody?: string;

0 commit comments

Comments
 (0)