initial commit
This commit is contained in:
735
backlog_buget_old.md
Normal file
735
backlog_buget_old.md
Normal file
@@ -0,0 +1,735 @@
|
||||
# SMS → n8n → LLM → БД/Telegram: принятые решения и контракты данных
|
||||
|
||||
Контекст: автоматизация "Семейный бюджет". Источник данных — банковские SMS, которые пересылаются в n8n вебхуком, далее парсятся и обогащаются (LLM), затем пишутся в БД и/или отправляются уведомления в Telegram.
|
||||
|
||||
---
|
||||
|
||||
## СТАТУС ПРОЕКТА
|
||||
|
||||
### ✅ ВЫПОЛНЕНО (100% основного функционала)
|
||||
|
||||
#### Инфраструктура
|
||||
|
||||
- [x] PostgreSQL 16 развернут в Docker на Synology NAS (порт 5433)
|
||||
- [x] pgAdmin настроен (порт 5050)
|
||||
- [x] Таблица `transactions` создана и расширена полями для LLM и human-in-the-loop
|
||||
- [x] n8n запущен в Docker-контейнере
|
||||
- [x] Telegram-бот создан через @BotFather
|
||||
- [x] Telegram credentials настроены в n8n (Base URL: <https://api.telegram.org>)
|
||||
|
||||
#### n8n Workflow - Основная ветка обработки SMS
|
||||
|
||||
- [x] Webhook (POST) настроен для приёма SMS
|
||||
- [x] Auth check (проверка x-api-key заголовка)
|
||||
- [x] Code node (parse_sms) для парсинга SMS → NormalizedTransaction JSON
|
||||
- [x] PostgreSQL INSERT (create_from_sms) для записи базовых данных транзакции
|
||||
- Always Output Data = ON для получения id
|
||||
- [x] Edit Fields (Set) для data minimization перед LLM
|
||||
- [x] HTTP Request (post_to_llm) для обращения к LM Studio API
|
||||
- [x] Code node (parse_from_llm) для парсинга JSON-ответа от LLM
|
||||
- Очистка markdown обёртки (```json)
|
||||
- Merge с originalData из create_from_sms
|
||||
- Исправлена ошибка: choices.message → choices[0].message
|
||||
- [x] PostgreSQL UPDATE (update_llm_fields) для записи обогащённых данных
|
||||
- counterparty, category, subcategory
|
||||
- llm_processed_at
|
||||
- human_verified = FALSE (пока не подтверждено)
|
||||
- [x] Telegram Send Message (edit_or_confirm) для отправки карточки транзакции с кнопками
|
||||
- Inline Keyboard: Подтвердить / Изменить категорию / Изменить контрагента
|
||||
|
||||
#### LLM - Локальная модель
|
||||
|
||||
- [x] LM Studio установлен и настроен
|
||||
- [x] Модель Qwen2.5-7B-Instruct (Q6_K, 6 ГБ) загружена
|
||||
- [x] API-сервер запущен (порт 1234, Serve on Local Network включен)
|
||||
- [x] Параметры модели оптимизированы (Context: 8192, Temperature: 0.2, Max Tokens: 512)
|
||||
- [x] Промпт улучшен для корректной работы с неизвестными контрагентами
|
||||
- [x] Обработка markdown-обёртки в ответах (```json)
|
||||
|
||||
#### Human-in-the-loop через Telegram - ПОЛНОСТЬЮ ЗАВЕРШЕНО ✅
|
||||
|
||||
**Единый Telegram Trigger (решена проблема конфликта webhook):**
|
||||
|
||||
- [x] Один answer_trigger для ВСЕХ событий (callback_query + message)
|
||||
- [x] Решена проблема 403 Forbidden при регистрации webhook
|
||||
- [x] Telegram Credential настроен с Base URL = <https://api.telegram.org>
|
||||
- [x] IF node (split_event_type) для разделения типов событий
|
||||
|
||||
**TRUE ветка (подтверждение) - ЗАВЕРШЕНА:**
|
||||
|
||||
- [x] Code node (parse_from_callback) для парсинга callback_data
|
||||
- Извлечение: action, field, transactionId, chatId, messageId, messageText
|
||||
- Определение isEditedCard (проверка текста "не сохранено")
|
||||
- [x] IF node (confirm_or_edit) для разделения подтверждения/редактирования
|
||||
- [x] IF node (check_if_edited) для определения источника подтверждения
|
||||
- TRUE: подтверждение после редактирования → parse_card_text
|
||||
- FALSE: первое подтверждение без изменений → row_select_by_id
|
||||
- [x] Code node (parse_card_text) для извлечения данных из текста карточки
|
||||
- [x] Postgres UPDATE (update_edited_transaction) для сохранения отредактированных данных
|
||||
- counterparty, category, human_verified = TRUE
|
||||
- [x] Postgres SELECT (row_select_by_id) для получения данных транзакции
|
||||
- [x] Postgres UPDATE (update_after_confirm) для первого подтверждения
|
||||
- human_verified = TRUE
|
||||
- human_verified_at = timestamp
|
||||
- [x] Merge node (merge_confirm_paths) для объединения двух путей подтверждения
|
||||
- [x] Telegram Answer Callback Query (popup_confirm) для всплывающего уведомления
|
||||
- [x] Telegram Edit Message Text (confirmation_message) для обновления сообщения
|
||||
|
||||
**FALSE ветка (редактирование) - ЗАВЕРШЕНА:**
|
||||
|
||||
- [x] IF node (which_field_to_edit) для определения редактируемого поля
|
||||
- [x] Telegram Send Message (category_edit) для запроса новой категории
|
||||
- С Reply To Message ID для связи с транзакцией
|
||||
- Напоминание о лимите 12 символов
|
||||
- [x] Telegram Send Message (counterparty_edit) для запроса нового контрагента
|
||||
- С Reply To Message ID для связи с транзакцией
|
||||
- Напоминание о лимите 12 символов
|
||||
- [x] IF node (split_event_type) после единого триггера
|
||||
- isEmpty(callback_query) = TRUE → текстовое сообщение → parse_text_input
|
||||
- isEmpty(callback_query) = FALSE → callback от кнопки → parse_from_callback
|
||||
- [x] Code node (parse_text_input) для парсинга ответа пользователя
|
||||
- Извлечение transactionId из reply_to_message (#123)
|
||||
- Определение changedField по ключевым словам
|
||||
- Игнорирование команд (/)
|
||||
- Output: newValue, chatId, transactionId, changedField
|
||||
- [x] Postgres SELECT (get_current_transaction) для получения текущих данных
|
||||
- [x] Code node (merge_changes) для применения изменений
|
||||
- Merge новых и существующих данных
|
||||
- Не пишет в БД (временное состояние)
|
||||
- [x] Telegram Send Message (show_updated_card) для отображения обновлённой карточки
|
||||
- Маркер "не сохранено" для определения источника
|
||||
- Те же три кнопки для продолжения редактирования
|
||||
- ЦИКЛ ЗАМЫКАЕТСЯ: можно редактировать оба поля многократно
|
||||
|
||||
### 🔄 ОСТАЛОСЬ СДЕЛАТЬ
|
||||
|
||||
#### 1. Интеграция с Google Sheets (следующий приоритет)
|
||||
|
||||
- [ ] Создать Google Sheets таблицу для семейного бюджета
|
||||
- [ ] Настроить Google Sheets API в n8n (Service Account)
|
||||
- [ ] Добавить Google Sheets node ПОСЛЕ merge_confirm_paths
|
||||
- Записывать только подтверждённые транзакции
|
||||
- [ ] Настроить запись строки: Дата | Контрагент | Сумма | Категория
|
||||
- [ ] Форматирование: цветовое кодирование расходов/доходов
|
||||
|
||||
#### 2. Автоматизация запуска
|
||||
|
||||
- [ ] Автозапуск LM Studio при загрузке системы
|
||||
- [ ] Автозапуск Docker-контейнеров (restart policy = always)
|
||||
- [ ] Health-check для мониторинга LM Studio API
|
||||
|
||||
#### 3. Улучшения (опционально)
|
||||
|
||||
- [ ] Кнопка "❌ Отклонить" для удаления ошибочных транзакций
|
||||
- [ ] Редактирование подкатегории
|
||||
- [ ] Валидация пользовательского ввода (длина, спецсимволы)
|
||||
- [ ] Обработка исторических данных 2025 года для создания базы знаний
|
||||
- [ ] Fine-tuning модели на собственных размеченных данных
|
||||
- [ ] Аналитика и отчёты по категориям
|
||||
|
||||
---
|
||||
|
||||
## ФИНАЛЬНАЯ АРХИТЕКТУРА WORKFLOW (05.01.2026)
|
||||
|
||||
### Ветка 1: Обработка входящих SMS (ЗАВЕРШЕНА)
|
||||
|
||||
```text
|
||||
Webhook (POST)
|
||||
→ Auth check (x-api-key)
|
||||
→ Code (parse_sms)
|
||||
→ Postgres INSERT (create_from_sms) [Always Output Data = ON]
|
||||
→ Edit Fields (data minimization)
|
||||
→ HTTP Request (post_to_llm)
|
||||
→ Code (parse_from_llm) [choices[0].message.content, очистка markdown, merge]
|
||||
→ Postgres UPDATE (update_llm_fields) [human_verified=FALSE]
|
||||
→ Telegram Send Message (edit_or_confirm) [карточка с Inline Keyboard]
|
||||
```
|
||||
|
||||
### Ветка 2: Единый Telegram Trigger (КРИТИЧНО)
|
||||
|
||||
```text
|
||||
answer_trigger (Telegram Trigger)
|
||||
Updates: callback_query + message
|
||||
Base URL: https://api.telegram.org
|
||||
↓
|
||||
split_event_type (IF: isEmpty(callback_query))
|
||||
↓ TRUE (текстовое сообщение)
|
||||
parse_text_input → get_current_transaction → merge_changes → show_updated_card
|
||||
↓ FALSE (callback от кнопки)
|
||||
parse_from_callback → confirm_or_edit
|
||||
```
|
||||
|
||||
**ВАЖНО**: Только ОДИН Telegram Trigger на весь workflow! Два триггера конфликтуют за один webhook URL.
|
||||
|
||||
### Ветка 3: Подтверждение транзакции (ЗАВЕРШЕНА)
|
||||
|
||||
```text
|
||||
parse_from_callback [isEditedCard = messageText.includes("не сохранено")]
|
||||
↓
|
||||
confirm_or_edit (IF: action == "confirm")
|
||||
↓ TRUE (подтверждение)
|
||||
check_if_edited (IF: isEditedCard == true)
|
||||
↓ TRUE (после редактирования)
|
||||
parse_card_text → update_edited_transaction
|
||||
↓ FALSE (первое подтверждение)
|
||||
row_select_by_id → update_after_confirm
|
||||
↓
|
||||
merge_confirm_paths (объединение двух путей)
|
||||
↓
|
||||
popup_confirm (Answer Callback Query)
|
||||
↓
|
||||
confirmation_message (Edit Message Text)
|
||||
```
|
||||
|
||||
### Ветка 4: Редактирование транзакции с циклом (ЗАВЕРШЕНА)
|
||||
|
||||
```text
|
||||
parse_from_callback
|
||||
↓
|
||||
confirm_or_edit (IF: action == "confirm")
|
||||
↓ FALSE (редактирование)
|
||||
which_field_to_edit (IF: field == "category")
|
||||
↓ TRUE
|
||||
category_edit (Send Message: "Введите категорию #ID, лимит 12 символов")
|
||||
↓ FALSE
|
||||
counterparty_edit (Send Message: "Введите контрагента #ID, лимит 12 символов")
|
||||
↓
|
||||
[Пользователь вводит текст]
|
||||
↓
|
||||
answer_trigger → split_event_type → parse_text_input
|
||||
↓
|
||||
get_current_transaction (SELECT текущих данных)
|
||||
↓
|
||||
merge_changes (применение изменений БЕЗ записи в БД)
|
||||
↓
|
||||
show_updated_card (карточка с маркером "не сохранено")
|
||||
[Те же три кнопки: Подтвердить / Изменить категорию / Изменить контрагента]
|
||||
↓
|
||||
[ЦИКЛ ЗАМЫКАЕТСЯ: нажатие любой кнопки → answer_trigger]
|
||||
```
|
||||
|
||||
**Ключевая особенность**: Изменения НЕ сохраняются в БД до финального подтверждения. Данные передаются через текст карточки (лимит 12 символов на поле для влезания в callback_data при необходимости).
|
||||
|
||||
---
|
||||
|
||||
## ТЕХНИЧЕСКИЕ ДЕТАЛИ
|
||||
|
||||
### База данных PostgreSQL
|
||||
|
||||
**Connection**:
|
||||
|
||||
- Host: Synology NAS IP
|
||||
- Port: 5433
|
||||
- Database: budget
|
||||
- User: postgres
|
||||
- Credential в n8n: "postgres account"
|
||||
|
||||
**Таблица 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
|
||||
|
||||
-- LLM и Human-in-the-loop поля
|
||||
counterparty VARCHAR(255)
|
||||
category VARCHAR(100)
|
||||
subcategory VARCHAR(100)
|
||||
llm_processed_at TIMESTAMP WITH TIME ZONE
|
||||
human_verified BOOLEAN DEFAULT FALSE
|
||||
human_verified_at TIMESTAMP WITH TIME ZONE
|
||||
|
||||
-- Индексы
|
||||
CREATE INDEX idx_transactions_received_at ON transactions(received_at DESC);
|
||||
CREATE INDEX idx_transactions_action ON transactions(action);
|
||||
CREATE INDEX idx_transactions_counterparty ON transactions(counterparty);
|
||||
CREATE INDEX idx_transactions_category ON transactions(category);
|
||||
CREATE INDEX idx_transactions_human_verified ON transactions(human_verified) WHERE human_verified = TRUE;
|
||||
```
|
||||
|
||||
### Telegram Configuration
|
||||
|
||||
**Telegram Credential (в n8n)**:
|
||||
|
||||
- Access Token: токен от @BotFather
|
||||
- Base URL: `https://api.telegram.org` (ВАЖНО!)
|
||||
|
||||
**Почему именно этот Base URL**:
|
||||
|
||||
- НЕ пустой (не polling mode)
|
||||
- НЕ адрес n8n (не webhook mode через собственный сервер)
|
||||
- Стандартный API Telegram - n8n использует гибридный режим получения событий
|
||||
|
||||
**Проблема с двумя триггерами**:
|
||||
|
||||
- Telegram позволяет только ОДИН webhook на бота
|
||||
- Два Telegram Trigger в одном workflow конфликтуют
|
||||
- Решение: единый триггер + IF для разделения типов событий
|
||||
|
||||
### LLM Промпт (финальная версия)
|
||||
|
||||
**System Message:**
|
||||
|
||||
```text
|
||||
Ты эксперт по категоризации расходов семейного бюджета в России. Анализируй банковские SMS и извлекай данные.
|
||||
|
||||
ВАЖНО:
|
||||
- Если контрагент НЕИЗВЕСТЕН или непонятен - укажи confidence < 0.5
|
||||
- Уверенность 1.0 ставь ТОЛЬКО для известных российских брендов
|
||||
- Для неочевидных названий снижай confidence до 0.3-0.6
|
||||
|
||||
Известные сети: PYATEROCHKA, MAGNIT, ЗОЛОТОЕ ЯБЛОКО, WILDBERRIES, YANDEX TAXI
|
||||
|
||||
Для неизвестных: category=Неизвестно, confidence < 0.5
|
||||
|
||||
Отвечай ТОЛЬКО валидным JSON без markdown обёртки.
|
||||
```
|
||||
|
||||
**User Message:**
|
||||
|
||||
```text
|
||||
Проанализируй SMS: {{ $json.sms_text }}
|
||||
Тип: {{ $json.action }}
|
||||
Сумма: {{ $json.amount }} {{ $json.currency }}
|
||||
|
||||
Если контрагент неизвестен - confidence < 0.5!
|
||||
|
||||
Верни JSON: {"counterparty": "", "category": "", "subcategory": "", "confidence": 0.0}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ФИНАЛЬНЫЕ CODE NODES (готовые к использованию)
|
||||
|
||||
### 1. parse_from_llm (ИСПРАВЛЕНА ошибка choices.message)
|
||||
|
||||
```javascript
|
||||
// Исправлено: choices.message → choices[0].message
|
||||
let content = $input.first().json.choices[0].message.content;
|
||||
|
||||
// Очистка markdown обёртки
|
||||
content = content.replace(/```json\n?/g, '').replace(/```\n?/g, '').trim();
|
||||
|
||||
const llmResponse = JSON.parse(content);
|
||||
const originalData = $('create_from_sms').first().json;
|
||||
|
||||
return {
|
||||
...originalData,
|
||||
counterparty: llmResponse.counterparty || 'Не определён',
|
||||
category: llmResponse.category || 'Неизвестно',
|
||||
subcategory: llmResponse.subcategory || 'Требует уточнения',
|
||||
confidence: llmResponse.confidence !== undefined ? llmResponse.confidence : 0.0
|
||||
};
|
||||
```
|
||||
|
||||
### 2. parse_from_callback (ОБНОВЛЕНА: добавлено isEditedCard)
|
||||
|
||||
```javascript
|
||||
const callbackData = $input.first().json.callback_query.data;
|
||||
const chatId = $input.first().json.callback_query.message.chat.id;
|
||||
const messageId = $input.first().json.callback_query.message.message_id;
|
||||
const messageText = $input.first().json.callback_query.message.text;
|
||||
|
||||
const parts = callbackData.split('_');
|
||||
const action = parts[0]; // confirm, edit
|
||||
const field = parts.length === 3 ? parts[1] : null; // category, counterparty
|
||||
const transactionId = parseInt(parts[parts.length - 1]);
|
||||
|
||||
// Проверка: это подтверждение после редактирования?
|
||||
const isEditedCard = messageText && messageText.includes('не сохранено');
|
||||
|
||||
return {
|
||||
action: action,
|
||||
field: field,
|
||||
transactionId: transactionId,
|
||||
chatId: chatId,
|
||||
messageId: messageId,
|
||||
messageText: messageText,
|
||||
isEditedCard: isEditedCard,
|
||||
originalCallbackData: callbackData
|
||||
};
|
||||
```
|
||||
|
||||
### 3. parse_text_input (для обработки текстовых ответов)
|
||||
|
||||
```javascript
|
||||
const userText = $input.first().json.message.text;
|
||||
const chatId = $input.first().json.message.chat.id;
|
||||
const messageData = $input.first().json.message;
|
||||
|
||||
// Игнорируем команды
|
||||
if (userText.startsWith('/')) {
|
||||
throw new Error('Команда проигнорирована');
|
||||
}
|
||||
|
||||
// Проверяем что это ответ на наше сообщение
|
||||
if (!messageData.reply_to_message) {
|
||||
throw new Error('Сообщение не является ответом на запрос');
|
||||
}
|
||||
|
||||
// Извлекаем ID транзакции из текста запроса
|
||||
const originalText = messageData.reply_to_message.text;
|
||||
const match = originalText.match(/#(\d+)/);
|
||||
|
||||
if (!match) {
|
||||
throw new Error('Не удалось найти ID транзакции');
|
||||
}
|
||||
|
||||
const transactionId = parseInt(match[1]);
|
||||
|
||||
// Определяем какое поле пользователь меняет
|
||||
let changedField = null;
|
||||
if (originalText.includes('категорию')) {
|
||||
changedField = 'category';
|
||||
} else if (originalText.includes('контрагента')) {
|
||||
changedField = 'counterparty';
|
||||
}
|
||||
|
||||
return {
|
||||
newValue: userText.trim(),
|
||||
chatId: chatId,
|
||||
transactionId: transactionId,
|
||||
changedField: changedField
|
||||
};
|
||||
```
|
||||
|
||||
### 4. merge_changes (применение изменений без записи в БД)
|
||||
|
||||
```javascript
|
||||
const textInput = $('parse_text_input').first().json;
|
||||
const currentData = $('get_current_transaction').first().json;
|
||||
|
||||
// Применяем изменение
|
||||
const updatedCategory = textInput.changedField === 'category'
|
||||
? textInput.newValue
|
||||
: currentData.category;
|
||||
|
||||
const updatedCounterparty = textInput.changedField === 'counterparty'
|
||||
? textInput.newValue
|
||||
: currentData.counterparty;
|
||||
|
||||
return {
|
||||
...currentData,
|
||||
category: updatedCategory,
|
||||
counterparty: updatedCounterparty,
|
||||
chatId: textInput.chatId
|
||||
};
|
||||
```
|
||||
|
||||
### 5. parse_card_text (извлечение данных из текста карточки)
|
||||
|
||||
```javascript
|
||||
const messageText = $input.first().json.messageText;
|
||||
const transactionId = $input.first().json.transactionId;
|
||||
|
||||
// Извлекаем контрагента (между "Контрагент: " и "\n")
|
||||
const counterpartyMatch = messageText.match(/Контрагент:\s*(.+?)(?:\n|$)/);
|
||||
const counterparty = counterpartyMatch ? counterpartyMatch[1].trim() : null;
|
||||
|
||||
// Извлекаем категорию (между "Категория: " и "\n")
|
||||
const categoryMatch = messageText.match(/Категория:\s*(.+?)(?:\n|$)/);
|
||||
const category = categoryMatch ? categoryMatch[1].trim() : null;
|
||||
|
||||
if (!counterparty || !category) {
|
||||
throw new Error('Не удалось извлечь данные из карточки');
|
||||
}
|
||||
|
||||
return {
|
||||
transactionId: transactionId,
|
||||
counterparty: counterparty,
|
||||
category: category,
|
||||
chatId: $input.first().json.chatId,
|
||||
messageId: $input.first().json.messageId
|
||||
};
|
||||
```
|
||||
|
||||
### 6. prepare_confirmation_data (подготовка данных для финальных нод)
|
||||
|
||||
```javascript
|
||||
const parseData = $('parse_card_text').first().json;
|
||||
|
||||
return {
|
||||
transactionId: parseData.transactionId,
|
||||
chatId: parseData.chatId,
|
||||
messageId: parseData.messageId,
|
||||
counterparty: parseData.counterparty,
|
||||
category: parseData.category
|
||||
};
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## НАСТРОЙКИ ВСЕХ НОД
|
||||
|
||||
### Telegram Nodes
|
||||
|
||||
**answer_trigger (Telegram Trigger)**:
|
||||
|
||||
- Credential: Telegram account
|
||||
- Trigger On: callback_query
|
||||
- Updates: `callback_query`, `message` (оба!)
|
||||
|
||||
**edit_or_confirm (Telegram Send Message)**:
|
||||
|
||||
- Chat ID: `{{ $json.chatId }}`
|
||||
- Text:
|
||||
|
||||
```text
|
||||
Требуется подтверждение транзакции #{{ $json.id }}
|
||||
|
||||
Сумма: {{ $json.signed_amount }} {{ $json.currency }}
|
||||
Контрагент: {{ $json.counterparty }}
|
||||
Категория: {{ $json.category }}
|
||||
Дата: {{ $json.received_at }}
|
||||
```
|
||||
|
||||
- Reply Markup:
|
||||
|
||||
```json
|
||||
{
|
||||
"inline_keyboard": [
|
||||
[{"text": "✅ Подтвердить", "callback_data": "confirm_{{ $json.id }}"}],
|
||||
[{"text": "✏️ Изменить категорию", "callback_data": "edit_category_{{ $json.id }}"}],
|
||||
[{"text": "✏️ Изменить контрагента", "callback_data": "edit_counterparty_{{ $json.id }}"}]
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
**category_edit (Telegram Send Message)**:
|
||||
|
||||
- Chat ID: `{{ $json.chatId }}`
|
||||
- Text: `Введите новую категорию для транзакции #{{ $json.transactionId }}\n\nВАЖНО: Лимит 12 символов`
|
||||
- Reply To Message ID: `{{ $json.messageId }}`
|
||||
|
||||
**counterparty_edit (Telegram Send Message)**:
|
||||
|
||||
- Chat ID: `{{ $json.chatId }}`
|
||||
- Text: `Введите нового контрагента для транзакции #{{ $json.transactionId }}\n\nВАЖНО: Лимит 12 символов`
|
||||
- Reply To Message ID: `{{ $json.messageId }}`
|
||||
|
||||
**show_updated_card (Telegram Send Message)**:
|
||||
|
||||
- Chat ID: `{{ $json.chatId }}`
|
||||
- Text:
|
||||
|
||||
```text
|
||||
Данные обновлены (не сохранено)
|
||||
|
||||
Сумма: {{ $json.signed_amount }} {{ $json.currency }}
|
||||
Контрагент: {{ $json.counterparty }}
|
||||
Категория: {{ $json.category }}
|
||||
Дата: {{ $json.received_at }}
|
||||
|
||||
ВАЖНО: Лимит 12 символов на поле
|
||||
```
|
||||
|
||||
- Reply Markup: (те же три кнопки что и в edit_or_confirm)
|
||||
|
||||
**popup_confirm (Telegram Answer Callback Query)**:
|
||||
|
||||
- Query ID: `{{ $json.callback_query.id }}`
|
||||
- Text: `✅ Транзакция подтверждена и сохранена`
|
||||
|
||||
**confirmation_message (Telegram Edit Message Text)**:
|
||||
|
||||
- Chat ID: `{{ $json.chatId }}`
|
||||
- Message ID: `{{ $json.messageId }}`
|
||||
- Text: `✅ Транзакция #{{ $json.transactionId }} подтверждена\n\nКонтрагент: {{ $json.counterparty }}\nКатегория: {{ $json.category }}`
|
||||
|
||||
### PostgreSQL Nodes
|
||||
|
||||
**create_from_sms (Postgres INSERT)**:
|
||||
|
||||
- Operation: Insert
|
||||
- Table: transactions
|
||||
- Columns: все базовые поля из parse_sms
|
||||
- Options: Always Output Data = ON
|
||||
|
||||
**update_llm_fields (Postgres UPDATE)**:
|
||||
|
||||
- Operation: Update
|
||||
- Table: transactions
|
||||
- Columns: counterparty, category, subcategory, llm_processed_at, human_verified=FALSE
|
||||
- Where: id = {{ $json.id }}
|
||||
- Options: Always Output Data = ON
|
||||
|
||||
**get_current_transaction (Postgres SELECT)**:
|
||||
|
||||
- Operation: Select
|
||||
- Table: transactions
|
||||
- Return All: OFF
|
||||
- Where: id = {{ $json.transactionId }}
|
||||
- Options: Always Output Data = ON
|
||||
|
||||
**update_edited_transaction (Postgres UPDATE)**:
|
||||
|
||||
- Operation: Update
|
||||
- Table: transactions
|
||||
- Columns:
|
||||
- counterparty = {{ $json.counterparty }}
|
||||
- category = {{ $json.category }}
|
||||
- human_verified = true (toggle ON)
|
||||
- Where: id = {{ $json.transactionId }}
|
||||
|
||||
**row_select_by_id (Postgres SELECT)**:
|
||||
|
||||
- Operation: Select
|
||||
- Table: transactions
|
||||
- Return All: OFF
|
||||
- Where: id = {{ $json.transactionId }}
|
||||
|
||||
**update_after_confirm (Postgres UPDATE)**:
|
||||
|
||||
- Operation: Update
|
||||
- Table: transactions
|
||||
- Columns:
|
||||
- human_verified = true
|
||||
- human_verified_at = {{ $now }}
|
||||
- Where: id = {{ $json.transactionId }}
|
||||
|
||||
### IF Nodes
|
||||
|
||||
**split_event_type (разделение типов событий)**:
|
||||
|
||||
- Condition: `{{ $json.callback_query }}` Is Empty
|
||||
- TRUE → текстовое сообщение → parse_text_input
|
||||
- FALSE → callback от кнопки → parse_from_callback
|
||||
|
||||
**confirm_or_edit (разделение подтверждения/редактирования)**:
|
||||
|
||||
- Condition: `{{ $json.action }}` Equal `confirm`
|
||||
- TRUE → подтверждение
|
||||
- FALSE → редактирование
|
||||
|
||||
**check_if_edited (определение источника подтверждения)**:
|
||||
|
||||
- Condition: `{{ $json.isEditedCard }}` Equal `true`
|
||||
- TRUE → после редактирования → parse_card_text
|
||||
- FALSE → первое подтверждение → row_select_by_id
|
||||
|
||||
**which_field_to_edit (определение редактируемого поля)**:
|
||||
|
||||
- Condition: `{{ $json.field }}` Equal `category`
|
||||
- TRUE → category_edit
|
||||
- FALSE → counterparty_edit
|
||||
|
||||
### Merge Node
|
||||
|
||||
**merge_confirm_paths (объединение путей подтверждения)**:
|
||||
|
||||
- Type: Merge
|
||||
- Mode: Append
|
||||
- Input 1: update_after_confirm (первое подтверждение)
|
||||
- Input 2: prepare_confirmation_data (после редактирования)
|
||||
- Output: popup_confirm
|
||||
|
||||
---
|
||||
|
||||
## CHANGELOG
|
||||
|
||||
### 2026-01-05 - Human-in-the-loop ПОЛНОСТЬЮ ЗАВЕРШЁН ✅
|
||||
|
||||
**Критические исправления:**
|
||||
|
||||
- Исправлена ошибка в parse_from_llm: `choices.message` → `choices[0].message.content`
|
||||
- Решена проблема конфликта двух Telegram триггеров за один webhook
|
||||
- Удалён edit_trigger, всё через единый answer_trigger
|
||||
- Добавлен split_event_type IF для разделения callback_query и message
|
||||
- Исправлена ошибка 403 Forbidden при регистрации webhook
|
||||
- Telegram Credential Base URL установлен в <https://api.telegram.org>
|
||||
|
||||
**Реализован полный цикл редактирования:**
|
||||
|
||||
- parse_text_input: парсинг текстовых ответов пользователя
|
||||
- get_current_transaction: SELECT текущих данных
|
||||
- merge_changes: применение изменений БЕЗ записи в БД
|
||||
- show_updated_card: обновлённая карточка с маркером "не сохранено"
|
||||
- ЦИКЛ: можно редактировать category и counterparty многократно
|
||||
|
||||
**Реализовано двойное подтверждение:**
|
||||
|
||||
- check_if_edited IF: определение источника подтверждения
|
||||
- parse_card_text: извлечение данных из текста карточки
|
||||
- update_edited_transaction: UPDATE после редактирования
|
||||
- prepare_confirmation_data: подготовка данных
|
||||
- merge_confirm_paths: объединение двух путей подтверждения
|
||||
|
||||
**Технические решения:**
|
||||
|
||||
- Лимит 12 символов на поле (для влезания в callback_data)
|
||||
- Маркер "не сохранено" в тексте карточки для определения типа
|
||||
- Данные передаются через текст сообщения, не через БД
|
||||
- Единый триггер + IF вместо двух конфликтующих триггеров
|
||||
|
||||
### 2026-01-02 - Human-in-the-loop через Telegram (90% завершено)
|
||||
|
||||
- Изменена архитектура: все транзакции через человека
|
||||
- LLM-данные сохраняются сразу (human_verified=false)
|
||||
- Создан Telegram-бот, настроены credentials
|
||||
- TRUE ветка (подтверждение) завершена
|
||||
- FALSE ветка (редактирование) на 90%
|
||||
- Парсинг markdown обёртки LLM
|
||||
- Reply To Message для связи с транзакцией
|
||||
|
||||
### 2026-01-01 - LM Studio интегрирован
|
||||
|
||||
- Установлен LM Studio, загружена Qwen2.5-7B-Instruct
|
||||
- Настроены параметры модели
|
||||
- Решена проблема подключения n8n Docker к LM Studio
|
||||
- Создан HTTP Request узел для LLM API
|
||||
- Разработан промпт для категоризации
|
||||
|
||||
### 2025-12-XX - Базовая инфраструктура
|
||||
|
||||
- Развернут PostgreSQL 16 в Docker
|
||||
- Создана таблица transactions
|
||||
- Настроен n8n workflow: Webhook → Auth → Code → PostgreSQL
|
||||
- Реализован парсинг SMS
|
||||
|
||||
---
|
||||
|
||||
## СЛЕДУЮЩИЕ ШАГИ
|
||||
|
||||
### Приоритет 1: Интеграция с Google Sheets
|
||||
|
||||
1. Создать Google Sheets таблицу для семейного бюджета
|
||||
2. Настроить Google Sheets API в n8n (Service Account)
|
||||
3. Добавить Google Sheets node после merge_confirm_paths
|
||||
4. Записывать только подтверждённые транзакции
|
||||
5. Настроить колонки: Дата | Контрагент | Сумма | Категория | Подкатегория
|
||||
6. Форматирование: цветовое кодирование расходов/доходов
|
||||
|
||||
### Приоритет 2: Автоматизация запуска
|
||||
|
||||
1. Автозапуск LM Studio при загрузке системы (systemd service)
|
||||
2. Docker restart policy = always для всех контейнеров
|
||||
3. Health-check для мониторинга LM Studio API
|
||||
4. Уведомления в Telegram при падении сервисов
|
||||
|
||||
### Приоритет 3: Улучшения (опционально)
|
||||
|
||||
1. Кнопка "❌ Отклонить" для удаления ошибочных транзакций
|
||||
2. Редактирование подкатегории
|
||||
3. Валидация пользовательского ввода (длина, спецсимволы)
|
||||
4. Обработка исторических данных 2025 года
|
||||
5. Fine-tuning модели на собственных данных
|
||||
6. Аналитика и отчёты по категориям расходов
|
||||
7. Экспорт данных в различные форматы
|
||||
|
||||
---
|
||||
|
||||
**Версия документа**: 3.0
|
||||
**Дата последнего обновления**: 2026-01-05
|
||||
**Статус**: Основной функционал ЗАВЕРШЁН (100%), готов к продакшн-использованию
|
||||
**Следующий шаг**: Интеграция с Google Sheets
|
||||
**Автор обновления**: AI Assistant (Perplexity)
|
||||
Reference in New Issue
Block a user