v1.7.0: refactor max_bot to flat structure, add VCF+UserModel+NLP history context, portal pages and proxy fixes
- Refactored max_bot from nested packages to flat module structure - Q2: Extended BotUser model (patronymic, email, org, address, vcf_raw, contact_hash, phone_verified, email_verified, last_interaction, total_conversations, total_tickets) - Q2: VCF parser (FN, N, TEL, EMAIL, ORG, ADR), upsert on re-contact, NLP history context (_get_user_history_context -> YandexGPT) - Q1: Broadcast preview modal with 10s confirmation timer - Q3: CSS var(--white)->var(--bg-card), var(--text)->var(--text-primary) - Q4: bot_settings showNotification(), editable max_bot_id - Q5: Webhook secret passthrough via X-Max-Bot-Api-Secret - Masking sensitive keys, dialog_cleared handler, migrate via _add_column_if_not_exists() - Rate limit (asyncio.sleep 0.5 per 10), dead code removed, conv.intent context in contact.py - Portal pages: bot_consent, bot_kb (edit), bot_settings, bot_test, bot_tickets, portal_settings - Tests: 21/21 passing, added test_yandex_gpt.py, test_email_sender.py - Deploy: deploy_full.sh, schema.sql, seed_knowledge_base.sql
This commit is contained in:
@@ -0,0 +1,529 @@
|
||||
29.5.4. Использование webhook
|
||||
29.5.4.1. Схема использования webhook
|
||||
Как было сказано ранее, при использовании вебхуков, интеграция работает только в одну сторону: сообщения передаются со стороны внешней системы в систему взаимодействия и, затем, в прикладное решение.
|
||||
|
||||
Пользователь, который будет олицетворять систему, которая отправляет сообщения в прикладное решение, будет называться пользователем интеграции. Для того, чтобы внешняя система могла пользоваться вебхуком, ей необходимо сообщить адрес точки подключения, который можно получить с помощью метода ИнтеграцияСистемыВзаимодействия.НавигационнаяСсылкаТочкиПодключения(). Этот адрес следует использовать при настройки системы, которая будет отправлять сообщения.
|
||||
|
||||
После того, как сообщение от внешней системы попадает в точку подключения, выполняются следующие действия:
|
||||
|
||||
● Тело сообщения разбирается.
|
||||
|
||||
● Создается пользователь интеграции, если он еще не создан.
|
||||
|
||||
В качестве имени пользователя интеграции выступает:
|
||||
|
||||
● Имя пользователя, переданное в сообщении createUser.
|
||||
|
||||
● Имя экземпляра интеграции, если в сообщении внешней системы не указан пользователь (автор сообщения).
|
||||
|
||||
● Создается обсуждение системы взаимодействия, если оно еще не создано.
|
||||
|
||||
● В качестве участников обсуждения выступают:
|
||||
|
||||
● Пользователь интеграции.
|
||||
|
||||
● Пользователи, заданные при настройке интеграции как участники по умолчанию.
|
||||
|
||||
● Если необходимо, чтобы сообщения попадали в заранее созданное обсуждение с выбранным составом участников, то следует:
|
||||
|
||||
● Предварительно создать обсуждение с помощью встроенного языка.
|
||||
|
||||
● Передать идентификатор созданного обсуждения внешней системе, чтобы эта система использовала этот идентификатор в своих сообщениях. Строковое представление идентификатора обсуждения можно получить с помощью функции Строка(), параметром которой выступает объект типа ОбсуждениеСистемыВзаимодействия.Идентификатор.
|
||||
|
||||
● В обсуждение добавляется сообщение, автором которого является пользователь интеграции и содержимое определено сообщением внешней системы.
|
||||
|
||||
29.5.4.2. Поддерживаемые команды
|
||||
29.5.4.2.1. Общая информация
|
||||
В качестве команды выступает POST-запроса внешней системы. Этот запрос должен удовлетворять следующим требованиям:
|
||||
|
||||
● Тело запроса формируется в кодировке UTF-8 без BOM.
|
||||
|
||||
● Тело запроса представляет собой JSON-документ.
|
||||
|
||||
● Тело запроса, в общем, имеет следующий вид:
|
||||
|
||||
Копировать в буфер обмена
|
||||
{
|
||||
"command" : {
|
||||
"param1": value1,
|
||||
…
|
||||
"paramN": valueN
|
||||
}
|
||||
}
|
||||
Если в команде допускается текст в формате HTML, то в этом случае поддерживаются следующие возможности оформления:
|
||||
|
||||
● Поддерживаемые элементы: a, b, big, font, i, li, ol, s, small, span, strike, strong, u, ul.
|
||||
|
||||
● Атрибут элемента style: background-color, color, font-family, font-size, font-style, font-weight, text-decoration.
|
||||
|
||||
Результат выполнения команды отражается кодом возврата:
|
||||
|
||||
● 200 OK ‑ команда выполнена успешно.
|
||||
|
||||
● 404 No active integration ‑ интеграция не найдена или не активна.
|
||||
|
||||
● 409 Conflict ‑ при вызове команды create… для уже существующего объекта.
|
||||
|
||||
● 500 <Текст ошибки> ‑ при обработке команды произошла ошибка.
|
||||
|
||||
Далее в разделе будет описаны форматы команд.
|
||||
|
||||
29.5.4.2.2. createConversation
|
||||
Описание:
|
||||
|
||||
Создает новое обсуждение.
|
||||
|
||||
Синтаксис:
|
||||
|
||||
Копировать в буфер обмена
|
||||
{
|
||||
"createConversation": {
|
||||
"extConversationId": "1",
|
||||
"title": "Заголовок обсуждения",
|
||||
"extUserId": "1",
|
||||
"members": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
}
|
||||
Параметры:
|
||||
|
||||
● extConversationId ‑ внешний идентификатор создаваемого обсуждения.
|
||||
|
||||
● title ‑ заголовок создаваемого обсуждения (в виде строки).
|
||||
|
||||
● extUserId ‑ внешний идентификатор пользователя, от имени которого создается обсуждение.
|
||||
|
||||
Если не задан, то будет создан пользователь системы взаимодействия с именем, совпадающим с именем интеграции, и он будет добавлен в обсуждение. В этом случае во всех запросах, относящихся к этому обсуждению, не должны быть указаны параметры extUserId, members, addMembers, removeMembers.
|
||||
|
||||
Необязательный параметр.
|
||||
|
||||
● members ‑ массив внешних идентификаторов пользователей, которые являются участниками обсуждения со стороны внешней системы.
|
||||
|
||||
Необязательный параметр.
|
||||
|
||||
29.5.4.2.3. createMessage
|
||||
Описание:
|
||||
|
||||
Создает новое сообщение в обсуждении.
|
||||
|
||||
Синтаксис:
|
||||
|
||||
Копировать в буфер обмена
|
||||
{
|
||||
"createMessage": {
|
||||
"extId": "1",
|
||||
"text": "Тест сообщения",
|
||||
"textFormat": "text/plain",
|
||||
"extUserId": "1",
|
||||
"extConversationId": "1"
|
||||
}
|
||||
}
|
||||
Параметры:
|
||||
|
||||
● extId ‑ внешний идентификатор создаваемого сообщения.
|
||||
|
||||
● text ‑ текст сообщения.
|
||||
|
||||
● textFormat ‑ формат текста сообщения. Возможные значения: text/plain, text/html.
|
||||
|
||||
Необязательный параметр. Значение по умолчанию ‑ text/plain.
|
||||
|
||||
● extUserId ‑ внешний идентификатор пользователя-автора сообщения.
|
||||
|
||||
Если не задан, то будет создан пользователь системы взаимодействия с именем, совпадающим с именем интеграции, и он будет добавлен в обсуждение. В этом случае во всех запросах, относящихся к этому обсуждению, не должны быть указаны параметры extUserId, members, addMembers, removeMembers.
|
||||
|
||||
Необязательный параметр.
|
||||
|
||||
● extConversationId ‑ внешний идентификатор обсуждения, в котором создается сообщение.
|
||||
|
||||
29.5.4.2.4. createUser
|
||||
Описание:
|
||||
|
||||
Создает пользователя интеграции в системе взаимодействия.
|
||||
|
||||
Синтаксис:
|
||||
|
||||
Копировать в буфер обмена
|
||||
{
|
||||
"createUser" : {
|
||||
"extUserId": ,
|
||||
"name": ,
|
||||
"fullName": ,
|
||||
"picture":
|
||||
}
|
||||
}
|
||||
Параметры:
|
||||
|
||||
● extUserId ‑ внешний идентификатор внешнего пользователя.
|
||||
|
||||
● name ‑ короткое имя внешнего пользователя (в виде строки).
|
||||
|
||||
● fullName ‑ полное имя внешнего пользователя (в виде строки). Может быть пустой строкой.
|
||||
|
||||
● picture ‑ картинка пользователя в виде строки в формате base64. Необязательный параметр.
|
||||
|
||||
29.5.4.2.5. updateConversation
|
||||
Описание:
|
||||
|
||||
Изменяет параметры обсуждения. Позволяет изменить заголовок обсуждения, а также добавить или удалить пользователей обсуждения.
|
||||
|
||||
Синтаксис:
|
||||
|
||||
Копировать в буфер обмена
|
||||
{
|
||||
"updateConversation": {
|
||||
"extConversationId": "1",
|
||||
"title": "Новый заголовок обсуждения",
|
||||
"extUserId": "5",
|
||||
"addMembers": [
|
||||
"5"
|
||||
],
|
||||
"removeMembers": [
|
||||
"1"
|
||||
]
|
||||
}
|
||||
}
|
||||
Параметры:
|
||||
|
||||
● extConversationId ‑ внешний идентификатор изменяемого обсуждения.
|
||||
|
||||
● title ‑ заголовок обсуждения.
|
||||
|
||||
Необязательный параметр.
|
||||
|
||||
● extUserId ‑ внешний идентификатор пользователя, от имени которого изменяется обсуждение.
|
||||
|
||||
Если не задан, то будет создан пользователь системы взаимодействия с именем, совпадающим с именем интеграции, и он будет добавлен в обсуждение. В этом случае во всех запросах, относящихся к этому обсуждению, не должны быть указаны параметры extUserId, members, addMembers, removeMembers.
|
||||
|
||||
Необязательный пользователь.
|
||||
|
||||
● addMembers ‑ массив внешних идентификаторов пользователей, которые будут добавлены к участникам обсуждения.
|
||||
|
||||
Необязательный параметр.
|
||||
|
||||
● removeMembers ‑ массив внешних идентификаторов пользователей, которые будут удалены из участников обсуждения.
|
||||
|
||||
Необязательный параметр.
|
||||
|
||||
29.5.4.2.6. updateMessage
|
||||
Описание:
|
||||
|
||||
Изменяет существующее сообщение.
|
||||
|
||||
Синтаксис:
|
||||
|
||||
Копировать в буфер обмена
|
||||
{
|
||||
"updateMessage": {
|
||||
"extId": "1",
|
||||
"text": "Тест сообщения",
|
||||
"textFormat": "text/plain",
|
||||
"extUserId": "1",
|
||||
"extConversationId": "1"
|
||||
}
|
||||
}
|
||||
Параметры:
|
||||
|
||||
● extId ‑ внешний идентификатор изменяемого сообщения.
|
||||
|
||||
● text ‑ текст сообщения.
|
||||
|
||||
● textFormat ‑ формат текста сообщения. Возможные значения: text/plain, text/html.
|
||||
|
||||
Необязательный параметр. Значение по умолчанию ‑ text/plain.
|
||||
|
||||
● extUserId ‑ внешний идентификатор пользователя-автора сообщения.
|
||||
|
||||
Если не задан, то будет создан пользователь системы взаимодействия с именем, совпадающим с именем интеграции, и он будет добавлен в обсуждение. В этом случае во всех запросах, относящихся к этому обсуждению, не должны быть указаны параметры extUserId, members, addMembers, removeMembers.
|
||||
|
||||
Необязательный параметр.
|
||||
|
||||
● extConversationId ‑ внешний идентификатор обсуждения, в котором изменяется сообщение.
|
||||
|
||||
29.5.4.2.7. updateUser
|
||||
Описание:
|
||||
|
||||
Обновляет параметры пользователя интеграции в системе взаимодействия.
|
||||
|
||||
Синтаксис:
|
||||
|
||||
Копировать в буфер обмена
|
||||
{
|
||||
"updateUser" : {
|
||||
"extUserId": ,
|
||||
"name": ,
|
||||
"fullName": ,
|
||||
"picture":
|
||||
}
|
||||
}
|
||||
Параметры:
|
||||
|
||||
● extUserId ‑ внешний идентификатор внешнего пользователя.
|
||||
|
||||
● name ‑ новое короткое имя внешнего пользователя (в виде строки). Необязательный параметр.
|
||||
|
||||
● fullName ‑ новое полное имя внешнего пользователя (в виде строки). Необязательный параметр.
|
||||
|
||||
● picture ‑ новая картинка пользователя в виде строки в формате base64. Необязательный параметр.
|
||||
|
||||
29.5.5. Чат на сайте
|
||||
29.5.5.1. Общая информация
|
||||
Данный способ интеграции предназначен для решения следующей задачи: необходимо обеспечить возможность для пользователя некоторого сайта общаться с компанией, которая использует этот сайт для выполнения своей деятельности. На сайте может быть интернет-магазин, витрина с примерами продукции и т. д.
|
||||
|
||||
Для того, чтобы реализовать такую интеграцию, необходимо выполнить следующие действия:
|
||||
|
||||
1. Доработать реализацию сайта (как клиентскую, так и серверную части) так, чтобы на этом сайте пользователь смог начать разговор в чате. При необходимости, код сайта может использовать программный интерфейс чата для выполнения некоторых действий.
|
||||
|
||||
2. Создать нужный тип интеграции в системе «1С:Предприятие».
|
||||
|
||||
3. Реализовать на встроенном языке системы «1С:Предприятие» требуемое взаимодействие с чатом, если необходимо. Если чат используется только в режиме мессенджера (т. е. только для интерактивного обмена текстовыми сообщениями) ‑ никаких дополнительных действий выполнять не требуется.
|
||||
|
||||
29.5.5.2. Интерфейс чата
|
||||
Для того, чтобы подключить чат к веб-сайту, необходимо в код веб-сайта вставить следующий фрагмент кода:
|
||||
|
||||
Копировать в буфер обмена
|
||||
<script src="<URL точки подключения>" async></script>
|
||||
URL точки подключения можно получить или при создании интеграции (в стандартной обработке управления системой взаимодействия) или с помощью метода НавигационнаяСсылкаТочкиПодключения() объекта типа ИнтеграцияСистемыВзаимодействия.
|
||||
|
||||
После загрузки интерфейса чата, в глобальном контексте сайта становится доступен объект CollaborationSystemWebChat1CE (типа CollaborationSystemWebChat1CEClass). Через этот объект возможно взаимодействие с чатом.
|
||||
|
||||
Объект CollaborationSystemWebChat1CE предоставляет следующие методы:
|
||||
|
||||
open()
|
||||
|
||||
Описание:
|
||||
|
||||
Разворачивает окно чата.
|
||||
|
||||
Возвращаемое значение:
|
||||
|
||||
void.
|
||||
|
||||
close()
|
||||
|
||||
Описание:
|
||||
|
||||
Сворачивает окно чата.
|
||||
|
||||
Возвращаемое значение:
|
||||
|
||||
void.
|
||||
|
||||
setContactInfo(<contactInfo>)
|
||||
|
||||
Описание:
|
||||
|
||||
Устанавливает контактные данные пользователя.
|
||||
|
||||
Параметры:
|
||||
|
||||
contactInfoобязательный
|
||||
|
||||
Тип: Object.
|
||||
|
||||
Объект, содержащий значения полей контактных данных пользователя. Объект содержит следующие свойства:
|
||||
|
||||
● name ‑ тип String ‑ содержит имя пользователя.
|
||||
|
||||
● fullName ‑ тип String ‑ содержит полное имя пользователя.
|
||||
|
||||
● email ‑ тип String ‑ содержит адрес электронной почты пользователя.
|
||||
|
||||
● phone ‑ тип String ‑ содержит номер телефона пользователя.
|
||||
|
||||
Возвращаемое значение:
|
||||
|
||||
void.
|
||||
|
||||
getContactInfo()
|
||||
|
||||
Описание:
|
||||
|
||||
Возвращает поданные пользователем в форме представления контактные данные.
|
||||
|
||||
Возвращаемое значение:
|
||||
|
||||
Promise<Object>. Результатом выполнения обещания является значение типа Object, которое содержит следующие свойства:
|
||||
|
||||
● name ‑ тип String ‑ содержит имя пользователя.
|
||||
|
||||
● fullName ‑ тип String ‑ содержит полное имя пользователя.
|
||||
|
||||
● email ‑ тип String ‑ содержит адрес электронной почты пользователя.
|
||||
|
||||
● phone ‑ тип String ‑ содержит номер телефона пользователя.
|
||||
|
||||
setMatchingKeyToken(<matchingKeyToken>)
|
||||
|
||||
Описание:
|
||||
|
||||
Устанавливает токен с ключом сопоставления пользователя системы взаимодействия и пользователя на сайте.
|
||||
|
||||
Параметры:
|
||||
|
||||
matchingKeyTokenобязательный
|
||||
|
||||
Тип: String.
|
||||
|
||||
Токен, содержащий ключ сопоставления пользователя системы взаимодействия и пользователя на сайте. Токен может быть подписан (JWS) с использованием созданного в параметрах интеграции ключа подписи signKey или не подписан (JWT). Данный ключ сопоставления будет доступен через свойство ИдентификаторПользователяВнешнейСистемы типа ПользовательСистемыВзаимодействия.
|
||||
|
||||
Возвращаемое значение:
|
||||
|
||||
void.
|
||||
|
||||
logout()
|
||||
|
||||
Описание:
|
||||
|
||||
Осуществляет завершение сеанса для текущего в чате пользователя.
|
||||
|
||||
Возвращаемое значение:
|
||||
|
||||
Promise<void>. Результатом выполнения обещания является значение типа void.
|
||||
|
||||
isVideoconferenceEnabled()
|
||||
|
||||
Описание:
|
||||
|
||||
Возвращает информацию о доступности видеозвонков.
|
||||
|
||||
Возвращаемое значение:
|
||||
|
||||
Promise<Boolean>. Результатом выполнения обещания является значение типа Boolean:
|
||||
|
||||
● true ‑ видеозвонки доступны.
|
||||
|
||||
● false ‑ видеозвонки не доступны.
|
||||
|
||||
startVideoconference()
|
||||
|
||||
Описание:
|
||||
|
||||
Начинает видеозвонок пользователя сайта пользователю системы «1С:Предприятие».
|
||||
|
||||
Возвращаемое значение:
|
||||
|
||||
Promise<Boolean>. Результатом выполнения обещания является значение типа Boolean:
|
||||
|
||||
● true ‑ видеозвонок начался.
|
||||
|
||||
● false ‑ видеозвонок был отменен.
|
||||
|
||||
sendMessage(<message>)
|
||||
|
||||
Описание:
|
||||
|
||||
Отправляет текст сообщения.
|
||||
|
||||
Параметры:
|
||||
|
||||
Messageобязательный
|
||||
|
||||
Тип: Object.
|
||||
|
||||
Сообщение, которое необходимо отправить в чат. Объект имеет следующие свойства:
|
||||
|
||||
● text ‑ тип String -текст сообщения.
|
||||
|
||||
● textFormat- тип String ‑ тип сообщения: text/plain или text/html.
|
||||
|
||||
Возвращаемое значение:
|
||||
|
||||
void.
|
||||
|
||||
addListener(<eventType>, <eventListener>)
|
||||
|
||||
Описание:
|
||||
|
||||
Добавляет обработчик события <event>.
|
||||
|
||||
Параметры:
|
||||
|
||||
eventTypeобязательный
|
||||
|
||||
Тип: String. Имя события чата (описано далее).
|
||||
|
||||
eventListenerобязательный
|
||||
|
||||
Тип: Function. Содержит ссылку на метод обработчика события.
|
||||
|
||||
Возвращаемое значение:
|
||||
|
||||
void.
|
||||
|
||||
removeListener(<eventType>, <eventListener>)
|
||||
|
||||
Описание:
|
||||
|
||||
Удаляет обработчик события <event>.
|
||||
|
||||
Параметры:
|
||||
|
||||
eventTypeобязательный
|
||||
|
||||
Тип: String. Имя события чата (описано далее).
|
||||
|
||||
eventListenerобязательный
|
||||
|
||||
Тип: Function. Содержит ссылку на метод обработчика события.
|
||||
|
||||
Возвращаемое значение:
|
||||
|
||||
void.
|
||||
|
||||
Объект CollaborationSystemWebChat1CE предоставляет возможность обрабатывать следующие события чата:
|
||||
|
||||
Событие
|
||||
|
||||
Описание
|
||||
|
||||
close
|
||||
|
||||
Вызывается при закрытии (сворачивании) чата.
|
||||
|
||||
initialized
|
||||
|
||||
Вызывается после полной инициализации чата (когда чат полностью готов к работе).
|
||||
|
||||
open
|
||||
|
||||
Вызывается при открытии (разворачивании) чата.
|
||||
|
||||
videoconferenceend
|
||||
|
||||
Вызывается при окончании видеозвонка.
|
||||
|
||||
videoconferencestart
|
||||
|
||||
Вызывается при начале видеозвонка.
|
||||
|
||||
Отдельно остановимся на процессе сопоставления пользователей сайта и системы взаимодействия. Как уже было отмечено ранее, работа системы взаимодействия всегда выполняется от лица какого-либо пользователя. Из этого следует, что любой пользователь, который подключается к системе взаимодействия, должен иметь свое цифровое олицетворение в системе взаимодействия. Однако, пользователь сайта, на котором развернут чат, и пользователь системы взаимодействия (в общем случае) ничего не знают друг о друге.
|
||||
|
||||
При выполнении интеграции можно будет использовать один из следующих механизмов интеграции:
|
||||
|
||||
● Не выполнять явного соответствия. В этом случае пользователь сайта какое-то время будет «узнаваться» чатом (через механизм хранения данных веб-браузером), но при входе с другого компьютера или через продолжительный промежуток времени будет создан новый пользователь системы взаимодействия. Для такого использования ничего делать не требуется.
|
||||
|
||||
● Сопоставлять пользователей. В этом случае необходима доработка не только клиентской части сайта, но и его серверной части. В кратком изложении схема сопоставления выглядит следующим образом: программное обеспечение сайта сообщает серверу взаимодействия уникальный идентификатор пользователя, который сейчас работает на сайте. Сервер взаимодействия по этому идентификатору находит «своего» пользователя и передает сайту сообщения обсуждения с этим пользователем. Теперь рассмотрим эту схему более подробно.
|
||||
|
||||
Сайт, на котором развернут чат, должен сообщить системе взаимодействия некоторую информацию, по которой можно однозначно определить, какой пользователь сайта входит в чат. Для этого сайт должен сформировать токен в формате JWT или JWS (JSON Web Signature).
|
||||
|
||||
Т. к. JWS ‑ это (фактически) подписанный токен JWT. Таким образом, вначале необходимо сформировать сам токен, полезная нагрузка которого должна содержать следующие претензии:
|
||||
|
||||
● sub ‑ в этой претензии должен располагаться уникальный идентификатор пользователя сайта (строковое представление). Это значение в дальнейшем будет в системе «1С:Предприятие» с помощью свойства ПользовательСистемыВзаимодействия.ИдентификаторПользователяВнешнейСистемы.
|
||||
|
||||
● iat ‑ в этой претензии размещается время создания JWT по времени сервера сайта, в который интегрируется чат.
|
||||
|
||||
JWT должен формироваться на стороне серверной части сайта. Теперь этот токен необходимо передать в чат системы взаимодействия, который передаст эту информацию далее, на сервер взаимодействия. Для такой передачи следует использовать метод чата setMatchingKeyToken(). Сервер взаимодействия извлекает из токена полезную нагрузку и сопоставляет пользователя сайта с пользователем системы взаимодействия. Затем сервер взаимодействия «отдает» сайту содержимое обсуждения для указанного пользователя. Если в процессе работы сайта пользователь сайта меняется (например, текущий пользователь вышел из личного кабинета, а другой пользователь авторизовался), то сайт должен повторить процедуру формирования токена для нового пользователя и повторно выполнить установку ключа сопоставления. После этого содержимое чата обновится.
|
||||
|
||||
Описанная выше схема работает в том случае, когда в настройках интеграции не указан параметр signKey.
|
||||
|
||||
У рассмотренного метода есть неприятная особенность, связанная с безопасностью интеграции (и данных пользователей). Если предположить, что клиентская часть сайта является ненадежным элементом, то злоумышленник может указать произвольный идентификатор пользователя сайта в претензии JWT и получить доступ к чужим данным.
|
||||
|
||||
Чтобы избежать этого, в настройках интеграции следует указать параметр signKey. В этом случае сервер взаимодействия будет ожидать в параметрах метода setMatchingKeyToken() не JWT, а JWS-токен. Также необходимо доработать серверную часть сайта таким образом, чтобы после формирования необходимого JWT, сервер сайта подписывал этот токен (формировал JWS) с помощью ключа, указанного в параметре интеграции signKey. Этот ключ необходимо вручную указать в серверном коде сайта. Формирование JWS должно выполняться с помощью библиотек работы с токенами языка программирования, который используется для разработки серверной части сайта.
|
||||
|
||||
В результате указанных доработок клиентская часть не сможет изменить полезную нагрузку JWT без нарушения подписи. Вся остальная схема остается той же: JWS передается в чат с помощью метода setMatchingKeyToken(), чат передает токен на сервер взаимодействия. Только теперь, прежде чем начать поиск пользователя по информации из полезной нагрузки токена, сервер взаимодействия проверит подпись токена. И дальнейшая работа будет возможна только в том случае, если переданный токен не был изменен.
|
||||
|
||||
Смотри также:
|
||||
|
||||
● JSON Web Signature, веб-подпись JSON: https://datatracker.ietf.org/doc/html/rfc7515.
|
||||
@@ -0,0 +1,270 @@
|
||||
# Внимание: на сервере работает несколько проектов
|
||||
|
||||
## Дата обновления
|
||||
2026-05-21
|
||||
|
||||
## Сервер
|
||||
81.177.141.34 — единый VPS для независимых проектов.
|
||||
Пользователь: `angel`
|
||||
ОС: Ubuntu 24.04, Docker Engine
|
||||
|
||||
---
|
||||
|
||||
## 1. Проекты на сервере
|
||||
|
||||
### Проект A: aegisone — Сервисный портал AegisOne Engineering
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Тип | Python FastAPI + PHP (публичный сайт) + PostgreSQL |
|
||||
| Python | FastAPI на Uvicorn (4 workers), порт **8000** |
|
||||
| PHP | Nginx (порт 8080) + PHP-FPM (порт 9000) |
|
||||
| PostgreSQL | 16-alpine, порт **5432**, БД: `aegisone` |
|
||||
| Домены | `service.aegisone.ru` (Python), `aegisone.ru` (PHP) |
|
||||
| Директории | `/opt/projects/aegisone-py/` (Python), `/opt/projects/aegisone-php/` (PHP) |
|
||||
| Docker проект | `aegisone` — контейнеры: `aegisone-app`, `aegisone-postgres` |
|
||||
| Deploy | `py_service/deploy.sh` — версионирование, архив, SCP, docker compose up |
|
||||
|
||||
**Nginx-роутинг (service.aegisone.ru → localhost:8000):**
|
||||
```
|
||||
/ → localhost:8000 (FastAPI root — страница логина)
|
||||
/service/* → localhost:8000 (весь сервис FastAPI)
|
||||
/service-style.php, /logo.php, /assets/* → localhost:8080 (PHP)
|
||||
```
|
||||
|
||||
**Структура Python-приложения:**
|
||||
```
|
||||
py_service/
|
||||
├── app/
|
||||
│ ├── main.py — точка входа FastAPI, lifespan, статика
|
||||
│ ├── config.py — настройки (DB, SECRET_KEY, DOCS_DIR и т.д.)
|
||||
│ ├── database.py — подключение к PostgreSQL
|
||||
│ ├── dependencies.py — get_current_user, require_role
|
||||
│ ├── models/
|
||||
│ │ └── models.py — SQLAlchemy модели (User, Object, Client, SLA и т.д.)
|
||||
│ ├── routers/
|
||||
│ │ └── service_pages.py — все основные роуты + API бота Max
|
||||
│ ├── documents_router.py — роуты документов (/service/documents/)
|
||||
│ ├── services/
|
||||
│ │ └── docs_permissions.py — discover_docs(), load_permissions()
|
||||
│ ├── templates/ — Jinja2 шаблоны
|
||||
│ │ ├── base.html — базовый HTML (noindex, favicon, theme switcher)
|
||||
│ │ ├── login.html — страница входа
|
||||
│ │ ├── page.html — основной layout с сайдбаром
|
||||
│ │ ├── dashboard.html — дашборд (дублирует сайдбар из page.html)
|
||||
│ │ ├── documents/ — шаблоны документов
|
||||
│ │ ├── pages/ — шаблоны страниц (customers, objects, sla и т.д.)
|
||||
│ │ └── bot_*.html — страницы управления ботом Max
|
||||
│ └── static/
|
||||
│ ├── css/ — service.css, service-theme.css
|
||||
│ ├── js/ — theme.js
|
||||
│ └── img/ — favicons, svg, logo
|
||||
├── docs/ — .md файлы документов (26 шт)
|
||||
├── scripts/ — bump_version.py, migrate_mysql_to_pg.py
|
||||
├── Dockerfile, docker-compose.yml
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
**Роли пользователей:** `owner` (владелец), `engineer` (инженер), `technician` (техник)
|
||||
|
||||
**Управление ботом Max (раздел в сайдбаре для owner):**
|
||||
- `/service/bot-settings` — настройки (имя, телефоны, email, часы работы, YandexGPT)
|
||||
- `/service/bot-settings/responses` — редактирование шаблонов ответов
|
||||
- `/service/bot-settings/features` — toggle функций бота
|
||||
- `/service/bot-settings/categories` — CRUD категорий обращений
|
||||
- `/service/bot-settings/kb` — база знаний (FAQ)
|
||||
- `/service/bot-settings/conversations` — просмотр диалогов
|
||||
- `/service/bot-settings/users` — пользователи бота
|
||||
- `/service/bot-settings/test-runner` — тестовый прогон диалога
|
||||
- `/service/bot-settings/analytics` — статистика + рассылки
|
||||
|
||||
---
|
||||
|
||||
### Проект C: max_bot — Чат-бот «София» для Max Messenger
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Тип | Python FastAPI (Max Bot API) |
|
||||
| FastAPI | порт **8001** |
|
||||
| PostgreSQL | общая БД `aegisone` (порт 5432), таблицы с префиксом `bot_` |
|
||||
| Домен | `max.aegisone.ru` |
|
||||
| Директория | `/opt/projects/aegisone-py/max_bot/` |
|
||||
| Контейнер | `aegisone-max-bot` |
|
||||
| Webhook | `https://max.aegisone.ru/webhook` → localhost:8001 |
|
||||
| Токен бота | `id2311381465_bot` (хранится в bot_settings) |
|
||||
|
||||
**Структура:**
|
||||
```
|
||||
max_bot/
|
||||
├── app/
|
||||
│ ├── main.py — FastAPI app, webhook, API для портала
|
||||
│ ├── config.py — настройки (shared DB URL)
|
||||
│ ├── database.py — AsyncSession (общая БД aegisone)
|
||||
│ ├── max_api.py — клиент Max API (send_message, subscribe)
|
||||
│ ├── bot_engine.py — обработчик webhook событий
|
||||
│ ├── fsm.py — конечный автомат состояний
|
||||
│ ├── conversation.py — управление диалогами
|
||||
│ ├── settings_cache.py — кэш настроек (5 мин TTL)
|
||||
│ ├── text_renderer.py — рендер шаблонов с переменными
|
||||
│ ├── file_storage.py — сохранение файлов (local → Yandex Disk future)
|
||||
│ ├── handlers/ — обработчики состояний
|
||||
│ │ ├── greeting.py — /start, приветствие
|
||||
│ │ ├── consent.py — ФЗ-152 согласие
|
||||
│ │ ├── contact.py — сбор контактов
|
||||
│ │ ├── inquiry.py — сбор обращения + авто-категоризация
|
||||
│ │ ├── handoff.py — передача оператору
|
||||
│ │ ├── main_menu.py — главное меню
|
||||
│ │ └── features/ — доп. функции (risk_score, sla_status, etc.)
|
||||
│ ├── integrations/
|
||||
│ │ ├── _1c_unf.py — webhook в 1С:УНФ + ответ от 1С
|
||||
│ │ ├── portal_db.py — чтение из общей БД
|
||||
│ │ └── yandex_gpt.py — YandexGPT для категоризации/эмоций
|
||||
│ ├── keyboards/
|
||||
│ │ └── inline.py — генераторы inline-клавиатур
|
||||
│ └── models/
|
||||
│ └── models.py — SQLAlchemy модели (bot_*)
|
||||
├── sql/
|
||||
│ └── bot_schema.sql — CREATE TABLE + начальные данные
|
||||
├── nginx/
|
||||
│ └── max-aegisone.conf — nginx config
|
||||
├── Dockerfile, docker-compose.yml
|
||||
└── requirements.txt
|
||||
```
|
||||
|
||||
**Таблицы бота в общей БД:**
|
||||
`bot_users`, `bot_conversations`, `bot_messages`, `bot_consent_logs`, `bot_1c_sync_log`,
|
||||
`bot_knowledge_base`, `bot_categories`, `bot_settings`, `bot_features`, `bot_response_templates`, `bot_broadcasts`
|
||||
|
||||
**Nginx-роутинг (max.aegisone.ru → localhost:8001):**
|
||||
```
|
||||
/webhook → localhost:8001/webhook (Max API webhook)
|
||||
/api/* → localhost:8001/api/* (API для сервисного портала)
|
||||
/health → localhost:8001/health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### Проект B: VoIdeaAI — AI-платформа
|
||||
|
||||
| Параметр | Значение |
|
||||
|----------|----------|
|
||||
| Тип | Python FastAPI + React Frontend + PostgreSQL + Redis |
|
||||
| FastAPI | порт **8020** |
|
||||
| PostgreSQL | порт **5444**, БД: `voidea` |
|
||||
| Redis | порт **6380** |
|
||||
| Gitea | порт **3000**, PostgreSQL **5433** |
|
||||
| Домены | `voideaai.ru`, `git.voideaai.ru` |
|
||||
| Директория | `/opt/projects/voidea/` |
|
||||
| Docker проект | `voidea` |
|
||||
| Nginx конфиг | `/opt/projects/nginx-proxy/conf.d/voideaai.ru.conf` |
|
||||
|
||||
---
|
||||
|
||||
## 2. Критические правила
|
||||
|
||||
### Порты — НЕ занимать!
|
||||
```
|
||||
8000 — aegisone FastAPI
|
||||
8001 — max_bot FastAPI
|
||||
8080 — aegisone PHP nginx
|
||||
9000 — aegisone PHP-FPM
|
||||
5432 — aegisone PostgreSQL
|
||||
8020 — VoIdea FastAPI
|
||||
5444 — VoIdea PostgreSQL
|
||||
6380 — VoIdea Redis
|
||||
3000 — Gitea web
|
||||
5433 — Gitea PostgreSQL
|
||||
9001 — Portainer HTTP
|
||||
9443 — Portainer HTTPS
|
||||
```
|
||||
|
||||
### nginx-proxy общий
|
||||
- Конфиги: `/opt/projects/nginx-proxy/conf.d/`
|
||||
- Файлы: `aegisone.conf`, `service-aegisone.conf`, `max-aegisone.conf`, `voideaai.ru.conf`, `gitea.conf`
|
||||
- **Перед рестартом nginx:** `docker compose -p nginx-proxy exec nginx nginx -t`
|
||||
|
||||
### Docker
|
||||
- **Все контейнеры используют `network_mode: host`** (bridge не работает на OpenVZ)
|
||||
- `docker system prune -af` удалит build cache ВСЕХ проектов
|
||||
- Префиксы Docker Compose:
|
||||
- `docker compose -p aegisone` (или `cd /opt/projects/aegisone-py && docker compose`)
|
||||
- `docker compose -p voidea`
|
||||
- `docker compose -p gitea`
|
||||
- `docker compose -p nginx-proxy`
|
||||
- `docker compose -p portainer`
|
||||
|
||||
### Диск
|
||||
- 20GB всего, ~13GB свободно
|
||||
- Проверка: `df -h /`
|
||||
- Очистка: `docker builder prune -af`
|
||||
|
||||
### Публичная часть (PHP) НЕ ТРОГАТЬ
|
||||
- PHP сайт (aegisone.ru) — отдельный проект, не связан с Python-сервисом
|
||||
- Все изменения только в `py_service/` и `max_bot/`
|
||||
|
||||
---
|
||||
|
||||
## 3. Команды для проверки состояния
|
||||
|
||||
### aegisone (Python)
|
||||
```bash
|
||||
docker ps --filter name=aegisone
|
||||
docker logs aegisone-app --tail=50
|
||||
curl -s -o /dev/null -w '%{http_code}' http://localhost:8000/service/dashboard/
|
||||
cat /opt/projects/aegisone-py/version.txt
|
||||
docker exec aegisone-postgres psql -U aegisone -d aegisone -c "\dt"
|
||||
```
|
||||
|
||||
### max_bot (София)
|
||||
```bash
|
||||
docker ps --filter name=max-bot
|
||||
docker logs aegisone-max-bot --tail=50
|
||||
curl -s http://localhost:8001/health
|
||||
# Проверить webhook:
|
||||
curl -s http://localhost:8001/api/bot/settings
|
||||
```
|
||||
|
||||
### VoIdeaAI
|
||||
```bash
|
||||
docker compose -p voidea ps
|
||||
docker compose -p voidea logs --tail=20 app
|
||||
curl -s http://localhost:8020/health
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Деплой
|
||||
|
||||
### aegisone (py_service)
|
||||
```bash
|
||||
# Локально (из py_service/)
|
||||
./deploy.sh [--bump patch|minor|major]
|
||||
```
|
||||
|
||||
### max_bot
|
||||
```bash
|
||||
# SCP на сервер, затем:
|
||||
cd /opt/projects/aegisone-py/max_bot
|
||||
docker compose build --no-cache
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Troubleshooting
|
||||
|
||||
| Проблема | Действие |
|
||||
|----------|----------|
|
||||
| aegisone FastAPI не отвечает | `docker logs aegisone-app --tail=50` |
|
||||
| max_bot не отвечает | `docker logs aegisone-max-bot --tail=50` |
|
||||
| Ошибка БД | `docker logs aegisone-postgres --tail=20` |
|
||||
| Документ не найден | Проверить `discover_docs()` — файл .md есть в `py_service/docs/`? |
|
||||
| Webhook не работает | Проверить `curl http://localhost:8001/health` и nginx конфиг |
|
||||
| Portainer не открывается | Проверить `http://81.177.141.34:9001` и `https://81.177.141.34:9443` |
|
||||
| Не хватает места | `df -h /`, `docker builder prune -af` |
|
||||
| nginx ошибка | `docker compose -p nginx-proxy exec nginx nginx -t` |
|
||||
|
||||
---
|
||||
|
||||
*Создан: 2026-05-20*
|
||||
*Обновлён: 2026-05-21 — добавлен max_bot (София)*
|
||||
@@ -0,0 +1,289 @@
|
||||
# VPS Infrastructure Guide for AI Agents
|
||||
|
||||
## Overview
|
||||
|
||||
This document describes the VPS infrastructure at **81.177.141.34** to help AI agents migrate or deploy new projects (e.g., `voideaai.ru`).
|
||||
|
||||
---
|
||||
|
||||
## 1. VPS Specifications
|
||||
|
||||
| Property | Value |
|
||||
|---|---|
|
||||
| IP | 81.177.141.34 |
|
||||
| User | `angel` |
|
||||
| Password | `.-SHGWa_` |
|
||||
| Sudo | passwordless via `/etc/sudoers.d/angel` |
|
||||
| SSH key | added to `~/.ssh/authorized_keys` |
|
||||
| OS | Ubuntu 24.04 (Noble) |
|
||||
| Kernel | OpenVZ/LXC container — limited kernel modules |
|
||||
| Network interface | `venet0` (no bridge NAT support) |
|
||||
| Disk | 20GB total, ~13GB available |
|
||||
| Docker | 29.5.0, iptables in nft mode (`iptables-nft`) |
|
||||
|
||||
---
|
||||
|
||||
## 2. Docker Constraints
|
||||
|
||||
### Critical: `network_mode: host` only
|
||||
|
||||
Bridge networking (`network_mode: bridge`) **does not work** on this VPS. The OpenVZ/LXC kernel does not support masquerade for bridge interfaces through `venet0`. All containers **must** use:
|
||||
|
||||
```yaml
|
||||
network_mode: host
|
||||
```
|
||||
|
||||
### Docker daemon config (`/etc/docker/daemon.json`)
|
||||
|
||||
```json
|
||||
{"dns": ["8.8.8.8", "8.8.4.4"]}
|
||||
```
|
||||
|
||||
### Build constraint
|
||||
|
||||
All Docker builds require:
|
||||
|
||||
```yaml
|
||||
build:
|
||||
network: host
|
||||
```
|
||||
|
||||
Without this, `apt-get` and `pip` cannot download packages (no bridge NAT).
|
||||
|
||||
### Docker Hub connectivity issues
|
||||
|
||||
Pulling large images (>100MB) from Docker Hub may time out (`failed to copy: read tcp... connection timed out`). Smaller images (nginx:alpine, postgres:16-alpine) work. Gitea and Portainer images **cannot be pulled** reliably.
|
||||
|
||||
**Solutions attempted:**
|
||||
- Retry with `--platform linux/amd64`
|
||||
- Increase `max-download-attempts` (not supported in Docker 29.5)
|
||||
- Clean build cache with `docker builder prune -af`
|
||||
- All fail on large image layers
|
||||
|
||||
**Workaround:** Pull images locally and SCP (`docker save → scp → docker load`), or use a registry mirror.
|
||||
|
||||
---
|
||||
|
||||
## 3. Port Allocation (all on host)
|
||||
|
||||
| Port | Service | Container |
|
||||
|---|---|---|
|
||||
| 80 | nginx-proxy HTTP | `nginx-proxy` |
|
||||
| 443 | nginx-proxy HTTPS | `nginx-proxy` |
|
||||
| 8080 | PHP nginx (aegisone.ru) | `aegisone-php-nginx` |
|
||||
| 9000 | PHP-FPM | `aegisone-php-fpm` |
|
||||
| 8000 | FastAPI (service.aegisone.ru) | `aegisone-app` |
|
||||
| 5432 | PostgreSQL (shared, aegisone) | `aegisone-postgres` |
|
||||
| 5433 | PostgreSQL (Gitea) | `gitea-db` (planned) |
|
||||
| 3000 | Gitea | `gitea` (planned) |
|
||||
| 9001 | Portainer HTTP | `portainer` (planned) |
|
||||
| 9443 | Portainer HTTPS | `portainer` (planned) |
|
||||
|
||||
### Address resolution between containers
|
||||
|
||||
Since all containers use `network_mode: host`, they communicate via `localhost:PORT`. **Docker DNS names do not resolve.** In `nginx.conf` and app configs, use:
|
||||
|
||||
```nginx
|
||||
proxy_pass http://localhost:8080; # NOT container_name:port
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Project Structure
|
||||
|
||||
```
|
||||
/opt/projects/
|
||||
nginx-proxy/ # Reverse proxy (nginx + certbot)
|
||||
docker-compose.yml
|
||||
nginx.conf
|
||||
conf.d/ # Per-domain configs
|
||||
certs/ # SSL certificates
|
||||
html/ # Webroot for certbot
|
||||
aegisone-php/ # PHP frontend (aegisone.ru)
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
nginx.conf # PHP nginx config (listen 8080)
|
||||
src/ # PHP source files
|
||||
aegisone-py/ # FastAPI backend (service.aegisone.ru)
|
||||
docker-compose.yml
|
||||
Dockerfile
|
||||
app/ # Python source
|
||||
sql/ # DB schema
|
||||
gitea/ # Git hosting (planned)
|
||||
docker-compose.yml
|
||||
portainer/ # Docker UI (planned)
|
||||
docker-compose.yml
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. nginx-proxy Configuration Pattern
|
||||
|
||||
### Global config (`nginx.conf`)
|
||||
- `resolver 127.0.0.11` — not used in host mode, kept for compatibility
|
||||
- SSL termination handled by nginx-proxy
|
||||
- Upstream protocols are HTTP (nginx-proxy terminates SSL and proxies via HTTP)
|
||||
|
||||
### Conf.d file structure
|
||||
Each domain has one `.conf` file with two server blocks:
|
||||
|
||||
```nginx
|
||||
# HTTP (no redirect — temporary, until LE certs)
|
||||
server {
|
||||
listen 80;
|
||||
server_name example.ru;
|
||||
|
||||
location / {
|
||||
proxy_pass http://localhost:PORT;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
|
||||
# HTTPS (self-signed, for future LE)
|
||||
server {
|
||||
listen 443 ssl;
|
||||
http2 on;
|
||||
server_name example.ru;
|
||||
|
||||
ssl_certificate /etc/nginx/certs/live/aegisone.ru/fullchain.pem;
|
||||
ssl_certificate_key /etc/nginx/certs/live/aegisone.ru/privkey.pem;
|
||||
|
||||
# ... same location block
|
||||
}
|
||||
```
|
||||
|
||||
Note: HTTP→HTTPS redirects (`return 301 https://...`) caused **redirect loops** externally due to provider/proxy intercepting port 443. If you add them, test thoroughly.
|
||||
|
||||
### SSL certificates
|
||||
Currently using self-signed certs generated via:
|
||||
```bash
|
||||
sudo openssl req -x509 -nodes -days 30 -newkey rsa:2048 \
|
||||
-keyout /opt/projects/nginx-proxy/certs/live/aegisone.ru/privkey.pem \
|
||||
-out /opt/projects/nginx-proxy/certs/live/aegisone.ru/fullchain.pem \
|
||||
-subj '/CN=aegisone.ru'
|
||||
```
|
||||
|
||||
For Let's Encrypt:
|
||||
```bash
|
||||
cd /opt/projects/nginx-proxy
|
||||
docker compose run --rm certbot certonly --webroot \
|
||||
--webroot-path=/var/www/certbot \
|
||||
-d example.ru \
|
||||
--email admin@example.ru \
|
||||
--agree-tos --non-interactive
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Deployment Workflow
|
||||
|
||||
### Initial setup
|
||||
1. Prepare files locally (this repo)
|
||||
2. Pack into tarball: `tar czf project.tar.gz .`
|
||||
3. SCP to VPS: `scp project.tar.gz angel@81.177.141.34:/home/angel/`
|
||||
4. SSH and extract: `ssh angel@81.177.141.34 "tar xzf project.tar.gz -C /opt/projects/project-name/"`
|
||||
5. Build and start: `ssh angel@81.177.141.34 "cd /opt/projects/project-name && docker compose up -d --build"`
|
||||
|
||||
### Updating a running project
|
||||
1. Edit files locally
|
||||
2. SCP changed files to `/home/angel/` on VPS
|
||||
3. SSH and copy to correct location: `sudo cp /home/angel/file /opt/projects/project-name/`
|
||||
4. Restart container: `docker compose -p project-name restart service-name`
|
||||
|
||||
### Copying files into a running container (alternative to rebuild)
|
||||
```bash
|
||||
docker cp /home/angel/file.py container-name:/app/path/file.py
|
||||
docker compose -p project-name restart app
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Database
|
||||
|
||||
### PostgreSQL (aegisone shared DB)
|
||||
- Host: `localhost:5432`
|
||||
- User: `aegisone`
|
||||
- Password: `aegisone_pass`
|
||||
- Database: `aegisone`
|
||||
- Container: `aegisone-postgres` (postgres:16-alpine)
|
||||
|
||||
### Schema initialization
|
||||
SQL files in `/opt/projects/aegisone-py/sql/`:
|
||||
- `schema_postgresql.sql` — full schema
|
||||
- `seed_data.sql` — seed data
|
||||
|
||||
Init scripts run only on first volume creation. To add missing tables later:
|
||||
```bash
|
||||
docker exec -i aegisone-postgres psql -U aegisone -d aegisone < /opt/projects/aegisone-py/sql/schema_postgresql.sql
|
||||
```
|
||||
|
||||
### Key schema pattern
|
||||
- All tables use `BOOLEAN` for `is_active` (not integer)
|
||||
- Timestamps use `TIMESTAMP NOT NULL DEFAULT NOW()`
|
||||
- `updated_at` triggers via `update_updated_at_column()` function
|
||||
|
||||
---
|
||||
|
||||
## 8. Useful Commands
|
||||
|
||||
```bash
|
||||
# Check running containers
|
||||
docker ps -a
|
||||
|
||||
# Check compose projects
|
||||
docker compose ls
|
||||
|
||||
# View logs
|
||||
docker logs container-name --tail 50
|
||||
|
||||
# Restart service
|
||||
docker compose -p project-name restart service-name
|
||||
|
||||
# Rebuild and restart
|
||||
docker compose -p project-name up -d --build
|
||||
|
||||
# Clean build cache
|
||||
docker builder prune -af
|
||||
|
||||
# Test nginx config
|
||||
docker compose -p nginx-proxy exec nginx nginx -t
|
||||
|
||||
# Reload nginx
|
||||
docker compose -p nginx-proxy exec nginx nginx -s reload
|
||||
|
||||
# Check listening ports
|
||||
ss -tlnp | grep -E ':(80|443|8080|8000|9000|5432)'
|
||||
|
||||
# Check disk
|
||||
df -h /
|
||||
docker system df
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 9. Adding a New Project (e.g., voideaai.ru)
|
||||
|
||||
### Steps:
|
||||
1. Create directory: `mkdir -p /opt/projects/voideaai`
|
||||
2. Create `docker-compose.yml` with `network_mode: host` and `build.network: host`
|
||||
3. Create `Dockerfile` (use `--only-binary :all:` for pip if avoiding apt)
|
||||
4. Create nginx-proxy config: `nginx/conf.d/voideaai.conf` (listen 80, proxy_pass localhost:PORT)
|
||||
5. Generate cert if needed
|
||||
6. Restart nginx-proxy: `docker compose -p nginx-proxy restart nginx`
|
||||
7. Start project: `cd /opt/projects/voideaai && docker compose up -d --build`
|
||||
|
||||
### Port allocation for new project
|
||||
Check existing ports first (`ss -tlnp`). Choose an unused port for the app and add it to the nginx-proxy config.
|
||||
|
||||
---
|
||||
|
||||
## 10. Known Issues
|
||||
|
||||
1. **Docker Hub timeouts on large images** — consider pulling on local machine and transferring with `docker save`/`docker load`
|
||||
2. **HTTPS redirect loops externally** — port 443 may be intercepted by provider/proxy; test HTTP first
|
||||
3. **Bridge networking doesn't work** — all containers must use `network_mode: host`
|
||||
4. **Docker build needs `network: host`** — without it, apt-get/pip fail
|
||||
5. **Config files with `$` in content** — when editing via SSH from PowerShell, `$variables` get expanded; use SCP (write locally, then copy)
|
||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
+222
@@ -0,0 +1,222 @@
|
||||
#!/bin/bash
|
||||
# ==========================================
|
||||
# AegisOne Engineering — Full Deployment Script
|
||||
# ==========================================
|
||||
# Usage: bash deploy.sh [--skip-ssl]
|
||||
# Run on VPS as angel user
|
||||
set -e
|
||||
|
||||
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; CYAN='\033[0;36m'; NC='\033[0m'
|
||||
log() { echo -e "${CYAN}[$(date +%H:%M:%S)]${NC} $1"; }
|
||||
ok() { echo -e " ${GREEN}✅ $1${NC}"; }
|
||||
info() { echo -e " ${YELLOW}ℹ️ $1${NC}"; }
|
||||
fail() { echo -e " ${RED}❌ $1${NC}"; exit 1; }
|
||||
|
||||
SKIP_SSL=false
|
||||
for arg in "$@"; do [ "$arg" = "--skip-ssl" ] && SKIP_SSL=true; done
|
||||
|
||||
cd "$(dirname "$0")"
|
||||
|
||||
echo -e "${CYAN}"
|
||||
echo "=========================================="
|
||||
echo " AegisOne Engineering — Deploy v2.0"
|
||||
echo "=========================================="
|
||||
echo -e "${NC}"
|
||||
|
||||
# ==========================================
|
||||
# PART 1: Server Preparation
|
||||
# ==========================================
|
||||
log "PART 1: Server preparation"
|
||||
|
||||
if ! command -v docker &>/dev/null; then
|
||||
log "Installing Docker..."
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
sudo usermod -aG docker "$USER"
|
||||
ok "Docker installed. Re-login required for docker group"
|
||||
newgrp docker
|
||||
else
|
||||
ok "Docker already installed: $(docker --version)"
|
||||
fi
|
||||
|
||||
# Create project structure
|
||||
log "Creating /opt/projects/ structure..."
|
||||
sudo mkdir -p /opt/projects/{nginx-proxy,gitea,aegisone-php,aegisone-py,portainer,project2,backups,certs,data}
|
||||
sudo chown -R "$USER":"$USER" /opt/projects
|
||||
ok "Structure created"
|
||||
|
||||
# Create Docker networks
|
||||
log "Creating Docker networks..."
|
||||
docker network inspect proxy-net &>/dev/null || docker network create proxy-net && ok "proxy-net"
|
||||
docker network inspect aegisone-net &>/dev/null || docker network create aegisone-net && ok "aegisone-net"
|
||||
docker network inspect gitea-net &>/dev/null || docker network create gitea-net && ok "gitea-net"
|
||||
|
||||
# ==========================================
|
||||
# PART 2: Deploy nginx-proxy
|
||||
# ==========================================
|
||||
log "PART 2: Deploy nginx-proxy"
|
||||
NGX="/opt/projects/nginx-proxy"
|
||||
|
||||
cp nginx/nginx.conf "$NGX/"
|
||||
cp -r nginx/conf.d/* "$NGX/conf.d/"
|
||||
|
||||
# Generate self-signed cert as placeholder
|
||||
mkdir -p "$NGX/certs/live/aegisone.ru" "$NGX/html/.well-known/acme-challenge"
|
||||
cat > "$NGX/html/index.html" <<< "<!DOCTYPE html><html><head><title>AegisOne</title></head><body><h1>AegisOne Engineering</h1></body></html>"
|
||||
|
||||
if [ ! -f "$NGX/certs/live/aegisone.ru/fullchain.pem" ]; then
|
||||
log "Generating self-signed SSL cert..."
|
||||
openssl req -x509 -nodes -days 30 -newkey rsa:2048 \
|
||||
-keyout "$NGX/certs/live/aegisone.ru/privkey.pem" \
|
||||
-out "$NGX/certs/live/aegisone.ru/fullchain.pem" \
|
||||
-subj "/CN=aegisone.ru" 2>/dev/null || {
|
||||
docker run --rm -v "$NGX/certs:/certs" alpine:latest sh -c "
|
||||
apk add openssl >/dev/null 2>&1
|
||||
mkdir -p /certs/live/aegisone.ru
|
||||
openssl req -x509 -nodes -days 30 -newkey rsa:2048 \
|
||||
-keyout /certs/live/aegisone.ru/privkey.pem \
|
||||
-out /certs/live/aegisone.ru/fullchain.pem \
|
||||
-subj '/CN=aegisone.ru'
|
||||
"
|
||||
}
|
||||
ok "Self-signed cert created"
|
||||
fi
|
||||
|
||||
cp nginx/docker-compose.yml "$NGX/"
|
||||
cd "$NGX" && docker compose up -d
|
||||
ok "nginx-proxy started"
|
||||
|
||||
# ==========================================
|
||||
# PART 3: Deploy aegisone-py (FastAPI + PostgreSQL)
|
||||
# ==========================================
|
||||
log "PART 3: Deploy aegisone-py (FastAPI + PostgreSQL)"
|
||||
PY="/opt/projects/aegisone-py"
|
||||
|
||||
# Copy all py_service files
|
||||
rsync -a --delete py_service/ "$PY/" 2>/dev/null || cp -r py_service/* "$PY/"
|
||||
# Override docker-compose.yml with updated network config
|
||||
cp py_service/docker-compose.yml "$PY/"
|
||||
|
||||
cd "$PY" && docker compose up -d --build
|
||||
ok "aegisone-py started"
|
||||
|
||||
# ==========================================
|
||||
# PART 3B: Deploy max_bot (MAX messenger bot)
|
||||
# ==========================================
|
||||
log "PART 3B: Deploy max_bot (MAX messenger bot on port 8002)"
|
||||
BOT="/opt/projects/aegisone-py/max_bot"
|
||||
|
||||
mkdir -p "$BOT"
|
||||
rsync -a --delete max_bot/ "$BOT/" 2>/dev/null || cp -r max_bot/* "$BOT/"
|
||||
|
||||
# Copy nginx config for max.aegisone.ru
|
||||
cp max_bot/nginx/max-aegisone.conf "$NGX/conf.d/max-aegisone.conf"
|
||||
docker compose -f "$NGX/docker-compose.yml" exec nginx nginx -s reload 2>/dev/null || true
|
||||
ok "max_bot nginx config deployed"
|
||||
|
||||
cd "$BOT" && docker compose up -d --build
|
||||
ok "max_bot started on port 8002"
|
||||
|
||||
# ==========================================
|
||||
# PART 4: Deploy aegisone-php (Public site)
|
||||
# ==========================================
|
||||
log "PART 4: Deploy aegisone-php (Public site)"
|
||||
PHP="/opt/projects/aegisone-php"
|
||||
|
||||
# Copy PHP source files from repo root (excluding certain dirs)
|
||||
mkdir -p "$PHP/src"
|
||||
rsync -a --delete \
|
||||
--exclude='.git' --exclude='node_modules' --exclude='*.zip' \
|
||||
--exclude='py_service' --exclude='aegisone-php' --exclude='nginx' \
|
||||
--exclude='gitea' --exclude='portainer' --exclude='opencode.json' \
|
||||
--exclude='site_aegisone.zip' \
|
||||
./ "$PHP/src/"
|
||||
|
||||
# Copy Docker infrastructure
|
||||
cp aegisone-php/Dockerfile "$PHP/"
|
||||
cp aegisone-php/nginx.conf "$PHP/"
|
||||
cp aegisone-php/docker-compose.yml "$PHP/"
|
||||
|
||||
cd "$PHP" && docker compose up -d --build
|
||||
ok "aegisone-php started"
|
||||
|
||||
# ==========================================
|
||||
# PART 5: Deploy Gitea
|
||||
# ==========================================
|
||||
log "PART 5: Deploy Gitea"
|
||||
GITEA="/opt/projects/gitea"
|
||||
cp gitea/docker-compose.yml "$GITEA/"
|
||||
cd "$GITEA" && docker compose up -d
|
||||
ok "Gitea started"
|
||||
|
||||
# ==========================================
|
||||
# PART 6: Deploy Portainer (port 9000)
|
||||
# ==========================================
|
||||
log "PART 6: Deploy Portainer"
|
||||
P="/opt/projects/portainer"
|
||||
cp portainer/docker-compose.yml "$P/"
|
||||
cd "$P" && docker compose up -d
|
||||
ok "Portainer started on port 9000"
|
||||
|
||||
# ==========================================
|
||||
# PART 7: Configure UFW
|
||||
# ==========================================
|
||||
log "PART 7: Configure UFW"
|
||||
if ! sudo ufw status | grep -q "Status: active"; then
|
||||
sudo ufw --force disable 2>/dev/null || true
|
||||
sudo ufw default deny incoming
|
||||
sudo ufw default allow outgoing
|
||||
sudo ufw allow 22/tcp comment 'SSH'
|
||||
sudo ufw allow 80/tcp comment 'HTTP'
|
||||
sudo ufw allow 443/tcp comment 'HTTPS'
|
||||
sudo ufw allow 9000/tcp comment 'Portainer'
|
||||
sudo ufw --force enable
|
||||
ok "UFW configured"
|
||||
else
|
||||
ok "UFW already active"
|
||||
fi
|
||||
|
||||
# ==========================================
|
||||
# PART 8: Get SSL certificates
|
||||
# ==========================================
|
||||
if [ "$SKIP_SSL" = false ]; then
|
||||
log "PART 8: Obtaining Let's Encrypt SSL certificates"
|
||||
cd "$NGX"
|
||||
docker compose run --rm certbot certonly --webroot \
|
||||
--webroot-path=/var/www/certbot \
|
||||
-d aegisone.ru -d www.aegisone.ru \
|
||||
-d service.aegisone.ru \
|
||||
-d max.aegisone.ru \
|
||||
-d git.aegisone.ru \
|
||||
--email admin@aegisone.ru \
|
||||
--agree-tos --non-interactive 2>/dev/null && {
|
||||
ok "SSL certificates obtained"
|
||||
docker compose exec nginx nginx -s reload
|
||||
ok "Nginx reloaded with real certs"
|
||||
} || info "Certbot failed — run manually after DNS propagation"
|
||||
else
|
||||
info "SSL step skipped (--skip-ssl)"
|
||||
fi
|
||||
|
||||
# ==========================================
|
||||
# Done
|
||||
# ==========================================
|
||||
echo ""
|
||||
echo -e "${GREEN}=========================================="
|
||||
echo " Deploy complete!"
|
||||
echo "==========================================${NC}"
|
||||
echo ""
|
||||
echo " nginx-proxy: http://81.177.141.34 → aegisone.ru"
|
||||
echo " Portainer: http://81.177.141.34:9000"
|
||||
echo " FastAPI (direct): http://81.177.141.34:8000"
|
||||
echo " Gitea (direct): http://81.177.141.34:3000"
|
||||
echo ""
|
||||
echo " After DNS propagation, run certbot:"
|
||||
echo " cd /opt/projects/nginx-proxy"
|
||||
echo " docker compose run --rm certbot certonly --webroot \\"
|
||||
echo " --webroot-path=/var/www/certbot \\"
|
||||
echo " -d aegisone.ru -d www.aegisone.ru \\"
|
||||
echo " -d service.aegisone.ru -d max.aegisone.ru \\"
|
||||
echo " -d git.aegisone.ru \\"
|
||||
echo " --email admin@aegisone.ru --agree-tos --non-interactive"
|
||||
echo " docker compose exec nginx nginx -s reload"
|
||||
echo ""
|
||||
@@ -0,0 +1,152 @@
|
||||
#!/bin/bash
|
||||
set -e
|
||||
|
||||
echo "=========================================="
|
||||
echo " AegisOne Fix & Deploy Script"
|
||||
echo "=========================================="
|
||||
|
||||
# 1. Импортировать схему в PostgreSQL
|
||||
echo ""
|
||||
echo "[1/7] Импорт схемы в PostgreSQL..."
|
||||
if [ -f /tmp/schema.sql ]; then
|
||||
docker exec -i aegisone-postgres psql -U aegisone -d aegisone < /tmp/schema.sql
|
||||
echo " ✅ Схема импортирована"
|
||||
else
|
||||
echo " ❌ /tmp/schema.sql не найден"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. Импортировать seed-данные
|
||||
echo ""
|
||||
echo "[2/7] Импорт seed-данных..."
|
||||
if [ -f /tmp/seed.sql ]; then
|
||||
docker exec -i aegisone-postgres psql -U aegisone -d aegisone < /tmp/seed.sql
|
||||
echo " ✅ Seed-данные импортированы"
|
||||
else
|
||||
echo " ❌ /tmp/seed.sql не найден"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 3. Проверить таблицы
|
||||
echo ""
|
||||
echo "[3/7] Проверка таблиц..."
|
||||
TABLE_COUNT=$(docker exec aegisone-postgres psql -U aegisone -d aegisone -t -c "SELECT count(*) FROM information_schema.tables WHERE table_schema='public';")
|
||||
echo " Найдено таблиц: $TABLE_COUNT"
|
||||
|
||||
# 4. Сгенерировать bcrypt-хеш и обновить пароль
|
||||
echo ""
|
||||
echo "[4/7] Генерация валидного bcrypt-хеша..."
|
||||
HASH=$(python3 -c "
|
||||
from passlib.context import CryptContext
|
||||
ctx = CryptContext(schemes=['bcrypt'], deprecated='auto')
|
||||
print(ctx.hash('AegisOne2024!'))
|
||||
" 2>/dev/null || echo "ERROR")
|
||||
|
||||
if [ "$HASH" = "ERROR" ] || [ -z "$HASH" ]; then
|
||||
echo " ⚠️ passlib не установлен, используем альтернативный метод..."
|
||||
HASH=$(python3 -c "
|
||||
import bcrypt
|
||||
password = b'AegisOne2024!'
|
||||
salt = bcrypt.gensalt()
|
||||
hashed = bcrypt.hashpw(password, salt)
|
||||
print(hashed.decode())
|
||||
" 2>/dev/null || echo "ERROR2")
|
||||
fi
|
||||
|
||||
if [ "$HASH" != "ERROR" ] && [ "$HASH" != "ERROR2" ]; then
|
||||
docker exec aegisone-postgres psql -U aegisone -d aegisone -c "
|
||||
UPDATE users SET password_hash='$HASH' WHERE login='owner';
|
||||
"
|
||||
echo " ✅ Пароль обновлен (логин: owner, пароль: AegisOne2024!)"
|
||||
else
|
||||
echo " ❌ Не удалось сгенерировать хеш"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 5. Копирование CSS
|
||||
echo ""
|
||||
echo "[5/7] Копирование CSS..."
|
||||
mkdir -p /opt/projects/aegisone-py/app/static/css
|
||||
cp /opt/projects/aegisone-php/src/assets/css/style.css /opt/projects/aegisone-py/app/static/css/service.css
|
||||
echo " ✅ CSS скопирован"
|
||||
|
||||
# 6. Исправить шаблон login.html
|
||||
echo ""
|
||||
echo "[6/7] Исправление шаблона login.html..."
|
||||
cat > /opt/projects/aegisone-py/app/templates/login.html << 'TEMPLATE'
|
||||
{% extends "base.html" %}
|
||||
{% block body_class %}login-page{% endblock %}
|
||||
{% block content %}
|
||||
<link rel="stylesheet" href="/static/css/service.css">
|
||||
<div class="login-box">
|
||||
<div class="login-brand">
|
||||
<img src="/logo.php" alt="AegisOne" class="login-brand__img">
|
||||
<div class="login-brand__title">AegisOne Engineering</div>
|
||||
<div class="login-brand__subtitle">Service Portal</div>
|
||||
</div>
|
||||
<div class="login-divider"></div>
|
||||
<h1>Вход в сервисный портал</h1>
|
||||
{% if error %}
|
||||
<div class="alert alert-error">{{ error }}</div>
|
||||
{% endif %}
|
||||
<form method="post">
|
||||
<div class="form-group">
|
||||
<label for="login">Логин</label>
|
||||
<input type="text" id="login" name="login" placeholder="Введите логин" required autocomplete="username" autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="password">Пароль</label>
|
||||
<input type="password" id="password" name="password" placeholder="Введите пароль" required autocomplete="current-password">
|
||||
</div>
|
||||
<button type="submit" class="btn btn-primary btn-block">Войти</button>
|
||||
</form>
|
||||
<p class="login-box__footnote">* Сервисный портал для сотрудников AegisOne Engineering</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
TEMPLATE
|
||||
echo " ✅ Шаблон исправлен"
|
||||
|
||||
# 7. Исправить main.py - добавить StaticFiles
|
||||
echo ""
|
||||
echo "[7/7] Исправление main.py..."
|
||||
sed -i 's/from fastapi.staticfiles import StaticFiles/from fastapi.staticfiles import StaticFiles\nfrom pathlib import Path/' /opt/projects/aegisone-py/app/main.py
|
||||
|
||||
# Проверить, есть ли уже mount static
|
||||
if ! grep -q "app.mount.*static" /opt/projects/aegisone-py/app/main.py; then
|
||||
# Добавить после templates = Jinja2Templates(...)
|
||||
sed -i '/templates = Jinja2Templates/a\
|
||||
app.mount("/static", StaticFiles(directory="app/static"), name="static")' /opt/projects/aegisone-py/app/main.py
|
||||
echo " ✅ StaticFiles добавлен"
|
||||
else
|
||||
echo " ℹ️ StaticFiles уже есть"
|
||||
fi
|
||||
|
||||
# 8. Перезапуск
|
||||
echo ""
|
||||
echo "[8/8] Перезапуск приложения..."
|
||||
cd /opt/projects/aegisone-py
|
||||
docker compose restart app
|
||||
sleep 5
|
||||
|
||||
# 9. Проверка
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Проверка работоспособности"
|
||||
echo "=========================================="
|
||||
|
||||
HEALTH=$(curl -s http://localhost:8000/health 2>/dev/null || echo "FAILED")
|
||||
echo "Health check: $HEALTH"
|
||||
|
||||
LOGIN=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/service/login 2>/dev/null || echo "FAILED")
|
||||
echo "Login page: HTTP $LOGIN"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo " Готово!"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "Логин: owner"
|
||||
echo "Пароль: AegisOne2024!"
|
||||
echo ""
|
||||
echo "URL: http://81.177.141.34:8000/service/login"
|
||||
echo ""
|
||||
+3933
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,67 @@
|
||||
py_service/.env.example
|
||||
py_service/CHANGELOG.md
|
||||
py_service/Dockerfile
|
||||
py_service/app/config.py
|
||||
py_service/app/dependencies.py
|
||||
py_service/app/documents_router.py
|
||||
py_service/app/main.py
|
||||
py_service/app/models/models.py
|
||||
py_service/app/routers/service_pages.py
|
||||
py_service/app/static/css/service-theme.css
|
||||
py_service/app/static/css/service.css
|
||||
py_service/app/static/js/theme.js
|
||||
py_service/app/templates/base.html
|
||||
py_service/app/templates/dashboard.html
|
||||
py_service/app/templates/page.html
|
||||
py_service/docker-compose.yml
|
||||
py_service/requirements.txt
|
||||
py_service/version.txt
|
||||
py_service/.dockerignore
|
||||
py_service/app/file_storage.py
|
||||
py_service/app/file_sync.py
|
||||
py_service/app/max_notifications.py
|
||||
py_service/app/static/js/role-switcher.js
|
||||
py_service/app/templates/pages/bot_analytics.html
|
||||
py_service/app/templates/pages/bot_categories.html
|
||||
py_service/app/templates/pages/bot_conversations.html
|
||||
py_service/app/templates/pages/bot_features.html
|
||||
py_service/app/templates/pages/bot_kb.html
|
||||
py_service/app/templates/pages/bot_responses.html
|
||||
py_service/app/templates/pages/bot_settings.html
|
||||
py_service/app/templates/pages/bot_storage.html
|
||||
py_service/app/templates/pages/bot_test_runner.html
|
||||
py_service/app/templates/pages/bot_tickets.html
|
||||
py_service/app/templates/pages/bot_users.html
|
||||
py_service/app/templates/pages/formulas.html
|
||||
py_service/app/templates/pages/portal_settings.html
|
||||
py_service/app/templates/pages/role_settings.html
|
||||
py_service/app/yandex_disk.py
|
||||
"py_service/docs/KPI_\320\270\320\275\320\266\320\265\320\275\320\265\321\200\320\276\320\262.md"
|
||||
"py_service/docs/SQL-\320\267\320\260\320\277\321\200\320\276\321\201\321\213.md"
|
||||
"py_service/docs/\320\220\321\200\321\205\320\265\321\202\320\270\320\272\321\202\321\203\321\200\320\260 \320\261\321\200\320\265\320\275\320\264\320\260.md"
|
||||
"py_service/docs/\320\220\321\200\321\205\320\270\321\202\320\265\320\272\321\202\321\203\321\200\320\260 \320\272\320\276\320\274\320\277\320\260\320\275\320\270\320\270.md"
|
||||
"py_service/docs/\320\221\320\273\320\276\320\263.md"
|
||||
"py_service/docs/\320\222\320\276\321\200\320\276\320\275\320\272\320\260.md"
|
||||
"py_service/docs/\320\222\320\276\321\200\320\276\320\275\320\272\320\270 \321\202\320\265\320\273\320\265\320\263\321\200\320\260\320\274 \320\270 crm.md"
|
||||
"py_service/docs/\320\224\320\260\321\210\320\261\320\276\321\200\320\264 \320\270\320\275\320\266\320\265\320\275\320\265\321\200\320\260.md"
|
||||
"py_service/docs/\320\224\320\260\321\210\320\261\320\276\321\200\320\264 \321\202\320\265\321\205\320\275\320\270\320\272\320\260.md"
|
||||
"py_service/docs/\320\224\320\276\320\272\320\276\320\267\320\260\321\202\320\265\320\273\321\214\320\275\320\260\321\217 \321\215\320\272\321\201\320\277\320\265\321\200\321\202\320\270\320\267\320\260.md"
|
||||
"py_service/docs/\320\230\320\275\321\201\321\202\321\200\321\203\320\272\321\206\320\270\321\217 \320\277\320\276 \320\260\320\264\320\274\320\270\320\275-\320\277\320\260\320\275\320\265\320\273\320\270.md"
|
||||
"py_service/docs/\320\232\320\237 \320\270 \321\201\321\202\321\200\321\203\320\272\321\202\321\203\321\200\320\260 \320\260\321\203\320\264\320\270\321\202\320\260.md"
|
||||
"py_service/docs/\320\232\320\276\320\275\321\202\320\265\320\275\321\202 \320\277\320\273\320\260\320\275.md"
|
||||
"py_service/docs/\320\234\320\260\321\200\320\272\320\265\321\202\321\202\320\270\320\275\320\263.md"
|
||||
"py_service/docs/\320\236\320\277\321\200\320\276\321\201\320\275\320\270\320\272-\321\200\320\260\321\201\321\207\320\265\321\202.md"
|
||||
"py_service/docs/\320\236\321\201\320\275\320\276\320\262\320\260.md"
|
||||
"py_service/docs/\320\241\320\260\320\271\321\202 \320\270 \321\203\320\277\320\260\320\272\320\276\320\262\320\272\320\260.md"
|
||||
"py_service/docs/\320\241\320\272\321\200\320\270\320\277\321\202\321\213 \320\277\321\200\320\276\320\264\320\260\320\266.md"
|
||||
"py_service/docs/\320\243\320\242\320\237 \320\270 \320\232\320\237.md"
|
||||
"py_service/docs/\320\244\320\276\321\200\320\274\321\203\320\273\321\213 \320\270 \321\200\320\260\321\201\321\207\321\221\321\202\321\213.md"
|
||||
"py_service/docs/\320\246\320\270\321\204\321\200\320\276\320\262\320\260\321\217 \320\274\320\276\320\264\320\265\320\273\321\214 \320\276\320\261\321\211\320\260\321\217.md"
|
||||
"py_service/docs/\320\260\320\262\321\202\320\276\320\274\320\260\321\202\320\270\321\207\320\265\321\201\320\272\320\260\321\217_\321\201\320\270\321\201\321\202\320\265\320\274\320\260_SHS.md"
|
||||
py_service/scripts/migrate_to_yandex.py
|
||||
py_service/sql/v1.5.0_migration.sql
|
||||
py_service/tests/__init__.py
|
||||
py_service/tests/conftest.py
|
||||
py_service/tests/test_routes.py
|
||||
py_service/tests/test_routes_defined.py
|
||||
py_service/tests/test_smoke_server.py
|
||||
@@ -0,0 +1,46 @@
|
||||
Чтобы зарегистрировать URL https://max.aegisone.ru/webhook в качестве вебхука на платформе MAX, необходимо отправить POST-запрос на метод /subscriptions через API. Для этого потребуется токен бота, сам URL и, опционально, дополнительные параметры — типы событий и секретный ключ для проверки подписи.
|
||||
|
||||
Требования к URL вебхука
|
||||
Протокол: только HTTPS. HTTP не поддерживается.
|
||||
|
||||
Порт: только 443. Порт в URL не указывается.
|
||||
|
||||
Сертификат: должен быть выдан доверенным центром сертификации. Самоподписанные сертификаты не поддерживаются.
|
||||
|
||||
Доменное имя в URL должно совпадать с CN (Common Name) или SAN (Subject Alternative Name) сертификата.
|
||||
|
||||
Сервер должен предоставлять полную цепочку сертификатов.
|
||||
|
||||
Параметры запроса
|
||||
При отправке POST-запроса на /subscriptions укажите следующие параметры в теле запроса:
|
||||
|
||||
url (обязательный) — HTTPS-адрес вашего endpoint, например https://max.aegisone.ru/webhook.
|
||||
update_types (необязательный) — массив типов событий, которые хочет получать бот. Полный список типов событий можно найти в документации MAX.
|
||||
maxo.readthedocs.io +1
|
||||
secret (необязательный) — строка от 5 до 256 символов, состоящая из букв (A-Z, a-z), цифр (0-9) и дефиса. Если указать этот параметр, MAX будет добавлять его в заголовок X-Max-Bot-Api-Secret каждого callback-запроса. На сервере можно проверять это значение для верификации запросов.
|
||||
|
||||
Пример запроса
|
||||
|
||||
bash
|
||||
curl -X POST "https://platform-api.max.ru/subscriptions" \
|
||||
-H "Authorization: YOUR_BOT_TOKEN" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"url": "https://max.aegisone.ru/webhook",
|
||||
"update_types": ["message_created", "bot_started"],
|
||||
"secret": "your_secret_key"
|
||||
}'
|
||||
|
||||
В заголовке Authorization передаётся токен бота без префикса Bearer.
|
||||
|
||||
Дополнительные сведения
|
||||
После успешной регистрации вебхука MAX будет отправлять HTTPS POST-запросы на указанный URL при наступлении выбранных событий.
|
||||
Вебхук-эндпоинт должен возвращать HTTP 200 в течение 30 секунд. Любой другой код ответа или превышение тайм-аута считается ошибкой доставки.
|
||||
|
||||
Если доставка не удалась, MAX выполняет до 10 повторных попыток с экспоненциально растущим интервалом. Если в течение 8 часов не получен успешный ответ, бот автоматически отписывается от вебхука.
|
||||
Одновременно можно использовать только один механизм доставки событий: либо вебхук, либо long polling. При регистрации вебхука long polling автоматически отключается.
|
||||
|
||||
Проверка подписки
|
||||
Чтобы убедиться, что подписка прошла успешно, можно отправить GET-запрос на /subscriptions с токеном бота в заголовке. В ответе следует проверить, что указанный URL и параметры (если они задавались) соответствуют ожидаемым.
|
||||
|
||||
Если при регистрации возникает ошибка, стоит проверить корректность токена, JSON-запроса и соответствие URL требованиям (HTTPS на порту 443).
|
||||
Reference in New Issue
Block a user