Files
test/backlog_buget.md
2026-01-16 18:37:32 +03:00

370 lines
15 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# SMS → n8n → LLM → PostgreSQL → Telegram → Google Sheets
Текущая архитектура workflow (11.01.2026)
## 1. Общий контур решения
Источник данных — банковские SMS, которые попадают в n8n через Webhook.
Дальше SMS:
1. парсятся в нормализованный JSON с определением типа операции;
2. сохраняются в PostgreSQL (таблица `transactions`);
3. обогащаются локальной LLM (Qwen2.5 через LM Studio);
4. отправляются в Telegram-бота в виде карточки с кнопками;
5. проходят через human-in-the-loop: пользователь подтверждает или редактирует контрагента и категорию;
6. после финального подтверждения данные фиксируются в БД и отправляются в Google Sheets для отчётности.
Проект развернут на Synology NAS: PostgreSQL. n8n в Docker и LM Studio — на локальной машине.
***
## 2. База данных
### 2.1. Таблица `transactions`
```sql
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 (какую транзакцию и какое поле сейчас меняет пользователь).
```sql
"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`
Кэш популярных категорий пользователей для персонализации.
```sql
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):**
```javascript
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 }
];
```
**Логика знаков:**
```javascript
// Проверка валидности суммы
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 (фрагмент):**
```text
ДОПУСТИМЫЕ КАТЕГОРИИ:
"Продукты", "Авто", "Здоровье", "Арчи", "ЖКХ", "Дом", "Проезд", "Одежда",
"Химия", "Косметика", "Инвестиции", "Развлечения", "Общепит", "Штрафы",
"Налоги", "Подписки", "Перевод", "Наличные", "Подарки", "Спорт", "Поступления"
КАТЕГОРИЗАЦИЯ ПО ТИПУ ОПЕРАЦИИ:
- Для снятия наличных категория ВСЕГДА "Наличные".
- Для внесения наличных категория ВСЕГДА "Поступления".
- Для зарплаты или поступлений категория ВСЕГДА "Поступления".
- Для возвратов/отмен категория ВСЕГДА "Поступления".
- Для переводов определяй категорию по контрагенту.
```
**Параметры:** `temperature: 0.2`, `max_tokens: 256`
### 3.6. Code: `parse_from_llm`
```javascript
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 }}`
#### После редактирования
- 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 }}`
- Code: `prepare_confirmation_data`
```javascript
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:**
```javascript
дата: {{ $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-клавиатуры для категорий.