# 使用长连接接收事件

长连接是[飞书 SDK](https://open.feishu.cn/document/ukTMukTMukTM/uETO1YjLxkTN24SM5UjN) 内提供的能力，你可以在本地服务器集成飞书 SDK， 与开放平台建立一条 WebSocket 全双工通道（你的服务器需要能够访问公网）。后续当应用订阅的事件发生时，开放平台会通过该通道向你的服务器发送事件消息。

## 功能优势

相较于传统的 Webhook 模式，长连接模式大大降低了接入成本，将原先 1 周左右的开发周期降低到 5 分钟。具体优势如下：

- **开发调用方便快捷**

只需保证运行环境具备访问公网的能力即可，无需提供公网 IP 或域名、无需使用内网穿透工具，通过长连接模式在本地开发环境中即可接收事件消息，后续在线上部署本地服务后也可以直接生效。
- **加密传输，无需额外处理加密解密逻辑**

长连接方式内置了通信通信加密和鉴权的逻辑，事件数据网络上传输时为加密传输，对于开发者而言无需额外解密、验签逻辑，也无需部署防火墙和配置白名单。

## 注意事项

在开始配置之前，你需要确保已了解以下注意事项：

- 长连接模式仅支持企业自建应用。商店应用事件订阅方式参考[将事件发送至开发者服务器](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/choose-a-subscription-mode/send-notifications-to-developers-server)。
- 长连接模式下接收到消息后，需要在 3 秒内处理完成且不抛出异常，否则会触发超时重推机制。
- 每个应用最多建立 50 个连接（在配置长连接时，每初始化一个 client 就是一个连接）。
- 长连接模式的消息推送为 **集群模式**，不支持广播，即如果同一应用部署了多个客户端（client），那么只有 **其中随机一个** 客户端会收到消息。

## 上手体验

开放平台提供了一键开发自动回复机器人的场景体验教程，你可以前往体验基于[接收消息](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/events/receive)事件所搭建的机器人应用，详情参见：

- [一键快速开发自动回复机器人](https://open.feishu.cn/document/uAjLw4CM/uMzNwEjLzcDMx4yM3ATM/develop-an-echo-bot/introduction)
- [自动回复机器人示例代码解释](https://open.feishu.cn/document/uAjLw4CM/uMzNwEjLzcDMx4yM3ATM/develop-an-echo-bot/explanation-of-example-code)

## 步骤一：集成 SDK

以下提供了飞书 SDK 的 Golang、Python、Java、Node.js 示例代码，代码内以[接收消息](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/events/receive)事件为例进行配置。通过飞书 SDK 除了可以实现事件订阅，还可以实现 API 调用、订阅回调，关于 SDK 详细介绍参见[服务端 SDK](https://open.feishu.cn/document/ukTMukTMukTM/uETO1YjLxkTN24SM5UjN)。

### Golang

#### 安装 SDK

```
go get -u github.com/larksuite/oapi-sdk-go/v3
```

#### 示例代码

```go
package main
import (
   "context"
   "fmt"
   larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
   larkevent "github.com/larksuite/oapi-sdk-go/v3/event"
   "github.com/larksuite/oapi-sdk-go/v3/event/dispatcher"
   larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
   larkws "github.com/larksuite/oapi-sdk-go/v3/ws"
)
func main() {
   // 注册事件回调，OnP2MessageReceiveV1 为接收消息 v2.0；OnCustomizedEvent 内的 message 为接收消息 v1.0。
   eventHandler := dispatcher.NewEventDispatcher("", "").
      OnP2MessageReceiveV1(func(ctx context.Context, event *larkim.P2MessageReceiveV1) error {
         fmt.Printf("[ OnP2MessageReceiveV1 access ], data: %s\n", larkcore.Prettify(event))
         return nil
      }).
      OnCustomizedEvent("这里填入你要自定义订阅的 event 的 key，例如 out_approval", func(ctx context.Context, event *larkevent.EventReq) error {
         fmt.Printf("[ OnCustomizedEvent access ], type: message, data: %s\n", string(event.Body))
         return nil
      })
   // 创建Client
   cli := larkws.NewClient("YOUR_APP_ID", "YOUR_APP_SECRET",
      larkws.WithEventHandler(eventHandler),
      larkws.WithLogLevel(larkcore.LogLevelDebug),
   )
   // 启动客户端
   err := cli.Start(context.Background())
   if err != nil {
      panic(err)
   }
}
```

代码实现说明：
1. 通过 dispatcher.NewEventDispatcher() 初始化事件处理器（eventHandler），注意**两个参数必须填空字符串**。
1. 通过 eventHandler 的 OnXXXX() 方法处理不同的事件。上述示例中分别监听了「接收消息 v1.0」和「接收消息 v2.0」两个事件。

**说明**：开放平台提供的事件包括 v1.0 和 v2.0 两个版本。两个版本的结构不同。在处理事件时，可通过[事件列表](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-list)文档或在开发者后台[添加事件](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/subscription-event-case)时，获取事件的类型与版本信息。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/4dbc1cd97d95f1af949354ce3c350461_4PX46ts1Hs.png?height=380&lazyload=true&maxWidth=600&width=1666)

如果是 v1.0 事件，则注册服务器时需要查找并使用 OnCustomizedEvent 方法；如果是 v2.0 事件，则注册服务器时需要查找并使用 onP2xxxx 开头的方法。
例如，使用 `onP2MessageReceiveV1` 注册接收消息事件回调时，`P2`便对应的是 v2.0 版本事件结构。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/8bbadcadbadcf70bb9cb4c44afea25cd_z7bPrzvx1l.png?height=286&lazyload=true&maxWidth=600&width=1598)

1. 通过 larkws.NewClient() 初始化长连接客户端，必填参数为应用的 APP_ID 和 APP_SECRET，需登录[开发者后台](https://open.feishu.cn/app)，在应用详情页的 **凭证与基础信息** > **应用凭证** 区域获取。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/f7f89950be7e57c2760a8b5b1f5e17c9_NouE7PqEkQ.png?height=524&lazyload=true&maxWidth=600&width=3594)
1. 可选参数传入 eventHandler，同时可设置日志级别。
1. 通过 cli.Start() 启动客户端，如连接成功，控制台会打印 "connected to wss://xxxxx"，主线程将阻塞，直到进程结束。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/b1b3d6b4d3a724fa7408f5c71a11d19e_ysuov6r3Bn.png?height=338&lazyload=true&maxWidth=600&width=2286)

### Python

#### 安装 SDK

```
pip install lark-oapi -U
```

#### 示例代码

```python
import lark_oapi as lark

## P2ImMessageReceiveV1 为接收消息 v2.0；CustomizedEvent 内的 message 为接收消息 v1.0。

def do_p2_im_message_receive_v1(data: lark.im.v1.P2ImMessageReceiveV1) -> None:
    print(f'[ do_p2_im_message_receive_v1 access ], data: {lark.JSON.marshal(data, indent=4)}')
def do_message_event(data: lark.CustomizedEvent) -> None:
    print(f'[ do_customized_event access ], type: message, data: {lark.JSON.marshal(data, indent=4)}')
event_handler = lark.EventDispatcherHandler.builder("", "") \
    .register_p2_im_message_receive_v1(do_p2_im_message_receive_v1) \
    .register_p1_customized_event("这里填入你要自定义订阅的 event 的 key，例如 out_approval", do_message_event) \
    .build()
def main():
    cli = lark.ws.Client("YOUR_APP_ID", "YOUR_APP_SECRET",
                         event_handler=event_handler,
                         log_level=lark.LogLevel.DEBUG)
    cli.start()
if __name__ == "__main__":
    main()
```

代码实现说明：

1. 通过 lark.EventDispatcherHandler.builder() 初始化事件处理器（event_handler），该方法的两个参数必须填空字符串。

2. 通过 event_handler 的 `register_{事件版本}_{事件类型 或 customized_event}` 方法处理不同的事件。

在开发者后台应用详情页添加事件时（具体操作可参见[添加事件](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/subscription-event-case)），可获取事件的类型与版本信息。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/4dbc1cd97d95f1af949354ce3c350461_bidnBwXPPi.png?height=380&lazyload=true&maxWidth=600&width=1666)

**v2.0 版本的事件**

- 使用`data: lark.im.v1.P2ImMessageReceiveV1` 指定所选事件，其中：

- `lark.im.v1`表示对应的业务域以及版本，可通过事件类型的前缀获取业务域代码，例如 `im.message.receive_v1` 对应的业务域代码为 `im`。

- `P2ImMessageReceiveV1` 中，**P2** 表示事件版本为 v2.0，**ImMessageReceiveV1** 对应事件类型 `im.message.receive_v1`。

- 使用 `.register_p2_im_message_receive_v1()` 处理事件，其中：

- **p2** 表示事件版本为 v2.0。

- **im_message_receive_v1** 对应事件类型 `im.message.receive_v1`。
        **说明**：代码中的方法格式与事件类型不一致，注意甄别格式上的差异，避免因格式问题导致程序运行失败。  

**v1.0 版本的事件**

- 固定使用 `data: lark.CustomizedEvent` 方法，表示处理自定义事件。
        - 使用 `.register_p1_customized_event("这里填入你要自定义订阅的事件类型，例如 out_approval", do_message_event)` 处理事件：
            - **p1** 表示事件版本为 v1.0。
            - **customized_event** 为固定的方法名称。
            - customized_event 方法内需要传入事件 Key（String 类型），例如[外出审批](https://open.feishu.cn/document/ukTMukTMukTM/uIDO24iM4YjLygjN/event/out-of-office)的事件类型为 out_approval。

你也可以参考代码片段注释了解如何配置：

```python
    ## P2ImMessageReceiveV1：
    ## P2 对应 v2.0 版本，
    ## ImMessageReceiveV1 对应接收消息事件类型 im.message.receive_v1。
    def do_p2_im_message_receive_v1(data: lark.im.v1.P2ImMessageReceiveV1) -> None:
        print(f'[ do_p2_im_message_receive_v1 access ], data: {lark.JSON.marshal(data, indent=4)}')

## 如果是 v1.0 版本事件，需要使用 lark.CustomizedEvent
    def do_message_event(data: lark.CustomizedEvent) -> None:
        print(f'[ do_customized_event access ], type: message, data: {lark.JSON.marshal(data, indent=4)}')

event_handler = lark.EventDispatcherHandler.builder("", "") \
        ## register_p2 对应 v2.0 版本，
        ## im_message_receive_v1 对应接收消息事件类型 im.message.receive_v1。
        .register_p2_im_message_receive_v1(do_p2_im_message_receive_v1) \
        ## register_p1 对应 v1.0 版本，
        ## 参数值需要传入事件类型。例如 out_approval
        .register_p1_customized_event("这里填入你要自定义订阅的 event 的 key，例如 out_approval", do_message_event) \
        .build()
    ```

3. 通过 lark.ws.Client() 初始化长连接客户端，必填参数为应用的 APP_ID 和 APP_SECRET，需登录[开发者后台](https://open.feishu.cn/app)，在应用详情页的 **凭证与基础信息** > **应用凭证** 区域获取。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/f7f89950be7e57c2760a8b5b1f5e17c9_QVldl2kW9x.png?height=524&lazyload=true&maxWidth=600&width=3594)

4. 可选参数传入 event_handler，同时可设置日志级别。
5. 通过 cli.start() 启动客户端，如连接成功，控制台会打印 "connected to wss://xxxxx"，主线程将阻塞，直到进程结束。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/2085839331117b6f9817482a5493eba6_mDSqdoypa5.png?height=264&lazyload=true&maxWidth=600&width=2488)

### Java

#### 安装 SDK

SDK 最新版本可以在[这里](https://mvnrepository.com/artifact/com.larksuite.oapi/oapi-sdk)查看。

```xml
<dependencies>
  ...
  <dependency>
    <groupId>com.larksuite.oapi</groupId>
    <artifactId>oapi-sdk</artifactId>
    <version>{latest version}</version>
  </dependency>
  ...
</dependencies>
```

#### 示例代码

```java
package com.lark.oapi.sample.ws;
import com.lark.oapi.core.request.EventReq;
import com.lark.oapi.core.utils.Jsons;
import com.lark.oapi.event.CustomEventHandler;
import com.lark.oapi.event.EventDispatcher;
import com.lark.oapi.service.im.ImService;
import com.lark.oapi.service.im.v1.model.P2MessageReceiveV1;
import com.lark.oapi.ws.Client;
import java.nio.charset.StandardCharsets;
public class Sample {
          // onP2MessageReceiveV1 为接收消息 v2.0；onCustomizedEvent 内的 message 为接收消息 v1.0。
    private static final EventDispatcher EVENT_HANDLER = EventDispatcher.newBuilder("", "")
            .onP2MessageReceiveV1(new ImService.P2MessageReceiveV1Handler() {
                @Override
                public void handle(P2MessageReceiveV1 event) throws Exception {
                    System.out.printf("[ onP2MessageReceiveV1 access ], data: %s\n", Jsons.DEFAULT.toJson(event.getEvent()));
                }
            })
            .onCustomizedEvent("这里填入你要自定义订阅的 event 的 key，例如 out_approval", new CustomEventHandler() {
                @Override
                public void handle(EventReq event) throws Exception {
                    System.out.printf("[ onCustomizedEvent access ], type: message, data: %s\n", new String(event.getBody(), StandardCharsets.UTF_8));
                }
            })
            .build();
    public static void main(String[] args) {
        Client cli = new Client.Builder("YOUR_APP_ID", "YOUR_APP_SECRET")
                .eventHandler(EVENT_HANDLER)
                .build();
        cli.start();
    }
}
```

代码实现说明：
1. 通过 EventDispatcher.*newBuilder*() 初始化事件处理器（*EVENT_HANDLER*），注意**两个参数必须填空字符串**。
1. 通过 *EVENT_HANDLER* 的 onXXXX() 方法处理不同的事件。上述示例中分别监听了「接收消息 v1.0」和「接收消息 v2.0」两个事件。

**说明**：开放平台提供的事件包括 v1.0 和 v2.0 两个版本。两个版本的结构不同。在处理事件时，可通过[事件列表](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-list)文档或在开发者后台[添加事件](https://open.feishu.cn/document/ukTMukTMukTM/uYDNxYjL2QTM24iN0EjN/event-subscription-configure-/subscription-event-case)时，获取事件的类型与版本信息。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/4dbc1cd97d95f1af949354ce3c350461_4PX46ts1Hs.png?height=380&lazyload=true&maxWidth=600&width=1666)

如果是 v1.0 事件，则注册服务器时需要查找并使用 onP1xxxx 开头的方法；如果是 v2.0 事件，则注册服务器时需要查找并使用 onP2xxxx 开头的方法。
例如，使用 `onP2MessageReceiveV1` 注册接收消息事件回调时，`P2`便对应的是 v2.0 版本事件结构。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/8bbadcadbadcf70bb9cb4c44afea25cd_z7bPrzvx1l.png?height=286&lazyload=true&maxWidth=600&width=1598)

1. 通过 new Client.Builder() 初始化长连接客户端，必填参数为应用的 APP_ID 和 APP_SECRET，需登录[开发者后台](https://open.feishu.cn/app)，在应用详情页的 **凭证与基础信息** > **应用凭证** 区域获取。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/f7f89950be7e57c2760a8b5b1f5e17c9_1BhhLr3htD.png?height=524&lazyload=true&maxWidth=600&width=3594)

1. 可选参数传入 *EVENT_HANDLER*，同时可设置日志级别，可在项目的日志框架（log4j2、logback等）配置中修改。
1. 通过 cli.start() 启动客户端，如连接成功，控制台会打印 "connected to wss://xxxxx"，主线程将阻塞，直到进程结束。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/90d8aab594e3fa8ca22bf4b291723efd_8BitTMiPGo.png?height=176&lazyload=true&maxWidth=600&width=2992)

### Node.js

#### 安装 SDK

版本需大于等于`1.24.0`。

```
// 用自己项目中的包管理工具，如 yarn
yarn add @larksuiteoapi/node-sdk
```

#### 示例代码

```js
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.debug});
wsClient.start({
  // 处理「接收消息」事件，事件类型为 im.message.receive_v1
  eventDispatcher: new Lark.EventDispatcher({}).register({
    'im.message.receive_v1': async (data) => {
      const {
        message: { chat_id, content}
      } = data;
      // 示例操作：接收消息后，调用「发送消息」API 进行消息回复。
      await client.im.v1.message.create({
        params: {
          receive_id_type: "chat_id"
        },
        data: {
          receive_id: chat_id,
          content: Lark.messageCard.defaultCard({
            title: `回复： ${JSON.parse(content).text}`,
            content: '新年好'
          }),
          msg_type: 'interactive'
        }
      });
    }
  })
});
```

代码实现说明：

1. 初始化 WSClient。

1. 在 baseConfig 中，传入应用的 appId、appSecret。获取方式：登录[开发者后台](https://open.feishu.cn/app)，在应用详情页的 **凭证与基础信息** 页面内，获取应用凭证（包括 App ID 和 App Secret）。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/e78da7b6e674f8ab77a16313123d60b0_I9ZxXgd9Qe.png?height=356&lazyload=true&maxWidth=600&width=2536)

1. 可选添加 domain 等其他参数，domain 默认为 `open.feishu.cn`。

Client 支持配置的参数参考[调用服务端 API](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/server-side-sdk/nodejs-sdk/invoke-server-api) 中的 **步骤一：创建并配置 API Client** 章节。

2. 基于 Client 完成初始化时，实例上的 start 方法用于启动长连接客户端。

其中，start 函数中的 eventDispatcher 参数接收 EventDispatcher 实例，长连接客户端启动成功后，eventDispatcher.register 注册的相关事件 handler 会被执行。

如下示例代码部分，需要将 `'im.message.receive_v1'` 替换为需要处理的事件的 **事件类型**。**事件类型** 可参考指定事件的文档，例如，查阅[接收消息](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/reference/im-v1/message/events/receive)事件文档可知，该事件的事件类型为 **im.message.receive_v1**。

```javascript
    wsClient.start({
      eventDispatcher: new Lark.EventDispatcher({}).register({
        'im.message.receive_v1': async (data) => {
          const {
            message: { chat_id, content}
          } = data;
    ```

如成功启动长连接，会打印如下信息。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/a5139264b8e6fcbbeafce287aa86efec_YGMzsCtK7W.png?height=694&lazyload=true&maxWidth=600&width=3124)

## 步骤二：设置订阅方式

1. 启动本地长连接客户端。

具体操作参考上述 **步骤一：集成 SDK**。

2. 登录[开发者后台](https://open.feishu.cn/app)，选择一个企业自建应用。

目前长连接模式不支持商店应用。

3. 进入 **事件与回调 > 事件配置** 页面。

4. 编辑订阅方式，选择 **使用长连接接收事件**，并保存。warning
    必须确保本地客户端启动正常，有长连接在线的情况下，才能保存成功。

![](//sf3-cn.feishucdn.com/obj/open-platform-opendoc/cb9b31525ac1bf0be297feba0eb264b4_b2P2yQrvFW.png?height=790&lazyload=true&maxWidth=600&width=1726)

## 常见问题

配置过程中如遇问题，可参考[常见问题](https://open.feishu.cn/document/uAjLw4CM/ukTMukTMukTM/event-subscription-guide/event-subscriptions/faq)排查解决。
