Skip to content

处理回调

通过回调订阅功能,应用可以及时接收并处理飞书中特定的交互行为(例如,飞书卡片交互、链接预览等),并根据交互结果做对应的业务处理,详情参见回调概述。在应用内订阅回调时,还需要在本地服务端建立与应用的连接,以便接收回调数据。服务端 SDK 封装了长连接方式,可以快速建立数据通道处理回调;你也可以选择自建 HTTP 服务器处理回调。两种方式介绍如下:

订阅方式介绍
使用长连接接收回调该方式是飞书 SDK 内提供的能力,你可以通过集成飞书 SDK 与开放平台建立一条 WebSocket 全双工通道(你的服务器需要能够访问公网)。后续当应用订阅的回调发生时,开放平台会通过该通道向你的服务器发送消息。相较于传统的 Webhook 模式,长连接模式大大降低了接入成本,将原先 1 周左右的开发周期降低到 5 分钟。具体优势如下:测试阶段无需使用内网穿透工具,通过长连接模式在本地开发环境中即可接收回调。SDK 内封装了鉴权逻辑,只在建连时进行鉴权,后续回调推送均为明文数据,无需再处理解密和验签逻辑。只需保证运行环境具备访问公网能力即可,无需提供公网 IP 或域名。无需部署防火墙和配置白名单。
将回调发送至开发者服务器传统的 Webhook 模式,该方式需要你提供用于接收回调消息的服务器公网地址。后续当应用订阅的回调发生时,开放平台会向服务器的公网地址发送 HTTP POST 请求,请求内包含回调数据。

注意事项

开放平台 SDK 仅支持对象类型的卡片回传参数,不支持字符串类型。

json
{
  "behaviors": [
    { // 声明交互类型是卡片回传交互。
      "type": "callback",
      "value": {
        // 回传交互数据。开放平台 SDK 仅支持对象类型的卡片回传参数。
        "key": "value"
      }
    }
  ]
}

(推荐)方式一:使用长连接接收回调

如果回调订阅方式需要选择 使用长连接接收回调,则需要先使用 SDK 建立与应用的连接。本章节提供建立长连接的示例代码与代码解析,通过 SDK 建立长连接之后,你才能在应用的回调订阅方式中保存 使用长连接接收回调 方式。关于应用内配置回调订阅方式的介绍,参考配置回调订阅方式

使用限制

  • 长连接模式仅支持企业自建应用。
  • 消息卡片回传交互(旧)回调不支持 使用长连接接收回调 订阅方式,只能选择 将回调发送至开发者服务器 订阅方式。
  • 每个应用最多建立 50 个连接(在配置长连接时,每初始化一个 client 就是一个连接)。

注意事项

  • 将回调发送至开发者服务器 方式的要求相同,长连接模式下接收到消息后,需要在 3 秒内处理完成。
  • 长连接模式的消息推送为 集群模式,不支持广播,即如果同一应用部署了多个客户端(client),那么只有其中随机一个客户端会收到消息。

长连接代码

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.event.cardcallback.P2CardActionTriggerHandler;
import com.lark.oapi.event.cardcallback.P2URLPreviewGetHandler;
import com.lark.oapi.event.cardcallback.model.*;
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 {
    private static final EventDispatcher EVENT_HANDLER = EventDispatcher.newBuilder("", "")  // 长连接不需要这两个参数,请保持空字符串
            // 监听「卡片回传交互 card.action.trigger」
            .onP2CardActionTrigger(new P2CardActionTriggerHandler() {
                @Override
                public P2CardActionTriggerResponse handle(P2CardActionTrigger event) throws Exception {
                    System.out.printf("[ P2CardActionTrigger access ], data: %s\n", Jsons.DEFAULT.toJson(event.getEvent()));
                    P2CardActionTriggerResponse resp = new P2CardActionTriggerResponse();
                    CallBackToast toast = new CallBackToast();
                    toast.setType("info");
                    toast.setContent("卡片交互成功 from Java SDk");
                    resp.setToast(toast);
                    return resp;
                }
            })
            // 监听「拉取链接预览数据 url.preview.get」
            .onP2URLPreviewGet(new P2URLPreviewGetHandler() {
                @Override
                public P2URLPreviewGetResponse handle(P2URLPreviewGet event) throws Exception {
                    System.out.printf("[ P2URLPreviewGet access ], data: %s\n", Jsons.DEFAULT.toJson(event.getEvent()));
                    P2URLPreviewGetResponse resp = new P2URLPreviewGetResponse();
                    URLPreviewGetInline inline = new URLPreviewGetInline();
                    inline.setTitle("链接预览测试fromJavaSDK");
                    resp.setInline(inline);
                    return resp;
                }
            })
            .build();
    public static void main(String[] args) {
        Client client = new Client.Builder("", "")
                .eventHandler(EVENT_HANDLER)
                .build();
        client.start();
    }
}

