一个偶然的机会,认识了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

先看下效果

grafana 企业微信 docker node-red 企业微信_3c

推送的消息,我在jenkins,openwrt软路由,群晖nas里配置了消息通知

群晖7.0支持webhook推送,可以直接配置使用
6.2.3 可以通过自定义短信推送服务实现,将我们自己这个推送接口伪装成一个短信服务,但短信推送的默认触发条件较少,可以在群晖的推送的高级配置里自己选更多场景

grafana 企业微信 docker node-red 企业微信_grafana 企业微信 docker_02

准备企业微信

个人也可以开通企业微信,开通企业微信后,添加一个子定义应用,通过这个应用的消息推送接口向用户发送消息。

企业微信开通及应用申请申请流程

grafana 企业微信 docker node-red 企业微信_3c_03

流程简单说明

  • 提供两个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"
            ]
        ]
    }
]