# 凱基人壽教育訓練 [TOC] ## Code ### nodejs call watson assistant ```javascript= // 1. 引用函示庫 const AssistantV1 = require('ibm-watson/assistant/v1'); const { IamAuthenticator } = require('ibm-watson/auth'); const APIKEY = 'xxxxxxxxxxxx'; const URL = 'xxxxxxxxxx'; const SKILLID = 8xxxxxxxxxxxxx'; // ibm-watson/assistant/v1 使用的初始化 const assistant = new AssistantV1({ version: '2021-06-14', authenticator: new IamAuthenticator({ apikey: APIKEY, }), serviceUrl: URL, }); // 對assistant發訊息 assistant.message({ workspaceId: SKILLID, input: {'text': 'Hello'} }) .then(res => { console.log(JSON.stringify(res.result, null, 2)); }) .catch(err => { console.log(err) }); ``` ### async/await api call ```javascript= async function askWatson(text) { // 對assistant發訊息 await assistant.message({ workspaceId: SKILLID, input: {'text': text} }) .then(res => { console.log(JSON.stringify(res.result, null, 2)); }) .catch(err => { console.log(err) }); } async function main() { await askWatson('我想吃披薩'); console.log('1'); await askWatson('海陸'); console.log('2'); await askWatson('大'); console.log('3'); } ``` ### squelize ```javascript= const { Sequelize, DataTypes } = require('sequelize'); const MSSQL_DATABASE = 'demo'; const MSSQL_ACCOUNT = 'SA'; const MSSQL_PASSWORD = 'nks^=Dr^38w^i38'; const MSSQL_HOST = '127.0.0.1'; const MSSQL_PORT = 1433; const sequelize = new Sequelize(MSSQL_DATABASE, MSSQL_ACCOUNT, MSSQL_PASSWORD, { host: MSSQL_HOST, port: MSSQL_PORT, dialect: 'mssql', pool: { max: 10, min: 0, idle: 10000 }, dialectOptions: { encrypt: true, }, }); const CONVERSATION_LOG = sequelize.define( 'CONVERSATION_LOG', { ID: { type: DataTypes.INTEGER, autoIncrement: true, allowNull: false, primaryKey: true, }, USER_ID: { type: DataTypes.STRING(50), allowNull: true, }, METHOD: { type: DataTypes.STRING(20), allowNull: true, }, REQUEST_DATA: { type: DataTypes.STRING(1000), allowNull: true, }, RESPONSE_DATA: { type: DataTypes.STRING('MAX'), allowNull: true, }, INTENT: { type: DataTypes.STRING(255), allowNull: true, }, CONFIDENCE: { type: DataTypes.STRING(255), allowNull: true, }, ENTITY: { type: DataTypes.STRING(255), allowNull: true, }, ERROR: { type: DataTypes.STRING('MAX'), allowNull: true, }, CREATE_TIME: { type: DataTypes.DATE, allowNull: false, defaultValue: Sequelize.fn('GETDATE'), } }, { freezeTableName: true, timestamps: true, createdAt: 'CREATE_TIME', updatedAt: false, } ); ``` ### ANQP sender example ```javascript= let amqp = require('amqplib'); class AmqpSender { constructor ({ amqpUrl, exchange }) { this.exchange = exchange; this.amqpUrl = amqpUrl || 'amqp://root:root@localhost'; this.connect(); } connect() { let _this = this; return amqp.connect(_this.amqpUrl).then(connection => { return connection.createChannel().then(channel => { var ok = channel.assertExchange(_this.exchange, 'direct'); return ok.then(function() { _this.channel = channel; console.log('[AMQP] channel', _this.exchange, 'established!'); }); }) }).catch(err => { console.error('[AMQP] channel', _this.exchange, 'establish failed!'); console.error(err); }); } /** * publish message to amqp message broker * @param {*} key amqp queue subscribed key name * @param {*} data can be string, number, array or JSON * @returns */ async publish(key, data) { if(!key || !data) { return; } if(typeof(data) == 'number') { data = { type: 'number', data: data } } else if(data != '' && typeof(data) == 'string') { data = { type: 'string', data: data } } else { try { let str = JSON.stringify(data); JSON.parse(str); if(str.startsWith('[')){ data = { type: 'array', data: data } } else { data = { type: 'json', data: data } } } catch(err) { // console.error(err) throw new Error('data must be not empty string, number or JSON object'); } } if(this.channel) { this.channel.publish(this.exchange, key, Buffer.from(JSON.stringify(data))); } } } module.exports = AmqpSender; ``` ### Amqp receiver example ```javascript= const AMQP_URL = '' const MAX_RETRY_COUNT = 5; let amqp = require('amqp-connection-manager'); let fs = require('fs'); let path = require('path'); // 根據檔案來增加channel let channelsPath = path.join(__dirname , './channels'); let files = fs.readdirSync('./channels'); let channelConfigs = files .filter(file => { return file.match(/.+\.js/g); }) .map(file => { return require(path.join(channelsPath, file)); }) .filter(({ exchange, queue, key, handler }) => { if(!exchange || !queue || !key || !handler) { return false; } return true; }) // 沒有可用的 channel 設定 if(channelConfigs.length == 0) { console.log('no configs'); process.exit(1); } let connection = amqp.connect([AMQP_URL]); connection.on('connect', () => console.log('Connected!')); connection.on('disconnect', err => console.log('Disconnected.', err)); let channelWrapper = connection.createChannel({ json: true, setup: channel => { let { exchange, queue, key, handler } = channelConfigs.shift(); return createChannel(channel, exchange, queue, key, handler); } }); channelWrapper.waitForConnect() .then(() => { return Promise.all(channelConfigs.map(({ exchange, queue, key, handler }) => { return channelWrapper.addSetup(function (channel) { return createChannel(channel, exchange, queue, key, handler); }); })); }).then(() => { console.log("start listening for messages"); }).catch((err) => { console.error('err:', err) }); /** * 建立 amqp 通道方法 * 錯誤處理參考 * https://medium.com/@lalayueh/%E5%A6%82%E4%BD%95%E5%84%AA%E9%9B%85%E5%9C%B0%E5%9C%A8rabbitmq%E5%AF%A6%E7%8F%BE%E5%A4%B1%E6%95%97%E9%87%8D%E8%A9%A6-c050efd72cdb * @exchange {string} 交換器名稱 * @queue {string} 隊列名稱 * @topic {string} 訂閱topic * @handler {function} 訊息處理方法 (不須包含channel ack) * * @returns amqp channel */ function createChannel(channel, exchange, queue, key, handler) { console.log(`[new channel created]:\n - exchange: ${exchange}\n - queue: ${queue}\n - key: ${key}`); return Promise.all([ // 建立交換器(同時建立一般交換器與失敗重作等待器) // 重送機制目前僅支援direct模式 (2022/2/8) channel.assertExchange(exchange, 'direct', { during: true }), channel.assertExchange(`${exchange}_wait`, 'direct', { during: true }) ]) // 建立隊列 .then(() => channel.assertQueue(queue, { exclusive: true, autoDelete: true } )) // 同時取出處理中訊息 .then(() => channel.prefetch(1)) // 隊列訂閱交換器 .then(() => channel.bindQueue(queue, exchange, key)) // 消化訊息 .then(() => channel.consume(queue, (message) => handlerWrapper({ message }) )) .catch(e => { console.error('[unexpected error]', e); }); // 包裝消化訊息方法 function handlerWrapper({ message }) { return handler(JSON.parse(message.content.toString())) .then(() => { channel.ack(message); }).catch(e => { console.error('[handlerWrapper]', e) errorHandler(); }) // 錯誤處理部分 function errorHandler() { let retryCount = message.properties.headers['x-retry-count'] || 0; if (retryCount > MAX_RETRY_COUNT) { // logger.error('[fail to handle] reach max retry times:', MAX_RETRY_COUNT); // logger.info(`[fail to handle] exchange: ${exchange}, queue: ${queue}, key: ${key}`); // logger.info(message.content.toString()); // console.log('fail to handle', message.content.toString()); return channel.ack(message); } // randomlize delay time let delaySeconds = Math.floor(Math.random() * (1 << retryCount)) + 1; let waitExchange = `${exchange}_wait`; let waitQueue = `${queue}_wait@${delaySeconds}`; let waitKey = `${key}_wait@${delaySeconds}`; // [now ] project log reqres // [wait] project_wait log_wait@1 reqres_wait@1 // console.log('[now ]', exchange, queue, key) // console.log('[wait]', waitExchange, waitQueue, waitKey) // create delay retry queue return channel.assertQueue(waitQueue, { arguments: { 'x-dead-letter-exchange': exchange, 'x-dead-letter-routing-key': key, 'x-message-ttl': delaySeconds * 1000 * 2, 'x-expires': delaySeconds * 1000 * 4, }, }) // bind delay retry queue .then(() => channel.bindQueue(waitQueue, waitExchange, waitKey)) // ack the original meesage .then(() => channel.ack(message)) // push the message to the wait exchange .then(() => channel.publish(waitExchange, waitKey, message.content, { headers: { 'x-retry-count': retryCount + 1, }, })) } } } ``` ### 正規取代 ```javascript= // 取代 %xxxx 的字串 // 範例: callBirthdayApi(" %userId ") const result = message.api.replace(/\s+%([^\s])+\s+/g, (match) => { console.log(match); const variable = match.trim().replace('%', ''); console.log(variable); return eval(variable); }) ``` ### 答案包call api替換字串範例 v1 ```javascript= const axios = require('axios'); const userId = 'user01'; const answerpackString = JSON.stringify({ ansId: 'ANS-TEST', messages: [{ type: 'api-call', api: 'CallBirthdayApi(" %userId ")', template: '%userId 的生日是 %response.birthday' }] }) const answerpack = JSON.parse(answerpackString); for(message of answerpack.messages) { const result = message.api.replace(/\s*%([^\s])+\s*/g, (match) => { const variable = match.trim().replace('%', ''); return eval(variable); }) console.log('result:', result); eval(result) .then(response => { // '%userId 的生日是 %response.birthday' const result = message.template.replace(/\s*%([^\s])+\s*/g, (match) => { const variable = match.trim().replace('%', ''); return eval(variable); }) console.log(result); }) .catch(e => console.log) } function CallBirthdayApi(userId) { return axios({ method: 'GET', url: `http://localhost:3000/api/${userId}` }) .then(response => response.data) } ``` ### 答案包call api替換字串範例 v2 ```javascript= const axios = require('axios'); const userId = 'user01'; const answerpackString = JSON.stringify({ ansId: 'ANS-TEST', messages: [{ type: 'api-call', api: 'CallBirthdayApi(" %userId ")', template: '%userId 的生日是 %response.birthday' }] }) const answerpack = JSON.parse(answerpackString); for(message of answerpack.messages) { const result = replaceVariables(message.api, { userId }); console.log('result:', result); eval(result) .then(response => { const result = replaceVariables(message.template, { response, userId }); console.log(result); }) .catch(e => console.log) } function CallBirthdayApi(userId) { return axios({ method: 'GET', url: `http://localhost:3000/api/${userId}` }) .then(response => response.data) } function replaceVariables(originString, variables) { return originString.replace(/\s*%([^\s])+\s*/g, (match) => { const variable = match.trim().replace('%', ''); return eval(`variables.${variable}`); }) } ``` ### Dockerfile ```dockerfile= FROM node:latest # Create app directory WORKDIR /usr/src/app # Install app dependencies # A wildcard is used to ensure both package.json AND package-lock.json are copied # where available (npm@5+) COPY package*.json ./ RUN npm install # If you are building your code for production # RUN npm ci --only=production # Bundle app source COPY . . EXPOSE 3000 CMD [ "node", "./bin/www" ] .dockerignore node_modules *.log # anything you want to ignore when building docker image ``` ### 前端 render 答案包 AssistantSay function ```javascript= function AssistantSay(msg) { if(typeof msg === 'string') { msg = { type: 'text', value: msg } } let msgHtml = ''; switch(msg.type) { case 'text': msgHtml = parseTextMsg(msg); break; case 'buttons': msgHtml = parseButtonsMsg(msg); break; default: msgHtml = JSON.stringify(msg); break; } $(".chat-room-content").append(msgHtml); // 自動捲動 $('.chat-room-content').scrollTo($(this).height()); function parseTextMsg(msg) { // msg => { "type": "text", "value": "xxx" } return ` <div class="group-rom"> <div class="first-part">${assistantName}</div> <div class="second-part">${msg.value}</div> <div class="third-part">${new Date(Date.now()).toLocaleString('zh-Hans-CN')}</div> </div>` } function parseButtonsMsg(msg) { // msg => { "type": "buttons", "desc": "xxx", "value": [button]} // button => { "label": "", "type": "", "value": ""} return ` <div class="group-rom"> <div class="first-part">${assistantName}</div> <div class="second-part"> <div>${msg.desc}</div> <div>${msg.value.map(btn => { return ` <button type="button" btn-type="${btn.type}" btn-value="${btn.value}"> ${btn.label} </button>` }).join('')}</div> </div> <div class="third-part">${new Date(Date.now()).toLocaleString('zh-Hans-CN')}</div> </div>` } } ``` ## 參考資訊 ### github project https://github.com/sleepingcat103/Nodejs-Assistant-Class/tree/main ### FireWall https://cloud.ibm.com/docs/cloud-infrastructure?topic=cloud-infrastructure-ibm-cloud-ip-ranges https://cloud.ibm.com/apidocs/assistant-v1?code=node#endpoint-cloud ### sequelize model generator https://github.com/sequelize/sequelize-auto ### nodejs line sdk doc https://line.github.io/line-bot-sdk-nodejs/#getting-started ### codepen 嵌入 chatbot iframe 範例 https://codepen.io/sleepingcat103/pen/jOvPwPz ### Docker run command ```bash= # mssql 2017-latest x) docker run --name mssql-test --env=ACCEPT_EULA=Y --env=SA_PASSWORD=nks^=Dr^38w^i38 -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-latest o) docker run --name mssql-test -e "ACCEPT_EULA=Y" -e "SA_PASSWORD=nks^=Dr^38w^i38" -p 1433:1433 -d mcr.microsoft.com/mssql/server:2017-latest # redis docker run --name redis-test -d -it -p 6379:6379 redis --requirepass "123456" # rabbitmq docker run --name rabbitmq-test --env=RABBITMQ_DEFAULT_USER=root --env=RABBITMQ_DEFAULT_PASS=root -p 15672:15672 -p 5672:5672 --restart=no --runtime=runc -d rabbitmq:3.9.12-management ``` ### GUIs sql類資料庫操作介面 https://dbeaver.io/download/ redis操作介面 https://github.com/qishibo/AnotherRedisDesktopManager