Добавлен field_label в результат переименования файлов, исправлена загрузка черновиков, обновлен формат пути S3 с project_name
This commit is contained in:
@@ -208,3 +208,4 @@ $2 = {{ $json.claim_id }} (TEXT, строка "CLM-2025-11-18-GEQ3K
|
||||
|
||||
Оба запроса теперь используют строковый `claim_id` и правильно находят UUID.
|
||||
|
||||
|
||||
|
||||
@@ -101,3 +101,4 @@ function mapCombinedDocs(cds = []) {
|
||||
|
||||
Но для `mapDialogHistory` это критично, т.к. она вызывается первой и падает.
|
||||
|
||||
|
||||
|
||||
@@ -210,3 +210,4 @@ const results = arr
|
||||
|
||||
return results.length ? results : [{ json: null }];
|
||||
|
||||
|
||||
|
||||
@@ -75,3 +75,5 @@ return [{
|
||||
}
|
||||
}];
|
||||
|
||||
|
||||
|
||||
|
||||
163
ticket_form/docs/CODE_CLAIMSAVE_PRIMARY_PREPARE.js
Normal file
163
ticket_form/docs/CODE_CLAIMSAVE_PRIMARY_PREPARE.js
Normal file
@@ -0,0 +1,163 @@
|
||||
// ============================================================================
|
||||
// Code Node: Подготовка данных для claimsave_primary
|
||||
// ============================================================================
|
||||
// Назначение: Собрать все данные из предыдущих узлов и подготовить payload
|
||||
// для сохранения первичного черновика в PostgreSQL
|
||||
//
|
||||
// Позиция: После Code4, перед claimsave_primary (PostgreSQL)
|
||||
// ============================================================================
|
||||
|
||||
const items = $input.all();
|
||||
|
||||
// Получаем данные из разных узлов
|
||||
const code4Data = $('Code4').first().json.redis_value || {};
|
||||
const editFields16 = $('Edit Fields16').first().json;
|
||||
const aiAgent1Facts = $('пробрасываем факт фул и факт шорт1').first().json;
|
||||
const aiAgent13 = $('AI Agent13').first().json;
|
||||
const redisTrigger = $('Redis Trigger').first().json.message || {};
|
||||
const editFields11 = $('Edit Fields11').first().json || {};
|
||||
const editFields10 = $('Edit Fields10').first().json || {};
|
||||
const propertyNameRaw = $('propertyName').first().json || {};
|
||||
|
||||
// propertyName может быть массивом или объектом
|
||||
// Если массив - берем первый элемент, если объект - используем как есть
|
||||
const propertyName = Array.isArray(propertyNameRaw)
|
||||
? (propertyNameRaw[0] || {})
|
||||
: propertyNameRaw;
|
||||
|
||||
// Логирование для отладки unified_id
|
||||
console.log('🔍 Поиск unified_id:');
|
||||
console.log(' - propertyNameRaw (тип):', Array.isArray(propertyNameRaw) ? 'массив' : 'объект');
|
||||
console.log(' - propertyName:', propertyName);
|
||||
console.log(' - propertyName.unified_id:', propertyName?.unified_id);
|
||||
console.log(' - propertyName.body?.unified_id:', propertyName?.body?.unified_id);
|
||||
console.log(' - propertyName.result?.unified_id:', propertyName?.result?.unified_id);
|
||||
console.log(' - editFields10.unified_id:', editFields10?.unified_id);
|
||||
console.log(' - redisTrigger.unified_id:', redisTrigger?.unified_id);
|
||||
|
||||
// Собираем payload для сохранения
|
||||
const payload = {
|
||||
// Описание проблемы от пользователя
|
||||
problem_description: editFields16?.chatInput || redisTrigger?.description || null,
|
||||
|
||||
// Wizard plan от AI Agent12 (через Code4)
|
||||
wizard_plan: code4Data.wizard_plan || null,
|
||||
|
||||
// Предзаполненные ответы (если есть)
|
||||
answers_prefill: code4Data.answers_prefill || [],
|
||||
|
||||
// Отчёт о покрытии (если есть)
|
||||
coverage_report: code4Data.coverage_report || {},
|
||||
|
||||
// Данные из AI Agent1 (факты)
|
||||
ai_agent1_facts: {
|
||||
facts_short: aiAgent1Facts?.facts_short || null,
|
||||
facts_full: aiAgent1Facts?.facts_full || null,
|
||||
problem: aiAgent1Facts?.problem || null
|
||||
},
|
||||
|
||||
// Данные из AI Agent13 (RAG ответ)
|
||||
ai_agent13_rag: aiAgent13?.output || null,
|
||||
|
||||
// Контакты
|
||||
phone: redisTrigger?.phone || null,
|
||||
email: redisTrigger?.email || null,
|
||||
|
||||
// Тип дела (из wizard_plan или по умолчанию)
|
||||
type_code: code4Data.wizard_plan?.case_type || 'consumer'
|
||||
};
|
||||
|
||||
// Получаем session_token (приоритет: Edit Fields11 > Redis Trigger)
|
||||
const session_token = editFields11.session_token
|
||||
|| redisTrigger.session_id
|
||||
|| null;
|
||||
|
||||
// Получаем unified_id (приоритет: propertyName > Edit Fields10 > Redis Trigger)
|
||||
// propertyName может быть массивом, объектом, или содержать unified_id в вложенных объектах
|
||||
let unified_id = null;
|
||||
|
||||
// Если propertyName - массив, ищем unified_id в элементах массива
|
||||
if (Array.isArray(propertyNameRaw)) {
|
||||
for (const item of propertyNameRaw) {
|
||||
unified_id = item?.unified_id
|
||||
|| item?.body?.unified_id
|
||||
|| item?.result?.unified_id
|
||||
|| item?.data?.unified_id
|
||||
|| null;
|
||||
if (unified_id) break;
|
||||
}
|
||||
} else {
|
||||
// Если propertyName - объект, ищем unified_id напрямую или в вложенных объектах
|
||||
unified_id = propertyName.unified_id
|
||||
|| propertyName.body?.unified_id
|
||||
|| propertyName.result?.unified_id
|
||||
|| propertyName.data?.unified_id
|
||||
|| null;
|
||||
}
|
||||
|
||||
// Fallback на другие источники
|
||||
if (!unified_id) {
|
||||
unified_id = editFields10.unified_id
|
||||
|| redisTrigger.unified_id
|
||||
|| null;
|
||||
}
|
||||
|
||||
// Валидация обязательных полей
|
||||
if (!session_token) {
|
||||
throw new Error('❌ session_token не найден! Проверьте узлы Edit Fields11 и Redis Trigger.');
|
||||
}
|
||||
|
||||
if (!payload.wizard_plan) {
|
||||
console.warn('⚠️ wizard_plan отсутствует! Черновик будет сохранён без плана вопросов.');
|
||||
}
|
||||
|
||||
if (!payload.problem_description) {
|
||||
console.warn('⚠️ problem_description отсутствует! Черновик будет сохранён без описания проблемы.');
|
||||
}
|
||||
|
||||
// Логирование для отладки
|
||||
console.log('🔍 Подготовка данных для claimsave_primary:');
|
||||
console.log(' - session_token:', session_token ? '✅' : '❌');
|
||||
console.log(' - unified_id:', unified_id || 'null');
|
||||
if (!unified_id) {
|
||||
console.warn('⚠️ unified_id не найден! Проверьте ноды: propertyName, Edit Fields10, Redis Trigger');
|
||||
}
|
||||
console.log(' - wizard_plan:', payload.wizard_plan ? '✅' : '❌');
|
||||
console.log(' - problem_description:', payload.problem_description ? '✅' : '❌');
|
||||
console.log(' - ai_agent1_facts:', payload.ai_agent1_facts.facts_short ? '✅' : '❌');
|
||||
console.log(' - ai_agent13_rag:', payload.ai_agent13_rag ? '✅' : '❌');
|
||||
console.log(' - phone:', payload.phone || 'null');
|
||||
console.log(' - email:', payload.email || 'null');
|
||||
|
||||
// Возвращаем данные для PostgreSQL узла
|
||||
return {
|
||||
json: {
|
||||
// Payload для параметра $1
|
||||
payload_json: payload,
|
||||
|
||||
// Session token для параметра $2
|
||||
session_token: session_token,
|
||||
|
||||
// Unified ID для параметра $3 (может быть null)
|
||||
unified_id: unified_id,
|
||||
|
||||
// Дополнительная информация для отладки
|
||||
_debug: {
|
||||
has_wizard_plan: !!payload.wizard_plan,
|
||||
has_problem_description: !!payload.problem_description,
|
||||
has_ai_agent1: !!payload.ai_agent1_facts.facts_short,
|
||||
has_ai_agent13: !!payload.ai_agent13_rag,
|
||||
source_nodes: {
|
||||
code4: 'Code4',
|
||||
editFields16: 'Edit Fields16',
|
||||
aiAgent1: 'пробрасываем факт фул и факт шорт1',
|
||||
aiAgent13: 'AI Agent13',
|
||||
redisTrigger: 'Redis Trigger',
|
||||
editFields11: 'Edit Fields11',
|
||||
editFields10: 'Edit Fields10',
|
||||
propertyName: 'propertyName'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -42,3 +42,5 @@ return {
|
||||
ttl: 604800
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
164
ticket_form/docs/CODE_FILES_RENAME_FIXED.js
Normal file
164
ticket_form/docs/CODE_FILES_RENAME_FIXED.js
Normal file
@@ -0,0 +1,164 @@
|
||||
// === НАСТРОЙКА ===
|
||||
|
||||
const FILES_ROWS_NODE = 'editfiletobd1'; // <--- имя ноды, где формировались filesRows
|
||||
|
||||
// === ВХОД ИЗ PG ===
|
||||
|
||||
const sql = $json || {};
|
||||
|
||||
const claim_id = sql?.claim?.claim_id || $json.claim_id || null;
|
||||
|
||||
const docs = Array.isArray(sql.documents) ? sql.documents : [];
|
||||
|
||||
// === filesRows из предыдущей ноды ===
|
||||
|
||||
const filesRows = $items(FILES_ROWS_NODE)?.[0]?.json?.filesRows || [];
|
||||
|
||||
// === Получаем project_id и project_name ===
|
||||
// Пробуем получить из Edit Fields6, затем из текущего $json, затем из предыдущих нод
|
||||
|
||||
let project_id = null;
|
||||
let project_name = null;
|
||||
|
||||
// 1. Пробуем из Edit Fields6
|
||||
try {
|
||||
const editFields6 = $('Edit Fields6').first().json.propertyName || {};
|
||||
project_id = editFields6.project_id || null;
|
||||
project_name = editFields6.project_name || null;
|
||||
} catch (e) {
|
||||
// Игнорируем, пробуем другие источники
|
||||
}
|
||||
|
||||
// 2. Пробуем из текущего $json
|
||||
if (!project_id) {
|
||||
project_id = $json?.project_id || null;
|
||||
}
|
||||
if (!project_name) {
|
||||
project_name = $json?.project_name || null;
|
||||
}
|
||||
|
||||
// 3. Пробуем из предыдущей ноды (если есть нода, которая получает данные из Redis)
|
||||
if (!project_name) {
|
||||
try {
|
||||
// Пробуем найти ноду, которая получает данные сессии из Redis
|
||||
const redisNode = $node["Get Session"] || $node["GetSession"] || $node["Redis Get"];
|
||||
if (redisNode && redisNode.json) {
|
||||
const sessionData = typeof redisNode.json.value === 'string'
|
||||
? JSON.parse(redisNode.json.value)
|
||||
: redisNode.json.value;
|
||||
if (!project_name) {
|
||||
project_name = sessionData?.project_name || null;
|
||||
}
|
||||
if (!project_id) {
|
||||
project_id = sessionData?.project_id || null;
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Игнорируем ошибки
|
||||
}
|
||||
}
|
||||
|
||||
// === Утилиты ===
|
||||
|
||||
const S = v => (v == null ? '' : String(v));
|
||||
|
||||
const extOf = n => (S(n).match(/\.([a-z0-9]+)$/i)?.[1]?.toLowerCase() || 'pdf');
|
||||
|
||||
const baseName = k => S(k).split('/').pop() || 'file.pdf';
|
||||
|
||||
const slugify = s => {
|
||||
s = S(s).toLowerCase().trim();
|
||||
const map = {а:'a',б:'b',в:'v',г:'g',д:'d',е:'e',ё:'e',ж:'zh',з:'z',и:'i',й:'y',к:'k',л:'l',
|
||||
м:'m',н:'n',о:'o',п:'p',р:'r',с:'s',т:'t',у:'u',ф:'f',х:'h',ц:'c',ч:'ch',ш:'sh',
|
||||
щ:'sch',ъ:'',ы:'y',ь:'',э:'e',ю:'yu',я:'ya'};
|
||||
s = s.replace(/[а-яё]/g, ch => map[ch] ?? ch);
|
||||
return s.replace(/[^a-z0-9]+/g,'-').replace(/^-+|-+$/g,'') || 'doc';
|
||||
};
|
||||
|
||||
// field_name -> doc_id
|
||||
|
||||
const byField = Object.create(null);
|
||||
|
||||
for (const d of docs) if (d?.field_name && d?.id) byField[d.field_name] = d.id;
|
||||
|
||||
// Правило финального пути
|
||||
// Новый формат: /f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c/crm2/CRM_Active_Files/Documents/Project/{project_name}_{project_id}/{doc_id}__{slug}.{ext}
|
||||
// Пример: /.../Project/ERV_6381_КлиентПрав_398957/{doc_id}__{slug}.{ext}
|
||||
// project_name уже содержит "ERV_6381_КлиентПрав", просто добавляем к нему _project_id
|
||||
|
||||
const buildFinalKey = (row, doc_id) => {
|
||||
const bucketPrefix = 'f9825c87-4e3558f6-f9b6-405c-ad3d-d1535c49b61c';
|
||||
const basePath = 'crm2/CRM_Active_Files/Documents/Project';
|
||||
|
||||
// Используем название поля из формы визарда
|
||||
// Приоритет: field_label (из uploads_field_labels) > field_name (из uploads_field_names) > description > group_index
|
||||
// field_label должен приходить из n8n workflow, где формируется filesRows из uploads_field_labels[i]
|
||||
const fieldLabel = row.field_label || row.field_name || row.description || `group-${row.group_index}`;
|
||||
const slug = slugify(fieldLabel);
|
||||
const ext = extOf(row.original_name || 'pdf');
|
||||
|
||||
// Формируем название папки: {project_name}_{project_id}
|
||||
// project_name уже содержит "ERV_6381_КлиентПрав", добавляем к нему _project_id
|
||||
let projectFolder = 'unknown';
|
||||
if (project_name && project_id) {
|
||||
projectFolder = `${project_name}_${project_id}`;
|
||||
} else if (project_id) {
|
||||
// Fallback: если нет project_name, используем старый формат
|
||||
projectFolder = `${project_id}_Клиентправ`;
|
||||
}
|
||||
|
||||
// Очищаем от недопустимых символов для пути
|
||||
projectFolder = String(projectFolder)
|
||||
.replace(/[\/\\\?\*\:\|\"<>]/g, '_')
|
||||
.replace(/^\.+|\.+$/g, '')
|
||||
.trim() || 'unknown';
|
||||
|
||||
// Формируем путь: /{bucketPrefix}/{basePath}/{project_folder}/{doc_id}__{slug}.{ext}
|
||||
return `/${bucketPrefix}/${basePath}/${projectFolder}/${doc_id}__${slug}.${ext}`;
|
||||
};
|
||||
|
||||
// Собираем renames + финальные метаданные
|
||||
|
||||
const renames = [];
|
||||
|
||||
const finalDocumentsMeta = [];
|
||||
|
||||
const nowIso = new Date().toISOString();
|
||||
|
||||
for (const row of filesRows) {
|
||||
const g = Number(row.group_index) || 0;
|
||||
const field_name = `uploads[${g}][0]`;
|
||||
const doc_id = byField[field_name] || `grp${g}`; // фолбэк если чего-то не сошлось
|
||||
const fromKey = S(row.draft_key);
|
||||
const toKey = buildFinalKey(row, doc_id);
|
||||
|
||||
// Получаем название поля из row (field_label должен быть добавлен в ноде editfiletobd1)
|
||||
const field_label = row.field_label || row.field_name || row.description || `group-${g}`;
|
||||
|
||||
renames.push({
|
||||
from: fromKey,
|
||||
to: toKey,
|
||||
doc_id,
|
||||
field_name,
|
||||
field_label // ✅ Добавляем название поля
|
||||
});
|
||||
|
||||
finalDocumentsMeta.push({
|
||||
field_name,
|
||||
field_label, // ✅ Добавляем название поля
|
||||
file_id: toKey,
|
||||
file_name: baseName(toKey),
|
||||
original_file_name: baseName(row.original_name || toKey),
|
||||
uploaded_at: nowIso
|
||||
});
|
||||
}
|
||||
|
||||
return [{
|
||||
json: {
|
||||
claim_id,
|
||||
project_id,
|
||||
renames, // план копирования/переименования на S3
|
||||
payload_partial_json: { documents_meta: finalDocumentsMeta } // для финального апдейта в БД
|
||||
}
|
||||
}];
|
||||
|
||||
120
ticket_form/docs/CODE_MERGE_PROJECT_TO_SESSION.js
Normal file
120
ticket_form/docs/CODE_MERGE_PROJECT_TO_SESSION.js
Normal file
@@ -0,0 +1,120 @@
|
||||
// ========================================
|
||||
// Code Node: Мерж данных проекта в сессию
|
||||
// ========================================
|
||||
|
||||
// 1. Берём первый item
|
||||
const inputItem = $input.all()[0];
|
||||
|
||||
if (!inputItem || !inputItem.json) {
|
||||
throw new Error('Пустой input в Code Node (нет json)');
|
||||
}
|
||||
|
||||
// root — то, что реально пришло в эту ноду
|
||||
const root = inputItem.json;
|
||||
|
||||
// 2. Универсально получаем body
|
||||
// - если нода стоит сразу после Webhook → данные лежат в root.body
|
||||
// - если кто-то выше уже отдал только body → root и есть body
|
||||
const body = root.body || root;
|
||||
|
||||
// 3. Парсим body.other (если есть) как сессию
|
||||
let sessionData = {};
|
||||
const rawOther = body.other;
|
||||
|
||||
if (rawOther) {
|
||||
if (typeof rawOther === 'string') {
|
||||
try {
|
||||
sessionData = JSON.parse(rawOther);
|
||||
} catch (e) {
|
||||
throw new Error('Не смог распарсить body.other как JSON: ' + e.message + '. rawOther: ' + rawOther);
|
||||
}
|
||||
} else if (typeof rawOther === 'object') {
|
||||
sessionData = rawOther;
|
||||
}
|
||||
}
|
||||
|
||||
// 4. Определяем claimId (основной путь)
|
||||
let claimId = body.claim_id || sessionData.claim_id || null;
|
||||
|
||||
// 5. Fallback: пробуем достать claim_id напрямую из Webhook, если его до сих пор нет
|
||||
if (!claimId) {
|
||||
try {
|
||||
const webhookNodeJson = $('Webhook').first()?.json;
|
||||
if (webhookNodeJson?.body?.claim_id) {
|
||||
claimId = webhookNodeJson.body.claim_id;
|
||||
}
|
||||
} catch (e) {
|
||||
// молча игнорируем, просто не удалось взять из Webhook
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Если всё ещё нет claimId — это реально критичная ситуация
|
||||
if (!claimId) {
|
||||
throw new Error(
|
||||
'Нет claim_id ни в body, ни в sessionData, ни в Webhook. ' +
|
||||
'body: ' + JSON.stringify(body) +
|
||||
', sessionData: ' + JSON.stringify(sessionData)
|
||||
);
|
||||
}
|
||||
|
||||
// 7. Забираем результат ноды CreateClientProject (или CreateWebPorject, если опечатка в названии ноды)
|
||||
let projectNode = null;
|
||||
let projectNodeName = null;
|
||||
|
||||
// Пробуем найти ноду безопасно
|
||||
try {
|
||||
projectNode = $node["CreateClientProject"];
|
||||
if (projectNode && projectNode.json) {
|
||||
projectNodeName = "CreateClientProject";
|
||||
}
|
||||
} catch (e) {
|
||||
// Нода CreateClientProject не найдена, пробуем альтернативное название
|
||||
}
|
||||
|
||||
if (!projectNode || !projectNode.json) {
|
||||
try {
|
||||
projectNode = $node["CreateWebPorject"];
|
||||
if (projectNode && projectNode.json) {
|
||||
projectNodeName = "CreateWebPorject";
|
||||
}
|
||||
} catch (e) {
|
||||
// Нода CreateWebPorject тоже не найдена
|
||||
}
|
||||
}
|
||||
|
||||
if (!projectNode || !projectNode.json) {
|
||||
throw new Error('Нет данных от ноды CreateClientProject/CreateWebPorject. Убедитесь, что нода существует и выполнена.');
|
||||
}
|
||||
|
||||
const projectResult = projectNode.json.result;
|
||||
// Ожидаем что-то типа: { "project_id": "398095", "project_name": "Иванов_КлиентПрав", "is_new": false }
|
||||
|
||||
if (!projectResult || !projectResult.project_id) {
|
||||
throw new Error('Нет projectResult.project_id. result: ' + JSON.stringify(projectNode.json));
|
||||
}
|
||||
|
||||
// 8. Собираем обновлённую сессию
|
||||
const updatedSession = {
|
||||
...sessionData, // всё, что было в other
|
||||
claim_id: claimId, // актуальный claim_id
|
||||
project_id: projectResult.project_id, // id проекта из CRM
|
||||
project_name: projectResult.project_name || null, // название проекта из CRM (новое поле)
|
||||
is_new_project: projectResult.is_new, // флаг новый/старый
|
||||
current_step: 2, // двигаем визард на шаг 2
|
||||
updated_at: new Date().toISOString(),
|
||||
// опционально дотащим полезные поля из body:
|
||||
problem: body.problem ?? sessionData.problem,
|
||||
last_analysis_output: body.output ?? sessionData.last_analysis_output,
|
||||
};
|
||||
|
||||
// 9. Возвращаем один item для Redis SET
|
||||
return [
|
||||
{
|
||||
json: {
|
||||
redis_key: `claim:${claimId}`,
|
||||
redis_value: JSON.stringify(updatedSession),
|
||||
ttl: 604800, // 7 дней
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -181,3 +181,4 @@ clpr_user_accounts (channel='telegram', channel_user_id=telegram_id)
|
||||
clpr_users (id)
|
||||
```
|
||||
|
||||
|
||||
|
||||
225
ticket_form/docs/N8N_CLAIMSAVE_PRIMARY_SETUP.md
Normal file
225
ticket_form/docs/N8N_CLAIMSAVE_PRIMARY_SETUP.md
Normal file
@@ -0,0 +1,225 @@
|
||||
# Инструкция: Добавление узла `claimsave_primary` в workflow b4K4u851b4JFivyD
|
||||
|
||||
## Позиция узла
|
||||
|
||||
**Между узлами:**
|
||||
- **После:** `Code4` (форматирует данные для Redis)
|
||||
- **Перед:** `push_wizard1` (пушит wizard_plan в Redis для SSE)
|
||||
|
||||
## Порядок узлов в workflow
|
||||
|
||||
```
|
||||
1. Redis Trigger
|
||||
2. get_claime_data1
|
||||
3. Edit Fields8
|
||||
4. Merge2
|
||||
5. Get row(s) in sheet2
|
||||
6. Edit Fields16
|
||||
7. AI Agent1
|
||||
8. пробрасываем факт фул и факт шорт1
|
||||
9. AI Agent13
|
||||
10. output_set1
|
||||
11. Edit Fields11
|
||||
12. AI Agent12
|
||||
13. Code
|
||||
14. Code4
|
||||
15. ⭐ Code: Prepare Claimsave Data ← ВСТАВИТЬ ЗДЕСЬ (Code Node)
|
||||
16. ⭐ claimsave_primary ← ВСТАВИТЬ ЗДЕСЬ (PostgreSQL)
|
||||
17. push_wizard1
|
||||
```
|
||||
|
||||
## Шаги настройки
|
||||
|
||||
### 1. Добавить Code Node для подготовки данных
|
||||
|
||||
**Рекомендуется:** Добавить Code Node перед PostgreSQL для удобства отладки и валидации данных.
|
||||
|
||||
1. Откройте workflow `b4K4u851b4JFivyD` в n8n
|
||||
2. Найдите узел `Code4`
|
||||
3. Добавьте новый узел **Code** после `Code4`
|
||||
4. Назовите узел: `Code: Prepare Claimsave Data`
|
||||
5. Подключите:
|
||||
- **Вход:** от узла `Code4`
|
||||
- **Выход:** к узлу `claimsave_primary` (PostgreSQL)
|
||||
|
||||
**Код для Code Node:** См. файл `docs/CODE_CLAIMSAVE_PRIMARY_PREPARE.js`
|
||||
|
||||
**Режим выполнения:** `Run Once for All Items`
|
||||
|
||||
### 2. Добавить новый узел PostgreSQL
|
||||
|
||||
1. Откройте workflow `b4K4u851b4JFivyD` в n8n
|
||||
2. Найдите узел `Code4`
|
||||
3. Добавьте новый узел **PostgreSQL** после `Code4`
|
||||
4. Назовите узел: `claimsave_primary`
|
||||
5. Подключите:
|
||||
- **Вход:** от узла `Code4`
|
||||
- **Выход:** к узлу `push_wizard1`
|
||||
|
||||
### 2. Настройка PostgreSQL узла
|
||||
|
||||
**Connection:** Выберите подключение к PostgreSQL (то же, что используется в других узлах)
|
||||
|
||||
**Operation:** `Execute Query`
|
||||
|
||||
**Query:** Вставьте SQL из файла `docs/SQL_CLAIMSAVE_PRIMARY_DRAFT_CLEAN.sql` (чистая версия без плейсхолдеров)
|
||||
|
||||
**⚠️ ВАЖНО:** Используйте файл `SQL_CLAIMSAVE_PRIMARY_DRAFT_CLEAN.sql`, а не `SQL_CLAIMSAVE_PRIMARY_DRAFT.sql`!
|
||||
|
||||
### 3. Параметры запроса
|
||||
|
||||
**Query Replacement:** Оставьте пустым (не используем)
|
||||
|
||||
**Parameters:** Добавьте 3 параметра (если используете Code Node для подготовки):
|
||||
|
||||
#### Параметр $1 (payload_json):
|
||||
```javascript
|
||||
{{ JSON.stringify($('Code: Prepare Claimsave Data').first().json.payload_json) }}
|
||||
```
|
||||
|
||||
#### Параметр $2 (session_token):
|
||||
```javascript
|
||||
{{ $('Code: Prepare Claimsave Data').first().json.session_token }}
|
||||
```
|
||||
|
||||
#### Параметр $3 (unified_id):
|
||||
```javascript
|
||||
{{ $('Code: Prepare Claimsave Data').first().json.unified_id }}
|
||||
```
|
||||
|
||||
**Примечание:** Code Node берёт `unified_id` из ноды `propertyName` (приоритет: `propertyName` > `Edit Fields10` > `Redis Trigger`)
|
||||
|
||||
---
|
||||
|
||||
**Альтернатива (без Code Node):** Если не используете Code Node, можно собрать данные напрямую:
|
||||
|
||||
#### Параметр $1 (payload_json):
|
||||
```javascript
|
||||
{{ JSON.stringify({
|
||||
problem_description: $('Edit Fields16').first().json.chatInput,
|
||||
wizard_plan: $('Code4').first().json.redis_value.wizard_plan,
|
||||
answers_prefill: $('Code4').first().json.redis_value.answers_prefill || [],
|
||||
coverage_report: $('Code4').first().json.redis_value.coverage_report || {},
|
||||
ai_agent1_facts: {
|
||||
facts_short: $('пробрасываем факт фул и факт шорт1').first().json.facts_short,
|
||||
facts_full: $('пробрасываем факт фул и факт шорт1').first().json.facts_full,
|
||||
problem: $('пробрасываем факт фул и факт шорт1').first().json.problem
|
||||
},
|
||||
ai_agent13_rag: $('AI Agent13').first().json.output,
|
||||
phone: $('Redis Trigger').first().json.message.phone,
|
||||
email: $('Redis Trigger').first().json.message.email || null,
|
||||
type_code: $('Code4').first().json.redis_value.wizard_plan?.case_type || 'consumer'
|
||||
}) }}
|
||||
```
|
||||
|
||||
#### Параметр $2 (session_token):
|
||||
```javascript
|
||||
{{ $('Edit Fields11').first().json.session_token || $('Redis Trigger').first().json.message.session_id }}
|
||||
```
|
||||
|
||||
#### Параметр $3 (unified_id):
|
||||
```javascript
|
||||
{{ $('propertyName').first().json.unified_id || $('Edit Fields10').first().json.unified_id || $('Redis Trigger').first().json.message.unified_id || null }}
|
||||
```
|
||||
|
||||
**Примечание:** Приоритет источников `unified_id`: `propertyName` > `Edit Fields10` > `Redis Trigger`
|
||||
|
||||
### 4. Проверка подключений
|
||||
|
||||
Убедитесь, что:
|
||||
- ✅ Узел `Code: Prepare Claimsave Data` получает данные от `Code4`
|
||||
- ✅ Узел `claimsave_primary` получает данные от `Code: Prepare Claimsave Data`
|
||||
- ✅ Узел `push_wizard1` получает данные от `claimsave_primary` (или от `Code4`, если нужно)
|
||||
- ✅ Все пути данных корректны
|
||||
|
||||
## Преимущества использования Code Node
|
||||
|
||||
✅ **Упрощение параметров PostgreSQL:** Вместо сложных выражений в параметрах SQL, используем простые ссылки на Code Node
|
||||
|
||||
✅ **Валидация данных:** Code Node проверяет наличие обязательных полей и выводит предупреждения
|
||||
|
||||
✅ **Отладка:** Легче отслеживать, какие данные собраны, через логи Code Node
|
||||
|
||||
✅ **Обработка edge cases:** Можно добавить fallback значения и обработку ошибок
|
||||
|
||||
✅ **Читаемость:** Код подготовки данных отделён от SQL запроса
|
||||
|
||||
## Что сохраняется
|
||||
|
||||
После выполнения узла `claimsave_primary` в БД будет создана/обновлена запись в `clpr_claims`:
|
||||
|
||||
- ✅ `session_token` - для связи
|
||||
- ✅ `unified_id` - если передан
|
||||
- ✅ `status_code = 'draft'` - статус черновика
|
||||
- ✅ `payload.wizard_plan` - план вопросов
|
||||
- ✅ `payload.problem_description` - описание проблемы
|
||||
- ✅ `payload.answers_prefill` - предзаполненные ответы
|
||||
- ✅ `payload.coverage_report` - отчёт о покрытии
|
||||
- ✅ `payload.ai_agent1_facts` - факты из AI Agent1
|
||||
- ✅ `payload.ai_agent13_rag` - RAG ответ
|
||||
- ✅ `payload.phone`, `payload.email` - контакты
|
||||
- ⚠️ `payload.claim_id = NULL` - будет сгенерирован позже
|
||||
|
||||
## Возвращаемое значение
|
||||
|
||||
Узел возвращает объект:
|
||||
```json
|
||||
{
|
||||
"claim": {
|
||||
"claim_id": "uuid-записи",
|
||||
"session_token": "sess-...",
|
||||
"status_code": "draft",
|
||||
"payload": { ... }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Важные замечания
|
||||
|
||||
1. **Узел работает в режиме UPSERT:**
|
||||
- Если запись с таким `session_token` существует → обновляет её
|
||||
- Если записи нет → создаёт новую
|
||||
|
||||
2. **`claim_id` генерируется позже:**
|
||||
- На этом этапе `claim_id` в `payload` = `NULL`
|
||||
- UUID записи (`clpr_claims.id`) используется как временный идентификатор
|
||||
- Позже `claim_id` будет сгенерирован в формате `CLM-YYYY-MM-DD-XXXXXX`
|
||||
|
||||
3. **Данные из предыдущих узлов:**
|
||||
- `wizard_plan` берётся из `Code4.redis_value.wizard_plan`
|
||||
- `problem_description` берётся из `Edit Fields16.chatInput`
|
||||
- `ai_agent1_facts` берётся из узла `пробрасываем факт фул и факт шорт1`
|
||||
- `ai_agent13_rag` берётся из `AI Agent13.output`
|
||||
|
||||
## Тестирование
|
||||
|
||||
После добавления узла:
|
||||
|
||||
1. Запустите workflow с тестовыми данными
|
||||
2. Проверьте, что узел выполняется без ошибок
|
||||
3. Проверьте в БД, что запись создана/обновлена:
|
||||
```sql
|
||||
SELECT id, session_token, unified_id, status_code, payload->>'wizard_plan'
|
||||
FROM clpr_claims
|
||||
WHERE session_token = 'sess-...'
|
||||
ORDER BY updated_at DESC
|
||||
LIMIT 1;
|
||||
```
|
||||
|
||||
## Если что-то не работает
|
||||
|
||||
1. **Ошибка "column does not exist":**
|
||||
- Проверьте, что все поля в SQL запросе существуют в таблице `clpr_claims`
|
||||
|
||||
2. **Ошибка "invalid input syntax for type jsonb":**
|
||||
- Проверьте, что параметр `$1` правильно сериализован через `JSON.stringify()`
|
||||
- Убедитесь, что все вложенные объекты корректны
|
||||
|
||||
3. **Ошибка "session_token is null":**
|
||||
- Проверьте, что `Edit Fields11` содержит `session_token`
|
||||
- Проверьте fallback на `Redis Trigger.message.session_id`
|
||||
|
||||
4. **Данные не сохраняются:**
|
||||
- Проверьте логи n8n на наличие ошибок
|
||||
- Проверьте, что все узлы-источники данных выполнены успешно
|
||||
|
||||
@@ -36,3 +36,4 @@ return {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -45,3 +45,4 @@ return {
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
|
||||
261
ticket_form/docs/N8N_FORM_GET_NO_FILES_BRANCH.json
Normal file
261
ticket_form/docs/N8N_FORM_GET_NO_FILES_BRANCH.json
Normal file
@@ -0,0 +1,261 @@
|
||||
{
|
||||
"meta": {
|
||||
"description": "Ноды для обработки формы БЕЗ файлов в workflow form_get",
|
||||
"date": "2025-11-21",
|
||||
"action": "Добавить в TRUE ветку IF-ноды 'проверка наличия файлов'"
|
||||
},
|
||||
"nodes": [
|
||||
{
|
||||
"name": "extract_webhook_data_no_files",
|
||||
"description": "Извлекаем данные из webhook для случая без файлов",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [-320, 400],
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "session_id",
|
||||
"name": "session_id",
|
||||
"value": "={{ $('Webhook').item.json.body.session_id }}",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"id": "claim_id",
|
||||
"name": "claim_id",
|
||||
"value": "={{ $('Webhook').item.json.body.claim_id }}",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"id": "unified_id",
|
||||
"name": "unified_id",
|
||||
"value": "={{ $('Webhook').item.json.body.unified_id }}",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"id": "contact_id",
|
||||
"name": "contact_id",
|
||||
"value": "={{ $('Webhook').item.json.body.contact_id }}",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"id": "phone",
|
||||
"name": "phone",
|
||||
"value": "={{ $('Webhook').item.json.body.phone }}",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"id": "wizard_plan",
|
||||
"name": "wizard_plan",
|
||||
"value": "={{ $('Webhook').item.json.body.wizard_plan }}",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"id": "wizard_answers",
|
||||
"name": "wizard_answers",
|
||||
"value": "={{ $('Webhook').item.json.body.wizard_answers }}",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"id": "type_code",
|
||||
"name": "type_code",
|
||||
"value": "={{ $('Webhook').item.json.body.type_code || 'consumer' }}",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "prepare_payload_no_files",
|
||||
"description": "Формируем payload для PostgreSQL",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [-80, 400],
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "payload_partial_json",
|
||||
"name": "payload_partial_json",
|
||||
"value": "={{ {\n session_id: $json.session_id,\n unified_id: $json.unified_id,\n contact_id: $json.contact_id,\n phone: $json.phone,\n type_code: $json.type_code,\n wizard_plan: $json.wizard_plan,\n wizard_answers: $json.wizard_answers,\n documents_meta: []\n} }}",
|
||||
"type": "object"
|
||||
},
|
||||
{
|
||||
"id": "claim_id",
|
||||
"name": "claim_id",
|
||||
"value": "={{ $json.claim_id }}",
|
||||
"type": "string"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "save_claim_no_files",
|
||||
"description": "Сохраняем claim без файлов в PostgreSQL",
|
||||
"type": "n8n-nodes-base.postgres",
|
||||
"typeVersion": 2.6,
|
||||
"position": [180, 400],
|
||||
"parameters": {
|
||||
"operation": "executeQuery",
|
||||
"query": "WITH partial AS (\n SELECT \n $1::jsonb AS p, \n $2::text AS claim_id_str\n),\n\n-- Парсим wizard_answers\nwizard_answers_parsed AS (\n SELECT \n CASE \n WHEN partial.p->>'wizard_answers' IS NOT NULL \n THEN (partial.p->>'wizard_answers')::jsonb\n WHEN partial.p->'wizard_answers' IS NOT NULL \n AND jsonb_typeof(partial.p->'wizard_answers') = 'object'\n THEN partial.p->'wizard_answers'\n ELSE '{}'::jsonb\n END AS answers\n FROM partial\n),\n\n-- Парсим wizard_plan\nwizard_plan_parsed AS (\n SELECT \n CASE \n WHEN partial.p->>'wizard_plan' IS NOT NULL \n THEN (partial.p->>'wizard_plan')::jsonb\n WHEN partial.p->'wizard_plan' IS NOT NULL \n AND jsonb_typeof(partial.p->'wizard_plan') = 'object'\n THEN partial.p->'wizard_plan'\n ELSE NULL\n END AS wizard_plan\n FROM partial\n),\n\n-- UPSERT claim\nclaim_upsert AS (\n INSERT INTO clpr_claims (\n id,\n session_token,\n unified_id,\n contact_id,\n phone,\n channel,\n type_code,\n status_code,\n payload,\n created_at,\n updated_at,\n expires_at\n )\n SELECT \n partial.claim_id_str::uuid,\n COALESCE(partial.p->>'session_id', 'sess-unknown'),\n partial.p->>'unified_id',\n partial.p->>'contact_id',\n partial.p->>'phone',\n 'web_form',\n COALESCE(partial.p->>'type_code', 'consumer'),\n 'draft',\n jsonb_build_object(\n 'claim_id', partial.claim_id_str,\n 'answers', (SELECT answers FROM wizard_answers_parsed),\n 'documents_meta', '[]'::jsonb,\n 'wizard_plan', (SELECT wizard_plan FROM wizard_plan_parsed)\n ),\n COALESCE(\n (SELECT created_at FROM clpr_claims WHERE id = partial.claim_id_str::uuid),\n now()\n ),\n now(),\n now() + interval '14 days'\n FROM partial\n ON CONFLICT (id) DO UPDATE SET\n session_token = EXCLUDED.session_token,\n unified_id = COALESCE(EXCLUDED.unified_id, clpr_claims.unified_id),\n contact_id = COALESCE(EXCLUDED.contact_id, clpr_claims.contact_id),\n phone = COALESCE(EXCLUDED.phone, clpr_claims.phone),\n status_code = 'draft',\n payload = (\n clpr_claims.payload \n - 'answers' \n - 'documents_meta' \n - 'wizard_plan' \n - 'claim_id'\n ) || EXCLUDED.payload,\n updated_at = now(),\n expires_at = now() + interval '14 days'\n RETURNING id, status_code, payload, unified_id, contact_id, phone, session_token\n)\n\nSELECT\n (SELECT jsonb_build_object(\n 'claim_id', cu.id::text,\n 'claim_id_str', (cu.payload->>'claim_id'),\n 'status_code', cu.status_code,\n 'unified_id', cu.unified_id,\n 'contact_id', cu.contact_id,\n 'phone', cu.phone,\n 'session_token', cu.session_token,\n 'payload', cu.payload\n ) FROM claim_upsert cu) AS claim;",
|
||||
"options": {
|
||||
"queryReplacement": "={{ $json.payload_partial_json }}, {{ $json.claim_id }}"
|
||||
}
|
||||
},
|
||||
"credentials": {
|
||||
"postgres": {
|
||||
"id": "sGJ0fJhU8rz88w3k",
|
||||
"name": "timeweb_bd"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "prepare_redis_event_no_files",
|
||||
"description": "Готовим событие для публикации в Redis",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.4,
|
||||
"position": [440, 400],
|
||||
"parameters": {
|
||||
"assignments": {
|
||||
"assignments": [
|
||||
{
|
||||
"id": "redis_key",
|
||||
"name": "redis_key",
|
||||
"value": "=ocr_events:{{ $('extract_webhook_data_no_files').item.json.session_id }}",
|
||||
"type": "string"
|
||||
},
|
||||
{
|
||||
"id": "redis_value",
|
||||
"name": "redis_value",
|
||||
"value": "={{ {\n event_type: 'form_saved',\n claim_id: $json.claim.claim_id,\n status_code: $json.claim.status_code,\n unified_id: $json.claim.unified_id,\n contact_id: $json.claim.contact_id,\n phone: $json.claim.phone,\n session_token: $json.claim.session_token,\n has_files: false,\n timestamp: new Date().toISOString()\n} }}",
|
||||
"type": "object"
|
||||
}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "publish_to_redis_no_files",
|
||||
"description": "Публикуем событие в Redis",
|
||||
"type": "n8n-nodes-base.redis",
|
||||
"typeVersion": 1,
|
||||
"position": [700, 400],
|
||||
"parameters": {
|
||||
"operation": "publish",
|
||||
"channel": "={{ $json.redis_key }}",
|
||||
"value": "={{ JSON.stringify($json.redis_value) }}",
|
||||
"options": {}
|
||||
},
|
||||
"credentials": {
|
||||
"redis": {
|
||||
"id": "RKICQB2ZaisVK4WS",
|
||||
"name": "Local Redis"
|
||||
}
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "respond_no_files",
|
||||
"description": "Возвращаем ответ клиенту",
|
||||
"type": "n8n-nodes-base.respondToWebhook",
|
||||
"typeVersion": 1.1,
|
||||
"position": [960, 400],
|
||||
"parameters": {
|
||||
"options": {
|
||||
"responseCode": 200
|
||||
},
|
||||
"respondWith": "json",
|
||||
"responseBody": "={{ {\n success: true,\n claim_id: $('save_claim_no_files').item.json.claim.claim_id,\n status_code: $('save_claim_no_files').item.json.claim.status_code,\n has_files: false,\n message: 'Заявка сохранена без файлов'\n} }}"
|
||||
}
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"проверка наличия файлов": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "extract_webhook_data_no_files",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
],
|
||||
[
|
||||
{
|
||||
"node": "set_token1",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"extract_webhook_data_no_files": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "prepare_payload_no_files",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"prepare_payload_no_files": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "save_claim_no_files",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"save_claim_no_files": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "prepare_redis_event_no_files",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"prepare_redis_event_no_files": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "publish_to_redis_no_files",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"publish_to_redis_no_files": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "respond_no_files",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"instructions": {
|
||||
"step1": "Открыть workflow 'form_get' (ID: 8ZVMTsuH7Cmw7snw) в n8n",
|
||||
"step2": "Найти IF-ноду 'проверка наличия файлов' (ID: b7497f29-dab3-41cd-aaa3-a43ee83e607c)",
|
||||
"step3": "TRUE ветка (index 0) сейчас пустая - туда нужно добавить новые ноды",
|
||||
"step4": "Импортировать ноды из этого JSON или создать вручную по схеме",
|
||||
"step5": "Подключить TRUE ветку IF к первой ноде: extract_webhook_data_no_files",
|
||||
"step6": "Сохранить и активировать workflow",
|
||||
"step7": "Протестировать отправку формы БЕЗ файлов"
|
||||
}
|
||||
}
|
||||
|
||||
401
ticket_form/docs/N8N_FORM_GET_NO_FILES_INSTRUCTIONS.md
Normal file
401
ticket_form/docs/N8N_FORM_GET_NO_FILES_INSTRUCTIONS.md
Normal file
@@ -0,0 +1,401 @@
|
||||
# Инструкция: Добавление обработки формы БЕЗ файлов в workflow form_get
|
||||
|
||||
**Дата:** 2025-11-21
|
||||
**Workflow ID:** `8ZVMTsuH7Cmw7snw`
|
||||
**Workflow Name:** `form_get`
|
||||
|
||||
---
|
||||
|
||||
## 🎯 Цель
|
||||
|
||||
Добавить ветку обработки для случая, когда форма отправляется **без файлов** (`has_files === false`).
|
||||
|
||||
---
|
||||
|
||||
## 📍 Где добавлять
|
||||
|
||||
В workflow `form_get` есть IF-нода **"проверка наличия файлов"** (ID: `b7497f29-dab3-41cd-aaa3-a43ee83e607c`):
|
||||
|
||||
- **TRUE ветка** (index 0) — файлов НЕТ → **ПУСТАЯ** ❌
|
||||
- **FALSE ветка** (index 1) — файлы ЕСТЬ → существующий flow ✅
|
||||
|
||||
**Задача:** Добавить ноды в TRUE ветку.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Пошаговая инструкция
|
||||
|
||||
### Шаг 1: Открыть workflow
|
||||
|
||||
1. Перейти в n8n: https://n8n.clientright.pro
|
||||
2. Открыть workflow **"form_get"**
|
||||
3. Найти ноду **"проверка наличия файлов"** (IF)
|
||||
|
||||
---
|
||||
|
||||
### Шаг 2: Добавить ноду #1 - Extract Data
|
||||
|
||||
**Название:** `extract_webhook_data_no_files`
|
||||
**Тип:** `Edit Fields` (Set)
|
||||
**Позиция:** справа от IF-ноды, выше основного flow
|
||||
|
||||
**Параметры:**
|
||||
|
||||
| Field Name | Type | Value |
|
||||
|------------|------|-------|
|
||||
| `session_id` | String | `={{ $('Webhook').item.json.body.session_id }}` |
|
||||
| `claim_id` | String | `={{ $('Webhook').item.json.body.claim_id }}` |
|
||||
| `unified_id` | String | `={{ $('Webhook').item.json.body.unified_id }}` |
|
||||
| `contact_id` | String | `={{ $('Webhook').item.json.body.contact_id }}` |
|
||||
| `phone` | String | `={{ $('Webhook').item.json.body.phone }}` |
|
||||
| `wizard_plan` | Object | `={{ $('Webhook').item.json.body.wizard_plan }}` |
|
||||
| `wizard_answers` | Object | `={{ $('Webhook').item.json.body.wizard_answers }}` |
|
||||
| `type_code` | String | `={{ $('Webhook').item.json.body.type_code || 'consumer' }}` |
|
||||
|
||||
**Подключение:**
|
||||
- Из ноды **"проверка наличия файлов"** → TRUE (верхний выход)
|
||||
- К ноде **"extract_webhook_data_no_files"**
|
||||
|
||||
---
|
||||
|
||||
### Шаг 3: Добавить ноду #2 - Prepare Payload
|
||||
|
||||
**Название:** `prepare_payload_no_files`
|
||||
**Тип:** `Edit Fields` (Set)
|
||||
|
||||
**Параметры:**
|
||||
|
||||
| Field Name | Type | Value |
|
||||
|------------|------|-------|
|
||||
| `payload_partial_json` | Object | См. ниже ⬇️ |
|
||||
| `claim_id` | String | `={{ $json.claim_id }}` |
|
||||
|
||||
**Значение для `payload_partial_json`:**
|
||||
|
||||
```javascript
|
||||
={{ {
|
||||
session_id: $json.session_id,
|
||||
unified_id: $json.unified_id,
|
||||
contact_id: $json.contact_id,
|
||||
phone: $json.phone,
|
||||
type_code: $json.type_code,
|
||||
wizard_plan: $json.wizard_plan,
|
||||
wizard_answers: $json.wizard_answers,
|
||||
documents_meta: []
|
||||
} }}
|
||||
```
|
||||
|
||||
**Подключение:**
|
||||
- Из ноды **"extract_webhook_data_no_files"**
|
||||
- К ноде **"prepare_payload_no_files"**
|
||||
|
||||
---
|
||||
|
||||
### Шаг 4: Добавить ноду #3 - Save to PostgreSQL
|
||||
|
||||
**Название:** `save_claim_no_files`
|
||||
**Тип:** `Postgres`
|
||||
**Operation:** `Execute Query`
|
||||
|
||||
**Credentials:** `timeweb_bd` (существующие)
|
||||
|
||||
**Query:**
|
||||
|
||||
```sql
|
||||
WITH partial AS (
|
||||
SELECT
|
||||
$1::jsonb AS p,
|
||||
$2::text AS claim_id_str
|
||||
),
|
||||
|
||||
wizard_answers_parsed AS (
|
||||
SELECT
|
||||
CASE
|
||||
WHEN partial.p->>'wizard_answers' IS NOT NULL
|
||||
THEN (partial.p->>'wizard_answers')::jsonb
|
||||
WHEN partial.p->'wizard_answers' IS NOT NULL
|
||||
AND jsonb_typeof(partial.p->'wizard_answers') = 'object'
|
||||
THEN partial.p->'wizard_answers'
|
||||
ELSE '{}'::jsonb
|
||||
END AS answers
|
||||
FROM partial
|
||||
),
|
||||
|
||||
wizard_plan_parsed AS (
|
||||
SELECT
|
||||
CASE
|
||||
WHEN partial.p->>'wizard_plan' IS NOT NULL
|
||||
THEN (partial.p->>'wizard_plan')::jsonb
|
||||
WHEN partial.p->'wizard_plan' IS NOT NULL
|
||||
AND jsonb_typeof(partial.p->'wizard_plan') = 'object'
|
||||
THEN partial.p->'wizard_plan'
|
||||
ELSE NULL
|
||||
END AS wizard_plan
|
||||
FROM partial
|
||||
),
|
||||
|
||||
claim_upsert AS (
|
||||
INSERT INTO clpr_claims (
|
||||
id,
|
||||
session_token,
|
||||
unified_id,
|
||||
contact_id,
|
||||
phone,
|
||||
channel,
|
||||
type_code,
|
||||
status_code,
|
||||
payload,
|
||||
created_at,
|
||||
updated_at,
|
||||
expires_at
|
||||
)
|
||||
SELECT
|
||||
partial.claim_id_str::uuid,
|
||||
COALESCE(partial.p->>'session_id', 'sess-unknown'),
|
||||
partial.p->>'unified_id',
|
||||
partial.p->>'contact_id',
|
||||
partial.p->>'phone',
|
||||
'web_form',
|
||||
COALESCE(partial.p->>'type_code', 'consumer'),
|
||||
'draft',
|
||||
jsonb_build_object(
|
||||
'claim_id', partial.claim_id_str,
|
||||
'answers', (SELECT answers FROM wizard_answers_parsed),
|
||||
'documents_meta', '[]'::jsonb,
|
||||
'wizard_plan', (SELECT wizard_plan FROM wizard_plan_parsed)
|
||||
),
|
||||
COALESCE(
|
||||
(SELECT created_at FROM clpr_claims WHERE id = partial.claim_id_str::uuid),
|
||||
now()
|
||||
),
|
||||
now(),
|
||||
now() + interval '14 days'
|
||||
FROM partial
|
||||
ON CONFLICT (id) DO UPDATE SET
|
||||
session_token = EXCLUDED.session_token,
|
||||
unified_id = COALESCE(EXCLUDED.unified_id, clpr_claims.unified_id),
|
||||
contact_id = COALESCE(EXCLUDED.contact_id, clpr_claims.contact_id),
|
||||
phone = COALESCE(EXCLUDED.phone, clpr_claims.phone),
|
||||
status_code = 'draft',
|
||||
payload = (
|
||||
clpr_claims.payload
|
||||
- 'answers'
|
||||
- 'documents_meta'
|
||||
- 'wizard_plan'
|
||||
- 'claim_id'
|
||||
) || EXCLUDED.payload,
|
||||
updated_at = now(),
|
||||
expires_at = now() + interval '14 days'
|
||||
RETURNING id, status_code, payload, unified_id, contact_id, phone, session_token
|
||||
)
|
||||
|
||||
SELECT
|
||||
(SELECT jsonb_build_object(
|
||||
'claim_id', cu.id::text,
|
||||
'claim_id_str', (cu.payload->>'claim_id'),
|
||||
'status_code', cu.status_code,
|
||||
'unified_id', cu.unified_id,
|
||||
'contact_id', cu.contact_id,
|
||||
'phone', cu.phone,
|
||||
'session_token', cu.session_token,
|
||||
'payload', cu.payload
|
||||
) FROM claim_upsert cu) AS claim;
|
||||
```
|
||||
|
||||
**Query Replacement:** `={{ $json.payload_partial_json }}, {{ $json.claim_id }}`
|
||||
|
||||
**Подключение:**
|
||||
- Из ноды **"prepare_payload_no_files"**
|
||||
- К ноде **"save_claim_no_files"**
|
||||
|
||||
---
|
||||
|
||||
### Шаг 5: Добавить ноду #4 - Prepare Redis Event
|
||||
|
||||
**Название:** `prepare_redis_event_no_files`
|
||||
**Тип:** `Edit Fields` (Set)
|
||||
|
||||
**Параметры:**
|
||||
|
||||
| Field Name | Type | Value |
|
||||
|------------|------|-------|
|
||||
| `redis_key` | String | `=ocr_events:{{ $('extract_webhook_data_no_files').item.json.session_id }}` |
|
||||
| `redis_value` | Object | См. ниже ⬇️ |
|
||||
|
||||
**Значение для `redis_value`:**
|
||||
|
||||
```javascript
|
||||
={{ {
|
||||
event_type: 'form_saved_no_files',
|
||||
claim_id: $json.claim.claim_id,
|
||||
status_code: $json.claim.status_code,
|
||||
unified_id: $json.claim.unified_id,
|
||||
contact_id: $json.claim.contact_id,
|
||||
phone: $json.claim.phone,
|
||||
session_token: $json.claim.session_token,
|
||||
has_files: false,
|
||||
timestamp: new Date().toISOString()
|
||||
} }}
|
||||
```
|
||||
|
||||
**Подключение:**
|
||||
- Из ноды **"save_claim_no_files"**
|
||||
- К ноде **"prepare_redis_event_no_files"**
|
||||
|
||||
---
|
||||
|
||||
### Шаг 6: Добавить ноду #5 - Publish to Redis
|
||||
|
||||
**Название:** `publish_to_redis_no_files`
|
||||
**Тип:** `Redis`
|
||||
**Operation:** `Publish`
|
||||
|
||||
**Credentials:** `Local Redis` (существующие)
|
||||
|
||||
**Параметры:**
|
||||
|
||||
| Parameter | Value |
|
||||
|-----------|-------|
|
||||
| Channel | `={{ $json.redis_key }}` |
|
||||
| Value | `={{ JSON.stringify($json.redis_value) }}` |
|
||||
|
||||
**Подключение:**
|
||||
- Из ноды **"prepare_redis_event_no_files"**
|
||||
- К ноде **"publish_to_redis_no_files"**
|
||||
|
||||
---
|
||||
|
||||
### Шаг 7: Добавить ноду #6 - Respond to Webhook
|
||||
|
||||
**Название:** `respond_no_files`
|
||||
**Тип:** `Respond to Webhook`
|
||||
**Response Code:** `200`
|
||||
|
||||
**Response Body:**
|
||||
|
||||
```javascript
|
||||
={{ {
|
||||
success: true,
|
||||
claim_id: $('save_claim_no_files').item.json.claim.claim_id,
|
||||
status_code: $('save_claim_no_files').item.json.claim.status_code,
|
||||
has_files: false,
|
||||
message: 'Заявка сохранена без файлов'
|
||||
} }}
|
||||
```
|
||||
|
||||
**Подключение:**
|
||||
- Из ноды **"publish_to_redis_no_files"**
|
||||
- К ноде **"respond_no_files"** (финальная нода)
|
||||
|
||||
---
|
||||
|
||||
## 🔄 Финальная структура workflow
|
||||
|
||||
```
|
||||
Webhook → Code17 (парсинг файлов)
|
||||
↓
|
||||
IF "проверка наличия файлов"
|
||||
│
|
||||
├─ TRUE (файлов НЕТ) → [НОВАЯ ВЕТКА]
|
||||
│ ↓
|
||||
│ extract_webhook_data_no_files
|
||||
│ ↓
|
||||
│ prepare_payload_no_files
|
||||
│ ↓
|
||||
│ save_claim_no_files (PostgreSQL)
|
||||
│ ↓
|
||||
│ prepare_redis_event_no_files
|
||||
│ ↓
|
||||
│ publish_to_redis_no_files
|
||||
│ ↓
|
||||
│ respond_no_files
|
||||
│
|
||||
└─ FALSE (файлы ЕСТЬ) → существующий flow
|
||||
↓
|
||||
set_token1 → get_data1 → Upload → ...
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## ✅ Проверка
|
||||
|
||||
После добавления нод:
|
||||
|
||||
1. **Сохранить workflow** (Ctrl+S)
|
||||
2. **Активировать workflow** (если не активен)
|
||||
3. **Протестировать:**
|
||||
- Отправить форму БЕЗ файлов
|
||||
- Проверить, что заявка сохранилась в PostgreSQL
|
||||
- Проверить событие в Redis: `redis-cli GET "ocr_events:sess-xxx"`
|
||||
- Проверить ответ webhook: `success: true, has_files: false`
|
||||
|
||||
---
|
||||
|
||||
## 📊 Ожидаемый результат
|
||||
|
||||
**Вход (webhook body):**
|
||||
```json
|
||||
{
|
||||
"session_id": "sess_xxx",
|
||||
"claim_id": "uuid",
|
||||
"unified_id": "usr_xxx",
|
||||
"contact_id": "12345",
|
||||
"phone": "79262306381",
|
||||
"wizard_answers": {"q1": "answer1"},
|
||||
"wizard_plan": null
|
||||
}
|
||||
```
|
||||
|
||||
**Выход (claim в PostgreSQL):**
|
||||
```json
|
||||
{
|
||||
"claim": {
|
||||
"claim_id": "uuid",
|
||||
"status_code": "draft",
|
||||
"unified_id": "usr_xxx",
|
||||
"contact_id": "12345",
|
||||
"phone": "79262306381",
|
||||
"session_token": "sess_xxx",
|
||||
"payload": {
|
||||
"claim_id": "uuid",
|
||||
"answers": {"q1": "answer1"},
|
||||
"documents_meta": [],
|
||||
"wizard_plan": null
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Redis событие:**
|
||||
```json
|
||||
{
|
||||
"event_type": "form_saved_no_files",
|
||||
"claim_id": "uuid",
|
||||
"status_code": "draft",
|
||||
"has_files": false,
|
||||
"timestamp": "2025-11-21T15:00:00.000Z"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Troubleshooting
|
||||
|
||||
### Проблема 1: "session_token": "sess-unknown"
|
||||
**Причина:** В payload не передан `session_id`
|
||||
**Решение:** Проверить, что фронтенд отправляет `session_id` в body
|
||||
|
||||
### Проблема 2: "contact_id": null
|
||||
**Причина:** Поле не извлекается из webhook
|
||||
**Решение:** Проверить путь в expression: `$('Webhook').item.json.body.contact_id`
|
||||
|
||||
### Проблема 3: Ошибка PostgreSQL
|
||||
**Причина:** Неправильный формат данных
|
||||
**Решение:** Проверить логи n8n и формат `payload_partial_json`
|
||||
|
||||
---
|
||||
|
||||
**Автор:** AI Assistant
|
||||
**Дата:** 2025-11-21
|
||||
**Статус:** Готово к внедрению ✅
|
||||
|
||||
104
ticket_form/docs/N8N_OCR_EVENTS_MINIMAL_PAYLOAD.md
Normal file
104
ticket_form/docs/N8N_OCR_EVENTS_MINIMAL_PAYLOAD.md
Normal file
@@ -0,0 +1,104 @@
|
||||
# Минимальный payload для ocr_events
|
||||
|
||||
## Назначение
|
||||
|
||||
После сохранения первичного черновика в PostgreSQL через `claimsave_primary`, n8n должен пушить в Redis канал `ocr_events:{session_token}` только минимальный набор данных.
|
||||
|
||||
**Важно:** Канал формируется как `ocr_events:{session_token}`, где `session_token` - это токен сессии, который генерируется на фронтенде (например, `sess-1763201209156-hyjye5u9h`).
|
||||
|
||||
Бэкенд сам достанет полные данные из PostgreSQL по `claim_id` через эндпоинт `/api/v1/claims/wizard/load/{claim_id}`.
|
||||
|
||||
## Формат данных для ocr_events
|
||||
|
||||
```json
|
||||
{
|
||||
"event_type": "wizard_ready",
|
||||
"status": "ready",
|
||||
"message": "Wizard plan готов",
|
||||
"data": {
|
||||
"claim_id": "9d22d3f4-0306-4b77-a102-c0ca57b24a70",
|
||||
"session_token": "sess-1763201209156-hyjye5u9h",
|
||||
"status_code": "draft",
|
||||
"unified_id": "usr_90599ff2-ac79-4236-b950-0df85395096c",
|
||||
"contact_id": "320096",
|
||||
"phone": "79262306381"
|
||||
},
|
||||
"timestamp": "2025-11-20T11:40:41Z"
|
||||
}
|
||||
```
|
||||
|
||||
## Поля data
|
||||
|
||||
- **claim_id** (string, обязательное) - UUID заявки из PostgreSQL
|
||||
- **session_token** (string, обязательное) - токен сессии пользователя
|
||||
- **status_code** (string, опциональное) - статус заявки (обычно "draft")
|
||||
- **unified_id** (string, опциональное) - unified_id пользователя
|
||||
- **contact_id** (string, опциональное) - ID контакта в CRM
|
||||
- **phone** (string, опциональное) - нормализованный номер телефона
|
||||
|
||||
## Что НЕ нужно пушить
|
||||
|
||||
- ❌ `wizard_plan` - бэкенд достанет из PostgreSQL
|
||||
- ❌ `problem_description` - бэкенд достанет из PostgreSQL
|
||||
- ❌ `wizard_answers` - бэкенд достанет из PostgreSQL
|
||||
- ❌ `ai_agent1_facts` - бэкенд достанет из PostgreSQL
|
||||
- ❌ `ai_agent13_rag` - бэкенд достанет из PostgreSQL
|
||||
- ❌ Любые другие данные из `payload` - всё в PostgreSQL
|
||||
|
||||
## Как бэкенд получает данные
|
||||
|
||||
1. Фронтенд подключается к SSE через `/events/{session_token}` (например, `/events/sess-1763201209156-hyjye5u9h`)
|
||||
2. Бэкенд подписывается на Redis канал `ocr_events:{session_token}` (например, `ocr_events:sess-1763201209156-hyjye5u9h`)
|
||||
3. n8n пушит событие в этот канал с минимальным payload (только `claim_id`, `session_token` и т.д.)
|
||||
4. Бэкенд получает событие, извлекает `claim_id` из `data.claim_id`
|
||||
5. Бэкенд вызывает `GET /api/v1/claims/wizard/load/{claim_id}` для получения полных данных из PostgreSQL
|
||||
6. Бэкенд отправляет полные данные (wizard_plan, problem_description и т.д.) на фронтенд через SSE
|
||||
|
||||
## Пример использования в n8n
|
||||
|
||||
После выполнения `claimsave_primary`:
|
||||
|
||||
1. **Code Node** - формирует минимальный payload и определяет канал:
|
||||
```javascript
|
||||
const claimData = $('claimsave_primary').first().json.claim;
|
||||
const sessionToken = claimData.session_token;
|
||||
|
||||
const result = {
|
||||
event_type: "wizard_ready",
|
||||
status: "ready",
|
||||
message: "Wizard plan готов",
|
||||
data: {
|
||||
claim_id: claimData.claim_id,
|
||||
session_token: sessionToken,
|
||||
status_code: claimData.status_code,
|
||||
unified_id: $('propertyName').first().json.unified_id || null,
|
||||
contact_id: $('Edit Fields10').first().json.contact_id || null,
|
||||
phone: $('Edit Fields10').first().json.phone || null
|
||||
},
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
|
||||
// Сохраняем session_token для использования в URL
|
||||
return [{
|
||||
json: result,
|
||||
session_token: sessionToken // Для использования в следующей ноде
|
||||
}];
|
||||
```
|
||||
|
||||
2. **HTTP Request Node** - пушит в бэкенд:
|
||||
- URL: `http://backend:8000/api/v1/events/{{ $json.session_token }}`
|
||||
- Method: POST
|
||||
- Body: JSON из Code Node (весь объект `result`)
|
||||
|
||||
**Важно:** URL должен быть `http://backend:8000/api/v1/events/{session_token}`, где `{session_token}` берётся из предыдущей ноды (например, `sess-1763201209156-hyjye5u9h`).
|
||||
|
||||
Это создаст канал `ocr_events:sess-1763201209156-hyjye5u9h`, к которому подключён фронтенд через SSE.
|
||||
|
||||
## Преимущества
|
||||
|
||||
- ✅ Минимум данных в Redis (только идентификаторы)
|
||||
- ✅ PostgreSQL как единственный источник истины
|
||||
- ✅ Легче отлаживать (всё в одном месте)
|
||||
- ✅ Меньше нагрузка на Redis
|
||||
- ✅ Проще масштабировать
|
||||
|
||||
@@ -92,3 +92,4 @@ updateFormData({
|
||||
4. **PostgreSQL: Find or Create User** → выполняет SQL запрос → возвращает `unified_id`
|
||||
5. **Response** → возвращает полный ответ с `unified_id`
|
||||
|
||||
|
||||
|
||||
@@ -142,3 +142,4 @@ return {
|
||||
}
|
||||
```
|
||||
|
||||
|
||||
|
||||
@@ -131,3 +131,4 @@ WHERE ua.channel = 'web_form'
|
||||
|
||||
Должна быть запись с `unified_id` в формате `usr_...`.
|
||||
|
||||
|
||||
|
||||
@@ -429,3 +429,4 @@ return claim;
|
||||
- ✅ Возобновление заполнения формы
|
||||
- ✅ Быстрая загрузка состояния формы
|
||||
|
||||
|
||||
|
||||
@@ -189,3 +189,4 @@ if (channel === 'telegram') {
|
||||
|
||||
Все данные уже в PostgreSQL, и этого достаточно. Redis используется только для Pub/Sub событий (`ocr_events:{claim_id}`).
|
||||
|
||||
|
||||
|
||||
@@ -196,3 +196,4 @@ if (channel === 'web_form' && enable_cache === true) {
|
||||
|
||||
Но это опционально и не обязательно для веб-формы.
|
||||
|
||||
|
||||
|
||||
@@ -70,3 +70,4 @@
|
||||
3. Если API вызывается, но возвращает 0 - проверить SQL запрос в backend
|
||||
4. Если SQL работает, но asyncpg не возвращает данные - проверить формат параметров
|
||||
|
||||
|
||||
|
||||
115
ticket_form/docs/SESSION_LOG_2025-11-20.md
Normal file
115
ticket_form/docs/SESSION_LOG_2025-11-20.md
Normal file
@@ -0,0 +1,115 @@
|
||||
# Лог сессии разработки - 20 ноября 2025
|
||||
|
||||
## Проблема (из предыдущей сессии)
|
||||
После верификации телефона не отображался список черновиков, хотя в базе данных есть заявки с `unified_id`.
|
||||
|
||||
## Решение
|
||||
|
||||
### 1. Исправлен SQL запрос в backend (`claims.py`)
|
||||
**Проблема:** Запрос строился через конкатенацию строк, что могло приводить к проблемам с параметрами.
|
||||
|
||||
**Решение:** Переписан SQL запрос - теперь используется прямой запрос для каждого случая:
|
||||
- Для `unified_id`: прямой запрос `WHERE c.unified_id = $1`
|
||||
- Для `phone`: подзапрос через `clpr_user_accounts` и `clpr_users`
|
||||
- Для `session_id`: прямой запрос `WHERE c.session_token = $1`
|
||||
|
||||
```python
|
||||
if unified_id:
|
||||
query = """
|
||||
SELECT
|
||||
c.id,
|
||||
c.payload->>'claim_id' as claim_id,
|
||||
c.session_token,
|
||||
c.status_code,
|
||||
c.channel,
|
||||
c.payload,
|
||||
c.created_at,
|
||||
c.updated_at
|
||||
FROM clpr_claims c
|
||||
WHERE c.unified_id = $1
|
||||
ORDER BY c.updated_at DESC
|
||||
LIMIT 20
|
||||
"""
|
||||
params = [unified_id]
|
||||
```
|
||||
|
||||
### 2. Улучшена обработка черновиков в frontend (`ClaimForm.tsx`)
|
||||
**Проблема:** Черновики из Telegram имеют другую структуру данных (данные в `payload.body`), а не напрямую в `payload`.
|
||||
|
||||
**Решение:** Добавлена поддержка обоих форматов:
|
||||
- **Telegram формат:** данные в `payload.body.wizard_plan`, `payload.body.answers`
|
||||
- **Web form формат:** данные напрямую в `payload.wizard_plan`, `payload.answers`
|
||||
|
||||
```typescript
|
||||
// ✅ Для telegram черновиков данные могут быть в payload.body
|
||||
const body = payload.body || {};
|
||||
const isTelegramFormat = !!payload.body;
|
||||
|
||||
// ✅ Извлекаем данные из body (telegram) или напрямую из payload (web_form)
|
||||
const wizardPlanRaw = body.wizard_plan || payload.wizard_plan;
|
||||
const answersRaw = body.answers || payload.answers;
|
||||
const problemDescription = body.problem_description || payload.problem_description || body.description || payload.description;
|
||||
|
||||
// ✅ Парсим wizard_plan и answers, если они строки (JSON)
|
||||
let wizardPlan = wizardPlanRaw;
|
||||
if (typeof wizardPlanRaw === 'string') {
|
||||
try {
|
||||
wizardPlan = JSON.parse(wizardPlanRaw);
|
||||
} catch (e) {
|
||||
console.warn('⚠️ Не удалось распарсить wizard_plan:', e);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Улучшена обработка `claim_id`
|
||||
**Проблема:** `claim_id` может быть в разных местах в зависимости от формата данных.
|
||||
|
||||
**Решение:** Добавлен поиск `claim_id` в нескольких местах:
|
||||
```typescript
|
||||
const finalClaimId = claim.claim_id || payload.claim_id || body.claim_id || claim.id || formData.claim_id || claimId;
|
||||
```
|
||||
|
||||
### 4. Добавлено детальное логирование
|
||||
- В `loadDraft`: логирование всех этапов загрузки черновика
|
||||
- В `get_draft` (backend): логирование найденных данных
|
||||
- В `list_drafts` (backend): тестовые COUNT запросы для отладки
|
||||
|
||||
### 5. Исправлена обработка `claim_id` в backend
|
||||
В `get_draft` теперь извлекается `claim_id` из `payload`, если его нет в `row`:
|
||||
```python
|
||||
claim_id_from_payload = payload.get('claim_id') if isinstance(payload, dict) else None
|
||||
final_claim_id = row.get('claim_id') or claim_id_from_payload
|
||||
```
|
||||
|
||||
## Результат
|
||||
✅ **Черновики теперь возвращаются!** API корректно возвращает список черновиков для `unified_id`.
|
||||
|
||||
## Файлы изменены
|
||||
|
||||
1. `backend/app/api/claims.py`:
|
||||
- Переписан SQL запрос для `list_drafts`
|
||||
- Добавлено логирование и тестовые COUNT запросы
|
||||
- Улучшена обработка `claim_id` в `get_draft`
|
||||
|
||||
2. `frontend/src/pages/ClaimForm.tsx`:
|
||||
- Добавлена поддержка формата Telegram черновиков
|
||||
- Улучшена обработка `claim_id` из разных источников
|
||||
- Добавлено детальное логирование загрузки черновика
|
||||
|
||||
3. `frontend/src/components/form/Step1Phone.tsx`:
|
||||
- (Возможно, были изменения для передачи unified_id)
|
||||
|
||||
4. `frontend/src/components/form/StepDraftSelection.tsx`:
|
||||
- (Возможно, были изменения для отображения черновиков)
|
||||
|
||||
## Текущий статус
|
||||
✅ **Работает:** API возвращает черновики
|
||||
✅ **Работает:** Загрузка черновиков поддерживает оба формата (Telegram и web_form)
|
||||
⚠️ **Требует проверки:** Отображение черновиков в UI (StepDraftSelection)
|
||||
|
||||
## Следующие шаги
|
||||
1. Проверить отображение черновиков в UI
|
||||
2. Протестировать загрузку черновика из Telegram формата
|
||||
3. Убедиться, что все данные корректно восстанавливаются в форму
|
||||
|
||||
|
||||
@@ -129,3 +129,4 @@ WITH existing AS (
|
||||
2. Вернуть `unified_id` в ответе frontend (в `result.unified_id`)
|
||||
3. При создании/обновлении черновика заполнять `clpr_claims.unified_id = unified_id`
|
||||
|
||||
|
||||
|
||||
@@ -209,3 +209,4 @@ SELECT
|
||||
- ✅ Все подзапросы используют `LIMIT 1` для гарантии одной строки
|
||||
- ✅ Правильное слияние `answers` и `documents_meta`
|
||||
|
||||
|
||||
|
||||
@@ -111,3 +111,4 @@
|
||||
|
||||
Выполни задачу прямо сейчас и верни JSON согласно схеме.
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user