飞书开放平台提供了一系列服务端的原子api来实现多元化的功能,但在实际编码过程中感受不是很顺畅,原因在于使用这些api完成功能时,需要考虑很多额外的工作,如token的获取及其维护、数据加解密、请求的验签等等;再者,在实际编码过程中,少了函数调用的语义化描述,类型系统的支持,使得心智负担过重。
凡此种种,都使得整体的开发体验不佳,基于此,为了让开放能力变得易用,我们编写了该SDK,将所有冗长的逻辑内置处理,提供完备的类型系统,对外提供语义化的编程接口,提高编码体验。😙
以下是一些基于该sdk的官方教程:
-
开发文档:开放平台的开放接口的参考,开发者必看,可以使用搜索功能,高效的查询文档。更多介绍说明 。
-
开发者后台:开发者开发应用的管理后台,更多介绍说明 。
-
企业自建应用:应用仅仅可在本企业内安装使用,更多介绍说明 。
npm
npm install @larksuiteoapi/node-sdk
yarn
yarn add @larksuiteoapi/node-sdk
提供ECMAScript,CommonJS2个版本,支持原生Javascript和Typescript的使用,示例均以Typescript为例。
Typescript
import * as lark from '@larksuiteoapi/node-sdk';
CommonJS
const lark = require('@larksuiteoapi/node-sdk');
ECMAScript
import * as lark from '@larksuiteoapi/node-sdk';
飞书开放平台开放的所有 API 列表,可点击这里查看。
SDK提供了语义化的调用方式,只需要依据相关参数构造出client实例,接着使用其上的语义化方法(client.业务域.资源.方法)即可完成api调用,调用过程及调用结果均有完备的类型进行提示,如向群聊中发送消息:
import * as lark from '@larksuiteoapi/node-sdk';
const client = new lark.Client({
appId: 'app id',
appSecret: 'app secret',
appType: lark.AppType.SelfBuild,
domain: lark.Domain.Feishu,
});
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',
},
});
tips:
- 如果想调试某个api,可以点击注释中的链接进入api调试台进行调试:
- 如何获取语义化调用接口:点击这里
对于自建应用,可以使用下面的代码创建一个client:
import * as lark from '@larksuiteoapi/node-sdk';
const client = new lark.Client({
appId: 'app id',
appSecret: 'app secret'
});
对于商店应用,需要显示的指定appType为lark.AppType.ISV:
import * as lark from '@larksuiteoapi/node-sdk';
const client = new lark.Client({
appId: 'app id',
appSecret: 'app secret',
appType: lark.AppType.ISV,
});
使用创建好的商店应用的client发起api调用时,还需在请求时手动传递tenant_key,可以使用lark.withTenantKey来完成:
client.im.message.create({
params: {
receive_id_type: 'chat_id',
},
data: {
receive_id: 'chat_id',
content: JSON.stringify({text: 'hello world'}),
msg_type: 'text'
},
}, lark.withTenantKey('tenant key'));
参数 | 描述 | 类型 | 必须 | 默认 |
---|---|---|---|---|
appId | 应用的id | string | 是 | - |
appSecret | 应用的密码 | string | 是 | - |
domain | 应用的域,分为飞书(https://open.feishu.cn)、lark(https://open.larksuite.com)、其它(需要传递完整的域名) | Domain | string | 否 | Domain.Feishu |
httpInstance | sdk发送请求的http实例。sdk内部默认使用axios.create()构造出一个defaultHttpInstance来进行http调用。 | HttpInstance | 否 | defaultHttpInstance。可以从sdk中import它,在其上添加interceptors来完成业务需求。 |
loggerLevel | 日志级别 | LoggerLevel | 否 | info |
logger | - | Logger | 否 | - |
cache | 缓存器 | Cache | 否 | - |
disableTokenCache | 是否禁用缓存,如若禁用,则token等不会进行缓存,每次需要使用时都会重新拉取 | boolean | 否 | false |
appType | 应用的类型,分为商店应用或者自建应用 | AppType | 否 | AppType.SelfBuild |
helpDeskId | 服务台id | string | 否 | - |
helpDeskToken | 服务台token | string | 否 | - |
针对返回值以分页形式呈现的接口,对其提供了迭代器方式的封装(方法名后缀为WithIterator),提高易用性,消弭了根据page_token来反复获取数据的繁琐操作,如获取用户列表:
// 每次处理20条数据
for await (const items of await client.contact.user.listWithIterator({
params: {
department_id: '0',
page_size: 20,
},
})) {
console.log(items);
}
// 也可用next来手动控制迭代,每次取20条数据
const listIterator = await SDKClient.contact.user.listWithIterator({
params: {
department_id: '0',
page_size: 20,
},
});
const { value } = await listIterator[Symbol.asyncIterator]().next();
console.log(value);
当然也可以使用无迭代器封装的版本,这时候需要自己每次根据返回的page_token来手动进行分页调用。
和调用普通api的方式一样,按类型提示传递参数即可,内部封装了对文件上传的处理,如:
const res = await client.im.file.create({
data: {
file_type: 'mp4',
file_name: 'test.mp4',
file: fs.readFileSync('file path'),
},
});
对返回的二进制流进行了封装,消弭了对流本身的处理,只需调用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);
注意:流只能被消费一次,即如果使用了writeFile消费了流,则getReadableStream获取流会报错/获取到的流为空;如需消费多次流,可以使用getReadableStream获取流,然后读取流中的数据做缓存,将缓存的数据给消费方使用。
某些老版本的开放接口,无法生成对应的语义化调用方法,需要使用client上的request方法来进行手动调用:
import * as lark from '@larksuiteoapi/node-sdk';
const client = new lark.Client({
appId: 'app id',
appSecret: 'app secret',
appType: lark.AppType.SelfBuild,
domain: lark.Domain.Feishu,
});
const res = await client.request({
method: 'POST',
url: 'xxx',
data: {},
params: {},
});
在发送消息卡片信息时,会先在消息卡片搭建工具中搭建出消息卡片的模版,拿到生成的模版json,用数据替换其中内容相关的部分,将结果作为支持消息卡片api的参数来使用。如发送一个简单的具有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'
}
})
这样使用会有一个问题:如果消息卡片内容比较丰富,生成的模版json比较大,与之相关需要数据填充的内容部分也会比较多,手动维护比较繁琐。针对这个问题,开放平台提供了模版消息的能力,发送消息卡片时只需要提供模版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"
}
}
});
如果想要快速体验消息卡片,可以使用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调用过程中修改请求的参数,如携带一些header,自定义tenantToken等,则可以使用请求方法的第二个参数来进行修改:
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 | 设置tenant key |
withTenantToken | 设置tenant token |
withHelpDeskCredential | 是否在请求中带入服务台token |
withUserAccessToken | 设置access 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.withTenantToken('tenant token'));
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')
]));
飞书开放平台开放的所有事件列表,可点击这里查看。
针对事件处理的场景,我们所关心的仅是监听何种事件,以及事件发生后我们做些什么,其它诸如数据解密等工作是我们不想关心的。SDK提供了直观的方式来描述这部分逻辑:
- 构造事件处理器
EventDispatcher
的实例; - 在实例上注册需要监听的事件及其处理函数;
- 将实例和服务进行绑定;
EventDispatcher
内部会进行数据解密等操作,如果没有传递相关参数,则会自动忽略。
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;
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;
}
});
const server = http.createServer();
server.on('request', lark.adaptDefault('/webhook/event', eventDispatcher));
server.listen(3000);
参数 | 描述 | 类型 | 必须 | 默认 |
---|---|---|---|---|
encryptKey | 推送数据加密的key,开启加密推送时需要使用来进行数据解密 | string | 否 | - |
loggerLevel | 日志级别 | LoggerLevel | 否 | lark.LoggerLevel.info |
logger | - | Logger | 否 | - |
cache | 缓存器 | Cache | 否 | - |
注:有一些事件是v1.0版本且已经不在维护了,SDK保留了对其的支持,强烈建议使用与之功能相一致的新版事件代替。鼠标移动到相应事件订阅函数上即可看到相关文档:
SDK提供了针对experss的适配器,用于将eventDispatcher转化为express的中间件,可无缝与使用express编写的服务相结合(示例中的bodyParser的使用不是必须的,但社区大多用其来格式化body数据):
import * as lark from '@larksuiteoapi/node-sdk';
import express from 'express';
import bodyParser from 'body-parser';
const server = express();
server.use(bodyParser.json());
const eventDispatcher = new lark.EventDispatcher({
encryptKey: 'encryptKey',
}).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;
}
});
server.use('/webhook/event', lark.adaptExpress(eventDispatcher));
server.listen(3000);
SDK提供了针对koa的适配器,用于将eventDispatcher转化为koa的中间件,可无缝与使用koa编写的服务相结合(示例中的koaBody的使用不是必须的,但社区大多用其来格式化body数据):
import * as lark from '@larksuiteoapi/node-sdk';
import Koa from 'koa';
import koaBody from 'koa-body';
const server = new Koa();
server.use(koaBody());
const eventDispatcher = new lark.EventDispatcher({
encryptKey: 'encryptKey',
}).register({
'im.message.receive_v1': async (data) => {
const open_chat_id = data.message.chat_id;
const res = await client.im.message.create({
params: {
receive_id_type: 'chat_id',
},
data: {
receive_id: open_chat_id,
content: JSON.stringify({text: 'hello world'}),
msg_type: 'text'
},
});
return res;
},
});
server.use(lark.adaptKoa('/webhook/event', eventDispatcher));
server.listen(3000);
在使用koa来编写服务时,大多情况下会配合使用koa-router来对路由进行处理,因此SDK也提供了针对这一情况的适配:
import * as lark from '@larksuiteoapi/node-sdk';
import Koa from 'koa';
import Router from '@koa/router';
import koaBody from 'koa-body';
const server = new Koa();
const router = new Router();
server.use(koaBody());
const eventDispatcher = new lark.EventDispatcher({
encryptKey: 'encryptKey',
}).register({
'im.message.receive_v1': async (data) => {
const open_chat_id = data.message.chat_id;
const res = await client.im.message.create({
params: {
receive_id_type: 'chat_id',
},
data: {
receive_id: open_chat_id,
content: JSON.stringify({text: 'hello world'}),
msg_type: 'text'
},
});
return res;
},
});
router.post('/webhook/event', lark.adaptKoaRouter(eventDispatcher));
server.use(router.routes());
server.listen(3000);
如果要适配其它库编写的服务,目前需要自己来封装相应的适配器。将接收到的事件数据传递给实例化的eventDispatcher
的invoke方法进行事件的处理即可:
const data = server.getData();
const result = await dispatcher.invoke(data);
server.sendResult(result);
在配置事件请求地址时,开放平台会向请求地址推送一个application/json
格式的 POST请求,该POST请求用于验证所配置的请求地址的合法性,请求体中会携带一个challenge
字段,应用需要在 1 秒内,将接收到的challenge值原样返回给飞书开放平台。详见:文档
上面sdk提供出的适配器内部封装了这部分验证的逻辑,将options参数中的autoChallenge
字段设为true即可启用:
// adaptDefault
lark.adaptDefault('/webhook/event', eventDispatcher, {
autoChallenge: true,
});
// express
lark.adaptExpress(eventDispatcher, {
autoChallenge: true,
});
// koa
lark.adaptKoa('/webhook/event', eventDispatcher, {
autoChallenge: true,
});
// koa-router
router.post(
'/webhook/event',
lark.adaptKoaRouter(eventDispatcher, {
autoChallenge: true,
})
);
官方文档:文档
开发者通过集成飞书 SDK 与开放平台建立一条 WebSocket 全双工通道,当有事件回调发生时,开放平台会通过该通道向开发者发送消息。与传统的 Webhook 模式相比,长连接模式大大降低了接入成本,将原先 1 周左右的开发周期降低到 5 分钟。具体优势如下:
- 测试阶段无需使用内网穿透工具,通过长连接模式在本地开发环境中即可接收事件回调。
- 只在建连时进行鉴权,后续事件推送均为明文数据,无需开发者再处理解密和验签逻辑。
- 只需保证运行环境具备访问公网能力即可,无需提供公网 IP 或域名。
- 无需部署防火墙和配置白名单。
注意事项
- 与 Webhook 相同, 长连接模式下开发者接收到消息后,需要在 3 秒内处理完成,否则会触发超时重推。
- 消息推送为 集群模式,不支持广播,即如果同一应用部署了多个客户端,那么只有其中随机一个客户端会收到消息。
- 目前长连接模式仅支持事件订阅,不支持回调订阅。
SDK支持了该功能集成,1.24.0
及之后的版本可用,示例代码:
import * as Lark from '@larksuiteoapi/node-sdk';
const baseConfig = {
appId: 'xxx',
appSecret: 'xxx'
}
const client = new Lark.Client(baseConfig);
const wsClient = new Lark.WSClient({...baseConfig, loggerLevel: Lark.LoggerLevel.info});
wsClient.start({
eventDispatcher: new Lark.EventDispatcher({}).register({
'im.message.receive_v1': async (data) => {
const {
message: { chat_id, content}
} = data;
await client.im.v1.message.create({
params: {
receive_id_type: "chat_id"
},
data: {
receive_id: chat_id,
content: Lark.messageCard.defaultCard({
title: `reply: ${JSON.parse(content).text}`,
content: 'hello'
}),
msg_type: 'interactive'
}
});
}
})
});
对消息卡片的处理亦是对事件处理的一种,两者的不同点仅在于消息卡片的处理器用于响应用户与消息卡片交互所产生的事件,若处理器有返回值(返回值的数据结构理应为符合消息卡片结构所定义的结构),则返回值被用来更新被响应的消息卡片:
import http from 'http';
import * as lark from '@larksuiteoapi/node-sdk';
import type { InteractiveCardActionEvent, InteractiveCard } from '@larksuiteoapi/node-sdk';
const cardDispatcher = new lark.CardActionHandler(
{
encryptKey: 'encrypt key',
verificationToken: 'verification token'
},
async (data: InteractiveCardActionEvent) => {
console.log(data);
const newCard: InteractiveCard = {
// your new interactive card content
header: {},
elements: []
};
return newCard;
}
);
const server = http.createServer();
server.on('request', lark.adaptDefault('/webhook/card', cardDispatcher));
server.listen(3000);
参数 | 描述 | 类型 | 必须 | 默认 |
---|---|---|---|---|
encryptKey | 推送数据加密的key,开启加密推送时需要使用来进行数据解密 | string | 否 | - |
verificationToken | 安全校验,开启消息的安全校验时需要使用 | string | 否 | - |
loggerLevel | 日志级别 | LoggerLevel | 否 | LoggerLevel.info |
logger | - | Logger | 否 | - |
cache | 缓存器 | Cache | 否 | - |
解密。如果配置了加密推送,开放平台会推送加密的数据,这时候需要对数据进行解密处理,调用此方法可以便捷的进行解密。(一般情况下,SDK中内置了解密逻辑,不需要手动进行处理)。
import * as lark from '@larksuiteoapi/node-sdk';
new lark.AESCipher('encrypt key').decrypt('content');
MIT
点击服务端SDK 页面右上角【这篇文档是否对你有帮助?】提交反馈😘