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:
Yifeng Wang
2026-02-05 18:26:05 +08:00
committed by cpojer
parent 02842bef91
commit 2267d58afc
66 changed files with 5702 additions and 4486 deletions

View File

@@ -0,0 +1,156 @@
import type { ClawdbotConfig, RuntimeEnv, HistoryEntry } from "openclaw/plugin-sdk";
import * as Lark from "@larksuiteoapi/node-sdk";
import type { FeishuConfig } from "./types.js";
import { resolveFeishuCredentials } from "./accounts.js";
import { handleFeishuMessage, type FeishuMessageEvent, type FeishuBotAddedEvent } from "./bot.js";
import { createFeishuWSClient, createEventDispatcher } from "./client.js";
import { probeFeishu } from "./probe.js";
export type MonitorFeishuOpts = {
config?: ClawdbotConfig;
runtime?: RuntimeEnv;
abortSignal?: AbortSignal;
accountId?: string;
};
let currentWsClient: Lark.WSClient | null = null;
let botOpenId: string | undefined;
async function fetchBotOpenId(cfg: FeishuConfig): Promise<string | undefined> {
try {
const result = await probeFeishu(cfg);
return result.ok ? result.botOpenId : undefined;
} catch {
return undefined;
}
}
export async function monitorFeishuProvider(opts: MonitorFeishuOpts = {}): Promise<void> {
const cfg = opts.config;
if (!cfg) {
throw new Error("Config is required for Feishu monitor");
}
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
const creds = resolveFeishuCredentials(feishuCfg);
if (!creds) {
throw new Error("Feishu credentials not configured (appId, appSecret required)");
}
const log = opts.runtime?.log ?? console.log;
const error = opts.runtime?.error ?? console.error;
if (feishuCfg) {
botOpenId = await fetchBotOpenId(feishuCfg);
log(`feishu: bot open_id resolved: ${botOpenId ?? "unknown"}`);
}
const connectionMode = feishuCfg?.connectionMode ?? "websocket";
if (connectionMode === "websocket") {
return monitorWebSocket({
cfg,
feishuCfg: feishuCfg!,
runtime: opts.runtime,
abortSignal: opts.abortSignal,
});
}
log("feishu: webhook mode not implemented in monitor, use HTTP server directly");
}
async function monitorWebSocket(params: {
cfg: ClawdbotConfig;
feishuCfg: FeishuConfig;
runtime?: RuntimeEnv;
abortSignal?: AbortSignal;
}): Promise<void> {
const { cfg, feishuCfg, runtime, abortSignal } = params;
const log = runtime?.log ?? console.log;
const error = runtime?.error ?? console.error;
log("feishu: starting WebSocket connection...");
const wsClient = createFeishuWSClient(feishuCfg);
currentWsClient = wsClient;
const chatHistories = new Map<string, HistoryEntry[]>();
const eventDispatcher = createEventDispatcher(feishuCfg);
eventDispatcher.register({
"im.message.receive_v1": async (data) => {
try {
const event = data as unknown as FeishuMessageEvent;
await handleFeishuMessage({
cfg,
event,
botOpenId,
runtime,
chatHistories,
});
} catch (err) {
error(`feishu: error handling message event: ${String(err)}`);
}
},
"im.message.message_read_v1": async () => {
// Ignore read receipts
},
"im.chat.member.bot.added_v1": async (data) => {
try {
const event = data as unknown as FeishuBotAddedEvent;
log(`feishu: bot added to chat ${event.chat_id}`);
} catch (err) {
error(`feishu: error handling bot added event: ${String(err)}`);
}
},
"im.chat.member.bot.deleted_v1": async (data) => {
try {
const event = data as unknown as { chat_id: string };
log(`feishu: bot removed from chat ${event.chat_id}`);
} catch (err) {
error(`feishu: error handling bot removed event: ${String(err)}`);
}
},
});
return new Promise((resolve, reject) => {
const cleanup = () => {
if (currentWsClient === wsClient) {
currentWsClient = null;
}
};
const handleAbort = () => {
log("feishu: abort signal received, stopping WebSocket client");
cleanup();
resolve();
};
if (abortSignal?.aborted) {
cleanup();
resolve();
return;
}
abortSignal?.addEventListener("abort", handleAbort, { once: true });
try {
wsClient.start({
eventDispatcher,
});
log("feishu: WebSocket client started");
} catch (err) {
cleanup();
abortSignal?.removeEventListener("abort", handleAbort);
reject(err);
}
});
}
export function stopFeishuMonitor(): void {
if (currentWsClient) {
currentWsClient = null;
}
}