调用服务端 API
本文档介绍通过 NodeJS SDK 构造 API 请求、调用开放平台服务端 API 的详细步骤。
步骤一:创建并配置 API Client
调用 API 之前,需要先创建一个 API Client,用于指定应用信息、日志级别、超时时间等基本信息。
创建 API Client。
企业自建应用与商店应用的创建方式有所不同,具体说明如下:
企业自建应用,使用以下代码创建 API Client。
javascriptimport * as lark from '@larksuiteoapi/node-sdk'; const client = new lark.Client({ appId: 'app id', appSecret: 'app secret' });商店应用,使用以下代码创建 API Client,其中需要通过
appType: lark.AppType.ISV标识当前的应用为商店应用。javascriptimport * as lark from '@larksuiteoapi/node-sdk'; const client = new lark.Client({ appId: 'app id', appSecret: 'app secret', appType: lark.AppType.ISV, });:::note
说明:使用商店应用发起 API 调用时,必须传入租户 Key(tenant_key)以及 app_ticket(用于获取商店应用的 app_access_token)。详细说明参见下文 商店应用调用 API 的必要操作 章节。 :::
(可选)自定义 API Client 配置
在创建 API Client 时,支持自定义 API Client 的配置。例如,设置日志级别、缓存器等。Client 构造参数说明如下表所示。
| 参数 | 类型 | 是否必填 | 描述 |
|---|---|---|---|
| appId | string | 是 | 应用的 App ID。获取方式:登录开发者后台,在应用详情页的 凭证与基础信息 页面内,获取应用凭证(包括 App ID 和 App Secret)。 |
| appSecret | string | 是 | 应用的 App Secret。 |
| domain | Domain | string | 否 |
| httpInstance | HttpInstance | 否 | SDK 发送请求的 HTTP 实例。SDK 内部默认使用 axios.create() 构造出一个 defaultHttpInstance 进行 HTTP 调用。 可以从 SDK 中 import defaultHttpInstance,在其中添加 interceptors 完成业务需求。例如,SDK 默认过滤了响应结果中的 data,如需获取 logid 等数据,可以重置 interceptors。示例代码如下: javascript import { defaultHttpInstance } from '@larksuiteoapi/node-sdk'; defaultHttpInstance.interceptors.response.use((resp) => { // 这里可以做一些业务逻辑处理 // 记得返回resp.data return resp.data; }) 默认值:defaultHttpInstance |
| loggerLevel | LoggerLevel | 否 | 日志级别。枚举值: - error:记录错误事件,这些事件阻止了部分程序的执行。 - warn:记录一些异常问题,但这些异常可能不影响程序继续运行。 - info:记录运行过程中关键的、需要被监控的信息。 - debug:记录调试信息,用于在调试时诊断问题。 - trace:记录详细信息,可用于开发或调试时跟踪程序运行过程。 默认值:info |
| logger | Logger | 否 | 日志器,可根据需要自定义配置。 |
| cache | Cache | 否 | 缓存器,用于缓存数据的存储与获取,如 token。如果你没有指定缓存器,SDK 会初始化一个缓存器。如果需要和现有系统共享数据,可以自定义缓存器,实现 Cache 的接口即可。默认缓存器的实现:default-cache.ts ```javascript import get from 'lodash.get'; import { Cache } from '@node-sdk/typings'; export class DefaultCache implements Cache { values: Map< string |
| disableTokenCache | boolean | 否 | 是否禁用用于保存 token 的缓存。如果禁用(取值 true),则不会缓存 token,系统会在每次需要使用 token 时自动重新拉取。 默认值:false,表示不禁用 |
| appType | AppType | 否 | 应用类型。默认不传值表示企业自建应用,如果是商店应用则必须传入 AppType.ISV。 - AppType.ISV:商店应用 - AppType.SelfBuild:自建应用 默认值:AppType.SelfBuild |
| helpDeskId | string | 否 | 服务台的 ID。仅在调用服务台业务的 API 时需要配置。可在服务台管理后台 设置中心 > API 凭证 处获取,详情参见服务台接入指南。 注意:服务台的 ID、Token 只有服务台创建者可以查看到。 ![]() |
| helpDeskToken | string | 否 | 服务台的 token。仅在调用服务台业务的 API 时需要配置。可在服务台管理后台 设置中心 > API 凭证 处获取,详情参见服务台接入指南。 注意:服务台的 ID、Token 只有服务台创建者可以查看到。 ![]() |
步骤二:构造 API 请求
在项目内创建 API Client 后,即可开始调用飞书开放接口。你可以使用 client.业务域.资源.方法名称 来定位具体的 API 方法,然后对具体 API 发起调用。建议通过 API 调试台快速定位 API 方法,以发送消息接口为例,可以通过地址栏参数拼接 API 方法,也可以查看接口示例代码定位 API 方法,如下图所示。
方式一:查阅指定 API 的示例代码,从代码中直接获取用于构造 API 请求的方法。
方式二:通过指定 API 的浏览器地址栏获取相关参数,以 client.业务域.资源.方法名称 格式拼接 API 方法。
- 下图中 ① project 代表 业务域
- 下图中 ② resource 代表 资源
- 下图中 ③ apiName 代表 方法名称

在 NodeJS SDK 1.22 版本开始支持多版本的调用,如果遇到某个业务域(project)下的 API 在 SDK 中搜索不到,可通过 project.version 来获取,例如下图,任务(task)下包含 v1、v2 两个版本(version)。

选择 v2 后,会多出一些方法。

调用的示例代码如下,代码内通过 client 调用发送消息接口。
import * as lark from '@larksuiteoapi/node-sdk';
// 构建 API Client
const client = new lark.Client({
appId: 'app id',
appSecret: 'app secret'
});
// 通过 Client 调用「发送消息」接口
const res = await client.im.message.create({
params: {
receive_id_type: 'chat_id',
},
data: {
receive_id: 'receive_id',
content: JSON.stringify({text: 'hello world'}),
msg_type: 'text',
},
});在 SDK 中包含了 API 对应的飞书开放平台 API 调试台链接,在开发工具内,你可在具体 API 的方法注释中点击 click to debug 跳转 API 调试台进行调试。

商店应用调用 API 的必要操作
如果使用商店应用调用 API,在 client 中必须声明 appType: lark.AppType.ISV,SDK 会根据该声明执行不同的逻辑分支。商店应用的逻辑分支不同于企业自建应用,商店应用必须在代码内传入租户 Key(tenant_key)以及 app_ticket。
Note 商店应用调用 API 为什么需要租户 Key(tenant_key)以及 app_ticket:
tenant_key:一个商店应用会被安装到多个租户中,tenant_key 是租户的唯一标识,用来区分不同的租户。
app_ticket:为了提高数据访问的安全性,飞书对商店应用应用增加了
app_ticket作为安全凭证,用于获取商店应用的 app_access_token。每隔 1 小时向商店应用配置的事件订阅请求地址自动推送一次app_ticket。
获取企业的授权访问凭证 tenant_access_token 时,需要用到这两个值,但这两个值通过 SDK 无法主动获取到,必须由外部传递进来。因此对于商店应用,SDK 提供了一种方式来传递这两个值。
在应用内订阅 app_ticket 事件。
如何订阅事件参考,具体说明参考 app_ticket 事件。
在项目中构造好商店应用的 API Client 后,需要使用
EventDispatcher接收并处理 app_ticket 事件。以 node 的默认 HTTP 服务为例,示例代码如下:
javascriptimport * as http from "http"; const ISVEventDispatcher = new lark.EventDispatcher(); const server = http.createServer(); // /webhook/event 为在开放平台配置的事件接收地址的 Path server.on('request', lark.adaptDefault('/webhook/event', ISVEventDispatcher)); server.listen(3000);EventDispatcher内部会默认接收app_ticket事件,将获得的app_ticket写到缓存(Cache)中。使用 client 调用接口,使用
withTenantKey方法显示传递tenant_key值。client 会结合上一步写入缓存中的
app_ticket,完成tenant_access_token的获取(并缓存),最后发起接口调用请求。javascriptimport * as fs from "fs"; const res = await SDKClient.im.file.create({ data: { file_type: "mp4", file_name: "测试.mp4", file: fs.readFileSync("文件路径"), } }, lark.withTenantKey('tenant key')); console.log(res);
步骤三(可选):设置请求选项
在每次发起 API 调用时,你可以设置请求级别的相关参数,例如传递 userAccessToken(用户访问凭证)、自定义 headers 等。
- 自定义 headers 示例代码如下所示。
await client.im.message.create({
params: {
receive_id_type: 'chat_id',
},
data: {
receive_id: 'receive_id',
content: JSON.stringify({text: 'hello world'}),
msg_type: 'text',
},
}, {
headers: {
customizedHeaderKey: 'customizedHeaderValue'
}
});- SDK 内将常用的修改操作封装成了方法,可以使用的方法如下所示。
| 方法 | 描述 |
|---|---|
| withTenantKey | 设置租户 Key(tenant_key),使用商店应用调用 API 时,必须传入该值。如何获取参见 tenant_key。 |
| withTenantToken | 设置应用身份 Token(tenant_access_token)。获取方式: - 商店应用获取 tenant_access_token - 自建应用获取 tenant_access_token |
| withUserAccessToken | 设置用户身份 Token(user_access_token)。获取方式:获取 user_access_token |
| withHelpDeskCredential | 是否在请求中带入服务台 Token。仅在调用服务台业务的 API 时需要配置,Token 可在服务台管理后台 设置中心 > API 凭证 处获取,详情参见服务台接入指南。 注意:服务台的 Token 只有服务台创建者可以查看到。 ![]() |
| withAll | 用于合并上述方法的结果。 |
以上方法在代码中的配置示例如下,更多代码示例参考 request-with.ts。
// 单个方法的使用示例
await client.im.message.create({
params: {
receive_id_type: 'chat_id',
},
data: {
receive_id: 'receive_id',
content: JSON.stringify({text: 'hello world'}),
msg_type: 'text',
},
}, lark.withTenantToken('tenant token'));
// 多个方法的使用示例,需要使用 withAll 合并方法
await client.im.message.create({
params: {
receive_id_type: 'chat_id',
},
data: {
receive_id: 'receive_id',
content: JSON.stringify({text: 'hello world'}),
msg_type: 'text',
},
}, lark.withAll([
lark.withTenantToken('tenant token'),
lark.withTenantKey('tenant key')
]));步骤四:运行项目
完成以上步骤后,即可运行项目调用 API。你可以通过开发工具运行项目,也可以根据项目部署情况在命令行内通过 node {.js 项目文件} 命令运行,如下图所示。
运行成功将返回接口响应参数

运行失败则会返回对应的错误码与错误信息

API 调用方式介绍
NodeJS SDK 为了提高 API 调用效率,封装了部分 API 的调用方法,包括分页查询、上传文件、下载文件等。
分页查询
针对返回值以分页形式呈现的接口,SDK 提供了迭代器方式的封装(方法名后缀为 WithIterator),消除了根据 page_token 反复获取数据的操作,提升易用性。例如,以获取部门直属用户列表接口为例,分页查询方式如下所示。
// 每次处理20条数据
for await (const items of await client.contact.user.findByDepartment({
params: {
department_id: '0',
page_size: 20,
},
})) {
console.log(items);
}
// 也可用next来手动控制迭代,每次取20条数据
const listIterator = await SDKClient.contact.user.findByDepartment({
params: {
department_id: '0',
page_size: 20,
},
});
const { value } = await listIterator[Symbol.asyncIterator]().next();
console.log(value);你也可以使用无迭代器封装的版本,需要自己每次根据返回的 page_token 来手动进行分页调用。
上传文件
SDK 内封装了对文件上传的处理逻辑,和普通的 API 调用方式一样,以上传文件接口为例,传递参数即可调用。
const res = await client.im.file.create({
data: {
file_type: 'mp4',
file_name: 'test.mp4',
file: fs.readFileSync('file path'),
},
});如果以上方式实际应用时报错 1061002,可以尝试将 file 类型变为 stream。示例代码如下:
let Duplex = require('stream').Duplex;
function bufferToStream(buffer) {
let stream = new Duplex();
stream.push(buffer);
stream.push(null);
return stream;
}下载文件
SDK 对返回的二进制流进行了封装,消除了对流本身的处理,调用下载文件接口时,只需要调用 writeFile 方法即可将数据写入文件。
const resp = await client.im.file.get({
path: {
file_key: 'file key',
},
});
await resp.writeFile(`filepath.suffix`);如果想要自定义对流的处理,可以调用 getReadableStream 方法获取到流,如下示例将流写入文件。
import * as fs from 'fs';
const resp = await client.im.file.get({
path: {
file_key: 'file key',
},
});
const readableStream = resp.getReadableStream();
const writableStream = fs.createWriteStream('file url');
readableStream.pipe(writableStream);Note注意:流只能被消费一次,即如果使用了 writeFile 消费了流,则 getReadableStream 获取流会报错或者获取到的流为空。如需消费多次流,可以使用 getReadableStream 获取流,然后读取流中的数据做缓存,将缓存的数据给消费方使用。
飞书卡片
开放平台提供了卡片模板能力,在发送卡片消息时只需要提供模板 ID 和内容即可。SDK 对卡片模板进行了调用上的封装,在支持消息卡片的接口中增加了 ByCard 的调用方式,你只需要传递 template_id 和 template_variable 即可。
client.im.message.createByCard({
params: {
receive_id_type: 'chat_id',
},
data: {
receive_id: 'your receive_id',
template_id: 'your template_id',
template_variable: {
content: "Card Content",
title: "Card Title"
}
}
});如果你需要基于卡片的 JSON 数据发送消息,以调用发送消息接口发送一个简单的具备 title 和 content 的卡片为例,示例代码如下。
client.im.message.create({
params: {
receive_id_type: 'chat_id',
},
data: {
receive_id: 'your receive_id',
content: JSON.stringify({
"config": {
"wide_screen_mode": true
},
"elements": [
{
"tag": "markdown",
"content": "Card Content"
}
],
"header": {
"template": "blue",
"title": {
"content": "Card Title",
"tag": "plain_text"
}
}
}
),
msg_type: 'interactive'
}
})示例效果如下:

SDK 内置了一个基础消息卡片供你体验。
import * as lark from '@larksuiteoapi/node-sdk';
client.im.message.create({
params: {
receive_id_type: 'chat_id',
},
data: {
receive_id: 'your receive_id',
content: lark.messageCard.defaultCard({
title: 'Card Title',
content: 'Card Content'
}),
msg_type: 'interactive'
}
})示例效果如下:

常见问题
如何快速获取接口对应的示例代码?
飞书开放平台提供了 API 调试台,通过该平台可以快速调试服务端 API,快速获取资源 ID 及生成多语言示例代码的能力,为您节省开发成本。例如,通过 API 调试台调用发送消息接口,在调试台成功完成测试后,可通过 示例代码 页面查阅 Node SDK 对应的接口调用代码。

如何调用历史版本 API、API 调试台搜索不到的 API、SDK 内找不到方法的 API ?
可以使用 SDK 提供的原生模式调用 API(需要使用 Client 上的 request 方法)。
import * as lark from '@larksuiteoapi/node-sdk';
const client = new lark.Client({
appId: 'app id',
appSecret: 'app secret'
});
const res = await client.request({
method: 'POST',
url: 'xxx',
data: {},
params: {},
});参数说明如下,你可以通过具体的 API 文档获取以下接口信息。例如:发送消息
- method:指定 API 所需的 HTTP Method。
- url:指定 API 的 HTTP URL,如果接口有路径参数也需要拼接在 url 内。
- data:接口的请求体。
- params:接口的查询参数。
在 Client 中自定义 domain 时 URL 后是否需要 ‘/’ ?
不需要。例如:domain: https://www.example.com 后面不需要加 /。
调用 API 时 Query 参数为列表如何传值?
方式一:将 NodeJS SDK 升级到 1.37.2 版本解决。
方式二:使用 paramsSerializer 并通过 https://www.npmjs.com/package/qs 序列化参数。
jsaxios.get('/myController/myAction', { params: { storeIds: [1,2,3] }, paramsSerializer: params => { return qs.stringify(params) } })
Token 的自动获取、缓存与刷新
SDK 默认会自动管理 tenant_access_token 的获取、缓存和刷新。您在初始化 Client 时只需提供 appId 和 appSecret,SDK 会在首次调用 API 前自动请求 Token,并将其缓存在Cache。 为防止因网络延迟等因素导致 Token 在传输过程中恰好过期,SDK 会在其 expire 时间的基础上提前 3 分钟将其置为失效,并在下次请求时重新获取,从而有效避免临界点过期的风险。
import { Client } from '@larksuiteoapi/node-sdk';
// 只需要提供 App ID 和 App Secret
const client = new Client({
appId: 'YOUR_APP_ID',
appSecret: 'YOUR_APP_SECRET',
});
// 直接调用 API,SDK 会自动处理 Token
async function main() {
const response = await client.im.message.create({
/* ...params */
});
console.log(response);
}实践建议:
- 除非有特殊需求(如自行实现中心化的 Token 管理服务),否则建议保持默认行为,这是最高效、最简单的使用方式。
- 飞书开放平台 API Explorer 中的示例代码为了方便调试,可能会展示
lark.withTenantToken("token")的用法。这种方式适用于临时测试,但在生产环境中不推荐,因为 Token 会过期,硬编码或手动传递会导致服务中断。
禁用缓存与手动传递 Token
在某些场景下(例如,在 Serverless 等无状态环境中使用外部缓存),您可以禁用 SDK 的内置缓存。将 disableTokenCache 设置为 true 后,每次 API 请求都必须通过 lark.withTenantToken() 或 lark.withUserAccessToken() 手动传递有效 Token。
import { Client, withTenantToken } from '@larksuiteoapi/node-sdk';
const client = new Client({
appId: 'YOUR_APP_ID',
appSecret: 'YOUR_APP_SECRET',
disableTokenCache: true, // 禁用内置 Token 缓存
});
async function sendMessageWithManualToken(token: string) {
const res = await client.im.chat.list(
{},
withTenantToken(token) // 每次调用都必须手动传递 Token
);
console.log(res);
}注意:如果禁用了缓存却未手动传递 Token,将会收到 Missing access token for authorization 的错误(错误码 99991661)。
使用 withUserAccessToken 代表用户调用
当需要代表某个用户执行操作时(例如,操作该用户的云空间文件),应使用 withUserAccessToken 来传递该用户的 user_access_token。
import { Client, withUserAccessToken } from '@larksuiteoapi/node-sdk';
const client = new Client({
appId: 'YOUR_APP_ID',
appSecret: 'YOUR_APP_SECRET',
});
// 使用 User Access Token 访问用户云空间
async function listUserDriveFiles(userToken: string) {
const res = await client.drive.file.list(
{},
withUserAccessToken(userToken) // 传入用户的 Access Token
);
console.log(res.data.files);
}自定义缓存实现
如果您希望将 Token 存储在 Redis 或其他外部存储中,可以自定义缓存实现。只需创建一个符合 ICache 接口(get, set, has 方法)的对象,并在初始化客户端时传入即可。
import { Client, ICache } from '@larksuiteoapi/node-sdk';
// 示例:使用 Map 实现的简单缓存,实际场景可替换为 Redis
class RedisCache implements ICache {
// private redisClient; // 假设这是您的 Redis 客户端实例
async set(key: string, value: any, expired?: number): Promise<void> {
// 在实际场景中,您可以将 expired 参数用于 Redis 的 TTL
// await this.redisClient.set(key, JSON.stringify(value), { EX: expired });
console.log(`Setting cache for key: ${key}`);
}
async get(key: string): Promise<any> {
// const value = await this.redisClient.get(key);
// return value ? JSON.parse(value) : null;
console.log(`Getting cache for key: ${key}`);
return null;
}
async has(key: string): Promise<boolean> {
// const exists = await this.redisClient.exists(key);
// return exists === 1;
console.log(`Checking cache for key: ${key}`);
return false;
}
}
const client = new Client({
appId: 'YOUR_APP_ID',
appSecret: 'YOUR_APP_SECRET',
cache: new RedisCache(),
});多实例Client的缓存隔离 从 SDK v1.10.0 版本开始,默认的内存缓存已支持基于 App ID 的 命名空间 隔离。这意味着每个 Client 实例会自动拥有独立的缓存空间,您无需额外配置即可安全地在同一进程中使用多个 Client 实例。 如果您使用自定义缓存(如上例),请确保您的缓存 key 设计中也包含了 appId 作为隔离维度,以防止不同应用的 Token 发生混淆。SDK 传递给 cache.set 的 key 默认已包含 appId,您直接使用即可。
获取完整的响应信息(如 Log ID)
默认情况下,SDK 的 API 调用仅返回响应体中的 data 部分。如果您需要获取完整的响应信息,例如 headers(其中包含 X-Tt-Logid 用于问题排查),可以通过修改 axios 拦截器来实现。 差异说明:自定义拦截器与响应结构风险 SDK 允许通过修改 defaultHttpInstance 或传入自定义 httpInstance 来添加拦截器,但这两种方式在修改响应结构时存在不同级别的风险。
- 方式一(低风险):修改默认 实例 以获取额外信息 如果您只是想获取
logid等额外信息,然后继续返回resp.data,可以直接修改defaultHttpInstance。这种方式风险较低,因为 SDK 的 Token 管理逻辑最终仍然能拿到期望的data结构。 - 方式二(高风险):传入自定义 实例 并改变响应结构 如果您在初始化
Client时传入一个全新的httpInstance,并在其响应拦截器中彻底改变了返回结构(例如,返回resp而不是resp.data),这会破坏 SDK 内置的 Token 管理机制,导致 Token 获取和刷新失败。 结论:除非您完全理解 SDK 内部机制并打算自行管理 Token,否则不要在自定义httpInstance的响应拦截器中改变最终返回给 SDK 的数据结构。 示例:仅获取 Log ID ,不破坏原有流程
import { defaultHttpInstance } from '@larksuiteoapi/node-sdk';
// 清空默认的响应拦截器
defaultHttpInstance.interceptors.response.handlers = [];
// 添加一个新的拦截器,返回完整的响应对象
defaultHttpInstance.interceptors.response.use((resp) => {
// resp 对象包含 headers, status, data 等完整信息
console.log('Log ID:', resp.headers['x-tt-logid']);
// 您可以在这里进行自定义处理,但最后必须返回 data 以确保 SDK 正常工作
return resp.data;
});
// 在此之后的所有 API 请求都将经过新的拦截器批量添加自定义请求头
如果您需要为所有发出的 API 请求统一添加自定义 Header(例如,用于网关验证、链路追踪),可以利用 request 拦截器实现。
import { defaultHttpInstance } from '@larksuiteoapi/node-sdk';
defaultHttpInstance.interceptors.request.use(
(req) => {
if (req.headers) {
req.headers['X-Custom-Header'] = 'Your-Value';
}
return req;
}
);如何定位语义化方法?
SDK 的核心优势之一是提供了与飞书开放平台 API Explorer 一致的语义化调用方法。您可以根据 API 文档或调试台的 URL 路径来拼接对应的方法名,其结构遵循 client.{project}.{resource}.{method}。 示例:获取用户信息的 API https://open.feishu.cn/open-apis/contact/v3/users/:user_id:
project:contactversion:v3resource:usermethod:get(HTTP 方法) 对应的 SDK 调用为client.contact.v3.user.get()。 实践建议:- 最快方式:在 API Explorer 页面的代码示例中,可以直接找到对应的 Node.js SDK 用法。
- 版本注意:如果某个 API 在
client.{project}下找不到,请尝试加上版本号,如client.{project}.{version}。
API 多版本支持
从 v1.22.0 版本开始,SDK 支持了 API 的多版本调用。当一个 API 有多个版本时(例如 v1, v2),建议显式指定版本号进行调用,以使用最新的功能和定义。
// 调用 v1 版本的创建任务接口
await client.task.v1.task.create({ /* ... */ });
// 调用 v2 版本的创建任务接口
await client.task.v2.task.create({ /* ... */ });说明:为了保持向后兼容,不带版本号的调用方式(如 client.task.comment.create)将继续指向 v1.22.0 版本发布时所固化的历史版本。
如何处理数组形式的 Query 参数?
对于某些 GET 请求,其查询参数(Query Parameter)可能是一个数组,例如批量获取用户信息时传递多个 user_id。 解决方案:
- 方案一(推荐) :升级 SDK 到
1.37.2或更高版本。从该版本开始,SDK 内部已自动处理数组参数的序列化,您可以直接在params中传递数组。 - 方案二(旧版 SDK ) :如果暂时无法升级,可以手动调用
client.request方法,并提供自定义的paramsSerializer函数,将数组参数“拍平”。
import qs from 'qs';
// 仅适用于无法升级 SDK 的旧版处理方式
await client.request({
url: '/open-apis/contact/v3/users/batch',
method: 'GET',
params: {
user_ids: ['ou_xxx', 'ou_yyy'], // 直接传递数组
},
// 使用 qs 库来序列化参数
paramsSerializer: params => {
return qs.stringify(params, { arrayFormat: 'repeat' });
},
});SDK未覆盖的 API 如何调用?
尽管 SDK 的目标是覆盖所有飞书开放平台 API,但由于文档结构、发布周期或灰度测试等原因,仍有少量 API 未被包含。 对于这种情况,您可以使用通用的 client.request 方法来发起调用,它提供了与 axios 类似的配置项。
const response = await client.request({
method: 'POST',
url: '/open-apis/some/unsupported/api/v1',
data: {
// request body
},
params: {
// query params
}
});在 Cloudflare Workers 等非 Node.js 环境中使用
Cloudflare Workers、Deno 等现代 Serverless 环境基于 Fetch API,与标准的 Node.js 环境不同。axios(SDK 的底层依赖)的默认 adapter 在这些环境下无法工作,会导致 adapter is not a function 的错误。 解决方案:您需要手动为 axios 实例更换一个兼容 Fetch API 的适配器,例如 @haverstack/axios-fetch-adapter。 差异说明:两种适配方式 有两种方式可以实现对 Cloudflare Workers 的适配,它们效果相同,您可以根据自己的代码风格和是否需要更多自定义 axios 功能来选择。
- 方式一(推荐):传入自定义
httpInstance这种方式更灵活,允许您在创建axios实例时进行更多配置(如设置超时、添加拦截器等)。 - 方式二:直接修改默认 实例 的
adapter这种方式更直接,如果您只需要更换adapter而不做其他改动,代码会更简洁。 方式一:传入自定义httpInstance(推荐)
import { Client } from '@larksuiteoapi/node-sdk';
import axios from 'axios';
import fetchAdapter from '@haverstack/axios-fetch-adapter';
// 1. 创建一个配置了 fetch adapter 的 axios 实例
const customHttpInstance = axios.create({
adapter: fetchAdapter,
// 您还可以在此添加其他 axios 配置
});
// 2. 初始化 Client 时传入该实例
const client = new Client({
appId: 'YOUR_APP_ID',
appSecret: 'YOUR_APP_SECRET',
httpInstance: customHttpInstance,
});
// 现在 client 可以在 Cloudflare Workers 环境中正常发起请求方式二:直接修改默认 实例 的 adapter
import { Client } from '@larksuiteoapi/node-sdk';
import fetchAdapter from '@haverstack/axios-fetch-adapter';
const client = new Client({
appId: 'YOUR_APP_ID',
appSecret: 'YOUR_APP_SECRET',
});
// 直接更换默认 HTTP 实例的适配器
client.httpInstance.defaults.adapter = fetchAdapter;
// 现在 client 同样可以正常发起请求Deno 支持现状
当前 SDK 主要为 Node.js 环境设计,并未正式支持 Deno。主要障碍是底层依赖(如 axios)的打包和环境兼容性问题。社区有通过 import_map 强制指定 axios 兼容版本的临时方案,但这并非官方支持,且可能随依赖更新而失效。 如果您需要在 Deno 环境中调用飞书 API,现阶段更稳妥的选择是直接使用 Deno 内置的 fetch API 进行调用。
分页与迭代器
SDK 为许多支持分页的 list 接口提供了便捷的迭代器封装(如 client.im.chat.listWithIterator()),可以自动处理分页逻辑。 但是,并非所有分页接口都支持此特性。如果某个 list 接口没有提供对应的 ...WithIterator 方法,这通常是因为其 API 定义不符合自动生成迭代器的规范。在这种情况下,您需要手动进行分页轮询:
async function listAllItemsManually(client) {
let allItems = [];
let pageToken: string | undefined = undefined;
let hasMore = true;
while (hasMore) {
const res = await client.im.chat.list({ // 假设此接口无迭代器
params: {
page_size: 100,
page_token: pageToken,
},
});
if (res.data.items) {
allItems = allItems.concat(res.data.items);
}
hasMore = res.data.has_more;
pageToken = res.data.page_token;
}
return allItems;
}何时使用手动分页 vs. 语义化方法?
- 优先使用迭代器:如果
client.{...}.listWithIterator方法存在,请始终优先使用它,代码更简洁且不易出错。 - 备选手动 轮询:仅在迭代器方法缺失时,采用上述手动分页轮询的模式。
注册与处理事件
EventDispatcher 是处理所有事件订阅的核心。您可以通过 register 方法为不同类型的事件注册异步处理函数,并在您的 Web 框架中调用 invoke 方法进行分发。
import { Client, EventDispatcher } from '@larksuiteoapi/node-sdk';
import express from 'express';
// 1. 初始化 Client 和 EventDispatcher
const client = new Client({ /* ... */ });
const eventDispatcher = new EventDispatcher({
verificationToken: 'YOUR_VERIFICATION_TOKEN',
encryptKey: 'YOUR_ENCRYPT_KEY', // 如果开启了加密
}).register({
// 2. 注册消息接收事件处理器
'im.message.receive_v1': async (event) => {
console.log('Received message event:', event);
const { message } = event;
// 在此处理您的业务逻辑,例如回复消息
if (message.message_type === 'text') {
await client.im.message.reply({
path: { message_id: message.message_id },
data: {
content: JSON.stringify({ text: '消息已收到!' }),
msg_type: 'text',
},
});
}
return { success: true };
},
'contact.user.created_v3': async (data) => {
console.log('New user created:', data);
}
});
// 3. 在您的 Web 框架中调用
const app = express();
app.use(express.json());
app.post('/webhook/event', (req, res) => {
const data = { headers: req.headers, body: req.body };
// 建议:先响应 200,再异步处理,避免超时重试
res.status(200).send('OK');
eventDispatcher.invoke(data).catch(err => {
console.error('Event processing error:', err);
});
});
app.listen(3000, () => console.log('Server is running on port 3000'));URL 验证挑战(Challenge)
当您在开放平台配置事件订阅的 Request URL 时,飞书会向该 URL 发送一个 url_verification 类型的 challenge 请求。SDK 内置的 EventDispatcher 或框架适配器(如 Express, Koa)会自动处理此过程,您无需编写任何特殊代码。只要您的服务能正确接收请求并将其传递给 SDK,验证就会自动通过。
避免事件重复推送与事件去重
飞书的事件推送机制要求您的服务在收到事件后的 3 秒内 返回 HTTP 200 状态码。如果超时未收到响应,飞书会认为推送失败并进行重试,这会导致您的业务逻辑被重复执行。 综合解决方案:
- 快速响应, 异步 处理:在您的 HTTP 路由处理函数中,立即响应 200 OK,然后将事件数据传递给后台进行异步处理。这可以从根本上避免因处理耗时过长导致的超时重试。
- 业务侧 幂等 设计:即使快速响应,网络抖动等原因仍可能导致事件重复。因此,业务侧进行幂等性设计至关重要。飞书的每个事件都包含一个唯一标识符,可用于去重:
- V2.0 协议事件:
header.event_id - V1.0 协议事件:
uuid示例:结合快速响应和内存去重(生产环境建议使用 Redis )
- V2.0 协议事件:
const processedEventIds = new Set<string>();
// 在事件处理器中增加去重逻辑
eventDispatcher.register({
'im.message.receive_v1': async (event) => {
const eventId = event.header.event_id;
if (processedEventIds.has(eventId)) {
console.log(`Event ${eventId} already processed, skipping.`);
return;
}
// ... 执行您的业务逻辑 ...
processedEventIds.add(eventId);
// 设置一个定时器来清理旧的 ID,防止内存无限增长
setTimeout(() => processedEventIds.delete(eventId), 5 * 60 * 1000); // 5 分钟后清理
},
});处理未在 SDK 中定义的事件
由于文档同步延迟或版本较老,某些事件类型可能未在 SDK 的 TypeScript 定义中提供。此时,您可以直接将事件的字符串名称作为 key 进行注册。这在运行时是有效的,只是在编译时会有类型检查错误。 差异说明:处理未知事件的两种方式
- 方式一(简单直接):使用
@ts-ignore这是最快捷的方式,只需在注册的代码行上方添加// @ts-ignore即可忽略 TypeScript 的类型检查错误。 - 方式二(类型安全):使用泛型 如果您希望在处理未知事件时也能获得一定的类型提示,可以在
.register()方法上使用泛型,手动定义事件体的接口。 方式一:使用@ts-ignore
eventDispatcher.register({
// @ts-ignore
'approval.instance.status_changed_v4': async (data: any) => {
console.log('Handling custom event:', data);
}
});方式二:使用泛型
interface MyCustomEventData {
// 根据实际事件体定义接口
approval_code: string;
status: string;
}
eventDispatcher.register<{ 'approval.instance.create_v4'?: MyCustomEventData }>({
'approval.instance.create_v4': async (data) => {
// 此时 `data` 会被推断为 MyCustomEventData 类型
console.log(data.approval_code);
}
});发送与更新消息卡片
- 发送卡片:调用
client.im.message.create,将msg_type设置为interactive,并将卡片 JSON 结构字符串化后作为content发送。 - 更新卡片:对于已发送的交互式卡片,可以使用
client.im.message.patch方法通过message_id对其进行更新。这对于实现延迟响应或多步交互非常有用。
// 示例:发送并随后更新一张卡片
async function sendAndUpdateCard(chatId: string) {
// 1. 发送初始卡片
const initialCard = { /* ... 卡片 JSON ... */ };
const createRes = await client.im.message.create({
params: { receive_id_type: 'chat_id' },
data: {
receive_id: chatId,
msg_type: 'interactive',
content: JSON.stringify(initialCard),
},
});
const messageId = createRes.data?.message_id;
if (!messageId) return;
// 2. 延迟 5 秒后,使用 patch 方法更新卡片
setTimeout(async () => {
const updatedCard = { /* ... 新的卡片 JSON ... */ };
await client.im.message.patch({
path: { message_id: messageId },
data: { content: JSON.stringify(updatedCard) },
});
console.log('Card updated successfully.');
}, 5000);
}卡片交互回调验签失败
如果您使用了新版消息卡片(Interactive Message Card 2.0),并遇到了交互回调的验签失败问题,这通常是因为 SDK 版本过低。 解决方案:请将 @larksuiteoapi/node-sdk 升级到 1.27.0 或更高版本。该版本已兼容新版卡片的验签逻辑。
// 检查当前版本
npm list @larksuiteoapi/node-sdk
// 升级到最新版本
npm install @larksuiteoapi/node-sdk@latestfields 字段的格式:使用对象而非 Map
在创建(create)或更新(update/batchUpdate)多维表格记录时,fields 字段应为一个 JavaScript 普通对象 (Object) ,其 key 为字段名,value 为对应的值。
await client.bitable.appTableRecord.create({
path: {
app_token: 'YOUR_APP_TOKEN',
table_id: 'YOUR_TABLE_ID',
},
data: {
fields: {
"文本字段": "这是一个字符串",
"数字字段": 123,
"单选字段": "选项A",
},
},
});注意:API Explorer 或旧文档中可能存在使用 Map 对象的错误示例,这是不正确的。请始终使用普通对象。
清空单元格:使用 null
要清空一个或多个单元格的内容,可以在更新记录时将对应字段的值设置为 null。 注意:将字段值设置为 undefined 是无效的。该字段在请求中会被忽略,无法达到清空单元格的效果。
await client.bitable.appTableRecord.update({
path: {
app_token: 'YOUR_APP_TOKEN',
table_id: 'YOUR_TABLE_ID',
record_id: 'recXXXXX',
},
data: {
fields: {
"需要清空的字段": null,
"另一个需要清空的字段": null,
},
},
});处理多维表格相关事件
多维表格的事件(如记录变更 drive.file.bitable_record_changed_v1)同样通过 EventDispatcher 订阅和处理。如果某些事件在 SDK 的类型定义中缺失,您可以参考上文“处理未在 SDK 中定义的事件”一节进行手动注册。 要了解 field_value 中各种字段类型对应的 JSON 结构,请参考开放平台的官方文档:数据结构概述。
文件上传:优先使用 Stream
无论是上传到云空间(drive.file.uploadAll)还是作为消息附件发送图片(im.image.create),都建议优先使用文件流 (Stream) 而非 Buffer。Stream 的内存效率更高,尤其适合大文件,可以有效避免内存溢出。
import * as fs from 'fs';
const filePath = '/path/to/your/file.png';
const fileStream = fs.createReadStream(filePath);
const fileSize = fs.statSync(filePath).size;
await client.drive.file.uploadAll({
data: {
file_name: 'my-image.png',
parent_type: 'explorer',
parent_node: 'YOUR_FOLDER_TOKEN',
size: fileSize,
file: fileStream,
},
});文件下载/导出:处理返回的流
对于文件下载(drive.file.download)或表格导出等操作,从 SDK v1.37.0 版本开始,已支持直接获取文件流 (ReadableStream)。您可以直接将其 pipe 到一个文件写入流中,从而高效处理大文件。
import * as fs from 'fs';
const resp = await client.im.file.get({
path: {
file_key: 'file key',
},
});
const readableStream = resp.getReadableStream();
const writableStream = fs.createWriteStream('file url');
readableStream.pipe(writableStream);Node.js v18+ 文件上传兼容性策略
在 node-sdk 的 v1.17.1 之前的版本中,使用 Node.js v18+ 并通过 Buffer 上传文件时,可能会遇到 source.on is not a function 的错误。这是因为 Node.js v18 引入了原生的 Blob 类型,导致 axios 的依赖库 form-data 内部逻辑出现兼容性问题。 最佳实践与解决方案
- 升级 SDK (推荐) :将
@larksuiteoapi/node-sdk升级到1.17.1或更高版本,该版本已从内部解决此兼容性问题。 - 使用 Stream (推荐) :如上所述,始终优先使用
fs.createReadStream进行文件上传。这不仅是最佳实践,也能从根本上规避不同 Node.js 版本间的底层差异。 - 缓解措施 (如无法升级) :如果暂时无法升级 SDK,可以将
Buffer手动转换为Stream作为临时解决方案。 缓解措施示例:Buffer 转 Stream
import { Duplex } from 'stream';
function bufferToStream(buffer: Buffer): Duplex {
const stream = new Duplex();
stream.push(buffer);
stream.push(null);
return stream;
}
// 在上传时使用
// const fileBuffer = fs.readFileSync(filePath);
// const fileStream = bufferToStream(fileBuffer);
// ... 然后在 API 调用中传递 fileStream如何实现自定义日志记录?
您可以通过为 Client 提供自定义的 logger 来接管 SDK 的日志输出。logger 需要实现 debug, info, warn, error 四个方法。
import { Client, ILogger, LogLevel } from '@larksuiteoapi/node-sdk';
class MyCustomLogger implements ILogger {
debug(message: any, ...args: any[]) { console.debug(`[SDK-DEBUG]`, message, ...args); }
info(message: any, ...args: any[]) { console.info(`[SDK-INFO]`, message, ...args); }
warn(message: any, ...args: any[]) { console.warn(`[SDK-WARN]`, message, ...args); }
error(message: any, ...args: any[]) { console.error(`[SDK-ERROR]`, message, ...args); }
}
const client = new Client({
appId: 'YOUR_APP_ID',
appSecret: 'YOUR_APP_SECRET',
logger: new MyCustomLogger(),
logLevel: LogLevel.DEBUG, // 设置日志级别,如 'debug'
});此外,结合自定义 axios 实例的拦截器,您可以实现更详细的 HTTP 请求级别日志。



