
Add Feishu
- 427 installs
- 130 repo stars
- Updated June 19, 2026
- sugarforever/01coder-agent-skills
add-feishu is an agent skill that scaffolds Feishu (Lark) API integrations, bots, and workspace hooks into a codebase for developers who need enterprise messaging inside their application.
About
add-feishu is an agent skill from the 01coder-agent-skills collection that helps developers add Feishu, also known as Lark, integrations to an existing project. The skill guides wiring REST APIs, bot endpoints, webhook handlers, and workspace configuration so notifications, approvals, or team workflows run inside Feishu rather than a separate channel. Developers reach for add-feishu when building SaaS tools, internal platforms, or agent automations that must post messages, receive events, or sync data with Feishu tenants. It targets the build-phase integration step where backend services and third-party APIs are connected before testing and deployment.
- add-feishu
Add Feishu by the numbers
- 427 all-time installs (skills.sh)
- +11 installs in the week ending Aug 5, 2026 (Skillselion tracking)
- Ranked #1,031 of 4,347 Backend & APIs skills by installs in the Skillselion catalog
- Data as of Aug 5, 2026 (Skillselion catalog sync)
npx skills add https://github.com/sugarforever/01coder-agent-skills --skill add-feishuAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 427 |
|---|---|
| repo stars | ★ 130 |
| Last updated | June 19, 2026 |
| Repository | sugarforever/01coder-agent-skills ↗ |
How do you integrate Feishu Lark APIs into an app?
Use add-feishu for development tasks
Who is it for?
Backend developers adding Feishu/Lark bots, notifications, or workspace API hooks to an existing SaaS or internal tool.
Skip if: Projects with no Feishu/Lark tenant or teams that only need Slack or Discord integrations.
When should I use this skill?
A developer asks to connect Feishu, Lark, or enterprise messaging APIs to an application or agent workflow.
What you get
Feishu API client setup, webhook handlers, and workspace integration configuration
- API client module
- webhook route handlers
Files
Add Feishu Channel
This skill adds Feishu (飞书) support to NanoClaw using the skills engine for deterministic code changes, then walks through interactive setup.
Phase 1: Pre-flight
Check if already applied
Read .nanoclaw/state.yaml. If feishu is in applied_skills, skip to Phase 3 (Setup). The code changes are already in place.
Ask the user
Use AskUserQuestion to collect configuration:
AskUserQuestion: Do you have a Feishu app already, or do you need to create one?
If they have one, collect FEISHU_APP_ID and FEISHU_APP_SECRET now. If not, we'll create one in Phase 3.
Phase 2: Apply Code Changes
Run the skills engine to apply this skill's code package. The package files are in this directory alongside this SKILL.md.
Initialize skills system (if needed)
If .nanoclaw/ directory doesn't exist yet:
npx tsx scripts/apply-skill.ts --initApply the skill
npx tsx scripts/apply-skill.ts .claude/skills/add-feishuThis deterministically:
- Adds
src/channels/feishu.ts(FeishuChannel class implementing Channel interface) - Three-way merges Feishu support into
src/index.ts(reads credentials viareadEnvFile, creates FeishuChannel if configured) - Installs the
@larksuiteoapi/node-sdknpm dependency - Updates
.envwithFEISHU_APP_IDandFEISHU_APP_SECRET - Records the application in
.nanoclaw/state.yaml
If the apply reports merge conflicts, read the intent file:
modify/src/index.ts.intent.md— what changed and invariants for index.ts
Validate code changes
npm run buildBuild must be clean before proceeding.
Phase 3: Setup
Create Feishu App (if needed)
If the user doesn't have a Feishu app, tell them:
I need you to create a Feishu bot:
>
1. Go to Feishu Open Platform and create a new app
2. Under Credentials, copy the App ID and App Secret
3. Under Event Subscriptions, enable Long Connection (WebSocket) mode
4. Add the event: im.message.receive_v1 (Receive messages)5. Under Permissions, add:
- im:message:send_as_bot (Send messages as bot) - im:message (Read messages)6. Publish the app (or create a version and approve it)
Wait for the user to provide the App ID and App Secret.
Configure environment
Add to .env:
FEISHU_APP_ID=<their-app-id>
FEISHU_APP_SECRET=<their-app-secret>Build and restart
npm run build
launchctl kickstart -k gui/$(id -u)/com.nanoclaw # macOS
# Linux: systemctl --user restart nanoclawPhase 4: Registration
Get Chat ID
Tell the user:
1. Start the bot: npm run dev2. Send any message to the bot in Feishu (DM or group)
3. Check the logs — the chat_id will appear in the metadata
4. The JID format is {chat_id}@feishuOr check the database directly:
sqlite3 store/messages.db "SELECT jid FROM chats WHERE jid LIKE '%@feishu'"Register the chat
Register directly in SQLite:
INSERT INTO registered_groups (jid, name, folder, trigger_pattern, added_at, requires_trigger)
VALUES ('{chat_id}@feishu', 'feishu', 'feishu', '@{ASSISTANT_NAME}', datetime('now'), 0);Note: requires_trigger is set to 0 (false) so the bot responds to all messages without needing @mention.
Then restart the service to pick up the new registration.
Phase 5: Verify
Test the connection
Tell the user:
Send a message to the bot in Feishu. It should respond within a few seconds.
Check logs if needed
tail -f logs/nanoclaw.log
# Or run interactively:
npm run devLook for:
Feishu bot info fetched— bot connected and identified itselfConnected to Feishu via WebSocket— WebSocket establishedFeishu message sent— outbound message delivered
Troubleshooting
Bot not responding
Check: 1. FEISHU_APP_ID and FEISHU_APP_SECRET are set in .env 2. Chat is registered: sqlite3 store/messages.db "SELECT * FROM registered_groups WHERE jid LIKE '%@feishu'" 3. App is published on Feishu Open Platform (draft apps don't receive events) 4. Event subscription im.message.receive_v1 is enabled 5. Long Connection (WebSocket) mode is enabled (not webhook) 6. Service is running: launchctl list | grep nanoclaw (macOS) or systemctl --user status nanoclaw (Linux)
Bot connects but doesn't receive messages
- Verify the app has
im:messagepermission - Verify the event
im.message.receive_v1is subscribed - Check that the app version is published and approved
Bot receives but can't send
- Verify the app has
im:message:send_as_botpermission - For group chats: the bot must be added to the group first
"Failed to fetch Feishu bot info"
Non-critical warning. Bot message detection (filtering own messages) won't work, but message sending/receiving still functions. Usually means the bot API endpoint isn't accessible — check network connectivity.
Removal
To remove Feishu integration:
1. Delete src/channels/feishu.ts 2. Remove FeishuChannel import and creation block from src/index.ts 3. Remove readEnvFile import if no other channel uses it 4. Remove FEISHU_APP_ID and FEISHU_APP_SECRET from .env 5. Remove Feishu registrations: sqlite3 store/messages.db "DELETE FROM registered_groups WHERE jid LIKE '%@feishu'" 6. Uninstall: npm uninstall @larksuiteoapi/node-sdk 7. Rebuild: npm run build && launchctl kickstart -k gui/$(id -u)/com.nanoclaw (macOS) or npm run build && systemctl --user restart nanoclaw (Linux)
import * as lark from '@larksuiteoapi/node-sdk';
import { ASSISTANT_NAME } from '../config.js';
import { logger } from '../logger.js';
import { Channel, OnInboundMessage, OnChatMetadata, RegisteredGroup } from '../types.js';
export interface FeishuChannelOpts {
onMessage: OnInboundMessage;
onChatMetadata: OnChatMetadata;
registeredGroups: () => Record<string, RegisteredGroup>;
appId: string;
appSecret: string;
}
export class FeishuChannel implements Channel {
name = 'feishu';
private client!: lark.Client;
private connected = false;
private botOpenId: string | undefined;
private opts: FeishuChannelOpts;
constructor(opts: FeishuChannelOpts) {
this.opts = opts;
}
async connect(): Promise<void> {
const { appId, appSecret } = this.opts;
this.client = new lark.Client({ appId, appSecret });
// Fetch bot's own open_id so we can detect our own messages
try {
const resp = await this.client.request({
method: 'GET',
url: 'https://open.feishu.cn/open-apis/bot/v3/info',
});
this.botOpenId = (resp as any)?.bot?.open_id;
logger.info({ botOpenId: this.botOpenId }, 'Feishu bot info fetched');
} catch (err) {
logger.warn({ err }, 'Failed to fetch Feishu bot info, bot message detection may not work');
}
const wsClient = new lark.WSClient({
appId,
appSecret,
loggerLevel: lark.LoggerLevel.warn,
});
const eventDispatcher = new lark.EventDispatcher({}).register({
'im.message.receive_v1': async (data: any) => {
await this.handleMessage(data);
},
});
wsClient.start({ eventDispatcher });
this.connected = true;
logger.info('Connected to Feishu via WebSocket');
}
private async handleMessage(data: any): Promise<void> {
// SDK may pass data as {event: {message, sender}} or directly as {message, sender}
const msg = data?.message || data?.event?.message;
const sender = data?.sender || data?.event?.sender;
if (!msg) return;
// Skip bot's own messages
if (sender?.sender_id?.open_id && sender.sender_id.open_id === this.botOpenId) return;
const chatId = msg.chat_id;
if (!chatId) return;
// Only handle text messages
if (msg.message_type !== 'text') return;
let content = '';
try {
const parsed = JSON.parse(msg.content || '{}');
content = parsed.text || '';
} catch {
return;
}
if (!content) return;
const chatJid = `${chatId}@feishu`;
const timestamp = new Date(Number(msg.create_time)).toISOString();
const senderName = sender?.sender_id?.open_id || 'unknown';
// Notify chat metadata
const isGroup = msg.chat_type === 'group';
this.opts.onChatMetadata(chatJid, timestamp, undefined, 'feishu', isGroup);
// Deliver message if group is registered
const groups = this.opts.registeredGroups();
if (groups[chatJid]) {
this.opts.onMessage(chatJid, {
id: msg.message_id || '',
chat_jid: chatJid,
sender: sender?.sender_id?.open_id || '',
sender_name: senderName,
content,
timestamp,
is_from_me: false,
is_bot_message: false,
});
}
}
async sendMessage(jid: string, text: string): Promise<void> {
const chatId = jid.replace(/@feishu$/, '');
const prefixed = `${ASSISTANT_NAME}: ${text}`;
try {
await this.client.im.v1.message.create({
params: { receive_id_type: 'chat_id' },
data: {
receive_id: chatId,
msg_type: 'text',
content: JSON.stringify({ text: prefixed }),
},
});
logger.info({ jid, length: prefixed.length }, 'Feishu message sent');
} catch (err) {
logger.error({ jid, err }, 'Failed to send Feishu message');
}
}
isConnected(): boolean {
return this.connected;
}
ownsJid(jid: string): boolean {
return jid.endsWith('@feishu');
}
async disconnect(): Promise<void> {
this.connected = false;
}
}
skill: feishu
version: 1.0.0
description: "Feishu (飞书/Lark) integration via WebSocket long connection"
core_version: 0.1.0
adds:
- src/channels/feishu.ts
modifies:
- src/index.ts
structured:
npm_dependencies:
"@larksuiteoapi/node-sdk": "^1"
env_additions:
- FEISHU_APP_ID
- FEISHU_APP_SECRET
conflicts: []
depends: []
test: "npm run build"
import fs from 'fs';
import path from 'path';
import {
ASSISTANT_NAME,
IDLE_TIMEOUT,
MAIN_GROUP_FOLDER,
POLL_INTERVAL,
TRIGGER_PATTERN,
} from './config.js';
import { FeishuChannel } from './channels/feishu.js';
import { WhatsAppChannel } from './channels/whatsapp.js';
import { readEnvFile } from './env.js';
import {
ContainerOutput,
runContainerAgent,
writeGroupsSnapshot,
writeTasksSnapshot,
} from './container-runner.js';
import { cleanupOrphans, ensureContainerRuntimeRunning } from './container-runtime.js';
import {
getAllChats,
getAllRegisteredGroups,
getAllSessions,
getAllTasks,
getMessagesSince,
getNewMessages,
getRouterState,
initDatabase,
setRegisteredGroup,
setRouterState,
setSession,
storeChatMetadata,
storeMessage,
} from './db.js';
import { GroupQueue } from './group-queue.js';
import { resolveGroupFolderPath } from './group-folder.js';
import { startIpcWatcher } from './ipc.js';
import { findChannel, formatMessages, formatOutbound } from './router.js';
import { startSchedulerLoop } from './task-scheduler.js';
import { Channel, NewMessage, RegisteredGroup } from './types.js';
import { logger } from './logger.js';
// Re-export for backwards compatibility during refactor
export { escapeXml, formatMessages } from './router.js';
let lastTimestamp = '';
let sessions: Record<string, string> = {};
let registeredGroups: Record<string, RegisteredGroup> = {};
let lastAgentTimestamp: Record<string, string> = {};
let messageLoopRunning = false;
let whatsapp: WhatsAppChannel;
const channels: Channel[] = [];
const queue = new GroupQueue();
function loadState(): void {
lastTimestamp = getRouterState('last_timestamp') || '';
const agentTs = getRouterState('last_agent_timestamp');
try {
lastAgentTimestamp = agentTs ? JSON.parse(agentTs) : {};
} catch {
logger.warn('Corrupted last_agent_timestamp in DB, resetting');
lastAgentTimestamp = {};
}
sessions = getAllSessions();
registeredGroups = getAllRegisteredGroups();
logger.info(
{ groupCount: Object.keys(registeredGroups).length },
'State loaded',
);
}
function saveState(): void {
setRouterState('last_timestamp', lastTimestamp);
setRouterState(
'last_agent_timestamp',
JSON.stringify(lastAgentTimestamp),
);
}
function registerGroup(jid: string, group: RegisteredGroup): void {
let groupDir: string;
try {
groupDir = resolveGroupFolderPath(group.folder);
} catch (err) {
logger.warn(
{ jid, folder: group.folder, err },
'Rejecting group registration with invalid folder',
);
return;
}
registeredGroups[jid] = group;
setRegisteredGroup(jid, group);
// Create group folder
fs.mkdirSync(path.join(groupDir, 'logs'), { recursive: true });
logger.info(
{ jid, name: group.name, folder: group.folder },
'Group registered',
);
}
/**
* Get available groups list for the agent.
* Returns groups ordered by most recent activity.
*/
export function getAvailableGroups(): import('./container-runner.js').AvailableGroup[] {
const chats = getAllChats();
const registeredJids = new Set(Object.keys(registeredGroups));
return chats
.filter((c) => c.jid !== '__group_sync__' && c.is_group)
.map((c) => ({
jid: c.jid,
name: c.name,
lastActivity: c.last_message_time,
isRegistered: registeredJids.has(c.jid),
}));
}
/** @internal - exported for testing */
export function _setRegisteredGroups(groups: Record<string, RegisteredGroup>): void {
registeredGroups = groups;
}
/**
* Process all pending messages for a group.
* Called by the GroupQueue when it's this group's turn.
*/
async function processGroupMessages(chatJid: string): Promise<boolean> {
const group = registeredGroups[chatJid];
if (!group) return true;
const channel = findChannel(channels, chatJid);
if (!channel) {
console.log(`Warning: no channel owns JID ${chatJid}, skipping messages`);
return true;
}
const isMainGroup = group.folder === MAIN_GROUP_FOLDER;
const sinceTimestamp = lastAgentTimestamp[chatJid] || '';
const missedMessages = getMessagesSince(chatJid, sinceTimestamp, ASSISTANT_NAME);
if (missedMessages.length === 0) return true;
// For non-main groups, check if trigger is required and present
if (!isMainGroup && group.requiresTrigger !== false) {
const hasTrigger = missedMessages.some((m) =>
TRIGGER_PATTERN.test(m.content.trim()),
);
if (!hasTrigger) return true;
}
const prompt = formatMessages(missedMessages);
// Advance cursor so the piping path in startMessageLoop won't re-fetch
// these messages. Save the old cursor so we can roll back on error.
const previousCursor = lastAgentTimestamp[chatJid] || '';
lastAgentTimestamp[chatJid] =
missedMessages[missedMessages.length - 1].timestamp;
saveState();
logger.info(
{ group: group.name, messageCount: missedMessages.length },
'Processing messages',
);
// Track idle timer for closing stdin when agent is idle
let idleTimer: ReturnType<typeof setTimeout> | null = null;
const resetIdleTimer = () => {
if (idleTimer) clearTimeout(idleTimer);
idleTimer = setTimeout(() => {
logger.debug({ group: group.name }, 'Idle timeout, closing container stdin');
queue.closeStdin(chatJid);
}, IDLE_TIMEOUT);
};
await channel.setTyping?.(chatJid, true);
let hadError = false;
let outputSentToUser = false;
const output = await runAgent(group, prompt, chatJid, async (result) => {
// Streaming output callback — called for each agent result
if (result.result) {
const raw = typeof result.result === 'string' ? result.result : JSON.stringify(result.result);
// Strip <internal>...</internal> blocks — agent uses these for internal reasoning
const text = raw.replace(/<internal>[\s\S]*?<\/internal>/g, '').trim();
logger.info({ group: group.name }, `Agent output: ${raw.slice(0, 200)}`);
if (text) {
await channel.sendMessage(chatJid, text);
outputSentToUser = true;
}
// Only reset idle timer on actual results, not session-update markers (result: null)
resetIdleTimer();
}
if (result.status === 'success') {
queue.notifyIdle(chatJid);
}
if (result.status === 'error') {
hadError = true;
}
});
await channel.setTyping?.(chatJid, false);
if (idleTimer) clearTimeout(idleTimer);
if (output === 'error' || hadError) {
// If we already sent output to the user, don't roll back the cursor —
// the user got their response and re-processing would send duplicates.
if (outputSentToUser) {
logger.warn({ group: group.name }, 'Agent error after output was sent, skipping cursor rollback to prevent duplicates');
return true;
}
// Roll back cursor so retries can re-process these messages
lastAgentTimestamp[chatJid] = previousCursor;
saveState();
logger.warn({ group: group.name }, 'Agent error, rolled back message cursor for retry');
return false;
}
return true;
}
async function runAgent(
group: RegisteredGroup,
prompt: string,
chatJid: string,
onOutput?: (output: ContainerOutput) => Promise<void>,
): Promise<'success' | 'error'> {
const isMain = group.folder === MAIN_GROUP_FOLDER;
const sessionId = sessions[group.folder];
// Update tasks snapshot for container to read (filtered by group)
const tasks = getAllTasks();
writeTasksSnapshot(
group.folder,
isMain,
tasks.map((t) => ({
id: t.id,
groupFolder: t.group_folder,
prompt: t.prompt,
schedule_type: t.schedule_type,
schedule_value: t.schedule_value,
status: t.status,
next_run: t.next_run,
})),
);
// Update available groups snapshot (main group only can see all groups)
const availableGroups = getAvailableGroups();
writeGroupsSnapshot(
group.folder,
isMain,
availableGroups,
new Set(Object.keys(registeredGroups)),
);
// Wrap onOutput to track session ID from streamed results
const wrappedOnOutput = onOutput
? async (output: ContainerOutput) => {
if (output.newSessionId) {
sessions[group.folder] = output.newSessionId;
setSession(group.folder, output.newSessionId);
}
await onOutput(output);
}
: undefined;
try {
const output = await runContainerAgent(
group,
{
prompt,
sessionId,
groupFolder: group.folder,
chatJid,
isMain,
assistantName: ASSISTANT_NAME,
},
(proc, containerName) => queue.registerProcess(chatJid, proc, containerName, group.folder),
wrappedOnOutput,
);
if (output.newSessionId) {
sessions[group.folder] = output.newSessionId;
setSession(group.folder, output.newSessionId);
}
if (output.status === 'error') {
logger.error(
{ group: group.name, error: output.error },
'Container agent error',
);
return 'error';
}
return 'success';
} catch (err) {
logger.error({ group: group.name, err }, 'Agent error');
return 'error';
}
}
async function startMessageLoop(): Promise<void> {
if (messageLoopRunning) {
logger.debug('Message loop already running, skipping duplicate start');
return;
}
messageLoopRunning = true;
logger.info(`NanoClaw running (trigger: @${ASSISTANT_NAME})`);
while (true) {
try {
const jids = Object.keys(registeredGroups);
const { messages, newTimestamp } = getNewMessages(jids, lastTimestamp, ASSISTANT_NAME);
if (messages.length > 0) {
logger.info({ count: messages.length }, 'New messages');
// Advance the "seen" cursor for all messages immediately
lastTimestamp = newTimestamp;
saveState();
// Deduplicate by group
const messagesByGroup = new Map<string, NewMessage[]>();
for (const msg of messages) {
const existing = messagesByGroup.get(msg.chat_jid);
if (existing) {
existing.push(msg);
} else {
messagesByGroup.set(msg.chat_jid, [msg]);
}
}
for (const [chatJid, groupMessages] of messagesByGroup) {
const group = registeredGroups[chatJid];
if (!group) continue;
const channel = findChannel(channels, chatJid);
if (!channel) {
console.log(`Warning: no channel owns JID ${chatJid}, skipping messages`);
continue;
}
const isMainGroup = group.folder === MAIN_GROUP_FOLDER;
const needsTrigger = !isMainGroup && group.requiresTrigger !== false;
// For non-main groups, only act on trigger messages.
// Non-trigger messages accumulate in DB and get pulled as
// context when a trigger eventually arrives.
if (needsTrigger) {
const hasTrigger = groupMessages.some((m) =>
TRIGGER_PATTERN.test(m.content.trim()),
);
if (!hasTrigger) continue;
}
// Pull all messages since lastAgentTimestamp so non-trigger
// context that accumulated between triggers is included.
const allPending = getMessagesSince(
chatJid,
lastAgentTimestamp[chatJid] || '',
ASSISTANT_NAME,
);
const messagesToSend =
allPending.length > 0 ? allPending : groupMessages;
const formatted = formatMessages(messagesToSend);
if (queue.sendMessage(chatJid, formatted)) {
logger.debug(
{ chatJid, count: messagesToSend.length },
'Piped messages to active container',
);
lastAgentTimestamp[chatJid] =
messagesToSend[messagesToSend.length - 1].timestamp;
saveState();
// Show typing indicator while the container processes the piped message
channel.setTyping?.(chatJid, true)?.catch((err) =>
logger.warn({ chatJid, err }, 'Failed to set typing indicator'),
);
} else {
// No active container — enqueue for a new one
queue.enqueueMessageCheck(chatJid);
}
}
}
} catch (err) {
logger.error({ err }, 'Error in message loop');
}
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL));
}
}
/**
* Startup recovery: check for unprocessed messages in registered groups.
* Handles crash between advancing lastTimestamp and processing messages.
*/
function recoverPendingMessages(): void {
for (const [chatJid, group] of Object.entries(registeredGroups)) {
const sinceTimestamp = lastAgentTimestamp[chatJid] || '';
const pending = getMessagesSince(chatJid, sinceTimestamp, ASSISTANT_NAME);
if (pending.length > 0) {
logger.info(
{ group: group.name, pendingCount: pending.length },
'Recovery: found unprocessed messages',
);
queue.enqueueMessageCheck(chatJid);
}
}
}
function ensureContainerSystemRunning(): void {
ensureContainerRuntimeRunning();
cleanupOrphans();
}
async function main(): Promise<void> {
ensureContainerSystemRunning();
initDatabase();
logger.info('Database initialized');
loadState();
// Graceful shutdown handlers
const shutdown = async (signal: string) => {
logger.info({ signal }, 'Shutdown signal received');
await queue.shutdown(10000);
for (const ch of channels) await ch.disconnect();
process.exit(0);
};
process.on('SIGTERM', () => shutdown('SIGTERM'));
process.on('SIGINT', () => shutdown('SIGINT'));
// Channel callbacks (shared by all channels)
const channelOpts = {
onMessage: (_chatJid: string, msg: NewMessage) => storeMessage(msg),
onChatMetadata: (chatJid: string, timestamp: string, name?: string, channel?: string, isGroup?: boolean) =>
storeChatMetadata(chatJid, timestamp, name, channel, isGroup),
registeredGroups: () => registeredGroups,
};
// Create and connect channels
whatsapp = new WhatsAppChannel(channelOpts);
channels.push(whatsapp);
await whatsapp.connect();
// Connect Feishu channel if configured
const feishuEnv = readEnvFile(['FEISHU_APP_ID', 'FEISHU_APP_SECRET']);
const feishuAppId = process.env.FEISHU_APP_ID || feishuEnv.FEISHU_APP_ID;
const feishuAppSecret = process.env.FEISHU_APP_SECRET || feishuEnv.FEISHU_APP_SECRET;
if (feishuAppId && feishuAppSecret) {
const feishu = new FeishuChannel({ ...channelOpts, appId: feishuAppId, appSecret: feishuAppSecret });
channels.push(feishu);
await feishu.connect();
}
// Start subsystems (independently of connection handler)
startSchedulerLoop({
registeredGroups: () => registeredGroups,
getSessions: () => sessions,
queue,
onProcess: (groupJid, proc, containerName, groupFolder) => queue.registerProcess(groupJid, proc, containerName, groupFolder),
sendMessage: async (jid, rawText) => {
const channel = findChannel(channels, jid);
if (!channel) {
console.log(`Warning: no channel owns JID ${jid}, cannot send message`);
return;
}
const text = formatOutbound(rawText);
if (text) await channel.sendMessage(jid, text);
},
});
startIpcWatcher({
sendMessage: (jid, text) => {
const channel = findChannel(channels, jid);
if (!channel) throw new Error(`No channel for JID: ${jid}`);
return channel.sendMessage(jid, text);
},
registeredGroups: () => registeredGroups,
registerGroup,
syncGroupMetadata: (force) => whatsapp?.syncGroupMetadata(force) ?? Promise.resolve(),
getAvailableGroups,
writeGroupsSnapshot: (gf, im, ag, rj) => writeGroupsSnapshot(gf, im, ag, rj),
});
queue.setProcessMessagesFn(processGroupMessages);
recoverPendingMessages();
startMessageLoop().catch((err) => {
logger.fatal({ err }, 'Message loop crashed unexpectedly');
process.exit(1);
});
}
// Guard: only run when executed directly, not when imported by tests
const isDirectRun =
process.argv[1] &&
new URL(import.meta.url).pathname === new URL(`file://${process.argv[1]}`).pathname;
if (isDirectRun) {
main().catch((err) => {
logger.error({ err }, 'Failed to start NanoClaw');
process.exit(1);
});
}
Intent: src/index.ts modifications
What changed
Added Feishu channel initialization alongside existing channels.
Key sections
Imports (top of file)
- Added:
FeishuChannelfrom./channels/feishu.js - Added:
readEnvFilefrom./env.js
main()
- Added: After WhatsApp connect, reads
FEISHU_APP_IDandFEISHU_APP_SECRETviareadEnvFile()(NanoClaw does NOT load.envintoprocess.env) - Added: If both are set, creates
FeishuChannelwith sharedchannelOptsplusappId/appSecret, pushes tochannels[], callsconnect()
Invariants
- All existing message processing logic is preserved
- WhatsApp channel creation is unchanged
- State management, recovery, scheduler, IPC — all unchanged
- The
channels[]array andfindChannel()routing already exist (added by multi-channel refactor)
Must-keep
- All existing exports (
escapeXml,formatMessages,_setRegisteredGroups) - The
isDirectRunguard at bottom - All error handling and cursor rollback logic
- The
readEnvFilepattern — all.envvalues must go through this function, NOTprocess.envalone
Related skills
How it compares
Pick add-feishu when the target messaging platform is Feishu/Lark rather than generic REST or Slack-specific integration skills.
FAQ
What does add-feishu integrate?
add-feishu helps developers wire Feishu, also called Lark, REST APIs, bot webhooks, and workspace event handlers into an existing backend so applications can send notifications and receive enterprise messaging events.
When should I use add-feishu?
Use add-feishu during build-phase integration work when a SaaS or internal tool must connect to a Feishu/Lark tenant for bots, alerts, approvals, or workspace automation rather than Slack or email.