UNPKG

msbot

Version:

MSBot command line tool for manipulating Microsoft Bot Framework .bot files

871 lines 59.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); /** * Copyright(c) Microsoft Corporation.All rights reserved. * Licensed under the MIT License. */ const botframework_config_1 = require("botframework-config"); const chalk = require("chalk"); const child_process = require("child_process"); const program = require("commander"); const fs = require("fs"); const path = require("path"); const process = require("process"); const txtfile = require("read-text-file"); const readline = require("readline-sync"); const url = require("url"); const util = require("util"); const uuid = require("uuid"); const processUtils_1 = require("./processUtils"); const stdioAsync_1 = require("./stdioAsync"); const utils_1 = require("./utils"); const msbot_clone_service_version_1 = require("./msbot-clone-service-version"); const Table = require('cli-table3'); const opn = require('opn'); const commandExistsSync = require('command-exists').sync; const exec = util.promisify(child_process.exec); const AZCLIMINVERSION = '(2.0.53)'; // This corresponds to the AZ CLI version that shipped after the Bot Builder 4.2 release (December 2018). program.Command.prototype.unknownOption = (flag) => { console.error(chalk.default.redBright(`Unknown arguments: ${flag}`)); program.help(); }; program .name('msbot clone services') .option('-n, --name <name>', 'Name of new bot') .option('-f, --folder <folder>', 'Path to folder containing exported resources') .option('-l, --location <location>', 'Location to create the bot service in (westus, ...)') .option('--luisAuthoringKey <luisAuthoringKey>', 'Authoring key from the appropriate luisAuthoringRegion for LUIS resources') .option('--luisAuthoringRegion <luisAuthoringRegion>', '(OPTIONAL) [westus|westeurope|australiaeast] authoring region to put LUIS models into (default is based on location)') .option('--luisPublishRegion <luisRegion>', '(OPTIONAL) Region to publish LUIS models to (default fallback is based location || luisAuthoringRegion)') .option('--subscriptionId <subscriptionId>', '(OPTIONAL) Azure subscriptionId to clone bot to, if not passed then current az account will be used') .option('--insightsRegion <insightsRegion>', '(OPTIONAL) Region to create appInsights account in (default is based on location)') .option('--groupName <groupName>', '(OPTIONAL) groupName for cloned bot, if not passed then new bot name will be used for the new group') .option('--sdkLanguage <sdkLanguage>', '(OPTIONAL) language for bot [Csharp|Node] (Default:CSharp)') .option('--sdkVersion <sdkVersion>', '(OPTIONAL) SDK version for bot [v3|v4] (Default:v4)') .option('--prefix', 'Append [msbot] prefix to all messages') .option('--appId <appId>', '(OPTIONAL) Application ID for an existing application, if not passed then a new Application will be created') .option('--appSecret <appSecret>', '(OPTIONAL) Application Secret for an existing application, if not passed then a new Application will be created') .option('--searchSku <searchSku>', '(OPTIONAL) Set the Sku for provisioned azure search (free|basic|standard|standard2|standard3|...)') .option('--proj-file <projfile>', '(OPTIONAL) The local project file to created bot service') .option('--code-dir <path>', '(OPTIONAL) Passing in --code-dir will auto publish the folder path to created bot service') .option('-e, --encryption', 'Enables bot file encryption') .option('-q, --quiet', 'Minimize output') .option('--verbose', 'Show commands') .option('--force', 'Do not prompt for confirmation') .description('allows you to clone all of the services a bot uses into a new Azure resource group') .action((cmd, actions) => undefined); const cmd = program.parse(process.argv); const args = {}; Object.assign(args, cmd); if (typeof (args.name) != 'string') { console.error(chalk.default.redBright('missing --name argument')); showErrorHelp(); } if (args.name.length < 4 || args.name.length > 42) { console.error(chalk.default.redBright('name has to be between 4 and 42 characters long')); showErrorHelp(); } // verify that the user has AZ CLI if (!commandExistsSync('az')) { console.error(chalk.default.redBright('AZ CLI is not installed or cannot be found. \n\nSee https://aka.ms/msbot-clone-services for pre-requisites.')); showErrorHelp(); } if (fs.existsSync(args.name + '.bot')) { console.error(chalk.default.redBright(`${args.name}.bot already exists. Please choose a different name or delete ${args.name}.bot and try again.`)); showErrorHelp(); } if (!args.folder) { console.error(chalk.default.redBright(`missing --folder argument`)); showErrorHelp(); } if (!fs.existsSync(path.join(args.folder, 'bot.recipe'))) { console.error(chalk.default.redBright(`No bot.recipe file found under ${args.folder}. Please provide the folder that contains the recipe file`)); showErrorHelp(); } if (!args.location) { console.error(chalk.default.redBright(`missing --location argument`)); showErrorHelp(); } if (!Object.values(utils_1.RegionCodes).find((r) => args.location == r)) { console.error(chalk.default.redBright(`${args.location} is not a valid region code. Supported Regions are:\n${Object.values(utils_1.RegionCodes).join(',\n')}`)); showErrorHelp(); } let config = new botframework_config_1.BotConfiguration(); config.name = args.name; config.saveAs(config.name + '.bot') .then(processConfiguration) .catch((reason) => { if (reason.message) { console.error(chalk.default.redBright(reason.message)); } else { console.error(chalk.default.redBright(reason)); } showErrorHelp(); }); async function processConfiguration() { if (!args.sdkVersion) { args.sdkVersion = "v4"; } if (!args.sdkLanguage) { if (fs.existsSync("package.json")) { args.sdkLanguage = "Node"; } else { args.sdkLanguage = "CSharp"; } } if (!args.groupName) { args.groupName = args.name; } if (args['proj-file']) { args.projFile = args['proj-file']; console.log(args.projFile); } else if (args['code-dir']) { args.codeDir = args['code-dir']; console.log(args.codeDir); } if (!args.projFile && !args.codeDir) { let files = fs.readdirSync('.'); for (let file of files) { if (path.extname(file) == '.csproj') { args.projFile = file; break; } } } if (!args.searchSku) { args.searchSku = "basic"; } // verify az command exists and is correct version await checkAzBotServiceVersion(); if (args.projFile) { await checkDotNetRequirement(); } let recipeJson = await txtfile.read(path.join(args.folder, `bot.recipe`)); let recipe = JSON.parse(recipeJson); // get subscription account data let command = `az account show `; if (args.subscriptionId) { command += `--subscription ${args.subscriptionId}`; } let azAccount = await runCommand(command, `Fetching subscription account`); args.subscriptionName = azAccount.name; args.subscriptionId = azAccount.id; args.tenantId = azAccount.tenantId; try { // pass 0 - tell the user what are going to create if (!args.quiet) { let bot = 0; let appInsights = 0; let storage = 0; let sitePlan = 0; console.log("The following services will be created by this operation:"); const table = new Table({ // don't use lines for table chars: { 'top': '', 'top-mid': '', 'top-left': '', 'top-right': '', 'bottom': '', 'bottom-mid': '', 'bottom-left': '', 'bottom-right': '', 'left': '', 'left-mid': '', 'right': '', 'right-mid': '', 'mid': '', 'mid-mid': '', 'middle': '' }, head: [chalk.default.bold('Service'), chalk.default.bold('Location'), chalk.default.bold('SKU'), chalk.default.bold("Resource Group")], colWidths: [40, 20, 20, 20], style: { 'padding-left': 1, 'padding-right': 1 }, wordWrap: true }); let hasSitePlan = false; let rows = []; for (let resource of recipe.resources) { switch (resource.type) { case botframework_config_1.ServiceTypes.AppInsights: let appInsightsRegion = utils_1.regionToAppInsightRegionNameMap[args.location]; if (appInsightsRegion) { rows.push([`Azure AppInsights Service`, `${appInsightsRegion}`, `F0`, args.groupName]); } break; case botframework_config_1.ServiceTypes.BlobStorage: rows.push(['Azure Blob Storage Service', `${args.location}`, 'Standard_LRS', args.groupName]); break; case botframework_config_1.ServiceTypes.Bot: rows.push([`Azure Bot Service Registration`, `Global`, ``, args.groupName]); if (!hasSitePlan) { rows.push([`Azure App Site Plan`, `${args.location}`, `S1`, args.groupName]); hasSitePlan = true; } rows.push([`Azure WebApp Service (Bot)`, `${args.location}`, ``, args.groupName]); break; case botframework_config_1.ServiceTypes.CosmosDB: rows.push([`Azure CosmosDB Service`, `${args.location}`, `1 write region`, args.groupName]); break; case botframework_config_1.ServiceTypes.Endpoint: break; case botframework_config_1.ServiceTypes.File: break; case botframework_config_1.ServiceTypes.Generic: break; case botframework_config_1.ServiceTypes.Dispatch: case botframework_config_1.ServiceTypes.Luis: if (!args.luisAuthoringKey) { throw new Error('missing --luisAuthoringKey argument'); } if (!args.luisPublishRegion) { args.luisPublishRegion = utils_1.luisPublishRegions.find((value) => value == args.location); if (!args.luisPublishRegion) { args.luisPublishRegion = utils_1.regionToLuisPublishRegionMap[args.location]; } } if (resource.type == botframework_config_1.ServiceTypes.Dispatch) rows.push([`Azure LUIS Cognitive Service (Dispatch)`, `${args.luisPublishRegion}`, `S0`, args.groupName]); else rows.push([`Azure LUIS Cognitive Service`, `${args.luisPublishRegion}`, `S0`, args.groupName]); break; case botframework_config_1.ServiceTypes.QnA: rows.push([`Azure QnA Maker Service`, `westus`, `S0`, args.groupName]); if (!hasSitePlan) { rows.push([`Azure App Site Plan`, `${args.location}`, `S1`, args.groupName]); hasSitePlan = true; } rows.push([`Azure WebApp Service (QnA)`, `${args.location}`, ``, args.groupName]); rows.push([`Azure Search Service`, `${utils_1.regionToSearchRegionMap[args.location]}`, args.searchSku, args.groupName]); break; default: break; } } rows.sort((a, b) => a[0].localeCompare(b[0])); for (let row of rows) table.push(row); await stdioAsync_1.logAsync(table.toString()); console.log(`Resources will be created in subscription: ${chalk.default.bold(azAccount.name)} (${azAccount.id})`); if (!args.force) { const answer = readline.question(`Would you like to perform this operation? [y/n]`); if (answer == "no" || answer == "n") { // remove orphaned bot file if it exists if (fs.existsSync(args.name + '.bot')) fs.unlinkSync(args.name + '.bot'); console.log("Canceling the operation"); process.exit(1); } } } // pass 1 - create bot if we are going to need one. This will create // * group // * sitePlan // * site // * appInsights // * storage // create group let azGroup; let azBot; let azSitePlan; let storageInfo; let azQnaSubscription; let azLuisSubscription; let azAppInsights; let azBotExtended; let azBotEndpoint; // create group if not created yet azGroup = await createGroup(); for (let resource of recipe.resources) { if (resource.type == botframework_config_1.ServiceTypes.Bot) { if (!azBot) { azBot = await createBot(); azBotEndpoint = azBot; azBotExtended = await runCommand(`az bot show -g ${args.groupName} -n ${args.name} --subscription ${args.subscriptionId}`, `Fetching bot extended information [${args.name}]`); // fetch co-created resources so we can get blob and appinsights data let azGroupResources = await runCommand(`az resource list -g ${azGroup.name} --subscription ${args.subscriptionId}`, `Fetching co-created resources [${args.name}]`); let botWebSite; for (let resource of azGroupResources) { switch (resource.type) { case "microsoft.insights/components": azAppInsights = resource; break; case "Microsoft.Storage/storageAccounts": storageInfo = resource; break; case "Microsoft.Web/serverfarms": azSitePlan = resource; break; case "Microsoft.Web/sites": // the website for the bot does have the bot name in it (qna host does) if (resource.name.indexOf('-qnahost') < 0) botWebSite = resource; break; } } // get appSettings from botAppSite (specifically to get secret) if (botWebSite) { if (args.encryption) { let botAppSettings = await runCommand(`az webapp config appsettings list -g ${args.groupName} -n ${botWebSite.name} --subscription ${args.subscriptionId}`, `Fetching bot website appsettings [${args.name}]`); for (let setting of botAppSettings) { if (setting.name == "botFileSecret") { args.secret = setting.value; break; } } if (!args.secret) { args.secret = botframework_config_1.BotConfiguration.generateKey(); // set the appsetting await runCommand(`az webapp config appsettings set -g ${args.groupName} -n ${botWebSite.name} --settings botFileSecret="${args.secret}" --subscription ${args.subscriptionId}`, `Setting bot website appsettings secret [${args.name}]`); } } } else { throw new Error('botsite was not found'); } config.services.push(new botframework_config_1.BotService({ type: botframework_config_1.ServiceTypes.Bot, id: resource.id, name: azBot.name, tenantId: args.tenantId, subscriptionId: args.subscriptionId, resourceGroup: args.groupName, serviceName: azBot.name, appId: azBot.appId })); await config.save(); } } } // pass 2 - create LUIS and QNA cognitive service subscriptions (and hosting services) for (let resource of recipe.resources) { switch (resource.type) { case botframework_config_1.ServiceTypes.Luis: case botframework_config_1.ServiceTypes.Dispatch: if (!azLuisSubscription) { if (!args.luisAuthoringRegion) { if (utils_1.regionToLuisAuthoringRegionMap.hasOwnProperty(args.location)) args.luisAuthoringRegion = utils_1.regionToLuisAuthoringRegionMap[args.location]; else throw new Error(`${args.location} does not have a valid luisAuthoringRegion. Pass --luisAuthoringRegion to tell us which region you are in`); } if (!args.luisPublishRegion) { args.luisPublishRegion = utils_1.luisPublishRegions.find((value) => value == args.location); if (!args.luisPublishRegion) { args.luisPublishRegion = utils_1.regionToLuisAuthoringRegionMap[args.location]; } } // create luis subscription let luisCogsName = `${args.name}-LUIS`; azLuisSubscription = await runCommand(`az cognitiveservices account create -g ${azGroup.name} --kind LUIS -n "${luisCogsName}" --location ${args.luisPublishRegion} --sku S0 --yes --subscription ${args.subscriptionId}`, `Creating LUIS Cognitive Service [${luisCogsName}]`); // get keys let luisKeys = await runCommand(`az cognitiveservices account keys list -g ${azGroup.name} -n "${luisCogsName}" --subscription ${args.subscriptionId}`, `Fetching LUIS Keys [${luisCogsName}]`); args.luisSubscriptionKey = luisKeys.key1; } break; case botframework_config_1.ServiceTypes.QnA: if (!azQnaSubscription) { if (!azSitePlan) { azSitePlan = await runCommand(`az appservice plan create -g ${args.groupName} --sku s1 --name ${args.name} --subscription ${args.subscriptionId}`, `Creating site plan [${args.name}]`); } // create qnaMaker service in resource group // we have a group, and app service, // provision search instance let searchName = args.name.toLowerCase() + '-search'; let searchResult = await runCommand(`az search service create -g ${azGroup.name} -n "${searchName}" --location ${utils_1.regionToSearchRegionMap[args.location]} --sku ${args.searchSku} --subscription ${args.subscriptionId}`, `Creating Azure Search Service [${searchName}]`); // get search keys let searchKeys = await runCommand(`az search admin-key show -g ${azGroup.name} --service-name "${searchName}" --subscription ${args.subscriptionId}`, `Fetching Azure Search Service keys [${searchName}]`); // create qna host service let qnaHostName = args.name + '-qnahost'; let createQnaWeb = await runCommand(`az webapp create -g ${azGroup.name} -n ${qnaHostName} --plan ${args.name} --subscription ${args.subscriptionId}`, `Creating QnA Maker host web service [${qnaHostName}]`); // configure qna web service settings command = `az webapp config appsettings set -g ${azGroup.name} -n ${qnaHostName} --subscription ${args.subscriptionId} --settings `; command += `"AzureSearchName=${searchName}" `; command += `AzureSearchAdminKey=${searchKeys.primaryKey} `; command += `PrimaryEndpointKey=${qnaHostName}-PrimaryEndpointKey `; command += `SecondaryEndpointKey=${qnaHostName}-SecondaryEndpointKey `; command += `DefaultAnswer="No good match found in KB." `; command += `QNAMAKER_EXTENSION_VERSION="latest"`; await runCommand(command, `Configuring QnA Maker host web service settings [${qnaHostName}]`); await runCommand(`az webapp cors add -g ${azGroup.name} -n ${qnaHostName} -a "*" --subscription ${args.subscriptionId}`, `Configuring QnA Maker host web service CORS [${qnaHostName}]`); // create qnamaker account let qnaAccountName = args.name + '-QnAMaker'; command = `az cognitiveservices account create -g ${azGroup.name} --kind QnAMaker -n "${qnaAccountName}" --sku S0 --subscription ${args.subscriptionId} `; // NOTE: currently qnamaker is only available in westus command += `--location westus --yes `; command += `--api-properties qnaRuntimeEndpoint=https://${qnaHostName}.azurewebsites.net`; azQnaSubscription = await runCommand(command, `Creating QnA Maker Cognitive Service [${qnaAccountName}]`); // get qna subscriptionKey let azQnaKeys = await runCommand(`az cognitiveservices account keys list -g ${azGroup.name} -n "${qnaAccountName}" --subscription ${args.subscriptionId}`, `Fetching QnA Maker Cognitive Service [${qnaAccountName}]`); args.qnaSubscriptionKey = azQnaKeys.key1; } break; default: if (!args.location) { throw new Error('missing --location argument'); } break; } } // pass 3- create the actual services for (let resource of recipe.resources) { switch (resource.type) { case botframework_config_1.ServiceTypes.AppInsights: { // this was created via az bot create, hook it up if (azAppInsights && resource.id) { let appInsights = await getAppInsightsService(azAppInsights); appInsights.id = resource.id; // keep original id config.services.push(appInsights); } else { console.warn("WARNING: No bot appInsights plan was created because no bot was created"); } await config.save(); } break; case botframework_config_1.ServiceTypes.BlobStorage: { // this was created via az bot create, get the connection string and then hook it up if (!storageInfo) { let storageName = `${azGroup.name.toLowerCase() + generateShortId()}storage`; storageInfo = await runCommand(`az storage account create -g ${azGroup.name} -n "${storageName}" --location ${args.location} --sku Standard_LRS --subscription ${args.subscriptionId}`, `Creating Azure Blob Storage [${storageName}]`); } if (storageInfo) { let blobConnection = await runCommand(`az storage account show-connection-string -g ${azGroup.name} -n "${storageInfo.name}" --subscription ${args.subscriptionId}`, `Fetching Azure Blob Storage connection string [${args.name}]`); let blobResource = resource; config.services.push(new botframework_config_1.BlobStorageService({ type: botframework_config_1.ServiceTypes.BlobStorage, id: resource.id, name: storageInfo.name, serviceName: storageInfo.name, tenantId: args.tenantId, subscriptionId: args.subscriptionId, resourceGroup: args.groupName, connectionString: blobConnection.connectionString, container: blobResource.container })); } await config.save(); } break; case botframework_config_1.ServiceTypes.Bot: { // already created } break; case botframework_config_1.ServiceTypes.CosmosDB: { let cosmosResource = resource; let cosmosName = `${args.name.toLowerCase()}`; // az cosmosdb create --n name -g Group1 let cosmosDb = await runCommand(`az cosmosdb create -n ${cosmosName} -g ${azGroup.name} --subscription ${args.subscriptionId}`, `Creating Azure CosmosDB account [${cosmosName}] ${chalk.default.italic.yellow(`(Please be patient, this may take 5 minutes)`)}`); // get keys let cosmosDbKeys = await runCommand(`az cosmosdb list-keys -g ${azGroup.name} -n ${cosmosName} --subscription ${args.subscriptionId}`, `Fetching Azure CosmosDB account keys [${args.name}]`); // az cosmosdb database create -n clonebot1cosmosdb --key <key> -d db1 --url-connection https://clonebot1cosmosdb.documents.azure.com:443/ await runCommand(`az cosmosdb database create -g ${azGroup.name} -n ${cosmosName} --key ${cosmosDbKeys.primaryMasterKey} -d ${cosmosResource.database} --url-connection https://${cosmosName}.documents.azure.com:443/ --subscription ${args.subscriptionId}`, `Creating Azure CosmosDB database [${cosmosResource.database}]`); // az cosmosdb collection create -n clonebot1cosmosdb --key <key> -d db1 --url-connection https://clonebot1cosmosdb.documents.azure.com:443/ --collection-name collection await runCommand(`az cosmosdb collection create -g ${azGroup.name} -n ${cosmosName} --key ${cosmosDbKeys.primaryMasterKey} -d ${cosmosResource.database} --url-connection https://${cosmosName}.documents.azure.com:443/ --collection-name ${cosmosResource.collection} --subscription ${args.subscriptionId}`, `Creating Azure CosmosDB collection [${cosmosResource.collection}]`); // get connection string is broken // command = `az cosmosdb list-connection-strings -g ${azGroup.name} -n ${args.name}`; // logCommand(args, `Fetching cosmosdb connection strings ${cosmosResource.collection}`, command); // p = await exec(command); // let connections = JSON.parse(p.stdout); // register it as a service config.services.push(new botframework_config_1.CosmosDbService({ type: botframework_config_1.ServiceTypes.CosmosDB, id: cosmosResource.id, name: cosmosName, serviceName: cosmosName, tenantId: args.tenantId, subscriptionId: args.subscriptionId, resourceGroup: args.groupName, endpoint: `https://${cosmosName}.documents.azure.com:443/`, key: cosmosDbKeys.primaryMasterKey, database: cosmosResource.database, collection: cosmosResource.collection, })); } await config.save(); break; case botframework_config_1.ServiceTypes.Endpoint: { let urlResource = resource; if (urlResource.url && urlResource.url.indexOf('localhost') > 0) { // add localhost record as is, but add appId/password config.services.push(new botframework_config_1.EndpointService({ type: botframework_config_1.ServiceTypes.Endpoint, id: resource.id, name: resource.name, appId: (azBotEndpoint) ? azBotEndpoint.appId : '', appPassword: (azBotEndpoint) ? azBotEndpoint.appPassword : '', endpoint: urlResource.url })); } else { // merge oldUrl and new Url hostname let oldUrl = new url.URL(urlResource.url); if (azBotEndpoint) { let azUrl = new url.URL(azBotEndpoint.endpoint); oldUrl.hostname = azUrl.hostname; config.services.push(new botframework_config_1.EndpointService({ type: botframework_config_1.ServiceTypes.Endpoint, id: resource.id, name: resource.name, appId: azBotEndpoint.appId, appPassword: azBotEndpoint.appPassword, endpoint: oldUrl.href })); if (oldUrl != azUrl) { // TODO update bot service record with merged url } } else { console.warn("There is no cloud endpoint to because there is no bot created"); } } await config.save(); } break; case botframework_config_1.ServiceTypes.File: { let fileResource = resource; config.services.push(new botframework_config_1.FileService({ type: botframework_config_1.ServiceTypes.File, id: fileResource.id, name: fileResource.name, path: fileResource.path, })); await config.save(); } break; case botframework_config_1.ServiceTypes.Generic: { let genericResource = resource; config.services.push(new botframework_config_1.GenericService({ type: botframework_config_1.ServiceTypes.Generic, id: genericResource.id, name: genericResource.name, url: genericResource.url, configuration: genericResource.configuration, })); await config.save(); } break; case botframework_config_1.ServiceTypes.Dispatch: { let luisService = await importAndTrainLuisApp(resource); let dispatchResource = resource; let dispatchService = Object.assign({ serviceIds: dispatchResource.serviceIds, }, luisService); dispatchService.type = botframework_config_1.ServiceTypes.Dispatch; dispatchService.id = resource.id; // keep same resource id config.services.push(new botframework_config_1.DispatchService(dispatchService)); await config.save(); } break; case botframework_config_1.ServiceTypes.Luis: { if (!commandExistsSync('luis')) { console.error(chalk.default.redBright(`Unable to find LUIS CLI. Please install via npm i -g luis-apis and try again. \n\nSee https://aka.ms/msbot-clone-services for pre-requisites.`)); showErrorHelp(); } let luisService = await importAndTrainLuisApp(resource); luisService.id = `${resource.id}`; // keep same resource id config.services.push(luisService); await config.save(); } break; case botframework_config_1.ServiceTypes.QnA: { if (!commandExistsSync('qnamaker')) { console.error(chalk.default.redBright(`Unable to find QnAMaker CLI. Please install via npm i -g qnamaker and try again. \n\nSee https://aka.ms/msbot-clone-services for pre-requisites.`)); showErrorHelp(); } let qnaPath = path.join(args.folder, `${resource.id}.qna`); let kbName = resource.name; // These values pretty much gaurantee success. We can decrease them if the QnA backend gets faster const initialDelaySeconds = 30; let retryAttemptsRemaining = 3; let retryDelaySeconds = 15; const retryDelayIncrease = 30; console.log(`Waiting ${initialDelaySeconds} seconds for QnAMaker backend to finish setting up...`); await sleep(initialDelaySeconds); let svc; while (retryAttemptsRemaining >= 0) { try { svc = await runCommand(`qnamaker create kb --subscriptionKey ${args.qnaSubscriptionKey} --name "${kbName}" --in ${qnaPath} --wait --msbot`, `Creating QnA Maker KB [${kbName}]`); break; } catch (err) { // Helpful error messages are mostly in err.stderr, when available. err.stderr can't be parsed to JSON, so we have to search for substrings if (err.stderr) { const generalFailedString = `"operationState\": \"Failed\"`; const invalidSubscriptionString = `Access denied due to invalid subscription key`; // Usually the first failure if (err.stderr.includes(invalidSubscriptionString)) { console.warn(chalk.default.yellowBright(`QnAMaker backend still generating API keys. Waiting ${retryDelaySeconds} seconds before trying again. ${retryAttemptsRemaining} attempts remaining.`)); // Usually the remaining, non-breaking failures } else if (err.stderr.includes(generalFailedString)) { console.warn(chalk.default.yellowBright(`QnAMaker backend not ready. Waiting ${retryDelaySeconds} seconds before trying again. ${retryAttemptsRemaining} attempts remaining.`)); } else { console.error(chalk.default.yellowBright(`QnAMaker doesn't seem to be working. Waiting ${retryDelaySeconds} seconds before trying again. ${retryAttemptsRemaining} attempts remaining.`)); } } retryAttemptsRemaining--; await sleep(retryDelaySeconds); retryDelaySeconds += retryDelayIncrease; if (retryAttemptsRemaining < 0) { console.error(chalk.default.redBright(`Unable to create QnA KB.`)); showErrorHelp(); } else { continue; } } } let service = new botframework_config_1.QnaMakerService(svc); service.id = `${resource.id}`; // keep id service.name = kbName; config.services.push(service); await config.save(); // publish try { await runCommand(`qnamaker publish kb --subscriptionKey ${service.subscriptionKey} --kbId ${service.kbId} --hostname ${service.hostname} --endpointKey ${service.endpointKey}`, `Publishing QnA Maker KB [${kbName}]`); } catch (err) { console.warn(err.message || err); } } break; default: break; } } // hook up appinsights and blob storage if it hasn't been already if (azBot) { let hasBot = false; let hasBlob = false; let hasAppInsights = false; for (let service of config.services) { switch (service.type) { case botframework_config_1.ServiceTypes.AppInsights: hasAppInsights = true; break; case botframework_config_1.ServiceTypes.BlobStorage: hasBlob = true; break; case botframework_config_1.ServiceTypes.Bot: hasBot = true; break; } } if (!hasBot) { // created via az bot create, register the result config.connectService(new botframework_config_1.BotService({ name: azBot.name, tenantId: args.tenantId, subscriptionId: args.subscriptionId, resourceGroup: args.groupName, serviceName: azBot.name, appId: azBot.appId })); } if (azBotEndpoint) { // add endpoint config.connectService(new botframework_config_1.EndpointService({ type: botframework_config_1.ServiceTypes.Endpoint, name: "production", appId: azBotEndpoint.appId, appPassword: azBotEndpoint.appPassword, endpoint: azBotEndpoint.endpoint })); await config.save(); } if (!hasAppInsights) { if (azAppInsights) { let appInsights = await getAppInsightsService(azAppInsights); config.connectService(appInsights); } else { console.warn("WARNING: No bot appInsights plan was created because no bot was created"); } await config.save(); } if (!hasBlob && storageInfo) { // this was created via az bot create, get the connection string and then hook it up let blobConnection = await runCommand(`az storage account show-connection-string -g ${azGroup.name} -n "${storageInfo.name}" --subscription ${args.subscriptionId}`, `Fetching storage connection string [${storageInfo.name}]`); config.connectService(new botframework_config_1.BlobStorageService({ name: storageInfo.name, serviceName: storageInfo.name, tenantId: args.tenantId, subscriptionId: args.subscriptionId, resourceGroup: args.groupName, connectionString: blobConnection.connectionString, container: null })); await config.save(); } } console.log(`${config.getPath()} created.`); if (args.secret) { console.log(`\nThe secret used to decrypt ${config.getPath()} is:`); console.log(chalk.default.magentaBright(args.secret)); console.log(chalk.default.yellowBright(`\nNOTE: This secret is not recoverable and you should save it in a safe place according to best security practices.`)); console.log(chalk.default.yellowBright(` Copy this secret and use it to open the ${config.getPath()} the first time.`)); await config.save(args.secret); } if (azBot) { // update local safe settings await updateLocalSafeSettings(azBot); // publish local bot code to web service await publishBot(azBot); // show emulator url with secret if exist let fullPath = path.resolve(process.cwd(), config.getPath()); let productionEndpoint = config.findServiceByNameOrId('production'); let botFileUrl = `bfemulator://bot.open?path=${encodeURIComponent(fullPath)}`; if (args.secret) { botFileUrl += `&secret=${encodeURIComponent(args.secret)}`; } if (productionEndpoint) { botFileUrl += `&id=${productionEndpoint.id}`; } console.log('To open this bot file in emulator:'); console.log(chalk.default.cyanBright(botFileUrl)); // auto launch emulator with url so it can memorize the secret so you don't need to remember it. <whew!> process.env.ELECTRON_NO_ATTACH_CONSOLE = "true"; opn(botFileUrl, { wait: false }); } console.log(`Done.`); } catch (error) { if (error.message) { let lines = error.message.split('\n'); let message = ''; for (let line of lines) { // trim to copyright symbol, help from inner process command line args is inappropriate if (line.indexOf('©') > 0) break; message += line; } throw new Error(message); } throw new Error(error); } } async function checkDotNetRequirement() { let minVersion = [2, 1, 500]; if (!commandExistsSync('dotnet')) { ShowDotnetRequirementHelp(minVersion); process.exit(1); } else { let dotnetVersion = await runCommand('dotnet --version', 'checking dotnet requirement'); let versions = dotnetVersion.split('.'); if (parseInt(versions[0]) < minVersion[0]) { ShowDotnetRequirementHelp(minVersion); process.exit(1); } else if (parseInt(versions[0]) == minVersion[0] && parseInt(versions[1]) < minVersion[1]) { ShowDotnetRequirementHelp(minVersion); process.exit(1); } else if (parseInt(versions[0]) == minVersion[0] && parseInt(versions[1]) == minVersion[1] && parseInt(versions[2]) < minVersion[2]) { ShowDotnetRequirementHelp(minVersion); process.exit(1); } } } function ShowDotnetRequirementHelp(minVersion) { console.error(chalk.default.redBright(`This operation requires Dotnet Core SDK ${minVersion.join('.')} or newer to be installed.`)); console.error(chalk.default.redBright('Go to https://www.microsoft.com/net/download to install on your system.')); } /** * Updates local appsettings.json and .env files with botFilePath and botFileSecret entries. * @param {IBotService} azBot */ async function updateLocalSafeSettings(azBot) { if (azBot) { // update local environment settings if (fs.existsSync('appsettings.json')) { console.log(`Updating appsettings.json with botFilePath=${config.getPath()}`); let settings = JSON.parse(txtfile.readSync('appsettings.json')); settings.botFilePath = config.getPath(); fs.writeFileSync('appsettings.json', JSON.stringify(settings, null, 4), { encoding: 'utf8' }); if (args.secret) { // some projfiles won't have a userSecret set, check for it if (args.projFile) { let proj = txtfile.readSync(args.projFile); if (proj.indexOf('<UserSecretsId>') < 0) { // doesn't have it, add one let end = proj.indexOf('</Project'); let newProj = proj.substring(0, end); newProj += `<PropertyGroup><UserSecretsId>${uuid.v4()}</UserSecretsId></PropertyGroup>\n`; newProj += proj.substring(end); fs.writeFileSync(args.projFile, newProj, { encoding: 'utf8' }); } } // save secret await runCommand(`dotnet user-secrets set botFileSecret ${args.secret}`, `Saving the botFileSecret with dotnet user-secrets`); // make sure that startup.cs has configuration information if (fs.existsSync('startup.cs')) { let startup = txtfile.readSync('startup.cs'); // if it doesn't have .AddUserSecrets call if (startup.indexOf('.AddUserSecrets') < 0) { let i = startup.indexOf('Configuration = builder.Build();'); if (i > 0) { let newStartup = startup.substring(0, i); console.log('Updating startup.cs to use user-secrets'); newStartup += 'if (env.IsDevelopment()) \n builder.AddUserSecrets<Startup>();\n\n ' + startup.substring(i); fs.writeFileSync('startup.cs', newStartup, { encoding: 'utf8' }); } else { console.log(chalk.default.yellow('You need to add following code to your dotnet configuration\n')); console.log('if (env.IsDevelopment()) builder.AddUserSecrets<Startup>();'); } } } } } else if (fs.existsSync('.env')) { console.log(`Updating .env with path and secret`); let lines = txtfile.readSync('.env').split('\n'); let newEnv = ''; let pathLine = `botFilePath="${config.getPath()}"\n`; let secretLine = `botFileSecret="${args.secret}"\n`; let foundPath = false; let foundSecret = false; for (let line of lines) { let i = line.indexOf('='); if (i > 0) { let name = line.substring(0, i); let value = line.substring(i + 1); switch (name) { case 'botFilePath': newEnv += pathLine; foundPath = true; break; case "botFileSecret": newEnv += secretLine; foundSecret = true; break; default: newEnv += line + '\n'; break; } } else { // pass through newEnv += line + '\n'; } } if (!foundPath) { newEnv += pathLine; } if (!foundSecret) { newEnv += secretLine; } fs.writeFileSync('.env', newEnv.trimRight(), { encoding: 'utf8' }); } } } /** * Calls creates a publish command and potentially calls it if code-dir is passed in. * Also creates local scripts to make it easier to publish local source code by already having the code formulated. * @param azBot */ async function publishBot(azBot) { let azPublishCmd =