代码实现说明:

  1. 通过 EventDispatcher.newBuilder() 初始化回调处理器(EVENT_HANDLER),注意两个参数必须填空字符串

  2. 通过 EVENT_HANDLER 的 onXXXX() 方法监听不同的回调类型,上述示例中分别监听了 卡片回传交互拉取链接预览数据 两个回调。

  3. 通过 new Client.Builder() 初始化长连接客户端,必填参数为应用的 APP_ID 和 APP_SECRET,可在开发者后台获取。

  4. 可选参数传入 EVENT_HANDLER

  5. 日志级别在自己项目的日志框架(log4j2、logback等)配置中修改。

  6. 通过 cli.start() 启动客户端,如连接成功,控制台会打印 "connected to wss://xxxxx",主线程将阻塞,直到进程结束。

方式二:集成 Servlet 容器,将回调发送至开发者服务器

如果回调订阅方式选择 将回调发送至开发者服务器,则需要设置回调请求网址,并订阅回调。例如,你配置了可交互的卡片后,当用户在卡片内进行交互后,飞书服务器会向请求网址回调包含 JSON 数据的 HTTP POST 请求。因此,你需要启动一个 HTTP 服务器接收回调数据。

image.png

该章节将介绍如何集成基于 Servlet 技术栈实现的 SpringBoot Web 框架。

步骤一:安装集成包

要实现 SDK 集成已有的 SpringBoot 框架,你需要在项目 pom.xml 文件中引入集成包。相应的 maven 坐标如下:

xml
<dependency>
  <artifactId>oapi-sdk-servlet-ext</artifactId>
  <groupId>com.larksuite.oapi</groupId>
  <version>1.0.0-rc3</version>
  <exclusions>
    <exclusion>
      <artifactId>oapi-sdk</artifactId>
      <groupId>com.larksuite.oapi</groupId>
    </exclusion>
  </exclusions>
</dependency>

步骤二:通过代码集成 Servlet 容器

以下为集成示例:

  1. 注入 ServletAdapter 实例到 IOC 容器。

    java
    import com.lark.oapi.sdk.servlet.ext.ServletAdapter;
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.context.annotation.Bean;
    
    @SpringBootApplication
    public class AppStartup {
    
      public static void main(String[] args) {
        SpringApplication.run(AppStartup.class, args);
      }
    
      // 注入扩展实例到 IOC 容器
      @Bean
      public ServletAdapter getServletAdapter() {
        return new ServletAdapter();
      }
    }
  2. 编写 Controller 注册卡片处理器。

  • 要基于新版卡片回传交互处理,你需订阅新版卡片回传交互,然后使用以下代码处理卡片行为:

java
import com.lark.oapi.core.utils.Jsons;
import com.lark.oapi.event.EventDispatcher;
import com.lark.oapi.event.cardcallback.P2CardActionTriggerHandler;
import com.lark.oapi.event.cardcallback.P2URLPreviewGetHandler;
import com.lark.oapi.event.cardcallback.model.*;
import com.lark.oapi.sdk.servlet.ext.ServletAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@RestController
public class EventController {

