Lark Suite Node.js SDK

repository·main·Indexed 42 days ago

https://github.com/larksuite/node-sdk

The Lark Suite (Feishu) open interface SDK for Node.js simplifies development for the Feishu Open Platform by automating token maintenance, data encryption/decryption, and request signature verification. Version 1.74.0 provides type-safe semantic API calls, an EventDispatcher for handling webhooks, and support for both Self-built and Store (ISV) applications. It also includes a Vercel Chat adapter for integrating Lark bots with the Chat SDK.

Tokens
24.6K
Snippets
61
Records
91
Agent score
54%

What's inside Lark Suite

  1. Overview of the Larksuite Node.js SDK

    main

    The Larksuite Node.js SDK is designed to simplify the use of Feishu (Lark) open APIs. It abstracts away complex manual tasks required when using raw APIs, such as:

    • Token Management: Automatic acquisition and maintenance of access tokens.
    • Security: Handling data encryption/decryption and request signature verification.
    • Developer Experience: Providing semantic programming interfaces and a complete type system to reduce cognitive load and improve coding efficiency.
  2. What is the Channel module and when to use it

    main

    The Channel module is a high-level abstraction built on top of WSClient, EventDispatcher, and Client. It manages the complexities of running a Feishu bot, including transport, message normalization, safety policies, outbound sending, streaming replies, media uploads, and card interactions.

    Use Channel when building:

    • Conversational bots (AI chat, streaming replies).
    • Interactive bots (using card buttons).
    • Bots requiring media handling or specific mention policies.

    Use WSClient + EventDispatcher directly if:

    • You only need to receive a few events and perform simple processing without high-level bot features.
  3. Handle events using EventDispatcher

    main

    The SDK provides an EventDispatcher to handle incoming events from the Feishu platform. It manages event registration, data decryption, and integration with web servers.

    Workflow

    1. Create an instance of EventDispatcher.
    2. Register event listeners using .register({ 'event.key': handler }).
    3. Bind the instance to your server using an adapter (e.g., adaptDefault, adaptExpress, adaptKoa).

    EventDispatcher Configuration

    ParameterDescriptionTypeRequiredDefault
    encryptKeyKey used to decrypt encrypted event datastringNo-
    loggerLevelLogging levelLoggerLevelNolark.LoggerLevel.info
    loggerCustom logger instanceLoggerNo-
    cacheCache providerCacheNo-

    Unregistering Events

    Use .unregister(...keys) to remove handlers for specific event keys. This supports chaining.

    const eventDispatcher = new lark.EventDispatcher({}).register({
        'im.message.receive_v1': async (data) => {},
    });
    
    eventDispatcher
        .unregister('im.message.receive_v1')
        .register({
            'im.message.receive_v1': async (data) => {},
        });
    const eventDispatcher = new lark.EventDispatcher({
        encryptKey: 'encrypt key'
    }).register({
        'im.message.receive_v1': async (data) => {
            const chatId = data.message.chat_id;
            const res = await client.im.message.create({
                params: {
                    receive_id_type: 'chat_id',
                },
                data: {
                    receive_id: chatId,
                    content: JSON.stringify({text: 'hello world'}),
                    msg_type: 'text'
                },
            });
            return res;
        }
    });
  4. Understand Lark thread ID formats

    main

    The adapter encodes Lark thread IDs using the format lark:{chatId}:{rootId}:

    • chatId: oc_* for group/p2p chats, or ou_* for openDM() placeholders.
    • rootId: The root_id if the message is a reply; otherwise, the message_id (the message is its own root).

    Warning: Lark's native thread_id (topic containers, omt_*) is not used as the rootId segment. It is a topic container ID and cannot be used as replyTo in the send API.

  5. Key Concepts for Larksuite Development

    main

    To use the SDK effectively, you should understand the following core concepts of the Larksuite ecosystem:

    • Developer Documentation: The primary reference for all open APIs. Use the search function within the official documentation to find specific API details.
    • Developer Console: The management backend where you configure and manage your applications.
    • Enterprise Self-built Apps: Applications that are installed and used only within a single specific enterprise.
    • App Store Apps: Applications listed in the App Directory that can be installed by various different enterprises.
  6. Handle paginated API results with Iterators

    main

    For APIs that return paginated data, the SDK provides methods suffixed with WithIterator. These return an async iterator that automatically handles page_token logic, allowing you to loop through all results easily.

    Alternatively, you can use the standard non-iterator method and manage page_token manually.

    // Using the automatic iterator
    for await (const items of await client.contact.user.listWithIterator({
        params: {
            department_id: '0',
            page_size: 20,
        },
    })) {
        console.log(items);
    }
    
    // Manual control using the iterator object
    const listIterator = await client.contact.user.listWithIterator({
        params: {
            department_id: '0',
            page_size: 20,
        },
    });
    const { value } = await listIterator[Symbol.asyncIterator]().next();
    console.log(value);
  7. Iterate through paginated results with iterators

    main

    For APIs that return paginated data, the SDK provides methods with the WithIterator suffix. These allow you to use for await...of loops, abstracting away the page_token management.

    // Option 1: Using for await...of loop
    for await (const items of await client.contact.user.listWithIterator({
        params: {
            department_id: '0',
            page_size: 20,
        },
    })) {
        console.log(items);
    }
    
    // Option 2: Manual control using the async iterator
    const listIterator = await client.contact.user.listWithIterator({
        params: {
            department_id: '0',
            page_size: 20,
        },
    });
    const { value } = await listIterator[Symbol.asyncIterator]().next();
    console.log(value);
  8. Use the Channel module for conversational bots

    main

    The Channel module is a high-level abstraction built on top of WSClient, EventDispatcher, and Client. It is designed for building conversational bots that require features like message normalization, security policies, outbound sending, streaming replies, media uploads, and card interactions.

    When to use Channel:

    • Building AI chat bots.
    • Implementing streaming replies (typewriter effect).
    • Handling card button interactions.
    • Managing media uploads and @all strategies.

    When to use lower-level components:

    • If you only need to process a small number of events with simple logic, the WSClient + EventDispatcher combination is sufficient.
    import { createLarkChannel, LoggerLevel } from '@larksuiteoapi/node-sdk';
    
    const channel = createLarkChannel({
        appId: process.env.FEISHU_APP_ID!,
        appSecret: process.env.FEISHU_APP_SECRET!,
        loggerLevel: LoggerLevel.info,
        policy: { requireMention: true, dmMode: 'open' },
    });
    
    channel.on('message', async (msg) => {
        await channel.send(
            msg.chatId,
            { markdown: `收到:${msg.content}` },
            { replyTo: msg.messageId },
        );
    });
    
    await channel.connect();
    console.log(`connected as ${channel.botIdentity!.name}`);
    
    process.on('SIGINT', async () => {
        await channel.disconnect();
        process.exit(0);
    });
  9. Quickstart: Minimal example with Channel

    main

    To create a basic bot that responds to messages, use createLarkChannel, call connect(), and listen for the message event. The connect() method resolves once the WebSocket handshake is complete, at which point channel.botIdentity becomes available.

    import { createLarkChannel, LoggerLevel } from '@larksuiteoapi/node-sdk';
    
    const channel = createLarkChannel({
        appId: process.env.FEISHU_APP_ID!,
        appSecret: process.env.FEISHU_APP_SECRET!,
        loggerLevel: LoggerLevel.info,
        policy: { requireMention: true, dmMode: 'open' },
    });
    
    channel.on('message', async (msg) => {
        await channel.send(
            msg.chatId,
            { markdown: `received: ${msg.content}` },
            { replyTo: msg.messageId },
        );
    });
    
    await channel.connect();
    console.log(`connected as ${channel.botIdentity!.name}`);
    
    process.on('SIGINT', async () => {
        await channel.disconnect();
        process.exit(0);
    });
  10. Handle Events with EventDispatcher

    main

    To process events (like receiving a message), use the EventDispatcher. It handles data decryption internally if an encryptKey is provided.

    Workflow:

    1. Create an EventDispatcher instance.
    2. Register event keys and their handler functions using .register().
    3. Use an adapter (like adaptExpress, adaptKoa, or adaptDefault) to bind the dispatcher to your web server.

    Challenge Verification: When configuring a webhook, the platform sends a challenge field to verify the URL. Use the autoChallenge: true option in your adapter to handle this automatically.

    import http from 'http';
    import * as lark from '@larksuiteoapi/node-sdk';
    
    const eventDispatcher = new lark.EventDispatcher({
        encryptKey: 'encrypt key'
    }).register({
        'im.message.receive_v1': async (data) => {
            const chatId = data.message.chat_id;
            // Handle event logic here
        }
    });
    
    const server = http.createServer();
    // Use autoChallenge: true to handle URL verification
    server.on('request', lark.adaptDefault('/webhook/event', eventDispatcher, {
        autoChallenge: true,
    }));
    server.listen(3000);
  11. Register an app with one-click OAuth

    main

    The registerApp method allows for one-click app creation using the OAuth 2.0 Device Authorization Grant protocol. It generates a verification URL for users to scan, which then automatically registers the app and returns credentials.

    Customizing Scopes and Events (Addons)

    You can use the addons parameter to incrementally request specific permissions (scopes, events, or callbacks) during the registration process.

    • addons.preset:
      • true (default): Keeps the platform's default template and adds your requested items on top.
      • false: Drops the default template and starts from a minimal base (bot capability only), showing only your declared items.
    • addons is additive only: You cannot use it to remove base permissions.
    import * as lark from '@larksuiteoapi/node-sdk';
    
    try {
        const result = await lark.registerApp({
            addons: {
                preset: false,
                scopes: { tenant: ['im:message:send_as_bot'] },
                events: { items: { tenant: ['im.message.receive_v1'] } },
            },
            onQRCodeReady(info) {
                console.log(`Scan the QR code: ${info.url}`);
            },
        });
    
        console.log('App ID:', result.client_id);
        console.log('App Secret:', result.client_secret);
    } catch (e) {
        console.error(e.code, e.description);
    }
  12. Download files and handle streams

    main

    When downloading files, the SDK returns a response object that simplifies file writing or stream handling.

    Write directly to a file: Use resp.writeFile(path) to save the binary data to a file.

    Handle raw streams: Use resp.getReadableStream() to get a ReadableStream. You can then pipe this to a WritableStream.

    Warning: A stream can only be consumed once. If you call writeFile, the stream is consumed and getReadableStream will return an empty/error state.

    import * as fs from 'fs';
    
    // Write to file
    const resp = await client.im.file.get({
        path: {
            file_key: 'file key',
        },
    });
    await resp.writeFile(`filepath.suffix`);
    
    // Use stream
    const readableStream = resp.getReadableStream();
    const writableStream = fs.createWriteStream('file url');
    readableStream.pipe(writableStream);