├── README.pdf ├── context.js ├── .gitignore ├── .github └── workflows │ └── nodejs.yml ├── package.json ├── example ├── aixbot-server.js └── koa-server.js ├── test ├── request.js ├── context.js ├── response.js └── aixbot.js ├── request.js ├── response.js ├── aixbot.js └── README.md /README.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/MagicBowen/aixbot/HEAD/README.pdf -------------------------------------------------------------------------------- /context.js: -------------------------------------------------------------------------------- 1 | const Response = require('./response'); 2 | const delegate = require('delegates'); 3 | 4 | class Context { 5 | constructor(req) { 6 | this.req = req; 7 | this.res = new Response(); 8 | } 9 | 10 | get request() { 11 | return this.req; 12 | } 13 | 14 | get response() { 15 | return this.res; 16 | } 17 | } 18 | 19 | delegate(Context.prototype, 'res') 20 | .method('speak') 21 | .method('reply') 22 | .method('query') 23 | .method('directiveAudio') 24 | .method('directiveTts') 25 | .method('directiveRecord') 26 | .method('display') 27 | .method('playMsgs') 28 | .method('launchQuickApp') 29 | .method('launchApp') 30 | .getter('body'); 31 | 32 | module.exports = Context; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | 6 | # Runtime data 7 | pids 8 | *.pid 9 | *.seed 10 | 11 | # Directory for instrumented libs generated by jscoverage/JSCover 12 | lib-cov 13 | 14 | # Coverage directory used by tools like istanbul 15 | coverage 16 | 17 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 18 | .grunt 19 | 20 | # node-waf configuration 21 | .lock-wscript 22 | 23 | # lock scripts 24 | yarn.lock 25 | 26 | # Compiled binary addons (http://nodejs.org/api/addons.html) 27 | build/Release 28 | 29 | # Dependency directory 30 | node_modules 31 | 32 | # Optional npm cache directory 33 | .npm 34 | 35 | # Optional REPL history 36 | .node_repl_history 37 | .nyc_output 38 | coverage 39 | _docpress 40 | .env 41 | .vscode 42 | -------------------------------------------------------------------------------- /.github/workflows/nodejs.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [10.x, 12.x] 20 | 21 | steps: 22 | - uses: actions/checkout@v2 23 | - name: Use Node.js ${{ matrix.node-version }} 24 | uses: actions/setup-node@v1 25 | with: 26 | node-version: ${{ matrix.node-version }} 27 | - run: npm ci 28 | - run: npm run build --if-present 29 | - run: npm test 30 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aixbot", 3 | "version": "1.1.0", 4 | "description": "Robot SDK for XiaoAi Public Platform", 5 | "author": "magicbowen ", 6 | "license": "MIT", 7 | "homepage": "https://github.com/MagicBowen/aixbot#readme", 8 | "main": "aixbot.js", 9 | "scripts": { 10 | "test": "mocha" 11 | }, 12 | "engines": { 13 | "node": ">=8.11.1" 14 | }, 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/magicbowen/aixbot.git" 18 | }, 19 | "bugs": { 20 | "url": "https://github.com/MagicBowen/aixbot/issues" 21 | }, 22 | "dependencies": { 23 | "debug": "^3.1.0", 24 | "delegates": "^1.0.0", 25 | "koa-compose": "^4.1.0" 26 | }, 27 | "devDependencies": { 28 | "mocha": "^5.2.0", 29 | "deep-equal": "^1.0.1", 30 | "should": "^13.2.1" 31 | }, 32 | "keywords": [ 33 | "aixbot", 34 | "xiaoai", 35 | "xiaomi", 36 | "xiaoai public platform api", 37 | "bot", 38 | "botapi", 39 | "bot framework" 40 | ] 41 | } 42 | -------------------------------------------------------------------------------- /example/aixbot-server.js: -------------------------------------------------------------------------------- 1 | const AixBot = require('../aixbot'); 2 | 3 | const aixbot = new AixBot(); 4 | 5 | // define middleware 6 | aixbot.use(async (ctx, next) => { 7 | console.log(`process request for '${ctx.request.query}' ...`); 8 | var start = new Date().getTime(); 9 | await next(); 10 | var execTime = new Date().getTime() - start; 11 | console.log(`... response in duration ${execTime}ms`); 12 | }); 13 | 14 | aixbot.use(async (ctx, next) => { 15 | ctx.db = { 16 | username : 'Bowen' 17 | }; 18 | await next(); 19 | }); 20 | 21 | // define event handler 22 | aixbot.onEvent('enterSkill', (ctx) => { 23 | ctx.query('你好'); 24 | }); 25 | 26 | // define text handler 27 | aixbot.hears('你是谁', (ctx) => { 28 | ctx.speak(`我是${ctx.db.username}`).wait(); 29 | }); 30 | 31 | // define regex handler 32 | aixbot.hears(/\W+/, (ctx) => { 33 | ctx.speak(ctx.request.query); 34 | }); 35 | 36 | // close session 37 | aixbot.onEvent('quitSkill', (ctx) => { 38 | ctx.reply('再见').closeSession(); 39 | }); 40 | 41 | // define error handler 42 | aixbot.onError((err, ctx) => { 43 | logger.error(`error occurred: ${err}`); 44 | ctx.reply('内部错误,稍后再试').closeSession(); 45 | }); 46 | 47 | // run http server 48 | aixbot.run(8080); 49 | -------------------------------------------------------------------------------- /test/request.js: -------------------------------------------------------------------------------- 1 | const should = require('should'); 2 | const Request = require('../request') 3 | 4 | describe('Request', function () { 5 | describe('#getAttributes()', function () { 6 | it('should get the correct attributes of a request', function () { 7 | let message = { 8 | version: "1.0", 9 | query : "hello", 10 | session: { 11 | session_id: "xxxxx", 12 | application: { 13 | app_id: "123" 14 | }, 15 | user: { 16 | user_id: "456" 17 | } 18 | }, 19 | request: { 20 | type: 1, 21 | request_id: "ttttt", 22 | timestamp: 452453534523, 23 | locale: "zh-CN", 24 | no_response: false, 25 | event_type: "leavemsg.finished" 26 | } 27 | }; 28 | let request = new Request(message); 29 | request.appId.should.be.exactly('123'); 30 | request.query.should.be.exactly('hello'); 31 | request.isEnterSkill.should.be.exactly(false); 32 | request.isInSkill.should.be.exactly(true); 33 | request.isQuitSkill.should.be.exactly(false); 34 | request.isNoResponse.should.be.exactly(false); 35 | request.isRecordFinish.should.be.exactly(true); 36 | request.isPlayFinishing.should.be.exactly(false); 37 | }); 38 | }); 39 | }); 40 | -------------------------------------------------------------------------------- /example/koa-server.js: -------------------------------------------------------------------------------- 1 | ////////////////////////////////////////////////// 2 | const AixBot = require('../aixbot'); 3 | 4 | const aixbot = new AixBot(); 5 | 6 | // define axibot middleware 7 | aixbot.use(async (ctx, next) => { 8 | ctx.db = { 9 | username : 'Bowen' 10 | }; 11 | await next(); 12 | }); 13 | 14 | // define event handler 15 | aixbot.onEvent('enterSkill', (ctx) => { 16 | ctx.query('你好'); 17 | }); 18 | 19 | // define text handler 20 | aixbot.hears('你是谁', (ctx) => { 21 | ctx.speak(`我是${ctx.db.username}`).wait(); 22 | }); 23 | 24 | // define regex handler 25 | aixbot.hears(/\W+/, (ctx) => { 26 | ctx.speak(ctx.request.query); 27 | }); 28 | 29 | // close session 30 | aixbot.onEvent('quitSkill', (ctx) => { 31 | ctx.reply('再见').closeSession(); 32 | }); 33 | 34 | // define error handler 35 | aixbot.onError((err, ctx) => { 36 | logger.error(`error occurred: ${err}`); 37 | ctx.reply('内部错误,稍后再试').closeSession(); 38 | }); 39 | 40 | ////////////////////////////////////////////////// 41 | const Koa = require('koa'); 42 | const koaBody = require('koa-body'); 43 | const Router = require('koa-router'); 44 | 45 | const router = new Router(); 46 | const app = new Koa(); 47 | 48 | // koa middleware 49 | app.use(async (ctx, next) => { 50 | console.log(`process request for '${ctx.request.url}' ...`); 51 | var start = new Date().getTime(); 52 | await next(); 53 | var execTime = new Date().getTime() - start; 54 | console.log(`... response in duration ${execTime}ms`); 55 | }); 56 | 57 | app.use(koaBody()); 58 | router.get('/', (ctx, next) => { 59 | ctx.response.body = 'welcome'; 60 | ctx.response.status = 200; 61 | }); 62 | 63 | // register aixbot handler to koa router 64 | router.post('/aixbot', aixbot.httpHandler()); 65 | 66 | app.use(router.routes()); 67 | 68 | app.listen(8080); 69 | console.log('KOA server is runing...'); -------------------------------------------------------------------------------- /request.js: -------------------------------------------------------------------------------- 1 | class Request { 2 | constructor(req) { 3 | this._body = req; 4 | } 5 | 6 | get body() { 7 | return this._body; 8 | } 9 | 10 | get query() { 11 | return this.body.query; 12 | } 13 | 14 | get session() { 15 | return this.body.session; 16 | } 17 | 18 | get appId() { 19 | return this.session.application.app_id; 20 | } 21 | 22 | get user() { 23 | return this.session.user; 24 | } 25 | 26 | get context() { 27 | return this.body.context; 28 | } 29 | 30 | get slotInfo() { 31 | return this.body.request.slot_info; 32 | } 33 | 34 | get intentName() { 35 | if (!this.slotInfo) return null; 36 | return this.slotInfo.intent_name; 37 | } 38 | 39 | get eventType() { 40 | return this.body.request.event_type; 41 | } 42 | 43 | get eventProperty() { 44 | return this.body.request.event_property; 45 | } 46 | 47 | get requestId() { 48 | return this.body.request.request_id; 49 | } 50 | 51 | get requestType() { 52 | return this.body.request.type; 53 | } 54 | 55 | get isEnterSkill() { 56 | return (this.requestType === 0); 57 | } 58 | 59 | get isInSkill() { 60 | return (this.requestType === 1); 61 | } 62 | 63 | get isQuitSkill() { 64 | return (this.requestType === 2); 65 | } 66 | 67 | get isNoResponse() { 68 | if (!this.body.request.no_response) return false; 69 | return ((this.isInSkill) && this.body.request.no_response); 70 | } 71 | 72 | get isRecordFinish() { 73 | if (!this.eventType) return false; 74 | return ((this.isInSkill) && this.eventType === 'leavemsg.finished'); 75 | } 76 | 77 | get isRecordFail() { 78 | if (!this.eventType) return false; 79 | return ((this.isInSkill) && this.eventType === 'leavemsg.failed'); 80 | } 81 | 82 | get isPlayFinishing() { 83 | if (!this.eventType) return false; 84 | return ((this.isInSkill) && this.eventType === 'mediaplayer.playbacknearlyfinished'); 85 | } 86 | } 87 | 88 | module.exports = Request; -------------------------------------------------------------------------------- /test/context.js: -------------------------------------------------------------------------------- 1 | const should = require('should'); 2 | const equal = require('deep-equal'); 3 | const Request = require('../request') 4 | const Context = require('../context') 5 | 6 | describe('Context', function () { 7 | describe('#request()', function () { 8 | it('should get the request properties from context', function () { 9 | let request = { 10 | version: "1.0", 11 | query : "hello", 12 | session: { 13 | session_id: "xxxxx", 14 | application: { 15 | app_id: "123" 16 | }, 17 | user: { 18 | user_id: "456" 19 | } 20 | }, 21 | request: { 22 | type: 1, 23 | request_id: "ttttt", 24 | no_response: false, 25 | event_type: "leavemsg.finished" 26 | } 27 | }; 28 | let context = new Context(new Request(request)); 29 | context.request.appId.should.be.exactly('123'); 30 | context.request.query.should.be.exactly('hello'); 31 | context.request.isEnterSkill.should.be.exactly(false); 32 | context.request.isInSkill.should.be.exactly(true); 33 | context.request.isQuitSkill.should.be.exactly(false); 34 | context.request.isNoResponse.should.be.exactly(false); 35 | context.request.isRecordFinish.should.be.exactly(true); 36 | context.request.isPlayFinishing.should.be.exactly(false); 37 | }); 38 | }); 39 | describe('#response()', function () { 40 | it('should get the response properties from context', function () { 41 | let context = new Context(null); 42 | context.response.reply('hello'); 43 | expect = { version: '1.0', 44 | response: { open_mic: false, to_speak: { type: 0, text: 'hello' } }, 45 | is_session_end: false 46 | }; 47 | equal(context.body, expect).should.be.exactly(true); 48 | }); 49 | it('should delegate the response methods from context', function () { 50 | let context = new Context(null); 51 | context.query('hello'); 52 | expect = { version: '1.0', 53 | response: { open_mic: true, to_speak: { type: 0, text: 'hello' } }, 54 | is_session_end: false 55 | }; 56 | equal(context.body, expect).should.be.exactly(true); 57 | }); 58 | }); 59 | }); 60 | -------------------------------------------------------------------------------- /response.js: -------------------------------------------------------------------------------- 1 | class Response { 2 | constructor() { 3 | this._body = { 4 | version : "1.0", 5 | response : { 6 | open_mic : false 7 | }, 8 | is_session_end : false 9 | } 10 | } 11 | 12 | speak(text) { 13 | this._body.response['to_speak'] = { type : 0, text :text}; 14 | return this; 15 | } 16 | 17 | reply(text) { 18 | return this.speak(text); 19 | } 20 | 21 | query(text) { 22 | return this.speak(text).openMic(); 23 | } 24 | 25 | wait() { 26 | return this.openMic(true); 27 | } 28 | 29 | directiveAudio(url, token, offsetMs) { 30 | let item = { type: 'audio', audio_item : {stream: { url : url}}}; 31 | 32 | if (token) item.audio_item.stream['token'] = token; 33 | if (offsetMs) item.audio_item.stream['offset_in_milliseconds'] = offsetMs; 34 | 35 | return this.appendToDirectives(item); 36 | } 37 | 38 | directiveTts(text) { 39 | return this.appendToDirectives({type : 'tts', tts_item : { type : 'text', text : text}}); 40 | } 41 | 42 | directiveRecord(fileId) { 43 | return this.appendToDirectives({type : 'file_id', file_id_item : {file_id : fileId}}); 44 | } 45 | 46 | appendToDirectives(item) { 47 | if (!this._body.response.hasOwnProperty('directives')) { 48 | this._body.response['directives'] = [item]; 49 | return this; 50 | } 51 | this._body.response.directives.push(item); 52 | return this; 53 | } 54 | 55 | display(type, url, text, template) { 56 | this._body.response['to_display'] = { 57 | type : type, url : url, text : text, ui_template : template 58 | }; 59 | return this; 60 | } 61 | 62 | openMic(flag = true) { 63 | this._body.response.open_mic = flag; 64 | return this; 65 | } 66 | 67 | closeSession(flag = true) { 68 | this._body.is_session_end = flag; 69 | return this; 70 | } 71 | 72 | notUnderstand(flag = true) { 73 | this._body.response['not_understand'] = flag; 74 | return this; 75 | } 76 | 77 | setSession(obj) { 78 | this._body.session_attributes = obj; 79 | return this; 80 | } 81 | 82 | record() { 83 | this._body.response['action'] = 'leave_msg'; 84 | this.openMic(); 85 | return this; 86 | } 87 | 88 | playMsgs(fileIdList) { 89 | this._body.response['action'] = 'play_msg'; 90 | this._body.response['action_property'] = {file_id_list : fileIdList}; 91 | return this; 92 | } 93 | 94 | registerPlayFinishing() { 95 | this._body.response['register_events'] = [{event_name : 'mediaplayer.playbacknearlyfinished'}]; 96 | return this; 97 | } 98 | 99 | launchQuickApp(path) { 100 | this._body.response['action'] = 'App.LaunchQuickApp'; 101 | this._body.response['action_property'] = {quick_app_path : path}; 102 | return this; 103 | } 104 | 105 | launchApp(type, uri, permission) { 106 | // type should be : [activity|service|broadcast] 107 | this._body.response['action'] = 'App.LaunchIntent'; 108 | let info = {intent_type : type, uri : uri}; 109 | if (permission) info.permission = permission; 110 | this._body.response['action_property'] = {app_intent_info : info}; 111 | return this; 112 | } 113 | 114 | get body() { 115 | return this._body; 116 | } 117 | } 118 | 119 | module.exports = Response; 120 | -------------------------------------------------------------------------------- /test/response.js: -------------------------------------------------------------------------------- 1 | const should = require('should'); 2 | const equal = require('deep-equal'); 3 | const Response = require('../response') 4 | 5 | describe('Response', function () { 6 | describe('#getBody()', function () { 7 | it('should get the correct speak response', function () { 8 | let response = new Response().reply('hello'); 9 | let expect = { 10 | version: "1.0", 11 | is_session_end : false, 12 | response: { 13 | open_mic: false, 14 | to_speak: { 15 | type: 0, 16 | text: "hello" 17 | } 18 | } 19 | }; 20 | equal(response.body, expect).should.be.exactly(true); 21 | }); 22 | it('should get the correct audio response', function () { 23 | let response = new Response().directiveAudio('http://www.xx.cn/audio.mp3'); 24 | let expect = { 25 | version: "1.0", 26 | is_session_end : false, 27 | response: { 28 | open_mic: false, 29 | directives: [ 30 | { 31 | type: "audio", 32 | audio_item: { 33 | stream: { 34 | url: "http://www.xx.cn/audio.mp3", 35 | } 36 | } 37 | } 38 |      ], 39 | } 40 | }; 41 | equal(response.body, expect).should.be.exactly(true); 42 | }); 43 | it('should get the correct action response', function () { 44 | let response = new Response().record(); 45 | let expect = { 46 | version: "1.0", 47 | is_session_end : false, 48 | response: { 49 | open_mic: true, 50 | action : "leave_msg" 51 | } 52 | }; 53 | equal(response.body, expect).should.be.exactly(true); 54 | }); 55 | it('should get the correct open mic response', function () { 56 | let response = new Response().query('hello'); 57 | let expect = { 58 | version: "1.0", 59 | is_session_end : false, 60 | response: { 61 | open_mic: true, 62 | to_speak: { 63 | type: 0, 64 | text: "hello" 65 | } 66 | } 67 | }; 68 | equal(response.body, expect).should.be.exactly(true); 69 | }); 70 | it('should get the correct close session response', function () { 71 | let response = new Response().reply('hello').closeSession(); 72 | let expect = { 73 | version: "1.0", 74 | is_session_end : true, 75 | response: { 76 | open_mic: false, 77 | to_speak: { 78 | type: 0, 79 | text: "hello" 80 | } 81 | } 82 | }; 83 | equal(response.body, expect).should.be.exactly(true); 84 | }); 85 | it('should get the correct playing record response', function () { 86 | let response = new Response().playMsgs(['123.m']).registerPlayFinishing(); 87 | let actual = response.body; 88 | let expect = { 89 | version: "1.0", 90 | is_session_end : false, 91 | response: { 92 | open_mic: false, 93 | action : "play_msg", 94 | action_property : {file_id_list : ["123.m"]}, 95 | register_events:[ 96 | { 97 | event_name:"mediaplayer.playbacknearlyfinished" 98 | } 99 | ] 100 | } 101 | } 102 | equal(expect, actual).should.be.exactly(true); 103 | }); 104 | it('should get the correct launch quick app response', function () { 105 | let response = new Response().launchQuickApp('/'); 106 | let actual = response.body; 107 | let expect = { 108 | version: "1.0", 109 | is_session_end : false, 110 | response: { 111 | open_mic: false, 112 | action : "App.LaunchQuickApp", 113 | action_property : {quick_app_path : '/'} 114 | } 115 | } 116 | equal(expect, actual).should.be.exactly(true); 117 | }); 118 | it('should get the correct launch app response', function () { 119 | let response = new Response().launchApp('activity', 'xxx'); 120 | let actual = response.body; 121 | let expect = { 122 | version: "1.0", 123 | is_session_end : false, 124 | response: { 125 | open_mic: false, 126 | action : "App.LaunchIntent", 127 | action_property : {app_intent_info : {intent_type : 'activity', uri : 'xxx'}} 128 | } 129 | } 130 | equal(expect, actual).should.be.exactly(true); 131 | }); 132 | }); 133 | }); 134 | -------------------------------------------------------------------------------- /test/aixbot.js: -------------------------------------------------------------------------------- 1 | const should = require('should'); 2 | const AixBot = require('../') 3 | 4 | describe('AixBot', function () { 5 | describe('#getAppId()', function () { 6 | it('should get the correct app id', function () { 7 | const aixbot = new AixBot('12345'); 8 | aixbot.appId.should.be.exactly('12345'); 9 | }); 10 | }); 11 | describe('#onEvent()', function () { 12 | it('should set handler for valid event type', function () { 13 | const aixbot = new AixBot('12345'); 14 | let handler = () => {}; 15 | aixbot.onEvent('enterSkill', handler); 16 | aixbot.eventListeners.enterSkill.should.be.exactly(handler); 17 | }); 18 | it('should throw exception for invalid event type', function () { 19 | const aixbot = new AixBot('12345'); 20 | should(function() {aixbot.onEvent('enterSkills', () => {})}).throw(); 21 | }); 22 | it('should throw exception for null handler type', function () { 23 | const aixbot = new AixBot('12345'); 24 | should(function() {aixbot.onEvent('enterSkill')}).throw(); 25 | }); 26 | it('should throw exception for invalid handler type', function () { 27 | const aixbot = new AixBot('12345'); 28 | should(function() {aixbot.onEvent('enterSkill', 123)}).throw(); 29 | }); 30 | }); 31 | describe('#handleRequest()', function() { 32 | it('should invoke event handler for valid request', async function () { 33 | const aixbot = new AixBot('12345'); 34 | 35 | let handlerEntered = false; 36 | 37 | aixbot.onEvent('enterSkill', function(ctx) { 38 | handlerEntered=true; 39 | }); 40 | 41 | await aixbot.handleRequest({ session: { 42 | application: { app_id: "12345"}, user: {user_id: "456"}} 43 | , request: {type: 0}} 44 | ); 45 | 46 | handlerEntered.should.be.exactly(true); 47 | }); 48 | it('should not invoke event handler for invalid app id', async function () { 49 | const aixbot = new AixBot('12345'); 50 | 51 | let handlerEntered = false; 52 | let errorHandled = false; 53 | 54 | aixbot.onEvent('enterSkill', function(ctx) { 55 | handlerEntered=true; 56 | }); 57 | 58 | aixbot.onError((ctx) => { 59 | errorHandled = true; 60 | }); 61 | 62 | await aixbot.handleRequest({ session: { 63 | application: {app_id: "1234"}, user: {user_id: "456"}} 64 | , request: {type: 0}} 65 | ); 66 | 67 | handlerEntered.should.be.exactly(false); 68 | errorHandled.should.be.exactly(true); 69 | }); 70 | it('should not care app id in request when not set app id for aixbot', async function () { 71 | const aixbot = new AixBot(); 72 | 73 | let handlerEntered = false; 74 | let errorHandled = false; 75 | 76 | aixbot.onEvent('enterSkill', function(ctx) { 77 | handlerEntered=true; 78 | }); 79 | 80 | aixbot.onError((ctx) => { 81 | errorHandled = true; 82 | }); 83 | 84 | await aixbot.handleRequest({ session: { 85 | application: {app_id: "1234"}, user: {user_id: "456"}} 86 | , request: {type: 0}} 87 | ); 88 | 89 | handlerEntered.should.be.exactly(true); 90 | errorHandled.should.be.exactly(false); 91 | }); 92 | it('should not invoke enter skill event handler for quit skill request', async function () { 93 | const aixbot = new AixBot('12345'); 94 | 95 | let eventType = -1; 96 | 97 | aixbot.onEvent('enterSkill', function(ctx) { 98 | eventType = 0; 99 | }); 100 | 101 | aixbot.onEvent('quitSkill', function(ctx) { 102 | eventType = 2; 103 | }); 104 | 105 | await aixbot.handleRequest({ session: { 106 | application: {app_id: "12345"}, user: {user_id: "456"}} 107 | , request: {type: 2}} 108 | ); 109 | 110 | eventType.should.be.exactly(2); 111 | }); 112 | it('should invoke intent handler for intent request', async function () { 113 | const aixbot = new AixBot('12345'); 114 | 115 | let intentHandled = false; 116 | 117 | aixbot.onIntent('query-name', function(ctx) { 118 | intentHandled = true; 119 | }); 120 | 121 | await aixbot.handleRequest({ session: { 122 | application: {app_id: "12345"}, user: {user_id: "456"}} 123 | , request: {type: 1, slot_info: {intent_name : 'query-name'}}} 124 | ); 125 | 126 | intentHandled.should.be.exactly(true); 127 | }); 128 | it('should not invoke intent handler for unmatched intent name request', async function () { 129 | const aixbot = new AixBot('12345'); 130 | 131 | let intentHandled = false; 132 | 133 | aixbot.onIntent('query-name', function(ctx) { 134 | intentHandled = true; 135 | }); 136 | 137 | await aixbot.handleRequest({ session: { 138 | application: {app_id: "12345"}, user: {user_id: "456"}} 139 | , request: {type: 1, slot_info: {intent_name : 'query-sex'}}} 140 | ); 141 | 142 | intentHandled.should.be.exactly(false); 143 | }); 144 | it('should invoke text handler for matched query text request', async function () { 145 | const aixbot = new AixBot('12345'); 146 | 147 | let textHandled = false; 148 | 149 | aixbot.hears('hello', function(ctx) { 150 | textHandled = true; 151 | }); 152 | 153 | await aixbot.handleRequest({ session: { 154 | application: {app_id: "12345"}, user: {user_id: "456"}} 155 | , query: 'hello' 156 | , request: {type: 1}} 157 | ); 158 | 159 | textHandled.should.be.exactly(true); 160 | }); 161 | it('should invoke regex handler for matched query text request', async function () { 162 | const aixbot = new AixBot('12345'); 163 | 164 | let regexHandled = false; 165 | 166 | aixbot.hears(/\w+/, function(ctx) { 167 | regexHandled = true; 168 | }); 169 | 170 | await aixbot.handleRequest({ session: { 171 | application: {app_id: "12345"}, user: {user_id: "456"}} 172 | , query: 'hello' 173 | , request: {type: 1}} 174 | ); 175 | 176 | regexHandled.should.be.exactly(true); 177 | }); 178 | it('should not invoke regex handler for unmatched query text request', async function () { 179 | const aixbot = new AixBot('12345'); 180 | 181 | let regexHandled = false; 182 | 183 | aixbot.hears(/\d+/, function(ctx) { 184 | regexHandled = true; 185 | }); 186 | 187 | await aixbot.handleRequest({ session: { 188 | application: {app_id: "12345"}, user: {user_id: "456"}} 189 | , query: 'hello' 190 | , request: {type: 1}} 191 | ); 192 | 193 | regexHandled.should.be.exactly(false); 194 | }); 195 | }); 196 | }); -------------------------------------------------------------------------------- /aixbot.js: -------------------------------------------------------------------------------- 1 | const compose = require('koa-compose'); 2 | const Context = require('./context'); 3 | const Request = require('./request'); 4 | const debug = require('debug')('aixbot:Aixbot'); 5 | 6 | class AixBot { 7 | constructor(appId = null) { 8 | this.appId = appId; 9 | this.middlewares = []; 10 | this.eventListeners = { 11 | enterSkill: null, 12 | quitSkill: null, 13 | inSkill: null, 14 | noResponse: null, 15 | recordFinish: null, 16 | recordFail: null, 17 | playFinishing: null 18 | }; 19 | this.intentListeners = {}; 20 | this.textListeners = {}; 21 | this.regExpListeners = {}; 22 | this.errorListener = null; 23 | } 24 | 25 | run(port, host, tlsOptions) { 26 | this.server = tlsOptions ? 27 | require('https').createServer(tlsOptions, this.callback()) 28 | : require('http').createServer(this.callback()); 29 | this.server.listen(port, host, () => { debug(`AixBot listening on port: ${port}`) }); 30 | } 31 | 32 | use(middleware) { 33 | if (typeof middleware !== 'function') throw new TypeError('middleware must be a function!'); 34 | this.middlewares.push(middleware); 35 | return this; 36 | } 37 | 38 | callback() { 39 | this.middlewares.push(this.getFinalHandler()); 40 | const aixbotHandlers = compose(this.middlewares); 41 | 42 | const responseJson = (res, data, statusCode = 200) => { 43 | const body = JSON.stringify(data); 44 | res.writeHead(statusCode, { 45 | 'Content-Length': Buffer.byteLength(body), 46 | 'Content-Type': 'application/json' 47 | }); 48 | res.end(body); 49 | } 50 | let that = this; 51 | return (req, res) => { 52 | if (req.headers['content-type'] !== 'application/json') { 53 | responseJson(res, { cause: 'incorrect content type, wish json!' }, 404); 54 | return; 55 | } 56 | let reqBody = ""; 57 | req.on('data', function (chunk) { 58 | reqBody += chunk; 59 | }); 60 | req.on('end', async function () { 61 | try { 62 | let resBody = await that.handleRequest(JSON.parse(reqBody), aixbotHandlers); 63 | responseJson(res, resBody); 64 | } catch (err) { 65 | responseJson(res, { cause: `${err}` }, 404); 66 | } 67 | }); 68 | }; 69 | } 70 | 71 | httpHandler() { 72 | this.middlewares.push(this.getFinalHandler()); 73 | const aixbotHandlers = compose(this.middlewares); 74 | let that = this; 75 | return async (ctx, next) => { 76 | try { 77 | ctx.response.body = await that.handleRequest(ctx.request.body, aixbotHandlers); 78 | ctx.response.status = 200; 79 | } catch (err) { 80 | ctx.response.status = 404; 81 | ctx.response.body = { cause: `${err}` }; 82 | } 83 | next(); 84 | } 85 | } 86 | 87 | expressHttpHandler() { 88 | this.middlewares.push(this.getFinalHandler()); 89 | const aixbotHandlers = compose(this.middlewares); 90 | let that = this; 91 | return async (req, res, next) => { 92 | try { 93 | let replay = await that.handleRequest(req.body, aixbotHandlers); 94 | res.status(200).json(replay); 95 | } catch (err) { 96 | res.status(404) 97 | } 98 | next(); 99 | } 100 | } 101 | 102 | async handleRequest(request, handler) { 103 | if (!handler) handler = this.getFinalHandler(); 104 | let req = new Request(request); 105 | let ctx = new Context(req); 106 | await handler(ctx); 107 | return ctx.body; 108 | } 109 | 110 | getFinalHandler() { 111 | let that = this; 112 | return async function (ctx) { 113 | try { 114 | if ((that.appId !== null) && (ctx.request.appId != that.appId)) { 115 | throw (new Error(`appId(${ctx.request.appId}) does not match the aixbot(${that.appId})`)); 116 | } 117 | await that.handle(ctx); 118 | return ctx.body; 119 | } catch (err) { 120 | if (that.errorListener) { 121 | that.errorListener(err, ctx); 122 | } else { 123 | debug('Unhandled error occurred!') 124 | throw err; 125 | } 126 | } 127 | } 128 | } 129 | 130 | async handle(ctx) { 131 | if (await this.doHandle(ctx, this.eventListeners.enterSkill, ctx.request.isEnterSkill)) return; 132 | if (await this.doHandle(ctx, this.eventListeners.quitSkill, ctx.request.isQuitSkill)) return; 133 | if (await this.doHandle(ctx, this.eventListeners.noResponse, ctx.request.isNoResponse)) return; 134 | if (await this.doHandle(ctx, this.eventListeners.recordFinish, ctx.request.isRecordFinish)) return; 135 | if (await this.doHandle(ctx, this.eventListeners.recordFail, ctx.request.isRecordFail)) return; 136 | if (await this.doHandle(ctx, this.eventListeners.playFinishing, ctx.request.isPlayFinishing)) return; 137 | if (await this.doHandle(ctx, this.intentListeners[ctx.request.intentName], () => { 138 | return this.intentListeners.hasOwnProperty(ctx.request.intentName); 139 | })) return; 140 | if (await this.doHandle(ctx, this.textListeners[ctx.request.query], () => { 141 | return this.textListeners.hasOwnProperty(ctx.request.query); 142 | })) return; 143 | if (await this.doHandle(ctx, this.getRegExpHandler(ctx.request.query))) return; 144 | if (await this.doHandle(ctx, this.eventListeners.inSkill, ctx.request.isInSkill)) return; 145 | } 146 | 147 | async doHandle(ctx, handler, trigger) { 148 | if (!handler) return false; 149 | if ((trigger != undefined) && (trigger != null)) { 150 | if ((typeof trigger === 'boolean') && !trigger) return false; 151 | if ((typeof trigger === 'function') && !trigger()) return false; 152 | } 153 | await handler(ctx); 154 | return true; 155 | } 156 | 157 | onEvent(eventType, handler) { 158 | if (!this.eventListeners.hasOwnProperty(eventType)) { 159 | throw new Error(`ApiBot does not support event type of ${eventType}`); 160 | } 161 | if (this.eventListeners[eventType]) { 162 | debug(`Warning: override the existing handler of event type ${eventType}`); 163 | } 164 | this.verifyHandler(handler); 165 | this.eventListeners[eventType] = handler; 166 | } 167 | 168 | onIntent(intent, handler) { 169 | this.verifyHandler(handler); 170 | if (this.intentListeners.hasOwnProperty(intent)) { 171 | debug(`Warning: override the existing handler of intent ${intent}`); 172 | } 173 | this.intentListeners[intent] = handler; 174 | } 175 | 176 | onError(handler) { 177 | this.verifyHandler(handler); 178 | this.errorListener = handler; 179 | } 180 | 181 | hears(text, handler) { 182 | this.verifyHandler(handler); 183 | if (text instanceof RegExp) { 184 | this.onRegExp(text, handler); 185 | } else if (typeof text === 'string') { 186 | this.onText(text, handler); 187 | } 188 | else { 189 | throw new Error(`ApiBot only support hearing String or RegExp!`); 190 | } 191 | } 192 | 193 | onRegExp(regex, handler) { 194 | let regexStr = (new RegExp(regex)).source; 195 | if (this.regExpListeners.hasOwnProperty(regexStr)) { 196 | debug(`Warning: override the existing handler of regex ${regex}`); 197 | } 198 | this.regExpListeners[regexStr] = handler; 199 | } 200 | 201 | onText(text, handler) { 202 | if (this.textListeners.hasOwnProperty(text)) { 203 | debug(`Warning: override the existing handler of text ${text}`); 204 | } 205 | this.textListeners[text] = handler; 206 | } 207 | 208 | verifyHandler(handler) { 209 | if (!handler) { 210 | throw new Error(`Event handler must not be empty`); 211 | } 212 | if (typeof handler != 'function') { 213 | throw new TypeError(`Event handler must be a function`); 214 | } 215 | } 216 | 217 | getRegExpHandler(query) { 218 | for (let regexStr in this.regExpListeners) { 219 | if (RegExp(regexStr).test(query)) return this.regExpListeners[regexStr]; 220 | } 221 | return null; 222 | } 223 | } 224 | 225 | module.exports = AixBot; -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 小爱开放平台语音技能SDK 2 | 3 | 4 | 5 | - [小爱开放平台语音技能SDK](#小爱开放平台语音技能sdk) 6 | - [安装](#安装) 7 | - [用法](#用法) 8 | - [快速启动](#快速启动) 9 | - [HTTPS启动](#https启动) 10 | - [定义中间件](#定义中间件) 11 | - [和KOA结合使用](#和koa结合使用) 12 | - [对接NLU平台](#对接nlu平台) 13 | - [API](#api) 14 | - [AixBot](#aixbot) 15 | - [Constructor](#constructor) 16 | - [use](#use) 17 | - [onEvent](#onevent) 18 | - [onIntent](#onintent) 19 | - [onText](#ontext) 20 | - [onRegExp](#onregexp) 21 | - [hears](#hears) 22 | - [onError](#onerror) 23 | - [run](#run) 24 | - [httpHandler](#httphandler) 25 | - [Context](#context) 26 | - [Request](#request) 27 | - [Response](#response) 28 | - [speak](#speak) 29 | - [wait](#wait) 30 | - [query](#query) 31 | - [reply](#reply) 32 | - [directiveAudio](#directiveaudio) 33 | - [directiveTts](#directivetts) 34 | - [directiveRecord](#directiverecord) 35 | - [display](#display) 36 | - [setSession](#setsession) 37 | - [playMsgs](#playmsgs) 38 | - [registerPlayFinishing](#registerplayfinishing) 39 | - [launchQuickApp](#launchquickapp) 40 | - [launchApp](#launchapp) 41 | - [record](#record) 42 | - [closeSession](#closesession) 43 | - [notUnderstand](#notunderstand) 44 | - [body](#body) 45 | - [context delegates](#context-delegates) 46 | - [其它](#其它) 47 | - [作者](#作者) 48 | 49 | 50 | 51 | --- 52 | 53 | [小爱开放平台语音技能](https://xiaoai.mi.com/skill/create/index)的非官方nodejs SDK,帮助你轻松对接小爱开放平台,快速构建起属于自己的语音技能。 54 | 55 | 使用前需要先在小爱开放平台注册开发者身份,申请语音技能,并确定服务器URL。具体参见[小爱平台文档](https://xiaoai.mi.com/documents/Home)。 56 | 57 | ## 安装 58 | 59 | ```bash 60 | npm install aixbot 61 | ``` 62 | 63 | ## 用法 64 | 65 | AixBot和nodejs社区著名的[koa](https://www.npmjs.com/package/koa)框架用法基本一致,通过定义中间件和事件监听回调来完成任务。 66 | 67 | ### 快速启动 68 | 69 | 以下示例实现了一个简单的语音技能: 70 | - 支持进入和退出技能时的礼貌用语 71 | - 支持用户直接询问"你是谁" 72 | - 其它消息环回播放 73 | 74 | ```javascript 75 | const AixBot = require('aixbot'); 76 | 77 | const aixbot = new AixBot(); 78 | 79 | // define event handler 80 | aixbot.onEvent('enterSkill', (ctx) => { 81 | ctx.speak('你好').wait(); 82 | }); 83 | 84 | // define text handler 85 | aixbot.hears('你是谁', (ctx) => { 86 | ctx.speak(`我是Bowen`).wait(); 87 | }); 88 | 89 | // define regex handler, echo message 90 | aixbot.hears(/\W+/, (ctx) => { 91 | ctx.speak(ctx.request.query); 92 | }); 93 | 94 | // close session 95 | aixbot.onEvent('quitSkill', (ctx) => { 96 | ctx.reply('再见').closeSession(); 97 | }); 98 | 99 | // run http server 100 | aixbot.run(8080); 101 | ``` 102 | 103 | ### HTTPS启动 104 | 105 | AixBot默认使用http协议。由于小爱开放平台需要开发者提供https,建议最好在nginx上配置好SSL证书,然后代理到内部aixbot的端口。 106 | 107 | AixBot也支持直接以https启动,如下。 108 | 109 | ```javascript 110 | // config your ssl key and pem 111 | let tlsOptions = { 112 | key: fs.readFileSync('./keys/1522555444697.key'), 113 | cert: fs.readFileSync('./keys/1522555444697.pem') 114 | }; 115 | 116 | aixbot.run(8080, '0.0.0.0', tlsOptions); 117 | ``` 118 | 119 | ### 定义中间件 120 | 121 | AixBot支持像koa那样注册中间件。AixBot当前只支持中间件使用`async`和`await`的方式处理异步。 122 | 123 | ```javascript 124 | const AixBot = require('aixbot'); 125 | 126 | const aixbot = new AixBot(); 127 | 128 | // define middleware for response time 129 | aixbot.use(async (ctx, next) => { 130 | console.log(`process request for '${ctx.request.query}' ...`); 131 | var start = new Date().getTime(); 132 | await next(); 133 | var execTime = new Date().getTime() - start; 134 | console.log(`... response in duration ${execTime}ms`); 135 | }); 136 | 137 | // define middleware for DB 138 | aixbot.use(async (ctx, next) => { 139 | ctx.db = { 140 | username : 'Bowen' 141 | }; 142 | await next(); 143 | }); 144 | 145 | // define event handler 146 | aixbot.onEvent('enterSkill', (ctx) => { 147 | ctx.speak('你好').wait(); 148 | }); 149 | 150 | // define text handler 151 | aixbot.hears('你是谁', (ctx) => { 152 | ctx.speak(`我是${ctx.db.username}`).wait(); 153 | }); 154 | 155 | // define regex handler 156 | aixbot.hears(/\W+/, (ctx) => { 157 | ctx.speak(ctx.request.query); 158 | }); 159 | 160 | // close session 161 | aixbot.onEvent('quitSkill', (ctx) => { 162 | ctx.reply('再见').closeSession(); 163 | }); 164 | 165 | // define error handler 166 | aixbot.onError((err, ctx) => { 167 | logger.error(`error occurred: ${err}`); 168 | ctx.reply('内部错误,稍后再试').closeSession(); 169 | }); 170 | 171 | // run http server 172 | aixbot.run(8080); 173 | ``` 174 | 175 | 如上我们定义了两个中间件,一个打印消息的处理时间,一个为context添加访问DB的属性。 176 | 由于中间件或者消息处理过程中可能会抛出异常,所以我们为异常定义了处理方式`aixbot.onError((err, ctx) => {...})`。 177 | 178 | ### 和KOA结合使用 179 | 180 | 大多数场景下我们只用像上面那样将AixBot独立启动就可以了,但是某些场景下我们需要在同一个程序里同时发布其它的web接口,这时可以将AixBot和koa结合使用。 181 | 182 | ```javascript 183 | const AixBot = require('aixbot'); 184 | 185 | const aixbot = new AixBot(); 186 | 187 | // define axibot middleware 188 | aixbot.use(async (ctx, next) => { 189 | ctx.db = { 190 | username : 'Bowen' 191 | }; 192 | await next(); 193 | }); 194 | 195 | // define event handler 196 | aixbot.onEvent('enterSkill', (ctx) => { 197 | ctx.query('你好'); 198 | }); 199 | 200 | // define text handler 201 | aixbot.hears('你是谁', (ctx) => { 202 | ctx.speak(`我是${ctx.db.username}`).wait(); 203 | }); 204 | 205 | // define regex handler 206 | aixbot.hears(/\W+/, (ctx) => { 207 | ctx.speak(ctx.request.query); 208 | }); 209 | 210 | // close session 211 | aixbot.onEvent('quitSkill', (ctx) => { 212 | ctx.reply('再见').closeSession(); 213 | }); 214 | 215 | // define error handler 216 | aixbot.onError((err, ctx) => { 217 | logger.error(`error occurred: ${err}`); 218 | ctx.reply('内部错误,稍后再试').closeSession(); 219 | }); 220 | 221 | const Koa = require('koa'); 222 | const koaBody = require('koa-body'); 223 | const Router = require('koa-router'); 224 | 225 | const router = new Router(); 226 | const app = new Koa(); 227 | 228 | // koa middleware 229 | app.use(async (ctx, next) => { 230 | console.log(`process request for '${ctx.request.url}' ...`); 231 | var start = new Date().getTime(); 232 | await next(); 233 | var execTime = new Date().getTime() - start; 234 | console.log(`... response in duration ${execTime}ms`); 235 | }); 236 | 237 | app.use(koaBody()); 238 | router.get('/', (ctx, next) => { 239 | ctx.response.body = 'welcome'; 240 | ctx.response.status = 200; 241 | }); 242 | 243 | // register aixbot handler to koa router 244 | router.post('/aixbot', aixbot.httpHandler()); 245 | 246 | app.use(router.routes()); 247 | 248 | app.listen(8080); 249 | console.log('KOA server is runing...'); 250 | ``` 251 | 252 | 在上面的例子里,我们没有直接调用`aixbot.run()`,而是使用`router.post('/aixbot', aixbot.httpHandler())`将aixbot的处理绑定到koa router指定的`/aixbot`路由上。同时我们为AixBot和koa定义了各自的消息中间件。在运行时会先执行koa的中间件,然后再根据koa的路由规则进行消息分派。分派到`/aixbot`上的post消息先会执行AixBot的中间件,然后执行对应的已注册的AixBot消息回调。 253 | 254 | ### 对接NLU平台 255 | 256 | AixBot支持对小爱发来的消息按照事件类型或者消息内容定义回调方法,并支持对消息内容以正则表达式的方式定义规则。但是如果需要完成复杂的语音技能,就必须对接功能完备的NLU处理平台。 257 | 258 | 对于NLU处理平台,最直接的是使用[小爱开放平台的NLU配置界面](https://xiaoai.mi.com/documents/Home?type=/api/doc/render_markdown/SkillAccess/BackendDocument/NLPModel)进行配置,配置好后在收到的消息里就会携带NLU处理后得到的intent和slot信息。 259 | 260 | AixBot可以监听指定的intent,在context中可以取出对应的slot信息。 261 | 262 | ```javascript 263 | // define intent handler 264 | aixbot.onIntent('query-weather', (ctx) => { 265 | console.log(JSON.stringify(ctx.request.slotInfo)); 266 | }); 267 | ``` 268 | 269 | 如果需要完成更复杂的NLU处理,可以将AixBot对接其它更专业的NLU处理平台。遗憾的是[DialogFlow](https://dialogflow.com/)、[wit.ai](https://wit.ai/)目前都在墙外,微软的[LUIS](https://www.luis.ai/home)当前还可以用。国内类似的开放平台也有,基本和小爱当前的NLU能力差不多。作为一名程序员,说实话我不是很喜欢这种通过网页配置的方式来构建对话,我更喜欢经过良好封装的能够以代码的形式来定义和处理对话的chatbot引擎库,这样可以更加灵活地完成复杂功能。如果你自己有类似的NLU处理能力,那就会很方便了。 270 | 271 | AixBot和非小爱的NLU平台对接,无非是在AixBot的回调里面将小爱发来消息里的对话内容转发到对应的NLU平台,然后根据NLU平台的返回结果构造给小爱的回复。这里和具体的NLU平台相关,就不再详述了。 272 | 273 | ## API 274 | 275 | ### AixBot 276 | 277 | AixBot API reference 278 | 279 | ```js 280 | const AixBot = require('aixbot') 281 | ``` 282 | 283 | #### Constructor 284 | 285 | Initialize new AixBot bot. 286 | 287 | `const aixbot = new AixBot([appId])` 288 | 289 | | Param | Type | Description | 290 | | --- | --- | --- | 291 | | [appId] | `String` | app_id of skill | 292 | 293 | 在小爱开放平台上申请的每一个技能都有一个`app_id`。 294 | 如果需要对收到的每条消息的`app_id`进行严格校验,则在构造AixBot的实例时提供该值。 295 | 296 | #### use 297 | 298 | Registers a middleware. 299 | 300 | `aixbot.use(...middleware)` 301 | 302 | | Param | Type | Description | 303 | | --- | --- | --- | 304 | | middleware | `function` | Middleware function | 305 | 306 | ```js 307 | aixbot.use(async (ctx, next) => { 308 | ctx.db = { 309 | username : 'Bowen' 310 | }; 311 | await next(); 312 | }); 313 | ``` 314 | 315 | #### onEvent 316 | 317 | Registers event handler. 318 | 319 | `aixbot.onEvent(eventType, handler)` 320 | 321 | | Param | Type | Description | 322 | | --- | --- | --- | 323 | | eventType | `String` | event type | 324 | | handler | `function` | handler function | 325 | 326 | 现在支持以下事件类型: 327 | 328 | | Event Type | Description | 329 | | --- | --- | 330 | | enterSkill | 进入技能 | 331 | | quitSkill | 离开技能 | 332 | | inSkill | 技能进行中 | 333 | | noResponse | 音箱无响应 | 334 | | recordFinish | 录音完成 | 335 | | recordFail | 录音失败 | 336 | | playFinishing | 录音播放即将完成 | 337 | 338 | 339 | ```js 340 | aixbot.onEvent('enterSkill', (ctx) => { 341 | ctx.speak('你好').wait(); 342 | }); 343 | 344 | aixbot.onEvent('inSkill', (ctx) => { 345 | console.log(`received : ${ctx.request.query}`); 346 | }); 347 | ``` 348 | 349 | **注意:`inSkill`事件的处理优先级是最低的,比随后介绍的`onIntent`、`onText`和`onRegExp`都要低。可以用它来做一些默认处理。** 350 | 351 | #### onIntent 352 | 353 | Registers intent handler. 354 | 355 | `aixbot.onIntent(intent, handler)` 356 | 357 | | Param | Type | Description | 358 | | --- | --- | --- | 359 | | intent | `String` | intent name | 360 | | handler | `function` | handler function | 361 | 362 | ```js 363 | aixbot.onIntent('query-weather', (ctx) => { 364 | console.log(JSON.stringify(ctx.request.slotInfo)); 365 | }); 366 | ``` 367 | 368 | #### onText 369 | 370 | Registers text handler. 371 | 372 | `aixbot.onText(text, handler)` 373 | 374 | | Param | Type | Description | 375 | | --- | --- | --- | 376 | | text | `String` | query content | 377 | | handler | `function` | handler function | 378 | 379 | ```js 380 | aixbot.onText('hi', (ctx) => { 381 | ctx.speak('hello'); 382 | }); 383 | ``` 384 | 385 | #### onRegExp 386 | 387 | Registers regex handler. 388 | 389 | `aixbot.onRegExp(regex, handler)` 390 | 391 | | Param | Type | Description | 392 | | --- | --- | --- | 393 | | regex | `RegExp` | regular expression | 394 | | handler | `function` | handler function | 395 | 396 | ```js 397 | aixbot.onRegExp(/\d+/, (ctx) => { 398 | ctx.speak(`收到数字:${ctx.request.query}`); 399 | }); 400 | ``` 401 | 402 | **注意:所有`regex handler`的优先级低于`text handler`**。 403 | 404 | #### hears 405 | 406 | Wrapper of onText and onRegExp. 407 | 408 | `aixbot.hear(text, handler)` 409 | 410 | | Param | Type | Description | 411 | | --- | --- | --- | 412 | | text | `String` or `RegExp` | query or regular expression | 413 | | handler | `function` | handler function | 414 | 415 | ```js 416 | aixbot.hears('你是谁', (ctx) => { 417 | ctx.speak(`我是${ctx.db.username}`).wait(); 418 | }); 419 | 420 | aixbot.hears(/\W+/, (ctx) => { 421 | ctx.speak(ctx.request.query); 422 | }); 423 | ``` 424 | 425 | #### onError 426 | 427 | Registers error handler. 428 | 429 | `aixbot.onError(handler)` 430 | 431 | | Param | Type | Description | 432 | | --- | --- | --- | 433 | | handler | `function` | handler function | 434 | 435 | ```js 436 | aixbot.onError((err, ctx) => { 437 | logger.error(`error occurred: ${err}`); 438 | ctx.reply('内部错误,稍后再试').closeSession(); 439 | }); 440 | ``` 441 | 442 | #### run 443 | 444 | Run http/https server. 445 | 446 | `aixbot.run(port, host, tlsOptions)` 447 | 448 | | Param | Type | Description | 449 | | --- | --- | --- | 450 | | port | `number` | port number | 451 | | host | `String` | host address | 452 | | tlsOptions | `object` | https options | 453 | 454 | 如果不提供`tlsOptions`,则启动**http server**,否则启动**https server** 455 | 456 | ```js 457 | let tlsOptions = { 458 | key: fs.readFileSync('./keys/1522555444697.key'), 459 | cert: fs.readFileSync('./keys/1522555444697.pem') 460 | }; 461 | 462 | aixbot.run(8080, '0.0.0.0', tlsOptions); 463 | ``` 464 | 465 | #### httpHandler 466 | 467 | get middleware for KOA. 468 | 469 | `aixbot.httpHandler()` 470 | 471 | ```js 472 | const router = new Router(); 473 | const app = new Koa(); 474 | app.use(koaBody()); 475 | router.post('/aixbot', aixbot.httpHandler()); 476 | app.use(router.routes()); 477 | app.listen(8080); 478 | ``` 479 | 480 | ### Context 481 | 482 | Context API reference. 483 | 484 | Context是每一个Aixbot中间件和消息回调的参数,通过它可以得到request和response,访问request和response的属性和方法。 485 | 486 | ```js 487 | aixbot.onEvent('enterSkill', (ctx) => { 488 | console.log(JSON.stringify(ctx.request.body)); // 打印接收消息体的所有内容 489 | console.log(ctx.request.query); // 打印接收到的消息文本;具体Request封装过的属性和接口参见Request的API介绍 490 | ctx.response.reply('欢迎!'); // 构造回复消息;具体Response封装过的属性和接口参见Response的API介绍 491 | console.log(JSON.stringify(ctx.response.body)); // 打印发送消息体的所有内容 492 | }); 493 | ``` 494 | 495 | 另外,为了方便使用,Context代理了Response的一些主要接口,这些接口可以通过Context直接使用。例如: 496 | 497 | ```js 498 | aixbot.onEvent('enterSkill', (ctx) => { 499 | ctx.reply('欢迎!'); // 效果和 ctx.response.reply('欢迎!') 相同 500 | console.log(JSON.stringify(ctx.body)); // ctx.body 和 ctx.response.body 相同 501 | }); 502 | ``` 503 | 504 | 由于Response支持连贯接口调用,所以Context上代理的Response接口也同样支持。 505 | 506 | ```js 507 | aixbot.hears('你是谁', (ctx) => { 508 | ctx.speak('我是Bowen,你是谁?').wait(); // wait()指示开启麦克风,用于直接的多轮对话 509 | }); 510 | ``` 511 | 512 | 最后,Context的存在方便中间件为其添加其它的属性和方法: 513 | 514 | ```js 515 | aixbot.use(async (ctx, next) => { 516 | ctx.db = { 517 | username : 'Bowen' 518 | }; 519 | await next(); 520 | }); 521 | 522 | aixbot.hears('你是谁', (ctx) => { 523 | ctx.speak(`我是${ctx.db.username}`).wait(); 524 | }); 525 | ``` 526 | 527 | ### Request 528 | 529 | Request API reference. 530 | 531 | Request封装了从小爱收到的消息体。通过Context可以访问到Request实例:`ctx.request`。 532 | 533 | Request对接收消息体进行了封装,对常用字段提供了直接的读取属性。 534 | 535 | | attribute | type | Description | 536 | | --- | --- | --- | 537 | | body | `object` | 消息体原始内容 | 538 | | query | `String` | message.request.query | 539 | | session | `object` | message.session | 540 | | appId | `String` | message.session.application.app_id | 541 | | user | `object` | message.session.user | 542 | | context | `object` | message.context | 543 | | slotInfo | `object` | message.request.slot_info | 544 | | intentName | `String` | message.request.slot_info.intent_name | 545 | | eventType | `String` | message.request.event_type | 546 | | eventProperty | `object` | message.request.event_property | 547 | | requestId | `String` | message.request.request_id | 548 | | requestType | `number` | message.request.type | 549 | | isEnterSkill | `boolean` | message.request.type == 0| 550 | | isInSkill | `boolean` | message.request.type == 1| 551 | | isQuitSkill | `boolean` | message.request.type == 2| 552 | | isNoResponse | `boolean` | message.request.no_response| 553 | | isRecordFinish | `boolean` | message.request.event_type == 'leavemsg.finished'| 554 | | isRecordFail | `boolean` | message.request.event_type == 'leavemsg.failed'| 555 | | isPlayFinishing | `boolean` | message.request.event_type == 'mediaplayer.playbacknearlyfinished'| 556 | 557 | 558 | ```js 559 | aixbot.hears(/\W+/, (ctx) => { 560 | console.log(ctx.request.appId); 561 | console.log(ctx.request.query); 562 | if (ctx.request.isNoResponse) { 563 | console.log('received no response'); 564 | } 565 | // ... 566 | }) 567 | ``` 568 | 569 | ### Response 570 | 571 | Response API reference. 572 | 573 | Response封装了发送给小爱的消息,通过`ctx.response`可以获取到Response的实例。 574 | 575 | Response对发送消息体进行了封装,提供了更具有语义性的操作接口。 576 | 577 | #### speak 578 | 579 | Reply a text. 580 | 581 | `ctx.response.speak(text)` 582 | 583 | | Param | Type | Description | 584 | | --- | --- | --- | 585 | | text | `String` | 返回的消息文本 | 586 | 587 | speak默认是关闭麦克风的,如果想要打开麦克风则需要和后面的`wait`接口一起使用。 588 | 589 | #### wait 590 | 591 | Open mic. 592 | 593 | `ctx.response.speak(text).wait()` 594 | 595 | `wait`接口不能单独使用,必须跟在其它有内容回复的接口后面。 596 | 597 | #### query 598 | 599 | `response.speak(text).wait()`的语法糖,可以直接写 `response.query(text)` 600 | 601 | #### reply 602 | 603 | 与`response.speak(text)`等价,可以直接写 `response.reply(text)` 604 | 605 | #### directiveAudio 606 | 607 | Reply a audio directive. 608 | 609 | `directiveAudio(url, token, offsetMs)` 610 | 611 | | Param | Type | Description | 612 | | --- | --- | --- | 613 | | url | `String` | 资源url | 614 | | token | `String` | 获取资源的token | 615 | | offsetMs | `Long` | 偏移时间 | 616 | 617 | #### directiveTts 618 | 619 | Reply a tts directive. 620 | 621 | `directiveTts(text)` 622 | 623 | | Param | Type | Description | 624 | | --- | --- | --- | 625 | | text | `String` | 语音合成文本 | 626 | 627 | #### directiveRecord 628 | 629 | Reply a record directive. 630 | 631 | `directiveRecord(fileId)` 632 | 633 | | Param | Type | Description | 634 | | --- | --- | --- | 635 | | fileId | `String` | 录音文件ID | 636 | 637 | #### display 638 | 639 | Reply a display. 640 | 641 | `display(type, url, text, template)` 642 | 643 | | Param | Type | Description | 644 | | --- | --- | --- | 645 | | type | `Int` | 1:html,2:native ui,3:widgets | 646 | | url | `String` | html address | 647 | | text | `String` | display text | 648 | | template | `UlTemplate` | 参见 [UlTemplate](https://xiaoai.mi.com/documents/Home?type=/api/doc/render_markdown/SkillAccess/SkillDocument/CustomSkills) | 649 | 650 | #### setSession 651 | 652 | Add paramter in session. 653 | 654 | 为当前对话上下文的session中添加变量,小爱会在随后的消息中携带该session参数。 655 | 656 | `setSession(obj)` 657 | 658 | | Param | Type | Description | 659 | | --- | --- | --- | 660 | | obj | `Any` | parameter store in session | 661 | 662 | #### playMsgs 663 | 664 | Reply to play record msgs. 665 | 666 | 指示播放列表中所有的录音文件。 667 | 668 | `playMsgs(fileIdList)` 669 | 670 | | Param | Type | Description | 671 | | --- | --- | --- | 672 | | fileIdList | `Array` | file_id array | 673 | 674 | `ctx.response.speak('请收听录音').playMsgs(['4747c167f000400f15f4d42x'])` 675 | 676 | #### registerPlayFinishing 677 | 678 | 指示播放录音即将完成后发送回调消息,具体参见[小爱相关文档](https://xiaoai.mi.com/documents/Home?type=/api/doc/render_markdown/SkillAccess/SkillDocument/EventsAndTheme) 679 | 680 | `ctx.response.speak('请收听录音').playMsgs(['4747c167f000400f15f4d42x']).registerPlayFinishing();` 681 | 682 | #### launchQuickApp 683 | 684 | 启动特定路径的快应用。 685 | 快应用语音技能的注册及配置见[小爱文档](https://xiaoai.mi.com/documents/Home?type=/api/doc/render_markdown/SkillAccess/SkillDocument/VoiceAssistantSkill)。 686 | 687 | `launchQuickApp(path)` 688 | 689 | | Param | Type | Description | 690 | | --- | --- | --- | 691 | | path | `String` | path of quick app | 692 | 693 | `ctx.response.launchQuickApp('/')` 694 | 695 | #### launchApp 696 | 697 | 启动APP。 698 | 启动APP的语音技能的注册及配置见[小爱文档](https://xiaoai.mi.com/documents/Home?type=/api/doc/render_markdown/SkillAccess/SkillDocument/VoiceAssistantSkill)。 699 | 700 | `launchApp(type, uri, permission)` 701 | 702 | | Param | Type | Description | 703 | | --- | --- | --- | 704 | | type | `String` | 启动APP的intent的类型;支持的类型 1 activity; 2 service; 3 broadcast | 705 | | uri | `String` | 启动APP的路径 | 706 | | permission | `String` | 权限信息;非必须参数 | 707 | 708 | `ctx.response.launchApp('activity', 'xxxxxxx')` 709 | 710 | #### record 711 | 712 | 指示开始录音,跟在回复后面使用。 713 | 714 | `ctx.response.speak('start record').record()` 715 | 716 | #### closeSession 717 | 718 | 指示结束回话,跟在回复后面使用。 719 | 720 | `ctx.response.speak('bye').closeSession()` 721 | 722 | #### notUnderstand 723 | 724 | 指示未理解的对话,跟在回复后面使用。 725 | 726 | `ctx.response.speak('what').notUnderstand()` 727 | 728 | #### body 729 | 730 | 获取消息体内容 731 | 732 | ```js 733 | ctx.response.speak('hello'); 734 | console.log(JSON.stringify(ctx.response.body)); 735 | ``` 736 | 737 | #### context delegates 738 | 739 | 为了方便使用,Context对Response的下列属性和方法进行了代理: 740 | 741 | - `speak` 742 | - `reply` 743 | - `query` 744 | - `directiveAudio` 745 | - `directiveTts` 746 | - `directiveRecord` 747 | - `display` 748 | - `playMsgs` 749 | - `launchQuickApp` 750 | - `launchApp` 751 | - `body` 752 | 753 | ```js 754 | ctx.speak('hi').wait(); // same as : ctx.response.speak('hi').wait() 755 | ``` 756 | 757 | ## 其它 758 | 759 | 源码在[github](https://github.com/MagicBowen/aixbot),有问题请提issue。 760 | 761 | 使用 `npm test`可以对源码进行测试。 762 | 763 | 如果运行时想打开AixBot的debug打印,可以在启动时加上 `DEBUG=aixbot:*`,例如`DEBUG=aixbot:* node index.js`。 764 | 765 | 本人使用的是 `node 8.11.1`版本,其它更低版本的不支持`class`,`const`,`let`,`async`,`await`等特性的node版本请绕路。 766 | 767 | ## 作者 768 | 769 | - Bowen 770 | - Email:[e.wangbo@gmail.com](e.wangbo@gmail.com) 771 | --------------------------------------------------------------------------------