├── .env ├── .foreverignore ├── .gitignore ├── .travis.yml ├── Makefile ├── Procfile ├── README.md ├── app.js ├── lib └── support.js ├── logo.png ├── manifest.yml ├── package.json ├── qrcode.jpg ├── rules ├── dialog.yaml └── index.js └── test ├── bootstrap.js ├── mocha.opts └── rule.js /.env: -------------------------------------------------------------------------------- 1 | NODE_ENV=production 2 | DEBUG="webot-example:* -*:verbose" 3 | -------------------------------------------------------------------------------- /.foreverignore: -------------------------------------------------------------------------------- 1 | node_modules/**/*.js 2 | **/.git/** 3 | *.bak 4 | .DS_Store 5 | Makefile 6 | Gruntfile.js 7 | *.styl 8 | *.css 9 | *.gif 10 | *.png 11 | *.jpg 12 | *.min.js 13 | *.jade 14 | *.yaml 15 | *.xml 16 | *.md 17 | **.statictmp/** 18 | **static/**/*.js 19 | templates/** 20 | var/* 21 | *.rdb 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | *.swp 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 0.8 4 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | start: 2 | @export DEBUG=webot* && npm start 3 | 4 | clear: 5 | @clear 6 | 7 | test: clear 8 | @export DEBUG="webot* -*verbose" && export WX_TOKEN=test123 && ./node_modules/.bin/mocha 9 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: node app.js 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [微信公共帐号机器人](https://github.com/node-webot/weixin-robot)示例 [![Build Status](https://api.travis-ci.org/node-webot/webot-example.png?branch=master)](https://travis-ci.org/node-webot/webot-example) 2 | 3 | ## 本地运行 4 | 5 | ```bash 6 | git clone https://github.com/node-webot/webot-example.git 7 | cd webot-example/ 8 | npm install 9 | make start 10 | ``` 11 | 12 | 其中,`make start` 命令会调用 `node app.js` 。 13 | 建议你 fork 一份自己的版本,这样你就可以任意做出更改和调试了。 14 | 15 | 16 | ## 消息调试 17 | 18 | 使用 `webot-cli` [命令行工具](https://github.com/node-webot/webot-cli)来发送测试消息。 19 | 20 | 安装: 21 | 22 | ```bash 23 | npm install webot-cli -g 24 | ``` 25 | 26 | `npm install -g` 代表全局安装 npm 模块,你可能需要 `sudo` 权限。 27 | 28 | 使用: 29 | 30 | ``` 31 | webot help # 查看使用帮助 32 | webot send Hello # 发送一条叫「Hello」的消息 33 | webot send image # 调试图片消息 34 | webot send location # 调试地理位置 35 | webot send event # 调试事件消息 36 | ``` 37 | 38 | `webot-cli` 默认访问的接口地址是 http://127.0.0.1:3000 ,要调试本示例的程序, 39 | 你需要指定 `webot send --des http://127.0.0.1:3000/wechat' 40 | 41 | 42 | ## 在微信上试用此示例 43 | 44 | - 微信账号:webot-test 45 | 46 | ![qrcode: webot-test](https://raw.github.com/node-webot/webot-example/master/qrcode.jpg) 47 | 48 | # 搭建你自己的机器人 49 | 50 | 1. fork 本仓库,修改 package.json 里的各项属性 51 | 2. 修改你自己的 app.js ,填写你在微信后台输入的 token 52 | 3. 参考 rules/index.js ,新建你自己的回复规则 53 | 54 | ## 发布到云平台 55 | 56 | 仓库中的 `Procfile` 为 [heroku](http://www.heroku.com/) 的配置文件。 57 | `manifest.yml` 为 [cloudfoundry](http://www.cloudfoundry.com/) 的示例配置文件。 58 | 59 | # Credit 60 | 61 | [weixin-robot](https://github.com/node-webot/weixin-robot) 的[初始版本](https://github.com/node-webot/weixin-robot/tree/0.0.x)由[@ktmud](https://github.com/ktmud)实现, 62 | [@atian](https://github.com/atian25)重构并扩展为 0.2 版本。目前的测试用例也大部分由他完成。 63 | 64 | [weixin-robot] 使用了 [@JacksonTian](https://github.com/JacksonTian) 的 [wechat](https://github.com/node-webot/wechat) 组件。 65 | -------------------------------------------------------------------------------- /app.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var webot = require('weixin-robot'); 3 | 4 | var log = require('debug')('webot-example:log'); 5 | var verbose = require('debug')('webot-example:verbose'); 6 | 7 | // 启动服务 8 | var app = express(); 9 | 10 | // 实际使用时,这里填写你在微信公共平台后台填写的 token 11 | var wx_token = process.env.WX_TOKEN || 'keyboardcat123'; 12 | var wx_token2 = process.env.WX_TOKEN_2 || 'weixinToken2'; 13 | 14 | // 建立多个实例,并监听到不同 path , 15 | var webot2 = new webot.Webot(); 16 | 17 | // 载入webot1的回复规则 18 | require('./rules')(webot); 19 | // 为webot2也指定规则 20 | webot2.set('hello', 'hi.'); 21 | 22 | // 启动机器人, 接管 web 服务请求 23 | webot.watch(app, { token: wx_token, path: '/wechat' }); 24 | // 若省略 path 参数,会监听到根目录 25 | // webot.watch(app, { token: wx_token }); 26 | 27 | // 后面指定的 path 不可为前面实例的子目录 28 | webot2.watch(app, { token: wx_token2, path: '/wechat_2' }); 29 | 30 | // 如果需要 session 支持,sessionStore 必须放在 watch 之后 31 | app.use(express.cookieParser()); 32 | // 为了使用 waitRule 功能,需要增加 session 支持 33 | app.use(express.session({ 34 | secret: 'abced111', 35 | store: new express.session.MemoryStore() 36 | })); 37 | // 在生产环境,你应该将此处的 store 换为某种永久存储。 38 | // 请参考 http://expressjs.com/2x/guide.html#session-support 39 | 40 | // 在环境变量提供的 $PORT 或 3000 端口监听 41 | var port = process.env.PORT || 3000; 42 | app.listen(port, function(){ 43 | log("Listening on %s", port); 44 | }); 45 | 46 | // 微信接口地址只允许服务放在 80 端口 47 | // 所以需要做一层 proxy 48 | app.enable('trust proxy'); 49 | 50 | // 当然,如果你的服务器允许,你也可以直接用 node 来 serve 80 端口 51 | // app.listen(80); 52 | 53 | if(!process.env.DEBUG){ 54 | console.log("set env variable `DEBUG=webot-example:*` to display debug info."); 55 | } 56 | -------------------------------------------------------------------------------- /lib/support.js: -------------------------------------------------------------------------------- 1 | var debug = require('debug'); 2 | var log = debug('webot-example:log'); 3 | 4 | var _ = require('underscore')._; 5 | var request = require('request'); 6 | 7 | /** 8 | * 通过高德地图API查询用户的位置信息 9 | */ 10 | exports.geo2loc = function geo2loc(param, cb){ 11 | var options = { 12 | url: 'http://restapi.amap.com/rgeocode/simple', 13 | qs: { 14 | resType: 'json', 15 | encode: 'utf-8', 16 | range: 3000, 17 | roadnum: 0, 18 | crossnum: 0, 19 | poinum: 0, 20 | retvalue: 1, 21 | sid: 7001, 22 | region: [param.lng, param.lat].join(',') 23 | } 24 | }; 25 | log('querying amap for: [%s]', options.qs.region); 26 | 27 | //查询 28 | request.get(options, function(err, res, body){ 29 | if(err){ 30 | error('geo2loc failed', err); 31 | return cb(err); 32 | } 33 | var data = JSON.parse(body); 34 | if(data.list && data.list.length>=1){ 35 | data = data.list[0]; 36 | var location = data.city.name || data.province.name; 37 | log('location is %s, %j', location, data); 38 | return cb(null, location, data); 39 | } 40 | log('geo2loc found nth.'); 41 | return cb('geo2loc found nth.'); 42 | }); 43 | }; 44 | 45 | /** 46 | * 搜索百度 47 | * 48 | * @param {String} keyword 关键词 49 | * @param {Function} cb 回调函数 50 | * @param {Error} cb.err 错误信息 51 | * @param {String} cb.result 查询结果 52 | */ 53 | exports.search = function(keyword, cb){ 54 | log('searching: %s', keyword); 55 | var options = { 56 | url: 'http://www.baidu.com/s', 57 | qs: { 58 | wd: keyword 59 | } 60 | }; 61 | request.get(options, function(err, res, body){ 62 | if (err || !body){ 63 | return cb(null, '现在暂时无法搜索,待会儿再来好吗?'); 64 | } 65 | var regex = /

\s*(.*?<\/a>)[\s\S]*?<\/h3>/gi; 66 | var links = []; 67 | var i = 1; 68 | 69 | while (true) { 70 | var m = regex.exec(body); 71 | if (!m || i > 5) break; 72 | links.push(i + '. ' + m[1]); 73 | i++; 74 | } 75 | 76 | var result; 77 | if (links.length) { 78 | result = '在百度搜索:' + keyword +',得到以下结果:\n' + links.join('\n'); 79 | result = result.replace(/\s*data-click="[\s\S]*?"/gi, ''); 80 | result = result.replace(/\s*onclick="[\s\S]*?"/gi, ''); 81 | result = result.replace(/\s*target="[\s\S]*?"/gi, ''); 82 | result = result.replace(/\s{2,}/gi, ' '); 83 | result = result.replace(/([\s\S]*?)<\/em>/gi, '$1'); 84 | result = result.replace(/([\s\S]*?)<\/font>/gi, '$1'); 85 | result = result.replace(/([\s\S]*?)<\/span>/gi, '$1'); 86 | } else { 87 | result = '搜不到任何结果呢'; 88 | } 89 | 90 | // result 会直接作为 91 | // robot.reply() 的返回值 92 | // 93 | // 如果返回的是一个数组: 94 | // result = [{ 95 | // pic: 'http://img.xxx....', 96 | // url: 'http://....', 97 | // title: '这个搜索结果是这样的', 98 | // description: '哈哈哈哈哈....' 99 | // }]; 100 | // 101 | // 则会生成图文列表 102 | return cb(null, result); 103 | }); 104 | }; 105 | 106 | /** 107 | * 下载图片 108 | * 109 | * 注意:只是简陋的实现,不负责检测下载是否正确,实际应用还需要检查statusCode. 110 | * @param {String} url 目标网址 111 | * @param {String} path 保存路径 112 | */ 113 | exports.download = function(url, stream){ 114 | log('downloading %s a stream', url); 115 | return request(url).pipe(stream); 116 | }; 117 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-webot/webot-example/25a542fbd8fb44e624419de04206725522278348/logo.png -------------------------------------------------------------------------------- /manifest.yml: -------------------------------------------------------------------------------- 1 | --- 2 | applications: 3 | - name: webot 4 | framework: node 5 | runtime: node08 6 | memory: 64M 7 | instances: 1 8 | url: webot.${target-base} 9 | path: . 10 | env: 11 | DEBUG: webot.* 12 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "weixin-robot-example", 3 | "version": "0.0.5", 4 | "main": "app.js", 5 | "dependencies": { 6 | "express": "~3.0", 7 | "request": "~2.12", 8 | "underscore": "~1.4", 9 | "debug": "~0.7.2", 10 | "js-yaml": "~2.0.3", 11 | "weixin-robot": "~0.5.0" 12 | }, 13 | "devDependencies": { 14 | "mocha": "~1.13.0", 15 | "should": "~2.0.1", 16 | "xml2js": "~0.2.8" 17 | }, 18 | "engines": { 19 | "node" : ">=0.8.0", 20 | "npm" : ">=1.1.6" 21 | }, 22 | "scripts": { 23 | "start": "node app.js", 24 | "test": "make test" 25 | }, 26 | "keywords": [ 27 | "weixin", 28 | "webot", 29 | "wechat" 30 | ], 31 | "description": "weixin-robot 模块示例", 32 | "repository": "https://github.com/node-webot/webot-example.git", 33 | "author": "Jesse Yang ", 34 | "contributors": [ 35 | "Jesse Yang (http://github.com/ktmud)", 36 | "TZ (http://atian25.iteye.com/)" 37 | ], 38 | "license": "MIT" 39 | } 40 | -------------------------------------------------------------------------------- /qrcode.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/node-webot/webot-example/25a542fbd8fb44e624419de04206725522278348/qrcode.jpg -------------------------------------------------------------------------------- /rules/dialog.yaml: -------------------------------------------------------------------------------- 1 | --- 2 | # 直接回复 3 | hi|你好|nihao: 'hi, I am robot' 4 | 5 | # 随机回复一个 6 | hello: 7 | - 你好 8 | - fine 9 | - how are you 10 | 11 | test: 12 | - Roger that! 13 | # YAML可能会把微信的表情符解析为数组,引号引起来比较保险 14 | - "收到你的测试消息了!嘻嘻..[可爱]" 15 | 16 | # 回复多行文本,只需在冒号后面加上竖线(|) 17 | 帮助: | 18 | 帮助这个事情, 19 | 说起来也不容易, 20 | 我也不知道怎么跟你解释, 21 | 发送 help 试试看呢 22 | 23 | 24 | # 匹配组替换 25 | /key (.*)/i: '你输入的匹配关键词是:{1}, \{1}replaced' 26 | 27 | # 可以是一个rule配置,如果没有pattern,自动使用key 28 | yaml: 29 | name: 'test_yaml_object' 30 | handler: '这是一个yaml的object配置' 31 | -------------------------------------------------------------------------------- /rules/index.js: -------------------------------------------------------------------------------- 1 | var crypto = require('crypto'); 2 | 3 | var debug = require('debug'); 4 | var log = debug('webot-example:log'); 5 | var verbose = debug('webot-example:verbose'); 6 | var error = debug('webot-example:error'); 7 | 8 | var _ = require('underscore')._; 9 | var search = require('../lib/support').search; 10 | var geo2loc = require('../lib/support').geo2loc; 11 | 12 | var package_info = require('../package.json'); 13 | 14 | /** 15 | * 初始化路由规则 16 | */ 17 | module.exports = exports = function(webot){ 18 | var reg_help = /^(help|\?)$/i 19 | webot.set({ 20 | // name 和 description 都不是必须的 21 | name: 'hello help', 22 | description: '获取使用帮助,发送 help', 23 | pattern: function(info) { 24 | //首次关注时,会收到subscribe event 25 | return info.is('event') && info.param.event === 'subscribe' || reg_help.test(info.text); 26 | }, 27 | handler: function(info){ 28 | var reply = { 29 | title: '感谢你收听webot机器人', 30 | pic: 'https://raw.github.com/node-webot/webot-example/master/qrcode.jpg', 31 | url: 'https://github.com/node-webot/webot-example', 32 | description: [ 33 | '你可以试试以下指令:', 34 | 'game : 玩玩猜数字的游戏吧', 35 | 's+空格+关键词 : 我会帮你百度搜索喔', 36 | 's+空格+nde : 可以试试我的纠错能力', 37 | '使用「位置」发送你的经纬度', 38 | '重看本指令请回复help或问号', 39 | '更多指令请回复more', 40 | 'PS: 点击下面的「查看全文」将跳转到我的github页' 41 | ].join('\n') 42 | }; 43 | // 返回值如果是list,则回复图文消息列表 44 | return reply; 45 | } 46 | }); 47 | 48 | // 更简单地设置一条规则 49 | webot.set(/^more$/i, function(info){ 50 | var reply = _.chain(webot.gets()).filter(function(rule){ 51 | return rule.description; 52 | }).map(function(rule){ 53 | //console.log(rule.name) 54 | return '> ' + rule.description; 55 | }).join('\n').value(); 56 | 57 | return ['我的主人还没教我太多东西,你可以考虑帮我加下.\n可用的指令:\n'+ reply, 58 | '没有更多啦!当前可用指令:\n' + reply]; 59 | }); 60 | 61 | webot.set('who_are_you', { 62 | description: '想知道我是谁吗? 发送: who?', 63 | // pattern 既可以是函数,也可以是 regexp 或 字符串(模糊匹配) 64 | pattern: /who|你是[谁\?]+/i, 65 | // 回复handler也可以直接是字符串或数组,如果是数组则随机返回一个子元素 66 | handler: ['我是神马机器人', '微信机器人'] 67 | }); 68 | 69 | // 正则匹配后的匹配组存在 info.query 中 70 | webot.set('your_name', { 71 | description: '自我介绍下吧, 发送: I am [enter_your_name]', 72 | pattern: /^(?:my name is|i am|我(?:的名字)?(?:是|叫)?)\s*(.*)$/i, 73 | 74 | // handler: function(info, action){ 75 | // return '你好,' + info.param[1] 76 | // } 77 | // 或者更简单一点 78 | handler: '你好,{1}' 79 | }); 80 | 81 | // 简单的纯文本对话,可以用单独的 yaml 文件来定义 82 | require('js-yaml'); 83 | webot.dialog(__dirname + '/dialog.yaml'); 84 | 85 | // 支持一次性加多个(方便后台数据库存储规则) 86 | webot.set([{ 87 | name: 'morning', 88 | description: '打个招呼吧, 发送: good morning', 89 | pattern: /^(早上?好?|(good )?moring)[啊\!!\.。]*$/i, 90 | handler: function(info){ 91 | var d = new Date(); 92 | var h = d.getHours(); 93 | if (h < 3) return '[嘘] 我这边还是深夜呢,别吵着大家了'; 94 | if (h < 5) return '这才几点钟啊,您就醒了?'; 95 | if (h < 7) return '早啊官人!您可起得真早呐~ 给你请安了!\n 今天想参加点什么活动呢?'; 96 | if (h < 9) return 'Morning, sir! 新的一天又开始了!您今天心情怎么样?'; 97 | if (h < 12) return '这都几点了,还早啊...'; 98 | if (h < 14) return '人家中午饭都吃过了,还早呐?'; 99 | if (h < 17) return '如此美好的下午,是很适合出门逛逛的'; 100 | if (h < 21) return '早,什么早?找碴的找?'; 101 | if (h >= 21) return '您还是早点睡吧...'; 102 | } 103 | }, { 104 | name: 'time', 105 | description: '想知道几点吗? 发送: time', 106 | pattern: /^(几点了|time)\??$/i, 107 | handler: function(info) { 108 | var d = new Date(); 109 | var h = d.getHours(); 110 | var t = '现在是服务器时间' + h + '点' + d.getMinutes() + '分'; 111 | if (h < 4 || h > 22) return t + ',夜深了,早点睡吧 [月亮]'; 112 | if (h < 6) return t + ',您还是再多睡会儿吧'; 113 | if (h < 9) return t + ',又是一个美好的清晨呢,今天准备去哪里玩呢?'; 114 | if (h < 12) return t + ',一日之计在于晨,今天要做的事情安排好了吗?'; 115 | if (h < 15) return t + ',午后的冬日是否特别动人?'; 116 | if (h < 19) return t + ',又是一个充满活力的下午!今天你的任务完成了吗?'; 117 | if (h <= 22) return t + ',这样一个美好的夜晚,有没有去看什么演出?'; 118 | return t; 119 | } 120 | }]); 121 | 122 | // 等待下一次回复 123 | webot.set('guess my sex', { 124 | pattern: /是男.还是女.|你.*男的女的/, 125 | handler: '你猜猜看呐', 126 | replies: { 127 | '/女|girl/i': '人家才不是女人呢', 128 | '/男|boy/i': '是的,我就是翩翩公子一枚', 129 | 'both|不男不女': '你丫才不男不女呢', 130 | '不猜': '好的,再见', 131 | // 请谨慎使用通配符 132 | '/.*/': function reguess(info) { 133 | if (info.rewaitCount < 2) { 134 | info.rewait(); 135 | return '你到底还猜不猜嘛!'; 136 | } 137 | return '看来你真的不想猜啊'; 138 | }, 139 | } 140 | 141 | // 也可以用一个函数搞定: 142 | // replies: function(info){ 143 | // return 'haha, I wont tell you' 144 | // } 145 | 146 | // 也可以是数组格式,每个元素为一条rule 147 | // replies: [{ 148 | // pattern: '/^g(irl)?\\??$/i', 149 | // handler: '猜错' 150 | // },{ 151 | // pattern: '/^b(oy)?\\??$/i', 152 | // handler: '猜对了' 153 | // },{ 154 | // pattern: 'both', 155 | // handler: '对你无语...' 156 | // }] 157 | }); 158 | 159 | // 定义一个 wait rule 160 | webot.waitRule('wait_guess', function(info) { 161 | var r = Number(info.text); 162 | 163 | // 用户不想玩了... 164 | if (isNaN(r)) { 165 | info.resolve(); 166 | return null; 167 | } 168 | 169 | var num = info.session.guess_answer; 170 | 171 | if (r === num) { 172 | return '你真聪明!'; 173 | } 174 | 175 | var rewaitCount = info.session.rewait_count || 0; 176 | if (rewaitCount >= 2) { 177 | return '怎么这样都猜不出来!答案是 ' + num + ' 啊!'; 178 | } 179 | 180 | //重试 181 | info.rewait(); 182 | return (r > num ? '大了': '小了') +',还有' + (2 - rewaitCount) + '次机会,再猜.'; 183 | }); 184 | 185 | webot.set('guess number', { 186 | description: '发送: game , 玩玩猜数字的游戏吧', 187 | pattern: /(?:game|玩?游戏)\s*(\d*)/, 188 | handler: function(info){ 189 | //等待下一次回复 190 | var num = Number(info.param[1]) || _.random(1,9); 191 | 192 | verbose('answer is: ' + num); 193 | 194 | info.session.guess_answer = num; 195 | 196 | info.wait('wait_guess'); 197 | return '玩玩猜数字的游戏吧, 1~9,选一个'; 198 | } 199 | }); 200 | 201 | webot.waitRule('wait_suggest_keyword', function(info, next){ 202 | if (!info.text) { 203 | return next(); 204 | } 205 | 206 | // 按照定义规则的 name 获取其他 handler 207 | var rule_search = webot.get('search'); 208 | 209 | // 用户回复回来的消息 210 | if (info.text.match(/^(好|要|y)$/i)) { 211 | // 修改回复消息的匹配文本,传入搜索命令执行 212 | info.param[0] = 's nodejs'; 213 | info.param[1] = 'nodejs'; 214 | 215 | // 执行某条规则 216 | webot.exec(info, rule_search, next); 217 | // 也可以调用 rule 的 exec 方法 218 | // rule_search.exec(info, next); 219 | } else { 220 | info.param[1] = info.session.last_search_word; 221 | // 或者直接调用 handler : 222 | rule_search.handler(info, next); 223 | // 甚至直接用命名好的 function name 来调用: 224 | // do_search(info, next); 225 | } 226 | // remember to clean your session object. 227 | delete info.session.last_search_word; 228 | }); 229 | // 调用已有的action 230 | webot.set('suggest keyword', { 231 | description: '发送: s nde ,然后再回复Y或其他', 232 | pattern: /^(?:搜索?|search|s\b)\s*(.+)/i, 233 | handler: function(info){ 234 | var q = info.param[1]; 235 | if (q === 'nde') { 236 | info.session.last_search_word = q; 237 | info.wait('wait_suggest_keyword'); 238 | return '你输入了:' + q + ',似乎拼写错误。要我帮你更改为「nodejs」并搜索吗?'; 239 | } 240 | } 241 | }); 242 | 243 | function do_search(info, next){ 244 | // pattern的解析结果将放在param里 245 | var q = info.param[1]; 246 | log('searching: ', q); 247 | // 从某个地方搜索到数据... 248 | return search(q , next); 249 | } 250 | 251 | // 可以通过回调返回结果 252 | webot.set('search', { 253 | description: '发送: s 关键词 ', 254 | pattern: /^(?:搜索?|search|百度|s\b)\s*(.+)/i, 255 | //handler也可以是异步的 256 | handler: do_search 257 | }); 258 | 259 | 260 | webot.waitRule('wait_timeout', function(info) { 261 | if (new Date().getTime() - info.session.wait_begin > 5000) { 262 | delete info.session.wait_begin; 263 | return '你的操作超时了,请重新输入'; 264 | } else { 265 | return '你在规定时限里面输入了: ' + info.text; 266 | } 267 | }); 268 | 269 | // 超时处理 270 | webot.set('timeout', { 271 | description: '输入timeout, 等待5秒后回复,会提示超时', 272 | pattern: 'timeout', 273 | handler: function(info) { 274 | info.session.wait_begin = new Date().getTime(); 275 | info.wait('wait_timeout'); 276 | return '请等待5秒后回复'; 277 | } 278 | }); 279 | 280 | /** 281 | * Wait rules as lists 282 | * 283 | * 实现类似电话客服的自动应答流程 284 | * 285 | */ 286 | webot.set(/^ok webot$/i, function(info) { 287 | info.wait('list'); 288 | return '可用指令:\n' + 289 | '1 - 查看程序信息\n' + 290 | '2 - 进入名字选择'; 291 | }); 292 | webot.waitRule('list', { 293 | '1': 'webot ' + package_info.version, 294 | '2': function(info) { 295 | info.wait('list-2'); 296 | return '请选择人名:\n' + 297 | '1 - Marry\n' + 298 | '2 - Jane\n' + 299 | '3 - 自定义' 300 | } 301 | }); 302 | webot.waitRule('list-2', { 303 | '1': '你选择了 Marry', 304 | '2': '你选择了 Jane', 305 | '3': function(info) { 306 | info.wait('list-2-3'); 307 | return '请输入你想要的人'; 308 | } 309 | }); 310 | webot.waitRule('list-2-3', function(info) { 311 | if (info.text) { 312 | return '你输入了 ' + info.text; 313 | } 314 | }); 315 | 316 | 317 | //支持location消息 此examples使用的是高德地图的API 318 | //http://restapi.amap.com/rgeocode/simple?resType=json&encode=utf-8&range=3000&roadnum=0&crossnum=0&poinum=0&retvalue=1&sid=7001®ion=113.24%2C23.08 319 | webot.set('check_location', { 320 | description: '发送你的经纬度,我会查询你的位置', 321 | pattern: function(info){ 322 | return info.is('location'); 323 | }, 324 | handler: function(info, next){ 325 | geo2loc(info.param, function(err, location, data) { 326 | location = location || info.label; 327 | next(null, location ? '你正在' + location : '我不知道你在什么地方。'); 328 | }); 329 | } 330 | }); 331 | 332 | //图片 333 | webot.set('check_image', { 334 | description: '发送图片,我将返回其hash值', 335 | pattern: function(info){ 336 | return info.is('image'); 337 | }, 338 | handler: function(info, next){ 339 | verbose('image url: %s', info.param.picUrl); 340 | try{ 341 | var shasum = crypto.createHash('md5'); 342 | 343 | var req = require('request')(info.param.picUrl); 344 | 345 | req.on('data', function(data) { 346 | shasum.update(data); 347 | }); 348 | req.on('end', function() { 349 | return next(null, '你的图片hash: ' + shasum.digest('hex')); 350 | }); 351 | }catch(e){ 352 | error('Failed hashing image: %s', e) 353 | return '生成图片hash失败: ' + e; 354 | } 355 | } 356 | }); 357 | 358 | // 回复图文消息 359 | webot.set('reply_news', { 360 | description: '发送news,我将回复图文消息你', 361 | pattern: /^news\s*(\d*)$/, 362 | handler: function(info){ 363 | var reply = [ 364 | {title: '微信机器人', description: '微信机器人测试帐号:webot', pic: 'https://raw.github.com/node-webot/webot-example/master/qrcode.jpg', url: 'https://github.com/node-webot/webot-example'}, 365 | {title: '豆瓣同城微信帐号', description: '豆瓣同城微信帐号二维码:douban-event', pic: 'http://i.imgur.com/ijE19.jpg', url: 'https://github.com/node-webot/weixin-robot'}, 366 | {title: '图文消息3', description: '图文消息描述3', pic: 'https://raw.github.com/node-webot/webot-example/master/qrcode.jpg', url: 'http://www.baidu.com'} 367 | ]; 368 | // 发送 "news 1" 时只回复一条图文消息 369 | return Number(info.param[1]) == 1 ? reply[0] : reply; 370 | } 371 | }); 372 | 373 | // 可以指定图文消息的映射关系 374 | webot.config.mapping = function(item, index, info){ 375 | //item.title = (index+1) + '> ' + item.title; 376 | return item; 377 | }; 378 | 379 | //所有消息都无法匹配时的fallback 380 | webot.set(/.*/, function(info){ 381 | // 利用 error log 收集听不懂的消息,以利于接下来完善规则 382 | // 你也可以将这些 message 存入数据库 383 | log('unhandled message: %s', info.text); 384 | info.flag = true; 385 | return '你发送了「' + info.text + '」,可惜我太笨了,听不懂. 发送: help 查看可用的指令'; 386 | }); 387 | }; 388 | -------------------------------------------------------------------------------- /test/bootstrap.js: -------------------------------------------------------------------------------- 1 | var xml2js = require('xml2js'); 2 | var xmlParser = new xml2js.Parser(); 3 | var _ = require('underscore')._; 4 | var request = require('request'); 5 | var crypto = require('crypto'); 6 | 7 | /** 8 | * @class WeBotShell 测试辅助 9 | */ 10 | function WeBotShell(){ 11 | } 12 | 13 | /** 14 | * @method makeAuthQuery 组装querystring 15 | * @param {String} token 微信token 16 | */ 17 | WeBotShell.makeAuthQuery = function(token, timestamp, nonce){ 18 | var obj = { 19 | token: token, 20 | timestamp: timestamp || new Date().getTime().toString(), 21 | nonce: nonce || parseInt((Math.random() * 10e10), 10).toString(), 22 | echostr: 'echostr_' + parseInt((Math.random() * 10e10), 10).toString() 23 | }; 24 | 25 | var s = [obj.token, obj.timestamp, obj.nonce].sort().join(''); 26 | obj.signature = crypto.createHash('sha1').update(s).digest('hex'); 27 | return obj; 28 | }; 29 | 30 | /** 31 | * @method makeRequest 获取发送请求的函数 32 | * 33 | * @param {String} url 服务地址 34 | * @param {Object} token 微信token 35 | * @return {Function} 发送请求的回调函数,签名为function(info, cb(err, result)) 36 | * 37 | * - info {Object} 要发送的内容: 38 | * 39 | * - sp {String} 微信公众平台ID 40 | * - user {String} 用户ID 41 | * - type {String} 消息类型: text / location / image 42 | * - text {String} 文本消息的内容 43 | * - xPos {Number} 地理位置纬度 44 | * - yPos {Number} 地理位置经度 45 | * - scale {Number} 地图缩放大小 46 | * - label {String} 地理位置信息 47 | * - pic {String} 图片链接 48 | * 49 | * - cb {Function} 回调函数 50 | * 51 | * - err {Error} 错误消息 52 | * - result {Object} 服务器回传的结果,JSON 53 | * 54 | * - return content {String} 返回发送的XML 55 | */ 56 | WeBotShell.makeRequest = function(url, token){ 57 | return function(info, cb){ 58 | //默认值 59 | info = _.isString(info) ? {text: info} : info; 60 | 61 | _.defaults(info, { 62 | sp: 'webot', 63 | user: 'client', 64 | type: 'text', 65 | text: 'help', 66 | }); 67 | 68 | var content = _.template(WeBotShell.TEMPLATE)(info); 69 | 70 | //发送请求 71 | request.post({ 72 | url: url, 73 | qs: WeBotShell.makeAuthQuery(token), 74 | body: content 75 | }, function(err, res, body){ 76 | if(err || res.statusCode=='403' || !body){ 77 | cb(err || res.statusCode, body); 78 | }else{ 79 | xmlParser.parseString(body, function(err, result){ 80 | if (err || !result || !result.xml){ 81 | cb(err || 'result format incorrect', result); 82 | }else{ 83 | var json = result.xml; 84 | json.ToUserName = json.ToUserName && String(json.ToUserName); 85 | json.FromUserName = json.FromUserName && String(json.FromUserName); 86 | json.CreateTime = json.CreateTime && Number(json.CreateTime); 87 | json.FuncFlag = json.FuncFlag && Number(json.FuncFlag); 88 | json.MsgType = json.MsgType && String(json.MsgType); 89 | json.Content = json.Content && String(json.Content); 90 | if(json.MsgType=='news'){ 91 | json.ArticleCount = json.ArticleCount && Number(json.ArticleCount); 92 | json.Articles = json.Articles && json.Articles.length>=1 && json.Articles[0]; 93 | } 94 | cb(err, json); 95 | } 96 | }); 97 | } 98 | }); 99 | return content; 100 | }; 101 | }; 102 | 103 | /** 104 | * @property {String} tpl XML模版 105 | */ 106 | WeBotShell.TEMPLATE= [ 107 | '', 108 | ']]>', 109 | ']]>', 110 | '<%=(new Date().getTime())%>', 111 | ']]>', 112 | '<% if(type=="text"){ %>', 113 | ']]>', 114 | '<% }else if(type=="location"){ %>', 115 | '<%=xPos%>', 116 | '<%=yPos%>', 117 | '<%=scale%>', 118 | '', 119 | '<% }else if(type=="event"){ %>', 120 | ']]>', 121 | ']]>', 122 | '<% }else if(type=="image"){ %>', 123 | ']]>', 124 | '<% } %>', 125 | '' 126 | ].join(''); 127 | 128 | 129 | module.exports = exports = WeBotShell; 130 | -------------------------------------------------------------------------------- /test/mocha.opts: -------------------------------------------------------------------------------- 1 | --require should 2 | --reporter spec 3 | --ui bdd 4 | --growl 5 | --timeout 6000 6 | -------------------------------------------------------------------------------- /test/rule.js: -------------------------------------------------------------------------------- 1 | var should = require('should'); 2 | 3 | var token = process.env.WX_TOKEN || 'keyboardcat123'; 4 | var token2 = process.env.WX_TOKEN_2 || 'weixinToken2'; 5 | var port = process.env.PORT || 3000; 6 | 7 | var bootstrap = require('./bootstrap.js'); 8 | var makeRequest = bootstrap.makeRequest; 9 | var sendRequest = makeRequest('http://localhost:' + port + '/wechat', token); 10 | var sendRequest2 = makeRequest('http://localhost:' + port + '/wechat_2', token2); 11 | 12 | var app = require('../app.js'); 13 | 14 | //公用检测指令 15 | var detect = function(info, err, json, content){ 16 | should.exist(info); 17 | should.not.exist(err); 18 | should.exist(json); 19 | json.should.have.type('object'); 20 | if(content){ 21 | json.should.have.property('Content'); 22 | json.Content.should.match(content); 23 | } 24 | }; 25 | 26 | describe('wechat2', function(){ 27 | //初始化 28 | var info = null; 29 | beforeEach(function(){ 30 | info = { 31 | sp: 'webot', 32 | user: 'client', 33 | type: 'text' 34 | }; 35 | }); 36 | 37 | //测试文本消息 38 | describe('text', function(){ 39 | //检测more指令 40 | it('should return hi', function(done){ 41 | info.text = 'hello'; 42 | sendRequest2(info, function(err, json){ 43 | detect(info, err, json, /^hi.$/); 44 | done(); 45 | }); 46 | }); 47 | }); 48 | }); 49 | 50 | describe('wechat1', function(){ 51 | //初始化 52 | var info = null; 53 | beforeEach(function(){ 54 | info = { 55 | sp: 'webot', 56 | user: 'client', 57 | type: 'text' 58 | }; 59 | }); 60 | 61 | //测试文本消息 62 | describe('text', function(){ 63 | //检测more指令 64 | it('should return more msg', function(done){ 65 | info.text = 'more'; 66 | sendRequest(info, function(err, json){ 67 | detect(info, err, json, /指令/ ); 68 | done(); 69 | }); 70 | }); 71 | 72 | it('should pass multi line yaml', function(done){ 73 | info.text = '帮助'; 74 | sendRequest(info, function(err, json){ 75 | detect(info, err, json, /,\n/ ); 76 | done(); 77 | }); 78 | }); 79 | 80 | //检测who指令 81 | it('should return who msg', function(done){ 82 | info.text = 'who'; 83 | sendRequest(info, function(err, json){ 84 | detect(info, err, json, /机器人/); 85 | done(); 86 | }); 87 | }); 88 | 89 | //检测name指令 90 | it('should return name msg', function(done){ 91 | info.text = 'I am a mocha tester'; 92 | sendRequest(info, function(err, json){ 93 | detect(info, err, json, /a mocha tester/); 94 | done(); 95 | }); 96 | }); 97 | 98 | //检测time指令 99 | it('should return time msg', function(done){ 100 | info.text = '几点了'; 101 | sendRequest(info, function(err, json){ 102 | detect(info, err, json, /时间/); 103 | done(); 104 | }); 105 | }); 106 | 107 | //检测不匹配指令 108 | it('should return not_match msg', function(done){ 109 | info.text = '#$%^&!@#$'; 110 | sendRequest(info, function(err, json){ 111 | detect(info, err, json, /我太笨了/); 112 | done(); 113 | }); 114 | }); 115 | }); 116 | 117 | //测试dialog消息 118 | describe('dialog', function(){ 119 | //检测key指令 120 | it('should return key msg', function(done){ 121 | info.text = 'key aaaa'; 122 | sendRequest(info, function(err, json){ 123 | detect(info, err, json, /aaaa/); 124 | json.Content.should.not.match(/太笨了/); 125 | done(); 126 | }); 127 | }); 128 | 129 | //检测hello指令 130 | it('should return hello msg', function(done){ 131 | info.text = 'hello'; 132 | sendRequest(info, function(err, json){ 133 | detect(info, err, json, /你好|fine|(how are you)/); 134 | done(); 135 | }); 136 | }); 137 | 138 | //检测yaml指令 139 | it('should return yaml msg', function(done){ 140 | info.text = 'yaml'; 141 | sendRequest(info, function(err, json){ 142 | detect(info, err, json, /这是一个yaml的object配置/); 143 | done(); 144 | }); 145 | }); 146 | }); 147 | 148 | //测试wait 149 | describe('wait', function(){ 150 | //检测sex指令 151 | it('should pass guess sex', function(done){ 152 | info.text = '你是男人还是女人'; 153 | sendRequest(info, function(err, json){ 154 | detect(info, err, json, /猜猜看/); 155 | //下次回复 156 | info.text = '哈哈'; 157 | sendRequest(info, function(err, json){ 158 | detect(info, err, json, /还猜不猜嘛/); 159 | info.text = '男的'; 160 | sendRequest(info, function(err, json){ 161 | detect(info, err, json, /是的/); 162 | done(); 163 | }); 164 | }); 165 | }); 166 | }); 167 | 168 | //检测game指令 169 | it('should pass game-no-found', function(done){ 170 | info.text = 'game 1'; 171 | sendRequest(info, function(err, json){ 172 | detect(info, err, json, /游戏/); 173 | info.text = '2'; 174 | sendRequest(info, function(err, json){ 175 | detect(info, err, json, /再猜/); 176 | info.text = '3'; 177 | sendRequest(info, function(err, json){ 178 | detect(info, err, json, /再猜/); 179 | info.text = '4'; 180 | sendRequest(info, function(err, json){ 181 | detect(info, err, json, /答案是/); 182 | done(); 183 | }); 184 | }); 185 | }); 186 | }); 187 | }); 188 | 189 | //检测game指令 190 | it('should return game-found msg', function(done){ 191 | info.text = 'game 1'; 192 | sendRequest(info, function(err, json){ 193 | detect(info, err, json, /游戏/); 194 | info.text = '2'; 195 | sendRequest(info, function(err, json){ 196 | detect(info, err, json, /再猜/); 197 | info.text = '3'; 198 | sendRequest(info, function(err, json){ 199 | detect(info, err, json, /再猜/); 200 | info.text = '1'; 201 | sendRequest(info, function(err, json){ 202 | detect(info, err, json, /聪明/); 203 | done(); 204 | }); 205 | }); 206 | }); 207 | }); 208 | }); 209 | 210 | //检测suggest_keyword指令 211 | it('should return keyword correction accepted result.', function(done){ 212 | info.text = 's nde'; 213 | sendRequest(info, function(err, json){ 214 | detect(info, err, json,/拼写错误.*nodejs/); 215 | //下次回复 216 | info.text = 'y'; 217 | sendRequest(info, function(err, json){ 218 | detect(info, err, json, /百度搜索.*nodejs/); 219 | done(); 220 | }); 221 | }); 222 | }); 223 | 224 | //检测suggest_keyword指令 225 | it('should return refused keyword correction result.', function(done){ 226 | info.text = 's nde'; 227 | sendRequest(info, function(err, json){ 228 | detect(info, err, json,/拼写错误.*nodejs/); 229 | //下次回复 230 | info.text = 'n'; 231 | sendRequest(info, function(err, json){ 232 | detect(info, err, json, /百度搜索.*nde/); 233 | done(); 234 | }); 235 | }); 236 | }); 237 | 238 | //检测search指令 239 | it('should return search msg', function(done){ 240 | info.text = 's javascript'; 241 | sendRequest(info, function(err, json){ 242 | detect(info, err, json, /百度搜索.*javascript/); 243 | done(); 244 | }); 245 | }); 246 | 247 | //检测timeout指令 248 | it('should pass not timeout', function(done){ 249 | info.text = 'timeout'; 250 | sendRequest(info, function(err, json){ 251 | detect(info, err, json, /请等待/); 252 | setTimeout(function(){ 253 | info.text = 'Hehe...'; 254 | sendRequest(info, function(err, json){ 255 | detect(info, err, json, new RegExp('输入了: ' + info.text)); 256 | done(); 257 | }); 258 | }, 2000); 259 | }); 260 | }); 261 | 262 | //检测timeout指令 263 | it('should return timeout msg', function(done){ 264 | info.text = 'timeout'; 265 | sendRequest(info, function(err, json){ 266 | detect(info, err, json, /请等待/); 267 | setTimeout(function(){ 268 | info.text = 'timeout ok'; 269 | sendRequest(info, function(err, json){ 270 | detect(info, err, json, /超时/); 271 | done(); 272 | }); 273 | }, 5100); 274 | }); 275 | }); 276 | 277 | it('should handle list', function(done) { 278 | info.text = 'ok webot'; 279 | sendRequest(info, function(err, json){ 280 | detect(info, err, json, /可用指令/); 281 | info.text = '2'; 282 | sendRequest(info, function(err, json){ 283 | detect(info, err, json, /请选择人名/); 284 | info.text = '3'; 285 | sendRequest(info, function(err, json){ 286 | detect(info, err, json, /请输入/); 287 | info.text = 'David'; 288 | sendRequest(info, function(err, json){ 289 | detect(info, err, json, /输入了 David/); 290 | done(); 291 | }); 292 | }); 293 | }); 294 | }); 295 | }); 296 | }); 297 | 298 | //测试地理位置 299 | describe('location', function(){ 300 | //检测check_location指令 301 | it('should return check_location msg', function(done){ 302 | info.type = 'location'; 303 | info.xPos = '23.08'; 304 | info.yPos = '113.24'; 305 | info.scale = '12'; 306 | info.label = '广州市 某某地点'; 307 | sendRequest(info, function(err, json){ 308 | detect(info, err, json, /广州/); 309 | done(); 310 | }); 311 | }); 312 | }); 313 | 314 | //测试图片 315 | describe('image', function(){ 316 | //检测check_location指令 317 | it('should return good hash', function(done){ 318 | info.type = 'image'; 319 | info.pic = 'http://www.baidu.com/img/baidu_sylogo1.gif'; 320 | sendRequest(info, function(err, json){ 321 | detect(info, err, json, /图片/); 322 | json.Content.should.include('你的图片'); 323 | done() 324 | }); 325 | }); 326 | }); 327 | 328 | //测试图文消息 329 | describe('news', function(){ 330 | //检测首次收听指令 331 | it('should return subscribe message.', function(done){ 332 | info.type = 'event'; 333 | info.event = 'subscribe'; 334 | info.eventKey = ''; 335 | sendRequest(info, function(err, json){ 336 | detect(info, err, json); 337 | json.should.have.property('MsgType', 'news'); 338 | json.Articles.item.should.have.length(json.ArticleCount); 339 | json.Articles.item[0].Title[0].toString().should.match(/感谢你收听/); 340 | done(); 341 | }); 342 | }); 343 | 344 | //检测image指令 345 | it('should return news msg', function(done){ 346 | info.type = 'text'; 347 | info.text = 'news'; 348 | sendRequest(info, function(err, json){ 349 | detect(info, err, json); 350 | json.should.have.property('MsgType', 'news'); 351 | json.Articles.item.should.have.length(json.ArticleCount); 352 | json.Articles.item[0].Title[0].toString().should.match(/微信机器人/); 353 | done(); 354 | }); 355 | }); 356 | }); 357 | 358 | }); 359 | --------------------------------------------------------------------------------