initial commit
This commit is contained in:
618
backlog_buget_260111.md
Normal file
618
backlog_buget_260111.md
Normal file
@@ -0,0 +1,618 @@
|
||||
# SMS → n8n → LLM → PostgreSQL → Telegram
|
||||
|
||||
Текущая архитектура workflow (09.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 для отчётности.[^1]
|
||||
|
||||
Проект развернут на Synology NAS: PostgreSQL. n8n в Docker и LM Studio — на локальной машине.[^1]
|
||||
|
||||
***
|
||||
|
||||
## 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,
|
||||
|
||||
-- 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`.[^1]
|
||||
|
||||
### 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, -- 'category' | 'counterparty'
|
||||
"createdAt" TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
|
||||
"tempCounterparty" VARCHAR(255),
|
||||
"tempCategory" VARCHAR(100)
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 3. Ветка 1. Обработка входящих SMS (Webhook → LLM → БД → Telegram)
|
||||
|
||||
### 3.1. Webhook (Node: `Webhook`)
|
||||
|
||||
- Method: `POST`.
|
||||
- Принимает JSON с полями SMS от прокси/смс‑шлюза.
|
||||
- Входные данные: `raw_sms`, `sms_text`, `sms_sender`, дата и пр.[^1]
|
||||
|
||||
### 3.2. Code: `parse_from_sms`
|
||||
|
||||
- Преобразует входящий JSON из Webhook в нормализованный объект:
|
||||
- `action` (тип операции: payment/credit/cash и т.п.);
|
||||
- `amount`, `signed_amount`, `currency`;
|
||||
- `balance`;
|
||||
- `received_at`;
|
||||
- `raw_sms`, `sms_text`, `sms_sender`;
|
||||
- `raw_json` — исходный JSON целиком.
|
||||
|
||||
Результат — один объект `NormalizedTransaction`.[^1]
|
||||
|
||||
### 3.3. PostgreSQL: `create_from_sms` (INSERT)
|
||||
|
||||
- Operation: **Insert**
|
||||
- Table: `transactions`
|
||||
- Поля: все базовые из `parse_from_sms` (без LLM‑полей).
|
||||
- Options: **Always Output Data = ON** — чтобы сразу получить `id` созданной строки.[^1]
|
||||
|
||||
### 3.4. Set: `Edit Fields`
|
||||
|
||||
- Урезает объект перед LLM (data minimization): оставляет только то, что нужно модели:
|
||||
- `sms_text`, `action`, `amount`, `currency`, `balance`.[^1]
|
||||
|
||||
### 3.5. HTTP Request: `post_to_llm`
|
||||
|
||||
- Метод: POST.
|
||||
- URL: локальный LM Studio API (`http://<LM_STUDIO_IP>:1234/v1/chat/completions`).
|
||||
- Модель: Qwen2.5‑7B‑Instruct (через LM Studio).[^1]
|
||||
|
||||
**Тело запроса (JSON, Expression):**
|
||||
|
||||
```js
|
||||
{
|
||||
"model": "qwen2.5-7b-instruct",
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": "Ты эксперт по категоризации расходов семейного бюджета в России. Анализируй банковские SMS и извлекай данные.\n\nТВОЯ ЗАДАЧА:\n- Определить контрагента (магазин, сервис, банк и т.п.).\n- Определить категорию расхода (еда, транспорт, подписки, одежда, здоровье, дом, техника, развлечения, авто, услуги и т.п.).\n\nВАЖНО:\n- Никогда не используй значения 'Неизвестно', 'Не определён', 'Требует уточнения' или похожие.\n- Даже если не уверен, выбери наиболее вероятного контрагента и категорию по тексту SMS.\n- Для контрагента используй удобное для человека название (например, 'Пятёрочка', 'Магнит', 'Яндекс Такси').\n- Если название контрагента написано латиницей или капсом (например, PYATEROCHKA, PEREKRESTOK, YANDEXTAXI), считай, что это русские бренды и приведи их к удобному русскому названию для человека: «Пятёрочка», «Перекрёсток», «Яндекс Такси» и т.п.\n\nСтарайся находить наиболее близкий известный российский бренд по написанию, а не считать его неизвестным.\n\nДОПУСТИМЫЕ КАТЕГОРИИ:\nИспользуй ТОЛЬКО следующие значения поля \"category\" (строго как написано, без синонимов):\n\"Продукты\" - продуктовые магазины, продовольственные товары\n\"Авто\" - топливо, парковки, платные дороги, ремонт авто и т.д.\n\"Здоровье\" - аптеки, поликлиники, больницы и т.д.\n\"Арчи\" - зоотовары, вет. клиники, вет. аптеки и т.д.\n\"ЖКХ\" - коммунальные платежи и т.д.\n\"Дом\" - мелочи для интерьера, напольные покрытия, мебель, техника для дома, бытовые мелочи и т.д. сюда можно отнести товары от контрагента OZON\n\"Проезд\" - все что связано с общественным транспортом (электрички, метро, такси и т.д.)\n\"Одежда\" - одежда и обувь\n\"Химия\" - бытовая химия и т.д.\n\"Косметика\" - косметика\n\"Инвестиции\" - перевод во вклады, покупка акций и т.д.\n\"Развлечения\" - любые развлечения (кино, музеи, концерты, отпуск и т.д.)\n\"Общепит\" - кафе, рестораны, столовые и другая еда вне дома\n\"Штрафы\" - любые штрафы\n\"Налоги\" - государственные налоги\n\"Подписки\" - обязательные переодические платежи (подписки, связь, интернет и т.д.)\n\"Перевод\" - перевод денежных средств\n\"Наличные\" - снятие или внесение наличных\n\"Подарки\" - сотовые телефоны, драгоценности, цветы и т.д.\n\"Спорт\" - покупка билетов (слотов) на спортивные мероприятия, покупка спортивного инвентаря, в том числе одежда и обувь из спортивных магазинов\n\nНикогда не придумывай новые формулировки категорий и не используй синонимы.\nЕсли не уверен, выбери наиболее подходящую категорию из этого списка.\n\nОтвечай ТОЛЬКО валидным JSON без каких‑либо пояснений."
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": "Проанализируй банковскую SMS и верни только JSON.\n\nSMS:\n{{ $json.sms_text }}\n\nВерни строго валидный JSON одного объекта вида:\n{\"counterparty\": \"\", \"category\": \"\"}\n\nГде:\n- \"counterparty\" — человекопонятное название контрагента на русском (например, \"Пятёрочка\", \"Магнит\", \"Яндекс Такси\").\n- \"category\" — ОДНА из допустимых категорий из системного промпта. Не придумывай других категорий и не используй синонимы.\n\nНе добавляй никаких других полей и комментариев. Только JSON."
|
||||
}
|
||||
],
|
||||
"temperature": 0.2,
|
||||
"max_tokens": 256
|
||||
}
|
||||
```
|
||||
|
||||
### 3.6. Code: `parse_from_llm`
|
||||
|
||||
Актуальная логика:
|
||||
|
||||
- Берёт `choices[^0].message.content` из ответа.
|
||||
- Убирает возможные обёртки ```json /``` (markdown).
|
||||
- Парсит JSON вида:
|
||||
|
||||
```json
|
||||
{"counterparty": "...", "category": "..."}
|
||||
```
|
||||
|
||||
- Объединяет с исходными данными из `create_from_sms`.
|
||||
- Без `subcategory` и `confidence`.
|
||||
- Подстраховывает пустые поля дефолтами.[^1]
|
||||
|
||||
Код:
|
||||
|
||||
```javascript
|
||||
// Получаем content от LLM
|
||||
let content = $input.first().json.choices[^0].message.content;
|
||||
|
||||
// Убираем markdown-обёртку ```json ... ``` если есть
|
||||
content = content
|
||||
.replace(/```json\n?/gi, '')
|
||||
.replace(/```\n?/g, '')
|
||||
.trim();
|
||||
|
||||
// Парсим очищенный JSON
|
||||
const llmResponse = JSON.parse(content);
|
||||
|
||||
// Берём все данные из первоначальной транзакции
|
||||
const originalData = $('create_from_sms').first().json;
|
||||
|
||||
// Объединяем: базовые данные + результаты LLM
|
||||
return {
|
||||
...originalData,
|
||||
counterparty: llmResponse.counterparty || originalData.counterparty || 'Продукты',
|
||||
category: llmResponse.category || originalData.category || 'Продукты'
|
||||
};
|
||||
```
|
||||
|
||||
### 3.7. PostgreSQL: `update_llm_fields` (UPDATE)
|
||||
|
||||
- Operation: **Update**
|
||||
- Table: `transactions`
|
||||
- Where: `id = {{ $json.id }}`
|
||||
- Поля для обновления:
|
||||
- `counterparty`, `category`;
|
||||
- `llm_processed_at = NOW()`;
|
||||
- `human_verified = FALSE`.
|
||||
- Options: **Always Output Data = ON** — чтобы вернуть актуальную строку и `chatId`.[^1]
|
||||
|
||||
### 3.8. Telegram Send Message: `edit_or_confirm`
|
||||
|
||||
- Resource: Message.
|
||||
- Operation: Send Message.
|
||||
- Chat ID: `{{ $json.chatId }}`
|
||||
- Text:
|
||||
|
||||
```text
|
||||
Требуется подтверждение транзакции #{{ $json.id }}
|
||||
|
||||
Сумма: {{ $json.signed_amount }} {{ $json.currency }}
|
||||
Контрагент: {{ $json.counterparty }}
|
||||
Категория: {{ $json.category }}
|
||||
Дата: {{ $json.received_at }}
|
||||
|
||||
Подтвердить или изменить?
|
||||
```
|
||||
|
||||
- Reply Markup (Inline Keyboard):
|
||||
- `✅ Подтвердить` → `confirm_{{ $json.id }}`
|
||||
- `✏️ Изменить категорию` → `edit_category_{{ $json.id }}`
|
||||
- `✏️ Изменить контрагента` → `edit_counterparty_{{ $json.id }}`.[^1]
|
||||
|
||||
***
|
||||
|
||||
## 4. Ветка 2. Единый Telegram Trigger и разветвление по типу события
|
||||
|
||||
### 4.1. Telegram Trigger: `answer_trigger`
|
||||
|
||||
- Credential: Telegram Bot.
|
||||
- Trigger On: `callback_query`.
|
||||
- Updates: `callback_query`, `message` (оба включены).
|
||||
- Base URL: `https://api.telegram.org`.[^1]
|
||||
|
||||
Все события (клики по кнопкам и текстовые сообщения) приходят в этот один триггер.[^1]
|
||||
|
||||
### 4.2. IF: `split_event_type`
|
||||
|
||||
- Condition: `{{ $json.callback_query }}` **Is Empty**.
|
||||
- TRUE → текстовое сообщение пользователя → ветка редактирования.
|
||||
- FALSE → callback от кнопки → ветка подтверждения/редактирования.[^1]
|
||||
|
||||
***
|
||||
|
||||
## 5. Ветка 3. Подтверждение транзакции (без/после редактирования)
|
||||
|
||||
### 5.1. Code: `parse_from_callback`
|
||||
|
||||
- Вход: объект update с `callback_query`.
|
||||
- Достаёт:
|
||||
- `callbackData = $json.callback_query.data`;
|
||||
- `chatId = $json.callback_query.message.chat.id`;
|
||||
- `messageId = $json.callback_query.message.message_id`;
|
||||
- `messageText = $json.callback_query.message.text`.[^1]
|
||||
- Разбирает callback:
|
||||
|
||||
```javascript
|
||||
const parts = callbackData.split('_');
|
||||
const action = parts; // 'confirm' | 'edit'
|
||||
const field = parts.length === 3 ? parts[^1] : null; // 'category' | 'counterparty'
|
||||
const transactionId = parseInt(parts[parts.length - 1]);
|
||||
```
|
||||
|
||||
- Определяет, редактировалась ли карточка:
|
||||
|
||||
```javascript
|
||||
const isEditedCard = messageText && messageText.includes('не сохранено');
|
||||
```
|
||||
|
||||
- Парсит текущие значения из текста карточки:
|
||||
|
||||
```javascript
|
||||
const counterpartyMatch = messageText.match(/Контрагент:\s*(.+?)(?:\n|$)/);
|
||||
const categoryMatch = messageText.match(/Категория:\s*(.+?)(?:\n|$)/);
|
||||
```
|
||||
|
||||
- Возвращает:
|
||||
- `action`, `field`, `transactionId`, `chatId`, `messageId`,
|
||||
- `messageText`, `isEditedCard`, `originalCallbackData`,
|
||||
- `counterparty`, `category`.[^1]
|
||||
|
||||
### 5.2. IF: `confirm_or_edit`
|
||||
|
||||
- Condition: `{{ $json.action }}` Equal `confirm`.
|
||||
- TRUE → ветка подтверждения.
|
||||
- FALSE → ветка редактирования.[^1]
|
||||
|
||||
### 5.3. IF: `check_if_edited` (ветка подтверждения)
|
||||
|
||||
- Condition: `{{ $json.isEditedCard }}` Equal `true`.
|
||||
- TRUE → подтверждение после редактирования.
|
||||
- FALSE → первое подтверждение без правок.[^1]
|
||||
|
||||
#### 5.3.1. FALSE (первое подтверждение)
|
||||
|
||||
- PostgreSQL SELECT: `row_select_by_id`
|
||||
- Operation: Select
|
||||
- Table: `transactions`
|
||||
- Where: `id = {{ $json.transactionId }}`
|
||||
- Always Output Data = ON.[^1]
|
||||
- PostgreSQL UPDATE: `update_after_confirm`
|
||||
- Operation: Update
|
||||
- Table: `transactions`
|
||||
- Columns:
|
||||
- `human_verified = true`;
|
||||
- `human_verified_at = {{ $now }}`
|
||||
- Where: `id = {{ $json.transactionId }}`
|
||||
- Always Output Data = ON (на выходе есть строка транзакции).[^1]
|
||||
|
||||
#### 5.3.2. TRUE (подтверждение после редактирования)
|
||||
|
||||
- Code: `parse_card_text`
|
||||
- Парсит `counterparty` и `category` из текста карточки по regex.
|
||||
- Возвращает: `transactionId`, `counterparty`, `category`, `chatId`, `messageId`.[^1]
|
||||
- PostgreSQL UPDATE: `update_edited_transaction`
|
||||
- Operation: Update
|
||||
- Table: `transactions`
|
||||
- Columns:
|
||||
- `counterparty = {{ $json.counterparty }}`;
|
||||
- `category = {{ $json.category }}`;
|
||||
- `human_verified = true`.
|
||||
- Where: `id = {{ $json.transactionId }}`
|
||||
- Always Output Data = ON (строка транзакции).[^1]
|
||||
- Code: `prepare_confirmation_data`
|
||||
|
||||
```javascript
|
||||
const parseData = $('parse_card_text').first().json;
|
||||
const tx = $input.first().json; // результат update_edited_transaction
|
||||
|
||||
return {
|
||||
transactionId: parseData.transactionId,
|
||||
chatId: parseData.chatId,
|
||||
messageId: parseData.messageId,
|
||||
counterparty: parseData.counterparty,
|
||||
category: parseData.category,
|
||||
raw_json: {
|
||||
signed_amount: tx.raw_json.signed_amount,
|
||||
currency: tx.raw_json.currency,
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
### 5.4. Merge: `merge_confirm_paths`
|
||||
|
||||
- Type: **Merge**
|
||||
- Mode: **Append**
|
||||
- Input 1: `update_after_confirm` (первое подтверждение).
|
||||
- Input 2: `prepare_confirmation_data` (после редактирования).[^1]
|
||||
|
||||
Важно: обе ветки несут в себе данные транзакции (`signed_amount`, `currency`, `counterparty`, `category`, `chatId`, `messageId`) к моменту входа в `confirmation_message`.[^1]
|
||||
|
||||
### 5.5. Telegram Answer Callback Query: `popup_confirm`
|
||||
|
||||
- Resource: Callback.
|
||||
- Operation: Answer Callback Query.
|
||||
- Query ID: `{{ $json.callback_query.id }}`
|
||||
- Text: `✅ Транзакция подтверждена и сохранена`.[^1]
|
||||
|
||||
### 5.6. Telegram Send Message: `confirmation_message`
|
||||
|
||||
- Resource: Message.
|
||||
- Operation: Send Message.
|
||||
- Chat ID: `{{ $('answer_trigger').first().json.callback_query.message.chat.id }}`
|
||||
- Text:
|
||||
|
||||
```text
|
||||
✅ Транзакция подтверждена
|
||||
|
||||
Сумма: {{ $json.raw_json.signed_amount }} {{ $json.raw_json.currency }}
|
||||
Контрагент: {{ $json.counterparty }}
|
||||
Категория: {{ $json.category }}
|
||||
Подтверждено: {{ $now.plus({hours: 3}).toFormat('dd.MM HH:mm') }}
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 6. Ветка 4. Редактирование транзакции с циклом
|
||||
|
||||
### 6.1. Построение сессии редактирования
|
||||
|
||||
После `confirm_or_edit`, если `action = 'edit'`, управление идёт в:
|
||||
|
||||
#### 6.1.1. PostgreSQL Execute Query: `empty_session`
|
||||
|
||||
- Удаляет старую сессию и пробрасывает входные данные дальше.
|
||||
- Query:
|
||||
|
||||
```sql
|
||||
DELETE FROM user_sessions WHERE "chatId" = {{ $json.chatId }};
|
||||
|
||||
SELECT
|
||||
'{{ $json.action }}'::text AS "action",
|
||||
'{{ $json.field }}'::text AS "field",
|
||||
{{ $json.transactionId }}::bigint AS "transactionId",
|
||||
{{ $json.chatId }}::bigint AS "chatId",
|
||||
{{ $json.messageId }}::bigint AS "messageId",
|
||||
{{ $json.isEditedCard }}::boolean AS "isEditedCard",
|
||||
'{{ $json.originalCallbackData }}'::text AS "originalCallbackData",
|
||||
'{{ $json.counterparty }}'::text AS "counterparty",
|
||||
'{{ $json.category }}'::text AS "category";
|
||||
```
|
||||
|
||||
#### 6.1.2. IF: `which_field_to_edit`
|
||||
|
||||
- Condition: `{{ $json.field }}` Equal `category`.
|
||||
- TRUE → редактируем категорию.
|
||||
- FALSE → редактируем контрагента.[^1]
|
||||
|
||||
#### 6.1.3. PostgreSQL INSERT: `add_session_category`
|
||||
|
||||
- Table: `user_sessions`
|
||||
- Columns:
|
||||
- `chatId` = `{{ $json.chatId }}`
|
||||
- `transactionId` = `{{ $json.transactionId }}`
|
||||
- `waitingFor` = `category`
|
||||
- `tempCounterparty` = `{{ $json.counterparty }}`
|
||||
- `tempCategory` = `{{ $json.category }}`.[^1]
|
||||
|
||||
#### 6.1.4. PostgreSQL INSERT: `add_session_counterparty`
|
||||
|
||||
- Аналогично, но `waitingFor = 'counterparty'`.[^1]
|
||||
|
||||
### 6.2. Запрос на ввод значения
|
||||
|
||||
#### 6.2.1. Telegram Send Message: `category_edit`
|
||||
|
||||
- Chat ID: `{{ $json.chatId }}`
|
||||
- Text:
|
||||
|
||||
```text
|
||||
Введите новую категорию для транзакции #{{ $json.transactionId }}
|
||||
|
||||
ВАЖНО: Лимит 12 символов
|
||||
```
|
||||
|
||||
#### 6.2.2. Telegram Send Message: `counterparty_edit`
|
||||
|
||||
- Chat ID: `{{ $json.chatId }}`
|
||||
- Text:
|
||||
|
||||
```text
|
||||
Введите нового контрагента для транзакции #{{ $json.transactionId }}
|
||||
|
||||
ВАЖНО: Лимит 12 символов
|
||||
```
|
||||
|
||||
***
|
||||
|
||||
## 7. Ветка 5. Обработка текстового ответа пользователя
|
||||
|
||||
### 7.1. answer_trigger → split_event_type (TRUE)
|
||||
|
||||
Приходит `message` от пользователя (без `callback_query`).
|
||||
TRUE‑ветка `split_event_type` ведёт в `get_session`.[^1]
|
||||
|
||||
### 7.2. PostgreSQL SELECT: `get_session`
|
||||
|
||||
- Operation: Select
|
||||
- Table: `user_sessions`
|
||||
- Where: `chatId = {{ $json.message.chat.id }}`
|
||||
- Always Output Data = ON.[^1]
|
||||
|
||||
Возвращает:
|
||||
|
||||
```json
|
||||
{
|
||||
"chatId": "...",
|
||||
"transactionId": "...",
|
||||
"waitingFor": "category" | "counterparty",
|
||||
"tempCounterparty": "...",
|
||||
"tempCategory": "...",
|
||||
"createdAt": "..."
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3. Code: `merge_user_and_session`
|
||||
|
||||
```javascript
|
||||
const sessionData = $input.first().json;
|
||||
const userMessage = $('split_event_type').first().json;
|
||||
|
||||
return {
|
||||
message: userMessage.message,
|
||||
session: sessionData
|
||||
};
|
||||
```
|
||||
|
||||
### 7.4. Code: `parse_text_input`
|
||||
|
||||
```javascript
|
||||
const userText = $input.first().json.message.text;
|
||||
const chatId = $input.first().json.message.chat.id;
|
||||
|
||||
// Игнорируем команды
|
||||
if (userText.startsWith('/')) {
|
||||
throw new Error('Команда проигнорирована');
|
||||
}
|
||||
|
||||
const sessionData = $input.first().json.session;
|
||||
|
||||
if (!sessionData || !sessionData.transactionId) {
|
||||
throw new Error('Сессия не найдена. Начните редактирование заново из карточки транзакции.');
|
||||
}
|
||||
|
||||
return {
|
||||
newValue: userText.trim(),
|
||||
chatId: chatId,
|
||||
transactionId: parseInt(sessionData.transactionId),
|
||||
changedField: sessionData.waitingFor // 'category' | 'counterparty'
|
||||
};
|
||||
```
|
||||
|
||||
### 7.5. PostgreSQL SELECT: `get_current_transaction`
|
||||
|
||||
- Operation: Select
|
||||
- Table: `transactions`
|
||||
- Where: `id = {{ $json.transactionId }}`
|
||||
- Always Output Data = ON.[^1]
|
||||
|
||||
### 7.6. Code: `merge_changes`
|
||||
|
||||
```javascript
|
||||
const textInput = $('parse_text_input').first().json;
|
||||
const sessionData = $('get_session').first().json;
|
||||
const currentData = $input.first().json;
|
||||
|
||||
const tempCategory =
|
||||
sessionData?.tempCategory != null ? sessionData.tempCategory : currentData.category;
|
||||
|
||||
const tempCounterparty =
|
||||
sessionData?.tempCounterparty != null ? sessionData.tempCounterparty : currentData.counterparty;
|
||||
|
||||
const updatedCategory =
|
||||
textInput.changedField === 'category'
|
||||
? textInput.newValue
|
||||
: tempCategory;
|
||||
|
||||
const updatedCounterparty =
|
||||
textInput.changedField === 'counterparty'
|
||||
? textInput.newValue
|
||||
: tempCounterparty;
|
||||
|
||||
return {
|
||||
...currentData,
|
||||
category: updatedCategory,
|
||||
counterparty: updatedCounterparty,
|
||||
chatId: textInput.chatId
|
||||
};
|
||||
```
|
||||
|
||||
### 7.7. Telegram Send Message: `show_updated_card`
|
||||
|
||||
- Chat ID: `{{ $json.chatId }}`
|
||||
- Text:
|
||||
|
||||
```text
|
||||
Данные обновлены (не сохранено)
|
||||
|
||||
Сумма: {{ $json.signed_amount }} {{ $json.currency }}
|
||||
Контрагент: {{ $json.counterparty }}
|
||||
Категория: {{ $json.category }}
|
||||
Дата: {{ $json.received_at }}
|
||||
|
||||
ВАЖНО: Лимит 12 символов на поле
|
||||
```
|
||||
|
||||
- Inline Keyboard:
|
||||
- `✅ Подтвердить` → `confirm_{{ $json.id }}`
|
||||
- `✏️ Изменить категорию` → `edit_category_{{ $json.id }}`
|
||||
- `✏️ Изменить контрагента` → `edit_counterparty_{{ $json.id }}`.[^1]
|
||||
|
||||
***
|
||||
|
||||
## 8. Интеграция с Google Sheets
|
||||
|
||||
### 8.1. Структура таблицы
|
||||
|
||||
- Отдельный документ семейного бюджета в Google Sheets.
|
||||
- Лист, например `Transactions`.
|
||||
- Колонки:
|
||||
|
||||
1. дата
|
||||
2. контрагент
|
||||
3. сумма
|
||||
4. категория
|
||||
5. примечание (заполняется пользователем вручную).
|
||||
|
||||
### 8.2. Доступ через Google Service Account
|
||||
|
||||
- В Google Cloud проекте `budget` создан сервисный аккаунт.
|
||||
- Включены API: Google Sheets API, Google Drive API.
|
||||
- Скачан JSON‑ключ сервисного аккаунта; в n8n созданы credentials типа **Google Service Account**, использован весь JSON или поля `client_email` + `private_key`
|
||||
- В самой таблице сервисный аккаунт добавлен как редактор по `client_email`.
|
||||
|
||||
### 8.3. Узел Google Sheets после `merge_confirm_paths`
|
||||
|
||||
- После ноды `merge_confirm_paths` добавлен узел **Google Sheets** (v2).
|
||||
- Credentials: Google Service Account.
|
||||
- Resource: `Sheet Within Document`.
|
||||
- Operation: `Append Row`.
|
||||
- Document: семейный бюджет.
|
||||
- Sheet: лист с транзакциями (например, `Transactions`).
|
||||
|
||||
**Mapping колонок (Map Each Column Manually):**
|
||||
|
||||
- `дата` → `{{ $json.received_at }}`
|
||||
- `контрагент` → `{{ $json.counterparty }}`
|
||||
- `сумма` → `{{ $json.raw_json?.signed_amount ?? $json.signed_amount }}`
|
||||
- `категория` → `{{ $json.category }}`
|
||||
- `примечание` → `""` (пусто, заполняется человеком).
|
||||
|
||||
**Фильтрация только подтверждённых транзакций:**
|
||||
|
||||
- Ветка к Google Sheets подключена после подтверждения (`update_after_confirm` / `update_edited_transaction` через `merge_confirm_paths`), где `human_verified = true` гарантирован.
|
||||
|
||||
***
|
||||
|
||||
## 9. Планируемые доработки
|
||||
|
||||
### 9.1. LLM / категории (Приоритет 1)
|
||||
|
||||
- [ ] Собрать статистику по качеству автокатегоризации и при необходимости скорректировать описания категорий/словарь.
|
||||
|
||||
### 9.2. Автоматизация запуска (Приоритет 2)
|
||||
|
||||
- [ ] Автозапуск LM Studio при старте системы (service/systemd).
|
||||
- [ ] Docker `restart: always` для n8n и PostgreSQL.
|
||||
- [ ] Health‑check LM Studio API и уведомления в Telegram при падении.
|
||||
|
||||
### 9.3. Улучшения и UX (Приоритет 3)
|
||||
|
||||
- [ ] Кнопка «❌ Отклонить» для удаления/игнорирования ошибочных транзакций.
|
||||
- [ ] Ограничения и валидация пользовательского ввода (длина, спецсимволы).
|
||||
- [ ] Периодическая очистка старых записей в `user_sessions` (например, старше 1 часа).
|
||||
- [ ] Добавление исходного текста SMS в карточку (с безопасным форматированием или экранированием).
|
||||
- [ ] Обработка исторических данных (2025 год и далее) для накопления статистики.
|
||||
- [ ] Fine‑tuning модели LLM на собственных размеченных данных.
|
||||
- [ ] Аналитика по категориям, отчёты и экспорт (CSV/Excel/BI).
|
||||
|
||||
***
|
||||
|
||||
**Версия документа:** 4.2
|
||||
**Дата:** 2026‑01‑09
|
||||
**Статус:** Архитектура workflow обновлена: LLM работает с фиксированным набором категорий, подтверждённые транзакции пишутся в Google Sheets, дальнейшая работа — автоматизация запуска и улучшения UX.
|
||||
Reference in New Issue
Block a user