feat(feishu): replace built-in SDK with community plugin
Replace the built-in Feishu SDK with the community-maintained clawdbot-feishu plugin by @m1heng. Changes: - Remove src/feishu/ directory (19 files) - Remove src/channels/plugins/outbound/feishu.ts - Remove src/channels/plugins/normalize/feishu.ts - Remove src/config/types.feishu.ts - Remove feishu exports from plugin-sdk/index.ts - Remove FeishuConfig from types.channels.ts New features in community plugin: - Document tools (read/create/edit Feishu docs) - Wiki tools (navigate/manage knowledge base) - Drive tools (folder/file management) - Bitable tools (read/write table records) - Permission tools (collaborator management) - Emoji reactions support - Typing indicators - Rich media support (bidirectional image/file transfer) - @mention handling - Skills for feishu-doc, feishu-wiki, feishu-drive, feishu-perm Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
356
extensions/feishu/src/send.ts
Normal file
356
extensions/feishu/src/send.ts
Normal file
@@ -0,0 +1,356 @@
|
||||
import type { ClawdbotConfig } from "openclaw/plugin-sdk";
|
||||
import type { MentionTarget } from "./mention.js";
|
||||
import type { FeishuConfig, FeishuSendResult } from "./types.js";
|
||||
import { createFeishuClient } from "./client.js";
|
||||
import { buildMentionedMessage, buildMentionedCardContent } from "./mention.js";
|
||||
import { getFeishuRuntime } from "./runtime.js";
|
||||
import { resolveReceiveIdType, normalizeFeishuTarget } from "./targets.js";
|
||||
|
||||
export type FeishuMessageInfo = {
|
||||
messageId: string;
|
||||
chatId: string;
|
||||
senderId?: string;
|
||||
senderOpenId?: string;
|
||||
content: string;
|
||||
contentType: string;
|
||||
createTime?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Get a message by its ID.
|
||||
* Useful for fetching quoted/replied message content.
|
||||
*/
|
||||
export async function getMessageFeishu(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
messageId: string;
|
||||
}): Promise<FeishuMessageInfo | null> {
|
||||
const { cfg, messageId } = params;
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
throw new Error("Feishu channel not configured");
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
|
||||
try {
|
||||
const response = (await client.im.message.get({
|
||||
path: { message_id: messageId },
|
||||
})) as {
|
||||
code?: number;
|
||||
msg?: string;
|
||||
data?: {
|
||||
items?: Array<{
|
||||
message_id?: string;
|
||||
chat_id?: string;
|
||||
msg_type?: string;
|
||||
body?: { content?: string };
|
||||
sender?: {
|
||||
id?: string;
|
||||
id_type?: string;
|
||||
sender_type?: string;
|
||||
};
|
||||
create_time?: string;
|
||||
}>;
|
||||
};
|
||||
};
|
||||
|
||||
if (response.code !== 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const item = response.data?.items?.[0];
|
||||
if (!item) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Parse content based on message type
|
||||
let content = item.body?.content ?? "";
|
||||
try {
|
||||
const parsed = JSON.parse(content);
|
||||
if (item.msg_type === "text" && parsed.text) {
|
||||
content = parsed.text;
|
||||
}
|
||||
} catch {
|
||||
// Keep raw content if parsing fails
|
||||
}
|
||||
|
||||
return {
|
||||
messageId: item.message_id ?? messageId,
|
||||
chatId: item.chat_id ?? "",
|
||||
senderId: item.sender?.id,
|
||||
senderOpenId: item.sender?.id_type === "open_id" ? item.sender?.id : undefined,
|
||||
content,
|
||||
contentType: item.msg_type ?? "text",
|
||||
createTime: item.create_time ? parseInt(item.create_time, 10) : undefined,
|
||||
};
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export type SendFeishuMessageParams = {
|
||||
cfg: ClawdbotConfig;
|
||||
to: string;
|
||||
text: string;
|
||||
replyToMessageId?: string;
|
||||
/** Mention target users */
|
||||
mentions?: MentionTarget[];
|
||||
};
|
||||
|
||||
function buildFeishuPostMessagePayload(params: { feishuCfg: FeishuConfig; messageText: string }): {
|
||||
content: string;
|
||||
msgType: string;
|
||||
} {
|
||||
const { messageText } = params;
|
||||
return {
|
||||
content: JSON.stringify({
|
||||
zh_cn: {
|
||||
content: [
|
||||
[
|
||||
{
|
||||
tag: "md",
|
||||
text: messageText,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
}),
|
||||
msgType: "post",
|
||||
};
|
||||
}
|
||||
|
||||
export async function sendMessageFeishu(
|
||||
params: SendFeishuMessageParams,
|
||||
): Promise<FeishuSendResult> {
|
||||
const { cfg, to, text, replyToMessageId, mentions } = params;
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
throw new Error("Feishu channel not configured");
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const receiveId = normalizeFeishuTarget(to);
|
||||
if (!receiveId) {
|
||||
throw new Error(`Invalid Feishu target: ${to}`);
|
||||
}
|
||||
|
||||
const receiveIdType = resolveReceiveIdType(receiveId);
|
||||
const tableMode = getFeishuRuntime().channel.text.resolveMarkdownTableMode({
|
||||
cfg,
|
||||
channel: "feishu",
|
||||
});
|
||||
|
||||
// Build message content (with @mention support)
|
||||
let rawText = text ?? "";
|
||||
if (mentions && mentions.length > 0) {
|
||||
rawText = buildMentionedMessage(mentions, rawText);
|
||||
}
|
||||
const messageText = getFeishuRuntime().channel.text.convertMarkdownTables(rawText, tableMode);
|
||||
|
||||
const { content, msgType } = buildFeishuPostMessagePayload({
|
||||
feishuCfg,
|
||||
messageText,
|
||||
});
|
||||
|
||||
if (replyToMessageId) {
|
||||
const response = await client.im.message.reply({
|
||||
path: { message_id: replyToMessageId },
|
||||
data: {
|
||||
content,
|
||||
msg_type: msgType,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.code !== 0) {
|
||||
throw new Error(`Feishu reply failed: ${response.msg || `code ${response.code}`}`);
|
||||
}
|
||||
|
||||
return {
|
||||
messageId: response.data?.message_id ?? "unknown",
|
||||
chatId: receiveId,
|
||||
};
|
||||
}
|
||||
|
||||
const response = await client.im.message.create({
|
||||
params: { receive_id_type: receiveIdType },
|
||||
data: {
|
||||
receive_id: receiveId,
|
||||
content,
|
||||
msg_type: msgType,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.code !== 0) {
|
||||
throw new Error(`Feishu send failed: ${response.msg || `code ${response.code}`}`);
|
||||
}
|
||||
|
||||
return {
|
||||
messageId: response.data?.message_id ?? "unknown",
|
||||
chatId: receiveId,
|
||||
};
|
||||
}
|
||||
|
||||
export type SendFeishuCardParams = {
|
||||
cfg: ClawdbotConfig;
|
||||
to: string;
|
||||
card: Record<string, unknown>;
|
||||
replyToMessageId?: string;
|
||||
};
|
||||
|
||||
export async function sendCardFeishu(params: SendFeishuCardParams): Promise<FeishuSendResult> {
|
||||
const { cfg, to, card, replyToMessageId } = params;
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
throw new Error("Feishu channel not configured");
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const receiveId = normalizeFeishuTarget(to);
|
||||
if (!receiveId) {
|
||||
throw new Error(`Invalid Feishu target: ${to}`);
|
||||
}
|
||||
|
||||
const receiveIdType = resolveReceiveIdType(receiveId);
|
||||
const content = JSON.stringify(card);
|
||||
|
||||
if (replyToMessageId) {
|
||||
const response = await client.im.message.reply({
|
||||
path: { message_id: replyToMessageId },
|
||||
data: {
|
||||
content,
|
||||
msg_type: "interactive",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.code !== 0) {
|
||||
throw new Error(`Feishu card reply failed: ${response.msg || `code ${response.code}`}`);
|
||||
}
|
||||
|
||||
return {
|
||||
messageId: response.data?.message_id ?? "unknown",
|
||||
chatId: receiveId,
|
||||
};
|
||||
}
|
||||
|
||||
const response = await client.im.message.create({
|
||||
params: { receive_id_type: receiveIdType },
|
||||
data: {
|
||||
receive_id: receiveId,
|
||||
content,
|
||||
msg_type: "interactive",
|
||||
},
|
||||
});
|
||||
|
||||
if (response.code !== 0) {
|
||||
throw new Error(`Feishu card send failed: ${response.msg || `code ${response.code}`}`);
|
||||
}
|
||||
|
||||
return {
|
||||
messageId: response.data?.message_id ?? "unknown",
|
||||
chatId: receiveId,
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateCardFeishu(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
messageId: string;
|
||||
card: Record<string, unknown>;
|
||||
}): Promise<void> {
|
||||
const { cfg, messageId, card } = params;
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
throw new Error("Feishu channel not configured");
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const content = JSON.stringify(card);
|
||||
|
||||
const response = await client.im.message.patch({
|
||||
path: { message_id: messageId },
|
||||
data: { content },
|
||||
});
|
||||
|
||||
if (response.code !== 0) {
|
||||
throw new Error(`Feishu card update failed: ${response.msg || `code ${response.code}`}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build a Feishu interactive card with markdown content.
|
||||
* Cards render markdown properly (code blocks, tables, links, etc.)
|
||||
*/
|
||||
export function buildMarkdownCard(text: string): Record<string, unknown> {
|
||||
return {
|
||||
config: {
|
||||
wide_screen_mode: true,
|
||||
},
|
||||
elements: [
|
||||
{
|
||||
tag: "markdown",
|
||||
content: text,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a message as a markdown card (interactive message).
|
||||
* This renders markdown properly in Feishu (code blocks, tables, bold/italic, etc.)
|
||||
*/
|
||||
export async function sendMarkdownCardFeishu(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
to: string;
|
||||
text: string;
|
||||
replyToMessageId?: string;
|
||||
/** Mention target users */
|
||||
mentions?: MentionTarget[];
|
||||
}): Promise<FeishuSendResult> {
|
||||
const { cfg, to, text, replyToMessageId, mentions } = params;
|
||||
// Build message content (with @mention support)
|
||||
let cardText = text;
|
||||
if (mentions && mentions.length > 0) {
|
||||
cardText = buildMentionedCardContent(mentions, text);
|
||||
}
|
||||
const card = buildMarkdownCard(cardText);
|
||||
return sendCardFeishu({ cfg, to, card, replyToMessageId });
|
||||
}
|
||||
|
||||
/**
|
||||
* Edit an existing text message.
|
||||
* Note: Feishu only allows editing messages within 24 hours.
|
||||
*/
|
||||
export async function editMessageFeishu(params: {
|
||||
cfg: ClawdbotConfig;
|
||||
messageId: string;
|
||||
text: string;
|
||||
}): Promise<void> {
|
||||
const { cfg, messageId, text } = params;
|
||||
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
||||
if (!feishuCfg) {
|
||||
throw new Error("Feishu channel not configured");
|
||||
}
|
||||
|
||||
const client = createFeishuClient(feishuCfg);
|
||||
const tableMode = getFeishuRuntime().channel.text.resolveMarkdownTableMode({
|
||||
cfg,
|
||||
channel: "feishu",
|
||||
});
|
||||
const messageText = getFeishuRuntime().channel.text.convertMarkdownTables(text ?? "", tableMode);
|
||||
|
||||
const { content, msgType } = buildFeishuPostMessagePayload({
|
||||
feishuCfg,
|
||||
messageText,
|
||||
});
|
||||
|
||||
const response = await client.im.message.update({
|
||||
path: { message_id: messageId },
|
||||
data: {
|
||||
msg_type: msgType,
|
||||
content,
|
||||
},
|
||||
});
|
||||
|
||||
if (response.code !== 0) {
|
||||
throw new Error(`Feishu message edit failed: ${response.msg || `code ${response.code}`}`);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user