feat(n8n): RAG workflow для извлечения данных из документов
- Обновлён Code1: нормализация данных из give_data1 с поддержкой payload.applicant, ai_analysis, wizard_plan - Обновлён Code6: генерация промптов для RAG (user, project, offenders) - Добавлена документация по настройке n8n нод для OCR статуса - Добавлен эндпоинт check-ocr-status в documents.py - Добавлен лог сессии с полным описанием workflow Workflow itX62h38faB51y9J успешно извлекает: - Данные пользователя (ФИО, контакты, адрес) - Данные проекта (сумма, предмет, даты договора) - Несколько контрагентов с разными ролями (seller, service_provider)
This commit is contained in:
433
ticket_form/docs/SESSION_LOG_2025-11-29_RAG_WORKFLOW.md
Normal file
433
ticket_form/docs/SESSION_LOG_2025-11-29_RAG_WORKFLOW.md
Normal file
@@ -0,0 +1,433 @@
|
||||
# Лог сессии: Настройка RAG workflow для извлечения данных
|
||||
**Дата:** 2025-11-29
|
||||
**Workflow ID:** `itX62h38faB51y9J` ("6 ocr_check:attempt")
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Цель сессии
|
||||
|
||||
Настроить workflow для автоматического извлечения данных из документов с использованием RAG (Retrieval-Augmented Generation) для заполнения формы заявления.
|
||||
|
||||
---
|
||||
|
||||
## 📊 Структура workflow
|
||||
|
||||
```
|
||||
ocr_check:attempt (Redis Trigger)
|
||||
↓
|
||||
clime_id (Set) → извлекает claim_id, session_id
|
||||
↓
|
||||
analiz (Set) → добавляет prefix, session_token
|
||||
↓
|
||||
give_data1 (PostgreSQL) → большой SQL, собирает все данные
|
||||
↓
|
||||
Code1 (Code) → нормализует данные
|
||||
↓
|
||||
prepare_rag_items (Code) → создаёт 3 items: user, project, offenders
|
||||
↓
|
||||
Loop Over Items → итерация по типам
|
||||
↓
|
||||
Code6 (Code) → генерация промптов для AI
|
||||
↓
|
||||
AI Agent2 (LLM + RAG) → извлечение данных из документов
|
||||
↓
|
||||
Code5 (Code) → парсинг JSON из LLM
|
||||
↓
|
||||
Edit Fields4 → извлекает output
|
||||
↓
|
||||
Aggregate → собирает все результаты
|
||||
↓
|
||||
dataset (Set) → финальная сборка
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 📝 Обновлённые ноды
|
||||
|
||||
### 1. Code1 — нормализация данных из give_data1
|
||||
|
||||
```javascript
|
||||
// Code1 — нормализация данных из give_data1
|
||||
// ИСПРАВЛЕНО: извлекаем payload.applicant, ai_analysis, wizard_plan, полные documents
|
||||
|
||||
function toNullish(v) {
|
||||
if (v === undefined || v === null) return null;
|
||||
if (typeof v === 'string' && v.trim() === '') return null;
|
||||
return v;
|
||||
}
|
||||
|
||||
function pick(...vals) {
|
||||
return vals.find(v => v !== undefined && v !== null && v !== '') ?? null;
|
||||
}
|
||||
|
||||
function mapDocuments(docs = []) {
|
||||
if (!docs || !Array.isArray(docs)) return [];
|
||||
return docs.map(d => ({
|
||||
id: toNullish(d.id),
|
||||
claim_document_id: toNullish(d.id),
|
||||
file_id: toNullish(d.file_id),
|
||||
file_url: toNullish(d.file_url),
|
||||
file_name: toNullish(d.file_name),
|
||||
original_file_name: toNullish(d.original_file_name),
|
||||
field_name: toNullish(d.field_name),
|
||||
uploaded_at: toNullish(d.uploaded_at),
|
||||
filename_for_upload: toNullish(d.filename_for_upload),
|
||||
// AI данные
|
||||
document_type: toNullish(d.document_type),
|
||||
document_label: toNullish(d.document_label),
|
||||
document_summary: toNullish(d.document_summary),
|
||||
ocr_status: toNullish(d.ocr_status),
|
||||
match_score: toNullish(d.match_score),
|
||||
match_status: toNullish(d.match_status),
|
||||
match_reason: toNullish(d.match_reason),
|
||||
}));
|
||||
}
|
||||
|
||||
function mapCombinedDocs(cds = []) {
|
||||
if (!cds || !Array.isArray(cds)) return [];
|
||||
return cds.map(c => ({
|
||||
claim_document_id: toNullish(c.claim_document_id),
|
||||
combined_document_id: toNullish(c.combined_document_id),
|
||||
pages: toNullish(c.pages),
|
||||
combined_text: toNullish(c.combined_text),
|
||||
}));
|
||||
}
|
||||
|
||||
function normalizeOne(src) {
|
||||
const claim = src.claim ?? {};
|
||||
const payload = claim.payload ?? {};
|
||||
const userInfo = src.user_info ?? {};
|
||||
|
||||
// Извлекаем applicant из payload
|
||||
const applicant = payload.applicant ?? {};
|
||||
const aiAnalysis = payload.ai_analysis ?? {};
|
||||
const answersPrefill = payload.answers_prefill ?? [];
|
||||
const wizardPlan = payload.wizard_plan ?? null;
|
||||
|
||||
// USER: приоритет payload.applicant
|
||||
const user = {
|
||||
firstname: pick(applicant.firstname, applicant.first_name),
|
||||
secondname: pick(applicant.middle_name, applicant.secondname),
|
||||
lastname: pick(applicant.lastname, applicant.last_name),
|
||||
mobile: pick(payload.phone),
|
||||
email: pick(payload.email),
|
||||
tgid: pick(claim.telegram_id, payload.tg_id),
|
||||
birthday: pick(applicant.birthday, applicant.birth_date),
|
||||
birthplace: pick(applicant.birthplace, applicant.birth_place),
|
||||
mailingstreet: pick(applicant.address),
|
||||
inn: pick(applicant.inn),
|
||||
zip: pick(applicant.zip),
|
||||
channel: pick(userInfo.channel, claim.channel),
|
||||
unified_id: pick(claim.unified_id),
|
||||
session_token: pick(claim.session_token),
|
||||
};
|
||||
|
||||
// CASE
|
||||
const caseData = {
|
||||
id: toNullish(claim.id),
|
||||
prefix: toNullish(claim.prefix),
|
||||
channel: toNullish(claim.channel),
|
||||
type_code: toNullish(claim.type_code),
|
||||
status_code: toNullish(claim.status_code),
|
||||
created_at: toNullish(claim.created_at),
|
||||
updated_at: toNullish(claim.updated_at),
|
||||
telegram_id: toNullish(claim.telegram_id),
|
||||
session_token: toNullish(claim.session_token),
|
||||
unified_id: toNullish(claim.unified_id),
|
||||
case_type: pick(wizardPlan?.case_type, claim.type_code),
|
||||
};
|
||||
|
||||
// ANSWERS
|
||||
const answers = {};
|
||||
if (Array.isArray(answersPrefill)) {
|
||||
answersPrefill.forEach(a => {
|
||||
if (a?.name && a?.value !== undefined) {
|
||||
answers[a.name] = a.value;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// AI_ANALYSIS
|
||||
const ai = {
|
||||
problem: toNullish(aiAnalysis.problem),
|
||||
facts_short: toNullish(aiAnalysis.facts_short),
|
||||
facts_full: toNullish(aiAnalysis.facts_full),
|
||||
recommendation: toNullish(aiAnalysis.recommendation),
|
||||
};
|
||||
|
||||
const problemDescription = toNullish(payload.problem_description);
|
||||
|
||||
return {
|
||||
case: caseData,
|
||||
user,
|
||||
answers: Object.keys(answers).length ? answers : null,
|
||||
answers_prefill: answersPrefill.length ? answersPrefill : null,
|
||||
ai_analysis: ai,
|
||||
problem_description: problemDescription,
|
||||
documents: mapDocuments(src.documents),
|
||||
combined_docs: mapCombinedDocs(src.combined_docs),
|
||||
wizard_plan: wizardPlan,
|
||||
meta: {
|
||||
claim_id: caseData.id,
|
||||
session_token: caseData.session_token,
|
||||
unified_id: caseData.unified_id,
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const raw = items[0]?.json ?? {};
|
||||
const arr = Array.isArray(raw) ? raw : [raw];
|
||||
const results = arr.map(normalizeOne).map(obj => ({ json: obj }));
|
||||
return results.length ? results : [{ json: null }];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Code6 — генерация промптов для RAG
|
||||
|
||||
```javascript
|
||||
// n8n Code node: Генерация prompt'а под конкретный тип
|
||||
// ВХОД: { type: 'user'|'project'|'offenders', data: {...} }
|
||||
|
||||
const type = $json.type;
|
||||
const data = $json.data;
|
||||
|
||||
const code1Data = (() => {
|
||||
try {
|
||||
return $('Code1').first().json || {};
|
||||
} catch(_) {
|
||||
return {};
|
||||
}
|
||||
})();
|
||||
|
||||
const aiAnalysis = code1Data.ai_analysis || {};
|
||||
const problemDescription = code1Data.problem_description || '';
|
||||
const wizardPlan = code1Data.wizard_plan || {};
|
||||
const caseType = wizardPlan.case_type || code1Data.case?.type_code || 'consumer';
|
||||
|
||||
let schema = '';
|
||||
let searchHints = '';
|
||||
let contextInfo = '';
|
||||
|
||||
contextInfo = `
|
||||
КОНТЕКСТ ДЕЛА:
|
||||
- Тип: ${caseType}
|
||||
- Проблема: ${aiAnalysis.problem || 'не указана'}
|
||||
- Краткие факты: ${aiAnalysis.facts_short || 'не указаны'}
|
||||
`;
|
||||
|
||||
if (type === 'user') {
|
||||
schema = `{
|
||||
"user": {
|
||||
"firstname": string|null,
|
||||
"secondname": string|null,
|
||||
"lastname": string|null,
|
||||
"mobile": string|null,
|
||||
"email": string|null,
|
||||
"tgid": number|null,
|
||||
"birthday": "YYYY-MM-DD"|null,
|
||||
"birthplace": string|null,
|
||||
"mailingstreet": string|null,
|
||||
"inn": string|null (12 цифр для физлица)
|
||||
}
|
||||
}`;
|
||||
|
||||
searchHints = `Ищи данные ПОКУПАТЕЛЯ/ЗАКАЗЧИКА:
|
||||
- ФИО: после "Покупатель:", "Заказчик:", "Потребитель:"
|
||||
- Адрес: "адрес регистрации", "адрес проживания", "место жительства"
|
||||
- ИНН физлица = 12 цифр
|
||||
- Телефон: в реквизитах, после "тел:", "моб:"
|
||||
- Email: в реквизитах`;
|
||||
|
||||
} else if (type === 'project') {
|
||||
schema = `{
|
||||
"project": {
|
||||
"category": string|null (тема обращения),
|
||||
"direction": string|null,
|
||||
"agrprice": number|null (сумма в рублях, только цифры!),
|
||||
"subject": string|null (предмет договора - что купили/заказали),
|
||||
"agrdate": "YYYY-MM-DD"|null (дата заключения договора),
|
||||
"startdate": "YYYY-MM-DD"|null (дата начала услуги/поездки),
|
||||
"finishdate": "YYYY-MM-DD"|null (дата окончания),
|
||||
"country": string|null (страна для турпутёвок),
|
||||
"hotel": string|null (название отеля),
|
||||
"transport": "да"|"нет"|null (включён ли трансфер),
|
||||
"insurance": "да"|"нет"|null (включена ли страховка),
|
||||
"description": string|null (краткое описание сделки)
|
||||
}
|
||||
}`;
|
||||
|
||||
searchHints = `Ищи данные ДОГОВОРА/СДЕЛКИ:
|
||||
- Сумма: "Цена договора", "Стоимость", "Итого к оплате", "Сумма заказа"
|
||||
- Дата: "Дата заключения", "Договор № ... от ...", "Заказ от ..."
|
||||
- Предмет: что именно купили или заказали (товар, услуга, тур)
|
||||
- Для туров: страна, отель, даты заезда/выезда`;
|
||||
|
||||
} else if (type === 'offenders') {
|
||||
schema = `{
|
||||
"offenders": [
|
||||
{
|
||||
"role": "seller"|"service_provider"|"tour_agent"|"tour_operator"|"delivery"|"installer"|"intermediary"|null,
|
||||
"accountname": string|null (название организации или ФИО ИП),
|
||||
"address": string|null (юридический адрес),
|
||||
"email": string|null,
|
||||
"website": string|null,
|
||||
"phone": string|null,
|
||||
"inn": string|null (10 цифр для юрлица, 12 для ИП),
|
||||
"ogrn": string|null (13 цифр ОГРН или 15 цифр ОГРНИП)
|
||||
}
|
||||
]
|
||||
}`;
|
||||
|
||||
searchHints = `Ищи данные ВСЕХ КОНТРАГЕНТОВ (может быть несколько!):
|
||||
|
||||
ГДЕ ИСКАТЬ:
|
||||
- "Продавец:", "Исполнитель:", "Поставщик:"
|
||||
- После ООО, ИП, ЗАО, ОАО, ПАО, АО
|
||||
- В реквизитах договора, в шапке чека
|
||||
|
||||
РЕКВИЗИТЫ:
|
||||
- ИНН юрлица = 10 цифр, ИП = 12 цифр
|
||||
- ОГРН = 13 цифр, ОГРНИП = 15 цифр
|
||||
|
||||
РОЛИ (определи по контексту):
|
||||
- seller — продавец товара (магазин, салон)
|
||||
- service_provider — исполнитель услуги
|
||||
- tour_agent — турагент (кто продал путёвку)
|
||||
- tour_operator — туроператор (кто организует тур, указан в договоре отдельно)
|
||||
- delivery — служба доставки
|
||||
- installer — сборщик/установщик
|
||||
- intermediary — посредник, маркетплейс
|
||||
|
||||
ВАЖНО: Если в документах несколько организаций — добавь всех!`;
|
||||
}
|
||||
|
||||
const filledCount = Object.values(data || {}).filter(v => v !== null && v !== undefined && v !== '').length;
|
||||
const totalCount = Object.keys(data || {}).length;
|
||||
|
||||
return [{
|
||||
json: {
|
||||
systemMessage: `Ты — юридический помощник-экстрактор. У тебя есть инструмент vectorStore для поиска по документам.
|
||||
|
||||
${contextInfo}
|
||||
|
||||
${searchHints}
|
||||
|
||||
ПРАВИЛА:
|
||||
1. Ищи только поля из схемы ниже
|
||||
2. Возвращай строго JSON в указанном формате
|
||||
3. Если данные не найдены — ставь null
|
||||
4. НЕ ПРИДУМЫВАЙ данные!
|
||||
5. Дозаполняй только пустые/null поля`,
|
||||
|
||||
userMessage: `Текущие данные (заполнено ${filledCount} из ${totalCount}, дозаполни остальное):
|
||||
${JSON.stringify(data, null, 2)}
|
||||
|
||||
Схема для ответа:
|
||||
${schema}`,
|
||||
|
||||
_meta: {
|
||||
type,
|
||||
filledCount,
|
||||
totalCount,
|
||||
caseType
|
||||
}
|
||||
}
|
||||
}];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3. prepare_rag_items — создание 3 items для RAG (НУЖНО ДОБАВИТЬ!)
|
||||
|
||||
```javascript
|
||||
// Code node: prepare_rag_items
|
||||
// Создаёт 3 items для RAG: user, project, offenders
|
||||
// Вставить между Code1 и Loop Over Items
|
||||
|
||||
const src = $('Code1').first().json;
|
||||
|
||||
// USER — из уже собранных данных Code1
|
||||
const userData = src.user || {};
|
||||
|
||||
// PROJECT — пока пустой, RAG заполнит
|
||||
const projectData = {
|
||||
category: null,
|
||||
direction: null,
|
||||
agrprice: null,
|
||||
subject: null,
|
||||
agrdate: null,
|
||||
startdate: null,
|
||||
finishdate: null,
|
||||
country: null,
|
||||
hotel: null,
|
||||
transport: null,
|
||||
insurance: null,
|
||||
description: src.problem_description || null,
|
||||
};
|
||||
|
||||
// OFFENDERS — пока пустой массив, RAG найдёт
|
||||
const offendersData = [];
|
||||
|
||||
// Выдаём 3 items для Loop Over Items
|
||||
return [
|
||||
{ json: { type: 'user', data: userData } },
|
||||
{ json: { type: 'project', data: projectData } },
|
||||
{ json: { type: 'offenders', data: offendersData } }
|
||||
];
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Результат работы workflow
|
||||
|
||||
Тестовый запуск на claim `509872e2-9666-4c5e-8ab7-2304dd6a5d18`:
|
||||
|
||||
### USER — полностью заполнен
|
||||
- firstname: Федор
|
||||
- secondname: Владимирович
|
||||
- lastname: Коробков
|
||||
- mobile: 79262306381
|
||||
- email: help@clientright.ru
|
||||
- tgid: 295410106
|
||||
- birthday: 1981-09-18
|
||||
- birthplace: Москва
|
||||
- mailingstreet: МО, г. Балашиха, мкр. Железнодорожный, ул. Советская, д.20, кв. 52
|
||||
|
||||
### PROJECT — основное заполнено
|
||||
- category: задержка ремонта/недоставка комплектующих и отказ в оказании услуги сборки
|
||||
- agrprice: **89620** (сумма договора)
|
||||
- subject: кровать-подиум Hemwood Base 180х200 и тумбы к ней
|
||||
- agrdate: **2025-08-09** (дата договора)
|
||||
- startdate: **2025-08-16** (дата доставки)
|
||||
- description: полное описание проблемы
|
||||
|
||||
### OFFENDERS — найдено 2 контрагента!
|
||||
|
||||
**1. Продавец (seller):**
|
||||
- accountname: ИП Хациев Зелимхан Зелимханович
|
||||
- inn: 201471261963 (12 цифр — ИП ✅)
|
||||
- ogrn: 315774600000123 (15 цифр — ОГРНИП ✅)
|
||||
- website: raiton.ru
|
||||
|
||||
**2. Исполнитель услуг (service_provider):**
|
||||
- accountname: АО «ОРМАТЕК»
|
||||
- inn: 7724890784 (10 цифр — юрлицо ✅)
|
||||
- email: kassa@ormatek.com
|
||||
|
||||
---
|
||||
|
||||
## 📋 TODO (следующие шаги)
|
||||
|
||||
1. [ ] Добавить ноду `prepare_rag_items` между Code1 и Loop Over Items
|
||||
2. [ ] Добавить постобработку данных (валидация, исправление ошибок AI)
|
||||
3. [ ] Сохранение результата в Redis для формы
|
||||
4. [ ] Подключить к генерации формы заявления
|
||||
|
||||
---
|
||||
|
||||
## 📁 Связанные файлы
|
||||
|
||||
- `ticket_form/docs/SQL_CLAIMSAVE_FINAL_FIXED_NEW_FLOW_WITH_UPLOADED_FIXED.sql` — SQL для сохранения документов
|
||||
- `ticket_form/frontend/src/components/form/generateConfirmationFormHTML.ts` — шаблон формы заявления
|
||||
|
||||
99
ticket_form/docs/n8n_nodes/README_SETUP.md
Normal file
99
ticket_form/docs/n8n_nodes/README_SETUP.md
Normal file
@@ -0,0 +1,99 @@
|
||||
# Настройка OCR Status Tracking в n8n
|
||||
|
||||
## Шаги для добавления нод в workflow `fnSo3FTTbQcMjwt3`
|
||||
|
||||
### 1. Открой workflow в n8n
|
||||
https://n8n.clientright.pro/workflow/fnSo3FTTbQcMjwt3
|
||||
|
||||
### 2. Добавь ноды в следующем порядке:
|
||||
|
||||
#### Нода 1: `update_ocr_status` (PostgreSQL)
|
||||
**Расположение:** После `Postgres PGVector Store1`
|
||||
**Позиция:** [3850, 1664]
|
||||
|
||||
**SQL запрос:**
|
||||
```sql
|
||||
UPDATE clpr_claim_documents
|
||||
SET
|
||||
ocr_status = 'ready',
|
||||
ocr_processed_at = NOW()
|
||||
WHERE id = '{{ $('files').item.json.claim_document_id }}'::uuid
|
||||
RETURNING
|
||||
id AS doc_id,
|
||||
claim_id,
|
||||
ocr_status,
|
||||
(SELECT COUNT(*) FROM clpr_claim_documents WHERE claim_id = clpr_claim_documents.claim_id) AS total_docs,
|
||||
(SELECT COUNT(*) FROM clpr_claim_documents WHERE claim_id = clpr_claim_documents.claim_id AND ocr_status = 'ready') AS ready_docs;
|
||||
```
|
||||
|
||||
**Credentials:** `timeweb_bd` (Postgres account 2)
|
||||
|
||||
---
|
||||
|
||||
#### Нода 2: `redis_incr_ready` (Redis)
|
||||
**Расположение:** После `update_ocr_status`
|
||||
**Позиция:** [4100, 1664]
|
||||
|
||||
**Параметры:**
|
||||
- Operation: `incr`
|
||||
- Key: `claim:ocr:ready:{{ $json.claim_id }}`
|
||||
|
||||
**Credentials:** `Redis account`
|
||||
|
||||
---
|
||||
|
||||
#### Нода 3: `check_all_ready` (IF)
|
||||
**Расположение:** После `redis_incr_ready`
|
||||
**Позиция:** [4350, 1664]
|
||||
|
||||
**Условие:**
|
||||
```
|
||||
{{ $('update_ocr_status').item.json.total_docs }} == {{ $('update_ocr_status').item.json.ready_docs }}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
#### Нода 4: `publish_docs_ready` (Redis)
|
||||
**Расположение:** TRUE выход из `check_all_ready`
|
||||
**Позиция:** [4600, 1550]
|
||||
|
||||
**Параметры:**
|
||||
- Operation: `publish`
|
||||
- Channel: `clpr:claim:docs_ready`
|
||||
- Message:
|
||||
```javascript
|
||||
{{ JSON.stringify({
|
||||
claim_id: $('update_ocr_status').item.json.claim_id,
|
||||
total_docs: $('update_ocr_status').item.json.total_docs,
|
||||
ready_docs: $('update_ocr_status').item.json.ready_docs,
|
||||
timestamp: new Date().toISOString()
|
||||
}) }}
|
||||
```
|
||||
|
||||
**Credentials:** `Redis account`
|
||||
|
||||
---
|
||||
|
||||
### 3. Соединения (Connections)
|
||||
|
||||
```
|
||||
Postgres PGVector Store1 → update_ocr_status
|
||||
update_ocr_status → redis_incr_ready
|
||||
redis_incr_ready → check_all_ready
|
||||
check_all_ready (TRUE) → publish_docs_ready
|
||||
check_all_ready (FALSE) → (конец)
|
||||
```
|
||||
|
||||
### 4. Сохрани и активируй workflow
|
||||
|
||||
---
|
||||
|
||||
## Проверка
|
||||
|
||||
После добавления нод, при обработке документа:
|
||||
1. Статус в `clpr_claim_documents.ocr_status` будет меняться на `'ready'`
|
||||
2. Счётчик в Redis `claim:ocr:ready:{claim_id}` будет инкрементиться
|
||||
3. Когда все документы готовы, событие `clpr:claim:docs_ready` будет опубликовано в Redis
|
||||
|
||||
|
||||
|
||||
32
ticket_form/docs/n8n_nodes/check_all_ready.json
Normal file
32
ticket_form/docs/n8n_nodes/check_all_ready.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "check_all_ready",
|
||||
"type": "n8n-nodes-base.if",
|
||||
"typeVersion": 2.2,
|
||||
"position": [4350, 1664],
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict",
|
||||
"version": 2
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "ocr-ready-check-001",
|
||||
"leftValue": "={{ $('update_ocr_status').item.json.total_docs }}",
|
||||
"rightValue": "={{ $('update_ocr_status').item.json.ready_docs }}",
|
||||
"operator": {
|
||||
"type": "number",
|
||||
"operation": "equals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
20
ticket_form/docs/n8n_nodes/publish_docs_ready.json
Normal file
20
ticket_form/docs/n8n_nodes/publish_docs_ready.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "publish_docs_ready",
|
||||
"type": "n8n-nodes-base.redis",
|
||||
"typeVersion": 1,
|
||||
"position": [4600, 1550],
|
||||
"parameters": {
|
||||
"operation": "publish",
|
||||
"channel": "clpr:claim:docs_ready",
|
||||
"messageData": "={{ JSON.stringify({ claim_id: $('update_ocr_status').item.json.claim_id, total_docs: $('update_ocr_status').item.json.total_docs, ready_docs: $('update_ocr_status').item.json.ready_docs, timestamp: new Date().toISOString() }) }}"
|
||||
},
|
||||
"credentials": {
|
||||
"redis": {
|
||||
"id": "F2IkIEYT9O7UTtz9",
|
||||
"name": "Redis account"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
19
ticket_form/docs/n8n_nodes/redis_incr_ready.json
Normal file
19
ticket_form/docs/n8n_nodes/redis_incr_ready.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"name": "redis_incr_ready",
|
||||
"type": "n8n-nodes-base.redis",
|
||||
"typeVersion": 1,
|
||||
"position": [4100, 1664],
|
||||
"parameters": {
|
||||
"operation": "incr",
|
||||
"key": "=claim:ocr:ready:{{ $json.claim_id }}"
|
||||
},
|
||||
"credentials": {
|
||||
"redis": {
|
||||
"id": "F2IkIEYT9O7UTtz9",
|
||||
"name": "Redis account"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
21
ticket_form/docs/n8n_nodes/update_ocr_error.json
Normal file
21
ticket_form/docs/n8n_nodes/update_ocr_error.json
Normal file
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"name": "update_ocr_error",
|
||||
"type": "n8n-nodes-base.postgres",
|
||||
"typeVersion": 2.6,
|
||||
"position": [3850, 1850],
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "-- Обновляем статус OCR при ошибке\nUPDATE clpr_claim_documents\nSET \n ocr_status = 'error',\n ocr_error = '{{ $json.error || \"OCR processing failed\" }}',\n ocr_processed_at = NOW()\nWHERE id = '{{ $('files').item.json.claim_document_id }}'::uuid\nRETURNING id, claim_id, ocr_status, ocr_error;",
|
||||
"options": {}
|
||||
},
|
||||
"credentials": {
|
||||
"postgres": {
|
||||
"id": "sGJ0fJhU8rz88w3k",
|
||||
"name": "timeweb_bd"
|
||||
}
|
||||
},
|
||||
"onError": "continueRegularOutput"
|
||||
}
|
||||
|
||||
|
||||
|
||||
20
ticket_form/docs/n8n_nodes/update_ocr_status.json
Normal file
20
ticket_form/docs/n8n_nodes/update_ocr_status.json
Normal file
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"name": "update_ocr_status",
|
||||
"type": "n8n-nodes-base.postgres",
|
||||
"typeVersion": 2.6,
|
||||
"position": [3850, 1664],
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "-- Обновляем статус OCR для документа и возвращаем счётчики\nUPDATE clpr_claim_documents\nSET \n ocr_status = 'ready',\n ocr_processed_at = NOW()\nWHERE id = '{{ $('files').item.json.claim_document_id }}'::uuid\nRETURNING \n id AS doc_id,\n claim_id,\n ocr_status,\n (SELECT COUNT(*) FROM clpr_claim_documents WHERE claim_id = clpr_claim_documents.claim_id) AS total_docs,\n (SELECT COUNT(*) FROM clpr_claim_documents WHERE claim_id = clpr_claim_documents.claim_id AND ocr_status = 'ready') AS ready_docs;",
|
||||
"options": {}
|
||||
},
|
||||
"credentials": {
|
||||
"postgres": {
|
||||
"id": "sGJ0fJhU8rz88w3k",
|
||||
"name": "timeweb_bd"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user