15 KiB
SMS → n8n → LLM → PostgreSQL → Telegram → Google Sheets
Текущая архитектура workflow (11.01.2026)
1. Общий контур решения
Источник данных — банковские SMS, которые попадают в n8n через Webhook. Дальше SMS:
- парсятся в нормализованный JSON с определением типа операции;
- сохраняются в PostgreSQL (таблица
transactions); - обогащаются локальной LLM (Qwen2.5 через LM Studio);
- отправляются в Telegram-бота в виде карточки с кнопками;
- проходят через human-in-the-loop: пользователь подтверждает или редактирует контрагента и категорию;
- после финального подтверждения данные фиксируются в БД и отправляются в Google Sheets для отчётности.
Проект развернут на Synology NAS: PostgreSQL. n8n в Docker и LM Studio — на локальной машине.
2. База данных
2.1. Таблица transactions
id BIGSERIAL PRIMARY KEY,
raw_sms TEXT NOT NULL,
sms_text TEXT NOT NULL,
sms_sender VARCHAR(50),
action VARCHAR(20) NOT NULL,
amount NUMERIC(15, 2),
signed_amount NUMERIC(15, 2),
currency VARCHAR(3) DEFAULT 'RUB',
balance NUMERIC(15, 2),
received_at TIMESTAMP WITH TIME ZONE NOT NULL,
processed_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
raw_json JSONB NOT NULL,
chat_id BIGINT,
-- LLM / Human-in-the-loop
counterparty VARCHAR(255),
category VARCHAR(100),
llm_processed_at TIMESTAMP WITH TIME ZONE,
human_verified BOOLEAN DEFAULT FALSE,
human_verified_at TIMESTAMP WITH TIME ZONE
Индексы:
- По дате, действию, контрагенту, категории и флагу
human_verified idx_chat_categoriesна(chat_id, category, received_at DESC)для персонализации
2.2. Таблица user_sessions
Используется для хранения состояния редактирования в Telegram (какую транзакцию и какое поле сейчас меняет пользователь).
"chatId" BIGINT PRIMARY KEY,
"transactionId" BIGINT NOT NULL REFERENCES transactions(id) ON DELETE CASCADE,
"waitingFor" VARCHAR(20) NOT NULL,
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
"tempCounterparty" VARCHAR(255),
"tempCategory" VARCHAR(100)
2.3. Таблица user_category_stats
Кэш популярных категорий пользователей для персонализации.
chat_id BIGINT,
category VARCHAR(100),
count INTEGER DEFAULT 1,
last_used TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (chat_id, category)
Индекс: idx_user_cats на (chat_id, count DESC)
3. Ветка 1. Обработка входящих SMS (Webhook → LLM → БД → Telegram)
3.1. Webhook (Node: Webhook)
- Method:
POST - Принимает JSON с полями SMS от прокси/смс-шлюза
- Входные данные:
raw_sms,sms_text,sms_sender, дата и пр.
3.2. Code: parse_from_sms
Преобразует входящий JSON в нормализованный объект.
Типы операций (action):
const rules = [
{ action: 'salary', re: /зарплат/i },
{ action: 'reversal', re: /(отмена|возврат)/i },
{ action: 'payment', re: /(оплата|покупк)/i },
{ action: 'transfer', re: /перевод/i },
{ action: 'writeoff', re: /списан/i },
{ action: 'income', re: /(поступлен|поступление|зачислен)/i },
{ action: 'withdrawal', re: /снятие/i },
{ action: 'deposit', re: /внесение/i }
];
Логика знаков:
// Проверка валидности суммы
if (amount === null || amount === 0) {
throw new Error('Не удалось распарсить сумму из SMS');
}
let signed_amount = amount;
// Расходные операции (минус)
if (['payment', 'transfer', 'writeoff', 'withdrawal'].includes(action)) {
signed_amount = -Math.abs(amount);
}
// Приходные операции (плюс)
if (['salary', 'income', 'deposit', 'reversal'].includes(action)) {
signed_amount = Math.abs(amount);
}
Результат: объект с полями action, amount, signed_amount, currency, balance, received_at, raw_sms, sms_text, sms_sender, raw_json.
3.3. PostgreSQL: create_from_sms (INSERT)
- Operation: Insert
- Table:
transactions - Поля: все базовые из
parse_from_sms(без LLM-полей) - Options: Always Output Data = ON
3.4. Set: Edit Fields
Урезает объект перед LLM (data minimization): sms_text, action, amount, currency, balance.
3.5. HTTP Request: post_to_llm
- Метод: POST
- URL:
http://<LM_STUDIO_IP>:1234/v1/chat/completions - Модель: Qwen2.5-7B-Instruct
System prompt (фрагмент):
ДОПУСТИМЫЕ КАТЕГОРИИ:
"Продукты", "Авто", "Здоровье", "Арчи", "ЖКХ", "Дом", "Проезд", "Одежда",
"Химия", "Косметика", "Инвестиции", "Развлечения", "Общепит", "Штрафы",
"Налоги", "Подписки", "Перевод", "Наличные", "Подарки", "Спорт", "Поступления"
КАТЕГОРИЗАЦИЯ ПО ТИПУ ОПЕРАЦИИ:
- Для снятия наличных категория ВСЕГДА "Наличные".
- Для внесения наличных категория ВСЕГДА "Поступления".
- Для зарплаты или поступлений категория ВСЕГДА "Поступления".
- Для возвратов/отмен категория ВСЕГДА "Поступления".
- Для переводов определяй категорию по контрагенту.
Параметры: temperature: 0.2, max_tokens: 256
3.6. Code: parse_from_llm
let content = $input.first().json.choices.message.content;
content = content.replace(/```json\n?/gi, '').replace(/```\n?/g, '').trim();
const llmResponse = JSON.parse(content);
const originalData = $('create_from_sms').first().json;
return {
...originalData,
counterparty: llmResponse.counterparty || originalData.counterparty || 'Продукты',
category: llmResponse.category || originalData.category || 'Продукты'
};
3.7. PostgreSQL: update_llm_fields (UPDATE)
- Table:
transactions - Where:
id = {{ $json.id }} - Columns:
counterparty,category,llm_processed_at = NOW(),human_verified = FALSE - Options: Always Output Data = ON
3.8. Telegram Send Message: edit_or_confirm
- Chat ID:
{{ $json.chatId }} - Text: карточка транзакции с суммой, контрагентом, категорией, датой
- Inline Keyboard:
✅ Подтвердить→confirm_{{ $json.id }}✏️ Изменить категорию→edit_category_{{ $json.id }}✏️ Изменить контрагента→edit_counterparty_{{ $json.id }}
4. Ветка 2. Telegram Trigger и разветвление
4.1. Telegram Trigger: answer_trigger
- Updates:
callback_query,message - Все события (кнопки и текст) приходят в один триггер
4.2. IF: split_event_type
- Condition:
{{ $json.callback_query }}Is Empty - TRUE → текстовое сообщение → ветка редактирования
- FALSE → callback от кнопки → ветка подтверждения/редактирования
5. Ветка 3. Подтверждение транзакции
5.1. Code: parse_from_callback
Извлекает из callback: action, field, transactionId, chatId, messageId, isEditedCard, counterparty, category.
5.2. IF: confirm_or_edit
- TRUE → подтверждение
- FALSE → редактирование
5.3. IF: check_if_edited
- TRUE → подтверждение после редактирования
- FALSE → первое подтверждение
Первое подтверждение
- PostgreSQL SELECT:
row_select_by_id - PostgreSQL UPDATE:
update_after_confirm- Columns:
human_verified = true,human_verified_at = {{ $now }},chat_id = {{ $('answer_trigger').first().json.callback_query.message.chat.id }}
- Columns:
После редактирования
- Code:
parse_card_text - PostgreSQL UPDATE:
update_edited_transaction- Columns:
counterparty,category,human_verified = true,chat_id = {{ $('answer_trigger').first().json.callback_query.message.chat.id }}
- Columns:
- Code:
prepare_confirmation_data
const messageData = $('parse_card_text').first().json;
const parseData = $input.first().json;
return {
transactionId: parseData.transactionId,
chatId: messageData.chatId,
messageId: messageData.messageId,
counterparty: parseData.counterparty,
category: parseData.category,
raw_json: {
action: parseData.raw_json.action,
signed_amount: parseData.raw_json.signed_amount,
currency: parseData.raw_json.currency,
}
};
5.4. Merge: merge_confirm_paths
- Mode: Append
- Объединяет обе ветки подтверждения
5.5. Telegram Answer Callback Query: popup_confirm
5.6. Telegram Send Message: confirmation_message
6. Ветка 4. Редактирование транзакции
6.1. PostgreSQL Execute Query: empty_session
Удаляет старую сессию и пробрасывает данные.
6.2. IF: which_field_to_edit
- TRUE → категория
- FALSE → контрагент
6.3. PostgreSQL INSERT: add_session_category / add_session_counterparty
Создает сессию в user_sessions.
6.4. Telegram Send Message: category_edit / counterparty_edit
Запрашивает ввод нового значения.
7. Ветка 5. Обработка текстового ответа
7.1. PostgreSQL SELECT: get_session
7.2. Code: merge_user_and_session
7.3. Code: parse_text_input
7.4. PostgreSQL SELECT: get_current_transaction
7.5. Code: merge_changes
7.6. Telegram Send Message: show_updated_card
Отображает обновленную карточку с надписью "не сохранено".
8. Интеграция с Google Sheets
8.1. Структура таблицы
Колонки: дата, действие, контрагент, сумма, категория, примечание
8.2. Узел Google Sheets (после merge_confirm_paths)
- Operation: Append Row
- Credentials: Google Service Account
Mapping:
дата: {{ $json.received_at }}
действие: {{ $json.raw_json.action === 'salary' ? 'Зарплата' : $json.raw_json.action === 'reversal' ? 'Возврат' : $json.raw_json.action === 'payment' ? 'Оплата' : $json.raw_json.action === 'transfer' ? 'Перевод' : $json.raw_json.action === 'writeoff' ? 'Списание' : $json.raw_json.action === 'income' ? 'Поступление' : $json.raw_json.action === 'withdrawal' ? 'Снятие' : $json.raw_json.action === 'deposit' ? 'Внесение' : $json.raw_json.action }}
контрагент: {{ $json.counterparty }}
сумма: {{ String($json.raw_json.signed_amount).replace('.', ',') }}
категория: {{ $json.category }}
примечание:
9. Планируемые доработки
9.1. Архитектурные улучшения (Приоритет 1)
- Переход на stateless редактирование: Хранить состояние в
callback_dataкнопок вместо таблицыuser_sessions, убрать 7 узлов workflow, снизить нагрузку на БД на 70% - Inline-клавиатура для категорий: Заменить текстовый ввод на кнопки с персонализированными популярными категориями (топ-3 за 30 дней) + полный список
- Кэширование популярных категорий: Использовать таблицу
user_category_statsдля быстрого получения часто используемых категорий - Fallback для длинных callback_data: MD5-хэш + временное хранилище для значений >64 байт
9.2. LLM и категоризация (Приоритет 2)
- Собрать статистику по качеству автокатегоризации
- Fine-tuning модели LLM на собственных размеченных данных
- Скорректировать описания категорий/словарь на основе статистики
9.3. Автоматизация запуска (Приоритет 3)
- Автозапуск LM Studio при старте системы (service/systemd)
- Docker
restart: alwaysдля n8n и PostgreSQL - Health-check LM Studio API и уведомления в Telegram при падении
9.4. Улучшения UX (Приоритет 4)
- Кнопка «❌ Отклонить» для удаления/игнорирования ошибочных транзакций
- Ограничения и валидация пользовательского ввода (длина, спецсимволы)
- Периодическая очистка старых записей в
user_sessions(>1 часа) - Добавление исходного текста SMS в карточку
- Аналитика по категориям, отчёты и экспорт (CSV/Excel/BI)
Версия документа: 5.0
Дата: 2026-01-11
Статус: Добавлены типы операций (withdrawal/deposit), логика знаков, категория "Поступления", поле chat_id, экспорт в Google Sheets с колонкой "действие". Следующий этап — переход на stateless редактирование и inline-клавиатуры для категорий.