feat(feishu): add support for merge_forward message parsing (openclaw#28707) thanks @tsu-builds

Verified:
- pnpm build
- pnpm check
- pnpm test:macmini

Co-authored-by: tsu-builds <264409075+tsu-builds@users.noreply.github.com>
Co-authored-by: Tak Hoffman <781889+Takhoffman@users.noreply.github.com>
This commit is contained in:
tsu-builds
2026-02-28 10:57:18 +08:00
committed by GitHub
parent 8241145ada
commit f53ef73a2b
3 changed files with 265 additions and 0 deletions

View File

@@ -208,12 +208,119 @@ function parseMessageContent(content: string, messageType: string): string {
}
return "[Forwarded message]";
}
if (messageType === "merge_forward") {
// Return placeholder; actual content fetched asynchronously in handleFeishuMessage
return "[Merged and Forwarded Message - loading...]";
}
return content;
} catch {
return content;
}
}
/**
* Parse merge_forward message content and fetch sub-messages.
* Returns formatted text content of all sub-messages.
*/
function parseMergeForwardContent(params: {
content: string;
log?: (...args: any[]) => void;
}): string {
const { content, log } = params;
const maxMessages = 50;
// For merge_forward, the API returns all sub-messages in items array
// with upper_message_id pointing to the merge_forward message.
// The 'content' parameter here is actually the full API response items array as JSON.
log?.(`feishu: parsing merge_forward sub-messages from API response`);
let items: Array<{
message_id?: string;
msg_type?: string;
body?: { content?: string };
sender?: { id?: string };
upper_message_id?: string;
create_time?: string;
}>;
try {
items = JSON.parse(content);
} catch {
log?.(`feishu: merge_forward items parse failed`);
return "[Merged and Forwarded Message - parse error]";
}
if (!Array.isArray(items) || items.length === 0) {
return "[Merged and Forwarded Message - no sub-messages]";
}
// Filter to only sub-messages (those with upper_message_id, skip the merge_forward container itself)
const subMessages = items.filter((item) => item.upper_message_id);
if (subMessages.length === 0) {
return "[Merged and Forwarded Message - no sub-messages found]";
}
log?.(`feishu: merge_forward contains ${subMessages.length} sub-messages`);
// Sort by create_time
subMessages.sort((a, b) => {
const timeA = parseInt(a.create_time || "0", 10);
const timeB = parseInt(b.create_time || "0", 10);
return timeA - timeB;
});
// Format output
const lines: string[] = ["[Merged and Forwarded Messages]"];
const limitedMessages = subMessages.slice(0, maxMessages);
for (const item of limitedMessages) {
const msgContent = item.body?.content || "";
const msgType = item.msg_type || "text";
const formatted = formatSubMessageContent(msgContent, msgType);
lines.push(`- ${formatted}`);
}
if (subMessages.length > maxMessages) {
lines.push(`... and ${subMessages.length - maxMessages} more messages`);
}
return lines.join("\n");
}
/**
* Format sub-message content based on message type.
*/
function formatSubMessageContent(content: string, contentType: string): string {
try {
const parsed = JSON.parse(content);
switch (contentType) {
case "text":
return parsed.text || content;
case "post": {
const { textContent } = parsePostContent(content);
return textContent;
}
case "image":
return "[Image]";
case "file":
return `[File: ${parsed.file_name || "unknown"}]`;
case "audio":
return "[Audio]";
case "video":
return "[Video]";
case "sticker":
return "[Sticker]";
case "merge_forward":
return "[Nested Merged Forward]";
default:
return `[${contentType}]`;
}
} catch {
return content;
}
}
function checkBotMentioned(event: FeishuMessageEvent, botOpenId?: string): boolean {
if (!botOpenId) return false;
const mentions = event.message.mentions ?? [];
@@ -602,6 +709,38 @@ export async function handleFeishuMessage(params: {
const isGroup = ctx.chatType === "group";
const senderUserId = event.sender.sender_id.user_id?.trim() || undefined;
// Handle merge_forward messages: fetch full message via API then expand sub-messages
if (event.message.message_type === "merge_forward") {
log(
`feishu[${account.accountId}]: processing merge_forward message, fetching full content via API`,
);
try {
// Websocket event doesn't include sub-messages, need to fetch via API
// The API returns all sub-messages in the items array
const client = createFeishuClient(account);
const response = (await client.im.message.get({
path: { message_id: event.message.message_id },
})) as { code?: number; data?: { items?: unknown[] } };
if (response.code === 0 && response.data?.items && response.data.items.length > 0) {
log(
`feishu[${account.accountId}]: merge_forward API returned ${response.data.items.length} items`,
);
const expandedContent = parseMergeForwardContent({
content: JSON.stringify(response.data.items),
log,
});
ctx = { ...ctx, content: expandedContent };
} else {
log(`feishu[${account.accountId}]: merge_forward API returned no items`);
ctx = { ...ctx, content: "[Merged and Forwarded Message - could not fetch]" };
}
} catch (err) {
log(`feishu[${account.accountId}]: merge_forward fetch failed: ${String(err)}`);
ctx = { ...ctx, content: "[Merged and Forwarded Message - fetch error]" };
}
}
// Resolve sender display name (best-effort) so the agent can attribute messages correctly.
const senderResult = await resolveFeishuSenderName({
account,