fix(feishu): parse code blocks and share_chat messages (openclaw#28591) thanks @kevinWangSheng

Verified:
- pnpm install --frozen-lockfile
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: kevinWangSheng <118158941+kevinWangSheng@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
Shawn
2026-02-28 10:15:48 +08:00
committed by GitHub
parent 89669a33bd
commit da00ead652
3 changed files with 90 additions and 0 deletions

View File

@@ -22,6 +22,7 @@ Docs: https://docs.openclaw.ai
- Feishu/Typing backoff: re-throw Feishu typing add/remove rate-limit and quota errors (`429`, `99991400`, `99991403`) and detect SDK non-throwing backoff responses so the typing keepalive circuit breaker can stop retries instead of looping indefinitely. (#28494)
- Feishu/Probe status caching: cache successful `probeFeishu()` bot-info results for 10 minutes (bounded cache with per-account keying) to reduce repeated status/onboarding probe API calls, while bypassing cache for failures and exceptions. (#28907) Thanks @Glucksberg.
- Feishu/Opus media send type: send `.opus` attachments with `msg_type: "audio"` (instead of `"media"`) so Feishu voice messages deliver correctly while `.mp4` remains `msg_type: "media"` and documents remain `msg_type: "file"`. (#28269) Thanks @Glucksberg.
- 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.
- 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.
- Security/Feishu webhook ingress: bound unauthenticated webhook rate-limit state with stale-window pruning and a hard key cap to prevent unbounded pre-auth memory growth from rotating source keys. (#26050) Thanks @bmendonca3.
- Security/Compaction audit: remove the post-compaction audit injection message. (#28507) Thanks @fuller-stack-dev and @vincentkoc.

View File

@@ -36,6 +36,20 @@ function makePostEvent(content: unknown) {
};
}
function makeShareChatEvent(content: unknown) {
return {
sender: { sender_id: { user_id: "u1", open_id: "ou_sender" } },
message: {
message_id: "msg_1",
chat_id: "oc_chat1",
chat_type: "group",
message_type: "share_chat",
content: JSON.stringify(content),
mentions: [],
},
};
}
describe("parseFeishuMessageEvent mentionedBot", () => {
const BOT_OPEN_ID = "ou_bot_123";
@@ -127,4 +141,36 @@ describe("parseFeishuMessageEvent mentionedBot", () => {
const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
expect(ctx.mentionedBot).toBe(false);
});
it("preserves post code and code_block content", () => {
const event = makePostEvent({
content: [
[
{ tag: "text", text: "before " },
{ tag: "code", text: "inline()" },
],
[{ tag: "code_block", language: "ts", text: "const x = 1;" }],
],
});
const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
expect(ctx.content).toContain("before `inline()`");
expect(ctx.content).toContain("```ts\nconst x = 1;\n```");
});
it("uses share_chat body when available", () => {
const event = makeShareChatEvent({
body: "Merged and Forwarded Message",
share_chat_id: "sc_abc123",
});
const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
expect(ctx.content).toBe("Merged and Forwarded Message");
});
it("falls back to share_chat identifier when body is unavailable", () => {
const event = makeShareChatEvent({
share_chat_id: "sc_abc123",
});
const ctx = parseFeishuMessageEvent(event as any, "ou_bot_123");
expect(ctx.content).toBe("[Forwarded message: sc_abc123]");
});
});

View File

@@ -188,6 +188,26 @@ function parseMessageContent(content: string, messageType: string): string {
const { textContent } = parsePostContent(content);
return textContent;
}
if (messageType === "share_chat") {
// Preserve available summary text for merged/forwarded chat messages.
if (parsed && typeof parsed === "object") {
const share = parsed as {
body?: unknown;
summary?: unknown;
share_chat_id?: unknown;
};
if (typeof share.body === "string" && share.body.trim().length > 0) {
return share.body.trim();
}
if (typeof share.summary === "string" && share.summary.trim().length > 0) {
return share.summary.trim();
}
if (typeof share.share_chat_id === "string" && share.share_chat_id.trim().length > 0) {
return `[Forwarded message: ${share.share_chat_id.trim()}]`;
}
}
return "[Forwarded message]";
}
return content;
} catch {
return content;
@@ -293,6 +313,29 @@ function parsePostContent(content: string): {
if (imageKey) {
imageKeys.push(imageKey);
}
} else if (element.tag === "code") {
// Inline code
const code =
typeof element.text === "string"
? element.text
: typeof element.content === "string"
? element.content
: "";
if (code) {
textContent += `\`${code}\``;
}
} else if (element.tag === "code_block" || element.tag === "pre") {
// Multiline code block
const lang = typeof element.language === "string" ? element.language : "";
const code =
typeof element.text === "string"
? element.text
: typeof element.content === "string"
? element.content
: "";
if (code) {
textContent += `\n\`\`\`${lang}\n${code}\n\`\`\`\n`;
}
}
}
textContent += "\n";