    //1. 注册消息处理器
    private final EventDispatcher EVENT_DISPATCHER = EventDispatcher.newBuilder("", "")
            .onP2CardActionTrigger(new P2CardActionTriggerHandler() {
                // 监听「卡片回传交互 card.action.trigger」
                @Override
                public P2CardActionTriggerResponse handle(P2CardActionTrigger event) throws Exception {
                    System.out.printf("[ P2CardActionTrigger access ], data: %s\n", Jsons.DEFAULT.toJson(event.getEvent()));
                    P2CardActionTriggerResponse resp = new P2CardActionTriggerResponse();
                    CallBackToast toast = new CallBackToast();
                    toast.setType("info");
                    toast.setContent("卡片交互成功 from Java SDk");
                    resp.setToast(toast);
                    return resp;
                }
            })
            // 监听「拉取链接预览数据 url.preview.get」
            .onP2URLPreviewGet(new P2URLPreviewGetHandler() {
                @Override
                public P2URLPreviewGetResponse handle(P2URLPreviewGet event) throws Exception {
                    System.out.printf("[ P2URLPreviewGet access ], data: %s\n", Jsons.DEFAULT.toJson(event.getEvent()));
                    P2URLPreviewGetResponse resp = new P2URLPreviewGetResponse();
                    URLPreviewGetInline inline = new URLPreviewGetInline();
                    inline.setTitle("链接预览测试fromJavaSDK");
                    resp.setInline(inline);
                    return resp;
                }
            })
            .build();

    //2. 注入 ServletAdapter 实例
    @Autowired
    private ServletAdapter servletAdapter;

    //3. 创建路由处理器
    @RequestMapping("/webhook/event")
    public void event(HttpServletRequest request, HttpServletResponse response)
            throws Throwable {
        //3.1 回调扩展包提供的事件回调处理器
        servletAdapter.handleEvent(request, response, EVENT_DISPATCHER);
    }
}
  • 要基于旧版卡片回传交互处理,你需订阅旧版消息卡片回传交互(旧),然后使用以下代码处理卡片行为:

    image.png

    java
    import com.lark.oapi.card.CardActionHandler;
    import com.lark.oapi.card.model.CardAction;
    import com.lark.oapi.core.utils.Jsons;
    import com.lark.oapi.sdk.servlet.ext.ServletAdapter;
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.HttpServletResponse;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    public class CardActionController {
    
      //1. 注册卡片处理器
      private final CardActionHandler CARD_ACTION_HANDLER = CardActionHandler.newBuilder("v", "e",
          new CardActionHandler.ICardHandler() {
            @Override
            public Object handle(CardAction cardAction) {
              System.out.println(Jsons.DEFAULT.toJson(cardAction));
              System.out.println(cardAction.getRequestId());
              return null;
            }
          }).build();
    
      // 2. 注入 ServletAdapter 示例
      @Autowired
      private ServletAdapter servletAdapter;
    
      //3. 注册服务路由
      @RequestMapping("/webhook/card")
      public void card(HttpServletRequest request, HttpServletResponse response)
          throws Throwable {
        //3.1 回调扩展包卡片行为处理回调
        servletAdapter.handleCardAction(request, response, CARD_ACTION_HANDLER);
      }
    }

如上代码中,如果不需要处理器内返回业务结果至飞书服务端,则直接在处理器内返回 null 即可。如需了解其他卡片回调示例,可参见 GitHub 代码仓库

(可选)返回卡片消息

如果你需要在卡片处理器内同步返回用于更新消息卡片的消息体,则可以使用以下方式进行处理。

  • 要基于新版卡片回传交互处理,你需订阅新版卡片回传交互,然后使用以下代码处理卡片行为:

java
import com.lark.oapi.core.utils.Jsons;
import com.lark.oapi.event.EventDispatcher;
import com.lark.oapi.event.cardcallback.P2CardActionTriggerHandler;
import com.lark.oapi.event.cardcallback.P2URLPreviewGetHandler;
import com.lark.oapi.event.cardcallback.model.*;
import com.lark.oapi.sdk.servlet.ext.ServletAdapter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@RestController
public class EventController {

