initial commit

This commit is contained in:
vakabunga
2026-01-16 18:37:32 +03:00
commit 643eddb5fc
14 changed files with 3469 additions and 0 deletions

735
backlog_buget_old.md Normal file
View 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)