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>
233 lines
7.9 KiB
TypeScript
233 lines
7.9 KiB
TypeScript
import type { ChannelPlugin, ClawdbotConfig } from "openclaw/plugin-sdk";
|
|
import { DEFAULT_ACCOUNT_ID, PAIRING_APPROVED_MESSAGE } from "openclaw/plugin-sdk";
|
|
import type { ResolvedFeishuAccount, FeishuConfig } from "./types.js";
|
|
import { resolveFeishuAccount, resolveFeishuCredentials } from "./accounts.js";
|
|
import {
|
|
listFeishuDirectoryPeers,
|
|
listFeishuDirectoryGroups,
|
|
listFeishuDirectoryPeersLive,
|
|
listFeishuDirectoryGroupsLive,
|
|
} from "./directory.js";
|
|
import { feishuOnboardingAdapter } from "./onboarding.js";
|
|
import { feishuOutbound } from "./outbound.js";
|
|
import { resolveFeishuGroupToolPolicy } from "./policy.js";
|
|
import { probeFeishu } from "./probe.js";
|
|
import { sendMessageFeishu } from "./send.js";
|
|
import { normalizeFeishuTarget, looksLikeFeishuId } from "./targets.js";
|
|
|
|
const meta = {
|
|
id: "feishu",
|
|
label: "Feishu",
|
|
selectionLabel: "Feishu/Lark (飞书)",
|
|
docsPath: "/channels/feishu",
|
|
docsLabel: "feishu",
|
|
blurb: "飞书/Lark enterprise messaging.",
|
|
aliases: ["lark"],
|
|
order: 70,
|
|
} as const;
|
|
|
|
export const feishuPlugin: ChannelPlugin<ResolvedFeishuAccount> = {
|
|
id: "feishu",
|
|
meta: {
|
|
...meta,
|
|
},
|
|
pairing: {
|
|
idLabel: "feishuUserId",
|
|
normalizeAllowEntry: (entry) => entry.replace(/^(feishu|user|open_id):/i, ""),
|
|
notifyApproval: async ({ cfg, id }) => {
|
|
await sendMessageFeishu({
|
|
cfg,
|
|
to: id,
|
|
text: PAIRING_APPROVED_MESSAGE,
|
|
});
|
|
},
|
|
},
|
|
capabilities: {
|
|
chatTypes: ["direct", "group"],
|
|
media: true,
|
|
reactions: true,
|
|
threads: false,
|
|
polls: false,
|
|
nativeCommands: true,
|
|
blockStreaming: true,
|
|
},
|
|
agentPrompt: {
|
|
messageToolHints: () => [
|
|
"- Feishu targeting: omit `target` to reply to the current conversation (auto-inferred). Explicit targets: `user:open_id` or `chat:chat_id`.",
|
|
"- Feishu supports interactive cards for rich messages.",
|
|
],
|
|
},
|
|
groups: {
|
|
resolveToolPolicy: resolveFeishuGroupToolPolicy,
|
|
},
|
|
reload: { configPrefixes: ["channels.feishu"] },
|
|
configSchema: {
|
|
schema: {
|
|
type: "object",
|
|
additionalProperties: false,
|
|
properties: {
|
|
enabled: { type: "boolean" },
|
|
appId: { type: "string" },
|
|
appSecret: { type: "string" },
|
|
encryptKey: { type: "string" },
|
|
verificationToken: { type: "string" },
|
|
domain: {
|
|
oneOf: [
|
|
{ type: "string", enum: ["feishu", "lark"] },
|
|
{ type: "string", format: "uri", pattern: "^https://" },
|
|
],
|
|
},
|
|
connectionMode: { type: "string", enum: ["websocket", "webhook"] },
|
|
webhookPath: { type: "string" },
|
|
webhookPort: { type: "integer", minimum: 1 },
|
|
dmPolicy: { type: "string", enum: ["open", "pairing", "allowlist"] },
|
|
allowFrom: { type: "array", items: { oneOf: [{ type: "string" }, { type: "number" }] } },
|
|
groupPolicy: { type: "string", enum: ["open", "allowlist", "disabled"] },
|
|
groupAllowFrom: {
|
|
type: "array",
|
|
items: { oneOf: [{ type: "string" }, { type: "number" }] },
|
|
},
|
|
requireMention: { type: "boolean" },
|
|
historyLimit: { type: "integer", minimum: 0 },
|
|
dmHistoryLimit: { type: "integer", minimum: 0 },
|
|
textChunkLimit: { type: "integer", minimum: 1 },
|
|
chunkMode: { type: "string", enum: ["length", "newline"] },
|
|
mediaMaxMb: { type: "number", minimum: 0 },
|
|
renderMode: { type: "string", enum: ["auto", "raw", "card"] },
|
|
},
|
|
},
|
|
},
|
|
config: {
|
|
listAccountIds: () => [DEFAULT_ACCOUNT_ID],
|
|
resolveAccount: (cfg) => resolveFeishuAccount({ cfg }),
|
|
defaultAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
setAccountEnabled: ({ cfg, enabled }) => ({
|
|
...cfg,
|
|
channels: {
|
|
...cfg.channels,
|
|
feishu: {
|
|
...cfg.channels?.feishu,
|
|
enabled,
|
|
},
|
|
},
|
|
}),
|
|
deleteAccount: ({ cfg }) => {
|
|
const next = { ...cfg } as ClawdbotConfig;
|
|
const nextChannels = { ...cfg.channels };
|
|
delete (nextChannels as Record<string, unknown>).feishu;
|
|
if (Object.keys(nextChannels).length > 0) {
|
|
next.channels = nextChannels;
|
|
} else {
|
|
delete next.channels;
|
|
}
|
|
return next;
|
|
},
|
|
isConfigured: (_account, cfg) =>
|
|
Boolean(resolveFeishuCredentials(cfg.channels?.feishu as FeishuConfig | undefined)),
|
|
describeAccount: (account) => ({
|
|
accountId: account.accountId,
|
|
enabled: account.enabled,
|
|
configured: account.configured,
|
|
}),
|
|
resolveAllowFrom: ({ cfg }) =>
|
|
(cfg.channels?.feishu as FeishuConfig | undefined)?.allowFrom ?? [],
|
|
formatAllowFrom: ({ allowFrom }) =>
|
|
allowFrom
|
|
.map((entry) => String(entry).trim())
|
|
.filter(Boolean)
|
|
.map((entry) => entry.toLowerCase()),
|
|
},
|
|
security: {
|
|
collectWarnings: ({ cfg }) => {
|
|
const feishuCfg = cfg.channels?.feishu as FeishuConfig | undefined;
|
|
const defaultGroupPolicy = (
|
|
cfg.channels as Record<string, { groupPolicy?: string }> | undefined
|
|
)?.defaults?.groupPolicy;
|
|
const groupPolicy = feishuCfg?.groupPolicy ?? defaultGroupPolicy ?? "allowlist";
|
|
if (groupPolicy !== "open") return [];
|
|
return [
|
|
`- Feishu groups: groupPolicy="open" allows any member to trigger (mention-gated). Set channels.feishu.groupPolicy="allowlist" + channels.feishu.groupAllowFrom to restrict senders.`,
|
|
];
|
|
},
|
|
},
|
|
setup: {
|
|
resolveAccountId: () => DEFAULT_ACCOUNT_ID,
|
|
applyAccountConfig: ({ cfg }) => ({
|
|
...cfg,
|
|
channels: {
|
|
...cfg.channels,
|
|
feishu: {
|
|
...cfg.channels?.feishu,
|
|
enabled: true,
|
|
},
|
|
},
|
|
}),
|
|
},
|
|
onboarding: feishuOnboardingAdapter,
|
|
messaging: {
|
|
normalizeTarget: normalizeFeishuTarget,
|
|
targetResolver: {
|
|
looksLikeId: looksLikeFeishuId,
|
|
hint: "<chatId|user:openId|chat:chatId>",
|
|
},
|
|
},
|
|
directory: {
|
|
self: async () => null,
|
|
listPeers: async ({ cfg, query, limit }) => listFeishuDirectoryPeers({ cfg, query, limit }),
|
|
listGroups: async ({ cfg, query, limit }) => listFeishuDirectoryGroups({ cfg, query, limit }),
|
|
listPeersLive: async ({ cfg, query, limit }) =>
|
|
listFeishuDirectoryPeersLive({ cfg, query, limit }),
|
|
listGroupsLive: async ({ cfg, query, limit }) =>
|
|
listFeishuDirectoryGroupsLive({ cfg, query, limit }),
|
|
},
|
|
outbound: feishuOutbound,
|
|
status: {
|
|
defaultRuntime: {
|
|
accountId: DEFAULT_ACCOUNT_ID,
|
|
running: false,
|
|
lastStartAt: null,
|
|
lastStopAt: null,
|
|
lastError: null,
|
|
port: null,
|
|
},
|
|
buildChannelSummary: ({ snapshot }) => ({
|
|
configured: snapshot.configured ?? false,
|
|
running: snapshot.running ?? false,
|
|
lastStartAt: snapshot.lastStartAt ?? null,
|
|
lastStopAt: snapshot.lastStopAt ?? null,
|
|
lastError: snapshot.lastError ?? null,
|
|
port: snapshot.port ?? null,
|
|
probe: snapshot.probe,
|
|
lastProbeAt: snapshot.lastProbeAt ?? null,
|
|
}),
|
|
probeAccount: async ({ cfg }) =>
|
|
await probeFeishu(cfg.channels?.feishu as FeishuConfig | undefined),
|
|
buildAccountSnapshot: ({ account, runtime, probe }) => ({
|
|
accountId: account.accountId,
|
|
enabled: account.enabled,
|
|
configured: account.configured,
|
|
running: runtime?.running ?? false,
|
|
lastStartAt: runtime?.lastStartAt ?? null,
|
|
lastStopAt: runtime?.lastStopAt ?? null,
|
|
lastError: runtime?.lastError ?? null,
|
|
port: runtime?.port ?? null,
|
|
probe,
|
|
}),
|
|
},
|
|
gateway: {
|
|
startAccount: async (ctx) => {
|
|
const { monitorFeishuProvider } = await import("./monitor.js");
|
|
const feishuCfg = ctx.cfg.channels?.feishu as FeishuConfig | undefined;
|
|
const port = feishuCfg?.webhookPort ?? null;
|
|
ctx.setStatus({ accountId: ctx.accountId, port });
|
|
ctx.log?.info(`starting feishu provider (mode: ${feishuCfg?.connectionMode ?? "websocket"})`);
|
|
return monitorFeishuProvider({
|
|
config: ctx.cfg,
|
|
runtime: ctx.runtime,
|
|
abortSignal: ctx.abortSignal,
|
|
accountId: ctx.accountId,
|
|
});
|
|
},
|
|
},
|
|
};
|