一个偶然的机会,认识了node-red。这种拖拽控件编写代码的方式给了我很深刻的印象。能够通过简单的拖拽实现mqtt,http,websocket,tcp的服务,作为基于网络的业务流程demon非常方便。最近用它实践了一把,给自己做了一个企业微信消息推送的流程,还是很方便的。
Node-RED背景介绍
• Node-Red是IBM公司开发的一个可视化的编程工具。它允许程序员通过组合各部件来编写应用程序。这些部件可以是硬件设备(如:Arduino板子)、Web API(如:WebSocket in和WebSocket out)、功能函数(如:range)或者在线服务(如:email)。
• Node-Red提供基于网页的编程环境。通过拖拽已定义node到工作区并用线连接node创建数据流来实现编程。程序员通过点击‘Deploy’按钮实现一键保存并执行。程序以JSON字符串的格式保存,方便用户分享、修改。
• Node-Red基于Node.js,它的执行模型和Node.js一样,也是事件驱动非阻塞的。理论上,Node.js的所有模块都可以被封装成Node-Red的一个或几个node。(Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境。使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量又高效。 Node.js 的包管理器 npm是全球最大的开源库生态系统。)
安装Node-red
还是采用docker安装,docker hub 原始地址
docker run -it -p 1880:1880 -v myNodeREDdata:/data --name mynodered nodered/node-red
通过1880端口即可访问web配置界面,比如: http://192.168.1.22:1880
先看下效果
推送的消息,我在jenkins,openwrt软路由,群晖nas里配置了消息通知
群晖7.0支持webhook推送,可以直接配置使用
6.2.3 可以通过自定义短信推送服务实现,将我们自己这个推送接口伪装成一个短信服务,但短信推送的默认触发条件较少,可以在群晖的推送的高级配置里自己选更多场景
准备企业微信
个人也可以开通企业微信,开通企业微信后,添加一个子定义应用,通过这个应用的消息推送接口向用户发送消息。
企业微信开通及应用申请申请流程
流程简单说明
- 提供两个post和get节点,做为请求入口
- 处理,格式化请求消息,记录请求时间
- 从文件读取accesstoken,判断过期时间,如果过期则进入重新请求accesstoken流程
- 异常处理节点,监听读取文件异常,发生异常就进入重新请求accesstoken流程
- 获取新accesstoken,设置企业微信企业id,应用密钥,调用微信接口获取accesstoken,设置当前时间。将报文保存到文件里
- 如果读取accesstoken正常,且未过期,则调用微信的消息推送接口发生消息。
接口地址: http://bbb.xxx.net:1880/send?msg=2222&title=334&url=https://nas.good365.net:5151/
我只接受了三个参数,title,msg,url。post可以支持markdown格式
流程json
[
{
"id": "e12bd28c78ef39c0",
"type": "tab",
"label": "流程 1",
"disabled": false,
"info": "",
"env": []
},
{
"id": "f148c0acd769ebef",
"type": "http in",
"z": "e12bd28c78ef39c0",
"name": "收到post请求",
"url": "/send",
"method": "post",
"upload": false,
"swaggerDoc": "",
"x": 110,
"y": 40,
"wires": [
[
"4f91d762766100d0"
]
]
},
{
"id": "e9e926d7401a4479",
"type": "file in",
"z": "e12bd28c78ef39c0",
"name": "读取accesstoken文件",
"filename": "/data/accesstoken.json",
"format": "utf8",
"chunk": false,
"sendError": false,
"encoding": "none",
"allProps": false,
"x": 1180,
"y": 160,
"wires": [
[
"e95dacdaff16ad7a"
]
]
},
{
"id": "9d919d103ee25af7",
"type": "http request",
"z": "e12bd28c78ef39c0",
"name": "请求新accesstoken",
"method": "GET",
"ret": "obj",
"paytoqs": "ignore",
"url": "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={{corpid}}&corpsecret={{corpsecret}}",
"tls": "",
"persist": false,
"proxy": "",
"authType": "",
"senderr": false,
"x": 950,
"y": 600,
"wires": [
[
"43c6a5df6cb8bd83"
]
]
},
{
"id": "dc7285ddba731a99",
"type": "file",
"z": "e12bd28c78ef39c0",
"name": "存储accesstoken文件",
"filename": "/data/accesstoken.json",
"appendNewline": true,
"createDir": false,
"overwriteFile": "true",
"encoding": "none",
"x": 1500,
"y": 600,
"wires": [
[]
]
},
{
"id": "d2c5926ca0673808",
"type": "http response",
"z": "e12bd28c78ef39c0",
"name": "返回响应",
"statusCode": "",
"headers": {},
"x": 1120,
"y": 40,
"wires": []
},
{
"id": "774bd7eb3a39c99d",
"type": "catch",
"z": "e12bd28c78ef39c0",
"name": "读取accesstoken文件异常",
"scope": [
"e9e926d7401a4479"
],
"uncaught": false,
"x": 370,
"y": 600,
"wires": [
[
"28a3c108d1c14b6f"
]
]
},
{
"id": "1465025fdf4a4e2b",
"type": "http request",
"z": "e12bd28c78ef39c0",
"name": "调用微信接口",
"method": "POST",
"ret": "txt",
"paytoqs": "ignore",
"url": "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={{access_token}}",
"tls": "",
"persist": false,
"proxy": "",
"authType": "",
"senderr": false,
"x": 1540,
"y": 500,
"wires": [
[]
]
},
{
"id": "2e438fa49d1c61d0",
"type": "function",
"z": "e12bd28c78ef39c0",
"name": "整理接口报文",
"func": "var sendmsg = flow.get(\"SEND_MSG\")\nvar sendtitle = flow.get(\"SEND_TITLE\")\nvar sendurl = flow.get(\"SEND_URL\")\n\nvar mk = flow.get(\"SEND_MK\")\n\nnode.log(\"msg:\"+sendmsg);\nnode.log(\"sendtitle:\"+sendtitle);\nnode.log(\"sendurl:\"+sendurl);\n\nnode.log(JSON.stringify(mk));\n\nvar newmsg = {\n access_token: msg.payload.access_token,\n};\nif(mk){\n newmsg.payload = {\n agentid:\"1000002\",\n touser : \"@all\",\n msgtype : \"markdown\",\n markdown : {content:mk.content}\n };\n}else{\n if(sendurl){\n newmsg.payload = {\n agentid:\"1000002\",\n touser : \"@all\",\n msgtype : \"textcard\",\n textcard : {\n title : sendtitle,\n description : sendmsg,\n url : sendurl,\n btntxt:\"更多\"\n }\n };\n}else{\n newmsg.payload = {\n agentid:\"1000002\",\n touser : \"@all\",\n msgtype : \"text\",\n text : {\n content : sendtitle+\"\\n\"+sendmsg\n }\n };\n}\n}\n\n\nnode.log(newmsg.access_token);\n\nreturn newmsg;\n",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 1220,
"y": 500,
"wires": [
[
"1465025fdf4a4e2b"
]
]
},
{
"id": "e95dacdaff16ad7a",
"type": "json",
"z": "e12bd28c78ef39c0",
"name": "转json对象",
"property": "payload",
"action": "",
"pretty": false,
"x": 690,
"y": 320,
"wires": [
[
"bc6f68aab4cfa651"
]
]
},
{
"id": "4f91d762766100d0",
"type": "function",
"z": "e12bd28c78ef39c0",
"name": "格式化请求消息",
"func": "if(msg.payload.text!=undefined){\n if(typeof msg.payload.text == 'object'){\n msg.payload.text = JSON.stringify(msg.payload.text);\n }\nmsg.payload.text = msg.payload.text.replace(/<\\/b>/g,\"\");\nmsg.payload.text = msg.payload.text.replace(/<b>/g,\"\");\n}\n\nflow.set(\"SEND_MSG\",msg.payload.text);\nflow.set(\"SEND_TITLE\",msg.payload.title!=undefined?msg.payload.title:\"通知\");\nflow.set(\"SEND_URL\",msg.payload.url!=undefined?msg.payload.url:\"\");\n\nflow.set(\"SEND_MK\",msg.payload.markdown);\n\nvar sendmsg = flow.get(\"SEND_MSG\")\n\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 300,
"y": 100,
"wires": [
[
"4745e8a209395e11"
]
]
},
{
"id": "43c6a5df6cb8bd83",
"type": "change",
"z": "e12bd28c78ef39c0",
"name": "设定时间戳",
"rules": [
{
"t": "set",
"p": "payload.time",
"pt": "msg",
"to": "",
"tot": "date"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 1210,
"y": 600,
"wires": [
[
"dc7285ddba731a99"
]
]
},
{
"id": "e18066622fbcb8c8",
"type": "switch",
"z": "e12bd28c78ef39c0",
"name": "判断accesstoken过期",
"property": "expires",
"propertyType": "flow",
"rules": [
{
"t": "neq",
"v": "1",
"vt": "num"
},
{
"t": "eq",
"v": "1",
"vt": "num"
}
],
"checkall": "true",
"repair": false,
"outputs": 2,
"x": 720,
"y": 440,
"wires": [
[
"2e438fa49d1c61d0"
],
[
"28a3c108d1c14b6f"
]
]
},
{
"id": "4745e8a209395e11",
"type": "change",
"z": "e12bd28c78ef39c0",
"name": "记录请求时间",
"rules": [
{
"t": "set",
"p": "reqtime",
"pt": "flow",
"to": "",
"tot": "date"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 480,
"y": 100,
"wires": [
[
"217079256df21e74",
"212125fe0ceebd22"
]
]
},
{
"id": "28a3c108d1c14b6f",
"type": "function",
"z": "e12bd28c78ef39c0",
"name": "设置微信密钥",
"func": "msg.corpid = \"ww*************\";\nmsg.corpsecret = \"************************************************\";\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 700,
"y": 600,
"wires": [
[
"9d919d103ee25af7"
]
]
},
{
"id": "bc6f68aab4cfa651",
"type": "function",
"z": "e12bd28c78ef39c0",
"name": "计算过期时间",
"func": "var sendmsg = flow.get(\"reqtime\")\n\nsendmsg = sendmsg - 7200000; \n\nnode.log(msg.payload.time);\nif( msg.payload.time==undefined || msg.payload.time< sendmsg){\n flow.set(\"expires\",1);\n node.log(\"expires\");\n}else{\n flow.set(\"expires\",0);\n node.log(\"not expires\");\n}\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 920,
"y": 320,
"wires": [
[
"e18066622fbcb8c8"
]
]
},
{
"id": "a97da9563e8ee19d",
"type": "change",
"z": "e12bd28c78ef39c0",
"name": "消息赋值",
"rules": [
{
"t": "set",
"p": "payload.timestamp",
"pt": "msg",
"to": "",
"tot": "date"
},
{
"t": "set",
"p": "payload.success",
"pt": "msg",
"to": "1",
"tot": "str"
},
{
"t": "delete",
"p": "payload.msg",
"pt": "msg"
}
],
"action": "",
"property": "",
"from": "",
"to": "",
"reg": false,
"x": 880,
"y": 40,
"wires": [
[
"d2c5926ca0673808"
]
]
},
{
"id": "217079256df21e74",
"type": "function",
"z": "e12bd28c78ef39c0",
"name": "",
"func": "msg.payload = {\n timestamp:0,\n success:0\n};\n\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 690,
"y": 40,
"wires": [
[
"a97da9563e8ee19d"
]
]
},
{
"id": "b3c58bd87bb9ebdb",
"type": "http in",
"z": "e12bd28c78ef39c0",
"name": "",
"url": "/send",
"method": "get",
"upload": false,
"swaggerDoc": "",
"x": 100,
"y": 240,
"wires": [
[
"4f91d762766100d0"
]
]
},
{
"id": "5be2793473ab0a12",
"type": "complete",
"z": "e12bd28c78ef39c0",
"name": "存储完accesstoken重新读取",
"scope": [
"dc7285ddba731a99"
],
"uncaught": false,
"x": 400,
"y": 240,
"wires": [
[
"212125fe0ceebd22"
]
]
},
{
"id": "3fb5dc7b0e082a78",
"type": "switch",
"z": "e12bd28c78ef39c0",
"name": "执行次数控制",
"property": "count",
"propertyType": "msg",
"rules": [
{
"t": "lte",
"v": "3",
"vt": "str"
}
],
"checkall": "true",
"repair": false,
"outputs": 1,
"x": 900,
"y": 160,
"wires": [
[
"e9e926d7401a4479"
]
]
},
{
"id": "212125fe0ceebd22",
"type": "function",
"z": "e12bd28c78ef39c0",
"name": "执行计数",
"func": "if(msg.count==undefined){\n msg.count=1;\n}else{\n msg.count++;\n}\nreturn msg;",
"outputs": 1,
"noerr": 0,
"initialize": "",
"finalize": "",
"libs": [],
"x": 680,
"y": 160,
"wires": [
[
"3fb5dc7b0e082a78"
]
]
}
]