Skip to content

Latest commit

 

History

History
642 lines (561 loc) · 24.5 KB

README.zh.md

File metadata and controls

642 lines (561 loc) · 24.5 KB

飞书开放接口SDK

GitHub Repo stars NPM Downloads NPM License

English

概述

飞书开放平台提供了一系列服务端的原子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调用

飞书开放平台开放的所有 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

对于自建应用,可以使用下面的代码创建一个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'));

Client构造参数:

参数 描述 类型 必须 默认
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的参数来使用。如发送一个简单的具有titlecontent的消息卡片:

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_idtemplate_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提供了直观的方式来描述这部分逻辑:

  1. 构造事件处理器EventDispatcher的实例;
  2. 在实例上注册需要监听的事件及其处理函数;
  3. 将实例和服务进行绑定;

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);

EventDispatcher构造参数

参数 描述 类型 必须 默认
encryptKey 推送数据加密的key,开启加密推送时需要使用来进行数据解密 string -
loggerLevel 日志级别 LoggerLevel lark.LoggerLevel.info
logger - Logger -
cache 缓存器 Cache -

注:有一些事件是v1.0版本且已经不在维护了,SDK保留了对其的支持,强烈建议使用与之功能相一致的新版事件代替。鼠标移动到相应事件订阅函数上即可看到相关文档:

和express结合

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);

和koa结合

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-router结合

在使用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);

challenge校验

在配置事件请求地址时,开放平台会向请求地址推送一个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 或域名。
  • 无需部署防火墙和配置白名单。

注意事项

  1. 与 Webhook 相同, 长连接模式下开发者接收到消息后,需要在 3 秒内处理完成,否则会触发超时重推。
  2. 消息推送为 集群模式,不支持广播,即如果同一应用部署了多个客户端,那么只有其中随机一个客户端会收到消息。
  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);

CardActionHandler构造参数

参数 描述 类型 必须 默认
encryptKey 推送数据加密的key,开启加密推送时需要使用来进行数据解密 string -
verificationToken 安全校验,开启消息的安全校验时需要使用 string -
loggerLevel 日志级别 LoggerLevel LoggerLevel.info
logger - Logger -
cache 缓存器 Cache -

工具方法

AESCipher

解密。如果配置了加密推送,开放平台会推送加密的数据,这时候需要对数据进行解密处理,调用此方法可以便捷的进行解密。(一般情况下,SDK中内置了解密逻辑,不需要手动进行处理)。

import * as lark from '@larksuiteoapi/node-sdk';

new lark.AESCipher('encrypt key').decrypt('content');

Examples

快速开发自动回复机器人

Blog

ISV(商店应用)开发指南

许可协议

MIT

联系我们

点击服务端SDK 页面右上角【这篇文档是否对你有帮助?】提交反馈😘