MAX bot + n8n: webhook, нормализация, меню, доки, схемы БД

- register_max_webhook.py, fetch_schema.py
- n8n-code-node-max-normalize.js (max_id, callback из callback.user, contact из vcf_info)
- n8n-code-add-menu-buttons.js (меню с callback, request_contact, Главное меню)
- docs: max-webhook, max-curl-http-request, max-api (форматы, кнопки, контакт), clpr vs sprf
- README, SITUATION, схемы sprf_ и clpr_, .gitignore

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
root
2026-02-16 09:23:26 +03:00
commit 7cd3ccf21c
17 changed files with 1888 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@@ -0,0 +1,7 @@
.env
*.pyc
__pycache__/
.venv/
venv/
*.log
.DS_Store

72
README.md Normal file
View File

@@ -0,0 +1,72 @@
# MAX Bot + n8n (СПРФ / Клиент)
Интеграция бота в мессенджере **MAX** с **n8n**: webhook, нормализация входящих, отправка сообщений и кнопок, работа с БД (PostgreSQL, схемы sprf_ / clpr_).
## Содержимое репозитория
### Скрипты
| Файл | Назначение |
|------|------------|
| **register_max_webhook.py** | Регистрация webhook бота MAX на URL n8n (читает .env: MAX_BOT_TOKEN, N8N_MAX_WORKFLOW, MAX_WEBHOOK_SECRET). |
| **fetch_schema.py** | Выгрузка структуры таблиц `sprf_*` из PostgreSQL в `sprf_tables_schema.md`. |
### Ноды для n8n (Code node)
| Файл | Назначение |
|------|------------|
| **n8n-code-node-max-normalize.js** | Нормализация входящего Webhook MAX: один объект с `max_id`, `max_chat_id`, `answer_text`, `answer_type` (text, command, callback, contact, voice, photo, file и т.д.), `callback_id`, `callback_message_text`, `contact_payload` и др. Личные чаты (dialog); при callback пользователь берётся из `callback.user`. |
| **n8n-code-add-menu-buttons.js** | Формирование тела сообщения с меню: текст + inline_keyboard (callback-кнопки, request_contact, кнопка «Главное меню» type message). Выход: `message_body` для POST /messages. |
### Документация
| Путь | Описание |
|------|----------|
| **docs/max-webhook.md** | Настройка Webhook в n8n, регистрация в MAX, отправка ответа (POST /messages), ответ на callback (POST /answers), удаление кнопок. |
| **docs/max-curl-http-request.md** | Примеры curl и настройка HTTP Request в n8n: отправка сообщения, кнопки, ответ на callback, удаление кнопок. |
| **docs/max-api/** | Локальная копия/выжимка MAX Bot API: обзор, методы (messages, updates, subscriptions, answers), объекты (Update, Message, MessageBody, NewMessageBody), форматы текста (markdown/html), кнопки (inline_keyboard: callback, message, link, request_contact и др.), контакт (vcf_info, max_info). |
| **docs/clpr-vs-sprf-schema-diff.md** | Сравнение структуры таблиц БД с префиксами clpr_ и sprf_. |
| **SITUATION.md** | Текущая ситуация: что настроено, команды, файлы. |
### Схемы БД
| Файл | Описание |
|------|----------|
| **sprf_tables_schema.md** | Структура таблиц с префиксом `sprf_` (public). |
| **clpr_tables_schema.md** | Структура таблиц с префиксом `clpr_` (public). |
## Требования
- Python 3, зависимости: `psycopg2-binary` (для fetch_schema).
- В корне файл **.env** (не коммитить): `MAX_BOT_TOKEN`, `N8N_MAX_WORKFLOW`, `MAX_WEBHOOK_SECRET`, при необходимости `MAX_API_BASE`; для выгрузки схемы: `PGHOST`, `PGPORT`, `PGDATABASE`, `PGUSER`, `PGPASSWORD`.
## Быстрый старт
1. Настроить Webhook в n8n (path = `sprf_max`, POST), включить воркфлоу.
2. Выполнить: `python3 register_max_webhook.py`.
3. В воркфлоу после Webhook вставить Code node с содержимым `n8n-code-node-max-normalize.js`.
4. Ответ пользователю: HTTP Request — POST `https://platform-api.max.ru/messages?user_id={{ $json.max_id }}`, body из `message_body` или свой JSON (текст, кнопки — см. docs).
Подробнее: **docs/max-webhook.md**, **docs/max-curl-http-request.md**, **docs/max-api/04-formats-and-buttons.md**.
## Ограничения MAX API
- Редактирование (PUT /messages) и удаление (DELETE /messages) — только для сообщений **моложе 24 часов**.
- POST /answers (обновление сообщения с кнопками) — по факту тоже редактирование; при старше 24 ч может не сработать.
## Git
Репозиторий инициализирован, первый коммит на ветке `main`. Remote: `origin` → Gitea (при необходимости измените URL).
Чтобы отправить код на сервер (после создания репозитория MAX в Gitea при необходимости):
```bash
cd /dev/MAX
git push -u origin main
```
Логин/пароль Gitea запросит при первом push.
## Лицензия / конфиденциальность
Скрипты и доки — для внутреннего использования. Не коммитить .env и токены.

60
SITUATION.md Normal file
View File

@@ -0,0 +1,60 @@
# Текущая ситуация — MAX / СПРФ (актуализировано 13.02.2025)
## Что это за проект
- **Бот MAX** (мессенджер) получает события по **Webhook** на n8n.
- n8n обрабатывает сообщения и коллбэки, при необходимости ходит в **PostgreSQL** (таблицы `sprf_*`) и в **MAX API** (ответы, кнопки).
- Документация MAX API и инструкции по webhook лежат в `docs/`.
## Что уже настроено и работает
| Компонент | Статус |
|-----------|--------|
| **.env** | Есть: `MAX_BOT_TOKEN`, `N8N_MAX_WORKFLOW`, `MAX_WEBHOOK_SECRET`, параметры PostgreSQL |
| **Webhook в MAX** | Зарегистрирован на `https://n8n.clientright.pro/webhook/sprf_max` (события: `message_created`, `message_callback`, `bot_started`) |
| **Секрет** | Задан в .env; в n8n проверяй заголовок `X-Max-Bot-Api-Secret` |
| **Схема БД** | Выгружена в `sprf_tables_schema.md` (таблицы sprf_claims, sprf_chat_messages, sprf_conversation_state и др.) |
## Что нужно проверить вручную
1. **n8n** (https://n8n.clientright.pro): воркфлоу с нодой **Webhook** включён (Production), путь = `sprf_max`, метод POST.
2. **Проверка доставки**: написать боту в MAX — во входящих данных Webhook в n8n должен появиться объект с `update_type`, `message` и т.д.
## Полезные команды
```bash
# Заново зарегистрировать webhook (если меняли URL или пересоздавали воркфлоу)
python3 register_max_webhook.py
# Проверить, какие подписки зарегистрированы в MAX
python3 -c "
import os, json, urllib.request
from pathlib import Path
for line in Path('.env').read_text().splitlines():
s = line.strip()
if s and not s.startswith('#') and '=' in s:
k, v = s.split('=', 1)
os.environ[k.strip()] = v.strip()
r = urllib.request.urlopen(urllib.request.Request(
os.environ.get('MAX_API_BASE','https://platform-api.max.ru').rstrip('/') + '/subscriptions',
headers={'Authorization': os.environ['MAX_BOT_TOKEN']}, method='GET'))
print(r.read().decode())
"
# Обновить схему таблиц sprf_ из PostgreSQL (нужны PGHOST, PGUSER, PGPASSWORD, PGDATABASE в .env)
python3 fetch_schema.py
```
## Файлы в проекте
- `register_max_webhook.py` — регистрация webhook в MAX
- `fetch_schema.py` — выгрузка схемы таблиц sprf_ в `sprf_tables_schema.md`
- `docs/max-webhook.md` — пошаговая настройка webhook в n8n и в MAX
- `docs/max-api/` — обзор API, методы, объекты (0103)
- `sprf_tables_schema.md` — структура таблиц БД
- `.env` — токены и URL (не коммитить)
## Дальше
- Дорабатывать воркфлоу в n8n под логику бота (ответы, кнопки, запись в БД).
- При смене URL webhook — обновить `N8N_MAX_WORKFLOW` в .env и снова запустить `python3 register_max_webhook.py`.

191
clpr_tables_schema.md Normal file
View File

@@ -0,0 +1,191 @@
# Структура таблиц clpr_ (public, default_db)
## clpr_chat_messages
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | integer | | NO | nextval('clpr_chat_messages_id_seq'::reg... |
| claim_id | uuid | | YES | |
| from_user | boolean | | YES | |
| message_text | text | | YES | |
| file_id | text | | YES | |
| sent_at | timestamp with time zone | | YES | now() |
## clpr_claim_documents
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | uuid | | NO | gen_random_uuid() |
| claim_id | character varying | | YES | |
| field_name | text | | YES | |
| file_id | text | | YES | |
| uploaded_at | timestamp with time zone | | YES | now() |
| file_name | text | | YES | |
| original_file_name | text | | YES | |
| file_hash | character varying | 64 | YES | |
| ocr_status | character varying | 20 | YES | 'pending'::character varying |
| ocr_processed_at | timestamp with time zone | | YES | |
| ocr_error | text | | YES | |
| document_type | character varying | 50 | YES | |
| document_label | character varying | 255 | YES | |
| match_score | integer | | YES | |
| match_status | character varying | 20 | YES | 'pending'::character varying |
| match_reason | text | | YES | |
| match_checked_at | timestamp without time zone | | YES | |
| document_summary | text | | YES | |
## clpr_claim_statuses
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| code | text | | NO | |
| description | text | | YES | |
## clpr_claim_types
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| code | text | | NO | |
| description | text | | YES | |
## clpr_claims
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | uuid | | NO | gen_random_uuid() |
| session_token | character varying | | YES | |
| unified_id | character varying | | YES | |
| telegram_id | bigint | | YES | |
| channel | text | | YES | |
| user_id | integer | | YES | |
| type_code | text | | YES | |
| status_code | text | | YES | |
| policy_number | text | | YES | |
| payload | jsonb | | YES | |
| is_confirmed | boolean | | YES | false |
| created_at | timestamp with time zone | | YES | now() |
| updated_at | timestamp with time zone | | YES | now() |
| expires_at | timestamp with time zone | | YES | |
| contact_id | text | | YES | |
| phone | text | | YES | |
## clpr_conversation_state
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| user_id | integer | | NO | |
| current_step | text | | YES | |
| data | jsonb | | YES | |
| updated_at | timestamp with time zone | | YES | |
## clpr_dialog_history_tg
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | integer | | NO | nextval('clpr_dialog_history_tg_id_seq':... |
| telegram_id | bigint | | YES | |
| role | character varying | | YES | |
| message | text | | YES | |
| created_at | timestamp with time zone | | YES | now() |
| session_token | character varying | | YES | |
| claim_id | uuid | | YES | |
| message_type | text | | YES | |
| payload | jsonb | | YES | |
| tg_message_id | bigint | | YES | |
| tg_update_id | bigint | | YES | |
## clpr_document_embeddings
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | uuid | | YES | |
| embedding | USER-DEFINED | | YES | |
| text | text | | YES | |
| metadata | jsonb | | YES | |
## clpr_documents
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | uuid | | NO | gen_random_uuid() |
| source | text | | YES | |
| content | text | | YES | |
| metadata | jsonb | | YES | |
| created_at | timestamp with time zone | | YES | now() |
## clpr_menu_commands
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | bigint | | NO | nextval('clpr_menu_commands_id_seq'::reg... |
| command | text | | NO | |
| description | text | | YES | |
| action | text | | YES | |
| reply_text | text | | YES | |
| menu_id | text | | NO | 'main'::text |
| menu_version | integer | | NO | 1 |
| created_at | timestamp with time zone | | NO | now() |
| updated_at | timestamp with time zone | | YES | |
## clpr_operators
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | integer | | NO | nextval('clpr_operators_id_seq'::regclas... |
| telegram_id | bigint | | YES | |
| name | text | | YES | |
| is_active | boolean | | YES | |
| created_at | timestamp with time zone | | YES | now() |
## clpr_sessions
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | uuid | | NO | gen_random_uuid() |
| user_id | integer | | YES | |
| session_token | character varying | | YES | |
| created_at | timestamp with time zone | | YES | now() |
| last_activity | timestamp with time zone | | YES | |
| expires_at | timestamp with time zone | | YES | |
## clpr_user_accounts
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | integer | | NO | nextval('clpr_user_accounts_id_seq'::reg... |
| user_id | integer | | YES | |
| channel | text | | YES | |
| channel_user_id | text | | YES | |
## clpr_users
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | integer | | NO | nextval('clpr_users_id_seq'::regclass) |
| universal_id | uuid | | YES | |
| unified_id | character varying | | YES | |
| phone | character varying | | YES | |
| created_at | timestamp with time zone | | YES | now() |
| updated_at | timestamp with time zone | | YES | now() |
| contact_data_confirmed_at | timestamp with time zone | | YES | |
## clpr_users_tg
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| telegram_id | bigint | | NO | |
| phone_number | character varying | | YES | |
| first_name_tg | character varying | | YES | |
| last_name_tg | character varying | | YES | |
| username | character varying | | YES | |
| language_code | character varying | | YES | |
| is_premium | boolean | | YES | |
| unified_id | character varying | | YES | |
| first_name | character varying | | YES | |
| last_name | character varying | | YES | |
| middle_name | character varying | | YES | |
| birth_date | character varying | | YES | |
| birth_place | character varying | | YES | |
| inn | character varying | | YES | |
| address | character varying | | YES | |
| email | character varying | | YES | |
| is_confirmed | boolean | | YES | false |
| created_at | timestamp with time zone | | YES | now() |
| updated_at | timestamp with time zone | | YES | now() |
## clpr_wizard_questions
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | integer | | NO | nextval('clpr_wizard_questions_id_seq'::... |
| claim_type | text | | YES | |
| step_key | text | | YES | |
| question_text | text | | YES | |
| answer_type | text | | YES | |
| step_order | integer | | YES | |
| options | jsonb | | YES | |
| is_required | boolean | | YES | |

View File

@@ -0,0 +1,354 @@
# Сравнение структуры таблиц clpr_ и sprf_
## 1. Состав: что только в одной схеме
| Только в **sprf_** | Только в **clpr_** |
|-------------------|--------------------|
| sprf_court_decisions (файлы судебных решений, OCR, вектор, nsfw, CRM) | clpr_menu_commands (команды меню: command, action, reply_text, menu_id) |
| sprf_court_decisions_view (view поверх court_decisions) | — |
Остальные таблицы есть в обеих схемах (с разными именами префикса).
---
## 2. Общие таблицы — отличия по колонкам и типам
### chat_messages
| Аспект | sprf | clpr |
|--------|------|------|
| sent_at | без default | default `now()` |
Остальные колонки совпадают.
---
### claim_documents
| Аспект | sprf | clpr |
|--------|------|------|
| Колонки | Базовый набор: id, claim_id, field_name, file_id, uploaded_at | Расширенный: + file_name, original_file_name, file_hash, ocr_status, ocr_processed_at, ocr_error, document_type, document_label, match_score, match_status, match_reason, match_checked_at, document_summary |
| id | uuid, NO, без default в схеме | uuid, NO, gen_random_uuid() |
| uploaded_at | без default | default now() |
**Итог:** в clpr заложена полноценная модель документов с OCR, матчингом и саммари; в sprf — минимальный набор под «файл к заявке».
---
### claim_statuses, claim_types
Структура совпадает (code, description).
---
### claims
| Аспект | sprf | clpr |
|--------|------|------|
| id | character varying, NO | uuid, NO, gen_random_uuid() |
| Доп. поля | — | expires_at, contact_id, phone |
| created_at / updated_at | без default в схеме | default now() |
Остальные поля (user_id, type_code, status_code, payload, session_token, unified_id, telegram_id, channel, is_confirmed) совпадают.
---
### conversation_state
Структура совпадает (user_id, current_step, data, updated_at).
---
### dialog_history_tg
| Аспект | sprf | clpr |
|--------|------|------|
| claim_id | character varying | uuid |
| created_at | default now() | default now() |
Остальные колонки те же.
---
### document_embeddings
| Аспект | sprf | clpr |
|--------|------|------|
| Модель | Чанки документа: document_id, chunk_index, embedding | Один объект на запись: id, embedding, text, metadata |
| Колонки | document_id, chunk_index, embedding | id, embedding, text, metadata |
Разная семантика: sprf — эмбеддинги чанков с привязкой к документу; clpr — эмбеддинг + текст + метаданные без явного document_id/chunk_index.
---
### documents
| Аспект | sprf | clpr |
|--------|------|------|
| id | uuid, NO, без default в схеме | uuid, NO, gen_random_uuid() |
| created_at | без default | default now() |
Остальное совпадает (source, content, metadata).
---
### operators
Структура совпадает. В clpr у created_at указан default now(), в sprf в схеме default не показан.
---
### sessions
Структура совпадает (id uuid, user_id, session_token, created_at, last_activity, expires_at). В clpr created_at = now().
---
### user_accounts
Структура совпадает (user_id, channel, channel_user_id).
---
### users
| Аспект | sprf | clpr |
|--------|------|------|
| Доп. поле | — | contact_data_confirmed_at |
| created_at / updated_at | без default в схеме | default now() |
Остальное совпадает (universal_id, unified_id, phone).
---
### users_tg
Набор полей совпадает. В clpr у created_at и updated_at default now(); в sprf в схеме default не показан.
---
### wizard_questions
Структура совпадает (claim_type, step_key, question_text, answer_type, step_order, options, is_required).
---
## 3. Краткая сводка
- **Только sprf:** судебные решения (court_decisions + view) — загрузка файлов, OCR, вектор, nsfw, CRM.
- **Только clpr:** меню команд (menu_commands).
- **clpr в среднем «богаче»:** в claims — uuid, expires_at, contact_id, phone; в claim_documents — OCR, матчинг, саммари; в users — contact_data_confirmed_at; чаще default now() на датах.
- **sprf.claims.id** — varchar, **clpr.claims.id** — uuid.
- **document_embeddings** устроены по-разному: sprf — по чанкам (document_id, chunk_index), clpr — id + text + metadata.
Если делать общий слой поверх двух схем (например, для MAX/Telegram), маппинг по именам таблиц 1:1, но нужно учитывать типы (claim_id, id в claims) и наличие/отсутствие полей (contact_id, phone, expires_at, OCR/match в claim_documents).
---
## 4. Детальное сравнение по полям (тип, NULL, default)
Для каждой общей таблицы: колонка | sprf тип / NULL / default | clpr тип / NULL / default | примечание.
### chat_messages
| Колонка | sprf (тип / NULL / default) | clpr (тип / NULL / default) | Примечание |
|-------------|---------------------------------|----------------------------------|--------------|
| id | integer / NO / nextval | integer / NO / nextval | одинаково |
| claim_id | uuid / YES / — | uuid / YES / — | одинаково |
| from_user | boolean / YES / — | boolean / YES / — | одинаково |
| message_text| text / YES / — | text / YES / — | одинаково |
| file_id | text / YES / — | text / YES / — | одинаково |
| sent_at | timestamptz / YES / — | timestamptz / YES / **now()** | разный default |
---
### claim_documents
| Колонка | sprf (тип / NULL / default) | clpr (тип / NULL / default) | Примечание |
|--------------------|-----------------------------|-----------------------------|-----------------|
| id | uuid / NO / — | uuid / NO / gen_random_uuid() | разный default |
| claim_id | character varying / YES / — | character varying / YES / — | одинаково |
| field_name | text / YES / — | text / YES / — | одинаково |
| file_id | text / YES / — | text / YES / — | одинаково |
| uploaded_at | timestamptz / YES / — | timestamptz / YES / now() | разный default |
| file_name | — | text / YES / — | только clpr |
| original_file_name | — | text / YES / — | только clpr |
| file_hash | — | varchar(64) / YES / — | только clpr |
| ocr_status | — | varchar(20) / YES / 'pending'| только clpr |
| ocr_processed_at | — | timestamptz / YES / — | только clpr |
| ocr_error | — | text / YES / — | только clpr |
| document_type | — | varchar(50) / YES / — | только clpr |
| document_label | — | varchar(255) / YES / — | только clpr |
| match_score | — | integer / YES / — | только clpr |
| match_status | — | varchar(20) / YES / 'pending'| только clpr |
| match_reason | — | text / YES / — | только clpr |
| match_checked_at | — | timestamp / YES / — | только clpr |
| document_summary | — | text / YES / — | только clpr |
---
### claim_statuses
| Колонка | sprf | clpr | Примечание |
|-------------|------|------|------------|
| code | text / NO / — | text / NO / — | одинаково |
| description | text / YES / — | text / YES / — | одинаково |
---
### claim_types
| Колонка | sprf | clpr | Примечание |
|-------------|------|------|------------|
| code | text / NO / — | text / NO / — | одинаково |
| description | text / YES / — | text / YES / — | одинаково |
---
### claims
| Колонка | sprf (тип / NULL / default) | clpr (тип / NULL / default) | Примечание |
|---------------|-----------------------------|-----------------------------|-----------------|
| id | **character varying** / NO / — | **uuid** / NO / gen_random_uuid() | разный тип и default |
| session_token | varchar / YES / — | varchar / YES / — | одинаково |
| unified_id | varchar / YES / — | varchar / YES / — | одинаково |
| telegram_id | bigint / YES / — | bigint / YES / — | одинаково |
| channel | text / YES / — | text / YES / — | одинаково |
| user_id | integer / YES / — | integer / YES / — | одинаково |
| type_code | text / YES / — | text / YES / — | одинаково |
| status_code | text / YES / — | text / YES / — | одинаково |
| policy_number | text / YES / — | text / YES / — | одинаково |
| payload | jsonb / YES / — | jsonb / YES / — | одинаково |
| is_confirmed | boolean / YES / false | boolean / YES / false | одинаково |
| created_at | timestamptz / YES / — | timestamptz / YES / **now()** | разный default |
| updated_at | timestamptz / YES / — | timestamptz / YES / **now()** | разный default |
| expires_at | — | timestamptz / YES / — | только clpr |
| contact_id | — | text / YES / — | только clpr |
| phone | — | text / YES / — | только clpr |
---
### conversation_state
| Колонка | sprf | clpr | Примечание |
|--------------|------|------|------------|
| user_id | integer / NO / — | integer / NO / — | одинаково |
| current_step | text / YES / — | text / YES / — | одинаково |
| data | jsonb / YES / — | jsonb / YES / — | одинаково |
| updated_at | timestamptz / YES / — | timestamptz / YES / — | одинаково |
---
### dialog_history_tg
| Колонка | sprf (тип / NULL / default) | clpr (тип / NULL / default) | Примечание |
|---------------|-----------------------------|-----------------------------|----------------|
| id | integer / NO / nextval | integer / NO / nextval | одинаково |
| telegram_id | bigint / YES / — | bigint / YES / — | одинаково |
| role | varchar / YES / — | varchar / YES / — | одинаково |
| message | text / YES / — | text / YES / — | одинаково |
| created_at | timestamptz / YES / now() | timestamptz / YES / now() | одинаково |
| session_token | varchar / YES / — | varchar / YES / — | одинаково |
| claim_id | **character varying** / YES / — | **uuid** / YES / — | разный тип |
| message_type | text / YES / — | text / YES / — | одинаково |
| payload | jsonb / YES / — | jsonb / YES / — | одинаково |
| tg_message_id | bigint / YES / — | bigint / YES / — | одинаково |
| tg_update_id | bigint / YES / — | bigint / YES / — | одинаково |
---
### document_embeddings
| Колонка | sprf | clpr | Примечание |
|--------------|------|------|------------|
| document_id | uuid / YES / — | — | только sprf (модель по чанкам) |
| chunk_index | integer / YES / — | — | только sprf |
| embedding | USER-DEFINED / YES / — | USER-DEFINED / YES / — | одинаково |
| id | — | uuid / YES / — | только clpr |
| text | — | text / YES / — | только clpr |
| metadata | — | jsonb / YES / — | только clpr |
Разная модель: sprf — чанки документа; clpr — запись с id, text, metadata.
---
### documents
| Колонка | sprf (тип / NULL / default) | clpr (тип / NULL / default) | Примечание |
|------------|-----------------------------|-----------------------------|--------------|
| id | uuid / NO / — | uuid / NO / gen_random_uuid() | разный default |
| source | text / YES / — | text / YES / — | одинаково |
| content | text / YES / — | text / YES / — | одинаково |
| metadata | jsonb / YES / — | jsonb / YES / — | одинаково |
| created_at | timestamptz / YES / — | timestamptz / YES / **now()** | разный default |
---
### operators
| Колонка | sprf (тип / NULL / default) | clpr (тип / NULL / default) | Примечание |
|------------|-----------------------------|-----------------------------|--------------|
| id | integer / NO / nextval | integer / NO / nextval | одинаково |
| telegram_id| bigint / YES / — | bigint / YES / — | одинаково |
| name | text / YES / — | text / YES / — | одинаково |
| is_active | boolean / YES / — | boolean / YES / — | одинаково |
| created_at | timestamptz / YES / — | timestamptz / YES / **now()** | разный default |
---
### sessions
| Колонка | sprf (тип / NULL / default) | clpr (тип / NULL / default) | Примечание |
|----------------|-----------------------------|-----------------------------|--------------|
| id | uuid / NO / gen_random_uuid() | uuid / NO / gen_random_uuid() | одинаково |
| user_id | integer / YES / — | integer / YES / — | одинаково |
| session_token | varchar / YES / — | varchar / YES / — | одинаково |
| created_at | timestamptz / YES / now() | timestamptz / YES / now() | одинаково |
| last_activity | timestamptz / YES / — | timestamptz / YES / — | одинаково |
| expires_at | timestamptz / YES / — | timestamptz / YES / — | одинаково |
---
### user_accounts
| Колонка | sprf | clpr | Примечание |
|----------------|------|------|------------|
| id | integer / NO / nextval | integer / NO / nextval | одинаково |
| user_id | integer / YES / — | integer / YES / — | одинаково |
| channel | text / YES / — | text / YES / — | одинаково |
| channel_user_id| text / YES / — | text / YES / — | одинаково |
---
### users
| Колонка | sprf (тип / NULL / default) | clpr (тип / NULL / default) | Примечание |
|-----------|-----------------------------|-----------------------------|--------------|
| id | integer / NO / nextval | integer / NO / nextval | одинаково |
| universal_id | uuid / YES / — | uuid / YES / — | одинаково |
| unified_id| varchar / YES / — | varchar / YES / — | одинаково |
| phone | varchar / YES / — | varchar / YES / — | одинаково |
| created_at| timestamptz / YES / — | timestamptz / YES / **now()** | разный default |
| updated_at| timestamptz / YES / — | timestamptz / YES / **now()** | разный default |
| contact_data_confirmed_at | — | timestamptz / YES / — | только clpr |
---
### users_tg
| Колонка | sprf (тип / NULL / default) | clpr (тип / NULL / default) | Примечание |
|---------------|-----------------------------|-----------------------------|--------------|
| telegram_id | bigint / NO / — | bigint / NO / — | одинаково |
| phone_number | varchar / YES / — | varchar / YES / — | одинаково |
| first_name_tg | varchar / YES / — | varchar / YES / — | одинаково |
| last_name_tg | varchar / YES / — | varchar / YES / — | одинаково |
| username | varchar / YES / — | varchar / YES / — | одинаково |
| language_code | varchar / YES / — | varchar / YES / — | одинаково |
| is_premium | boolean / YES / — | boolean / YES / — | одинаково |
| unified_id | varchar / YES / — | varchar / YES / — | одинаково |
| first_name, last_name, middle_name | varchar / YES / — | varchar / YES / — | одинаково |
| birth_date, birth_place, inn, address, email | varchar / YES / — | varchar / YES / — | одинаково |
| is_confirmed | boolean / YES / false | boolean / YES / false | одинаково |
| created_at | timestamptz / YES / — | timestamptz / YES / **now()** | разный default |
| updated_at | timestamptz / YES / — | timestamptz / YES / **now()** | разный default |
---
### wizard_questions
| Колонка | sprf | clpr | Примечание |
|---------------|------|------|------------|
| id | integer / NO / nextval | integer / NO / nextval | одинаково |
| claim_type | text / YES / — | text / YES / — | одинаково |
| step_key | text / YES / — | text / YES / — | одинаково |
| question_text | text / YES / — | text / YES / — | одинаково |
| answer_type | text / YES / — | text / YES / — | одинаково |
| step_order | integer / YES / — | integer / YES / — | одинаково |
| options | jsonb / YES / — | jsonb / YES / — | одинаково |
| is_required | boolean / YES / — | boolean / YES / — | одинаково |

View File

@@ -0,0 +1,11 @@
# Обзор Max Bot API
Базовый URL: **https://platform-api.max.ru**. Авторизация: заголовок `Authorization: <token>`. Токен через query не поддерживается. Токен берётся в платформе MAX для партнёров: Интеграция → Получить токен.
Коды ответов: 200 — успех; 400 — неверный запрос; 401 — ошибка аутентификации; 404 — не найден; 405 — метод не разрешён; 429 — лимит запросов; 503 — сервис недоступен.
Рекомендации: для разработки — Long Polling (GET /updates), для production — только Webhook. Не более 30 запросов в секунду.
Клавиатура (inline_keyboard): до 210 кнопок, до 30 рядов, до 7 кнопок в ряду. Типы: callback (событие message_callback), link, request_contact, request_geo_location, open_app, message. Для Webhook поддерживается только HTTPS.
Форматирование: в NewMessageBody поле format: markdown или html. Markdown: *курсив*, **жирный**, `код`, [ссылка](url). HTML: теги b, i, del, u, code, a.

View File

@@ -0,0 +1,86 @@
# Методы Max Bot API
## POST /messages — отправить сообщение
`POST https://platform-api.max.ru/messages?user_id={user_id}` или `?chat_id={chat_id}`
Заголовки: `Authorization: <token>`, `Content-Type: application/json`.
Query: user_id (int64) или chat_id (int64), опционально disable_link_preview (bool).
Тело (NewMessageBody): text (до 4000 символов), attachments, link, notify (по умолч. true), format (markdown | html).
Пример:
```bash
curl -X POST "https://platform-api.max.ru/messages?user_id=123" \
-H "Authorization: TOKEN" \
-H "Content-Type: application/json" \
-d '{"text": "Привет!", "format": "markdown"}'
```
Ответ: объект message (Message).
---
## GET /updates — Long Polling
`GET https://platform-api.max.ru/updates`
Параметры: limit (1-1000, по умолч. 100), timeout (0-90 сек, по умолч. 30), marker (int64), types (массив типов, напр. message_created, message_callback).
Ответ: updates (массив Update), marker для следующего запроса.
---
## POST /subscriptions — Webhook
`POST https://platform-api.max.ru/subscriptions`
Тело: url (обязательно https), update_types (массив), secret (5-256 символов, [a-zA-Z0-9_-]) — приходит в заголовке X-Max-Bot-Api-Secret.
Порты сервера: 80, 8080, 443, 8443, 16384-32383.
Пример:
```json
{
"url": "https://your-domain.com/webhook",
"update_types": ["message_created", "bot_started"],
"secret": "your_secret"
}
```
---
## POST /answers — ответ на нажатие кнопки
`POST https://platform-api.max.ru/answers?callback_id={callback_id}`
callback_id берётся из Update с типом message_callback.
Тело: message (NewMessageBody, опц.) — обновить сообщение; notification (string, опц.) — одноразовое уведомление.
---
## PUT /messages — редактировать сообщение
`PUT https://platform-api.max.ru/messages?message_id={message_id}`
Тело: text, attachments (null = не менять, [] = удалить все), link, notify, format.
**Ограничение:** редактировать можно только сообщения, отправленные **менее 24 часов назад**.
---
## DELETE /messages — удалить сообщение
`DELETE https://platform-api.max.ru/messages?message_id={message_id}`
**Ограничение:** удалять можно только сообщения, отправленные **менее 24 часов назад**.
---
## Остальные методы
GET /me — информация о боте. GET/PATCH/DELETE /chats, GET/POST/DELETE /chats/{chatId}/members, GET /subscriptions, DELETE /subscriptions, POST /uploads, GET/PUT/DELETE /messages, GET /messages/{messageId}, POST /chats/{chatId}/actions и др. — см. https://dev.max.ru/docs-api

131
docs/max-api/03-objects.md Normal file
View File

@@ -0,0 +1,131 @@
# Объекты Max Bot API
## Update
Событие, приходящее в Long Polling или на Webhook.
| Поле | Тип | Описание |
|------|-----|----------|
| update_type | string | Тип события, например `message_created`, `message_callback` |
| timestamp | int64 | Unix-время события |
| message | Message | Сообщение (для message_created и др.) |
| user_locale | string | Язык пользователя (IETF BCP 47), только в диалогах |
Для событий из группового чата или канала бот должен быть назначен администратором.
**Пример:**
```json
{
"update_type": "message_created",
"timestamp": 0,
"message": { ... },
"user_locale": "ru"
}
```
При нажатии кнопки приходит тип `message_callback`; в объекте есть данные callback (в т.ч. `callback_id` для POST /answers).
---
## NewMessageBody
Тело сообщения при отправке (POST /messages) или обновлении (POST /answers и т.п.).
| Поле | Тип | Описание |
|------|-----|----------|
| text | string | Текст до 4000 символов |
| attachments | AttachmentRequest[] | Вложения (inline_keyboard и др.) |
| link | NewMessageLink | Ссылка на сообщение |
| notify | bool | Уведомлять участников (по умолчанию true) |
| format | "markdown" \| "html" | Формат текста |
**Пример:**
```json
{
"text": "Текст сообщения",
"attachments": [{ "type": "inline_keyboard", "payload": { "buttons": [...] } }],
"notify": true,
"format": "markdown"
}
```
---
## Message
Объект сообщения (приходит в Update и в ответах API). Полное описание: [dev.max.ru/docs-api/objects/Message](https://dev.max.ru/docs-api/objects/Message).
| Поле | Тип | Описание |
|------|-----|----------|
| sender | User | Отправитель (опц.) |
| recipient | Recipient | Получатель |
| timestamp | int64 | Unix-время |
| link | LinkedMessage | Пересланное/ответное сообщение (опц.) |
| **body** | **MessageBody** | Содержимое: текст + вложения. Может быть `null`, если только пересланное |
| stat | MessageStat | Статистика (опц.) |
| url | string | Публичная ссылка на пост в канале (опц.) |
---
## MessageBody (входящее сообщение)
Содержимое сообщения при получении по Webhook или GET /updates. Источник: [Message](https://dev.max.ru/docs-api/objects/Message), [POST /uploads](https://dev.max.ru/docs-api/methods/POST/uploads).
| Поле | Тип | Описание |
|------|-----|----------|
| **text** | string | Текст сообщения или подпись к медиа. До 4000 символов. Может отсутствовать (только вложение). |
| **attachments** | array | Вложения. Каждый элемент: `{ "type": "<тип>", "payload": { ... } }`. |
### Типы вложений (attachments[].type)
По документации загрузки файлов (POST /uploads) поддерживаются типы: **`image`**, **`video`**, **`audio`**, **`file`**. Значение `photo` больше не используется — приходит как **`image`**.
| Тип | Описание | Форматы (при загрузке) |
|-----|----------|-------------------------|
| **image** | Фото/картинка | JPG, JPEG, PNG, GIF, TIFF, BMP, HEIC |
| **video** | Видео | MP4, MOV, MKV, WEBM, MATROSKA |
| **audio** | Голос/аудио | MP3, WAV, M4A и др. |
| **file** | Документ или любой файл | Любые типы |
У видео и аудио в `payload` приходит **token** (используется в т.ч. для GET /videos/{videoToken}). У image/file — в `payload` приходят данные файла (токен или URL после обработки сервером).
### Как приходят сообщения в Webhook
- **Только текст:** `message.body.text` — строка, `message.body.attachments` — пустой или отсутствует.
- **Только голос/аудио:** `message.body.attachments` — один элемент `type: "audio"`, `payload` с токеном; `message.body.text` может быть пустым.
- **Только видео:** `message.body.attachments` — один элемент `type: "video"`, `payload` с токеном; `message.body.text` — по желанию.
- **Документ/файл:** `message.body.attachments` — элемент `type: "file"`, в `payload` — данные файла; `message.body.text` — опционально.
- **Фото (image):** `message.body.attachments` — элемент `type: "image"` (не `photo`), в `payload` — данные изображения.
- **Фото с подписью:** то же, что фото, плюс **`message.body.text`** — подпись (caption).
Пример (фото с подписью):
```json
{
"update_type": "message_created",
"message": {
"sender": { ... },
"recipient": { ... },
"timestamp": 1234567890,
"body": {
"text": "Вот документ по делу",
"attachments": [
{ "type": "image", "payload": { "token": "..." } }
]
}
}
}
```
Точную структуру `payload` для каждого типа смотри в ответах API (например, отправить боту сообщение и посмотреть тело Webhook в n8n) или в [официальной документации](https://dev.max.ru/docs-api).
---
## User, Chat
- **User:** [dev.max.ru/docs-api/objects/User](https://dev.max.ru/docs-api/objects/User)
- **Chat:** [dev.max.ru/docs-api/objects/Chat](https://dev.max.ru/docs-api/objects/Chat)
Полный список методов и объектов — в [официальной документации](https://dev.max.ru/docs-api).

View File

@@ -0,0 +1,193 @@
# Форматы текста и кнопки (MAX Bot API)
## Форматы текста (поле `format` в теле сообщения)
В **NewMessageBody** укажи `"format": "markdown"` или `"format": "html"`. Тогда текст сообщения будет отформатирован.
### Markdown
| Как написать | Результат |
|--------------|-----------|
| `*курсив*` или `_курсив_` | *курсив* |
| `**жирный**` или `__жирный__` | **жирный** |
| `~~зачёркнутый~~` | ~~зачёркнутый~~ |
| `++подчёркнутый++` | подчёркнутый |
| `` `код` `` | моноширинный (переводы строк внутри — как пробелы) |
| `[текст ссылки](https://example.com)` | кликабельная ссылка |
| @упоминание | `"text": "[Имя Фамилия](max://user/user_id)", "format": "markdown"` — полное имя из профиля MAX |
### HTML
| Теги | Результат |
|------|-----------|
| `<i>`, `<em>` | курсив |
| `<b>`, `<strong>` | жирный |
| `<del>`, `<s>` | зачёркнутый |
| `<ins>`, `<u>` | подчёркнутый |
| `<pre>`, `<code>` | моноширинный |
| `<a href="https://example.com">Текст</a>` | ссылка |
| @упоминание | `"text": "<a href=\"max://user/user_id\">Имя Фамилия</a>", "format": "html"` |
Пример тела с markdown:
```json
{
"text": "**Внимание!** Вы отправили *голосовое*. Обрабатываем.",
"format": "markdown"
}
```
---
## Кнопки (inline_keyboard)
Кнопки добавляются через **attachments**: один элемент с `type: "inline_keyboard"` и `payload.buttons` — массив **рядов**, каждый ряд — массив **кнопок**.
Ограничения:
- до **210 кнопок** всего;
- до **30 рядов**;
- до **7 кнопок в ряду** (для типов `link`, `open_app`, `request_geo_location`, `request_contact` — до **3** в ряду);
- для кнопки типа `link` ссылка до **2048** символов.
### Структура
```json
{
"text": "Текст сообщения над кнопками",
"format": "markdown",
"attachments": [
{
"type": "inline_keyboard",
"payload": {
"buttons": [
[
{ "type": "callback", "text": "Надпись кнопки", "payload": "значение при нажатии" }
],
[
{ "type": "link", "text": "Открыть сайт", "url": "https://example.com" }
]
]
}
}
]
}
```
`buttons` — массив рядов. Каждый ряд — массив кнопок. Одна кнопка — объект с полями в зависимости от типа.
### Типы кнопок
| type | Описание | Поля кнопки |
|------|----------|-------------|
| **callback** | При нажатии в Webhook приходит `message_callback` с `callback_id` и payload. Нужен для ответа через POST /answers. | `text`, `payload` (строка или объект — то, что придёт в бот) |
| **link** | Открывает ссылку в браузере. | `text`, `url` (до 2048 символов) |
| **message** | Отправляет боту текстовое сообщение (как будто пользователь написал это). | `text` |
| **request_contact** | Запрос контакта (номер телефона). Пользователь нажимает → клиент MAX предлагает отправить контакт → в Webhook приходит `message_created` с данными контакта (телефон и т.д.) в теле сообщения. | `text` (подпись на кнопке) |
| **request_geo_location** | Запрос геолокации. Пользователь нажимает → отправляет геолокацию → в Webhook приходит сообщение с координатами. | `text` |
| **open_app** | Открывает мини-приложение. | уточнять в [доках](https://dev.max.ru/docs-api) |
### Пример: кнопка «Поделиться контактом»
У кнопки тип **request_contact**, поле **text** — подпись (например «📱 Отправить номер телефона»). В одном ряду с такими кнопками MAX разрешает до 3 кнопок.
```json
{
"text": "Чтобы мы могли связаться, поделитесь номером телефона:",
"format": "markdown",
"attachments": [
{
"type": "inline_keyboard",
"payload": {
"buttons": [
[
{ "type": "request_contact", "text": "📱 Отправить номер телефона" }
]
]
}
}
]
}
```
После нажатия пользователь подтверждает отправку контакта в клиенте MAX. В Webhook придёт **message_created** с вложением `attachments[0].type === "contact"`. Структура:
- **attachments[0].payload.vcf_info** — строка VCARD (например `TEL;TYPE=cell:79262306381`, `FN:Имя Фамилия`). Телефон достаётся из строки `TEL...:номер`.
- **attachments[0].payload.max_info** — объект пользователя MAX: `user_id`, `first_name`, `last_name`, `name`, `is_bot`, `last_activity_time`.
В нормализаторе для такого сообщения: `answer_type: 'contact'`, `answer_text` — извлечённый номер, `contact_payload` — весь payload, `contact_name` — из max_info.name.
### Пример: одна callback-кнопка
```json
{
"text": "Выберите действие:",
"format": "markdown",
"attachments": [
{
"type": "inline_keyboard",
"payload": {
"buttons": [
[
{ "type": "callback", "text": "Подтвердить", "payload": "confirm" },
{ "type": "callback", "text": "Отмена", "payload": "cancel" }
]
]
}
}
]
}
```
При нажатии «Подтвердить» в Webhook придёт `update_type: "message_callback"`, в нормализованном объекте будет `answer_text: "confirm"` и `callback_id` для POST /answers (уведомление или обновление сообщения).
### Пример: кнопка-ссылка и callback в одном сообщении
```json
{
"text": "Официальный сайт и обратная связь:",
"format": "markdown",
"attachments": [
{
"type": "inline_keyboard",
"payload": {
"buttons": [
[
{ "type": "link", "text": "Перейти на сайт", "url": "https://example.com" }
],
[
{ "type": "callback", "text": "Написать в поддержку", "payload": "support" }
]
]
}
}
]
}
```
### В n8n (HTTP Request — тело с кнопками)
В **JSON Body** ноды можно задать статичное тело или собрать через выражение. Пример статичного тела с кнопками:
```json
{
"text": "Выберите действие:",
"format": "markdown",
"attachments": [
{
"type": "inline_keyboard",
"payload": {
"buttons": [
[
{ "type": "callback", "text": "Да", "payload": "yes" },
{ "type": "callback", "text": "Нет", "payload": "no" }
]
]
}
}
]
}
```
URL и заголовки — как раньше: `POST https://platform-api.max.ru/messages?user_id={{ $json.max_id }}`, `Authorization: <token>`, `Content-Type: application/json`.
После нажатия callback-кнопки пользователем обрабатывай событие в воркфлоу (по `answer_type === 'callback'` и `answer_text` / `callback_id`) и при необходимости вызывай **POST /answers?callback_id=...** для уведомления или обновления сообщения (см. `02-methods.md`).

10
docs/max-api/README.md Normal file
View File

@@ -0,0 +1,10 @@
# Max Bot API — документация (бот Союз потребителей РФ)
Локальная копия. Официально: https://dev.max.ru/docs-api
- **01-overview.md** — обзор API, авторизация, клавиатура, форматирование
- **02-methods.md** — методы: сообщения, updates, webhook, callback
- **03-objects.md** — объекты Update, NewMessageBody, Message, MessageBody (формат входящих: текст, голос, видео, документ, фото с подписью)
- **04-formats-and-buttons.md** — форматы текста (markdown, html) и кнопки (inline_keyboard: callback, link, message и др.)
Базовый URL: `https://platform-api.max.ru`. Авторизация: заголовок `Authorization: <token>`. Лимит 30 rps. В production — только Webhook.

View File

@@ -0,0 +1,88 @@
# cURL и настройка HTTP Request ноды (ответ пользователю в MAX)
## cURL — отправить сообщение
Подставь свой `MAX_BOT_TOKEN` и `max_id` (или `max_chat_id`) получателя:
```bash
curl -X POST "https://platform-api.max.ru/messages?user_id=6200846" \
-H "Authorization: ВАШ_MAX_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text": "Привет! Сообщение получено.", "format": "markdown"}'
```
С `chat_id` вместо `user_id`:
```bash
curl -X POST "https://platform-api.max.ru/messages?chat_id=188573833" \
-H "Authorization: ВАШ_MAX_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"text": "Привет!", "format": "markdown"}'
```
---
## Как ввести в HTTP Request ноду n8n
| Поле | Значение |
|------|----------|
| **Method** | `POST` |
| **URL** | `https://platform-api.max.ru/messages?user_id={{ $json.max_id }}` |
| **Authentication** | Header Auth → Name: `Authorization`, Value: `ВАШ_MAX_BOT_TOKEN` (или credential) |
| **Send Headers** | включить, добавить: Name `Content-Type`, Value `application/json` (если нет из Auth) |
| **Send Body** | Yes |
| **Body Content Type** | JSON |
| **Specify Body** | Using JSON |
| **JSON Body** | `{"text": "Привет! Сообщение получено.", "format": "markdown"}` |
Динамический текст из предыдущей ноды:
```json
{
"text": "{{ $json.answer_text }}",
"format": "markdown"
}
```
Или свой ответ из другой ноды:
```json
{
"text": "{{ $('Твоя нода с ответом').item.json.reply_text }}",
"format": "markdown"
}
```
---
Подробнее про форматы текста (markdown/html) и про кнопки (callback, link, message и т.д.) — в **`docs/max-api/04-formats-and-buttons.md`**.
---
## cURL — ответ на нажатие кнопки (callback)
Подставь `callback_id` из нормализованного объекта и токен:
```bash
curl -X POST "https://platform-api.max.ru/answers?callback_id=ВАШ_CALLBACK_ID" \
-H "Authorization: ВАШ_MAX_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"notification": "Нажато!"}'
```
В n8n (только когда есть `callback_id`):
- **URL:** `https://platform-api.max.ru/answers?callback_id={{ $json.callback_id }}`
- **Method:** POST
- **Headers:** те же
- **Body:** `{"notification": "Нажато!"}` или `{"message": {"text": "Новый текст сообщения", "format": "markdown"}}`
### Удалить кнопки после нажатия
Обнови сообщение тем же текстом, но **без** `attachments` — клавиатура исчезнет:
- **URL:** `https://platform-api.max.ru/answers?callback_id={{ $json.callback_id }}`
- **Method:** POST
- **Body:** `{"message": {"text": "{{ $json.callback_message_text }}", "format": "markdown"}}`
Текст бери из нормализатора: после callback там есть **`callback_message_text`** (и при желании **`callback_message_mid`**). В body передаётся только `text` и `format`, без `attachments` — MAX перерисует сообщение без кнопок.

144
docs/max-webhook.md Normal file
View File

@@ -0,0 +1,144 @@
# Как активировать Webhook для бота MAX
## 1. В n8n: воркфлоу с Webhook
1. Открой n8n (https://n8n.clientright.pro).
2. Создай новый воркфлоу или открой существующий для MAX.
3. Добавь ноду **Webhook** (Trigger → Webhook).
4. Настрой Webhook:
- **HTTP Method:** POST
- **Path:** должен совпадать с путём из URL в `.env`. Сейчас в `.env` указано:
- `N8N_MAX_WORKFLOW=https://n8n.clientright.pro/webhook/sprf_max`
- значит в ноде Webhook задай **Path** = `sprf_max` (без `/webhook/`).
- **Authentication:** Optional (проверку секрета можно сделать следующей нодой: сравнить заголовок `X-Max-Bot-Api-Secret` с переменной `MAX_WEBHOOK_SECRET`).
5. Включи воркфлоу (Production mode), чтобы URL был доступен по HTTPS.
Итог: запросы на `https://n8n.clientright.pro/webhook/sprf_max` будут попадать в этот воркфлоу.
## 2. Регистрация URL в MAX
Из каталога проекта выполни:
```bash
python3 register_max_webhook.py
```
Скрипт читает из `.env`:
- `MAX_BOT_TOKEN` — токен бота
- `N8N_MAX_WORKFLOW` — полный URL webhook (HTTPS)
- `MAX_WEBHOOK_SECRET` — секрет (опционально; если задан, MAX будет присылать его в заголовке `X-Max-Bot-Api-Secret`)
После успешного запуска MAX начнёт отправлять события (новые сообщения, нажатия кнопок, старт бота) на этот URL.
## 3. Нормализация входящего (Code node под MAX)
Чтобы не обрабатывать сырой Update, а получать один и тот же формат, что и для Telegram (для общего воркфлоу), используй Code node с содержимым из файла **`n8n-code-node-max-normalize.js`** в корне проекта. Он отдаёт один объект с полями:
- `max_id` — id пользователя MAX (sender)
- `max_chat_id` — id чата/диалога (для лички может совпадать с user_id)
- `answer_text` — текст сообщения, подпись к медиа или данные callback
- `answer_type``text` | `command` | `callback` | `voice` | `audio` | `video` | `file` | `photo`
- `channel``'max'`
- `reply_to_*` — если было ответ/пересланное (из `message.link`)
- `callback_id` — только при `answer_type === 'callback'` (для POST /answers)
- `attachment_token` / `file_id` — при медиа-вложениях
- `raw_update` — исходное тело Webhook
Групповые чаты и каналы отфильтровываются (возвращается пустой массив).
## 4. Отправка ответа пользователю (пушим в MAX)
Чтобы бот написал пользователю, вызывай **MAX Bot API**: отправка сообщения — метод **POST /messages**.
### Параметры
- **URL:** `https://platform-api.max.ru/messages?user_id={user_id}` или `?chat_id={chat_id}`
В личке можно использовать либо `user_id` (id отправителя), либо `chat_id` (id диалога). Из нашей нормализации: `max_id` — это user_id, `max_chat_id` — chat_id; для ответа подойдёт любой из них в query.
- **Метод:** POST
- **Заголовки:**
- `Authorization: <MAX_BOT_TOKEN>` — токен бота (из .env или из credentials в n8n)
- `Content-Type: application/json`
- **Тело (JSON):** объект **NewMessageBody**:
- `text` (string) — текст до 4000 символов, обязателен если нет вложений
- `format` (опц.) — `"markdown"` или `"html"` для форматирования
- `attachments` (опц.) — массив вложений (например inline_keyboard с кнопками)
- `notify` (опц., по умолч. true) — уведомлять ли пользователя
Пример тела:
```json
{
"text": "Привет! Ваше сообщение получено.",
"format": "markdown"
}
```
### В n8n (HTTP Request node)
1. Добавь ноду **HTTP Request** после логики (после Code или ветки, где есть нормализованный объект с `max_id`/`max_chat_id`).
2. Настрой:
- **Method:** POST
- **URL:** `https://platform-api.max.ru/messages?user_id={{ $json.max_id }}`
(или `?chat_id={{ $json.max_chat_id }}`оба варианта для лички рабочие)
- **Authentication:** Generic Credential Type → **Header Auth**
- Name: `Authorization`
- Value: твой `MAX_BOT_TOKEN` (создай в n8n Credentials или подставь через переменную окружения)
- **Body Content Type:** JSON
- **Specify Body:** Using JSON
- **JSON Body:** например `{ "text": "{{ $json.answer_text }}", "format": "markdown" }` или свой текст/поля из предыдущих нод
Токен бота лучше хранить в n8n Credentials (тип Header Auth или просто в переменной workflow/environment), а не в коде.
### Ответ на нажатие кнопки (callback)
Если пользователь нажал кнопку, приходит `message_callback` и в нормализованном объекте есть **`callback_id`**. Чтобы обновить сообщение с кнопкой или показать уведомление:
- **URL:** `https://platform-api.max.ru/answers?callback_id={{ $json.callback_id }}`
- **Method:** POST
- **Headers:** те же (`Authorization`, `Content-Type: application/json`)
- **Body (JSON):**
- `message` (опц.) — объект NewMessageBody (обновить сообщение)
- `notification` (опц.) — строка, одноразовое уведомление пользователю
Подробнее: `docs/max-api/02-methods.md`.
## 5. Проверка
Напиши боту в мессенджере MAX — в n8n во входящих данных Webhook должен появиться объект с полями `update_type`, `message` и т.д.
## Проверка: зарегистрирован ли Webhook
Выполни в каталоге проекта:
```bash
python3 -c "
import os, json, urllib.request
from pathlib import Path
for line in Path('.env').read_text().splitlines():
s = line.strip()
if s and not s.startswith('#') and '=' in s:
k, v = s.split('=', 1)
os.environ[k.strip()] = v.strip()
r = urllib.request.urlopen(urllib.request.Request(
os.environ.get('MAX_API_BASE','https://platform-api.max.ru').rstrip('/') + '/subscriptions',
headers={'Authorization': os.environ['MAX_BOT_TOKEN']}, method='GET'))
print(r.read().decode())
"
```
В ответе должен быть твой URL в списке `subscriptions`. Если список пустой — заново запусти `python3 register_max_webhook.py`.
---
## Отписка от Webhook
Чтобы отключить доставку на этот URL, вызови в MAX API:
```bash
curl -X DELETE "https://platform-api.max.ru/subscriptions" \
-H "Authorization: ВАШ_MAX_BOT_TOKEN" \
-H "Content-Type: application/json" \
-d '{"url": "https://n8n.clientright.pro/webhook/sprf_max"}'
```
Точный формат отписки см. в документации MAX (DELETE /subscriptions).

57
fetch_schema.py Normal file
View File

@@ -0,0 +1,57 @@
#!/usr/bin/env python3
"""Выгрузка структуры таблиц sprf_ из PostgreSQL."""
import os
import psycopg2
from pathlib import Path
# Загрузка .env
env_path = Path(__file__).parent / ".env"
for line in env_path.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
k, v = line.split("=", 1)
os.environ[k.strip()] = v.strip()
conn = psycopg2.connect(
host=os.environ["PGHOST"],
port=int(os.environ.get("PGPORT", 5432)),
dbname=os.environ["PGDATABASE"],
user=os.environ["PGUSER"],
password=os.environ["PGPASSWORD"],
)
cur = conn.cursor()
cur.execute("""
SELECT table_name, column_name, data_type,
character_maximum_length, is_nullable, column_default, ordinal_position
FROM information_schema.columns
WHERE table_schema = 'public' AND table_name LIKE 'sprf_%%'
ORDER BY table_name, ordinal_position
""")
rows = cur.fetchall()
cur.close()
conn.close()
# Группировка по таблицам
from collections import defaultdict
by_table = defaultdict(list)
for table_name, col_name, data_type, char_max, nullable, default, ord_pos in rows:
by_table[table_name].append((col_name, data_type, char_max, nullable, default))
# Вывод в файл
out_path = Path(__file__).parent / "sprf_tables_schema.md"
lines = ["# Структура таблиц sprf_ (public, default_db)\n"]
for table in sorted(by_table.keys()):
lines.append(f"## {table}\n")
lines.append("| Колонка | Тип | Размер | NULL | Default |\n")
lines.append("|---------|-----|--------|------|--------|\n")
for col_name, data_type, char_max, nullable, default in by_table[table]:
size = str(char_max) if char_max else ""
default_str = (default or "").strip()[:40]
if len((default or "")) > 40:
default_str += "..."
lines.append(f"| {col_name} | {data_type} | {size} | {nullable} | {default_str} |\n")
lines.append("\n")
out_path.write_text("".join(lines), encoding="utf-8")
print(f"Сохранено: {out_path}")

View File

@@ -0,0 +1,41 @@
// Code node (Run once for each item)
// Добавляет text, buttons и готовое тело для MAX (message_body с inline_keyboard, callback).
return items.map(item => {
const text = "Вас давно не было. Выберите, чем хотите заняться:";
const buttons = [
{ title: " О сервисе", payload: "about" },
{ title: "📝 Подать жалобу", payload: "complaint" },
{ title: "📋 Мои обращения", payload: "my_tickets" },
{ title: "💬 Поддержка", payload: "support" }
];
// MAX: каждый ряд — одна кнопка (во всю ширину). Внизу кнопка "Главное меню" (type: message) —
// при нажатии бот получит сообщение "/menu" и можно снова показать это меню.
const callbackRows = buttons.map(b => [ { type: "callback", text: b.title, payload: b.payload } ]);
// Кнопка "Главное меню": type message — при нажатии бот получит этот text.
const menuButtonRow = [ { type: "message", text: "📋 Главное меню" } ];
// Кнопка "Поделиться контактом": request_contact — MAX запросит телефон, в webhook придёт message_created с контактом (структуру смотри в payload).
const contactButtonRow = [ { type: "request_contact", text: "📱 Отправить номер телефона" } ];
const message_body = {
text,
format: "markdown",
attachments: [
{
type: "inline_keyboard",
payload: {
buttons: [ ...callbackRows, contactButtonRow, menuButtonRow ]
}
}
]
};
return {
json: {
...item.json,
text,
buttons,
message_body
}
};
});

View File

@@ -0,0 +1,151 @@
// Function node для n8n — нормализация входящего Webhook MAX
// Выход: max_id, max_chat_id, answer_text, answer_type, channel: "max", reply_to_*, raw_update
const input = $input.first().json;
// Тело Webhook: в n8n обычно item = { body, headers, params, query }; если прилетел только body — используем input как payload
let raw = input?.body ?? input;
if (typeof raw === 'string') {
try { raw = JSON.parse(raw); } catch (_) {}
}
// ----------------- 0) Игнорируем НЕ-private чаты (группы, каналы) -----------------
// В MAX в личке приходит recipient с chat_type: "dialog". В группах/каналах — другой chat_type.
const recipient = raw.message?.recipient ?? raw.recipient;
const chatType = recipient?.chat_type ?? '';
if (recipient && chatType !== '' && chatType !== 'dialog') {
return []; // групповой чат или канал
}
// ----------------- Утилиты -----------------
const trim = (s) => (s || '').trim();
const takeLast = (arr) => (Array.isArray(arr) && arr.length ? arr[arr.length - 1] : null);
const safe = (v, fallback = null) => (v === undefined ? fallback : v);
const EMOJI_RE = /[\p{Extended_Pictographic}\u200D\uFE0F]/gu;
function cleanTextForMeaning(txt) {
if (!txt) return '';
const noEmoji = txt.replace(EMOJI_RE, '').replace(/[\u2000-\u206F\u2E00-\u2E7F\\'!"#$%&()*+,\-./:;<=>?@[\\\]^_`{|}~]/g, ' ');
return noEmoji.replace(/\s+/g, ' ').trim();
}
function isReactionOnly(originalText) {
if (!originalText) return false;
const cleaned = cleanTextForMeaning(originalText);
if (cleaned.length === 0) return true;
const lettersCount = (cleaned.match(/[A-Za-zА-Яа-я0-9]/g) || []).length;
return lettersCount < 3 && originalText.trim().length <= 3;
}
// ----------------- Результат -----------------
const result = {
max_id: null,
max_chat_id: null,
answer_text: null,
answer_type: null,
channel: 'max',
raw_update: raw,
reply_to_message_id: null,
reply_to_from_id: null,
reply_to_from_username: null,
reply_to_text: null,
};
const msg = raw?.message ?? raw;
const body = msg?.body;
const sender = msg?.sender ?? raw?.sender;
// ----- 1) ID пользователя и чата (MAX) -----
// При message_callback сообщение от бота (sender = бот), нажал пользователь — он в raw.callback.user
const callbackUser = raw?.callback?.user;
const userId = callbackUser?.user_id ?? callbackUser?.id ?? sender?.user_id ?? sender?.id ?? raw?.user_id ?? msg?.sender?.user_id;
result.max_id = userId;
const chatId = msg?.recipient?.chat_id ?? msg?.recipient?.user_id ?? recipient?.chat_id ?? recipient?.user_id ?? userId;
result.max_chat_id = chatId;
// ----- 2) Ответ на сообщение / пересланное (message.link = LinkedMessage) -----
const link = msg?.link;
if (link) {
result.reply_to_message_id = safe(link.message_id ?? link.id);
result.reply_to_from_id = safe(link.sender?.user_id ?? link.sender?.id);
result.reply_to_from_username = safe(link.sender?.username);
if (link.body?.text) {
result.reply_to_text = String(link.body.text).replace(/\r?\n/g, ' ').slice(0, 1000);
} else if (link.body?.attachments?.length) {
const first = link.body.attachments[0];
const typeMap = { image: '[photo]', video: '[video]', audio: '[voice]', file: '[document]' };
result.reply_to_text = typeMap[first?.type] ?? '[attachment]';
}
}
// ----- 3) Типы входящих: callback, message_created (текст/медиа), bot_started -----
const updateType = raw.update_type;
if (updateType === 'message_callback') {
// Нажатие кнопки: callback_id для POST /answers, payload — данные кнопки
const callbackId = raw.callback_id ?? raw.callback?.callback_id ?? msg?.callback_id;
const payload = raw.callback?.payload ?? raw.callback?.data ?? msg?.callback?.payload ?? msg?.callback?.data;
result.answer_text = typeof payload === 'string' ? payload : (payload != null ? JSON.stringify(payload) : '');
result.answer_type = 'callback';
result.callback_id = callbackId;
// Текст сообщения с кнопками — чтобы обновить его через POST /answers без кнопок (удалить клавиатуру)
result.callback_message_text = msg?.body?.text ?? raw.message?.body?.text ?? null;
result.callback_message_mid = msg?.body?.mid ?? raw.message?.body?.mid ?? null;
} else if (updateType === 'bot_started') {
result.answer_text = '/start';
result.answer_type = 'command';
} else if (updateType === 'message_created' && body) {
const hasText = body.text && trim(body.text).length > 0;
const attachments = body.attachments ?? [];
const firstAtt = attachments[0];
if (firstAtt) {
const type = firstAtt.type;
if (type === 'contact') {
// Поделился контактом (кнопка request_contact). MAX присылает payload.vcf_info (VCARD) и payload.max_info (user).
const payload = firstAtt.payload ?? {};
let phone = payload.phone_number ?? payload.phone ?? '';
if (!phone && payload.vcf_info) {
const m = payload.vcf_info.match(/TEL[^:]*:([+\d\s\-()]+)/);
if (m) phone = m[1].replace(/\s/g, '').trim();
}
result.answer_text = phone || '[contact]';
result.answer_type = 'contact';
result.contact_payload = payload;
if (payload.max_info) result.contact_name = payload.max_info.name ?? [payload.max_info.first_name, payload.max_info.last_name].filter(Boolean).join(' ');
} else {
// Вложение: image | video | audio | file
result.answer_text = hasText ? body.text.replace(/\r?\n/g, ' ').trim() : (type === 'image' ? '[photo]' : type === 'video' ? '[video]' : type === 'audio' ? '[voice]' : '[document]');
result.answer_type = type === 'image' ? 'photo' : type === 'video' ? 'video' : type === 'audio' ? 'voice' : 'file';
if (firstAtt.payload?.token) result.attachment_token = firstAtt.payload.token;
if (firstAtt.payload?.file_id) result.file_id = firstAtt.payload.file_id;
if (firstAtt.payload) result.attachment_payload = firstAtt.payload;
}
} else if (body.contact) {
// Контакт в body.contact (альтернативный формат MAX)
const phone = body.contact.phone_number ?? body.contact.phone ?? '';
result.answer_text = phone || '[contact]';
result.answer_type = 'contact';
result.contact_payload = body.contact;
} else if (hasText) {
// Только текст
const rawText = body.text;
if (isReactionOnly(rawText)) return [];
result.answer_text = rawText.replace(/\r?\n/g, ' ').trim();
result.answer_type = result.answer_text.startsWith('/') ? 'command' : 'text';
} else {
return [];
}
} else {
return [];
}
// ----- 4) Валидация -----
if (result.max_id == null) throw new Error('Не удалось извлечь max_id');
if (result.max_chat_id == null) throw new Error('Не удалось извлечь max_chat_id');
if (!result.answer_type) throw new Error('Не удалось определить тип ответа');
// ----- 5) Нормализация строк "null" (как в старой ноде) -----
if (raw.body?.last_name === 'null') raw.body.last_name = null;
if (result.reply_to_text === 'null') result.reply_to_text = null;
return [{ json: result }];

75
register_max_webhook.py Normal file
View File

@@ -0,0 +1,75 @@
#!/usr/bin/env python3
"""
Регистрация Webhook бота MAX на URL n8n.
Вызов: python3 register_max_webhook.py
Перед запуском:
1. В n8n создан воркфлоу с нодой Webhook (Production), путь = последняя часть N8N_MAX_WORKFLOW
(например, для https://n8n.clientright.pro/webhook/sprf_max путь в ноде = sprf_max).
2. В .env заданы MAX_BOT_TOKEN, N8N_MAX_WORKFLOW (полный HTTPS URL webhook).
3. По желанию задан MAX_WEBHOOK_SECRET — тогда MAX будет присылать его в заголовке X-Max-Bot-Api-Secret.
"""
import os
import json
import urllib.request
from pathlib import Path
# Загрузка .env
env_path = Path(__file__).parent / ".env"
for line in env_path.read_text().splitlines():
line = line.strip()
if not line or line.startswith("#"):
continue
if "=" in line:
k, v = line.split("=", 1)
os.environ[k.strip()] = v.strip()
MAX_API_BASE = os.environ.get("MAX_API_BASE", "https://platform-api.max.ru").rstrip("/")
MAX_BOT_TOKEN = os.environ.get("MAX_BOT_TOKEN", "").strip()
N8N_MAX_WORKFLOW = os.environ.get("N8N_MAX_WORKFLOW", "").strip()
MAX_WEBHOOK_SECRET = os.environ.get("MAX_WEBHOOK_SECRET", "").strip()
if not MAX_BOT_TOKEN:
print("Ошибка: в .env не задан MAX_BOT_TOKEN")
exit(1)
if not N8N_MAX_WORKFLOW or not N8N_MAX_WORKFLOW.startswith("https://"):
print("Ошибка: в .env задайте N8N_MAX_WORKFLOW — полный HTTPS URL webhook (например https://n8n.clientright.pro/webhook/sprf_max)")
exit(1)
# Типы событий, которые бот будет получать
update_types = ["message_created", "message_callback", "bot_started"]
body = {"url": N8N_MAX_WORKFLOW, "update_types": update_types}
if MAX_WEBHOOK_SECRET:
body["secret"] = MAX_WEBHOOK_SECRET
print("Секрет для проверки в n8n (X-Max-Bot-Api-Secret): задан в .env")
else:
print("Секрет не задан (MAX_WEBHOOK_SECRET в .env). Рекомендуется задать для проверки запросов в n8n.")
req = urllib.request.Request(
f"{MAX_API_BASE}/subscriptions",
data=json.dumps(body).encode("utf-8"),
headers={
"Authorization": MAX_BOT_TOKEN,
"Content-Type": "application/json",
},
method="POST",
)
try:
with urllib.request.urlopen(req, timeout=15) as resp:
data = resp.read().decode()
out = json.loads(data) if data else {}
if out.get("success") is True:
print("Webhook зарегистрирован успешно.")
print("URL:", N8N_MAX_WORKFLOW)
print("Типы событий:", ", ".join(update_types))
else:
print("Ответ API:", data)
exit(1)
except urllib.error.HTTPError as e:
body = e.read().decode()
print(f"Ошибка HTTP {e.code}: {e.reason}")
print("Тело ответа:", body)
exit(1)
except Exception as e:
print("Ошибка:", e)
exit(1)

217
sprf_tables_schema.md Normal file
View File

@@ -0,0 +1,217 @@
# Структура таблиц sprf_ (public, default_db)
## sprf_chat_messages
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | integer | | NO | nextval('sprf_chat_messages_id_seq'::reg... |
| claim_id | uuid | | YES | |
| from_user | boolean | | YES | |
| message_text | text | | YES | |
| file_id | text | | YES | |
| sent_at | timestamp with time zone | | YES | |
## sprf_claim_documents
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | uuid | | NO | |
| claim_id | character varying | | YES | |
| field_name | text | | YES | |
| file_id | text | | YES | |
| uploaded_at | timestamp with time zone | | YES | |
## sprf_claim_statuses
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| code | text | | NO | |
| description | text | | YES | |
## sprf_claim_types
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| code | text | | NO | |
| description | text | | YES | |
## sprf_claims
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | character varying | | NO | |
| user_id | integer | | YES | |
| type_code | text | | YES | |
| status_code | text | | YES | |
| policy_number | text | | YES | |
| payload | jsonb | | YES | |
| created_at | timestamp with time zone | | YES | |
| updated_at | timestamp with time zone | | YES | |
| session_token | character varying | | YES | |
| unified_id | character varying | | YES | |
| telegram_id | bigint | | YES | |
| channel | text | | YES | |
| is_confirmed | boolean | | YES | false |
## sprf_conversation_state
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| user_id | integer | | NO | |
| current_step | text | | YES | |
| data | jsonb | | YES | |
| updated_at | timestamp with time zone | | YES | |
## sprf_court_decisions
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | uuid | | NO | gen_random_uuid() |
| uuid | character varying | 36 | NO | (gen_random_uuid())::text |
| file_name | character varying | 500 | NO | |
| file_size | bigint | | YES | |
| mime_type | character varying | 100 | YES | |
| file_hash | character varying | 64 | NO | |
| s3_url | text | | NO | |
| telegram_message_id | bigint | | YES | |
| telegram_chat_id | bigint | | YES | |
| telegram_user_id | bigint | | NO | |
| telegram_username | character varying | 255 | YES | |
| telegram_full_name | character varying | 500 | YES | |
| ocr_processed | boolean | | YES | false |
| ocr_processed_at | timestamp with time zone | | YES | |
| ocr_text | text | | YES | |
| ocr_pages_data | jsonb | | YES | |
| ocr_pages_count | integer | | YES | |
| vector_processed | boolean | | YES | false |
| vector_processed_at | timestamp with time zone | | YES | |
| vector_store_id | character varying | 255 | YES | |
| vector_file_ids | jsonb | | YES | |
| nsfw_checked | boolean | | YES | false |
| nsfw_result | boolean | | YES | false |
| nsfw_score | numeric | | YES | |
| processing_status | character varying | 50 | YES | 'pending'::character varying |
| processing_error | text | | YES | |
| uploaded_at | timestamp with time zone | | YES | CURRENT_TIMESTAMP |
| updated_at | timestamp with time zone | | YES | CURRENT_TIMESTAMP |
| crm_claim_id | integer | | YES | |
| crm_project_id | integer | | YES | |
| metadata | jsonb | | YES | '{}'::jsonb |
| court_raw_json | jsonb | | YES | '{}'::jsonb |
## sprf_court_decisions_view
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | uuid | | YES | |
| uuid | character varying | 36 | YES | |
| file_name | character varying | 500 | YES | |
| file_size | bigint | | YES | |
| mime_type | character varying | 100 | YES | |
| telegram_user_id | bigint | | YES | |
| telegram_username | character varying | 255 | YES | |
| telegram_full_name | character varying | 500 | YES | |
| ocr_processed | boolean | | YES | |
| ocr_pages_count | integer | | YES | |
| vector_processed | boolean | | YES | |
| processing_status | character varying | 50 | YES | |
| uploaded_at | timestamp with time zone | | YES | |
| updated_at | timestamp with time zone | | YES | |
| ocr_text_preview | text | | YES | |
| crm_claim_id | integer | | YES | |
| crm_project_id | integer | | YES | |
## sprf_dialog_history_tg
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | integer | | NO | nextval('sprf_dialog_history_tg_id_seq':... |
| telegram_id | bigint | | YES | |
| role | character varying | | YES | |
| message | text | | YES | |
| created_at | timestamp with time zone | | YES | now() |
| session_token | character varying | | YES | |
| claim_id | character varying | | YES | |
| message_type | text | | YES | |
| payload | jsonb | | YES | |
| tg_message_id | bigint | | YES | |
| tg_update_id | bigint | | YES | |
## sprf_document_embeddings
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| document_id | uuid | | YES | |
| chunk_index | integer | | YES | |
| embedding | USER-DEFINED | | YES | |
## sprf_documents
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | uuid | | NO | |
| source | text | | YES | |
| content | text | | YES | |
| metadata | jsonb | | YES | |
| created_at | timestamp with time zone | | YES | |
## sprf_operators
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | integer | | NO | nextval('sprf_operators_id_seq'::regclas... |
| telegram_id | bigint | | YES | |
| name | text | | YES | |
| is_active | boolean | | YES | |
| created_at | timestamp with time zone | | YES | |
## sprf_sessions
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | uuid | | NO | gen_random_uuid() |
| user_id | integer | | YES | |
| session_token | character varying | | YES | |
| created_at | timestamp with time zone | | YES | now() |
| last_activity | timestamp with time zone | | YES | |
| expires_at | timestamp with time zone | | YES | |
## sprf_user_accounts
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | integer | | NO | nextval('sprf_user_accounts_id_seq'::reg... |
| user_id | integer | | YES | |
| channel | text | | YES | |
| channel_user_id | text | | YES | |
## sprf_users
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | integer | | NO | nextval('sprf_users_id_seq'::regclass) |
| universal_id | uuid | | YES | |
| phone | character varying | | YES | |
| created_at | timestamp with time zone | | YES | |
| updated_at | timestamp with time zone | | YES | |
| unified_id | character varying | | YES | |
## sprf_users_tg
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| telegram_id | bigint | | NO | |
| phone_number | character varying | | YES | |
| first_name_tg | character varying | | YES | |
| last_name_tg | character varying | | YES | |
| username | character varying | | YES | |
| language_code | character varying | | YES | |
| is_premium | boolean | | YES | |
| unified_id | character varying | | YES | |
| birth_date | character varying | | YES | |
| birth_place | character varying | | YES | |
| inn | character varying | | YES | |
| address | character varying | | YES | |
| email | character varying | | YES | |
| created_at | timestamp with time zone | | YES | now() |
| updated_at | timestamp with time zone | | YES | now() |
| first_name | character varying | | YES | |
| last_name | character varying | | YES | |
| middle_name | character varying | | YES | |
| is_confirmed | boolean | | YES | false |
## sprf_wizard_questions
| Колонка | Тип | Размер | NULL | Default |
|---------|-----|--------|------|--------|
| id | integer | | NO | nextval('sprf_wizard_questions_id_seq'::... |
| claim_type | text | | YES | |
| step_key | text | | YES | |
| question_text | text | | YES | |
| answer_type | text | | YES | |
| step_order | integer | | YES | |
| options | jsonb | | YES | |
| is_required | boolean | | YES | |