Demonstrate using uiRouter and serving UI at /ui path of Bot (#337)
* Serve UI at /ui path of bot server * chore: fix other UI starting related variables or docs * chore: update to SDK version which has new uiRouter * Add npm run ui command back with print out of change * Adjust Bot Summary page more * chore: adjust sentences on index.html and remove UserVoice link * chore: change npm run ui text
This commit is contained in:
Родитель
d385582e04
Коммит
b8786f496a
|
@ -15,9 +15,6 @@ LUIS_SUBSCRIPTION_KEY=
|
||||||
# the field "CONVERSATION_LEARNER_MODEL_ID".
|
# the field "CONVERSATION_LEARNER_MODEL_ID".
|
||||||
CONVERSATION_LEARNER_MODEL_ID=
|
CONVERSATION_LEARNER_MODEL_ID=
|
||||||
|
|
||||||
# Optionally Configure UI port to avoid conflicts with other locally running services
|
|
||||||
# CONVERSATION_LEARNER_UI_PORT=5050
|
|
||||||
|
|
||||||
# By default @conversationlearner/sdk will store Bot memory/state in local memory storage.
|
# By default @conversationlearner/sdk will store Bot memory/state in local memory storage.
|
||||||
# To run the Redis storage demo (demoStorage.ts) add the server uri and key
|
# To run the Redis storage demo (demoStorage.ts) add the server uri and key
|
||||||
# CONVERSATION_LEARNER_REDIS_SERVER=
|
# CONVERSATION_LEARNER_REDIS_SERVER=
|
||||||
|
|
22
README.md
22
README.md
|
@ -66,15 +66,7 @@ Project Conversation Learner consists of an SDK you add to your bot, and a cloud
|
||||||
|
|
||||||
This runs the generic empty bot in `my-bot-01/src/app.ts`.
|
This runs the generic empty bot in `my-bot-01/src/app.ts`.
|
||||||
|
|
||||||
3. Run Conversation Learner UI:
|
3. Open browser to http://localhost:3978
|
||||||
|
|
||||||
```bash
|
|
||||||
[open second command prompt window]
|
|
||||||
cd my-bot-01
|
|
||||||
npm run ui
|
|
||||||
```
|
|
||||||
|
|
||||||
4. Open browser to http://localhost:5050
|
|
||||||
|
|
||||||
You're now using Conversation Learner and can create and teach a Conversation Learner model.
|
You're now using Conversation Learner and can create and teach a Conversation Learner model.
|
||||||
|
|
||||||
|
@ -82,9 +74,9 @@ You're now using Conversation Learner and can create and teach a Conversation Le
|
||||||
|
|
||||||
The instructions above started the generic empty bot. To run a tutorial or demo bot instead:
|
The instructions above started the generic empty bot. To run a tutorial or demo bot instead:
|
||||||
|
|
||||||
1. If you have the Conversation Learner web UI open, return to the list of models at http://localhost:5050/home.
|
1. If you have the Conversation Learner web UI open, return to the list of models at http://localhost:3978/ui/home.
|
||||||
|
|
||||||
2. If another bot is running (like `npm start` or `npm run demo-pizza`), stop it. You do not need to stop the UI process, or close the web browser.
|
2. If another bot is running (like `npm start` or `npm run demo-pizza`), stop it. You do not need close the web browser.
|
||||||
|
|
||||||
3. Run a demo bot from the command line (step 2 above). Demos include:
|
3. Run a demo bot from the command line (step 2 above). Demos include:
|
||||||
|
|
||||||
|
@ -99,7 +91,7 @@ The instructions above started the generic empty bot. To run a tutorial or demo
|
||||||
npm run demo-vrapp
|
npm run demo-vrapp
|
||||||
```
|
```
|
||||||
|
|
||||||
4. If you're not already, switch to the Conversation Learner web UI in Chrome by loading http://localhost:5050/home.
|
4. If you're not already, switch to the Conversation Learner web UI in Chrome by loading http://localhost:3978/ui/home.
|
||||||
|
|
||||||
5. Click on "Import tutorials" (only needs to be done once). This will take about a minute and will copy the Conversation Learner models for all the tutorials into your Conversation Learner account.
|
5. Click on "Import tutorials" (only needs to be done once). This will take about a minute and will copy the Conversation Learner models for all the tutorials into your Conversation Learner account.
|
||||||
|
|
||||||
|
@ -109,9 +101,9 @@ Source files for the demos are in `my-bot-01/src/demos`
|
||||||
|
|
||||||
## Create a bot which includes back-end code
|
## Create a bot which includes back-end code
|
||||||
|
|
||||||
1. If you have the Conversation Learner web UI open, return to the list of models at http://localhost:5050/home.
|
1. If you have the Conversation Learner web UI open, return to the list of models at http://localhost:3978/ui/home.
|
||||||
|
|
||||||
2. If a bot is running (like `npm run demo-pizza`), stop it. You do not need to stop the UI process, or close the web browser.
|
2. If a bot is running (like `npm run demo-pizza`), stop it. You do not need to close the web browser.
|
||||||
|
|
||||||
3. If desired, edit code in `my-bot-01/src/app.ts`.
|
3. If desired, edit code in `my-bot-01/src/app.ts`.
|
||||||
|
|
||||||
|
@ -122,7 +114,7 @@ Source files for the demos are in `my-bot-01/src/demos`
|
||||||
npm start
|
npm start
|
||||||
```
|
```
|
||||||
|
|
||||||
5. If you're not already, switch to the Conversation Learner web UI in Chrome by loading http://localhost:5050/home.
|
5. If you're not already, switch to the Conversation Learner web UI in Chrome by loading http://localhost:3978/ui/home.
|
||||||
|
|
||||||
6. Create a new Conversation Learner application in the UI, and start teaching.
|
6. Create a new Conversation Learner application in the UI, and start teaching.
|
||||||
|
|
||||||
|
|
|
@ -33,17 +33,17 @@
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@conversationlearner/models": {
|
"@conversationlearner/models": {
|
||||||
"version": "0.193.1",
|
"version": "0.193.2",
|
||||||
"resolved": "https://registry.npmjs.org/@conversationlearner/models/-/models-0.193.1.tgz",
|
"resolved": "https://registry.npmjs.org/@conversationlearner/models/-/models-0.193.2.tgz",
|
||||||
"integrity": "sha512-JmrYk3EFW/MZHGVjWTFrEt9YcAfzHikNXB1z04wDF0SWS8kR4NK7LTgAahywuY+i7VU3exhEwbPbUNlijNd4tw=="
|
"integrity": "sha512-0fEtPoUMrVvS8aY5XVGDQMLLQ/xMJjq4nxkJ2ZnrV2bQGcriZ0r7aeqxjnyuU4qURQCOKhAOod2v4XHi5vrkmA=="
|
||||||
},
|
},
|
||||||
"@conversationlearner/sdk": {
|
"@conversationlearner/sdk": {
|
||||||
"version": "0.313.5",
|
"version": "0.314.0",
|
||||||
"resolved": "https://registry.npmjs.org/@conversationlearner/sdk/-/sdk-0.313.5.tgz",
|
"resolved": "https://registry.npmjs.org/@conversationlearner/sdk/-/sdk-0.314.0.tgz",
|
||||||
"integrity": "sha512-zaA10Ske/YviRGgaNmATHVsW4KdIo+kwL57xdsz89ZI1N8vtKHe4GrGIoG07EoVUo4iyMxhLR/GvcJfykreBYw==",
|
"integrity": "sha512-sFAA30105Vo3p1GUsb7tJV4ZAZ62mdFBmD0NDkH+Ucghz6nQVBGOsStFpD89jgU2XC1mfi7bnSJsYQT5rTEbOw==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"@conversationlearner/models": "0.193.1",
|
"@conversationlearner/models": "0.193.2",
|
||||||
"@conversationlearner/ui": "0.335.10",
|
"@conversationlearner/ui": "0.337.0",
|
||||||
"@types/supertest": "2.0.4",
|
"@types/supertest": "2.0.4",
|
||||||
"async-file": "^2.0.2",
|
"async-file": "^2.0.2",
|
||||||
"body-parser": "1.18.3",
|
"body-parser": "1.18.3",
|
||||||
|
@ -64,6 +64,11 @@
|
||||||
"xmldom": "^0.1.27"
|
"xmldom": "^0.1.27"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@conversationlearner/ui": {
|
||||||
|
"version": "0.337.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/@conversationlearner/ui/-/ui-0.337.0.tgz",
|
||||||
|
"integrity": "sha512-wrJlMC5xevIsIHn/Lrj03ICZVfCajj2pNonuVaUmk5gJtOsFxCASEZpiRUnPk6qTLVewqXsH7gXrQkitKGl2IQ=="
|
||||||
|
},
|
||||||
"body-parser": {
|
"body-parser": {
|
||||||
"version": "1.18.3",
|
"version": "1.18.3",
|
||||||
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
|
"resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.18.3.tgz",
|
||||||
|
@ -99,11 +104,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@conversationlearner/ui": {
|
|
||||||
"version": "0.335.10",
|
|
||||||
"resolved": "https://registry.npmjs.org/@conversationlearner/ui/-/ui-0.335.10.tgz",
|
|
||||||
"integrity": "sha512-Uq4z18X+QpZaTv0GwIwn59XKc1/Lzy7efSqPosjLRBVHd+HmErqvJjvC/0806CWHTi/49Z7cJfMR0SLUKkj8Ng=="
|
|
||||||
},
|
|
||||||
"@types/bluebird": {
|
"@types/bluebird": {
|
||||||
"version": "3.5.20",
|
"version": "3.5.20",
|
||||||
"resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.20.tgz",
|
"resolved": "https://registry.npmjs.org/@types/bluebird/-/bluebird-3.5.20.tgz",
|
||||||
|
@ -463,7 +463,7 @@
|
||||||
},
|
},
|
||||||
"async": {
|
"async": {
|
||||||
"version": "1.5.2",
|
"version": "1.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
|
"resolved": "http://registry.npmjs.org/async/-/async-1.5.2.tgz",
|
||||||
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo="
|
"integrity": "sha1-7GphrlZIDAw8skHJVhjiCJL5Zyo="
|
||||||
},
|
},
|
||||||
"async-each": {
|
"async-each": {
|
||||||
|
@ -3110,7 +3110,7 @@
|
||||||
},
|
},
|
||||||
"http-proxy-middleware": {
|
"http-proxy-middleware": {
|
||||||
"version": "0.18.0",
|
"version": "0.18.0",
|
||||||
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz",
|
"resolved": "http://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.18.0.tgz",
|
||||||
"integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==",
|
"integrity": "sha512-Fs25KVMPAIIcgjMZkVHJoKg9VcXcC1C8yb9JUgeDvVXY0S/zgVIhMb+qVswDIgtJe2DfckMSY2d6TuTEutlk6Q==",
|
||||||
"requires": {
|
"requires": {
|
||||||
"http-proxy": "^1.16.2",
|
"http-proxy": "^1.16.2",
|
||||||
|
@ -5970,7 +5970,7 @@
|
||||||
},
|
},
|
||||||
"requires-port": {
|
"requires-port": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
"resolved": "http://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz",
|
||||||
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
|
"integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8="
|
||||||
},
|
},
|
||||||
"resolve": {
|
"resolve": {
|
||||||
|
@ -6525,7 +6525,7 @@
|
||||||
},
|
},
|
||||||
"superagent": {
|
"superagent": {
|
||||||
"version": "2.3.0",
|
"version": "2.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-2.3.0.tgz",
|
"resolved": "http://registry.npmjs.org/superagent/-/superagent-2.3.0.tgz",
|
||||||
"integrity": "sha1-cDUpoHFOV+EjlZ3e+84ZOy5Q0RU=",
|
"integrity": "sha1-cDUpoHFOV+EjlZ3e+84ZOy5Q0RU=",
|
||||||
"requires": {
|
"requires": {
|
||||||
"component-emitter": "^1.2.0",
|
"component-emitter": "^1.2.0",
|
||||||
|
@ -6542,7 +6542,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"form-data": {
|
"form-data": {
|
||||||
"version": "1.0.0-rc4",
|
"version": "1.0.0-rc4",
|
||||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz",
|
"resolved": "http://registry.npmjs.org/form-data/-/form-data-1.0.0-rc4.tgz",
|
||||||
"integrity": "sha1-BaxrwiIntD5EYfSIFhVUaZ1Pi14=",
|
"integrity": "sha1-BaxrwiIntD5EYfSIFhVUaZ1Pi14=",
|
||||||
"requires": {
|
"requires": {
|
||||||
"async": "^1.5.2",
|
"async": "^1.5.2",
|
||||||
|
@ -6596,7 +6596,7 @@
|
||||||
},
|
},
|
||||||
"swagger-client": {
|
"swagger-client": {
|
||||||
"version": "2.2.21",
|
"version": "2.2.21",
|
||||||
"resolved": "https://registry.npmjs.org/swagger-client/-/swagger-client-2.2.21.tgz",
|
"resolved": "http://registry.npmjs.org/swagger-client/-/swagger-client-2.2.21.tgz",
|
||||||
"integrity": "sha1-WWa+I0dyRm5EcW9l4yAIFm2u66Q=",
|
"integrity": "sha1-WWa+I0dyRm5EcW9l4yAIFm2u66Q=",
|
||||||
"requires": {
|
"requires": {
|
||||||
"btoa": "^1.1.2",
|
"btoa": "^1.1.2",
|
||||||
|
|
|
@ -15,7 +15,7 @@
|
||||||
"pretest": "npm run build",
|
"pretest": "npm run build",
|
||||||
"test": "jest",
|
"test": "jest",
|
||||||
"tsc": "tsc",
|
"tsc": "tsc",
|
||||||
"ui": "node ./lib/ui.js",
|
"ui": "echo You no longer have to start the UI separately. It will start when you start the Bot. Go to: http://localhost:3978/ui",
|
||||||
"verifypackagelock": "tsc -p ./scripts/tsconfig.json && node ./scripts/verifypackagelock.js",
|
"verifypackagelock": "tsc -p ./scripts/tsconfig.json && node ./scripts/verifypackagelock.js",
|
||||||
"watch": "concurrently --kill-others -p [{name}-{pid}] -n tsc,bot \"tsc -w\" \"nodemon\"",
|
"watch": "concurrently --kill-others -p [{name}-{pid}] -n tsc,bot \"tsc -w\" \"nodemon\"",
|
||||||
"demo-password": "node ./lib/demos/demoPasswordReset.js",
|
"demo-password": "node ./lib/demos/demoPasswordReset.js",
|
||||||
|
@ -31,7 +31,7 @@
|
||||||
"author": "Microsoft Conversation Learner Team",
|
"author": "Microsoft Conversation Learner Team",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@conversationlearner/sdk": "0.313.5",
|
"@conversationlearner/sdk": "0.314.0",
|
||||||
"botbuilder": "4.1.7",
|
"botbuilder": "4.1.7",
|
||||||
"chalk": "2.4.1",
|
"chalk": "2.4.1",
|
||||||
"convict": "^4.0.2",
|
"convict": "^4.0.2",
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<meta http-equiv="X-UA-Compatible" content="ie=edge">
|
||||||
|
<title>Bot Summary</title>
|
||||||
|
|
||||||
|
<style type="text/css">
|
||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
color: white;
|
||||||
|
font-size: 24px;
|
||||||
|
font-family: 'Segoe UI Light', Arial, Helvetica, sans-serif;
|
||||||
|
background-color: #0065B3;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
a:link,
|
||||||
|
a:visited {
|
||||||
|
color: #7CD300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.center {
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
font-size: 300%;
|
||||||
|
}
|
||||||
|
|
||||||
|
main {
|
||||||
|
width: 800px;
|
||||||
|
margin: 1em auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link {
|
||||||
|
padding: 1em;
|
||||||
|
background-color: #f1f1f1;
|
||||||
|
color: black;
|
||||||
|
border-radius: 0.5em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.link a:link,
|
||||||
|
.link a:visited {
|
||||||
|
color: #0065B3;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1 class="center">Sample Bot Summary Page</h1>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<p>Your bot homepage. Here you can show basic information about your bot.</p>
|
||||||
|
|
||||||
|
<p>This default page is included with the Conversation Learner sample bot. You should customize it to fit your needs.</p>
|
||||||
|
|
||||||
|
<div class="center">
|
||||||
|
<h2 class="link">Go to UI: <a href="http://localhost:3978/ui">http://localhost:3978/ui</a></h2>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<h3>Questions this page can answer:</h3>
|
||||||
|
<ul>
|
||||||
|
<li>What is your bots purpose?</li>
|
||||||
|
<li>How does it help people?</li>
|
||||||
|
<li>Provide examples or links to support</li>
|
||||||
|
<li>Branding for your company</li>
|
||||||
|
</ul>
|
||||||
|
|
||||||
|
<h3>Resources:</h3>
|
||||||
|
<ul>
|
||||||
|
<li><a href="https://labs.cognitive.microsoft.com/en-us/project-conversation-learner">Project Conversation Learner Documentation</a></li>
|
||||||
|
<li><a href="https://dev.botframework.com/">Azure Bot Framework</a></li>
|
||||||
|
<li><a href="https://www.microsoft.com/en-us/research/publication/responsible-bots/">Responsible bots: 10 guidelines for developers of conversational AI</a></li>
|
||||||
|
</ul>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
|
@ -13,6 +13,11 @@ describe('Test bot server', () => {
|
||||||
expect(response.status).toBe(200)
|
expect(response.status).toBe(200)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it('given request to known (ui) route should return 200', async () => {
|
||||||
|
const response = await botServer.get('/ui/home')
|
||||||
|
expect(response.status).toBe(200)
|
||||||
|
})
|
||||||
|
|
||||||
it('given request to unknown route should return 404', async () => {
|
it('given request to unknown route should return 404', async () => {
|
||||||
const response = await botServer.get('/unknown')
|
const response = await botServer.get('/unknown')
|
||||||
expect(response.status).toBe(404)
|
expect(response.status).toBe(404)
|
||||||
|
|
|
@ -5,7 +5,7 @@
|
||||||
import * as path from 'path'
|
import * as path from 'path'
|
||||||
import * as express from 'express'
|
import * as express from 'express'
|
||||||
import { BotFrameworkAdapter } from 'botbuilder'
|
import { BotFrameworkAdapter } from 'botbuilder'
|
||||||
import { ConversationLearner, ClientMemoryManager, FileStorage } from '@conversationlearner/sdk'
|
import { ConversationLearner, ClientMemoryManager, FileStorage, uiRouter } from '@conversationlearner/sdk'
|
||||||
import chalk from 'chalk'
|
import chalk from 'chalk'
|
||||||
import config from './config'
|
import config from './config'
|
||||||
|
|
||||||
|
@ -40,8 +40,15 @@ const includeSdk = ['development', 'test'].includes(process.env.NODE_ENV || '')
|
||||||
if (includeSdk) {
|
if (includeSdk) {
|
||||||
console.log(chalk.cyanBright(`Adding /sdk routes`))
|
console.log(chalk.cyanBright(`Adding /sdk routes`))
|
||||||
server.use('/sdk', sdkRouter)
|
server.use('/sdk', sdkRouter)
|
||||||
|
|
||||||
|
// Note: Must be mounted at root to use internal /ui paths
|
||||||
|
console.log(chalk.greenBright(`Adding /ui routes`))
|
||||||
|
server.use(uiRouter)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serve default bot summary page. Should be customized by customer.
|
||||||
|
server.use(express.static(path.join(__dirname, '..', 'site')))
|
||||||
|
|
||||||
const cl = new ConversationLearner(modelId)
|
const cl = new ConversationLearner(modelId)
|
||||||
|
|
||||||
//=================================
|
//=================================
|
||||||
|
|
|
@ -32,11 +32,6 @@ export const config = convict({
|
||||||
default: "https://westus.api.cognitive.microsoft.com/conversationlearner/v1.0/",
|
default: "https://westus.api.cognitive.microsoft.com/conversationlearner/v1.0/",
|
||||||
env: 'CONVERSATION_LEARNER_SERVICE_URI'
|
env: 'CONVERSATION_LEARNER_SERVICE_URI'
|
||||||
},
|
},
|
||||||
CONVERSATION_LEARNER_UI_PORT: {
|
|
||||||
format: 'port',
|
|
||||||
default: 5050,
|
|
||||||
env: 'CONVERSATION_LEARNER_UI_PORT',
|
|
||||||
},
|
|
||||||
modelId: {
|
modelId: {
|
||||||
format: String,
|
format: String,
|
||||||
default: undefined,
|
default: undefined,
|
||||||
|
|
|
@ -14,6 +14,6 @@ if (isDevelopment) {
|
||||||
}
|
}
|
||||||
|
|
||||||
app.listen(config.botPort, () => {
|
app.listen(config.botPort, () => {
|
||||||
console.log(`Server listening to port: ${config.botPort}`)
|
console.log(`Server listening at: http://localhost:${config.botPort}`)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
|
@ -1,8 +0,0 @@
|
||||||
/**
|
|
||||||
* Copyright (c) Microsoft Corporation. All rights reserved.
|
|
||||||
* Licensed under the MIT License.
|
|
||||||
*/
|
|
||||||
import config from './config'
|
|
||||||
import { startUiServer } from '@conversationlearner/sdk'
|
|
||||||
|
|
||||||
startUiServer(config.CONVERSATION_LEARNER_UI_PORT)
|
|
Загрузка…
Ссылка в новой задаче