This commit is contained in:
tomatoxman 2026-01-30 23:13:05 +08:00 committed by GitHub
commit 9c0eb08e2d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 901 additions and 0 deletions

143
extensions/feishu/README.md Normal file
View File

@ -0,0 +1,143 @@
# Feishu Extension for Clawdbot
This extension allows Clawdbot to integrate with Feishu (Lark), enabling it to send and receive messages within your Feishu organization.
## Configuration
To use this extension, you need to configure it with credentials from the Feishu Open Platform.
### Prerequisites
1. A Feishu (Lark) account and an organization.
2. Access to the [Feishu Open Platform](https://open.feishu.cn/app?lang=en-US).
### Quick Start
You can interactively configure this extension using the CLI:
```bash
pnpm clawdbot channels add feishu
```
This wizard will guide you through entering the required credentials.
### Features
* **Send & Receive Messages**: Supports sending and receiving **Text** messages in direct chats and group chats.
* *Note: Other message types (images, files, etc.) may be displayed as generic placeholders.*
* **Multi-Account Support**: Configure multiple Feishu bots/accounts.
### Step-by-Step Configuration Guide
1. **Create a Feishu Application:**
* Log in to the [Feishu Open Platform](https://open.feishu.cn/app?lang=en-US).
* Create a specific "Enterprise Self-Built App" for your bot.
2. **Get App Credentials:**
* Navigate to **Credentials & Basic Info**.
* Copy the **App ID** and **App Secret**. These correspond to `appId` and `appSecret` in the configuration.
3. **Configure Event Subscriptions:**
* Navigate to **Event Subscriptions**.
* Set the **Encrypt Key** (Optional, but recommended).
* Set the **Verification Token** (Optional).
* Set the Request URL to your bot's endpoint (e.g., `https://your-bot-domain.com/api/feishu`).
* **Add Events**: Search for and add the following event:
* `im.message.receive_v1` (Receive messages)
4. **Add Permissions:**
* Navigate to **Permissions & Scopes**.
* Add the necessary permissions:
* `im:message` (Access messages)
* `im:message:send_as_bot` (Send messages as bot)
* `im:chat` (Access group chats)
* **Important**: Create and publish a version of your app to apply these permissions.
5. **Enable Bot Capability:**
* Navigate to **App Capabilities** -> **Bot**.
* Enable the bot capability.
### Configuration Example
Add the following to your `clawdbot` configuration (e.g., in `clawdbot.config.json` or via environment variables):
```json
{
"extensions": {
"feishu": {
"appId": "cli_...",
"appSecret": "...",
"encryptKey": "...", // Optional: Required if encryption is enabled
"verificationToken": "..." // Optional: Required for event verification
}
}
}
```
> [!NOTE]
> `encryptKey` and `verificationToken` are **optional** for basic bot functionality (sending messages). However, they are **required** if you want to:
> * Receive events securely (verify the source).
> * Have enabled **Encrypt Key** in the Feishu Event Subscriptions settings.
### Multi-Account Configuration
If you need to configure multiple Feishu bots, you can use the accounts structure:
```json
{
"channels": {
"feishu": {
"enabled": true,
"accounts": {
"default": {
"enabled": true,
"appId": "cli_xxx",
"appSecret": "xxx",
"encryptKey": "xxx",
"verificationToken": "xxx"
},
"team-bot": {
"enabled": true,
"name": "Team Bot",
"appId": "cli_yyy",
"appSecret": "yyy",
"encryptKey": "yyy",
"verificationToken": "yyy"
}
}
}
}
}
```
## Troubleshooting
### Bot not receiving messages
1. **Check Event Subscription URL**: Ensure the Request URL is correctly configured and accessible from Feishu servers.
2. **Verify Event Subscription**: Make sure `im.message.receive_v1` event is added and the app version is published.
3. **Check Permissions**: Ensure all required permissions are granted and the app version is published.
4. **Review Logs**: Check Clawdbot logs for connection errors or event processing issues.
### Authentication errors
1. **Verify Credentials**: Double-check that `appId` and `appSecret` are correct.
2. **Check App Status**: Ensure the app is enabled and not suspended in Feishu Open Platform.
### Encryption/Verification errors
1. **Match Configuration**: Ensure `encryptKey` and `verificationToken` in your config match exactly what's set in Feishu Event Subscriptions.
2. **Optional Fields**: If you haven't enabled encryption in Feishu, you can leave these fields empty.
## Current Limitations
* **Message Types**: Currently only **text messages** are fully supported. Other types (images, files, cards) will be displayed as generic placeholders.
* **Reactions**: Message reactions are not yet supported.
* **Threads**: Message threads are not yet supported.
* **Media Upload**: Sending images/files is not yet implemented.
## Resources
* [Feishu Open Platform Documentation](https://open.feishu.cn/document/home/index)
* [Feishu Bot Development Guide](https://open.feishu.cn/document/home/develop-a-bot-in-5-minutes/create-an-app)

View File

@ -0,0 +1,24 @@
{
"id": "feishu",
"channels": [
"feishu"
],
"configSchema": {
"type": "object",
"additionalProperties": false,
"properties": {
"appId": {
"type": "string"
},
"appSecret": {
"type": "string"
},
"encryptKey": {
"type": "string"
},
"verificationToken": {
"type": "string"
}
}
}
}

View File

@ -0,0 +1,18 @@
import type { ClawdbotPluginApi } from "clawdbot/plugin-sdk";
import { emptyPluginConfigSchema } from "clawdbot/plugin-sdk";
import { feishuDock, feishuPlugin } from "./src/channel.js";
import { setFeishuRuntime } from "./src/runtime.js";
const plugin = {
id: "feishu",
name: "Feishu",
description: "Clawdbot Feishu (Lark) channel plugin",
configSchema: emptyPluginConfigSchema(),
register(api: ClawdbotPluginApi) {
setFeishuRuntime(api.runtime);
api.registerChannel({ plugin: feishuPlugin, dock: feishuDock });
},
};
export default plugin;

View File

@ -0,0 +1,38 @@
{
"name": "@moltbot/feishu",
"version": "2026.1.27",
"type": "module",
"description": "Clawdbot Feishu (Lark) channel plugin",
"clawdbot": {
"extensions": [
"./index.ts"
],
"channel": {
"id": "feishu",
"label": "Feishu (Lark)",
"selectionLabel": "Feishu / Lark (WebSocket)",
"detailLabel": "Feishu / Lark",
"docsPath": "/channels/feishu",
"docsLabel": "feishu",
"blurb": "Feishu/Lark bot via WebSocket.",
"aliases": [
"lark"
],
"order": 56
},
"install": {
"npmSpec": "@moltbot/feishu",
"localPath": "extensions/feishu",
"defaultChoice": "npm"
}
},
"dependencies": {
"@larksuiteoapi/node-sdk": "^1.26.0"
},
"devDependencies": {
"clawdbot": "workspace:*"
},
"peerDependencies": {
"clawdbot": ">=2026.1.26"
}
}

View File

@ -0,0 +1,56 @@
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
import type { FeishuConfig, FeishuAccount } from "./types.js";
const DEFAULT_ACCOUNT_ID = "default";
export function resolveFeishuAccount(params: {
cfg: ClawdbotConfig;
accountId?: string;
}): FeishuAccount {
const { cfg, accountId = DEFAULT_ACCOUNT_ID } = params;
// Type assertion to access channel specific config
// In a real plugin structure, config is typed, but here we access structure dynamically
const feishuCfg = (cfg.channels as any)?.feishu;
// Config can be at root of feishu block (default) or in accounts map
const defaults = feishuCfg;
const account = feishuCfg?.accounts?.[accountId];
return {
accountId,
name: account?.name ?? accountId,
enabled: account?.enabled ?? defaults?.enabled ?? true,
// Merge defaults with account specific overrides
config: {
appId: account?.appId ?? defaults?.appId,
appSecret: account?.appSecret ?? defaults?.appSecret,
encryptKey: account?.encryptKey ?? defaults?.encryptKey,
verificationToken: account?.verificationToken ?? defaults?.verificationToken,
} as FeishuConfig,
};
}
export function listFeishuAccountIds(cfg: ClawdbotConfig): string[] {
const feishuCfg = (cfg.channels as any)?.feishu;
if (!feishuCfg) return [];
const ids = new Set<string>();
// If base fields exist, "default" is an account
if (feishuCfg.appId) {
ids.add(DEFAULT_ACCOUNT_ID);
}
// Add explicit accounts
if (feishuCfg.accounts) {
Object.keys(feishuCfg.accounts).forEach(id => ids.add(id));
}
return Array.from(ids);
}
export function isFeishuConfigured(account: FeishuAccount): boolean {
return Boolean(account.config.appId && account.config.appSecret);
}

View File

@ -0,0 +1,174 @@
import type {
ChannelDock,
ChannelPlugin,
ClawdbotConfig,
} from "clawdbot/plugin-sdk";
import {
applyAccountNameToChannelSection,
buildChannelConfigSchema,
DEFAULT_ACCOUNT_ID,
emptyPluginConfigSchema,
migrateBaseNameToDefaultAccount,
normalizeAccountId,
} from "clawdbot/plugin-sdk";
import type { FeishuAccount, FeishuConfig } from "./types.js";
import { startFeishuMonitor } from "./monitor.js";
import { sendFeishuMessage } from "./client.js";
import { feishuOnboardingAdapter } from "./onboarding.js";
import { listFeishuAccountIds, resolveFeishuAccount, isFeishuConfigured } from "./accounts.js";
// Dock Definition
export const feishuDock: ChannelDock = {
id: "feishu",
capabilities: {
chatTypes: ["direct", "group"],
reactions: false, // Pending implementation
media: false, // Pending implementation
threads: false, // Pending implementation
blockStreaming: true,
},
outbound: { textChunkLimit: 2000 }, // Feishu limit is usually ~4k chars, safely 2k
config: {
resolveAllowFrom: () => [],
formatAllowFrom: () => [],
}
};
// Plugin Definition
export const feishuPlugin: ChannelPlugin<FeishuAccount> = {
id: "feishu",
meta: {
id: "feishu",
label: "Feishu",
blurb: "Feishu/Lark Workspace",
},
onboarding: feishuOnboardingAdapter,
capabilities: {
chatTypes: ["direct", "group"],
reactions: false,
media: false,
threads: false,
nativeCommands: false,
blockStreaming: true,
},
configSchema: emptyPluginConfigSchema(),
config: {
listAccountIds: (cfg) => listFeishuAccountIds(cfg as ClawdbotConfig),
resolveAccount: (cfg, accountId) => resolveFeishuAccount({ cfg: cfg as ClawdbotConfig, accountId }),
defaultAccountId: () => "default",
isConfigured: (account) => isFeishuConfigured(account),
describeAccount: (account) => ({
accountId: account.accountId,
name: account.name,
enabled: account.enabled,
configured: Boolean(account.config.appId && account.config.appSecret),
}),
},
gateway: {
startAccount: async (ctx) => {
ctx.log?.info(`[${ctx.account.accountId}] Starting Feishu monitor...`);
const monitor = await startFeishuMonitor({
account: ctx.account,
config: ctx.cfg as ClawdbotConfig,
runtime: ctx.runtime,
statusSink: (patch) => ctx.setStatus({ accountId: ctx.account.accountId, ...patch }),
});
ctx.setStatus({ accountId: ctx.account.accountId, running: true });
return () => {
monitor.stop().catch(console.error);
ctx.setStatus({ accountId: ctx.account.accountId, running: false });
};
},
},
messaging: {
outbound: {
sendText: async ({ cfg, to, text, accountId }: { cfg: ClawdbotConfig, to: string, text: string, accountId?: string }) => {
const account = feishuPlugin.config.resolveAccount(cfg, accountId || "default");
const res = await sendFeishuMessage({
account,
receiveId: to,
msgType: "text",
content: JSON.stringify({ text }), // Feishu content is JSON string
});
return {
channel: "feishu",
messageId: res?.message_id,
chatId: to,
};
}
}
},
setup: {
resolveAccountId: ({ accountId }: { accountId: string }) => normalizeAccountId(accountId),
applyAccountName: ({ cfg, accountId, name }: { cfg: ClawdbotConfig, accountId: string, name: string }) =>
applyAccountNameToChannelSection({
cfg: cfg as ClawdbotConfig,
channelKey: "feishu",
accountId,
name,
}),
validateInput: ({ accountId, input }: { accountId: string, input: any }) => {
if (!input.appId || !input.appSecret) {
return "Feishu requires --app-id and --app-secret.";
}
return null;
},
applyAccountConfig: ({ cfg, accountId, input }: { cfg: ClawdbotConfig, accountId: string, input: any }) => {
const namedConfig = applyAccountNameToChannelSection({
cfg: cfg as ClawdbotConfig,
channelKey: "feishu",
accountId,
name: input.name,
});
const next = accountId !== DEFAULT_ACCOUNT_ID
? migrateBaseNameToDefaultAccount({
cfg: namedConfig as ClawdbotConfig,
channelKey: "feishu",
})
: namedConfig;
const configPatch = {
...(input.appId ? { appId: input.appId } : {}),
...(input.appSecret ? { appSecret: input.appSecret } : {}),
...(input.encryptKey ? { encryptKey: input.encryptKey } : {}),
...(input.verificationToken ? { verificationToken: input.verificationToken } : {}),
};
if (accountId === DEFAULT_ACCOUNT_ID) {
return {
...next,
channels: {
...next.channels,
"feishu": {
...(next.channels?.["feishu"] ?? {}),
enabled: true,
...configPatch,
},
},
} as ClawdbotConfig;
}
return {
...next,
channels: {
...next.channels,
"feishu": {
...(next.channels?.["feishu"] ?? {}),
enabled: true,
accounts: {
...(next.channels?.["feishu"]?.accounts ?? {}),
[accountId]: {
...(next.channels?.["feishu"]?.accounts?.[accountId] ?? {}),
enabled: true,
...configPatch,
},
},
},
},
} as ClawdbotConfig;
},
}
};

View File

@ -0,0 +1,70 @@
import * as lark from "@larksuiteoapi/node-sdk";
import type { FeishuAccount } from "./types.js";
export function createFeishuClient(account: FeishuAccount) {
if (!account.config.appId || !account.config.appSecret) {
throw new Error("Feishu appId and appSecret are required");
}
return new lark.Client({
appId: account.config.appId,
appSecret: account.config.appSecret,
disableTokenCache: false,
});
}
export async function sendFeishuMessage(params: {
account: FeishuAccount;
receiveId: string;
receiveIdType?: "open_id" | "user_id" | "union_id" | "email" | "chat_id";
msgType: "text" | "post" | "image" | "interactive" | "share_chat" | "share_user" | "audio" | "media" | "file" | "sticker";
content: string;
}) {
const { account, receiveId, receiveIdType = "chat_id", msgType, content } = params;
const client = createFeishuClient(account);
const response = await client.im.message.create({
params: {
receive_id_type: receiveIdType,
},
data: {
receive_id: receiveId,
msg_type: msgType,
content: content,
},
});
if (response.code !== 0) {
throw new Error(`Feishu send message failed: ${response.msg} (code: ${response.code}, logId: ${response.log_id})`);
}
return response.data;
}
export async function uploadFeishuImage(params: {
account: FeishuAccount;
imagePath: string;
imageType: "message";
}) {
const { account, imagePath, imageType } = params;
const client = createFeishuClient(account);
// Note: SDK wrapper might handle reading file, or we pass stream.
// Using standard fs.createReadStream if SDK supports it.
// Checking SDK docs or type definition would be ideal, but assuming standard node stream support for now.
const fs = await import("node:fs");
const file = fs.createReadStream(imagePath);
const response = await client.im.image.create({
data: {
image_type: imageType,
image: file,
}
});
if (response.code !== 0) {
throw new Error(`Feishu upload image failed: ${response.msg} (code: ${response.code})`);
}
return response.data;
}

View File

@ -0,0 +1,163 @@
import * as lark from "@larksuiteoapi/node-sdk";
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
import { createFeishuClient, sendFeishuMessage } from "./client.js";
import type { FeishuAccount, FeishuMessageEvent } from "./types.js";
import { getFeishuRuntime } from "./runtime.js";
export type FeishuRuntimeEnv = {
log?: (message: string) => void;
error?: (message: string) => void;
};
export async function startFeishuMonitor(params: {
account: FeishuAccount;
config: ClawdbotConfig;
runtime: FeishuRuntimeEnv;
statusSink?: (patch: { lastInboundAt?: number; lastOutboundAt?: number }) => void;
}) {
const { account, config, runtime, statusSink } = params;
// Feishu WS Client
const client = new lark.WSClient({
appId: account.config.appId || "",
appSecret: account.config.appSecret || "",
loggerLevel: 2, // Info
logger: {
// Adaptation for Logger
debug: () => { },
info: (msg) => runtime.log?.(`[feishu-sdk] ${msg}`),
warn: (msg) => runtime.log?.(`[feishu-sdk] WARN: ${msg}`),
error: (msg) => runtime.error?.(`[feishu-sdk] ERROR: ${msg}`),
}
});
// Event Dispatcher
const eventDispatcher = new lark.EventDispatcher({
encryptKey: account.config.encryptKey || "",
verificationToken: account.config.verificationToken || "",
})
eventDispatcher.register({
"im.message.receive_v1": async (data) => {
try {
// Feishu SDK EventDispatcher unpacks the payload.
// 'data' IS the event object (containing message, sender, etc.)
const event = data as FeishuMessageEvent;
const message = event.message;
const sender = event.sender;
if (!message || !sender) {
runtime.log?.(`[feishu] Received incomplete message event`);
return;
}
const chatId = message.chat_id;
const messageId = message.message_id;
const senderId = sender.sender_id.user_id || sender.sender_id.open_id || sender.sender_id.union_id;
runtime.log?.(`[feishu] Received message ${messageId} from ${chatId}`);
let text = "";
let rawBody = "";
// Handle message type compatibility (SDK vs API raw)
const msgType = message.message_type || (message as any).msg_type;
if (msgType === "text") {
try {
const content = JSON.parse(message.content);
text = content.text;
rawBody = content.text;
} catch {
text = "[Invalid JSON Content]";
rawBody = message.content;
}
} else {
rawBody = `[${msgType}]`;
text = rawBody;
}
const core = getFeishuRuntime();
if (!core) {
runtime.error?.("[feishu] Core runtime not available during message processing");
return;
}
const fromLabel = `feishu:${senderId}`;
const ctxPayload = core.channel.reply.finalizeInboundContext({
Body: text,
RawBody: rawBody,
CommandBody: text,
From: fromLabel,
To: `feishu:${chatId}`,
SessionKey: `feishu:${chatId}`,
AccountId: account.accountId,
ChatType: message.chat_type === "group" ? "channel" : "direct",
ConversationLabel: message.chat_type === "group" ? `Group ${chatId}` : `User ${senderId}`,
SenderId: senderId,
SenderName: "FeishuUser",
Provider: "feishu",
Surface: "feishu",
MessageSid: messageId,
MessageSidFull: messageId,
OriginatingChannel: "feishu",
OriginatingTo: `feishu:${chatId}`,
});
await core.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
ctx: ctxPayload,
cfg: config,
dispatcherOptions: {
deliver: async (payload) => {
if (payload.text) {
await sendFeishuMessage({
account,
receiveId: chatId,
msgType: "text",
content: JSON.stringify({ text: payload.text }),
});
}
},
onError: (err) => {
runtime.error?.(`[feishu] Reply failed: ${err}`);
}
}
});
statusSink?.({ lastInboundAt: Date.now() });
} catch (err) {
runtime.error?.(`[feishu] Process message failed: ${err}`);
}
},
"im.message.message_read_v1": async (data) => {
// Optional: Handle read receipts or just log for debug
// runtime.log?.(`[feishu] Message read event received: ${JSON.stringify(data)}`);
}
});
try {
await client.start({ eventDispatcher });
runtime.log?.(`[feishu] WebSocket client started for account ${account.accountId}`);
return {
stop: async () => {
try {
// WSClient may have close/stop method - attempt graceful shutdown
if (typeof (client as any).close === "function") {
await (client as any).close();
} else if (typeof (client as any).stop === "function") {
await (client as any).stop();
}
runtime.log?.(`[feishu] WebSocket client stopped for account ${account.accountId}`);
} catch (err) {
runtime.error?.(`[feishu] Error stopping WebSocket client: ${err}`);
}
}
};
} catch (err) {
runtime.error?.(`[feishu] Failed to start WebSocket client: ${err}`);
throw err;
}
}

View File

@ -0,0 +1,167 @@
import type { ClawdbotConfig } from "clawdbot/plugin-sdk";
import {
formatDocsLink,
promptAccountId,
normalizeAccountId,
migrateBaseNameToDefaultAccount,
DEFAULT_ACCOUNT_ID,
type ChannelOnboardingAdapter,
type ChannelOnboardingConfigureContext,
type ChannelOnboardingResult,
type ChannelOnboardingStatus,
type ChannelOnboardingStatusContext,
type WizardPrompter,
} from "clawdbot/plugin-sdk";
import { listFeishuAccountIds, resolveFeishuAccount, isFeishuConfigured } from "./accounts.js";
const channel = "feishu" as const;
function applyFeishuConfig(params: {
cfg: ClawdbotConfig;
accountId: string;
patch: Record<string, unknown>;
}): ClawdbotConfig {
const { cfg, accountId, patch } = params;
// Helper to ensure structure exists
const ensureAccount = (config: ClawdbotConfig, accId: string) => {
const next = { ...config };
next.channels = { ...(next.channels ?? {}) };
next.channels.feishu = { ...(next.channels.feishu ?? {}) };
if (accId === DEFAULT_ACCOUNT_ID) {
next.channels.feishu = {
...next.channels.feishu,
enabled: true,
...patch,
};
} else {
next.channels.feishu.accounts = { ...(next.channels.feishu.accounts ?? {}) };
next.channels.feishu.accounts[accId] = {
...(next.channels.feishu.accounts[accId] ?? {}),
enabled: true,
...patch,
};
}
return next;
};
return ensureAccount(cfg, accountId);
}
async function promptCredentials(params: {
cfg: ClawdbotConfig;
prompter: WizardPrompter;
accountId: string;
}): Promise<ClawdbotConfig> {
const { cfg, prompter, accountId } = params;
const current = resolveFeishuAccount({ cfg, accountId });
const appId = await prompter.text({
message: "Feishu App ID",
placeholder: "cli_...",
initialValue: current.config.appId,
validate: (value: unknown) => {
const val = String(value ?? "").trim();
if (!val) return "Required";
if (!val.startsWith("cli_")) return "App ID usually starts with 'cli_'";
return undefined;
},
});
const appSecret = await prompter.text({
message: "Feishu App Secret",
placeholder: "...",
initialValue: current.config.appSecret,
validate: (value: unknown) => (String(value ?? "").trim() ? undefined : "Required"),
});
const encryptKey = await prompter.text({
message: "Encrypt Key",
hint: "Optional; required only if you configured Encrypt Key in Feishu Event Subscriptions",
initialValue: current.config.encryptKey,
});
const verificationToken = await prompter.text({
message: "Verification Token",
hint: "Optional; required only if you configured Verification Token in Feishu Event Subscriptions",
initialValue: current.config.verificationToken,
});
return applyFeishuConfig({
cfg,
accountId,
patch: {
appId: String(appId).trim(),
appSecret: String(appSecret).trim(),
...(encryptKey ? { encryptKey: String(encryptKey).trim() } : {}),
...(verificationToken ? { verificationToken: String(verificationToken).trim() } : {}),
},
});
}
function getStatus(ctx: ChannelOnboardingStatusContext): Promise<ChannelOnboardingStatus> {
const { cfg } = ctx;
// Simple check: is default account configured?
// Ideally we iterate all accounts, but for status summary usually checking if ANY are configured is enough,
// or just the generic status.
// Let's use listAccountIds from plugin config.
const accountIds = listFeishuAccountIds(cfg);
const configured = accountIds.some((accId: string) => isFeishuConfigured(resolveFeishuAccount({ cfg, accountId: accId })));
return Promise.resolve({
channel,
configured,
statusLines: [
`Feishu: ${configured ? "configured" : "needs credentials"}`,
],
selectionHint: configured ? "configured" : "setup",
});
}
async function configure(ctx: ChannelOnboardingConfigureContext): Promise<ChannelOnboardingResult> {
const { cfg, prompter, accountOverrides, shouldPromptAccountIds } = ctx;
const override = accountOverrides["feishu"]?.trim();
// Feishu defaults to "default" account ID
const defaultAccountId = "default";
let accountId = override ? normalizeAccountId(override) : defaultAccountId;
if (shouldPromptAccountIds && !override) {
accountId = await promptAccountId({
cfg,
prompter,
label: "Feishu",
currentId: accountId,
listAccountIds: (c: ClawdbotConfig) => listFeishuAccountIds(c),
defaultAccountId,
});
}
await prompter.note(
[
"Feishu setup requires App ID and App Secret from the Feishu Open Platform.",
"Encrypt Key and Verification Token are optional but recommended for event security.",
`Docs: ${formatDocsLink("/channels/feishu", "channels/feishu")}`,
].join("\n"),
"Feishu Setup"
);
let next = cfg;
next = await promptCredentials({ cfg: next, prompter, accountId });
// Ensure migration if needed (standard pattern)
const namedConfig = migrateBaseNameToDefaultAccount({
cfg: next,
channelKey: "feishu",
});
return { cfg: namedConfig, accountId };
}
export const feishuOnboardingAdapter: ChannelOnboardingAdapter = {
channel,
getStatus,
configure,
};

View File

@ -0,0 +1,16 @@
import type { ClawdbotPluginRuntime } from "clawdbot/plugin-sdk";
type FeishuRuntime = ClawdbotPluginRuntime;
let _runtime: FeishuRuntime | undefined;
export function setFeishuRuntime(runtime: FeishuRuntime) {
_runtime = runtime;
}
export function getFeishuRuntime(): FeishuRuntime {
if (!_runtime) {
throw new Error("Feishu runtime not initialized");
}
return _runtime;
}

View File

@ -0,0 +1,32 @@
export type FeishuConfig = {
appId?: string;
appSecret?: string;
encryptKey?: string;
verificationToken?: string;
};
export type FeishuAccount = {
accountId: string;
name?: string;
enabled?: boolean;
config: FeishuConfig;
};
export interface FeishuMessageEvent {
message: {
chat_id: string;
message_id: string;
chat_type: string;
message_type: string; // SDK types say message_type, raw might be msg_type
content: string; // JSON string
create_time: string;
};
sender: {
sender_id: {
user_id?: string;
open_id?: string;
union_id?: string;
};
sender_type: string;
};
}