知识点
- 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。实现的效果就是配置前:

配置后:

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

进行简单配置

然后就进入了应用管理界面,几个关键点
添加 机器人 能力

权限管理->开通权限,通过搜索添加以下权限:
- im:message
- im:message.p2p_msg:readonly
- im:message:send_as_bot
- event:ip_list

事件与回调->加密策略 拿到 Encrypt Key和Verification Token

凭证与基础信息 拿到AppID 和 AppSecret

创建一次版本

回到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倍信息获取效率