    //1. 注册消息处理器
    private final EventDispatcher EVENT_DISPATCHER = EventDispatcher.newBuilder("", "")
            .onP2CardActionTrigger(new P2CardActionTriggerHandler() {
                // 监听「卡片回传交互 card.action.trigger」
                @Override
                public P2CardActionTriggerResponse handle(P2CardActionTrigger event) throws Exception {
                    System.out.printf("[ P2CardActionTrigger access ], data: %s\n", Jsons.DEFAULT.toJson(event.getEvent()));
                    P2CardActionTriggerResponse resp = new P2CardActionTriggerResponse();
                    CallBackToast toast = new CallBackToast();
                    toast.setType("info");
                    toast.setContent("卡片交互成功 from Java SDk");
                    CallBackCard card = new CallBackCard();
                    Map<String, Object> cardData = new HashMap<>();

                    // Config map
                    Map<String, Object> configMap = new HashMap<>();
                    configMap.put("enable_forward", true);

                    // Text map inside elements
                    Map<String, String> textMap = new HashMap<>();
                    textMap.put("content", "This is the plain text");
                    textMap.put("tag", "plain_text");

                    // Element map
                    Map<String, Object> elementMap = new HashMap<>();
                    elementMap.put("tag", "div");
                    elementMap.put("text", textMap);

                    // Elements list
                    List<Map<String, Object>> elementsList = new ArrayList<>();
                    elementsList.add(elementMap);

                    // Title map inside header
                    Map<String, String> titleMap = new HashMap<>();
                    titleMap.put("content", "This is the title");
                    titleMap.put("tag", "plain_text");

                    // Header map
                    Map<String, Object> headerMap = new HashMap<>();
                    headerMap.put("template", "blue");
                    headerMap.put("title", titleMap);

                    // Putting everything into the main map
                    cardData.put("config", configMap);
                    cardData.put("elements", elementsList);
                    cardData.put("header", headerMap);

                    card.setType("raw");
                    card.setData(cardData); // 这里的data必须是一个对象,不是序列化后的字符串
                    resp.setToast(toast);
                    resp.setCard(card);
                    return resp;
                }
            })
            // 监听「拉取链接预览数据 url.preview.get」
            .onP2URLPreviewGet(new P2URLPreviewGetHandler() {
                @Override
                public P2URLPreviewGetResponse handle(P2URLPreviewGet event) throws Exception {
                    System.out.printf("[ P2URLPreviewGet access ], data: %s\n", Jsons.DEFAULT.toJson(event.getEvent()));
                    P2URLPreviewGetResponse resp = new P2URLPreviewGetResponse();
                    URLPreviewGetInline inline = new URLPreviewGetInline();
                    inline.setTitle("链接预览测试fromJavaSDK");
                    resp.setInline(inline);
                    return resp;
                }
            })
            .build();

    //2. 注入 ServletAdapter 实例
    @Autowired
    private ServletAdapter servletAdapter;

    //3. 创建路由处理器
    @RequestMapping("/webhook/event")
    public void event(HttpServletRequest request, HttpServletResponse response)
            throws Throwable {
        //3.1 回调扩展包提供的事件回调处理器
        servletAdapter.handleEvent(request, response, EVENT_DISPATCHER);
    }
}
java
import com.lark.oapi.card.CardActionHandler;
import com.lark.oapi.card.model.CardAction;
import com.lark.oapi.card.model.MessageCard;
import com.lark.oapi.card.model.MessageCardElement;
import com.lark.oapi.core.utils.Jsons;
import com.lark.oapi.sdk.servlet.ext.ServletAdapter;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class CardActionController {
  //1. 注册卡片处理器
  private final CardActionHandler CARD_ACTION_HANDLER = CardActionHandler.newBuilder("v", "e",
      new CardActionHandler.ICardHandler() {
        @Override
        public Object handle(CardAction cardAction) {
          // 1.1 处理卡片行为
          System.out.println(Jsons.DEFAULT.toJson(cardAction));
          System.out.println(cardAction.getRequestId());
          // 1.2 构建响应卡片内容
          MessageCard card = MessageCard.newBuilder()
              .cardLink(cardURL)
              .config(config)
              .header(header)
              .elements(new MessageCardElement[]{div, note, image, cardAction, hr})
              .build();
          return card;
        }
      }).build();
  // 2. 注入 ServletAdapter 示例
  @Autowired
  private ServletAdapter servletAdapter;
  //3. 注册服务路由
  @RequestMapping("/webhook/card")
  public void card(HttpServletRequest request, HttpServletResponse response)
      throws Throwable {
    //3.1 回调扩展包卡片行为处理回调
    servletAdapter.handleCardAction(request, response, CARD_ACTION_HANDLER);
  }
}

内容来源:飞书开放平台 · 自动爬取整理