# 凱基人壽教育訓練
[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