sougood
小众、高效的搜索引擎

n8n:基于WebHook做一个飞书回声应用

知识点

  • n8n节点WebHook使用
  • n8n节点redis使用
  • n8n插件n8n-nodes-feishu-lite使用
  • 配置飞书自建应用以用户发送的消息应答

今天这篇文章属于比较复杂的东西了。因为想要获取用户的信息需要创建一个飞书应用,之前说我开发alertmanager 发送飞书的小程序里没有实现这个就是因为除了要开发一堆东西,还要在飞书开放平台建个自建应用,背离了我开发那个 go 应用并短小精悍的初衷。

网上查询资料得知我们需要用到插件n8n-nodes-feishu-lite 所以第一步插件安装,这个比较简单,点击左下角图标,弹出的菜单点 Settings

点击左侧列表中的 Community nodes

右侧点击 Install a community node

在弹出的菜单,输入 n8n-nodes-feishu-lite ,勾选,然后点 Install

社区插件安装成功!

接下来,我们要把n8n通过域名的方式暴露在公网,并通过WebHook节点提供URL给飞书设置回调。这块其实很复杂,简单来说就是分配一个公网域名,解析到服务器,服务器配置接收这个域名,并转给n8n。实现的效果就是配置前:

配置后:

接下来要去飞书开放平台创建一个应用,地址是

https://open.feishu.cn

进行简单配置

然后就进入了应用管理界面,几个关键点

添加 机器人 能力

权限管理->开通权限,通过搜索添加以下权限:

  • im:message
  • im:message.p2p_msg:readonly
  • im:message:send_as_bot
  • event:ip_list

事件与回调->加密策略 拿到 Encrypt KeyVerification Token

凭证与基础信息 拿到AppIDAppSecret

创建一次版本

回到n8n,创建一个workflow,将下面的内容粘贴进去

