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:
2026-05-29 02:30:30 +03:00
parent 493e0b37a1
commit 72b6879f4b
234 changed files with 26768 additions and 6240 deletions
+529
View File
@@ -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 (София)*
+289
View File
@@ -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
View File
@@ -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 ""
+152
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+67
View File
@@ -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
+46
View File
@@ -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).