├── .dockerignore ├── .editorconfig ├── .env.sample ├── .eslintrc.js ├── .gitignore ├── CHANGELOG.md ├── Dockerfile ├── LICENCE ├── README.md ├── api └── index.js ├── assets ├── demo.gif ├── run_all_tests.gif ├── run_failed_tests.gif └── scenarios.gif ├── bin └── nuxt-start.js ├── components ├── CardItem.vue ├── Dialog.vue ├── Message.vue ├── MessageButton.vue ├── SearchInput.vue └── Sidebar.vue ├── docker-compose.yml ├── layouts └── default.vue ├── nuxt.config.js ├── package-lock.json ├── package.json ├── pages └── index.vue ├── plugins ├── element-ui.js ├── localStorage.js ├── vue-awesome.js └── vue-slideout.js ├── scripts └── deploy.sh ├── static ├── CNAME ├── android-chrome-192x192.png ├── android-chrome-512x512.png ├── apple-touch-icon.png ├── browserconfig.xml ├── favicon-16x16.png ├── favicon-32x32.png ├── favicon.ico ├── icon.png ├── mstile-150x150.png └── safari-pinned-tab.svg └── store ├── index.js └── settings.js /.dockerignore: -------------------------------------------------------------------------------- 1 | assets 2 | node_modules 3 | npm-debug.log 4 | Dockerfile* 5 | docker-compose* 6 | .dockerignore 7 | .git 8 | .gitignore 9 | README.md 10 | LICENSE 11 | .vscode 12 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_size = 2 6 | indent_style = space 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | BASE_URL="http://localhost:3856" 2 | IS_PROXY=1 3 | SPEECH_ENGINE=yandex 4 | YANDEX_WEBSPEECH_KEY="your-key" 5 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | browser: true, 5 | node: true 6 | }, 7 | parserOptions: { 8 | parser: "babel-eslint" 9 | }, 10 | extends: [ 11 | // https://github.com/vuejs/eslint-plugin-vue#priority-a-essential-error-prevention 12 | // consider switching to `plugin:vue/strongly-recommended` or `plugin:vue/recommended` for stricter rules. 13 | "plugin:vue/essential" 14 | ], 15 | // required to lint *.vue files 16 | plugins: ["vue"], 17 | // add your custom rules here 18 | rules: {} 19 | }; 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # dependencies 2 | node_modules 3 | 4 | # logs 5 | npm-debug.log 6 | 7 | # Nuxt build 8 | .nuxt 9 | 10 | # Nuxt generate 11 | dist 12 | 13 | # service worker 14 | sw.* 15 | 16 | # environment 17 | .env 18 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # [1.2.0](https://github.com/popstas/yandex-dialogs-client/compare/v1.1.12...v1.2.0) (2020-06-16) 2 | 3 | 4 | ### Bug Fixes 5 | 6 | * timeout по умолчанию 3000 мс, поправлено сообщение об ошибке ([978c9fe](https://github.com/popstas/yandex-dialogs-client/commit/978c9fe)), closes [#3](https://github.com/popstas/yandex-dialogs-client/issues/3) 7 | * добавлены подсказки к переключателям ([83932f5](https://github.com/popstas/yandex-dialogs-client/commit/83932f5)) 8 | 9 | 10 | ### Features 11 | 12 | * переключатель "Устройство с экраном" (meta.interfaces.screen) ([81189f3](https://github.com/popstas/yandex-dialogs-client/commit/81189f3)), closes [#2](https://github.com/popstas/yandex-dialogs-client/issues/2) 13 | * ссылка поблагодарить автора - https://www.tinkoff.ru/sl/83wd1FGIcMR ([06bf50f](https://github.com/popstas/yandex-dialogs-client/commit/06bf50f)) 14 | 15 | 16 | 17 | ## [1.1.12](https://github.com/popstas/yandex-dialogs-client/compare/v1.1.11...v1.1.12) (2020-04-13) 18 | 19 | 20 | ### Bug Fixes 21 | 22 | * proxy was not work ([5bfee04](https://github.com/popstas/yandex-dialogs-client/commit/5bfee04)) 23 | 24 | 25 | 26 | ## [1.1.11](https://github.com/popstas/yandex-dialogs-client/compare/v1.1.10...v1.1.11) (2019-06-16) 27 | 28 | 29 | 30 | ## [1.1.10](https://github.com/popstas/yandex-dialogs-client/compare/v1.1.9...v1.1.10) (2019-03-23) 31 | 32 | 33 | 34 | ## [1.1.9](https://github.com/popstas/yandex-dialogs-client/compare/v1.1.8...v1.1.9) (2019-03-15) 35 | 36 | 37 | 38 | ## [1.1.8](https://github.com/popstas/yandex-dialogs-client/compare/v1.1.7...v1.1.8) (2019-03-15) 39 | 40 | 41 | 42 | ## [1.1.7](https://github.com/popstas/yandex-dialogs-client/compare/v1.1.6...v1.1.7) (2019-03-15) 43 | 44 | 45 | 46 | ## [1.1.6](https://github.com/popstas/yandex-dialogs-client/compare/v1.1.5...v1.1.6) (2019-03-14) 47 | 48 | 49 | 50 | ## [1.1.5](https://github.com/popstas/yandex-dialogs-client/compare/v1.1.4...v1.1.5) (2019-03-14) 51 | 52 | 53 | 54 | ## [1.1.4](https://github.com/popstas/yandex-dialogs-client/compare/v1.1.3...v1.1.4) (2019-03-14) 55 | 56 | 57 | 58 | ## [1.1.3](https://github.com/popstas/yandex-dialogs-client/compare/v1.1.2...v1.1.3) (2019-03-14) 59 | 60 | 61 | 62 | ## [1.1.2](https://github.com/popstas/yandex-dialogs-client/compare/v1.1.1...v1.1.2) (2019-03-14) 63 | 64 | 65 | ### Features 66 | 67 | * yandex-dialogs-client as cli tool and npm package ([9d4c226](https://github.com/popstas/yandex-dialogs-client/commit/9d4c226)) 68 | 69 | 70 | 71 | ## [1.1.1](https://github.com/popstas/yandex-dialogs-client/compare/v1.1.0...v1.1.1) (2019-02-27) 72 | 73 | 74 | ### Features 75 | 76 | * проверка /scerarios.yml по умолчанию отключена ([47daf24](https://github.com/popstas/yandex-dialogs-client/commit/47daf24)) 77 | 78 | 79 | 80 | # [1.1.0](https://github.com/popstas/yandex-dialogs-client/compare/v1.0.2...v1.1.0) (2019-02-11) 81 | 82 | 83 | ### Bug Fixes 84 | 85 | * более бледный маркер системного сообщения ([131d616](https://github.com/popstas/yandex-dialogs-client/commit/131d616)) 86 | 87 | 88 | ### Features 89 | 90 | * карточки ItemsList ([acad67f](https://github.com/popstas/yandex-dialogs-client/commit/acad67f)) 91 | 92 | 93 | 94 | ## [1.0.2](https://github.com/popstas/yandex-dialogs-client/compare/v1.0.1...v1.0.2) (2019-01-30) 95 | 96 | 97 | ### Bug Fixes 98 | 99 | * выводить сообщения об ошибках в scenarios.yml ([ae37b1a](https://github.com/popstas/yandex-dialogs-client/commit/ae37b1a)) 100 | 101 | 102 | 103 | ## [1.0.1](https://github.com/popstas/yandex-dialogs-client/compare/v1.0.0...v1.0.1) (2019-01-12) 104 | 105 | 106 | ### Bug Fixes 107 | 108 | * вывод времени в json запросов и ответов в консоли ([d0bed96](https://github.com/popstas/yandex-dialogs-client/commit/d0bed96)) 109 | * ошибка в request.nlu.tokens при нажатии кнопки ([0e4b806](https://github.com/popstas/yandex-dialogs-client/commit/0e4b806)) 110 | 111 | 112 | 113 | # [1.0.0](https://github.com/popstas/yandex-dialogs-client/compare/v0.9.0...v1.0.0) (2019-01-12) 114 | 115 | 116 | ### Features 117 | 118 | * горячие кнопки для очистки input и истории сообщений (ctrl+C, ctrl+L) ([c6b9f81](https://github.com/popstas/yandex-dialogs-client/commit/c6b9f81)) 119 | 120 | 121 | 122 | # [0.9.0](https://github.com/popstas/yandex-dialogs-client/compare/v0.8.2...v0.9.0) (2019-01-12) 123 | 124 | 125 | ### Bug Fixes 126 | 127 | * убрано скакание ширины при пустом диалоге ([274cfb8](https://github.com/popstas/yandex-dialogs-client/commit/274cfb8)) 128 | 129 | 130 | ### Features 131 | 132 | * возможность удалять вебхуки из списка ([ed79b9e](https://github.com/popstas/yandex-dialogs-client/commit/ed79b9e)) 133 | * добавлен request.nlu ([efb4912](https://github.com/popstas/yandex-dialogs-client/commit/efb4912)) 134 | * передача первого сообщения через ?msg=abc ([695094d](https://github.com/popstas/yandex-dialogs-client/commit/695094d)) 135 | * сохранение истории сообщений между обновлениями страницы ([663ad18](https://github.com/popstas/yandex-dialogs-client/commit/663ad18)) 136 | 137 | 138 | 139 | ## [0.8.2](https://github.com/popstas/yandex-dialogs-client/compare/v0.8.1...v0.8.2) (2018-12-30) 140 | 141 | 142 | ### Features 143 | 144 | * session.skill_id в запросе ([6a35ca3](https://github.com/popstas/yandex-dialogs-client/commit/6a35ca3)), closes [#1](https://github.com/popstas/yandex-dialogs-client/issues/1) 145 | 146 | 147 | 148 | ## [0.8.1](https://github.com/popstas/yandex-dialogs-client/compare/v0.8.0...v0.8.1) (2018-07-14) 149 | 150 | 151 | ### Bug Fixes 152 | 153 | * максимальная задержка уменьшена до 1.5 сек ([16bd9ea](https://github.com/popstas/yandex-dialogs-client/commit/16bd9ea)) 154 | * при неответе навыка тест не считался проваленным ([170265d](https://github.com/popstas/yandex-dialogs-client/commit/170265d)) 155 | 156 | 157 | ### Features 158 | 159 | * настраиваемый timeout ([3872ea1](https://github.com/popstas/yandex-dialogs-client/commit/3872ea1)) 160 | * ограничение кол-ва сообщений в чате (для скорости) ([6a14b89](https://github.com/popstas/yandex-dialogs-client/commit/6a14b89)) 161 | 162 | 163 | 164 | # [0.8.0](https://github.com/popstas/yandex-dialogs-client/compare/v0.7.2...v0.8.0) (2018-07-08) 165 | 166 | 167 | ### Bug Fixes 168 | 169 | * правильная обработка удаления текущего webhookURL ([4a71fd5](https://github.com/popstas/yandex-dialogs-client/commit/4a71fd5)) 170 | * убраны скакания верстки при наведении, на некоторых зумах ([7349d69](https://github.com/popstas/yandex-dialogs-client/commit/7349d69)) 171 | 172 | 173 | ### Features 174 | 175 | * возможность отменить все навыки через "use " ([aee7e1b](https://github.com/popstas/yandex-dialogs-client/commit/aee7e1b)) 176 | * кнопки "повторить" на каждом пройденном тесте ([01da90b](https://github.com/popstas/yandex-dialogs-client/commit/01da90b)) 177 | * переверстаны карточки, меньше рамок и заливок ([c8efd84](https://github.com/popstas/yandex-dialogs-client/commit/c8efd84)) 178 | * статус теста на кнопке цветом ([49ea6eb](https://github.com/popstas/yandex-dialogs-client/commit/49ea6eb)) 179 | * ширина приложения сделана более узкой, 480px ([59f8d5c](https://github.com/popstas/yandex-dialogs-client/commit/59f8d5c)) 180 | 181 | 182 | 183 | ## [0.7.2](https://github.com/popstas/yandex-dialogs-client/compare/v0.7.1...v0.7.2) (2018-07-08) 184 | 185 | 186 | ### Bug Fixes 187 | 188 | * тестилка иногда брала для проверки сообщение клиента ([a7d1c62](https://github.com/popstas/yandex-dialogs-client/commit/a7d1c62)) 189 | * убрана кнопка "все тесты" при отсутствии scenarios.yml ([215bcbf](https://github.com/popstas/yandex-dialogs-client/commit/215bcbf)) 190 | 191 | 192 | ### Features 193 | 194 | * выдавать ошибку, если в течение 2 секунд от навыка не было ответа ([fb012da](https://github.com/popstas/yandex-dialogs-client/commit/fb012da)) 195 | * тесты теперь не выводятся в сообщении, если они выводятся внизу ([2d1ccb9](https://github.com/popstas/yandex-dialogs-client/commit/2d1ccb9)) 196 | 197 | 198 | 199 | ## [0.7.1](https://github.com/popstas/yandex-dialogs-client/compare/v0.7.0...v0.7.1) (2018-07-07) 200 | 201 | 202 | ### Bug Fixes 203 | 204 | * включение микрофона после ответа бота ([0b0f9b5](https://github.com/popstas/yandex-dialogs-client/commit/0b0f9b5)) 205 | 206 | 207 | ### Features 208 | 209 | * прямая ссылка на запуск конкретного навыка ([69ce07f](https://github.com/popstas/yandex-dialogs-client/commit/69ce07f)) 210 | 211 | 212 | 213 | # [0.7.0](https://github.com/popstas/yandex-dialogs-client/compare/v0.6.1...v0.7.0) (2018-07-07) 214 | 215 | 216 | ### Features 217 | 218 | * вывод кнопок тестов на проваленных тестах ([c34b215](https://github.com/popstas/yandex-dialogs-client/commit/c34b215)) 219 | * оформление в стиле Алисы ([2c6d4d3](https://github.com/popstas/yandex-dialogs-client/commit/2c6d4d3)) 220 | * распознавание через yandex speech api ([7b71705](https://github.com/popstas/yandex-dialogs-client/commit/7b71705)) 221 | * тест рандомного ответа one_of ([52ea6a8](https://github.com/popstas/yandex-dialogs-client/commit/52ea6a8)) 222 | 223 | 224 | 225 | ## [0.6.1](https://github.com/popstas/yandex-dialogs-client/compare/v0.6.0...v0.6.1) (2018-07-02) 226 | 227 | 228 | ### Bug Fixes 229 | 230 | * исправлено дублирование внутренних message.id ([71de9a9](https://github.com/popstas/yandex-dialogs-client/commit/71de9a9)) 231 | * снова включен вывод запросов и ответов в консоли ([2113de1](https://github.com/popstas/yandex-dialogs-client/commit/2113de1)) 232 | 233 | 234 | ### Features 235 | 236 | * возможность всегда показывать кнопки тестов ([51f936b](https://github.com/popstas/yandex-dialogs-client/commit/51f936b)) 237 | * возможность скрыть JSON запросов в консоли ([0600cee](https://github.com/popstas/yandex-dialogs-client/commit/0600cee)) 238 | * вывод url навыка в title ([d77cd97](https://github.com/popstas/yandex-dialogs-client/commit/d77cd97)) 239 | 240 | 241 | 242 | # [0.6.0](https://github.com/popstas/yandex-dialogs-client/compare/v0.5.0...v0.6.0) (2018-07-02) 243 | 244 | 245 | ### Bug Fixes 246 | 247 | * новые пользователи не могли получить guid ([d1a6683](https://github.com/popstas/yandex-dialogs-client/commit/d1a6683)) 248 | 249 | 250 | ### Features 251 | 252 | * более удобный вывод тестов ([e6fd86c](https://github.com/popstas/yandex-dialogs-client/commit/e6fd86c)) 253 | * запуск всех тестов одной кнопкой ([5719b37](https://github.com/popstas/yandex-dialogs-client/commit/5719b37)) 254 | * иконка Алисы ([12ccfc6](https://github.com/popstas/yandex-dialogs-client/commit/12ccfc6)) 255 | 256 | 257 | 258 | # [0.5.0](https://github.com/popstas/yandex-dialogs-client/compare/v0.4.1...v0.5.0) (2018-07-01) 259 | 260 | 261 | ### Bug Fixes 262 | 263 | * ошибка SSR рендеринга Slideout ([a3e9192](https://github.com/popstas/yandex-dialogs-client/commit/a3e9192)) 264 | * **css:** кнопки выровнены по краям ([5a1132e](https://github.com/popstas/yandex-dialogs-client/commit/5a1132e)) 265 | 266 | 267 | ### Features 268 | 269 | * анимация голосового ввода ([f90732d](https://github.com/popstas/yandex-dialogs-client/commit/f90732d)) 270 | * вывод в консоль JSON запросов и ответов в удобном формате ([51675ae](https://github.com/popstas/yandex-dialogs-client/commit/51675ae)) 271 | * цветовые обозначения сообщений: info, success, warning, error ([a8d7478](https://github.com/popstas/yandex-dialogs-client/commit/a8d7478)) 272 | 273 | 274 | 275 | ## [0.4.1](https://github.com/popstas/yandex-dialogs-client/compare/v0.4.0...v0.4.1) (2018-07-01) 276 | 277 | 278 | ### Bug Fixes 279 | 280 | * консистентное поведение при выборе хука из последних ([0b0836b](https://github.com/popstas/yandex-dialogs-client/commit/0b0836b)) 281 | 282 | 283 | ### Features 284 | 285 | * выбор из последних отправленных команд стрелками вниз-вверх ([438cafb](https://github.com/popstas/yandex-dialogs-client/commit/438cafb)) 286 | * фокус на поле ввода после каждого ответа бота ([c318de7](https://github.com/popstas/yandex-dialogs-client/commit/c318de7)) 287 | 288 | 289 | 290 | # [0.4.0](https://github.com/popstas/yandex-dialogs-client/compare/v0.3.0...v0.4.0) (2018-07-01) 291 | 292 | 293 | ### Features 294 | 295 | * боковая панель, настройка прокси ([c4f2b96](https://github.com/popstas/yandex-dialogs-client/commit/c4f2b96)) 296 | * выбор из последних URL навыков ([ab90805](https://github.com/popstas/yandex-dialogs-client/commit/ab90805)) 297 | * сценарии тестирования навыка ([6e4f30e](https://github.com/popstas/yandex-dialogs-client/commit/6e4f30e)) 298 | 299 | 300 | 301 | # [0.3.0](https://github.com/popstas/yandex-dialogs-client/compare/v0.2.0...v0.3.0) (2018-06-30) 302 | 303 | 304 | ### Bug Fixes 305 | 306 | * IS_PROXY while docker build ([c0131d5](https://github.com/popstas/yandex-dialogs-client/commit/c0131d5)) 307 | * render response without text ([3a9ca21](https://github.com/popstas/yandex-dialogs-client/commit/3a9ca21)) 308 | 309 | 310 | ### Features 311 | 312 | * session.message_id support ([ed22de8](https://github.com/popstas/yandex-dialogs-client/commit/ed22de8)) 313 | * session.new support ([79d1aea](https://github.com/popstas/yandex-dialogs-client/commit/79d1aea)) 314 | * поддержка response.end_session ([88bfb4a](https://github.com/popstas/yandex-dialogs-client/commit/88bfb4a)) 315 | * сессия теперь сбрасывается при каждом обновлении страницы ([c1023cc](https://github.com/popstas/yandex-dialogs-client/commit/c1023cc)) 316 | 317 | 318 | 319 | # [0.2.0](https://github.com/popstas/yandex-dialogs-client/compare/v0.1.0...v0.2.0) (2018-06-27) 320 | 321 | 322 | ### Features 323 | 324 | * buttons support ([5ea37f5](https://github.com/popstas/yandex-dialogs-client/commit/5ea37f5)) 325 | * IS_PROXY option ([acd76ca](https://github.com/popstas/yandex-dialogs-client/commit/acd76ca)) 326 | * resend message ([f30b25d](https://github.com/popstas/yandex-dialogs-client/commit/f30b25d)) 327 | 328 | 329 | 330 | # [0.1.0](https://github.com/popstas/yandex-dialogs-client/compare/2473aeb...v0.1.0) (2018-06-17) 331 | 332 | 333 | ### Bug Fixes 334 | 335 | * always output outgoing message ([15275ad](https://github.com/popstas/yandex-dialogs-client/commit/15275ad)) 336 | 337 | 338 | ### Features 339 | 340 | * api endpoint for avoid CORS restrictions ([2473aeb](https://github.com/popstas/yandex-dialogs-client/commit/2473aeb)) 341 | * docker support ([1c1d1ef](https://github.com/popstas/yandex-dialogs-client/commit/1c1d1ef)) 342 | 343 | 344 | 345 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:10-alpine 2 | ENV NODE_ENV production 3 | ENV IS_PROXY 1 4 | 5 | RUN apk --update add --no-cache git 6 | 7 | # use changes to package.json to force Docker not to use the cache 8 | # when we change our application's nodejs dependencies: 9 | COPY package.json /tmp/package.json 10 | RUN cd /tmp && npm install 11 | RUN mkdir -p /app && cp -a /tmp/node_modules /app/ 12 | 13 | WORKDIR /app 14 | COPY . . 15 | RUN npm run build 16 | 17 | ENV HOST 0.0.0.0 18 | EXPOSE 3000 19 | CMD npm start 20 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | Copyright 2018 Stanislav Popov 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # yandex-dialogs-client 2 | 3 | Клиент для работы с навыками Яндекс.Диалогов Алисы локально. 4 | 5 |  6 | 7 | [Блогпост](https://blog.popstas.ru/blog/2020/04/14/yandex-dialogs/#yandex-dialogs-client---инструмент-для-тестирования-навыков). 8 | 9 | Идея была в том, чтобы написать реализацию, умеющую работать с навыками Яндекс.Диалогов без изменения. 10 | Клиент использует тот же протокол, что и Яндекс.Диалоги с ограничениями (см. ниже). 11 | 12 | ## Зачем это может быть нужно: 13 | - Тестирование навыков 14 | - Запуск навыков локально 15 | - Автоматическое тестирование навыков по сценариям 16 | 17 | ## Возможности 18 | - Сохранение истории последних сообщений (по умолчанию 20) 19 | - Выбор из последних отправленных команд стрелками вниз-вверх 20 | - Возможность переотправить выбранное сообщение 21 | - Вывод в консоль JSON запросов и ответов в удобном формате 22 | - Вывод кнопок, отправка запросов с `type = 'ButtonPressed'` 23 | - Подключение навыка по Webhook URL 24 | - Выбор из последних URL навыков 25 | - Прямая ссылка на запуск конкретного навыка и отправку первого сообщения 26 | - Возможность обращаться к навыку из браузера или через сервер, для обхода [CORS](#CORS) 27 | - Сценарии тестирования навыков 28 | - Прогон всех тестовых сценариев одной кнопкой 29 | - Вывод проваленных тестов с кнопками повтора 30 | - Горячие кнопки для очистки input и истории сообщений (Ctrl+C, Ctrl+L) 31 | - Голосовой ввод через браузерный Speech API или через Яндекс SpeechKit 32 | 33 | ## Ограничения, особенности 34 | - `session.session_id` сбрасывается при каждом обновлении страницы или при завершении диалога из навыка 35 | - `session.user_id` хранится в localStorage, то есть каждый браузер считается новым юзером 36 | - Изображения в ответе навыка не поддерживаются 37 | - Голоса нет 38 | 39 | ## Использование 40 | Проще всего зайти на https://dialogs.popstas.ru (это приложение хостится на github pages, серверной части нет), 41 | ввести `use http://webhook.url` (URL вашего навыка) и пользоваться. URL запоминается в localStorage. 42 | 43 | Данные отправляются только в Speech API и на URL, который вы укажете, но приложение может быть сломано в процессе разработки, 44 | поэтому надежнее скачать и запустить у себя. 45 | 46 | ### Прямая ссылка на навык 47 | Вы можете указывать прямые ссылки на навыки через GET параметр `use`, `?use=http://webhook.url. 48 | 49 | Например, это будет работать: https://dialogs.home.popstas.ru/?use=https://dialogflower.com/webhook/d4dbb4f93bed4e5e989107d679e20083 50 | 51 | Так удобно давать друзьям потестить свой навык. 52 | 53 | ### Отправка сообщения боту по F5 или Ctrl+R 54 | Бывает, что удобнее тестить навык по одной фразе. Клиент поддерживает повтор любой фразы и повтор последней фразы по нажатию стрелки вверх, но Alt+Tab, Ctrl+R может быть быстрее, т.к. не надо ставить фокус на input и перемещать руку в правую часть. 55 | 56 | Можно указать сообщение в GET параметре msg, например, https://dialogs.home.popstas.ru/?use=https://dialogflower.com/webhook/d4dbb4f93bed4e5e989107d679e20083&msg=привет 57 | 58 | ### Сценарии тестирования навыков 59 | В приложении встроена простая система тестирования: ввод последовательностей фраз и проверка текстов ответов. 60 | 61 | #### Как это работает: 62 | - При подключении навыка приложение пытается получить /scenarios.yml GET запросом 63 | - Если ему это удается, выводятся кнопки для запуска тестов 64 | - При нажатии на кнопку отправляются реплики к навыку и проверяются ответы 65 | 66 | [Пример scenarios.yml](https://github.com/popstas/yandex-dialogs-whatis/blob/master/static/scenarios.yml). 67 | 68 |  69 | 70 | ### Запуск всех сценариев 71 | Вы можете запустить все имеющиеся тесты одной кнопкой. 72 | 73 | Тесты выполняются в том порядке, в котором перечислены в yml: 74 | 75 |  76 | 77 | 78 | ### Повтор проваленных тестов 79 | Когда вы пишете новую команду для навыка, вам не нужно прогонять все тесты после каждого изменения кода. 80 | Вывод кнопок тестов на проваленных тестах дает возможность быстро тестировать разрабатываемую фичу: 81 | 82 | - Запустить все тесты 83 | - Увидеть проваленные 84 | - Запустить только проваленные 85 | - Запускать их одной кнопкой, пока проваленных не останется 86 | 87 |  88 | 89 | ### CORS 90 | У приложения на https://dialogs.popstas.ru есть ограничение: так как у него нет серверной части, 91 | запросы к навыку шлются прямо из браузера, из-за этого все общедоступные источники навыков, вроде 92 | [verter](https://www.verter.online/), 93 | [tobotornot](http://alisa.tobotornot.com/) и 94 | [dialogflower](https://dialogflower.com/) 95 | не будут отвечать вам, т.к. будут действовать правила [CORS](https://developer.mozilla.org/ru/docs/Web/HTTP/CORS). 96 | Вы можете использовать его только с навыком, который отдает разрешающие заголовки. 97 | 98 | Это можно обойти, если использовать версию с серверной частью, она перенаправляет через себя все запросы к навыкам, 99 | обходя ограничения. Вы можете собрать свою версию сами или зайти на https://dialogs.home.popstas.ru, 100 | она с серверной частью, но только для внешних запросов к навыкам, все данные по-прежнему хранятся в localStorage. 101 | 102 | При сборке за это отвечает переменная окружения `IS_PROXY`, если она равна единице, `1`, то запросы будут идти через сервер. 103 | Но если вы делаете `npm run generate`, то переменную надо ставить на сборку. 104 | 105 | Если приложение изначально запущено с `IS_PROXY`, то в интерфейсе вы можете переключать режим на запросы напрямую. 106 | 107 | 108 | ## Установка 109 | 110 | #### Запуск через Docker 111 | 112 | ``` bash 113 | docker run --name yandex-dialogs-client -d -p 3000:3000 popstas/yandex-dialogs-client 114 | ``` 115 | 116 | #### Установка через npm 117 | 118 | ``` bash 119 | npm install -g yandex-dialogs-client 120 | 121 | # launch: 122 | yandex-dialogs-client 123 | ``` 124 | 125 | #### Запуск через Docker compose 126 | ``` bash 127 | git clone https://github.com/popstas/yandex-dialogs-client.git 128 | cd yandex-dialogs-client 129 | cp .env.sample .env 130 | ``` 131 | 132 | Нужно скопировать `.env.sample` в `.env` и заполнить. После этого собрать и поднять: 133 | 134 | ``` bash 135 | docker-compose build 136 | docker-compose up -d 137 | ``` 138 | 139 | #### Сборка из исходников 140 | 141 | ``` bash 142 | git clone https://github.com/popstas/yandex-dialogs-client.git 143 | cd yandex-dialogs-client 144 | npm install 145 | npm run build 146 | npm run start 147 | ``` 148 | 149 | #### Выбор голосового распознавания от Яндекс 150 | По умолчанию работает распознавание через SpeechAPI, встроенный в браузер (Chome). 151 | Оно работает в офлайне, но качество у него хуже, чем у Яндекса. 152 | 153 | Можно включить Яндекс через переменные окружения: 154 | 155 | - `SPEECH_ENGINE` - выбор движка, `browser` или `yandex` 156 | - `YANDEX_WEBSPEECH_KEY` - API ключ 157 | -------------------------------------------------------------------------------- /api/index.js: -------------------------------------------------------------------------------- 1 | const axios = require('axios'); 2 | 3 | module.exports = async function(req, res) { 4 | let { url, post } = req.body; 5 | let answer = await axios.post(url, post); 6 | res.writeHead(200, { 'Content-Type': 'application/json' }); 7 | res.end(JSON.stringify(answer.data)); 8 | }; 9 | -------------------------------------------------------------------------------- /assets/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popstas/yandex-dialogs-client/d000c9bdf97e7579c8183c30dc227724f4670d6f/assets/demo.gif -------------------------------------------------------------------------------- /assets/run_all_tests.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popstas/yandex-dialogs-client/d000c9bdf97e7579c8183c30dc227724f4670d6f/assets/run_all_tests.gif -------------------------------------------------------------------------------- /assets/run_failed_tests.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popstas/yandex-dialogs-client/d000c9bdf97e7579c8183c30dc227724f4670d6f/assets/run_failed_tests.gif -------------------------------------------------------------------------------- /assets/scenarios.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popstas/yandex-dialogs-client/d000c9bdf97e7579c8183c30dc227724f4670d6f/assets/scenarios.gif -------------------------------------------------------------------------------- /bin/nuxt-start.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | process.chdir(__dirname + '/..'); 3 | require('@nuxt/cli').run(['start']); 4 | -------------------------------------------------------------------------------- /components/CardItem.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 22 | 23 | 85 | -------------------------------------------------------------------------------- /components/Dialog.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 21 | 26 | 27 | 28 | 29 | 36 | 37 | 38 | 39 | 69 | 70 | 244 | -------------------------------------------------------------------------------- /components/Message.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ message.author }} {{ message.date }} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 23 | 24 | 25 | 26 | 27 | 121 | 122 | 152 | -------------------------------------------------------------------------------- /components/MessageButton.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 26 | 27 | 92 | -------------------------------------------------------------------------------- /components/SearchInput.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 16 | 17 | 28 | 29 | 30 | 31 | 59 | 60 | 391 | -------------------------------------------------------------------------------- /components/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 7 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | 22 | 23 | 24 | 25 | 26 | 31 | 32 | 33 | 34 | 35 | 36 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | Макс. кол-во сообщений в чате 48 | 49 | 50 | 51 | Макс. кол-во сообщений при открытии страницы 52 | 53 | 54 | 55 | Макс. время ответа 56 | 57 | 58 | 59 | 60 | Последние URL навыков: 61 | 62 | 63 | 64 | x 65 | 66 | 67 | 68 | 69 | 70 | 73 | 74 | {{ $store.state.name }} {{ $store.state.version }} 75 | 76 | 77 | 78 | 79 | 80 | Справка 81 | 82 | 83 | Changelog 84 | 85 | 86 | 90 | Статья 91 | 92 | 93 | 94 | 💲 Поблагодарить автора 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 141 | 142 | 249 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2.1' 2 | 3 | services: 4 | yandex-dialogs-client: 5 | image: popstas/yandex-dialogs-client 6 | build: . 7 | restart: always 8 | environment: 9 | BASE_URL: ${BASE_URL} 10 | IS_PROXY: ${IS_PROXY} 11 | SPEECH_ENGINE: ${SPEECH_ENGINE} 12 | YANDEX_WEBSPEECH_KEY: ${YANDEX_WEBSPEECH_KEY} 13 | ports: 14 | - 3856:3000 15 | -------------------------------------------------------------------------------- /layouts/default.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 13 | 14 | 15 | ☰ 16 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 128 | 129 | 160 | -------------------------------------------------------------------------------- /nuxt.config.js: -------------------------------------------------------------------------------- 1 | const nodeExternals = require('webpack-node-externals'); 2 | const bodyParser = require('body-parser'); 3 | 4 | if(process.env.IS_PROXY === undefined) process.env.IS_PROXY = '0'; 5 | 6 | module.exports = { 7 | // mode: 'spa', 8 | env: { 9 | baseUrl: process.env.BASE_URL || 'http://localhost:3000', 10 | production: process.env.NODE_ENV === "production", 11 | isProxy: process.env.IS_PROXY ? (['false', '0', ''].includes(process.env.IS_PROXY) ? false : true) : true, 12 | speechEngine: process.env.SPEECH_ENGINE || 'browser', 13 | yandexAPIKey: process.env.YANDEX_WEBSPEECH_KEY 14 | }, 15 | 16 | modules: [ 17 | '@nuxtjs/axios', 18 | '@nuxtjs/pwa' 19 | ], 20 | 21 | css: [ 22 | 'element-ui/lib/theme-chalk/index.css', 23 | 'element-ui/lib/theme-chalk/display.css' 24 | ], 25 | 26 | plugins: [ 27 | '~/plugins/element-ui', 28 | '~/plugins/vue-awesome', 29 | { src: '~/plugins/vue-slideout', ssr: false }, 30 | { src: '~/plugins/localStorage.js', ssr: false } 31 | ], 32 | 33 | serverMiddleware: [ 34 | bodyParser.json(), 35 | { path: '/api/request', handler: '~/api/index.js' }, 36 | ], 37 | 38 | axios: { 39 | baseURL: process.env.baseUrl 40 | }, 41 | 42 | /* 43 | ** Headers of the page 44 | */ 45 | head: { 46 | title: 'yandex-dialogs-client', 47 | meta: [ 48 | { charset: 'utf-8' }, 49 | { name: 'viewport', content: 'width=device-width, initial-scale=1' }, 50 | { hid: 'description', name: 'description', content: 'Клиент для работы с навыками Яндекс.Диалогов Алисы локально' } 51 | ], 52 | link: [ 53 | { rel: 'icon', type: 'image/x-icon', href: '/favicon.ico' } 54 | ], 55 | 56 | script: [ 57 | { src: 'https://webasr.yandex.net/jsapi/v1/webspeechkit.js' } 58 | ] 59 | }, 60 | /* 61 | ** Customize the progress bar color 62 | */ 63 | loading: { color: '#3B8070' }, 64 | /* 65 | ** Build configuration 66 | */ 67 | build: { 68 | transpile: [/^vue-awesome/], 69 | /* 70 | ** Run ESLint on save 71 | */ 72 | extend(config, ctx) { 73 | if (ctx.isDev && process.client) { 74 | config.module.rules.push({ 75 | enforce: 'pre', 76 | test: /\.(js|vue)$/, 77 | loader: 'eslint-loader', 78 | exclude: /(node_modules)/ 79 | }) 80 | } 81 | } 82 | }, 83 | 84 | hooks: { 85 | listen() { 86 | console.log('isProxy: ' + module.exports.env.isProxy); 87 | } 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "bin": { 3 | "yandex-dialogs-client": "bin/nuxt-start.js" 4 | }, 5 | "files": [ 6 | "api", 7 | "bin", 8 | ".nuxt", 9 | "plugins", 10 | "src", 11 | "static", 12 | "nuxt.config.js" 13 | ], 14 | "scripts": { 15 | "dev": "cross-env HOST=0.0.0.0 PORT=3003 nuxt", 16 | "dev-proxy": "cross-env IS_PROXY=1 HOST=0.0.0.0 PORT=3003 nuxt", 17 | "dev-yandex": "cross-env IS_PROXY=1 SPEECH_ENGINE=yandex HOST=0.0.0.0 PORT=3003 nuxt", 18 | "dev-debug": "node --inspect node_modules/.bin/nuxt", 19 | "build": "nuxt build", 20 | "start": "nuxt start", 21 | "generate": "nuxt generate", 22 | "version": "npm run changelog && git add CHANGELOG.md", 23 | "postversion": "git push && npm run release", 24 | "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", 25 | "release": "conventional-github-releaser -p angular && bash scripts/deploy.sh && npm run build && npm publish", 26 | "deploy": "bash scripts/deploy.sh", 27 | "lint": "eslint --ext .js,.vue --ignore-path .gitignore .", 28 | "precommit": "npm run lint" 29 | }, 30 | "dependencies": { 31 | "@nuxt/cli": "^2.4.5", 32 | "@nuxtjs/axios": "^5.3.6", 33 | "@nuxtjs/pwa": "^2.6.0", 34 | "body-parser": "^1.18.3", 35 | "element-ui": "^2.4.11", 36 | "es6-promise": "^4.2.5", 37 | "js-yaml": "^3.12.1", 38 | "node-sass": "^4.11.0", 39 | "nuxt": "^2.3.4", 40 | "sass-loader": "^7.1.0", 41 | "style-loader": "^0.21.0", 42 | "vue-awesome": "^3.3.1", 43 | "vue-slideout": "^1.7.0", 44 | "vue-style-loader": "^4.1.2", 45 | "vuex-persistedstate": "^2.5.4" 46 | }, 47 | "devDependencies": { 48 | "babel-eslint": "^10.0.1", 49 | "conventional-changelog-cli": "^2.0.22", 50 | "conventional-github-releaser": "^3.1.3", 51 | "eslint": "^5.16.0", 52 | "eslint-config-standard": ">=12.0.0", 53 | "eslint-friendly-formatter": "^3.0.0", 54 | "eslint-loader": "^2.1.2", 55 | "eslint-plugin-import": "^2.17.3", 56 | "eslint-plugin-jest": "^22.6.4", 57 | "eslint-plugin-node": "^9.1.0", 58 | "eslint-plugin-promise": "^4.1.1", 59 | "eslint-plugin-standard": ">=4.0.0", 60 | "eslint-plugin-vue": "^5.2.2" 61 | }, 62 | "name": "yandex-dialogs-client", 63 | "version": "1.2.0", 64 | "description": "Клиент для работы с навыками Яндекс.Диалогов Алисы локально", 65 | "repository": { 66 | "type": "git", 67 | "url": "git+https://github.com/popstas/yandex-dialogs-client.git" 68 | }, 69 | "keywords": [ 70 | "speech", 71 | "vue" 72 | ], 73 | "author": "Stanislav Popov ", 74 | "license": "MIT", 75 | "bugs": { 76 | "url": "https://github.com/popstas/yandex-dialogs-client/issues" 77 | }, 78 | "homepage": "https://github.com/popstas/yandex-dialogs-client" 79 | } 80 | -------------------------------------------------------------------------------- /pages/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 7 | 8 | 16 | -------------------------------------------------------------------------------- /plugins/element-ui.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Element from 'element-ui'; 3 | import locale from 'element-ui/lib/locale/lang/en'; 4 | 5 | export default () => { 6 | Vue.use(Element, { locale }); 7 | }; 8 | -------------------------------------------------------------------------------- /plugins/localStorage.js: -------------------------------------------------------------------------------- 1 | import createPersistedState from 'vuex-persistedstate'; 2 | 3 | export default ({ store }) => { 4 | createPersistedState({ 5 | paths: ['settings', 'messages'] 6 | })(store); 7 | }; 8 | -------------------------------------------------------------------------------- /plugins/vue-awesome.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Icon from 'vue-awesome/components/Icon.vue'; 3 | 4 | Vue.component('icon', Icon); 5 | -------------------------------------------------------------------------------- /plugins/vue-slideout.js: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Slideout from 'vue-slideout'; 3 | Vue.component('Slideout', Slideout); 4 | -------------------------------------------------------------------------------- /scripts/deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | rm -rf dist 5 | IS_PROXY=0 npm run generate 6 | cd dist 7 | git init 8 | git add -A 9 | git commit -m "deploy" 10 | git remote add origin git@github.com:popstas/yandex-dialogs-client.git 11 | git push --force origin master:gh-pages 12 | cd .. 13 | -------------------------------------------------------------------------------- /static/CNAME: -------------------------------------------------------------------------------- 1 | dialogs.popstas.ru 2 | -------------------------------------------------------------------------------- /static/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popstas/yandex-dialogs-client/d000c9bdf97e7579c8183c30dc227724f4670d6f/static/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popstas/yandex-dialogs-client/d000c9bdf97e7579c8183c30dc227724f4670d6f/static/android-chrome-512x512.png -------------------------------------------------------------------------------- /static/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popstas/yandex-dialogs-client/d000c9bdf97e7579c8183c30dc227724f4670d6f/static/apple-touch-icon.png -------------------------------------------------------------------------------- /static/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffffff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /static/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popstas/yandex-dialogs-client/d000c9bdf97e7579c8183c30dc227724f4670d6f/static/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popstas/yandex-dialogs-client/d000c9bdf97e7579c8183c30dc227724f4670d6f/static/favicon-32x32.png -------------------------------------------------------------------------------- /static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popstas/yandex-dialogs-client/d000c9bdf97e7579c8183c30dc227724f4670d6f/static/favicon.ico -------------------------------------------------------------------------------- /static/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popstas/yandex-dialogs-client/d000c9bdf97e7579c8183c30dc227724f4670d6f/static/icon.png -------------------------------------------------------------------------------- /static/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/popstas/yandex-dialogs-client/d000c9bdf97e7579c8183c30dc227724f4670d6f/static/mstile-150x150.png -------------------------------------------------------------------------------- /static/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /store/index.js: -------------------------------------------------------------------------------- 1 | import pjson from '~/package.json'; 2 | import yaml from 'js-yaml'; 3 | 4 | export const SET_ANSWERS = 'SET_ANSWERS'; 5 | export const ALICE_REQUEST = 'ALICE_REQUEST'; 6 | export const SET_USER_ID = 'SET_USER_ID'; 7 | export const SET_SESSION_ID = 'SET_SESSION_ID'; 8 | export const SET_SESSION_NEW = 'SET_SESSION_NEW'; 9 | export const SET_MESSAGE_ID = 'SET_MESSAGE_ID'; 10 | export const ADD_MESSAGE = 'ADD_MESSAGE'; 11 | export const SET_MESSAGES = 'SET_MESSAGES'; 12 | export const LIMIT_MESSAGES = 'LIMIT_MESSAGES'; 13 | export const SET_WEBHOOK_URL = 'SET_WEBHOOK_URL'; 14 | export const SET_WEBHOOK_URLS = 'SET_WEBHOOK_URLS'; 15 | export const ADD_WEBHOOK_URL = 'ADD_WEBHOOK_URL'; 16 | export const REMOVE_WEBHOOK_URL = 'REMOVE_WEBHOOK_URL'; 17 | export const SESSION_START = 'SESSION_START'; 18 | export const SESSION_END = 'SESSION_END'; 19 | export const RUN_TEST = 'RUN_TEST'; 20 | export const SET_TESTS = 'SET_TESTS'; 21 | export const SET_TEST_SUCCESS = 'SET_TEST_SUCCESS'; 22 | 23 | export const AUTHOR_NAME = 'Я'; 24 | 25 | const expandedLog = (() => { 26 | var MAX_DEPTH = 1; 27 | 28 | return (item, depth) => { 29 | depth = depth || 0; 30 | 31 | if (depth > MAX_DEPTH) { 32 | console.log(item); 33 | return; 34 | } 35 | 36 | if (typeof item === 'object') { 37 | for (let key in item) { 38 | let value = item[key]; 39 | let msg = ''; 40 | if (value.request && value.request.command) msg = value.request.command; 41 | if (value.response && value.response.text) msg = value.response.text; 42 | 43 | let groupName = `${key}: ${msg}`; 44 | if (depth == 0) groupName = `[${new Date().toLocaleTimeString()}] ${groupName}`; 45 | console.group(groupName); 46 | 47 | expandedLog(value, depth + 1); 48 | console.groupEnd(); 49 | } 50 | } else { 51 | console.log(item); 52 | } 53 | }; 54 | })(); 55 | 56 | const string2Hex = strIn => { 57 | var str = ''; 58 | for (let i = 0; i < strIn.length; i++) { 59 | str += strIn[i].charCodeAt(0).toString(16); 60 | } 61 | return str; 62 | }; 63 | 64 | export const state = () => ({ 65 | // data 66 | messages: [], 67 | 68 | // constants 69 | name: pjson.name, 70 | version: pjson.version, 71 | description: pjson.description, 72 | homepage: pjson.homepage, 73 | 74 | // app state 75 | speechEngine: process.env.speechEngine, 76 | yandexAPIKey: process.env.yandexAPIKey, 77 | skillId: '', 78 | userId: '', 79 | sessionId: '', 80 | sessionNew: true, 81 | messageId: 1, 82 | webhookURL: '', 83 | webhookURLs: [], 84 | tests: [] 85 | }); 86 | 87 | export const computed = { 88 | settings: state => state.settings 89 | }; 90 | 91 | export const mutations = { 92 | [SET_USER_ID](state, userId) { 93 | state.userId = userId; 94 | }, 95 | 96 | [SET_SESSION_ID](state, sessionId) { 97 | state.sessionId = sessionId; 98 | }, 99 | 100 | [SET_SESSION_NEW](state, sessionNew) { 101 | state.sessionNew = sessionNew; 102 | }, 103 | 104 | [SET_MESSAGE_ID](state, messageId) { 105 | state.messageId = messageId; 106 | }, 107 | 108 | [SET_TESTS](state, tests) { 109 | state.tests = tests; 110 | }, 111 | 112 | [SET_TEST_SUCCESS](state, { name, success }) { 113 | const found = state.tests.find(test => test.name === name); 114 | if (found) found.success = success; 115 | }, 116 | 117 | [SET_WEBHOOK_URL](state, webhookURL) { 118 | state.webhookURL = webhookURL; 119 | localStorage.setItem('webhookURL', webhookURL); 120 | state.skillId = string2Hex(webhookURL); 121 | }, 122 | 123 | [SET_WEBHOOK_URLS](state, webhookURLs) { 124 | if (!webhookURLs) webhookURLs = []; 125 | state.webhookURLs = webhookURLs; 126 | }, 127 | 128 | // last webhookURLs 129 | [ADD_WEBHOOK_URL](state, webhookURL) { 130 | if (state.webhookURLs.indexOf(webhookURL) == -1) { 131 | state.webhookURLs.push(webhookURL); 132 | localStorage.setItem('webhookURLs', JSON.stringify(state.webhookURLs)); 133 | } 134 | }, 135 | 136 | [REMOVE_WEBHOOK_URL](state, webhookURL) { 137 | const index = state.webhookURLs.indexOf(webhookURL); 138 | if (index == -1) return; 139 | 140 | state.webhookURLs.splice(index, 1); 141 | localStorage.setItem('webhookURLs', JSON.stringify(state.webhookURLs)); 142 | }, 143 | 144 | [ADD_MESSAGE](state, message) { 145 | message = { 146 | ...message, 147 | ...{ 148 | id: window.performance.now(), 149 | date: new Date().toTimeString().split(' ')[0] 150 | } 151 | }; 152 | state.messages.push(message); 153 | }, 154 | 155 | [SET_MESSAGES](state, messages) { 156 | state.messages = messages; 157 | } 158 | }; 159 | 160 | export const getters = { 161 | // используется в Dialog.vue 162 | randomGuid() { 163 | const S4 = function() { 164 | return (((1 + Math.random()) * 0x10000) | 0).toString(16).substring(1); 165 | }; 166 | return S4() + S4() + '-' + S4() + '-' + S4() + '-' + S4() + '-' + S4() + S4() + S4(); 167 | }, 168 | 169 | lastBotMessage(state) { 170 | for (let i = state.messages.length - 1; i >= 0; i--) { 171 | if (state.messages[i].author == 'Робот') return state.messages[i]; 172 | } 173 | } 174 | }; 175 | 176 | export const actions = { 177 | async [ALICE_REQUEST]({ dispatch, commit, state }, command) { 178 | const offset = new Date().getTimezoneOffset() / 60; 179 | const timezone = 'GMT' + (offset < 0 ? '+' : '-') + Math.abs(offset); 180 | const userAgent = 'popstas/yandex-dialogs-client/' + state.version; 181 | 182 | let requestOpts = { 183 | type: 'SimpleUtterance', 184 | payload: {}, 185 | nlu: { 186 | tokens: [], 187 | entities: [] 188 | }, 189 | markup: { 190 | // "dangerous_context": true 191 | } 192 | }; 193 | 194 | if (typeof command === 'string') { 195 | requestOpts = { 196 | ...requestOpts, 197 | ...{ 198 | command: command, 199 | original_utterance: command 200 | } 201 | }; 202 | } else { 203 | requestOpts = { ...requestOpts, ...command }; 204 | requestOpts.original_utterance = requestOpts.command; 205 | } 206 | 207 | requestOpts.nlu.tokens = requestOpts.command.split(' '); 208 | 209 | const data = { 210 | meta: { 211 | locale: 'ru-RU', 212 | timezone: timezone, 213 | client_id: userAgent, 214 | interfaces: {} 215 | }, 216 | request: requestOpts, 217 | session: { 218 | message_id: state.messageId, 219 | new: state.sessionNew, 220 | session_id: state.sessionId, 221 | skill_id: state.skillId, 222 | user_id: state.userId 223 | }, 224 | version: '1.0' 225 | }; 226 | 227 | if(state.settings.isScreen) { 228 | data.meta.interfaces.screen = {}; 229 | } 230 | 231 | if (state.sessionNew) { 232 | commit(SET_SESSION_NEW, false); 233 | } 234 | commit(SET_MESSAGE_ID, state.messageId + 1); 235 | 236 | const axiosData = { post: data, url: state.webhookURL }; 237 | 238 | try { 239 | if (state.webhookURL) { 240 | let responseData; 241 | if (state.settings.isConsoleRequests) { 242 | expandedLog({ request: data }); 243 | console.log('\n'); 244 | } 245 | if (state.settings.isProxy) { 246 | responseData = await this.$axios.$post('/api/request', axiosData, { 247 | timeout: state.settings.timeout 248 | }); 249 | } else { 250 | responseData = await this.$axios.$post(state.webhookURL, data, { 251 | timeout: state.settings.timeout 252 | }); 253 | } 254 | if (state.settings.isConsoleRequests) { 255 | expandedLog({ response: responseData }); 256 | console.log('\n\n\n\n\n'); 257 | } 258 | 259 | commit(ADD_MESSAGE, { 260 | text: responseData.response.text, 261 | card: responseData.response.card || { header: {}, items: [] }, 262 | buttons: responseData.response.buttons, 263 | end_session: responseData.response.end_session, 264 | author: 'Робот', 265 | class: 'answer' 266 | }); 267 | 268 | if (responseData.response.end_session) { 269 | dispatch(SESSION_END); 270 | } 271 | } else { 272 | commit(ADD_MESSAGE, { 273 | text: 'Не указан адрес навыка, пожалуйста, введите его так: use https://localhost:1234', 274 | author: 'Клиент', 275 | class: 'warning' 276 | }); 277 | } 278 | } catch (err) { 279 | const textPost = err.message.match(/timeout/) ? `не ответил за ${state.settings.timeout} мс` : 'см. консоль'; 280 | commit(ADD_MESSAGE, { 281 | text: `Ошибка запроса к ${state.webhookURL} (${textPost})`, 282 | author: '', 283 | class: 'error' 284 | }); 285 | console.error(err); 286 | return false; 287 | } 288 | return true; 289 | }, 290 | 291 | async [SET_WEBHOOK_URL]({ dispatch, commit, state }, url) { 292 | if (!url || url == 'null') { 293 | url = ''; 294 | } 295 | commit(SET_WEBHOOK_URL, url); 296 | commit(ADD_WEBHOOK_URL, url); 297 | if (url) { 298 | commit(ADD_MESSAGE, { 299 | text: 300 | 'Используется навык по адресу ' + 301 | url + 302 | (state.settings.isProxy ? ', через прокси' : ', без прокси'), 303 | author: 'Клиент', 304 | class: 'info' 305 | }); 306 | } 307 | dispatch(ALICE_REQUEST, ''); 308 | 309 | // scenarios.yml 310 | if (state.settings.isScenarios) { 311 | try { 312 | const responseData = await this.$axios.$get(state.webhookURL + '/scenarios.yml'); 313 | const doc = yaml.safeLoad(responseData); 314 | 315 | let tests = []; 316 | let buttons = []; 317 | for (let name in doc) { 318 | const dialog = { 319 | name: name, 320 | messages: doc[name] 321 | }; 322 | tests.push(dialog); 323 | buttons.push({ 324 | title: name, 325 | payload: JSON.stringify({ scenarios_test: [dialog] }) 326 | }); 327 | } 328 | commit(SET_TESTS, tests); 329 | 330 | buttons.push({ 331 | title: 'все тесты', 332 | payload: JSON.stringify({ scenarios_test: tests }) 333 | }); 334 | 335 | commit(ADD_MESSAGE, { 336 | text: 337 | 'У навыка есть scenarios.yml, в нем есть следующие сценарии (' + 338 | state.tests.length + 339 | '):', 340 | buttons: state.settings.isBottomTests ? [] : buttons, 341 | author: 'Клиент', 342 | class: 'info' 343 | }); 344 | } catch (err) { 345 | //console.error(err); 346 | // it's normal 347 | if (typeof err == 'object' && err.name == 'YAMLException') { 348 | console.log(err); 349 | commit(ADD_MESSAGE, { 350 | text: 'Ошибка в scenarios.yml: ' + err.message, 351 | buttons: [], 352 | author: 'Клиент', 353 | class: 'error' 354 | }); 355 | } 356 | } 357 | } 358 | }, 359 | 360 | [SESSION_START]({ commit, getters }) { 361 | commit(SET_SESSION_ID, getters.randomGuid); 362 | commit(SET_SESSION_NEW, true); 363 | commit(SET_MESSAGE_ID, 1); 364 | }, 365 | 366 | [SESSION_END]({ dispatch, commit }) { 367 | dispatch(SESSION_START); 368 | commit(ADD_MESSAGE, { 369 | text: 'Сессия закончена', 370 | author: 'Клиент', 371 | class: 'info' 372 | }); 373 | }, 374 | 375 | [LIMIT_MESSAGES]({ state, commit }) { 376 | if (state.messages.length > state.settings.messageLimit) { 377 | const messages = state.messages.slice(state.messages.length - state.settings.messageLimit); 378 | commit(SET_MESSAGES, messages); 379 | // state.messages.shift(); 380 | } 381 | }, 382 | 383 | async [RUN_TEST]({ dispatch, getters, commit }, dialogs) { 384 | let allFailedTests = []; 385 | const verbose = false; 386 | 387 | // clean buttons success colors 388 | dialogs.forEach(dialog => { 389 | commit(SET_TEST_SUCCESS, { name: dialog.name, success: null }); 390 | }); 391 | 392 | // test suites (one dialog - one button) 393 | for (let d in dialogs) { 394 | const dialog = dialogs[d]; 395 | const rerunButton = { 396 | title: `повторить "${dialog.name}"`, 397 | payload: JSON.stringify({ scenarios_test: [dialog] }) 398 | }; 399 | commit(ADD_MESSAGE, { 400 | text: `Тест: ${dialog.name}`, 401 | author: 'Клиент', 402 | class: 'info' 403 | }); 404 | 405 | let isDialogErrors = false; 406 | let isUser = true; 407 | 408 | // test steps 409 | for (let i in dialog.messages) { 410 | const message = dialog.messages[i]; 411 | 412 | // send user message 413 | if (isUser) { 414 | commit(ADD_MESSAGE, { 415 | text: message, 416 | author: AUTHOR_NAME 417 | }); 418 | const result = await dispatch(ALICE_REQUEST, message); 419 | if (!result) { 420 | isDialogErrors = true; 421 | commit(ADD_MESSAGE, { 422 | text: `Тест не пройден`, 423 | buttons: [rerunButton], 424 | author: 'Клиент', 425 | class: 'error' 426 | }); 427 | break; 428 | } 429 | isUser = !isUser; 430 | continue; 431 | } 432 | isUser = !isUser; 433 | 434 | // check bot's answer 435 | let msg = getters.lastBotMessage; 436 | 437 | // simple equals string 438 | if (typeof message === 'string') { 439 | if (verbose) console.log(`test ${msg.text} == ${message}`); 440 | if (msg.text != message) { 441 | isDialogErrors = true; 442 | commit(ADD_MESSAGE, { 443 | text: `Тест не пройден:\nотвечено: ${msg.text}\nожидалось: ${message}`, 444 | buttons: [rerunButton], 445 | author: 'Клиент', 446 | class: 'error' 447 | }); 448 | break; // end test 449 | } 450 | } 451 | 452 | // message tests 453 | if (typeof message === 'object') { 454 | if (!message.tests) { 455 | isDialogErrors = true; 456 | commit(ADD_MESSAGE, { 457 | text: `Тест не пройден: в объекте ` + JSON.stringify(message) + 'нет поля tests', 458 | buttons: [rerunButton], 459 | author: 'Клиент', 460 | class: 'error' 461 | }); 462 | break; 463 | } 464 | 465 | let messageErrors = []; 466 | message.tests.forEach(testmessage => { 467 | let testType = Object.keys(testmessage)[0]; 468 | let testVal = testmessage[testType]; 469 | if (verbose) console.log(`test ${testType} ${testVal}`); 470 | // contains 471 | if (testType == 'contains' && !msg.text.includes(testVal)) { 472 | messageErrors.push(`ответ не содержит "${testVal}"`); 473 | return; 474 | } 475 | 476 | // not contains 477 | if (testType == 'not_contains' && msg.text.includes(testVal)) { 478 | messageErrors.push(`ответ содержит "${testVal}", но не должен`); 479 | return; 480 | } 481 | 482 | // not contains 483 | if (testType == 'one_of' && testVal.indexOf(msg.text) == -1) { 484 | messageErrors.push('не соответствует ни одному из ответов: ' + testVal.join(', ')); 485 | return; 486 | } 487 | }); 488 | 489 | // found message errors 490 | if (messageErrors.length > 0) { 491 | isDialogErrors = true; 492 | commit(ADD_MESSAGE, { 493 | text: 'Тест не пройден:\n' + messageErrors.join('\n'), 494 | buttons: [rerunButton], 495 | author: 'Клиент', 496 | class: 'error' 497 | }); 498 | break; 499 | } 500 | } 501 | // end of message 502 | } 503 | 504 | commit(SET_TEST_SUCCESS, { name: dialog.name, success: !isDialogErrors }); 505 | if (isDialogErrors) allFailedTests.push(dialog); 506 | if (!isDialogErrors) { 507 | commit(ADD_MESSAGE, { 508 | text: `Тест пройден`, 509 | buttons: [rerunButton], 510 | author: 'Клиент', 511 | class: 'success' 512 | }); 513 | } 514 | dispatch(LIMIT_MESSAGES); 515 | // end of dialog 516 | } 517 | 518 | if (dialogs.length > 1) { 519 | if (allFailedTests.length > 0) { 520 | let buttons = allFailedTests.map(dialog => { 521 | return { 522 | title: dialog.name, 523 | payload: JSON.stringify({ scenarios_test: [dialog] }) 524 | }; 525 | }); 526 | if (allFailedTests.length > 1) { 527 | buttons.push({ 528 | title: 'все проваленные тесты', 529 | payload: JSON.stringify({ scenarios_test: allFailedTests }) 530 | }); 531 | } 532 | 533 | commit(ADD_MESSAGE, { 534 | text: 'Не все тесты пройдены :(', 535 | author: 'Клиент', 536 | buttons: buttons, 537 | class: 'error' 538 | }); 539 | } else { 540 | commit(ADD_MESSAGE, { 541 | text: 'Все тесты пройдены: ' + dialogs.length, 542 | author: 'Клиент', 543 | class: 'success' 544 | }); 545 | } 546 | } 547 | // end of dialogs test 548 | } 549 | }; 550 | 551 | export const strict = !process.env.production; 552 | -------------------------------------------------------------------------------- /store/settings.js: -------------------------------------------------------------------------------- 1 | export const SET_IS_BOTTOM_TESTS = 'SET_IS_BOTTOM_TESTS'; 2 | export const SET_IS_PROXY = 'SET_IS_PROXY'; 3 | export const SET_IS_SCREEN = 'SET_IS_SCREEN'; 4 | export const SET_IS_SCENARIOS = 'SET_IS_SCENARIOS'; 5 | export const SET_IS_CONSOLE_REQUESTS = 'SET_IS_CONSOLE_REQUESTS'; 6 | export const SET_MESSAGE_LIMIT = 'SET_MESSAGE_LIMIT'; 7 | export const SET_MESSAGE_STORE_LIMIT = 'SET_MESSAGE_STORE_LIMIT'; 8 | export const SET_TIMEOUT = 'SET_TIMEOUT'; 9 | 10 | export const state = () => ({ 11 | isBottomTests: false, 12 | isProxy: process.env.isProxy, 13 | isConsoleRequests: false, 14 | isScreen: true, 15 | isScenarios: false, 16 | messageLimit: 200, // никогда не может быть больше этого 17 | messageStoreLimit: 20, // после обновления страницы 18 | timeout: 3000 19 | }); 20 | 21 | export const mutations = { 22 | [SET_IS_BOTTOM_TESTS](state, isBottomTests) { 23 | state.isBottomTests = isBottomTests; 24 | }, 25 | [SET_IS_PROXY](state, isProxy) { 26 | state.isProxy = isProxy; 27 | }, 28 | [SET_IS_SCREEN](state, isScreen) { 29 | state.isScreen = isScreen; 30 | }, 31 | [SET_IS_SCENARIOS](state, isScenarios) { 32 | state.isScenarios = isScenarios; 33 | }, 34 | [SET_IS_CONSOLE_REQUESTS](state, isConsoleRequests) { 35 | state.isConsoleRequests = isConsoleRequests; 36 | }, 37 | [SET_MESSAGE_LIMIT](state, messageLimit) { 38 | state.messageLimit = messageLimit; 39 | }, 40 | [SET_MESSAGE_STORE_LIMIT](state, messageStoreLimit) { 41 | state.messageStoreLimit = messageStoreLimit; 42 | }, 43 | [SET_TIMEOUT](state, timeout) { 44 | state.timeout = timeout; 45 | } 46 | }; 47 | --------------------------------------------------------------------------------