[{"data":1,"prerenderedAt":2834},["ShallowReactive",2],{"header-latest":3,"post-\u002Fru\u002Fblog\u002Ftelegram-bot-ai-rag-support":1368},[4],{"id":5,"title":6,"body":7,"date":1342,"description":1343,"extension":1344,"faq":1345,"image":1355,"lang":1356,"meta":1357,"navigation":106,"path":1358,"published":106,"readTime":213,"seo":1359,"stem":1360,"tags":1361,"updated":1342,"__hash__":1367},"blog\u002Fblog\u002Fru\u002Ftelegram-web-apps-sells.md","Telegram Web Apps (TWA): Как запустить полноценный SaaS внутри Telegram",{"type":8,"value":9,"toc":1331},"minimark",[10,19,22,25,30,33,44,51,61,68,71,854,858,861,866,872,920,924,927,1212,1216,1225,1228,1266,1279,1287,1291,1311,1314,1327],[11,12,13,14,18],"p",{},"Telegram давно перешагнул рамки классического мессенджера для обмена сообщениями, превратившись в полноценную кроссплатформенную среду выполнения приложений. С появлением технологии ",[15,16,17],"strong",{},"Telegram Web Apps (TWA)",", также известной как Telegram Mini Apps, разработчики могут запускать интерактивные SPA-приложения прямо внутри интерфейса мессенджера.",[11,20,21],{},"Это дает стартапам и бизнесу мгновенный бесшовный доступ к аудитории более 900 миллионов активных пользователей. Размещая свой B2B SaaS, личный кабинет CRM или бизнес-инструмент в боте Telegram, вы устраняете классические барьеры воронки: необходимость скачивать приложения из сторов, регистрироваться и авторизовываться на сторонних сайтах.",[11,23,24],{},"В этом техническом руководстве мы разберем архитектуру TWA, напишем безопасный бэкенд-валидатор на Node.js\u002FTypeScript и настроим синхронизацию стилей на фронтенде.",[26,27,29],"h2",{"id":28},"архитектура-экосистемы-telegram-mini-apps","Архитектура экосистемы Telegram Mini Apps",[11,31,32],{},"В отличие от стандартных веб-приложений, TWA имеет двухстороннюю связь с клиентом. Фронтенд работает в изолированном компоненте WebView, контролируемом клиентом Telegram, в то время как бэкенд взаимодействует как с API Telegram Bot, так и с базой данных приложения.",[34,35,41],"pre",{"className":36,"code":38,"language":39,"meta":40},[37],"language-text","+-----------------------------------------------------------+\n|                     Клиент Telegram                       |\n|  +-----------------------------------------------------+  |\n|  |                 Mini App (WebView UI)               |  |\n|  |     (Использует window.Telegram.WebApp SDK)         |  |\n|  +--------------------------+--------------------------+  |\n+-----------------------------|-----------------------------+\n       | (initData параметры) | (Защищенный HTTPS-запрос)\n       v                      v\n+--------------+       +------------------------------------+\n| Telegram API |       |            Ваш Бэкенд              |\n|  (Вебхуки)   |       | (Проверяет подпись через HMAC-256) |\n+------+-------+       +-----------------+------------------+\n       |                                 |\n       +----------------> [Синхр. БД] \u003C---+\n","text","",[42,43,38],"code",{"__ignoreMap":40},[26,45,47,48],{"id":46},"_1-безопасность-валидация-строки-initdata","1. Безопасность: Валидация строки ",[42,49,50],{},"initData",[11,52,53,54,56,57,60],{},"Когда пользователь открывает Mini App, Telegram передает на фронтенд строку параметров ",[42,55,50],{}," (содержащую данные профиля пользователя, тему оформления, контекст запуска) и защитную подпись ",[42,58,59],{},"hash",".",[11,62,63,64,67],{},"Чтобы злоумышленники не могли подделать идентификатор пользователя (User ID) или эмулировать оплату тарифа, вы ",[15,65,66],{},"обязаны валидировать этот хэш на вашем бэкенде"," с помощью токена вашего Telegram-бота.",[11,69,70],{},"Вот реализация валидации на TypeScript\u002FNode.js:",[34,72,76],{"className":73,"code":74,"language":75,"meta":40,"style":40},"language-typescript shiki shiki-themes github-dark","import crypto from 'crypto';\n\ninterface TelegramUserData {\n  id: number;\n  first_name: string;\n  last_name?: string;\n  username?: string;\n  language_code?: string;\n  is_premium?: boolean;\n}\n\ninterface ValidationResult {\n  isValid: boolean;\n  user?: TelegramUserData;\n}\n\nexport function verifyTelegramInitData(rawQueryString: string, botToken: string): ValidationResult {\n  const urlParams = new URLSearchParams(rawQueryString);\n  const hash = urlParams.get('hash');\n  \n  if (!hash) {\n    return { isValid: false };\n  }\n\n  \u002F\u002F 1. Сортируем все переданные ключи в алфавитном порядке, кроме самого hash\n  const keys = Array.from(urlParams.keys()).filter(key => key !== 'hash').sort();\n  \n  \u002F\u002F 2. Восстанавливаем проверочную строку данных\n  const dataCheckString = keys\n    .map(key => `${key}=${urlParams.get(key)}`)\n    .join('\\n');\n\n  \u002F\u002F 3. Создаем секретный ключ\n  \u002F\u002F Мы используем константную строку \"WebAppData\" для шифрования токена бота\n  const secretKey = crypto\n    .createHmac('sha256', 'WebAppData')\n    .update(botToken)\n    .digest();\n\n  \u002F\u002F 4. Вычисляем хэш-сигнатуру от проверочной строки\n  const computedHash = crypto\n    .createHmac('sha256', secretKey)\n    .update(dataCheckString)\n    .digest('hex');\n\n  \u002F\u002F 5. Безопасно сравниваем вычисленный хэш с тем, который прислал клиент\n  const isValid = computedHash === hash;\n\n  if (!isValid) {\n    return { isValid: false };\n  }\n\n  \u002F\u002F Десериализуем объект пользователя, если валидация прошла успешно\n  try {\n    const userRaw = urlParams.get('user');\n    const user: TelegramUserData = userRaw ? JSON.parse(userRaw) : undefined;\n    return { isValid: true, user };\n  } catch (error) {\n    return { isValid: true };\n  }\n}\n","typescript",[42,77,78,101,108,121,137,150,163,175,187,200,206,211,221,233,245,250,255,296,317,341,347,362,377,383,388,395,448,453,459,472,514,534,539,545,551,564,584,595,605,610,616,628,642,652,666,671,677,696,701,713,724,729,734,740,748,770,808,821,833,844,849],{"__ignoreMap":40},[79,80,83,87,91,94,98],"span",{"class":81,"line":82},"line",1,[79,84,86],{"class":85},"snl16","import",[79,88,90],{"class":89},"s95oV"," crypto ",[79,92,93],{"class":85},"from",[79,95,97],{"class":96},"sU2Wk"," 'crypto'",[79,99,100],{"class":89},";\n",[79,102,104],{"class":81,"line":103},2,[79,105,107],{"emptyLinePlaceholder":106},true,"\n",[79,109,111,114,118],{"class":81,"line":110},3,[79,112,113],{"class":85},"interface",[79,115,117],{"class":116},"svObZ"," TelegramUserData",[79,119,120],{"class":89}," {\n",[79,122,124,128,131,135],{"class":81,"line":123},4,[79,125,127],{"class":126},"s9osk","  id",[79,129,130],{"class":85},":",[79,132,134],{"class":133},"sDLfK"," number",[79,136,100],{"class":89},[79,138,140,143,145,148],{"class":81,"line":139},5,[79,141,142],{"class":126},"  first_name",[79,144,130],{"class":85},[79,146,147],{"class":133}," string",[79,149,100],{"class":89},[79,151,153,156,159,161],{"class":81,"line":152},6,[79,154,155],{"class":126},"  last_name",[79,157,158],{"class":85},"?:",[79,160,147],{"class":133},[79,162,100],{"class":89},[79,164,166,169,171,173],{"class":81,"line":165},7,[79,167,168],{"class":126},"  username",[79,170,158],{"class":85},[79,172,147],{"class":133},[79,174,100],{"class":89},[79,176,178,181,183,185],{"class":81,"line":177},8,[79,179,180],{"class":126},"  language_code",[79,182,158],{"class":85},[79,184,147],{"class":133},[79,186,100],{"class":89},[79,188,190,193,195,198],{"class":81,"line":189},9,[79,191,192],{"class":126},"  is_premium",[79,194,158],{"class":85},[79,196,197],{"class":133}," boolean",[79,199,100],{"class":89},[79,201,203],{"class":81,"line":202},10,[79,204,205],{"class":89},"}\n",[79,207,209],{"class":81,"line":208},11,[79,210,107],{"emptyLinePlaceholder":106},[79,212,214,216,219],{"class":81,"line":213},12,[79,215,113],{"class":85},[79,217,218],{"class":116}," ValidationResult",[79,220,120],{"class":89},[79,222,224,227,229,231],{"class":81,"line":223},13,[79,225,226],{"class":126},"  isValid",[79,228,130],{"class":85},[79,230,197],{"class":133},[79,232,100],{"class":89},[79,234,236,239,241,243],{"class":81,"line":235},14,[79,237,238],{"class":126},"  user",[79,240,158],{"class":85},[79,242,117],{"class":116},[79,244,100],{"class":89},[79,246,248],{"class":81,"line":247},15,[79,249,205],{"class":89},[79,251,253],{"class":81,"line":252},16,[79,254,107],{"emptyLinePlaceholder":106},[79,256,258,261,264,267,270,273,275,277,280,283,285,287,290,292,294],{"class":81,"line":257},17,[79,259,260],{"class":85},"export",[79,262,263],{"class":85}," function",[79,265,266],{"class":116}," verifyTelegramInitData",[79,268,269],{"class":89},"(",[79,271,272],{"class":126},"rawQueryString",[79,274,130],{"class":85},[79,276,147],{"class":133},[79,278,279],{"class":89},", ",[79,281,282],{"class":126},"botToken",[79,284,130],{"class":85},[79,286,147],{"class":133},[79,288,289],{"class":89},")",[79,291,130],{"class":85},[79,293,218],{"class":116},[79,295,120],{"class":89},[79,297,299,302,305,308,311,314],{"class":81,"line":298},18,[79,300,301],{"class":85},"  const",[79,303,304],{"class":133}," urlParams",[79,306,307],{"class":85}," =",[79,309,310],{"class":85}," new",[79,312,313],{"class":116}," URLSearchParams",[79,315,316],{"class":89},"(rawQueryString);\n",[79,318,320,322,325,327,330,333,335,338],{"class":81,"line":319},19,[79,321,301],{"class":85},[79,323,324],{"class":133}," hash",[79,326,307],{"class":85},[79,328,329],{"class":89}," urlParams.",[79,331,332],{"class":116},"get",[79,334,269],{"class":89},[79,336,337],{"class":96},"'hash'",[79,339,340],{"class":89},");\n",[79,342,344],{"class":81,"line":343},20,[79,345,346],{"class":89},"  \n",[79,348,350,353,356,359],{"class":81,"line":349},21,[79,351,352],{"class":85},"  if",[79,354,355],{"class":89}," (",[79,357,358],{"class":85},"!",[79,360,361],{"class":89},"hash) {\n",[79,363,365,368,371,374],{"class":81,"line":364},22,[79,366,367],{"class":85},"    return",[79,369,370],{"class":89}," { isValid: ",[79,372,373],{"class":133},"false",[79,375,376],{"class":89}," };\n",[79,378,380],{"class":81,"line":379},23,[79,381,382],{"class":89},"  }\n",[79,384,386],{"class":81,"line":385},24,[79,387,107],{"emptyLinePlaceholder":106},[79,389,391],{"class":81,"line":390},25,[79,392,394],{"class":393},"sAwPA","  \u002F\u002F 1. Сортируем все переданные ключи в алфавитном порядке, кроме самого hash\n",[79,396,398,400,403,405,408,410,413,416,419,422,424,427,430,433,436,439,442,445],{"class":81,"line":397},26,[79,399,301],{"class":85},[79,401,402],{"class":133}," keys",[79,404,307],{"class":85},[79,406,407],{"class":89}," Array.",[79,409,93],{"class":116},[79,411,412],{"class":89},"(urlParams.",[79,414,415],{"class":116},"keys",[79,417,418],{"class":89},"()).",[79,420,421],{"class":116},"filter",[79,423,269],{"class":89},[79,425,426],{"class":126},"key",[79,428,429],{"class":85}," =>",[79,431,432],{"class":89}," key ",[79,434,435],{"class":85},"!==",[79,437,438],{"class":96}," 'hash'",[79,440,441],{"class":89},").",[79,443,444],{"class":116},"sort",[79,446,447],{"class":89},"();\n",[79,449,451],{"class":81,"line":450},27,[79,452,346],{"class":89},[79,454,456],{"class":81,"line":455},28,[79,457,458],{"class":393},"  \u002F\u002F 2. Восстанавливаем проверочную строку данных\n",[79,460,462,464,467,469],{"class":81,"line":461},29,[79,463,301],{"class":85},[79,465,466],{"class":133}," dataCheckString",[79,468,307],{"class":85},[79,470,471],{"class":89}," keys\n",[79,473,475,478,481,483,485,487,490,492,495,498,500,502,504,506,508,511],{"class":81,"line":474},30,[79,476,477],{"class":89},"    .",[79,479,480],{"class":116},"map",[79,482,269],{"class":89},[79,484,426],{"class":126},[79,486,429],{"class":85},[79,488,489],{"class":96}," `${",[79,491,426],{"class":89},[79,493,494],{"class":96},"}=${",[79,496,497],{"class":89},"urlParams",[79,499,60],{"class":96},[79,501,332],{"class":116},[79,503,269],{"class":96},[79,505,426],{"class":89},[79,507,289],{"class":96},[79,509,510],{"class":96},"}`",[79,512,513],{"class":89},")\n",[79,515,517,519,522,524,527,530,532],{"class":81,"line":516},31,[79,518,477],{"class":89},[79,520,521],{"class":116},"join",[79,523,269],{"class":89},[79,525,526],{"class":96},"'",[79,528,529],{"class":133},"\\n",[79,531,526],{"class":96},[79,533,340],{"class":89},[79,535,537],{"class":81,"line":536},32,[79,538,107],{"emptyLinePlaceholder":106},[79,540,542],{"class":81,"line":541},33,[79,543,544],{"class":393},"  \u002F\u002F 3. Создаем секретный ключ\n",[79,546,548],{"class":81,"line":547},34,[79,549,550],{"class":393},"  \u002F\u002F Мы используем константную строку \"WebAppData\" для шифрования токена бота\n",[79,552,554,556,559,561],{"class":81,"line":553},35,[79,555,301],{"class":85},[79,557,558],{"class":133}," secretKey",[79,560,307],{"class":85},[79,562,563],{"class":89}," crypto\n",[79,565,567,569,572,574,577,579,582],{"class":81,"line":566},36,[79,568,477],{"class":89},[79,570,571],{"class":116},"createHmac",[79,573,269],{"class":89},[79,575,576],{"class":96},"'sha256'",[79,578,279],{"class":89},[79,580,581],{"class":96},"'WebAppData'",[79,583,513],{"class":89},[79,585,587,589,592],{"class":81,"line":586},37,[79,588,477],{"class":89},[79,590,591],{"class":116},"update",[79,593,594],{"class":89},"(botToken)\n",[79,596,598,600,603],{"class":81,"line":597},38,[79,599,477],{"class":89},[79,601,602],{"class":116},"digest",[79,604,447],{"class":89},[79,606,608],{"class":81,"line":607},39,[79,609,107],{"emptyLinePlaceholder":106},[79,611,613],{"class":81,"line":612},40,[79,614,615],{"class":393},"  \u002F\u002F 4. Вычисляем хэш-сигнатуру от проверочной строки\n",[79,617,619,621,624,626],{"class":81,"line":618},41,[79,620,301],{"class":85},[79,622,623],{"class":133}," computedHash",[79,625,307],{"class":85},[79,627,563],{"class":89},[79,629,631,633,635,637,639],{"class":81,"line":630},42,[79,632,477],{"class":89},[79,634,571],{"class":116},[79,636,269],{"class":89},[79,638,576],{"class":96},[79,640,641],{"class":89},", secretKey)\n",[79,643,645,647,649],{"class":81,"line":644},43,[79,646,477],{"class":89},[79,648,591],{"class":116},[79,650,651],{"class":89},"(dataCheckString)\n",[79,653,655,657,659,661,664],{"class":81,"line":654},44,[79,656,477],{"class":89},[79,658,602],{"class":116},[79,660,269],{"class":89},[79,662,663],{"class":96},"'hex'",[79,665,340],{"class":89},[79,667,669],{"class":81,"line":668},45,[79,670,107],{"emptyLinePlaceholder":106},[79,672,674],{"class":81,"line":673},46,[79,675,676],{"class":393},"  \u002F\u002F 5. Безопасно сравниваем вычисленный хэш с тем, который прислал клиент\n",[79,678,680,682,685,687,690,693],{"class":81,"line":679},47,[79,681,301],{"class":85},[79,683,684],{"class":133}," isValid",[79,686,307],{"class":85},[79,688,689],{"class":89}," computedHash ",[79,691,692],{"class":85},"===",[79,694,695],{"class":89}," hash;\n",[79,697,699],{"class":81,"line":698},48,[79,700,107],{"emptyLinePlaceholder":106},[79,702,704,706,708,710],{"class":81,"line":703},49,[79,705,352],{"class":85},[79,707,355],{"class":89},[79,709,358],{"class":85},[79,711,712],{"class":89},"isValid) {\n",[79,714,716,718,720,722],{"class":81,"line":715},50,[79,717,367],{"class":85},[79,719,370],{"class":89},[79,721,373],{"class":133},[79,723,376],{"class":89},[79,725,727],{"class":81,"line":726},51,[79,728,382],{"class":89},[79,730,732],{"class":81,"line":731},52,[79,733,107],{"emptyLinePlaceholder":106},[79,735,737],{"class":81,"line":736},53,[79,738,739],{"class":393},"  \u002F\u002F Десериализуем объект пользователя, если валидация прошла успешно\n",[79,741,743,746],{"class":81,"line":742},54,[79,744,745],{"class":85},"  try",[79,747,120],{"class":89},[79,749,751,754,757,759,761,763,765,768],{"class":81,"line":750},55,[79,752,753],{"class":85},"    const",[79,755,756],{"class":133}," userRaw",[79,758,307],{"class":85},[79,760,329],{"class":89},[79,762,332],{"class":116},[79,764,269],{"class":89},[79,766,767],{"class":96},"'user'",[79,769,340],{"class":89},[79,771,773,775,778,780,782,784,787,790,793,795,798,801,803,806],{"class":81,"line":772},56,[79,774,753],{"class":85},[79,776,777],{"class":133}," user",[79,779,130],{"class":85},[79,781,117],{"class":116},[79,783,307],{"class":85},[79,785,786],{"class":89}," userRaw ",[79,788,789],{"class":85},"?",[79,791,792],{"class":133}," JSON",[79,794,60],{"class":89},[79,796,797],{"class":116},"parse",[79,799,800],{"class":89},"(userRaw) ",[79,802,130],{"class":85},[79,804,805],{"class":133}," undefined",[79,807,100],{"class":89},[79,809,811,813,815,818],{"class":81,"line":810},57,[79,812,367],{"class":85},[79,814,370],{"class":89},[79,816,817],{"class":133},"true",[79,819,820],{"class":89},", user };\n",[79,822,824,827,830],{"class":81,"line":823},58,[79,825,826],{"class":89},"  } ",[79,828,829],{"class":85},"catch",[79,831,832],{"class":89}," (error) {\n",[79,834,836,838,840,842],{"class":81,"line":835},59,[79,837,367],{"class":85},[79,839,370],{"class":89},[79,841,817],{"class":133},[79,843,376],{"class":89},[79,845,847],{"class":81,"line":846},60,[79,848,382],{"class":89},[79,850,852],{"class":81,"line":851},61,[79,853,205],{"class":89},[26,855,857],{"id":856},"_2-интеграция-во-фронтенд-и-адаптация-темы-оформления","2. Интеграция во фронтенд и адаптация темы оформления",[11,859,860],{},"Для создания премиального интерфейса (UI\u002FUX) приложение должно подстраиваться под цветовую гамму клиента Telegram (темная\u002Fсветлая тема). Доступ к этим параметрам осуществляется через библиотеку JS-моста.",[862,863,865],"h3",{"id":864},"шаг-1-подключение-скрипта-в-nuxt-3-html","Шаг 1: Подключение скрипта в Nuxt 3 \u002F HTML",[11,867,868,869,130],{},"Добавьте скрипт в секцию head в Nuxt с помощью composable ",[42,870,871],{},"useHead",[34,873,875],{"className":73,"code":874,"language":75,"meta":40,"style":40},"\u002F\u002F app.vue или layout страницы\nuseHead({\n  script: [\n    { src: 'https:\u002F\u002Ftelegram.org\u002Fjs\u002Ftelegram-web-app.js', defer: true }\n  ]\n})\n",[42,876,877,882,889,894,910,915],{"__ignoreMap":40},[79,878,879],{"class":81,"line":82},[79,880,881],{"class":393},"\u002F\u002F app.vue или layout страницы\n",[79,883,884,886],{"class":81,"line":103},[79,885,871],{"class":116},[79,887,888],{"class":89},"({\n",[79,890,891],{"class":81,"line":110},[79,892,893],{"class":89},"  script: [\n",[79,895,896,899,902,905,907],{"class":81,"line":123},[79,897,898],{"class":89},"    { src: ",[79,900,901],{"class":96},"'https:\u002F\u002Ftelegram.org\u002Fjs\u002Ftelegram-web-app.js'",[79,903,904],{"class":89},", defer: ",[79,906,817],{"class":133},[79,908,909],{"class":89}," }\n",[79,911,912],{"class":81,"line":139},[79,913,914],{"class":89},"  ]\n",[79,916,917],{"class":81,"line":152},[79,918,919],{"class":89},"})\n",[862,921,923],{"id":922},"шаг-2-использование-js-моста-в-vue-компонентах","Шаг 2: Использование JS-моста в Vue-компонентах",[11,925,926],{},"Создадим реактивный composable для считывания настроек Telegram:",[34,928,930],{"className":73,"code":929,"language":75,"meta":40,"style":40},"\u002F\u002F composables\u002FuseTelegram.ts\nimport { ref, onMounted } from 'vue';\n\nexport function useTelegram() {\n  const isReady = ref(false);\n  const user = ref\u003Cany>(null);\n\n  onMounted(() => {\n    const tg = (window as any).Telegram?.WebApp;\n    if (tg) {\n      tg.ready();\n      tg.expand(); \u002F\u002F Разворачиваем окно на максимальную высоту\n      \n      user.value = tg.initDataUnsafe?.user;\n      isReady.value = true;\n\n      \u002F\u002F Передаем цвета темы Telegram в переменные CSS (Custom Properties)\n      const root = document.documentElement;\n      root.style.setProperty('--color-tg-bg', tg.themeParams.bg_color);\n      root.style.setProperty('--color-tg-text', tg.themeParams.text_color);\n      root.style.setProperty('--color-tg-button', tg.themeParams.button_color);\n      root.style.setProperty('--color-tg-button-text', tg.themeParams.button_text_color);\n    }\n  });\n\n  return { isReady, user };\n}\n",[42,931,932,937,951,955,967,985,1009,1013,1026,1047,1055,1065,1078,1083,1094,1106,1110,1115,1128,1144,1158,1172,1186,1191,1196,1200,1208],{"__ignoreMap":40},[79,933,934],{"class":81,"line":82},[79,935,936],{"class":393},"\u002F\u002F composables\u002FuseTelegram.ts\n",[79,938,939,941,944,946,949],{"class":81,"line":103},[79,940,86],{"class":85},[79,942,943],{"class":89}," { ref, onMounted } ",[79,945,93],{"class":85},[79,947,948],{"class":96}," 'vue'",[79,950,100],{"class":89},[79,952,953],{"class":81,"line":110},[79,954,107],{"emptyLinePlaceholder":106},[79,956,957,959,961,964],{"class":81,"line":123},[79,958,260],{"class":85},[79,960,263],{"class":85},[79,962,963],{"class":116}," useTelegram",[79,965,966],{"class":89},"() {\n",[79,968,969,971,974,976,979,981,983],{"class":81,"line":139},[79,970,301],{"class":85},[79,972,973],{"class":133}," isReady",[79,975,307],{"class":85},[79,977,978],{"class":116}," ref",[79,980,269],{"class":89},[79,982,373],{"class":133},[79,984,340],{"class":89},[79,986,987,989,991,993,995,998,1001,1004,1007],{"class":81,"line":152},[79,988,301],{"class":85},[79,990,777],{"class":133},[79,992,307],{"class":85},[79,994,978],{"class":116},[79,996,997],{"class":89},"\u003C",[79,999,1000],{"class":133},"any",[79,1002,1003],{"class":89},">(",[79,1005,1006],{"class":133},"null",[79,1008,340],{"class":89},[79,1010,1011],{"class":81,"line":165},[79,1012,107],{"emptyLinePlaceholder":106},[79,1014,1015,1018,1021,1024],{"class":81,"line":177},[79,1016,1017],{"class":116},"  onMounted",[79,1019,1020],{"class":89},"(() ",[79,1022,1023],{"class":85},"=>",[79,1025,120],{"class":89},[79,1027,1028,1030,1033,1035,1038,1041,1044],{"class":81,"line":189},[79,1029,753],{"class":85},[79,1031,1032],{"class":133}," tg",[79,1034,307],{"class":85},[79,1036,1037],{"class":89}," (window ",[79,1039,1040],{"class":85},"as",[79,1042,1043],{"class":133}," any",[79,1045,1046],{"class":89},").Telegram?.WebApp;\n",[79,1048,1049,1052],{"class":81,"line":202},[79,1050,1051],{"class":85},"    if",[79,1053,1054],{"class":89}," (tg) {\n",[79,1056,1057,1060,1063],{"class":81,"line":208},[79,1058,1059],{"class":89},"      tg.",[79,1061,1062],{"class":116},"ready",[79,1064,447],{"class":89},[79,1066,1067,1069,1072,1075],{"class":81,"line":213},[79,1068,1059],{"class":89},[79,1070,1071],{"class":116},"expand",[79,1073,1074],{"class":89},"(); ",[79,1076,1077],{"class":393},"\u002F\u002F Разворачиваем окно на максимальную высоту\n",[79,1079,1080],{"class":81,"line":223},[79,1081,1082],{"class":89},"      \n",[79,1084,1085,1088,1091],{"class":81,"line":235},[79,1086,1087],{"class":89},"      user.value ",[79,1089,1090],{"class":85},"=",[79,1092,1093],{"class":89}," tg.initDataUnsafe?.user;\n",[79,1095,1096,1099,1101,1104],{"class":81,"line":247},[79,1097,1098],{"class":89},"      isReady.value ",[79,1100,1090],{"class":85},[79,1102,1103],{"class":133}," true",[79,1105,100],{"class":89},[79,1107,1108],{"class":81,"line":252},[79,1109,107],{"emptyLinePlaceholder":106},[79,1111,1112],{"class":81,"line":257},[79,1113,1114],{"class":393},"      \u002F\u002F Передаем цвета темы Telegram в переменные CSS (Custom Properties)\n",[79,1116,1117,1120,1123,1125],{"class":81,"line":298},[79,1118,1119],{"class":85},"      const",[79,1121,1122],{"class":133}," root",[79,1124,307],{"class":85},[79,1126,1127],{"class":89}," document.documentElement;\n",[79,1129,1130,1133,1136,1138,1141],{"class":81,"line":319},[79,1131,1132],{"class":89},"      root.style.",[79,1134,1135],{"class":116},"setProperty",[79,1137,269],{"class":89},[79,1139,1140],{"class":96},"'--color-tg-bg'",[79,1142,1143],{"class":89},", tg.themeParams.bg_color);\n",[79,1145,1146,1148,1150,1152,1155],{"class":81,"line":343},[79,1147,1132],{"class":89},[79,1149,1135],{"class":116},[79,1151,269],{"class":89},[79,1153,1154],{"class":96},"'--color-tg-text'",[79,1156,1157],{"class":89},", tg.themeParams.text_color);\n",[79,1159,1160,1162,1164,1166,1169],{"class":81,"line":349},[79,1161,1132],{"class":89},[79,1163,1135],{"class":116},[79,1165,269],{"class":89},[79,1167,1168],{"class":96},"'--color-tg-button'",[79,1170,1171],{"class":89},", tg.themeParams.button_color);\n",[79,1173,1174,1176,1178,1180,1183],{"class":81,"line":364},[79,1175,1132],{"class":89},[79,1177,1135],{"class":116},[79,1179,269],{"class":89},[79,1181,1182],{"class":96},"'--color-tg-button-text'",[79,1184,1185],{"class":89},", tg.themeParams.button_text_color);\n",[79,1187,1188],{"class":81,"line":379},[79,1189,1190],{"class":89},"    }\n",[79,1192,1193],{"class":81,"line":385},[79,1194,1195],{"class":89},"  });\n",[79,1197,1198],{"class":81,"line":390},[79,1199,107],{"emptyLinePlaceholder":106},[79,1201,1202,1205],{"class":81,"line":397},[79,1203,1204],{"class":85},"  return",[79,1206,1207],{"class":89}," { isReady, user };\n",[79,1209,1210],{"class":81,"line":450},[79,1211,205],{"class":89},[26,1213,1215],{"id":1214},"_3-прием-платежей-через-telegram-stars","3. Прием платежей через Telegram Stars",[11,1217,1218,1219,355,1222,441],{},"При продаже цифровых товаров (подписки на SaaS, баланс лимитов ИИ, инфопродукты) правила Apple\u002FGoogle обязывают использовать внутреннюю валюту мессенджера — ",[15,1220,1221],{},"Telegram Stars",[42,1223,1224],{},"XTR",[11,1226,1227],{},"Алгоритм покупки цифрового товара:",[1229,1230,1231,1238,1250,1256],"ol",{},[1232,1233,1234,1237],"li",{},[15,1235,1236],{},"Создание счета",": TWA запрашивает бэкенд сгенерировать счет на оплату.",[1232,1239,1240,1243,1244,1247,1248,60],{},[15,1241,1242],{},"Отправка инвойса",": Бэкенд делает запрос к Bot API ",[42,1245,1246],{},"sendInvoice"," с валютой ",[42,1249,1224],{},[1232,1251,1252,1255],{},[15,1253,1254],{},"Оплата в клиенте",": Telegram открывает нативный экран оплаты, где пользователь списывает «Звезды».",[1232,1257,1258,1261,1262,1265],{},[15,1259,1260],{},"Синхронизация",": Приходит вебхук ",[42,1263,1264],{},"successful_payment"," на бэкенд бота, после чего лимиты зачисляются в вашей базе данных.",[11,1267,1268,1269,1274,1275,60],{},"В моей платформе конструктора ботов ",[1270,1271,1273],"a",{"href":1272},"\u002Fru\u002Fprojects\u002Ftelego","TeleGo.io"," мы предоставляем именно такие встроенные платежные сценарии, позволяя пользователям создавать свои TWA и сразу принимать Stars. Кроме того, мы поддерживаем гибридные хелпдески поддержки, о настройке которых вы можете узнать в статье ",[1270,1276,1278],{"href":1277},"\u002Fru\u002Fblog\u002Ftelegram-bot-ai-rag-support","Как создать ИИ-бота поддержки с RAG",[11,1280,1281,1282,1286],{},"Для физических товаров и консультаций можно использовать классический биллинг. Читайте мое руководство ",[1270,1283,1285],{"href":1284},"\u002Fru\u002Fblog\u002Fsaas-stripe-billing-integration","Монетизация SaaS и интеграция Stripe"," для ознакомления со скриптами Express\u002FTypeScript.",[26,1288,1290],{"id":1289},"источники-и-документация","Источники и документация",[1292,1293,1294,1303],"ul",{},[1232,1295,1296,1302],{},[1270,1297,1301],{"href":1298,"rel":1299},"https:\u002F\u002Fcore.telegram.org\u002Fbots\u002Fwebapps",[1300],"nofollow","Telegram Mini Apps"," — официальная документация WebApp SDK и initData",[1232,1304,1305,1310],{},[1270,1306,1309],{"href":1307,"rel":1308},"https:\u002F\u002Fcore.telegram.org\u002Fbots\u002Fpayments",[1300],"Bot Payments API"," — счета, Telegram Stars и вебхуки оплат",[11,1312,1313],{},"Сочетание современных веб-фронтендов с API Telegram WebApp позволяет создавать удобные B2B-продукты внутри популярного мессенджера, минуя сложные регистрационные формы.",[11,1315,1316,1317,1321,1322,1326],{},"Если вы планируете разработку Telegram Mini App с платежами Stars\u002FStripe и динамической темой оформления, ознакомьтесь с моей услугой ",[1270,1318,1320],{"href":1319},"\u002Fru\u002Ftelegram-bots","Создание Telegram-ботов"," или запишитесь на ",[1270,1323,1325],{"href":1324},"\u002Fru\u002Fconsultations","Консультацию",", чтобы разработать архитектуру.",[1328,1329,1330],"style",{},"html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}",{"title":40,"searchDepth":103,"depth":103,"links":1332},[1333,1334,1336,1340,1341],{"id":28,"depth":103,"text":29},{"id":46,"depth":103,"text":1335},"1. Безопасность: Валидация строки initData",{"id":856,"depth":103,"text":857,"children":1337},[1338,1339],{"id":864,"depth":110,"text":865},{"id":922,"depth":110,"text":923},{"id":1214,"depth":103,"text":1215},{"id":1289,"depth":103,"text":1290},"2026-07-05","Детальное руководство для разработчиков по использованию Telegram Mini Apps в качестве веб-фронтенда, криптографической валидации данных и приему платежей.","md",[1346,1349,1352],{"q":1347,"a":1348},"Чем Telegram Mini App отличается от обычного бота?","Бот общается сообщениями и кнопками, а Mini App открывает внутри Telegram полноценный веб-интерфейс: дашборды, каталоги, формы, графики. Это SPA-приложение в WebView со своим фронтендом, при этом авторизация происходит автоматически через аккаунт Telegram.",{"q":1350,"a":1351},"Как проверить, что данные пользователя в Mini App не подделаны?","Через криптографическую валидацию initData: Telegram подписывает данные пользователя HMAC-SHA256-подписью на основе токена бота. Бэкенд обязан пересчитать и сверить подпись при каждом запросе — без этого любой может выдать себя за другого пользователя.",{"q":1353,"a":1354},"Когда обязательны Telegram Stars, а когда можно Stripe?","Цифровые товары и подписки внутри Mini App по правилам Apple и Google оплачиваются через Telegram Stars. Физические товары, услуги и консультации можно продавать через классические шлюзы вроде Stripe.","\u002Fimages\u002Fblog\u002Fblog_telegram_twa.jpg","ru",{},"\u002Fblog\u002Fru\u002Ftelegram-web-apps-sells",{"title":6,"description":1343},"blog\u002Fru\u002Ftelegram-web-apps-sells",[1362,1363,1364,1365,1366],"Telegram","Mini Apps","SaaS","Node.js","Frontend","YuRgxnAaTWzdgHfj4TSFOXTRw0OPRN5jjGwHaAdmD4k",{"id":1369,"title":1370,"body":1371,"date":2811,"description":2812,"extension":1344,"faq":2813,"image":2823,"lang":1356,"meta":2824,"navigation":106,"path":2825,"published":106,"readTime":213,"seo":2826,"stem":2827,"tags":2828,"updated":1342,"__hash__":2833},"blog\u002Fblog\u002Fru\u002Ftelegram-bot-ai-rag-support.md","ИИ-поддержка клиентов: Как собрать RAG-бота с бесшовной передачей менеджеру",{"type":8,"value":1372,"toc":2803},[1373,1376,1383,1390,1397,1401,1407,1411,1421,1495,1498,1582,1586,1589,2023,2027,2030,2284,2288,2295,2744,2750,2762,2764,2789,2792,2800],[11,1374,1375],{},"Внедрение больших языковых моделей (LLM) вроде ChatGPT для ответов на вопросы клиентов — один из самых популярных способов снижения нагрузки на техподдержку. Однако стандартные ИИ-модели имеют два критических недостатка для корпоративного использования: у них нет доступа к вашим закрытым данным (инструкции, цены, API) и они склонны придумывать недостоверные факты («галлюцинировать»).",[11,1377,1378,1379,1382],{},"Для решения этих проблем в продакшене используется архитектура ",[15,1380,1381],{},"RAG (Retrieval-Augmented Generation)",". Сначала бот ищет ответ в локальной базе знаний компании, извлекает релевантные текстовые фрагменты и передает их в качестве контекста в LLM. Модель формулирует ответ строго на основе предоставленных документов.",[11,1384,1385,1386,1389],{},"Для обеспечения премиального клиентского сервиса система должна содержать ",[15,1387,1388],{},"протокол эскалации на оператора",". Если ИИ не находит ответа в базе, диалог мгновенно переключается на человека.",[11,1391,1392,1393,1396],{},"В этой статье мы соберем полноценный RAG-цикл на Node.js, настроим поиск в PostgreSQL с расширением ",[42,1394,1395],{},"pgvector"," и опишем логику переключения диалога.",[26,1398,1400],{"id":1399},"архитектура-работы-rag-бота-поддержки","Архитектура работы RAG-бота поддержки",[34,1402,1405],{"className":1403,"code":1404,"language":39,"meta":40},[37],"[Пользователь спрашивает: \"Как настроить Stripe?\"]\n                       │\n                       v\n     (Генерация векторного эмбеддинга)\n     [text-embedding-3-small] (1536 dim)\n                       │\n                       v\n     (Векторный поиск в PostgreSQL)\n     SELECT * FROM match_kb_docs(vector, threshold = 0.75)\n                       │\n                       +-----------------------+\n                       |                       |\n            (Контекст НАЙДЕН)         (Контекст НЕ НАЙДЕН \u002F Низкий скор)\n                       │                       │\n                       v                       v\n     [Формируем промпт с текстом KB]   [Ставим сессии статус human_handling]\n                       │                       │\n                       v                       v\n     [Отправляем запрос в gpt-4o-mini]  [Оповещаем менеджеров в Slack\u002FCRM]\n                       │                       │\n                       v                       v\n     [Отправляем ИИ-ответ клиенту]     [Отправляем: \"Перевожу на менеджера\"]\n",[42,1406,1404],{"__ignoreMap":40},[26,1408,1410],{"id":1409},"_1-настройка-базы-данных-pgvector","1. Настройка базы данных Pgvector",[11,1412,1413,1414,1417,1418,441],{},"Включите расширение ",[42,1415,1416],{},"vector"," в вашей БД PostgreSQL и создайте таблицу для хранения фрагментов документации. Количество измерений (dimensions) вектора должно совпадать с выбранной моделью эмбеддингов (1536 для OpenAI ",[42,1419,1420],{},"text-embedding-3-small",[34,1422,1426],{"className":1423,"code":1424,"language":1425,"meta":40,"style":40},"language-sql shiki shiki-themes github-dark","-- Включаем поддержку векторного расширения\nCREATE EXTENSION IF NOT EXISTS vector;\n\n-- Создаем таблицу Базы Знаний\nCREATE TABLE kb_documents (\n  id SERIAL PRIMARY KEY,\n  content TEXT NOT NULL,\n  embedding VECTOR(1536) NOT NULL,\n  category VARCHAR(50) DEFAULT 'general',\n  created_at TIMESTAMP DEFAULT NOW()\n);\n\n-- Индексируем векторную колонку (используем HNSW для высокой скорости на масштабе)\nCREATE INDEX kb_hnsw_idx ON kb_documents USING hnsw (embedding vector_cosine_ops);\n","sql",[42,1427,1428,1433,1438,1442,1447,1452,1457,1462,1467,1472,1477,1481,1485,1490],{"__ignoreMap":40},[79,1429,1430],{"class":81,"line":82},[79,1431,1432],{},"-- Включаем поддержку векторного расширения\n",[79,1434,1435],{"class":81,"line":103},[79,1436,1437],{},"CREATE EXTENSION IF NOT EXISTS vector;\n",[79,1439,1440],{"class":81,"line":110},[79,1441,107],{"emptyLinePlaceholder":106},[79,1443,1444],{"class":81,"line":123},[79,1445,1446],{},"-- Создаем таблицу Базы Знаний\n",[79,1448,1449],{"class":81,"line":139},[79,1450,1451],{},"CREATE TABLE kb_documents (\n",[79,1453,1454],{"class":81,"line":152},[79,1455,1456],{},"  id SERIAL PRIMARY KEY,\n",[79,1458,1459],{"class":81,"line":165},[79,1460,1461],{},"  content TEXT NOT NULL,\n",[79,1463,1464],{"class":81,"line":177},[79,1465,1466],{},"  embedding VECTOR(1536) NOT NULL,\n",[79,1468,1469],{"class":81,"line":189},[79,1470,1471],{},"  category VARCHAR(50) DEFAULT 'general',\n",[79,1473,1474],{"class":81,"line":202},[79,1475,1476],{},"  created_at TIMESTAMP DEFAULT NOW()\n",[79,1478,1479],{"class":81,"line":208},[79,1480,340],{},[79,1482,1483],{"class":81,"line":213},[79,1484,107],{"emptyLinePlaceholder":106},[79,1486,1487],{"class":81,"line":223},[79,1488,1489],{},"-- Индексируем векторную колонку (используем HNSW для высокой скорости на масштабе)\n",[79,1491,1492],{"class":81,"line":235},[79,1493,1494],{},"CREATE INDEX kb_hnsw_idx ON kb_documents USING hnsw (embedding vector_cosine_ops);\n",[11,1496,1497],{},"Создадим SQL-функцию поиска документов по косинусному расстоянию:",[34,1499,1501],{"className":1423,"code":1500,"language":1425,"meta":40,"style":40},"CREATE OR REPLACE FUNCTION match_kb_docs(\n  query_embedding VECTOR(1536),\n  match_threshold FLOAT,\n  match_count INT\n)\nRETURNS TABLE (id INT, content TEXT, similarity FLOAT)\nLANGUAGE plpgsql AS $$\nBEGIN\n  RETURN QUERY\n  SELECT kb.id, kb.content, 1 - (kb.embedding \u003C=> query_embedding) AS similarity\n  FROM kb_documents kb\n  WHERE 1 - (kb.embedding \u003C=> query_embedding) > match_threshold\n  ORDER BY kb.embedding \u003C=> query_embedding\n  LIMIT match_count;\nEND;\n$$;\n",[42,1502,1503,1508,1513,1518,1523,1527,1532,1537,1542,1547,1552,1557,1562,1567,1572,1577],{"__ignoreMap":40},[79,1504,1505],{"class":81,"line":82},[79,1506,1507],{},"CREATE OR REPLACE FUNCTION match_kb_docs(\n",[79,1509,1510],{"class":81,"line":103},[79,1511,1512],{},"  query_embedding VECTOR(1536),\n",[79,1514,1515],{"class":81,"line":110},[79,1516,1517],{},"  match_threshold FLOAT,\n",[79,1519,1520],{"class":81,"line":123},[79,1521,1522],{},"  match_count INT\n",[79,1524,1525],{"class":81,"line":139},[79,1526,513],{},[79,1528,1529],{"class":81,"line":152},[79,1530,1531],{},"RETURNS TABLE (id INT, content TEXT, similarity FLOAT)\n",[79,1533,1534],{"class":81,"line":165},[79,1535,1536],{},"LANGUAGE plpgsql AS $$\n",[79,1538,1539],{"class":81,"line":177},[79,1540,1541],{},"BEGIN\n",[79,1543,1544],{"class":81,"line":189},[79,1545,1546],{},"  RETURN QUERY\n",[79,1548,1549],{"class":81,"line":202},[79,1550,1551],{},"  SELECT kb.id, kb.content, 1 - (kb.embedding \u003C=> query_embedding) AS similarity\n",[79,1553,1554],{"class":81,"line":208},[79,1555,1556],{},"  FROM kb_documents kb\n",[79,1558,1559],{"class":81,"line":213},[79,1560,1561],{},"  WHERE 1 - (kb.embedding \u003C=> query_embedding) > match_threshold\n",[79,1563,1564],{"class":81,"line":223},[79,1565,1566],{},"  ORDER BY kb.embedding \u003C=> query_embedding\n",[79,1568,1569],{"class":81,"line":235},[79,1570,1571],{},"  LIMIT match_count;\n",[79,1573,1574],{"class":81,"line":247},[79,1575,1576],{},"END;\n",[79,1578,1579],{"class":81,"line":252},[79,1580,1581],{},"$$;\n",[26,1583,1585],{"id":1584},"_2-реализация-векторного-поиска-на-nodejs","2. Реализация векторного поиска на Node.js",[11,1587,1588],{},"Напишем сервис, который будет обращаться к API OpenAI для генерации вектора из вопроса пользователя и осуществлять поиск по базе PostgreSQL:",[34,1590,1592],{"className":73,"code":1591,"language":75,"meta":40,"style":40},"import { OpenAI } from 'openai';\nimport pg from 'pg';\n\nconst openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });\nconst pool = new pg.Pool({ connectionString: process.env.DATABASE_URL });\n\nexport async function getEmbedding(text: string): Promise\u003Cnumber[]> {\n  const response = await openai.embeddings.create({\n    model: 'text-embedding-3-small',\n    input: text.replace(\u002F\\n\u002Fg, ' '),\n  });\n  return response.data[0].embedding;\n}\n\nexport async function findRelevantContext(queryText: string): Promise\u003Cstring> {\n  const queryVector = await getEmbedding(queryText);\n  const sql = `SELECT * FROM match_kb_docs($1::vector, 0.75, 3)`;\n  \n  const client = await pool.connect();\n  try {\n    const result = await client.query(sql, [JSON.stringify(queryVector)]);\n    if (result.rows.length === 0) {\n      return '';\n    }\n    \u002F\u002F Объединяем найденные статьи в один текстовый блок\n    return result.rows.map(row => row.content).join('\\n---\\n');\n  } finally {\n    client.release();\n  }\n}\n",[42,1593,1594,1608,1622,1626,1650,1675,1679,1714,1734,1745,1773,1777,1790,1794,1798,1832,1848,1862,1866,1885,1891,1922,1941,1951,1955,1960,1996,2005,2015,2019],{"__ignoreMap":40},[79,1595,1596,1598,1601,1603,1606],{"class":81,"line":82},[79,1597,86],{"class":85},[79,1599,1600],{"class":89}," { OpenAI } ",[79,1602,93],{"class":85},[79,1604,1605],{"class":96}," 'openai'",[79,1607,100],{"class":89},[79,1609,1610,1612,1615,1617,1620],{"class":81,"line":103},[79,1611,86],{"class":85},[79,1613,1614],{"class":89}," pg ",[79,1616,93],{"class":85},[79,1618,1619],{"class":96}," 'pg'",[79,1621,100],{"class":89},[79,1623,1624],{"class":81,"line":110},[79,1625,107],{"emptyLinePlaceholder":106},[79,1627,1628,1631,1634,1636,1638,1641,1644,1647],{"class":81,"line":123},[79,1629,1630],{"class":85},"const",[79,1632,1633],{"class":133}," openai",[79,1635,307],{"class":85},[79,1637,310],{"class":85},[79,1639,1640],{"class":116}," OpenAI",[79,1642,1643],{"class":89},"({ apiKey: process.env.",[79,1645,1646],{"class":133},"OPENAI_API_KEY",[79,1648,1649],{"class":89}," });\n",[79,1651,1652,1654,1657,1659,1661,1664,1667,1670,1673],{"class":81,"line":139},[79,1653,1630],{"class":85},[79,1655,1656],{"class":133}," pool",[79,1658,307],{"class":85},[79,1660,310],{"class":85},[79,1662,1663],{"class":89}," pg.",[79,1665,1666],{"class":116},"Pool",[79,1668,1669],{"class":89},"({ connectionString: process.env.",[79,1671,1672],{"class":133},"DATABASE_URL",[79,1674,1649],{"class":89},[79,1676,1677],{"class":81,"line":152},[79,1678,107],{"emptyLinePlaceholder":106},[79,1680,1681,1683,1686,1688,1691,1693,1695,1697,1699,1701,1703,1706,1708,1711],{"class":81,"line":165},[79,1682,260],{"class":85},[79,1684,1685],{"class":85}," async",[79,1687,263],{"class":85},[79,1689,1690],{"class":116}," getEmbedding",[79,1692,269],{"class":89},[79,1694,39],{"class":126},[79,1696,130],{"class":85},[79,1698,147],{"class":133},[79,1700,289],{"class":89},[79,1702,130],{"class":85},[79,1704,1705],{"class":116}," Promise",[79,1707,997],{"class":89},[79,1709,1710],{"class":133},"number",[79,1712,1713],{"class":89},"[]> {\n",[79,1715,1716,1718,1721,1723,1726,1729,1732],{"class":81,"line":177},[79,1717,301],{"class":85},[79,1719,1720],{"class":133}," response",[79,1722,307],{"class":85},[79,1724,1725],{"class":85}," await",[79,1727,1728],{"class":89}," openai.embeddings.",[79,1730,1731],{"class":116},"create",[79,1733,888],{"class":89},[79,1735,1736,1739,1742],{"class":81,"line":189},[79,1737,1738],{"class":89},"    model: ",[79,1740,1741],{"class":96},"'text-embedding-3-small'",[79,1743,1744],{"class":89},",\n",[79,1746,1747,1750,1753,1755,1758,1760,1762,1765,1767,1770],{"class":81,"line":202},[79,1748,1749],{"class":89},"    input: text.",[79,1751,1752],{"class":116},"replace",[79,1754,269],{"class":89},[79,1756,1757],{"class":96},"\u002F",[79,1759,529],{"class":133},[79,1761,1757],{"class":96},[79,1763,1764],{"class":85},"g",[79,1766,279],{"class":89},[79,1768,1769],{"class":96},"' '",[79,1771,1772],{"class":89},"),\n",[79,1774,1775],{"class":81,"line":208},[79,1776,1195],{"class":89},[79,1778,1779,1781,1784,1787],{"class":81,"line":213},[79,1780,1204],{"class":85},[79,1782,1783],{"class":89}," response.data[",[79,1785,1786],{"class":133},"0",[79,1788,1789],{"class":89},"].embedding;\n",[79,1791,1792],{"class":81,"line":223},[79,1793,205],{"class":89},[79,1795,1796],{"class":81,"line":235},[79,1797,107],{"emptyLinePlaceholder":106},[79,1799,1800,1802,1804,1806,1809,1811,1814,1816,1818,1820,1822,1824,1826,1829],{"class":81,"line":247},[79,1801,260],{"class":85},[79,1803,1685],{"class":85},[79,1805,263],{"class":85},[79,1807,1808],{"class":116}," findRelevantContext",[79,1810,269],{"class":89},[79,1812,1813],{"class":126},"queryText",[79,1815,130],{"class":85},[79,1817,147],{"class":133},[79,1819,289],{"class":89},[79,1821,130],{"class":85},[79,1823,1705],{"class":116},[79,1825,997],{"class":89},[79,1827,1828],{"class":133},"string",[79,1830,1831],{"class":89},"> {\n",[79,1833,1834,1836,1839,1841,1843,1845],{"class":81,"line":252},[79,1835,301],{"class":85},[79,1837,1838],{"class":133}," queryVector",[79,1840,307],{"class":85},[79,1842,1725],{"class":85},[79,1844,1690],{"class":116},[79,1846,1847],{"class":89},"(queryText);\n",[79,1849,1850,1852,1855,1857,1860],{"class":81,"line":257},[79,1851,301],{"class":85},[79,1853,1854],{"class":133}," sql",[79,1856,307],{"class":85},[79,1858,1859],{"class":96}," `SELECT * FROM match_kb_docs($1::vector, 0.75, 3)`",[79,1861,100],{"class":89},[79,1863,1864],{"class":81,"line":298},[79,1865,346],{"class":89},[79,1867,1868,1870,1873,1875,1877,1880,1883],{"class":81,"line":319},[79,1869,301],{"class":85},[79,1871,1872],{"class":133}," client",[79,1874,307],{"class":85},[79,1876,1725],{"class":85},[79,1878,1879],{"class":89}," pool.",[79,1881,1882],{"class":116},"connect",[79,1884,447],{"class":89},[79,1886,1887,1889],{"class":81,"line":343},[79,1888,745],{"class":85},[79,1890,120],{"class":89},[79,1892,1893,1895,1898,1900,1902,1905,1908,1911,1914,1916,1919],{"class":81,"line":349},[79,1894,753],{"class":85},[79,1896,1897],{"class":133}," result",[79,1899,307],{"class":85},[79,1901,1725],{"class":85},[79,1903,1904],{"class":89}," client.",[79,1906,1907],{"class":116},"query",[79,1909,1910],{"class":89},"(sql, [",[79,1912,1913],{"class":133},"JSON",[79,1915,60],{"class":89},[79,1917,1918],{"class":116},"stringify",[79,1920,1921],{"class":89},"(queryVector)]);\n",[79,1923,1924,1926,1929,1932,1935,1938],{"class":81,"line":364},[79,1925,1051],{"class":85},[79,1927,1928],{"class":89}," (result.rows.",[79,1930,1931],{"class":133},"length",[79,1933,1934],{"class":85}," ===",[79,1936,1937],{"class":133}," 0",[79,1939,1940],{"class":89},") {\n",[79,1942,1943,1946,1949],{"class":81,"line":379},[79,1944,1945],{"class":85},"      return",[79,1947,1948],{"class":96}," ''",[79,1950,100],{"class":89},[79,1952,1953],{"class":81,"line":385},[79,1954,1190],{"class":89},[79,1956,1957],{"class":81,"line":390},[79,1958,1959],{"class":393},"    \u002F\u002F Объединяем найденные статьи в один текстовый блок\n",[79,1961,1962,1964,1967,1969,1971,1974,1976,1979,1981,1983,1985,1987,1990,1992,1994],{"class":81,"line":397},[79,1963,367],{"class":85},[79,1965,1966],{"class":89}," result.rows.",[79,1968,480],{"class":116},[79,1970,269],{"class":89},[79,1972,1973],{"class":126},"row",[79,1975,429],{"class":85},[79,1977,1978],{"class":89}," row.content).",[79,1980,521],{"class":116},[79,1982,269],{"class":89},[79,1984,526],{"class":96},[79,1986,529],{"class":133},[79,1988,1989],{"class":96},"---",[79,1991,529],{"class":133},[79,1993,526],{"class":96},[79,1995,340],{"class":89},[79,1997,1998,2000,2003],{"class":81,"line":450},[79,1999,826],{"class":89},[79,2001,2002],{"class":85},"finally",[79,2004,120],{"class":89},[79,2006,2007,2010,2013],{"class":81,"line":455},[79,2008,2009],{"class":89},"    client.",[79,2011,2012],{"class":116},"release",[79,2014,447],{"class":89},[79,2016,2017],{"class":81,"line":461},[79,2018,382],{"class":89},[79,2020,2021],{"class":81,"line":474},[79,2022,205],{"class":89},[26,2024,2026],{"id":2025},"_3-системный-промпт-против-галлюцинаций-ии","3. Системный промпт против галлюцинаций ИИ",[11,2028,2029],{},"Чтобы модель не придумывала несуществующие тарифы и правила, промпт должен жестко запретить генерацию ответов при отсутствии информации в контексте:",[34,2031,2033],{"className":73,"code":2032,"language":75,"meta":40,"style":40},"export async function generateAIResponse(userQuestion: string, context: string): Promise\u003Cstring> {\n  if (!context) {\n    return 'ERROR_UNRESOLVED';\n  }\n\n  const systemPrompt = `\nВы — оператор техподдержки TeleGo.io.\nОтвечайте на вопросы пользователя, используя ТОЛЬКО предоставленный контекст.\nПравила:\n1. Опирайтесь исключительно на предоставленные факты. Не придумывайте ничего от себя.\n2. Если в контексте нет ответа на вопрос или он не связан с темой, ответьте строго одним словом: \"ERROR_UNRESOLVED\".\n3. Пишите вежливо, в деловом стиле.\n\nКонтекст:\n---\n${context}\n---\n`;\n\n  const response = await openai.chat.completions.create({\n    model: 'gpt-4o-mini',\n    messages: [\n      { role: 'system', content: systemPrompt },\n      { role: 'user', content: userQuestion }\n    ],\n    temperature: 0.0, \u002F\u002F Нулевая температура гарантирует стабильный результат\n  });\n\n  return response.choices[0].message.content || 'ERROR_UNRESOLVED';\n}\n",[42,2034,2035,2076,2087,2096,2100,2104,2116,2121,2126,2131,2136,2141,2146,2150,2155,2160,2169,2173,2180,2184,2201,2210,2215,2226,2235,2240,2253,2257,2261,2280],{"__ignoreMap":40},[79,2036,2037,2039,2041,2043,2046,2048,2051,2053,2055,2057,2060,2062,2064,2066,2068,2070,2072,2074],{"class":81,"line":82},[79,2038,260],{"class":85},[79,2040,1685],{"class":85},[79,2042,263],{"class":85},[79,2044,2045],{"class":116}," generateAIResponse",[79,2047,269],{"class":89},[79,2049,2050],{"class":126},"userQuestion",[79,2052,130],{"class":85},[79,2054,147],{"class":133},[79,2056,279],{"class":89},[79,2058,2059],{"class":126},"context",[79,2061,130],{"class":85},[79,2063,147],{"class":133},[79,2065,289],{"class":89},[79,2067,130],{"class":85},[79,2069,1705],{"class":116},[79,2071,997],{"class":89},[79,2073,1828],{"class":133},[79,2075,1831],{"class":89},[79,2077,2078,2080,2082,2084],{"class":81,"line":103},[79,2079,352],{"class":85},[79,2081,355],{"class":89},[79,2083,358],{"class":85},[79,2085,2086],{"class":89},"context) {\n",[79,2088,2089,2091,2094],{"class":81,"line":110},[79,2090,367],{"class":85},[79,2092,2093],{"class":96}," 'ERROR_UNRESOLVED'",[79,2095,100],{"class":89},[79,2097,2098],{"class":81,"line":123},[79,2099,382],{"class":89},[79,2101,2102],{"class":81,"line":139},[79,2103,107],{"emptyLinePlaceholder":106},[79,2105,2106,2108,2111,2113],{"class":81,"line":152},[79,2107,301],{"class":85},[79,2109,2110],{"class":133}," systemPrompt",[79,2112,307],{"class":85},[79,2114,2115],{"class":96}," `\n",[79,2117,2118],{"class":81,"line":165},[79,2119,2120],{"class":96},"Вы — оператор техподдержки TeleGo.io.\n",[79,2122,2123],{"class":81,"line":177},[79,2124,2125],{"class":96},"Отвечайте на вопросы пользователя, используя ТОЛЬКО предоставленный контекст.\n",[79,2127,2128],{"class":81,"line":189},[79,2129,2130],{"class":96},"Правила:\n",[79,2132,2133],{"class":81,"line":202},[79,2134,2135],{"class":96},"1. Опирайтесь исключительно на предоставленные факты. Не придумывайте ничего от себя.\n",[79,2137,2138],{"class":81,"line":208},[79,2139,2140],{"class":96},"2. Если в контексте нет ответа на вопрос или он не связан с темой, ответьте строго одним словом: \"ERROR_UNRESOLVED\".\n",[79,2142,2143],{"class":81,"line":213},[79,2144,2145],{"class":96},"3. Пишите вежливо, в деловом стиле.\n",[79,2147,2148],{"class":81,"line":223},[79,2149,107],{"emptyLinePlaceholder":106},[79,2151,2152],{"class":81,"line":235},[79,2153,2154],{"class":96},"Контекст:\n",[79,2156,2157],{"class":81,"line":247},[79,2158,2159],{"class":96},"---\n",[79,2161,2162,2165,2167],{"class":81,"line":252},[79,2163,2164],{"class":96},"${",[79,2166,2059],{"class":89},[79,2168,205],{"class":96},[79,2170,2171],{"class":81,"line":257},[79,2172,2159],{"class":96},[79,2174,2175,2178],{"class":81,"line":298},[79,2176,2177],{"class":96},"`",[79,2179,100],{"class":89},[79,2181,2182],{"class":81,"line":319},[79,2183,107],{"emptyLinePlaceholder":106},[79,2185,2186,2188,2190,2192,2194,2197,2199],{"class":81,"line":343},[79,2187,301],{"class":85},[79,2189,1720],{"class":133},[79,2191,307],{"class":85},[79,2193,1725],{"class":85},[79,2195,2196],{"class":89}," openai.chat.completions.",[79,2198,1731],{"class":116},[79,2200,888],{"class":89},[79,2202,2203,2205,2208],{"class":81,"line":349},[79,2204,1738],{"class":89},[79,2206,2207],{"class":96},"'gpt-4o-mini'",[79,2209,1744],{"class":89},[79,2211,2212],{"class":81,"line":364},[79,2213,2214],{"class":89},"    messages: [\n",[79,2216,2217,2220,2223],{"class":81,"line":379},[79,2218,2219],{"class":89},"      { role: ",[79,2221,2222],{"class":96},"'system'",[79,2224,2225],{"class":89},", content: systemPrompt },\n",[79,2227,2228,2230,2232],{"class":81,"line":385},[79,2229,2219],{"class":89},[79,2231,767],{"class":96},[79,2233,2234],{"class":89},", content: userQuestion }\n",[79,2236,2237],{"class":81,"line":390},[79,2238,2239],{"class":89},"    ],\n",[79,2241,2242,2245,2248,2250],{"class":81,"line":397},[79,2243,2244],{"class":89},"    temperature: ",[79,2246,2247],{"class":133},"0.0",[79,2249,279],{"class":89},[79,2251,2252],{"class":393},"\u002F\u002F Нулевая температура гарантирует стабильный результат\n",[79,2254,2255],{"class":81,"line":450},[79,2256,1195],{"class":89},[79,2258,2259],{"class":81,"line":455},[79,2260,107],{"emptyLinePlaceholder":106},[79,2262,2263,2265,2268,2270,2273,2276,2278],{"class":81,"line":461},[79,2264,1204],{"class":85},[79,2266,2267],{"class":89}," response.choices[",[79,2269,1786],{"class":133},[79,2271,2272],{"class":89},"].message.content ",[79,2274,2275],{"class":85},"||",[79,2277,2093],{"class":96},[79,2279,100],{"class":89},[79,2281,2282],{"class":81,"line":474},[79,2283,205],{"class":89},[26,2285,2287],{"id":2286},"_4-логика-маршрутизации-и-передача-человеку","4. Логика маршрутизации и передача человеку",[11,2289,2290,2291,2294],{},"При получении сообщения в Telegram проверяем, не общается ли пользователь с живым оператором. Если нет — запускаем RAG-цикл. Если ИИ вернул ",[42,2292,2293],{},"ERROR_UNRESOLVED"," — переключаем статус чата.",[34,2296,2298],{"className":73,"code":2297,"language":75,"meta":40,"style":40},"import TelegramBot from 'node-telegram-bot-api';\n\nconst bot = new TelegramBot(process.env.TELEGRAM_BOT_TOKEN!, { polling: true });\n\nbot.on('message', async (msg) => {\n  const chatId = msg.chat.id;\n  const userText = msg.text;\n\n  if (!userText) return;\n\n  \u002F\u002F 1. Получаем статус сессии из БД\n  const session = await db.getChatSession(chatId);\n\n  \u002F\u002F Если чат переключен на человека, пересылаем сообщения в Slack\u002Fпанель менеджеров\n  if (session?.status === 'human_handling') {\n    await forwardToManagers(chatId, msg);\n    return;\n  }\n\n  \u002F\u002F 2. Выполняем RAG-поиск\n  try {\n    const context = await findRelevantContext(userText);\n    const aiResponse = await generateAIResponse(userText, context);\n\n    \u002F\u002F 3. Проверка на эскалацию\n    if (aiResponse.trim() === 'ERROR_UNRESOLVED') {\n      \u002F\u002F Обновляем статус сессии в базе данных\n      await db.updateChatSession(chatId, { status: 'human_handling' });\n      \n      \u002F\u002F Оповещаем менеджеров через вебхук\n      await notifySupportStaff(chatId, userText);\n      \n      await bot.sendMessage(\n        chatId, \n        \"К сожалению, я не нашел ответ в документации. Я переключил этот чат на службу поддержки. Специалист ответит вам в ближайшее время.\"\n      );\n      return;\n    }\n\n    \u002F\u002F 4. Отправляем ИИ-ответ пользователю\n    await bot.sendMessage(chatId, aiResponse);\n\n  } catch (error) {\n    console.error('Ошибка в цикле RAG:', error);\n    await bot.sendMessage(chatId, \"Произошла ошибка. Перевожу диалог на оператора.\");\n    await db.updateChatSession(chatId, { status: 'human_handling' });\n  }\n});\n",[42,2299,2300,2314,2318,2347,2351,2381,2393,2405,2409,2425,2429,2434,2454,2458,2463,2477,2488,2494,2498,2502,2507,2513,2529,2545,2549,2554,2573,2578,2596,2600,2605,2615,2619,2632,2637,2642,2647,2653,2657,2661,2666,2677,2681,2689,2705,2721,2735,2739],{"__ignoreMap":40},[79,2301,2302,2304,2307,2309,2312],{"class":81,"line":82},[79,2303,86],{"class":85},[79,2305,2306],{"class":89}," TelegramBot ",[79,2308,93],{"class":85},[79,2310,2311],{"class":96}," 'node-telegram-bot-api'",[79,2313,100],{"class":89},[79,2315,2316],{"class":81,"line":103},[79,2317,107],{"emptyLinePlaceholder":106},[79,2319,2320,2322,2325,2327,2329,2332,2335,2338,2340,2343,2345],{"class":81,"line":110},[79,2321,1630],{"class":85},[79,2323,2324],{"class":133}," bot",[79,2326,307],{"class":85},[79,2328,310],{"class":85},[79,2330,2331],{"class":116}," TelegramBot",[79,2333,2334],{"class":89},"(process.env.",[79,2336,2337],{"class":133},"TELEGRAM_BOT_TOKEN",[79,2339,358],{"class":85},[79,2341,2342],{"class":89},", { polling: ",[79,2344,817],{"class":133},[79,2346,1649],{"class":89},[79,2348,2349],{"class":81,"line":123},[79,2350,107],{"emptyLinePlaceholder":106},[79,2352,2353,2356,2359,2361,2364,2366,2369,2371,2374,2377,2379],{"class":81,"line":139},[79,2354,2355],{"class":89},"bot.",[79,2357,2358],{"class":116},"on",[79,2360,269],{"class":89},[79,2362,2363],{"class":96},"'message'",[79,2365,279],{"class":89},[79,2367,2368],{"class":85},"async",[79,2370,355],{"class":89},[79,2372,2373],{"class":126},"msg",[79,2375,2376],{"class":89},") ",[79,2378,1023],{"class":85},[79,2380,120],{"class":89},[79,2382,2383,2385,2388,2390],{"class":81,"line":152},[79,2384,301],{"class":85},[79,2386,2387],{"class":133}," chatId",[79,2389,307],{"class":85},[79,2391,2392],{"class":89}," msg.chat.id;\n",[79,2394,2395,2397,2400,2402],{"class":81,"line":165},[79,2396,301],{"class":85},[79,2398,2399],{"class":133}," userText",[79,2401,307],{"class":85},[79,2403,2404],{"class":89}," msg.text;\n",[79,2406,2407],{"class":81,"line":177},[79,2408,107],{"emptyLinePlaceholder":106},[79,2410,2411,2413,2415,2417,2420,2423],{"class":81,"line":189},[79,2412,352],{"class":85},[79,2414,355],{"class":89},[79,2416,358],{"class":85},[79,2418,2419],{"class":89},"userText) ",[79,2421,2422],{"class":85},"return",[79,2424,100],{"class":89},[79,2426,2427],{"class":81,"line":202},[79,2428,107],{"emptyLinePlaceholder":106},[79,2430,2431],{"class":81,"line":208},[79,2432,2433],{"class":393},"  \u002F\u002F 1. Получаем статус сессии из БД\n",[79,2435,2436,2438,2441,2443,2445,2448,2451],{"class":81,"line":213},[79,2437,301],{"class":85},[79,2439,2440],{"class":133}," session",[79,2442,307],{"class":85},[79,2444,1725],{"class":85},[79,2446,2447],{"class":89}," db.",[79,2449,2450],{"class":116},"getChatSession",[79,2452,2453],{"class":89},"(chatId);\n",[79,2455,2456],{"class":81,"line":223},[79,2457,107],{"emptyLinePlaceholder":106},[79,2459,2460],{"class":81,"line":235},[79,2461,2462],{"class":393},"  \u002F\u002F Если чат переключен на человека, пересылаем сообщения в Slack\u002Fпанель менеджеров\n",[79,2464,2465,2467,2470,2472,2475],{"class":81,"line":247},[79,2466,352],{"class":85},[79,2468,2469],{"class":89}," (session?.status ",[79,2471,692],{"class":85},[79,2473,2474],{"class":96}," 'human_handling'",[79,2476,1940],{"class":89},[79,2478,2479,2482,2485],{"class":81,"line":252},[79,2480,2481],{"class":85},"    await",[79,2483,2484],{"class":116}," forwardToManagers",[79,2486,2487],{"class":89},"(chatId, msg);\n",[79,2489,2490,2492],{"class":81,"line":257},[79,2491,367],{"class":85},[79,2493,100],{"class":89},[79,2495,2496],{"class":81,"line":298},[79,2497,382],{"class":89},[79,2499,2500],{"class":81,"line":319},[79,2501,107],{"emptyLinePlaceholder":106},[79,2503,2504],{"class":81,"line":343},[79,2505,2506],{"class":393},"  \u002F\u002F 2. Выполняем RAG-поиск\n",[79,2508,2509,2511],{"class":81,"line":349},[79,2510,745],{"class":85},[79,2512,120],{"class":89},[79,2514,2515,2517,2520,2522,2524,2526],{"class":81,"line":364},[79,2516,753],{"class":85},[79,2518,2519],{"class":133}," context",[79,2521,307],{"class":85},[79,2523,1725],{"class":85},[79,2525,1808],{"class":116},[79,2527,2528],{"class":89},"(userText);\n",[79,2530,2531,2533,2536,2538,2540,2542],{"class":81,"line":379},[79,2532,753],{"class":85},[79,2534,2535],{"class":133}," aiResponse",[79,2537,307],{"class":85},[79,2539,1725],{"class":85},[79,2541,2045],{"class":116},[79,2543,2544],{"class":89},"(userText, context);\n",[79,2546,2547],{"class":81,"line":385},[79,2548,107],{"emptyLinePlaceholder":106},[79,2550,2551],{"class":81,"line":390},[79,2552,2553],{"class":393},"    \u002F\u002F 3. Проверка на эскалацию\n",[79,2555,2556,2558,2561,2564,2567,2569,2571],{"class":81,"line":397},[79,2557,1051],{"class":85},[79,2559,2560],{"class":89}," (aiResponse.",[79,2562,2563],{"class":116},"trim",[79,2565,2566],{"class":89},"() ",[79,2568,692],{"class":85},[79,2570,2093],{"class":96},[79,2572,1940],{"class":89},[79,2574,2575],{"class":81,"line":450},[79,2576,2577],{"class":393},"      \u002F\u002F Обновляем статус сессии в базе данных\n",[79,2579,2580,2583,2585,2588,2591,2594],{"class":81,"line":455},[79,2581,2582],{"class":85},"      await",[79,2584,2447],{"class":89},[79,2586,2587],{"class":116},"updateChatSession",[79,2589,2590],{"class":89},"(chatId, { status: ",[79,2592,2593],{"class":96},"'human_handling'",[79,2595,1649],{"class":89},[79,2597,2598],{"class":81,"line":461},[79,2599,1082],{"class":89},[79,2601,2602],{"class":81,"line":474},[79,2603,2604],{"class":393},"      \u002F\u002F Оповещаем менеджеров через вебхук\n",[79,2606,2607,2609,2612],{"class":81,"line":516},[79,2608,2582],{"class":85},[79,2610,2611],{"class":116}," notifySupportStaff",[79,2613,2614],{"class":89},"(chatId, userText);\n",[79,2616,2617],{"class":81,"line":536},[79,2618,1082],{"class":89},[79,2620,2621,2623,2626,2629],{"class":81,"line":541},[79,2622,2582],{"class":85},[79,2624,2625],{"class":89}," bot.",[79,2627,2628],{"class":116},"sendMessage",[79,2630,2631],{"class":89},"(\n",[79,2633,2634],{"class":81,"line":547},[79,2635,2636],{"class":89},"        chatId, \n",[79,2638,2639],{"class":81,"line":553},[79,2640,2641],{"class":96},"        \"К сожалению, я не нашел ответ в документации. Я переключил этот чат на службу поддержки. Специалист ответит вам в ближайшее время.\"\n",[79,2643,2644],{"class":81,"line":566},[79,2645,2646],{"class":89},"      );\n",[79,2648,2649,2651],{"class":81,"line":586},[79,2650,1945],{"class":85},[79,2652,100],{"class":89},[79,2654,2655],{"class":81,"line":597},[79,2656,1190],{"class":89},[79,2658,2659],{"class":81,"line":607},[79,2660,107],{"emptyLinePlaceholder":106},[79,2662,2663],{"class":81,"line":612},[79,2664,2665],{"class":393},"    \u002F\u002F 4. Отправляем ИИ-ответ пользователю\n",[79,2667,2668,2670,2672,2674],{"class":81,"line":618},[79,2669,2481],{"class":85},[79,2671,2625],{"class":89},[79,2673,2628],{"class":116},[79,2675,2676],{"class":89},"(chatId, aiResponse);\n",[79,2678,2679],{"class":81,"line":630},[79,2680,107],{"emptyLinePlaceholder":106},[79,2682,2683,2685,2687],{"class":81,"line":644},[79,2684,826],{"class":89},[79,2686,829],{"class":85},[79,2688,832],{"class":89},[79,2690,2691,2694,2697,2699,2702],{"class":81,"line":654},[79,2692,2693],{"class":89},"    console.",[79,2695,2696],{"class":116},"error",[79,2698,269],{"class":89},[79,2700,2701],{"class":96},"'Ошибка в цикле RAG:'",[79,2703,2704],{"class":89},", error);\n",[79,2706,2707,2709,2711,2713,2716,2719],{"class":81,"line":668},[79,2708,2481],{"class":85},[79,2710,2625],{"class":89},[79,2712,2628],{"class":116},[79,2714,2715],{"class":89},"(chatId, ",[79,2717,2718],{"class":96},"\"Произошла ошибка. Перевожу диалог на оператора.\"",[79,2720,340],{"class":89},[79,2722,2723,2725,2727,2729,2731,2733],{"class":81,"line":673},[79,2724,2481],{"class":85},[79,2726,2447],{"class":89},[79,2728,2587],{"class":116},[79,2730,2590],{"class":89},[79,2732,2593],{"class":96},[79,2734,1649],{"class":89},[79,2736,2737],{"class":81,"line":679},[79,2738,382],{"class":89},[79,2740,2741],{"class":81,"line":698},[79,2742,2743],{"class":89},"});\n",[11,2745,2746,2747,2749],{},"В моем проекте ",[1270,2748,1273],{"href":1272}," мы создали аналогичную RAG-систему для обработки частых вопросов пользователей по настройкам ботов и платежам. Если пользователь задает сложный технический вопрос, на который нет готового ответа, сессия автоматически переводится на панель оператора.",[11,2751,2752,2753,2757,2758,60],{},"Этот процесс эскалации становится еще эффективнее, если связать его с ",[1270,2754,2756],{"href":2755},"\u002Fru\u002Fblog\u002Ftelegram-bot-crm-integration","Интеграцией Telegram-ботов с CRM"," для автоматического логирования событий. А саму панель общения оператора с клиентами можно удобно разместить прямо внутри ",[1270,2759,2761],{"href":2760},"\u002Fru\u002Fblog\u002Ftelegram-web-apps-sells","Telegram Web App",[26,2763,1290],{"id":1289},[1292,2765,2766,2774,2781],{},[1232,2767,2768,2773],{},[1270,2769,2772],{"href":2770,"rel":2771},"https:\u002F\u002Fplatform.openai.com\u002Fdocs\u002Fguides\u002Fembeddings",[1300],"OpenAI Embeddings"," — генерация векторов, модель text-embedding-3-small из примера",[1232,2775,2776,2780],{},[1270,2777,1395],{"href":2778,"rel":2779},"https:\u002F\u002Fgithub.com\u002Fpgvector\u002Fpgvector",[1300]," — расширение PostgreSQL для векторного поиска",[1232,2782,2783,2788],{},[1270,2784,2787],{"href":2785,"rel":2786},"https:\u002F\u002Fcore.telegram.org\u002Fbots\u002Fapi",[1300],"Telegram Bot API"," — приём сообщений и long polling",[11,2790,2791],{},"Внедрение RAG-архитектуры с автоматическим переключением на оператора кратно снижает затраты на поддержку: типовые вопросы закрывает ИИ, а люди подключаются только к сложным диалогам — без потери качества сервиса.",[11,2793,2794,2795,1321,2798,60],{},"Если вы хотите разработать умного ИИ-бота для техподдержки, настроить векторный поиск Pgvector или интегрировать чаты с AmoCRM\u002FBitrix24, ознакомьтесь с моей услугой ",[1270,2796,2797],{"href":1319},"Создания Telegram-ботов под ключ",[1270,2799,1325],{"href":1324},[1328,2801,2802],{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html pre.shiki code .snl16, html code.shiki .snl16{--shiki-default:#F97583}html pre.shiki code .s95oV, html code.shiki .s95oV{--shiki-default:#E1E4E8}html pre.shiki code .sU2Wk, html code.shiki .sU2Wk{--shiki-default:#9ECBFF}html pre.shiki code .sDLfK, html code.shiki .sDLfK{--shiki-default:#79B8FF}html pre.shiki code .svObZ, html code.shiki .svObZ{--shiki-default:#B392F0}html pre.shiki code .s9osk, html code.shiki .s9osk{--shiki-default:#FFAB70}html pre.shiki code .sAwPA, html code.shiki .sAwPA{--shiki-default:#6A737D}",{"title":40,"searchDepth":103,"depth":103,"links":2804},[2805,2806,2807,2808,2809,2810],{"id":1399,"depth":103,"text":1400},{"id":1409,"depth":103,"text":1410},{"id":1584,"depth":103,"text":1585},{"id":2025,"depth":103,"text":2026},{"id":2286,"depth":103,"text":2287},{"id":1289,"depth":103,"text":1290},"2026-06-04","Техническое руководство по проектированию бота поддержки в Telegram с использованием OpenAI Embeddings, PostgreSQL Pgvector и гибридной системы эскалации на оператора.",[2814,2817,2820],{"q":2815,"a":2816},"Чем RAG-бот отличается от «просто ChatGPT в боте»?","RAG-бот отвечает строго по вашей базе знаний: сначала находит релевантные фрагменты документации через векторный поиск, затем передаёт их модели как контекст. Это устраняет главные проблемы «голой» LLM — галлюцинации и незнание ваших цен, инструкций и API.",{"q":2818,"a":2819},"Что происходит, если бот не знает ответа?","Срабатывает протокол эскалации: если векторный поиск не нашёл релевантного контекста или модель вернула маркер неуверенности, сессия помечается как human_handling, менеджеры получают уведомление, а все следующие сообщения клиента пересылаются живому оператору.",{"q":2821,"a":2822},"Сколько стоит эксплуатация такого бота?","Две статьи расходов: генерация эмбеддингов при индексации базы знаний (разовая и копеечная) и вызовы LLM на каждый вопрос — оплата по фактическому использованию API. На типовом потоке вопросов это заметно дешевле ставки оператора поддержки.","\u002Fimages\u002Fblog\u002Fblog_ai_rag_support.jpg",{},"\u002Fblog\u002Fru\u002Ftelegram-bot-ai-rag-support",{"title":1370,"description":2812},"blog\u002Fru\u002Ftelegram-bot-ai-rag-support",[1362,2829,2830,2831,1365,2832],"AI","RAG","LLM","PostgreSQL","225RZ1ngfAy8AELI0CQam9IZb61lmQOrlgyE7TxMJJI",1784561425139]