# 如何巧用飞书消息卡片输入框实现一套业务交互逻辑

- Link: https://www.ixiqin.com/2023/08/22/how-to-cleverly-use-the-input-box-of-feishu-message-card-to/
- Published: 2023-08-22T06:36:57+08:00
- Author: 白宦成

文章目录

 [Toggle](https://www.ixiqin.com/2023/08/22/how-to-cleverly-use-the-input-box-of-feishu-message-card-to/#)

 * [流程图示意](https://www.ixiqin.com/2023/08/22/how-to-cleverly-use-the-input-box-of-feishu-message-card-to/#%e6%b5%81%e7%a8%8b%e5%9b%be%e7%a4%ba%e6%84%8f)
 * [目标说明](https://www.ixiqin.com/2023/08/22/how-to-cleverly-use-the-input-box-of-feishu-message-card-to/#%e7%9b%ae%e6%a0%87%e8%af%b4%e6%98%8e)
 * [实现逻辑](https://www.ixiqin.com/2023/08/22/how-to-cleverly-use-the-input-box-of-feishu-message-card-to/#%e5%ae%9e%e7%8e%b0%e9%80%bb%e8%be%91)
    - [点击按钮并回复卡片](https://www.ixiqin.com/2023/08/22/how-to-cleverly-use-the-input-box-of-feishu-message-card-to/#%e7%82%b9%e5%87%bb%e6%8c%89%e9%92%ae%e5%b9%b6%e5%9b%9e%e5%a4%8d%e5%8d%a1%e7%89%87)
    - [对卡片输入内容进行校验](https://www.ixiqin.com/2023/08/22/how-to-cleverly-use-the-input-box-of-feishu-message-card-to/#%e5%af%b9%e5%8d%a1%e7%89%87%e8%be%93%e5%85%a5%e5%86%85%e5%ae%b9%e8%bf%9b%e8%a1%8c%e6%a0%a1%e9%aa%8c)
       * [1. 在本地校验文件长短](https://www.ixiqin.com/2023/08/22/how-to-cleverly-use-the-input-box-of-feishu-message-card-to/#1_%e5%9c%a8%e6%9c%ac%e5%9c%b0%e6%a0%a1%e9%aa%8c%e6%96%87%e4%bb%b6%e9%95%bf%e7%9f%ad)
       * [2. 输入两个参数才发起请求](https://www.ixiqin.com/2023/08/22/how-to-cleverly-use-the-input-box-of-feishu-message-card-to/#2_%e8%be%93%e5%85%a5%e4%b8%a4%e4%b8%aa%e5%8f%82%e6%95%b0%e6%89%8d%e5%8f%91%e8%b5%b7%e8%af%b7%e6%b1%82)
       * [3. 在服务端验证有无](https://www.ixiqin.com/2023/08/22/how-to-cleverly-use-the-input-box-of-feishu-message-card-to/#3_%e5%9c%a8%e6%9c%8d%e5%8a%a1%e7%ab%af%e9%aa%8c%e8%af%81%e6%9c%89%e6%97%a0)
    - [完整代码参考](https://www.ixiqin.com/2023/08/22/how-to-cleverly-use-the-input-box-of-feishu-message-card-to/#%e5%ae%8c%e6%95%b4%e4%bb%a3%e7%a0%81%e5%8f%82%e8%80%83)

飞书开放平台最近开始内测了输入框的能力，基于输入框，为消息卡片提供了进一步业务系统
打通的可能性，你可以不需要开发一整个网页应用，只需要借助飞书机器人和飞书消息卡片，
就可以实现一套业务交互逻辑。

## 流程图示意

![](https://postimg.aliavv.com/m1/w0052z.jpg)

## 目标说明

这里首先确定要实现的逻辑：这里我要做的是一个短链接应用，功能很简单，点击下方的机器
人菜单，并在弹出的窗口中输入对应的短链接后缀和要跳转的链接，点击确定就会帮我创建
一个短链接。

![](https://postimg.aliavv.com/m1/4rbg8i.png)

具体效果如下：

![](https://postimg.aliavv.com/m1/dqk8d2.png)

如果后缀已经被占用，则展示如下内容：

![](https://postimg.aliavv.com/m1/50tl60.png)

在实现这个功能时，我首先使用了飞书提供的[输入框组件](https://bytedance.feishu.cn/docx/Iy8EdadF9ohXepxGInHcdgFLn5g)
的能力和[表单组件](https://bytedance.feishu.cn/docx/YcR7d6CMTokvXAx8KpccoXQpnff)
能力，来实现整个业务交互，当然，你也可以根据业务形态，来选择合适的组件，构成一整个
输入表单。

## 实现逻辑

整体的功能可以分为三步：

 1. **点击按钮**：机器人需要响应点击事件，并发送一个带有输入框的消息卡片。
 2. **验证卡片输入内容**：消息卡片中提供了输入框，但是用户的输入是否我们能用，需要设计
    一些验证的能力。
 3. **反馈用户是否创建成功**：当我们创建成功后，需要给开发者提示，告诉他是否已经创建
    成功，帮助他结束整个流程。

接下来就是具体的实现步骤了。

### 点击按钮并回复卡片

首先，我先是使用了机器人的菜单功能，来实现在机器人底部配置菜单。你需要访问飞书开发
者后台，找到机器人能力中的「机器人自定义菜单」，就可以配置一个机器人的自定义菜单
了。机器人菜单支持跳转到指定链接，或者是推送事件，我选择推送事件，这样我就可以在
服务端响应用户的创建的行为。这里我设定了事件内容为 create ，便于后续处理。

![](https://postimg.aliavv.com/m1/ogypfv.png)

机器人菜单的处理则可以参考[机器人菜单使用说明](https://open.feishu.cn/document/client-docs/bot-v3/bot-customized-menu)，
通过订阅「机器人自定义事件」来完成对于相应行为的接受和对应的处理。

这部分的处理逻辑可以参考如下代码

    ```javascript
    // 判断请求体当中是否有 header 字段 && 来源的事件是否是机器人菜单
    if (Object.hasOwn(ctx.body, "header") && ctx.body.header.event_type == 'application.bot.menu_v6') {

        // 请求的事件是否是创建短链接对应的事件。
        if (ctx.body.event.event_key == "create") {
          try {
            await client.request({
              method: "POST",
              url: "https://open.feishu.cn/open-apis/im/v1/messages",
              data: {
                receive_id: ctx.body.event.operator.operator_id.open_id, // 从事件体中提取事件的触发人
                msg_type: 'interactive',
                content: "", // 推送卡片 JSON
              },
              params: {
                receive_id_type: 'open_id',
              },
            })
            return {};
          } catch (e) {
            console.log(`key: ${ctx.body.event.event_key}, user:${ctx.body.event.operator.operator_id.open_id},error`, e);
            return {};
          }
        }
      }
    Code language: JavaScript (javascript)
    ```

### 对卡片输入内容进行校验

在完成卡片响应的设定后，接下来我实现的是校验的逻辑，这里分为两层：第一层是客户端
可以完成的校验：比如短链接应该少于 10 个字符。第二层是只有客户端才能完成的校验。

#### 1. 在本地校验文件长短

如果每次发起请求都需要发送到服务端进行校验，则有比较高的校验成本。好在消息卡片提供
了本地校验的能力，你可以通过 `max_length` 字段来验证输入框长短.

这里我是使用输入框组件的字段，来验证输入的内容长度不得大于 10 。

![](https://postimg.aliavv.com/m1/du1glf.png)

#### 2. 输入两个参数才发起请求

在消息卡片的输入框组件中，只要输入内容就会发现校验，因此我不能直接使用输入框组件，
而是需要借助 form 组件，来实现用户输入两个内容再手动发起提交。则具体我构建的卡片
JSON 是这样的。

    ```shcb-line-numbers
    {
      "header": {
        "template": "turquoise",
        "title": {
          "content": "创建短链接",
          "tag": "plain_text"
        }
      },
      "elements": [
        {
          "tag": "form",
          "name": "form_1",
          "elements": [
            {
              "tag": "input",
              "name": "postfix",
              "placeholder": {
                "tag": "plain_text",
                "content": "请输入后缀"
              },
              "max_length": 10,
              "label": {
                "tag": "plain_text",
                "content": "请输入后缀:"
              },
              "label_position": "left",
              "value": {
                "k": "v"
              }
            },
            {
              "tag": "input",
              "name": "link",
              "placeholder": {
                "tag": "plain_text",
                "content": "请输入要跳转链接"
              },
              "label": {
                "tag": "plain_text",
                "content": "请输入要跳转链接:"
              },
              "label_position": "left",
              "value": {
                "k": "v"
              }
            },
            {
              "action_type": "form_submit",
              "name": "submit",
              "tag": "button",
              "text": {
                "content": "提交",
                "tag": "lark_md"
              },
              "type": "primary",
              "confirm": {
                "title": {
                  "tag": "plain_text",
                  "content": "创建短链接"
                },
                "text": {
                  "tag": "plain_text",
                  "content": "确认提交吗"
                }
              }
            }
          ]
        }
      ]
    }
    Code language: JSON / JSON with Comments (json)
    ```

这部分的关键是用 form 组件包裹 Input 组件，从而规避了 Input 组件输入内容就会发送
到服务端校验的问题。

![](https://postimg.aliavv.com/m1/7yro9r.png)

#### 3. 在服务端验证有无

这部分逻辑我在实现的时候相对简单，没有专门去进行校验（主要是因为我的短链接服务和
机器人是两个不同的服务），而是通过短链服务返回 200 还是 401 来判断是否出现了重复
的问题，所以这里只是简单的使用了一个 try catch 来完成校验。

需要注意的是，这里你会注意到，返回是直接返回了一段 JSON String，这是因为触发这个
事件是通过消息卡片的回调能力，如果你在消息卡片的回调能力返回一个 JSON，就会直接把
UI 层面的卡片渲染为你返回的卡片结果。靠着这个功能，我来实现的成功与失败返回不同的
内容。

    ```javascript
    if (Object.hasOwn(ctx.body, "action") && ctx.body.action) {    
        try {
          // create link

          if (status == 200) {
            return JSON.stringify({
              "type": "template",
              "data": {
                "template_id": "ctp_AAmFBm5vnlt0",
                "template_variable": {
                  "source": ctx.body.action.form_value.postfix,
                  "target": ctx.body.action.form_value.link
                }
              }
            })
          }
          return {};
        } catch (e) {
          return JSON.stringify({
            "type": "template",
            "data": {
              "template_id": "ctp_AAmFBm5vZYuo",
              "template_variable": {
                "POSTFIX": ctx.body.action.form_value.postfix
              }
            }
          });
        }
      }
    Code language: JavaScript (javascript)
    ```

### 完整代码参考

整个机器人的部分的代码只有 170 余行，不多，供你参考

    ```javascript
    import cloud from '@lafjs/cloud'
    import axios from 'axios'

    let appid = "";
    let secret = ""

    const lark = require('@larksuiteoapi/node-sdk');

    const client = new lark.Client({
      appId: appid,
      appSecret: secret
    });


    export default async function (ctx: FunctionContext) {

      console.log("event",ctx.body);

      if (ctx.body.challenge) {
        return ctx.body
      }

      if (Object.hasOwn(ctx.body, "action") && ctx.body.action) {
        if (ctx.body.action.name != "submit") return { code: 1 };
        try {
          // function to create link

          if (status == 200) {
            return JSON.stringify({
              "type": "template",
              "data": {
                "template_id": "ctp_AAmFBm5vnlt0",
                "template_variable": {
                  "source": ctx.body.action.form_value.postfix,
                  "target": ctx.body.action.form_value.link
                }
              }
            })
          }
          return {};
        } catch (e) {
          return JSON.stringify({
            "type": "template",
            "data": {
              "template_id": "ctp_AAmFBm5vZYuo",
              "template_variable": {
                "POSTFIX": ctx.body.action.form_value.postfix
              }
            }
          });
        }
      }

      if (Object.hasOwn(ctx.body, "header") && ctx.body.header.event_type == 'application.bot.menu_v6') {
        // 处理按钮
        if (ctx.body.event.event_key == "help") {
          try {
            let content = JSON.stringify({
              template_id: "ctp_AAmFBFOpYX0S"
            });
            await client.request({
              method: "POST",
              url: "https://open.feishu.cn/open-apis/im/v1/messages",
              data: {
                receive_id: ctx.body.event.operator.operator_id.open_id,
                msg_type: 'interactive',
                content: JSON.stringify({
                  "type": "template",
                  "data": {
                    "template_id": "ctp_AAmFBFOpYX0S",
                  }
                }),
              },
              params: {
                receive_id_type: 'open_id',
              },
            })
            return {};
          } catch (e) {
            console.log(`key: ${ctx.body.event.event_key}, user:${ctx.body.event.operator.operator_id.open_id},error`, e);
            return {};
          }
        }
        if (ctx.body.event.event_key == "mylink") {
          try {
            // function to get all my link 
            let links = data.data.map(item => {
              return {
                source: `[${item.Postfix}](https://link.feishu.io/${item.Postfix})`,
                target: item.Link
              }
            })
            await client.request({
              method: "POST",
              url: "https://open.feishu.cn/open-apis/im/v1/messages",
              data: {
                receive_id: ctx.body.event.operator.operator_id.open_id,
                msg_type: 'interactive',
                content: JSON.stringify({
                  "type": "template",
                  "data": {
                    "template_id": "ctp_AAmFBm5vnHfs",
                    "template_variable": {
                      "CONTENT": links
                    }
                  }
                }),
              },
              params: {
                receive_id_type: 'open_id',
              },
            })
            return {};
          } catch (e) {
            console.log(`key: ${ctx.body.event.event_key}, user:${ctx.body.event.operator.operator_id.open_id},error`, e);
            return {};
          }
        }
        if (ctx.body.event.event_key == "create") {
          try {
            await client.request({
              method: "POST",
              url: "https://open.feishu.cn/open-apis/im/v1/messages",
              data: {
                receive_id: ctx.body.event.operator.operator_id.open_id,
                msg_type: 'interactive',
                content: "{\"header\":{\"template\":\"turquoise\",\"title\":{\"content\":\"创建短链接\",\"tag\":\"plain_text\"}},\"elements\":[{\"tag\":\"form\",\"name\":\"form_1\",\"elements\":[{\"tag\":\"input\",\"name\":\"postfix\",\"placeholder\":{\"tag\":\"plain_text\",\"content\":\"请输入后缀\"},\"max_length\":10,\"label\":{\"tag\":\"plain_text\",\"content\":\"请输入后缀:\"},\"label_position\":\"left\",\"value\":{\"k\":\"v\"}},{\"tag\":\"input\",\"name\":\"link\",\"placeholder\":{\"tag\":\"plain_text\",\"content\":\"请输入要跳转链接\"},\"label\":{\"tag\":\"plain_text\",\"content\":\"请输入要跳转链接:\"},\"label_position\":\"left\",\"value\":{\"k\":\"v\"}},{\"action_type\":\"form_submit\",\"name\":\"submit\",\"tag\":\"button\",\"text\":{\"content\":\"提交\",\"tag\":\"lark_md\"},\"type\":\"primary\",\"confirm\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"创建短链接\"},\"text\":{\"tag\":\"plain_text\",\"content\":\"确认提交吗\"}}}]}]}",
              },
              params: {
                receive_id_type: 'open_id',
              },
            })
            return {};
          } catch (e) {
            console.log(`key: ${ctx.body.event.event_key}, user:${ctx.body.event.operator.operator_id.open_id},error`, e);
            return {};
          }
        }
      }
      return { data: 'hi, laf' }
    }
    Code language: JavaScript (javascript)
    ```