{  "name": "飞书应用回声示例",  "nodes": [    {      "parameters": {        "httpMethod": "POST",        "path": "4946e7d3-a97e-448d-ba00-c73c62b454cb",        "responseMode": "responseNode",        "options": {}      },      "type": "n8n-nodes-base.webhook",      "typeVersion": 2,      "position": [        -1824,        656      ],      "id": "f4cc1a30-d60d-43ef-9dd9-88d389418428",      "name": "Webhook",      "webhookId": "4946e7d3-a97e-448d-ba00-c73c62b454cb"    },    {      "parameters": {        "rules": {          "values": [            {              "conditions": {                "options": {                  "caseSensitive": true,                  "leftValue": "",                  "typeValidation": "loose",                  "version": 2                },                "conditions": [                  {                    "id": "32a6b4df-2701-4cb3-bddb-711c7ed7d3d6",                    "leftValue": "={{ $json?.event?.type }}",                    "rightValue": "p2p_chat_create",                    "operator": {                      "type": "string",                      "operation": "equals",                      "name": "filter.operator.equals"                    }                  }                ],                "combinator": "and"              },              "renameOutput": true,              "outputKey": "首次创建回话"            },            {              "conditions": {                "options": {                  "caseSensitive": true,                  "leftValue": "",                  "typeValidation": "loose",                  "version": 2                },                "conditions": [                  {                    "id": "ad9e3044-73b9-48ea-8dca-7ddfabde6f63",                    "leftValue": "={{ $json?.header?.event_type }}",                    "rightValue": "im.message.receive_v1",                    "operator": {                      "type": "string",                      "operation": "equals",                      "name": "filter.operator.equals"                    }                  }                ],                "combinator": "and"              },              "renameOutput": true,              "outputKey": "接受来自用户的消息"            },            {              "conditions": {                "options": {                  "caseSensitive": true,                  "leftValue": "",                  "typeValidation": "loose",                  "version": 2                },                "conditions": [                  {                    "id": "76b2ecac-96db-4d60-9bda-a62198ccebc7",                    "leftValue": "={{ $json?.header?.event_type }}",                    "rightValue": "im.message.message_read_v1",                    "operator": {                      "type": "string",                      "operation": "equals",                      "name": "filter.operator.equals"                    }                  }                ],                "combinator": "and"              },              "renameOutput": true,              "outputKey": "用户已经阅读了消息"            }          ]        },        "looseTypeValidation": true,        "options": {}      },      "type": "n8n-nodes-base.switch",      "typeVersion": 3.2,      "position": [        208,        416      ],      "id": "ed8c8b4b-89bd-49e3-a62a-1b2d7bfea2a2",      "name": "判断事件",      "alwaysOutputData": false    },    {      "parameters": {        "jsCode": "const crypto = require('crypto');\n\nclass AESCipher {\n  constructor(key) {\n    const hash = crypto.createHash('sha256');\n    hash.update(key);\n    this.key = hash.digest();\n  }\n\n  decrypt(encrypt) {\n    try {\n      const encryptBuffer = Buffer.from(encrypt, 'base64');\n\n      const decipher = crypto.createDecipheriv('aes-256-cbc', this.key, encryptBuffer.slice(0, 16));\n\n      let decrypted = decipher.update(encryptBuffer.slice(16)\n        .toString('hex'), 'hex', 'utf8');\n      decrypted += decipher.final('utf8');\n      return decrypted;\n    } catch (err) {\n      return null;\n    }\n  }\n\n  encrypt(content, ivKey) {\n    try {\n      ivKey = ivKey ? Buffer.from(ivKey) : crypto.randomBytes(16);\n      let encrypted;\n      const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(this.key), ivKey);\n      cipher.setAutoPadding(true);// 自动填充\n      encrypted = cipher.update(content);\n      encrypted = Buffer.concat([ encrypted, cipher.final() ]);\n      return Buffer.concat([ ivKey, encrypted ]).toString('base64');\n    } catch (err) {\n      return null;\n    }\n  }\n}\n\nconst encryptKey = $input.first().json.encryptKey\nconst encryptData = $input.first().json.requestBody.encrypt\nconst cipher = new AESCipher(encryptKey);\nlet data = cipher.decrypt(encryptData);\n\nif(!data){\n  throw new Error(\"解密失败\")\n}\n\nconst newData = JSON.parse(data);\nreturn newData\n\n"      },      "type": "n8n-nodes-base.code",      "typeVersion": 2,      "position": [        -592,        416      ],      "id": "24133ee6-810a-44dd-8478-b017c0ac2a02",      "name": "解密请求体"    },    {      "parameters": {        "respondWith": "noData",        "options": {}      },      "type": "n8n-nodes-base.respondToWebhook",      "typeVersion": 1.2,      "position": [        -592,        672      ],      "id": "e8265717-2ea0-4778-b8e9-b24f8835ef9a",      "name": "先回复消息再处理流程"    },    {      "parameters": {        "respondWith": "json",        "responseBody": "={\n  \"challenge\": \"{{ $json.challenge }}\"\n} ",        "options": {}      },      "type": "n8n-nodes-base.respondToWebhook",      "typeVersion": 1.2,      "position": [        -112,        256      ],      "id": "fb58ab1e-835b-4541-801d-bbb40a5f4b4e",      "name": "验证challenge"    },    {      "parameters": {        "respondWith": "text",        "responseBody": "=",        "options": {}      },      "type": "n8n-nodes-base.respondToWebhook",      "typeVersion": 1.2,      "position": [        -16,        432      ],      "id": "29f918a3-260b-4eee-aaff-aeb3a3f38673",      "name": "响应空数据数据1"    },    {      "parameters": {        "conditions": {          "options": {            "caseSensitive": true,            "leftValue": "",            "typeValidation": "strict",            "version": 2          },          "conditions": [            {              "id": "e98496c8-e55e-4655-aeb4-7b2d22efb4af",              "leftValue": "={{ $json.type }}",              "rightValue": "url_verification",              "operator": {                "type": "string",                "operation": "equals",                "name": "filter.operator.equals"              }            }          ],          "combinator": "and"        },        "options": {}      },      "type": "n8n-nodes-base.if",      "typeVersion": 2.2,      "position": [        -352,        416      ],      "id": "b191cd1d-7f11-4c07-8eea-c46a7e632408",      "name": "判断是验证模式还是对话模式"    },    {      "parameters": {        "conditions": {          "options": {            "caseSensitive": true,            "leftValue": "",            "typeValidation": "strict",            "version": 2          },          "conditions": [            {              "id": "585586d4-d1c2-4e2c-913a-b187586d6554",              "leftValue": "={{ $json.accessToken }}",              "rightValue": "",              "operator": {                "type": "string",                "operation": "empty",                "singleValue": true              }            }          ],          "combinator": "and"        },        "options": {}      },      "type": "n8n-nodes-base.if",      "typeVersion": 2.2,      "position": [        -1264,        352      ],      "id": "173b75cf-db06-404c-88ea-c794f2bfb6f1",      "name": "redis数据不存在"    },    {      "parameters": {        "operation": "set",        "key": "={{ $('加密策略及变量定义').item.json.feishuAccessTokenRedisKey }}",        "value": "={{ $json.accessToken }}",        "keyType": "string",        "expire": true,        "ttl": 7200      },      "type": "n8n-nodes-base.redis",      "typeVersion": 1,      "position": [        -848,        160      ],      "id": "6b2ddf7f-4c99-4d39-89cc-129d17841ec2",      "name": "写入",      "alwaysOutputData": true,      "credentials": {        "redis": {          "id": "KsBGRBHleMPalzuV",          "name": "Redis account"        }      }    },    {      "parameters": {        "operation": "get",        "propertyName": "accessToken",        "key": "={{ $json.feishuAccessTokenRedisKey }}",        "keyType": "string",        "options": {}      },      "type": "n8n-nodes-base.redis",      "typeVersion": 1,      "position": [        -1440,        352      ],      "id": "b8e101da-af51-475c-8ce3-827e9416003e",      "name": "读取",      "credentials": {        "redis": {          "id": "KsBGRBHleMPalzuV",          "name": "Redis account"        }      }    },    {      "parameters": {        "fieldToSplitOut": "accessToken",        "options": {}      },      "type": "n8n-nodes-base.splitOut",      "typeVersion": 1,      "position": [        -848,        368      ],      "id": "c742d911-25e3-436e-86c4-e73ca033a2a0",      "name": "accessToken"    },    {      "parameters": {        "mode": "combine",        "combineBy": "combineAll",        "options": {}      },      "type": "n8n-nodes-base.merge",      "typeVersion": 3.1,      "position": [        -1104,        656      ],      "id": "5bd8039f-d9f6-4122-bec5-559d79227541",      "name": "合并变量数据"    },    {      "parameters": {        "content": "# 事件订阅\n## 比如群聊,单聊,等其他事件都属于事件回调\n\n# 回调订阅\n## 比如用户点击了卡片上的按钮,触发了消息回调\n\n# 注意项:\n## 这里都是先响应后处理流程,原因是飞书对于响应有要求,必须要3秒内响应才行",        "height": 424,        "width": 672      },      "type": "n8n-nodes-base.stickyNote",      "position": [        -848,        832      ],      "typeVersion": 1,      "id": "746cc5e6-c581-4390-ae81-57de04abef51",      "name": "Sticky Note1"    },    {      "parameters": {},      "type": "n8n-nodes-base.noOp",      "typeVersion": 1,      "position": [        -384,        672      ],      "id": "038028f1-4d6a-4dde-b4f1-43c61404365c",      "name": "发挥你的想象力1"    },    {      "parameters": {        "content": "## 配置加密策略",        "height": 80,        "width": 200      },      "type": "n8n-nodes-base.stickyNote",      "position": [        -1616,        816      ],      "typeVersion": 1,      "id": "c5b32afc-6270-423e-993e-725ffce3fef0",      "name": "Sticky Note4"    },    {      "parameters": {        "conditions": {          "options": {            "caseSensitive": true,            "leftValue": "",            "typeValidation": "strict",            "version": 2          },          "conditions": [            {              "id": "ecd35f1f-ede6-4331-bb54-3b2c5181bc6b",              "leftValue": "={{ $json?.requestBody?.encrypt }}",              "rightValue": "",              "operator": {                "type": "string",                "operation": "exists",                "singleValue": true              }            }          ],          "combinator": "and"        },        "options": {}      },      "type": "n8n-nodes-base.if",      "typeVersion": 2.2,      "position": [        -832,        656      ],      "id": "686d6d5f-bc62-4638-8ab5-8fdd6057ea07",      "name": "判断是事件还是消息回调"    },    {      "parameters": {        "assignments": {          "assignments": [            {              "id": "2129e27e-64b3-4709-8aea-1c1cd5388abe",              "name": "encryptKey",              "value": "获取路径:事件与回调 - 加密策略 - Encrypt Key",              "type": "string"            },            {              "id": "c75d7f68-1e87-43c4-827d-f6feaa9ecb38",              "name": "verificationToken",              "value": "获取路径:事件与回调 - 加密策略 - Verification Token",              "type": "string"            },            {              "id": "3cdd133d-739d-4061-bf74-0e4f65befc5f",              "name": "requestBody",              "value": "={{ $json.body }}",              "type": "object"            },            {              "id": "090d1681-a682-4635-83d6-bbddf011c7dc",              "name": "feishuAccessTokenRedisKey",              "value": "N8N:feishuChat-accessToken",              "type": "string"            },            {              "id": "25e5f158-4fae-4fae-8d39-1f362ca4adbe",              "name": "appName",              "value": "N8N",              "type": "string"            }          ]        },        "options": {}      },      "type": "n8n-nodes-base.set",      "typeVersion": 3.4,      "position": [        -1600,        656      ],      "id": "756bb73b-1dae-4f99-a230-f09d9eda8fbf",      "name": "加密策略及变量定义"    },    {      "parameters": {        "content": "# Redis流程目的\n## 为了缓存access-token,避免触发飞书频控",        "height": 180,        "width": 380      },      "type": "n8n-nodes-base.stickyNote",      "position": [        -1520,        80      ],      "typeVersion": 1,      "id": "e3264dc5-f2dc-4ffa-b8f7-df0457ca548b",      "name": "Sticky Note"    },    {      "parameters": {        "resource": "auth",        "operation": "auth:getAccessToken"      },      "type": "n8n-nodes-feishu-lite.feishuNode",      "typeVersion": 1,      "position": [        -1056,        160      ],      "id": "af5ca4e2-810b-434b-ba95-72d252b098c4",      "name": "获取应用级别凭证",      "credentials": {        "feishuCredentialsApi": {          "id": "GAq6jm3Ww9VoTiKM",          "name": "Feishu Credentials account"        }      }    },    {      "parameters": {        "resource": "message",        "operation": "message:send",        "receive_id": "={{ $json.event.sender.sender_id.open_id }}",        "content": "={{ $json.event.message.content }}"      },      "type": "n8n-nodes-feishu-lite.feishuNode",      "typeVersion": 1,      "position": [        496,        432      ],      "id": "9b0f0922-7e6d-4d3f-bfe9-c9a5e378332d",      "name": "应用级别凭证回复消息",      "credentials": {        "feishuCredentialsApi": {          "id": "GAq6jm3Ww9VoTiKM",          "name": "Feishu Credentials account"        }      }    }  ],  "pinData": {},  "connections": {    "Webhook": {      "main": [        [          {            "node": "加密策略及变量定义",            "type": "main",            "index": 0          }        ]      ]    },    "解密请求体": {      "main": [        [          {            "node": "判断是验证模式还是对话模式",            "type": "main",            "index": 0          }        ]      ]    },    "先回复消息再处理流程": {      "main": [        [          {            "node": "发挥你的想象力1",            "type": "main",            "index": 0          }        ]      ]    },    "响应空数据数据1": {      "main": [        [          {            "node": "判断事件",            "type": "main",            "index": 0          }        ]      ]    },    "判断是验证模式还是对话模式": {      "main": [        [          {            "node": "验证challenge",            "type": "main",            "index": 0          }        ],        [          {            "node": "响应空数据数据1",            "type": "main",            "index": 0          }        ]      ]    },    "redis数据不存在": {      "main": [        [          {            "node": "获取应用级别凭证",            "type": "main",            "index": 0          }        ],        [          {            "node": "accessToken",            "type": "main",            "index": 0          }        ]      ]    },    "写入": {      "main": [        [          {            "node": "accessToken",            "type": "main",            "index": 0          }        ]      ]    },    "读取": {      "main": [        [          {            "node": "redis数据不存在",            "type": "main",            "index": 0          }        ]      ]    },    "accessToken": {      "main": [        [          {            "node": "合并变量数据",            "type": "main",            "index": 0          }        ]      ]    },    "合并变量数据": {      "main": [        [          {            "node": "判断是事件还是消息回调",            "type": "main",            "index": 0          }        ]      ]    },    "判断事件": {      "main": [        [],        [          {            "node": "应用级别凭证回复消息",            "type": "main",            "index": 0          }        ]      ]    },    "判断是事件还是消息回调": {      "main": [        [          {            "node": "解密请求体",            "type": "main",            "index": 0          }        ],        [          {            "node": "先回复消息再处理流程",            "type": "main",            "index": 0          }        ]      ]    },    "加密策略及变量定义": {      "main": [        [          {            "node": "读取",            "type": "main",            "index": 0          },          {            "node": "合并变量数据",            "type": "main",            "index": 1          }        ]      ]    },    "获取应用级别凭证": {      "main": [        [          {            "node": "写入",            "type": "main",            "index": 0          }        ]      ]    }  },  "active": false,  "settings": {    "executionOrder": "v1",    "callerPolicy": "workflowsFromSameOwner",    "executionTimeout": 60,    "availableInMCP": false,    "timeSavedPerExecution": 1  },  "versionId": "b5a3cc44-c4d7-4e2c-bed2-879685f00b56",  "meta": {    "templateCredsSetupCompleted": true,    "instanceId": "c35e491af1014aad5946d9297c87cbdedd71d7a058335a7a90b1314b3f307034"  },  "id": "VsZb5PWnCSCTObQS",  "tags": []}

共有三处需要配置

先来配置加密策略,双击,将之前获取到值,填写到框中

再来配置Redis,我没有配置,所以要新创建一个。

需要填入Password和Host,这块需要你知道什么是Redis,没有话可以直接docker run一个。

下一步配置 获取应用级别凭证 . 我这也没有,需要创建一个

注意选择应用级别凭证,把之前获取到的Appid和AppSecret写上。

创建完后,选择Resource和Operation

万事俱备,激活workflow

双击Webhook,拿到正式的回调地址

回到飞书开放平台,找到

保存成功后,添加监听事件

  • im.message.receive_v1
  • p2p_chat_create
  • im.chat.access_event.bot_p2p_chat_entered_v1

提示需要创建一个版本

打开飞书,找到应用 N8N回声应用,见证奇迹

结语:

应用可以自动回复你,是不是很酷!有没有感觉打开了一个新的世界?既然可以原样返回,就可以把用户发的信息发给模型分析一下,然后再回给用户,想象空间超大哟~


@sougood 社交搜索 —— 寥寥输入、万千结果,10倍信息获取效率