---
# System prepended metadata

title: 凱基人壽教育訓練

---

# 凱基人壽教育訓練

[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

